aboutsummaryrefslogtreecommitdiff
path: root/docs/tomes
diff options
context:
space:
mode:
Diffstat (limited to 'docs/tomes')
-rw-r--r--docs/tomes/05-render.md401
-rw-r--r--docs/tomes/06-behavior.md165
2 files changed, 461 insertions, 105 deletions
diff --git a/docs/tomes/05-render.md b/docs/tomes/05-render.md
index ef7dc52..266cb47 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,35 @@ 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 при каждом обновлении камеры, после анимации
+модели. Нулевое значение временно выключает диапазон индексов; при удалении
+камеры диапазон восстанавливается. Расстояние использует тот же приближённый
+обратный корень, что и оригинал: округление влияет на записанную alpha.
+Подробности вычисления приведены в [описании кадра](../reference/render-frame.md#прозрачность-порталов-msh).
+
## Texm: текстуры, mip-уровни и атласы
`Texm` -- основной формат изображений. Он хранится в `Textures.lib`,
@@ -646,15 +659,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 +836,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 +1013,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 +1034,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 +1084,10 @@ for (face, assignment) in mesh.faces.iter().zip(assignments.iter()) {
исходные фрагменты рельефа вместе с треугольниками отмостки в одном наборе.
`TerrainWorld::with_basement_foundation` принимает этот набор во владение и
устанавливает те же координаты и маски в BVH высотных запросов и пересечений.
-Предпросмотр миссии на Vulkan пока строит только LOD0: связывание BAS-записей
-с этой подготовкой ещё не подключено, поэтому наличие API не означает, что
-геометрия миссии уже обрезана.
+Предпросмотр миссии связывает BAS-записи с этой подготовкой до загрузки
+материалов. Полученная поверхность общая для изображения, теней и запросов;
+поправка размещения основания один раз применяется к корню строения и его
+присоединённым компонентам.
Внешнее кольцо должно лежать на области LOD0, а каждый его сегмент — оставаться
покрытым текущей поверхностью. Непокрытый сегмент или пересекающиеся контуры
@@ -997,13 +1098,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 +1142,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 +1246,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 3af2c21..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,15 +188,54 @@ 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. Никакое значение не подставляется
+вместо этого действия.
### Первый запуск сценария
+Перед настройкой первого шага каждый AI получает начальный массив DWORD. В
+`iron3d.dll` SHA-256
+`d1dc4ea8535e2069b0a05c9e8be2724dd438ba44cca7a83007d8b2e50f9e35fa`
+`FUN_100392e0` (RVA `0x392e0`) повторяет вызов слота AI `+0x38` столько раз,
+сколько задаёт `second_resource.tag` этой строки TMA. Этот слот ведёт в
+`ai.dll` SHA-256
+`c19cd6cd31cd2896f8c4ba0f0e700dd3408aec8182daaf4863d98a7d233a2abc`:
+метод RVA `0x2010` передаёт коллекцию при `AI+0x83c` в `FUN_10002980`
+(RVA `0x2980`), а она добавляет один DWORD через `FUN_100048f0` (RVA
+`0x48f0`). Каждый вызов копирует `0xffffffff`; входное слово не меняется.
+Поэтому тег `3` даёт `[0xffffffff, 0xffffffff, 0xffffffff]`. Это число
+начальных ячеек, а не назначение трёх ID.
+
+Изолированная x86-проверка вызовом оригинальной `ai.dll` подтвердила пары
+«число слотов/ёмкость» `0/0`, `1/17`, `17/17` и `18/34`. Все добавленные слова
+равны `0xffffffff`, а входное значение, контрольные поля, память вне заголовка
+массива и указатель стека сохраняются. Это подтверждает добавление начального
+слова, но не работу последующих обработчиков.
+
+Rust хранит такой снимок в отдельном `MissionAiStartupState` для каждого TMA-клана,
+а не в пакете сценария. При полной загрузке (`Full`) создаётся запись для каждого
+клана, в том числе если у него нет пути сценария или пути таблицы исследований. Счётчик
+берётся из сырых битов `second_resource.tag` как `u32` у обеих форм записи —
+`Standard` и `Spatial`; нулевой счётчик даёт существующее состояние с пустым
+списком. Статический режим предпросмотра не создаёт состояний AI.
+
+Загрузка отклоняет суммарный объём свыше 1 000 000 DWORD и резервирует память
+проверяемой операцией; это защита Rust от чрезмерного выделения памяти, а не
+ограничение оригинальной игры. Rust сохраняет начальные слова, но не
+восстанавливает работу последующих обработчиков и не назначает этим словам ID
+объектов. Этот массив также не является списком
+`BehaviorDistributorState.registered_objects`.
+
До регулярной работы Behavior настраивает расписание своего первого шага. Он
берёт текущее 32-битное время игровой очереди и назначает проверку через три
интервала по 64 мс, добавляя небольшую случайную задержку от 0 до 63 мс. Эта
@@ -213,7 +254,7 @@ SuperAI для записей кланов, затем обновляет Behavi
всё равно устанавливается. В полной загрузке Rust вызывающая сторона передаёт
значение времени очереди; после регистрации объектов загрузчик последовательно
подготавливает каждый сценарный пакет и выполняет восстановленные действия
-`Handler(19)` и числовые literal assignments из первого `Init`. Статический
+`Handler(19)` и поддержанные формульные присваивания из первого `Init`. Статический
просмотр не запускает Behavior, но может сохранить эти записи `Init` в сценарном
varset.
@@ -546,8 +587,14 @@ let cost = calc_full_research_cost(&candidate, &table)?;
штатный Handler(43) и запуск миссии здесь не проверялись.
Владелец начальной записи исследования — индекс текущего прохода по кланам.
-Движок сохраняет его в `descriptor[0]`, а затем переносит в контекст, чтобы
-связать запрос с AI-данными того же клана.
+В GOG-сборке `iron3d.dll!FUN_1005ff40` (RVA `0x5ff40`) проходит записи кланов
+и вызывает для каждой `FUN_100392e0` (RVA `0x392e0`), которая создаёт AI;
+`ai.dll!CreateSuperAI` (RVA `0xf710`) выделяет объект и вызывает конструктор
+по RVA `0x1000`, не проверяя наличие сценарного пути. Для исследовательского
+пути нужны отдельный флаг включения и непустой `second_resource.path`; наличие
+сценарного пакета условием не является. Индекс клана сохраняется в
+`descriptor[0]` и переносится в контекст, чтобы связать запрос с AI-данными
+того же клана.
Это значение отдельно от аргумента 1 `AddNewObjectToGame`, берущегося из
`GetNetPlayerNum`, и от TMA-тега `second_resource`, задающего число вызовов
@@ -557,12 +604,14 @@ Handler(43) и штатный запуск миссии остаются непÑ
Полный Rust-загрузчик принимает явный `MissionStartupInput`: время очереди
и отдельный `research_creation_gate`. Режим миссии и имя файла этот
-флаг не заменяют. При включённом флаге таблица загружается только для клана,
-чей сценарный пакет уже прочитан и успешно разобран; путь берётся из второго
-ресурса этого клана как C-строка. Пустая C-строка пропускается, а не-ASCII
-байты дают явную ошибку. Клан индексируется по порядку в TMA, тег ресурса на
-владение не влияет. Если несколько кланов указывают на один файл, каждый
-получает отдельно декодированную таблицу с собственными строками TRF0.
+флаг не заменяют. При включённом флаге загрузчик использует уже имеющееся
+`MissionAiStartupState` каждого TMA-клана, даже если у него нет сценарного
+пакета; путь берётся из второго ресурса как C-строка до первого NUL. Пустой
+путь и выключенный флаг пропускают чтение, а не-ASCII байты дают явную ошибку.
+Предпросмотр тоже не читает TRF. Клан индексируется по порядку в TMA, а тег
+`second_resource` задаёт число слотов AI, не владельца. Если несколько кланов
+указывают на один файл, каждый получает отдельно декодированную таблицу со
+своими строками TRF0.
```rust
MissionStartupInput {
@@ -571,9 +620,10 @@ MissionStartupInput {
}
```
-Это сохраняет исходные таблицы и их связь с владельцами для следующих этапов.
-Оно ещё не создаёт полный исследовательский контекст оригинала и не запускает
-`Handler(43)`; эта обработка пока остаётся явно неподдержанной.
+Это сохраняет декодированные таблицы и связь с владельцами для следующих
+этапов. Оно ещё не создаёт полный исследовательский контекст оригинала, не
+строит AI-каталог и не запускает `Handler(43)`; эти части пока не
+восстановлены.
Отдельная frozen-матрица непосредственно вызвала chooser GOG `ai.dll` SHA-256
`c19cd6cd31cd2896f8c4ba0f0e700dd3408aec8182daaf4863d98a7d233a2abc`: функция
@@ -1089,8 +1139,31 @@ NDP, а `FUN_1000a100` (RVA `0xa100`) добавляет их в общую та
последовательность provider-строк: её длина берётся из NDP, она не дополняется
до числа узлов MSH и не участвует во второй перенумерации ключа.
-Так этот селектор включён в цепочку добычи руды оригинала. В той же сборке
-`Behavior.dll` (SHA-256
+Индексный selector `0x400` выдаёт значение выбранной строки provider как есть.
+Сначала общий индекс устройств выбирает дескриптор, затем его ключ выбирает
+строку NDP; это разные номера. Результатом становится текущее type-1 значение
+этой строки, а не запас `CurrentOre`, предел или значение после умножения на
+коэффициент и `M`. Например, provider-строка с `0.625` возвращает `0.625`, а
+записанный в ней ноль тоже остаётся успешным результатом. Rust использует уже
+подготовленную таблицу и сообщает отдельную ошибку, если provider-таблица или
+строка по ключу недоступна. Этот запрос читает снимок состояния и сам не меняет
+таблицу и не запускает её обновление.
+
+У правила есть два разных подтверждения. Разбор AutoDemo нашёл восемь таблиц
+Control и 100 устройств; все 100 значений по ключам устройств в этом корпусе
+равны `1.0`, поэтому Rust-проверка подтверждает реальные ключи и доступность
+строк, но сама по себе не различает выбор по ключу и по номеру устройства.
+Отдельная x86-проверка исходного `Control.dll` включала шесть вызовов на
+синтетической таблице, где значения строк были заданы вручную и различались.
+Например, при номере устройства `2`, ключе `1` и нулевом слове
+`device+0x50` оригинал вернул `0.625` из выбранной строки. Точные биты ответов
+приведены в [техническом разборе](../appendices/script-vm.md#индексный-control-getter-сырой-селектор-0x400).
+Этот опыт не создавал оригинальные factory-объекты и не исполнял полную миссию:
+полный интерфейс IID `0x204`, интеграция Behavior и игровое поведение остаются
+непроверенными.
+
+В цепочке добычи руды оригинал использует описанный выше selector `0x300`.
+В той же сборке `Behavior.dll` (SHA-256
`ddfbdbde07832358fe6e35e01c8dba9b58e7313789498b6049ed51d4ed9baf99`)
`FUN_100186b0` (RVA `0x186b0`) перечисляет устройства типа `0x1a`, а
`FUN_100198e0` (RVA `0x198e0`) суммирует успешные запросы IID `0x204` с selector
@@ -1369,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`; этот коэффициент не является ставкой
@@ -1502,7 +1599,7 @@ offsets 119–121. В `fr_l_gener.ctl` найдено 35 строк type 4, вк
`f_gener_ball` с offset 85.
Control.dll `+0x8B10` передаёт указатель текущей записи диспетчеру `+0x2D4B0`.
-Для tags `2`, `3`, `4`, `5`, `8`, `10`, `21` и `27` подтверждён общий читатель
+Для tags `2`, `3`, `4`, `5`, `8`, `10`, `17`, `21` и `27` подтверждён общий читатель
`+0x21D50`: таблица виртуальных методов tag `2` (`+0x3CBD8`) вызывает его через
обёртку `+0x29650`, остальные перечисленные vtable указывают прямо на него в
позиции `+0x1C`. Читатель потребляет
@@ -1513,6 +1610,28 @@ Control.dll `+0x8B10` передаёт указатель текущей запÐ
а также отказ при оборванном завершающем NUL. Это подтверждает структурный
формат, но не назначает смысл всем коэффициентам устройств.
+Путь записи с меткой `17` проверен на GOG `Control.dll` с SHA-256
+`bcb86318089028548a178b03a21accdce15df0cc85ddd91674cf3c2f55012c6e`:
+ветвь `0x11` фабрики `+0x2D4B0` вызывает конструктор `+0x24760`, который
+устанавливает указатель на виртуальную таблицу `0x1003C840`. Ячейка `+0x1C`
+этой таблицы ссылается прямо на общий читатель `+0x21D50`. В ресурсах
+`weapon.rlb:bm_m_01.ctl` и `weapon.rlb:bm_l_01.ctl` таблица привязок
+заканчивается на смещении `0x120`, где начинается запись с меткой `17`.
+Общий префикс размером `0xB0` байт включает саму метку; счётчик по `+0xAC` и
+слово длины в этих двух ресурсах равны нулю, поэтому указатель чтения
+переходит на `0x1D4`. Там начинаются `0x54` байта заголовка таблиц эффектов;
+таблицы содержат соответственно `4`, `2`, `2` и `2` строки по 100 байт и
+заканчиваются на `0x620`. Записи из реестра объектов `bm_m_01` и `bm_l_01`
+при обычной подготовке ресурсов сохраняют явно указанные списки точек CPT
+(`bm_m_a.cpt` и `bm_l_a.cpt`) и таблицы узлов NDP (`bm_m_01.ndp` и
+`bm_l_01.ndp`); в первой таблице эффектов находятся три строки type 4.
+Синтетический тест проверяет непустые массивы значений и строки у записи с
+меткой 17 и следующей записи с меткой 4, затем переход к таблице эффектов.
+Если считать 17 неизвестной, чтение остановится перед
+ней; общий разбор позволяет пройти запись и прочитать следующие таблицы,
+оставляя слова tag 17 числами без толкования. Обработчики этой метки и их
+игровое поведение ещё не восстановлены.
+
Resolver прототипов хранит явную registry-ссылку на `.cpt` отдельно от ссылки
на `.ctl`. Если строки type 4 разобраны и явная ссылка на CPT есть, подготовка
читает ровно указанные archive/name; имя CPT не выводится из имени CTL. В CPT