Files
psxrecomp/runtime/tests/test_psx_ram_runtime_map.c
Matthew Stanley 07aaa6e700 Add side-effect-free psx_peek_word + test
Debug reads of RAM/scratchpad/BIOS without MMIO, read hooks or unmapped_fatal.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-04 10:50:02 -07:00

333 lines
15 KiB
C

/* Live main-RAM geometry, end to end, through the REAL runtime paths.
*
* Links the runtime's own memory.c (guest loads/stores, MMIO routing),
* dma.c (channel 6 OTC, a DMA that writes RAM from a MADR the guest chose),
* boot_state.c (savestate save/load) and psx_ram_geometry.c (the state machine
* memory_init() drives). Everything else those files call is a counted stub in
* psx_ram_runtime_map_stubs.c. Both geometries are exercised in one process,
* exactly as a launch with and without the 8 MB mod would set them.
*
* Load-bearing claims:
* 1. Retail (mod off): every KUSEG/KSEG0/KSEG1 alias and every 2nd-4th RAM
* mirror of an address reaches the same DRAM byte, for word/half/byte
* loads and stores; the stack in the 4th mirror (Kula World) works.
* 2. Expanded (mod on): the whole 8 MiB window decodes uniquely.
* 3. A DMA cursor folds exactly like the CPU in each geometry.
* 4. A savestate records the live size; a state from the other geometry is
* REJECTED BEFORE ANY MUTATION (CPU, RAM, devices untouched), and a state
* whose header is compatible but whose RAM section is the wrong size is
* refused by the two-pass loader with zero mutation too.
* 5. The geometry helpers agree with hardware on every page of the window. */
#include "psx_memory.h"
#include "mod_plugins.h"
#include "boot_state.h"
#include "cpu_state.h"
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
extern uint32_t psx_read_word(uint32_t addr);
extern uint16_t psx_read_half(uint32_t addr);
extern uint8_t psx_read_byte(uint32_t addr);
extern void psx_write_word(uint32_t addr, uint32_t val);
extern void psx_write_half(uint32_t addr, uint16_t val);
extern void psx_write_byte(uint32_t addr, uint8_t val);
extern uint8_t *memory_get_ram_ptr(void);
extern uint8_t *memory_get_scratchpad_ptr(void);
/* Stub-side mutation counters (psx_ram_runtime_map_stubs.c). */
extern int g_stub_device_restores;
static int failures;
static int check(int cond, const char *label) {
if (!cond) {
fprintf(stderr, "FAIL: %s\n", label);
failures++;
return 0;
}
return 1;
}
static void set_geometry(int expanded) {
psx_ram_reset_size_request();
if (expanded) psx_mod_set_main_ram_8mb(1);
psx_ram_apply_size_request();
}
/* ---- 5. helpers vs hardware on every page ---------------------------- */
static void check_helpers_everywhere(int expanded, const char *phase) {
static const uint32_t segs[3] = { 0x00000000u, 0x80000000u, 0xA0000000u };
static const uint32_t offs[3] = { 0x000u, 0x7F0u, 0xFFCu };
const uint32_t mask = expanded ? 0x007FFFFFu : 0x001FFFFFu;
uint32_t page, s, o, bad = 0;
for (page = 0; page < (PSX_MAIN_RAM_WINDOW_BYTES >> 12); page++) {
for (o = 0; o < 3; o++) {
uint32_t phys = (page << 12) | offs[o];
uint32_t want = phys & mask;
uint32_t off = 0xFFFFFFFFu;
if (psx_ram_map_read(phys) != want) bad++;
if (psx_ram_map_write(phys) != want) bad++;
for (s = 0; s < 3; s++) {
uint32_t va = segs[s] | phys;
if (psx_ram_canonical_offset(va) != want) bad++;
if (!psx_ram_resolve(va, 4u, &off) || off != want) bad++;
}
}
}
if (bad) fprintf(stderr, " %s: %u geometry violations\n", phase, bad);
check(bad == 0u, phase);
check(memory_get_ram_bytes() == (expanded ? PSX_MAIN_RAM_EXPANDED_BYTES
: PSX_MAIN_RAM_RETAIL_BYTES), phase);
check(psx_ram_8mb_active() == expanded, phase);
/* Addresses outside the window are not RAM, whatever the geometry. */
check(psx_ram_map_read(0x1FC00000u) == 0x1FC00000u, "BIOS ROM is not folded");
check(!psx_ram_resolve(0x1F801000u, 4u, 0), "MMIO never resolves to RAM");
check(!psx_ram_resolve(0x00800000u, 4u, 0), "first byte past the window");
/* Retail: a span crossing a 2 MiB mirror boundary has no contiguous bytes. */
check(psx_ram_resolve(0x801FFFFEu, 4u, 0) == expanded,
"span across the 2 MiB end resolves only in the 8 MB map");
check(psx_ram_resolve(0x803FFFFEu, 4u, 0) == expanded,
"span across a mirror boundary resolves only in the 8 MB map");
}
/* ---- 1/2. real guest loads and stores -------------------------------- */
static void check_guest_access(int expanded) {
uint8_t *ram = memory_get_ram_ptr();
memset(ram, 0, PSX_MAIN_RAM_BACKING_BYTES);
/* Word store through KSEG0, visible through every alias of its bytes. */
psx_write_word(0x80001000u, 0x11223344u);
check(ram[0x1000] == 0x44 && ram[0x1003] == 0x11, "word store lands in DRAM");
check(psx_read_word(0x00001000u) == 0x11223344u, "KUSEG alias reads it");
check(psx_read_word(0xA0001000u) == 0x11223344u, "KSEG1 alias reads it");
for (uint32_t m = 1; m < 4; ++m) {
uint32_t mirror = 0x80001000u + m * 0x00200000u;
char label[96];
snprintf(label, sizeof label, "%s: mirror %u of a word",
expanded ? "8 MB" : "retail", m);
check((psx_read_word(mirror) == 0x11223344u) == !expanded, label);
}
/* A store to a high mirror address. */
psx_write_word(0x80601000u, 0xCAFEF00Du);
if (expanded) {
check(ram[0x601000] == 0x0D && psx_read_word(0x80001000u) == 0x11223344u,
"8 MB: a high-bank store is unique");
} else {
check(psx_read_word(0x80001000u) == 0xCAFEF00Du && ram[0x601000] == 0,
"retail: a 4th-mirror store folds onto the low 2 MiB");
}
/* Kula World parks $sp in the 4th mirror: push/pop through it. */
psx_write_word(0x807FFFF8u, 0x80012344u);
check(psx_read_word(0x807FFFF8u) == 0x80012344u, "stack slot round-trips");
check((psx_read_word(0x801FFFF8u) == 0x80012344u) == !expanded,
"4th-mirror stack aliases the 2 MiB top only on retail RAM");
/* Half and byte widths fold the same way. */
psx_write_half(0x80402002u, 0xBEEFu);
check((psx_read_half(0x80002002u) == 0xBEEFu) == !expanded, "half mirror");
psx_write_byte(0x80204005u, 0x5Au);
check((psx_read_byte(0x80004005u) == 0x5Au) == !expanded, "byte mirror");
check(psx_read_byte(0x80204005u) == 0x5Au, "byte reads back at its own address");
}
/* ---- psx_peek_word: debug read that must never reach the bus ----------- */
static void check_peek(int expanded) {
uint32_t v = 0xDEADBEEFu;
memset(memory_get_ram_ptr(), 0, PSX_MAIN_RAM_BACKING_BYTES);
psx_write_word(0x80001000u, 0x11223344u);
check(psx_peek_word(0x80001000u, &v) && v == 0x11223344u, "peek: KSEG0 RAM");
check(psx_peek_word(0x00001000u, &v) && v == 0x11223344u, "peek: KUSEG RAM");
check(psx_peek_word(0xA0001000u, &v) && v == 0x11223344u, "peek: KSEG1 RAM");
v = 0;
check(psx_peek_word(0x80201000u, &v) && ((v == 0x11223344u) == !expanded),
"peek: 2nd mirror follows the live geometry");
check(psx_peek_word(0x807FFFFCu, &v), "peek: top of the RAM window");
memory_get_scratchpad_ptr()[0x10] = 0x78; memory_get_scratchpad_ptr()[0x13] = 0x12;
check(psx_peek_word(0x1F800010u, &v) && (v & 0xFF) == 0x78 && (v >> 24) == 0x12,
"peek: scratchpad");
check(psx_peek_word(0x1F8003FCu, &v) && !psx_peek_word(0x1F800400u, &v),
"peek: scratchpad end bound");
check(psx_peek_word(0xBFC00000u, &v) && psx_peek_word(0x1FC7FFFCu, &v) &&
!psx_peek_word(0x1FC80000u, &v), "peek: BIOS ROM bounds");
/* Device / unmapped space is refused WITHOUT touching the bus (the stubbed
* psx_fatal_halt traps if a real access reached unmapped_fatal). */
v = 0xDEADBEEFu;
check(!psx_peek_word(0x1F801044u, &v), "peek: JOY_RX refused");
check(!psx_peek_word(0x1F801810u, &v), "peek: GPUREAD refused");
check(!psx_peek_word(0x1F802000u, &v), "peek: expansion-2 MMIO refused");
check(!psx_peek_word(0x1F000000u, &v), "peek: expansion-1 refused");
check(!psx_peek_word(0x80800000u, &v), "peek: past the RAM window refused");
check(!psx_peek_word(0xFFFE0130u, &v) && !psx_peek_word(0xC0000000u, &v),
"peek: KSEG2 refused");
check(!psx_peek_word(0x80001002u, &v), "peek: unaligned refused");
check(v == 0xDEADBEEFu, "peek: refused reads leave *out untouched");
}
/* ---- 3. a real DMA writing RAM from a guest-chosen MADR --------------- */
#define DMA_BASE 0x1F801080u
static void run_otc(uint32_t madr, uint32_t entries) {
psx_write_word(0x1F8010F0u, 0x0FEDCBA9u | (1u << 27)); /* DPCR: ch6 enabled */
psx_write_word(DMA_BASE + 6u * 16u + 0u, madr); /* MADR */
psx_write_word(DMA_BASE + 6u * 16u + 4u, entries); /* BCR */
psx_write_word(DMA_BASE + 6u * 16u + 8u, 0x11000002u); /* CHCR: start, trigger, step -4 */
}
static void check_dma(int expanded) {
uint8_t *ram = memory_get_ram_ptr();
memset(ram, 0, PSX_MAIN_RAM_BACKING_BYTES);
/* OTC writes a backwards linked list ending in the 0x00FFFFFF terminator,
* from MADR down. MADR in the 4th mirror. */
run_otc(0x80601010u, 4u);
uint32_t w_hi_low, w_hi_high;
memcpy(&w_hi_low, ram + 0x001010u, 4);
memcpy(&w_hi_high, ram + 0x601010u, 4);
if (expanded) {
check(w_hi_high == 0x0060100Cu && w_hi_low == 0u,
"8 MB: DMA cursor addresses the unique high bank");
} else {
check(w_hi_low == 0x0000100Cu && w_hi_high == 0u,
"retail: DMA cursor folds exactly like the DMAC (0x1FFFFC)");
}
uint32_t term;
memcpy(&term, ram + (expanded ? 0x601004u : 0x001004u), 4);
check(term == 0x00FFFFFFu, "OTC terminator written at the folded tail");
}
/* ---- 4. savestates ---------------------------------------------------- */
static void fill_cpu(CPUState *cpu, uint32_t salt) {
memset(cpu, 0, sizeof *cpu);
for (int i = 0; i < 32; i++) cpu->gpr[i] = 0x1000u * (uint32_t)i + salt;
cpu->pc = 0x80010000u + salt;
}
static int cpu_equal(const CPUState *a, const CPUState *b) {
return memcmp(a->gpr, b->gpr, sizeof a->gpr) == 0 && a->pc == b->pc &&
a->hi == b->hi && a->lo == b->lo &&
memcmp(a->cop0, b->cop0, sizeof a->cop0) == 0;
}
static uint8_t *save_state(size_t *len, uint32_t salt) {
CPUState cpu;
uint8_t *data = NULL;
fill_cpu(&cpu, salt);
memset(memory_get_ram_ptr(), (int)(salt & 0xFF), memory_get_ram_bytes());
if (!boot_state_save_buffer(&cpu, 0x12345678u, 0x80010000u, &data, len))
return NULL;
return data;
}
/* Load `state` and require that it is refused with no observable mutation. */
static void expect_refused_untouched(const uint8_t *state, size_t len,
const char *label, const char *reason_part) {
CPUState live, before;
uint8_t *ram = memory_get_ram_ptr();
uint8_t *ram_before = (uint8_t *)malloc(PSX_MAIN_RAM_BACKING_BYTES);
char reason[256];
int restores_before = g_stub_device_restores;
fill_cpu(&live, 0x777u);
before = live;
memset(ram, 0xC3, PSX_MAIN_RAM_BACKING_BYTES);
memcpy(ram_before, ram, PSX_MAIN_RAM_BACKING_BYTES);
check(!boot_state_load_buffer(state, len, 0x12345678u, 0x80010000u, &live), label);
check(cpu_equal(&live, &before), label);
check(memcmp(ram, ram_before, PSX_MAIN_RAM_BACKING_BYTES) == 0, label);
check(g_stub_device_restores == restores_before, label);
if (reason_part) {
check(!boot_state_check_buffer(state, len, 0x12345678u, 0x80010000u,
reason, sizeof reason) &&
strstr(reason, reason_part) != NULL,
label);
}
free(ram_before);
}
static void check_savestates(void) {
size_t len2 = 0, len8 = 0;
uint8_t *retail, *expanded;
CPUState cpu, want;
set_geometry(0);
retail = save_state(&len2, 0x21u);
set_geometry(1);
expanded = save_state(&len8, 0x81u);
check(retail && expanded, "both geometries save");
if (!retail || !expanded) return;
/* 8 MB runtime refuses the retail state, and vice versa, untouched. */
expect_refused_untouched(retail, len2, "8 MB runtime refuses a 2 MiB state",
"main_ram=2MiB(want 8MiB)");
set_geometry(0);
expect_refused_untouched(expanded, len8, "retail runtime refuses an 8 MiB state",
"main_ram=8MiB(want 2MiB)");
/* Two-pass: a header forged to look compatible (the RAM cookie cleared)
* still carries an 8 MiB RAM section AFTER the CPU section. The single-pass
* loader restored the CPU before rejecting RAM; now nothing is applied. */
{
uint8_t *forged = (uint8_t *)malloc(len8);
uint32_t cookie;
memcpy(forged, expanded, len8);
memcpy(&cookie, forged + 32, 4); /* header.reserved */
cookie ^= 0x384D4252u; /* drop the 8 MiB term */
memcpy(forged + 32, &cookie, 4);
check(boot_state_check_buffer(forged, len8, 0x12345678u, 0x80010000u,
NULL, 0),
"forged header passes the compatibility check");
expect_refused_untouched(forged, len8,
"wrong-size RAM section refused before the CPU applies",
NULL);
/* A truncated tail (last section cut) is refused untouched as well. */
expect_refused_untouched(retail, len2 - 64u,
"truncated state refused before anything applies", NULL);
free(forged);
}
/* The matching geometry loads, and restores what was saved. */
fill_cpu(&want, 0x21u);
fill_cpu(&cpu, 0x999u);
memset(memory_get_ram_ptr(), 0, PSX_MAIN_RAM_BACKING_BYTES);
check(boot_state_load_buffer(retail, len2, 0x12345678u, 0x80010000u, &cpu),
"retail state loads on a retail runtime");
check(cpu_equal(&cpu, &want), "retail load restores the CPU");
check(memory_get_ram_ptr()[0x1FFFFF] == 0x21 &&
memory_get_ram_ptr()[0x200000] == 0,
"retail load restores exactly 2 MiB of RAM");
set_geometry(1);
fill_cpu(&want, 0x81u);
check(boot_state_load_buffer(expanded, len8, 0x12345678u, 0x80010000u, &cpu),
"8 MiB state loads on an 8 MB runtime");
check(cpu_equal(&cpu, &want), "8 MB load restores the CPU");
check(memory_get_ram_ptr()[0x7FFFFF] == 0x81, "8 MB load restores all 8 MiB");
free(retail);
free(expanded);
}
int main(void) {
/* Process start and memory_init() without the mod: retail. */
check_helpers_everywhere(0, "default geometry is retail 2 MiB mirroring");
set_geometry(0);
check_helpers_everywhere(0, "memory_init without the mod stays retail");
/* The request is inert until memory_init() applies it. */
psx_ram_reset_size_request();
check(psx_mod_set_main_ram_8mb(1) == 1, "8 MB request accepted");
check_helpers_everywhere(0, "8 MB request is inert before memory_init");
psx_ram_apply_size_request();
check_helpers_everywhere(1, "8 MB request applies unique 8 MiB decode");
/* A later boot whose plan drops the mod returns to retail mirroring. */
set_geometry(0);
check_helpers_everywhere(0, "reboot without the mod restores retail");
for (int expanded = 0; expanded < 2; ++expanded) {
set_geometry(expanded);
check_guest_access(expanded);
check_peek(expanded);
check_dma(expanded);
}
check_savestates();
if (failures == 0) printf("PASS psx_ram_runtime_map (2 and 8 MiB, real memory/DMA/savestate paths)\n");
return failures ? 1 : 0;
}