# Capital-ship part placement — runtime capture (ground truth)

**Status:** ✅✅ **STATIC ASSEMBLY IS EXACT — no captures needed anymore
(2026-07-26).** The capture became the oracle that cracked the static encoding:
the node joint tables are **9 keyframe channels `[TX TY TZ RY RX RZ SX SY SZ]`
with the 8 table pointers shifted one track forward** (channel 0 = TX sits at
`ptr[0]−0x48`; the old 8-slot read took TY as X, never saw TX — why hulls
stacked on the centreline). Euler order `Ry·Rx·Rz`, angles direct
(`mesh.rs::read_trs9` / `node_rotation`). `assemble_ship` now places every
composite-node instance (cross-id turrets ×2), mounts the `e_rou_<id>_eng`
cluster rig at `GN_Engine_01` (two mirrored nacelles + centre engine), puts
`brg`/`sld` at their exact GN frames, and X-reflects the shared-geometry twin
whose lateral offset opposes the geometry's dominant side.
`ship::tests::static_assembly_matches_runtime_capture` asserts static ==
captured for all 8 e106 parts (T < 1.0, R < 0.02) + both nacelles + both
turrets + the hull mirror. The viewer uses pure static assembly; the baked
table + correlator remain as the verification oracle only.

## Problem

Capital ships ship as **split XBG7 parts** (hull bodies, bridge, engines, turrets)
in `hidden/resource3d/Stage_SNN.xpr`. Reconstructing the whole ship needs each
part's placement. Static reverse-engineering of the composite scene graph
(`e_rou_<id>`, `GN_*` frames) got the **hull right but external parts only
approximately** — a `GN_*` frame is the mount *pivot*, and the real outboard /
rotational offset lives in a detail sub-rig or in runtime code not recoverable
statically. See `sylpheed-formats::ship::assemble_ship` (the static best-effort).

## Finding

