Tier 3 matched a part to its hardpoint by trailing index, so `e105_brg` compared "01" == "" against GN_Bridge_01 and fell through silently. The runtime capture is what exposed it: the game draws the bridge and places it at [0, 70, -1850] rel e105_bdy_01, and assemble_ship emitted nothing there. With no index to match on, take the lowest-numbered frame of the category. Diffing assemble_ship part counts across every container: 34 (stage, ship) entries gain parts — e102 +2 (bridge and engine), e104 +1, e105 +1, Stages 02-29. ship_audit is unchanged, so nothing regressed, and the capture now agrees to dT 0.03 / dR 0.000. Also fixes the diff itself: correlate_frames compared static against a rotation sampled from the first block, which can belong to another INSTANCE of the class. Scoped to the position-agreeing cluster, e105_eng_01 goes 1.711 -> 0.000 and both e106 nacelles to 0.000. The one remaining rotation delta (e106_wep_02_01, 0.134) is a turret whose rotation varies by 0.182 between blocks that agree on its position — the runtime disagrees with itself more than with the assembler. The new rotVar column makes that distinction visible. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
15 KiB
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 — "Capital ships assemble wrong in the viewer",
reported 2026-07-30. The oracle and the correlator already exist
(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_05centroid[-1398 6251 918],dist=6540vs a cluster spread of1071. 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 inassemble_ship.f002_bdy_05is 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 thee901boss with 2–15 tracks per pose. Several of these (Missile_open, thee901_attack*poses) are obviously animation and harmless for a static pose; the plain hull rigsf104/f105/f106/e102are 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
— 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.shskipped 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 ascaptures/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 <id> 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 shaders0xDA51B0745ABF85D2(1258),0xE0BAFB4F520FE441(1091),0xEEA84C59D7F95371(770). None is thee106ship-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:
- 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; - the position-validation step rejects otherwise-correct vcount hits (the capture dumps ≤64 positions — a set-membership test against the wrong variant fails);
- the ships in view at launch are drawn by a different draw path than
e106in 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 -- <capture.log> Stage_S02 [top_n] [--ship f101]
cargo run --release --example vcount_index -- ../sylph_extract/hidden/resource3d <capture.log>
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:
f1011 of 15 resources had a drawn vcount (f101_bdy_03_l, 90 verts),e1053 of 37,e1063 of 34 — and each of those hits is a 44–225-vertex_l/_bpiece 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..c2WVP rows — so the capture path itself is healthy and unfiltered. - The screenshot (
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
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. 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
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
_mcopy (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 <Stage_S02.xpr> 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").
Both first-pass exceptions were chased down, and neither survives as an open question:
- ✅
e105_brgwas genuinely missing — a real assembler bug, now fixed. Tier 3 matched a part to itsGN_*hardpoint by trailing index, so an index-less part (e105_brg) compared"01" == ""againstGN_Bridge_01and fell through silently. With no index to match on, take the lowest-numbered frame of the category. The runtime is the check:e105_brgnow assembles at[0.0, 70.0, -1850.0]relative toe105_bdy_01, dT 0.03, dR 0.000 against the capture. Reach measured by diffingassemble_shippart counts over all containers before and after: 34 (stage, ship) entries gain parts —e102+2 (bridge and engine),e104+1,e105+1, across Stages 02–29. Every one of those ships was assembling without its bridge.ship_auditis unchanged (still exactly thef002_bdy_05outlier), so nothing regressed. - ✅ The rotation deltas were an artefact of my own aggregation, plus one real
articulation. The static diff was comparing against a rotation taken from the
first sampled block, which can belong to another instance of the class; scoping it
to the position-agreeing cluster drops
e105_eng_01from dR 1.711 to 0.000 and bothe106nacelles to 0.000. What remains ise106_wep_02_01at dR 0.134 — and that part's rotation varies by 0.182 between blocks that agree on its position, i.e. the runtime disagrees with itself more than it disagrees with the assembler. It is a turret aiming, not an assembly error.correlate_framesnow prints thatrotVarcolumn precisely so "the part moved" cannot be mistaken for "the rotation is wrong".
Final numbers, three classes, 21 parts: worst dT 0.43, worst dR 0.000 for every part that is not articulating.
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;e106has 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. Thee106-derived rules generalise; the MULTIKEY hypothesis in §1 is not needed to explain anything observed so far (f105has 2 multikey tracks and still matches exactly). - ✅ One real assembler bug found and fixed by this route: index-less
brg/eng/sldparts never matched theirGN_*frame, so 34 (stage, ship) entries assembled without a bridge (ande102also without its engine). Verified against the capture. - ▶ Next: the viewer itself (
include_external, node-instance recursion) — the format layer is now measured, not assumed. A per-ship regression table over the checked-in captures would keep it that way.