Cheb's Game Engine

Планы, идеология, архитектура и т.п.

Модератор: Модераторы

Cheb's Game Engine

Сообщение Cheb » 08.12.2014 00:50:12

!Тема создана впрок, ну и для потрепаться на тему.
Надо было создать ещё восемь лет назад, когда начинал разработку, но всё казалось: вот ещё чуть доделаю, чтобы было что показать...
А потом подумал: да не, нунафиг. Линус вон ждал, ждал, а потом хряп! - и три ноль, типа живи-живи и ня топни? Вобщем, создам тему сейчас, чтобы потом, когда добьюсь величия, было что вспомнить, и не пришлось срочно строить домик, где прошло детство президента. Как-то так. :roll:

Предыдущие достижения:
http://chentrah.chebmaster.com/?what=log - трудное детство проЭкта.
http://www.gamedev.ru/code/forum/?id=184462 - разглагольствования (и рабочий пример с вертящимся кубиком) на тему Dynamic Resolution Rendering

Что пилю сейчас:
Тест №18, фоновая загрузка из pk3 / zip архивов и рендеринг дофига md3 моделей из OpenArena.

Почему ещё не готово:
Яйца... танцору... :oops: А конкретно - выгребал старый говнокод и приводил месиво исходников в минимальный порядок, чтоб можно было хоть вносить изменения, не путаясь в коде аки тов. Лаокоонт.
За последние полгода удавлено более 10 леденящих кровь багов. Цитирую себя:
Код: Выделить всё
1
The idea was to not use Free but Scrape, but with safeguard that Free is in fact redirected to Scrape.
It didn't work.
I *thought* I intercepted destructors in TManagedObject, redirecting them to Scrape. I was such an idiot. Should have intercepted FreeInstance instead of Destroy.
The fun: trashed memory due to blocks being released twice, as soon as I started using the garbage collector for real.

2
SetLength(ModuleCiMs, 1); ModuleCiMs[1]:= nil;

3
SetLength(ModuleCiMs, F.Count);
//this shit indexes beginning from 1, not 0!
//corrected to SetLength(ModuleCiMs, F.Count + 1);

4
var Res: TAOP;
...
TRes = packed record
    Hash: TResourceHash;
...   
PResourceHash(Res[i])^:=phash^;
...
with PRes(Res[i])^ do begin
...
PResourceHash(Res[i])^:=phash^;
...
inc(grri); if Resources[i]^.owner = CurrentOwner then begin //Wtf how did it even work?

5
CurrentOwner:= ActiveOwner; //where ActiveOwner was never set, remaining 0 (formerly the active module index)

6
Fas feeding get_caller_addr() with the result of a previous call to it, instead of the value returned by get_caller_frame(). It shouldn't have worked, but it was working. Somehow.

7
if Assigned(LockupGuard) then LockupGuard.Freeze; //after the LockupGuard is destroyed but the variable *not* unset :(

8
if EngineObject.Last is TThread then begin
            (EngineObject.Last as TThread).Terminate;
            if Mother^.State.DebugMode then AddLog('  waiting for the thread to terminate...');
            (EngineObject.Last as TThread).WaitFor;
          end;
          EngineObject.Last.Free;
Should've either
A) declared  the destructor as virtual (it _wasn't_)
TLockupGuard = class (TThread)
    destructor Destroy;
//or
B) set the fucking FFreeOnTerminate to fucking false.

9
function WidePos(a, u: WideString): integer;
was parsing beyond the end of u

10
Was using SetUnhandledExceptionFilter() in each thread thinking it's a per-thread setting.
While MSDN states: Issuing SetUnhandledExceptionFilter replaces the existing top-level exception filter for all existing and all future threads in the calling process.
Of course that fucked up the error processing, exceptions were popping during error processing, up to infinite error cycles and crash-to-desktop s.


Добавлено спустя 28 минут 47 секунд:
7 декабря

Затеянный на прошлой неделе кро-охотный поход в сторону, чтобы малость причесать оконный менеджер, вылился в
А) капитальный перетрях оконного менеджера, обработки сообщений окна и способа хранения позиции окна в конфиге (как вещественное число в долях экрана) и работающим переключением оконный/полноэкранный по F11.
что в итоге вылилось в
Б) организацию класса "фреймворк", в который напихал всю работу с окном, звуком и вводом, выдрав из основной помойки. Попотеть пришлось аки Франкенштейну, но! Теперь перевод с WinAPI (и X11 когда воскрешу-таки поддержку линукса), например, на GTK+ или SDL, будет гораздо проще, поскольку теперь всё это соединяется с основным телом движка через дюжину виртуальных методов и ооочень много полей глобальной переменной Mother :roll:
Как побочная плюшка, выделенный сервер делается из основной программы одной подстановкой в рантайме TWinApiFramework на TServerFramework . У последнего почти все методы - пустышки.
Возвращаясь после этого к А), вспомнил, что конь там по прежнему не валялся: окно иногда уплывает под панель задач, и плохо держит позицию при переключении с полноэкранного. Буду перелопачивать с SM_CXSCREEN на GetMonitorInfo

