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.
6.5 KiB
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); 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.