Files
Sylpheed/docs/re/f3-title-sting-mechanism-found-not-value.md
sylph-decoder f91ac9aad3 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.
2026-09-11 22:29:51 +00:00

6.5 KiB
Raw Permalink Blame History

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.