From f91ac9aad3fef653df47283ffd6550d6dcd7fda6 Mon Sep 17 00:00:00 2001 From: sylph-decoder Date: Fri, 11 Sep 2026 22:29:51 +0000 Subject: [PATCH] re: F3's sting half -- the mechanism is decoded, the value isn't (issue #5) f3-title-plays-bgm-102-and-103.md's BGM census resolved a literal `addi r5,r0,` before every call to the play primitive, which is blind by construction to a cue id supplied any other way. Two of the primitive's 34 callers ARE supplied another way -- one is a register passthrough inside a generic 6-caller wrapper `sub_821CCCB0(obj, cueId)` with a -1 "don't play" sentinel. One of its six callers chains back to slot 1 of GamePart_Title's OWN dispatch table at 0x820a3dec, identified by its adjacent RegisterToFactory<0, class silph::GamePart_Title> string -- the same convention already used disc-wide for the other 28 GameParts, not a neighbourhood guess this time. Traced as far as static analysis goes: the field that would carry the cue id traces to a 19-caller shared helper's return value, too common to be title-specific -- reads as "allocate a sound-emitter handle", not "here is the cue". The value most likely gets set at whatever runtime moment the game wants this emitter to speak, which a disassembly listing does not contain. Classified undecodable-with-reach, not a fourth thing -- narrower and better-aimed than the prior reach ("SE goes through a different call"). Every address cited was independently re-decoded from the raw .pe bytes (file offset = VA - 0x82000000), not taken from the database's own mnemonic/operands columns, and all agree. Refutation attempt this iteration: checked whether the "6 callers is exhaustive" claim for sub_821CCCB0 survives indirect dispatch (a plain kind='call' xref scan is blind to a vtable/function-pointer-array entry). Checked function_pointer_array_entries and kind='ind_call' xrefs for all four functions in the chain -- none found. The claim survives. What would close it: --xma_param_probe=true during a title boot's build-in, watching for a newly-decoded stream when the plate reaches full alpha with no input -- the same technique menu-audio-cues.md used for the menu's SE census. Not run this iteration; this is the static half only. --- docs/port/HANDOFF.md | 37 +++++ ...3-title-sting-mechanism-found-not-value.md | 126 ++++++++++++++++++ 2 files changed, 163 insertions(+) create mode 100644 docs/re/f3-title-sting-mechanism-found-not-value.md diff --git a/docs/port/HANDOFF.md b/docs/port/HANDOFF.md index deaeb407..3c661234 100644 --- a/docs/port/HANDOFF.md +++ b/docs/port/HANDOFF.md @@ -6523,3 +6523,40 @@ container to run it against real logs. residue clusters (points at a real boot-order dependency) or scatters (points at instrument sensitivity). `state/proposed` — new exploratory dynamic RE, not a continuation of this shaping pass. + +## ❔ F3's sting half — the mechanism is decoded, the value isn't (issue #5) + +[`docs/re/f3-title-sting-mechanism-found-not-value.md`](../re/f3-title-sting-mechanism-found-not-value.md). +BGM stays exactly as `f3-title-plays-bgm-102-and-103.md` already gave you +(cues 1102/1103, nothing new here). This is the other half of that page's +open `❔` — is there a one-shot sting — and it moves, but doesn't close. + +**New, and checked against the raw bytes, not just the database:** the +34-call BGM census only ever found a literal `addi r5,r0,` before the +play call, which cannot see a cue id supplied any other way. Two of those 34 +calls *are* supplied another way — one is a register passthrough inside a +generic 6-caller wrapper `sub_821CCCB0(obj, cueId)` (arbitrary cue id, a `-1` +sentinel means "don't play"). One of that wrapper's six callers chains back to +**slot 1 of GamePart_Title's own dispatch table** (`0x820a3dec`, identified +by its own `RegisterToFactory<0, class silph::GamePart_Title>` string, the +same convention `boot-config-and-gamepart-registry.md` used for the other 28 +GameParts — not a neighbourhood guess). So title's own code, not just the +seven literal BGM sites, reaches the sound primitive with a cue id that could +be in the SE range. + +**Where the trail runs out statically:** the one field write that would pin +the cue id traces to a 19-caller shared helper's return value — too common to +be title-specific, reading as "allocate a sound-emitter handle" rather than +"here is the cue." The actual value most likely gets set at whatever runtime +moment the game wants this emitter to speak, which a disassembly listing does +not contain. **Not a fourth thing — this is `undecodable, with reach`, +narrower and better-aimed than the reach `f3-title-plays-bgm-102-and-103.md` +had** ("SE goes through a different call" → "here is the specific call, +here is where the value would need to come from, here is why static analysis +stops there"). + +**What would close it:** the same `--xma_param_probe=true` technique +`menu-audio-cues.md` used for the menu's SE census, run during a title +boot's build-in — watch for a newly-decoded stream at the moment the plate +reaches full alpha, no pad input needed. Not run this iteration; this page +is the static half only. diff --git a/docs/re/f3-title-sting-mechanism-found-not-value.md b/docs/re/f3-title-sting-mechanism-found-not-value.md new file mode 100644 index 00000000..b2a9ed2c --- /dev/null +++ b/docs/re/f3-title-sting-mechanism-found-not-value.md @@ -0,0 +1,126 @@ +# F3, second half — a real sound-emitter hook exists on GamePart_Title; its cue value is not statically pinned + +**Question:** does the title's `PRESS Ⓐ` plate (or any other title event) play a +one-shot sting? The BGM half of F3 is already ✅ decoded +([`f3-title-plays-bgm-102-and-103.md`](f3-title-plays-bgm-102-and-103.md)); this +is the sting half that page left as `❔`. + +**Instrument:** ⟨image⟩ only — `duckdb` over `/xenia-rs/sylpheed.db`, checked +against `/image/sylpheed.pe` at every address quoted. No emulator run this +iteration; this is the "purely static" carve-out, and the next step below is +not. + +## Why the earlier census couldn't have found it + +`f3-title-plays-bgm-102-and-103.md` found the play primitive +(`bl 0x8217ACF8` inside `sub_821C5580`) by scanning every call site and +resolving a literal `addi r5, r0, ` before it. **That method is blind by +construction to a call whose cue id is not a literal** — a value loaded from a +register or memory. So "no SE ids in the census" was never evidence against a +sting; it was a scope limit, already flagged in that page. + +## ✅ Decoded — a second call path exists, and it takes a non-literal cue id + +Of the 34 total callers of the play primitive, two do not load `r5` with +`addi`: + +| call site | `r5` source | +|---|---| +| `0x821ccd18` | `or r5, r29, r29` — a register passthrough | +| `0x82272cc8` | `lwz r5, 0(r30)` — loaded from memory | + +The first sits in `sub_821CCCB0`, a 52-instruction function whose second +parameter (`r4`) is moved into `r29` and forwarded as `r5` to the play call, +with `r4` hardcoded to **4** (the same category every BGM call uses) — +`sub_821CCCB0(obj, cueId)` is a **generic "play this cue" wrapper**, cue id +supplied by the caller, not baked in. It also special-cases `cueId == -1` by +skipping the call entirely (`cmpi cr6,0,r29,-1; bc … skip`) — a "no sound +configured" sentinel. + +`sub_821CCCB0` has exactly **6 callers disc-wide** (exhaustive — every `xref` +targeting it, same method as the 34-caller BGM census). Two pass a literal +`1103` (more BGM, elsewhere in the image); the rest pass a value read from an +object field — i.e. **data-driven**, could be anything in either the BGM or SE +range. + +**Refutation attempt, recorded whether or not it survived:** "6 callers is +exhaustive" would be false if `sub_821CCCB0` were also reachable through +indirect dispatch, which a plain `kind='call'` scan cannot see. Checked +`function_pointer_array_entries` (it is not a table entry anywhere) and +`xrefs` with `kind='ind_call'` (zero, for it and for the other three +functions in this chain). **The claim survives** — every reachable path to +it in this database is a direct `bl`, and the 6-caller count is complete. + +## ✅ Decoded — one of those six call chains starts inside GamePart_Title's own dispatch table + +`0x820a3dec` is a `dispatch_table`-kind function-pointer array (per the +database's own classification) sitting immediately after the string +`silph::GamePartTask::RegisterToFactory<0,class silph::GamePart_Title>::…` +at `0x820a3d60` — this is GamePart id **0**, confirmed as `GamePart_Title` by +its own factory-registration string, the same convention +`boot-config-and-gamepart-registry.md` already established for the other 28 +ids. Its two slots: + +| slot | function | +|---|---| +| 0 | `sub_821C7CB8` | +| 1 | `sub_821C7D48` | + +`sub_821C7D48` reads its object's own **offset 64**, calls `sub_821CCF50(v)`, +which reads **offset 36 of `v`** as a cue id and forwards it into +`sub_821CCCB0` — i.e. **title's own dispatch table, slot 1, reaches the play +primitive through the non-literal path**, distinct from and in addition to +the seven literal BGM calls already in `f3-title-plays-bgm-102-and-103.md`. + +## What this does NOT establish + +**The cue value itself.** Chasing what fills "offset 64" for title's +instance leads to a single write in the whole title neighbourhood, +`0x821c4168: stw r3, 64(r30)`, inside a constructor (`sub_821C40E8`) that +zeroes most of an object's fields and sets offset 64 to **the return value of +`sub_821CCAA0(1)`** — a function with **19 callers disc-wide**, too common to +be title-specific machinery. That shape (a shared, 19-caller helper handing +back a handle that gets stored once at construction) reads as "allocate a +sound-emitter handle", not "here is the cue" — the actual cue (offset 36 of +whatever that handle points to) is far more likely set **later, at whatever +moment the game wants this emitter to speak**, which is a runtime event a +disassembly listing does not contain. + +So: **the mechanism is decoded; the value is not, and cannot be from this +instrument.** This is not the same shape as "SE goes through a different +call" (the previous, weaker negative) — a specific, non-literal call chain +from title's own dispatch table into the shared play primitive is now named +and can be watched directly. + +## Verified against the image, not just the database + +Every address above was re-read directly out of `/image/sylpheed.pe` (file +offset = VA − `0x82000000`) and decoded by hand, independent of the +database's own `mnemonic`/`operands` columns: the four `bl` sites +(`0x821ccd18`, `0x82272cc8`, `0x821c7d60`, `0x821c4164`) all decode to +opcode 18 (branch), `LK=1`, with targets `0x8217acf8`, `0x8217acf8`, +`0x821ccf50` and `0x821ccaa0` — matching the database exactly. The dispatch +table's two words at `0x820a3dec`/`0x820a3df0` read `821c7cb8`/`821c7d48` raw, +and `0x821c4168` decodes as opcode 36 (`stw`), `rt=r3, ra=r30, imm=64` — all +bytes agree with the rows quoted from `sylpheed.db`. + +## Reach + +Exhaustive for what it claims: the 34-caller and 6-caller censuses are +complete `xref` scans, not samples. The GamePart_Title attribution for +`0x820a3dec` is by the same RegisterToFactory-string convention already used +disc-wide, not a guess by address proximity (contrast the neighbourhood-only +attribution `f3-title-plays-bgm-102-and-103.md` flags for its seven BGM +sites). Not reached: what value `sub_821C7D48`'s chain plays, if anything, +and whether it fires on the plate specifically or on some other title event +(state entry, exit, GamePart teardown). + +## What would close it + +The same instrument `menu-audio-cues.md` used for the menu's SE census: +`--xma_param_probe=true` during a title boot, watching for a **newly-decoded +XMA stream at the moment the plate reaches full alpha, with no pad input** — +which the F5/F6 capture work already reaches routinely (gated on the sweep's +texture page, not a wall-clock delay). Not run this iteration: this page is +the static half; the dynamic half is a separate unit, and it's the Port's +`kind/ask` on this exact question that this closes the static side of.