core: audio determinism fix and pipeline cleanup

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.
This commit is contained in:
U-DESKTOP-SPFP6AQ\twistedtechre
2026-05-04 05:08:11 +02:00
parent 6359a0b36e
commit 9f6b84cf33
4 changed files with 85 additions and 37 deletions

View File

@@ -1,4 +1,3 @@
#include <math.h>
#include "fceu-types.h"
#include "sound.h"
@@ -11,35 +10,34 @@
static uint32_t mrindex;
static uint32_t mrratio;
void SexyFilter2(int32_t *in, int32_t count) {
#ifdef moo
static int64_t acc = 0;
double x, p;
int64_t c;
int64_t sexyfilter_acc1 = 0, sexyfilter_acc2 = 0;
x = 2 * M_PI * 6000 / FSettings.SndRate;
p = ((double)2 - cos(x)) - sqrt(pow((double)2 - cos(x), 2) - 1);
/* SexyFilter2's separate accumulator. Like sexyfilter_acc1/_acc2 it
* was a function-local static, which meant FCEUSND_Power couldn't
* reset it - cart B in a long-running process would inherit cart A's
* lowpass state. Lifted to file scope alongside the others so
* SexyFilter_Reset can zero it too. */
int64_t sexyfilter2_acc = 0;
c = p * 0x100000;
#endif
static int64_t acc = 0;
while (count--) {
int64_t dropcurrent;
dropcurrent = ((*in << 16) - acc) >> 3;
acc += dropcurrent;
*in = acc >> 16;
in++;
#if 0
acc=((int64_t)0x100000-c)* *in + ((c*acc)>>20);
*in=acc>>20;
in++;
#endif
}
void SexyFilter_Reset(void)
{
sexyfilter_acc1 = 0;
sexyfilter_acc2 = 0;
sexyfilter2_acc = 0;
/* mrindex is reset in MakeFilters, which FCEUI_Sound calls on
* every cart load, so it doesn't need to be reset here. */
}
int64_t sexyfilter_acc1 = 0, sexyfilter_acc2 = 0;
void SexyFilter2(int32_t *in, int32_t count) {
while (count--) {
int64_t dropcurrent;
dropcurrent = ((*in << 16) - sexyfilter2_acc) >> 3;
sexyfilter2_acc += dropcurrent;
*in = sexyfilter2_acc >> 16;
in++;
}
}
void SexyFilter(int32_t *in, int32_t *out, int32_t count) {
int32_t mul1, mul2, vmul;