From aa51f3574d3d4d4e0fa59653eedd718fceb5d1bd Mon Sep 17 00:00:00 2001 From: Valentin Popov Date: Sun, 6 Sep 2026 05:22:12 +0400 Subject: chore: simplify project to engine docs and tests Remove planning and acceptance scaffolding while retaining the native Vulkan mission preview, format readers, runtime algorithms, and ordinary Rust tests. Keep the book aligned with the runnable project and validate checked-in shaders without generated tool metadata. --- docs/tomes/05-render.md | 1177 +++-------------------------------------------- 1 file changed, 74 insertions(+), 1103 deletions(-) (limited to 'docs/tomes/05-render.md') 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: `.readback-vkformat-.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 ` выполняет 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 ` 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=` 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 `. 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 `. 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 -``` +Горизонтальная плоскость карты — 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 `. 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 ` 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; его необходимо учитывать +при преобразовании в обычное изображение. -- cgit v1.2.3