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

1370 lines
34 KiB
C
Raw Normal View History

/* FCE Ultra - NES/Famicom Emulator
*
* Copyright notice for this file:
* Copyright (C) 2002 Xodnizel
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include <stdlib.h>
#include <string.h>
#include "fceu-types.h"
#include "x6502.h"
#include "fceu.h"
#include "sound.h"
#include "filter.h"
#include "state.h"
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 wlookup1[32];
static uint32_t wlookup2[203];
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 Wave[2048 + 512];
int32_t WaveHi[40000];
int32_t WaveFinal[2048 + 512];
EXPSOUND GameExpSound = { 0, 0, 0, 0, 0, 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
static uint8_t TriCount = 0;
static uint8_t TriMode = 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
static int32_t tristep = 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
static int32_t wlcount[4] = { 0, 0, 0, 0 }; /* Wave length counters. */
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 uint8_t IRQFrameMode = 0; /* $4017 / xx000000 */
static uint8_t PSG[0x10];
static uint8_t RawDALatch = 0; /* $4011 0xxxxxxx */
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
uint8_t EnabledChannels = 0; /* Byte written to $4015 */
typedef struct {
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
uint8_t Speed;
uint8_t Mode; /* Fixed volume(1), and loop(2) */
uint8_t DecCountTo1;
uint8_t decvolume;
int reloaddec;
} ENVUNIT;
2021-06-05 15:05:07 +02:00
unsigned DMC_7bit = 0; /* used to skip overclocking */
static ENVUNIT EnvUnits[3];
static const int RectDuties[4] = { 1, 2, 4, 6 };
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 int32_t RectDutyCount[2];
static uint8_t sweepon[2];
static int32_t curfreq[2];
static uint8_t SweepCount[2];
static uint8_t sweepReload[2];
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 uint16_t nreg = 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
static uint8_t fcnt = 0;
static int32_t fhcnt = 0;
static int32_t fhinc = 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
uint32_t soundtsoffs = 0;
/* Variables exclusively for low-quality sound. */
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 nesincsize = 0;
uint32_t soundtsinc = 0;
uint32_t soundtsi = 0;
static int32_t sqacc[2];
static uint32_t lq_tcout;
static int32_t lq_triacc;
static int32_t lq_noiseacc;
/* LQ variables segment ends. */
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 int32_t lengthcount[4];
static const uint8_t lengthtable[0x20] =
{
0x0A, 0xFE, 0x14, 0x02, 0x28, 0x04, 0x50, 0x06,
0xa0, 0x08, 0x3c, 0x0a, 0x0e, 0x0c, 0x1a, 0x0e,
0x0c, 0x10, 0x18, 0x12, 0x30, 0x14, 0x60, 0x16,
0xc0, 0x18, 0x48, 0x1a, 0x10, 0x1c, 0x20, 0x1E
};
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 const uint32_t NTSCNoiseFreqTable[0x10] =
{
0x004, 0x008, 0x010, 0x020, 0x040, 0x060, 0x080, 0x0A0,
0x0CA, 0x0FE, 0x17C, 0x1FC, 0x2FA, 0x3F8, 0x7F2, 0xFE4
};
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 const uint32_t PALNoiseFreqTable[0x10] =
{
2017-09-02 09:12:42 +08:00
0x004, 0x008, 0x00E, 0x01E, 0x03C, 0x058, 0x076, 0x094,
0x0BC, 0x0EC, 0x162, 0x1D8, 0x2C4, 0x3B0, 0x762, 0xEC2
};
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 const uint32_t NTSCDMCTable[0x10] =
{
0x1AC, 0x17C, 0x154, 0x140, 0x11E, 0x0FE, 0x0E2, 0x0D6,
0x0BE, 0x0A0, 0x08E, 0x080, 0x06A, 0x054, 0x048, 0x036
};
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 const uint32_t PALDMCTable[0x10] =
{
0x18E, 0x162, 0x13C, 0x12A, 0x114, 0x0EC, 0x0D2, 0x0C6,
0x0B0, 0x094, 0x084, 0x076, 0x062, 0x04E, 0x042, 0x032
};
2017-10-15 03:13:11 +08:00
/* $4010 - Frequency
* $4011 - Actual data outputted
* $4012 - Address register: $c000 + V*64
* $4013 - Size register: Size in bytes = (V+1)*64
*/
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 int32_t DMCacc = 1;
static int32_t DMCPeriod = 0;
static uint8_t DMCBitCount = 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
static uint8_t DMCAddressLatch = 0, DMCSizeLatch = 0; /* writes to 4012 and 4013 */
static uint8_t DMCFormat = 0; /* Write to $4010 */
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 DMCAddress = 0;
static int32_t DMCSize = 0;
static uint8_t DMCShift = 0;
static uint8_t SIRQStat = 0;
static char DMCHaveDMA = 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
static uint8_t DMCDMABuf = 0;
static char DMCHaveSample = 0;
static void Dummyfunc(void) { }
static void (*DoNoise)(void) = Dummyfunc;
static void (*DoTriangle)(void) = Dummyfunc;
static void (*DoPCM)(void) = Dummyfunc;
static void (*DoSQ1)(void) = Dummyfunc;
static void (*DoSQ2)(void) = Dummyfunc;
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 ChannelBC[5];
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 void LoadDMCPeriod(uint8_t V) {
if (PAL)
DMCPeriod = PALDMCTable[V];
else
DMCPeriod = NTSCDMCTable[V];
}
static void PrepDPCM() {
DMCAddress = 0x4000 + (DMCAddressLatch << 6);
DMCSize = (DMCSizeLatch << 4) + 1;
}
/* Instantaneous? Maybe the new freq value is being calculated all of the time... */
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 int FASTAPASS(2) CheckFreq(uint32_t cf, uint8_t sr) {
uint32_t mod;
if (!(sr & 0x8)) {
mod = cf >> (sr & 7);
if ((mod + cf) & 0x800)
return(0);
}
return(1);
}
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 void SQReload(int x, uint8_t V) {
if (EnabledChannels & (1 << x))
lengthcount[x] = lengthtable[(V >> 3) & 0x1f];
curfreq[x] = (curfreq[x] & 0xff) | ((V & 7) << 8);
RectDutyCount[x] = 7;
EnvUnits[x].reloaddec = 1;
}
static DECLFW(Write_PSG) {
A &= 0x1F;
switch (A) {
2017-10-05 14:43:20 +08:00
case 0x0:
DoSQ1();
EnvUnits[0].Mode = (V & 0x30) >> 4;
EnvUnits[0].Speed = (V & 0xF);
2021-06-05 15:05:07 +02:00
if (swapDuty)
V = (V & 0x3F) | ((V & 0x80) >> 1) | ((V & 0x40) << 1);
break;
case 0x1:
DoSQ1();
sweepReload[0] = 1;
sweepon[0] = (V & 0x80);
break;
case 0x2:
DoSQ1();
curfreq[0] &= 0xFF00;
curfreq[0] |= V;
break;
case 0x3:
DoSQ1();
SQReload(0, V);
break;
case 0x4:
DoSQ2();
EnvUnits[1].Mode = (V & 0x30) >> 4;
EnvUnits[1].Speed = (V & 0xF);
2021-06-05 15:05:07 +02:00
if (swapDuty)
V = (V & 0x3F) | ((V & 0x80) >> 1) | ((V & 0x40) << 1);
break;
case 0x5:
DoSQ2();
sweepReload[1] = 1;
sweepon[1] = (V & 0x80);
break;
2017-10-05 14:43:20 +08:00
case 0x6:
DoSQ2();
curfreq[1] &= 0xFF00;
curfreq[1] |= V;
break;
case 0x7:
DoSQ2();
SQReload(1, V);
break;
2017-10-05 14:43:20 +08:00
case 0xa:
DoTriangle();
break;
case 0xb:
DoTriangle();
if (EnabledChannels & 0x4)
lengthcount[2] = lengthtable[(V >> 3) & 0x1f];
2017-10-05 14:43:20 +08:00
TriMode = 1; /* Load mode */
break;
2017-10-05 14:43:20 +08:00
case 0xC:
DoNoise();
EnvUnits[2].Mode = (V & 0x30) >> 4;
EnvUnits[2].Speed = (V & 0xF);
break;
2017-10-05 14:43:20 +08:00
case 0xE:
DoNoise();
break;
case 0xF:
DoNoise();
if (EnabledChannels & 0x8)
lengthcount[3] = lengthtable[(V >> 3) & 0x1f];
EnvUnits[2].reloaddec = 1;
break;
2017-10-05 14:43:20 +08:00
case 0x10:
DoPCM();
LoadDMCPeriod(V & 0xF);
if (SIRQStat & 0x80) {
if (!(V & 0x80)) {
X6502_IRQEnd(FCEU_IQDPCM);
SIRQStat &= ~0x80;
} else X6502_IRQBegin(FCEU_IQDPCM);
}
break;
}
PSG[A] = V;
}
static DECLFW(Write_DMCRegs) {
A &= 0xF;
switch (A) {
case 0x00: DoPCM();
LoadDMCPeriod(V & 0xF);
if (SIRQStat & 0x80) {
if (!(V & 0x80)) {
X6502_IRQEnd(FCEU_IQDPCM);
SIRQStat &= ~0x80;
} else X6502_IRQBegin(FCEU_IQDPCM);
}
DMCFormat = V;
break;
case 0x01: DoPCM();
RawDALatch = V & 0x7F;
2018-12-08 11:35:04 +08:00
if (RawDALatch)
DMC_7bit = 1;
break;
case 0x02:
2018-12-08 11:35:04 +08:00
DMCAddressLatch = V;
if (V)
DMC_7bit = 0;
2018-12-08 11:35:04 +08:00
break;
case 0x03:
2018-12-08 11:35:04 +08:00
DMCSizeLatch = V;
if (V)
DMC_7bit = 0;
break;
}
}
static DECLFW(StatusWrite) {
int x;
DoSQ1();
DoSQ2();
DoTriangle();
DoNoise();
DoPCM();
2018-12-08 11:35:04 +08:00
for (x = 0; x < 4; x++)
if (!(V & (1 << x))) lengthcount[x] = 0; /* Force length counters to 0. */
if (V & 0x10) {
if (!DMCSize)
PrepDPCM();
} else {
DMCSize = 0;
}
SIRQStat &= ~0x80;
X6502_IRQEnd(FCEU_IQDPCM);
EnabledChannels = V & 0x1F;
}
static DECLFR(StatusRead) {
int x;
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
uint8_t ret;
ret = SIRQStat;
for (x = 0; x < 4; x++) ret |= lengthcount[x] ? (1 << x) : 0;
if (DMCSize) ret |= 0x10;
{
SIRQStat &= ~0x40;
X6502_IRQEnd(FCEU_IQFCOUNT);
}
return ret;
}
static void FASTAPASS(1) FrameSoundStuff(int V) {
int P;
DoSQ1();
DoSQ2();
DoNoise();
DoTriangle();
if (!(V & 1)) { /* Envelope decay, linear counter, length counter, freq sweep */
if (!(PSG[8] & 0x80))
if (lengthcount[2] > 0)
lengthcount[2]--;
if (!(PSG[0xC] & 0x20)) /* Make sure loop flag is not set. */
if (lengthcount[3] > 0)
lengthcount[3]--;
for (P = 0; P < 2; P++) {
if (!(PSG[P << 2] & 0x20)) /* Make sure loop flag is not set. */
if (lengthcount[P] > 0)
lengthcount[P]--;
/* Frequency Sweep Code Here */
/* xxxx 0000 */
/* xxxx = hz. 120/(x+1)*/
/* http://wiki.nesdev.com/w/index.php/APU_Sweep */
if (SweepCount[P] > 0) SweepCount[P]--;
if (SweepCount[P] <= 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
uint32_t sweepShift = (PSG[(P << 2) + 0x1] & 7);
if (sweepon[P] && sweepShift && curfreq[P] >= 8) {
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 mod = (curfreq[P] >> sweepShift);
if (PSG[(P << 2) + 0x1] & 0x8) {
curfreq[P] -= (mod + (P ^ 1));
} else if ((mod + curfreq[P]) < 0x800) {
curfreq[P] += mod;
}
}
SweepCount[P] = (((PSG[(P << 2) + 0x1] >> 4) & 7) + 1);
}
if (sweepReload[P]) {
SweepCount[P] = (((PSG[(P << 2) + 0x1] >> 4) & 7) + 1);
sweepReload[P] = 0;
}
}
}
/* Now do envelope decay + linear counter. */
2017-10-15 03:13:11 +08:00
if (TriMode)/* In load mode? */
TriCount = PSG[0x8] & 0x7F;
else if (TriCount)
TriCount--;
if (!(PSG[0x8] & 0x80))
TriMode = 0;
for (P = 0; P < 3; P++) {
if (EnvUnits[P].reloaddec) {
EnvUnits[P].decvolume = 0xF;
EnvUnits[P].DecCountTo1 = EnvUnits[P].Speed + 1;
EnvUnits[P].reloaddec = 0;
continue;
}
if (EnvUnits[P].DecCountTo1 > 0) EnvUnits[P].DecCountTo1--;
if (EnvUnits[P].DecCountTo1 == 0) {
EnvUnits[P].DecCountTo1 = EnvUnits[P].Speed + 1;
if (EnvUnits[P].decvolume || (EnvUnits[P].Mode & 0x2)) {
EnvUnits[P].decvolume--;
EnvUnits[P].decvolume &= 0xF;
}
}
}
}
void FrameSoundUpdate(void) {
2017-10-15 03:13:11 +08:00
/* Linear counter: Bit 0-6 of $4008
* Length counter: Bit 4-7 of $4003, $4007, $400b, $400f
*/
if (fcnt == 3) {
if (IRQFrameMode & 0x2)
fhcnt += fhinc;
}
2018-12-08 11:35:04 +08:00
FrameSoundStuff(fcnt);
fcnt = (fcnt + 1) & 3;
2018-12-08 11:35:04 +08:00
/* has to be moved here to fix Dragon Warrior 4
* after irq inhibit fix for $4017 */
if (!fcnt && !(IRQFrameMode & 0x3)) {
SIRQStat |= 0x40;
X6502_IRQBegin(FCEU_IQFCOUNT);
}
}
static INLINE void tester(void) {
if (DMCBitCount == 0) {
if (!DMCHaveDMA)
DMCHaveSample = 0;
else {
DMCHaveSample = 1;
DMCShift = DMCDMABuf;
DMCHaveDMA = 0;
}
}
}
static INLINE void DMCDMA(void) {
if (DMCSize && !DMCHaveDMA) {
X6502_DMR(0x8000 + DMCAddress);
X6502_DMR(0x8000 + DMCAddress);
X6502_DMR(0x8000 + DMCAddress);
DMCDMABuf = X6502_DMR(0x8000 + DMCAddress);
DMCHaveDMA = 1;
DMCAddress = (DMCAddress + 1) & 0x7fff;
DMCSize--;
if (!DMCSize) {
if (DMCFormat & 0x40)
PrepDPCM();
else {
2018-12-08 11:35:04 +08:00
if (DMCFormat & 0x80) {
SIRQStat |= 0x80;
X6502_IRQBegin(FCEU_IQDPCM);
2018-12-08 11:35:04 +08:00
}
}
}
}
}
void FASTAPASS(1) FCEU_SoundCPUHook(int cycles) {
fhcnt -= cycles * 48;
if (fhcnt <= 0) {
FrameSoundUpdate();
fhcnt += fhinc;
}
DMCDMA();
DMCacc -= cycles;
while (DMCacc <= 0) {
if (DMCHaveSample) {
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
uint8_t bah = RawDALatch;
int t = ((DMCShift & 1) << 2) - 2;
/* Unbelievably ugly hack */
if (FSettings.SndRate) {
soundtsoffs += DMCacc;
DoPCM();
soundtsoffs -= DMCacc;
}
RawDALatch += t;
if (RawDALatch & 0x80)
RawDALatch = bah;
}
DMCacc += DMCPeriod;
DMCBitCount = (DMCBitCount + 1) & 7;
DMCShift >>= 1;
tester();
}
}
core: stride-aware savestate, iNES2 helpers, -Wundef, -Wmissing-prototypes Audit pass 5 - five distinct cleanups bundled into one omnibus. 1. Element-stride byte-swapping for savestate fields (state.h, state.c, fceu-endian.{h,c}) The SFORMAT 's' field was previously {bit 31 = RLSB, bits 0..30 = byte size}. RLSB triggers FlipByteOrder() on MSB hosts, which reverses the entire entry buffer end-to-end. That is correct for a single primitive (size <= 8 bytes) but wrong for an array of multi-byte primitives - reversing the whole buffer would swap element 0 with element N-1 and reverse their bytes too, scrambling the data. The previous workaround was either splitting an N-element array into N separate single-primitive entries with distinct chunk IDs (n106 PlayIndex split into IDX0..IDX7) or skipping the entry entirely on big-endian hosts (the GEKKO #ifndef in vrc6.c / vrc7.c). Both approaches mean BE saves are not portable to LE and vice versa, and force the same workaround at every new array site. This pass adds proper stride support: * SFORMAT 's' encoding is now {bit 31 = RLSB, bits 24..30 = stride in bytes (0 = legacy/unset), bits 0..23 = byte size}. 16 MiB max size, well above any actual savestate field. * FCEUSTATE_RLSB_ARRAY(stride) macro for the new pattern. * FlipByteOrderStrided() byte-swaps each element of an array independently. Round-trip identity verified: [01 00 00 00 ...] -> [00 00 00 01 ...] -> [01 00 00 00 ...]. * state.c's SubWrite / ReadStateChunk / CheckS use new helpers sf_size() / sf_stride() / sf_flip() that mask the size out of the new bit layout and dispatch to the strided variant when stride < size. Backwards compatible: legacy single-primitive entries (size == 1, 2, 4, 8) leave the stride bits at zero, which sf_stride() reads as "stride equals size" and falls through to FlipByteOrder() as before. No on-disk format change. Existing single-primitive RLSB sites are unchanged. The infrastructure is now in place so any future SFORMAT entry that is an array of multi-byte primitives can be expressed as a single entry (e.g. "{ buf, sizeof(buf) | FCEUSTATE_RLSB_ARRAY(4), "BUF." }") without splitting or skipping. The existing PlayIndex split and GEKKO #ifndefs are intentionally left untouched - migrating them would alter the on-disk savestate format and is a separate decision. 2. iNES1-vs-iNES2 sizing helpers (cart.h) Twelve sites across the codebase encoded the same conditional: info->iNES2 ? (info->PRGRamSize + info->PRGRamSaveSize) : default Sometimes for PRGRAM, sometimes for CHRRAM, sometimes in bytes, sometimes after dividing by 1024. The pattern is verbose and easy to write inconsistently. Added two inline helpers in cart.h: - CartInfo_PRGRAM_bytes(info, default_bytes) - CartInfo_CHRRAM_bytes(info, default_bytes) Migrated 9 of the 13 sites: cartram.c (2), 162.c, 163.c, 134.c, 399.c, 478.c, 480.c, 484.c. The remaining 4 are non-helper-fitting variants (164.c special masking, 2 cartram SaveGameLen sites with different fallback semantics, mmc3.c Boogerman submapper detection). 3. -Wundef enabled permanently (Makefile.libretro) Zero warnings out of the box - no #if-on-undefined-macro footguns in the codebase. Now part of WARNING_DEFINES alongside the existing -Wsign-compare. 4. -Wmissing-prototypes enabled permanently (Makefile.libretro) Started at 198 warnings, cleared all of them: * Mass-static-ified ~96 functions across 75 files that were defined non-static but only used within their own translation unit. (See static_prototype_fixer.py in the development notes.) * K&R-style empty-parens prototypes "()" replaced with explicit "(void)" across all asic_*.{c,h} files - GCC treats "()" as "any args" and refuses to match it against a separate K&R definition. * Added missing forward declarations to public headers: - fds.h (FDSLoad) - nsf.h (NSFLoad) - ines.h (iNESLoad) - unif.h (UNIFLoad) - latch.h (LatchHardReset, K&R fix) - eeprom_93Cx6.h (eeprom_93Cx6_read, K&R fix) Each header gained an "#include "file.h"" where needed. * fds_apu.c now includes its own fds_apu.h header (was missing). * fds_apu.h: removed unused FDSSoundRead declaration (the function is internal-static). * cartram.h: removed unused CartRAM_close declaration (the function is internal-static). * input.h: added a centralised block of FCEU_Init* prototypes (Zapper, Mouse, Powerpad, Arkanoid, VirtualBoy, FKB, SuborKB, PEC586KB, HS, Mahjong, FamilyTrainerA/B, OekaKids, TopRider, BarcodeWorld, BattleBox, QuizKing, FTrainerA/B, SpaceShadow, LCDCompZapper, ArkanoidFC) plus FCEU_ZapperSetTolerance. These were previously declared as "extern" inside src/input.c. * Static-ified FP_FASTAPASS callbacks in 106.c, 65.c, 67.c, asic_h3001.c, asic_vrc3.c (those with no external callers); left non-static for those that have header decls or are referenced from sibling .c files (asic_mmc1, asic_vrc6, asic_vrc7, flashrom). * For a small set of cross-file functions where adding a header was disproportionate to the value (MMC5_hb, NSFMMC5_Close, GetKeyboard, FCEU_GetJoyJoy), placed a forward declaration immediately above the definition. This satisfies -Wmissing-prototypes (which checks for any prior declaration in scope) without churning the public-header layout. 5. -Wshadow partial cleanup (not enabled permanently) Fixed five real shadows that were either bugs or actively misleading: * src/boards/476.c: removed an inner "int i" that shadowed the outer loop counter. * src/boards/mmc5.c MMC5_hb: parameter "scanline" renamed to "sl_param" (was shadowing the global "scanline"). * src/boards/n106.c DoNamcoSound: parameter "Wave" renamed to "WaveBuf" (was shadowing the global Wave audio buffer); also updated the forward declaration and the matching parameter on sound.h's NeoFill function pointer typedef. * src/boards/vrc7.c UpdateOPLNEO: same Wave -> WaveBuf rename. * src/ntsc/nes_ntsc_impl.h: renamed an inner loop counter "n" that shadowed an outer "n". * src/drivers/libretro/libretro.c FCEUD_RegionOverride: local "pal" renamed to "is_pal" (was shadowing the typedef "pal" from palette.h). * src/palette.c FCEUI_SetPaletteArray: parameter "pal" renamed to "data" (same shadow); driver.h declaration updated to match. -Wshadow itself is NOT enabled permanently because the remaining warnings are deliberate parameter naming conventions (XBuf in draw functions, X in cpu hooks) and third-party blargg ntsc code. In addition, four files were touched as part of an MSVC-build fix that came up mid-pass: src/fds.c, src/nsf.c, src/ines.c, and src/drivers/libretro/libretro_dipswitch.c had snprintf() calls introduced in pass 4 that fail to link on pre-MSVC2015 toolchains when STATIC_LINKING=1 (the libretro-common compat_snprintf.c shim isn't compiled in those configurations). Replaced each snprintf with either sprintf-into-bounded-buffer (the format strings have known maximum output) or strlcpy/strlcat for the dipswitch key-build case. All output is still bounded; truncation happens via strl*'s normal truncation semantics where applicable. All added code is C89-clean (top-of-block declarations only, no mixed decls, no // comments, INLINE macro from fceu-types.h instead of bare "inline"). Builds clean under -std=gnu11 with -Wno-write- strings -Wsign-compare -Wundef -Wmissing-prototypes; zero errors, zero warnings. Determinism audit (audit_determinism.py): no rand/time/long double/threads issues introduced.
2026-05-04 04:44:52 +02:00
static void RDoPCM(void) {
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
uint32_t V;
for (V = ChannelBC[4]; V < SOUNDTS; V++)
/* TODO: get rid of floating calculations to binary. set log volume scaling. */
WaveHi[V] += (((RawDALatch << 16) / 256) * FSettings.PCMVolume ) & (~0xFFFF);
ChannelBC[4] = SOUNDTS;
}
/* This has the correct phase. Don't mess with it. */
static INLINE void RDoSQ(int x) {
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 V;
int32_t amp;
int32_t rthresh;
int32_t *D;
int32_t currdc;
int32_t cf;
int32_t rc;
V = SOUNDTS - ChannelBC[x];
cf = (curfreq[x] + 1) * 2;
rc = wlcount[x];
/* added 2018/12/08 */
/* when pulse channel is silenced, resets length counters but not
* duty cycle, instead of resetting both */
if ((curfreq[x] < 8 || curfreq[x] > 0x7ff) ||
!CheckFreq(curfreq[x], PSG[(x << 2) | 0x1]) ||
!lengthcount[x]) {
rc -= V;
if (rc <= 0) {
rc = cf - (-rc % cf);
}
} else {
int dutyCycle;
if (EnvUnits[x].Mode & 0x1)
amp = EnvUnits[x].Speed;
else
amp = EnvUnits[x].decvolume;
/* Modify Square wave volume based on channel volume modifiers
* Note: the formulat x = x * y /100 does not yield exact results,
* but is "close enough" and avoids the need for using double values
* or implicit cohersion which are slower (we need speed here) */
/* TODO: Optimize this. */
if (FSettings.SquareVolume[x] != 256)
amp = (amp * FSettings.SquareVolume[x]) / 256;
amp <<= 24;
dutyCycle = (PSG[(x << 2)] & 0xC0) >> 6;
rthresh = RectDuties[dutyCycle];
currdc = RectDutyCount[x];
D = &WaveHi[ChannelBC[x]];
while (V > 0) {
if (currdc < rthresh)
*D += amp;
rc--;
if (!rc) {
rc = cf;
currdc = (currdc + 1) & 7;
}
V--;
D++;
}
RectDutyCount[x] = currdc;
}
wlcount[x] = rc;
ChannelBC[x] = SOUNDTS;
}
static void RDoSQ1(void) {
RDoSQ(0);
}
static void RDoSQ2(void) {
RDoSQ(1);
}
static void RDoSQLQ(void) {
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 start, end;
int32_t V;
int32_t amp[2];
int32_t rthresh[2];
int32_t freq[2];
int x;
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 inie[2];
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 ttable[2][8];
int32_t totalout;
start = ChannelBC[0];
end = (SOUNDTS << 16) / soundtsinc;
if (end <= start) return;
ChannelBC[0] = end;
for (x = 0; x < 2; x++) {
int y;
int dutyCycle;
inie[x] = nesincsize;
if (curfreq[x] < 8 || curfreq[x] > 0x7ff)
inie[x] = 0;
if (!CheckFreq(curfreq[x], PSG[(x << 2) | 0x1]))
inie[x] = 0;
if (!lengthcount[x])
inie[x] = 0;
if (EnvUnits[x].Mode & 0x1)
amp[x] = EnvUnits[x].Speed;
else
amp[x] = EnvUnits[x].decvolume;
/* Modify Square wave volume based on channel volume modifiers
* Note: the formulat x = x * y /100 does not yield exact results,
* but is "close enough" and avoids the need for using double vales
* or implicit cohersion which are slower (we need speed here)
* fixed - setting up maximum volume for square2 caused complete mute square2 channel.
* TODO: Optimize this. */
if (FSettings.SquareVolume[x] != 256)
amp[x] = (amp[x] * FSettings.SquareVolume[x]) / 256;
if (!inie[x]) amp[x] = 0; /* Correct? Buzzing in MM2, others otherwise... */
dutyCycle = (PSG[(x << 2)] & 0xC0) >> 6;
rthresh[x] = RectDuties[dutyCycle];
for (y = 0; y < 8; y++) {
if (y < rthresh[x])
ttable[x][y] = amp[x];
else
ttable[x][y] = 0;
}
freq[x] = (curfreq[x] + 1) << 1;
freq[x] <<= 17;
}
totalout = wlookup1[ ttable[0][RectDutyCount[0]] + ttable[1][RectDutyCount[1]] ];
if (!inie[0] && !inie[1]) {
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
/* Both squares silent: amp[x] was forced to 0 above (line
* "if (!inie[x]) amp[x] = 0"), 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. Skip the loop entirely. */
} else {
for (V = start; V < end; V++) {
2017-10-15 03:13:11 +08:00
/* int tmpamp=0;
if(RectDutyCount[0]<rthresh[0])
tmpamp=amp[0];
if(RectDutyCount[1]<rthresh[1])
tmpamp+=amp[1];
tmpamp=wlookup1[tmpamp];
tmpamp = wlookup1[ ttable[0][RectDutyCount[0]] + ttable[1][RectDutyCount[1]] ];
*/
2017-10-15 03:13:11 +08:00
Wave[V >> 4] += totalout; /* tmpamp; */
sqacc[0] -= inie[0];
sqacc[1] -= inie[1];
if (sqacc[0] <= 0) {
rea:
sqacc[0] += freq[0];
RectDutyCount[0] = (RectDutyCount[0] + 1) & 7;
if (sqacc[0] <= 0) goto rea;
totalout = wlookup1[ ttable[0][RectDutyCount[0]] + ttable[1][RectDutyCount[1]] ];
}
if (sqacc[1] <= 0) {
rea2:
sqacc[1] += freq[1];
RectDutyCount[1] = (RectDutyCount[1] + 1) & 7;
if (sqacc[1] <= 0) goto rea2;
totalout = wlookup1[ ttable[0][RectDutyCount[0]] + ttable[1][RectDutyCount[1]] ];
}
}
}
}
static void RDoTriangle(void) {
core: OOM safety, sign-compare cleanup, dead-code removal, const-correctness Omnibus cleanup pass. Build is clean on `make platform=unix` with zero errors and zero warnings, including under `-Wsign-compare -Wstrict-aliasing=2 -Wcast-align`. The `-Wsign-compare` flag is now permanently enabled in WARNING_DEFINES. ================================================================ A. FCEU_gmalloc no longer exit()s on OOM ================================================================ A libretro core must not call exit(): doing so tears down the entire frontend. FCEU_gmalloc previously did exactly that on allocation failure ("Doing a hard exit"). It now returns NULL with the same diagnostic message. Loader-level call sites that can fail their parent function (i.e. return 0 from FDSLoad/iNESLoad/NSFLoad to refuse the cart) now check the return value: - ines.c: trainerpoo, ExtraNTARAM - nsf.c: ExWRAM (both branches) - fds.c: FDSBIOS, CHRRAM, FDSRAM Mapper-level callers (~200 sites) are intentionally left as-is: they live in void-returning Init/Power functions where graceful failure isn't possible without a much larger restructuring. With NULL returns those mappers will null-deref on first access, which is contained to the core - the libretro frontend stays up. This is strictly better than the previous behaviour of exit()ing the entire frontend. ================================================================ B. -Wsign-compare cleanup (27 warnings -> 0) ================================================================ Surveyed every signed/unsigned comparison the compiler flagged and fixed each one. Most fell into a few patterns: Loop variables: changed `int x` to `uint32_t x` for loops over uint32_t counts (TotalSides in fds.c, the trainer-copy loop in 6_8_12_17_561_562.c, soundOffset->SOUNDTS in 594.c). scanline (signed) vs totalscanlines/normal_scanlines (unsigned) in ppu.c: cast (unsigned)scanline at the comparison sites. The scanline values are guaranteed >= 0 at every comparison site (the loop initialises scanline=0 and increments). rate_adjust macro in emu2413.c: the ?: was returning either signed or unsigned depending on `rate`. Cast both branches to uint32_t. Mixed ?: branches in cartram.c (PRGRamSaveSize signed vs WRAMSize unsigned): cast PRGRamSaveSize to uint32_t at use. Other one-off casts in libretro.c, n625092.c, nsf.c, sound.c, zapper.c, eeprom_93Cx6.c. The Makefile change keeps -Wsign-compare permanently enabled so new sign-compare bugs trip CI immediately. Files touched: 594.c, 6_8_12_17_561_562.c, cartram.c, eeprom_93Cx6.c, emu2413.c, n625092.c, fds.c, nsf.c, ppu.c, sound.c, input/zapper.c, drivers/libretro/libretro.c, plus Makefile.libretro. ================================================================ C. NULL-deref hardening on libretro callbacks ================================================================ retro_serialize and retro_unserialize now reject NULL data pointers before passing them to memstream_set_buffer (which would have null-deref'd inside the memstream code). Other retro_* entry points that take pointers were already guarded in earlier passes (retro_set_controller_port_device, retro_get_memory_data, retro_get_memory_size, retro_load_game, retro_cheat_set). ================================================================ D. Mapper coverage spot-check ================================================================ Wrote a heuristic scanner to find the savestate-load-array-index bug class fixed in pass 1 (variable masked at write but unmasked at restore -> OOB index). Scanner flagged 3 candidates across 424 mappers; manual review confirmed all three are false positives: - sachen.c `cmd`: theoretical bug, but only one writer is wired per game and that writer masks at use. - unrom512.c `flash_state` and `latcha`: both already clamped in StateRestore (added in pass 1). The systematic bug class was thoroughly addressed in pass 1. ================================================================ E. Strip never-defined #ifdef symbols ================================================================ Surveyed every #ifdef symbol in the build and cross-checked against #defines (in source and in build files). Removed code gated on symbols that are never defined for any platform target: - DEBUG_MAPPER (datalatch.c, 2 sites): a debug-print NROMWrite handler. Dead. - FRAMESKIP (fceu.h, driver.h, ppu.c, 4 sites): a legacy-FCEU frameskip path. The libretro driver's `skip` argument to FCEUI_Emulate is always 0, and FCEUI_FrameSkip is never called by any libretro frontend. Removed the conditional rendering branch in FCEUPPU_Loop along with the FCEU_PutImageDummy declaration. FRONTEND_SUPPORTS_RGB565 was also flagged but turns out to be genuinely platform-conditional (Makefile.common defines it when WANT_32BPP=0). Kept. ================================================================ F. assert() audit ================================================================ Two assert() calls live in src/ntsc/nes_ntsc_impl.h - third-party NTSC filter code from blargg, both NaN sanity checks (`assert(x == x)`). NDEBUG is in the build flags so they compile to no-ops. No fceumm code uses assert. Nothing to do here. ================================================================ G. const-correctness ================================================================ Function signatures that take strings they don't modify now take const char *: FCEU_printf, FCEU_PrintError (const char *format) FCEUD_PrintError, FCEUD_Message (const char *) FCEU_MakeFName (const char *cd1) md5_update (const uint8_t *input) md5_process (const uint8_t data[64]) also made `static` ================================================================ H. Const-fold static lookup tables ================================================================ Marked static lookup tables const where they are never written: x6502.c: CycTable[256] (cycle counts) md5.c: md5_padding[64] (MD5 padding) vsuni.c: secdata[2][32], secptr (VS security data) palette.c: rtmul/gtmul/btmul[7] (palette multipliers) input/cursor.c: GunSight, FCEUcursor (sprite data) input/pec586kb.c, fkb.c, suborkb.c: matrix (key matrices) boards/8237.c: regperm, adrperm, protarray (mapper perms) boards/datalatch.c: M538Banks (bank table) boards/187.c: prot_data boards/121.c: prot_array boards/bonza.c: sim0reset boards/pec-586.c: bs_tbl, br_tbl boards/178.c: step_size, step_adj (ADPCM tables) boards/244.c: prg_perm, chr_perm boards/bmc42in1r.c: banks boards/emu2413.c: SL (sustain levels) NSFROM in nsf.c looks like a lookup table but is rewritten at runtime to patch in addresses, so it stays mutable. ================================================================ I. Reduce strlen calls ================================================================ Replaced `strlen(STRING_LITERAL)` with `sizeof(STRING_LITERAL) - 1` where the argument is a compile-time-known string literal: - libretro.c retro_set_environment APU loop: was calling strlen("fceumm_apu_") on every loop iteration to compute the same constant offset. - nsf.c visualizer: strlen("Song:"). Other strlen sites in cheat.c, libretro.c, unif.c either already cache to a local size_t or operate on runtime-supplied strings where caching would not help. ================================================================ Build status ================================================================ `make platform=unix` clean: zero errors, zero warnings. With -Wsign-compare -Wstrict-aliasing=2 -Wcast-align: zero warnings. audit_determinism.py: 0 issues. Output binary 4,388,408 bytes (was 4,388,576 from upstream 004c147; -168 bytes from dead-code removal).
2026-05-04 03:55:23 +02:00
uint32_t V;
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 tcout = (tristep & 0xF);
if (!(tristep & 0x10)) tcout ^= 0xF;
2017-10-15 03:13:11 +08:00
tcout = (tcout * 3) << 16; /* (tcout<<1); */
if (!lengthcount[2] || !TriCount) { /* Counter is halted, but we still need to output. */
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 *start = &WaveHi[ChannelBC[2]];
int32_t count = SOUNDTS - ChannelBC[2];
while (count--) {
*start += (tcout / 256 * FSettings.TriangleVolume) & (~0xFFFF); /* TODO OPTIMIZE ME */
start++;
}
/* cout = (tcout / 256 * FSettings.TriangleVolume) & (~0xFFFF);
2017-10-15 03:13:11 +08:00
for(V = ChannelBC[2]; V < SOUNDTS; V++)
WaveHi[V] += cout; */
} else {
for (V = ChannelBC[2]; V < SOUNDTS; V++) {
WaveHi[V] += (tcout / 256 * FSettings.TriangleVolume) & (~0xFFFF); /* TODO OPTIMIZE ME! */
wlcount[2]--;
if (!wlcount[2]) {
wlcount[2] = (PSG[0xa] | ((PSG[0xb] & 7) << 8)) + 1;
tristep++;
tcout = (tristep & 0xF);
if (!(tristep & 0x10)) tcout ^= 0xF;
tcout = (tcout * 3) << 16;
}
}
}
ChannelBC[2] = SOUNDTS;
}
static void RDoTriangleNoisePCMLQ(void) {
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 V;
int32_t start, end;
int32_t freq[2];
int32_t inie[2];
uint32_t amptab[2];
uint32_t noiseout;
int nshift;
core: APU channel disable - fix bitmasks and LQ-mode volume application Two related bugs in the user-facing fceumm_apu_1..5 channel disable toggles. Pre-existing since commit 448a231 (set_apu_channels) and at least as far back as the LQ rewrite for the second one. set_apu_channels (libretro.c) ----------------------------- The bitmask built by check_variables uses bit i for fceumm_apu_(i+1) enabled, ordered SQ1, SQ2, Triangle, Noise, DMC. The old code: FSettings.SquareVolume[1] = (chan & 1) ? 256 : 0; FSettings.SquareVolume[0] = (chan & 2) ? 256 : 0; FSettings.TriangleVolume = (chan & 3) ? 256 : 0; FSettings.NoiseVolume = (chan & 4) ? 256 : 0; FSettings.PCMVolume = (chan & 5) ? 256 : 0; crossed SQ1/SQ2 and used non-power-of-two masks for the remaining three. Practical effect: apu_1 muted SQ2 and apu_2 muted SQ1; apu_3 through apu_5 left their channels audible in any normal config (the masks happened to be true any time another bit was set). Fixed to read each channel's own bit: FSettings.SquareVolume[0] = (chan & 0x01) ? 256 : 0; /* SQ1 */ FSettings.SquareVolume[1] = (chan & 0x02) ? 256 : 0; /* SQ2 */ FSettings.TriangleVolume = (chan & 0x04) ? 256 : 0; /* Tri */ FSettings.NoiseVolume = (chan & 0x08) ? 256 : 0; /* Nse */ FSettings.PCMVolume = (chan & 0x10) ? 256 : 0; /* DMC */ RDoTriangleNoisePCMLQ (sound.c) ------------------------------- The combined Triangle/Noise/DMC LQ mix uses a non-linear lookup table: totalout = wlookup2[lq_tcout + noiseout + RawDALatch]; The old code took the noise envelope (EnvUnits[2], not Triangle - Triangle has no envelope; EnvUnits[0]=SQ1, [1]=SQ2, [2]=Noise) and scaled it by FSettings.TriangleVolume, despite the comment claiming to modify Triangle. Meanwhile lq_tcout (Triangle) and RawDALatch (DMC) went into wlookup2 unscaled, so neither TriangleVolume nor PCMVolume had any effect in LQ mode. Fixed: * amptab[0] (noise envelope) is now scaled by FSettings.NoiseVolume. * lq_tcout is scaled by FSettings.TriangleVolume into a local scaled_tcout fed to wlookup2. * RawDALatch is scaled by FSettings.PCMVolume into scaled_dmc. The volume scalars are loop-invariant within this function, so the Triangle volume is cached once in tri_vol at function entry to keep the inner loops tight; the != 256 fast paths preserve byte-exact behavior at full volume. Verification ------------ Built per-channel-only test ROMs (test_sq1_only, test_sq2_only, test_tri_only, test_nse_only, test_dmc_only) and an apu_test runner that measures output RMS with each fceumm_apu_N toggle. After the fix, in both LQ and HQ modes, each apu_N toggle mutes exactly the channel whose ROM is the only sound source, and no other toggle has any effect on it. Default (all-on) audio output is bit-exact identical to the baseline across the existing test_silent / test_idle / test_active / test_stress ROMs (1500 frames each), so games and homebrew that never touch the apu_N options see no change. No performance regression in the default path; the != 256 guards skip the multiplies.
2026-05-04 07:40:08 +02:00
uint32_t scaled_tcout;
uint32_t scaled_dmc;
const uint32_t tri_vol = FSettings.TriangleVolume;
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 totalout;
start = ChannelBC[2];
end = (SOUNDTS << 16) / soundtsinc;
if (end <= start) return;
ChannelBC[2] = end;
inie[0] = inie[1] = nesincsize;
freq[0] = (((PSG[0xa] | ((PSG[0xb] & 7) << 8)) + 1));
if (!lengthcount[2] || !TriCount || freq[0] <= 4)
inie[0] = 0;
freq[0] <<= 17;
if (EnvUnits[2].Mode & 0x1)
amptab[0] = EnvUnits[2].Speed;
else
amptab[0] = EnvUnits[2].decvolume;
core: APU channel disable - fix bitmasks and LQ-mode volume application Two related bugs in the user-facing fceumm_apu_1..5 channel disable toggles. Pre-existing since commit 448a231 (set_apu_channels) and at least as far back as the LQ rewrite for the second one. set_apu_channels (libretro.c) ----------------------------- The bitmask built by check_variables uses bit i for fceumm_apu_(i+1) enabled, ordered SQ1, SQ2, Triangle, Noise, DMC. The old code: FSettings.SquareVolume[1] = (chan & 1) ? 256 : 0; FSettings.SquareVolume[0] = (chan & 2) ? 256 : 0; FSettings.TriangleVolume = (chan & 3) ? 256 : 0; FSettings.NoiseVolume = (chan & 4) ? 256 : 0; FSettings.PCMVolume = (chan & 5) ? 256 : 0; crossed SQ1/SQ2 and used non-power-of-two masks for the remaining three. Practical effect: apu_1 muted SQ2 and apu_2 muted SQ1; apu_3 through apu_5 left their channels audible in any normal config (the masks happened to be true any time another bit was set). Fixed to read each channel's own bit: FSettings.SquareVolume[0] = (chan & 0x01) ? 256 : 0; /* SQ1 */ FSettings.SquareVolume[1] = (chan & 0x02) ? 256 : 0; /* SQ2 */ FSettings.TriangleVolume = (chan & 0x04) ? 256 : 0; /* Tri */ FSettings.NoiseVolume = (chan & 0x08) ? 256 : 0; /* Nse */ FSettings.PCMVolume = (chan & 0x10) ? 256 : 0; /* DMC */ RDoTriangleNoisePCMLQ (sound.c) ------------------------------- The combined Triangle/Noise/DMC LQ mix uses a non-linear lookup table: totalout = wlookup2[lq_tcout + noiseout + RawDALatch]; The old code took the noise envelope (EnvUnits[2], not Triangle - Triangle has no envelope; EnvUnits[0]=SQ1, [1]=SQ2, [2]=Noise) and scaled it by FSettings.TriangleVolume, despite the comment claiming to modify Triangle. Meanwhile lq_tcout (Triangle) and RawDALatch (DMC) went into wlookup2 unscaled, so neither TriangleVolume nor PCMVolume had any effect in LQ mode. Fixed: * amptab[0] (noise envelope) is now scaled by FSettings.NoiseVolume. * lq_tcout is scaled by FSettings.TriangleVolume into a local scaled_tcout fed to wlookup2. * RawDALatch is scaled by FSettings.PCMVolume into scaled_dmc. The volume scalars are loop-invariant within this function, so the Triangle volume is cached once in tri_vol at function entry to keep the inner loops tight; the != 256 fast paths preserve byte-exact behavior at full volume. Verification ------------ Built per-channel-only test ROMs (test_sq1_only, test_sq2_only, test_tri_only, test_nse_only, test_dmc_only) and an apu_test runner that measures output RMS with each fceumm_apu_N toggle. After the fix, in both LQ and HQ modes, each apu_N toggle mutes exactly the channel whose ROM is the only sound source, and no other toggle has any effect on it. Default (all-on) audio output is bit-exact identical to the baseline across the existing test_silent / test_idle / test_active / test_stress ROMs (1500 frames each), so games and homebrew that never touch the apu_N options see no change. No performance regression in the default path; the != 256 guards skip the multiplies.
2026-05-04 07:40:08 +02:00
/* Apply per-channel volume modifiers (set via fceumm_apu_N options).
*
* EnvUnits[2] is the Noise envelope (not Triangle - Triangle has no
* envelope; EnvUnits[0]=SQ1, [1]=SQ2, [2]=Noise). The previous code
* scaled amptab[0] by FSettings.TriangleVolume, which crossed the
* Triangle and Noise mute toggles in LQ mode and left Triangle
* itself never muted. Triangle's contribution enters wlookup2 via
* lq_tcout below; PCM enters via RawDALatch. Scale each input
* channel by its own volume before the non-linear DAC mix - 0 in
* still produces 0 out, and 256 leaves the value unchanged. */
if (FSettings.NoiseVolume != 256)
amptab[0] = (amptab[0] * FSettings.NoiseVolume) / 256;
amptab[1] = 0;
amptab[0] <<= 1;
if (!lengthcount[3])
amptab[0] = inie[1] = 0; /* Quick hack speedup, set inie[1] to 0 */
noiseout = amptab[(nreg >> 0xe) & 1];
if (PSG[0xE] & 0x80)
nshift = 8;
else
nshift = 13;
core: APU channel disable - fix bitmasks and LQ-mode volume application Two related bugs in the user-facing fceumm_apu_1..5 channel disable toggles. Pre-existing since commit 448a231 (set_apu_channels) and at least as far back as the LQ rewrite for the second one. set_apu_channels (libretro.c) ----------------------------- The bitmask built by check_variables uses bit i for fceumm_apu_(i+1) enabled, ordered SQ1, SQ2, Triangle, Noise, DMC. The old code: FSettings.SquareVolume[1] = (chan & 1) ? 256 : 0; FSettings.SquareVolume[0] = (chan & 2) ? 256 : 0; FSettings.TriangleVolume = (chan & 3) ? 256 : 0; FSettings.NoiseVolume = (chan & 4) ? 256 : 0; FSettings.PCMVolume = (chan & 5) ? 256 : 0; crossed SQ1/SQ2 and used non-power-of-two masks for the remaining three. Practical effect: apu_1 muted SQ2 and apu_2 muted SQ1; apu_3 through apu_5 left their channels audible in any normal config (the masks happened to be true any time another bit was set). Fixed to read each channel's own bit: FSettings.SquareVolume[0] = (chan & 0x01) ? 256 : 0; /* SQ1 */ FSettings.SquareVolume[1] = (chan & 0x02) ? 256 : 0; /* SQ2 */ FSettings.TriangleVolume = (chan & 0x04) ? 256 : 0; /* Tri */ FSettings.NoiseVolume = (chan & 0x08) ? 256 : 0; /* Nse */ FSettings.PCMVolume = (chan & 0x10) ? 256 : 0; /* DMC */ RDoTriangleNoisePCMLQ (sound.c) ------------------------------- The combined Triangle/Noise/DMC LQ mix uses a non-linear lookup table: totalout = wlookup2[lq_tcout + noiseout + RawDALatch]; The old code took the noise envelope (EnvUnits[2], not Triangle - Triangle has no envelope; EnvUnits[0]=SQ1, [1]=SQ2, [2]=Noise) and scaled it by FSettings.TriangleVolume, despite the comment claiming to modify Triangle. Meanwhile lq_tcout (Triangle) and RawDALatch (DMC) went into wlookup2 unscaled, so neither TriangleVolume nor PCMVolume had any effect in LQ mode. Fixed: * amptab[0] (noise envelope) is now scaled by FSettings.NoiseVolume. * lq_tcout is scaled by FSettings.TriangleVolume into a local scaled_tcout fed to wlookup2. * RawDALatch is scaled by FSettings.PCMVolume into scaled_dmc. The volume scalars are loop-invariant within this function, so the Triangle volume is cached once in tri_vol at function entry to keep the inner loops tight; the != 256 fast paths preserve byte-exact behavior at full volume. Verification ------------ Built per-channel-only test ROMs (test_sq1_only, test_sq2_only, test_tri_only, test_nse_only, test_dmc_only) and an apu_test runner that measures output RMS with each fceumm_apu_N toggle. After the fix, in both LQ and HQ modes, each apu_N toggle mutes exactly the channel whose ROM is the only sound source, and no other toggle has any effect on it. Default (all-on) audio output is bit-exact identical to the baseline across the existing test_silent / test_idle / test_active / test_stress ROMs (1500 frames each), so games and homebrew that never touch the apu_N options see no change. No performance regression in the default path; the != 256 guards skip the multiplies.
2026-05-04 07:40:08 +02:00
scaled_tcout = (tri_vol != 256)
? ((lq_tcout * tri_vol) / 256)
: lq_tcout;
scaled_dmc = (FSettings.PCMVolume != 256)
? ((RawDALatch * FSettings.PCMVolume) / 256)
: RawDALatch;
totalout = wlookup2[scaled_tcout + noiseout + scaled_dmc];
if (inie[0] && inie[1]) {
for (V = start; V < end; V++) {
Wave[V >> 4] += totalout;
lq_triacc -= inie[0];
lq_noiseacc -= inie[1];
if (lq_triacc <= 0) {
rea:
lq_triacc += freq[0]; /* t; */
tristep = (tristep + 1) & 0x1F;
if (lq_triacc <= 0) goto rea;
lq_tcout = (tristep & 0xF);
if (!(tristep & 0x10)) lq_tcout ^= 0xF;
lq_tcout = lq_tcout * 3;
core: APU channel disable - fix bitmasks and LQ-mode volume application Two related bugs in the user-facing fceumm_apu_1..5 channel disable toggles. Pre-existing since commit 448a231 (set_apu_channels) and at least as far back as the LQ rewrite for the second one. set_apu_channels (libretro.c) ----------------------------- The bitmask built by check_variables uses bit i for fceumm_apu_(i+1) enabled, ordered SQ1, SQ2, Triangle, Noise, DMC. The old code: FSettings.SquareVolume[1] = (chan & 1) ? 256 : 0; FSettings.SquareVolume[0] = (chan & 2) ? 256 : 0; FSettings.TriangleVolume = (chan & 3) ? 256 : 0; FSettings.NoiseVolume = (chan & 4) ? 256 : 0; FSettings.PCMVolume = (chan & 5) ? 256 : 0; crossed SQ1/SQ2 and used non-power-of-two masks for the remaining three. Practical effect: apu_1 muted SQ2 and apu_2 muted SQ1; apu_3 through apu_5 left their channels audible in any normal config (the masks happened to be true any time another bit was set). Fixed to read each channel's own bit: FSettings.SquareVolume[0] = (chan & 0x01) ? 256 : 0; /* SQ1 */ FSettings.SquareVolume[1] = (chan & 0x02) ? 256 : 0; /* SQ2 */ FSettings.TriangleVolume = (chan & 0x04) ? 256 : 0; /* Tri */ FSettings.NoiseVolume = (chan & 0x08) ? 256 : 0; /* Nse */ FSettings.PCMVolume = (chan & 0x10) ? 256 : 0; /* DMC */ RDoTriangleNoisePCMLQ (sound.c) ------------------------------- The combined Triangle/Noise/DMC LQ mix uses a non-linear lookup table: totalout = wlookup2[lq_tcout + noiseout + RawDALatch]; The old code took the noise envelope (EnvUnits[2], not Triangle - Triangle has no envelope; EnvUnits[0]=SQ1, [1]=SQ2, [2]=Noise) and scaled it by FSettings.TriangleVolume, despite the comment claiming to modify Triangle. Meanwhile lq_tcout (Triangle) and RawDALatch (DMC) went into wlookup2 unscaled, so neither TriangleVolume nor PCMVolume had any effect in LQ mode. Fixed: * amptab[0] (noise envelope) is now scaled by FSettings.NoiseVolume. * lq_tcout is scaled by FSettings.TriangleVolume into a local scaled_tcout fed to wlookup2. * RawDALatch is scaled by FSettings.PCMVolume into scaled_dmc. The volume scalars are loop-invariant within this function, so the Triangle volume is cached once in tri_vol at function entry to keep the inner loops tight; the != 256 fast paths preserve byte-exact behavior at full volume. Verification ------------ Built per-channel-only test ROMs (test_sq1_only, test_sq2_only, test_tri_only, test_nse_only, test_dmc_only) and an apu_test runner that measures output RMS with each fceumm_apu_N toggle. After the fix, in both LQ and HQ modes, each apu_N toggle mutes exactly the channel whose ROM is the only sound source, and no other toggle has any effect on it. Default (all-on) audio output is bit-exact identical to the baseline across the existing test_silent / test_idle / test_active / test_stress ROMs (1500 frames each), so games and homebrew that never touch the apu_N options see no change. No performance regression in the default path; the != 256 guards skip the multiplies.
2026-05-04 07:40:08 +02:00
scaled_tcout = (tri_vol != 256)
? ((lq_tcout * tri_vol) / 256)
: lq_tcout;
totalout = wlookup2[scaled_tcout + noiseout + scaled_dmc];
}
if (lq_noiseacc <= 0) {
rea2:
2017-10-15 03:13:11 +08:00
/* used to added <<(16+2) when the noise table
* values were half.
*/
if (PAL)
lq_noiseacc += PALNoiseFreqTable[PSG[0xE] & 0xF] << (16 + 1);
else
lq_noiseacc += NTSCNoiseFreqTable[PSG[0xE] & 0xF] << (16 + 1);
nreg = (nreg << 1) + (((nreg >> nshift) ^ (nreg >> 14)) & 1);
nreg &= 0x7fff;
noiseout = amptab[(nreg >> 0xe) & 1];
if (lq_noiseacc <= 0) goto rea2;
core: APU channel disable - fix bitmasks and LQ-mode volume application Two related bugs in the user-facing fceumm_apu_1..5 channel disable toggles. Pre-existing since commit 448a231 (set_apu_channels) and at least as far back as the LQ rewrite for the second one. set_apu_channels (libretro.c) ----------------------------- The bitmask built by check_variables uses bit i for fceumm_apu_(i+1) enabled, ordered SQ1, SQ2, Triangle, Noise, DMC. The old code: FSettings.SquareVolume[1] = (chan & 1) ? 256 : 0; FSettings.SquareVolume[0] = (chan & 2) ? 256 : 0; FSettings.TriangleVolume = (chan & 3) ? 256 : 0; FSettings.NoiseVolume = (chan & 4) ? 256 : 0; FSettings.PCMVolume = (chan & 5) ? 256 : 0; crossed SQ1/SQ2 and used non-power-of-two masks for the remaining three. Practical effect: apu_1 muted SQ2 and apu_2 muted SQ1; apu_3 through apu_5 left their channels audible in any normal config (the masks happened to be true any time another bit was set). Fixed to read each channel's own bit: FSettings.SquareVolume[0] = (chan & 0x01) ? 256 : 0; /* SQ1 */ FSettings.SquareVolume[1] = (chan & 0x02) ? 256 : 0; /* SQ2 */ FSettings.TriangleVolume = (chan & 0x04) ? 256 : 0; /* Tri */ FSettings.NoiseVolume = (chan & 0x08) ? 256 : 0; /* Nse */ FSettings.PCMVolume = (chan & 0x10) ? 256 : 0; /* DMC */ RDoTriangleNoisePCMLQ (sound.c) ------------------------------- The combined Triangle/Noise/DMC LQ mix uses a non-linear lookup table: totalout = wlookup2[lq_tcout + noiseout + RawDALatch]; The old code took the noise envelope (EnvUnits[2], not Triangle - Triangle has no envelope; EnvUnits[0]=SQ1, [1]=SQ2, [2]=Noise) and scaled it by FSettings.TriangleVolume, despite the comment claiming to modify Triangle. Meanwhile lq_tcout (Triangle) and RawDALatch (DMC) went into wlookup2 unscaled, so neither TriangleVolume nor PCMVolume had any effect in LQ mode. Fixed: * amptab[0] (noise envelope) is now scaled by FSettings.NoiseVolume. * lq_tcout is scaled by FSettings.TriangleVolume into a local scaled_tcout fed to wlookup2. * RawDALatch is scaled by FSettings.PCMVolume into scaled_dmc. The volume scalars are loop-invariant within this function, so the Triangle volume is cached once in tri_vol at function entry to keep the inner loops tight; the != 256 fast paths preserve byte-exact behavior at full volume. Verification ------------ Built per-channel-only test ROMs (test_sq1_only, test_sq2_only, test_tri_only, test_nse_only, test_dmc_only) and an apu_test runner that measures output RMS with each fceumm_apu_N toggle. After the fix, in both LQ and HQ modes, each apu_N toggle mutes exactly the channel whose ROM is the only sound source, and no other toggle has any effect on it. Default (all-on) audio output is bit-exact identical to the baseline across the existing test_silent / test_idle / test_active / test_stress ROMs (1500 frames each), so games and homebrew that never touch the apu_N options see no change. No performance regression in the default path; the != 256 guards skip the multiplies.
2026-05-04 07:40:08 +02:00
totalout = wlookup2[scaled_tcout + noiseout + scaled_dmc];
} /* noiseacc<=0 */
} /* for(V=... */
} else if (inie[0]) {
for (V = start; V < end; V++) {
Wave[V >> 4] += totalout;
lq_triacc -= inie[0];
if (lq_triacc <= 0) {
area:
lq_triacc += freq[0]; /* t; */
tristep = (tristep + 1) & 0x1F;
if (lq_triacc <= 0) goto area;
lq_tcout = (tristep & 0xF);
if (!(tristep & 0x10)) lq_tcout ^= 0xF;
lq_tcout = lq_tcout * 3;
core: APU channel disable - fix bitmasks and LQ-mode volume application Two related bugs in the user-facing fceumm_apu_1..5 channel disable toggles. Pre-existing since commit 448a231 (set_apu_channels) and at least as far back as the LQ rewrite for the second one. set_apu_channels (libretro.c) ----------------------------- The bitmask built by check_variables uses bit i for fceumm_apu_(i+1) enabled, ordered SQ1, SQ2, Triangle, Noise, DMC. The old code: FSettings.SquareVolume[1] = (chan & 1) ? 256 : 0; FSettings.SquareVolume[0] = (chan & 2) ? 256 : 0; FSettings.TriangleVolume = (chan & 3) ? 256 : 0; FSettings.NoiseVolume = (chan & 4) ? 256 : 0; FSettings.PCMVolume = (chan & 5) ? 256 : 0; crossed SQ1/SQ2 and used non-power-of-two masks for the remaining three. Practical effect: apu_1 muted SQ2 and apu_2 muted SQ1; apu_3 through apu_5 left their channels audible in any normal config (the masks happened to be true any time another bit was set). Fixed to read each channel's own bit: FSettings.SquareVolume[0] = (chan & 0x01) ? 256 : 0; /* SQ1 */ FSettings.SquareVolume[1] = (chan & 0x02) ? 256 : 0; /* SQ2 */ FSettings.TriangleVolume = (chan & 0x04) ? 256 : 0; /* Tri */ FSettings.NoiseVolume = (chan & 0x08) ? 256 : 0; /* Nse */ FSettings.PCMVolume = (chan & 0x10) ? 256 : 0; /* DMC */ RDoTriangleNoisePCMLQ (sound.c) ------------------------------- The combined Triangle/Noise/DMC LQ mix uses a non-linear lookup table: totalout = wlookup2[lq_tcout + noiseout + RawDALatch]; The old code took the noise envelope (EnvUnits[2], not Triangle - Triangle has no envelope; EnvUnits[0]=SQ1, [1]=SQ2, [2]=Noise) and scaled it by FSettings.TriangleVolume, despite the comment claiming to modify Triangle. Meanwhile lq_tcout (Triangle) and RawDALatch (DMC) went into wlookup2 unscaled, so neither TriangleVolume nor PCMVolume had any effect in LQ mode. Fixed: * amptab[0] (noise envelope) is now scaled by FSettings.NoiseVolume. * lq_tcout is scaled by FSettings.TriangleVolume into a local scaled_tcout fed to wlookup2. * RawDALatch is scaled by FSettings.PCMVolume into scaled_dmc. The volume scalars are loop-invariant within this function, so the Triangle volume is cached once in tri_vol at function entry to keep the inner loops tight; the != 256 fast paths preserve byte-exact behavior at full volume. Verification ------------ Built per-channel-only test ROMs (test_sq1_only, test_sq2_only, test_tri_only, test_nse_only, test_dmc_only) and an apu_test runner that measures output RMS with each fceumm_apu_N toggle. After the fix, in both LQ and HQ modes, each apu_N toggle mutes exactly the channel whose ROM is the only sound source, and no other toggle has any effect on it. Default (all-on) audio output is bit-exact identical to the baseline across the existing test_silent / test_idle / test_active / test_stress ROMs (1500 frames each), so games and homebrew that never touch the apu_N options see no change. No performance regression in the default path; the != 256 guards skip the multiplies.
2026-05-04 07:40:08 +02:00
scaled_tcout = (tri_vol != 256)
? ((lq_tcout * tri_vol) / 256)
: lq_tcout;
totalout = wlookup2[scaled_tcout + noiseout + scaled_dmc];
}
}
} else if (inie[1]) {
for (V = start; V < end; V++) {
Wave[V >> 4] += totalout;
lq_noiseacc -= inie[1];
if (lq_noiseacc <= 0) {
area2:
2017-10-15 03:13:11 +08:00
/* used to be added <<(16+2) when the noise table
* values were half.
*/
if (PAL)
lq_noiseacc += PALNoiseFreqTable[PSG[0xE] & 0xF] << (16 + 1);
else
lq_noiseacc += NTSCNoiseFreqTable[PSG[0xE] & 0xF] << (16 + 1);
nreg = (nreg << 1) + (((nreg >> nshift) ^ (nreg >> 14)) & 1);
nreg &= 0x7fff;
noiseout = amptab[(nreg >> 0xe) & 1];
if (lq_noiseacc <= 0) goto area2;
core: APU channel disable - fix bitmasks and LQ-mode volume application Two related bugs in the user-facing fceumm_apu_1..5 channel disable toggles. Pre-existing since commit 448a231 (set_apu_channels) and at least as far back as the LQ rewrite for the second one. set_apu_channels (libretro.c) ----------------------------- The bitmask built by check_variables uses bit i for fceumm_apu_(i+1) enabled, ordered SQ1, SQ2, Triangle, Noise, DMC. The old code: FSettings.SquareVolume[1] = (chan & 1) ? 256 : 0; FSettings.SquareVolume[0] = (chan & 2) ? 256 : 0; FSettings.TriangleVolume = (chan & 3) ? 256 : 0; FSettings.NoiseVolume = (chan & 4) ? 256 : 0; FSettings.PCMVolume = (chan & 5) ? 256 : 0; crossed SQ1/SQ2 and used non-power-of-two masks for the remaining three. Practical effect: apu_1 muted SQ2 and apu_2 muted SQ1; apu_3 through apu_5 left their channels audible in any normal config (the masks happened to be true any time another bit was set). Fixed to read each channel's own bit: FSettings.SquareVolume[0] = (chan & 0x01) ? 256 : 0; /* SQ1 */ FSettings.SquareVolume[1] = (chan & 0x02) ? 256 : 0; /* SQ2 */ FSettings.TriangleVolume = (chan & 0x04) ? 256 : 0; /* Tri */ FSettings.NoiseVolume = (chan & 0x08) ? 256 : 0; /* Nse */ FSettings.PCMVolume = (chan & 0x10) ? 256 : 0; /* DMC */ RDoTriangleNoisePCMLQ (sound.c) ------------------------------- The combined Triangle/Noise/DMC LQ mix uses a non-linear lookup table: totalout = wlookup2[lq_tcout + noiseout + RawDALatch]; The old code took the noise envelope (EnvUnits[2], not Triangle - Triangle has no envelope; EnvUnits[0]=SQ1, [1]=SQ2, [2]=Noise) and scaled it by FSettings.TriangleVolume, despite the comment claiming to modify Triangle. Meanwhile lq_tcout (Triangle) and RawDALatch (DMC) went into wlookup2 unscaled, so neither TriangleVolume nor PCMVolume had any effect in LQ mode. Fixed: * amptab[0] (noise envelope) is now scaled by FSettings.NoiseVolume. * lq_tcout is scaled by FSettings.TriangleVolume into a local scaled_tcout fed to wlookup2. * RawDALatch is scaled by FSettings.PCMVolume into scaled_dmc. The volume scalars are loop-invariant within this function, so the Triangle volume is cached once in tri_vol at function entry to keep the inner loops tight; the != 256 fast paths preserve byte-exact behavior at full volume. Verification ------------ Built per-channel-only test ROMs (test_sq1_only, test_sq2_only, test_tri_only, test_nse_only, test_dmc_only) and an apu_test runner that measures output RMS with each fceumm_apu_N toggle. After the fix, in both LQ and HQ modes, each apu_N toggle mutes exactly the channel whose ROM is the only sound source, and no other toggle has any effect on it. Default (all-on) audio output is bit-exact identical to the baseline across the existing test_silent / test_idle / test_active / test_stress ROMs (1500 frames each), so games and homebrew that never touch the apu_N options see no change. No performance regression in the default path; the != 256 guards skip the multiplies.
2026-05-04 07:40:08 +02:00
totalout = wlookup2[scaled_tcout + noiseout + scaled_dmc];
} /* noiseacc<=0 */
}
} else {
for (V = start; V < end; V++)
Wave[V >> 4] += totalout;
}
}
static void RDoNoise(void) {
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
uint32_t V;
int32_t outo;
uint32_t amptab[2];
if (EnvUnits[2].Mode & 0x1)
amptab[0] = EnvUnits[2].Speed;
else
amptab[0] = EnvUnits[2].decvolume;
2018-12-05 15:09:13 +08:00
/* Modify Noise wave volume based on channel volume modifiers
* Note: the formulat x = x * y /100 does not yield exact results,
* but is "close enough" and avoids the need for using double vales
* or implicit cohersion which are slower (we need speed here)
* TODO: Optimize this. */
if (FSettings.NoiseVolume != 256)
amptab[0] = (amptab[0] * FSettings.NoiseVolume) / 256;
amptab[0] <<= 16;
amptab[1] = 0;
amptab[0] <<= 1;
outo = amptab[(nreg >> 0xe) & 1];
if (!lengthcount[3]) {
outo = amptab[0] = 0;
}
if (PSG[0xE] & 0x80) {/* "short" noise */
for (V = ChannelBC[3]; V < SOUNDTS; V++) {
WaveHi[V] += outo;
wlcount[3]--;
if (!wlcount[3]) {
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
uint8_t feedback;
if (PAL)
wlcount[3] = PALNoiseFreqTable[PSG[0xE] & 0xF];
else
wlcount[3] = NTSCNoiseFreqTable[PSG[0xE] & 0xF];
feedback = ((nreg >> 8) & 1) ^ ((nreg >> 14) & 1);
nreg = (nreg << 1) + feedback;
nreg &= 0x7fff;
outo = amptab[(nreg >> 0xe) & 1];
}
}
} else {
for (V = ChannelBC[3]; V < SOUNDTS; V++) {
WaveHi[V] += outo;
wlcount[3]--;
if (!wlcount[3]) {
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
uint8_t feedback;
if (PAL)
wlcount[3] = PALNoiseFreqTable[PSG[0xE] & 0xF];
else
wlcount[3] = NTSCNoiseFreqTable[PSG[0xE] & 0xF];
feedback = ((nreg >> 13) & 1) ^ ((nreg >> 14) & 1);
nreg = (nreg << 1) + feedback;
nreg &= 0x7fff;
outo = amptab[(nreg >> 0xe) & 1];
}
}
}
ChannelBC[3] = SOUNDTS;
}
core: stride-aware savestate, iNES2 helpers, -Wundef, -Wmissing-prototypes Audit pass 5 - five distinct cleanups bundled into one omnibus. 1. Element-stride byte-swapping for savestate fields (state.h, state.c, fceu-endian.{h,c}) The SFORMAT 's' field was previously {bit 31 = RLSB, bits 0..30 = byte size}. RLSB triggers FlipByteOrder() on MSB hosts, which reverses the entire entry buffer end-to-end. That is correct for a single primitive (size <= 8 bytes) but wrong for an array of multi-byte primitives - reversing the whole buffer would swap element 0 with element N-1 and reverse their bytes too, scrambling the data. The previous workaround was either splitting an N-element array into N separate single-primitive entries with distinct chunk IDs (n106 PlayIndex split into IDX0..IDX7) or skipping the entry entirely on big-endian hosts (the GEKKO #ifndef in vrc6.c / vrc7.c). Both approaches mean BE saves are not portable to LE and vice versa, and force the same workaround at every new array site. This pass adds proper stride support: * SFORMAT 's' encoding is now {bit 31 = RLSB, bits 24..30 = stride in bytes (0 = legacy/unset), bits 0..23 = byte size}. 16 MiB max size, well above any actual savestate field. * FCEUSTATE_RLSB_ARRAY(stride) macro for the new pattern. * FlipByteOrderStrided() byte-swaps each element of an array independently. Round-trip identity verified: [01 00 00 00 ...] -> [00 00 00 01 ...] -> [01 00 00 00 ...]. * state.c's SubWrite / ReadStateChunk / CheckS use new helpers sf_size() / sf_stride() / sf_flip() that mask the size out of the new bit layout and dispatch to the strided variant when stride < size. Backwards compatible: legacy single-primitive entries (size == 1, 2, 4, 8) leave the stride bits at zero, which sf_stride() reads as "stride equals size" and falls through to FlipByteOrder() as before. No on-disk format change. Existing single-primitive RLSB sites are unchanged. The infrastructure is now in place so any future SFORMAT entry that is an array of multi-byte primitives can be expressed as a single entry (e.g. "{ buf, sizeof(buf) | FCEUSTATE_RLSB_ARRAY(4), "BUF." }") without splitting or skipping. The existing PlayIndex split and GEKKO #ifndefs are intentionally left untouched - migrating them would alter the on-disk savestate format and is a separate decision. 2. iNES1-vs-iNES2 sizing helpers (cart.h) Twelve sites across the codebase encoded the same conditional: info->iNES2 ? (info->PRGRamSize + info->PRGRamSaveSize) : default Sometimes for PRGRAM, sometimes for CHRRAM, sometimes in bytes, sometimes after dividing by 1024. The pattern is verbose and easy to write inconsistently. Added two inline helpers in cart.h: - CartInfo_PRGRAM_bytes(info, default_bytes) - CartInfo_CHRRAM_bytes(info, default_bytes) Migrated 9 of the 13 sites: cartram.c (2), 162.c, 163.c, 134.c, 399.c, 478.c, 480.c, 484.c. The remaining 4 are non-helper-fitting variants (164.c special masking, 2 cartram SaveGameLen sites with different fallback semantics, mmc3.c Boogerman submapper detection). 3. -Wundef enabled permanently (Makefile.libretro) Zero warnings out of the box - no #if-on-undefined-macro footguns in the codebase. Now part of WARNING_DEFINES alongside the existing -Wsign-compare. 4. -Wmissing-prototypes enabled permanently (Makefile.libretro) Started at 198 warnings, cleared all of them: * Mass-static-ified ~96 functions across 75 files that were defined non-static but only used within their own translation unit. (See static_prototype_fixer.py in the development notes.) * K&R-style empty-parens prototypes "()" replaced with explicit "(void)" across all asic_*.{c,h} files - GCC treats "()" as "any args" and refuses to match it against a separate K&R definition. * Added missing forward declarations to public headers: - fds.h (FDSLoad) - nsf.h (NSFLoad) - ines.h (iNESLoad) - unif.h (UNIFLoad) - latch.h (LatchHardReset, K&R fix) - eeprom_93Cx6.h (eeprom_93Cx6_read, K&R fix) Each header gained an "#include "file.h"" where needed. * fds_apu.c now includes its own fds_apu.h header (was missing). * fds_apu.h: removed unused FDSSoundRead declaration (the function is internal-static). * cartram.h: removed unused CartRAM_close declaration (the function is internal-static). * input.h: added a centralised block of FCEU_Init* prototypes (Zapper, Mouse, Powerpad, Arkanoid, VirtualBoy, FKB, SuborKB, PEC586KB, HS, Mahjong, FamilyTrainerA/B, OekaKids, TopRider, BarcodeWorld, BattleBox, QuizKing, FTrainerA/B, SpaceShadow, LCDCompZapper, ArkanoidFC) plus FCEU_ZapperSetTolerance. These were previously declared as "extern" inside src/input.c. * Static-ified FP_FASTAPASS callbacks in 106.c, 65.c, 67.c, asic_h3001.c, asic_vrc3.c (those with no external callers); left non-static for those that have header decls or are referenced from sibling .c files (asic_mmc1, asic_vrc6, asic_vrc7, flashrom). * For a small set of cross-file functions where adding a header was disproportionate to the value (MMC5_hb, NSFMMC5_Close, GetKeyboard, FCEU_GetJoyJoy), placed a forward declaration immediately above the definition. This satisfies -Wmissing-prototypes (which checks for any prior declaration in scope) without churning the public-header layout. 5. -Wshadow partial cleanup (not enabled permanently) Fixed five real shadows that were either bugs or actively misleading: * src/boards/476.c: removed an inner "int i" that shadowed the outer loop counter. * src/boards/mmc5.c MMC5_hb: parameter "scanline" renamed to "sl_param" (was shadowing the global "scanline"). * src/boards/n106.c DoNamcoSound: parameter "Wave" renamed to "WaveBuf" (was shadowing the global Wave audio buffer); also updated the forward declaration and the matching parameter on sound.h's NeoFill function pointer typedef. * src/boards/vrc7.c UpdateOPLNEO: same Wave -> WaveBuf rename. * src/ntsc/nes_ntsc_impl.h: renamed an inner loop counter "n" that shadowed an outer "n". * src/drivers/libretro/libretro.c FCEUD_RegionOverride: local "pal" renamed to "is_pal" (was shadowing the typedef "pal" from palette.h). * src/palette.c FCEUI_SetPaletteArray: parameter "pal" renamed to "data" (same shadow); driver.h declaration updated to match. -Wshadow itself is NOT enabled permanently because the remaining warnings are deliberate parameter naming conventions (XBuf in draw functions, X in cpu hooks) and third-party blargg ntsc code. In addition, four files were touched as part of an MSVC-build fix that came up mid-pass: src/fds.c, src/nsf.c, src/ines.c, and src/drivers/libretro/libretro_dipswitch.c had snprintf() calls introduced in pass 4 that fail to link on pre-MSVC2015 toolchains when STATIC_LINKING=1 (the libretro-common compat_snprintf.c shim isn't compiled in those configurations). Replaced each snprintf with either sprintf-into-bounded-buffer (the format strings have known maximum output) or strlcpy/strlcat for the dipswitch key-build case. All output is still bounded; truncation happens via strl*'s normal truncation semantics where applicable. All added code is C89-clean (top-of-block declarations only, no mixed decls, no // comments, INLINE macro from fceu-types.h instead of bare "inline"). Builds clean under -std=gnu11 with -Wno-write- strings -Wsign-compare -Wundef -Wmissing-prototypes; zero errors, zero warnings. Determinism audit (audit_determinism.py): no rand/time/long double/threads issues introduced.
2026-05-04 04:44:52 +02:00
static DECLFW(Write_IRQFM) {
V = (V & 0xC0) >> 6;
fcnt = 0;
if (V & 2)
FrameSoundUpdate();
2018-12-08 11:35:04 +08:00
/* fcnt = 1; */
fhcnt = fhinc;
2018-12-08 11:35:04 +08:00
if (V & 1) {
X6502_IRQEnd(FCEU_IQFCOUNT);
SIRQStat &= ~0x40;
}
IRQFrameMode = V;
}
void SetNESSoundMap(void) {
SetWriteHandler(0x4000, 0x400F, Write_PSG);
SetWriteHandler(0x4010, 0x4013, Write_DMCRegs);
SetWriteHandler(0x4017, 0x4017, Write_IRQFM);
SetWriteHandler(0x4015, 0x4015, StatusWrite);
SetReadHandler(0x4015, 0x4015, StatusRead);
}
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 int32_t inbuf = 0;
int FlushEmulateSound(void) {
int x;
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 end, left;
if (!sound_timestamp) return(0);
if (!FSettings.SndRate) {
left = 0;
end = 0;
goto nosoundo;
}
DoSQ1();
DoSQ2();
DoTriangle();
DoNoise();
DoPCM();
if (FSettings.soundq >= 1) {
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 *tmpo = &WaveHi[soundtsoffs];
if (GameExpSound.HiFill) GameExpSound.HiFill();
for (x = sound_timestamp; x; x--) {
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
uint32_t b = *tmpo;
*tmpo = (b & 65535) + wlookup2[(b >> 16) & 255] + wlookup1[b >> 24];
tmpo++;
}
end = NeoFilterSound(WaveHi, WaveFinal, SOUNDTS, &left);
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
/* Slide the trailing `left` coefficient-history samples back
* to the start of the buffer for next frame's filter, then
* clear the area between left and SOUNDTS so next frame's
* channel accumulators start at zero.
*
* The previous code cleared all the way to sizeof(WaveHi),
* but only indices [left, SOUNDTS) were dirtied this frame -
* everything past SOUNDTS is still zero from the prior
* frame's clear (or from FCEUSND_Power on first frame).
* WaveHi is 40000 entries = 160 KB; SOUNDTS is bounded by
* NES cycles per frame (~30000), so this saves ~40 KB of
* memset per HQ frame. The (SOUNDTS > left) guard handles
* the degenerate case of a very short frame where SOUNDTS
* may not have advanced past the coefficient history. */
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
memmove(WaveHi, WaveHi + SOUNDTS - left, left * sizeof(uint32_t));
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
if ((uint32_t)SOUNDTS > (uint32_t)left)
memset(WaveHi + left, 0, (SOUNDTS - left) * sizeof(uint32_t));
if (GameExpSound.HiSync) GameExpSound.HiSync(left);
for (x = 0; x < 5; x++)
ChannelBC[x] = left;
} else {
end = (SOUNDTS << 16) / soundtsinc;
if (GameExpSound.Fill)
GameExpSound.Fill(end & 0xF);
SexyFilter(Wave, WaveFinal, end >> 4);
2022-04-05 13:45:20 +01:00
if (FSettings.lowpass)
SexyFilter2(WaveFinal, end >> 4);
if (end & 0xF)
Wave[0] = Wave[(end >> 4)];
Wave[end >> 4] = 0;
}
nosoundo:
if (FSettings.soundq >= 1) {
soundtsoffs = left;
} else {
for (x = 0; x < 5; x++)
ChannelBC[x] = end & 0xF;
soundtsoffs = (soundtsinc * (end & 0xF)) >> 16;
end >>= 4;
}
inbuf = end;
2022-09-05 01:15:51 +02:00
return end;
}
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
int GetSoundBuffer(int32_t **W) {
*W = WaveFinal;
return(inbuf);
}
/* FIXME: Find out what sound registers get reset on reset. I know $4001/$4005 don't,
due to that whole MegaMan 2 Game Genie thing.
*/
void FCEUSND_Reset(void) {
int x;
fhcnt = fhinc;
fcnt = 0;
nreg = 1;
for (x = 0; x < 2; x++) {
wlcount[x] = 2048;
2017-10-15 03:13:11 +08:00
if (nesincsize) /* lq mode */
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
sqacc[x] = ((uint32_t)2048 << 17) / nesincsize;
else
sqacc[x] = 1;
sweepon[x] = 0;
curfreq[x] = 0;
}
2017-10-15 03:13:11 +08:00
wlcount[2] = 1; /* 2048; */
wlcount[3] = 2048;
DMCHaveDMA = DMCHaveSample = 0;
SIRQStat = 0x00;
RawDALatch = 0x00;
TriCount = 0;
TriMode = 0;
tristep = 0;
EnabledChannels = 0;
for (x = 0; x < 4; x++)
lengthcount[x] = 0;
DMCAddressLatch = 0;
DMCSizeLatch = 0;
DMCFormat = 0;
DMCAddress = 0;
DMCSize = 0;
DMCShift = 0;
DMCacc=1;
DMCBitCount=0;
}
void FCEUSND_Power(void) {
int x;
SetNESSoundMap();
memset(PSG, 0x00, sizeof(PSG));
FCEUSND_Reset();
memset(Wave, 0, sizeof(Wave));
memset(WaveHi, 0, sizeof(WaveHi));
memset(&EnvUnits, 0, sizeof(EnvUnits));
for (x = 0; x < 5; x++)
ChannelBC[x] = 0;
soundtsoffs = 0;
IRQFrameMode = 0x1; /* Only initialized by power-on reset, not by soft reset. NRS: don't start with Frame IRQ enabled for greater compatibility. Any game that actually uses frame IRQ will explicitly enable it, anyway. */
LoadDMCPeriod(DMCFormat & 0xF);
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
/* Reset post-mix filter accumulators. These are file-scope in
* filter.c and were not previously cleared on cart load, so a
* second cart loaded in the same process inherited the first
* cart's IIR state. Audibly minor on its own but breaks
* frame-determinism for the first samples of a new run. */
SexyFilter_Reset();
}
void SetSoundVariables(void) {
int x;
2017-10-15 03:13:11 +08:00
fhinc = PAL ? 16626 : 14915; /* *2 CPU clock rate */
fhinc *= 24;
if (FSettings.SndRate) {
wlookup1[0] = 0;
for (x = 1; x < 32; x++) {
wlookup1[x] = (double)16 * 16 * 16 * 4 * 95.52 / ((double)8128 / (double)x + 100);
if (!FSettings.soundq) wlookup1[x] >>= 4;
}
wlookup2[0] = 0;
for (x = 1; x < 203; x++) {
wlookup2[x] = (double)16 * 16 * 16 * 4 * 163.67 / ((double)24329 / (double)x + 100);
if (!FSettings.soundq) wlookup2[x] >>= 4;
}
if (FSettings.soundq >= 1) {
DoNoise = RDoNoise;
DoTriangle = RDoTriangle;
DoPCM = RDoPCM;
DoSQ1 = RDoSQ1;
DoSQ2 = RDoSQ2;
} else {
core: revert LQ dispatch deduplication from pass 6 (audio regression) Pass 6 (commit 9f6b84c) optimized SetSoundVariables() in LQ mode by pointing DoSQ2, DoNoise, and DoPCM at Dummyfunc instead of redundantly calling RDoSQLQ / RDoTriangleNoisePCMLQ. The reasoning at the time: those workers guard with `if (end <= start) return;` so re-entry within one FlushEmulateSound is a no-op and the second/third/fourth calls are wasted dispatch overhead. That reasoning was wrong. The Do* hooks are also called from two mid-frame paths: * Write_PSG (sound.c:189) on every APU register write, BEFORE the register update, to flush pending samples up to the current SOUNDTS using the pre-write register state. * FCEU_SoundCPUHook (sound.c:493) on every DMC bit advance, to flush samples up to the current SOUNDTS so the new PCM byte gets rendered against the correct prior state. Between any two such calls, sound_timestamp has grown with each CPU instruction, so each Do* call IS legitimately doing work - the early- return guard only fires within a single FlushEmulateSound batch, NOT across calls separated by CPU cycles. Stubbing DoSQ2 / DoNoise / DoPCM to Dummyfunc therefore skipped all the mid-frame flushes triggered by writes to $4004-$4007 (SQ2 registers), $400C-$400F (noise registers), and $4010-$4013 (DMC registers). For any game that writes to multiple APU registers in sequence (essentially all of them), the audio output diverged from the pre-pass-6 baseline. Bisect: built three test ROMs covering "APU disabled", "channels enabled with steady tone, no register writes after init", and "all channels active with continuous register pumping". The register-writing scenarios diverged starting at the first post-initialization register write; the steady-tone scenario diverged starting at the next DMC bit advance after init. Reverting just the LQ dispatch dedup restored bit-identical audio for all three scenarios. Fix: restore DoSQ2 -> RDoSQLQ, DoNoise -> RDoTriangleNoisePCMLQ, DoPCM -> RDoTriangleNoisePCMLQ. Add a comment explaining why this optimization is incorrect, so the same mistake is not repeated. The other pass 6 changes remain in place and are verified safe: * SexyFilter_Reset() called from FCEUSND_Power - filter state determinism fix. * sexyfilter2_acc lifted to file scope, added to FCEUSND_STATEINFO as "FAC3" - savestate completeness fix. * WaveHi memset bound tightened from sizeof(WaveHi) - left*4 to (SOUNDTS - left) * sizeof(uint32_t) - HQ-mode optimization. * RDoSQLQ silent-channels branch loop body removed - totalout is provably 0 in that branch, so the loop was a true no-op for ~30000 NES cycles per silent frame. * stereo_filter_apply_delay element-by-element loop replaced with memcpy. * filter.c #ifdef moo / #if 0 dead blocks and unused <math.h> include removed. Verified bit-identical to pre-pass-6 baseline for all three test ROMs. Performance impact of the cleanup pass (with the LQ dedup correctly reverted, attributed to the actually-correct optimizations) is ~9% per-frame reduction across silent / idle / active scenarios. Pass 6's previously-reported ~25% speedup on the active scenario was inflated by the buggy optimization skipping legitimate work. Build clean under -std=gnu11 -Wno-write-strings -Wsign-compare -Wundef -Wmissing-prototypes; zero errors, zero warnings. audit_determinism.py clean.
2026-05-04 05:32:28 +02:00
/* All five Do* pointers in LQ mode end up at one of two
* worker functions: RDoSQLQ (handles both squares) and
* RDoTriangleNoisePCMLQ (handles tri/noise/PCM).
*
* Pass 6 had stubbed DoSQ2 / DoNoise / DoPCM to Dummyfunc
* here on the reasoning that the workers guard with
* "if (end <= start) return;" and re-entry within one
* FlushEmulateSound is a no-op. That reasoning is
* incorrect: the Do* hooks are also called from
* Write_PSG (sound.c:189) on every APU register write
* AND from FCEU_SoundCPUHook (line 493) on every DMC
* bit advance. Between those callers, sound_timestamp
* grows with each CPU instruction, so each Do* call IS
* legitimately doing work - it flushes pending samples
* up to the current SOUNDTS using the pre-write register
* state, before the write updates the registers. Stubbing
* those hooks to Dummyfunc skips the mid-frame flushes
* and audibly changes output for any game that writes to
* multiple APU registers in sequence (essentially all
* of them). Verified bit-identical regression vs upstream
* for the test_idle ROM (channels enabled with steady
* settings) - the audio diverged starting at the first
* post-init register write. Restored 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
DoSQ1 = RDoSQLQ;
core: revert LQ dispatch deduplication from pass 6 (audio regression) Pass 6 (commit 9f6b84c) optimized SetSoundVariables() in LQ mode by pointing DoSQ2, DoNoise, and DoPCM at Dummyfunc instead of redundantly calling RDoSQLQ / RDoTriangleNoisePCMLQ. The reasoning at the time: those workers guard with `if (end <= start) return;` so re-entry within one FlushEmulateSound is a no-op and the second/third/fourth calls are wasted dispatch overhead. That reasoning was wrong. The Do* hooks are also called from two mid-frame paths: * Write_PSG (sound.c:189) on every APU register write, BEFORE the register update, to flush pending samples up to the current SOUNDTS using the pre-write register state. * FCEU_SoundCPUHook (sound.c:493) on every DMC bit advance, to flush samples up to the current SOUNDTS so the new PCM byte gets rendered against the correct prior state. Between any two such calls, sound_timestamp has grown with each CPU instruction, so each Do* call IS legitimately doing work - the early- return guard only fires within a single FlushEmulateSound batch, NOT across calls separated by CPU cycles. Stubbing DoSQ2 / DoNoise / DoPCM to Dummyfunc therefore skipped all the mid-frame flushes triggered by writes to $4004-$4007 (SQ2 registers), $400C-$400F (noise registers), and $4010-$4013 (DMC registers). For any game that writes to multiple APU registers in sequence (essentially all of them), the audio output diverged from the pre-pass-6 baseline. Bisect: built three test ROMs covering "APU disabled", "channels enabled with steady tone, no register writes after init", and "all channels active with continuous register pumping". The register-writing scenarios diverged starting at the first post-initialization register write; the steady-tone scenario diverged starting at the next DMC bit advance after init. Reverting just the LQ dispatch dedup restored bit-identical audio for all three scenarios. Fix: restore DoSQ2 -> RDoSQLQ, DoNoise -> RDoTriangleNoisePCMLQ, DoPCM -> RDoTriangleNoisePCMLQ. Add a comment explaining why this optimization is incorrect, so the same mistake is not repeated. The other pass 6 changes remain in place and are verified safe: * SexyFilter_Reset() called from FCEUSND_Power - filter state determinism fix. * sexyfilter2_acc lifted to file scope, added to FCEUSND_STATEINFO as "FAC3" - savestate completeness fix. * WaveHi memset bound tightened from sizeof(WaveHi) - left*4 to (SOUNDTS - left) * sizeof(uint32_t) - HQ-mode optimization. * RDoSQLQ silent-channels branch loop body removed - totalout is provably 0 in that branch, so the loop was a true no-op for ~30000 NES cycles per silent frame. * stereo_filter_apply_delay element-by-element loop replaced with memcpy. * filter.c #ifdef moo / #if 0 dead blocks and unused <math.h> include removed. Verified bit-identical to pre-pass-6 baseline for all three test ROMs. Performance impact of the cleanup pass (with the LQ dedup correctly reverted, attributed to the actually-correct optimizations) is ~9% per-frame reduction across silent / idle / active scenarios. Pass 6's previously-reported ~25% speedup on the active scenario was inflated by the buggy optimization skipping legitimate work. Build clean under -std=gnu11 -Wno-write-strings -Wsign-compare -Wundef -Wmissing-prototypes; zero errors, zero warnings. audit_determinism.py clean.
2026-05-04 05:32:28 +02:00
DoSQ2 = RDoSQLQ;
DoTriangle = RDoTriangleNoisePCMLQ;
core: revert LQ dispatch deduplication from pass 6 (audio regression) Pass 6 (commit 9f6b84c) optimized SetSoundVariables() in LQ mode by pointing DoSQ2, DoNoise, and DoPCM at Dummyfunc instead of redundantly calling RDoSQLQ / RDoTriangleNoisePCMLQ. The reasoning at the time: those workers guard with `if (end <= start) return;` so re-entry within one FlushEmulateSound is a no-op and the second/third/fourth calls are wasted dispatch overhead. That reasoning was wrong. The Do* hooks are also called from two mid-frame paths: * Write_PSG (sound.c:189) on every APU register write, BEFORE the register update, to flush pending samples up to the current SOUNDTS using the pre-write register state. * FCEU_SoundCPUHook (sound.c:493) on every DMC bit advance, to flush samples up to the current SOUNDTS so the new PCM byte gets rendered against the correct prior state. Between any two such calls, sound_timestamp has grown with each CPU instruction, so each Do* call IS legitimately doing work - the early- return guard only fires within a single FlushEmulateSound batch, NOT across calls separated by CPU cycles. Stubbing DoSQ2 / DoNoise / DoPCM to Dummyfunc therefore skipped all the mid-frame flushes triggered by writes to $4004-$4007 (SQ2 registers), $400C-$400F (noise registers), and $4010-$4013 (DMC registers). For any game that writes to multiple APU registers in sequence (essentially all of them), the audio output diverged from the pre-pass-6 baseline. Bisect: built three test ROMs covering "APU disabled", "channels enabled with steady tone, no register writes after init", and "all channels active with continuous register pumping". The register-writing scenarios diverged starting at the first post-initialization register write; the steady-tone scenario diverged starting at the next DMC bit advance after init. Reverting just the LQ dispatch dedup restored bit-identical audio for all three scenarios. Fix: restore DoSQ2 -> RDoSQLQ, DoNoise -> RDoTriangleNoisePCMLQ, DoPCM -> RDoTriangleNoisePCMLQ. Add a comment explaining why this optimization is incorrect, so the same mistake is not repeated. The other pass 6 changes remain in place and are verified safe: * SexyFilter_Reset() called from FCEUSND_Power - filter state determinism fix. * sexyfilter2_acc lifted to file scope, added to FCEUSND_STATEINFO as "FAC3" - savestate completeness fix. * WaveHi memset bound tightened from sizeof(WaveHi) - left*4 to (SOUNDTS - left) * sizeof(uint32_t) - HQ-mode optimization. * RDoSQLQ silent-channels branch loop body removed - totalout is provably 0 in that branch, so the loop was a true no-op for ~30000 NES cycles per silent frame. * stereo_filter_apply_delay element-by-element loop replaced with memcpy. * filter.c #ifdef moo / #if 0 dead blocks and unused <math.h> include removed. Verified bit-identical to pre-pass-6 baseline for all three test ROMs. Performance impact of the cleanup pass (with the LQ dedup correctly reverted, attributed to the actually-correct optimizations) is ~9% per-frame reduction across silent / idle / active scenarios. Pass 6's previously-reported ~25% speedup on the active scenario was inflated by the buggy optimization skipping legitimate work. Build clean under -std=gnu11 -Wno-write-strings -Wsign-compare -Wundef -Wmissing-prototypes; zero errors, zero warnings. audit_determinism.py clean.
2026-05-04 05:32:28 +02:00
DoNoise = RDoTriangleNoisePCMLQ;
DoPCM = RDoTriangleNoisePCMLQ;
}
} else {
DoNoise = DoTriangle = DoPCM = DoSQ1 = DoSQ2 = Dummyfunc;
return;
}
MakeFilters(FSettings.SndRate);
if (GameExpSound.RChange)
GameExpSound.RChange();
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
nesincsize = (int64_t)(((int64_t)1 << 17) * (double)(PAL ? PAL_CPU : NTSC_CPU) / (FSettings.SndRate * 16));
memset(sqacc, 0, sizeof(sqacc));
memset(ChannelBC, 0, sizeof(ChannelBC));
2017-10-15 03:13:11 +08:00
LoadDMCPeriod(DMCFormat & 0xF); /* For changing from PAL to NTSC */
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
/* Use double rather than long double here. long double has
* platform-dependent precision (80-bit on x87, 64-bit with
* -mfpmath=sse, 128-bit on some non-x86), so the cast-to-uint32
* result varies across platforms. double is guaranteed 64-bit
* IEEE-754 on every platform we target, keeping soundtsinc
* deterministic across builds for replay/netplay. */
soundtsinc = (uint32_t)((uint64_t)((double)(PAL ? PAL_CPU : NTSC_CPU) * 65536.0) / (FSettings.SndRate * 16));
}
void FCEUI_Sound(int Rate) {
FSettings.SndRate = Rate;
SetSoundVariables();
}
void FCEUI_SetLowPass(int q) {
FSettings.lowpass = q;
}
void FCEUI_SetSoundQuality(int quality) {
FSettings.soundq = quality;
SetSoundVariables();
}
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 FCEUI_SetSoundVolume(uint32_t volume) {
FSettings.SoundVolume = volume;
}
SFORMAT FCEUSND_STATEINFO[] = {
{ &fhcnt, 4 | FCEUSTATE_RLSB, "FHCN" },
{ &fcnt, 1, "FCNT" },
{ PSG, 0x10, "PSG" },
{ &EnabledChannels, 1, "ENCH" },
{ &IRQFrameMode, 1, "IQFM" },
{ &nreg, 2 | FCEUSTATE_RLSB, "NREG" },
{ &TriMode, 1, "TRIM" },
{ &TriCount, 1, "TRIC" },
{ &EnvUnits[0].Speed, 1, "E0SP" },
{ &EnvUnits[1].Speed, 1, "E1SP" },
{ &EnvUnits[2].Speed, 1, "E2SP" },
{ &EnvUnits[0].Mode, 1, "E0MO" },
{ &EnvUnits[1].Mode, 1, "E1MO" },
{ &EnvUnits[2].Mode, 1, "E2MO" },
{ &EnvUnits[0].DecCountTo1, 1, "E0D1" },
{ &EnvUnits[1].DecCountTo1, 1, "E1D1" },
{ &EnvUnits[2].DecCountTo1, 1, "E2D1" },
{ &EnvUnits[0].decvolume, 1, "E0DV" },
{ &EnvUnits[1].decvolume, 1, "E1DV" },
{ &EnvUnits[2].decvolume, 1, "E2DV" },
{ &lengthcount[0], 4 | FCEUSTATE_RLSB, "LEN0" },
{ &lengthcount[1], 4 | FCEUSTATE_RLSB, "LEN1" },
{ &lengthcount[2], 4 | FCEUSTATE_RLSB, "LEN2" },
{ &lengthcount[3], 4 | FCEUSTATE_RLSB, "LEN3" },
{ sweepon, 2, "SWEE" },
{ &curfreq[0], 4 | FCEUSTATE_RLSB, "CRF1" },
{ &curfreq[1], 4 | FCEUSTATE_RLSB, "CRF2" },
{ SweepCount, 2, "SWCT" },
{ &SIRQStat, 1, "SIRQ" },
{ &DMCacc, 4 | FCEUSTATE_RLSB, "5ACC" },
{ &DMCBitCount, 1, "5BIT" },
{ &DMCAddress, 4 | FCEUSTATE_RLSB, "5ADD" },
{ &DMCSize, 4 | FCEUSTATE_RLSB, "5SIZ" },
{ &DMCShift, 1, "5SHF" },
{ &DMCHaveDMA, 1, "5VDM" },
{ &DMCHaveSample, 1, "5VSP" },
{ &DMCSizeLatch, 1, "5SZL" },
{ &DMCAddressLatch, 1, "5ADL" },
{ &DMCFormat, 1, "5FMT" },
{ &RawDALatch, 1, "RWDA" },
/* these are important for smooth sound after loading state */
{ &sqacc[0], sizeof(sqacc[0]) | FCEUSTATE_RLSB, "SAC1" },
{ &sqacc[1], sizeof(sqacc[1]) | FCEUSTATE_RLSB, "SAC2" },
{ &RectDutyCount[0], sizeof(RectDutyCount[0]) | FCEUSTATE_RLSB, "RCD1"},
{ &RectDutyCount[1], sizeof(RectDutyCount[1]) | FCEUSTATE_RLSB, "RCD2"},
{ &tristep, sizeof(tristep) | FCEUSTATE_RLSB, "TRIS"},
{ &lq_triacc, sizeof(lq_triacc) | FCEUSTATE_RLSB, "TACC" },
{ &lq_noiseacc, sizeof(lq_noiseacc) | FCEUSTATE_RLSB, "NACC" },
/* less important but still necessary */
2018-03-16 23:25:48 -05:00
{ &ChannelBC[0], sizeof(ChannelBC[0]) | FCEUSTATE_RLSB, "CBC1" },
{ &ChannelBC[1], sizeof(ChannelBC[1]) | FCEUSTATE_RLSB, "CBC2" },
{ &ChannelBC[2], sizeof(ChannelBC[2]) | FCEUSTATE_RLSB, "CBC3" },
{ &ChannelBC[3], sizeof(ChannelBC[3]) | FCEUSTATE_RLSB, "CBC4" },
{ &ChannelBC[4], sizeof(ChannelBC[4]) | FCEUSTATE_RLSB, "CBC5" },
{ &sound_timestamp, sizeof(sound_timestamp) | FCEUSTATE_RLSB, "SNTS" },
{ &soundtsoffs, sizeof(soundtsoffs) | FCEUSTATE_RLSB, "TSOF"},
{ &wlcount[0], sizeof(wlcount[0]) | FCEUSTATE_RLSB, "WLC1" },
{ &wlcount[1], sizeof(wlcount[1]) | FCEUSTATE_RLSB, "WLC2" },
{ &wlcount[2], sizeof(wlcount[2]) | FCEUSTATE_RLSB, "WLC3" },
{ &wlcount[3], sizeof(wlcount[3]) | FCEUSTATE_RLSB, "WLC4" },
{ &sexyfilter_acc1, sizeof(sexyfilter_acc1) | FCEUSTATE_RLSB, "FAC1" },
{ &sexyfilter_acc2, sizeof(sexyfilter_acc2) | FCEUSTATE_RLSB, "FAC2" },
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, sizeof(sexyfilter2_acc) | FCEUSTATE_RLSB, "FAC3" },
{ &lq_tcout, sizeof(lq_tcout) | FCEUSTATE_RLSB, "TCOU"},
Wii related updates for save state fixes/workarounds (#246) * Wii: Fix uninitialized save state size causing load state problems - Issue origially from Wii and possibly affects similar devices where retroarch and the core are one program. The problem was that the serialize size was not reset when you change game, causing the last serialized size to be carried over to the next game loaded causing save state issues due to change in size. Initializing this variable during retro_init() seems enough for most of the games. - In this same observation, also initialized some variables in a similar way. - also exluded a variable used for sound state that was added for smooth sound after load state which causes similar loading issues as well. - default sample rate has been lowered to 32K as well as to minimize some stuttering for these devices, while maintaining 48K sample rate for others. - Some comments are added to modified section as necessary. * Add workaround for save state issue in Wii with expansion audio - Some mappers are not loading states as well. Seems to affect those that are using expansion audio. some of these mappers(bandai, mmc5, namco106), the state variables has to be expanded so that it will load states fine with big endian while others (vrc6, vrc7 sunsoft), some variables that were added for smoother audio during load state has been removed. - This does not guarantee though that other mappers might not have similar incompatibilities after loading a state but so far, some of the most common roms in each mapper has been tested to load fine. * FDS: Expand state variables for big endian compatibility * FDS: Change OSD label from Disk 0 to Disk 1... when switching disks - Minor osd label change that now shows Disk 1 of (# of disks) instead of just Disk 0 Side nth.
2018-12-15 10:23:43 +08:00
/* 2018-12-14 - Wii and possibly other big-endian platforms are having
* issues loading states with this. Increasing it only helps a few games.
* Disabling this state variable for Wii/WiiU/GC for now. */
/* TODO: fix this for better runahead feature for big-endian */
/* UPDATE: Try to ignore this for all big-endian for now */
#ifndef MSB_FIRST
/* wave buffer is used for filtering, only need first 17 values from it */
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
{ &Wave, 32 * sizeof(int32_t), "WAVE"},
Wii related updates for save state fixes/workarounds (#246) * Wii: Fix uninitialized save state size causing load state problems - Issue origially from Wii and possibly affects similar devices where retroarch and the core are one program. The problem was that the serialize size was not reset when you change game, causing the last serialized size to be carried over to the next game loaded causing save state issues due to change in size. Initializing this variable during retro_init() seems enough for most of the games. - In this same observation, also initialized some variables in a similar way. - also exluded a variable used for sound state that was added for smooth sound after load state which causes similar loading issues as well. - default sample rate has been lowered to 32K as well as to minimize some stuttering for these devices, while maintaining 48K sample rate for others. - Some comments are added to modified section as necessary. * Add workaround for save state issue in Wii with expansion audio - Some mappers are not loading states as well. Seems to affect those that are using expansion audio. some of these mappers(bandai, mmc5, namco106), the state variables has to be expanded so that it will load states fine with big endian while others (vrc6, vrc7 sunsoft), some variables that were added for smoother audio during load state has been removed. - This does not guarantee though that other mappers might not have similar incompatibilities after loading a state but so far, some of the most common roms in each mapper has been tested to load fine. * FDS: Expand state variables for big endian compatibility * FDS: Change OSD label from Disk 0 to Disk 1... when switching disks - Minor osd label change that now shows Disk 1 of (# of disks) instead of just Disk 0 Side nth.
2018-12-15 10:23:43 +08:00
#endif
{ 0 }
};
void FCEUSND_SaveState(void) {
}
void FCEUSND_LoadState(int version) {
2018-03-20 16:07:46 +01:00
int i;
LoadDMCPeriod(DMCFormat & 0xF);
RawDALatch &= 0x7F;
DMCAddress &= 0x7FFF;
/* minimal validation */
2018-03-20 16:07:46 +01:00
for (i = 0; i < 5; i++)
{
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
uint32_t BC_max = 15;
if (FSettings.soundq == 2)
{
BC_max = 1025;
}
else if (FSettings.soundq == 1)
{
BC_max = 485;
}
2020-10-26 06:51:20 +08:00
if (/* ChannelBC[i] < 0 || */ ChannelBC[i] > BC_max)
{
ChannelBC[i] = 0;
}
}
2018-03-20 16:07:46 +01:00
for (i = 0; i < 4; i++)
{
if (wlcount[i] < 0 || wlcount[i] > 2048)
{
wlcount[i] = 2048;
}
}
2018-03-20 16:07:46 +01:00
for (i = 0; i < 2; i++)
{
if (RectDutyCount[i] < 0 || RectDutyCount[i] > 7)
{
RectDutyCount[i] = 7;
}
}
2020-10-26 06:51:20 +08:00
/* Comparison is always false because access to array >= 0. */
/* if (sound_timestamp < 0)
{
sound_timestamp = 0;
}
if (soundtsoffs < 0)
{
soundtsoffs = 0;
2020-10-26 06:51:20 +08:00
} */
if (soundtsoffs + sound_timestamp >= soundtsinc)
{
soundtsoffs = 0;
sound_timestamp = 0;
}
if (tristep > 32)
{
tristep &= 0x1F;
}
}