Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ if(BUILD_TESTING AND NOT ANDROID)
target_include_directories(ppu_window_test PRIVATE
${SNESRECOMP_ROOT}/runner/src)
add_test(NAME ppu_widescreen_windows COMMAND ppu_window_test)
add_test(NAME sm_door_audio_trace
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tests/door_audio_trace_test.py)

endif()

Expand Down
147 changes: 147 additions & 0 deletions docs/door-audio-investigation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
# Door transition audio investigation

The F1 fixture used on September 10, 2026 is immediately to the right of an open
door. Holding left changes room `$91F8` to `$92FD`, with game state
`8 -> 9 -> 11 -> 8`. Supply that save locally; saves and ROMs are not distributed.

## Reproduce on the production path

Build with `CMAKE_BUILD_TYPE=Release` and `SNESRECOMP_ENABLE_TRACE=OFF`.
Copy the fixture into `saves/save0.sav` next to the executable, keep audio on,
and use normal frame delay (no turbo). For the stock-renderer comparison, disable
both custom renderer and custom presentation FPS. Set `SM_RUN_FRAMES=1100` and
`SM_AUDIO_PROBE` to a CSV path. Launch with `--script` followed by the absolute
path to `tools/door_audio_repro.txt`. Paths to config, ROM cache, and saves are
anchored next to the executable.

Run `python tools/check_door_audio.py path/to/trace.csv` afterward. A clean run
exits zero. Missing/dropped guest audio or excessive post-exit FPS exits one. An invalid
fixture (no actual door, same room after exit, incomplete post-exit window, or no
audio sample rate) exits two. The checker excludes all counters accumulated
before entering the door, including boot and loading the save.

The probe is optional and works without the debug server. It samples game state,
wall time, CPU master cycles, APU clock mapping, and audio counters once after
each guest frame. `missing_frames` counts output stereo frames which lacked
guest PCM, including the recovery fade; milliseconds use the actual stream
sample rate. This measures producer starvation, not silence intentionally
generated by the guest music. `longest_no_pcm_ms` describes a pause in production,
which may be fully covered by already-buffered audio.

## Findings

The shared runner timestamps APU accesses using the host frame number plus the
current frame's CPU master-cycle offset. Long door routines legitimately advance
that offset beyond a nominal frame. For this fixture, the live APU reaches about
38 frames ahead of the next nominal frame-boundary target. Later short frames
then produce no PCM until that target catches up; a production trace measured
about 218 ms with no new PCM during door step `$E4A9`.

This is not exclusively a single blocked host frame. The loader already advances
the APU during port reads. Adding synchronization at CPU block boundaries does
not address the subsequent backward frame timestamp. The shared timestamp
function must remain unclamped: NMI-disabled SPC uploads need to progress across
multiple frame periods to complete their handshakes.

The Super Metroid host also presents every stock-renderer catch-up frame, even
when the presentation deadline is not due. Each presentation can wait for VSync,
slowing recovery while the audio buffer drains. An experiment applied the existing
presentation deadline to the stock renderer too. That shortened some recovery
windows, but repeat tests still failed, so the behavior change was removed.

Loading also has limited CPU headroom. The room setup and tile-loading frames
took about 233 and 206 ms in a quiet run; a later run took 431 and 387 ms for the
same work and generated substantial underflows before the subsequent no-PCM
period. Wall-time measurements include scheduling and lock delays; they do not
by themselves identify why that later execution was slower.

## Measured results

These are Windows production builds with trace/debug-server code disabled,
stock renderer, a 32,000 Hz output stream, and the F1 fixture above. The older
policy comparison uses the same instrumented source with v0.3.1's unconditional
debt-discard policy, rather than claiming an untouched release ZIP was tested.

| Timing policy | Crossing | Missing output during door | Missing output in following 120 frames |
|---|---:|---:|---:|
| v0.3.1 debt discard | 1 | 815.438 ms | 613.938 ms |
| v0.3.2 | Initial | 5.500 ms | 0 ms |
| v0.3.2 | Repeat 1 | 10.000 ms | 0 ms |
| v0.3.2 | Repeat 2 | 17.969 ms | 0 ms |
| v0.3.2 | Repeat 3 | 20.500 ms | 0 ms |
| Presentation experiment | Initial | 0 ms | 0 ms |
| Presentation experiment | Repeat 1 | 656.000 ms | 0 ms |
| Presentation experiment | Repeat 2 | 0 ms | 9.969 ms |
| Presentation experiment | Repeat 3 | 370.188 ms | 0 ms |

Missing output is the sum of device frames lacking guest PCM, not necessarily
one uninterrupted silence. Every crossing traversed the same guest frames
(106 frames between entry and gameplay return) and produced 66,142 native
samples. Measured post-exit rates for v0.3.2 and the experiment were about
56-60 FPS, with no speed-up burst; the old policy run fell to about 46 FPS.
Execution time varied, so these runs establish failures and sensitivity to
scheduling; one zero-underflow pass does not validate a fix.

