aboutsummaryrefslogtreecommitdiff
path: root/docs
diff options
context:
space:
mode:
Diffstat (limited to 'docs')
-rw-r--r--docs/appendices/knowledge-boundaries.md20
-rw-r--r--docs/reference/atmosphere.md65
-rw-r--r--docs/reference/environment-effects.md35
-rw-r--r--docs/reference/render-frame.md41
-rw-r--r--docs/tomes/05-render.md123
5 files changed, 246 insertions, 38 deletions
diff --git a/docs/appendices/knowledge-boundaries.md b/docs/appendices/knowledge-boundaries.md
index 7f049fc..0df1e5a 100644
--- a/docs/appendices/knowledge-boundaries.md
+++ b/docs/appendices/knowledge-boundaries.md
@@ -11,7 +11,8 @@ authoring API без динамического подтверждения.
state transitions и окончательный порядок части transparent/FX/shadow subpasses.
Дистанционная кривая тумана по квадратам границ подтверждена в живом CShade.
-Выбор проходов по native shade-state и GPU fog color пока заменён общим туманом мира с цветом горизонта;
+Обычный GPU fog color из горизонта подтверждён исполнением; выбор проходов по
+native shade-state и их временные изменения цвета пока заменены общим туманом мира;
это приближение считается как `render_coverage fog_pass_eligibility_approx`.
После ограничения цели рельефом интерактивная камера проверяет движение
@@ -55,7 +56,18 @@ MSH-запрос со свободной preview-камерой; это не пÐ
Данные type 16 ещё не декодированы. Внутренний ракурс «Конструктора» после
настоящего входа игрока подтверждает исчезновение чёрной поверхности в проходе;
-для всей приёмки R2 ещё нужны пары обеих миссий на трёх дистанциях.
+наружные окна обеих миссий сверены на расстояниях около 60/85/110, а ближние
+окна — с внутренней стороны. Окно «Конструктора» на правом краю кадра при
+дистанции около 85 совпадает; внутренние дальние ракурсы и остальные случаи
+отсечения ещё не приняты. Общей готовности R2 эти пары не доказывают.
+
+Старые захваты без `geometry_projection` используют состояние камеры после
+обхода мира, которое может отсекать близкие поверхности. Это отмечается
+`captured_projection_unverified`; для приёмки таких поверхностей нужен новый захват.
+
+Захваты без `sky_uv_clock` не подтверждают начальное положение облаков,
+звёзд и фона. Replay использует elapsed clock приложения и считает
+это как `captured_sky_phase_unverified`; для приёмки неба нужен новый захват.
Закрывающий эксперимент: запустить оригинал в совместимой Windows/DirectX
среде, перехватить DirectDraw/Direct3D calls и surface flips, сохранить state
@@ -67,6 +79,10 @@ Control Op1 пока использует цвет и радиус без слу
это явное приближение считается в `render_coverage control_op1_jitter_unsupported`.
Масштаб радиуса длиной преобразованного направления и нижняя замена 0,01 восстановлены.
+Control Op2 воспроизводит modes 0/2; modes 1/3, body+76 и неподдержанные
+time modes/привязки считаются как `render_coverage control_op2_unsupported`.
+Управление фазой экземпляра командами Control остаётся до его подключения в runtime.
+
Размеры команд, resource references, lifecycle, flags families и используемые
time modes известны. Не закрыто значение каждого поля body opcodes 1--10,
отсутствующий во всех проверенных каталогах opcode 6 и точные формулы редких
diff --git a/docs/reference/atmosphere.md b/docs/reference/atmosphere.md
index 65ed176..dba20c2 100644
--- a/docs/reference/atmosphere.md
+++ b/docs/reference/atmosphere.md
@@ -50,7 +50,25 @@ key_time = floor(duration * (key_hour * 60 + key_minute) * 60 / 86400)
theta = (clamp((now - birth_start) / lifetime, 0, 1) * 1.2 - 0.1) * pi
```
-Затем к нему применяется матрица рождения. У солнца и луны независимые
+Затем к нему применяется матрица рождения как к вектору-столбцу. Если `A` и
+`B` — углы рождения, а `x = cos(theta)` и `z = -sin(theta)`, результат равен
+
+```text
+[x*cos(A) + z*sin(A)*sin(B),
+ x*sin(A) - z*cos(A)*sin(B),
+ z*cos(B)]
+```
+
+Порядок важен для всего освещения: транспонированное умножение переносит
+солнце на другую сторону мира и меняет освещённые склоны, блики и тени.
+В GOG `Terrain.dll` (SHA-256
+`af87d1b2e728a0be73c52be3b44cc196ab46da7799f25a15d40f8c9b0b425ead`)
+матрицу строит RVA `0x4A550`, а вектор преобразует `CSun::Update`, RVA
+`0x4B570`. Живой замер после 282184 мс при длительности 525000 мс и углах
+`[90, 30]` дал направление `[-0.4950135, -0.1408745, -0.8573891]`;
+Rust-тест проверяет этот результат с допуском `3e-6`.
+
+У солнца и луны независимые
интервалы и независимые направления: при отсутствии активной луны нельзя
подменять её направление противоположностью солнца. `SunFrame::color` — это
единственный primary directional RGB. Второй источник получает направление
@@ -131,6 +149,31 @@ gradient и clouds row 2. Для nebula/stars/clouds используются UV
directional RGB из `sky.packed[0]`. Это четыре фиксированных pass, а не три
независимых цветных слоя.
+Все три текстурных слоя движутся. Обновление GOG `CSky`
+(`Terrain.dll`, RVA `0x48B90`, запись UV `0x4942A–0x495B7`;
+[сборка и SHA-256](original-binaries.md)) берёт разность текущего `u32` clock
+и сохранённого поля объекта `+8` с переполнением. Это отдельные миллисекунды
+жизни неба, а не абсолютная фаза суток из SKE.
+
+| Слой | Период | Добавка к исходным UV |
+|---|---:|---|
+| Фон | 300 000 мс | одинаковый сдвиг U и V |
+| Звёзды | 100 000 мс | одинаковый сдвиг U и V |
+| Облака | 50 000 мс | сдвиг только U |
+
+Добавка равна остатку от деления elapsed clock на период, умноженному на
+нативный коэффициент `f32` (`3.3333333e-6`, `1e-5`, `2e-5`). Вершинные UV
+каждый раз вычисляются от позиции купола заново; накопление приращений
+создавало бы дрейф. `SkyMesh::update_uvs` получает текущие миллисекунды кадра
+и сохраняет геометрию купола. В работающем оригинале проверены UV всех трёх
+слоёв, включая разность clock с переполнением.
+
+Захват сохраняет `sky_uv_clock` на входе в рендер неба, проверяя его по
+фактическим UV вершины зенита и clock того же кадра. Replay удерживает эту
+фазу вместе с фазой суток. Без такого поля текстуры движутся по elapsed clock
+приложения; начальная фаза оригинала неизвестна и явно считается как
+`captured_sky_phase_unverified`.
+
Цвет первого screen-gradient и последнего пояса вычисляет
`SkyFrame::screen_gradient(yaw_radians, glare_rgb_delta)`. Native `CSky` сначала
округляет `(yaw + pi) * 180 / pi` режимом nearest-even, выбирает сектор
@@ -167,21 +210,27 @@ RGB palette 12 с минимумом `80/255` на канал и alpha `150/255`
## Туман и свет
-Нативное обновление `CSky` записывает две дальности и один packed RGB.
+Нативное обновление `CSky` записывает две дальности и нижнюю границу освещения.
`FogFrame` повторяет этот контракт:
```text
fog.start = sky.values[0] * 700
fog.end = sky.values[1] * 700
-fog.color = RGB(sky.packed[1]) / 255
+lighting_floor = RGB(sky.packed[1]) / 255
```
Поля не скрывают порядок исходных параметров: значения сохраняются в
-`SkySample`, а преобразование выполняется только в `fog_frame`. Vulkan path
-передаёт цвет и дальности в frame uniforms; vertex shader вычисляет линейный
-коэффициент по расстоянию до камеры, а fragment shader смешивает RGB материала
-с fog color в ветвях combiner, которые используют туман. Конкретный draw state
-может обходить это смешивание.
+`SkySample`, а преобразование выполняется только в `fog_frame`. Нижняя граница
+освещения ограничивает результат расчёта света снизу; она не задаёт цвет тумана.
+Обычный цвет тумана выбирается из `sky.colors[0..4]` по направлению камеры,
+как цвет горизонта. Это подтверждено в `CSky::Render` и живым совпадением
+его значения с `D3DRENDERSTATE_FOGCOLOR` в том же игровом тике.
+
+Vulkan path передаёт эти параметры раздельно. Vertex shader вычисляет
+коэффициент по квадратам расстояния и границ, а fragment shader смешивает
+RGB материала с цветом тумана. Конкретный native draw state может обходить
+смешивание или временно менять его цвет, например на чёрный для отдельного
+прохода. Полный выбор таких проходов ещё не воспроизведён.
Цвет primary света солнца — RGB palette 12, нормированный делением на 255 и
умноженный на trailing parameter 2. `SunFrame::direction` — нормированный
diff --git a/docs/reference/environment-effects.md b/docs/reference/environment-effects.md
index 45d7725..f3ecfb4 100644
--- a/docs/reference/environment-effects.md
+++ b/docs/reference/environment-effects.md
@@ -218,6 +218,41 @@ OneShot -> воспроизвести разовый звук молни
отдельный spatial source, применяет DirectSound range/pan и текущий listener.
Отсутствие устройства вывода не должно останавливать симуляцию.
+### Звуки Control
+
+Команда Op2 может принадлежать не погоде, а экземпляру эффекта на узле модели.
+`fparkan-fx::ambient_sound` хранит её временное окно и предыдущее значение
+фазы. В режиме 0 разовый звук начинается при строгом пересечении начала окна:
+`previous <= start && start < phase`, если фаза движется вперёд. Значение
+`phase == 1` для этой проверки заменяется нулём и подготавливает следующий
+цикл. Режим 2 включает loop на обеих границах окна; внутри окна уже работающий
+источник обновляется, а не запускается заново. Выход за окно останавливает его.
+
+Позиция интерполируется между двумя локальными точками и проходит через ту же
+матрицу владельца, что и графические команды. Минимальная и максимальная
+дистанции остаются постоянными; частота интерполируется по фазе окна.
+Каждая команда получает отдельный источник звука, даже если ресурс совпадает
+с ресурсом соседней команды. Дождь сохраняет свой loop. Перемещение слушателя,
+потеря фокуса и завершение приложения затрагивают все эти источники.
+
+Эти ветви восстановлены в GOG `Effect.dll` (сборка из
+[таблицы бинарников](original-binaries.md)): init RVA `0x12D10`, update
+`0x12EB0`. Живой loop `sounds.lib:f_defl.wav` передаёт в звуковой backend
+диапазон `12/80`, частоту `1` и мировую позицию из матрицы владельца.
+В режиме 2 признак loop берётся из первого DWORD body, а не из command flag.
+
+Обычное создание эффекта Control сохраняет time mode заголовка и задаёт
+постоянную фазу нулём. Raw selector строки служит ID экземпляра, а не номером
+режима времени. Поэтому `f_comp_sfx` с time mode 0 и началом окна `0.001`
+остаётся тихим до управляющей команды Control. Preview такие команды ещё не
+исполняет. Периодические звуки с time mode 2 уже следуют общим часам и текущей
+позе узла. `render_coverage control_op2_supported` считает подготовленные
+команды, а `control_sound_start/stop` показывает фактические переходы.
+
+Неизвестные backend modes 1/3, ненулевое поле body+76, неподдержанное время
+или привязка владельца пропускаются с `render_coverage control_op2_unsupported`.
+Они не заменяются разовым звуком или произвольным loop.
+
## Цвет солнца
Солнце берёт цвет спрайта и прозрачность бликов из `palette[10]` расписания
diff --git a/docs/reference/render-frame.md b/docs/reference/render-frame.md
index b2a6a93..df3f248 100644
--- a/docs/reference/render-frame.md
+++ b/docs/reference/render-frame.md
@@ -85,9 +85,13 @@ RVA `0x1B0F0`, заменяет diffuse alpha пакета без type 16 с bat
квадраты границ по этому наблюдению. GLSL вычисляет квадрат расстояния вершины,
после чего растеризатор интерполирует коэффициент по треугольнику.
`native_fog.effective.sink_rgb` совпадает с `sky.packed[1]`, нижней границей
-освещения CShade. Это не подтверждает значение GPU fog color. Приложение пока
-сохраняет цвет экранного горизонта; выбор цвета и проходов требует отдельной
-проверки GPU-состояния.
+освещения 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`.
@@ -134,21 +138,17 @@ RVA `0xF6E0`, преобразует направление текущей маÑ
глубину. За фиксированными диапазонами окружения сохраняются их явные состояния.
Для 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`.
В превью с фиксированной камерой диапазоны туманности и звёзд, купола и облаков
явно помечаются как прозрачный фон. Непрозрачные диапазоны мира остаются на
@@ -225,6 +225,17 @@ RVA `0x4EBE0` использует соглашение о вызовах `__std
`right - left` на `bottom - top`. Размер окна превью закреплён: если при
запуске или после ненулевого изменения окна физический размер не совпадает с
заданным, приложение завершает работу, а не растягивает изображение.
+
+Проекция во время рисования может отличаться от восстановленного состояния
+камеры после обхода мира. В GOG у `CStridedPrimitive::RenderVB` (Terrain RVA
+`0x1D110`) renderer содержит `near = 0.5`, а на границе World3D RVA `0x13CE4`
+уже `near = 5`. Поэтому захват отдельно сохраняет `geometry_projection` при
+первом вызове геометрии с той же мировой матрицей камеры. Поток, поколение,
+renderer, viewport, far и FOV сверяются с последующей границей; только
+подтверждённая запись используется при воспроизведении. Иначе пол в трёх
+единицах от камеры может исчезнуть из-за чужой ближней плоскости. Старые JSON
+без этой записи сохраняют прежнее поведение с явным счётчиком
+`captured_projection_unverified`; их нужно переснять для проверки близких поверхностей.
Сравнивайте результат с фрагментом исходного PNG по прямоугольнику
`[left, top, right, bottom]`. В этом режиме `FreeFlightCamera` не создаётся,
но при повторной отрисовке обновляются окружение и проецируемые тени для
diff --git a/docs/tomes/05-render.md b/docs/tomes/05-render.md
index 72b0685..c1563fc 100644
--- a/docs/tomes/05-render.md
+++ b/docs/tomes/05-render.md
@@ -845,6 +845,17 @@ projected shadows. Радиус источника масштабируется
синий свет завода почти незаметным. Неположительный итог заменяется на 0,01.
Случайные добавки к цвету и радиусу пока явно считаются в
`render_coverage control_op1_jitter_unsupported`.
+
+Эффект может одновременно рисовать поверхность и издавать звук. Для Op2
+приложение создаёт отдельный звуковой источник у каждого экземпляра команды:
+несколько одинаковых механизмов слышны одновременно и не заменяют дождь.
+Позиция следует анимируемому узлу, а фаза включает, обновляет и останавливает
+loop. Обычная строка Control создаёт экземпляр с нулевой постоянной фазой;
+звуки с таким режимом ожидают отдельной управляющей команды. Периодические
+звуки работают по часам уже в preview. Устройство вывода остаётся в приложении,
+а правила переходов и параметров — в `fparkan-fx::ambient_sound`
+([подробнее](../reference/environment-effects.md#звуки-control)).
+
Туман также зависит от расстояния нелинейно: между ближней и дальней границами
движок интерполирует по квадрату дистанции. При границах 0 и 700 точка в 350
получает четверть тумана, а не половину. Это сохраняет контраст среднего плана.
@@ -998,6 +1009,12 @@ sun-ray queries поэтому используют исходные коордÐ
она не равна одному лишь обратному преобразованию камеры.
В проверенном GOG контексте Ngi32 использовались `n=0.5`, `z=700`, `f=1.3`.
+Ближняя плоскость относится к конкретному проходу. После рисования движок
+может вернуть прежние настройки камеры: захват только в конце мира видел
+`n=5` и при воспроизведении вырезал пол перед игроком. Формат захвата поэтому
+хранит проверенную проекцию вызова геометрии отдельно от состояния камеры;
+подробности приведены в [описании кадра](../reference/render-frame.md#предпросмотр-по-захвату-камеры-d3d7).
+
У GOG `Terrain.dll + 0x7355c` хранит указатель на активную внешнюю камеру.
Её selector 0 возвращает 16 слов начиная с `+0x20`, перенос находится в словах
3, 7, 11. Интерфейс `LoadCamera` расположен отдельно, по `+0x134` объекта.
@@ -1013,11 +1030,39 @@ Node38, а диапазоны граней — в 68-байтовых Slot68. В
проверяются на единственное назначение, а выходящий за массив, повторный или
неподдерживаемый диапазон считается ошибкой загрузки.
-Первый рендер карты, terrain-запросы высоты и пересечения, receiver-ы теней и
-обрезка контуром зданий используют только LOD0. Отбор сохраняет порядок
-исходных граней, а контурный проход отдельно переводит индекс спроецированной
-грани обратно в исходный. LOD1 пока не подставляется по расстоянию: такой
-переход не реализован.
+Рисуемый уровень выбирается по ячейкам, а не по расстоянию до отдельного
+треугольника. Начало сетки находится в первых двух float заголовка Slot68,
+размер ячейки — в границах первого слота LOD0. Атрибут 2 потока Node38 задаёт
+число ячеек по X; индекс ячейки равен `x * count_y + y`. Приложение проверяет,
+что границы всех слотов образуют эту регулярную сетку. Все 33 карты GOG
+проходят такую проверку, включая вырожденные грани на общей границе ячеек:
+принадлежность грани берётся из слота, а не угадывается по её центру.
+
+Координаты камеры переводятся в целые координаты сетки округлением вниз.
+Если `dx * dx + dy * dy >= 9` и горизонтальный FOV не меньше 0,5 радиана,
+выбирается LOD1. При увеличении масштаба, отсутствии дальнего слота или
+наличии геометрического patch остаётся LOD0. Камера за границей карты не
+прижимается к крайней ячейке для этого расчёта. Правило восстановлено по
+GOG `Terrain.dll` ([сборка и SHA-256](../reference/original-binaries.md)):
+`CLandscape::Render`, RVA `0x11340`, выбор уровня `0x11878–0x11A03`.
+Исполнение оригинала подтверждает выбор дальнего слота, а getter камеры
+`Ngi32.dll` RVA `0x4D960` возвращает именно FOV.
+
+Основания строений сохраняют подробную геометрию. Нативная регистрация
+patch (`Terrain.dll` RVA `0x12E20`) проверяет его треугольники против каждой
+ячейки через `mrnParaIntersectsPoly`. Поэтому учитываются и соседние ячейки,
+которых касается край треугольника. Перед проверкой координаты нормализуются
+в `f32` относительно центра ячейки; замена этой операции сравнением мировых
+границ меняет результат на общих рёбрах. На «Конструкторе» игнорируемый
+ресурсный тест сверяет все 22 ячейки с принудительным LOD0 с набором,
+полученным в живом оригинале.
+
+Vulkan получает оба уровня один раз. Диапазоны индексов разделены по
+материалу и исходной ячейке; на каждом кадре включается только выбранный
+уровень. Сгенерированные отмостки остаются включены. Terrain-запросы высоты
+и пересечения, receiver-ы теней и обрезка контуром зданий используют
+канонический LOD0. Отбор сохраняет порядок исходных граней, а контурный проход
+отдельно переводит индекс спроецированной грани обратно в исходный.
```rust
let assignments = mesh.face_lod_assignments()?;
@@ -1035,9 +1080,10 @@ for (face, assignment) in mesh.faces.iter().zip(assignments.iter()) {
исходные фрагменты рельефа вместе с треугольниками отмостки в одном наборе.
`TerrainWorld::with_basement_foundation` принимает этот набор во владение и
устанавливает те же координаты и маски в BVH высотных запросов и пересечений.
-Предпросмотр миссии на Vulkan пока строит только LOD0: связывание BAS-записей
-с этой подготовкой ещё не подключено, поэтому наличие API не означает, что
-геометрия миссии уже обрезана.
+Предпросмотр миссии связывает BAS-записи с этой подготовкой до загрузки
+материалов. Полученная поверхность общая для изображения, теней и запросов;
+поправка размещения основания один раз применяется к корню строения и его
+присоединённым компонентам.
Внешнее кольцо должно лежать на области LOD0, а каждый его сегмент — оставаться
покрытым текущей поверхностью. Непокрытый сегмент или пересекающиеся контуры
@@ -1048,13 +1094,33 @@ for (face, assignment) in mesh.faces.iter().zip(assignments.iter()) {
## Материалы ландшафта
-Рядом с `Land.msh` лежат `Land1.wea` и `Land2.wea`. Старший байт
-`TerrainFace28.material_tag` выбирает материал первой таблицы, младший —
-второй. `0xff` в старшем байте означает отсутствие первого слоя.
+Рядом с `Land.msh` лежат `Land1.wea` и `Land2.wea`. Младший байт
+`TerrainFace28.material_tag` выбирает базовый материал `Land1.wea`, старший —
+дополнительный материал той же таблицы. `0xff` в старшем байте означает
+отсутствие дополнительного слоя. `Land2.wea` содержит соответствующие этим
+двум строкам detail-материалы.
В AutoDemo встречаются теги `0102`, `0203`, `ff01`, `ff02`, `ff03`.
Менеджер World3D получает селектор `(table_index << 16) | material_index`
и возвращает выбранную фазу MAT0 размером 76 байт.
+Таблица, строка и блок анимации — разные части запроса. В быстром проходе
+с двумя текстурами `CShade` запрашивает для Land2 блок анимации **1**. Он может
+выбирать другую текстуру, а не только другие коэффициенты освещения:
+например, в «Конструкторе» это `L09M.0` вместо `L09.0`. Подмена блока первым
+приводит к умножению базового изображения на неподходящую detail-текстуру и
+меняет яркость и контраст земли. `TerrainMaterialSelection` сохраняет номер
+блока отдельно от packed material key; приложение использует его при загрузке
+дескриптора и в последующих кадрах. Если второго блока нет, общий MAT0 sampler
+возвращается к первому, как оригинальный `GetMaterialPhase`.
+
+В GOG `Terrain.dll` (сборка из [таблицы бинарников](../reference/original-binaries.md))
+быстрая ветвь находится в RVA `0x1A7AC–0x1A7FB`. Живое исполнение передаёт
+handle `0x00010002`, block `1` и устанавливает режим текстур `9`; первая
+текстура умножается на diffuse, вторая — на результат с множителем два.
+Ближний дополнительный проход с порогом `260 / FOV` относится к fallback без
+этого режима. На проверенном устройстве он не выбирается, поэтому переносить
+его дистанционный порог в быстрый проход нельзя.
+
Type 18 в `Land.msh` оригинал называет microtexture mapping chunk;
он содержит четырёхбайтовые записи. Type 11 хранит пары `u16`, выбранные
таблицей terrain slots. Их первое поле — ключ shade cache, а не номер строки
@@ -1085,6 +1151,32 @@ WEAR и MAT0 до объединения геометрии. Индексы GPU
`u16`. Terrain передаёт в shader базовый, detail и overlay stages, lightmap и
animation phase; object batches сохраняют их legacy blend/depth/alpha state.
+При первоначальной загрузке одинаковые текстуры разных 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 и набора pipelines, диагностическом readback и завершении работы. Загрузка изменённых
+текстур пока использует отдельную синхронную transfer-команду. Строка
+`render_geometry_upload` показывает записанные vertex/index bytes и объём,
+который потребовался бы при полной перезаливке, без начальной загрузки.
+
После загрузки world path создаёт интерактивную free-flight камеру (`WASD`,
`E/Q`, `Shift`, `Ctrl`, RMB relative-look), обновляет listener и на каждом
кадре собирает environment frame. Для каждого отрисовываемого строения
@@ -1121,10 +1213,15 @@ drawable размер `(right - left) × (bottom - top)` и отклоняет Ð
`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. Фазы FX следуют
+и удерживает sample атмосферы на ней через readback frames. Дополнительный
+`sky_uv_clock` фиксирует положение движущихся текстур на том же кадре;
+без него их начальная фаза не подтверждена. Фазы FX следуют
выбранному control clock: без `--control-clock-ms` он обновляется по elapsed
time, а с явным значением readback frames переигрывают заданный clock. Это не
полное simulation time: `weather`, RNG и