diff options
Diffstat (limited to 'docs')
| -rw-r--r-- | docs/appendices/knowledge-boundaries.md | 191 | ||||
| -rw-r--r-- | docs/appendices/script-vm.md | 200 | ||||
| -rw-r--r-- | docs/reference/atmosphere.md | 65 | ||||
| -rw-r--r-- | docs/reference/environment-effects.md | 199 | ||||
| -rw-r--r-- | docs/reference/msh.md | 50 | ||||
| -rw-r--r-- | docs/reference/render-frame.md | 179 | ||||
| -rw-r--r-- | docs/tomes/05-render.md | 397 | ||||
| -rw-r--r-- | docs/tomes/06-behavior.md | 50 |
8 files changed, 1065 insertions, 266 deletions
diff --git a/docs/appendices/knowledge-boundaries.md b/docs/appendices/knowledge-boundaries.md index ab6c1f8..a60fa68 100644 --- a/docs/appendices/knowledge-boundaries.md +++ b/docs/appendices/knowledge-boundaries.md @@ -10,6 +10,11 @@ authoring API без динамического подтверждения. Не доказаны символами точные имена renderer vtable slots, полный набор CShade state transitions и окончательный порядок части transparent/FX/shadow subpasses. +Дистанционная кривая тумана по квадратам границ подтверждена в живом CShade. +Обычный GPU fog color из горизонта подтверждён исполнением; выбор проходов по +native shade-state и их временные изменения цвета пока заменены общим туманом мира; +это приближение считается как `render_coverage fog_pass_eligibility_approx`. + После ограничения цели рельефом интерактивная камера проверяет движение против каждого отрисовываемого корневого строения класса `FORT` и его отрисовываемых компонентов. Для этого пути используются текущая Node38-поза @@ -45,33 +50,50 @@ MSH-запрос со свободной preview-камерой; это не пÐ стратегической и следящей камерами не подтверждено ([MSH](../reference/msh.md)). -Для MSH без потока цветов type 16 подтверждён только один постоянный alpha -override для диапазонов с CPU-цветом и raw-флагом `Batch20` `0x0008`: GOG -`Terrain.dll`, SHA-256 -`af87d1b2e728a0be73c52be3b44cc196ab46da7799f25a15d40f8c9b0b425ead`, RVA -`0x1B0F0`, задаёт alpha `0`, если установлен bit `0x0040` и сброшен `0x0010`. -Это покрывает `0x0148`; `0x0108`, `0x0118` и `0x0058` сохраняют alpha MAT0, -причём в `0x0058` bit `0x0010` имеет приоритет. Поток type 16 или отсутствие -`0x0008` исключают override. Проверка заводской сцены с фиксированной нативной -камерой подтвердила на одном кадре входа восстановление X-образного рисунка -двери, который виден и в оригинале; прежний Vulkan-readback показывал на этом -месте сплошной чёрный прямоугольник. Сравнение выполнено при времени сцены -2000 мс и трёх кадрах, запуск с Vulkan validation завершился без предупреждений -и ошибок. Это подтверждает только структуру проёма в этой сцене: правильность -освещения и полный попиксельный паритет не установлены. - -Общее fade-поведение по расстоянию в preview не реализовано. Открыты точная -метрика расстояния, выбор первого CPU-принятого треугольника диапазона и -эквивалентность frustum/culling-отбора оригинала; эти вопросы нельзя вывести из -захвата заводской двери. Данные потока type 16 ещё не декодированы, поэтому -правило выше относится только к MSH-диапазонам без этого потока цветов. +Кривые alpha и метрика расстояния порталов реализованы для MSH без type 16 +([описание кадра](../reference/render-frame.md#прозрачность-порталов-msh)). Native CPU clipping может выбрать другую первую вершину; +это приближение явно считается как `portal_fade_packet_selection_approx`. + +Данные type 16 ещё не декодированы и считаются в `msh_type16_colors_unsupported`; +в статических сценах 29 миссий GOG (1736 загрузок моделей) этот поток отсутствует. +Его декодирование отложено до первого реального ненулевого счётчика. +Внутренний ракурс «Конструктора» после +настоящего входа игрока подтверждает исчезновение чёрной поверхности в проходе; +наружные окна обеих миссий сверены на расстояниях около 60/85/110, а ближние +окна — с внутренней стороны. Окно «Конструктора» на правом краю кадра при +дистанции около 85 совпадает; внутренние дальние ракурсы и остальные случаи +отсечения ещё не приняты. Общей готовности R2 эти пары не доказывают. + +Старые захваты без `geometry_projection` используют состояние камеры после +обхода мира, которое может отсекать близкие поверхности. Это отмечается +`captured_projection_unverified`; для приёмки таких поверхностей нужен новый захват. + +Захваты без `sky_uv_clock` не подтверждают начальное положение облаков, +звёзд и фона. Replay использует elapsed clock приложения и считает +это как `captured_sky_phase_unverified`; для приёмки неба нужен новый захват. Закрывающий эксперимент: запустить оригинал в совместимой Windows/DirectX среде, перехватить DirectDraw/Direct3D calls и surface flips, сохранить state log на минимальных сценах с одним типом материала. +Горизонтальные грани Land.msh с флагом `0x20000` используют исходную карту Du/Dv, окно shifted-камеры и квадратный размер текстуры, проверенные исполнением DLL. Диапазон глубины, небо/туман/FX основного вида и соответствие размеров при обрезанном capture остаются приближениями: `planar_reflection_render_context_approx`; неперспективная камера API использует обычное зеркало. +Raw queue clock задаёт фазу волн отдельно от времени суток и sky elapsed; старые захваты без него считаются в `captured_water_phase_unverified`. +Один target отражает ближайшую активную высоту воды; остальные высоты и неплоские грани считаются в `planar_reflection_other_planes_unsupported` и `planar_reflection_nonplanar_unsupported` и сохраняют обычный terrain shader. +`terrain_face8_pipeline_unverified` теперь считает только исходные грани с `0x8` без `0x20000`; сама семантика оставшихся специальных проходов ещё не установлена. + ## FXID field-level semantics +Control Op1 пока использует цвет и радиус без случайных добавок команды; +это явное приближение считается в `render_coverage control_op1_jitter_unsupported`. +Масштаб радиуса длиной преобразованного направления и нижняя замена 0,01 восстановлены. + +Control Op2 воспроизводит modes 0/2; modes 1/3, body+76 и неподдержанные +time modes/привязки считаются как `render_coverage control_op2_unsupported`. +Управление фазой экземпляра командами Control остаётся до его подключения в runtime. +При расширении выбранных начальных строк все пропущенные команды считаются +в `control_fx_unsupported`, а time modes 4–17 — в `control_fx_requires_game_state`; +причина и владелец выводятся в лог. Неразобранные FXID считаются отдельно в `control_fx_invalid_documents`. + Размеры команд, resource references, lifecycle, flags families и используемые time modes известны. Не закрыто значение каждого поля body opcodes 1--10, отсутствующий во всех проверенных каталогах opcode 6 и точные формулы редких @@ -133,7 +155,7 @@ control word процесса. Лицензированный AutoDemo по-прежнему загружается с raw TMA anchor-ами `[(500.2857, 751.8217), (728.0, 449.0)]` и записями `[(500, 752, 0), (728, 449, 1)]`. Rust теперь также читает optional `.fml` sibling для каждого -script base и поддерживает numeric literal assignments в первом `Init`. +script base и поддерживает узкий набор атомарных формул в первом `Init`. Отсутствующий `.fml` не мешает пакету с одними `Handler(19)`; другие ошибки чтения распространяются как VFS errors. @@ -144,9 +166,11 @@ script base и поддерживает numeric literal assignments в первÐ два доказанных действия: `Handler(19)` и formula assignment selector `-1` при source `-1`, нулевой глубине условий и корректных индексах. У `Handler(19)` control находится в `header_words[3]`, а `header_words[1]` — result-save -reference; оба равны `0xffffffff` в поддержанной форме. У formula assignment -`header_words[1]` — индекс целевой varset ячейки, `header_words[2]` — source, -а индекс formula row занимает `header_words[6]`. Маппинг подтверждён +reference; оба равны `0xffffffff` в поддержанной форме. В дисковом заголовке +formula assignment `header_words[1]` — индекс целевой varset ячейки, +`header_words[2]` — source, а индекс formula row занимает `header_words[6]`; +индексы цели и формулы в native record находятся по смещениям `+0x08` и `+0x1c` +соответственно. Маппинг подтверждён `ai.dll!FUN_10011b20` (RVA `0x11b20`) и исполнителем `FUN_10012020` (RVA `0x12020`), SHA-256 `c19cd6cd31cd2896f8c4ba0f0e700dd3408aec8182daaf4863d98a7d233a2abc`. `Handler(19)` (`FUN_1000aa38`, RVA `0xaa38`) использует references `0..2`; @@ -154,23 +178,58 @@ reference; оба равны `0xffffffff` в поддержанной форме Formula table загружается из `<bundle-base>.fml`; native `FUN_100123b0` (RVA `0x123b0`) проверяет ordinal, compiler `FUN_100152a0` (RVA `0x152a0`) -строит numeric literal node, а evaluator `FUN_10015b30` (RVA `0x15b30`) -возвращает его raw `f32`. Target type `3` сохраняет эти bits; type `5` проходит -через `FUN_10012c00` (RVA `0x12c00`) и `__ftol` (RVA `0x1df70`) к raw DWORD -с усечением к нулю. Изолированный x86 oracle на оригинальном `ai.dll` и -настоящих ресурсах подтвердил таблицы `tut1_pl2`/`scr_pl_1`/`scream` с +строит числовые, переменные и операторные узлы, а evaluator `FUN_10015b30` +(RVA `0x15b30`) в проверенных путях `+` и `-` считывает значения переменных и +сохраняет результаты как `f32` при x87 control word `0x007f`. В GOG-сборке +`ai.dll` с SHA-256 +`c19cd6cd31cd2896f8c4ba0f0e700dd3408aec8182daaf4863d98a7d233a2abc` поиск +переменной через varset vtable slot `+0x10` ведёт на RVA `0x16ad0`: сравнение +байтов точное, регистр учитывается, при повторах берётся первое объявление. +Getter `FUN_10013190` (RVA `0x13190`) читает type `3` как `f32`, type `5` как +беззнаковый `DWORD` и возвращает число через x87. Затем evaluator сохраняет +его как `f32` до операции. Оператор `+` +считает right+left, `-` — left-right; результат также сохраняется как `f32`. +Target type `3` сохраняет эти bits; type `5` проходит через `FUN_10012c00` +(RVA `0x12c00`) и `__ftol` (RVA `0x1df70`) к raw DWORD с усечением к нулю. +Изолированный x86 oracle на оригинальном `ai.dll` и настоящих ресурсах +подтвердил таблицы `tut1_pl2`/`scr_pl_1`/`scream` с 26/5/149 строками и 15 присваиваний: кампанийные `df0..df3` и `Single.01` `dT8/dT9` получают `0`; `scream` задаёт `fDifficulty = 0.8f` и остальные проверенные DWORD `0, 0, 1, 10, 0, 100, 400, 2`. -Граница Rust остаётся узкой: ссылка должна вести на один digit-starting numeric -token и типизированную `float`/`DWORD` ячейку. Неизвестные expression nodes, -переменные, unary operators, составные выражения и source-copy ветвь -`source != 0xffffffff` остаются явными ошибками; native source-copy переносит -всю varset record, не только числовое значение. В preview package сохраняется, -но `Unsupported` не применяет частичные writes; полная загрузка отклоняет весь -новый мир и сохраняет предыдущие world/RNG. Это политика Rust, не правило -оригинального dispatcher-а. +Граница Rust остаётся узкой: один числовой токен либо ASCII-имя переменной, +или два таких атома с одним бинарным `+`/`-`. Имя проверяется раньше числового +преобразования, сравнивается с учётом регистра и выбирает первое совпадение; +публичный `FormulaTable::evaluate_numeric_literal` принимает только числовые +литералы. Временная копия значений начинается со значений по умолчанию и +обновляется после каждого успешного действия `Init`, поэтому следующая формула +видит предыдущие записи. Нет +поддержки неизвестных операторов, унарных форм, третьего атома, отсутствующих +или не-ASCII имён и source-copy ветви `source != 0xffffffff`; native source-copy +переносит всю запись varset. Preview сохраняет package, но не применяет частичные +writes; полный load отклоняет новый мир и сохраняет прежние world/RNG. Это +политика ограниченного Rust resolver-а, а не правило оригинального dispatcher-а. + +Ресурсные строки с переменными найдены в поздних событиях: `c1m3e.fml` row 19 +(`0.8 + fT`) вызывается из `Fort_Captured`, row 22 +(`dBuildingProtectHits - dT2`) — из `PBM_BUILDING_PROTECT_Start`, а `c1m4e.fml` +row 0 (`fTemp + 0.00001`) — из `PBM_BUILDING_INF_CAPTURE_Start`. Декодирование +этих `.scr` не показывает эти формулы в первом `Init`. Игнорируемая проверка +читает настоящие `.fml` и `varset.var`, но вставляет строки в синтетический +`Init`; она проверяет resolver, а не оригинальное исполнение. Одноразовый x86 +oracle на исходной GOG `ai.dll` с SHA-256 +`c19cd6cd31cd2896f8c4ba0f0e700dd3408aec8182daaf4863d98a7d233a2abc` проверил +13 отдельных присваиваний: 9 контролируемых выражений и 4 вызова с настоящими +строками `.fml` (`c1m3e` rows 19/22, `c1m4e` row 0 дважды подряд). В каждом векторе +native harness записывал вычисленный `f32`, типизированное raw-слово и getter; +scratch-проход через публичный Rust parser/resolver совпал со всеми 13 +векторами по `f32`-результату и raw-записи. Для native вызовов control word +временно принудительно устанавливался в `0x007f`: 87 наблюдений сохранили +`0x027f` на входе и после восстановления, +а canary-проверки прошли. Менялись только varset-копии в памяти. Это +подтверждает компиляцию формулы, локальное вычисление и typed writes для этих +входов, но не запуск исходного поиска и обхода события `Init`, исполнение поздних +событий или поведение живой миссии. Лицензированный test загружает одну первую кампанийную миссию `CAMPAIGN/CAMPAIGN.00/Mission.01/data.tma` после literal writes; это не проход @@ -499,6 +558,41 @@ MSH-часть проверки подтверждает начальный сн остаётся статическим выводом по RVA `AniMesh.dll+0xa460`, а последующие изменения в полном Control/animation tick-е не проверялись. +Устройство фабрики `2` через selector `6` читает `+0x174` только при текущем descriptor tag `2`, +затем проверяет, что устройство создано фабрикой `2` и поле доступно. Значение +должно быть конечным нормальным числом или нулём любого знака. Строки в +`[27..35)` и `[35..43)` читаются до первого NUL: хвост игнорируется, +ASCII-регистр не важен. После изменения пустая строка оставляет старые биты; +пустая пара конструктора уже совпадает и не вызывает пересчёт. Проверка не +использует список имён: нужен один корневой mesh `BULL` из `ObjectsRegistry`, +без дочерних компонентов и записей timeline; все типизированные записи должны +иметь tag `17`, а эффекты и таблица NDP — разбираться полностью. Число строк +NDP произвольно, включая пустую таблицу. + +Сканер возвращает коды выбранной начальной группы в исходном порядке. Допустимы +`1/4/12`. `Some([])` означает, что по полному разбору данных команд нет: группа +отсутствует, пуста или сырое беззнаковое значение индекса не меньше числа +групп. `None` означает неподдержанный либо неполный разбор; повреждённые данные +дают ошибку. Для выбранного opcode `4` нужны FXID-типы `2/3/4/5/7/8/9` и +допустимые CPT-смещения внутри компонента; типы `1/6/10` остаются недоступными. +Непредставленный ключ или число не превращаются в ноль. Если распознанный +ресурс повреждён, новая миссия завершается ошибкой до публикации, сохраняя +прежние мир и RNG. + +Загрузка лицензированной AutoDemo и Rust-запрос selector `6` охватили 12 +устройств и семь пар с битовыми значениями: +`bb_m_01=43f98000`, `bm_m_01=44228000`, `bl_l_01=436a0000`, +`br_b_01=44c80000`, `bf_b_01=443b8000`, `bl_m_01=443b4000`, +`bm_l_01=43340000`. Все семь значений совпали с замороженными арифметическими +векторами. Пятнадцать изолированных вызовов selector `35` оригинальной DLL +(включая семь однострочных BULL-таблиц) проверили арифметику при x87 control +word `0x007f`, но не создавали полный native World. Таблица Control-устройств +Rust по-прежнему строится при полной загрузке +Unit DAT. CPU-проекция не воспроизводит временное создание объектов оригинала и +его побочные эффекты FX, звука и RNG, размещение в World или следующий Control +tick. Полное размещение объектов, игровой tick, Handler 43 и другие +неподдержанные форматы остаются открытыми. + Для provider-строки Rust хранит amount `+0x08`, накопитель `+0x10`, обработанное количество `+0x0c`, долю `+0x14`, стадию `+0x18` и flags `+0x28` раздельно. Инициализатор `Control.dll+0xf940` кладёт результат selector `3` в текущее и @@ -741,16 +835,19 @@ resource-device значения во время игры. `0x5780/0x57c0` участвуют в векторе; Rust воспроизводит его raw output bits и отдельные cache, order, clamp и signed-zero случаи. -Для обычной начальной загрузки scale-input тоже закрыт. AniMesh constructor -оставляет `+0x158/+0x15c/+0x160` равными `1.0`; перед Control event `4` World3D -проходит Agent event `0`, AniMesh RVA `0x1a80` вызывает базовый return `0x17df0` -и передаёт event `4/null` модели, FX, CollManager и CollObject. Их проверенные -ветви `0x75b0/0x6fc0`, Effect RVA `0x3d30`, Control RVA `0x1ce00` и `0x1fe00` -не вызывают AniMesh setter `0x14770` и не меняют outer scale. Поэтому FAC0 в -стандартном `LoadAgent` получает `[1, 1, 1]`. Другой маршрут -`AddNewObjectToGame`/mirror к setter и последующие placement/tick остаются -отдельными границами. В synthetic x86-векторе по-прежнему явно заданы `[2,3,5]`; -он не является источником scale для полного load. +Для обычной начальной загрузки входной масштаб единичный. Конструктор модели +AniMesh RVA `0x68a0` задаёт `Model+0x158/+0x15c/+0x160 = 1.0`; +`Agent+0x158` хранит указатель на отдельный Control. Перед Control event `4` +World3D проходит Agent event `0`: AniMesh RVA `0x1a80` вызывает базовый return +`0x17df0` и передаёт event `4/null` модели, FX, CollManager и CollObject. +Проверенные ветви модели `0x75b0/0x6fc0`, Effect `0x3d30` и CollManager +`0x1ce00` не меняют масштаб. CollObject получает событие через основной +интерфейс `0x3c2fc`, slot `+0x08 -> 0x1f530`: он кэширует host-интерфейсы, +а необязательный вызов CollManager `0x1c330` сохраняет только указатель +`CollObject+0x20` в `CollManager+0x3c`. Поэтому FAC0 получает `[1, 1, 1]`. +Отдельные пути `AddNewObjectToGame`/mirror и последующие placement/tick остаются +за этой границей. В synthetic x86-векторе scales `[2,3,5]` заданы отдельно. +Подробная привязка интерфейсов приведена в [разборе Script VM](script-vm.md). Смысл `Slot68 +0x30/+0x34`, tag-27 cache DWORD, owner accumulators `+0x530`, `+0x534`, `+0x538` и результата mutable word `26` не имеет доказанных единиц diff --git a/docs/appendices/script-vm.md b/docs/appendices/script-vm.md index 6e48445..f7f8b97 100644 --- a/docs/appendices/script-vm.md +++ b/docs/appendices/script-vm.md @@ -22,8 +22,8 @@ symbol/formula oracle; `varset.var` задаёт `VAR(...)`/`STRING(...)` defaul `fparkan-script::parse_varset` уже читает подтверждённые numeric `VAR(float|DWORD, name, default)` declarations byte-safe (comments остаются opaque, поэтому legacy non-UTF-8 text не ломает загрузку); `STRING(...)` и -general `FUNCTION(...)` evaluation остаётся вне numeric contract, но принятые -строки сохраняются для доказанных literal assignments первого `Init`; +general `FUNCTION(...)` evaluation остаются вне восстановленного контракта, +но принятые строки сохраняются для ограниченного поднабора формул первого `Init`; GOG `MISSIONS/SCRIPTS/varset.var` даёт через него ровно 231 declaration: 31 `float` и 200 `DWORD` (от `f0` до `fY`); loader `ai.dll!0x10001000` сначала открывает `<bundle-base>.var` и только при @@ -424,10 +424,13 @@ nearest-even `FISTP qword`, младший DWORD, преобразование D Ограниченный Rust-исполнитель `VarSet::resolve_init` ищет первое событие `Init` и проходит его инструкции по порядку. Он возвращает записи для -поддержанного `Handler(19)` и числовых literal assignments; прочая форма -останавливает preview на `Unsupported`. Runtime применяет упорядоченные записи -к отдельной копии varset выбранного клана лишь после успешного прохода всего -события. Отсутствующий `Init` даёт +поддержанного `Handler(19)` и формул с одним числовым или переменным атомом либо +с двумя такими атомами, соединёнными одним бинарным `+` или `-`; прочая форма +останавливает preview на `Unsupported`. Runtime начинает вычисление с копии +defaults и записывает в неё каждый успешно разрешённый шаг, поэтому поздняя +формула видит прежние записи этого события. Упорядоченные записи попадают в +состояние клана только после успешного прохода всего `Init`. Отсутствующий +`Init` даёт статус `Missing` и не создаёт записей инициализации; пустой первый `Init` даёт `Executed` без записей. Если форма инструкции пока не поддержана, package остаётся доступен в preview режиме со статусом `Unsupported`, а default-значения @@ -437,7 +440,7 @@ nearest-even `FISTP qword`, младший DWORD, преобразование D Это правила ограниченного Rust-исполнителя, а не обнаруженное поведение оригинального dispatcher-а. -#### Числовые литералы `.fml` в `Init` +#### Литералы и простые формулы `.fml` в `Init` В той же сборке `ai.dll` (`c19cd6cd31cd2896f8c4ba0f0e700dd3408aec8182daaf4863d98a7d233a2abc`) загрузчик `FUN_10001000` открывает `.fml` рядом с базой каждого `.scr`. @@ -455,16 +458,35 @@ nearest-even `FISTP qword`, младший DWORD, преобразование D остаётся ошибкой VFS. Native selector `-1` при source `-1` и нулевой глубине условий выбирает -formula assignment; в native record `header_words[1]` — индекс целевой varset -ячейки, а `header_words[6]` — индекс formula row. `FUN_100123b0` (RVA `0x123b0`) проверяет индекс строки, -`FUN_10015b30` (RVA `0x15b30`) возвращает результат единственного literal node -kind `3`, а `FUN_10013650` передаёт его типизированной ячейке через -`FUN_10012c00` (RVA `0x12c00`). Компилятор `FUN_100152a0` получает числовой -prefix как `double` через `FUN_10020af0` (RVA `0x20af0`), затем записывает -его как `float` в node `+0x10`. Rust ограничивает эту ветвь одним -digit-starting числовым токеном с необязательным хвостом пробельных байтов; -ведущие unary `+`/`-`, variable references, операторы и другие node kinds -остаются явными `UnsupportedExpression`. +formula assignment. В дисковом заголовке `.scr` `header_words[1]` — индекс +целевой varset-ячейки, а `header_words[6]` — индекс formula row; соответствующие +поля native record находятся по смещениям `+0x08` и `+0x1c` соответственно. +`FUN_100123b0` (RVA `0x123b0`) проверяет индекс строки. Компилятор +`FUN_100152a0` (RVA `0x152a0`) создаёт +числовые узлы, узлы переменных и узлы операторов; числовой prefix проходит через +`FUN_10020af0` (RVA `0x20af0`) как `double`, после чего сохраняется в узле как +`float`. Для переменной компилятор вызывает vtable slot `+0x10` таблицы varset; +в проверенной GOG-сборке `ai.dll` этот вызов ведёт к поиску по RVA `0x16ad0`. +Поиск сравнивает байты имени с учётом регистра и возвращает первое совпадение. +`FUN_10015b30` (RVA `0x15b30`) читает числовой узел как `f32`, а переменную — +через `FUN_10013190` (RVA `0x13190`): type `3` даёт `float`, type `5` — +беззнаковый `DWORD`. Getter возвращает число через x87; evaluator сохраняет +его как `f32` перед операцией. Оператор `+` складывает правый +и левый операнды, а `-` вычисляет левый минус правый; после узла результат +сохраняется как `f32`. `FUN_10013650` передаёт итог типизированной ячейке через +`FUN_10012c00` (RVA `0x12c00`). + +Rust в первом `Init` принимает один числовой токен или имя ASCII-переменной, +а также ровно два таких атома с одним бинарным `+` либо `-`. Например, +`source + 2.0` и `target - source` читают значения из defaults или из предыдущих +успешных записей события. Имя ищется раньше числового преобразования, поэтому +объявление с именем `1e-2` перекрывает такую числовую запись; сравнение имён +чувствительно к регистру, а повтор объявлений выбирает первое. `DWORD` читается +как беззнаковое число и округляется до `f32` до операции. Публичный +`FormulaTable::evaluate_numeric_literal` по-прежнему принимает только литералы. +Ведущие unary `+`/`-`, `*`, другие операторы, третий атом, отсутствующие или +не-ASCII имена и зарезервированные одиночные операторы остаются `Unsupported`; +так же не поддержана ветвь копирования `source != 0xffffffff`. Проверенный native tokenizer отличается от обычного строгого decimal parser: `1e-2`, `1.`, `1e`, `1e+`, `1ee2` и `1e2.3` были приняты как один literal node @@ -477,8 +499,8 @@ digit-starting числовым токеном с необязательным Ñ исходные `f32` bits; `DWORD` (tag `5`) проходит через `__ftol` (RVA `0x1df70`) с усечением к нулю и записывает младший DWORD. Поэтому literal `0.8` остаётся `0x3f4ccccd` в `float`, но записывается как `0` в `DWORD`; `100.0` даёт raw -DWORD `100`. Неизвестные выражения не заменяются нулём, а запись всего первого -`Init` применяется только после успешного прохода. +DWORD `100`. Неизвестные выражения не заменяются нулём, а записи первого `Init` +публикуются только после успешного прохода. Native x86 oracle на GOG `ai.dll` SHA-256 `c19cd6cd31cd2896f8c4ba0f0e700dd3408aec8182daaf4863d98a7d233a2abc` загрузил @@ -500,6 +522,27 @@ Native x86 oracle на GOG `ai.dll` SHA-256 | 7 | `dMaxPlant` (`DWORD`) | `400 → 400` | | 8 | `d7` (`DWORD`) | `2 → 2` | +Отдельный одноразовый x86 oracle на той же GOG `ai.dll` проверил ещё 13 +выражений: 9 строк контролируемой `controlled.fml`, строки 19 и 22 настоящей +`c1m3e.fml` и два последовательных вызова строки 0 настоящей `c1m4e.fml`. +Native harness записывал `f32`-результат evaluator-а, типизированную raw-запись +и getter после записи. В scratch-проверке те же входы прошли через публичные +`parse_varset`, `FormulaTable::parse` и `VarSet::resolve_init`; результат +evaluator-а и raw-слово Rust совпали с native во всех 13 случаях. Среди +проверенных границ: `2 - 3` записал в `DWORD` `0xffffffff`; чтение исходного +`DWORD 0xffffffff` дало `f32 0x4f800000`, а последующее `+ 0` записало в +`DWORD` ноль. Для настоящих строк `c1m3e` получены `fTemp = 0x40333333` и +`dTemp3 = 0`; два последовательных вызова строки 0 `c1m4e` дали +`fTemp = 0x4033335d`, затем `0x40333387`. + +Oracle принудительно устанавливал x87 control word `0x007f` на время каждого +проверяемого вызова: все 87 наблюдений показали входное и восстановленное +значение `0x027f`, активное и выходное — `0x007f`; canary-проверки прошли. +Менялись только varset-копии в памяти процесса. Эта выборка подтверждает +компиляцию, вычисление и типизированные записи ровно для перечисленных входов. +Она не запускала исходный поиск и обход события `Init` и не исполняла поздние +события или миссию; выборка не доказывает полную грамматику evaluator-а. + Rust сохраняет всю таблицу строк, но вычисляет только формулу, на которую ссылается поддержанная запись. Лицензированный runtime-тест подтверждает полную загрузку кампании Mission.01 после этих присваиваний; у обычной @@ -511,6 +554,17 @@ Rust сохраняет всю таблицу строк, но вычисляеÑ не перечисляют AI candidates и не выполняют этот handler. Это не подтверждение запуска самой игры или полной VM. +В других строках тех же ресурсов встречаются `0.8 + fT` (`c1m3e.fml`, row 19), +`dBuildingProtectHits - dT2` (row 22) и `fTemp + 0.00001` (`c1m4e.fml`, row 0). +Декодированные `c1m3e.scr` и `c1m4e.scr` ссылаются на них из более поздних +событий `Fort_Captured`, `PBM_BUILDING_PROTECT_Start` и +`PBM_BUILDING_INF_CAPTURE_Start`, а не из их `Init`. Игнорируемый Rust-тест +подаёт эти сохранённые строки в изменённый синтетический `Init` и использует +настоящий общий `varset.var`; это проверка resolver-а, а не утверждение о +исполнении таких выражений в исходном `Init`. Отдельный native oracle и +сравнение с Rust выше проверяют эти строки при явно заданных исходных ячейках, +но не подтверждают их исполнение в оригинальном сценарии или в живой игре. + ### Первый вызов Behavior и сценарного `Init` В той же GOG-сборке `Behavior.dll` (SHA-256 @@ -760,8 +814,9 @@ tie-векторов подтверждает реакцию `FISTP`, но не два слова и третье исходное слово, выдаёт три типизированные записи и отклоняет отсутствующие, выходящие за диапазон или не-`DWORD` цели. Общий для первого события метод `VarSet::resolve_init` добавляет этот `Handler(19)` и формулу из -одного numeric literal; остальные формы инструкции и содержимое `.scr` -сохраняются reader-ом, но остаются вне этого исполнения. +одного числового или переменного атома либо двух таких атомов с одним бинарным +`+` или `-`. Остальные формы инструкции и содержимое `.scr` сохраняются +reader-ом, но остаются вне этого исполнения. ### Начальная группировка подготовленных Control-дескрипторов @@ -1355,18 +1410,36 @@ payload. Позже та же функция `World3D+0x7cb0` повторяет времени из `DATWorld+0x32a38` (`0x7e6e`); `World3D+0x7ea0` передаёт туда же время в `0x80a9`. Setter `World3D+0x5140` записывает входной DWORD в эту ячейку. Нулевой вызов связывает интерфейсы, а ненулевой `NumericTime` запускает отдельную -ветвь активации. Конструктор AniMesh задаёт `+0x158/+0x15c/+0x160` равными `1.0`. -Обработчик модели `0x75b0/0x6fc0` привязывает интерфейсы и матрицу, Effect RVA -`0x3d30` и Control RVA `0x1ce00` сохраняют host/service указатели, а Control -`0x1fe00` меняет только локальный collision-флаг. Эти ветви не вызывают AniMesh -scale setter `0x14770` и не пишут поля масштаба. При загрузке AniMesh получает -Control secondary owner с QI selector `0xa` и сохраняет возвращённый base owner +ветвь активации. Модель занимает отдельный объект в памяти: её конструктор +AniMesh RVA `0x68a0` задаёт `Model+0x158/+0x15c/+0x160 = 1.0`. +Поле `Agent+0x158` хранит указатель на Control. Проверенные ветви модели +`0x75b0/0x6fc0`, Effect RVA `0x3d30` и CollManager RVA `0x1ce00` сохраняют +матрицу и служебные указатели, оставляя масштаб модели неизменным. + +Фабрика CollObject возвращает вторичный интерфейс `object+4`. QI `0x28` +вычитает `4` и сохраняет в `Agent+0x170` основной объект с vtable `0x3c2fc`. +Его event slot `+0x08` ведёт в Control RVA `0x1f530`. При event `4/null` +обработчик кэширует семь host-интерфейсов; при наличии QI `0x203` вызывает +CollManager slot `+0x10` таблицы `0x3c178`. Этот метод, RVA `0x1c330`, +сохраняет только указатель `CollObject+0x20` в `CollManager+0x3c`. +Для свежего BULL Agent без внешнего host QI `6` при привязке даёт основной Agent +с vtable `0x201d4`. Ненулевой event `4` вызывает его slot `+0x24`, AniMesh +RVA `0x17d60`, который читает `[Agent+0x120]`; вызывающий код сохраняет +результат в `CollObject+0x24`. +Затем CollObject slot `+0x0c`, Control RVA `0x1fe00`, сбрасывает бит `0` +поля `CollObject+0x30`. Event `0x15` с первым DWORD payload `9` завершается +без вызовов и записей. Поле Control `owner+0x34` хранит указатель на CollObject; +его slot `+0x14`, RVA `0x1f670`, меняет флаги `CollObject+0x30`. +Перечисленные ветви не вызывают AniMesh setter `0x14770`. + +При загрузке AniMesh получает Control secondary owner с QI selector `0xa` +и сохраняет возвращённый base owner в `Agent+0x158`: `AniMesh.dll+0x2ff0` делает этот вызов в RVA `0x32ee..0x3300`, а `Control.dll+0x76f0` для selector `0xa` вычитает `0x14` из `ECX` и возвращает указатель. Диспетчер AniMesh RVA `0x1afb..0x1b0d` посылает через `Agent+0x158` событие `4` в slot `+0x08`. Его base-owner vtable — Control RVA `0x3d298`, и slot ведёт прямо в `0x314f0`; при нулевом payload исходный обработчик проходит -`0x7830 -> 0xf940`, поэтому FAC0 входит с constructor scales `[1, 1, 1]`. +`0x7830 -> 0xf940`, поэтому FAC0 получает Model scale `[1, 1, 1]`. Встречающиеся рядом переходники относятся к другим интерфейсам: Control owner `+4` с таблицей `0x3d254` проходит через `0x31ae0` (adjust `-4`), а owner `+8` с таблицей `0x3d1fc` — через `0x31b00` (adjust `-8`). Последний путь не является @@ -1422,6 +1495,77 @@ descriptor word `8`. Текущий tag `26` тем же общим reader-ом умножает на `M`. Это правило не меняет отдельные callbacks бюджета или запись factory-26 selector `0x500`. +Сырой selector `6` в `FUN_1002bb40` выбирает устройство по ordinal через +интерфейс `owner+0x0c`: таблица `this+0x5b0` соответствует `owner+0x5bc`. +RVA `0x2bb91` проверяет тег текущего descriptor на равенство `2`. При совпадении +RVA `0x2bb9a` загружает `device+0x174` как `f32`, сохраняет значение в выходной +буфер и возвращает `AL=1`/`RET 8`; другие текущие теги возвращают false. +Наличие поля `+0x174` подтверждено для экземпляра фабрики `2`. INTO сохраняет +исходный тип экземпляра, поэтому проверка тега descriptor отдельно не доказывает +наличие этого поля у другой фабрики. Rust selector `6` сначала проверяет +текущий descriptor tag: тег не `2` даёт `Ok(None)`; затем требуется известная +фабрика `2` и конечное нормальное число либо ноль любого знака в `+0x174`. В +`Control.dll` (SHA-256 +`bcb86318089028548a178b03a21accdce15df0cc85ddd91674cf3c2f55012c6e`) поле +получает ноль в конструкторе по RVA `0x294c0`; чтение, сравнение пары строк и +пересчёт находятся по RVA `0x2bb40`, `0x29940` и `0x296f0`. + +Typed record хранит две ASCII C-строки в words `[27..35)` и `[35..43)`. Строка +заканчивается на первом NUL: байты за ним игнорируются, а регистр при сравнении +не учитывается. Если новая, изменённая пара содержит пустую строку, native +recalc завершается до `CreateObject` и selector `35`, поэтому сохраняются +прежние биты. Пустая пара конструктора уже совпадает с текущей и вообще не +запускает recalc. Иначе append и первая активная запись `INTO` применяют +результат в исходном порядке. Для нераспознанной формы поле остаётся +недоступным; ошибка разбора распознанного, но повреждённого ресурса прерывает +новую загрузку до публикации. + +В общем случае native selector 35 не равен сумме сырых Max: +`Control.dll` getter `0x0dc40` (selector 3) читает Max по `descriptor+4`, где +указатель descriptor лежит в начале runtime provider row, затем умножает его +на масштабы владельца `+0x660` и `+0x548`. Функция `0x13620` складывает выходы +в порядке строк с округлением `f32`; при `row+0x24 != 0xffffffff` добавляется +индексированный вклад. Конструктор `0xb240` задаёт runtime `row+0x24` +служебное значение `0xffffffff`. Только в свежей форме поддержанного BULL оба +масштаба равны `1`, а все `row+0x24` равны +`0xffffffff`, поэтому формула сводится к NDP Max сумме. + +Пересчёт создаёт временный объект запросом `9` через `World3D.dll+0x7cb0`. +Загрузчик `Control.dll+0x32280` формирует для его корня входной контекст с +полем `+0xc8 = 0xffffffff`. Для разрешённых opcode `1` и `12`, а +также `4` только после FXID/CPT-проверки, подтверждённые callbacks сохраняют +входы суммы: `Control.dll` RVA `0x43d0` меняет только локальные слова; в +`AniMesh.dll` (`65659bf67f20e5be77d67a5b2f59968f61b94293143807fa59ca451c560c4f7e`) +RVA `0x5500/0x93c0` меняют узлы и список модели, а `0x5320` не пишет её +масштабы. Это статическая граница свежего пути, не снимок живого native World +после всех creation callbacks. + +Проверка opcode `4` читает только выбранные строки и принимает FXID-типы +`2/3/4/5/7/8/9` с распознанными ASCII-ссылками и CPT offsets внутри компонента; +типы `1/6/10`, отсутствующие ресурсы и непроверенные offsets оставляют поле +недоступным. В `Effect.dll` (SHA-256 +`386a3e8db1ce23f61957825d633c7dae1ed40c25cab0cf10a534385fd7cee789`) проверены +инициализаторы типов `2/3/4/5/7/8/9` по RVA `0x12d10/0x103b0/0x108c0/0x28a0/ +0x1720/0x11230`; тип `9` разделяет путь типа `3`. + +`AssetManager::initial_control_effect_opcodes` собирает исходный порядок кодов +выбранной начальной группы до фильтра opcode `4`. `Some([])` означает, что +по структуре данных команд нет: группа отсутствует, пуста или её беззнаковый +индекс не меньше числа групп. Условия отдельных строк этот читатель не +вычисляет. `None` означает неподдержанный либо +неполный формат, а повреждённая структура возвращает ошибку. Такая разница не +позволяет принять неизвестный разбор за пустую сумму. + +Ресурсные callbacks в этой цепочке привязаны также к следующим образам. +Записи палитры и звуковые либо графические буферы имеют отдельное хранение; +проверенные начальные вызовы не меняют входы суммы NDP. + +| Модуль | SHA-256 | Существенные RVA | +|---|---|---| +| `World3D.dll` | `17e4a3089b2583a8cf2356c9db0390b1aba138356a09130d79b4e7e4791da61e` | `0x7cb0` — создание; `0x4b10/0x4cb0` — текстуры и lightmap | +| `Ngi32.dll` | `bab9840d94f4e4e74ffc26677724fa896cf4823845504d09a9e025f80016edf5` | `0x11f80` — чтение ресурса; `0x11410` — счётчик палитры; `0xd620/0xe900` — звуковые буферы | +| `Terrain.dll` | `af87d1b2e728a0be73c52be3b44cc196ab46da7799f25a15d40f8c9b0b425ead` | `0x166a0/0x17ac0` — получение указателя на числовое поле World | + FCA0 передаёт в owner query порядковый номер `i`; query `0x2bb40` находит устройство `i` и вызывает его reader. Уже reader `FUN_10021d00` получает provider key из поля устройства `+0x04`. Provider getter `FUN_1000dc40` (RVA `0x0dc40`) diff --git a/docs/reference/atmosphere.md b/docs/reference/atmosphere.md index 65ed176..dba20c2 100644 --- a/docs/reference/atmosphere.md +++ b/docs/reference/atmosphere.md @@ -50,7 +50,25 @@ key_time = floor(duration * (key_hour * 60 + key_minute) * 60 / 86400) theta = (clamp((now - birth_start) / lifetime, 0, 1) * 1.2 - 0.1) * pi ``` -Затем к нему применяется матрица рождения. У солнца и луны независимые +Затем к нему применяется матрица рождения как к вектору-столбцу. Если `A` и +`B` — углы рождения, а `x = cos(theta)` и `z = -sin(theta)`, результат равен + +```text +[x*cos(A) + z*sin(A)*sin(B), + x*sin(A) - z*cos(A)*sin(B), + z*cos(B)] +``` + +Порядок важен для всего освещения: транспонированное умножение переносит +солнце на другую сторону мира и меняет освещённые склоны, блики и тени. +В GOG `Terrain.dll` (SHA-256 +`af87d1b2e728a0be73c52be3b44cc196ab46da7799f25a15d40f8c9b0b425ead`) +матрицу строит RVA `0x4A550`, а вектор преобразует `CSun::Update`, RVA +`0x4B570`. Живой замер после 282184 мс при длительности 525000 мс и углах +`[90, 30]` дал направление `[-0.4950135, -0.1408745, -0.8573891]`; +Rust-тест проверяет этот результат с допуском `3e-6`. + +У солнца и луны независимые интервалы и независимые направления: при отсутствии активной луны нельзя подменять её направление противоположностью солнца. `SunFrame::color` — это единственный primary directional RGB. Второй источник получает направление @@ -131,6 +149,31 @@ gradient и clouds row 2. Для nebula/stars/clouds используются UV directional RGB из `sky.packed[0]`. Это четыре фиксированных pass, а не три независимых цветных слоя. +Все три текстурных слоя движутся. Обновление GOG `CSky` +(`Terrain.dll`, RVA `0x48B90`, запись UV `0x4942A–0x495B7`; +[сборка и SHA-256](original-binaries.md)) берёт разность текущего `u32` clock +и сохранённого поля объекта `+8` с переполнением. Это отдельные миллисекунды +жизни неба, а не абсолютная фаза суток из SKE. + +| Слой | Период | Добавка к исходным UV | +|---|---:|---| +| Фон | 300 000 мс | одинаковый сдвиг U и V | +| Звёзды | 100 000 мс | одинаковый сдвиг U и V | +| Облака | 50 000 мс | сдвиг только U | + +Добавка равна остатку от деления elapsed clock на период, умноженному на +нативный коэффициент `f32` (`3.3333333e-6`, `1e-5`, `2e-5`). Вершинные UV +каждый раз вычисляются от позиции купола заново; накопление приращений +создавало бы дрейф. `SkyMesh::update_uvs` получает текущие миллисекунды кадра +и сохраняет геометрию купола. В работающем оригинале проверены UV всех трёх +слоёв, включая разность clock с переполнением. + +Захват сохраняет `sky_uv_clock` на входе в рендер неба, проверяя его по +фактическим UV вершины зенита и clock того же кадра. Replay удерживает эту +фазу вместе с фазой суток. Без такого поля текстуры движутся по elapsed clock +приложения; начальная фаза оригинала неизвестна и явно считается как +`captured_sky_phase_unverified`. + Цвет первого screen-gradient и последнего пояса вычисляет `SkyFrame::screen_gradient(yaw_radians, glare_rgb_delta)`. Native `CSky` сначала округляет `(yaw + pi) * 180 / pi` режимом nearest-even, выбирает сектор @@ -167,21 +210,27 @@ RGB palette 12 с минимумом `80/255` на канал и alpha `150/255` ## Туман и свет -Нативное обновление `CSky` записывает две дальности и один packed RGB. +Нативное обновление `CSky` записывает две дальности и нижнюю границу освещения. `FogFrame` повторяет этот контракт: ```text fog.start = sky.values[0] * 700 fog.end = sky.values[1] * 700 -fog.color = RGB(sky.packed[1]) / 255 +lighting_floor = RGB(sky.packed[1]) / 255 ``` Поля не скрывают порядок исходных параметров: значения сохраняются в -`SkySample`, а преобразование выполняется только в `fog_frame`. Vulkan path -передаёт цвет и дальности в frame uniforms; vertex shader вычисляет линейный -коэффициент по расстоянию до камеры, а fragment shader смешивает RGB материала -с fog color в ветвях combiner, которые используют туман. Конкретный draw state -может обходить это смешивание. +`SkySample`, а преобразование выполняется только в `fog_frame`. Нижняя граница +освещения ограничивает результат расчёта света снизу; она не задаёт цвет тумана. +Обычный цвет тумана выбирается из `sky.colors[0..4]` по направлению камеры, +как цвет горизонта. Это подтверждено в `CSky::Render` и живым совпадением +его значения с `D3DRENDERSTATE_FOGCOLOR` в том же игровом тике. + +Vulkan path передаёт эти параметры раздельно. Vertex shader вычисляет +коэффициент по квадратам расстояния и границ, а fragment shader смешивает +RGB материала с цветом тумана. Конкретный native draw state может обходить +смешивание или временно менять его цвет, например на чёрный для отдельного +прохода. Полный выбор таких проходов ещё не воспроизведён. Цвет primary света солнца — RGB palette 12, нормированный делением на 255 и умноженный на trailing parameter 2. `SunFrame::direction` — нормированный diff --git a/docs/reference/environment-effects.md b/docs/reference/environment-effects.md index 799a969..677c159 100644 --- a/docs/reference/environment-effects.md +++ b/docs/reference/environment-effects.md @@ -218,6 +218,41 @@ OneShot -> воспроизвести разовый звук молни отдельный spatial source, применяет DirectSound range/pan и текущий listener. Отсутствие устройства вывода не должно останавливать симуляцию. +### Звуки Control + +Команда Op2 может принадлежать не погоде, а экземпляру эффекта на узле модели. +`fparkan-fx::ambient_sound` хранит её временное окно и предыдущее значение +фазы. В режиме 0 разовый звук начинается при строгом пересечении начала окна: +`previous <= start && start < phase`, если фаза движется вперёд. Значение +`phase == 1` для этой проверки заменяется нулём и подготавливает следующий +цикл. Режим 2 включает loop на обеих границах окна; внутри окна уже работающий +источник обновляется, а не запускается заново. Выход за окно останавливает его. + +Позиция интерполируется между двумя локальными точками и проходит через ту же +матрицу владельца, что и графические команды. Минимальная и максимальная +дистанции остаются постоянными; частота интерполируется по фазе окна. +Каждая команда получает отдельный источник звука, даже если ресурс совпадает +с ресурсом соседней команды. Дождь сохраняет свой loop. Перемещение слушателя, +потеря фокуса и завершение приложения затрагивают все эти источники. + +Эти ветви восстановлены в GOG `Effect.dll` (сборка из +[таблицы бинарников](original-binaries.md)): init RVA `0x12D10`, update +`0x12EB0`. Живой loop `sounds.lib:f_defl.wav` передаёт в звуковой backend +диапазон `12/80`, частоту `1` и мировую позицию из матрицы владельца. +В режиме 2 признак loop берётся из первого DWORD body, а не из command flag. + +Обычное создание эффекта Control сохраняет time mode заголовка и задаёт +постоянную фазу нулём. Raw selector строки служит ID экземпляра, а не номером +режима времени. Поэтому `f_comp_sfx` с time mode 0 и началом окна `0.001` +остаётся тихим до управляющей команды Control. Preview такие команды ещё не +исполняет. Периодические звуки с time mode 2 уже следуют общим часам и текущей +позе узла. `render_coverage control_op2_supported` считает подготовленные +команды, а `control_sound_start/stop` показывает фактические переходы. + +Неизвестные backend modes 1/3, ненулевое поле body+76, неподдержанное время +или привязка владельца пропускаются с `render_coverage control_op2_unsupported`. +Они не заменяются разовым звуком или произвольным loop. + ## Цвет солнца Солнце берёт цвет спрайта и прозрачность бликов из `palette[10]` расписания @@ -236,11 +271,19 @@ packed: [key.colors[10], key.colors[14]], `AtmosphereFrame` пока не несёт этот бит обновления, поэтому sampler выдаёт интерполированный цвет при каждом запросе. -## Экранная геометрия солнца и луны +## Отрисовка солнца, луны и бликов + +`CSun::Render` получает текущий viewport, проецирует направление солнца или +луны и собирает quad уже в его пикселях. `Terrain.dll` VA `0x1004AA40` передаёт +эту геометрию через shade callback; native FVF `0x1c4` задаёт +`XYZRHW | DIFFUSE | SPECULAR | TEX1`. Конвертер `0x10020700` сохраняет входные +`x`, `y` и `z`, а в `RHW` записывает `1.0`; исходный `z` равен `0.999`. Callback +включает depth test и выключает запись depth, поэтому более близкая поверхность +может закрыть спрайт, а сам спрайт не меняет глубину для последующих слоёв. -`CSun::Render` сначала переводит направление небесного тела в пиксели текущего -viewport, затем собирает quad с native XYZRHW-позицией. При положительной высоте -viewport Vulkan пиксели переводятся в clip-space так: +В Vulkan ряды 3 и 4 сохраняют этот contract отдельным bit 8 material mode. +CPU каждый кадр преобразует viewport-пиксели к clip space текущего размера +swapchain: ```rust position = [ @@ -250,13 +293,17 @@ position = [ ]; ``` -Vertex shader пропускает `clip_from_world` для строк солнца и луны. CPU каждый -кадр пересчитывает центр и размер quad по текущему viewport; старые пиксельные -координаты не переносятся на новый размер. World-space billboards бликов -остаются отдельным путём. Материал экранного quad не освещается, vertex fog -отключён, а sky far-depth bit к нему не применяется. +Положительная высота Vulkan viewport отображает эти координаты в тот же +top-down экран. Vertex shader пропускает мировую матрицу только для этого +режима: + +```glsl +vec4 clip_position = screen_tl + ? vec4(in_position, 1.0) + : frame.clip_from_world * vec4(in_position, 1.0); +``` -Для мировой геометрии +Материал остаётся unlit, fog factor равен нулю. Для мировой геометрии оригинал задаёт viewport `MinZ = 0.1`, `MaxZ = 0.99`: после отсечения проекционная глубина отображается в этот диапазон: @@ -280,35 +327,39 @@ RVA `0x7170`, участок `0x71E7–0x723C`, копирует MinZ/MaxZ в re [таблице бинарников](original-binaries.md). Нативные значения этих двух полей имеют биты `0x3DCCCCCD` и `0x3F7D70A4`. - -## Геометрия визуальных FX-команд - -`fparkan-fx::fx_visual` формирует кадры для двух подтверждённых вариантов -`f_gener_ball`. Op4 принимает mode 0 с положительным знаменателем в `body+0xC4` -и выдаёт camera-facing quad; расстояние камеры считается через native -component-wise projection родительской матрицы. Op9 принимает mode 2: -селектор `body+0xC4` со значением 0, 1 или 2 отображается в native flag base -3, 5 или 6 и выбирает одну из трёх сеток; категория render flags выбирает её -детализацию. `body+0xC8` выбирает UV path: -нулевое значение проецирует позицию вершины на XY, ненулевое использует -фиксированные координаты quad и cap. Кадр сохраняет MAT0 resource, фазу, -opacity, вершины и triangle indices для renderer. - -Evaluator получает готовые command body, phase, camera point и внешнюю -матрицу. Выбор Control row, manager gates, owner attachment, header jitter и -global RNG остаются задачей владельца. `fparkan-game` пока использует для Op9 -категорию 0 с наибольшей детализацией; выбор категории по projected size ещё -не реализован. - -Для Op3 поддерживаются body-типы 0, 1 и 2. Поля отсчитываются от -`FxCommand.raw_body`, то есть после 4-байтового command word. Типы 0 и 1 -используют общие position/scale/phase/opacity tracks и camera-facing основу; -тип 1 и тип 2 также интерполируют ось, а тип 2 строит basis через native -D110-порядок. Все три evaluator-а принимают уже собранную матрицу владельца и -не выбирают Control row или attachment. Текущий путь для типов 0 и 1 -предполагает native render context 0: `Effect.dll` проверяет общий gate -`context + 0x60 < 8`, а API этих evaluator-ов не выбирает другой context. -Evaluator типа 2 принимает индекс context явно и учитывает этот gate в `active`. +Sky-background bit 4 продолжает помещать слои на дальнюю плоскость. Отдельные +flare rows 5 и 6 пока используют world-space billboard builder: для них не +подтверждён этот же native TL путь. + +## Эффекты Control + +Погодное расписание `sky.ske` — только один источник эффектов. У объектов +миссии таблица Control (`.ctl`) связывает строки с FXID-командами. Текущий +preview выбирает начальную группу Control и исполняет только строки без +predicate, chain и state gate. Условия игровых событий и переходы между +группами принадлежат полному игровому Control runtime и здесь не моделируются. + +Type 4 row задаёт локальную basis эффекта через три точки CTL point list. Для +валидной привязки первая точка также задаёт owner node. Путь MSH выбирает позу +этого узла из текущей pose hierarchy; для внешнего mounted-компонента к ней +добавляется поза socket. После этого применяются object transform, header +scale и локальная матрица FX. Подробнее о порядке матриц см. +[описание MSH](msh.md#control-fx-и-owner-transform). + +Renderer preview поддерживает FXID-команды Op3 modes 0–2, Op4, Op7, Op8 и Op9. +Op3 mode0 строит camera-facing поверхность из анимируемых локальных позиции и +масштаба; Op7/Op8 обновляют отдельные элементы particle pool; Op9 создаёт +индексированную полусферическую сетку. Op1 вычисляется отдельно как список +point lights: selector 1 и записи без `flags & 0x80000000` входят в общий +diffuse light set. Записи с этим битом сохраняют native flags для специальных +путей и projected shadows, но не добавляются в общий diffuse список. + +Body discriminator Op3 читается из первого DWORD `FxCommand.raw_body`; offsets +в evaluator-е относительны к данным после четырёхбайтового command word. Mode 1 +добавляет анимируемую ось к camera-facing basis, mode 2 использует native +D110 basis. Для modes 0 и 1 preview предполагает render context 0, который +проходит общий gate `Effect.dll` `0x106C0`; их API не выбирает другой context. +Evaluator mode 2 принимает индекс context и учитывает gate при расчёте `active`. У Op3 mode 1 ось задаёт длину прямоугольника, а не его нормаль. Для локального вектора к камере `v` и анимируемой оси `a` столбцы базиса имеют @@ -359,27 +410,75 @@ RVA `0x93D0`, ветка mode 1 с `0x9739`, сборка столбцов `0x97 семена отдельных экземпляров лишь детерминированно заменяют process-global состояние и не воспроизводят общий порядок вызовов. +`f_gener_ball` evaluator покрывает Op4 mode 0 с положительным знаменателем в +`body+0xC4` и Op9 mode 2. Для Op9 selectors `body+0xC4` со значениями 0, 1 и 2 +отображаются в native flag bases 3, 5 и 6. Нулевой `body+0xC8` проецирует UV на +XY, ненулевой выбирает статические quad/cap координаты. Op4 вычисляет локальное +расстояние камеры тем же component-wise projection, что использует native path. + +FX-команды используют общий preview clock и ограниченный набор time modes: +mode 0 оставляет фазу равной нулю, mode 1 вычисляет `elapsed / duration` без +зажатия, mode 2 повторяет цикл. Header flags `0x200` и `0x20` преобразуют эту +фазу; command path `0x8120` зажимает результат только после преобразований. +В [эталонной Effect.dll](original-binaries.md) RVA `0x5C60` режимы 4–17 +читают состояние владельца или провайдера; режим 14 тоже входит в эту группу. +Пропуски при расширении выбранных начальных Control-строк учитываются по +командам: `control_fx_requires_game_state` для этих режимов и +`control_fx_unsupported` для остальных непреобразованных команд. Диагностика +сохраняет причину, ресурс, объект, компонент, строку и номер команды. +`control_fx_invalid_documents` отдельно считает неразобранные FXID, где число +команд неизвестно. Счётчики суммируются по компонентам миссии; они не заменяют +учёт ещё не выбранных Control-групп и условных строк в фазе G. +Effect renderer проверяет header bit `0x800` только при +render selector 0; native AniMesh component path был захвачен с selector 1, +поэтому preview использует selector 1 и не подавляет такие visual commands. +Этот capture также показал `0xD00` FX entries в 53-entry manager. Динамическое +состояние manager `A8`, связанное с header gate и фактической FX phase, пока не +моделируется. Header bit `0x10` относится к activation lifecycle и сам по себе +не отключает draw. +Header jitter и random shift хранятся один раз на Control row и совместно +применяются к его sibling-командам; отдельный детерминированный поток каждой +строки сохраняет эту связь, но не повторяет порядок глобального native RNG. +Неразобранные группы, event predicates и неразрешённые owner attachments +пропускаются. + +Для visual FX material берётся из resource reference команды. MAT0 выбирает +фазу по normalized FX phase, независимо от wall-clock анимации world materials. +Command opacity передаётся vertex alpha, а FX MAT0 alpha не умножает её второй +раз; texture alpha продолжает участвовать в shader. Текущая preview-ветка +использует SourceAlpha и read-only depth state и допускает только MAT0-фазы, +проходящие локальный predicate additive-RGB approximation. Семантика native +`flag40` и directional-light ветки остаётся открытой. + +Generated Op9 mesh разделяется на отдельный draw range для каждого native +quad и cap. Каждый range получает ключ по камере до первой индексированной +вершины, в порядке убывания расстояния. Равные ключи используют midpoint +insertion из native queue, поэтому их порядок не является стабильным по +первоначальному времени поступления. Preview использует category 0 как +highest-detail mesh; projected-size LOD и полное визуальное совпадение размера +и формы пока не реализованы. + ## Частицы Op8 `fparkan-fx::smoke` разбирает body Op8, хранит кольцевой пул команды и выдаёт quad-ы с ориентацией к камере для активных слотов. Нативная нормализованная -фаза окна ограничивается диапазоном 0–1 и задаёт интервал проверки следующего события; -сравнение строгое (`schedule_base + interval < update_clock`). Все события -catch-up в одном обновлении проверяются по тому же нормализованному интервалу; -после события нативный код продвигает базу расписания интервалом, вычисленным -по сырой фазе эффекта, а возраст слота считает по нормализованному интервалу. -Когда фаза равна единственной границе окна, деление `0/0` даёт NaN, который -нативный unordered-путь сравнения ограничивает нулём. +фаза окна ограничивается диапазоном 0–1 и задаёт интервал проверки следующего +события; сравнение строгое (`schedule_base + interval < update_clock`). Все +события catch-up в одном обновлении проверяются по тому же нормализованному +интервалу; после события нативный код продвигает базу расписания интервалом, +вычисленным по сырой фазе эффекта, а возраст слота считает по нормализованному +интервалу. Когда фаза равна единственной границе окна, деление `0/0` даёт NaN, +который нативный unordered-путь сравнения ограничивает нулём. Нулевая база расписания служит признаком начального состояния: обновление сохраняет текущую опору эмиттера и базу времени, не создавая частицу. Если первое время равно нулю, следующее обновление повторно захватывает опору. При событии скорость опоры считается от сохранённой позиции до текущей за время от базы расписания; слот смещает сохранённую опору на `скорость * due_interval`. -Обновление без событий не переносит сохранённую опору к текущему владельцу. -Откат часов корректирует только базу расписания до текущего времени: слоты, -опора и три потока случайных -чисел не сбрасываются. +При нулевой скорости оригинал подставляет ось X владельца и для направления, +и для этого смещения. Обновление без событий не переносит сохранённую опору к +текущему владельцу. Откат часов корректирует только базу расписания до текущего +времени: слоты, опора и три потока случайных чисел не сбрасываются. Evaluator принимает отдельные начальные состояния для эмиттера, знаковых отклонений и материала. Переход каждого потока совпадает с общим native LFSR, diff --git a/docs/reference/msh.md b/docs/reference/msh.md index 0a8a2cb..873cae1 100644 --- a/docs/reference/msh.md +++ b/docs/reference/msh.md @@ -28,7 +28,10 @@ type 20 rare auxiliary table Reader ищет entries по type, но сохраняет исходный порядок для roundtrip. Поле `ModelAsset.type16_present` отмечает только наличие entry типа 16. Данные payload сохраняются, но этот путь рендера пока не преобразует их в цвета -вершин. +вершин; приложение считает затронутые диапазоны в `msh_type16_colors_unsupported`. +В статических сценах всех 29 миссий GOG ни одна из 1736 загрузок моделей +не содержит этого потока. Это ограничение проверенного набора ресурсов; +неиспользуемые модели и будущие игровые состояния сюда не входят. ## Node and slot selection @@ -95,6 +98,51 @@ translation суммируется, а rotations умножаются; послÐ запускает игровой Control/AI и не обещает полного совпадения с runtime или всеми x87 rounding modes. +## Control clock и pose компонентов + +Статический Vulkan preview начинает с позы, вычисленной из сохранённых `.ctl` +bindings. Пока не задан `--static-animation-frame`, при каждом новом сэмпле +сцены приложение сэмплирует поддержанные tag 21 записи для каждого экземпляра +компонента и заново строит его MSH node hierarchy; time и transition используют +общий runtime clock. Динамический sampler принимает tag 21 mode `0`, +controller flags `5` (raw value `0xFFFFFFFF` трактуется как `5`), +разрешённый gate provider и static row mapping: оба +variable index равны `0xFFFFFFFF`, previous-row flag не установлен, transition +rate конечен и положителен. + +Fallback к статической CTLD-позе всего экземпляра компонента срабатывает только +при следующих ошибках sampler: `UnsupportedTag21Mode`, +`UnsupportedVariableMapping`, `MissingGateProvider`, +`GateProviderKeyOutOfRange`, `MissingGateProviderValue` и +`GateProviderCountMismatch`. Он заменяет весь динамический результат экземпляра, +а не одну запись. Остальные sampling errors завершают обновление сцены; например, +`UnsupportedPreviousRowMapping` и `InvalidTransitionRate` не включают fallback. + +`--control-clock-ms <u32>` воспроизводит общий MSH/FX clock линейно: первый из +`--frames` успешно представленных кадров получает `0`, последний — заданное +время. Replay clock привязан к числу представленных кадров: повторная подготовка +того же кадра, включая swapchain recreation, использует прежний sample. +`--static-animation-frame <u16>` отключает динамическую MSH выборку и задаёт один +frame всем компонентам. Этот override не включает AI и не превращает preview +в полный игровой Control runtime. + +## Control FX и owner transform + +Для выбранной Control effect group runtime берёт строки без predicate, chain и +state gate. Type 4 row offsets выбирают три точки из CTL point list; позиции +точек задают локальное начало, а их направления — локальную basis. К basis +применяются FX header scale и локальные random shifts. Затем матрица проходит +через mission rotation/translation и object scale, optional mounted-component +pose, owner-node pose и локальную матрицу эффекта. Поэтому FX mounted-компонента +следует её текущему MSH owner pose, а не остаётся привязанным к mission root. + +Когда point record задаёт допустимый owner node, его pose разрешается из MSH +hierarchy. Для mounted external components node pose вычисляется относительно +component root и применяется вместе с уже рассчитанным mount pose. Internal +`INTO` child controller/equipment links пока не интерпретируются как MSH socket; +если owner pose нельзя разрешить, его FX пропускается. Нативное сочетание child +component scale, equipment attachment и CPT basis остаётся неустановленным. + ## Vertex streams Основные vertex streams имеют фиксированный source index. У type 3 `attr1` diff --git a/docs/reference/render-frame.md b/docs/reference/render-frame.md index 10de209..80796a3 100644 --- a/docs/reference/render-frame.md +++ b/docs/reference/render-frame.md @@ -24,22 +24,91 @@ затенения, вызывается завершение рендерера, снимается `in_render`, затем рассылается `end-of-render`. -При retry кадра после пересоздания swapchain сохраняется прежний pending -sample, включая уже вычисленные позы и состояния FX и материалов. Для нового -drawable обновляются aspect, проекция и геометрия, зависящая от viewport; pose -берётся из pending camera. Новые события управления камерой остаются в -input controller и применяются следующим sample. Экранные billboards погоды -повторно проецируются из закэшированных мировых головы и хвоста, без второго -шага симуляции окружения и повторной отправки звуковых событий. Пауза нулевого -drawable не добавляет время. - -Pending frame хранит пакет MAT0 для мира и, при наличии GPU-окружения, отдельный -пакет неба. Пакеты содержат выбранные селектор и категорию конвейера, фазовые -коэффициенты материала, четыре UV-преобразования и кортеж фаз дополнительных -текстур. Повторное применение пакета при retry или нескольких resize не выбирает -фазу заново и не продвигает RNG. Перед отрисовкой сначала применяются материалы -мира и их порядок прозрачности, затем MAT0 неба, после чего viewport-зависимый -проход неба записывает свои переопределения. +В интерактивном preview часы материалов и окружения прибавляют полное время +между обновлениями. Ограничение шага в 100 мс остаётся у движения свободной +камеры и не обрезает эти часы. В режиме replay окружение получает разницу +последних применённых значений общего control clock. + +Если swapchain просит повторить ещё не представленный кадр, preview сохраняет +его общий clock, уже вычисленные позы, FX/RNG и фазы материалов. При изменении +drawable обновляются камера, aspect и viewport, после чего sky, environment +geometry и projected shadows пересобираются для нового размера. +Положение и ориентация камеры берутся из сохранённого состояния +ожидающего кадра; новый ввод мыши применяется при следующем сэмпле. Сэмпл +погоды остаётся прежним: экранные billboards дождя и снега повторно +проецируются из закэшированных мировых головы и хвоста, без второго шага +симуляции окружения или повторной отправки звуковых событий. Следующий такт +выбирается после успешного представления кадра; пауза нулевого drawable не +добавляет время. + +## Прозрачность порталов MSH + +GOG `Terrain.dll` (SHA-256 +`af87d1b2e728a0be73c52be3b44cc196ab46da7799f25a15d40f8c9b0b425ead`), +RVA `0x1B0F0`, заменяет diffuse alpha пакета без type 16 с batch bit `0x0008`. +При `0x0010` аргумент кривой — `sqrt(distance)`, границы — `2.6 / fov` и +`7.7999997 / fov`, наклон — `fov / (7.7999997 - 2.6)`. Без этого бита +`0x0040` задаёт ноль; иначе границы — `75 * 1.3 / fov`, `95 * 1.3 / fov`, +наклон — `fov / (95 * 1.3 - 75 * 1.3)`. До ближней границы alpha равна нулю, +после дальней — единице. Внутри перехода alpha равна произведению смещения от +ближней границы на наклон. Это замена alpha MAT0, а не дополнительный множитель. + +Метод расстояния `0x246A0` через `0x247B0` читает первую индексированную +вершину пакета, преобразует её в мир и вычисляет квадрат расстояния до камеры. +Текущий Ngi32 dispatch `FastProc+0x2C` указывает на RVA `0x1D6D0`: +`RSQRTSS` возвращает приближение обратного корня, которое умножается на квадрат +расстояния. Получается приближённое евклидово расстояние; дополнительный `FSQRT` +ветви `0x0010` выполняется уже над ним. Хеш Ngi32 — +`bab9840d94f4e4e74ffc26677724fa896cf4823845504d09a9e025f80016edf5`. +Скаляр камеры в шестом float getter `0x4F5F0` и поле CShade `+0xBB4` +проверены в работающем оригинале: оба содержат текущий горизонтальный FOV. + +`legacy_msh_portal_alpha` хранит кривую в `fparkan-render`. Приложение пока +берёт первую индексированную вершину своего диапазона и обычный `sqrt`, +поэтому выбор вершины после native CPU clipping и точность `RSQRTSS` не +воспроизводятся. Счётчик `render_coverage portal_fade_packet_selection_approx` +показывает число таких диапазонов. Поток type 16 сохраняет прежний отдельный +путь и ещё не декодирован. Это ограничивает приёмку на частично отсечённых +порталах и не позволяет считать интерьер принятой эталонной сценой. + +## Дистанционная кривая тумана + +Программный `CShade::ShadeIndexedStrided` в GOG Terrain.dll сравнивает квадрат +дистанции с квадратами ближней и дальней границ. По RVA `0x2EEED–0x2EF45` +видимость равна `1 - (distance² - near²) / (far² - near²)` между границами, +единице перед ближней и нулю за дальней. Такой же закон использует отдельный +путь цвета пакета `0x2F230`. Линейная интерполяция по самой дистанции давала +слишком плотный туман в середине диапазона. + +В работающем оригинале границы 0 и `0x442389E6`, квадрат дистанции +`0x47707B04` дали видимость `0x3F5B2B94`. Rust-тест проверяет передаваемые GPU +квадраты границ по этому наблюдению. GLSL вычисляет квадрат расстояния вершины, +после чего растеризатор интерполирует коэффициент по треугольнику. +`native_fog.effective.sink_rgb` совпадает с `sky.packed[1]`, нижней границей +освещения CShade. Обычный GPU fog color подтверждён отдельно: `CSky::Render` +по RVA `0x48341` задаёт renderer default из цвета горизонта. В живом замере +`0xFF404064` дошёл до `IDirect3DDevice7::SetRenderState(34)` в том же игровом +тике. В другом ракурсе палитра `[0xFFA8A5CE, 0xFFABA9D5, 0xFFBAA9D5, +0xFFABA9D5]` дала default `0xFFA9A7D1`. Это не покрывает временные изменения +цвета отдельных проходов: например, native render path устанавливает чёрный +цвет и затем восстанавливает сохранённый. +Выбор проходов по native shade-state пока приближён общим туманом мира и +считается как `render_coverage fog_pass_eligibility_approx`. + +## Радиус Control Op1 + +GOG `Effect.dll` (SHA-256 +`386a3e8db1ce23f61957825d633c7dae1ed40c25cab0cf10a534385fd7cee789`), +RVA `0xF6E0`, преобразует направление текущей матрицей, сохраняет его длину +перед нормализацией и умножает на неё интерполированный локальный радиус. +Умножение видно по RVA `0xFB13`; перед LightManager `+0x10` неположительный +результат заменяется на `0.01`. В живом «Конструкторе» для синего источника +завода локальный радиус 7 и длина `0x409B1F91` дали радиус `0x4207BB9F`. +Это же значение проверяется Rust-тестом общего Op1 evaluator. + +Случайные добавки цвета и радиуса ещё не воспроизводятся; приложение сообщает +число таких Control-команд как `control_op1_jitter_unsupported` и использует +их авторские значения. У синего источника в проверенном захвате добавки нулевые. ## Элемент отрисовки @@ -69,21 +138,17 @@ Pending frame хранит пакет MAT0 для мира и, при налич глубину. За фиксированными диапазонами окружения сохраняются их явные состояния. Для MSH без потока цветов type 16 диапазон, цвет которого вычисляет CPU, при -raw-флаге `Batch20` `0x0008` получает переопределение alpha только в одном -подтверждённом случае. В GOG `Terrain.dll` с SHA-256 -`af87d1b2e728a0be73c52be3b44cc196ab46da7799f25a15d40f8c9b0b425ead` проверка -по RVA `0x1B0F0` задаёт alpha `0`, когда установлен bit `0x0040` и сброшен -`0x0010`; поэтому для `0x0148` alpha равна нулю. Для `0x0108` и `0x0118` -превью сохраняет alpha MAT0; для `0x0058` действует приоритет bit `0x0010`, -также оставляя alpha MAT0. При наличии type 16 или отсутствии `0x0008` -переопределения нет. Оно относится к одному draw range и не меняет общий +raw-флаге `Batch20` `0x0008` получает переопределение alpha по +[дистанционной кривой портала](#прозрачность-порталов-msh). Bit `0x0010` +имеет приоритет; иначе bit `0x0040` задаёт нулевую alpha, а оставшаяся +ветвь использует границы 75/95 с поправкой на FOV. При наличии type 16 или +отсутствии `0x0008` переопределения нет. Оно относится к одному draw range и не меняет общий материал; alpha `0` временно задаёт число индексов `0`, а при очистке переопределения исходное число индексов восстанавливается. -Общий fade по расстоянию пока не реализован. Открыты точная метрика расстояния, -выбор первого CPU-принятого треугольника диапазона и эквивалентность frustum- и -culling-отбору оригинала. Поэтому состояния `0x0108` и `0x0118` оставляют alpha -MAT0, пока эти входные данные не установлены. +Fade обновляется с камерой каждый кадр. Открыт выбор первой принятой вершины +после native CPU clipping: приложение пока берёт первую индексированную +вершину диапазона и считает это приближение в `render_coverage`. В превью с фиксированной камерой диапазоны туманности и звёзд, купола и облаков явно помечаются как прозрачный фон. Непрозрачные диапазоны мира остаются на @@ -117,6 +182,10 @@ RVA `0x13D90`. Эти данные подтверждают только порÑ путь превью принимает входные данные с матрицей `selector0_words`, полученные через [процедуру захвата нативного кадра](../../tools/native-frame-capture/README.md). +Формат захвата сохраняет локальную матрицу `selector0_words` и, когда она +доступна, записывает `effective_world_matrix_words` с маркером пространства +`world`. + Процедура захвата записывает наблюдавшийся путь миссии и значение `atmosphere_seconds`, если проверка потока, поколения и часов прошла успешно. В превью с фиксированной камерой эта абсолютная фаза расписания используется, @@ -156,6 +225,17 @@ RVA `0x4EBE0` использует соглашение о вызовах `__std `right - left` на `bottom - top`. Размер окна превью закреплён: если при запуске или после ненулевого изменения окна физический размер не совпадает с заданным, приложение завершает работу, а не растягивает изображение. + +Проекция во время рисования может отличаться от восстановленного состояния +камеры после обхода мира. В GOG у `CStridedPrimitive::RenderVB` (Terrain RVA +`0x1D110`) renderer содержит `near = 0.5`, а на границе World3D RVA `0x13CE4` +уже `near = 5`. Поэтому захват отдельно сохраняет `geometry_projection` при +первом вызове геометрии с той же мировой матрицей камеры. Поток, поколение, +renderer, viewport, far и FOV сверяются с последующей границей; только +подтверждённая запись используется при воспроизведении. Иначе пол в трёх +единицах от камеры может исчезнуть из-за чужой ближней плоскости. Старые JSON +без этой записи сохраняют прежнее поведение с явным счётчиком +`captured_projection_unverified`; их нужно переснять для проверки близких поверхностей. Сравнивайте результат с фрагментом исходного PNG по прямоугольнику `[left, top, right, bottom]`. В этом режиме `FreeFlightCamera` не создаётся, но при повторной отрисовке обновляются окружение и проецируемые тени для @@ -163,14 +243,26 @@ RVA `0x4EBE0` использует соглашение о вызовах `__std события, слушателю задаются захваченные позиция, направление вперёд и верхняя ось; начальная сортировка диапазонов мира использует ту же позицию. +Если в JSON присутствует пара `effective_world_matrix_space: "world"` и +`effective_world_matrix_words`, превью использует мировую матрицу. Матрица +`selector0_words` остаётся локальной и используется как запасной вариант только +при отсутствии мировых полей, если `parented_camera` не равно `true`. Для +родительской камеры без эффективной мировой матрицы вход отклоняется. + Захват может содержать наблюдавшийся путь миссии и проверенное значение `atmosphere_seconds`; последнее используется только при отсутствии явного параметра командной строки. Это значение фиксирует фазу расписания атмосферы -при сравнении с неподвижной камерой, тогда как FX продолжает получать реальное +при сравнении с неподвижной камерой, тогда как FX по умолчанию получает реальное время кадра. `simulation_time_seconds`, погода, RNG и полное состояние миссии остаются неизвестными, поэтому совпадение камеры и фазы не подтверждает визуального совпадения с нативным кадром. +Параметр `--control-clock-ms` воспроизводит единые часы MSH/FX от нуля до +выбранного значения в миллисекундах за указанное число кадров. Без этого +параметра часы идут в реальном времени. Для `--readback-sequence-out` этот +управляемый режим обязателен. Он не восстанавливает +`simulation_time_seconds`, погоду, RNG или полное состояние миссии. + Для неба превью вычисляет направление как `atan2(forward.x, forward.y)`. Оно совпадает со вторым выходным значением, вычисляемым в `Terrain.dll` по RVA `0x4F9C0`; при выборе сектора экранного градиента `CSky` добавляет π по RVA @@ -183,15 +275,9 @@ RVA `0x4EBE0` использует соглашение о вызовах `__std `1.3` рад — параметры, наблюдавшиеся в проверенном контексте GOG Ngi32 ([формула native projection](../tomes/05-render.md#камера-и-размещение-в-оригинале)). Для отношения ширины к высоте `a` превью вычисляет вертикальный угол -```rust -fn vertical_fov(self, aspect: f32) -> f32 { - 2.0 * ((self.horizontal_fov * 0.5).tan() / aspect).atan() -} -``` - -Код заменяет невалидный `aspect` на отношение 16:9; горизонтальный угол -остаётся `1.3` рад после изменения размера окна, а новый viewport и матрица -камеры применяются при следующей отрисовке. +`2 * atan(tan(horizontal_fov / 2) / a)`. Поэтому горизонтальный угол остаётся +`1.3` рад после изменения размера окна; новый viewport и матрица камеры +применяются при следующей отрисовке. Проекция направления не сравнивает косинус угла с ближней дистанцией: она отбрасывает только лучи за камерой и почти параллельные плоскости изображения @@ -254,6 +340,17 @@ D3D7 в `fparkan-render`, а затем один раз меняет знак в - переход на запасной набор возможностей устройства; - квантование времени симуляции. -Для отладки требуется детерминированный захват кадра: состояние камеры, -идентификаторы видимых объектов, список элементов отрисовки, ключи конвейера, -матрицы и хэши промежуточных буферов. +Параметр `--readback-sequence-out` сохраняет необработанные байты каждого +отправленного кадра; в имя файла записываются индекс кадра, значение общих +управляемых часов и размер изображения. Байты соответствуют выбранному формату +изображения swapchain Vulkan и не гарантируют порядок каналов R, G, B, A. Числовое +формат выводится как `readback_format`; расширение `.rgba` само по себе порядок +каналов не определяет. Последовательность не содержит структурированных ID +видимых объектов, списка элементов отрисовки, ключей конвейера, матриц или +хэшей промежуточных буферов. + +При преобразовании кадра в PNG сохраняется исходный RGB, а прозрачность +выключается: окно показывает готовое непрозрачное изображение. Альфа-канал +GPU может содержать результат смешивания поверхностей; повторное применение +его к готовому цвету затемнило бы кадр ещё раз. Поэтому конвертер и сборщик +сравнительных пар игнорируют эту альфу, а raw-файл сохраняют без изменений. diff --git a/docs/tomes/05-render.md b/docs/tomes/05-render.md index ef7dc52..4aba919 100644 --- a/docs/tomes/05-render.md +++ b/docs/tomes/05-render.md @@ -362,30 +362,14 @@ local.set_translation(pose.position); world[n] = world[parent(n)] * local; ``` -Статический Vulkan preview берёт CTLD `.ctl` из явной ссылки прототипа. -Поддерживаемые Control timeline с tag 21 сэмплируются при запуске на native -clock `0`; разрешённые NDP providers передают конечные type-1 значения. -Неподдерживаемый режим или variable mapping, а также отсутствующее или -неразрешённое gate value оставляют компонент на статических CTLD bindings. В -timeline участвуют только включённые строки; при повторном node ID поздняя -строка заменяет раннюю. - -Без `--static-animation-frame` preview сохраняет экземпляр Control и при -каждом redraw сэмплирует поддерживаемый timeline на текущем native clock. -Обновлённая Node38-поза перепроецирует видимую геометрию в заранее выделенный -vertex span, оставляя индексы и draw ranges неизменными. Если в пути Unit DAT -компонента есть такой Control, заново вычисляется его EXTO mount socket и -перепроецируется дочерняя геометрия; дочерний mount использует preorder-порядок -Unit DAT. Внутренние INTO-компоненты здесь не считаются глобальными MSH -socket mounts. Обновлённые вершины и shadow-треугольники попадают в общий -mesh upload перед следующим кадром. Culling sphere восстанавливается от -начальной native sphere и расширяется до текущих shadow-треугольников; это -`shortcut:`, потому что native обновление bounding sphere для анимированной -геометрии пока не установлено. Это путь сэмплирования Control-позы, не AI или -анимация эффектов. - -Явный `--static-animation-frame N` переопределяет timeline и задаёт `N` всем -узлам, пропуская runtime Control-сэмпл. +Статический Vulkan preview сначала строит начальную позу по CTLD `.ctl`. +В ней участвуют только включённые строки; при повторе node ID поздняя строка +заменяет раннюю, а узлы без настройки остаются на кадре `0`. Без +`--static-animation-frame` preview сэмплирует поддержанные Control timelines +при каждом новом сэмпле сцены. Флаг `--static-animation-frame N` задаёт `N` +всем узлам и пропускает runtime Control sample. Это визуальный preview позы, +а не полный Control/AI runtime. + MSH sampler выбирает type 19 frame округлением `(time - 0.5)` к ближайшему целому с ties-to-even, затем интерполирует выбранный type 8 key и следующий key в исходном `time`; это portable-вариант native x87 conversion в default @@ -515,6 +499,31 @@ Material manager кэширует разобранный MAT0 и texture handles лучше вычислять на экземпляр материала, если random offset или локальное время различаются между объектами; immutable phase data остаются общими. +### Почему проём становится прозрачным вблизи + +Портал здания — поверхность, которая закрывает интерьер издалека и постепенно +исчезает при приближении камеры. Если оставить ей постоянную непрозрачность, +вместо окна или перехода получится тёмный прямоугольник. Прозрачность материала +здесь заменяется значением, вычисленным для всего пакета треугольников; alpha +текстуры продолжает участвовать в смешивании. + +Для MSH без потока цветов type 16 правило включает флаг batch `0x0008`. +В обычной ветви при горизонтальном FOV `1.3` поверхность прозрачна до расстояния +75 и непрозрачна после 95; между ними alpha растёт линейно. При другом FOV обе +границы умножаются на `1.3 / FOV`. Расстояние берётся до первой вершины первого +принятого треугольника, а не до центра здания. + +Флаг `0x0010` выбирает другую кривую: сравнивается квадратный корень уже +вычисленного расстояния, границы равны `2.6 / FOV` и `7.7999997 / FOV`. +Например, при FOV `1.04` поверхность на расстоянии 25 имеет alpha примерно +`0.5`, а на расстоянии 5 полностью исчезает. Флаг `0x0040` без `0x0010` +сразу задаёт нулевую alpha; при наличии обоих побеждает `0x0010`. + +FParkan пересчитывает эту alpha при каждом обновлении камеры, после анимации +модели. Нулевое значение временно выключает диапазон индексов; при удалении +камеры диапазон восстанавливается. Подробности оригинального вычисления и +пока приближённого выбора вершины приведены в [описании кадра](../reference/render-frame.md#прозрачность-порталов-msh). + ## Texm: текстуры, mip-уровни и атласы `Texm` -- основной формат изображений. Он хранится в `Textures.lib`, @@ -646,15 +655,15 @@ diffuse texture ломает LOD, atlas coordinates и динамическую Vulkan preview строит проецируемые тени для объектов, отбрасывающих тени, отбрасывает сферы вне шести clip-плоскостей с радиусом `sphere + 100` и сохраняет порядок видимых кандидатов; frame builder обрабатывает не более 20 -объектов. Текущая реализация выделяет страницы в atlas `256 x 256` с 28 page -slots и использует одно сглаживание. Preview применяет native defaults -`RobShadowsOn = true` и `BldShadowsOn = false`: объект kind `4` проверяет -первый флаг, остальные kinds — второй. Поэтому роботы BTLU остаются объектами, -отбрасывающими тени, а объекты FORT и STAT их не создают. Preview пока не -загружает `shade.cfg`, поэтому значения из пользовательского файла не меняют -список объектов, отбрасывающих тени. Проверка действует только на проецируемые -тени: отдельный список объектов для затенения солнца по-прежнему включает -FORT, BTLU и STAT. Вызов preview передаёт `fade = 1.0` и LOD defaults: +объектов, отбрасывающих тени. Текущая реализация выделяет страницы в atlas +`256 x 256` с 28 page slots и использует одно сглаживание. Preview применяет +native defaults `RobShadowsOn = true` и `BldShadowsOn = false`: объект kind `4` +проверяет первый флаг, остальные kinds — второй. Поэтому роботы BTLU остаются +объектами, отбрасывающими тени, а объекты FORT и STAT их не создают. Preview +пока не загружает `shade.cfg`, поэтому значения из пользовательского файла не +меняют список объектов, отбрасывающих тени. Проверка действует только на +проецируемые тени: отдельный список объектов для затенения солнца по-прежнему +включает FORT, BTLU и STAT. Вызов preview передаёт `fade = 1.0` и LOD defaults: minimum LOD `1` и detail multiplier `1.0` отдельно для зданий и роботов. Проекция учитывает вклад directional и point lights. @@ -823,6 +832,59 @@ emit не должен повторно менять игровое состоя типизировать header и resource references, а body редких commands сохранять raw до подтверждения field-level semantics. +В `fparkan-game` выбранные Control FX проходят отдельный transparent path. +Начальная CTL group запускает только строки без predicates, chain или state +gate. Type 4 point list задаёт локальную матрицу; текущие MSH owner и mounted +node poses входят в transform до локальной FX basis. Реализованные visuals +охватывают Op3 modes 0–2, Op4, Op7/8 particle pools и Op9 generated mesh. Op1 +selector 1 добавляет point lights без native flag `0x80000000` в общий diffuse +список; остальные active records сохраняют flags для специальных путей и +projected shadows. Радиус источника масштабируется длиной его преобразованного +направления до нормализации. Например, локальный радиус 7 при масштабе узла +около 4,85 освещает область радиусом около 34. Потеря этого множителя оставляла +синий свет завода почти незаметным. Неположительный итог заменяется на 0,01. +Случайные добавки к цвету и радиусу пока явно считаются в +`render_coverage control_op1_jitter_unsupported`. + +Эффект может одновременно рисовать поверхность и издавать звук. Для Op2 +приложение создаёт отдельный звуковой источник у каждого экземпляра команды: +несколько одинаковых механизмов слышны одновременно и не заменяют дождь. +Позиция следует анимируемому узлу, а фаза включает, обновляет и останавливает +loop. Обычная строка Control создаёт экземпляр с нулевой постоянной фазой; +звуки с таким режимом ожидают отдельной управляющей команды. Периодические +звуки работают по часам уже в preview. Устройство вывода остаётся в приложении, +а правила переходов и параметров — в `fparkan-fx::ambient_sound` +([подробнее](../reference/environment-effects.md#звуки-control)). + +Туман также зависит от расстояния нелинейно: между ближней и дальней границами +движок интерполирует по квадрату дистанции. При границах 0 и 700 точка в 350 +получает четверть тумана, а не половину. Это сохраняет контраст среднего плана. +Effect renderer подавляет header bit `0x800` только при +render selector 0. Native AniMesh component path захвачен с selector 1, а в +его 53-entry manager присутствуют FX entries с header `0xD00`; preview +использует selector 1 и пропускает такие visual commands. Изменения manager +`A8`, которые задают runtime phase/header gate, пока не моделируются. Header +bit `0x10` относится к activation lifecycle и не выключает draw. + +Для visual effect renderer использует MAT0, указанный самой командой. Его +phase выбирается по normalized FX phase, а не по wall-clock clock материала +мира. FX opacity остаётся в vertex alpha, MAT0 alpha для этой FX binding +фиксируется на единице, texture alpha остаётся в shader. Preview передаёт FX +через SourceAlpha/read-only-depth state. MAT0 phases проходят локальный +eligibility predicate additive-RGB approximation; native `flag40` и +directional-light branch остаются неразрешёнными. + +Op9 генерирует индексированную полусферу с отдельными vertex и UV данными и +отправляет каждый quad и cap как отдельный draw range. Native packet distance +callback берёт первую индексированную вершину (`Terrain.dll:0x246A0` через +`0x247B0`), преобразует её согласно режиму packet и использует евклидово +расстояние до камеры. Queue размещает большие расстояния раньше меньших; +равный ключ вставляется в найденный binary-search midpoint, поэтому порядок +равных packets не стабилен по времени поступления. Preview повторяет этот ключ +и insertion order. Renderer category 0 остаётся highest-detail mesh; native +category вычисляется из projected sphere size и quality table, поэтому +фиксированная категория не является native LOD parity. + ## Полный кадр Крупный вход в world render проходит через `World3D::stdRenderGame`. Доказан @@ -947,6 +1009,12 @@ sun-ray queries поэтому используют исходные коордÐ она не равна одному лишь обратному преобразованию камеры. В проверенном GOG контексте Ngi32 использовались `n=0.5`, `z=700`, `f=1.3`. +Ближняя плоскость относится к конкретному проходу. После рисования движок +может вернуть прежние настройки камеры: захват только в конце мира видел +`n=5` и при воспроизведении вырезал пол перед игроком. Формат захвата поэтому +хранит проверенную проекцию вызова геометрии отдельно от состояния камеры; +подробности приведены в [описании кадра](../reference/render-frame.md#предпросмотр-по-захвату-камеры-d3d7). + У GOG `Terrain.dll + 0x7355c` хранит указатель на активную внешнюю камеру. Её selector 0 возвращает 16 слов начиная с `+0x20`, перенос находится в словах 3, 7, 11. Интерфейс `LoadCamera` расположен отдельно, по `+0x134` объекта. @@ -962,11 +1030,39 @@ Node38, а диапазоны граней — в 68-байтовых Slot68. В проверяются на единственное назначение, а выходящий за массив, повторный или неподдерживаемый диапазон считается ошибкой загрузки. -Первый рендер карты, terrain-запросы высоты и пересечения, receiver-ы теней и -обрезка контуром зданий используют только LOD0. Отбор сохраняет порядок -исходных граней, а контурный проход отдельно переводит индекс спроецированной -грани обратно в исходный. LOD1 пока не подставляется по расстоянию: такой -переход не реализован. +Рисуемый уровень выбирается по ячейкам, а не по расстоянию до отдельного +треугольника. Начало сетки находится в первых двух float заголовка Slot68, +размер ячейки — в границах первого слота LOD0. Атрибут 2 потока Node38 задаёт +число ячеек по X; индекс ячейки равен `x * count_y + y`. Приложение проверяет, +что границы всех слотов образуют эту регулярную сетку. Все 33 карты GOG +проходят такую проверку, включая вырожденные грани на общей границе ячеек: +принадлежность грани берётся из слота, а не угадывается по её центру. + +Координаты камеры переводятся в целые координаты сетки округлением вниз. +Если `dx * dx + dy * dy >= 9` и горизонтальный FOV не меньше 0,5 радиана, +выбирается LOD1. При увеличении масштаба, отсутствии дальнего слота или +наличии геометрического patch остаётся LOD0. Камера за границей карты не +прижимается к крайней ячейке для этого расчёта. Правило восстановлено по +GOG `Terrain.dll` ([сборка и SHA-256](../reference/original-binaries.md)): +`CLandscape::Render`, RVA `0x11340`, выбор уровня `0x11878–0x11A03`. +Исполнение оригинала подтверждает выбор дальнего слота, а getter камеры +`Ngi32.dll` RVA `0x4D960` возвращает именно FOV. + +Основания строений сохраняют подробную геометрию. Нативная регистрация +patch (`Terrain.dll` RVA `0x12E20`) проверяет его треугольники против каждой +ячейки через `mrnParaIntersectsPoly`. Поэтому учитываются и соседние ячейки, +которых касается край треугольника. Перед проверкой координаты нормализуются +в `f32` относительно центра ячейки; замена этой операции сравнением мировых +границ меняет результат на общих рёбрах. На «Конструкторе» игнорируемый +ресурсный тест сверяет все 22 ячейки с принудительным LOD0 с набором, +полученным в живом оригинале. + +Vulkan получает оба уровня один раз. Диапазоны индексов разделены по +материалу и исходной ячейке; на каждом кадре включается только выбранный +уровень. Сгенерированные отмостки остаются включены. Terrain-запросы высоты +и пересечения, receiver-ы теней и обрезка контуром зданий используют +канонический LOD0. Отбор сохраняет порядок исходных граней, а контурный проход +отдельно переводит индекс спроецированной грани обратно в исходный. ```rust let assignments = mesh.face_lod_assignments()?; @@ -984,9 +1080,10 @@ for (face, assignment) in mesh.faces.iter().zip(assignments.iter()) { исходные фрагменты рельефа вместе с треугольниками отмостки в одном наборе. `TerrainWorld::with_basement_foundation` принимает этот набор во владение и устанавливает те же координаты и маски в BVH высотных запросов и пересечений. -Предпросмотр миссии на Vulkan пока строит только LOD0: связывание BAS-записей -с этой подготовкой ещё не подключено, поэтому наличие API не означает, что -геометрия миссии уже обрезана. +Предпросмотр миссии связывает BAS-записи с этой подготовкой до загрузки +материалов. Полученная поверхность общая для изображения, теней и запросов; +поправка размещения основания один раз применяется к корню строения и его +присоединённым компонентам. Внешнее кольцо должно лежать на области LOD0, а каждый его сегмент — оставаться покрытым текущей поверхностью. Непокрытый сегмент или пересекающиеся контуры @@ -997,13 +1094,33 @@ for (face, assignment) in mesh.faces.iter().zip(assignments.iter()) { ## Материалы ландшафта -Рядом с `Land.msh` лежат `Land1.wea` и `Land2.wea`. Старший байт -`TerrainFace28.material_tag` выбирает материал первой таблицы, младший — -второй. `0xff` в старшем байте означает отсутствие первого слоя. +Рядом с `Land.msh` лежат `Land1.wea` и `Land2.wea`. Младший байт +`TerrainFace28.material_tag` выбирает базовый материал `Land1.wea`, старший — +дополнительный материал той же таблицы. `0xff` в старшем байте означает +отсутствие дополнительного слоя. `Land2.wea` содержит соответствующие этим +двум строкам detail-материалы. В AutoDemo встречаются теги `0102`, `0203`, `ff01`, `ff02`, `ff03`. Менеджер World3D получает селектор `(table_index << 16) | material_index` и возвращает выбранную фазу MAT0 размером 76 байт. +Таблица, строка и блок анимации — разные части запроса. В быстром проходе +с двумя текстурами `CShade` запрашивает для Land2 блок анимации **1**. Он может +выбирать другую текстуру, а не только другие коэффициенты освещения: +например, в «Конструкторе» это `L09M.0` вместо `L09.0`. Подмена блока первым +приводит к умножению базового изображения на неподходящую detail-текстуру и +меняет яркость и контраст земли. `TerrainMaterialSelection` сохраняет номер +блока отдельно от packed material key; приложение использует его при загрузке +дескриптора и в последующих кадрах. Если второго блока нет, общий MAT0 sampler +возвращается к первому, как оригинальный `GetMaterialPhase`. + +В GOG `Terrain.dll` (сборка из [таблицы бинарников](../reference/original-binaries.md)) +быстрая ветвь находится в RVA `0x1A7AC–0x1A7FB`. Живое исполнение передаёт +handle `0x00010002`, block `1` и устанавливает режим текстур `9`; первая +текстура умножается на diffuse, вторая — на результат с множителем два. +Ближний дополнительный проход с порогом `260 / FOV` относится к fallback без +этого режима. На проверенном устройстве он не выбирается, поэтому переносить +его дистанционный порог в быстрый проход нельзя. + Type 18 в `Land.msh` оригинал называет microtexture mapping chunk; он содержит четырёхбайтовые записи. Type 11 хранит пары `u16`, выбранные таблицей terrain slots. Их первое поле — ключ shade cache, а не номер строки @@ -1021,12 +1138,103 @@ lightmap state передаются в `VulkanStaticMaterial`, а shader посÐ сэмплирует эти четыре стадии. Animation clock обновляет выбранные фазы и коэффициенты без пересборки terrain mesh, сохраняя native type-14 overlay mask. +### Отражение на воде + +Поверхность воды нельзя рисовать тем же умножением текстур, что и землю. +В проходе ландшафта GOG Terrain.dll флаг грани `0x20000` участвует в выборе +отражения; при включённом EMBM живой водный кадр использует texture mode 10. +Оригинал создаёт для этого отдельную камеру `REFLECTION_SHIFTED`. +В [эталонной сборке](../reference/original-binaries.md) выбор грани находится +в Terrain.dll RVA `0x28A50`, а подготовка EMBM-пакета — в RVA `0x1B540`. + +Для горизонтальной воды на высоте `h` камера отражения находится в точке +`[eye_x, eye_y, 2h − eye_z]`. Но она не повторяет наклон основной камеры: +смотрит вертикально вверх, а её окно лежит на прямоугольнике водоёма. Поэтому +текстура отражения привязана к местности, а не к экрану игрока. В плоскости +воды координаты изображения равны `U = 1 − Ynorm`, `V = 1 − Xnorm`. + +Terrain.dll RVA `0x4EFF0` передаёт shifted-проекции пять значений: +`[min_y − eye_y, min_x − eye_x, span_y, span_x, eye_z − h]`. Например, +для наблюдателя `[1200, 800, 185]`, воды `150` и границ +`[1000, 500, 1500, 1200]` это `[-300, -200, 700, 500, 35]`. +Матрица камеры и эти аргументы проверены исполнением оригинальной функции +при двух положениях и разных поворотах основной камеры. Оракул подменяет +ввод-вывод очереди, ландшафта и камеры; сам renderer и матрица глубины в эту +проверку не входят. Vulkan-проекция сохраняет near/far основного вида; +Rust-тест отдельно проверяет углы водоёма и границы глубины. + +Отражённый вид меняет сторону отсечения и отбрасывает геометрию ниже воды. +Небо и оставшаяся сцена попадают в отдельную цветовую текстуру с собственной +глубиной. В основном проходе водные треугольники читают её по координатам +водоёма с добавленным смещением волн. Для неперспективной камеры API остаётся +обычное зеркало с экранными координатами. + +Размер изображения отражения выбирается отдельно от размера основного кадра. +Terrain.dll в конструкторе CShade (RVA `0x278E0`, цикл с `0x27FC7`) берёт +ширину и высоту renderer через Ngi32.dll RVA `0x7FB0`. Сторона квадратной +текстуры — наибольшая степень двойки, не превосходящая меньшую из этих +величин и 1024; нижний предел равен 2. Например, экран 1280×1024 даёт +отражение 1024×1024, а 1280×720 — 512×512. Getter размеров и исходный цикл +выбора проверены исполнением DLL на десяти входах; создание нативной текстуры +в этом оракуле пропущено. Vulkan создаёт цвет и глубину с этой стороной и +задаёт каждому проходу собственные viewport и область очистки. Так уменьшение +отражения не обрезает основной кадр и воспроизводит исходный масштаб его +пикселей, от которого зависит чёткость берегов и ряби. + +Цветовой target переходит из attachment layout в shader-read layout после +записи. Следующий зеркальный проход ждёт предыдущего чтения этой текстуры; +основной и отражённый виды используют разные слоты uniform buffer. Во время +записи отражения привязан отдельный нейтральный descriptor, чтобы проход +не читал собственное вложение. + +Волны не требуют готовой текстуры из ресурсов: Terrain.dll RVA `0x2BF50` +создаёт карту 128×128 со знаковыми парами Du/Dv. Для нормированных координат +текселя `x` и `y` угол равен `(x + y − 1) × 12,566360473632812`, а компоненты — +округлённые `64 cos(угол)` и `64 sin(угол)`. Все 32 768 байт Rust-генератора +сверены с исполнением этой функции DLL при игровом x87 CW `0x007F`. + +Рябь привязана к водоёму, поэтому не плывёт вслед за экраном. Конструктор +ландшафта собирает прямоугольник всех отражающих граней; U меняется против +мировой Y, V — против X. Эти координаты повторяются сто раз, к обеим прибавляется +`(queue_clock % 10000) / 10000`. Это самостоятельный clock: время суток и +относительный clock неба не задают фазу волн. Захват хранит исходный +`sky_uv_clock.raw_clock`; ограниченный replay фиксирует его, интерактивный +продолжает с этого значения. Старые захваты без raw clock явно считаются в +`captured_water_phase_unverified`. + +Карта Du/Dv смещает координаты следующей текстуры с диагональными коэффициентами +`0,01`, без изменения яркости. Это правило +[Direct3D BUMPENVMAP](https://learn.microsoft.com/en-us/windows/win32/direct3d9/bump-mapping-formulas). +В Vulkan карта хранится в двух каналах несжатой текстуры и возвращается к +знаковым значениям перед вычислением смещения. + +Матрица положения камеры, её окно, размер текстуры и координаты волн восстановлены, но весь +контекст отражённого вида ещё не повторён. Диапазон глубины, небо, туман и FX +заимствуются из основного вида. Для захвата с обрезанным viewport ещё не +подтверждено соответствие размера окна FParkan полному renderer оригинала. +Эти ограничения учитываются в `planar_reflection_render_context_approx`; +совпадение генератора волн и окна не доказывает совпадения итогового отражения. +Один target обслуживает ближайшую +активную высоту воды; другие высоты и негоризонтальные поверхности явно +считаются как неподдержанные. Это промежуточный водный проход, а не полная +приёмка воды или лавы. + ## Реализация Vulkan `LegacyPipelineState` хранит blend, depth, cull и alpha-test. Из этих значений получается ключ Vulkan pipeline. Alpha reference передаётся отдельно через push constant; он не требует нового pipeline. +Набор pipelines создаётся до первого draw. Для каждого исходного состояния +готовятся его авторский вариант, прозрачный вариант с сохранённым alpha-test +и пять категорий MAT0 с обеими политиками глубины. Cull и decal bias остаются +исходными; одинаковые ключи объединяются. Смена категории материала или +прозрачности диапазона выбирает готовый ключ, сохраняя descriptors, buffers и +framebuffers. Vulkan `VkPipelineCache` принадлежит renderer, используется всеми +созданиями pipelines и переживает пересоздание swapchain. На диск он пока не +сохраняется. Счётчик `render_pipeline_cache` показывает число созданных +pipelines, включая новые наборы после resize. + Рабочий путь `fparkan-game` загружает terrain и все выбранные mission roots (по умолчанию весь список объектов), разрешая локальные material slots через WEAR и MAT0 до объединения геометрии. Индексы GPU имеют тип `u32`: вся карта @@ -1034,55 +1242,80 @@ WEAR и MAT0 до объединения геометрии. Индексы GPU `u16`. Terrain передаёт в shader базовый, detail и overlay stages, lightmap и animation phase; object batches сохраняют их legacy blend/depth/alpha state. -После загрузки world path создаёт интерактивную free-flight камеру (`WASD`, -`E/Q`, `Shift`, `Ctrl`, RMB relative-look), обновляет listener и собирает -environment frame для каждого подготовленного кадра. Event loop один раз -сэмплирует камеру, общий animation clock, геометрию и окружение перед запросом -redraw; redraw потребляет это состояние. Если surface требует пересоздать -swapchain, тот же sample времени и состояние мира отправляются повторно. -Перед повторной отправкой projection камеры и viewport-зависимые environment -и shadow geometry обновляются для нового размера без продвижения сцены. -Счётчик и replay clock продвигаются только после результата `Presented`. -После шага движения камера проверяет высоту -`TerrainWorld`: над конечной поверхностью сохраняется начальный просвет, а вне -поверхности высота не ограничивается. - -Для каждого отрисовываемого строения (корневого объекта класса `FORT`) и для -каждого его отрисовываемого компонента создаётся отдельная запись проверки. Она -использует те же текущие матрицы узлов и присоединённых компонентов, что и -геометрия. -После ограничения целевой позиции по TerrainWorld камера проверяет непрерывный -отрезок от прежней позиции до цели; при пересечении она остаётся в прежней -позиции, сохраняя новый поворот. Повторная отправка ожидающего кадра не -применяет движение второй раз. Это ограничение диагностической свободной -камеры, а не правило игрового или следящего режима оригинала. +При первоначальной загрузке одинаковые текстуры разных material slots +используют один GPU image. Ключ включает размеры, RGBA8 и всю авторскую +mip-цепочку: совпадения только базового изображения недостаточно. Хеш ускоряет +поиск, а полное сравнение данных исключает ошибочное объединение при коллизии. +Слоты сохраняют собственные descriptors, UV и параметры материала. При первой +смене общей текстуры renderer создаёт отдельный image только для изменяемого +слота; следующие фазы обновляют его на месте. Поэтому анимация одного материала +не меняет соседний. Строка `render_texture_upload` показывает число слотов, +начальных GPU images и отделений при обновлении. CPU-копии исходных пикселей +в материалах эта оптимизация не объединяет. + +Рельеф образует неизменяемое начало общего vertex buffer. Control-анимация, +FX и окружение обновляют следующие вершины, а новые индексы нужны только +геометрии проекционных теней в конце index buffer. Поэтому кадр записывает +лишь эти изменяемые части. При росте буфера новая память получает все данные, +включая неизменяемое начало; уменьшение числа теней не уменьшает выделенную +ёмкость. Это сохраняет номера вершин, диапазоны отрисовки и порядок проходов. + +Перед записью общих ресурсов renderer ждёт fences всех отправленных кадров. +Одного fence текущего кадрового слота недостаточно: другой кадр ещё может +читать ту же память. Ожидание всего устройства остаётся на пересоздании +swapchain, диагностическом readback и завершении работы. Загрузка изменённых +текстур пока использует отдельную синхронную transfer-команду. Строка +`render_geometry_upload` показывает записанные vertex/index bytes и объём, +который потребовался бы при полной перезаливке, без начальной загрузки. +После загрузки world path создаёт интерактивную free-flight камеру (`WASD`, +`E/Q`, `Shift`, `Ctrl`, RMB relative-look), обновляет listener и на каждом +кадре собирает environment frame. Для каждого отрисовываемого строения +(корневого объекта класса `FORT`) и для каждого его отрисовываемого компонента +создаётся отдельная запись проверки. Она использует те же текущие матрицы узлов +и присоединённых компонентов, что и геометрия. После ограничения целевой +позиции по TerrainWorld камера проверяет непрерывный отрезок от прежней позиции +до цели; при пересечении +она остаётся в прежней позиции, сохраняя новый поворот. Повторная отправка +ожидающего кадра не применяет движение второй раз. Это ограничение +диагностической свободной камеры, а не правило игрового или следящего режима +оригинала. Mission-local `sky.ske`/`sky.wea` выбирают атмосферное расписание и sky materials, `effects.rlb/env_lightning` даёт lightning visual; weather particles, flares, sun/moon, point lights, projected shadows и audio events обновляются вместе с камерой и временем. `--atmosphere-seconds` задаёт старт времени, `--frames 0` оставляет цикл -бесконечным, `--control-clock-ms N` воспроизводит общий clock анимации MSH и -материалов вместе с окружением от нуля до `N` за заданное число кадров и требует -как минимум два кадра. -`--preview-roots` служит только диагностическим ограничителем. +бесконечным, а `--preview-roots` служит только диагностическим ограничителем. `--legacy-camera-capture` выбирает воспроизводимую камеру без free-flight -управления: запись selector 0 задаёт D3D7 transform, clip параметры и native -viewport RECT `[left, top, right, bottom]`. Путь сохраняет оси basis из view -matrix вместе с roll, переносит полученный `clip_from_world` в preview camera, -переводит RECT в физический drawable размер `(right - left) × (bottom - top)` -и отклоняет несовпадение размера при -создании или resize вместо растягивания кадра. Результат нужно сравнивать с -тем же crop исходного PNG `[left, top, right, bottom]`. Контроллер -`FreeFlightCamera` в этом режиме не создаётся. Подготовка кадра обновляет +управления. `selector0_words` содержит локальную матрицу камеры. Если запись +содержит `effective_world_matrix_words` с маркером +`effective_world_matrix_space="world"`, preview использует эту мировую +матрицу. При `parented_camera=true` отсутствие мировой матрицы — ошибка; +локальный selector 0 остаётся legacy fallback только для записей без признака +parented и без мировой матрицы. +Если native helper явно применяет выбранную мировую камеру, setter работает в +mode 2, а restore возвращает исходную локальную матрицу в mode 1. Clip +параметры и native viewport RECT `[left, top, right, bottom]` идут из той же +записи. Путь сохраняет оси basis из view matrix вместе с roll, переносит +полученный `clip_from_world` в preview camera, переводит RECT в физический +drawable размер `(right - left) × (bottom - top)` и отклоняет несовпадение +размера при создании или resize вместо растягивания кадра. Результат нужно +сравнивать с тем же crop исходного PNG `[left, top, right, bottom]`. Контроллер +`FreeFlightCamera` в этом режиме не создаётся, но redraw продолжает обновлять environment и projected shadows от captured camera. Перед environment update и его audio events listener получает position, forward и up captured camera; -начальная сортировка world draw использует её же position. Phase capture может +начальная сортировка world draw использует её же position. Облака в обычном +запуске движутся с отдельным 50-секундным циклом UV; фон и звёзды имеют свои +периоды. Изменение цвета неба по расписанию не заменяет это движение. +Phase capture может содержать наблюдаемый mission path и проверенный `atmosphere_seconds`; viewer использует эту абсолютную фазу по умолчанию, если не указан явный CLI override, -и удерживает sample атмосферы на ней через readback frames. Без -`--control-clock-ms` FX получает реальный frame time; с параметром environment -и материалы используют replay clock. Это не полное simulation time: `weather`, RNG и +и удерживает sample атмосферы на ней через readback frames. Дополнительный +`sky_uv_clock` фиксирует положение движущихся текстур на том же кадре; +без него их начальная фаза не подтверждена. Фазы FX следуют +выбранному control clock: без `--control-clock-ms` он обновляется по elapsed +time, а с явным значением readback frames переигрывают заданный clock. Это не +полное simulation time: `weather`, RNG и остальное mission state остаются неизвестными, поэтому совпадение камеры и фазы не доказывает visual parity с native frame. diff --git a/docs/tomes/06-behavior.md b/docs/tomes/06-behavior.md index 1a2cedf..6edfb7a 100644 --- a/docs/tomes/06-behavior.md +++ b/docs/tomes/06-behavior.md @@ -169,8 +169,10 @@ handler лишь инициирует execution context (`+0x50 = 1`); его ga показывает предупреждение для чисел выше `10000`, но не отменяет создание AI. Rust исполняет подтверждённый поднабор первого `Init`: действие `Handler(19)` -и числовые literal assignments, описанные ниже. Поддержанные записи применяются -к состоянию лишь после успешного прохода всего первого `Init`. +и формулы из одного атома либо двух атомов с бинарным `+` или `-`. Здесь атом — +число или имя переменной. Поддержанные шаги сразу обновляют временную копию +varset, поэтому следующая формула читает уже записанные значения. К состоянию +клана записи попадают лишь после успешного прохода всего первого `Init`. Если встретилась неподдержанная форма, preview сохраняет пакет, сообщает `Unsupported` и оставляет значения по умолчанию; при полной загрузке та же форма становится ошибкой. Это политика Rust для ограниченного исполнителя, а не правило @@ -186,12 +188,18 @@ Rust исполняет подтверждённый поднабор перво нули в `dT8` и `dT9`. Клан 1 той же миссии использует первые девять строк `scream.fml`: восемь проверенных значений попадают в `DWORD` с усечением дробной части, а `0.8` -сохраняется как число `float`. Пока Rust вычисляет только числовой литерал, -начинающийся с цифры; операторы, ссылки на переменные и другие формы формул -остаются явными ошибками. В тесте полной загрузки миссия `Mission.01` -проходит этот `Init`; `Single.01` останавливается на следующем неподдержанном -`Handler(43)` в clan 1, записи 10. Никакое значение не подставляется вместо -этого действия. +сохраняется как число `float`. Например, если `source` равен `10`, выражение +`source + 2.0` записывает `12`; следующее `target - source` читает новое значение +`target` и получает `2`. Каждая операция работает с `f32`: значение ячейки +`DWORD` сначала читается как беззнаковое число и переводится в `f32`, а результат +сохраняет тип целевой ячейки. Поддерживаются только один или два атома и один +бинарный `+` либо `-`; произвольная грамматика формул и остальная VM не +восстановлены. В отдельной точечной проверке вычисления и записи оригинальной +DLL и Rust совпали для 13 выбранных формул. Эта сверка проверяет только +эти значения и не запускает исходную миссию. Тест полной загрузки подтверждает, +что `Mission.01` проходит этот `Init`; у `Single.01` следующий неподдержанный +`Handler(43)` встречается в clan 1, записи 10. Никакое значение не подставляется +вместо этого действия. ### Первый запуск сценария @@ -246,7 +254,7 @@ SuperAI для записей кланов, затем обновляет Behavi всё равно устанавливается. В полной загрузке Rust вызывающая сторона передаёт значение времени очереди; после регистрации объектов загрузчик последовательно подготавливает каждый сценарный пакет и выполняет восстановленные действия -`Handler(19)` и числовые literal assignments из первого `Init`. Статический +`Handler(19)` и поддержанные формульные присваивания из первого `Init`. Статический просмотр не запускает Behavior, но может сохранить эти записи `Init` в сценарном varset. @@ -1434,6 +1442,30 @@ descriptor через `INTO`. Так разные части Control могут загруженной таблицы; обновление пока не подключено к игровым часам и не заменяет полный Control tick. +Устройство фабрики `2` имеет поле selector `6`, `+0x174`; конструктор задаёт ему +ноль. Позднее Control может пересчитать его по паре строк записи и отдельному +прототипу `BULL`. Только для поддержанной свежей формы одного корневого mesh +`BULL` из `objects.rlb`, без дочерних компонентов, его NDP-строки сводятся к +сумме Max; например, `[1.5, 2.25]` +превращается в `3.75` с округлением `f32` после каждого шага: + +```rust +let mut total = 0.0_f32; +for row in ndp_rows { + total += row.max_volume; +} +``` + +Пустая таблица даёт ноль; физическую единицу этому числу не приписываем. Пара +сравнивается как две C-строки до первого NUL: хвост не учитывается, регистр не +важен. Если после изменения любая строка пуста, старое `+0x174` сохраняется; +пустая пара конструктора не вызывает пересчёт, потому что уже совпадает. Новые +значения применяются при добавлении и первой активной записи `INTO` в исходном +порядке. Непредставленные входы оставляют селектор недоступным, а ошибка чтения +узнаваемого, но повреждённого ресурса отменяет новую загрузку и сохраняет +прежние мир и RNG. Общая формула selector 35 и условия свежей формы разобраны в +[техническом приложении](../appendices/script-vm.md). + Отдельный путь объекта класса `4` умножает интервал на контактный коэффициент и передаёт изменение строке. В проверенном примере коэффициент `1`, интервал `2`, и значение строки уменьшается на `2`; этот коэффициент не является ставкой |
