Skip to content

genesis: Sonic 3 special-stage audio (S/H compositing fast path, I2S task priority, internal-RAM headroom) - #129

Merged
finger563 merged 6 commits into
mainfrom
feat/genesis-special-stage-audio
Oct 13, 2026
Merged

finger563 merged 6 commits into
mainfrom
feat/genesis-special-stage-audio

Conversation

@finger563

@finger563 finger563 commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

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

  1. A 7.5 ms per-pixel compositing loop. The special stages use the VDP's shadow/highlight mode. gwenesis's S/H path composited the sprite line over the plane line with a per-pixel switch for 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.
  2. The I²S feed task was being starved. The BSP's audio task (default priority 19) shares core 1 with the Genesis sound task and the video task (both 20) and only ran in the gaps between frames. With core 1 saturated the DMA underran and the audio queue overflowed: the trial counters showed ~7% of the audio dropped per 12 s window while the emulator held 60 fps. It does microseconds of work per block, so it now runs at priority 21, above both; drops went to zero.
  3. The I²S consumer pads partial blocks with silence (in espp's 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 bumps espp/esp-box to >=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 keeps ≥16 KB of internal RAM free. With every hot buffer preferring internal RAM, one launch left 36 bytes in the main internal heap, after which whatever needed a few hundred bytes in the first frame hung the box silently. Allocation order is now explicitly the priority order and each prefer-internal allocation spills to PSRAM once it would cross the reserve (on the BOX-3 only the sound queue moves, no measurable change).
  • [genesis perf] reports the VDP render split per drawn frame (planes= sprites= merge=) from counters in gwenesis_vdp_gfx.c.
  • The diagnostic task watchdog (only when CONFIG_ESP_TASK_WDT_EN is 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_PERF is defined once in gwenesis_vdp.h and now also guards the VDP's render-stage counters, so GENESIS_PERF=0 compiles all of the profiling out (both objects build warning-free with the flag off).
  • VRAM is allocated last in fact, not only in the comments: it moved to 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 except sound_q and VRAM (PSRAM), VRAM last.
  • genesis_index_buffer goes 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

finger563 and others added 3 commits October 12, 2026 14:39
…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
Copilot AI balanced review requested due to automatic review settings October 12, 2026 20:38
@github-actions

github-actions Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

✅Static analysis result - no issues found! ✅

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

Comment thread components/genesis/gwenesis/src/vdp/gwenesis_vdp_gfx.c Outdated
Comment thread components/genesis/src/genesis.cpp
Comment thread components/genesis/src/genesis_shared_memory.cpp
@github-actions

github-actions Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

ESP-IDF Size Report for 'Esp Box Emu'

Metric Base PR Delta
FLASH 4,054,072 bytes (64.44%) 4,054,828 bytes (64.45%) 🔺 +756 bytes (+0.01%)
DRAM 130,069 bytes (38.06%) 130,501 bytes (38.18%) 🔺 +432 bytes (+0.12%)
IRAM 0 bytes 0 bytes 0 bytes
RAM (DRAM+IRAM) 130,069 bytes 130,501 bytes 🔺 +432 bytes (+0.12%)

FLASH uses app .bin size or json2 flash sum. RAM sums DRAM+IRAM via idf_size. Percentages shown when totals are available.
DRAM/IRAM usage does not include memory used by the heap allocator at runtime.
This report was generated by esp-idf-size-delta.

finger563 and others added 3 commits October 12, 2026 16:57
…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
@finger563
finger563 merged commit f7d882e into main Oct 13, 2026
3 checks passed
@finger563
finger563 deleted the feat/genesis-special-stage-audio branch October 13, 2026 03:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants