Cheb's Game Engine

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

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

Re: Cheb's Game Engine

Сообщение Cheb » 22.06.2019 09:17:38

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

Зандронум предлагает два варианта выравнивания, оба в той или иной мере уродливые, оба дают артефакты.
1. спрайты всегда вертикальны в экранном пространстве. Нет артефактов при движении, но вертикальные колонны по краям экрана оказываются сильно отклонены от той вертикали, что у стен. Ибо перспектива жеж. Большие спрайты видимо разворачиваются относительно мира когда крутишь камерой. Особенно проявилось, когда я делал спрайтовые версии огромных скал в скайбоксе.
2. спрайты в мировом пространстве, всегда развёрнуты на камеру. Убирает проблемы предыдущего, но приводит к артефактам вблизи: когда идёшь, например, сквозь высокую траву - её спрайты слишком резко разворачиваются за камерой, когда проходишь сквозь них.
Я хочу попробовать гибридно-хитрожопый способ, объединяющий плюсы этих двух, но не имеющмий минусов второго. Но для этого нужна более изощрённая формула, которую хрен запихнёшь в вершинный шейдер. Поэтому надо на цпу.
Но Брутал Дум срёт и спамит спрайтами: когда монстра разносит в клочья, из одного спрайта получаются десятки. Теперь представляем, что монстров на уровне - тысячи.
А я как раз стремлюсь избавиться от ограничений, чтобы огромные карты с ордами монстров летали даже на малине.
Аватара пользователя
Cheb
энтузиаст
 
Сообщения: 755
Зарегистрирован: 06.06.2005 15:54:34

Re: Cheb's Game Engine

Сообщение Cheb » 16.08.2019 21:58:08

Провёл изыскания, выяснил: InterlockedIncrement - та ещё дрянь, по тормознутости практически равна квадратному корню (в полтора раза медленнее деления флоатов).
На основе этого сделал выбор архитектуры с далеко идущими последствиями (фактически однотредная физика, делегирующая дополнительным тредам только самые тупые расчёты). То есть, например, просчёты столкновений в других тредах - будет, но изменение состояния игровых объектов на их основе будет возможно лишь в главном логическом треде.
Последствия - потолок масштабируемости и в лучшем случае ~60% кпд использования остальных ядер.
Аватара пользователя
Cheb
энтузиаст
 
Сообщения: 755
Зарегистрирован: 06.06.2005 15:54:34

Re: Cheb's Game Engine

Сообщение runewalsh » 17.08.2019 04:37:26

>по тормознутости практически равна квадратному корню (в полтора раза медленнее деления флоатов)
Скорее эти операции быстрые, чем InterlockedIncrement медленный. А вообще какой смысл сравнивать тёплое с мягким.

Сейчас проверил, у меня простой инкремент выигрывает у InterlockedIncrement в 10 раз только при инкрементировании ячеек, тесно уложенных рядом. Если же инкрементировать слова с разных кэш-линий (общий случай), разница с простым инкрементом всего в пару десятков %. Это в общем-то и объясняется тем, что основная работа процессора при выполнении атомарной операции — обеспечить когерентность кэша.
Аватара пользователя
runewalsh
постоялец
 
Сообщения: 433
Зарегистрирован: 27.04.2010 00:15:25

Re: Cheb's Game Engine

Сообщение Cheb » 17.08.2019 09:41:29

Ага! Понятно, спасибо, я чуть не облажался.

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

Хммм... Я уже придумал хак, как не дёргать счётчики ссылок базового слоя при расслоении: просто ввести общую на всех дельту и в методе CopyOnChange сравнивать счётчик не с единицей, а с единицей плюс эта дельта, а у ново создаваемых выставлять не в единицу, а в единицу минус дельта.
..хотя нет, бред написал. Это всё хряпнет ся, когда дополнительный слой удалится. Нужно или нормальный инкремент *всем* объектам мира при отпочковывании частичного слоя, либо проходить по *всем* объектам базового слоя и править им счётчик, оказавшийся меньше единицы, при удалении частичного слоя, если я использую общую дельту.

Сделаю-ка сначала строго однотредовое с заложенной возможностью многотредовости - и посмотрю, как полетит.

P.S. Подозреваю, замеренное тобой отсутствие замедления всего лишь переносится в другое место и выстрелит, когда другой участок кода обратится к тому участку памяти. Тормоза ведь образуются за счёт сброса линейки кэша в память, так?

