This repository has been archived on 2026-09-16. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Syplheed-Reborn/docs/re/BACKLOG.md
Sylpheed RE agent 2483b7c3c9 docs/re: my own flag hypothesis fails its test, and the input path is mapped
With the capture cvar gone from the launch line entirely, a fresh run reached the
boot title and (A) still did nothing. The flag is therefore not necessary for the
failure, and 0-of-7-with vs 4-of-6-without does not survive as an explanation.

The conclusion is the plain one: (A) at the title succeeds about half the time
and nothing measurable from outside predicts which. Five candidates are now
eliminated — Xenia UI swallowing the keystroke, the driver filter, a sign-in or
content call on the press, the age of the title, and the capture cvar.

So the guest side was traced instead. XamInputGetKeystrokeEx has exactly one
caller in the title, and that has one caller: entry_point -> main loop
(sub_8216EA68) -> per-frame input update (sub_822F1AA8) -> the pad poll
(sub_82457038). The poller's layout is readable and matches the runtime trace
(keystroke queue at +68/+72 drained with flags=3; current state at +36 compared
field-by-field against the previous at +52), and nothing in it is state-gated.
Whatever ignores (A) is a consumer further up — a real RE job, and that is where
the thread now stands, stated rather than guessed at again.
2026-08-19 00:47:53 +00:00

