# Capital-ship placement β€” does the `e106` result generalise? (WIP, 2026-07-31) **Status:** 🚧 **WIP, time-boxed session.** Two results so far: a static audit across all 22 stage containers (done, below) and a first in-mission F10 capture run in Stage 02 (done β€” three capture logs, but **no capital-ship part correlated**; see "Open"). Context: [`BACKLOG.md`](BACKLOG.md) β€” "Capital ships assemble wrong in the viewer", reported 2026-07-30. The oracle and the correlator already exist ([`ship-placement-runtime-capture.md`](ship-placement-runtime-capture.md)); the open question is whether the rules derived from the one validated ship (`e106`, Stage_S01) hold for other classes. ## 1. Static audit across all stages (offline, reproducible) ``` cargo run --release --example ship_audit -- ../sylph_extract/hidden/resource3d ``` 87 lines of output, of which: - **Only two OUTLIER lines, and they are the same ship twice**: `Stage_S03`/`Stage_S27`, `f002_bdy_05` centroid `[-1398 6251 918]`, `dist=6540` vs a cluster spread of `1071`. Every other assembled ship in every other stage has all parts inside its own cluster. β†’ **The user-visible breakage is NOT a gross static-placement outlier for most classes.** Whatever is wrong in the viewer is either subtler than "part flung far away" (wrong rotation, wrong mirror, missing part) or lives in the viewer, not in `assemble_ship`. `f002_bdy_05` is a genuine, separate, reproducible static bug. - **MULTIKEY**: joint tracks with more than one keyframe, which `read_trs9`'s single-key read does not model. Recurring rigs: `e_rou_f104` (3), `e_rou_f105` (2), `e_rou_f106` (2), `e_rou_e102` (6), `e_rou_e108_Missile_open` (2), `e_rou_e501` (1), and the `e901` boss with 2–15 tracks per pose. Several of these (`Missile_open`, the `e901_attack*` poses) are obviously *animation* and harmless for a static pose; the plain hull rigs `f104`/`f105`/`f106`/`e102` are **not** obviously animation and are the best hypothesis for a class-specific assembly error. ❔ **HYPOTHESIS β€” not verified.** `e106`, the one validated ship, has **no** multikey tracks, which is exactly how a rule that only works for single-key rigs could have passed unnoticed. Raw audit output is reproducible with the command above (not checked in; it is deterministic from the disc). ## 2. First in-mission capture run (Stage 02) New tool: [`tools/re-capture/ship_capture_session.sh`](../../tools/re-capture/ship_capture_session.sh) β€” one blocking session (per the session-lifetime rule): boot β†’ Stage 02 in flight β†’ NΓ— {screenshot, F10, small yaw}. Each F10 writes its own `xenia_ship_capture_NN.log` next to the binary. Run 2026-07-31, 5 presses requested: - **Boot to in-flight took 24 s** (`skip_intro.sh` skipped the movie at 1 s and 6 s, title at 11 s, HUD shield bar at 24 s) β€” much faster than the ~100 s in the notes. - **3 of 5 F10 presses produced a log** (`_01`…`_03`, 2964 / 3111 / 3668 draws). Logs (8–10 MB each) and the screenshots are at `/sylph-home/re/shipcap/`; not committed for size. One screenshot is checked in as [`captures/shipcap-stage02-launch.png`](captures/shipcap-stage02-launch.png). - The screenshot confirms the capture frames are real in-mission combat frames (HUD live, `REMAINING OB 004`, ACROPOLIS + a Destroyer labelled on screen, a capital-ship hull filling the bottom of the frame). ### Result: no correlation yet ❌ ``` correlate_capture xenia_ship_capture_03.log Stage_S02 bdy_01 ``` for `f101` (ACROPOLIS), `f105`, `f106`, `e105` reports *"no draw matches any LOD (culled/off-screen?)"* for essentially every part β€” only two speculative LOD tries (`f101_bdy_03` vcount 90 `[l]`, `e105_wep_01` vcount 60 `[l]`) and **zero accepted matches**. That is a **negative result, and it is not yet explained**. Facts collected: - The capture is not empty or degenerate: 3668 draws in `_03`, top shaders `0xDA51B0745ABF85D2` (1258), `0xE0BAFB4F520FE441` (1091), `0xEEA84C59D7F95371` (770). None is the `e106` ship-shader hash from the 2026-07-26 capture; the F10 path does not filter by hash, so this alone is not the cause. - Large vertex counts *are* present (3024, 2772, 1736, 1612, 1240 …), so capital-ship- sized geometry is being drawn. Candidate explanations, **untested**: 1. the Stage-02 capital ships on screen are drawn from LOD/damage variants (`_d00`, `_m`, `_l`) whose vcounts the correlator's variant list does not cover; 2. the position-validation step rejects otherwise-correct vcount hits (the capture dumps ≀64 positions β€” a set-membership test against the wrong variant fails); 3. the ships in view at launch are drawn by a *different* draw path than `e106` in Stage_S01 (e.g. instanced/batched), so no single draw equals one part. **First step next session:** take the largest few vcounts in the capture and ask which decoded part in `Stage_S02.xpr` has that count (invert the match), instead of asking per-part whether a draw exists. That distinguishes (1)/(2) from (3) immediately. ## 3. The inverted match β€” the ships were never drawn (2026-08-10) βœ… explained The inversion was run and it settles the negative result. Two new tools: ``` cargo run --release --example invert_capture -- Stage_S02 [top_n] [--ship f101] cargo run --release --example vcount_index -- ../sylph_extract/hidden/resource3d ``` `invert_capture` asks, of the capture's own vertex counts, which resource in one stage container has that count; `vcount_index` asks the same across **all 166 containers** (5480 resources), so a draw whose geometry lives in `Common.xpr`, a `rou_*` weapon pack or a `DeltaSaber_*` player-craft pack is identified instead of coming back "unknown". On `xenia_ship_capture_03.log` (3668 draws, Stage 02): | capture vcount | draws | what it is | |---|---|---| | 10891 | 28 | **`DeltaSaber_T:f001`** β€” the player's own craft | | 6000 | 14 | `Stage_S02:n006_02` β€” backdrop | | 1096 / 1008 / 841 / 215 / 127 | 14–112 | `rou_f001_wep_*` β€” the player's weapons | | 417 / 279 / 201 / 167 | 104–448 | `Base:j00*`, `ptc_pack:*` β€” HUD/particles | | 8 / 4 / 3 / 1 | 317–590 | particle quads | - **Not one capital-ship hull part appears.** Per ship: `f101` **1 of 15** resources had a drawn vcount (`f101_bdy_03_l`, 90 verts), `e105` 3 of 37, `e106` 3 of 34 β€” and each of those hits is a 44–225-vertex `_l`/`_b` piece whose count also collides with dozens of unrelated resources, i.e. probably not even the ship. - The 3668 draws span **~14 frames** per F10 press and use only **10 distinct vertex shaders**, and the player's own craft is captured at **full detail with its `c0..c2` WVP rows** β€” so the capture path itself is healthy and unfiltered. - The screenshot ([`captures/shipcap-stage02-launch.png`](captures/shipcap-stage02-launch.png)) agrees once read carefully: the hull "filling the bottom of the frame" is the **player's own craft** in the chase view. The nearest contact on the HUD is a wingman's engine trail. **So hypothesis (3) is dead, and (1)/(2) never applied.** The correlator's message "no draw matches any LOD (culled/off-screen?)" was literally true: the ships were far enough away that the renderer drew nothing of them. `correlate` additionally cannot anchor without the reference part, and `f101_bdy_01` was never drawn at any LOD. **The variable that was never controlled is RANGE.** New tooling closes that gap: [`tools/re-capture/approach_capture.py`](../../tools/re-capture/approach_capture.py) locks onto a capital ship (definition size-radius β‰₯ 150 = not a fighter), flies at it with navigator.py's drift compensation and CPA avoidance, firing disabled, and presses F10 as each range band is crossed (8000 / 6000 / 4500 / 3000 / 2000 / 1400 / 900), stamping every capture with its distance in `approach-bands.jsonl`. Driver: [`ship_capture_close.sh`](../../tools/re-capture/ship_capture_close.sh). Besides giving the correlator a full-detail frame, the stamped bands measure the game's own **LOD ladder** per part, which the reborn renderer needs anyway. ## 4. Controlled-range capture β€” three classes verified (2026-08-10) βœ… `ship_capture_close.sh 240` ran one blocking session: boot β†’ Stage 02 in flight β†’ lock the `f105` cruiser β†’ close on it at full throttle, F10 at each range band. Six bands fired (7375 / 5937 / 4394 / 2994 / 1944 / 1259 units, stamped in `approach-bands.jsonl`); **3 of 6 presses produced a log** β€” the same 3-of-N as the earlier session, so a press during a previous 8 MB dump is still lost. Logs at `/sylph-home/re/shipcap-close/` (not committed, ~9 MB each). The difference from every earlier capture is immediate: `f105_bdy_01` (10926 verts), `bdy_02`, `bdy_03`, `eng_01`, `sld_01` are all **drawn at full detail**, and the far log additionally caught the `e105` and `e106` hulls. ### 4a. One log is ~14 frames, and mixing them silently corrupts the result `WV_ref⁻¹ Β· WV_p` cancels the camera **only within one frame**. The capture log has no frame delimiter, so `correlate_capture` was mixing ~14 frames; with the camera closing at ~760 u/s that is not a small error β€” two logs of the same cruiser disagreed by 1090 units on `f105_bdy_02`. New `ship_capture::segment_frames` splits the log wherever a vertex buffer recurs (over-splitting is harmless β€” a block is still one camera; under-splitting is what corrupts), and new [`correlate_frames`](../../crates/sylpheed-formats/examples/correlate_frames.rs) correlates each block independently and **cross-checks the blocks against each other**. Two aggregation rules had to be right, and both were wrong first: - **Only blocks with the requested reference part count.** A block that fell back to another reference expresses its parts in a different frame β€” averaging them in produces a "disagreement" of exactly the distance between the two references. - **Consensus, not median.** A stage holds several ships of one class; they share vertex buffers, and a block can hold one instance's full-LOD part beside another's `_m` copy (different buffers, so nothing splits them). The largest cluster of mutually-agreeing blocks is the placement; the rest are reported as `(+N other-instance)` rather than averaged into nonsense. With that, every part reproduces across independent frames to **≀1 unit** (typical spread 0.03–0.2). ### 4b. Static assembly matches the runtime on all three classes βœ… `correlate_frames … --static ` diffs `assemble_ship` against the capture, translation **and rotation**, both re-expressed in the reference part's frame: | ship | rig | parts compared | worst dT | worst dR | |---|---|---|---|---| | `f105` TCAF cruiser | 1 engine, mirrored `sld` pair | 5 | **0.12** | **0.000** | | `e105` ADAN cruiser | 6 hull bodies, bridge, engine | 7 | **0.05** | 1.711 (`eng_01` only) | | `e106` ADAN destroyer | 2 nacelles + centre, turret | 8 | **0.43** | 0.098 (`eng`/`wep` only) | **So the `e106` rules DO generalise.** Translation is exact for every part of every class β€” 20 of 21 comparisons under 0.5 units. This is the answer the BACKLOG item asked for, and it is the opposite of the assumption in it: `assemble_ship` is right, so the viewer's "capital ships assemble wrong" is the viewer's own transform stack (the backlog's own "worth ruling out first, cheaply"). Two real exceptions, both narrow: - **`e105_brg` is never produced by `assemble_ship`**, at either `include_external` setting, although the runtime draws it and places it at `[0.0, 70.0, -1850.0]` relative to `e105_bdy_01` (4/4 blocks, spread ≀0.13). ❔ A genuine missing part. - **Rotation differs only on parts that move**: `e106_wep_02_01` (turret, dR 0.093), `e106_eng_01`/`eng_02` (dR ~0.097) and `e105_eng_01` (dR **1.711**). A turret aiming and a nacelle gimballing at capture time is expected and is not an assembly error; 1.711 on `e105_eng_01` is too large for that and is ❔ **unexplained β€” NEEDS-HUMAN** (either a static rotation bug on that one part, or the part is articulated). `include_external` matters and is a caller-side trap: with `false` an `e106` assembles as **5** parts and with `true` as **11** β€” the engine cluster, the bridge and the cross-id `e303_wep_01` turrets live in separate composites (`e_rou_e106_eng`, 3 nodes) that the primary-composite pass never reaches. The viewer takes it as a parameter (`iso_loader.rs:4012`); if it is ever passed `false`, ships lose their engines and bridge β€” which looks exactly like "assembles wrong". ## Honest summary - βœ… Static assembly is **not** grossly broken across stages β€” 1 outlier ship (`f002_bdy_05`), reproducible. - 🟑 A concrete, testable hypothesis for class-specific breakage exists (multikey joint tracks on `f104`/`f105`/`f106`/`e102`; `e106` has none). - βœ… The earlier zero-match was **range**, not a format or correlator bug (Β§3): the captures were taken where no capital-ship geometry is drawn at all. - βœ… With range controlled (Β§4), **three classes** β€” `f105`, `e105`, `e106` β€” reproduce static assembly to **≀0.43 units** in translation, cross-checked across independent frames. The `e106`-derived rules generalise; the MULTIKEY hypothesis in Β§1 is *not* needed to explain anything observed so far (`f105` has 2 multikey tracks and still matches exactly). - ❌ Still open, both narrow and both evidenced: `e105_brg` is missing from `assemble_ship`, and `e105_eng_01`'s rotation differs by 1.711 β€” NEEDS-HUMAN. - β–Ά Next: chase those two, and point the viewer investigation at the viewer (`include_external`, node-instance recursion), not at the format layer.