Дано: DDR3 666 (10.3 Гб/с)
InterlockedIncrement выдаёт 0.119 миллиарда операций в секунду. Если при каждой пишется 64-байтная линейка кеша, то получается 7.616 Гб/с
Очень похоже, учитывая, что на ноутбуке пропускная способность памяти отжирается ещё на встроенное видео.
Аватара пользователя
Cheb
энтузиаст
 
Сообщения: 755
Зарегистрирован: 06.06.2005 15:54:34

Re: Cheb's Game Engine

Сообщение Cheb » 22.08.2019 08:58:21

Дропнул пока механику "флаги расслаиваются по битовым массивам", преждевременные оптимизации - зло.

Занимаюсь урезанием осетра. Изменение парадигмы сохранения в поток, может, и позволило бы сделать код чище, но плохо сказалось бы на дружественности к кешу (второй проход после загрузки, с модификацией загруженных объектов) И потребовало бы перелопачивать слишком многое.

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

Re: Cheb's Game Engine

Сообщение Alex2013 » 22.08.2019 15:41:22

Cheb писал(а):...и возможность матрёшечно вложенных сохранений тоже будет.

:shock: Это вообще как ? ( Если про сохранение класса то понятно ... но если про сэйвы в игре то тут даже мое больное воображение точно отказывает )
Изображение
Alex2013
долгожитель
 
Сообщения: 1575
Зарегистрирован: 03.04.2013 11:59:44

Re: Cheb's Game Engine

Сообщение Cheb » 22.08.2019 23:47:46

Допустим, посреди сохранения одному из объектов приспичило запустить ещё одно сохранение субсета по маске флагов в другой поток, начиная с другого корня.
Пример из практики - ассеты, сохраняющиеся в TMemoryStream матки, чтобы перезапуск длл не вызвал перезагрузку текстур.
Сейчас это реализовано эпическими городушками в отдельной не-класовой системе функций сохранения, где, фактически, две операции сохранения идут одна за другой. А можно будет запихать в метод BeforeSaving основного класса логики, да ещё с возможностью расширения.

Варианты возможны самые разные. Сохранить персонажа в отдельный контейнер для экспорта. Упаковать кусок мира в массив со сжатием, прежде, чем сохранять его. И т.п.

Добавлено спустя 9 часов 45 минут 52 секунды:
Условно,
класс МойКласс (ЧеперсиКласс) //где ЧеперсиКласс имеет поле Индекс: целое, в норме нуль но при сохранении туда пишется порядковый номер объекта
Фитюльки: баклуши;
Следующий: МойКласс;
Сепульки: баклуши;
конец;
переменная А, Б, В: МойКласс;
...
А:= МойКласс.Создать;
Б:= МойКласс.Создать;
В:= МойКласс.Создать;
А.Следующий:= Б;
Б.Следующий:= В;
В.Следующий:= А;

Предположим, Чеперси сохраняет их в поток с А в качестве корня. Тогда в потоке будет:
- А.Индекс (-1)
регистрационный № класса МойКласс
А.Фитюльки
- Б.Индекс (-2)
регистрационный № класса МойКласс
Б.Фитюльки
- В.Индекс (-3)
регистрационный № класса МойКласс
В.Фитюльки
А.Индекс (1)
В.Сепульки
Б.Сепульки
А.Сепульки

- как видим, крайне не рекомендуется использовать длинные цепочки объектов, соединённые ссылками: Чеперси справится, но может потребовать конский размер стека из-за глубокой рекурсии в сохраняторе.

Один из плюсов обновлённого механизма будет отсутствие необходимости чистить индексы в ноль после каждого сохранения. Этот второй проход был чудовищно недружествен кешу, поскольку *каждый* инстанс трастревоживался на запись из-за того, что индекс хранился внутри инстанса, как обычное поле.
Теперь эта память ведётся мемори менеджером и будет просто освобождаться посредством SetLength(, 0)
И это - то, что позволит матрёшечные сохранения, т.к. индексов может быть более одного.

