Document packed raw SFX addressing

This commit is contained in:
gamer147
2026-07-20 18:32:56 -04:00
parent 343fde08dc
commit 7a9fe52a9d
6 changed files with 44 additions and 23 deletions

View File

@@ -115,8 +115,9 @@ highest-risk area of the port. This doc is the steering state; it feeds the A2b
The active manifest is frame-local, not fixed to the root scene: every VM call frame brackets host work The active manifest is frame-local, not fixed to the root scene: every VM call frame brackets host work
with its script context. `set-texture` resolves the local id at load time and retains the normalized raw with its script context. `set-texture` resolves the local id at load time and retains the normalized raw
catalog id in the graphics surface, so that surface remains stable after a nested helper returns or a catalog id in the graphics surface, so that surface remains stable after a nested helper returns or a
sibling script becomes active. Voice, SFX, and movie calls likewise resolve against the executing frame. sibling script becomes active. Voice and movie calls likewise resolve against the executing frame;
4. **Audio.** **✅ WIRED (2026-07-06), VFS bytes complete (2026-07-11).** `IHost.PlayBgm/PlayVoice` + SFX is the separate universal packed-id family documented below.
4. **Audio.** **BGM/voice wired; VFS bytes complete (2026-07-11); packed-raw SFX correction pending.** `IHost.PlayBgm/PlayVoice` +
VM dispatch (`play-bgm` 0xbf / `play-voice` 0xc4, both argc 1); `ResourceMap.ReadAudio` opens the VM dispatch (`play-bgm` 0xbf / `play-voice` 0xc4, both argc 1); `ResourceMap.ReadAudio` opens the
resolved catalog entry through `IAssetStore`; `GodotAdvHost` passes the bytes to `Main`'s players resolved catalog entry through `IAssetStore`; `GodotAdvHost` passes the bytes to `Main`'s players
(`AudioStreamOggVorbis.LoadFromBuffer`; BGM loops, voice interrupt-on-new). Non-Godot hosts no-op it (`AudioStreamOggVorbis.LoadFromBuffer`; BGM loops, voice interrupt-on-new). Non-Godot hosts no-op it
@@ -132,8 +133,9 @@ highest-risk area of the port. This doc is the steering state; it feeds the A2b
to a graphics entry. Implemented as `ResourceMap.ResolveBgm(id)`; `GodotAdvHost.PlayBgm` uses it. to a graphics entry. Implemented as `ResourceMap.ResolveBgm(id)`; `GodotAdvHost.PlayBgm` uses it.
The prior "Frida-confirmed play-bgm 5→BGM006" record was a mis-attribution. The prior "Frida-confirmed play-bgm 5→BGM006" record was a mis-attribution.
Lily silent = correct (form-gated on `G[0xa57/0xa58/0xa59]`, unseeded). Scene-local SFX WAV entries use Lily silent = correct (form-gated on `G[0xa57/0xa58/0xa59]`, unseeded). SFX WAV entries use universal
the same byte path while retaining the existing channel lifecycle. See `docs/phase-a-slice-plan.md`. packed catalog ids through the same byte store while retaining the existing channel lifecycle. See
`docs/phase-a-slice-plan.md`.
Diagnostic: `Age.Cli audio <SCENE>`. Diagnostic: `Age.Cli audio <SCENE>`.
5. **Movies** (`OP`/`MVB`, MPEG) — SC0000 `0x236` now resolves its scene-local id through the same catalog 5. **Movies** (`OP`/`MVB`, MPEG) — SC0000 `0x236` now resolves its scene-local id through the same catalog
and reads owned payload bytes through `IAssetStore`; see the movie section below. and reads owned payload bytes through `IAssetStore`; see the movie section below.
@@ -150,19 +152,21 @@ rendering what the executed bytecode + the map produce (never a hardcoded image)
A2b-background: **steps 13 landed.** Step 1 = `build/asset-index.json`. Step 2 = **`resId → A2b-background: **steps 13 landed.** Step 1 = `build/asset-index.json`. Step 2 = **`resId →
files[section_base(scene) + resId]`** via SYS4INI per-scene sections (`tools/resolve_asset.py` + files[section_base(scene) + resId]`** via SYS4INI per-scene sections (`tools/resolve_asset.py` +
`build/asset-sections.json`) — no runtime capture, all archives/types + audio. Step 3 = **first-pass `build/asset-sections.json`) — no runtime capture, all archives/types plus scene-local voice. Step 3 = **first-pass
render** (ResourceMap + GodotAdvHost texture ops → TextureRect compositing): the full-screen event-CG render** (ResourceMap + GodotAdvHost texture ops → TextureRect compositing): the full-screen event-CG
layer renders end-to-end from the bytecode. Remaining (next chunk): the **graphics geometry/blend layer renders end-to-end from the bytecode. Remaining (next chunk): the **graphics geometry/blend
subsystem** — native geometry ops (`0x208` + sprite position/animation) so sprites/`BG*` position, plus subsystem** — native geometry ops (`0x208` + sprite position/animation) so sprites/`BG*` position, plus
alpha/blend for fades + chromakey. See `docs/phase-a-slice-plan.md` (A2b). Audio (step 4): **`play-voice` alpha/blend for fades + chromakey. See `docs/phase-a-slice-plan.md` (A2b). Audio (step 4): **`play-voice`
uses the manifest** (`files[base+id]`); **`play-bgm` uses direct names** (`BGM{id:03d}.OGG`) — NOT unified. uses the manifest** (`files[base+id]`); **`play-bgm` uses direct names** (`BGM{id:03d}.OGG`) — NOT unified.
## Native SFX resource proof (2026-07-11) ## Native SFX resource proof (2026-07-11; addressing corrected 2026-07-20)
SFX uses the same scene-local rule as graphics and voice: `files[section_base(scene)+resource_id]`. SFX uses a universal packed catalog id, not the scene-local graphics/voice rule. A zero high byte directly
The matching native trace at SC0000 `0xc29` captures resource `0x28`, channel 0; static resolution yields indexes SYS4INI; a nonzero high byte selects the matching AAI mount and uses the low 24-bit index. The
`DATA1/E0808.WAV`, and the port trace resolves the same file. The following `0xc31` preload uses the same matching native trace at SC0000 `0xc29` captures raw id `0x28`, channel 0, which is
resource on native secondary channel 4. `play-bgm` remains the separate direct-name exception. `DATA1/E0808.WAV`; SC0000 being the first section previously hid the distinction. TITLE makes it decisive:
`0x2aea` is raw `SE020.WAV` for hover, `0x3321` is raw `SE015.WAV` for activation, and neither fits TITLE's
14-entry manifest. `play-bgm` remains the separate direct-name family.
The Phase-A backend now resolves the OGG/WAV catalog entry and opens it through `IAssetStore`; Godot decodes The Phase-A backend now resolves the OGG/WAV catalog entry and opens it through `IAssetStore`; Godot decodes
the returned bytes into its existing BGM, voice, and fixed SC0000 SFX channel players. The earlier the returned bytes into its existing BGM, voice, and fixed SC0000 SFX channel players. The earlier
@@ -215,12 +219,13 @@ store.
on-disk `BinExtractALF.exe` are validation references; the Kelebek repository exposes no clear license, on-disk `BinExtractALF.exe` are validation references; the Kelebek repository exposes no clear license,
so its code should not be copied without clarification. The focused `LzssDecoder` is shared with so its code should not be copied without clarification. The focused `LzssDecoder` is shared with
`Sys4AssetCatalog`; raw and compressed information/pixel/ACIF sections use the same bounded primitive. `Sys4AssetCatalog`; raw and compressed information/pixel/ACIF sections use the same bounded primitive.
4. **Runtime consumers (complete for scripts, textures, and audio).** `ResourceMap.ResolveTexture` preserves 4. **Runtime consumers (scripts/textures complete; SFX resolver correction pending).** `ResourceMap.ResolveTexture` preserves
scene-local resolution and falls back to universal raw ids for SYSTEM4 assets; `GodotAdvHost` caches scene-local resolution and falls back to universal raw ids for SYSTEM4 assets; `GodotAdvHost` caches
decoded RGBA surfaces by catalog identity and supplies synchronous dimensions to opcode `0x208`. decoded RGBA surfaces by catalog identity and supplies synchronous dimensions to opcode `0x208`.
Godot no longer reads `build/textures/*.BMP`. BGM direct-name entries plus scene-local voice/SFX entries Godot no longer reads `build/textures/*.BMP`. BGM direct-name and scene-local voice entries are opened
are opened through the same `IAssetStore`; Godot decodes their OGG/WAV bytes in process. Extraction and through the same `IAssetStore`; the SFX WAV/player path is also present, but `GodotAdvHost` still applies
conversion tools remain diagnostics. scene-local resolution where op `0xb4` requires `ResolvePacked`. SC0000 worked because its raw ids happen
to coincide with the first section. Extraction and conversion tools remain diagnostics.
### Acceptance gates ### Acceptance gates

