Did the visual the backlog kept naming as the next step, and it overturns the
conclusion. Rendering e106 from the static assembly and from the baked runtime
capture: the capture has 8 placements, the static assembly 11 -- the same 8 plus
e303_wep_01 x2 and a second e106_eng_01. In the render the destroyer sits inside
a white slab that dwarfs it.
That slab is e303_wep_01: 172 verts, 110 tris, bounds X[-1000,600] Y[-1050,1050]
Z[-2400,2400] = 1600 x 2100 x 4800. A real e106 turret (wep_02_01) is 269x179x417
with 772 tris; the bridge is 105x76x305. So it is not a turret -- it reads as a
collision/trigger volume, and whatever it is the game does not draw it: the e106
capture contains no e303_wep_01 at all.
Why it was missed: assemble_ship treats every rou_* composite node as drawable,
the cross-id mount is documented as intended, and the test asserts
count("e303_wep_01") == 2. The capture's silence was explained as vbase dedup,
but dedup would show one instance, not zero -- and the test walks capture parts
looking them up in the static output, so EXTRA static placements cannot fail it.
Same shape as the earlier include_external gap: a test that can only fail one way.
Scope stated carefully: sweeping 335 ships for "ship-scale span with <400 tris"
flags 20 ships and 58 placements over 28 resources, but ONLY the e106 case is
proven (render + capture absence + geometry). Others may be legitimately large
low-poly parts and need the same three checks.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
211 lines
11 KiB
Markdown
211 lines
11 KiB
Markdown
# RE backlog
|
||
|
||
Open items that are *not* being worked right now. Each entry says what is wrong or
|
||
unknown, what evidence exists, and what the first step would be. Move an item into
|
||
`INDEX.md` (with a `structures/…md` or a parser + test) once it is actually settled.
|
||
|
||
---
|
||
|
||
## Capital ships assemble wrong in the viewer
|
||
|
||
**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.
|
||
|
||
One real format-side bug was found on the way and is **fixed**: index-less parts
|
||
(`e105_brg`) never matched their `GN_Bridge_01` hardpoint, so 34 (stage, ship) entries
|
||
— `e102`, `e104`, `e105` across Stages 02–29 — assembled without a bridge. The other
|
||
apparent exception (`e105_eng_01` rotation) was an aggregation artefact and is 0.000.
|
||
|
||
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
|
||
wrong place / wrong orientation.
|
||
|
||
**Why this is a real finding and not a known limitation:** the RE write-up
|
||
[`ship-placement-runtime-capture.md`](ship-placement-runtime-capture.md) declares
|
||
static assembly ✅ **exact** as of 2026-07-26 — 9-channel joint tables
|
||
`[TX TY TZ RY RX RZ SX SY SZ]`, Euler `Ry·Rx·Rz`, with
|
||
`ship::tests::static_assembly_matches_runtime_capture` asserting static == runtime
|
||
capture (T < 1.0, R < 0.02). So either the viewer is not using that path, or the
|
||
claim generalises worse than the test suggests.
|
||
|
||
**The likely gap:** that test is **one ship** — the `e106` destroyer, 8 parts plus
|
||
two nacelles, two turrets and the hull mirror. Nothing pins the other classes.
|
||
Rules that were derived from `e106` and could easily be `e106`-specific:
|
||
|
||
- the engine cluster rig mounted at `GN_Engine_01` (two mirrored nacelles + centre);
|
||
- "X-reflect the shared-geometry twin whose lateral offset opposes the geometry's
|
||
dominant side" — a heuristic, not a decoded flag;
|
||
- cross-id turret instancing (×2).
|
||
|
||
**First step (the oracle already exists):** re-run the runtime capture on a *different*
|
||
capital ship and diff static vs captured, exactly as `e106` was done — F10 in the
|
||
`capture-ship-placement` build of `xenia-canary-native` dumps the ship shader's
|
||
`c0..c2` WorldViewProjection rows per part; `WV_ref⁻¹ · WV_p` is the ship-space rigid
|
||
transform, which is ground truth. Pick a class whose rig differs from `e106`
|
||
(different engine count, a ship with no `sld`, a carrier). Then extend
|
||
`static_assembly_matches_runtime_capture` into a per-ship table so a regression in one
|
||
class cannot hide behind `e106` passing.
|
||
|
||
**Also worth ruling out first, cheaply:** that the viewer's own transform stack (scale,
|
||
handedness, node-instance recursion) is not re-breaking a correct assembly — compare
|
||
the viewer's placement against `assemble_ship`'s output directly before blaming the
|
||
format layer.
|
||
|
||
---
|
||
|
||
## Viewer: `include_external` is already on — that hypothesis is dead
|
||
|
||
**Checked 2026-08-11.** The item above names "first that it passes
|
||
`include_external = true` (`iso_loader.rs:4012`)" as the cheap first step. It
|
||
does: `ShipBrowser::show_external` defaults to `true`
|
||
(`iso_loader.rs:643`), the checkbox reads it (`ui.rs:1593`) and it is threaded
|
||
through `RequestShipRender` → `build_ship_model` → `assemble_ship` unchanged
|
||
(`ui.rs:1689`, `iso_loader.rs:4012`). So a ship rendered by the viewer is the
|
||
full external assembly, not the bare hull.
|
||
|
||
The viewer also does not have a transform stack of its own to blame: it bakes
|
||
`ScenePart::apply` straight into the vertices and rotates normals by the same
|
||
`p.m` (`iso_loader.rs:4030-4062`), so its placement is `assemble_ship`'s output
|
||
by construction. What remains unexcluded, in order of cheapness: the mirror
|
||
handling (`det < 0` reverses triangle winding only — a reflected part keeps its
|
||
reflected geometry), `Xbg7Model::models_named` resolving the wrong sub-model when
|
||
a resource name repeats, and the exhaust cones. **Next step is a visual**: the
|
||
diagnosis has run out of things it can settle by reading, so the viewer needs to
|
||
be run against a known-good class (`e106`) and its render compared with
|
||
`ship_render`'s.
|
||
|
||
---
|
||
|
||
## Viewer: the duplicate-resource-name hypothesis is dead too
|
||
|
||
**Checked 2026-08-11.** The diagnosis above left three candidates for why capital
|
||
ships assemble wrong in the viewer: mirror handling, `Xbg7Model::models_named`
|
||
resolving the wrong sub-model when a resource name repeats, and the exhaust
|
||
cones. The second is now **refuted**, and comprehensively.
|
||
|
||
`build_ship_model` resolves each placement with
|
||
`base.iter().find(|m| m.name == p.resource)` (`iso_loader.rs:4041`) — first match
|
||
wins — so a repeated resource name inside a container would silently draw the
|
||
wrong geometry. It cannot happen: decoding **every** XBG7 resource in **all 22
|
||
stage containers** gives **4 603 resources and zero repeated names**.
|
||
|
||
```
|
||
Stage_S01 62/62 Stage_S07 323/323 Stage_S13 290/290 Stage_S25 351/351
|
||
Stage_S02 304/304 Stage_S08 388/388 Stage_S14 22/22 Stage_S26 318/318
|
||
Stage_S03 214/214 Stage_S09 316/316 Stage_S15 386/386 Stage_S27 321/321
|
||
Stage_S04 179/179 Stage_S10 7/7 Stage_S16 65/65 Stage_S28 118/118
|
||
Stage_S05 92/92 Stage_S11 157/157 Stage_S24 162/162 Stage_S29 386/386
|
||
Stage_S06 266/266 Stage_S12 376/376
|
||
```
|
||
|
||
Per-ship it is tighter still: `e106` wants 9 distinct names and decodes exactly
|
||
9 models for 11 placements; `e105` 9 for 9; `f105` 5 for 6. Every placement
|
||
resolves to the one model it names.
|
||
|
||
**So two of the three candidates are gone** (this one and `include_external`),
|
||
leaving **mirror handling** and **the exhaust cones** — and the still-untried
|
||
visual comparison, which remains the right next step.
|
||
|
||
---
|
||
|
||
## Viewer: mirror handling and the exhaust cones are cleared too — the static avenue is exhausted
|
||
|
||
**Checked 2026-08-11.** Both remaining candidates were tested across every ship
|
||
on the disc, and neither shows the reported signature.
|
||
|
||
**Mirror handling.** The concern was that `ScenePart::apply` bakes `R·(S·v)+T`
|
||
while the viewer takes its winding-flip decision from `det(m)` alone and rotates
|
||
normals by `m` alone — both ignoring `s`. A mirror encoded as a *negative scale*
|
||
would then reflect geometry without flipping winding, drawing the part
|
||
inside-out. It never happens: across **1 485 assembled parts** in all 22
|
||
containers there are **22 mirrored parts, every one with `det(m) < 0`**, and
|
||
**zero** parts with a negative scale or a non-uniform one. `apply_twin_mirrors`
|
||
writes the reflection into `m` (negating its X column), so the viewer's flip
|
||
always fires, and ignoring `s` for normals is harmless because `s` is always
|
||
uniform.
|
||
|
||
**Exhaust cones.** These are the one piece of geometry the viewer *invents* — a
|
||
cone at each `GN_Jet`/`GN_SJet` frame, because the real engine geometry is
|
||
recessed and the game draws FX there instead. If they landed wrongly they would
|
||
read exactly as "a part in the wrong place". Across **335 assembled ships, 192 of
|
||
which have exhaust frames, not one cone sits outside its hull's bounding box**
|
||
(tolerance 10 % of the axis span).
|
||
|
||
**Caveat, stated rather than glossed:** "inside the hull box" does not prove a
|
||
cone is *right* — orientation and size are untested, and a cone could be wrong
|
||
while still inside. What it does rule out is the reported symptom for that part.
|
||
|
||
So every mechanism this diagnosis proposed is now eliminated: `include_external`,
|
||
duplicate resource names, mirror handling, and cones-in-the-wrong-place. The
|
||
format and assembly layers pass every static test available, and **the visual
|
||
comparison is no longer merely the next step — it is the only remaining one.**
|
||
Render `e106` in the viewer beside `ship_render`'s output of the same
|
||
`assemble_ship` result; if they agree, the bug is in neither and the original
|
||
report needs re-grounding against a specific ship and a specific expectation.
|
||
|
||
---
|
||
|
||
## ⚠️ The format layer is NOT exonerated — `assemble_ship` draws a non-renderable volume
|
||
|
||
**Found 2026-08-11 by finally doing the visual**, which the notes above kept
|
||
naming as the next step. It overturns their conclusion.
|
||
|
||
Render `e106` from the static assembly and from the baked runtime capture and
|
||
compare — `ship_render` does both:
|
||
|
||
| | placements | parts |
|
||
|---|---|---|
|
||
| runtime capture (ground truth) | **8** | `bdy_01…04`, `brg_01`, `eng_01`, `eng_02`, `wep_02_01` |
|
||
| `assemble_ship(--static)` | **11** | the same 8, **plus `e303_wep_01` ×2** and a second `e106_eng_01` |
|
||
|
||
The render makes it obvious: the destroyer sits inside a white slab that dwarfs
|
||
it ([capture](captures/e106-static-assembly-volume-bug.png)). That slab is
|
||
`e303_wep_01`, and its own geometry is:
|
||
|
||
```
|
||
e303_wep_01 172 verts, 110 tris bounds X[-1000, 600] Y[-1050, 1050] Z[-2400, 2400] 1600 x 2100 x 4800
|
||
e106_wep_02_01 1002 verts, 772 tris 269 x 179 x 417 ← what a real e106 turret looks like
|
||
e106_brg_01 202 verts, 202 tris 105 x 76 x 305
|
||
```
|
||
|
||
**110 triangles, perfectly round axis-aligned bounds, and bigger than the ship it
|
||
is mounted on.** That is not a turret. 🟡 It reads as a collision / trigger
|
||
volume, and ✅ whatever it is, **the game does not draw it**: the runtime capture
|
||
of e106 contains no `e303_wep_01` at all.
|
||
|
||
### Why this was missed
|
||
|
||
`assemble_ship` treats **every** `rou_*` node in the composite as a drawable
|
||
part, and the doc comment states the cross-id mount as intended behaviour —
|
||
`"INCLUDING repeated instances and cross-id turret mounts (rou_e303_wep_01_root
|
||
×2 on the e106 hull)"` — with
|
||
`ship::tests::static_assembly_matches_runtime_capture` asserting
|
||
`count("e303_wep_01") == 2`. The absence from the capture was explained away as
|
||
vbase dedup, but **dedup would show one instance, not zero**.
|
||
|
||
The test cannot catch it either: it walks the capture's parts and looks each up
|
||
in the static output, so **extra** static placements are invisible to it. That is
|
||
the same shape of gap as the earlier `include_external` hypothesis — a test that
|
||
can only fail one way.
|
||
|
||
### Scope, stated carefully
|
||
|
||
Sweeping all 335 assembled ships for the signature *ship-scale span with under
|
||
400 triangles* flags **20 ships and 58 placements** over 28 distinct resources
|
||
(`e005_ant_*`, `f001_ant_*`, `f002_bdy_*`, `f301_barrel`, `f303_body`,
|
||
`e303_wep_01`, …). **Only the `e106`/`e303_wep_01` case is proven** — by render,
|
||
by capture absence, and by geometry. Some of the others may be legitimately large
|
||
low-poly parts, and each needs the same three checks before being called a bug.
|
||
|
||
**Next:** decide the rule that separates drawable parts from volumes (candidates:
|
||
the node-name suffix, a descriptor flag, or the triangle-density heuristic), then
|
||
re-point `static_assembly_matches_runtime_capture` so it also fails on extras.
|