Files
ci-libretro-fceumm/src/cart.c
U-DESKTOP-SPFP6AQ\twistedtechre 766f84662b core: stdint typedefs, LE optimizations, frame determinism
Three follow-up audit passes on top of the memory-safety / leak /
savestate-portability work in 1185db8.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Combined with the memory-safety hardening in 1185db8 (which
prevents savestate-loaded indices from going out-of-bounds and
producing unpredictable behaviour), the core now offers genuine
frame-deterministic replay across runs, builds, and host endian.
2026-05-04 02:46:34 +02:00

564 lines
13 KiB
C

/* 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 <string.h>
#include <stdlib.h>
#include <string/stdstring.h>
#include <file/file_path.h>
#include <streams/file_stream.h>
#include "fceu-types.h"
#include "fceu.h"
#include "ppu.h"
#include "cart.h"
#include "fceu-memory.h"
#include "x6502.h"
#include "general.h"
/*
This file contains all code for coordinating the mapping in of the
address space external to the NES.
It's also (ab)used by the NSF code.
*/
uint8_t *Page[32], *VPage[8];
uint8_t **VPageR = VPage;
uint8_t *VPageG[8];
uint8_t *MMC5SPRVPage[8];
uint8_t *MMC5BGVPage[8];
static uint8_t PRGIsRAM[32]; /* This page is/is not PRG RAM. */
/* 16 are (sort of) reserved for UNIF/iNES and 16 to map other stuff. */
static int CHRram[32];
static int PRGram[32];
uint8_t *PRGptr[32];
uint8_t *CHRptr[32];
uint32_t PRGsize[32];
uint32_t CHRsize[32];
uint32_t PRGmask2[32];
uint32_t PRGmask4[32];
uint32_t PRGmask8[32];
uint32_t PRGmask16[32];
uint32_t PRGmask32[32];
uint32_t CHRmask1[32];
uint32_t CHRmask2[32];
uint32_t CHRmask4[32];
uint32_t CHRmask8[32];
int geniestage = 0;
int modcon;
uint8_t genieval[3];
uint8_t geniech[3];
uint32_t genieaddr[3];
static INLINE void setpageptr(int s, uint32_t A, uint8_t *p, int ram) {
uint32_t AB = A >> 11;
int x;
if (p)
for (x = (s >> 1) - 1; x >= 0; x--) {
PRGIsRAM[AB + x] = ram;
Page[AB + x] = p - A;
}
else
for (x = (s >> 1) - 1; x >= 0; x--) {
PRGIsRAM[AB + x] = 0;
Page[AB + x] = 0;
}
}
static uint8_t nothing[8192];
void ResetCartMapping(void) {
int x;
for (x = 0; x < 32; x++) {
Page[x] = nothing - x * 2048;
PRGptr[x] = CHRptr[x] = 0;
PRGsize[x] = CHRsize[x] = 0;
}
for (x = 0; x < 8; x++) {
MMC5SPRVPage[x] = MMC5BGVPage[x] = VPageR[x] = nothing - 0x400 * x;
}
}
void SetupCartPRGMapping(int chip, uint8_t *p, uint32_t size, int ram) {
PRGptr[chip] = p;
PRGsize[chip] = size;
PRGmask2[chip] = (size >> 11) - 1;
PRGmask4[chip] = (size >> 12) - 1;
PRGmask8[chip] = (size >> 13) - 1;
PRGmask16[chip] = (size >> 14) - 1;
PRGmask32[chip] = (size >> 15) - 1;
PRGram[chip] = ram ? 1 : 0;
}
void SetupCartCHRMapping(int chip, uint8_t *p, uint32_t size, int ram) {
CHRptr[chip] = p;
CHRsize[chip] = size;
CHRmask1[chip] = (size >> 10) - 1;
CHRmask2[chip] = (size >> 11) - 1;
CHRmask4[chip] = (size >> 12) - 1;
CHRmask8[chip] = (size >> 13) - 1;
CHRram[chip] = ram;
}
DECLFR(CartBR) {
return Page[A >> 11][A];
}
DECLFW(CartBW) {
if (PRGIsRAM[A >> 11] && Page[A >> 11])
Page[A >> 11][A] = V;
}
DECLFR(CartBROB) {
if (!Page[A >> 11])
return(X.DB);
else
return Page[A >> 11][A];
}
void FASTAPASS(3) setprg2r(int r, uint32_t A, uint32_t V) {
/* If the registered chip size is < 2KB, PRGmask2[r] underflowed to
* 0xFFFFFFFF in SetupCartPRGMapping. Clear the page rather than
* indexing past PRGptr[r]. (The only currently-shipped caller that
* registers a chip this small is malee.c with size==2048, which
* gives mask==0 and works fine; this guard is defensive for any
* future board.) */
if (!PRGptr[r] || PRGsize[r] < 2048) {
setpageptr(2, A, NULL, PRGram[r]);
return;
}
V &= PRGmask2[r];
setpageptr(2, A, &PRGptr[r][V << 11], PRGram[r]);
}
void FASTAPASS(2) setprg2(uint32_t A, uint32_t V) {
setprg2r(0, A, V);
}
void FASTAPASS(3) setprg4r(int r, uint32_t A, uint32_t V) {
if (!PRGptr[r] || PRGsize[r] < 4096) {
/* Fall back to two 2KB pages if size is at least 2KB; else
* clear both 2KB pages. */
if (PRGptr[r] && PRGsize[r] >= 2048) {
uint32_t mask2 = PRGmask2[r];
uint32_t VA = V << 1;
int x;
for (x = 0; x < 2; x++)
setpageptr(2, A + (x << 11), &PRGptr[r][((VA + x) & mask2) << 11], PRGram[r]);
} else {
setpageptr(2, A, NULL, PRGram[r]);
setpageptr(2, A + 0x800, NULL, PRGram[r]);
}
return;
}
V &= PRGmask4[r];
setpageptr(4, A, &PRGptr[r][V << 12], PRGram[r]);
}
void FASTAPASS(2) setprg4(uint32_t A, uint32_t V) {
setprg4r(0, A, V);
}
void FASTAPASS(3) setprg8r(int r, uint32_t A, uint32_t V) {
if (PRGsize[r] >= 8192) {
V &= PRGmask8[r];
setpageptr(8, A, PRGptr[r] ? (&PRGptr[r][V << 13]) : 0, PRGram[r]);
} else {
uint32_t VA = V << 2;
int x;
for (x = 0; x < 4; x++)
setpageptr(2, A + (x << 11), PRGptr[r] ? (&PRGptr[r][((VA + x) & PRGmask2[r]) << 11]) : 0, PRGram[r]);
}
}
void FASTAPASS(2) setprg8(uint32_t A, uint32_t V) {
setprg8r(0, A, V);
}
void FASTAPASS(3) setprg16r(int r, uint32_t A, uint32_t V) {
if (PRGsize[r] >= 16384) {
V &= PRGmask16[r];
setpageptr(16, A, PRGptr[r] ? (&PRGptr[r][V << 14]) : 0, PRGram[r]);
} else {
uint32_t VA = V << 3;
int x;
for (x = 0; x < 8; x++)
setpageptr(2, A + (x << 11), PRGptr[r] ? (&PRGptr[r][((VA + x) & PRGmask2[r]) << 11]) : 0, PRGram[r]);
}
}
void FASTAPASS(2) setprg16(uint32_t A, uint32_t V) {
setprg16r(0, A, V);
}
void FASTAPASS(3) setprg32r(int r, uint32_t A, uint32_t V) {
if (PRGsize[r] >= 32768) {
V &= PRGmask32[r];
setpageptr(32, A, PRGptr[r] ? (&PRGptr[r][V << 15]) : 0, PRGram[r]);
} else {
uint32_t VA = V << 4;
int x;
for (x = 0; x < 16; x++)
setpageptr(2, A + (x << 11), PRGptr[r] ? (&PRGptr[r][((VA + x) & PRGmask2[r]) << 11]) : 0, PRGram[r]);
}
}
void FASTAPASS(2) setprg32(uint32_t A, uint32_t V) {
setprg32r(0, A, V);
}
void FASTAPASS(3) setchr1r(int r, uint32_t A, uint32_t V) {
if (!CHRptr[r]) return;
if (CHRsize[r] < 1024) return; /* mask underflow guard */
FCEUPPU_LineUpdate();
V &= CHRmask1[r];
if (CHRram[r])
PPUCHRRAM |= (1 << (A >> 10));
else
PPUCHRRAM &= ~(1 << (A >> 10));
VPageR[(A) >> 10] = &CHRptr[r][(V) << 10] - (A);
}
void FASTAPASS(3) setchr2r(int r, uint32_t A, uint32_t V) {
if (!CHRptr[r]) return;
if (CHRsize[r] < 2048) return; /* mask underflow guard */
FCEUPPU_LineUpdate();
V &= CHRmask2[r];
VPageR[(A) >> 10] = VPageR[((A) >> 10) + 1] = &CHRptr[r][(V) << 11] - (A);
if (CHRram[r])
PPUCHRRAM |= (3 << (A >> 10));
else
PPUCHRRAM &= ~(3 << (A >> 10));
}
void FASTAPASS(3) setchr4r(int r, uint32_t A, uint32_t V) {
if (!CHRptr[r]) return;
if (CHRsize[r] < 4096) return; /* mask underflow guard */
FCEUPPU_LineUpdate();
V &= CHRmask4[r];
VPageR[(A) >> 10] = VPageR[((A) >> 10) + 1] =
VPageR[((A) >> 10) + 2] = VPageR[((A) >> 10) + 3] = &CHRptr[r][(V) << 12] - (A);
if (CHRram[r])
PPUCHRRAM |= (15 << (A >> 10));
else
PPUCHRRAM &= ~(15 << (A >> 10));
}
void FASTAPASS(2) setchr8r(int r, uint32_t V) {
int x;
if (!CHRptr[r]) return;
FCEUPPU_LineUpdate();
if (CHRsize[r] < 8192) {
/* Undersized chip: clamp V to 0 to avoid OOB indexing. The 2KB
* NTARAM-as-CHR pattern in mapper 218 currently relies on this
* (always called with V=0); treat anything else as a logic
* error and clamp. */
V = 0;
} else {
V &= CHRmask8[r];
}
for (x = 7; x >= 0; x--)
VPageR[x] = &CHRptr[r][V << 13];
if (CHRram[r])
PPUCHRRAM |= (255);
else
PPUCHRRAM = 0;
}
void FASTAPASS(2) setchr1(uint32_t A, uint32_t V) {
setchr1r(0, A, V);
}
void FASTAPASS(2) setchr2(uint32_t A, uint32_t V) {
setchr2r(0, A, V);
}
void FASTAPASS(2) setchr4(uint32_t A, uint32_t V) {
setchr4r(0, A, V);
}
void FASTAPASS(1) setchr8(uint32_t V) {
setchr8r(0, V);
}
/* This function can be called without calling SetupCartMirroring(). */
void FASTAPASS(3) setntamem(uint8_t * p, int ram, uint32_t b) {
FCEUPPU_LineUpdate();
vnapage[b] = p;
PPUNTARAM &= ~(1 << b);
if (ram)
PPUNTARAM |= 1 << b;
}
static int mirrorhard = 0;
void setmirrorw(int a, int b, int c, int d) {
FCEUPPU_LineUpdate();
vnapage[0] = NTARAM + a * 0x400;
vnapage[1] = NTARAM + b * 0x400;
vnapage[2] = NTARAM + c * 0x400;
vnapage[3] = NTARAM + d * 0x400;
}
void FASTAPASS(1) setmirror(int t) {
FCEUPPU_LineUpdate();
if (!mirrorhard) {
switch (t) {
case MI_H:
vnapage[0] = vnapage[1] = NTARAM; vnapage[2] = vnapage[3] = NTARAM + 0x400;
break;
case MI_V:
vnapage[0] = vnapage[2] = NTARAM; vnapage[1] = vnapage[3] = NTARAM + 0x400;
break;
case MI_0:
vnapage[0] = vnapage[1] = vnapage[2] = vnapage[3] = NTARAM;
break;
case MI_1:
vnapage[0] = vnapage[1] = vnapage[2] = vnapage[3] = NTARAM + 0x400;
break;
}
PPUNTARAM = 0xF;
}
}
void SetupCartMirroring(int m, int hard, uint8_t *extra) {
if (m < 4) {
mirrorhard = 0;
setmirror(m);
} else {
vnapage[0] = NTARAM;
vnapage[1] = NTARAM + 0x400;
vnapage[2] = extra;
vnapage[3] = extra + 0x400;
PPUNTARAM = 0xF;
}
mirrorhard = hard;
}
static uint8_t *GENIEROM = 0;
void FixGenieMap(void);
/* Called when a game(file) is opened successfully. */
void FCEU_OpenGenie(void) {
RFILE *fp = NULL;
int x;
if (!GENIEROM) {
char *fn;
if (!(GENIEROM = (uint8_t*)FCEU_malloc(4096 + 1024))) return;
fn = FCEU_MakeFName(FCEUMKF_GGROM, 0, 0);
if (!string_is_empty(fn) && path_is_valid(fn))
fp = filestream_open(fn,
RETRO_VFS_FILE_ACCESS_READ,
RETRO_VFS_FILE_ACCESS_HINT_NONE);
free(fn);
fn = NULL;
if (!fp) {
FCEU_PrintError("Error opening Game Genie ROM image!\n");
FCEUD_DispMessage(RETRO_LOG_WARN, 3000, "Game Genie ROM image (gamegenie.nes) missing");
free(GENIEROM);
GENIEROM = 0;
return;
}
if (filestream_read(fp, GENIEROM, 16) != 16) {
grerr:
FCEU_PrintError("Error reading from Game Genie ROM image!\n");
FCEUD_DispMessage(RETRO_LOG_WARN, 3000, "Failed to read Game Genie ROM image (gamegenie.nes)");
free(GENIEROM);
GENIEROM = 0;
filestream_close(fp);
return;
}
if (GENIEROM[0] == 0x4E) { /* iNES ROM image */
if (filestream_read(fp, GENIEROM, 4096) != 4096)
goto grerr;
if (filestream_seek(fp, 16384 - 4096, RETRO_VFS_SEEK_POSITION_CURRENT))
goto grerr;
if (filestream_read(fp, GENIEROM + 4096, 256) != 256)
goto grerr;
} else {
if (filestream_read(fp, GENIEROM + 16, 4352 - 16) != (4352 - 16))
goto grerr;
}
filestream_close(fp);
/* Workaround for the FCE Ultra CHR page size only being 1KB */
for (x = 0; x < 4; x++)
memcpy(GENIEROM + 4096 + (x << 8), GENIEROM + 4096, 256);
}
geniestage = 1;
}
/* Called when a game is closed. */
void FCEU_CloseGenie(void) {
/* No good reason to free() the Game Genie ROM image data. */
geniestage = 0;
FlushGenieRW();
VPageR = VPage;
}
void FCEU_KillGenie(void) {
if (GENIEROM) {
free(GENIEROM);
GENIEROM = 0;
}
}
static DECLFR(GenieRead) {
return GENIEROM[A & 4095];
}
static DECLFW(GenieWrite) {
switch (A) {
case 0x800c:
case 0x8008:
case 0x8004: genieval[((A - 4) & 0xF) >> 2] = V; break;
case 0x800b:
case 0x8007:
case 0x8003: geniech[((A - 3) & 0xF) >> 2] = V; break;
case 0x800a:
case 0x8006:
case 0x8002: genieaddr[((A - 2) & 0xF) >> 2] &= 0xFF00; genieaddr[((A - 2) & 0xF) >> 2] |= V; break;
case 0x8009:
case 0x8005:
case 0x8001: genieaddr[((A - 1) & 0xF) >> 2] &= 0xFF; genieaddr[((A - 1) & 0xF) >> 2] |= (V | 0x80) << 8; break;
case 0x8000:
if (!V)
FixGenieMap();
else {
modcon = V ^ 0xFF;
if (V == 0x71)
modcon = 0;
}
break;
}
}
static readfunc GenieBackup[3];
static DECLFR(GenieFix1) {
uint8_t r = GenieBackup[0](A);
if ((modcon >> 1) & 1) /* No check */
return genieval[0];
else if (r == geniech[0])
return genieval[0];
return r;
}
static DECLFR(GenieFix2) {
uint8_t r = GenieBackup[1](A);
if ((modcon >> 2) & 1) /* No check */
return genieval[1];
else if (r == geniech[1])
return genieval[1];
return r;
}
static DECLFR(GenieFix3) {
uint8_t r = GenieBackup[2](A);
if ((modcon >> 3) & 1) /* No check */
return genieval[2];
else if (r == geniech[2])
return genieval[2];
return r;
}
void FixGenieMap(void) {
int x;
geniestage = 2;
for (x = 0; x < 8; x++)
VPage[x] = VPageG[x];
VPageR = VPage;
FlushGenieRW();
for (x = 0; x < 3; x++)
if ((modcon >> (4 + x)) & 1) {
readfunc tmp[3] = { GenieFix1, GenieFix2, GenieFix3 };
GenieBackup[x] = GetReadHandler(genieaddr[x]);
SetReadHandler(genieaddr[x], genieaddr[x], tmp[x]);
}
}
void FCEU_GeniePower(void) {
uint32_t x;
if (!geniestage)
return;
geniestage = 1;
for (x = 0; x < 3; x++) {
genieval[x] = 0xFF;
geniech[x] = 0xFF;
genieaddr[x] = 0xFFFF;
}
modcon = 0;
SetWriteHandler(0x8000, 0xFFFF, GenieWrite);
SetReadHandler(0x8000, 0xFFFF, GenieRead);
for (x = 0; x < 8; x++)
VPage[x] = GENIEROM + 4096 - 0x400 * x;
if (AllocGenieRW())
VPageR = VPageG;
else
geniestage = 2;
}