View File

@@ -1768,8 +1768,9 @@ the shortcut carries the same state in the meantime.
The three SFX opcodes are a retained channel lifecycle, not immediate fire-and-forget calls. Handler The three SFX opcodes are a retained channel lifecycle, not immediate fire-and-forget calls. Handler
resolution and the saved `/v2` names are: resolution and the saved `/v2` names are:
- `0xb4` `op_0xb4_sfx_load@0x4201d0` -> `sfx_channel_load@0x482500`: `(resource_id, channel)` opens the - `0xb4` `op_0xb4_sfx_load@0x4201d0` -> `sfx_channel_load@0x482500`: `(packed_raw_resource_id, channel)`
scene-local SYS4 entry and replaces the channel decoder/buffer without starting it. The manager supports opens the universal SYS4INI entry directly when the high byte is zero, or the selected AAI append entry
when it is nonzero, then replaces the channel decoder/buffer without starting it. The manager supports
13 slots (`0..12`); SC0000 deliberately resets and uses the `0..9` subset. 13 slots (`0..12`); SC0000 deliberately resets and uses the `0..9` subset.
- `0xb5` `op_0xb5_sfx_start_once@0x420210` -> `sfx_channel_start@0x4825d0`: starts the loaded channel with - `0xb5` `op_0xb5_sfx_start_once@0x420210` -> `sfx_channel_start@0x4825d0`: starts the loaded channel with
logical loop mode 0. Adjacent op `0xba`, not this slice, passes mode 1. logical loop mode 0. Adjacent op `0xba`, not this slice, passes mode 1.
@@ -1789,11 +1790,20 @@ five handlers. The bounded port does not yet import native audio preferences, so
uses unity gain and centered pan rather than hard-coding the captured user's setting. uses unity gain and centered pan rather than hard-coding the captured user's setting.
The native trace in `build/native-sfx-trace.jsonl` captures SC0000's first pair: `0xb4@0xc29` resolves The native trace in `build/native-sfx-trace.jsonl` captures SC0000's first pair: `0xb4@0xc29` resolves
resource `0x28` to `E0808.WAV`, loads channel 0, and `0xb5@0xc2e` starts it in the same millisecond. The next raw catalog id `0x28` to `E0808.WAV`, loads channel 0, and `0xb5@0xc2e` starts it in the same millisecond. The next
`0xb4@0xc31` preloads the same WAV into engine-owned secondary channel 4 for a later service start. The `0xb4@0xc31` preloads the same WAV into engine-owned secondary channel 4 for a later service start. The
scratch global `G[0x6242d]` is maintained outside script-visible writes; the SC0000 port profile exposes it scratch global `G[0x6242d]` is maintained outside script-visible writes; the SC0000 port profile exposes it
as an external value of 4 rather than pretending the script assigned it. as an external value of 4 rather than pretending the script assigned it.
**TITLE SFX silence root cause (2026-07-20).** TITLE preloads raw id `0x2aea` (`SE020.WAV`) into channel 2
and executes `0xb5(2)` whenever mouse/joy selection changes. Each activation path loads raw id `0x3321`
(`SE015.WAV`) into channel 1 and immediately starts it; GAMESTART additionally uses raw `0x2aeb`
(`SE013.WAV`). A synchronized Godot trace captured repeated hover starts and the Game Start activation, but
each preceding load recorded `file:null`. `GodotAdvHost.LoadSoundEffect` currently applies the active
script's scene-local manifest, and TITLE has only 14 local entries, so all three large raw ids fail before
the existing WAV/player path. BGM remains audible because it uses its separate direct-name resolver. The
bounded fix is a scene-independent packed-raw SFX resolver, not changes to callbacks, mixing, or channels.
That later service start is opcode `0x2bf`, now identified as the facade's `SetDelay` operation. That later service start is opcode `0x2bf`, now identified as the facade's `SetDelay` operation.
`op_0x2bf_schedule_sfx_start@0x425240` passes `(channel,start_mode,delay_ms)` to `op_0x2bf_schedule_sfx_start@0x425240` passes `(channel,start_mode,delay_ms)` to
`sfx_set_delay@0x482720` on the inline sound facade at `ctx+0x14024`. The worker accepts channels `0..9` `sfx_set_delay@0x482720` on the inline sound facade at `ctx+0x14024`. The worker accepts channels `0..9`

