Files
ci-libretro-fceumm/src/filter.c

283 lines
9.3 KiB
C
Raw Normal View History

#include "fceu-types.h"
#include "sound.h"
#include "x6502.h"
#include "fceu.h"
#include "filter.h"
#include "fcoeffs.h"
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 13:58:49 +00:00
#if defined(__SSE2__)
#include <emmintrin.h>
#elif defined(__ARM_NEON) || defined(__ARM_NEON__)
#include <arm_neon.h>
#endif
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
static uint32_t mrindex;
static uint32_t mrratio;
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
int64_t sexyfilter_acc1 = 0, sexyfilter_acc2 = 0;
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
/* 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;
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. */
}
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
void SexyFilter2(int32_t *in, int32_t count) {
while (count--) {
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
int64_t dropcurrent;
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
dropcurrent = ((*in << 16) - sexyfilter2_acc) >> 3;
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
sexyfilter2_acc += dropcurrent;
*in = sexyfilter2_acc >> 16;
2017-10-15 03:13:11 +08:00
in++;
}
}
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
void SexyFilter(int32_t *in, int32_t *out, int32_t count) {
int32_t mul1, mul2, vmul;
mul1 = (94 << 16) / FSettings.SndRate;
mul2 = (24 << 16) / FSettings.SndRate;
vmul = (FSettings.SoundVolume << 16) * 3 / 4 / 100;
if (FSettings.soundq)
vmul /= 4;
else
vmul *= 2; /* TODO: Increase volume in low quality sound rendering code itself */
while (count) {
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
int64_t ino = (int64_t) * in * vmul;
sexyfilter_acc1 += ((ino - sexyfilter_acc1) * mul1) >> 16;
sexyfilter_acc2 += ((ino - sexyfilter_acc1 - sexyfilter_acc2) * mul2) >> 16;
*in = 0;
{
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
int32_t t = (sexyfilter_acc1 - ino + sexyfilter_acc2) >> 16;
if (t > 32767) t = 32767;
if (t < -32768) t = -32768;
*out = t;
}
in++;
out++;
count--;
}
}
/* Returns number of samples written to out. */
/* leftover is set to the number of samples that need to be copied
from the end of in to the beginning of in.
*/
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
/* static uint32_t mva=1000; */
/* This filtering code assumes that almost all input values stay below 32767.
Do not adjust the volume in the wlookup tables and the expansion sound
code to be higher, or you *might* overflow the FIR code.
*/
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 13:58:49 +00:00
/* Int16 mirrors of the symmetric windowed-sinc coefficient tables built
* in MakeFilters. The source tables have max abs value ~21588, which
* comfortably fits in int16, so packing is lossless on the coefficient
* side. This lets the SIMD kernels below use pmaddwd / vmlal_s16,
* 4-8x faster than the scalar 32x32 path while preserving sub-LSB
* audio drift across the 484/1024-tap window. */
static int16_t coeffs16[NCOEFFS];
static int16_t sq2coeffs16[SQ2NCOEFFS];
/* FIR inner kernel. ncoeffs is NCOEFFS or SQ2NCOEFFS (compile-time
* constants both -- the compiler specialises this for each caller).
* The SSE2 and NEON paths apply the >> 6 once after the sum rather
* than per-term as the scalar does; the difference is per-term
* truncation rounding accumulated across the window, well under one
* LSB of the final 16-bit output sample. Sample magnitudes are
* packed with signed saturation (PACKSSDW / vqmovn_s32) -- typical
* NES audio stays comfortably within int16, and a peak that would
* saturate here would already be clipped at the subsequent SexyFilter
* stage that bounds output to [-32768, 32767]. */
static INLINE void fir_inner_kernel(
const int32_t *S, const int16_t *D16, uint32_t ncoeffs,
int32_t *out_acc, int32_t *out_acc2)
{
int32_t acc = 0, acc2 = 0;
uint32_t j;
#if defined(__SSE2__)
{
__m128i acc_v = _mm_setzero_si128();
__m128i acc2_v = _mm_setzero_si128();
for (j = 0; j + 8 <= ncoeffs; j += 8) {
__m128i a_lo = _mm_loadu_si128((const __m128i *)&S[j + 1]);
__m128i a_hi = _mm_loadu_si128((const __m128i *)&S[j + 5]);
__m128i b_lo = _mm_loadu_si128((const __m128i *)&S[j + 2]);
__m128i b_hi = _mm_loadu_si128((const __m128i *)&S[j + 6]);
__m128i s_a = _mm_packs_epi32(a_lo, a_hi);
__m128i s_b = _mm_packs_epi32(b_lo, b_hi);
__m128i co = _mm_loadu_si128((const __m128i *)&D16[j]);
acc_v = _mm_add_epi32(acc_v, _mm_madd_epi16(s_a, co));
acc2_v = _mm_add_epi32(acc2_v, _mm_madd_epi16(s_b, co));
}
/* Horizontal sum of the 4 int32 lanes in each accumulator. */
{
__m128i shuf = _mm_shuffle_epi32(acc_v, _MM_SHUFFLE(2,3,0,1));
__m128i s1 = _mm_add_epi32(acc_v, shuf);
shuf = _mm_shuffle_epi32(s1, _MM_SHUFFLE(1,0,3,2));
acc = _mm_cvtsi128_si32(_mm_add_epi32(s1, shuf));
shuf = _mm_shuffle_epi32(acc2_v, _MM_SHUFFLE(2,3,0,1));
s1 = _mm_add_epi32(acc2_v, shuf);
shuf = _mm_shuffle_epi32(s1, _MM_SHUFFLE(1,0,3,2));
acc2 = _mm_cvtsi128_si32(_mm_add_epi32(s1, shuf));
}
acc >>= 6;
acc2 >>= 6;
}
#elif defined(__ARM_NEON) || defined(__ARM_NEON__)
{
int32x4_t acc_v0 = vdupq_n_s32(0), acc_v1 = vdupq_n_s32(0);
int32x4_t acc2_v0 = vdupq_n_s32(0), acc2_v1 = vdupq_n_s32(0);
for (j = 0; j + 8 <= ncoeffs; j += 8) {
int32x4_t a0 = vld1q_s32(&S[j + 1]);
int32x4_t a1 = vld1q_s32(&S[j + 5]);
int32x4_t b0 = vld1q_s32(&S[j + 2]);
int32x4_t b1 = vld1q_s32(&S[j + 6]);
int16x8_t s_a = vcombine_s16(vqmovn_s32(a0), vqmovn_s32(a1));
int16x8_t s_b = vcombine_s16(vqmovn_s32(b0), vqmovn_s32(b1));
int16x8_t co = vld1q_s16(&D16[j]);
acc_v0 = vmlal_s16(acc_v0, vget_low_s16(s_a), vget_low_s16(co));
acc_v1 = vmlal_s16(acc_v1, vget_high_s16(s_a), vget_high_s16(co));
acc2_v0 = vmlal_s16(acc2_v0, vget_low_s16(s_b), vget_low_s16(co));
acc2_v1 = vmlal_s16(acc2_v1, vget_high_s16(s_b), vget_high_s16(co));
}
{
int32x4_t s_acc = vaddq_s32(acc_v0, acc_v1);
int32x4_t s_acc2 = vaddq_s32(acc2_v0, acc2_v1);
#if defined(__aarch64__)
acc = vaddvq_s32(s_acc);
acc2 = vaddvq_s32(s_acc2);
#else
int32x2_t p = vadd_s32(vget_low_s32(s_acc), vget_high_s32(s_acc));
acc = vget_lane_s32(vpadd_s32(p, p), 0);
p = vadd_s32(vget_low_s32(s_acc2), vget_high_s32(s_acc2));
acc2 = vget_lane_s32(vpadd_s32(p, p), 0);
#endif
}
acc >>= 6;
acc2 >>= 6;
}
#else
j = 0;
#endif
/* Scalar tail handles whatever 0..7 taps the SIMD block didn't,
* and the entire window on builds without SSE2 or NEON. */
for (; j < ncoeffs; j++) {
acc += (S[j + 1] * (int32_t)D16[j]) >> 6;
acc2 += (S[j + 2] * (int32_t)D16[j]) >> 6;
}
*out_acc = acc;
*out_acc2 = acc2;
}
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
int32_t NeoFilterSound(int32_t *in, int32_t *out, uint32_t inlen, int32_t *leftover) {
uint32_t x;
int32_t *outsave = out;
int32_t count = 0;
uint32_t max = (inlen - 1) << 16;
if (FSettings.soundq == 2) {
for (x = mrindex; x < max; x += mrratio) {
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 13:58:49 +00:00
int32_t acc, acc2;
int32_t *S = &in[(x >> 16) - SQ2NCOEFFS];
fir_inner_kernel(S, sq2coeffs16, SQ2NCOEFFS, &acc, &acc2);
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
acc = ((int64_t)acc * (65536 - (x & 65535)) + (int64_t)acc2 * (x & 65535)) >> (16 + 11);
*out = acc;
out++;
count++;
}
} else {
for (x = mrindex; x < max; x += mrratio) {
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 13:58:49 +00:00
int32_t acc, acc2;
int32_t *S = &in[(x >> 16) - NCOEFFS];
fir_inner_kernel(S, coeffs16, NCOEFFS, &acc, &acc2);
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
acc = ((int64_t)acc * (65536 - (x & 65535)) + (int64_t)acc2 * (x & 65535)) >> (16 + 11);
*out = acc;
out++;
count++;
}
}
mrindex = x - max;
if (FSettings.soundq == 2) {
mrindex += SQ2NCOEFFS * 65536;
*leftover = SQ2NCOEFFS + 1;
} else {
mrindex += NCOEFFS * 65536;
*leftover = NCOEFFS + 1;
}
if (GameExpSound.NeoFill)
GameExpSound.NeoFill(outsave, count);
SexyFilter(outsave, outsave, count);
if (FSettings.lowpass)
SexyFilter2(outsave, count);
return(count);
}
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
void MakeFilters(int32_t rate) {
int32_t *tabs[6] = { C44100NTSC, C44100PAL, C48000NTSC, C48000PAL, C96000NTSC,
C96000PAL };
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
int32_t *sq2tabs[6] = { SQ2C44100NTSC, SQ2C44100PAL, SQ2C48000NTSC, SQ2C48000PAL,
SQ2C96000NTSC, SQ2C96000PAL };
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
int32_t *tmp;
int32_t x;
uint32_t nco;
if (FSettings.soundq == 2)
nco = SQ2NCOEFFS;
else
nco = NCOEFFS;
mrindex = (nco + 1) << 16;
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
mrratio = (PAL ? (int64_t)(PAL_CPU * 65536) : (int64_t)(NTSC_CPU * 65536)) / rate;
if (FSettings.soundq == 2)
tmp = sq2tabs[(PAL ? 1 : 0) | (rate == 48000 ? 2 : 0) | (rate == 96000 ? 4 : 0)];
else
tmp = tabs[(PAL ? 1 : 0) | (rate == 48000 ? 2 : 0) | (rate == 96000 ? 4 : 0)];
if (FSettings.soundq == 2)
for (x = 0; x < (SQ2NCOEFFS >> 1); x++)
sq2coeffs[x] = sq2coeffs[SQ2NCOEFFS - 1 - x] = tmp[x];
else
for (x = 0; x < (NCOEFFS >> 1); x++)
coeffs[x] = coeffs[NCOEFFS - 1 - x] = tmp[x];
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 13:58:49 +00:00
/* Build the int16 mirror used by the SIMD inner kernel. All
* source FIR tables have max abs value ~21588 (fits in int16
* losslessly), so a straight narrowing cast preserves every
* coefficient bit. Done once per filter rebuild -- 1024 entries
* is well under 1 us. */
if (FSettings.soundq == 2) {
for (x = 0; x < SQ2NCOEFFS; x++)
sq2coeffs16[x] = (int16_t)sq2coeffs[x];
} else {
for (x = 0; x < NCOEFFS; x++)
coeffs16[x] = (int16_t)coeffs[x];
}
}