Добавлено спустя 3 часа 54 минуты 18 секунд:
Приторочил по быстрому, пока не забыл: GLES не взлетала, если запускать Чентру из папки с расширенными символами в имени. Приторочил GetShortPathNameW в качестве обходного хака чтобы указывать путь к DLL Angle. Узнаем, помогло ли, когда закончу перелопачивание.
Аватара пользователя
Cheb
энтузиаст
 
Сообщения: 755
Зарегистрирован: 06.06.2005 15:54:34

Re: Cheb's Game Engine

Сообщение Alex2013 » 23.08.2019 22:33:21

Ну это как раз понятно (ООП я начинал изучать с ТурбоВижена а там "самосохраняться" в потоки умели тотально все объекты - помню как "от балды" написал в одной программе что-то вроде Application.SaveToFile ('ТрубоПоскакал.sav ' ) а в СОВЕРШЕННО ДРУГОЙ (!) Application.LoadFromFile('ТрубоПоскакал.sav ' ) и получил частично рабочую(причем даже часть нестандартных и перекрытых объектов успешно прочиталась! (Бо " пофигистический полиморфизм батенька "! :idea: ) ) копию интерфейса первой программы ... :shock:

Кстати интересно а с LCL такой фокус пройдет ? :roll:
Зы
И вот еще что в ТурбоВижене была любимая всеми абстракция под названием TCollection суть которой была в удобном сохранении любых объектов ( доступ как по индексу так и по имени сортировка навыбор + штатные итераторы, поиск, создание выборок,сохранение в поток, удаление с полиморфной очисткой, автоматическое увеличение и сжатие базовой "метрики" ) и все это очень быстро и главное ПРОСТО ! :idea: Что-то похожее но в разы примитивнее есть в функционале класса TList (и его наследников) но коллекции были в разы удобнее и универсальней .

К чему это я ? А к тому что что возможно стит возобновить функционал TCollection на новом уровне (без ограничения 16-ти разрядной модели памяти это делается достаточно просто ) Суть в том, что коллекции могут содержать что угодно, их можно рассматривать как ОДИН объект, и как массив объектов, и как кучу отдельных объектов со своим собственным поведением для каждого . То есть например "сборка мира" может представлять из себя "сборку коллекции " и суть удобства том что механика воспроизведения может совершенно ничего не знать о самых элементах и их взаимодействии, но уверенно отрабатывать простейшие базовые вызовы, а при необходимости запросто утрамбовать все это "разнотравье" в кусок архива и забыть о нем "до весны " .

Зы Зы
Кстати по моему любой SetLength() = "дикий тормоз" .
Последний раз редактировалось Alex2013 10.10.2019 18:46:35, всего редактировалось 1 раз.
Alex2013
долгожитель
 
Сообщения: 1575
Зарегистрирован: 03.04.2013 11:59:44

Re: Cheb's Game Engine

Сообщение Cheb » 24.08.2019 10:50:34

SetLength(<массив>, 0) - просто освобождает память, если массив не содержит менеджед сущностей в элементах.
Коллекции - интересная идея, но я слишком устал от сферических коней.
Будет в чём-то похожий функционал: разрежённые массивы объектов, со счётчиками ссылок, для индексации всех объектов физического мира longint индексами по слоям выполнения.
TurboVision я, фактически, не застал: ушёл в глубину, на низкий уровень, и плюнул на ООП, поскольку в 16-битной модели объекты страшно тормозили. А когда появился 32-битный Дельфи - её уже не стало.
Аватара пользователя
Cheb
энтузиаст
 
Сообщения: 755
Зарегистрирован: 06.06.2005 15:54:34

Re: Cheb's Game Engine

Сообщение Cheb » 25.08.2019 13:09:46

Обнаружил рассадник невалидного ассемблерного кода в часто используемой процедуре NoteThreadOccupation. Вполне вероятная причина крахов 64-битной версии.

Было
Код: Выделить всё
      asm
        mov rbx, [pes]
        mov rcx, qword[rbx + TMotherSEHState.tlitsc]
        mov eax, 0
        rdtsc
        mov dword[rbx + TMotherSEHState.tlitsc], eax;
        mov dword[rbx + 4 + TMotherSEHState.tlitsc], edx;
        shl rdx, 32
        add rdx, rax
        sub rdx, rcx
        xor rcx, rcx
        mov ecx, dword[rbx + TMotherSEHState.tlic]
        add qword[rbx + rcx * 8 + TMotherSEHState.tli], rdx
      end ['rax', 'rcx', 'rdx', 'rbx'];


Заменил на
Код: Выделить всё
      asm
        mov rax, [pes]
        mov rbx, rax
        mov rax, qword[rbx + TMotherSEHState.tlitsc]
        mov rcx, rax
        xor rax, rax
        rdtsc
        shl rdx, 32
        or rax, rdx
        mov qword[rbx + TMotherSEHState.tlitsc], rax;
        sub rax, rcx
        mov rdx, rax
        xor rax, rax
        mov eax, dword[rbx + TMotherSEHState.tlic]
        mov ecx, eax
        mov rax, rdx
        add qword[rbx + rcx * 8 + TMotherSEHState.tli], rax
      end ['rax', 'rcx', 'rdx', 'rbx']; 


З.Ы. Это - встроенный самопрофайлер, полная процедура:
Код: Выделить всё
function NoteThreadOccupation(o: TThreadLoadKind): TThreadLoadKind;
var
  pes: PMotherSehState;
{$ifdef cpuarm}
   oldc: timeval;
{$endif}
begin
  if not Assigned(Mother) or not Mother^.Safe then Exit(tlk_Working);
  pes:= GetExceptionStateIfExists(); // thread-local storage implemented
    // manually without relying on threadvar. Because reasons.
  if not Assigned(pes) then Exit(tlk_Working);
  if pes^.tlic = o then Exit(o); // no need, the value hasn't changed
  {$ifdef cpuarm}
    {$push}
    {$overflowchecks off}
    {$rangechecks off}
    oldc:= pes^.tlitsc;
    fpgettimeofday(@(pes^.tlitsc), NIL);
    pes^.tli[pes^.tlic]+=
      (int64(1000000) * int64(pes^.tlitsc.tv_sec - oldc.tv_sec))
      + int64(pes^.tlitsc.tv_usec - oldc.tv_usec);
    {$pop}
  {$else}
    // QueryPerformanceCounter doesn't hold a candle to this,
    //  both in call time performance and in precision
    // Because while QPC on most system uses TSC, WinAPI dumbs the result down
    //  as to not confuse legacy applications. Resulting in granularity of about
    //  400 nanoseconds.
    {$ifdef cpu64}
      asm
        mov rax, [pes]
        mov rbx, rax
        mov rax, qword[rbx + TMotherSEHState.tlitsc]
        mov rcx, rax
        xor rax, rax
        rdtsc
        shl rdx, 32
        or rax, rdx
        mov qword[rbx + TMotherSEHState.tlitsc], rax;
        sub rax, rcx
        mov rdx, rax
        xor rax, rax
        mov eax, dword[rbx + TMotherSEHState.tlic]
        mov ecx, eax
        mov rax, rdx
        add qword[rbx + rcx * 8 + TMotherSEHState.tli], rax
      end ['rax', 'rcx', 'rdx', 'rbx'];
    {$else}
      asm
        mov esi, [pes]
        mov ecx, dword[esi + TMotherSEHState.tlitsc]
        mov edi, dword[esi + 4 + TMotherSEHState.tlitsc]
        rdtsc
        mov dword[esi + TMotherSEHState.tlitsc], eax;
        mov dword[esi + 4 + TMotherSEHState.tlitsc], edx;
        sub eax, ecx
        sbb edx, edi
        mov ecx, dword[esi + TMotherSEHState.tlic]
        add dword[esi + ecx * 8 + TMotherSEHState.tli], eax
        adc dword[esi + ecx * 8 + 4 + TMotherSEHState.tli], edx
      end ['eax', 'ecx', 'edx', 'esi', 'edi'];
    {$endif}
  {$endif}
  Result:= pes^.tlic;
  pes^.tlic:= o;
end;
Аватара пользователя
Cheb
энтузиаст
 
Сообщения: 755
Зарегистрирован: 06.06.2005 15:54:34

Re: Cheb's Game Engine

Сообщение Cheb » 01.09.2019 23:46:59

Медленный прогресс медленный.

Обнаружил очередной ляпище во фрагменте кода, который срабатывает раз в столетие, когда луч последнего месяца падает на пердящего дрозда, сидящего на второй ёлке от вершины Одинокой горы.
И ведь могло спать годами.

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

Re: Cheb's Game Engine

Сообщение Mirage » 02.09.2019 00:07:07

Собсно преждевременная оптимизация корень всех зол именно поэтому, а не просто так. :)
Mirage
энтузиаст
 