The **guest vertex buffer holds LOCAL coordinates** — byte-identical to the
`.xpr` (verified: `e106_bdy_04`'s first vertex `(-20.0000,153.1539,45.9997)`
matches the decode exactly). So parts are placed **entirely in the vertex
shader**; the buffer carries no placement. (Contrast: static *stage* geometry is
pre-transformed into world space and byte-matches the `.xpr` at file offsets —
that's a different case.)

The per-part transform is in the **vertex-shader float constants**. For the ship
shader (`vs=0xC7F781F4C1D58054`), constants `c0..c2` are the **WorldViewProjection**
matrix rows:

- Their norms are `(sx, sy, 1)` = the projection x/y scales; `sy/sx ≈ 1.78 = 16:9`
  aspect. Dividing each row by its norm gives the rigid **WorldView** (verified:
  normalized rows orthonormal to 1e-4, `det = +1`).
- The camera View cancels when expressing every part relative to a reference part:
  `rel_p = WV_ref⁻¹ · WV_p` = `(Rᵀ_ref·R_p , Rᵀ_ref·(T_p − T_ref))` — a pure
  ship-space rigid transform. Applying `rel_p` to part `p`'s local geometry
  assembles the ship exactly, in the reference part's frame.

(`c4..c14` also vary per part — likely a normal matrix / a second WVP variant /
WorldView split; not needed, `c0..c2` suffice.)

## Capture instrumentation

Branch **`capture-ship-placement`** in the `xenia-canary-native` worktree (off the
`instrument-current`-descended native branch, which already has GPU `log_draws`).

- **F10** in the emulator window → `xe::gpu::RequestShipCaptureFrame()`.
- Dumps the next ≤8000 draws (de-duped by vertex-buffer address) to
  **`xenia_ship_capture.log`** in the binary's dir
  (`build/bin/Linux/Release/`). Per draw:
  ```
  DRAW vbase=0x… stride=… vcount=… indices=… prim=… vs=0x…
    pos: (x,y,z) …            up to 64 LOCAL vertex positions (== .xpr)
    vsconst base=N: c0=(x,y,z,w) c1=… …   first 48 VS float4 constants
  ```
- Files: `src/xenia/gpu/command_processor.{cc,h}` (`CaptureShipDrawForRE`,
  `RequestShipCaptureFrame`), `src/xenia/app/emulator_window.cc` (F10 key).
- Run: `run-canary-native.sh` (HW Vulkan, interactive). Play into the mission,
  frame the ship side-on, press F10.

## Correlator (now a library module)

The correlation math lives in **`sylpheed-formats::ship_capture`** (unit-tested with
synthetic captures): `parse_capture` → `CapturedDraw`s, `correlate(id, draws,
parts, ref_sub)` → a `ShipPlacement` (each part in the reference frame), and
`serialize_table`/`parse_table` for the checked-in text table.

Two capture formats are accepted (auto-detected): the F10 ship-capture snapshot
([`parse_capture`]) and the **draw-logger** format `mission_draws.log`/
`xenia_re_draws.log` (`parse_drawlog` — groups the ship shader `SHIP_VS_HASH`'s
draws by vertex-buffer `base`, `vcount = size_words/stride_words`). So a capital
ship seen in a normal instrumented run can be baked without a dedicated F10 pass.

### Matching rules (learned from the real 2026-07-26 capture)

- **LOD-aware**: a distant ship draws its `_m`/`_l` copies — same part, same
  local frame, different vcount. Each part tries every variant vcount.
- **Position-validated**: a vcount hit alone can be a coincidence (a foreign
  51-vert mesh nearly hijacked the bridge slot). Every candidate draw's dumped
  positions must be found in the part's decoded position set or it is rejected.
  Validation is **set-based** (buffer order ≠ decode order) against the **union**
  of the part's variants — the real capture had a draw whose `vcount` equalled
  the `_m` count while its buffer held the FULL-detail geometry.
- **Runtime mirror**: a port/starboard pair shares one file geometry; the engine
  uploads the second instance **X-reflected**. A draw that validates only under
  X-negation is accepted as the mirror, and the placement bakes `diag(−1,1,1)`
  (the viewer reverses triangle winding for det < 0).
- The mission ship shader can be a different hash per lighting variant
  (`0x3A0829CC71516789` in this capture) — the F10 path doesn't filter by hash,
  it validates by geometry instead.

`examples/correlate_capture.rs` is the CLI wrapper:
```
SYLPHEED_ISO=… cargo run --release --example correlate_capture -- \
    <capture.log> <Stage_SNN> <ship_id> [ref_part_substr] [--emit]
```
Decodes the ship's base parts (for their vertex counts = the match key), correlates,
prints each part's ship-relative transform, and with `--emit` prints the `ship …`
block to paste into the placement table.

## Baked table + viewer preference

`crates/sylpheed-formats/data/ship_placements.txt` is the checked-in placement
table (embedded via `ship_capture::embedded_placement`). The viewer's
`build_ship_model` uses a ship's captured entry when present, else falls back to
the static `assemble_ship` (`external` toggle). **The table is currently empty** —
a real F10 capture log is needed to bake in `e106` (and others); paste the
`--emit` block into that file and rebuild.

## Validated result — `e106` ADAN Destroyer (Mission 1 / Stage_S01, side view)

Vertex-count → part map (all matched exactly): bdy_01/02 = 1261, bdy_03 = 815,
bdy_04 = 1633, eng_01 = 583, eng_02 = 491, wep_02_01 = 1002, brg_01 = 202.

Ship-relative placement (reference = `bdy_04`, aft hull):

| part | T (ship-relative) | note |
|---|---|---|
| bdy_04 | (0, 0, 0) | reference, aft hull |
| eng_01 | (131, −133, −132) | **aft** (−Z), starboard | 
| eng_02 | (0, −49, −140) | aft, centre |
| bdy_01 | (**−264**, −150, 1165) | **port** hull, forward |
| bdy_02 | (**+264**, −150, 1165) | **starboard** hull, forward |
| bdy_03 | (0, −35, 1076) | forward keel/spine |
| wep_02_01 | (0, 49, 1009) | forward weapon |

Assembled extent **872 × 670 × 2181** (a coherent ~2181-long destroyer).

**Key wins vs static:** `bdy_01`/`bdy_02` are a **port/starboard pair** (X = ∓264),
which static had overlapping at one point; engines land correctly aft; the ship is
not the 1894-tall tower the static Y put out.

**Missing:** `brg_01` (bridge, vcount 202) was **culled/occluded** at that camera
angle (0 draws) — needs a second capture framing the superstructure.

## What the static assembler gets wrong (measured, `e106` in Stage_S01)

Decoding the real container (`examples/ship_dump.rs`) shows the static
`assemble_ship` is **incomplete** — the capture is required to fix all of:

- **`bdy_01` / `bdy_02` overlap**: both get the *same* composite transform
  (T = (−116, 0, 857)); the real ship is a **port/starboard pair** at X = ∓264
  (the mirror is applied at runtime, like the DeltaSaber fins in `saber_measured`).
- **`eng_02` and `wep_02_01` are never placed**: they aren't `rou_` hull nodes and
  aren't in the external category list, so they drop out entirely.
- Only `brg_01` + `eng_01` get the approximate `GN_*`-frame treatment.

So even the *hull* tier is not fully correct, and no static signal supplies the
missing offsets (verified: the part vertex buffers are pure local coords). The
capture-driven table is the only path to correctness — there is nothing more to
squeeze statically.

> Offline status: the draw logs on this box are the **player fighter**, not a
> capital ship, so no capital-ship table can be baked here yet. Capture `e106`
> (Stage_S01, side-on) on the emulator box — F10 **or** just save
> `mission_draws.log` with the ship on screen — then `correlate_capture … --emit`.

## Next steps

1. ~~**Bake:** promote the correlator to a module + checked-in table + viewer
   preference.~~ **DONE** (`ship_capture` module, `data/ship_placements.txt`,
   `build_ship_model` prefers it). Remaining: run the captures below and paste the
   `--emit` blocks in — needs the emulator (F10), can't be done offline.
2. **e106 + bridge:** re-capture `e106` (Stage_S01) framing the superstructure so
   `brg_01` (culled last time) is included, then `--emit` → bake.
3. **Other ships:** one side-on F10 capture each for the cruiser (`e105`,
   Stage_S02), ACROPOLIS (`f101`), TCAF cruiser/destroyer (`f105`/`f106`).
4. **Turrets:** the shared `e3NN`/`e4NN` gun models appear as their own draws in the
   capture too — correlate them to place turrets (static never resolved these).
