Repository navigation
Play queued buffers in order across the buffer id wrap - #1
Merged
Merged
Conversation
libctru numbers a channel's wave buffers 1 to 65535 and then from 1 again. dequeue() played the lowest id next, so at the wrap the last buffers numbered before it stayed in the queue for good while the new, lower ids played ahead of them. With those slots taken, later adds beyond the free slots were dropped while libctru counted them played: a channel fed without pause (a game's output stream, 60 buffers a second) ran about a quarter fast from 18 minutes in. The ids are now compared as a signed 16-bit distance. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
oneup03
added a commit
to oneup03/rt64-3ds
that referenced
this pull request
Nov 1, 2026
libctru numbers a channel's buffers 1 to 65535 and then from 1 again, and OpenDSP 1.2.2 plays the lowest number it holds next. At the wrap the last buffers numbered before it stayed in its five-slot queue for good, later buffers beyond the slots left were dropped while libctru counted them played, and the AI stream (and a game's music, which keeps pace with it) ran about a quarter fast from 18 minutes in: Goemon on an Old 3DS went from 44,095 frames a second to ~55,000, low water 2,590 to ~1,160, after ~65,500 buffers. With OpenDSP running, once the last buffer's number reaches 0xFF00 the channel is cleared (libctru numbers from 1 again, and the next buffer carries the partial reset that empties OpenDSP's queue) and what had yet to play is queued again from where the DSP is. audio_frames_played holds its count across it, so the DSP synthesizer's clock does not jump. The synthesizer's voices reset their channels at every start and never get near the wrap. Fixed upstream in Cellenseres/OpenDSP#1 (not yet in a release; the firmware image is embedded prebuilt). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
oneup03
added a commit
to oneup03/rt64-3ds
that referenced
this pull request
Nov 1, 2026
…d dropped The submodule moves from v1.2.2 to upstream main a15c861 (no release yet): Cellenseres/OpenDSP#1, the queue's oldest buffer played first across libctru's u16 buffer-id wrap, merged, and the firmware image rebuilt with it in "Fix audio resume", which also keeps the DSP's state across sleep and HOME (a 144 KB linear buffer while OpenDSP runs). opendsp-patches/0001 still applies. renumber() (eea1550) goes. Each wrap of the AI stream's buffer ids (about every 18 minutes) is logged instead, so a log shows whether the stream's rate held across it. firmware.s's .incbin images are OBJECT_DEPENDS of it: the assembler's dependency output leaves them out, and this bump otherwise linked the old firmware. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
Problem
libctru numbers each channel's wave buffers 1 to 65535 and then starts again from 1 (
ndspChnWaveBufAdd, which skips 0).dequeue()infirmware/voice.cplays the lowest id in the queue next, so it breaks at the wrap:dequeue()picks 1, the lowest id, ahead of 65533–65535.ndspiReadChnState, libctru walks its list up to the DSP's current id and marks 65533–65535 as done. On the DSP they stay queued, and because every later id is lower, they never play.voice_queue()returns -1 for some of libctru's adds. libctru still counts those buffers as sent and marks them done once the voice moves past them, so they never play.A channel that is fed without pause and never reset hits this after 65,535 buffers. From then on it uses up its stream faster than real time.
Where it showed up
rt64-3ds, the 3DS renderer and platform layer for several N64 recompilation ports (Goemon 64, Banjo-Kazooie, Donkey Kong 64, Dinosaur Planet), embeds OpenDSP 1.2.2. Each game's audio stream is one ndsp channel, fed with 60 buffers a second. On an Old 3DS log from Goemon 64:
A simulation of libctru's bookkeeping against this
dequeue()reproduces both changes. With buffers played in queue order instead, nothing changes at the wrap.Fix
The ids are compared as a signed 16-bit distance,
(s16)(a - b) < 0. Queued ids are always within a few of each other, so this gives the same order as before except across the wrap, where 1 now plays after 65535.Testing
firmware/opendsp.cdc, since I don't have the teak-llvm toolchain built, so this PR changes the source only. The image needs rebuilding, and the change hasn't run on hardware.ndspChnWaveBufClear, which resets libctru's counter; the next buffer then carries the partial reset that empties the DSP's queue. It then queues the unplayed buffers again.🤖 Generated with Claude Code