Сообщения: 858
Зарегистрирован: 06.05.2005 20:29:07
Откуда: Russia

Re: Cheb's Game Engine

Сообщение Cheb » 02.09.2019 09:19:31

Эта конкретная подсистема слишком дорогосторящая и слишком специфичная, чтобы переделывать её потом.
Как нельзя переделывать фундамент или киль у корабля
Я её два года пилил, с 2006 по 2008-й, а после этого - лишь выгребал говнокод из самых дремучих мест. Ещё, пару лет назад кучу глобальных переменных перенёс внутрь класса "заголовок потока", чтобы сэкономить на синтаксическом разборе, если поток определённого типа читается не один раз - что будет иметь место быть, т.к. сетевые пакеты будут из себя представлять тоже дерево объектов, сохранённое в беззаголовочный поток со сжатием.
Но в целом, код меняется мало. Там, например, спокойно живут потомки костыля дженериков, который я запилил ещё когда фпц 1.х не поддерживал динамические массивы. Только выкинул препроцессор специализации, навсегда зафиксировав эти классы в статичной форме. А самая важная часть основана на переменных процедурного типа, где указатели на нужные процедуры вставляются в процессе составления сценариев загрузки.
По принципу: работает? Не трогай.
Сейчас просто добавляю фичи, задуманные ещё в 2012-м (или ещё раньше?), но слишком геморройные, из-за чего всё тянул с ними.
Причём, архитектура кода опять меняется мало. Пару процедур перенёс в поля класса "обходчик", добавил поддержку вложенных обходчиков (строго в одном потоке) и пару полей базового класса заменил на обращения к менеджеру памяти, в который они выпихнуты.
Менеджер памяти - да, попаболь. Но без него, опять же, не будет никакой дружественноски к кешу при работе с индексами (которых теперь, кстати, несколько, по одному на каждый из вложенных обходчиков) или флагами.
Аватара пользователя
Cheb
энтузиаст
 