View File

@@ -128,9 +128,9 @@
## audio ## audio
### 0xb4 `sfx-load` (play-sound-effect, argc 2) ### 0xb4 `sfx-load` (play-sound-effect, argc 2)
- **summary:** (resource_id)(channel) — synchronously resolve/open the scene-manifest asset and replace the channel's decoded sound buffer without starting playback. Native manager supports channels 0..12; SC0000 uses 0..9. - **summary:** (packed_raw_resource_id)(channel) — synchronously open the universal SYS4INI/AAI catalog entry and replace the channel's decoded sound buffer without starting playback. A zero high byte is a raw SYS4INI index; a nonzero high byte selects an append catalog and uses the low 24-bit index. Native manager supports channels 0..12; SC0000 uses 0..9.
- **grounding:** source=investigation, confidence=high - **grounding:** source=investigation, confidence=high
- **evidence:** Ghidra op 0xb4 handler 0x4201d0 -> sfx_channel_load@0x482500 -> asset_open@0x44f390 + sound_decode_channel@0x483360. Native trace: SC0000 0xc29 loads resource 0x28 into channel 0; resource resolves by section_base+id to E0808.WAV; completion precedes 0xb5 in the same millisecond. - **evidence:** Ghidra /v2: op_0xb4_sfx_load@0x4201d0 -> sfx_channel_load@0x482500 -> asset_open_indexed_entry@0x44f390 + sound_decode_channel@0x483360. The opener indexes the base catalog directly for a zero high byte and selects mounted_aai[high_byte] otherwise. SC0000 0xc29 uses raw 0x28 = E0808.WAV. TITLE independently proves the addressing rule: raw 0x2aea = SE020.WAV is preloaded on channel 2 and started on every hover-selection change; raw 0x3321 = SE015.WAV is loaded/started on activation. A synchronized port trace executes those loads and starts but reports file=null because Godot incorrectly applies TITLE's 14-entry scene manifest.
### 0xb5 `sfx-start` (u0041D050, argc 1) ### 0xb5 `sfx-start` (u0041D050, argc 1)
- **summary:** (channel) — start the already-loaded channel once (logical loop=false). DirectSound publishes synchronously through Play(0,0,DSBPLAY_LOOPING); the low-level flag loops only the streaming ring, while decoder EOF stops logical playback. - **summary:** (channel) — start the already-loaded channel once (logical loop=false). DirectSound publishes synchronously through Play(0,0,DSBPLAY_LOOPING); the low-level flag loops only the streaming ring, while decoder EOF stops logical playback.