P.S. Да, чуть не забыл. Декларируемые плюшки проекта (в окончательном виде):
* LGPL
* всеядность (пойдёт на средней машине середины 2000-х) - нужна лишь OpenGL 2.0
* мощнейшая автонастройка рендера (сейчас уже вылезают траблы с перегревом ноута, т.к. оно выжимает из видеокарты всё даже на кубике, задирая рендерный буфер до 4000х3000)
* официальная поддержка Windows 98 SE. Чиста для понтов.
* официальная поддержка Линукс
* тысяча игроков против десяти тысяч монстров в мультиплеере, за счёт хитровывернутого варианта peer-to-peer (а точнее, сколько сеть потянет). Никаких бенчмарков или исследований я на эту тему пока не проводил.
Последний раз редактировалось Cheb 28.08.2017 13:56:40, всего редактировалось 1 раз.
Аватара пользователя
Cheb
энтузиаст
 
Сообщения: 590
Зарегистрирован: 06.06.2005 15:54:34

Re: Cheb's Game Engine

Сообщение Cheb » 20.12.2014 15:38:11

20 декабря

Перетрях оконного менеджера успешно завершён, теперь переключается между полноэкранным/оконным (вт.ч. по F11), запоминает позицию окна при переключении и выходе. И да, корректно проверяет чтобы не накласть себя на панель задач - спасибо MSDN. Даже адекватно реагирует в полноэкранном когда открываешь крышку ноутбука и винда переключается на встроенный экран. Чентра выдаёт "Внезапно! Размеры экрана не равны размерам окна!" и корректирует окно.

Для Windows 98 применяется старый метод (который использовался раньше), где *предполагается*, что панель задач - у нижнего края экрана и имеет высоту 28 пикселов. Проверить пока не мог: всё ещё не переустановил 98-ю винду на тестовой машине с тех пор, как она упала насмерть при попытке установить драйвер графического планшета.

Пока не делал перехват мыши, т.к. надо сначала сделать 3d окружение в котором можно было бы этот перехват потестировать. Поскольку задача это - тривиальная, как два пальца об асфальт, я пока сосредоточился на более важных частях подводной части айсберга.

Попутно состоялся сеанс закатывания губы. Мультиплеер на 1000 игроков требовал бы от сервера исходящий канал на 200 мегабайт в секунду. Это *решаемо*, т.к. они состояли бы из абсолютно идентичных 200 Кб в секунду, повторённых тысяче адресатов, но требует архитектурных извращений, подобных битторренту, где игроки ретранслируют друг другу. Теоретически возможно, но сложность задачи превышает весь мой текущий движок с потрохами. Откладываю в *очень* долгий ящик, пометив "движок потенциально расширяемый до ММО".

Текущая заморочка: перетрях менеджмента ресурсов.
Причина: упёрлось рогами в ограничение выбранной парадигмы. Поймал себя на том, что уже начинаю применять костыли. Очень рад, что затянул с менеджером pk3/zip архивов, поскольку он растёт из того же корня, и перелопачивать прилось бы втрое больше.

Вкратце о проблеме:
Мой движок построен вокруг уникальной архитектуры (было бы великой глупостью клепать просто ещё один клон огра, нэ?).
Фишка: исполняемый код можно менять на лету, не теряя данных.
Технически это организовано как вынесение практически всего кода движка в DLL, которая сохраняет состояние в файл, выгружается, другая загружается, восстанавливает состояние из файла. В чём же уникальность, спросите? Часть данных - а именно, текстуры, прочие объекты OpenGL и даже открытые файлы - сохраняются не в файле, а в памяти EXE-матки. Потом DLL берёт их обратно. Имеем: передёрнули исполняемый код, а все текстуры сохранились.
Конечно, это только на словах просто, а на деле там дофига и больше кода для сериализации, обеспечения совместимости вперёд/назад (состав полей класса после перекомпиляции может быть уже другим, нэ?) и прояая, и прочая.
Но итог таков: полная перезагрузка DLL занимает не дольше пары сотен милисекунд. Это часто даже на глаз незаметно. В итоговой игре данных будет поболе, но и тогда - не дольше 0.3..0.5 секунды.
[лирическое отступление]
Кстааати, я вот слышал много рассуждений, что потоки, мол, создавать и удалять таак дорого. Померил. Получилось где-то 4..5 милисекунд на 5 потоков. Ога, дорого.
Ничему не верю, пока сам не пощупаю.
[/лирическое отступление]

Но вернёмся к нашим баранам. Система сдачи ресурсов матке на хранение отлажена и работает, и даже умеет разруливать ситуации когда грузится боле ранняя сохранёнка (я, для простоты, применяю тот же метод при любой загрузке: DLL выгружается и загружается заново. Заодно чистить ничего не надо, т.к. её мемори менеджер закрывается вместе с любыми утечками памяти).
Но! В парадигме ранее постулировалось, что ресурс состоит из одного хэндла, приводимого к ptruint или pointer. В теории, и пока я возился в лягушатнике со статическими текстурами, работало отлично. Но стоило выйти на глубину, и оказалось, что ресурс - это не только ценный мех... В смысле, не только хэндл. У FBO, например, ещё и размеры имеются. Но и это мелочь. Что мне делать, когда я с различными атласами работать начну? Допустим, атлас *частично* изменился между точками сохранения. Отбрасывать весь? Фу, как некрасиво. И это то, от чего я положил столько усилий, чтобы уйти.