Local evidence is in `build-release-v0.3.2/door-audio-*.csv`, particularly
`door-audio-v031-policy.csv`, `door-audio-v032-precise.csv`,
`door-audio-v032-repeat.csv`, and `door-audio-present-repeat.csv`.

An initial release-path test was invalid: `loadstate 0` before the first guest
reset loaded the save, then reset erased it. That run stayed at the title screen.
The checked-in fixture loads after boot and the checker explicitly rejects this
false positive.

## Clock correction

The ordinary-cartridge interpreter was still advancing the SPC through relative
catch-up in addition to the absolute frame clock. Suppressing that path alone
removed the clock lead, but exposed the other half of the bug: long room-loading
code without APU port accesses produced only one frame's PCM at return. Three
crossings then lost about 412 ms each. That experiment was superseded.

The retained shared-runner correction synchronizes the absolute clock in the
interpreter's existing periodic batches, including work without port accesses.
The end of an iteration is the greater of one nominal frame and its executed
guest duration. The next iteration starts there, so a multi-frame loader cannot
leave the SPC waiting for a backward frame-count timestamp. NMI-disabled upload
handshakes remain unclamped. Unmapped startup retains relative catch-up until
APU port time is established.

Hosts opt in with `RtlEnableExtendedFrameTiming()` and must pace each iteration
using `RtlLastFramePeriods()`. Super Metroid now does that. Hosts that have not
adopted the duration contract retain their existing behavior, including SA-1's
existing absolute-clock policy. No sound is generated from wall time, resampler
speed is unchanged, and the normal-play policy still discards stale wall-time
debt. This fixes shared timing used by the title's LLE driver; it does not migrate
the entire driver to the hardware-frame scheduler.

Production traces `door-audio-final-clock-c.csv` and `door-audio-final-clock-d.csv`
contain six consecutive clean crossings:
zero missing output during and after the door, zero dropped samples, and zero
frames without newly produced PCM. Post-exit speed is 60.068-60.076 FPS. The
transition now produces 94,602 samples over about 2.947 seconds, accounting for
the loader's guest duration instead of forcing it into one host frame.
The Debug build with TCP/trace enabled also passed the fixture with zero missing
or dropped output, zero no-PCM frames, and 60.073 FPS after exit
(`build-codex-debug-dev/door-audio-debug-clock.csv`). Its largest guest call was
298 ms; audio production continued throughout that work.

Earlier candidate replays also recorded intermittent 123-821 ms gaps *between*
guest calls, including ordinary gameplay. Those runs are retained as
`door-audio-final-clock-a.csv` and `door-audio-final-clock-b.csv`; they are not
counted as clean passes. The B-run guest/raster/presentation timings do not
account for those pauses. The optional `SM_PROFILE` report now measures the event
pump and deadline waits as well, to help attribute any recurrence. The clean C
run measured at most 0.219 ms in the event pump and 73.839 ms in a deadline wait
(the latter includes intentional waiting after an extended loader).

Regression tests run the real SPC for a synthetic 40-frame loader followed by 45
short iterations, checking that each short iteration immediately produces 534
samples. Interpreter tests cover periodic absolute sync without port accesses,
no duplicate relative advancement, bootstrap progress, and legacy policy. The
host-clock test verifies an 18-period loader advances its deadline by 18 periods
and returns to ordinary pacing without a catch-up burst.

Track shared timing in `beads-8wg.2.33` and the player-visible doorway regression
in `beads-8wg.6.5`. Production measurements remain the acceptance gate; debug
instrumentation can materially change available execution headroom.
19 changes: 19 additions & 0 deletions docs/releases/v0.3.3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Super Metroid v0.3.3

- Fix audio interruptions during door transitions by advancing the sound
processor continuously through loading work and preserving its elapsed time.
- Pace long loading iterations by their guest duration, keeping normal gameplay
speed after the door. The tested F1 transition takes about 2.95 seconds.
- Keep fast loading work from overflowing the audio buffer and allow playback
to build a small cushion after startup or starvation.
- Use Linux's corrected monotonic clock for pacing so a drifting raw timer
cannot make gameplay fall behind the audio device.
- Add repeatable doorway audio checks and missing/dropped-output diagnostics.

Available as a Windows x64 ZIP and a Linux x86_64 AppImage. Existing saves remain
compatible; keep your ROM, configuration, and saves when updating.

The release pins snesrecomp `e2c75fd`, the framework revision validated with this
game and included in the history of framework PR #70. Advancing this title to all
newer framework changes requires a separate frame-driver compatibility update
tracked as `beads-8wg.6.7`.
2 changes: 1 addition & 1 deletion snesrecomp
Loading