Files
Syplheed-Reborn/docs/re/ship-placement-runtime-capture.md
MechaCat02 d9442a2106 ship_capture: bake e106 COMPLETE from the 2026-07-26 F10 capture — all 8 parts
The second F10 capture (ship at _m LOD distance) closes e106 entirely: all 8
parts placed including the bridge both earlier captures missed, cross-validating
the doc's 7 known translations to 0.1 units and adding brg_01 at the exact
centreline (0, 153.3, -164.0).

Matching hardened by what the real capture taught us:
- LOD-aware: each part tries every variant vcount (base/_m/_l/_d) — a distant
  ship draws its LOD copies, same local frame.
- Position-validated, SET-based, against the UNION of a part's variants: buffer
  order != decode order, and one draw's vcount equalled the _m count while its
  buffer held the FULL 1633-vert geometry. A coincidental vcount (foreign
  51-vert mesh vs the bridge) is rejected by geometry.
- Runtime mirror handled: twins share one file geometry; the engine uploads the
  starboard copy X-reflected. Mirror-validated draws bake diag(-1,1,1) and the
  viewer reverses triangle winding for det<0 so front faces stay outward.
- Near-axis rotation entries snapped to exact 0/+-1 for a clean table; eng_01
  keeps its genuine 30-degree nacelle rotation.

data/ship_placements.txt now ships e106 (viewer renders via the captured table);
embedded_e106_is_complete locks the baked data. part_pos + capture_match helper
examples added. 10 ship_capture tests; 80 formats-lib tests green; viewer builds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 18:26:23 +02:00

171 lines
9.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Capital-ship part placement — runtime capture (ground truth)
**Status:****`e106` ADAN Destroyer FULLY BAKED (2026-07-26)** — all 8 parts
(incl. the bridge both earlier captures missed, and the runtime-mirrored
starboard hull) recovered from a second F10 capture and checked in to
`data/ship_placements.txt`; the viewer renders it via the captured table.
Remaining ships (e105, f101, f105/f106, turrets) need one capture each.
## 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).