Короче, буду сильно менять устройство TGenericAsset и всех дочерних классов, чтобы могли сдавать матке на хранение информацию любой сложности, а после загрузки - сравнивали себя с полученным образцом и сами решали, как разруливать коллизии.

И чтобы убрать костыли.

И чтобы ресурсам не приходилось ссылаться обратно на своего владельца и ковыряться с его полями (фу, бяка).

Надеюсь, хоть до середины января управлюсь. :(

P.S. Как я дошёл до жизни такой: когда то давным-давно делал МОД для Морровинда с многими скриптами. Поправишь, бывало, в скрипте запятую. Потом запускаешь Морровинд. И ждёшь пока он загрузится. И ждёшь. И ждёшь... Наконец, загрузился. Выбираешь сохранёнку, и ждёшь пока она загрузится. И ждёшь. И ждёшь... Наконец, загрузилась. Потом идёшь к скриптованному неписю. Бывало, по дороге ждёшь, пока игра замирает и свопится. И ждёшь. И ждёшь... Наконец, дошёл. Получаешь сообщение о синтаксической ошибке в скрипте. Мочало, начинай сначала.
Короче, я в конце концов ОЗВЕРЕЛ :evil: И с тех пор стал неадекватным :twisted:

Прогресс идёт извилистыми путями, обходя эту проблему. Например, прикручивая скриптовые языки. Это ещё с квейка идёт, с его встроенным интерпретатором Си. Но это - не мой путь. :roll:
Аватара пользователя
Cheb
энтузиаст
 
Сообщения: 590
Зарегистрирован: 06.06.2005 15:54:34

Re: Cheb's Game Engine

Сообщение runewalsh » 20.12.2014 18:31:22

где-то 4..5 милисекунд на 5 потоков.

А у меня получается 20-40-80 мкс на поток. Но всё равно не рекомендуется постоянно (пере)создавать потоки. Для I/O значительно эффективнее неблокирующий I/O, а для всего остального либо используют пул потоков (QueueUserWorkItem), либо сохраняют потоки на какое-то время, т. е. поток ждёт работы N секунд и самоубивается, если не дождался.

В смысле, не только хэндл. У FBO, например, ещё и размеры имеются.

Но ведь FBO, как и всё остальное, идентифицируется единственным хэндлом, по которому уже можно спросить размеры и вообще что угодно.

Мультиплеер на 1000 игроков требовал бы от сервера исходящий канал на 200 мегабайт в секунду.

Должно быть раз в 100 меньше. Сообщения вида «объект A передвинулся в точку B» прекрасно посылаются с секундными промежутками (в зависимости от расстояния, скорости и т. п.), остальное клиент проинтерполирует и даже предскажет, пока ждёт обновления.
Аватара пользователя
runewalsh
постоялец
 
Сообщения: 324
Зарегистрирован: 27.04.2010 00:15:25

Re: Cheb's Game Engine

Сообщение скалогрыз » 20.12.2014 23:34:07

Cheb писал(а):P.S. Как я дошёл до жизни такой: когда то давным-давно делал МОД для Морровинда с многими скриптами. Поправишь, бывало, в скрипте запятую... И ждёшь пока он загрузится. И ждёшь. И ждёшь... И ждёшь. И ждёшь... ..И ждёшь. И ждёшь... Получаешь сообщение о синтаксической ошибке в скрипте. ... И с тех пор стал неадекватным :twisted:

:mrgreen: похвалю за честность! :)

но я так и не понял, в чём взаимосвязь игрового движка и средств разработки.
Надо думать, что Мод к морровинду как раз делался на соответствующем конструкторе?!Но если сам конструктор (как средство разработки) плохой и не даёт проверить скрипт, вне игры, то это же не причина ещё писать свой игровой движок с нуля?
скалогрыз
долгожитель
 
Сообщения: 1645
Зарегистрирован: 03.09.2008 02:36:48

Re: Cheb's Game Engine

Сообщение stanilar » 21.12.2014 21:05:29

Cheb писал(а):Как я дошёл до жизни такой


Да, удобство разработки может серьезно перекрыть многие недостатки.
stanilar
постоялец
 
Сообщения: 271
Зарегистрирован: 09.03.2010 19:09:02

Re: Cheb's Game Engine

Сообщение Cheb » 23.12.2014 20:32:14

Но всё равно не рекомендуется постоянно (пере)создавать потоки.

Это я так, к слову. Создаю их один раз при загрузке DLL.

Сообщения вида «объект A передвинулся в точку B»

