Implement native-backed SC0000 SFX lifecycle
This commit is contained in:
@@ -155,6 +155,17 @@ subsystem** — native geometry ops (`0x208` + sprite position/animation) so spr
|
||||
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.
|
||||
|
||||
## Native SFX resource proof (2026-07-11)
|
||||
|
||||
SFX uses the same scene-local rule as graphics and voice: `files[section_base(scene)+resource_id]`.
|
||||
The matching native trace at SC0000 `0xc29` captures resource `0x28`, channel 0; static resolution yields
|
||||
`DATA1/E0808.WAV`, and the port trace resolves the same file. The following `0xc31` preload uses the same
|
||||
resource on native secondary channel 4. `play-bgm` remains the separate direct-name exception.
|
||||
|
||||
The current Phase-A backend deliberately continues through the extracted-file bootstrap: `ResourceMap.AudioPath`
|
||||
accepts both OGG and WAV and Godot loads the WAV bytes into its fixed SC0000 channel pool. This does not change
|
||||
the scoped VFS plan below: ALF/AAI mounting and in-process asset reads remain a separate foundation track.
|
||||
|
||||
## Candidate runtime asset-VFS track (scoped 2026-07-10; not started)
|
||||
|
||||
The pre-extracted tree and `build/textures/*.BMP` pipeline were a Phase-A bootstrap, not the desired final
|
||||
|
||||
@@ -973,6 +973,46 @@ Matching evidence: `build/native-adv-text-trace.jsonl` and Godot timeline captur
|
||||
non-overlapping name/dialogue bands at y=447–468, 478–500, and 507–530. A manual run progressed 14 pages:
|
||||
11 clicks completed active reveals and 14 later clicks released 14 distinct waits through `0xe0c`.
|
||||
|
||||
### SC0000 native SFX / BGM-fade family — `0xb4`/`0xb5`/`0xb6`/`0xc2`/`0xd9` (2026-07-11)
|
||||
|
||||
The three SFX opcodes are a retained channel lifecycle, not immediate fire-and-forget calls. Handler
|
||||
resolution and the saved `/v2` names are:
|
||||
|
||||
- `0xb4` `op_0xb4_sfx_load@0x4201d0` -> `sfx_channel_load@0x482500`: `(resource_id, channel)` opens the
|
||||
scene-local SYS4 entry and replaces the channel decoder/buffer without starting it. The manager supports
|
||||
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
|
||||
logical loop mode 0. Adjacent op `0xba`, not this slice, passes mode 1.
|
||||
- `0xb6` `op_0xb6_sfx_release@0x420250` -> `sfx_channel_release@0x482600` ->
|
||||
`sound_buffer_destroy@0x4831a0`: stop/release and clear the retained resource/decoder; empty release is
|
||||
safe. SC0000's `0x62b..0x646` and `0x120d..0x1228` are ten-channel reset sweeps.
|
||||
|
||||
`sound_decode_channel@0x483360` selects the decoder by file signature, constructs a DirectSound buffer, and
|
||||
installs four quarter-buffer notifications. `sound_buffer_start@0x484270` primes the ring and synchronously
|
||||
calls `IDirectSoundBuffer::Play(0,0,DSBPLAY_LOOPING)` before returning. That flag loops the streaming ring,
|
||||
not the logical clip: `sound_stream_fill_quarter@0x483b70` rewinds the decoder only for logical mode 1;
|
||||
otherwise it pads after EOF and `sound_buffer_stop@0x483aa0` stops playback. This family carries no volume or
|
||||
pan operands. It inherits configured SFX volume and centered pan: the first-pair capture applies DirectSound
|
||||
attenuation `-2377` to both channel loads, and the shared audio service later records centered `SetPan(0)`.
|
||||
`sound_buffer_set_volume@0x483f80` computes that inherited attenuation; neither value is supplied by these
|
||||
five handlers. The bounded port does not yet import native audio preferences, so its extracted-WAV bootstrap
|
||||
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
|
||||
resource `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
|
||||
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.
|
||||
|
||||
Normal-speed windowed validation reached `wait-for-input@0x1a58` after 45.6 seconds without an audio stall;
|
||||
the user confirmed the opening effects were audible and sounded good.
|
||||
|
||||
`0xc2` is BGM rather than SFX: `op_0xc2_bgm_fade@0x4204c0` sets run-state `0x200`, arms the service timer,
|
||||
and calls `bgm_fade_arm@0x464830`. `bgm_fade_tick@0x464960` linearly interpolates current to target percent;
|
||||
durations at least 1000 ms take 100 steps, shorter durations take 10, and target zero releases the source.
|
||||
The VM is parked for the requested duration. `0xd9` is adjacent startup control, not audio data: it clears
|
||||
run/service bit `0x1000` in the primary and, when active, secondary context and has no VM-visible result.
|
||||
|
||||
### Scene-entry state snapshot — auto-seeding single-scene runs (2026-07-09)
|
||||
|
||||
**Problem the oracle surfaced:** single-scene VM runs diverge from the engine because they lack the
|
||||
|
||||
@@ -17,16 +17,31 @@
|
||||
|
||||
## audio
|
||||
|
||||
### 0xb6 `snd-ctrl?` (u0041D080, argc 1)
|
||||
- **summary:** 1 imm; self-chains, 0x41D family near play-sound-effect/0xb5 — sound channel/volume/stop control
|
||||
- **grounding:** source=inference, confidence=low
|
||||
- **evidence:** confirm via frida
|
||||
### 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.
|
||||
- **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.
|
||||
|
||||
### 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.
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **evidence:** Ghidra op 0xb5 handler 0x420210 passes mode 0 to sfx_channel_start@0x4825d0; mode 1 belongs to op 0xba. sound_buffer_start@0x484270 primes four quarter-buffer notifications then calls IDirectSoundBuffer::Play with flag 1 before returning. Native trace at SC0000 0xc2e: E0808 channel 0 start enters/leaves in the same ms, preloaded 1->0 and playing 0->1.
|
||||
|
||||
### 0xb6 `sfx-release` (u0041D080, argc 1)
|
||||
- **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.
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **evidence:** Ghidra op 0xb6 handler 0x420250 -> sfx_channel_release@0x482600 -> sound_buffer_destroy@0x4831a0, which releases the per-channel object under its critical section and clears the slot. Native trace captured SC0000's channels 0..9 release sweep in consecutive calls.
|
||||
|
||||
### 0xbf `play-bgm` (play-bgm, argc 1)
|
||||
- **summary:** Play background music by id. BGM is addressed by DIRECT LITERAL NAME: id -> BGM{id:03d}.OGG (in DATA3), NOT the per-scene section manifest (that's voices/textures). E.g. play-bgm 5 -> BGM005.
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **evidence:** By-ear confirmed (2026-07-06): SC0000 real game plays BGM005 for play-bgm 0x5 and BGM008 for play-bgm 0x8 (we initially mis-played BGM006/BGM009 via the manifest = off-by-one). Direct-name proven by play-bgm 0x23 -> BGM035.OGG, a real standalone track (BGM set skips 030-034) that the manifest mis-resolved to a graphics entry (EV049AA.AGF). CORRECTS the earlier 'unified manifest / Frida BGM006' claim, which was wrong by one. Voices/textures still use the manifest (files[base+id], offset 0). Diagnostic: `Age.Cli audio SC0000.BIN`.
|
||||
|
||||
### 0xc2 `fade-bgm` (u0041D2B0, argc 2)
|
||||
- **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.
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **evidence:** Ghidra op 0xc2 handler 0x4204c0 sets run-state 0x200, arms the service timer, and calls bgm_fade_arm@0x464830; bgm_fade_tick@0x464960 interpolates current/target percent and applies volume, releasing at target 0. Native SC0000 trace at 0x7c1/0x126c shows target 0, duration 3000, 1% ticks at about 30 ms.
|
||||
|
||||
### 0xc4 `play-voice` (play-voice, argc 1)
|
||||
- **summary:** Play a voice clip by id; id resolves via the SYS4INI section manifest -> files[section_base(scene)+id] (voice OGG in DATA1/DATA4). Same rule as set-texture (NOT play-bgm, which is direct-name BGM{id:03d}).
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
@@ -79,6 +94,11 @@ This also names the whole call graph statically (build/callscript-names.json).
|
||||
|
||||
Native handler sleep_op_0xc8 @0x420ec0 is NON-BLOCKING: it arms a timer (sleep_timer_arm @0x44cff0 at ctx+0x5f304 = active flag + start tick + duration) that the engine main loop polls, resuming the script when elapsed. Operand UNIT = MILLISECONDS (start = ms tick source DAT_0056f3d4, timeGetTime/GetTickCount class). duration<10 fast-paths via [0x56f0b8]; all real scene sleeps (100/750/1000) are >=10. The handler also writes gfx cmd-type 3 + runs anti-tamper checks, neither needed host-side. Port equivalent: the Godot host parks the VM thread for duration ms while the presentation compositor continues. Sleep is one proven presentation-capable service boundary; ordinary AE setup runs burst-fast to 0x21c and is not paced per opcode. Headless hosts no-op it (parity).
|
||||
|
||||
### 0xd9 `clear-run-state-0x1000` (u00415880, argc 0)
|
||||
- **summary:** Clear native run/service bit 0x1000; if the secondary context is active, clear the same bit there. SC0000 executes it once after the initial SFX-channel reset, with no VM-visible result.
|
||||
- **grounding:** source=investigation, confidence=high, noop_headless=True
|
||||
- **evidence:** Ghidra op 0xd9 handler 0x416da0: ctx->run_state_flags &= ~0x1000; when ctx+0x6f8b8 is nonzero, also clears bit 0x1000 at ctx+0x53d20. No operands, calls, or return value.
|
||||
|
||||
### 0x140 `coroutine-label-yield` (u0041F9C0, argc 4)
|
||||
- **summary:** (out)(name_str)(sub_str)(in) — scene-coroutine LOOP ITERATOR / labeled yield. Handler copies name/sub strings + the int operand and calls the NATIVE video/transition service (*DAT_005c6018)(8, ctx[0x54fe8], &{name,sub,in}); writes the returned PC-like value to operand 1. In SC0000 label_462 'ループ開始' (@0x46d): `out=G[0x6be]=LABEL('J',G[0x6be])`; loop runs the intro-setup body (incl. call label_125bd = slot-table fill G[0x3239..0x324e]=4..11) and jmps back until out==G[0x6c3] (a per-scene exit-PC immediate) → mov aba5c 0 → content. The gate G[0xaba5c]==1 that opens this loop is NATIVE scene-entry state (no script sets it to 1). DAT_005c6018 is runtime-resolved (all xrefs READ) = SAME class as the DirectDraw workers we don't model. PORT = HOST-MODEL IMPLEMENTED: synthesize the ADV scene-entry gate, run the LABEL/J setup body once, then return the structurally discovered per-scene terminal; do not emulate the video service. See engine-re.md §Scene-coroutine framework.
|
||||
- **grounding:** source=investigation, confidence=med
|
||||
@@ -578,14 +598,6 @@ op 0x90 (u0041BEB0, argc 7): `0x90 x y w h tgt_a tgt_b tgt_c`. Kelebek left it "
|
||||
- **summary:** —
|
||||
- **grounding:** source=kelebek, confidence=low
|
||||
|
||||
### 0xb4 `play-sound-effect` (play-sound-effect, argc 2)
|
||||
- **summary:** —
|
||||
- **grounding:** source=kelebek, confidence=med
|
||||
|
||||
### 0xb5 `u0041D050` (u0041D050, argc 1)
|
||||
- **summary:** —
|
||||
- **grounding:** source=kelebek, confidence=low
|
||||
|
||||
### 0xb7 `u0041D0E0` (u0041D0E0, argc 1)
|
||||
- **summary:** —
|
||||
- **grounding:** source=kelebek, confidence=low
|
||||
@@ -606,10 +618,6 @@ op 0x90 (u0041BEB0, argc 7): `0x90 x y w h tgt_a tgt_b tgt_c`. Kelebek left it "
|
||||
- **summary:** —
|
||||
- **grounding:** source=kelebek, confidence=low
|
||||
|
||||
### 0xc2 `u0041D2B0` (u0041D2B0, argc 2)
|
||||
- **summary:** —
|
||||
- **grounding:** source=kelebek, confidence=low
|
||||
|
||||
### 0xc5 `u0041D4A0` (u0041D4A0, argc 2)
|
||||
- **summary:** —
|
||||
- **grounding:** source=kelebek, confidence=low
|
||||
@@ -646,10 +654,6 @@ op 0x90 (u0041BEB0, argc 7): `0x90 x y w h tgt_a tgt_b tgt_c`. Kelebek left it "
|
||||
- **summary:** —
|
||||
- **grounding:** source=kelebek, confidence=low
|
||||
|
||||
### 0xd9 `u00415880` (u00415880, argc 0)
|
||||
- **summary:** —
|
||||
- **grounding:** source=kelebek, confidence=low
|
||||
|
||||
### 0xfb `joy_callback` (joy_callback, argc 2)
|
||||
- **summary:** —
|
||||
- **grounding:** source=kelebek, confidence=med
|
||||
|
||||
@@ -1089,3 +1089,43 @@ fixtures and installed-game integration checks rather than committing proprietar
|
||||
**Decision point:** this track is worthwhile before broadening beyond SC0000 because it establishes the
|
||||
modding contract and benefits scripts, UI chrome, SFX, and movies. It is not required to continue opcode
|
||||
coverage immediately, so choosing movie/SFX next remains valid.
|
||||
|
||||
### Phase A — native SC0000 SFX family (`0xb4`/`0xb5`/`0xb6`/`0xc2`/`0xd9`) DONE (2026-07-11)
|
||||
|
||||
Native RE and the matching trace resolve the bounded family. `0xb4(resource,channel)` synchronously loads
|
||||
and retains a scene-manifest sound; `0xb5(channel)` starts that loaded sound once; `0xb6(channel)` destroys
|
||||
and clears it. The manager supports 13 native slots, while SC0000 uses a fixed `0..9` pool. Native playback
|
||||
is a notification-fed DirectSound ring: `Play(...,DSBPLAY_LOOPING)` keeps the ring alive, but logical mode 0
|
||||
stops at decoder EOF. Mode 1 belongs to adjacent op `0xba` and remains outside this slice. These five ops do
|
||||
not set pan or SFX volume; SC0000 inherits centered pan and configured SFX volume. The captured loads both
|
||||
apply DirectSound attenuation `-2377`; because native audio-preference import is outside this slice, the
|
||||
bounded extracted-WAV port uses unity gain and centered pan instead of treating that user setting as opcode
|
||||
semantics.
|
||||
|
||||
At the requested first site, `0xb4@0xc29` resolves `0x28` to `E0808.WAV` and loads channel 0;
|
||||
`0xb5@0xc2e` publishes playback in the same native millisecond. `0xb4@0xc31` preloads the same WAV into
|
||||
engine-owned secondary channel 4. `G[0x6242d]` is native/profile-owned rather than script-written, so the
|
||||
port exposes value 4 through the new external-global seam for SC0000. The matching port timeline now records
|
||||
the same `(load ch0, start ch0, preload ch4)` sequence in one frame. Startup `0x62b..0x646` and the later
|
||||
`0x120d..0x1228` both release channels 0..9 in order.
|
||||
|
||||
`0xc2(target,duration)` is the adjacent blocking BGM fade: 100 linear steps for durations at least 1000 ms,
|
||||
10 steps below that, with target zero releasing the source. `0xd9` only clears native service bit `0x1000`;
|
||||
the isolated VM recognizes it with no host-visible effect. The Godot backend retains ten `AudioStreamPlayer`
|
||||
channels, loads the existing extracted WAV bytes, separates load from start, releases buffers deterministically,
|
||||
and parks BGM fades on the unified virtual clock. ALF/AAI/AGF VFS work and movie `0x236` remain separate.
|
||||
|
||||
Ghidra `/v2` now names/comments all five handlers plus the asset-open, decode, DirectSound start/refill/stop/
|
||||
volume/release, and BGM fade workers; the program is saved. Evidence is
|
||||
`build/native-sfx-trace.jsonl` and `godot/build/sfx-port-timeline4.jsonl` (the latter is the final rebuilt
|
||||
channel-4 trace; earlier diagnostic reruns captured the missing external-state mismatch).
|
||||
|
||||
**Automated validation:** engine **111/111**; corpus sweep unchanged at **284 exit / 13 STEP-LIMIT**;
|
||||
Godot build and threaded `SELFTEST OK`; all seven Python suites, opcode/ctx lint, 481-script decode, RECOVER,
|
||||
and tracer bytecode compilation clean. SC0000 coverage rises from **87/129 to 92/129 handled (71.3%)**,
|
||||
leaving 37 GAP ops / 101 GAP instructions. Headless sequence capture still emits the known dummy-renderer
|
||||
`GetImage` diagnostics while completing successfully; it is not an audio failure.
|
||||
|
||||
**Manual validation:** the normal-speed windowed port advanced through `wait-for-input@0x1a58` at 45.6 s,
|
||||
well past the first effects and dialogue pages, with no audio-related stall. The user confirmed the effects
|
||||
were audible and sounded good.
|
||||
|
||||
@@ -179,6 +179,7 @@ texture ops (no GPU context) — run windowed for real scenes. User args (after
|
||||
|
||||
| `tools/frida/capture_presentation_trace.py` | **Retained-state presentation trace:** correlates the current script offset with native draw/color writes, object composition, surface-command consumption, `gfx_render_frame`, queue clear, and D3D9 Present count. Read-only; distinguishes live retained state from state actually published to the window. | `py -3.11 -u -X utf8 tools/frida/capture_presentation_trace.py [secs] [pid\|AGE.EXE]` | native game → `build/native-presentation-trace.jsonl` |
|
||||
| `tools/frida/capture_adv_text_trace.py` | **ADV text trace:** correlates SC offsets with op `0x6e`/`0x7a`/`0x204`, layout cursor/origin and 20-byte record counts, CP932 strings, surface draw/bind, and timed glyph-record publication. Read-only and deliberately limited to low-frequency known handlers; the first experimental version's D3D scan plus hot per-glyph/render hooks crashed in `frida-agent.dll` during teardown and was removed. | `py -3.11 -u -X utf8 tools/frida/capture_adv_text_trace.py [secs] [pid\|AGE.EXE]` | native game → `build/native-adv-text-trace.jsonl` |
|
||||
| `tools/frida/capture_sfx_trace.py` | **SFX/DirectSound trace:** correlates SC offsets with `0xb4`/`0xb5`/`0xb6`/`0xc2`/`0xd9`, resource/channel load/start/release, decoder/buffer state, BGM fade ticks, and dynamically discovered DirectSound `Play`/`Stop`/volume/pan calls. Read-only. | `py -3.11 -u -X utf8 tools/frida/capture_sfx_trace.py [secs] [pid\|AGE.EXE]` | native game → `build/native-sfx-trace.jsonl` |
|
||||
|
||||
*(Static disassembly of `build/engine-dump/range_00400000.bin` uses **capstone** — `py -3.11 -m pip install capstone`; VA `X` → file offset `X−0x400000`.)*
|
||||
|
||||
|
||||
Reference in New Issue
Block a user