Сообщения: 755
Зарегистрирован: 06.06.2005 15:54:34

Re: Cheb's Game Engine

Сообщение Cheb » 03.09.2019 09:19:46

..иии ещё одна ересь, оставшаяся с времён когда pointer = dword. Сэкономил, понимаешь, на локальных переменных.
Вопрос в студию: что вернёт Assigned если 64-битный указатель был грязный перед записыванием в него dword'а?
Код: Выделить всё
procedure RP_Metaclass (PField: pointer; OP: TFieldOperation; Tind: integer;
  FieldProperties: TFieldPropertiesSet);  RP_CallingConvention;
begin
  case op of
    fio_Load: begin
      dword(PField^):= ReadDword();
      if Assigned(pointer(PField^)) then begin
         {$ifdef safeloading}
          if dword(PField^) >= Length(DH.Cnums) then DieInvalidContainer(RuEn('Данные запороты: индекс класса - за пределами!','Data corrupt: class index out of bounds!'));
         {$endif}
         CChepersyObject(PField^):= DH.B_Classes[dword(PField^)];
      end;
    end;
    fio_Clone: Assert(not (fpro_CloneByIncreasingRefCount in FieldProperties), 'This type does not support cloning-by-increasing-RefCount');
       //; //Do nothing. Any metaclass references are kept as is.
    fio_Save:
      if not Assigned(pointer(pfield^)) then WriteDword(0)
      else WriteLongInt(CChepersyObject(PField^).ClassIndex);
    fio_Skip: ReadDword; //SkipDword;
    fio_Walk:;
  end;
end;


Заменил на

Код: Выделить всё
procedure RP_Metaclass (PField: pointer; OP: TFieldOperation; Tind: integer;
  FieldProperties: TFieldPropertiesSet);  RP_CallingConvention;
var
  c: dword;