Не будет никаких по-объектных сообщений.
От сервера клиенту будут передаваться:
а) полный снапшот вселенной при входе на сервер
б) инпуты всех игроков
Дальше - строго целочисленная физика, и хировывернутая многослойная система лагокомпенсации. Все клиенты крутят побитово совпадающую копию вселенной, получая идентичные инпуты. Это кадр "идеального прошлого", отстающий от настоящего на 0.2..1.0 секунд и ведомый гарантированно доставленными инпутами, финализированными сервером (т.е. есл инпут не успел от клиента до сервера за эти 0.2...1 сек., он отбрасывается). Кроме этого существует несколько всплывающих неидеальных кадров (грубо, кусок мира вокруг игрока), управляемых негарантированно доставляемыми инпутами, по мере поступления. Сам локальный игрок тоже месится в неидеальном всплывающем фрейме, только лагов не замечает, поскольку всегда видит самый верхний фрейм и инпут всегда идёт в этот фрейм напрямую.

Это всё пока теория, возюсь над фундаментом для этого, закладывая все эти фичи в основу архитектуры.
Прорабатываю механизм оптимальной привязки многих фреймов к одному рендер-мешу (по мере того, как верхний фрейм сменяется всплывающими снизу, возможны всякие нежданчики: продырявили дырку в скале, хлынула вода - опа, на самом деле не продырявили, ничего никуда не течёт. Джибнули игрока, надо стенку покрасить кровью - опа, он невредим, трясёт попой, убрать кровь со стенки - опа, у него след от пули на попе)

:x Минусы:
- адски долгое приконнективание к игре
- мультиплеер требует недетской мощности от процессора/памяти (в худшем случае, минимум 4х относительно сингла). Т.е. 10-кратный просчёт физики не должен занимать больше половины времени кадра (т.к. распараллеливание я потяну, но далеко не сразу)
- чувствительно к лагам. Отстояние идеального прошлого от настоящего нельзя делать большим, нагрузка на процессор пропорциональна ему.
- без истинного Peer-to-peer ширина канала пропорциональна *квадрату* числа игроков
- невозможность плавной коррекции положения других игроков. Только скачками (тем заметнее, чем активнее они маневрируют)

8) Плюсы:
- неограниченная сложность физики/логики, *не* зависящая от толщины канала. Игроки смогут швыряться камехамехами, распидорашивающими до квадратного километра террейна за раз. И устраивать подлянки типа потопов.
- неограниченное количество неписей. Реалистично, планирую тестировать на зерговой орде из 10 тысяч мобов в лесу из 50 тысяч деревьев на пересечённой местности (рендер будет весьма хитро..мудрым, кеширующий 90% дальних объектов в спрайты).
- единожды созданный сетевой код достаточен на века. Править логику и алгоритмы можно без оглядки на сеть.
- потенциально, ммо на тысячу игроков.

делался на соответствующем конструкторе?!Но если сам конструктор (как средство разработки) плохой и не даёт проверить скрипт,

Это была лишь вершина айсберга. Я взглянул на проблему в целом, и понял, что без *мгновенного* передёргивания кода, или использования скриптового языка, тот вид творчества рождённого из кодинга, что я хочу - будет мне недоступен. Править алгоритмы на лету и мгновенно видеть результат - вот моя цель, моя мечта.

Да, удобство разработки может серьезно перекрыть многие недостатки.

Если бы я не тормозил, закончил бы ещё в 2010-м. Но... увлёкся писательством :roll: :oops:

Но ведь FBO, как и всё остальное, идентифицируется единственным хэндлом,

Он не *идентифицируется*, он *представлен* хендлом. Идентифицируется он идеальным хешем, генерируемым в момент создания ресурса. Хендлы в файл не сохраняются, только хеши. Тех же фбо может быть несколько разных, но при загрузке старой сохранёнки желательно реюзать существующий, если хеш совпадает. В идеале, при запуске/первой загрузке надо воссоздавать по сохранённым значениям. Но он может не подходить, если сохранёнку загрузить на более слабой видеокарте. И т.п
Для ресурсов, грузимых из файла (текстуры), хеш уникально выводится из размера и даты. Но на старой карте без поддержки нпот процесс загрузки может включать ресемплинг до степени двойки. И т.п.

Короче, без многоступенчатой системы не обойтись.

Игра не должна грузить то, что уже загружено. Игра не должна создавать атлас с нуля, если часть его можно реюзать. Загрузка дольше трёх - десяти секунд это преступление против юзверя и знак лени разраба.
Аватара пользователя
Cheb
энтузиаст
 
Сообщения: 590
Зарегистрирован: 06.06.2005 15:54:34

Re: Cheb's Game Engine

Сообщение скалогрыз » 23.12.2014 21:23:33

Cheb, runewalsh вам бы усиоия объеденить ? не?
а то "Cheb's Game Engine" и "CRUSIS 9000" не одно ли и то же? - в том смысле что движки?!
скалогрыз
долгожитель
 
Сообщения: 1645
Зарегистрирован: 03.09.2008 02:36:48

Re: Cheb's Game Engine

Сообщение Mirage » 24.12.2014 02:24:44

Cheb писал(а):От сервера клиенту будут передаваться:
а) полный снапшот вселенной при входе на сервер
б) инпуты всех игроков
Дальше - строго целочисленная физика, и хировывернутая многослойная система лагокомпенсации. Все клиенты крутят побитово совпадающую копию вселенной, получая идентичные инпуты.


