re: control the range, segment the frames — f105/e105/e106 all match static assembly

A capture at controlled range (ship_capture_close.sh: lock a capital ship, close
on it, F10 per range band) finally draws capital-ship hulls at full detail. Two
correctness fixes were needed before the numbers meant anything:

* one F10 log is ~14 frames with no delimiter, and WV_ref^-1 . WV_p only cancels
  the camera within one frame — segment_frames splits on vertex-buffer
  recurrence, and correlate_frames cross-checks the blocks against each other
  instead of trusting a single shot;
* aggregate by consensus, not median: a stage holds several ships of one class
  sharing vertex buffers, so a block can mix two instances.

Result: f105, e105 and e106 reproduce assemble_ship to <=0.43 units in
translation and 0.000 in rotation for every part that does not move. The e106
rules generalise, and the viewer bug report now points at the viewer. Narrow
leftovers: e105_brg is missing from assemble_ship, e105_eng_01 rotation differs
by 1.711.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-10 18:19:26 +00:00
parent 1d4b35df0f
commit 3d9f21f030
5 changed files with 386 additions and 8 deletions

View File

@@ -8,7 +8,18 @@ unknown, what evidence exists, and what the first step would be. Move an item in
## Capital ships assemble wrong in the viewer
**Reported:** 2026-07-30, by the user. **Status:** ❔ open, not investigated.
**Reported:** 2026-07-30, by the user. **Status:** 🔎 **diagnosed 2026-08-10 — the
format layer is exonerated.** Runtime captures of three classes (`f105`, `e105`,
`e106`) at controlled range reproduce `assemble_ship` to ≤0.43 units in translation
and to 0.000 in rotation for every part that does not move; see
[`ship-placement-capture-generalisation.md`](ship-placement-capture-generalisation.md)
§4. So look at **the viewer**: first that it passes `include_external = true`
(`iso_loader.rs:4012` — with `false` an e106 loses its bridge and both nacelles,
5 parts instead of 11), then its own transform stack. Two narrow format-side leftovers
remain: `e105_brg` is never produced by `assemble_ship`, and `e105_eng_01`'s rotation
differs by 1.711.
The original report and its reasoning follow.
The reborn viewer builds capital ships from the split XBG7 parts via
`sylpheed-formats::ship::assemble_ship`, and they come out **wrong** — parts in the

Binary file not shown.

After

Width:  |  Height:  |  Size: 562 KiB

View File

@@ -138,16 +138,92 @@ stamping every capture with its distance in `approach-bands.jsonl`. Driver:
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.030.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").
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 runtime oracle has **not** reproduced on a second ship class yet — but the
reason is now known and is not a format or correlator bug: **the captures were taken
at ranges where no capital-ship geometry is drawn at all** (§3, verified by inverting
the match against all 5480 decoded resources). Do not assume the `e106` rules
generalise; equally, do not read the zero-match as evidence against them.
- ▶ Next: run `ship_capture_close.sh` so the capture happens with a hull actually on
screen, then re-run `correlate_capture` on the closest band.
- 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.