begin
  case op of
    fio_Load: begin
      c:= ReadDword();
      if c = 0 then pointer(PField^):= nil
      else
         {$ifdef safeloading}
          if c >= Length(DH.Cnums) then DieInvalidContainer(RuEn('Данные запороты: индекс класса - за пределами!','Data corrupt: class index out of bounds!'));
         {$endif}
         CChepersyObject(PField^):= DH.B_Classes[c];
      end;
    end;
    fio_Clone: Assert(not (fpro_CloneByIncreasingRefCount in FieldProperties), 'This type does not support cloning-by-increasing-RefCount');
       //; //Do nothing. Any metaclass references are kept as is.
    fio_Save:
      if not Assigned(pointer(pfield^)) then WriteDword(0)
      else WriteLongInt(CChepersyObject(PField^).ClassIndex);
    fio_Skip: ReadDword;
    fio_Walk:;
  end;
end;
Аватара пользователя
Cheb
энтузиаст
 
Сообщения: 755
Зарегистрирован: 06.06.2005 15:54:34

Re: Cheb's Game Engine

Сообщение Cheb » 05.09.2019 11:22:48

Да. Когда-то надо остановиться.
Сегодня обдумывал устройство многослойного мира - и части головоломки щёлкнули наконец, складываясь в единое - и главное, простое! - целое.

1. Логика - однопоточная во веки веков (оставляя возможность распараллеливать операции, результат которых можно применить после их завершения - т.е. сами игровые объекты из других потоков не модифицируются, применение результатов происходит в логическом потоке).
2. Никаких оптимизированных массивов для индексации объектов по индексу типа longint. Все связи обычными ссылками (поля типа TObject, упрощённо говоря)
3. Во все поля эксплуатируется механизм "ускоренных" полей класса, которые я сейчас активно строгаю - и для реализации которых, собственно, потребовался менеджер памяти. Один из главных плюсов - все поля одного типа стираются командой менеджеру памяти, и операция эта практически не загрязняет кеш.
Ускоренные поля НЕ сериализуются в поток, но я просто решил, что в сохранёнку идёт только базовый слой (когда сохраняешь сетевую игру, например, или сервер пакует снапшот мира для нового игрока).
4. Перед использованием любого объекта надо убедиться, что ссылка ведёт на объект текущего слоя посредством метода ThisLayer.
например:
//BearIAmRunningFrom - поле класса
var bear: TBear; //локальная переменная метода
bear:= TBear(BearIAmRunningFrom.ThisLayer);
???:= bear.Hunger;
или
???:= TBear(BearIAmRunningFrom.ThisLayer).Hunger;
5. Перед любым изменением, объект надо получить из его же метода BeforeModifying, который склонирует объект, если он принадлежит не текущему слою.
пример: bear:= TBear(bear.BeforeModifying); //может вернуть себя, а может и клона
Для отлова мест, где забыл - специальный режим отладки (люто тормозной), который каждый тик обходит всё дерево и составляет CRC для всех объектов всех слоёв (хранятся, опять же, в ускоренных полях).
6. Клоны объекта для других слоёв хранятся в ускоренных полях, из которых их берут методы ThisLayer и BeforeModifying, так что ссылка на изначальный объект продолжает работать - и не требует ни модификации, ни проверок в момент клонирования!
7. Объект, имеющий клонов в других слоях, вместо удаления кладётся на кладбище и лежит там, пока клоны не удалятся. Таким образом, ссылка на изначальный объект продолжает работать.

Зачем нам нужно, чтобы ссылка на изначальный объект не нашего слоя продолжала работать? Чтобы обеспечить clone-on-write (посредством костыля BeforeModifying) самым тупым и пошлым образом: копированием куска памяти со всем содержимым (с дополнительным проходом чтобы подправить счётчики ссылок строк и массивов, правда). Проход по этим самым массивам (на случай, если содержат объекты) не нужен: ведь старые ссылки продолжают работать, надо лишь не забывать использовать ThisLayer.

Кажется ужасным костылём, но на деле даёт эпическую эффективность расслоения и эпическую дружественность к кешу: ничего не трогается и не клонируется, пока не вызван BeforeModifying.
А учитывая планируемую парадигму "ленивой" физики (просчитать линейное движение на дохрена тиков вперёд и заснуть, разбудите по таймеру или когда в меня ткнётся кто-нибудь) подавляющее большинство объектов просто не успеет расслоиться, поскольку просто не успеют тикнуть за то время, что живут слои лагокомпенсации (500 мс, чаще гораздо меньше)

8. Ещё одно больное место - механизм "я сплю, разбудите через N тиков" будет также реализован на уровне менеджера памяти и ускоренных полей.

Но цена всего этого, повторюсь - логика однопоточная навеки.