View File

@@ -99,6 +99,12 @@ SYSTEM4's computed resource id `G[0x699]=0x22`; `G[0]=1` and the script-produced
single-scene diagnostic harness. This lands the boot/title/New Game entry half of B1B3; proving a completed single-scene diagnostic harness. This lands the boot/title/New Game entry half of B1B3; proving a completed
scene return plus boundary cleanup still belongs to B1 completion. scene return plus boundary cleanup still belongs to B1 completion.
**TITLE SFX investigation (2026-07-20).** Hover and activation callbacks are already executing their
scripted `0xb5` starts. The load fails earlier because op `0xb4` uses universal packed SYS4INI/AAI ids,
while Godot currently treats them as active-script manifest ids. TITLE's `0x2aea`/`SE020.WAV` hover and
`0x3321`/`SE015.WAV` activation loads therefore resolve null; BGM is unaffected. The next bounded correction
is a packed-raw SFX resolver plus TITLE/GAMESTART regression coverage, with no mixer redesign indicated.
## Stage B1 — Persistent session and scene coordinator ## Stage B1 — Persistent session and scene coordinator
Replace the single-SC0000-root assumption with an application-owned session that runs SYSTEM4 as its root. Replace the single-SC0000-root assumption with an application-owned session that runs SYSTEM4 as its root.