Совпадать она будет недолго. Чуть потом будут рассинхронизации.
Теоретически от них можно избавиться, но на практике это сложно даже для примитивных механик. А в случае изменения сетевого кода, либо игровой логики, часто рассинхронизации появляются заново.
Mirage
энтузиаст
 
Сообщения: 752
Зарегистрирован: 06.05.2005 20:29:07
Откуда: Russia

Re: Cheb's Game Engine

Сообщение Cheb » 27.12.2014 13:03:50

а то "Cheb's Game Engine" и "CRUSIS 9000" не одно ли и то же?

Диаметрально противоположные подходы. Он стремится максимально освоить современные продвинутые фичи, где даже OpenGL 3.1 мало, Сырно рендерится завязанной в узел.
Я создаю приземлённую, по возможности простую и неприхотливую вещь, где 90% - это средства разработки и средства ускорения разработки. Я даже с "OGL 1.2 + расширения" на "OGL 2.0" перешёл с огромной неохотой, только когда понял, что поддержка древнеплатформ удвоит затраты времени на разработку и утроит сложность кода шейдеров. Но даже теперь я не бросаю поддержку виндовз 98, хотя для этого некоторые механизмы приходится делать по два раза.
Точек пересечения, реально, очень мало.
Ну, может, кусочками кода обменяемся.

Совпадать она будет недолго. Чуть потом будут рассинхронизации.

Для того и целочисленная физика в одном потоке.
Реально, для получения фана, особо продвинутых фич не нужно. Как пример - Brutal DooM.
Никакой рэгдолл-физики не будет. Анимации смерти - сделанные вручную, нарочито-драматические. Для трупов и приземистых монстров - выравнивание вдоль усреднённой плоскости поверхности. Для человека - блендинг нескольких анимаций "на ровной поверхности" / "на склоне 20 градусов" / "на склоне 40 градусов".
Зато армия из 10 000 неписей - как два пальца об асфальт.
Переход количества в качество, десу.
Я намеренно целю в незанятую нишу. Традиционно, с ростом мощности ПК всё больше детализированным делают каждый объект, и битвы остаются ограниченными по масштабу, только рюшечки улучшаются. В то же время, если ту же мощность направить на техники прошлых поколений... Можно получить реальный эпос. :roll:

Один из образцов для меня - Mount & Blade. Модели - низкополигональные шо ппц, зато бугурт сотня на сотню - обычное дело. :D А если *ещё* оптимизировать физику и ИИ, сделав их совсем легковесными, и применять рисование дальних неписей спрайтами? Вот и родилась моя мечта о тысячах. Вполне осуществимая.

Как я уже говорил, у меня за плечами недоделанный шутер для MS-DOS, где на уровне могли быть толпы до 64 монстров. И ничего, даже на 486-м физика не тормозила (тормозил рендер, да :oops: ) А в думе лимит монстров, ЕМНИП, был за сотню. А ведь это на 386-м шло. Короче, всё обычно упирается рогами в рендер.

А в случае изменения сетевого кода, либо игровой логики,

В идеале, клиент скачивает с сервера DLL той же версии, что на сервере, и уже её запускает.
О безопасности пусть думает антивирус.

Сетевой код, после завершения его отладки, меняться не должен. Поскольку передаются *только* инпуты и снапшоты. И то, и другое для сетевого кода - чёрные ящики с подписью и адресом. Их внутренним содержимым ведает игровая логика.

Но да ладно. До этого дожить ещё надо.
Написал сюжет и основную механику мега-игры, которая будет строиться на третьем этапе. Положил в стол: пусть дозреет. Сейчас до первого бы этапа доползти :roll:
Аватара пользователя
Cheb
энтузиаст
 
Сообщения: 590
Зарегистрирован: 06.06.2005 15:54:34

Re: Cheb's Game Engine

Сообщение Cheb » 15.01.2015 17:54:45

15 января

Две недели горбатился, но наконец не дошёл до стадии, когда код снова начал компилироваться. Вот представьте себе: такая малость, а ощущается как ужекаменьсплеч.

Даже запустилась и умерла в страшных корчах.

Перелопачивание системы менеджмета ресурсов было настолько всеобъемлющим, что поставил на уши буквально всё. Нерасковырянного кода почти не осталось. Целые деревья классов вывернулись наизнанку и перестроились.

Попутно обнаружил (вот радость то!) что моя многоэтажная система жонглирования исключениями принципиально рассчитана на однопоточную архитектуру.
Использовать threadvar, естественно, нельзя, поскольку движок сидит на двух стульях из EXE и DLL.
Дунул, плюну, потряс бубном, и надстроил к этой шаткой этажерке ещё один уровень. Чувствую, портировать на линукс будет.... кхм... одно удовольствие: теперь у меня везде торчат вызовы WinAPI типа if Mother^.State.MainThreadId <> GetCurrentThreadId() then

Теперь фоновый поток культурно журналирует исключение и завершается, а основной хряп выполняется в основном потоке во время обработки очередного кадра:

Изображение

