Promote the F10 ship-capture correlator from an example into a reusable, unit- tested library module (ship_capture): parse_capture (log -> per-draw rigid WorldView from the c0..c2 WVP constants), correlate (match parts to draws by vertex count, express each in the reference part's frame), and a checked-in placement-table format (serialize_table/parse_table, embedded via embedded_placement from data/ship_placements.txt). build_ship_model now prefers a ship's captured placement over the static assemble_ship when a table entry exists (empty table -> unchanged fallback). correlate_capture example refactored onto the module + gains --emit to print a table block. Doc updated: bake plumbing done; remaining is running real F10 captures to populate the table (needs the emulator). 5 new ship_capture tests (parse/normalize/correlate/table round-trip); 76 formats-lib tests + viewer build green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
124 lines
6.2 KiB
Markdown
124 lines
6.2 KiB
Markdown
# Capital-ship part placement — runtime capture (ground truth)
|
||
|
||
**Status:** pipeline working end-to-end for one ship (`e106` ADAN Destroyer);
|
||
correlator recovers exact ship-relative transforms. **Bake plumbing landed** — the
|
||
correlator is now a tested library module and the viewer prefers a captured table;
|
||
only real capture data still needs baking in (needs the emulator + F10).
|
||
|
||
## 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.
|
||
|
||
`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.
|
||
|
||
## 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).
|