Audit pass 6 - audio path. The libretro core only ever outputs 16-bit
stereo, so the question was whether anything in the audio pipeline is
either non-deterministic or pointlessly busy given that constraint.
DETERMINISM BUG FIX
src/filter.c's `sexyfilter_acc1` and `sexyfilter_acc2` are file-scope
int64_t IIR accumulators. They're saved/restored with the rest of the
sound state (sound.c "FAC1"/"FAC2" SFORMAT entries), but they were
NOT reset by FCEUSND_Power on cart load. So a second cart loaded in
the same process inherited the first cart's IIR state - audibly
minor (a few samples of transient before the IIR re-converges) but
non-deterministic across load orders.
`SexyFilter2`'s lowpass accumulator was even worse: a function-local
`static int64_t acc = 0` that no other code could touch. Its first-
load value was 0; from then on it just accumulated forever, with no
way to reset it. Same load-order-dependence problem, plus it wasn't
even savestate'd.
Fix:
- Lift SexyFilter2's local static to file scope as `sexyfilter2_acc`.
- Add `void SexyFilter_Reset(void)` that zeros all three accumulators.
- Call SexyFilter_Reset() from FCEUSND_Power.
- Add `sexyfilter2_acc` to the SFORMAT savestate list as "FAC3" so
runahead/replay/netplay restore it alongside FAC1/FAC2.
`mrindex` (filter.c file-scope, NeoFilterSound's input cursor) is
already reset in MakeFilters, which FCEUI_Sound calls on every cart
load, so it doesn't need explicit handling. `mrratio` is set in the
same path. Documented in the new SexyFilter_Reset comment.
PIPELINE CLEANUPS
(1) WaveHi memset bound. The HQ-mode flush was clearing the entire
WaveHi[40000] (160 KB) past `left` every frame, but channels only
write into [left, SOUNDTS) - everything past SOUNDTS is already
zero from the previous frame's clear (or FCEUSND_Power on the
first frame). Tightened the memset to (SOUNDTS - left) * 4 bytes
instead of sizeof(WaveHi) - left * 4. SOUNDTS is bounded by NES
cycles per frame (~30000 NTSC), so this saves ~40 KB of
pointless memset every HQ frame. The (SOUNDTS > left) guard
handles the degenerate case of a very short frame where SOUNDTS
might not have advanced past the coefficient history.
(2) RDoSQLQ silent-channel branch. When both square channels are
inactive, `amp[x]` is forced to 0 (line 634), which propagates
through `ttable[x]` to make `totalout = wlookup1[0] = 0`. The
previous code looped (end - start) iterations adding 0 to
Wave[V>>4] - genuinely no-op for ~30000 NES cycles per frame
when nothing is playing. Removed the loop body.
(3) LQ dispatch deduplication. `RDoSQLQ` mixes both squares in a
single call; `RDoTriangleNoisePCMLQ` mixes triangle/noise/PCM
in a single call. The previous code pointed DoSQ1 and DoSQ2
both at RDoSQLQ, and DoTriangle/DoNoise/DoPCM all at
RDoTriangleNoisePCMLQ. The first call did the work; the
subsequent calls entered the function only to be rejected by
the `if (end <= start) return;` guard at the top. Now point
DoSQ1/DoTriangle at the real workers and DoSQ2/DoNoise/DoPCM
at Dummyfunc - same behaviour, no redundant call+early-return
on every frame.
(4) stereo_filter_apply_delay copy loop -> memcpy. The element-by-
element copy `samples[i+pos] = sound_buffer[i]` is trivially
equivalent to memcpy for non-overlapping int32_t blocks; memcpy
lets the compiler/libc dispatch SIMD where available.
(5) Dead code in filter.c. Removed the `#ifdef moo` and `#if 0`
blocks (legacy commented-out lowpass formula attempts that
have been dead since the original blargg import) and the
`<math.h>` include, which is no longer needed once those
blocks are gone.
ARCHITECTURAL NOTES (not changed - documented for future passes)
- Per-frame sample count varies by +/-1 (e.g. 798/799 at 48000Hz
60Hz NTSC). This is required by libretro's variable-batch audio
model and is fundamental to the deterministic phase accumulator
(`soundtsoffs` carries the fractional remainder across frames).
Same input -> same count sequence. Replay/netplay-stable.
- The `stereo_filter_apply_null` post-pass exists to convert
in-place from int32 mono samples to int32 stereo-packed
((s<<16)|(s&0xFFFF)) format that, when the buffer is cast to
int16_t*, reads correctly as L,R,L,R pairs. Could be folded
into SexyFilter's clamp step to save one full-buffer pass
(~190 KB/s of redundant memory traffic) but requires sound.c
to know about the libretro frontend's stereo filter selection.
Too much frontend/core coupling to chase for a modest cache
win; left as-is.
- HQ path runs 5 in-place passes through WaveFinal[] per frame
(NeoFilterSound, SexyFilter, optional SexyFilter2,
stereo_filter_apply, audio_batch_cb read). LQ path runs 4.
Each pass is ~3.2 KB at 48000Hz - ~1 MB/s of buffer traffic
total. Modest; not worth restructuring.
- WaveHi[40000] is BSS-allocated (160 KB) even when in LQ mode
where it's never written. Cost is zero - BSS pages don't
materialise until first write - so making it conditional has
no runtime benefit, only adds complexity.
- stereo_filter_delay uses a linear buffer with memmove on each
frame to slide consumed samples back to position 0. A circular
buffer would eliminate the memmove but only matters when the
user opts into the delay filter (off by default).
Build clean under -std=gnu11 with -Wno-write-strings -Wsign-compare
-Wundef -Wmissing-prototypes; zero errors, zero warnings.
audit_determinism.py: no rand/time/long double/threads issues.