Repository navigation
genesis: Sonic 3 special-stage audio (S/H compositing fast path, I2S task priority, internal-RAM headroom) - #129
Merged
Conversation
…e counters Sonic 3's special stages run the VDP in shadow/highlight mode, and gwenesis's S/H path composited the sprite line over the plane line with a per-pixel switch for every pixel of the frame -- 7.5 ms of a ~25 ms drawn frame (the actual S/H colour math in that loop was already commented out, so it is priority compositing only). The sprite buffer is cleared per line and only sprite pixels write it, so most words are zero and can never win: walk the line a word at a time, copy the plane word through when the sprite word is zero, and only resolve priority per byte where a sprite pixel exists. Semantics are unchanged (same winner rule, same operator-sprite exemption). Measured in the special stage: merge 7.5 -> 0.9 ms per drawn frame, the stage goes from 56-59 fps to a locked 60.2-60.5 fps, and 10% more frames are drawn. The [genesis perf] line now also reports the render split per drawn frame (planes= sprites= merge=) from counters in gwenesis_vdp_gfx.c, which is what found this. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AWhoGwibqcKDhcG17mjVT1
With every hot buffer preferring internal RAM the Genesis left 36 bytes in the main internal heap at one Sonic 3 launch; whatever then needed a few hundred internal bytes in the first frame did not get them, and the box hung rather than report it. Allocation order is now explicitly the priority order (work RAM, YM2612 tables, CPU and chip state, Z80 RAM, line buffers, audio mix buffers, sound queue, VRAM) and each prefer-internal allocation spills to PSRAM once it would push free internal RAM below GENESIS_INTERNAL_RESERVE_BYTES (16 KB). On the BOX-3 that currently moves only the sound queue to PSRAM, with no measurable change in the [genesis perf] line. Also: the diagnostic task watchdog (CONFIG_ESP_TASK_WDT_EN builds) is fed from the pause-menu wait loop too -- it fired on the pause screen after 30 s -- and a launch that lands in that loop says so on the console. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AWhoGwibqcKDhcG17mjVT1
The BSP's audio task hands one block at a time to the I2S DMA -- microseconds
of work -- but at its default priority 19 it shared core 1 with the Genesis
sound task and the video task (both 20) and only ran in the gaps between
frames. When core 1 was saturated (Sonic 3's special stages) the DMA underran
and the audio queue overflowed: ~7% of the audio was dropped per 12 s window
while the emulator was at a locked 60 fps. At priority 21 it always runs
promptly; the special stage audio is now clean ('perfect' by ear) with zero
drops.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AWhoGwibqcKDhcG17mjVT1
|
✅Static analysis result - no issues found! ✅ |
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Allocation ordering and an unguarded buffer undermine the documented RAM policy, while VDP profiling cannot be disabled fully.
3 open findings
What changed in this PR
Optimizes Sonic 3 special-stage performance and audio reliability while reserving internal RAM for system operations.
Changes:
- Adds fast shadow/highlight compositing and render-stage profiling.
- Raises I²S task priority to prevent starvation.
- Introduces internal-RAM headroom and watchdog diagnostics.
| File | Description |
|---|---|
main/cart.hpp |
Feeds the watchdog in pause menus. |
components/genesis/src/genesis.cpp |
Adds profiling and reserve-aware allocations. |
components/genesis/src/genesis_shared_memory.cpp |
Implements RAM headroom and allocation ordering. |
components/genesis/include/genesis_shared_memory.hpp |
Declares the 16 KiB reserve policy. |
components/genesis/gwenesis/src/vdp/gwenesis_vdp.h |
Exposes render-stage counters. |
components/genesis/gwenesis/src/vdp/gwenesis_vdp_gfx.c |
Adds optimized compositing and timing. |
components/box-emu/src/box-emu.cpp |
Raises the I²S feed task priority. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
ESP-IDF Size Report for 'Esp Box Emu'
FLASH uses app .bin size or json2 flash sum. RAM sums DRAM+IRAM via idf_size. Percentages shown when totals are available. |
…ruly last, index buffer under the headroom rule - GENESIS_PERF is now defined in gwenesis_vdp.h (included by both genesis.cpp and gwenesis_vdp_gfx.c) and the VDP's per-stage cycle counters are guarded by it, so GENESIS_PERF=0 compiles all of the profiling out as documented (verified: both objects build warning-free with the flag off). - VRAM moves out of genesis_init_shared_memory() into genesis_init_shared_vram(), called from init() after the audio buffers, index line buffer and sound queue, so it is the last internal allocation in fact and not only in the comments. - genesis_index_buffer goes through allocate_hot_memory() like every other internal allocation, so it cannot eat into the 16 KB reserve. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AWhoGwibqcKDhcG17mjVT1
…ock) espp #845 (released in 1.3.8) changes the esp-box audio task to wait for a whole block instead of padding a partial one with silence, which was the remaining source of stutter in Sonic 3's special stages. Verified on the ESP-BOX with the released component: Sonic 3 60.1-60.5 fps, 0 dropped audio frames, "sound is still perfect". Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AWhoGwibqcKDhcG17mjVT1
…riableScope) t1 is only read when the frame is rendered; reading it there is equivalent (nothing runs between the m68k accumulation and the branch) and satisfies cppcheck's variableScope check at both call sites. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AWhoGwibqcKDhcG17mjVT1
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Follow-up to #128: Sonic 3's 3D special stages still ran 56–59 fps with stuttering audio. Measured with the
[genesis perf]line (plus a new render-stage split) and fixed in three steps, each verified by ear on the BOX-3. The special stage now runs a locked 60.2–60.5 fps with audio that the tester could not distinguish from the real thing.What was wrong, in order of discovery
switchfor every one of the 320×224 pixels — and the actual S/H colour math in it is commented out, so it was priority compositing only. The sprite buffer is cleared per line and only sprite pixels write it, so most words are zero and can never win: it now walks the line a word at a time, copies the plane word through when the sprite word is zero, and resolves priority per byte only where a sprite pixel exists. Same winner rule, same operator-sprite exemption. merge 7.5 → 0.9 ms per drawn frame; stage 56–59 → 60.2 fps, 10% more frames drawn.esp-box, not this repo): its block is sized for 48 kHz (3200 bytes = 30 ms at the Genesis's 26.6 kHz) and each time it wakes it takes whatever is queued up to a block and zero-fills the rest. The emulator pushes one 16.7 ms frame of audio at a time with frame-to-frame jitter, so any moment the queue held less than a block became up to 13 ms of injected silence. Fixed upstream in fix(bsp): audio tasks wait for a whole block instead of padding with silence (all 10 BSPs) espp#845 (receive with a trigger level of one block, timeout-protected; write exactly what was received; applied to all ten espp BSPs with audio), released as espp 1.3.8. This PR bumpsespp/esp-boxto>=1.3.8; the released component was verified on the box (Sonic 3 60.1–60.5 fps, 0 audio drops, special-stage sound unchanged from the trial build).Also in this PR
[genesis perf]reports the VDP render split per drawn frame (planes= sprites= merge=) from counters ingwenesis_vdp_gfx.c.CONFIG_ESP_TASK_WDT_ENis set in a local sdkconfig) is fed from the pause-menu loop too, and a launch that lands in that loop says so on the console.espp/esp-box >= 1.3.8(see 3 above).Review follow-up
GENESIS_PERFis defined once ingwenesis_vdp.hand now also guards the VDP's render-stage counters, soGENESIS_PERF=0compiles all of the profiling out (both objects build warning-free with the flag off).genesis_init_shared_vram(), called after the audio buffers, the index line buffer and the sound queue. Verified placement on the ESP-BOX: everything internal exceptsound_qandVRAM(PSRAM), VRAM last.genesis_index_buffergoes through the same headroom-aware allocator as every other internal buffer.Still open
Silent hangs at boot/launch (no panic output even with the task and interrupt watchdogs armed, which points below software: a bus stall or supply issue). They cluster at power-hungry moments and started the day the box lost its battery; being investigated on hardware first.
🤖 Generated with Claude Code
https://claude.ai/code/session_01AWhoGwibqcKDhcG17mjVT1