aboutsummaryrefslogtreecommitdiff
path: root/docs/reference
diff options
context:
space:
mode:
Diffstat (limited to 'docs/reference')
-rw-r--r--docs/reference/environment-effects.md66
-rw-r--r--docs/reference/msh.md47
-rw-r--r--docs/reference/original-binaries.md15
-rw-r--r--docs/reference/render-frame.md350
4 files changed, 306 insertions, 172 deletions
diff --git a/docs/reference/environment-effects.md b/docs/reference/environment-effects.md
index 31024f7..6c45038 100644
--- a/docs/reference/environment-effects.md
+++ b/docs/reference/environment-effects.md
@@ -189,6 +189,72 @@ OneShot -> воспроизвести разовый звук молни
отдельный spatial source, применяет DirectSound range/pan и текущий listener.
Отсутствие устройства вывода не должно останавливать симуляцию.
+## Цвет солнца
+
+Солнце берёт цвет спрайта и прозрачность бликов из `palette[10]` расписания
+`sky.ske`; `palette[9]` — чёрный specular-компонент. Вторичный направленный
+свет использует `palette[14]`. При выборке расписания это выражено так:
+
+```rust
+packed: [key.colors[10], key.colors[14]],
+```
+
+Этот порядок соответствует живому GOG-замеру: `CSun+0x95c` содержал
+`0x41FF3C00` (интерполяция `palette[10]`), `+0x960` — `0xFF000000`
+(`palette[9]`), а `+0x964` — вторичный свет из `palette[14]`. В `Terrain.dll`
+по VA `0x1004AA92..0x1004AAAB` цвет в `+0x95c` обновляется, только если
+установлен бит 0 результата sky-query; иначе остаётся прежнее значение.
+`AtmosphereFrame` пока не несёт этот бит обновления, поэтому sampler
+выдаёт интерполированный цвет при каждом запросе.
+
+## Экранная геометрия солнца и луны
+
+`CSun::Render` сначала переводит направление небесного тела в пиксели текущего
+viewport, затем собирает quad с native XYZRHW-позицией. При положительной высоте
+viewport Vulkan пиксели переводятся в clip-space так:
+
+```rust
+position = [
+ pixel[0] * (2.0 / width) - 1.0,
+ pixel[1] * (2.0 / height) - 1.0,
+ 0.999,
+];
+```
+
+Vertex shader пропускает `clip_from_world` для строк солнца и луны. CPU каждый
+кадр пересчитывает центр и размер quad по текущему viewport; старые пиксельные
+координаты не переносятся на новый размер. World-space billboards бликов
+остаются отдельным путём. Материал экранного quad не освещается, vertex fog
+отключён, а sky far-depth bit к нему не применяется.
+
+Native `z = 0.999` не означает, что спрайт гарантированно лежит за любой
+поверхностью. Vulkan использует `LESS_OR_EQUAL` и не записывает depth для
+прозрачного ряда. При near `0.5`, far `700` мировая поверхность на расстоянии
+`320` получает depth
+`700 / (700 - 0.5) * (1 - 0.5 / 320) ≈ 0.999151`; поэтому экранный `0.999`
+чуть ближе этой поверхности и проходит depth test перед ней. Это фиксированное
+native значение сохраняется для совместимости, но само по себе не обещает
+окклюзию солнца всей геометрией местности.
+
+## Геометрия визуальных 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 ещё
+не реализован.
+
## Что остаётся за владельцем
`EnvironmentSystem` не извлекает камеру из renderer, не разрешает материалы
diff --git a/docs/reference/msh.md b/docs/reference/msh.md
index 165b750..a28514d 100644
--- a/docs/reference/msh.md
+++ b/docs/reference/msh.md
@@ -153,3 +153,50 @@ Byte `material_index` выбирает строку WEAR; high word at `+0x02` Ñ
`index_start + index_count` остаётся внутри type 6, а каждый использованный
индекс через `base_vertex` попадает в position stream. `vertex_count` и
`local_batch_index` доступны renderer для исходного draw contract.
+
+## Сторона грани в цветовом проходе
+
+В цветном проходе `Terrain.dll`, функция `0x100296F0`, сравнивает камеру с
+каждой гранью по знаку тройного произведения:
+
+```c
+d = dot(eye - v0, cross(v1 - v0, v2 - v0));
+```
+
+Проход оставляет грань только при `d > 0`. Raw `Batch20.flags` задаёт два
+исключения: `0x0002` пропускает программную проверку стороны, поэтому обратная
+грань не отбрасывается этим условием. С `0x4000` проверяется только первая
+грань записи: если `d <= 0`, цикл её граней прекращается; если `d > 0`,
+оставшиеся грани этой записи уже не проходят повторную программную проверку.
+`Terrain.dll!0x100296F0` также настраивает пакет этого цветного пути: flags по
+адресу `packet+0x128` равны `0x404` или `0x414`. Путь отправки в
+`Terrain.dll!0x1001D660` перед вызовом renderer vtable `+0x48` очищает только
+маску `0x1`; `Ngi32.dll!0x10007470` проверяет маску `0x4` этого аргумента. При
+установленной маске render state 22 получает `D3DCULL_NONE`, иначе —
+`D3DCULL_CW`. Оба значения CShade сохраняют маску `0x4`, поэтому в этом
+цветном пути аппаратное отсечение выключено, а набор обычных граней определяет
+программный тест `d > 0`. Raw-флаги `0x0002` и `0x4000` меняют этот тест, как
+описано выше.
+
+Статический Vulkan preview сохраняет каждую исходную запись `Batch20` одним
+диапазоном рисования:
+`append_static_preview_component_with_lightmaps` добавляет исходные индексы без
+перестановки, а в диапазон переносит `index_count` и исходный `first_index` с
+общим смещением компонента. Поэтому `0x4000` выбирает ту же первую грань, даже
+если диапазон получил другой материал для lightmap.
+
+Поскольку вершины preview переведены в мировые координаты, знак `d` для MSH
+диапазона корректируется знаком детерминанта полной матрицы размещения. Повороты
+узлов и mounts — единичные кватернионы с determinant `+1`; отрицательный object
+scale меняет знак. Renderer Vulkan задаёт `front_face=CLOCKWISE` и viewport с
+положительной высотой. Для используемых матриц legacy и free-flight preview
+при положительном determinant native-visible `d > 0` относится к Vulkan
+back-face; поэтому preview отсекает `FrontFace`. При отрицательном determinant
+режим меняется на `BackFace`.
+Для `0x0002` и `0x4000` аппаратное отсечение выключено, чтобы сохранить их
+программную политику. Отклонённый `0x4000` диапазон получает число индексов 0;
+каждый кадр это число заново вычисляется из исходного значения, поэтому после
+изменения положения камеры или opacity материала диапазон может вернуться в
+draw queue. Проверка первой грани
+применяется только к MSH-диапазонам цветовой мировой очереди; terrain и
+shadow/sun запросы её не используют.
diff --git a/docs/reference/original-binaries.md b/docs/reference/original-binaries.md
index 05c0141..a988638 100644
--- a/docs/reference/original-binaries.md
+++ b/docs/reference/original-binaries.md
@@ -15,11 +15,16 @@ RVA в книге относится к определённой сборке DL
## Сборка GOG
The canonical disassembly source is the windowed GOG installation, not the
-Part 1/Part 2 test installations. Its `Terrain.dll` is a distinct revision:
-
-| Binary | SHA-256 | Size | Notes |
-| --- | --- | ---: | --- |
-| `Terrain.dll` | `af87d1b2e728a0be73c52be3b44cc196ab46da7799f25a15d40f8c9b0b425ead` | 499712 | GOG camera receiver evidence; do not reuse Part 1/2 RVAs without a matching hash |
+Part 1/Part 2 test installations. The rows below identify GOG-specific DLL
+builds. Match the module size and hash before applying one of their RVAs; the
+Part 1/Part 2 builds remain listed separately above.
+
+| Binary | Size (bytes) | SHA-256 |
+| --- | ---: | --- |
+| `AniMesh.dll` | 172032 | `65659bf67f20e5be77d67a5b2f59968f61b94293143807fa59ca451c560c4f7e` |
+| `Effect.dll` | 163840 | `386a3e8db1ce23f61957825d633c7dae1ed40c25cab0cf10a534385fd7cee789` |
+| `iron3d.dll` | 1175552 | `d1dc4ea8535e2069b0a05c9e8be2724dd438ba44cca7a83007d8b2e50f9e35fa` |
+| `Terrain.dll` | 499712 | `af87d1b2e728a0be73c52be3b44cc196ab46da7799f25a15d40f8c9b0b425ead` |
## Различающиеся модули
diff --git a/docs/reference/render-frame.md b/docs/reference/render-frame.md
index 015f9f3..de71603 100644
--- a/docs/reference/render-frame.md
+++ b/docs/reference/render-frame.md
@@ -1,174 +1,190 @@
-# Render frame
+# Кадр отрисовки
-Кадр является последней стадией цикла, а не самостоятельной функцией renderer-а.
-До draw calls уже накоплен input, рассчитан tick, применены отложенные операции,
-выбрана камера и обновлён 3D sound listener.
+Кадр завершает цикл и не является отдельной операцией рендерера. До вызовов
+отрисовки собирается ввод, вычисляется такт, применяются отложенные операции,
+выбирается камера и обновляется слушатель 3D-звука.
-## Frame skeleton
+## Порядок кадра
```text
-system messages and input
- -> simulation calculation
- -> deferred object operations
- -> animation and transforms
- -> camera and sound listener
- -> visibility and render queues
- -> materials and draw passes
- -> renderer completion
- -> end-of-render callbacks and UI
+системные сообщения и ввод
+ -> расчёт симуляции
+ -> отложенные операции над объектами
+ -> анимация и преобразования
+ -> камера и звуковой слушатель
+ -> видимость и очереди отрисовки
+ -> материалы и проходы отрисовки
+ -> завершение рендерера
+ -> обратные вызовы после отрисовки и UI
```
-В `World3D::stdRenderGame` доказан крупный порядок: camera передаётся Terrain,
-настраиваются viewport/matrices, вызываются renderer boundary slots,
-устанавливается `in_render`, выполняется traversal мира, закрывается world/shade
-pass, вызывается renderer completion, снимается `in_render`, рассылается
-end-of-render.
-
-## Draw item
-
-Подготовленный draw item содержит:
-
-- node world matrix;
-- batch flags and index range;
-- WEAR material handle;
-- MAT0 active phase and coefficients;
-- texture handle;
-- optional lightmap handle;
-- render phase and sorting key;
-- legacy pipeline state.
-
-Подготовленный item должен ссылаться на immutable данные кадра. Изменение phase
-или texture cache посреди прохода не должно менять уже собранную очередь.
-
-## MAT0 state and transparent backgrounds
-
-For mission world ranges, the selected MAT0 material supplies its native
-pipeline category. The category sets blend and alpha-test state while preserving
-the range's depth and cull state; an animated material selector updates the
-category with the selector. Queue membership is independent: sampled opacity
-below one or native batch flags `0x108` mark a world range transparent, and a
-queued range with depth enabled tests depth without writing it. Fixed
-environment ranges keep their explicit states.
-
-The fixed-camera preview explicitly marks nebula/stars, dome and cloud ranges as
-transparent backgrounds. Opaque world ranges stay in their world slots; the
-transparent world queue is sorted back-to-front and inserted immediately after
-the last marked background. Other fixed ranges retain their relative order.
-Thus cloud layers can be drawn before transparent foliage, while later sun,
-weather and other foreground ranges keep their existing relative order. This
-is a narrow queue rule, not a reordering of every sky or effects pass.
-
-## Regression evidence
-
-`cargo test -p fparkan-render-vulkan --lib material_category_and_transparency_preserve_world_state_in_either_order --offline`
-checks that category 1 remains source-alpha with alpha testing, retains depth
-and cull settings in either update order, and rejects unsupported categories.
-`cargo test -p fparkan-render-vulkan --lib tree_canopy_queue_follows_sky_and_precedes_foreground_effects --offline`
-checks the explicit queue order: screen gradient, opaque terrain/building,
-shadow, nebula/dome/clouds, far-to-near transparent canopy, then sun/weather.
-
-Viewer-only paired readbacks used `--frames 3 --validation
---legacy-camera-capture` with the pre-fix and patched builds. On AutoDemo TMA
-object 4 (`wor_l_n1.dat`, `fr_l_plant`), the 1280x720 camera at eye
-`[464.319, 904.719, 38.0]`, aimed at `[374.319, 814.719, 12.0]` with FOV
-`0.62`, changed the two black facade panels to tinted, textured portal
-surfaces. On Mission.01 object 12 (`s_tree_04`), the 1280x1024 camera at eye
-`[492.63855, 312.90244, 46.5]` aimed at
-`[492.63855, 442.90244, 55.0]` crosses the canopy against open sky; paired
-three-frame captures at the same synthetic atmosphere phase (600 seconds) show
-the patched build restoring the missing upper/right fronds along that sky
-boundary. Both runs reported clean Vulkan validation. These captures verify our
-preview behavior and do not prove the native renderer's pass order, portal
-transmittance or exact color.
-
-## GOG camera dispatch evidence
-
-For the GOG `World3D.dll` baseline with SHA-256
+По анализу вызовов установлен общий порядок в `World3D::stdRenderGame`: камера
+передаётся в Terrain, задаются `viewport` и матрицы, вызываются граничные слоты
+рендерера, выставляется `in_render`, обходится мир, завершаются проходы мира и
+затенения, вызывается завершение рендерера, снимается `in_render`, затем
+рассылается `end-of-render`.
+
+## Элемент отрисовки
+
+Подготовленный элемент отрисовки содержит:
+
+- мировую матрицу узла;
+- флаги пакета `batch` и диапазон индексов;
+- дескриптор материала WEAR;
+- активную фазу и коэффициенты MAT0;
+- дескриптор текстуры;
+- необязательный дескриптор карты освещения;
+- фазу отрисовки и ключ сортировки;
+- состояние конвейера устаревшей отрисовки.
+
+Подготовленный элемент должен ссылаться на неизменяемые данные кадра. Изменение
+фазы или кэша текстур посреди прохода не должно менять уже собранную очередь.
+
+## Состояние MAT0 и прозрачные фоны
+
+Для диапазонов мира миссии выбранный материал MAT0 задаёт нативную категорию
+конвейера. Категория определяет смешивание и альфа-тест, сохраняя настройки
+проверки глубины и отсечения граней. Если селектор материала анимируется,
+вместе с ним обновляется и категория.
+Принадлежность к очереди определяется отдельно: прозрачным считается диапазон
+мира с выбранной прозрачностью меньше единицы или с нативными флагами пакета
+`batch` `0x108`. Такой диапазон при включённой проверке глубины не записывает
+глубину. За фиксированными диапазонами окружения сохраняются их явные состояния.
+
+В превью с фиксированной камерой диапазоны туманности и звёзд, купола и облаков
+явно помечаются как прозрачный фон. Непрозрачные диапазоны мира остаются на
+своих местах; прозрачная очередь мира сортируется от дальних элементов к
+ближним и вставляется сразу после последнего помеченного фона. Порядок остальных
+фиксированных диапазонов сохраняется. Поэтому облака могут отрисовываться перед
+прозрачной листвой, а расположенные позднее солнце, погода и другие передние
+эффекты сохраняют свой относительный порядок. Это локальное правило очереди, а
+не перестановка всех проходов неба или эффектов.
+
+## Проверки
+
+`cargo test --locked --offline -p fparkan-render-vulkan --lib material_category_and_transparency_preserve_world_state_in_either_order`
+Проверяет, что категория 1 сохраняет смешивание `source-alpha` и альфа-тест,
+сохраняет настройки проверки глубины и отсечения граней при любом порядке
+обновления и отклоняет неподдерживаемые категории.
+`cargo test --locked --offline -p fparkan-render-vulkan --lib tree_canopy_queue_follows_sky_and_precedes_foreground_effects`
+Проверяет заданный порядок очереди: градиент экрана, непрозрачная местность и
+здание, тень, туманность, купол и облака, прозрачная крона от дальних элементов
+к ближним, затем солнце и погода.
+
+## Свидетельства диспетчеризации камеры GOG
+
+Для версии GOG `World3D.dll` с SHA-256
`17e4a3089b2583a8cf2356c9db0390b1aba138356a09130d79b4e7e4791da61e`,
-the exported `stdRenderGame` is RVA `0x13BD0`. It first calls
-`Terrain::stdSetCurrentCamera2(camera)`, stores the camera pointer only for
-the frame, and clears it before return. `sendEndOfRender` is a separate export
-at RVA `0x13D90`. This is frame-order evidence only: the camera ABI, matrices
-and viewport values are not recovered by this call-order analysis. The
-implemented preview path below accepts selector-0 input produced through the
-[native frame capture workflow](../../tools/native-frame-capture/README.md).
-The capture workflow also records an observed mission path and, when
-its thread/generation and clock checks succeed, an `atmosphere_seconds` sample.
-For a fixed-camera preview, the renderer uses that optional absolute schedule
-phase when no `--atmosphere-seconds` override is supplied and keeps schedule
-sampling at that exact phase across readback frames. An explicit CLI value takes
-precedence. The phase sample is not full simulation time: weather, RNG, and
-other mission state remain unknown, so this evidence does not establish
-full-frame parity.
-
-Receiver-side GOG Terrain evidence refines this contract. `stdSetCurrentCamera2`
-(RVA `0x4FD40` in Terrain SHA-256
-`af87d1b2e728a0be73c52be3b44cc196ab46da7799f25a15d40f8c9b0b425ead`) queries
-selector `18` on the supplied object and invokes slot `+12` on that result. It
-does not store the supplied pointer in `stdGetCurrentCamera2`; that getter
-returns a Terrain global populated by selector `8` during Terrain initialization.
-
-`Terrain.dll!LoadCamera` adds a separate factory-side contract. Its GOG entry
-at RVA `0x4EBE0` is `__stdcall` with four machine-word arguments, allocates
-`0x1A4` bytes, calls the constructor at RVA `0x4EC60`, and returns the interface
-at object offset `+0x134`. The constructor passes arguments 3 and 4 into base
-initialization at `this + 4`, writes argument 3 to `this + 0x138`, and calls a
-helper at RVA `0x4CEF0` with arguments 1, 2 and 4 plus `this`. Argument 1 is a
-mode string: only `REFLECTION` and `REFLECTION_SHIFTED` produce a non-zero mode
-value at `this + 0x1A0`. This is factory ABI evidence, not a recovered view or
-projection matrix contract.
-
-## Legacy camera preview path
-
-`fparkan-game --legacy-camera-capture <json>` consumes the selector-0 transform,
-the native D3D7 viewport RECT `[left, top, right, bottom]`, near/far planes and
-field of view. It derives the D3D7 view and clip transform from those inputs,
-carries the resulting `clip_from_world` matrix into the preview camera, and
-keeps the view-matrix camera axes directly, so the captured basis, including
-roll, is not reduced to yaw/pitch. The RECT becomes a physical drawable extent
-of `right - left` by `bottom - top`. The preview window is fixed to that extent;
-startup and non-zero resize events reject a physical-size mismatch instead of
-stretching the captured view. Compare the result with the corresponding
-`[left, top, right, bottom]` crop of the original PNG. `FreeFlightCamera` is not
-created in this mode, but redraws still update environment and projected
-shadows from the captured camera. Before environment updates can emit sound
-events, the audio listener receives the captured position, forward and up axes;
-initial world draw sorting uses the same captured position. A phase capture may
-include an observed mission path and a verified `atmosphere_seconds` value; the
-latter is used only when no explicit CLI override is supplied. This value pins
-the atmosphere schedule sample during a fixed-camera comparison while FX still
-receives real frame time. `simulation_time_seconds`, weather, RNG and complete
-mission state remain unknown, and a camera/phase match alone does not establish
-visual parity with a native frame.
-
-The preview's sky heading is `atan2(forward.x, forward.y)`, matching the second
-output calculated at `Terrain.dll` RVA `0x4F9C0`; `CSky` adds π at RVA `0x481C5`
-when selecting screen-gradient sectors. This mapping is applied only at the sky
-boundary and does not change `FreeFlightCamera` yaw or movement controls.
-Matched same-camera, same-phase three-frame captures at phases 56.227 and
-998.893 showed zero changed pixels before and after this correction; the
-remaining sky and color mismatch is unresolved, so this source-level match does
-not establish visual parity.
-
-## Parity risks
-
-`VulkanStaticCamera::from_legacy_d3d7` keeps the D3D7 view and projection
-reconstruction in `fparkan-render`, then negates the vertical projection term
-once at the Vulkan adapter boundary. D3D7's top-down viewport places positive
-NDC Y at the top; the Vulkan swapchain uses a positive viewport height, so the
-adapter flips NDC Y while the shader preserves `gl_Position`. The generic
-`from_row_major_view_projection` path remains unchanged; its callers supply
-Vulkan-ready matrices, as the free-flight camera already does.
-
-- x87 precision and rounding;
-- scalar/SIMD `g_FastProc` differences;
-- object, batch and transparent primitive order;
-- depth, cull, alpha test and blend transitions;
-- mip-skip, palette and Page coordinates;
-- material fallback and phase selection;
-- RNG sequence for FX and atmosphere;
-- device capability fallback;
-- simulation time quantization.
-
-Для отладки нужен deterministic frame capture: camera state, visible object IDs,
-draw-item list, pipeline keys, matrices и hashes промежуточных buffers.
+экспорт `stdRenderGame` находится по RVA `0x13BD0`. Вначале он вызывает
+`Terrain::stdSetCurrentCamera2(camera)`, сохраняет указатель камеры на время
+кадра и очищает его перед возвратом. `sendEndOfRender` — отдельный экспорт по
+RVA `0x13D90`. Эти данные подтверждают только порядок вызовов в кадре: анализ
+не восстанавливает ABI камеры, матрицы или значения `viewport`. Реализованный
+путь превью принимает входные данные с матрицей `selector0_words`, полученные через
+[процедуру захвата нативного кадра](../../tools/native-frame-capture/README.md).
+
+Процедура захвата записывает наблюдавшийся путь миссии и значение
+`atmosphere_seconds`, если проверка потока, поколения и часов прошла успешно.
+В превью с фиксированной камерой эта абсолютная фаза расписания используется,
+если не задан параметр `--atmosphere-seconds`; на всех кадрах чтения сохраняется
+та же фаза. Явный параметр командной строки имеет приоритет. Эта фаза не равна
+времени полной симуляции: погода, RNG и другие состояния миссии остаются
+неизвестны, поэтому она не подтверждает полного совпадения кадра.
+
+Анализ принимающей стороны уточняет этот контракт. `stdSetCurrentCamera2`
+(RVA `0x4FD40` в Terrain с SHA-256
+`af87d1b2e728a0be73c52be3b44cc196ab46da7799f25a15d40f8c9b0b425ead`)
+запрашивает селектор `18` у переданного объекта и вызывает слот `+12` полученного
+интерфейса. Указатель не сохраняется в `stdGetCurrentCamera2`: этот геттер
+возвращает глобальное значение Terrain, установленное селектором `8` при
+инициализации Terrain.
+
+`Terrain.dll!LoadCamera` задаёт отдельный контракт фабрики. Точка входа GOG по
+RVA `0x4EBE0` использует соглашение о вызовах `__stdcall` и принимает четыре
+машинных слова, выделяет `0x1A4` байта, вызывает конструктор по RVA `0x4EC60` и
+возвращает интерфейс по смещению объекта `+0x134`. Конструктор передаёт
+аргументы 3 и 4 в инициализацию базового объекта по `this + 4`, записывает
+аргумент 3 в `this + 0x138` и вызывает вспомогательную функцию по RVA `0x4CEF0`,
+передавая ей аргументы 1, 2 и 4 вместе с `this`. Аргумент 1 — строка режима:
+ненулевое значение по `this + 0x1A0` дают только `REFLECTION` и
+`REFLECTION_SHIFTED`. Это свидетельство ABI фабрики, а не восстановленный
+контракт матрицы вида или проекции.
+
+## Предпросмотр по захвату камеры D3D7
+
+`fparkan-game --legacy-camera-capture <json>` принимает матрицу камеры
+`selector0_words`, прямоугольник области отображения `viewport` D3D7
+`[left, top, right, bottom]`, ближнюю и дальнюю плоскости и угол поля зрения.
+По этим данным превью строит матрицы вида и проекции D3D7, передаёт полученную
+матрицу `clip_from_world` камере превью и напрямую использует оси камеры из
+матрицы вида. Поэтому базис захвата, включая крен, не сводится к рысканию и
+тангажу. Прямоугольник задаёт физический размер области отрисовки:
+`right - left` на `bottom - top`. Размер окна превью закреплён: если при
+запуске или после ненулевого изменения окна физический размер не совпадает с
+заданным, приложение завершает работу, а не растягивает изображение.
+Сравнивайте результат с фрагментом исходного PNG по прямоугольнику
+`[left, top, right, bottom]`. В этом режиме `FreeFlightCamera` не создаётся,
+но при повторной отрисовке обновляются окружение и проецируемые тени для
+захваченной камеры. До обновления окружения, которое может создавать звуковые
+события, слушателю задаются захваченные позиция, направление вперёд и верхняя
+ось; начальная сортировка диапазонов мира использует ту же позицию.
+
+Захват может содержать наблюдавшийся путь миссии и проверенное значение
+`atmosphere_seconds`; последнее используется только при отсутствии явного
+параметра командной строки. Это значение фиксирует фазу расписания атмосферы
+при сравнении с неподвижной камерой, тогда как FX продолжает получать реальное
+время кадра. `simulation_time_seconds`, погода, RNG и полное состояние миссии
+остаются неизвестными, поэтому совпадение камеры и фазы не подтверждает
+визуального совпадения с нативным кадром.
+
+Для неба превью вычисляет направление как `atan2(forward.x, forward.y)`. Оно
+совпадает со вторым выходным значением, вычисляемым в `Terrain.dll` по RVA
+`0x4F9C0`; при выборе сектора экранного градиента `CSky` добавляет π по RVA
+`0x481C5`. Это преобразование применяется только на границе неба и не меняет
+рыскание или управление движением `FreeFlightCamera`.
+
+## Камера свободного полёта
+
+Начальный профиль использует `near = 0.5`, `far = 700` и горизонтальный FOV
+`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 и матрица
+камеры применяются при следующей отрисовке.
+
+Проекция направления не сравнивает косинус угла с ближней дистанцией: она
+отбрасывает только лучи за камерой и почти параллельные плоскости изображения
+(`forward · direction <= 1e-4`). Лучи в передней полусфере могут проецироваться
+за край viewport; владелец спрайта отдельно решает, нужно ли его рисовать.
+
+## Ограничения точного совпадения
+
+`VulkanStaticCamera::from_legacy_d3d7` выполняет восстановление вида и проекции
+D3D7 в `fparkan-render`, а затем один раз меняет знак вертикального члена
+проекции на границе адаптера Vulkan. В области отображения D3D7 положительная
+координата NDC Y направлена вверх экрана; Vulkan использует положительную высоту
+области отображения, поэтому адаптер меняет знак NDC Y, а шейдер сохраняет
+`gl_Position`. Общий путь
+`from_row_major_view_projection` не меняется: вызывающие его функции передают
+матрицы, уже подготовленные для Vulkan, как и камера свободного полёта.
+
+- точность и округление x87;
+- различия scalar/SIMD в `g_FastProc`;
+- порядок объектов, пакетов и прозрачных примитивов;
+- переходы состояний проверки глубины, отсечения граней, альфа-теста и смешивания;
+- пропуск mip-уровней, палитра и координаты Page;
+- запасной материал и выбор фазы;
+- последовательность RNG для FX и атмосферы;
+- переход на запасной набор возможностей устройства;
+- квантование времени симуляции.
+
+Для отладки требуется детерминированный захват кадра: состояние камеры,
+идентификаторы видимых объектов, список элементов отрисовки, ключи конвейера,
+матрицы и хэши промежуточных буферов.