А в понедельник на работу, отпуск кончается :(
Аватара пользователя
Cheb
энтузиаст
 
Сообщения: 590
Зарегистрирован: 06.06.2005 15:54:34

Re: Cheb's Game Engine

Сообщение Cheb » 19.01.2015 00:11:45

18 января

Полностью перелопатил обработку исключений. Каждому thread теперь выделяется отдельный блок памяти, который используется для флагов и цепочки сообщений об ошибке. В целом, вроде бы, работает, но есть подозрительные моменты. Умерло насмерть при надругательства над paszlib (повторная инициализация файлового потока), и улетало в дефолтный хэндлер исключений пока я не исправил баг. А должно было культурно поймать и остановиться.

С другой стороны, надругательство над zlib моглоо элементарно разворотить стек или кучу.

Кроме этого, есть ещё какое-то рандомное зависание, не уверен, с этим связано, или нет.

Разные потенциальные неразруливыемые ситуации в обработке исключений экранируются при помощи repeat Sleep(1000) until false; - то есть, поток виснет, но приложение в целом не падает.

По крайней мере я теперь знаю, что Vampyre Imaging крашится при чтении png размером 14000х14000, а paszlib распаковывает 30 мегабайт за 700 милисекунд. Так что PK3 файлы как таковые работают.

Продолжу в след. выходные.
Аватара пользователя
Cheb
энтузиаст
 
Сообщения: 590
Зарегистрирован: 06.06.2005 15:54:34

Re: Cheb's Game Engine

Сообщение MiniQ » 19.01.2015 12:16:29

Читаю репорты и удивляюсь упорности и целеустремленности автора ).
MiniQ
новенький
 
Сообщения: 72
Зарегистрирован: 28.01.2013 16:31:55

Re: Cheb's Game Engine

Сообщение hinst » 19.01.2015 16:13:54

:arrow: Get current thread ID работает на линуксе так же как и на Windows (не в том смысле что в точности также, но в принципе - так же
:arrow: Обработка исключений в других потоках лучше всего делается так:
Код: Выделить всё
procedure TThread.Execute;
begin
  try
    Routine()
  except
    ReportException()
  end
end

То есть, у каждого потока должна быть его основная процедура обвёрнута сверху в try-except. Такое работает на Windows и на Linux
Аватара пользователя
hinst
энтузиаст
 
Сообщения: 782
Зарегистрирован: 12.04.2008 18:32:38

Re: Cheb's Game Engine

Сообщение Cheb » 19.01.2015 18:13:01

и удивляюсь упорности и целеустремленности автора

А то 8)

Get current thread ID работает на линуксе так же как и на Windows

Зело отрадно, спасибо, не знал :)

его основная процедура обвёрнута сверху в try-except. Такое работает на Windows и на Linux

Не-а. :evil:
В линукс - работает. В win64 - по идее, работает. В win32 же мы имеем замечательный баг RTL 4605, он же 12974: системные исключения в DLL не ловятся :evil: . Приходится ставить собственный обработчик, внутри которого совершать танцы с бубном, чтобы доставить хряп до нужного адресата :x

Вот маленький кусочек: (это я ещё не касаюсь дополнительных танцев с бубном когда DLL вызывает функции, экспортированные EXE маткой, типа дешифровки jpeg или работы с перзистентным TStream матки, который DLL считает ptruint хендлом. Там целый пинг-понг с экранировкой и переподъятием исключений)
Код: Выделить всё
  procedure JumpToDllRaiseFunction;
  begin
    {$ifdef windows}
     SysResetFPU;
    {$endif}
    Module.PassUnhandledException; //На стороне DLL это DieBySehHack() (см. ниже)
    repeat sleep(1000) until false;
    //raise Exception.Create('Oops... Internal Error in exception handling! (the module is probably invalid)');
  end;

  function GetModuleByAddr(addr: pointer): THandle;
  var
    Tmm: TMemoryBasicInformation;
  begin
    if VirtualQuery(addr, @Tmm, SizeOf(Tmm)) <> sizeof(Tmm)
      then Result:=0
      else Result:= THandle(Tmm.AllocationBase);
  end;

  function MyExceptionFilter(excep : PExceptionPointers) : Longint; stdcall;
  var
    pes: PMotherSehState;
  begin
    pes:= GetExceptionState();
    if
      not Mother^.State.Terminated
      and Assigned(Module) and Assigned(Module.PassUnhandledException) //DLL is loaded
      and not pes^.ModuleIsAlreadyProcessingUnhandledException
      and (
        (
          (pes^.IsModuleThread)//ThreadId <> Mother^.State.MainThreadId)
          and (Module.DllHandle
            = GetModuleByAddr(pointer(excep^.ContextRecord^.Eip))) //exception is in the module DLL
          //and (Mother^.State.ExeHandle <> GetModuleByAddr(pointer(excep^.ContextRecord^.Eip)) //exception is not in the mother exe
        )
       or
        (
          (pes^.ThreadId = Mother^.State.MainThreadId)
          and (Mother^.Module.CurrentlyExecutingInd <> NOT_A_MODULE) //we are really in the process of calling one of the the DLL functions
        )
      )
      (**)
       //this is NOT inside a mother callback function
    then begin
      pes^.ModuleIsAlreadyProcessingUnhandledException:= true;

      //save the bloody details
      pes^.ExceptionCode:= excep^.ExceptionRecord^.ExceptionCode;
      pes^.ExceptionAddress:= excep^.ContextRecord^.Eip;

      //cheat by changing the return address to the function
      //  that calls the module to raise its own language exception
      excep^.ContextRecord^.Eip := dword (*32 bits only!*) (@JumpToDllRaiseFunction);

      //tell Windows: we have corrected the problem, go on.
      excep^.ExceptionRecord^.ExceptionCode := 0;
      Result := EXCEPTION_CONTINUE_EXECUTION;
    end
    else begin
      Result:= OldFilter(excep);
    end;
  end;