563 lines
31 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.
# 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.
---
## The declaration table is not a paint order on every screen
**Found 2026-08-17**, building the Explorer's UI Screens browser on
[`ui_layout`](structures/ui-rat-layout.md). **Status: 🔎 open — the pause menu is
right, the title screen is not.**
`ui-rat-layout.md` says the bundle's element declaration table lists elements
"in back-to-front order", verified 11/11 on the tutorial pause bundle. That
holds — the tutorial and in-mission PAUSE builds both composite correctly, and
`pgpeff02a` → parent 3 / `pgp_ttrl_btn10` at (546,288) / the 70 px button pitch
all reproduce exactly.
**`GP_TITLE.pak` build 7 does not.** Painting in declaration order puts
`ptbase2.t32` (the full-screen background art, element **13**) *on top of* the
`ptlogo1`/`ptlogo2` wordmarks (elements **05**), which the real title screen
obviously does not do. The pause bundles never caught this because their
elements barely overlap.
**What has been ruled out:** there is no depth/layer key in the 60-byte
declaration entry. Dumping every word across the title build's 30 entries, the
unknown fields are constant — `+28` is 0 everywhere, `+44` is `0xffffffff`
everywhere, `+56` is 0 everywhere — and `+36`, which the doc lists as
`0xffffffff`, is not a depth either: it is `0`/`1` **only** on the `kind = 0x4`
repeated-instance entries (`ptlogo1`/`ptlogo2` copies), i.e. an instance index.
So the order is not recoverable by sorting the table on any field it carries.
**What that leaves.** The background group is contiguous — elements 12, 13, 14
are `pteff00.prm`, `ptbase2.t32`, `pteff04.t32`, and 12 carries `kind = 0x10`,
a flag no pause element has (theirs are `0x0` / `0x1` / `0x3002`). `pteff02.prm`
at 17 has it too. So `0x10` marking a `PRMD` primitive, and primitives opening a
layer that draws beneath what precedes them, is the cheapest hypothesis — but it
is a **hypothesis**, and "draw the `.prm` group first" would fit this one screen
without being evidence of anything.
**First step:** composite `GP_MISSION_SELECT` / `GP_READY_ROOM` / `GP_OPTIONS`,
which have both a background and overlapping foreground elements, and see
whether their background sits at a `0x10`-adjacent index too. Two more screens
agreeing turns the hypothesis into a rule; one disagreeing kills it. The
Explorer's `screen render`/`screen info` commands make that a minute's work per
screen, and the per-element visibility toggles isolate a suspect element.
**Meanwhile** the viewer paints in declaration order and does not pretend
otherwise — a screen whose background lands on top is showing you this bug, not
a decode failure.
### 2026-08-18 — measured against the running game; three orderings refuted, and half the symptom was a different bug
**The premise is confirmed by the oracle**, which this entry had not had: a
framebuffer capture of Canary on the title screen
([`captures/title-screen-oracle.png`](captures/title-screen-oracle.png)) shows
the `PROJECT SYLPHEED` wordmarks (elements 05) drawn **over** `ptbase2.t32`
(element 13), which is a full-screen background. Declaration order is therefore
not the paint order on this screen, and no reading of the element table changes
that.
**But part of what the render showed was not the paint order at all.** In the
capture `ptbase2` covers the whole screen; the compositor drew it as a
960×540-visible slab starting at (320,180), because a keyframe's `scale` was
being grown from the keyframe's corner instead of about the declared **pivot**.
Fixed, and pinned against the capture by cross-correlation (peak at (0,0)) — see
[`structures/ui-rat-layout.md`](structures/ui-rat-layout.md). That was a real
defect worth separating out: it moves **865** of the disc's 5 130 resting
placements, on every screen, independently of any ordering question.
**Three candidate orderings are now dead**, all cheaply:
- **The placement region is not a second ordering.** Its keyframe groups carry an
explicit element index, so they *could* be stored in a different order — they
are not, on **every** build on the disc (`placement_region_order_is_never_a_second_ordering`,
>500 builds, identity every time).
- **The RATC child order is not it either.** For the title build it is the
declaration order with the `.prm` elements absent — strictly less information,
and it has no place to put `ptbase2` other than where the table already puts it.
- **Reverse declaration order is refuted by the same capture**: it would draw
`ptbase2` (13) over `ptcopyright` (28), and the copyright line is visible.
**The `0x10`-adjacency first step was run, and it does not survive.** The
background *is* adjacent to a `kind = 0x10` `.prm` element on both screens that
have one — but on **opposite sides**. `GP_TITLE` build 7 is
`12 pteff00.prm (0x10)`, `13 ptbase2.t32`, `14 pteff04.t32`;
`GP_MISSION_SELECT` build 0 is `0 px_mission_base.tbm`, `1 px_mission_eff00.prm
(0x10)`. So "the `.prm` opens a layer that draws beneath what precedes it" cannot
place both, and no rule keyed on the `.prm`'s position orders the background.
`GP_READY_ROOM` and `GP_OPTIONS` turned out not to be the third and fourth
witnesses this entry hoped for: neither of their largest builds carries a `.prm`
or a full-screen background at all, so they cannot discriminate.
**What is still open, stated plainly:** nothing in the bundle has been found that
orders element 13 behind elements 05. Every ordering the file itself carries is
now either identical to the declaration table or refuted by the capture. The next
step is no longer static — it is either the guest code that walks this table, or a
per-draw capture of the title screen showing the order the game submits.
**Blocker, checked rather than assumed.** The obvious move is to reuse Canary's
existing RE instrumentation, which is already in the built binary on
`sylpheed-re`. Neither hook can answer this:
- **`--log_draws`** (`command_processor.cc`) de-dups by a *vertex-declaration
fingerprint* — shader hash + primitive type + per-stream element
formats/offsets + index-buffer guest base — and writes each distinct one once.
A screen's sprites share a declaration, so they collapse; and the record
carries no texture identity and no per-frame submission order, only first-seen
order. It is a mesh-format log, not a draw-order log.
- **The F10 ship capture** does preserve per-draw order within a frame and
de-dups on `(vertex base, WVP transform, index range)`, which would separate
the elements — but it **explicitly drops UI draws**:
`if (pos_off_bytes < 0 …) return; // no float-position stream (UI/effects) —
skip`. It requires an `f32x3` position attribute, which a 2D quad stream does
not have.
So this needs a **new hook in Canary** — log each draw in submission order with
its bound texture fetch (or its screen-space quad), gated behind a cvar the way
the other two are — and therefore a `build-canary` run. That is the cost to
state up front rather than discover halfway in; it is not a container
limitation, just a long build plus a title-screen run.
### 2026-08-18 (later) — the hook was built and run; the order is now measured
`log_ui_draws` exists (Canary branch `auto/re-ui-draw-order`), and the title
screen's paint order is **ground truth** rather than a candidate:
[`ui-title-paint-order-capture.md`](ui-title-paint-order-capture.md).
Background first, then the `back2` glow pair, then `ptlogo1` + `ptlogo_tm`, then
`ptlogo2`, then `ptcopyright`, then the `PRESS Ⓐ BUTTON` plate — i.e.
declaration indices `13, 22|24, 23, 0, 11, 1, 28` and then two elements that are
**not in build 7 at all**. Two more orderings die on that evidence (keyframe
start time, resting-keyframe time), and one structural fact reframes the whole
item: the screen composites **two bundles** (build 7 plus the one-element build
2 that is the button), so no single build's element table can be the paint order
whatever its order.
**Still open, and now sharper:** the rule. The bundle's 60-byte declaration entry
carries no depth field (dumped, above); the per-element `.rat` record has not
been checked for one against this ground truth, and nothing yet explains how the
two bundles are sequenced. Both are static questions again — the oracle side is
answered.
### 2026-08-18 (third pass) — the bundle does not carry the order at all
Three more places checked, all empty, so the static avenue for this item is
**exhausted** (detail and evidence in
[`ui-title-paint-order-capture.md`](ui-title-paint-order-capture.md)):
- **the geometry has no depth.** A UI quad's attribute 0 is `k_32_32_32_FLOAT`,
so it carries a Z — and every Z in the capture is 0.00000. Submission order is
the entire ordering.
- **the declaration table has no key.** Every word of every entry dumped for the
build the game actually runs: `+28` 0, `+32` `0xffffffff`, `+36` `0xffffffff`
(except an instance index on `kind = 0x4`), `+44` `0xffffffff`, `+56` 0.
- **the placement region has none either**, including its per-group lead word,
which is 0 for all 24 groups; and the region is followed straight by the RATC
child stream, so there is no table hiding behind it.
Also corrected: the running screen is **build 4**, not the largest build 7 that
`screen info` defaults to — the two disagree on sprite sizes and the capture
matches build 4. The conclusions are unchanged, the indices are not.
**So the next step is the guest code**, not the file: the splash draw path from
the emulator-era work (`sub_821CC7A0`, item vtable `0x820b30b4`) submits with
exactly the PS hash `E59B2B3D` this capture sees, and `xenia-rs/sylpheed.db` is
available in the container.
**And a second screen is NO LONGER BLOCKED, but it is not routine either.** The
main menu has been reached (screenshot in
[`canary-scripted-input-traps.md`](canary-scripted-input-traps.md)), so the
"Ⓐ is dead" reading is withdrawn. **Not routine after all** — see the 2026-08-19 tables in
[`canary-scripted-input-traps.md`](canary-scripted-input-traps.md): 4 of 5
successes without `--log_ui_draws`, 0 of 7 with it. An interleaved series
**refuted the boot-time confound** (the latest title of all, 268 s, accepted Ⓐ;
a 232 s title refused), and no mechanism exists for the flag — it is read only
when F10 arms a capture, and F10 was never pressed. The variable was removed rather than
believed — F10 now arms the capture unconditionally — and with it gone a fresh
run **still** failed, so the flag is not the cause either. Net: Ⓐ succeeds about
half the time and nothing measurable predicts which; five explanations are
eliminated. The input path is now mapped statically (`entry_point`
`sub_8216EA68` main loop → `sub_822F1AA8` per-frame input → `sub_82457038` pad
poll → `XamInputGetKeystrokeEx`), and the poll itself is not state-gated, so the
gate is in a consumer further up. Until that
is separated, capturing a screen *and* navigating to it in the same run is not
dependable. The earlier claim, kept: the title that ends the boot sequence
accepts a single Ⓐ (2 of 2 at the time); the title the attract loop returns to
accepts nothing (Ⓐ, START, B,
BACK, X, Y — dozens of delivered presses). The proposed tell was refuted on the
way: the two states draw **13 identical quads**, `ptbtn00` included, so they
differ only to the guest. Recipe: first title after boot, one tap, and never tap
during the boot (88 presses over the intro ends on a permanent black screen).
**The second screen is captured** — the main menu, `GP_TITLE` build 5 — and it
does not discriminate: its background sits at declaration indices 12, so
"declaration order" and "background first" predict the same sequence. Same
failure mode as `GP_READY_ROOM`/`GP_OPTIONS`. The next screen worth capturing is
one whose background sits **late** in its table, as the title's does.
The earlier reading, kept because it is what the evidence looked like: the
title's Ⓐ leads into a content/save path that crashes the guest with
`--mem_watch=true` and stalls it with `--mem_watch=false`. Three separate traps
had to be cleared to establish that much — see
[`canary-scripted-input-traps.md`](canary-scripted-input-traps.md), which also
carries the reproduction and the fix for two of them.
### 2026-08-18 (fourth pass) — the crash is named, and the code avenue is scoped
The crash PC resolves to an MSVC `std::map`/`set` erase that throws
`std::out_of_range` from the game's cache-manager flush, and the trigger is now
controlled: an **incomplete on-disc cache** throws ~100 s into a boot, a complete
one never does. The access violation people have been chasing is only that throw
*returning*, because this build does not unwind guest EH. And the handoff's
suspect #1 is **eliminated** — cold cache with `--mem_watch=false` throws just
the same, which withdraws a claim made here yesterday. See
[`title-crash-stl-tree.md`](title-crash-stl-tree.md). That is a by-product of
this item and belongs to whoever picks up the crash bisection.
For the ordering itself, three more negatives, all recorded in
[`ui-title-paint-order-capture.md`](ui-title-paint-order-capture.md): the two
time-based orderings were re-checked against **build 4** (the previous pass used
build 7's numbers, and build 7 is not what runs) and both still fail on the same
element; and a fresh candidate — painter's order by resting **Y** — reproduces
the capture to within a single transposition but is refuted by `ptlogo_tm` and by
the background, so it is not the rule either.
The code avenue is scoped rather than walked: the splash item vtable
`0x820b30b4` is real (25 slots, three construction sites), RTTI carries **no**
class names disc-wide, and the format tags are fourcc immediates behind a virtual
call rather than strings — so this needs a deliberate read of the UI engine, not
a keyword search.
## Capital ships assemble wrong in the viewer
**Reported:** 2026-07-30, by the user. **Status:** ✅ **format-side cause found and
fixed 2026-08-12** — see below for the 2026-08-10 diagnosis this supersedes.
The remaining format-side defect this entry pointed at (a shared turret decoding
~100× too large in some containers) was real and is gone. `e303_wep_01` decoded
as a 1600×2100×4800 block in `Stage_S02`, swallowing the `e106` hull; requiring an
index buffer to cover its vertex pool **exactly** moved it to the block every
other container agrees on, and it now decodes 49×23×42 everywhere and places at
±179 on the hull. The same fix repaired `e106_bdy_03` (a 600×1600×998 slab) and
moved 29 anchors disc-wide, 22 of which had been carrying **another resource's
geometry under their own name**. See
[`structures/xbg7-mesh.md`](structures/xbg7-mesh.md).
Two things are worth carrying forward rather than closing:
- the assembler was **audited and exonerated** — every composite node carries
scale 1.0 and an orthonormal matrix, so nothing on that side inflates a part;
- **no metric caught this.** Coverage, cross-container consistency, the capture
oracle and the twin invariant were all green while a 1 600-unit slab sat through
the ship. It was found by *rendering the ship and looking at it*, and the
numeric screens written afterwards to automate that check both failed.
The 2026-08-10 diagnosis follows, and its viewer-side pointers still stand.
**Status (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 0229 — 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.
---
## ⚠️ DIAGNOSED 2026-08-12 — a mis-decode; the locality fix was written, then withdrawn
> Resolution at the end of this entry. Kept in full because the two wrong turns
> along the way (a "stray volume", then "monotonic anchoring") are the useful part.
## ⚠️ The format layer is NOT exonerated — but the cause is a MIS-DECODE, not a stray 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.**
### CORRECTION (same day, one iteration later): it is not a volume — it is a bad decode
The first reading of this was that `e303_wep_01` is a collision/trigger volume
the assembler wrongly draws. **That is wrong, and the evidence that settles it is
decoding the same resource from every container that holds it:**
```
Stage_S01 172 verts 110 tris X[-24.5, 24.5] Y[0.0, 23.4] Z[-20.8, 20.8] ← 49 × 23 × 42, a turret
Stage_S02 172 verts 110 tris X[-1000, 600] Y[±1050] Z[±2400] ← 1600 × 2100 × 4800
Stage_S03… 172 verts 110 tris 49 × 23 × 42 (correct)
Stage_S08 … 1600 × 2100 × 4800
Stage_S26 … 1600 × 2100 × 4800
```
Same resource, same vertex and triangle count, **decoding correctly in eleven
containers and wrongly in exactly three** (`Stage_S02`, `S08`, `S26`). So:
- the **placement is legitimate**`e303_wep_01` is a small shared turret,
cross-mounted on `e101` and `e106`, and at its true size it is unremarkable;
- the original author's explanation of the capture's silence (**vbase dedup**)
stands, and my "dedup would show one, not zero" objection does not survive:
with the correct decode the turret is small, ordinary geometry;
- **the defect is in the mesh decoder**, which resolved this resource's vertex
data differently in three containers.
The render and the symptom are real; the cause named in the first version of this
entry was not.
### The part that matters more than this one resource
**The decoder can produce wrong geometry without declining.** The
[XBG7 audit](structures/xbg7-mesh.md) counted 814 resources it *refuses* — a
visible, honest failure. This is the other kind: `e303_wep_01` decodes "fine" in
`Stage_S02` and is silently 100× too large. Screening for the signature (bounds
that are exact multiples of 50 with a span over 1000) flags 2232 models in each
of `S02`, `S03`, `S08`, `S26`, `S27` — **but that screen also catches legitimate
`e_rou_*` composite proxies**, so it is a candidate list, not a count of bugs.
**Next:** diff the anchor scan's chosen `vb0` for `e303_wep_01` between
`Stage_S01` (correct) and `Stage_S02` (wrong) — same resource, two outcomes, so
the divergence is directly observable — then use whatever distinguishes them to
add a post-decode sanity check, so a silent 100× mis-decode becomes a decline.
### 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.
**Still true, and independent of the correction above:**
`static_assembly_matches_runtime_capture` walks the capture's parts and looks each
up in the static output, so **extra static placements can never fail it**. That is
worth fixing regardless — it is the same one-way-test shape as the earlier
`include_external` hypothesis.
Also unchanged: only **two** cross-id placements exist fleet-wide (`e303_wep_01`
on `e101` ×24 and `e106` ×36, across 335 assembled ships), so cross-id mounting is
a narrow, real feature rather than a systemic guess.
---
## Resolution (2026-08-12)
`anchor_pool_mesh` took the **first** candidate in file order from a
container-global scan, so a resource could be handed another resource's block
whenever both shared `(stride, vertex count, index count)`. Fixed by anchoring
each resource near its **descriptor neighbours** (two-pass: learn, then re-anchor).
- it took inconsistency **125 → 51** with coverage unchanged, and made `e106`
render correctly ([after](captures/e106-static-assembly-fixed.png))
- **but it flipped the `e106` twin-mirror decision**, which
`static_assembly_matches_runtime_capture` (ISO-gated, so it skips in a plain
`cargo test`) catches against the runtime capture — so it was **reverted**
- the user-reported "capital ships assemble wrong" is therefore **diagnosed, not
yet fixed**; see [xbg7](structures/xbg7-mesh.md) for what the real fix needs
Still open from this entry: `static_assembly_matches_runtime_capture` walks only
the capture's parts, so **extra** static placements still cannot fail it.
### 2026-08-18 — that last line was stale, and the residual gap is now closed too
**The one-way-test complaint had already been fixed** when this entry was
written down: `64d372c` (the revert commit itself) added an extras check, so
"extra static placements cannot fail it" has not been true since. Checked rather
than assumed — perturbing the expectation makes the test fail with the real disc
behind it, so it runs and is live, not a `SYLPHEED_ISO`-less skip.
**But it compared a set of resource *names*, which leaves one direction open**: a
resource placed *twice* when the capture lists it once changes no set. That is
not hypothetical — a duplicated instance is exactly what a bad node walk emits,
and the two legitimate duplicates here (`e106_eng_01`, `e303_wep_01`) are the
reason the test had to special-case counts at all. Replaced with the full
**multiset**, pinned to the e106 ground truth:
```
e106_bdy_01 1 e106_bdy_02 1 e106_bdy_03 1 e106_bdy_04 1 e106_brg_01 1
e106_eng_01 2 e106_eng_02 1 e106_wep_02_01 1 e303_wep_01 2
```
— 9 resources, 11 placements, against the capture's 8 dedup'd parts. That
subsumes the two hand-written count assertions, and it now fails on an extra
resource, a missing one, **and** a duplicated one. Refuted before believing:
declaring `e106_bdy_01` twice makes it fail, with the real multiset on the left.
**Not closed by this**, and worth keeping separate: the multiset is `e106`'s
alone. The generalisation this entry originally asked for — a per-ship table so
a regression in one class cannot hide behind `e106` passing — still needs a
runtime capture of a *second* capital ship, which needs the
`capture-ship-placement` build of Canary.