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

9.0 KiB
Raw Blame History

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_captureCapturedDraws, 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).