//В EXE используется:
function GetExceptionState(): PMotherSehState;
{
   Note: while it *can* allocate the block for a thread
   it knows nothing about, it is *not* a desired behavior

   Thus, any thread better cal this function beforehand,
   in the beginning of this Execute method.

   There is Mother^.Core.AllocateThreadExceptionState for the module
}
var
  T: TThreadId;
begin
  EnterCriticalsection(Mother^.SehCriticalSection);
  T:= GetCurrentThreadId();
  Result:= @Mother^.ExceptionState;
  while true do begin
    if Result^.ThreadId = T then begin
      LeaveCriticalsection(Mother^.SehCriticalSection);
      Exit;
    end;
    if not Assigned(Result^.Next) then begin
      VerboseLog('  Allocating exception state for thread ID=' + IntToHex(T, 8) + 'h...');
      Result^.Next:= new(PMotherSehState);
      Result:= Result^.Next;
      FillChar(Result^, SizeOf(Result^), 0);
      Result^.ThreadId:= T;
      LeaveCriticalsection(Mother^.SehCriticalSection);
      Exit;
    end;
    Result:= Result^.Next;
  end;
end;
 
//В DLL используется:
  function GetExceptionState(T: TThreadId = 0): PMotherSehState;
  {
     Note: unlike its counterpart from the mother,
     this one *cannot* allocate a block if there is none.

     Use Mother^.Core.AllocateThreadExceptionState for each thread beforehand!
  }
  var
    pes: PMotherSehState;
  begin
    EnterCriticalSection(Mother^.SehCriticalSection);
    if T = 0 then T:= GetCurrentThreadId();
    pes:= @Mother^.ExceptionState;
    while Assigned(pes) do begin
      if T = pes^.ThreadId then begin
        LeaveCriticalSection(Mother^.SehCriticalSection);
        Exit(pes);
      end;
      pes:= pes^.Next;
    end;
    LeaveCriticalSection(Mother^.SehCriticalSection);
    Mother^.State.StateTrashedRestartRequired:= true;
    AddLog(RuEn(
      'Фатальная ошибка при обработке ошибки: поток ID=%0h не имеет записи состояния.'
        + #10#13'  Блокирую его в бесконечном цикле.',
      'Fatal error when processing an error: thread ID=%0h doesn''t have a state record allocated'
        + #10#13'  Locking it in an infinite loop.'
      ), [IntToHex(T, 8)]);
    repeat Sleep(1000) until false;
  end;

//Также DLL использует эту процедуру, экспортированную из EXE (и использующую его менеджер памяти)
  procedure _AllocateThreadExceptionState(thread_title: PWideChar); cdecl;
   //to be called by module threads when they begin their Execute() 
  var
    pes: PMotherSehState;
  begin
   //no try block. If *this* simplest thing crashes it means things are already FUBAR.
    pes:= GetExceptionState();
    FillChar(pes^, sizeof(pes^), 0);
    pes^.ThreadId:= GetCurrentThreadId();
    pes^.IsModuleThread:= true;
    if Assigned(thread_title) then pes^.ThreadTitle:= PWideCharToWideString(thread_title);
  end;
 
//Ну, и, потроха DLL:

procedure DieBySEhHack;
var
  _dyell, _crash_details, ThreadName: WideString;
  pes: PMotherSehState;
begin
  pes:= GetExceptionState();
  if Assigned(pes^.ThreadTitle)
    then ThreadName:= PWideCharToWideString(pes^.ThreadTitle)
    else ThreadName:= 'thread ID=' + IntToHex(pes^.ThreadId, 8) + 'h';
  if Mother^.Debug.Verbose then AddLog(
    'The mother''s SEH hack has called the module''s callback in %0...',
    [ThreadName]);
  {$ifdef buildmein}
  _dyell:= 'This shouldn''t be happening!';
  {$else}
    _dyell:= RuEn('Системное исключение: ','System exception: ')
      + SehNameByCode(pes^.ExceptionCode)
      + #10#13'  ' + SehHackTellExceptionAddress(pes^.ExceptionAddress);
  {$endif}
  _dyell+= SehHackCheckForGenericDyingYells(_crash_details);
  if (pes^.IsModuleThread)
    and Assigned(ThreadManager)
    and (ThreadManager.GetThreadNumById(pes^.ThreadId) < 0)
  then begin
    AddLog('The thread manager failed to dispatch exception in thread "%0" (ID=%1h)!'
      + #10#13'Putting the thread into an infinite loop.'
     , [ThreadName, IntToHex(pes^.ThreadId, 8)]);
    repeat Sleep(1000) until false;
  end;

  Mother^.Module.StartDying(PWideChar(_dyell));

  if Mother^.Debug.Verbose then AddLog('  Module''s callback is done. Raising a language exception...');
  if pes^.ExceptionCode = $C00000FD then begin
    AddLog(RuEn(
      'Это было переполнение стека! OH SHI--',
      'That was stack overflow! OH SHI--')); //cuz what follows is usually a silent crash-to-desktop
    if Mother^.State.SavingTheSessionNow then
      //пиздеееец :(((((((((
      Mother^.Core.DisplaySessionFailedErrorMessage(PWideChar(RuEn(
      'Переполнение стека во время сохранения сессии.',
      'Stack overflow during saving the session.'
    )));
  end;

  raise exception.create(''); //raise a language exception. Pascal's try blocks DO catch these.
