aboutsummaryrefslogtreecommitdiff
path: root/docs
diff options
context:
space:
mode:
Diffstat (limited to 'docs')
-rw-r--r--docs/appendices/knowledge-boundaries.md96
-rw-r--r--docs/appendices/script-vm.md103
-rw-r--r--docs/reference/atmosphere.md40
-rw-r--r--docs/reference/environment-effects.md155
-rw-r--r--docs/reference/msh.md45
-rw-r--r--docs/reference/render-frame.md162
-rw-r--r--docs/tomes/05-render.md185
-rw-r--r--docs/tomes/06-behavior.md24
8 files changed, 604 insertions, 206 deletions
diff --git a/docs/appendices/knowledge-boundaries.md b/docs/appendices/knowledge-boundaries.md
index 7fb6798..7c9daae 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,26 +50,15 @@ 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 ещё не декодированы. Внутренний ракурс «Конструктора» после
+настоящего входа игрока подтверждает исчезновение чёрной поверхности в проходе;
+наружные окна обеих миссий сверены на расстояниях около 60/85/110, а ближние
+окна — с внутренней стороны. Внутренние дальние ракурсы и clipping на краю
+кадра ещё не приняты; общей готовности R2 эти пары не доказывают.
Закрывающий эксперимент: запустить оригинал в совместимой Windows/DirectX
среде, перехватить DirectDraw/Direct3D calls и surface flips, сохранить state
@@ -72,6 +66,10 @@ log на минимальных сценах с одним типом матер
## FXID field-level semantics
+Control Op1 пока использует цвет и радиус без случайных добавок команды;
+это явное приближение считается в `render_coverage control_op1_jitter_unsupported`.
+Масштаб радиуса длиной преобразованного направления и нижняя замена 0,01 восстановлены.
+
Размеры команд, resource references, lifecycle, flags families и используемые
time modes известны. Не закрыто значение каждого поля body opcodes 1--10,
отсутствующий во всех проверенных каталогах opcode 6 и точные формулы редких
@@ -536,6 +534,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` в текущее и
@@ -778,16 +811,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 51dd429..f7f8b97 100644
--- a/docs/appendices/script-vm.md
+++ b/docs/appendices/script-vm.md
@@ -1410,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`). Последний путь не является
@@ -1477,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..8d44e4e 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. Второй источник получает направление
@@ -167,21 +185,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..45d7725 100644
--- a/docs/reference/environment-effects.md
+++ b/docs/reference/environment-effects.md
@@ -236,11 +236,19 @@ packed: [key.colors[10], key.colors[14]],
`AtmosphereFrame` пока не несёт этот бит обновления, поэтому sampler
выдаёт интерполированный цвет при каждом запросе.
-## Экранная геометрия солнца и луны
+## Отрисовка солнца, луны и бликов
-`CSun::Render` сначала переводит направление небесного тела в пиксели текущего
-viewport, затем собирает quad с native XYZRHW-позицией. При положительной высоте
-viewport Vulkan пиксели переводятся в clip-space так:
+`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, поэтому более близкая поверхность
+может закрыть спрайт, а сам спрайт не меняет глубину для последующих слоёв.
+
+В Vulkan ряды 3 и 4 сохраняют этот contract отдельным bit 8 material mode.
+CPU каждый кадр преобразует viewport-пиксели к clip space текущего размера
+swapchain:
```rust
position = [
@@ -250,13 +258,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 +292,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 +375,66 @@ 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 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..130a203 100644
--- a/docs/reference/msh.md
+++ b/docs/reference/msh.md
@@ -95,6 +95,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..19ccf3e 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`, если проверка потока, поколения и часов прошла успешно.
В превью с фиксированной камерой эта абсолютная фаза расписания используется,
@@ -163,14 +232,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 +264,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 +329,11 @@ D3D7 в `fparkan-render`, а затем один раз меняет знак в
- переход на запасной набор возможностей устройства;
- квантование времени симуляции.
-Для отладки требуется детерминированный захват кадра: состояние камеры,
-идентификаторы видимых объектов, список элементов отрисовки, ключи конвейера,
-матрицы и хэши промежуточных буферов.
+Параметр `--readback-sequence-out` сохраняет необработанные байты каждого
+отправленного кадра; в имя файла записываются индекс кадра, значение общих
+управляемых часов и размер изображения. Байты соответствуют выбранному формату
+изображения swapchain Vulkan и не гарантируют порядок каналов R, G, B, A. Числовое
+формат выводится как `readback_format`; расширение `.rgba` само по себе порядок
+каналов не определяет. Последовательность не содержит структурированных ID
+видимых объектов, списка элементов отрисовки, ключей конвейера, матриц или
+хэшей промежуточных буферов.
diff --git a/docs/tomes/05-render.md b/docs/tomes/05-render.md
index ef7dc52..72b0685 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,48 @@ 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`.
+Туман также зависит от расстояния нелинейно: между ближней и дальней границами
+движок интерполирует по квадрату дистанции. При границах 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`. Доказан
@@ -1035,54 +1086,48 @@ WEAR и MAT0 до объединения геометрии. Индексы GPU
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 камера проверяет непрерывный
-отрезок от прежней позиции до цели; при пересечении она остаётся в прежней
-позиции, сохраняя новый поворот. Повторная отправка ожидающего кадра не
-применяет движение второй раз. Это ограничение диагностической свободной
-камеры, а не правило игрового или следящего режима оригинала.
-
+`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 может
содержать наблюдаемый 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. Фазы 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 3a4e51c..6edfb7a 100644
--- a/docs/tomes/06-behavior.md
+++ b/docs/tomes/06-behavior.md
@@ -1442,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`; этот коэффициент не является ставкой