aboutsummaryrefslogtreecommitdiff
path: root/docs/tomes/05-render.md
diff options
context:
space:
mode:
Diffstat (limited to 'docs/tomes/05-render.md')
-rw-r--r--docs/tomes/05-render.md1177
1 files changed, 74 insertions, 1103 deletions
diff --git a/docs/tomes/05-render.md b/docs/tomes/05-render.md
index e63c3ae..586485b 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
@@ -842,1107 +840,80 @@ 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` использует созданную загрузчиком геометрию.
+Поэтому повторить terrain shader только разбором WEAR нельзя. Точная композиция
+слоёв и микротекстур остаётся открытым вопросом.
+
+## Реализация Vulkan
+
+`LegacyPipelineState` хранит blend, depth, cull и alpha-test. Из этих значений
+получается ключ Vulkan pipeline. Alpha reference передаётся отдельно через
+push constant; он не требует нового pipeline.
+
+Статический путь `fparkan-game` загружает ландшафт и MSH-компоненты выбранных
+объектов. Локальные material slots разрешаются через WEAR и MAT0 перед
+объединением геометрии. Индексы GPU имеют тип `u32`: вся карта может содержать
+больше 65 535 вершин, даже если каждый исходный mesh использует `u16`.
+Текущий путь берёт базовую текстуру Land2; свет, составные слои и атмосфера
+пока не участвуют в этом статическом изображении.
+
+Для сохранения кадра surface должен поддерживать `TRANSFER_SRC`. Renderer
+копирует последний отправленный swapchain image в host-visible buffer и
+читает его после завершения GPU. Размер raw-файла — `width * height * 4`.
+Порядок каналов определяется выбранным Vulkan format; его необходимо учитывать
+при преобразовании в обычное изображение.