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,<imm>` 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.
This commit is contained in:
@@ -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,<imm>` 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.
|
||||
|
||||
126
docs/re/f3-title-sting-mechanism-found-not-value.md
Normal file
126
docs/re/f3-title-sting-mechanism-found-not-value.md
Normal file
@@ -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, <imm>` 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.
|
||||
Reference in New Issue
Block a user