diff options
Diffstat (limited to 'docs/tomes/05-render.md')
| -rw-r--r-- | docs/tomes/05-render.md | 1235 |
1 files changed, 121 insertions, 1114 deletions
diff --git a/docs/tomes/05-render.md b/docs/tomes/05-render.md index e63c3ae..71cad1d 100644 --- a/docs/tomes/05-render.md +++ b/docs/tomes/05-render.md @@ -86,10 +86,8 @@ drivers и video modes, проверяет поддержку 3D, перевод `IDirect3D` 1/2/3/7 и соответствующих `IDirect3DDevice`. Следовательно, современная реализация заменяет наблюдаемую fixed-function семантику над этой границей, а не должна эмулировать COM-объекты DirectX. Равный SHA-256 Ngi32 в -Частях 1 и 2 зафиксирован в -[evidence](../evidence/original_engine_hashes.md); это поддерживает единый -Vulkan backend contract, но не отменяет раздельных corpus/capture baselines -для изменённых assets и gameplay DLL. +Частях 1 и 2 зафиксирован в [таблице оригинальных модулей](../reference/original-binaries.md); +это поддерживает единый контракт Vulkan backend. ```text enumerate adapters and video modes @@ -361,9 +359,16 @@ local.set_translation(pose.position); world[n] = world[parent(n)] * local; ``` -Для parity особенно важны x87-compatible округление при выборе frame index и -порядок операций. Одинаковая формула на SSE может выбрать соседний кадр возле -границы. +Статический Vulkan preview берёт начальный кадр узла из явной ссылки прототипа +на CTLD `.ctl`. Используются только включённые строки, а при повторе node ID +поздняя строка заменяет раннюю. Без `--static-animation-frame` preview применяет +эти настройки узлов; узлы без настройки остаются на кадре `0`. Явный +`--static-animation-frame N` переопределяет их и задаёт `N` всем узлам. +Результат — одна статическая поза, не обновление анимации, Control или AI. +MSH sampler выбирает type 19 frame округлением `(time - 0.5)` к ближайшему +целому с ties-to-even, затем интерполирует выбранный type 8 key и следующий +key в исходном `time`; это portable-вариант native x87 conversion в default +rounding mode, без обещания совпадения при любом x87 control word. Проверки animation data: @@ -613,11 +618,20 @@ Lightmap не является обычной diffuse texture. WEAR содерж diffuse texture ломает LOD, atlas coordinates и динамическую модуляцию. Тени проходят отдельным render pass. Terrain содержит пути для теней зданий и -роботов, ограничения максимального числа, detail level и smoothing. Доказаны -shadow manager/pass, настройки detail/smoothing/count и зависимость от -Terrain/CShade; полная формула projection geometry для каждого caster требует -dynamic trace. Unknown settings из `shade.cfg` читаются и сохраняются по -именам, а не заменяются произвольными modern defaults. +роботов, ограничения максимального числа, detail level и smoothing. Реализованы +native visibility по шести Vulkan clip-плоскостям с радиусом sphere +100, +порядок кандидатов и лимит 20 до построения страниц, projected-size LOD, +directional/point light contribution, native page layout, raster, smoothing и +receiver projection с edge clipping. Unknown settings из `shade.cfg` читаются и +сохраняются по именам, а не заменяются произвольными modern defaults. Для native ray query +surface descriptor в `+0x10` требует наличия всех переданных bits, а descriptor +в `+0x14` исключает поверхности с любым переданным bit. Общий shadow query +использует full exclusion mask `0x2000`; actor four-ray path получает compact +mask `0x8008`, который landscape boundary разворачивает в full mask +`0x00200020`. Sun path передаёт full `0x20`, кодирует его как compact bit `8` +и затем получает тот же full bit после разворачивания. Hit record хранит в +`+0x20` квадрат евклидовой длины луча, поэтому native four-ray fade использует +`distance² * 1e-4`, а не линейную дальность. Atmosphere manager создаёт world objects для фоновых и погодных явлений. Отдельно подтверждены lightning, sun render, flare, `env_lightning`, rain @@ -626,6 +640,9 @@ background sound и обязательные ссылки на lightning effect. требует screen position и occlusion test, rain -- области рядом с observer, sound -- listener. Их нельзя один раз запечь в terrain. +Sun occlusion начинается от точки `camera + 0.5 * normalize(sun - camera)`; +этот offset является частью native query, а не произвольным bias renderer-а. + RNG для lightning, atmosphere phases и FX должен иметь стабильный порядок. Даже правильный средний интервал не даёт повторяемый кадр, если random values запрашиваются в другой последовательности. @@ -679,11 +696,14 @@ settings ID сохраняются. ```text opcode = command_word & 0xFF -enabled = (command_word >> 8) & 1 +native_flag = (command_word >> 8) & 1 ``` -Bits 9-31 являются частью данных и сохраняются. Между командами нет -выравнивания. Размер команды, включая word: +`native_flag` передаётся созданному native command object как режим/feature +flag и не отключает команду. Например, реальный `env_lightning` использует +`native_flag == 0` для opcode 3, opcode 1 и opcode 2, и все три команды +исполняются. Bits 9-31 являются частью данных и сохраняются. Между командами +нет выравнивания. Размер команды, включая word: ```text opcode 1 224 байта @@ -842,1107 +862,94 @@ draw_indexed(item.batch.base_vertex, item.batch.index_count); ``` -## Текущий контракт pipeline key - -`fparkan-render` теперь переносит вместе с каждым backend-neutral draw -`LegacyPipelineState` и детерминированный `PipelineKey`. Ключ не хешируется: -он явно упаковывает blend mode, depth mode, cull mode и флаг alpha-test, поэтому -одинаковые snapshots дают одинаковый capture и будущий Vulkan cache не зависит -от версии Rust или процесса. Alpha reference намеренно не входит в key: это -dynamic material constant, а не структурный вариант graphics pipeline. - -Текущий static Vulkan viewer дедуплицирует состояния ranges при каждой сборке -swapchain resources, создаёт один `vk::Pipeline` на уникальный `PipelineKey` и -выбирает его непосредственно перед соответствующим `vkCmdDrawIndexed`. -Кэш корректно уничтожается при recreate/teardown; setup failure откатывает уже -созданные variants. Baseline оригинального MSH подтверждён с состоянием -`opaque + depth disabled + cull disabled + alpha-test disabled`. - -`SourceAlpha`, front/back culling и depth modes имеют Vulkan mapping в этом -кэше. При создании swapchain resources renderer выбирает capability-approved -depth/stencil format, выделяет device-local attachment, очищает его в render -pass и прикрепляет к каждому framebuffer. `TestWrite` включает depth test и -write; `TestReadOnly` включает test без write. Alpha test выполняется во -fragment shader: перед каждым draw renderer передаёт через push constant -`alpha_test_reference / 255`, а shader отбрасывает fragment с меньшей alpha. -При выключенном `alpha_test` cutoff принудительно равен нулю; reference остаётся -dynamic material data и не входит в `PipelineKey`. Также не установлен источник -значений для Batch20/MAT0: их -поля нельзя объявлять blend/depth/cull mapping без dynamic capture или -дополнительного дизассемблирования. Это частично реализованная compatibility -boundary, а не заявление о готовой parity fixed-function state. - -### Capability-gated swapchain source for future pixel capture - -Pixel parity требует чтения результата GPU, но swapchain image разрешает это -только если surface сообщает `VK_IMAGE_USAGE_TRANSFER_SRC_BIT`. Поэтому -swapchain policy всегда запрашивает обязательный `COLOR_ATTACHMENT`, а -`TRANSFER_SRC` добавляет лишь при фактической поддержке surface; выбранные raw -usage bits попадают в deterministic plan и native smoke report. Это не создаёт -readback buffer, не копирует pixels и не закрывает pixel-capture acceptance. - -На Windows GOG native smoke 2026-07-18 (`MTCHECK.MSH` из `system.rlb` и -`DEFAULT.0` из `Textures.lib`) AMD Radeon Pro WX 3200 Series подтвердил -`image_usage=17` (`COLOR_ATTACHMENT | TRANSFER_SRC`): 300 frames, 3 resize -events, 2 swapchain recreations, Vulkan validation warnings/errors `0/0`. -Следующий шаг должен выполнить явный image-to-buffer copy и сравнить полученные -bytes с зафиксированным reference capture. - -### First synchronized Vulkan pixel-readback artifact - -The current artifact contract is one final swapchain image, not a -concatenation of every swapchain allocation. After the final successful -graphics submission, the renderer retains that image index; synchronized -teardown reads only its matching buffer. The raw payload is therefore exactly -`width * height * 4` bytes for the current format-50 path. Historical -multi-image byte counts below describe the superseded concatenation behavior -and are not comparable to this final-image contract. - -Stage 3 static viewer теперь выполняет фактический readback для surface с -`TRANSFER_SRC`: на каждый swapchain image создаётся host-visible coherent -`TRANSFER_DST` buffer. После render pass command buffer переводит image из -`PRESENT_SRC_KHR` в `TRANSFER_SRC_OPTIMAL`, выполняет `vkCmdCopyImageToBuffer` -и возвращает image в `PRESENT_SRC_KHR`. CPU отображает memory только после -`vkDeviceWaitIdle` during shutdown; это исключает чтение GPU work in flight. -Smoke JSON фиксирует число записанных copy-команд, final byte count и FNV-1a-64 -hash всех current-swapchain readback buffers, не сохраняя игровые pixels в -репозитории. - -На GOG `MTCHECK.MSH`/`DEFAULT.0` AMD Radeon Pro WX 3200 Series выполнил 300 -copy-команд, final artifact 4,147,200 bytes и hash `2184179010340020629` при -validation warnings/errors `0/0`. Повторный идентичный запуск дал тот же -размер и hash. Это доказывает Vulkan copy/readback path только для нашего -static viewer. Он ещё не захватывает original DirectDraw frame, не задаёт -fixed original camera и не сравнивает два изображения, поэтому pixel-parity -acceptance остаётся blocked. - -The following paragraph records the superseded multi-image export only as -historical bootstrap evidence; it is not the current artifact contract. - -Smoke также сохранял raw artifact рядом с JSON: `<report-stem>.readback-vkformat-<raw>.raw`. -Это были concatenated current-swapchain images in Vulkan order, каждый с dimensions -из JSON и четырьмя bytes per pixel; format намеренно указан в имени файла, -потому что bytes не перекодируются; JSON содержит actual raw enum. GOG selected `50` and produced a 4,147,200-byte file -for two 960x540 images. Артефакт остаётся локальным output и не попадает в Git. - -Smoke option `--expected-readback <path>` выполняет exact byte comparison после -readback и завершает run с первым differing byte или length mismatch. Fresh GOG -run с предыдущим format-50 artifact прошёл этот gate. Это regression contract -нашего static Vulkan viewer, не comparison с оригинальным renderer. -Comparator прежде сравнивает `vkformat` identity из имен обоих artifacts и -отклоняет mismatch до byte scan; raw blobs разных Vulkan formats нельзя -считать сопоставимыми без явной conversion contract. - -`fparkan-game` render-snapshot bridge больше не берёт только первый prepared -visual mission object. Каждый `MissionAssets::visuals_for_object` entry -создаёт отдельный backend-neutral draw с own mesh/material IDs; object без -prepared visual сохраняет один compatibility fallback draw. Это сохраняет -multi-component prototype graph для будущего real Vulkan renderer, однако -текущий game app всё ещё использует planning backend и triangle ranges. - -### Mission position/scale render bridge - -`fparkan-game` now maps preserved TMA position and non-uniform scale into each -backend-neutral draw matrix instead of diagnostic index placement. Raw -orientation deliberately remains uninterpreted: the original Euler order and -matrix convention are not yet proven. This preserves authored values without -inventing transform math; the app still uses planning backend and triangle -ranges, so it is not a full renderer claim. - -### Mission original-ID render provenance - -Each backend-neutral draw now retains the `OriginalObjectId` preserved by the -TMA loader for its source mission object. This gives capture and diagnostics a -stable link from a draw back to its original object record, including every -visual component emitted for that object. The bridge deliberately leaves the -value absent when no mission draft exists; it does not infer an original ID -from a runtime slot or draw order. - -### Opt-in mission static-Vulkan bridge - -`fparkan-game --backend static-vulkan` is an explicit native-window experiment, -not the default planning path. It visits only the first mission root, selects -every prepared MSH component of that root, merges their static XY clip-space -geometry, and renders a requested number of frames through -`VulkanSmokeRenderer`; teardown rejects validation warnings/errors and reports -swapchain/readback telemetry. For each used `Batch20.material_index`, it resolves -the positional prepared WEAR material and uploads mip 0 of that material's first -MAT0 diffuse texture request. Because `material_index` is local to an MSH/WEAR -component, the merged preview assigns a unique preview-local selector only after -this source resolution; it does not falsely treat equal local indexes as equal -materials. - -This is a narrow bootstrap from mission assets to a live Vulkan renderer. It -does not render every placed object, apply TMA transforms or orientation, select -later MAT0 phases or animation, bind lightmaps, or establish a game camera. Fresh GOG -`MISSIONS/Autodemo.00/data.tma` evidence now proves the narrow GPU bridge: one -presented frame completed in 39.6 seconds with a native 1280×720 two-image -swapchain, 14 merged mesh components and 14 selector-keyed original diffuse -descriptors, 7,372,800-byte synchronized readback (FNV-1a -`16595193636416981301`) and validation -warnings/errors `0/0`. This is not a -full-scene or original-renderer pixel-parity claim. - -The same bounded command was then run against the licensed installed Part 1 -(`C:\\Program Files (x86)\\Nikita\\IS`) and Part 2 -(`C:\\Program Files (x86)\\Nikita\\IS2`) corpora supplied for testing. Both -`MISSIONS/Autodemo.00/data.tma` runs completed with 14 mesh components and 14 -original diffuse descriptors. Part 1 took 28.2 seconds and matched the GOG -readback hash `16595193636416981301`; Part 2 took 93.6 seconds and remained -validation-clean but produced distinct hash `18268338333658342130`. Part 2's -synchronous checkpoint was `Graph` while it was still loading, so the longer -startup is not attributed to Vulkan. This is only a cross-corpus confirmation -of the first-root static-preview bridge, not a claim that the two games' full -mission renderers are compatible. - -The preview now has an explicit root-prefix contract. `--preview-roots N` -prepares the first non-zero `N` TMA roots for the opt-in static Vulkan path. -Without that diagnostic limiter, the static Vulkan path now requests every TMA -root (the runtime clamps the request to the decoded mission length), so a -normal native preview no longer silently displays just root zero. Normal mission -loading remains full and transactional; the prefix is retained for bounded -investigation rather than as a hidden relaxation of gameplay validation. - -### Shared terrain/root diagnostic frame - -The bounded native preview now retains the already validated `Land.msh` inside -`TerrainWorld`; this lets the application consume source geometry through the -runtime boundary instead of decoding archive formats a second time. It merges -one terrain component with the first TMA root's 14 reachable MSH components in -one explicit top-down XY frame. The frame is computed from terrain positions -and the root models after applying decoded TMA placement: non-uniform `scale`, -then the recovered `Rz * Ry * Rx` orientation, then `position`. The XY camera -itself remains diagnostic; applying the established placement transform here -prevents differently oriented mission objects from being framed or rasterized -at a false location. - -Terrain is assigned an explicit 1x1 white diagnostic texture, rather than a -guessed terrain material. `Land.msh` slot selection, `material_tag`, masks, -auxiliary streams, original terrain phases and camera remain unresolved. Thus -this is an integrated geometry/provenance checkpoint, not a claim of original -terrain shading or scene camera reconstruction. - -Fresh canonical GOG `MISSIONS/Autodemo.00/data.tma` evidence: one native -1280x720 Vulkan frame completed in 40.6 seconds with 14 root MSH components, -one terrain component, 15 descriptors, a 7,372,800-byte readback (FNV-1a -`10739087367165646439`) and validation warnings/errors `0/0`. The distinct hash -from the earlier first-root-only preview is expected because the submitted -geometry and descriptor set changed; it is not an original-frame comparison. - -### Multiple static-preview roots - -The native bridge now combines every mesh-backed visual of each selected root, -using that root's recovered static TMA placement (`Rz * Ry * Rx` after scale) -in the shared diagnostic XY frame. It reports the actual selected root count as -`preview_roots`, rather than implying that all mission objects are rendered. -The original camera/frustum, dynamic orientation ownership and visibility -remain unresolved. - -Fresh canonical GOG `Autodemo.00` evidence with `--preview-roots 2` completed -in 59.7 seconds: 25 submitted MSH components, one terrain component and 26 -descriptors on the native 1280x720 two-image swapchain, with validation -warnings/errors `0/0`. The readback FNV-1a remained -`10739087367165646439`, equal to the one-root terrain/root run. That equality -does **not** prove the second root is visible or equivalent: it was recorded as -an investigation target, not hidden as a parity result. - -The reproducible `fparkan-cli mission inspect` probe closes two tempting but -incorrect explanations for that equality. GOG `Autodemo.00` root 0 is -`w_m_wlk2.dat` at `(418.10318, 717.433, 3.0409389)` while root 1 is -`w_s_wlk1.dat` at `(479.12396, 795.95337, 1.6228507)`; therefore the TMA data -is neither duplicate nor co-located. Separately, the Vulkan static submit loop -calls `cmd_draw_indexed` for every prepared draw range and its depth comparison -is `LESS_OR_EQUAL`. A later direct `Land.msh` bounds probe identified the actual -static-viewer defect: GOG `AutoMAP/Land.msh` spans X/Y -`0..1190.6976` but Z only `0..94.50981`; the same TMA objects use X/Y for their -world placement and a small Z height. The old XZ CPU frame discarded the second -horizontal TMA component and treated height as the screen axis. The renderer now -uses a shared XY CPU frame and retains Z as geometry height. This proves only -the source-axis convention needed by the diagnostic bridge; it does not recover -the original camera, orientation order, culling or projection matrix. - -The new `fparkan-cli terrain inspect <Land.msh>` exposes these three-axis source -bounds without a renderer. The first XY GPU rechecks were intentionally bounded -to 120 seconds but remained at the existing `Graph` loading checkpoint and were -terminated without a native frame/report; therefore no new readback hash or GPU -acceptance result is claimed for the corrected projection yet. - -The load trace now splits base root expansion (`Graph`) from MSH/WEAR/MAT0/TEXM -visual-dependency expansion (`GraphVisuals`) and enters `Assets` immediately -before actual asset preparation. A controlled 60-second GOG first-root run of -the XY build stopped at `GraphVisuals`; the process was terminated by the probe -and produced no frame. The startup bottleneck is therefore narrowed to visual -graph expansion, rather than the base prototype graph, asset decoding, window -creation or Vulkan submission. This is a diagnostic boundary, not a claim that -the visual expansion is semantically optional or may be skipped. - -Visual expansion now caches the validation result of each unique diffuse TEXM -and each unique lightmap TEXM independently. It still emits an edge and request -count for every original material reference, and it replays the same cached -success or failure at every reference; only duplicate archive open/read/decode -work is removed. Under the same controlled 60-second GOG first-root probe, the -last checkpoint advanced to `Assets`. The process was deliberately terminated -before a frame, so this proves reduced visual-graph work rather than a Vulkan -acceptance result. - -Asset preparation additionally caches validated MSH models and decoded WEAR -tables by their complete resource key. This preserves separate prepared visual -provenance while avoiding repeat format work for repeated components. The same -60-second first-root GOG probe remained at `Assets`, so that corpus does not -demonstrate an additional checkpoint advance from this cache; it must not be -reported as a measured startup improvement. - -Visual-graph material resolution now also caches the complete success or failure -of each WEAR material request. Every material edge and failure remains in the -graph; the cache only avoids reopening and decoding an already resolved MAT0. -Under the controlled 60-second GOG first-root probe, the phase sequence advanced -from `GraphVisuals` to `AssetModelMeshes`, `AssetWearTables`, then -`AssetTextures` before termination. This is a bounded timing observation, not a -claim that a frame completed or that every run has identical timing. - -After these visual-graph caches, the corrected XY static-Vulkan preview again -completed on canonical GOG `MISSIONS/Autodemo.00/data.tma` within a controlled -120-second run. It reported `Complete`, a native 1280x720 two-image swapchain, -one frame, 14 root MSH components, one terrain component, 15 material -descriptors, validation warnings/errors `0/0`, and a 7,372,800-byte readback -FNV-1a `6451493914305554398`. This is the first GPU acceptance evidence for -the corrected XY diagnostic projection; the hash is not compared with the -original renderer and does not establish original camera, shading or parity. - -The identical bounded XY command also completed against the supplied licensed -Part 1 installation (`C:\\Program Files (x86)\\Nikita\\IS`). Its report had -the same 14 root MSH components, terrain component, 15 descriptors, native -1280x720 two-image swapchain, validation `0/0`, and FNV-1a -`6451493914305554398`. This confirms the current first-root diagnostic bridge -across GOG and Part 1 data; it does not claim full mission or Part 2 parity. - -The corresponding Part 2 run (`C:\\Program Files (x86)\\Nikita\\IS2`) did -not reach asset preparation or a Vulkan frame within a controlled 180-second -window: its final persisted checkpoint was `GraphVisuals`, and the exact child -process was terminated. This gives no Part 2 GPU acceptance or pixel result and -does not attribute the delay to Vulkan; its visual-dependency corpus remains a -separate profiling and compatibility target. - -`GraphVisuals` is now split observationally into `GraphVisualWears`, -`GraphVisualMaterials` and `GraphVisualTextures`; it preserves the existing -graph traversal, edge order and validation semantics. Repeating the controlled -180-second Part 2 probe once ended at `GraphVisualTextures`. That particular -execution had passed WEAR and MAT0 expansion before timeout, but a checkpoint -is only the last phase reached in one run: it is not proof of a single global -bottleneck or of Vulkan involvement. - -Visual expansion now reports each dependency-class entrance, its first request -and every 64th later request; runtime progress persists MAT0/TEXM counts at -those boundaries. This is diagnostic-only and does not alter traversal, edges -or validation. A -later controlled 180-second Part 2 probe ended at `GraphVisualMaterials`, -before the first TEXM progress marker. The differing checkpoints rule out the -previous overly narrow claim that TEXM is the sole remaining target. Profiling -must compare WEAR, MAT0, TEXM, graph allocation and archive I/O quantitatively. - -Visual expansion now snapshots each prototype's base-graph `Prototype→MSH` -anchor before it appends any WEAR/MAT0/TEXM nodes and edges. The subsequent -traversal reads this immutable vector rather than repeatedly linearly searching -the growing graph; graph content, node/edge IDs and provenance remain -unchanged. The same controlled 180-second Part 2 probe still ended at -`GraphVisualMaterials`, so this is a complexity/correctness improvement rather -than measured evidence of a startup checkpoint advance. - -The MAT0 counter now records `GraphVisualMaterialRequests(N)` with the same -64-request throttle. A fresh controlled Part 2 probe ended at -`GraphVisualMaterialRequests(71)`: at least 71 MAT0 requests had begun before -termination. It does not preserve the simultaneous TEXM count, so it neither -identifies a slow individual material nor attributes time to MAT0 parsing; the -next profiler revision needs one cumulative snapshot of all classes. - -Visual MAT0/TEXM request markers now also carry the graph node and edge counts -materialized immediately before that request resolves. A rebuilt bounded Part 2 -probe recorded MAT0 request 3 at 52 nodes / 51 edges and request 4 at 55 nodes -/ 54 edges. These are observational snapshots for graph-growth profiling only: -they do not change traversal order, node or edge identities, provenance, -validation, cache semantics, or rendering. - -The same throttled snapshots now retain the number of distinct validation-cache -keys for WEAR, MAT0, and TEXM before the request. In a fresh 50-second Part 2 -probe, MAT0 request 3 recorded cache counts 2 / 3 / 3 and request 4 recorded -3 / 4 / 4, while the graph moved from 52 / 51 to 55 / 54 nodes / edges. The -counts distinguish cache growth from graph growth without adding per-request -file writes or asserting which parser, archive I/O operation, or allocation -caused the elapsed interval. - -`fparkan-game --load-progress` now initializes its file with `Starting` and -appends each later mission-load event instead of replacing the prior line. A -bounded probe can therefore retain both MAT0 and TEXM request milestones even -when a later event is last. The behavior is diagnostic persistence only: it -does not alter graph traversal, resource validation, or rendering. - -Each trace row now keeps the phase name first and appends a monotonic -`elapsed_ms=<N>` field from one process-local `Instant`; successful completion -is appended rather than overwriting the trace. A short rebuilt Part 2 probe -confirmed real timestamps from `Map` at 5 ms through `GraphVisualTextures` at -2,389 ms, then later MAT0 request milestones at 28,841 and 35,486 ms. The -probe was deliberately terminated and provides no frame/GPU result. These -timestamps measure whole elapsed intervals, including archive I/O, allocation, -validation, cache effects and progress-file writes; they are not per-parser -benchmarks. - -One bounded Part 2 trace reached `GraphVisualTextureRequests(64)` at 134,793 -ms but did not reach `Assets`: its MAT0 markers were 3/4 at 28,823/35,473 ms, -13/16 at 88,260/88,334 ms, 25 at 108,137 ms, 38 at 121,369 ms, and 47 at -128,160 ms. The process was stopped immediately after the 134.8-second marker. -This makes visual-graph expansion, rather than asset preparation or Vulkan, -the measured unfinished interval for this run. It still does not allocate that -time among MAT0 decode, graph allocation, archive I/O, OS cache state, or the -interleaved TEXM requests. - -Visual expansion now also caches WEAR validation by the complete derived WEAR -archive/name key and replays both successes and failures. It still creates each -per-prototype WEAR edge, increments the same request counters, and reports the -same failure provenance. A unit test verifies an archive-qualified cached -failure is replayed without consulting the repository. In a matched bounded -Part 2 probe the first TEXM-64 marker occurred at 135,520 ms, versus 134,793 -ms in the preceding sample, and neither run reached `Assets`; therefore this -corpus does not evidence a startup advance from WEAR caching. The path is kept -as a correctness-preserving duplicate-resolve optimization, not a causal -performance claim. - -A rebuilt executable then ran a controlled 180-second Part 2 `Autodemo.00` -probe with the append-only trace. Before exact-child termination it recorded -`GraphVisualTextureRequests(64)`, `GraphVisualMaterialRequests(100)`, and every -asset-preparation checkpoint through `AssetTextures`. It produced no native -window, frame, readback, or Vulkan acceptance report because it was terminated -while preparation was still active. This is one warm-state timing sample, not -evidence that the trace change improved loading or that either MAT0/TEXM alone -caused the earlier timeout; it does show the prior last-event-only trace hid -concurrent request classes and later preparation progress. - -Mission loading now raises the decoded-payload cache entry budget from 64 to -256 while retaining its 64 MiB byte budget. This avoids premature entry-count -eviction during resource-rich loads without making memory unbounded. The same -180-second Part 2 probe nevertheless remained at `GraphVisualTextures`; this -change is a safe cache-capacity improvement, not measured evidence of a Part 2 -startup advance. - -`Assets` now has four ordered diagnostic sub-checkpoints: `AssetModelMeshes` -(MSH), `AssetWearTables` (WEAR), `AssetMaterials` (MAT0) and `AssetTextures` -(diffuse TEXM and lightmaps). The callback is observational: it neither changes -the preparation order nor deduplicates requests. A fresh controlled 60-second -canonical GOG `Autodemo.00` first-root probe reached `AssetWearTables` but not -`AssetMaterials`; its created process was terminated and no native frame was -reported. At least one MSH therefore finished before the timeout, but this does -not attribute the remaining interval to WEAR decoding rather than later -MSH/WEAR iteration or archive I/O. - -The same source-axis proof now applies to `TerrainWorld`: its `Land.msh` surface -height query and `Land.map` areal/grid lookup use XY ground coordinates, return -source Z height, and leave raycasts as full 3D intersections. The Part 1 and -Part 2 licensed `Land.map` gate successfully locates sampled polygon vertices -under that contract. This closes the former XZ mismatch in terrain queries; it -does not establish object orientation, physics/gravity, original culling or the -camera matrix. - -### Camera ownership boundary from the GOG renderer - -The GOG `World3D.dll` export `LoadCamera` at RVA `0x1FB06` is only an import -thunk: it jumps through IAT `0x10020148` to `Terrain.dll!LoadCamera`. The -Terrain export is at RVA `0x4EBE0` and is a `__stdcall` entry with `ret 0x10`, -so it receives four machine-word arguments. It allocates a `0x1A4`-byte object, -forwards those four words to its constructor, and returns a pointer at object -offset `+0x134`. The constructor is at RVA `0x4EC60`: it passes arguments 3 and -4 to the base initialization at `this + 4`, stores argument 3 at `this + 0x138`, -and passes arguments 1, 2 and 4 plus `this` to its helper at RVA `0x4CEF0`. -Argument 1 is a NUL-terminated mode string at this call boundary: only the -embedded literals `REFLECTION` and `REFLECTION_SHIFTED` select a non-zero value -stored at `this + 0x1A0`. This proves that `LoadCamera` supports reflection -variants, but not the semantic types of its remaining arguments or the meaning -of the resulting value. `Terrain.dll!stdGetCurrentCamera2` returns a global -camera pointer, and `stdSetCurrentCamera2` updates it through the object's -interface methods. - -This establishes that original camera creation/selection is Terrain-owned and -not a field that can safely be inferred from a TMA transform. It does **not** -yet establish world-to-view matrix layout, FOV, near/far values, projection -handedness, or initial mission camera selection. The Vulkan path must therefore -continue to label its XY projection as diagnostic until a dynamic capture or -further Terrain disassembly proves these contracts. - -On 2026-07-18 the canonical windowed GOG process was confirmed running with -one targetable window titled `Parkan. Железная Стратегия`. Two read-only -Windows.Graphics.Capture attempts failed before returning a frame with -`SetIsBorderRequired failed: Интерфейс не поддерживается (0x80004002)`. No game -input was sent and this contributes no visual, camera, timing, or memory -evidence; a different capture/debugger path is required for dynamic analysis. - -The public `World3D.dll!stdSetCurrentCamera` is a one-pointer `__stdcall` -wrapper: it delegates to the Terrain `stdSetCurrentCamera2` IAT thunk and then -writes the same pointer to a World3D mirror global. An `iron3d.dll` call site -at RVA `0x4FB95` loads the argument from an active game-state chain -`[esi + 0xBC90] + 0x94` before calling that wrapper. This proves that the -selected camera is an object reference supplied by game state, not a scalar -mission-coordinate field. The caller's enclosing type and the meaning of -offsets remain unnamed until a class layout is recovered. - -Terrain strings also identify `CBufferingCamera`, `ICamera::SetTransformMatrix` -and `ICamera::GetTransformMatrix`, plus frustum/clip methods. These names prove -that a transform-matrix interface exists, but give neither method slots nor a -matrix convention; they must not be treated as a usable renderer ABI yet. - -### Buffering-camera projection path from static Terrain analysis - -A further headless-IDA review of the same GOG `Terrain.dll` identifies a -`CBufferingCamera`-related implementation path without requiring a live game -process. The procedure at RVA `0x4D740` copies exactly 64 bytes (16 dwords) -into its receiver at offset `+0x10`. The frame-preparation procedure at RVA -`0x4D9C0` queries a viewport rectangle through a virtual method at `+0x3C`, derives its -width, height, centre and an aspect ratio from that rectangle, and obtains -projection data through the camera interface. Projection type `0` uses a -receiver float at offset `+0x234` as an angle passed to `tan(angle / 2)`; -projection type `2` obtains a five-float block through a virtual method at -`+0x70`. Any other non-zero type raises `Not supported projection type`. - -This is sufficient to prove that original viewport and projection state are -live camera inputs, rather than a fixed diagnostic projection. It does **not** -yet prove matrix row/column convention, angle units, near/far field mapping, -handedness, or the initial mission camera object. In particular, the exported -string-labelled `ICamera::SetTransformMatrix` (RVA `0x4F830`) and -`ICamera::GetTransformMatrix` (RVA `0x4F850`) are both obsolete-call stubs, so -they are not a usable method ABI. - -Live read-only evidence now confirms the relocation-aware global contract. In -one elevated GOG run, `Terrain.dll` loaded at `0x02510000`; its global at -`base + 0x7355C` held non-null `0x0B37DF08`, whose first dword was -`0x025765B4` — exactly the relocated `off_100665B4` vtable from the -`LoadCamera` construction path. This proves the global is a live camera object -with that outer vtable. It also corrects an overreach: raw reads at global -offsets `+0x10` and `+0x234` did not yield a finite 4×4 matrix and a projection -angle in this sample. The static procedures' receiver and the exported global -pointer are therefore not yet proven to have the same adjustment/layout; those -offsets must remain unassigned until the selector/interface relationship is -recovered. - -The unattended demo also proves that this global is dynamic. Across 25 seconds -of read-only sampling it held three distinct heap pointers; all began with the -same relocated outer vtables `0x025765B4` and `0x02576558`. Two selected -objects exposed synchronized translation triples in the paired raw blocks at -offsets `+0x2C/+0x3C/+0x4C` and `+0x6C/+0x7C/+0x8C`, with world-like values -such as `(491.562, 761.551, 7.361)`. A third object used the same vtable but -reported normalized-looking values near `(0.098, 0.018, 0.856)` and did not -match the paired block. The observed pointer transitions are consistent with -the unattended camera changes, but the sample carries no mode label and does -not prove first- versus third-person identity. It does prove that the static -direct-xref initializer is not the sole runtime writer of the global; indirect -or unanalyzed write paths remain to be recovered. - -The shared outer vtable now recovers the exact transform adjustment for the -world-like samples. Its slot `+0x54` asks the subobject at `outer + 4` for -selector `0` through that subobject's slot `+0x20`, then copies three dwords -from returned offsets `+0x0C`, `+0x1C`, and `+0x2C`. In the live world-like -object, the selector field at `outer + 0x10` was `0xFFFFFFFF`; the recovered -selector implementation therefore returns `outer + 0x20`. The copied triple is -exactly `outer + 0x2C/+0x3C/+0x4C`, proving its association with the active -affine transform rather than merely a sampled correlation. The paired block -starts at `outer + 0x60` under selector `2`. Another outer slot at `+0x70` -obtains that selector-2 transform and applies `atan2` to its axis values, so it -is a proven orientation-angle path. Field names, angle order and matrix -handedness remain unassigned, but a backend-neutral camera pose may now retain -the raw affine transform and the exact translation triple without guessing them. - -The render contract now represents that boundary directly. `RawCameraTransform` -preserves each selector result as sixteen original `u32` words, so diagnostic -data is not rounded or silently assigned a row/column convention. Its only -typed accessor reinterprets the proven X/Y/Z words `3/7/11` as IEEE-754 -floats. `RawCameraPose` retains selector `0` and selector `2` together, and -`CameraSnapshot::raw_pose` is optional. The present Vulkan renderer continues -to use its existing view/projection matrices: raw pose capture is intentionally -not converted into a view matrix until the original transform/projection -convention has evidence. - -The next live sample strengthens the representation contract but still does -not name the transform's camera-space direction. For six no-input samples of -one auto-demo camera object, selectors `0` and `2` were byte-identical, -finite affine blocks with bottom row `(0, 0, 0, 1)`. Their upper 3x3 basis -was normalized and their last-column translations changed from approximately -`(441.038, 687.481, 10.754)` to `(433.545, 652.293, 10.673)`. - -This storage/order is not guessed: live `NGI32.dll!g_FastProc[23]` dispatches -to RVA `0x1D9A0`, whose SSE implementation writes each output row as the sum -of four scalar-weighted rows. Thus a contiguous 16-float block is multiplied -as a conventional row-major 4x4 matrix. The render contract now exposes a -mathematical `try_inverse_affine_row_major` helper for such finite, -non-singular `[R | t; 0 0 0 1]` blocks. It returns no result for a singular or -non-affine block and, crucially, does **not** identify that inverse as a view -matrix. Whether a selector result is camera-to-world or already a renderer -transform remains an evidence boundary. - -The active camera now also has a proven live projection-interface boundary. -`LoadCamera` stores its selector-18 camera interface at outer offset `+0x19C`; -in the elevated GOG AutoDemo sample it was a `CBufferingCamera` object with -relocated vtable `0x02576344`. Its rectangle at `+0x10` was exactly -`(left=0, top=0, right=1024, bottom=768)`, its projection-type field at -`+0x230` was `0`, and its FOV field at `+0x234` was `1.04` radians. -`CBufferingCamera` passes that FOV to `tan(fov / 2)` on its type-0 projection -path. This proves the live angle input and viewport for this sample; -it does not yet assign the two remaining projection values, near/far planes, -depth range, clip-space Y direction, or a Vulkan projection matrix. - -The unresolved values are now located rather than guessed. `CBufferingCamera` -uses its primary-render interface at component offset `+0x220` to select a -render context, then asks that context for a five-float projection block. The -sample's primary vtable was relocated `0x02574DF0`; its selector at `+0x0C` -is only a table lookup keyed by two still-unnamed inputs. No virtual call was -made by the probe. Therefore the block's values cannot yet be assigned as -near/far or reused by the Vulkan adapter. - -The runtime render model now retains this evidence as `RawCameraProjection` on -`CameraSnapshot`: the viewport rectangle, projection selector, exact FOV bits, -and all five context words travel through the backend-neutral boundary without -changing the current Vulkan projection. Its float accessors deliberately leave -the context fields unnamed; preserving a raw ABI value is safer than encoding a -near/far or clip-space convention before it is demonstrated. - -The next downstream boundary is now also identified. In the canonical static -`CBufferingCamera` vtable (`0x10066344`), slot `+0x4C` is Terrain RVA -`0x4D9C0`; it packages the camera transform, rectangle-derived viewport data, -FOV-derived tangent, and primary-render context values for another renderer -component. It does not itself assemble the final projection matrix. The GOG -`Ngi32.dll` export named `vrtSetCameraParam` resolves to a no-op compatibility -stub, while the live process loads the Direct3D7 runtime (`D3DIM700.DLL`). The -next recovery target is therefore the Direct3D transform consumer in -`iron3d.dll`, not a guessed Vulkan perspective formula. The observed `0.5`, -`700`, `0.1`, and `0.99` values remain unlabelled until that consumer proves -their roles. - -### Ngi32 Direct3D7 camera matrices recovered - -The downstream renderer is now concrete. `iron3d.dll` obtains an opaque -render interface through `Ngi32!niGet3DRender`; Ngi32 RVA `0x5640` returns the -global renderer object at RVA `0x3A460`. Its constructor at RVA `0x5E10` gives -the `0x8CC`-byte object vtable `0x100315E0`. During device setup, Ngi32 RVA -`0x8E70` calls the Direct3D7 device vtable at `+0x2C` (`SetTransform`) with -state `3` for the matrix made by RVA `0x7030` and state `2` for the matrix made -by RVA `0x9450`: projection then view. - -RVA `0x7030` exactly builds the row-major D3D7 projection from renderer FOV -`f`, near `n`, far `z` and viewport width/height `w`/`h`: -```text -[ cos(f/2), 0, 0, 0 ] -[ 0, w/h * cos(f/2), 0, 0 ] -[ 0, 0, sin(f/2)*z/(z-n), sin(f/2) ] -[ 0, 0, -sin(f/2)*n*z/(z-n), 0 ] -``` +## Камера и размещение в оригинале -The sine in the depth scale and homogeneous-W term is intentional: after the -D3D perspective divide the diagonal has the expected cotangent scale and the -depth range remains finite. RVA `0x9450` applies a -specific axis permutation/sign change and translated dot products to selector -0 before the view `SetTransform`; it is not merely the generic affine inverse. - -An elevated read-only AutoDemo probe sampled a 1024×768 renderer with -near=`0.5`, far=`700`, FOV=`1.3` radians and showed its view-source pointer -byte-identical to the active Terrain outer camera's selector-0 block. This -proves ownership and the D3D7 boundary, while keeping the earlier -`CBufferingCamera` FOV=`1.04` as a distinct upstream interface value. - -The live GOG layout is now independently repeatable without invoking any game -method: `Terrain.dll + 0x7355c` contains the current outer camera-object -pointer, whose vtable relocates to `Terrain.dll` RVA `0x665b4`. Selector 0 -returns the affine block beginning at outer `+0x20`; its sixteen row-major -words have translations at indices 3, 7 and 11. The public `LoadCamera` -interface at outer `+0x134` is a different view and must not be substituted -for that block. Captures for `RawCameraTransform` therefore contain the sixteen -selector-0 words from outer `+0x20`. A passive AutoDemo sample produced a -finite affine camera matrix and passed through the offline Vulkan adapter as -source-world geometry, rather than relying on a guessed identity view. - -The Vulkan static path now accepts this recovered camera as -`VulkanStaticCamera`. It composes the row-major D3D7 result as -`view * projection`, uploads its 64 bytes through a vertex-stage push constant, -and changes static vertices from XY to XYZ. GLSL reads the same bytes as its -default column-major `mat4`; therefore `matrix * vec4(position, 1)` is the -transpose-equivalent of the original D3D7 row-vector multiplication. Both APIs -use the 0..1 depth range, and this renderer uses a positive viewport height, so -this narrow bridge needs no extra depth or Y correction. The fragment alpha -cutoff moved to byte offset 64 in its own push-constant range. The default -identity camera deliberately preserves the old XY diagnostic preview; feeding -live terrain camera/projection values into mission rendering remains later -runtime wiring, not a claim of scene parity. - -The geometry side of that handoff is also explicit. The static mesh adapter now -has source-world projections for `Land.msh` and MSH: terrain positions retain -all three decoded coordinates; model positions retain Z and apply the recovered -static mission placement. The diagnostic XY counterpart uses that same placement -before normalizing X/Y, so it no longer silently omits authored object rotation. -These APIs preserve triangle order, batch ranges and packed UV decoding. The -current game preview deliberately stays on its XY diagnostic projection until a -runtime supplies a real legacy camera; selecting source-world coordinates with -identity camera would be a misleading empty/off-screen viewer rather than a -compatibility result. - -Static analysis of GOG `iron3d.dll` recovers the placement primitive at RVA -`0x36610`: it evaluates a row-major affine `Rz(z) * Ry(y) * Rx(x)` and writes -translation in the final column. The backend-neutral -`LegacyIron3dEulerTransform` preserves that finite portable formula with golden -yaw and scaled-point tests. A read-only elevated AutoDemo scan closes the -previous TMA binding gap for static placement: it finds the exact raw mission -records (position, `(0, 0, z)`, scale) and live object blocks at the same -world positions. For example, `z = 1.6262363` produces the observed pair -`-sin(z) = -0.99846`, `cos(z) = -0.05541`; matching pairs occur for the other -observed objects. The opt-in captured-camera path therefore applies -`R * (scale * local_position) + translation` before Vulkan upload. It remains -a static-placement contract: dynamic/animated transforms, physics ownership, -terrain material selection, lighting and gameplay parity are still separate -evidence tasks. - -`VulkanSmokeRenderer::set_camera` now accepts a new finite camera between frame -submissions and uses it for the next command buffer without rebuilding the -swapchain, buffers, descriptors or pipelines. `VulkanStaticCamera` also accepts -generic finite row-major view/projection pairs, so the final game camera -controller need not be coupled to the D3D7 recovery type. The native Windows -smoke executes that update before each of 300 validation-clean frames. This is -the renderer-side cadence contract only; it does not yet supply a gameplay -camera controller or claim camera movement parity. - -For controlled differential work, `fparkan-game --backend static-vulkan` now -accepts `--legacy-camera-capture <path>`. The JSON capture stores only the -selector-0 words and the already recovered Ngi32 viewport/near/far/FOV inputs; -the application reconstructs the D3D7 camera offline, selects source-world -geometry, and never attaches to or controls the original process. A live -read-only AutoDemo confirmation corrected an ABI ambiguity: the Terrain global -is the base of the `0x1A4` camera object (vtables at `+0` and `+4`), while -`LoadCamera`'s public return is the separate interface view at `base + 0x134` -with its own vtable. This capture format deliberately preserves the former -base-object interpretation and does not invent camera ownership. - -A three-frame GOG AutoDemo run with one such read-only capture selected the -`legacy-d3d7-capture` path, rendered all eight mission roots / 66 MSH -components plus terrain, reported 5,226 vertices inside the recovered clip -volume, produced a 7,372,800-byte readback with FNV-1a -`1737487240747766901`, and ended with zero Vulkan validation warnings and -errors. This validates the offline handoff for one captured instant; it does -not claim live camera tracking, material parity, or an original-frame pixel -comparison. - -`tools/capture-original-camera.ps1` makes that handoff repeatable for a live -GOG process. It finds the actual `Terrain.dll` base with Toolhelp module -enumeration, opens the chosen PID with only -`PROCESS_QUERY_INFORMATION | PROCESS_VM_READ`, verifies the expected outer -vtable, and reads exactly 64 selector-0 bytes at outer `+0x20`. AutoDemo can -briefly select a normalized/reflection-like camera object whose finite -translation is near the origin. The tool therefore re-reads the global on -each attempt and accepts a sample only when the proven translation length -exceeds its explicit `-MinimumWorldTranslation` threshold (default `100`); -the JSON retains the accepted outer pointer and translation for audit. It emits the -`fparkan-legacy-camera-v1` JSON accepted by `fparkan-game`; it does not write -to the process, send window messages, inject code, suspend threads, or invoke -an original method. PowerShell execution policy may require the explicit -one-shot bypass below; that changes neither the script nor the game. - -```powershell -$capture = powershell -NoProfile -ExecutionPolicy Bypass -File ` - .\tools\capture-original-camera.ps1 -ProcessId 5632 -$capture | Set-Content -NoNewline target\fparkan\live-camera.json -cargo run -q -p fparkan-game -- --root 'C:\GOG Games\Parkan - Iron Strategy' ` - --mission 'MISSIONS\Autodemo.00\data.tma' --backend static-vulkan --frames 3 ` - --legacy-camera-capture target\fparkan\live-camera.json ` - --readback-out target\fparkan\autodemo-live-camera.raw -``` +Миссия хранит положение, три угла и масштаб объекта. Функция `iron3d.dll` +RVA `0x36610` строит `Rz(z) * Ry(y) * Rx(x)` с переносом в последнем столбце. +Точка модели преобразуется как `R * (scale * point) + position`. +Повороты и переносы узлов модели применяются до размещения объекта в миссии. -On the unattended live AutoDemo the accepted world-space capture completed the -same all-root static bridge with 10,152 clip-visible vertices, 71 descriptors, -a 7,372,800-byte format-50 readback, and zero Vulkan validation -warnings/errors. A passive desktop-surface capture of that original window also -proved that the selected instant contains the textured terrain, atmospheric -sky, HUD and weapon FX; `PrintWindow` itself returns a black Direct3D buffer, -so it must not be treated as an original-frame oracle. The paired images expose -the next real renderer gap: sharing the raw pose and Ngi32 projection inputs is -not yet pixel parity, because the static bridge still lacks the full runtime -camera-selection timing, terrain composition, culling, lighting, UI and FX. - -For visual regression work, `fparkan-game --backend static-vulkan` also accepts -`--readback-out <path>`. It writes the final synchronized Vulkan swapchain -image only after the renderer has completed its normal teardown evidence; the -JSON report records its raw Vulkan format, byte count, hash, and requested -path. At 1280x720, format `50` (`VK_FORMAT_B8G8R8A8_UNORM`) produces exactly -3,686,400 bytes. The artifact is a Vulkan-side comparison input, not an -asserted original-frame image. - -The first bounded launch showed that repeated fingerprinting, rather than -Vulkan initialization, was the load-path bottleneck: each new MAT0/TEXM request -re-opened an already decoded archive and re-hashed its entire source file. -`ResourceRepository::open_archive_unchanged` now makes the *explicit* bounded -loading-transaction contract available. The normal `open_archive` remains the -strict public path and still invalidates stale entry handles after an external -archive replacement; only mission asset preparation uses the transactional -variant because it consumes an immutable snapshot of the selected game root. - -With that scoped cache, canonical GOG -`MISSIONS/Autodemo.00/data.tma --backend static-vulkan --preview-roots 1` -completed preparation in 6.308 seconds (the visual graph completed in 6.012 -seconds) and produced a 1280×720 native Vulkan frame with the captured D3D7 -camera, 15 material descriptors, and zero validation warnings or errors. This -is a real original-asset rendering milestone, but not pixel parity or gameplay -camera parity: it intentionally renders the bounded first preview root and -uses a one-time offline camera capture. - -Expanding that same preview to all eight AutoDemo mission objects exposed a -different, renderer-local limit: the merged static scene crossed 65,535 -vertices before any Vulkan command was recorded. `VulkanStaticMesh` now uses a -32-bit GPU index buffer (`VK_INDEX_TYPE_UINT32`), while source MSH/TerrainFace28 -indices remain decoded in their original 16-bit representations and are widened -only at the static-render bridge. A regression test crosses the former boundary. -The full captured-camera AutoDemo preview completed at 13.369 seconds and -rendered three 1280×720 native frames: 8 mission objects, 66 mesh components, -67 material descriptors, and zero Vulkan validation warnings or errors. A later -orientation-enabled run rendered the same full scene for three 1280×720 frames -with `validation_warnings=0`, `validation_errors=0` and readback hash -`6406107678925513509`. It is still a static source-world preview: terrain -material selection, lighting, animation, physics and gameplay behavior have -not yet been claimed as parity. - -A fresh no-input launch of the canonical `iron_3d.exe` did create a responsive -window titled `Parkan. Железная Стратегия`. A read-only probe then requested -`PROCESS_QUERY_INFORMATION | PROCESS_VM_READ` and attempted to read the known -Terrain global at image address `0x1007355C`; `OpenProcess` returned -`ERROR_ACCESS_DENIED` (5), so no process memory or camera value was read. The -window was left running. A same- or higher-integrity 32-bit debugger is still -required for live camera evidence. - -`Land.msh` использует отдельный geometry-only bridge: validated `TerrainFace28` -сохраняет source triangle order, а его positions и packed UV0 попадают в тот же -static vertex/index upload path. Для текущего диагностического viewer XY bounds -нормализуются в clip-space, packed UV0 декодируется как signed `int16 / 1024`. -Один static draw range намеренно не трактует `material_tag`, surface mask, slots -или auxiliary streams как material/pipeline state: их связь с original terrain -renderer пока не доказана. GOG `DATA/MAPS/11/Land.msh` дал 6 458 vertices и -24 672 indices в 300-frame native Vulkan run без validation warning/error. - -Выбор depth/stencil attachment отделён от renderer lifetime: -`select_depth_stencil_attachment_format` применяет тот же фиксированный порядок -форматов, что и capability gate, к фактически поддерживаемому списку GPU. -Это исключает ситуацию, когда admission принимает один совместимый формат, а -allocation позднее выбирает другой; resource уничтожается после framebuffer и -render pass при recreate/teardown. - -После последнего world pass renderer закрывает сцену и выводит back buffer. -World3D снимает `in_render`, восстанавливает временный viewport state и вызывает -`on_end_render` у active objects. Только после этого допустимо освобождать -temporary vertex buffers или заменять render representation. UI/shell -обслуживается верхним уровнем после возврата из world-render path; для -диагностики полезно уметь сохранять world-only command list и финальный -framebuffer отдельно. - -Terrain material discovery is now reproducible instead of inferred from the -mesh shape. The original AutoDemo map places two textual sidecar WEAR tables, -`Land1.wea` and `Land2.wea`, beside `Land.msh`; `Terrain.dll` contains the -corresponding `1.wea` and `2.wea` loading suffixes. The new terrain inspector -reports all packed face tags. For `DATA/MAPS/AutoMAP/Land.msh`, its 3,174 faces -use `0x0102` (592), `0x0203` (386), `0xff01` (918), `0xff02` (970), and -`0xff03` (308). The low byte is always a valid positional selector 1..3 in -`Land2.wea`; the high byte is a `Land1.wea` selector or sentinel `0xff`. -This is evidence for a two-layer terrain-material contract, but not yet for -its blend equation, so the Vulkan bridge intentionally remains texture-agnostic -until the base/overlay composition is recovered. - -Static recovery of `Terrain.dll!CLandscape` now fixes the loader contract behind -that observation. The constructor opens `Land.msh`, creates one material manager -from `Land1.wea`, then invokes that manager's second-table load with `Land2.wea`. -It requires NRes type 18 with the original diagnostic “microtexture mapping -chunk” and retains it as four-byte entries; type 14 is optional. Therefore the -two WEAR selectors must not be collapsed into an invented one-layer material, -and the type-18 `aux18` stream is now documented as microtexture mapping data. -`TerrainFace28::material_layers` exposes the evidenced high-byte `Land1` -selector (with `0xff` absent sentinel) and low-byte `Land2` selector. The -manager's actual blend/pass operation remains unrecovered. `World3D.dll` -supplies the manager ABI: its load slot appends `Land1` as table 0 and `Land2` -as table 1, while its material-phase lookup receives the exact 32-bit selector -`(table_index << 16) | material_index`. `TerrainMaterialSelection` preserves -that key for the renderer without assuming how the two phases are blended. - -The same `World3D!GetMaterialPhase` recovery establishes the next boundary. It -validates the table/row selector, chooses a MAT0 phase (or interpolates two -phases for an animated material), returns the selected material record, and -hands its caller a 76-byte expanded phase-state block. It does not submit a -draw or compose the two terrain tables. The later consumer of that record and -the type-18 microtexture mapping remains the required evidence before adding -an overlay, blend, or microtexture shader. - -The terrain render traversal is now located at `Terrain.dll` RVA `0x11340`. -After visibility/cell work, it derives a 68-byte source-material record and -dispatches `Terrain!GetShade()` slot `+16` with four arguments: zero, the -record's word at `+2`, a pointer selected from a four-byte table by its word at -`+0`, and zero. `GetShade` is a Terrain singleton, not `niGet3DRender`; the -preceding slot `+60` call sets its visibility mode. Slot `+16` consumes the -second word as the count of adjacent `u16` pairs and the first as the pair-table -index, then groups the pairs by material lookup before constructing draw work. -`TerrainSlotTable::render_dispatch` exposes exactly those two proven disk -fields, while `LandMeshDocument::slot_material_pairs` decodes the selected -type-11 entries as `{ material_lookup: u16, flags: u16 }`. Flag `0x0010` is a -proven batch boundary in `GetShade`. Crucially, this lookup does **not** select -either map-local Land table: the `GetShade` constructor loads its own one-table -manager from `system.rlb` resource `Shade.wea`. That WEAR table has one row, -`LIGHT1`, whereas AutoDemo's 3,174 pairs use keys `0..3173`; therefore the -16-bit value is an opaque manager lookup/cache key, **not** a table-row -selector. `TerrainMaterialPair::shade_lookup_key` records this separate key -without conflating it with `Land1.wea`/`Land2.wea`. The AutoDemo inspector now -reports 128 slots and 392 `0x0010` batch boundaries. The meanings of remaining -flags, this cache mapping, and the final blend operation remain unassigned. - -The cache boundary is now mechanically separated from the WEAR loader. In the -`GetShade` object, the `Shade.wea` manager is stored at byte offset `3092`, but -the dispatcher calls a distinct object at byte offset `3244`. Its virtual slot -`+16` receives every type-11 key repeatedly with mode values `512`, `16`, -`20497`, and `20752`. The cache object itself has an eight-byte entry table at -`+316` and its exclusive key count at `+320`: the first dword of an entry is a -stored-record pointer and its byte at `+4` selects a 212-byte profile bank. A -key outside that count, or an entry pointer below `0x1000`, fails the lookup. - -The static constructor now identifies this object as the embedded `CLandscape` -subobject whose vtable is `Terrain.dll` RVA `0x643D0`. It installs that vtable -at parent offset `+316`, allocates the entry table at parent `+632` (subobject -`+316`) as `0x1f40` bytes / 1,000 eight-byte entries, and initializes its count -to zero. The profile-bank region starts at subobject `+332`: every one of the -100 banks has a 116-byte header followed by a 96-byte initialized stream, hence -the recovered 212-byte stride. The byte selected from each table entry is -therefore a bank index into a constructor-owned, fixed 100-bank region. This -proves the cache's allocation layout. Its population path is also now located. -`Terrain.dll` RVA `0x10280` resets the count, recreates the base entries from -the 28-byte stream at parent `+756`, and assigns bank zero. RVA `0x12e20` -copies one `0xd4`-byte profile header into bank `profile_count`, increments that -count, and remaps supplied keys to 28-byte records through the entry table. A -mapped record is stored as `profile_record_base + 28 * record_index` with the -new bank byte; a mapping whose second index is `-1` stores only the profile -ordinal, below `0x1000`, and is deliberately rejected by the lookup routine. -This explains the runtime sentinel without interpreting it as a missing WEAR -row. The input map is not a direct `Shade.wea` decode: the sole call site is -the terrain loader at `Terrain.dll` RVA `0x0a3f0`, after it has generated and -stitched 28-byte terrain records. That loader first appends pairs copied from -an intermediate primitive-key list with second value `-1`, then appends one -`(-1, generated_record_index)` pair for every generated record. Before calling -the cache builder it runs a three-edge matching pass over those records, using -their quantized positions and adjacency selectors. The cache profile therefore -describes loader-produced geometry and its generated correspondence map, not a -named on-disk `Shade.wea` section. The source of the intermediate primitive-key -list and the exact semantics of the 28-byte record's non-position fields remain -unassigned; this is sufficient to rule out an invented static WEAR parser, but -not to reproduce the profile generator yet. - -On success the method returns the shared result view at cache offset `+24`, not -the stored-record pointer and not a WEAR row. Thus the dispatcher's observed -flags at result-view `+60`, secondary selector `+24`, optional byte `+88`, -referenced state block `+104`, three signed shorts `+108`, and another selector -`+112` are materialized lookup state, not yet proven on-disk record offsets. -Mode `0x0200` (`512`) only validates the key in the recovered path; mode -`0x5011` includes the recovered selector-table transfer and mode `0x5110` -includes the recovered bank-byte transfer. The meanings of their remaining -bits, record population, and the result fields remain unrecovered, so the Vulkan -path must not map any of them to invented pipeline state. - -A read-only elevated AutoDemo probe confirms that this is a lifecycle boundary, -not a permanently allocated table: the live `GetShade` singleton is initialized -and its `Shade.wea` manager pointer is non-null, while its byte-offset `3244` -cache pointer is normally null. Passive 100-ms sampling of the unattended -AutoDemo caught it briefly at `0x035C64B4`; its relocated vtable is -`Terrain.dll` RVA `0x643D0` and slot `+16` is RVA `0x10910`, matching the -recovered lookup routine. Therefore a future runtime implementation must treat -cache availability as contextual and cannot dereference or prebuild the cache -solely from constructor state. - -`tools/capture-terrain-shade-cache.ps1` now makes the same evidence repeatable -without debugger attachment. It discovers `Terrain.dll` with Toolhelp, opens -only `PROCESS_QUERY_INFORMATION | PROCESS_VM_READ`, searches readable -32-bit-process regions for the already proven cache vtable, and excludes the -Terrain image itself because relocations there also contain that pointer value. -It reports only addresses and count metadata; it does not alter the process or -save game resources. On the unattended GOG AutoDemo process 5632 on 2026-07-18 -it found the live cache at `0x035C64B4`, with entry table `0x17284590` and -exclusive count `3462`. Two adjacent passive captures retained the cache -address/table/count but returned result views `0x0274BD89` and `0x008A60DC`. -That changing result view is direct evidence that it is transient lookup -state, not a persistent cache identity or a WEAR row. The observation narrows -the next terrain task to profiling the cache's generated entries and their -consumer; it still does not establish a Land1 blend equation. - -The probe now also reads the bounded eight-byte entry table and only hashes -the fixed 212-byte profile banks it observes; raw process bytes never enter -the repository. The same AutoDemo cache had 3,426 materialized record pointers -among 3,462 entries and referenced banks `0..4`. Two consecutive captures -kept that count, bank set, and all five FNV-1a-64 bank hashes stable: -`9426539192111566946`, `2915543853261903601`, `13156371703608727216`, -`11280405573172155968`, and `18234288329145104612`. Their result views changed -from `0x008A565C` to `0x008A55A8`. Thus the generated cache/profile state is -stable over these render samples while the shared result view is not. This -still identifies neither the record-field semantics nor the final terrain -blend/pass operation. - -```powershell -powershell -NoProfile -ExecutionPolicy Bypass -File ` - tools\capture-terrain-shade-cache.ps1 -ProcessId <iron_3d-pid> -``` +Горизонтальная плоскость карты — XY, высота — Z. У GOG AutoDemo диапазон XY +ландшафта составляет `0..1190.6976`. Высоты, сохранённые в `Land.msh`, переводятся +в единицы мира делением на 32. Это отдельное преобразование ландшафта; +применять его к координатам размещённых объектов нельзя. -The static Vulkan bridge now carries each contiguous face run as a separate -draw range keyed by the original packed `material_tag`; it neither reorders -triangles nor collapses the high byte. For the first usable visual layer, the -low byte resolves positionally through the adjacent standalone `Land2.wea`, -then the existing `MAT0 -> Textures.lib -> TEXM` chain supplies the RGBA8 -descriptor. `Land1.wea` remains preserved as the high byte/overlay channel and -is deliberately not drawn until its blend equation is recovered. A canonical -three-frame AutoDemo run now uses 71 descriptors (66 MSH plus five terrain -tags), produces readback hash `17034026600547661445`, and stays at zero Vulkan -validation warnings/errors. This is original terrain texture upload, not -terrain visual parity. - -```powershell -cargo run -q -p fparkan-cli -- terrain inspect ` - 'C:\GOG Games\Parkan - Iron Strategy\DATA\MAPS\AutoMAP\Land.msh' --format json -``` +`Ngi32!niGet3DRender` (RVA `0x5640`) возвращает графический интерфейс. +Вызов Direct3D7 `SetTransform` устанавливает projection из RVA `0x7030` +и view из RVA `0x9450`. Для FOV `f`, ближней плоскости `n`, дальней `z` +и размеров viewport `w`, `h` projection в порядке строк имеет вид: -## Проверки паритета - -Главные риски совпадения кадра: - -- x87 extended precision и правила округления; -- различия scalar/SIMD slots `g_FastProc`; -- порядок objects, batches и transparent primitives; -- depth write/test, cull, alpha test и blend transitions; -- mip-skip, palette и `Page` coordinates; -- material fallback и выбор phase; -- последовательность RNG для FX и atmosphere; -- capability fallback конкретного устройства; -- quantization времени и дополнительный simulation step; -- eager/lazy resource resolve и cache side effects. - -Минимальный deterministic frame capture должен включать camera state, viewport, -visible object IDs, выбранные LOD/group/slot, draw-item list, material и texture -handles, pipeline keys, matrices, render phase, sort key, причины culling и -hashes промежуточных buffers. Без такой трассировки нельзя уверенно отделить -ошибку формата MSH от ошибки state machine renderer-а или сортировки. - -Связанные справочные страницы с таблицами форматов: [MSH](../reference/msh.md), -[materials](../reference/materials.md), [Texm](../reference/texm.md) и -[render frame](../reference/render-frame.md). - -### Live AutoDemo: empty frame устранён, но parity ещё не достигнут - -Read-only `PrintWindow` capture работающего GOG AutoDemo даёт полноценный -оригинальный кадр (terrain, варбот и близкая geometry), поэтому теперь есть -безвводный source для будущего visual comparison. Текущий полный static-Vulkan -preview раньше состоял только из clear color. Validation `0/0` тогда доказывал -только GPU lifecycle, но не совпадение с original renderer. - -Новая CPU-диагностика применяет к тем же source vertices ту же row-major D3D7 -matrix, которую GLSL получает как column-major push constant. Для свежего -captured Ngi32 camera (`viewport 1024x768`, near `5`, far `700`, FOV `1.3`) -она насчитала `clip_visible_vertices=0`. Последующая сверка с -`Ngi32!sub_10009450` и `sub_10007030` нашла два literal translation defects: -Y translation view matrix должна быть `-(m23*m22 + m13*m12 + m03*m02)`, а -projection использует `width/height` и множитель `sin(fov/2)` в обоих depth -coefficients. После исправления того же capture диагностировал -`clip_visible_vertices=938`, а `PrintWindow` native Vulkan window показал -реальную rasterized terrain/model geometry. Это исключает прежний empty-frame -барьер; face culling и texture selection не были его причиной. - -Получившийся кадр всё ещё явно не похож на оригинал: geometry сосредоточена у -границы и material/terrain representation грубая. Значит, следующие задачи — -camera timing/selection, source model-node transforms, terrain material phases, -lighting и visibility, а не объявление pixel parity. - -MSH static bridge теперь также перестал отправлять каждый `Batch20` файла. -Для стандартного `Node38` он выбирает `slot_index[LOD0, group0]`, затем -соответствующий `Slot68.batch_start..batch_count`; alternate LOD/groups больше -не подмешиваются в initial static pose. Regression test фиксирует selection, -а fresh AutoDemo run меняет readback hash при тех же 938 clip-visible vertices -и остаётся validation-clean. Новый capture по-прежнему показывает сильное -смещение частей, поэтому одной selection недостаточно: нужны local node poses, -parent hierarchy и animation-key/fallback semantics. Модели без layout Node38 -сохраняют прежний all-batches diagnostic fallback. - -Чтобы следующий шаг не перечитывал raw NRes streams в renderer, validated -`ModelAsset` теперь переносит `ModelAnimation` с decoded type-8 keys, type-19 -map и declared frame count. `node38_fallback_pose` возвращает exact fallback -key pose. Licensed gates Part 1/Part 2 подтвердили 435/511 models, 157/200 -animated models, 3,469/5,233 node samples и утверждённые captures. - -Следующий узкий шаг применяет этот уже декодированный `fallback_key` в -legacy-camera static preview. Для каждого выбранного `Node38` bridge поворачивает -вершины normalized quaternion, прибавляет local translation pose и только затем -применяет доказанный TMA `Rz * Ry * Rx`, scale и mission translation. Вершины -намеренно разворачиваются per node/draw range, чтобы один source vertex мог -получить разные poses. `parent_or_link_raw == 0xFFFF` теперь доказан как root, -а любое меньшее child значение — parent index: inspected `R_B_01.msh` содержит -34-node parent-before-child tree, а Part 1/Part 2 animation gate подтвердил -этот контракт для всех standard nodes. Parent rotation корректно поворачивает -child translation до суммирования. Frame map всё ещё не семплируется, поэтому -это static fallback hierarchy, а не full animation parity. -На той же GOG `MISSIONS/Autodemo.00/data.tma` и offline Ngi32 camera capture -full preview из 8 objects / 66 mesh components / 67 descriptors завершился за -один native Vulkan run с `clip_visible_vertices=3415`, `validation_warnings=0`, -`validation_errors=0` и readback hash `1275533143935640133`. Рост с 2584 -видимых вершин подтверждает, что composed parent poses реально вошли в geometry -path; он не является оценкой сходства с original frame. - -Параллельно bridge для legacy-camera path теперь переводит только высоту -`Land.msh` в world units с масштабом `1/32`: raw AutoDemo heights -`95.29412..288.23532` становятся `2.978..9.007`, что согласуется с TMA Z и -live camera height. Это доказанное coordinate conversion, но оно само по себе -не сделало вершины видимыми и не является заявлением о pixel parity. - -### Explicit offline Node38 animation-frame preview - -The static Vulkan path now accepts `--static-animation-frame <u16>`. For each -standard `Node38` model it resolves the already-decoded type-19 map at that -logical frame, selects its mapped type-8 key when it is valid before the -node's fallback key, samples the selected key and its immediate successor with -the portable reference sampler, then composes the resulting parent hierarchy. -Nodes with no usable map, or a requested frame outside the declared map, -retain their exact fallback pose. An invalid/incomplete hierarchy deliberately -falls back to the existing unposed static geometry path instead of guessing a -runtime state. - -This option is a reproducible asset-viewer probe, not a gameplay clock: it -does not identify the original animation controller, x87 rounding profile, -clip selection, LOD/group controller, blending or FX timing. The JSON report -contains `animation_frame` (`null` for the established fallback preview) so -readbacks from the two modes cannot be mistaken for each other. On 2026-07-18, -the licensed GOG `MISSIONS/Autodemo.00/data.tma` run with all eight roots, -`--static-animation-frame 1`, two frames and format `50` completed with 66 -mesh components, 75,543 clip-visible vertices, a 1280×720 / 3,686,400-byte -readback hash `4638497144561211935`, and validation warnings/errors `0/0`. - -### Windows PowerShell camera-capture hand-off - -`tools/capture-original-camera.ps1` is an observer: it opens the original -process only with `PROCESS_QUERY_INFORMATION | PROCESS_VM_READ`, samples the -active Terrain camera and emits `fparkan-legacy-camera-v1` JSON. Windows -PowerShell 5.1 commonly redirects or pipes that text as UTF-16LE with a BOM. -The Vulkan viewer now accepts BOM-marked UTF-16LE and UTF-16BE in addition to -UTF-8 (including an UTF-8 BOM), so a normal PowerShell capture can be used -directly as `--legacy-camera-capture` without a lossy manual re-encoding step. -Malformed byte lengths and invalid Unicode remain explicit load errors. - -Fresh read-only evidence from the running licensed GOG AutoDemo sampled outer -camera `0x0B368FD8` in `Terrain.dll` at base `0x02510000`; selector-0 -translation was `(548.020752, 559.672852, 3.00516248)`. Passing the captured -UTF-16LE file to the native Vulkan viewer rendered all eight mission roots -with `camera_mode="legacy-d3d7-capture"`, 66 mesh components, 6,787 -clip-visible vertices, 71 material descriptors, a 1280x720 / 3,686,400-byte -format-50 readback hash `7904810245714997753`, and validation warnings/errors -`0/0`. This proves the live-camera hand-off and projection path; it does not -establish frame-by-frame camera-selection parity, terrain shading or pixel -parity with the original renderer. - -### Explicit MAT0 phase preview - -`fparkan-game --backend static-vulkan --static-material-phase <u16>` now -selects the exact decoded MAT0 texture for that phase of each MSH material. -The override is deliberately per material: when the requested phase is absent -or intentionally untextured, that material retains its existing first prepared -diffuse request, rather than fabricating a texture or aborting the whole -mission. Terrain remains on its separately proven Land2 base path. The JSON -report records `material_phase` so the artifact's selector is explicit. This -is an asset-viewer control, not a claim about the original material clock, -phase mode, interpolation, random offset, lighting or Land1 composition. - -On 2026-07-18 the licensed GOG AutoDemo run with all eight roots, phase `1`, -two frames and format `50` completed validation-clean with 71 descriptors and -a 1280×720 / 3,686,400-byte readback hash `3152437928708207395`. +```text +[ cos(f/2), 0, 0, 0 ] +[ 0, w/h*cos(f/2), 0, 0 ] +[ 0, 0, sin(f/2)*z/(z-n), sin(f/2) ] +[ 0, 0, -sin(f/2)*n*z/(z-n), 0 ] +``` + +После деления на однородную координату получается обычная перспективная +проекция с глубиной `0..1`. Матрица view дополнительно меняет оси и знаки; +она не равна одному лишь обратному преобразованию камеры. +В проверенном GOG контексте Ngi32 использовались `n=0.5`, `z=700`, `f=1.3`. + +У GOG `Terrain.dll + 0x7355c` хранит указатель на активную внешнюю камеру. +Её selector 0 возвращает 16 слов начиная с `+0x20`, перенос находится в словах +3, 7, 11. Интерфейс `LoadCamera` расположен отдельно, по `+0x134` объекта. +Эти два указателя нельзя подменять друг другом. Формулы камеры и размещения +реализованы и проверяются в `fparkan-render`. + +## Материалы ландшафта + +Рядом с `Land.msh` лежат `Land1.wea` и `Land2.wea`. Старший байт +`TerrainFace28.material_tag` выбирает материал первой таблицы, младший — +второй. `0xff` в старшем байте означает отсутствие первого слоя. +В AutoDemo встречаются теги `0102`, `0203`, `ff01`, `ff02`, `ff03`. +Менеджер World3D получает селектор `(table_index << 16) | material_index` +и возвращает выбранную фазу MAT0 размером 76 байт. + +Type 18 в `Land.msh` оригинал называет microtexture mapping chunk; +он содержит четырёхбайтовые записи. Type 11 хранит пары `u16`, выбранные +таблицей terrain slots. Их первое поле — ключ shade cache, а не номер строки +WEAR. Флаг `0x0010` отделяет группы при обходе. `GetShade` использует собственный +материал `LIGHT1` из `system.rlb::Shade.wea`. + +Shade cache в GOG `Terrain.dll` имеет vtable RVA `0x643D0`. Его таблица по +`+316` состоит из восьмибайтовых записей: указатель и номер банка. Число записей +находится по `+320`; банки начинаются с `+332`, их шаг — 212 байт. Lookup +RVA `0x10910` возвращает временное представление по `+24`, а не саму запись. +Построение в RVA `0x10280` и `0x12e20` использует созданную загрузчиком геометрию. +В рабочем Vulkan-пути `TerrainMaterialLayers` разрешает для каждого terrain slot +базовую, detail, overlay и overlay-detail фазы из WEAR/MAT0; их UV, alpha и +lightmap state передаются в `VulkanStaticMaterial`, а shader последовательно +сэмплирует эти четыре стадии. Animation clock обновляет выбранные фазы и +коэффициенты без пересборки terrain mesh, сохраняя native type-14 overlay mask. + +## Реализация Vulkan + +`LegacyPipelineState` хранит blend, depth, cull и alpha-test. Из этих значений +получается ключ Vulkan pipeline. Alpha reference передаётся отдельно через +push constant; он не требует нового pipeline. + +Рабочий путь `fparkan-game` загружает terrain и все выбранные mission roots +(по умолчанию весь список объектов), разрешая локальные material slots через +WEAR и MAT0 до объединения геометрии. Индексы GPU имеют тип `u32`: вся карта +может содержать больше 65 535 вершин, даже если каждый исходный mesh использует +`u16`. Terrain передаёт в shader базовый, detail и overlay stages, lightmap и +animation phase; object batches сохраняют их legacy blend/depth/alpha state. + +После загрузки world path создаёт интерактивную free-flight камеру (`WASD`, +`E/Q`, `Shift`, `Ctrl`, RMB relative-look), обновляет listener и на каждом +кадре собирает environment frame. 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` оставляет цикл +бесконечным, а `--preview-roots` служит только диагностическим ограничителем. +`--legacy-camera-capture` выбирает воспроизводимую камеру без free-flight +управления. + +Для сохранения кадра surface должен поддерживать `TRANSFER_SRC`. Renderer +копирует последний отправленный swapchain image в host-visible buffer и +читает его после завершения GPU. Размер raw-файла — `width * height * 4`. +Порядок каналов определяется выбранным Vulkan format; его необходимо учитывать +при преобразовании в обычное изображение. |