Добавлено спустя 3 часа 50 минут 26 секунд:
..в деталях, пока что вырисовывается такое:
(когда, наконец, соберётся - предстоит радость отладки всего сваянного)

Код: Выделить всё
procedure TChepersyObject.ChangeMask(values, maskmask: dword);
var
  i, cidx: integer;
  chunk: TChepersyMemoryManagerChunk;
  val, newval: dword;
begin
  chunk:= GetMemoryManagerChunk;
  cidx:= chunk.GetChunkInd(Self);
  val:= chunk.AcceleratedField[CPS_AFK_MASK][cidx];
  newval:= (val and (not maskmask)) or (values and maskmask);
  if newval <> val then chunk.AcceleratedField[CPS_AFK_MASK][cidx]:= newval;
end;

где

const
  CpsMMChunkSizePoT = 17; // 128K
  {$push}
    {$overflowchecks off}
    CpsMMChunkAddrMask = (ptruint(ptrint(-1)) shr CpsMMChunkSizePoT) shl CpsMMChunkSizePoT;
  {$pop} 

function TChepersyObject.GetMemoryManagerChunk: TChepersyMemoryManagerChunk;
begin
  // Yeah, I'm great! B)
  // zero out the lower bits of the address to get the chunk address
  ptruint(pointer(Result)):= ptruint(pointer(Self)) and CpsMMChunkAddrMask;
end;

где

  TChepersyMemoryManagerChunk = class
  protected
   ...
    function _GetAccField(kind: TCpsAccFieldKind; idx:integer): dword;
    procedure _SetAccField(kind: TCpsAccFieldKind;  idx:integer; value: dword);
  public
   ...
    property AcceleratedField[kind: TCpsAccFieldKind; idx: integer]: dword
      read _GetAccField write _SetAccField;
  end;
 
где

function TChepersyMemoryManagerChunk._GetAccField(kind: TCpsAccFieldKind; idx:integer): dword;
begin
  Assert((idx >= 0) and (idx <= f_IdxHigh), 'index out of bounds in TChepersyMemoryManagerChunk._GetAccField()');
  if not Assigned(f_AccField[kind]) then Exit(0);
  if 0 = (f_AccFieldMask[kind][idx div 32] and (dword(1) shl (idx mod 32))) then Exit(0);
  Result:= f_AccField[kind][idx];
end;

const ptruint_bits = sizeof(ptruint) * 8;

procedure TChepersyMemoryManagerChunk._SetAccField(kind: TCpsAccFieldKind;  idx:integer; value: dword);
begin
  Assert((idx >= 0) and (idx <= f_IdxHigh), 'index out of bounds in TChepersyMemoryManagerChunk._GetAccField()');
  if value = 0 then begin
    if not Assigned(f_AccField[kind]) then Exit;
    if 0 = (f_AccFieldMask[kind][idx div ptruint_bits]
      and (ptruint_bits(1) shl (idx mod ptruint_bits))) then Exit;
    dec(f_AccFieldCount[kind]);
    if 0 = f_AccFieldCount[kind] then begin
      SetLength(f_AccField[kind], 0);
      SetLength(f_AccFieldMask[kind], 0);
    end;
  end
  else begin
    if not Assigned(f_AccFieldMask[kind])
      then SetLength(f_AccFieldMask[kind], DivRoundUp(f_IdxHigh, ptruint_bits));
    if (High(f_AccField[kind]) < idx)
      then SetLength(f_AccField[kind], max(Length(f_AccField[kind]),
                                1 + min(f_IdxHigh, DivRoundUp(idx, 16) * 16)));
    if 0 = (f_AccFieldMask[kind][idx div ptruint_bits]
      and (ptruint_bits(1) shl (idx mod ptruint_bits)))
    then begin
      inc(f_AccFieldCount[kind]);
      f_AccFieldMask[kind][idx div ptruint_bits]:= f_AccFieldMask[kind][idx div ptruint_bits]
        or (ptruint_bits(1) shl (idx mod ptruint_bits));
    end;
    f_AccField[kind][idx]:= value;
  end;
end;
Аватара пользователя
Cheb
энтузиаст
 
Сообщения: 755
Зарегистрирован: 06.06.2005 15:54:34

Пред.След.

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

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

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

Рейтинг@Mail.ru