end; 


  procedure TChentrahModuleThread.Execute;
  var
   tn, tt: WideString;
   i: integer;
   pes: PMotherSehState;
  begin
    f_Done:= false;
    Self.f_MyId:= GetCurrentThreadId();
    if Mother^.Debug.Verbose then AddLog('Execute begins, Id=%0',[pointer(GetCurrentThreadId())]);
    i:= Threadmanager.GetThreadNumById(GetCurrentThreadId());

    tt:= RuEn('общего назначения', 'general-purpose');
    if ttt_File in f_TaskTypes then tt:= RuEn('для файловых операций', 'file processing');
    if ttt_Imaging in f_TaskTypes then tt:= RuEn('для обработки изображений', 'image-processing');
    tn:= PervertedFormat(RuEn(
      'Фоновый поток модуля №%0 %1',
      'Module background %1 thread #%0'), [i, tt]);

    Mother^.Core.AllocateThreadExceptionState(PWideChar(tn));

    pes:= GetExceptionState();
    try
      repeat
        if not Self.PerformOneTask then Self.Sleep;
      until
        pes^.NowDying //There could be errors screened by accident
        or Self.ItsTimeToTerminate;
      if pes^.NowDying then AddLog('silent CRASH');
    except
      AddLog('CRASH!');
      // do NOT call StopDying, thus leaving thread exception state for the thread manager to process
    end;
    f_done:= true;
    if Mother^.Debug.Verbose then AddLog('Execute ended');
  end;   


  procedure TThreadManager.Pulse;
  var
    i: integer;
    w, tn: widestring;
    AllDone: boolean = true;
    pes: PMotherSehState;
    a: array of WideString;
    ppw: PPWideChar;
  begin
    w:= '';
    for i:= 0 to High(T) do begin
      if not T[i].Done then continue; //thread always finishes execution if it crashes
      pes:= GetExceptionState(T[i].Id);
      if pes^.NowDying then begin
        if w <> '' then w+= #10#13#10#13;
        w+= PWideCharToWideString(pes^.ThreadTitle) + ':'#10#13;
        w+= PWideCharToWideString(Mother^.Module.StopDying(pes));
      end;
    end;
    if w <> '' then Die(w);

    for i:= 0 to High(T) do AllDone:= AllDone and T[i].Done;
    if AllDone then Mother^.Module.HasThreadsRunning:= false;
  end;


Добавлено спустя 14 минут:
P.S. Йолки, только что понял, где баг!
Код: Выделить всё
       or
        (
          (pes^.ThreadId = Mother^.State.MainThreadId)
          and (Mother^.Module.CurrentlyExecutingInd <> NOT_A_MODULE) //we are really in the process of calling one of the the DLL functions
        )

должно быть
Код: Выделить всё
       or
        (
          (pes^.ThreadId = Mother^.State.MainThreadId)
          and (Mother^.Module.CurrentlyExecutingInd <> NOT_A_MODULE) //we are really in the process of calling one of the the DLL functions
          and (Mother^.State.ExeHandle <> GetModuleByAddr(pointer(excep^.ContextRecord^.Eip)))
        )

, потому что когда модуль вызывает коллбек-функцию, оно передаёт исключения обработчику модуля вместо того, чтобы вызвать дефолтный обработчик и дать матке отработать цепочку обратно до шлюза! Вся информация кроме точки входа в шлюз теряется (в лучшем случае) или всё давится нежданчиком когда модуль пытается поднять языковое исключение внутри блока try, принадлежащего EXE :shock:
Аватара пользователя
Cheb
энтузиаст
 
Сообщения: 590
Зарегистрирован: 06.06.2005 15:54:34

Re: Cheb's Game Engine

Сообщение hinst » 20.01.2015 13:21:15

может прост не надо в DLL их ловить, пусть отдельные исполняемые модули ловят свои исключения отдельно
Основная программа будет ловить свои исключения, а динамическая библиотека - свои
Аватара пользователя
hinst
энтузиаст
 
Сообщения: 782
Зарегистрирован: 12.04.2008 18:32:38

След.

Вернуться в Разработки на нашем сайте

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и гости: 3

Рейтинг@Mail.ru