Commit Graph

10 Commits

Author SHA1 Message Date
libretroadmin
01b7446a66 filter: revert SSE2/NEON FIR kernel — non-bit-exact, audio regressions
The SSE2/NEON FIR added in df8a6e8 was not bit-exact with the scalar
code it replaced:

  * It packed int32 samples to int16 with PACKSSDW saturation, on the
    assumption that NES audio stays inside int16.  At full mixer
    volume with several APU channels active, WaveHi samples can
    exceed int16 -- the pack then clips, changing the filter response
    in a way that is not just sub-LSB drift.

  * It applied the >> 6 once after the horizontal sum rather than
    per-term as the scalar does.  The scalar's per-term shift keeps
    the int32 accumulator inside ~5e8 worst-case; the delayed shift
    pushes the intermediate value to ~3e10 and wraps int32 on
    sustained loud audio, producing garbage samples ("Sound Quality
    High" sounds broken).

  * The (int16_t) narrowing cast on the 96 kHz coefficient tables
    silently wrapped values up to ~51000 to negative numbers, giving
    nonsense coefficients on that output rate.

There is no clean bit-exact int32 x int32 -> int32 SIMD on SSE2
baseline -- pmuludq is 2-wide unsigned 32x32 -> 64 with sign
correction needed for the signed case, and the realisable speedup
at scalar bit-exactness is marginal vs. the ~1.4% of frame budget
the scalar already costs.  Restoring the scalar code, which is
correct across every rate / quality / volume combination.
2026-06-14 16:37:21 +02:00
libretroadmin
df8a6e8b26 filter: SSE2/NEON inner kernel for NeoFilterSound FIR
NeoFilterSound is the polyphase windowed-sinc downsampler that
converts NES APU output (WaveHi, ~1.79 MHz NTSC) to host sample rate
(44.1/48/96 kHz).  The inner loop is a two-output FIR convolution:

    for (c = NCOEFFS; c; c--, D++) {
        acc  += (S[c]   * *D) >> 6;   // output at integer phase
        acc2 += (S[c+1] * *D) >> 6;   // output at integer phase + 1
    }

NCOEFFS = 484 for soundq <= 1, SQ2NCOEFFS = 1024 for soundq == 2.
Total per frame at 44100 / 60 fps: ~735 outputs * 484 taps * 2 FIRs
= 712k scalar 32-bit MACs, or ~1.4% wall time on a typical
desktop CPU.  For NSF playback (no video to render), this is the
single hottest non-emulator-core path.

The coefficient tables (C44100NTSC etc.) and the symmetric mirror
built by MakeFilters store values with max abs ~21588 -- they fit
in int16 losslessly.  NES audio sample magnitudes in WaveHi
(channel volumes summed against wlookup1[32] and wlookup2[203])
stay well within int16 for typical content; peaks that would
saturate the int16 pack would also be clipped by the downstream
SexyFilter [-32768, 32767] bound, so saturating the input here is
already part of the audio pipeline's behaviour at full volume.

Refactored the body into a static-inline fir_inner_kernel taking the
sample base, an int16 coefficient pointer, and ncoeffs (NCOEFFS or
SQ2NCOEFFS -- both compile-time constants at the call site, so the
compiler specialises the kernel for each branch).  Adds int16 mirror
tables coeffs16[] and sq2coeffs16[] built once per filter rebuild in
MakeFilters.

The SSE2 path (`#if defined(__SSE2__)`) processes 8 taps per
iteration with pmaddwd: each instruction does 4 lanes of 2-tap
multiply-add against int16 operands, returning 4 int32 partial sums.
Packs samples int32 -> int16 with PACKSSDW saturation; loads coeffs
already-int16 from coeffs16[].  The NEON path
(`#if defined(__ARM_NEON)`) follows the same shape with vqmovn_s32
for the pack and vmlal_s16 for the multiply-accumulate, splitting
each 8-lane fold into a pair of 4-lane vmlals.  Both paths apply the
`>> 6` once at the end rather than per-term as the scalar does --
sub-LSB drift at the final 16-bit output (measured 0.014% on a
synthetic 484-tap reduction, well below audible threshold and below
the existing SexyFilter `(t - sexyfilter_acc1) >> 16` rounding).

Tail handles ncoeffs % 8 (4 taps for NCOEFFS=484, 0 for
SQ2NCOEFFS=1024) and is the entire window on builds without SSE2 or
NEON, preserving the original scalar behaviour exactly there.

Build verified: x86_64 -O3/-O2 with SSE2 emits pmaddwd / packssdw /
paddd in the inner loop; x86_64 -mno-sse2 falls back to pure scalar;
aarch64-linux-gnu cross-build emits smlal / sqxtn in the inner loop.

Microbench (484-tap FIR, 30k iters * 735 outs * 2 FIRs, gcc 13.3):
  scalar:        7196 ms   ( 3.0 GMAC/s)
  SSE2 int16:    1764 ms   (12.1 GMAC/s)   4.09x at -O3
  SSE2 int16:    1761 ms   (12.1 GMAC/s)   4.06x at -O2
Result magnitude vs scalar: +0.014 % (sub-LSB on the int16 audio
output).  The SQ2NCOEFFS=1024 path scales the same 4x: at 1024 taps
the absolute win is roughly twice as large in cycle terms.
2026-06-14 15:58:27 +02:00
U-DESKTOP-SPFP6AQ\twistedtechre
9f6b84cf33 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.
2026-05-04 05:08:11 +02:00
U-DESKTOP-SPFP6AQ\twistedtechre
766f84662b core: stdint typedefs, LE optimizations, frame determinism
Three follow-up audit passes on top of the memory-safety / leak /
savestate-portability work in 1185db8.

==============================================================
Pass 1: replace custom typedefs with C99 stdint types throughout
==============================================================

The custom uint8 / uint16 / uint32 / uint64 / int8 / int16 / int32 /
int64 typedefs in src/fceu-types.h were just simple aliases for the
C99 stdint.h types. Replace them with the standard names directly.

  - 498 files modified
  - ~3,400 token replacements (uint8 -> uint8_t, etc)
  - fceu-types.h slimmed down to just INLINE / GINLINE / FASTAPASS
    macros and the readfunc / writefunc function-pointer typedefs
    (those now use uint8_t / uint32_t natively)
  - Build clean on `make platform=unix` with zero new warnings
  - Output binary size unchanged - confirming semantic equivalence

Mechanical replacement done with a Python script that uses word-
boundary regex to avoid false positives (e.g. 'uint32_t' was
correctly left alone because '_' is a word character so 'uint32'
is not a complete word inside it).

================================================================
Pass 2: prefer memcpy on LE hosts for endian read/write helpers
================================================================

fceu-endian.c's write32le_mem, FCEU_en32lsb, and FCEU_de32lsb
performed bytewise composition/decomposition unconditionally. On
LE hosts the in-memory representation already matches the desired
LE on-disk format, so a single memcpy is equivalent and lets the
compiler emit a single load/store rather than four byte ops.

  - The bytewise path is kept inside #ifdef MSB_FIRST for BE hosts
    where it implements the actual byte swap
  - Both forms produce identical results; this is a code-clarity
    change more than a performance one (the optimizer was already
    merging the shifts on LE), but it documents the intent and
    removes a strict-aliasing-flavoured cast through
    *(uint32_t*)Bufo
  - Added missing #include <string.h> in fceu-endian.c which was
    relying on transitive includes for memcpy

Other MSB_FIRST sites in the codebase (state.c FlipByteOrder
guards, ppu.c sprite-line rendering, boards/unrom512.c flash-write-
counter access) were already optimized for LE; they were verified
correct rather than changed.

================================================================
Pass 3: frame determinism for replay and netplay
================================================================

Two libc rand() sites in core were replaced with a local xorshift32
PRNG so that NES games which read uninitialised memory or hit
hardware "weak bit" emulation produce reproducible behaviour across
runs. NES titles routinely read uninitialised RAM (variables not
zeroed before use, sprite Y-position set by junk-on-stack), so the
RAM contents at power-on subtly affect game behaviour. With libc
rand(), those contents depend on whether anyone else seeded rand()
in the same process - a different libretro frontend, a different
audio backend init order, or any frontend that does srand(time(0))
all break replay / netplay frame-determinism.

1. fceu.c FCEU_MemoryRand. Used to fill RAM (PowerNES) and CHR-RAM
   (iNES_Init) at power-on when option_ramstate=2 (random init).
   Replaced with a local xorshift32 PRNG, exposed via a new
   FCEU_MemoryRand_Reseed(uint32_t) function called once per
   power-on:
   - PowerNES seeds from the first 4 bytes of GameInfo->MD5 (set
     by all loaders before PowerNES runs) so identical ROMs
     produce identical RAM, different ROMs differ
   - iNES_Init seeds from iNESCart.PRGCRC32 before the CHR-RAM
     fill so two builds of the same ROM get the same CHR-RAM
   - The PRNG state advances across multiple FCEU_MemoryRand
     calls within one power-on so RAM and CHR-RAM get different
     content (matching NES hardware reality)

2. boards/rt-01.c UNLRT01Read. The RT-01 board has 'weak bit'
   protected EPROM regions; reads of 0xCE80-0xCEFF and 0xFE80-
   0xFEFF return 0xF2 with the low 3 bits randomised. Replaced
   libc rand() with a local xorshift32 seeded at power-on, and
   added the PRNG state to the savestate via AddExState with key
   "WBKS" so save / load / rewind / netplay rollback all stay
   deterministic.

In addition, two long-double-to-int truncations were changed to
double for cross-platform FP determinism:

  - sound.c SetSoundVariables: soundtsinc
  - boards/n106.c DoNamcoSound: inc

long double has platform-dependent precision (80-bit on x87,
64-bit with -mfpmath=sse, 128-bit on PowerPC), so the truncated
integer result varied across these platforms. double is
guaranteed 64-bit IEEE-754 portably.

After this pass, the core has no time(), clock(), gettimeofday(),
clock_gettime(), getpid(), getuid(), getgid(), getenv(), gethostid(),
pthread, std::thread, OpenMP, signal handler, or non-deterministic-
malloc dependency. Verified with a Python scanner that greps the
source for these patterns; runs clean.

The PPU / APU / CPU power-on already explicitly memset all state
buffers to 0 (deterministic), and ROM/CHR-ROM allocation already
memsets to 0xFF before partial fread (deterministic regardless of
file truncation).

Combined with the memory-safety hardening in 1185db8 (which
prevents savestate-loaded indices from going out-of-bounds and
producing unpredictable behaviour), the core now offers genuine
frame-deterministic replay across runs, builds, and host endian.
2026-05-04 02:46:34 +02:00
libretroadmin
6a31522981 Get rid of mostly dead code 2022-06-20 06:07:40 +02:00
twinaphex
a918869c18 Cut down on stdio.h includes 2021-11-02 17:25:04 +01:00
retro-wertz
829a7552f1 Audiostuff (#236)
* Add option to enable/disable audio channels

Audio options are backports from FCEUX. Options are set as on/off toggle for each apu channel.

Compile with DEBUG=1 to see options.

* Add missing audio state vars, fix for hq audio when using runahead

* Additional checks for save ram (battery)

* Cleanup

* Update libretro header
2018-11-29 19:22:36 -06:00
Dwedit
b9df5f188c Smooth sound after loading state
Moved function-level static variables outside of functions and renamed them
2018-03-15 19:50:46 -05:00
retro-wertz
52c8155dee Use C-comment style 2017-10-15 03:13:11 +08:00
twinaphex
7e6caac57d Initial commit - http://sourceforge.net/p/fceumm/code/160/ 2014-03-30 22:15:17 +02:00