View File

@@ -15,7 +15,7 @@ INFERRED: dict[int, dict] = {
0x94: dict(name='arm-hotspot-wait', category='input', noop=True, confidence='high', source='investigation', summary='Arm native hotspot input processing after the script has registered its rectangles.'), 0x94: dict(name='arm-hotspot-wait', category='input', noop=True, confidence='high', source='investigation', summary='Arm native hotspot input processing after the script has registered its rectangles.'),
0x97: dict(name='bind-hotspot-key', category='input', noop=True, confidence='high', source='investigation', summary='(x)(y)(w)(h)(input_bit) - find the already-registered rectangle with identical bounds and bind an input/key bit number to it.'), 0x97: dict(name='bind-hotspot-key', category='input', noop=True, confidence='high', source='investigation', summary='(x)(y)(w)(h)(input_bit) - find the already-registered rectangle with identical bounds and bind an input/key bit number to it.'),
0xae: dict(name='continue-save-load-stack-restore', category='control', noop=False, confidence='high', source='investigation', summary='() - during serialized save restoration, replace the current frame PC with its saved resume/call target and advance through the saved script-context stack; otherwise a no-op.'), 0xae: dict(name='continue-save-load-stack-restore', category='control', noop=False, confidence='high', source='investigation', summary='() - during serialized save restoration, replace the current frame PC with its saved resume/call target and advance through the saved script-context stack; otherwise a no-op.'),
0xb4: dict(name='sfx-load', category='audio', noop=False, confidence='high', source='investigation', summary="(resource_id)(channel) — synchronously resolve/open the scene-manifest asset and replace the channel's decoded sound buffer without starting playback. Native manager supports channels 0..12; SC0000 uses 0..9."), 0xb4: dict(name='sfx-load', category='audio', noop=False, confidence='high', source='investigation', summary="(packed_raw_resource_id)(channel) — synchronously open the universal SYS4INI/AAI catalog entry and replace the channel's decoded sound buffer without starting playback. A zero high byte is a raw SYS4INI index; a nonzero high byte selects an append catalog and uses the low 24-bit index. Native manager supports channels 0..12; SC0000 uses 0..9."),
0xb5: dict(name='sfx-start', category='audio', noop=False, confidence='high', source='investigation', summary='(channel) — start the already-loaded channel once (logical loop=false). DirectSound publishes synchronously through Play(0,0,DSBPLAY_LOOPING); the low-level flag loops only the streaming ring, while decoder EOF stops logical playback.'), 0xb5: dict(name='sfx-start', category='audio', noop=False, confidence='high', source='investigation', summary='(channel) — start the already-loaded channel once (logical loop=false). DirectSound publishes synchronously through Play(0,0,DSBPLAY_LOOPING); the low-level flag loops only the streaming ring, while decoder EOF stops logical playback.'),
0xb6: dict(name='sfx-release', category='audio', noop=False, confidence='high', source='investigation', summary='(channel) - stop/destroy the channel decoder and DirectSound buffer, clear its retained resource id, and leave the slot empty. Idempotent for an unused channel.'), 0xb6: dict(name='sfx-release', category='audio', noop=False, confidence='high', source='investigation', summary='(channel) - stop/destroy the channel decoder and DirectSound buffer, clear its retained resource id, and leave the slot empty. Idempotent for an unused channel.'),
0xc2: dict(name='fade-bgm', category='audio', noop=False, confidence='high', source='investigation', summary='(target_percent)(duration_ms) — block script service while linearly fading current BGM volume to 0..100%. Durations >=1000 ms use 100 steps; shorter fades use 10. Target 0 releases the current BGM source at completion.'), 0xc2: dict(name='fade-bgm', category='audio', noop=False, confidence='high', source='investigation', summary='(target_percent)(duration_ms) — block script service while linearly fading current BGM volume to 0..100%. Durations >=1000 ms use 100 steps; shorter fades use 10. Target 0 releases the current BGM source at completion.'),

View File

@@ -1731,16 +1731,16 @@ abi_source = "kelebek+decode-validated"
[opcode.semantics] [opcode.semantics]
name = "sfx-load" name = "sfx-load"
category = "audio" category = "audio"
summary = "(resource_id)(channel) — synchronously resolve/open the scene-manifest asset and replace the channel's decoded sound buffer without starting playback. Native manager supports channels 0..12; SC0000 uses 0..9." summary = "(packed_raw_resource_id)(channel) — synchronously open the universal SYS4INI/AAI catalog entry and replace the channel's decoded sound buffer without starting playback. A zero high byte is a raw SYS4INI index; a nonzero high byte selects an append catalog and uses the low 24-bit index. Native manager supports channels 0..12; SC0000 uses 0..9."
noop_headless = false noop_headless = false
source = "investigation" source = "investigation"
confidence = "high" confidence = "high"
depends_on = [] depends_on = []
evidence = "Ghidra op 0xb4 handler 0x4201d0 -> sfx_channel_load@0x482500 -> asset_open@0x44f390 + sound_decode_channel@0x483360. Native trace: SC0000 0xc29 loads resource 0x28 into channel 0; resource resolves by section_base+id to E0808.WAV; completion precedes 0xb5 in the same millisecond." evidence = "Ghidra /v2: op_0xb4_sfx_load@0x4201d0 -> sfx_channel_load@0x482500 -> asset_open_indexed_entry@0x44f390 + sound_decode_channel@0x483360. The opener indexes the base catalog directly for a zero high byte and selects mounted_aai[high_byte] otherwise. SC0000 0xc29 uses raw 0x28 = E0808.WAV. TITLE independently proves the addressing rule: raw 0x2aea = SE020.WAV is preloaded on channel 2 and started on every hover-selection change; raw 0x3321 = SE015.WAV is loaded/started on activation. A synchronized port trace executes those loads and starts but reports file=null because Godot incorrectly applies TITLE's 14-entry scene manifest."
[[opcode.semantics.args]] [[opcode.semantics.args]]
i = 1 i = 1
role = "resource_id" role = "packed raw resource id"
observed_types = ["imm", "g-int", "l-ptr"] observed_types = ["imm", "g-int", "l-ptr"]
[[opcode.semantics.args]] [[opcode.semantics.args]]