/* memory.c — Phase 2 PS1 memory system. * * Physical address routing: * 0x00000000..0x001FFFFF — 2 MB RAM * 0x1F800000..0x1F8003FF — 1 KB scratchpad * 0x1F801000..0x1F803FFF — MMIO (fatal abort) * 0x1FC00000..0x1FC7FFFF — 512 KB BIOS ROM (read-only) * Everything else — fatal abort (unmapped) */ #include "cpu_state.h" #include "cdrom.h" #include "crash_trace.h" #include "dma.h" #include "fntrace.h" #include "gpu.h" #include "mdec.h" #include "mod_memory.h" #include "pst_wire.h" #include "sio.h" #include "spu.h" #include "timers.h" #include "lockstep.h" #include "data_shards.h" #include "dirty_ram_interp.h" #include "guest_tty.h" #include "psx_cycles.h" #include "psx_icache.h" #include "psx_memory.h" #include "render_pass.h" #include "render_pass_plan.h" #include "starvation_ring.h" #include #include #include #include #define RAM_SIZE PSX_MAIN_RAM_BACKING_BYTES #define SCRATCHPAD_SIZE 1024 #define BIOS_ROM_SIZE (512 * 1024) #define MOD_MEMORY_BASE 0x1F000000u #define MOD_MEMORY_SIZE (1u * 1024u * 1024u) static uint8_t ram[RAM_SIZE]; static uint8_t scratchpad[SCRATCHPAD_SIZE]; static uint8_t bios_rom[BIOS_ROM_SIZE]; static uint8_t mod_memory[MOD_MEMORY_SIZE]; static uint32_t mod_memory_used; static uint8_t mod_gpu_dma_memory[PSX_MOD_GPU_DMA_APERTURE_SIZE]; static uint32_t mod_gpu_dma_memory_used; uint32_t psx_mod_memory_snapshot_bytes(void) { return mod_memory_used || mod_gpu_dma_memory_used ? 16u + mod_memory_used + mod_gpu_dma_memory_used : 0u; } uint32_t psx_mod_memory_layout_cookie(void) { /* Compatibility guard, not a cryptographic identity. Include aperture * revision; old enabled-mod saves must be rejected before applying RAM. */ if (!psx_mod_memory_snapshot_bytes()) return 0u; return 0x4D4F4402u ^ (mod_memory_used * 16777619u) ^ mod_gpu_dma_memory_used; } void psx_mod_memory_snapshot_write(uint8_t* out) { PstW w; if (!out || !psx_mod_memory_snapshot_bytes()) return; pst_w_init(&w, out, psx_mod_memory_snapshot_bytes()); pst_w_u32(&w, 1u); pst_w_u32(&w, PSX_MOD_GPU_DMA_APERTURE_BASE); pst_w_u32(&w, mod_memory_used); pst_w_u32(&w, mod_gpu_dma_memory_used); memcpy(out + 16u, mod_memory, mod_memory_used); memcpy(out + 16u + mod_memory_used, mod_gpu_dma_memory, mod_gpu_dma_memory_used); } /* Non-mutating pre-check for boot_state's two-pass load; the reader runs it. */ int psx_mod_memory_snapshot_validate(const uint8_t* data, uint32_t size) { PstR r; uint32_t version, base, cpu_bytes, dma_bytes; if (!data || size < 16u || size != psx_mod_memory_snapshot_bytes()) return 0; pst_r_init(&r, data, size); return pst_r_u32(&r, &version) && pst_r_u32(&r, &base) && pst_r_u32(&r, &cpu_bytes) && pst_r_u32(&r, &dma_bytes) && version == 1u && base == PSX_MOD_GPU_DMA_APERTURE_BASE && cpu_bytes == mod_memory_used && dma_bytes == mod_gpu_dma_memory_used; } int psx_mod_memory_snapshot_read(const uint8_t* data, uint32_t size) { uint32_t cpu_bytes, dma_bytes; if (!psx_mod_memory_snapshot_validate(data, size)) return 0; cpu_bytes = mod_memory_used; dma_bytes = mod_gpu_dma_memory_used; memcpy(mod_memory, data + 16u, cpu_bytes); memcpy(mod_gpu_dma_memory, data + 16u + cpu_bytes, dma_bytes); return 1; } /* * Trusted mods may opt into host-backed guest memory in Expansion 1. Before * the first allocation this entire region retains hardware open-bus behavior. */ uint32_t psx_mod_memory_alloc(uint32_t size, uint32_t alignment) { uint32_t start; if (size == 0u) return 0u; if (alignment == 0u) alignment = 1u; if ((alignment & (alignment - 1u)) != 0u || alignment > 4096u) return 0u; start = (mod_memory_used + alignment - 1u) & ~(alignment - 1u); if (start > MOD_MEMORY_SIZE || size > MOD_MEMORY_SIZE - start) return 0u; memset(mod_memory + start, 0, size); mod_memory_used = start + size; return 0x9F000000u + start; } static int mod_memory_offset(uint32_t phys, uint32_t width, uint32_t *offset) { uint32_t off; if (phys < MOD_MEMORY_BASE) return 0; off = phys - MOD_MEMORY_BASE; if (off > mod_memory_used || width > mod_memory_used - off) return 0; if (offset) *offset = off; return 1; } uint32_t psx_mod_gpu_dma_memory_alloc(uint32_t size, uint32_t alignment) { uint32_t start; if (size == 0u) return 0u; if (alignment == 0u) alignment = 1u; if ((alignment & (alignment - 1u)) != 0u || alignment > 4096u) return 0u; start = (mod_gpu_dma_memory_used + alignment - 1u) & ~(alignment - 1u); if (start > PSX_MOD_GPU_DMA_APERTURE_SIZE || size > PSX_MOD_GPU_DMA_APERTURE_SIZE - start) return 0u; memset(mod_gpu_dma_memory + start, 0, size); mod_gpu_dma_memory_used = start + size; return PSX_MOD_GPU_DMA_GUEST_BASE + start; } static int mod_gpu_dma_memory_offset(uint32_t phys, uint32_t width, uint32_t *offset) { return psx_mod_gpu_dma_aperture_offset_for( phys, width, mod_gpu_dma_memory_used, offset); } uint32_t psx_mod_gpu_dma_resolve_address(uint32_t address) { return psx_mod_gpu_dma_resolve_address_for( address, mod_gpu_dma_memory_used); } /* Exposed for inlined main-RAM load helpers in psx_cyc.h (VLC/decode hot path). */ uint8_t *g_psx_ram = ram; /* PSX_LOAD_DELAY gate (default on). −1 = unread; 0/1 after first resolve. */ int g_psx_load_delay = -1; /* Physical address translation for guest accesses. Retail 2 MB DRAM is * mirrored across the first 8 MB of each segment (Kula World's crt0 parks $sp * in the 4th mirror); the opt-in 8 MB map decodes the window uniquely. One * live mask does both: phys & 0x1FFFFF retail, phys & 0x7FFFFF expanded. */ static inline uint32_t psx_phys_addr(uint32_t addr) { return psx_ram_map_read(addr); } static inline uint32_t psx_phys_addr_store(uint32_t addr) { return psx_ram_map_write(addr); } /* Expose RAM pointer for oracle comparison (find_first_divergence). */ uint8_t *memory_get_ram_ptr(void) { return ram; } uint8_t *memory_get_scratchpad_ptr(void) { return scratchpad; } /* Side-effect-FREE aligned word peek for the debug/observability path ONLY: * main RAM (every KUSEG/KSEG0/KSEG1 mirror), scratchpad and BIOS ROM. Never * touches MMIO, the lockstep/DS read hooks, read watches, or unmapped_fatal. * Returns 1 and fills *out when addr is backed by one of those; 0 otherwise. */ int psx_peek_word(uint32_t addr, uint32_t *out) { uint32_t phys, off; const uint8_t *p; if (addr >= 0xC0000000u || (addr & 3u)) return 0; phys = addr & 0x1FFFFFFFu; if (psx_ram_resolve(addr, 4u, &off)) { p = &ram[off]; } else if (phys >= 0x1F800000u && phys <= 0x1F8003FCu) { p = &scratchpad[phys - 0x1F800000u]; } else if (phys >= 0x1FC00000u && phys <= 0x1FC7FFFCu) { p = &bios_rom[phys - 0x1FC00000u]; } else { return 0; } if (out) memcpy(out, p, sizeof(*out)); return 1; } void memory_clear_low_boot_scratch(void) { memset(ram, 0, 0x10u); } /* ---- Dirty-page tracking for install-at-runtime code (CLAUDE.md Rule 18) ---- * * The PS1 BIOS writes 4-instruction dispatch stubs into kernel RAM at runtime * (e.g. RAM 0xCF0 for the SIO data-byte handler). A static recompiler can't * see those instructions because they don't exist at compile time. We track * which RAM pages have been written-to since boot and route any psx_dispatch * landing in such a page through a small MIPS interpreter (dirty_ram_interp.c). * * Granularity: 4 KB pages. Ordinary CPU writes only mark the kernel-code * region (RAM 0..0xFFFF) where BIOS install stubs live. CD-ROM DMA can also * mark arbitrary RAM ranges as executable candidates, which covers game * overlays loaded from disc without treating every data write as code. * * Future option (Option B, see docs/dynamic_handler_install.md): when a page * goes dirty, JIT-compile its bytes via StrictTranslator instead of running * an interpreter. Pros: one source of MIPS semantics, hot install stubs run * as native compiled C. Cons: gcc-at-runtime build dep, ~200 ms compile latency * stall on first dispatch, file I/O on hot path, cache-invalidation complexity, * Windows MinGW + dlopen friction. Revisit only if install stubs become a * measurable hot path; today they're cold-path glue (~4k instructions per * directory-load is sub-microsecond to interpret). */ #define DIRTY_RAM_KERNEL_TRACK_BYTES 0x10000u #define DIRTY_RAM_PAGE_SHIFT 12 /* 4 KB pages */ #define DIRTY_RAM_PAGE_COUNT (RAM_SIZE >> DIRTY_RAM_PAGE_SHIFT) #define DIRTY_RAM_BITMAP_WORDS ((DIRTY_RAM_PAGE_COUNT + 31u) / 32u) /* Guest-visible extent (retail 2 MiB unless the 8 MB mod is live). Host arrays * are sized for the backing capacity; bounds and serialization use this. */ #define RAM_LIVE (g_psx_ram_size) #define DIRTY_RAM_LIVE_BITMAP_WORDS \ (((RAM_LIVE >> DIRTY_RAM_PAGE_SHIFT) + 31u) / 32u) static uint32_t dirty_ram_bitmap[DIRTY_RAM_BITMAP_WORDS]; /* Monotonic generation for RAM-resident CODE changes (kernel install-stub * writes + DMA/EXE loads that mark executable ranges). Consumers that cache * per-PC classifications of RAM instructions (the interp's widescreen * cull/backdrop site caches) compare against this and re-derive after any * code change — a cached kind must never survive an overlay reload. */ uint32_t g_dirty_ram_code_gen = 1; static inline void dirty_ram_mark_page(uint32_t phys) { if (phys >= RAM_LIVE) return; uint32_t page = phys >> DIRTY_RAM_PAGE_SHIFT; uint32_t bit = 1u << (page & 31u); /* Generation bumps on the clean->dirty TRANSITION only: kernel-window data * writes land here on every guest store, and bumping per write would * invalidate the per-PC site caches continuously. Overlay (re)loads always * bump via dirty_ram_mark_executable_range. */ if (!(dirty_ram_bitmap[page >> 5] & bit)) g_dirty_ram_code_gen++; dirty_ram_bitmap[page >> 5] |= bit; } /* ---- Kernel-image bless ------------------------------------------------- * Kernel Part 2 (RAM [0x500,0x8500)) is the BIOS's boot-time copy of ROM * [0x1FC10000,0x1FC18000); the statically-recompiled kernel functions in the * generated dispatch table were compiled FROM those ROM bytes. Those pages * are permanently dirty (TCB saves, event tables, install stubs share the * window), which diverted EVERY kernel dispatch to the interpreter — the * dominant cost of event-heavy scenes (a Tomba village frame samples ~97% * in kernel PCs) and CD-load drains. * * A dispatch key may run its static native function IFF the live RAM bytes * of everything that function can execute (its body extent, emitter-supplied * via psx_bios_kernel_bodies[]) still byte-match the ROM source. Per-entry, * lazily verified, cached; a guest write landing INSIDE an entry's body * drops that entry back to unverified (next dispatch re-compares). Entries * whose bodies were runtime-patched (the BIOS installs pad/SIO handlers into * ROM-zero gaps of the pad driver) simply never verify and stay on the * faithful interpreter path — their behavior is unchanged by design. This is * the runtime half of the relocation-manifest contract * (docs/RELOCATION_MANIFEST_FORMAT.md: "runtime verifies against live RAM * before dispatching the AOT function"). */ /* The window/offset constants come from the LINKED recompiled BIOS itself * (psx_bios_image, emitted into _dispatch.c from the BIOS profile) — * they cannot disagree with the code they describe. Snapshotted into * file-statics on the lazy init latch so the two hot paths (dispatch query, * guest-store invalidation) keep a two-load range test instead of reading * through the extern each time. A BIOS with no bless window exports all * zeros: the span-0 range test then rejects every address. */ #include "psx_bios_image.h" #include "kernel_patch_ranges.h" static uint32_t s_kb_lo = 0, s_kb_span = 0, s_kb_rom_off = 0; static const PsxKernelPatchRange* s_kb_pr = 0; static uint32_t s_kb_pr_n = 0; static uint64_t kbless_patch_skips = 0; /* segments skipped, TCP counter */ #define KBLESS_UNKNOWN 0u #define KBLESS_CLEAN 1u #define KBLESS_MISMATCH 2u #define KBLESS_MAX_ENTRIES 4096u static uint8_t kbless_state[KBLESS_MAX_ENTRIES]; /* parallel to bodies[] */ static int kbless_enabled = -1; /* env PSX_KERNEL_BLESS=0 disables */ /* Always-on counters (TCP kernel_bless). */ static uint64_t kbless_native_hits = 0; static uint64_t kbless_verifies = 0; static uint64_t kbless_mismatches = 0; static uint64_t kbless_invalidations = 0; static int kbless_on(void) { if (kbless_enabled < 0) { const char* e = getenv("PSX_KERNEL_BLESS"); kbless_enabled = (e && e[0] == '0') ? 0 : 1; if (psx_bios_kernel_body_count > KBLESS_MAX_ENTRIES) kbless_enabled = 0; s_kb_lo = psx_bios_image.kbless_ram_lo; s_kb_span = psx_bios_image.kbless_ram_hi - psx_bios_image.kbless_ram_lo; s_kb_rom_off = psx_bios_image.kbless_rom_off; s_kb_pr = psx_bios_kernel_patch_ranges; s_kb_pr_n = psx_bios_kernel_patch_ranges ? psx_bios_kernel_patch_range_count : 0u; /* PSX_KERNEL_PATCH_RANGES=0 drops the declared ranges, restoring the * whole-body memcmp. Same purpose as PSX_KERNEL_BLESS=0 one level up: * an A/B instrument. With the ranges dropped a patched body mismatches * forever and its emitted hook is unreachable, which is exactly the * behaviour before the ranges existed — so one binary measures both * sides. */ { const char* pe = getenv("PSX_KERNEL_PATCH_RANGES"); if (pe && pe[0] == '0') s_kb_pr_n = 0; } if (s_kb_span == 0) kbless_enabled = 0; /* BIOS with no bless window */ /* The emitted constants must agree with each other and the ROM * array: a window whose ROM source exceeds the image is a build * defect, not a runtime condition. */ if (s_kb_span > 0 && (uint64_t)s_kb_rom_off + s_kb_span > (uint64_t)BIOS_ROM_SIZE) { fprintf(stderr, "FATAL: psx_bios_image kbless window [0x%X,+0x%X) " "exceeds the %u-byte ROM\n", s_kb_rom_off, s_kb_span, (unsigned)BIOS_ROM_SIZE); exit(1); } } return kbless_enabled; } /* Declared patch ranges (psx_bios_kernel_patch_ranges, emitted from the * profile's [[recompiler.install_slots]]): kernel-RAM words the guest is * EXPECTED to overwrite at boot. They sit inside compiled bodies, so the * whole-body memcmp below used to fail forever on the first install — the * reason Breath of Fire III's BIOS exception handler interpreted 1.22 billion * instructions with its card stub sitting in the profile's declared slot. * The body is verified in segments that skip them instead: CLEAN now means * every byte OUTSIDE every declared range still matches the ROM source, which * is exactly the claim the native code depends on. The patched words * themselves still execute on the dirty-RAM interpreter (Rule 18), entered * through the emitted patch-range hook. * * Snapshotted on the same latch as the window constants. The decision itself * lives in kernel_patch_ranges.c so it can be unit-tested without the * runtime's globals. */ /* Does a declared patch range END at this RAM address? The emitter registered * that PC as a continuation key, so the dirty-RAM interpreter hands straight- * line flow back to static dispatch there and only the patched words * interpret (dirty_ram_interp.c). */ int psx_kernel_patch_range_ends_at(uint32_t phys) { return psx_kernel_patch_ends_at(s_kb_pr, s_kb_pr_n, phys); } /* Binary search the (key-sorted) body table. -1 if absent. */ static int kbless_find(uint32_t phys) { uint32_t lo = 0, hi = psx_bios_kernel_body_count; while (lo < hi) { uint32_t mid = (lo + hi) >> 1; uint32_t k = psx_bios_kernel_bodies[mid].key; if (k == phys) return (int)mid; if (k < phys) lo = mid + 1; else hi = mid; } return -1; } /* Dispatch-time query: may `phys` run its static native function? * Verifies lazily; every failure mode falls back to the interpreter. */ int psx_kernel_bless_dispatchable(uint32_t phys) { if (!kbless_on()) return 0; if (phys - s_kb_lo >= s_kb_span) return 0; int i = kbless_find(phys); if (i < 0) return 0; uint8_t st = kbless_state[i]; if (st == KBLESS_CLEAN) { kbless_native_hits++; return 1; } if (st == KBLESS_MISMATCH) return 0; const PsxKernelBody* b = &psx_bios_kernel_bodies[i]; kbless_verifies++; if (psx_kernel_patch_cmp(s_kb_pr, s_kb_pr_n, ram, bios_rom, s_kb_lo, s_kb_rom_off, b->body_lo, b->body_hi, &kbless_patch_skips) == 0) { kbless_state[i] = KBLESS_CLEAN; kbless_native_hits++; return 1; } kbless_state[i] = KBLESS_MISMATCH; kbless_mismatches++; return 0; } /* A write landed in the kernel window: any entry whose body contains the * written byte must re-verify before its next native dispatch. Kernel DATA * writes (TCB saves, event tables — the frequent case) lie outside every * body and fall through the loop without invalidating anything; genuine * code-window writes (install-time, patches) are rare. */ static void kbless_note_write(uint32_t phys) { if (kbless_enabled <= 0) return; /* also pre-init: nothing verified yet */ if (phys - s_kb_lo >= s_kb_span) return; uint32_t n = psx_bios_kernel_body_count; if (n > KBLESS_MAX_ENTRIES) return; for (uint32_t i = 0; i < n; i++) { /* No early exit: a continuation key can sort ABOVE the written * address while its parent body contains it (bodies overlap). n is * a few hundred; this path fires only on kernel-window writes. */ const PsxKernelBody* b = &psx_bios_kernel_bodies[i]; if (phys >= b->body_lo && phys < b->body_hi && kbless_state[i] != KBLESS_UNKNOWN) { kbless_state[i] = KBLESS_UNKNOWN; kbless_invalidations++; } } } /* Range write (DMA / EXE load / savestate restore) overlapping the window: * bulk, rare events — reset every entry rather than per-byte scanning. */ void psx_kernel_bless_note_range(uint32_t phys, uint32_t len) { if (kbless_enabled <= 0) return; /* also pre-init: nothing verified yet */ if (len == 0) return; uint32_t end = phys + len; if (end < phys) end = 0xFFFFFFFFu; if (end <= s_kb_lo || phys >= s_kb_lo + s_kb_span) return; uint32_t n = psx_bios_kernel_body_count; if (n > KBLESS_MAX_ENTRIES) return; for (uint32_t i = 0; i < n; i++) { if (kbless_state[i] != KBLESS_UNKNOWN) { kbless_state[i] = KBLESS_UNKNOWN; kbless_invalidations++; } } } void psx_kernel_bless_stats(uint64_t out[8]) { uint32_t n = psx_bios_kernel_body_count; uint32_t clean = 0, mism = 0; if (n > KBLESS_MAX_ENTRIES) n = KBLESS_MAX_ENTRIES; for (uint32_t i = 0; i < n; i++) { if (kbless_state[i] == KBLESS_CLEAN) clean++; if (kbless_state[i] == KBLESS_MISMATCH) mism++; } out[0] = psx_bios_kernel_body_count; out[1] = clean; out[2] = mism; out[3] = kbless_native_hits; out[4] = kbless_verifies; out[5] = kbless_invalidations; /* Declared patch ranges, and how many segments the verifier skipped * because of them: the proof that a body with a live install stub is * being blessed rather than failing forever. */ out[6] = s_kb_pr_n; out[7] = kbless_patch_skips; } void psx_kernel_bless_resync_after_restore(void) { /* kbless_state is host-only. CLEAN/MISMATCH are sticky across guest * stores only via kbless_note_write; savestate RAM memcpy never hits * that path. Leaving MISMATCH blocks native forever against restored * bytes that may match ROM again; leaving CLEAN skips re-verify against * restored patches. Both fork RB/selfcheck peers. */ memset(kbless_state, KBLESS_UNKNOWN, sizeof(kbless_state)); } void psx_kernel_bless_reset_for_boot(void) { /* SCPH and OpenBIOS use different kbless_rom_off / span. The one-shot * latch in kbless_on() survives soft-return, so rematch BIOS switches * would bless against the wrong ROM slice. */ kbless_enabled = -1; s_kb_lo = 0; s_kb_span = 0; s_kb_rom_off = 0; s_kb_pr = NULL; s_kb_pr_n = 0; memset(kbless_state, KBLESS_UNKNOWN, sizeof(kbless_state)); } static inline void dirty_ram_mark_kernel_write(uint32_t phys) { if (phys >= DIRTY_RAM_KERNEL_TRACK_BYTES) return; dirty_ram_mark_page(phys); kbless_note_write(phys); } int dirty_ram_is_dirty(uint32_t phys); /* Establish the clean compiled-image baseline for the game-EXE text region. * Called ONCE when the game entry is first reached (fntrace game-start): by then * the BIOS has fully loaded the boot EXE into [0x10000, FLOOR) — which IS the * compiled image — but no gameplay overlay has run yet. The EXE load (CD DMA via * dirty_ram_mark_executable_range) marks the whole text dirty as a FALSE POSITIVE * (RAM == compiled image); clearing it here means dirty_ram_is_dirty() afterwards * is true ONLY for pages a later overlay actually overwrote. The dispatch can then * trust a clean text page to run its compiled function and divert only truly- * overlaid pages to the interpreter (Tomba 2 loads a loader overlay over * 0x8001Dxxx). The kernel window [0,0x10000) (BIOS install stubs) and the overlay * region OUTSIDE [BASE, FLOOR) are left untouched — including the RAM BELOW the * text image, which is overlay space for a high-loading boot EXE (Klonoa loads * at 0x180000). Baselining from the kernel-window end instead of the real text * base wiped that whole region's dirty bits. See dirty_ram_interp.h. */ extern uint32_t g_overlay_region_floor; void dirty_ram_clear_image_baseline(void) { uint32_t floor = g_overlay_region_floor; if (floor <= DIRTY_RAM_KERNEL_TRACK_BYTES) return; if (floor > RAM_LIVE) floor = RAM_LIVE; uint32_t base = g_text_image_lo; if (base < DIRTY_RAM_KERNEL_TRACK_BYTES) base = DIRTY_RAM_KERNEL_TRACK_BYTES; if (base >= floor) return; uint32_t first_page = base >> DIRTY_RAM_PAGE_SHIFT; uint32_t last_page = (floor - 1u) >> DIRTY_RAM_PAGE_SHIFT; for (uint32_t page = first_page; page <= last_page; page++) dirty_ram_bitmap[page >> 5] &= ~(1u << (page & 31u)); } /* Text-image divergence guard. * * dirty_ram_is_dirty() is intentionally coarse: it says a page was touched by * runtime code loading. For deciding whether the original static game dispatch * is still safe, a dirty page is not enough information: data in a code page can * be written without changing the instructions at a target, while packed games * can also rewrite their own text with completely different instructions. * * main.cpp registers the boot EXE bytes as the reference image. Writes inside * that range mark pages whose bytes differ from the reference. At dispatch time, * a modified page is still allowed to use the static native entry if the code * bytes at the target match the original image; otherwise it is sticky-diverged * and must execute from live RAM through the dirty interpreter. */ /* Mutable: the runtime owns this heap buffer (main.cpp mallocs the EXE image and * never frees it). Intentional data patches by the translation layer are blessed * into it via dirty_ram_text_bless so they are not mistaken for self-modifying * code — see dirty_ram_text_native_ok / dirty_ram_text_bless. */ static uint8_t *text_ref_image = NULL; static uint32_t text_ref_lo = 0, text_ref_hi = 0; static uint32_t text_modified_bitmap[DIRTY_RAM_BITMAP_WORDS]; static uint32_t text_diverged_bitmap[DIRTY_RAM_BITMAP_WORDS]; static uint64_t g_text_native_blocked = 0; static uint32_t g_text_diverged_pages = 0; static uint64_t g_text_exact_mismatches = 0; static uint32_t g_text_exact_last_range_lo = 0; static uint32_t g_text_exact_last_range_len = 0; static uint32_t g_text_exact_last_mismatch = 0; static uint32_t g_text_exact_last_live = 0; static uint32_t g_text_exact_last_ref = 0; void dirty_ram_register_text_image(uint32_t phys_lo, const uint8_t *bytes, uint32_t len) { if (!bytes || len == 0 || phys_lo >= RAM_LIVE) return; if (len > RAM_LIVE - phys_lo) len = RAM_LIVE - phys_lo; text_ref_image = (uint8_t *)bytes; /* runtime-owned mutable heap buffer */ text_ref_lo = phys_lo; text_ref_hi = phys_lo + len; memset(text_modified_bitmap, 0, sizeof(text_modified_bitmap)); memset(text_diverged_bitmap, 0, sizeof(text_diverged_bitmap)); g_text_native_blocked = 0; g_text_diverged_pages = 0; g_text_exact_mismatches = 0; g_text_exact_last_range_lo = 0; g_text_exact_last_range_len = 0; g_text_exact_last_mismatch = 0; g_text_exact_last_live = 0; g_text_exact_last_ref = 0; } int dirty_ram_text_image_registered(void) { return text_ref_image != NULL; } static inline void text_guard_note_write(uint32_t phys, uint32_t val, int size) { if (!text_ref_image) return; if (phys < text_ref_lo || phys + (uint32_t)size > text_ref_hi) return; const uint8_t *ref = text_ref_image + (phys - text_ref_lo); uint8_t buf[4] = { (uint8_t)val, (uint8_t)(val >> 8), (uint8_t)(val >> 16), (uint8_t)(val >> 24) }; if (memcmp(ref, buf, (size_t)size) != 0) { uint32_t page = phys >> DIRTY_RAM_PAGE_SHIFT; text_modified_bitmap[page >> 5] |= (1u << (page & 31u)); } } /* PSX_FORCE_INTERP=1 (tooling, defined below): no game text is native-safe, * so every dispatch into it takes the dirty-RAM interpreter. The byte * compare in the two checks below would otherwise pass for untouched text * and run the compiled image, since force-interp only marks pages dirty. */ static int dirty_ram_force_interp(void); int dirty_ram_text_native_ok(uint32_t phys) { if (dirty_ram_force_interp() && phys >= DIRTY_RAM_KERNEL_TRACK_BYTES) return 0; if (!text_ref_image || phys < text_ref_lo || phys >= text_ref_hi) return !dirty_ram_is_dirty(phys); uint32_t page = phys >> DIRTY_RAM_PAGE_SHIFT; uint32_t bit = 1u << (page & 31u); if (text_diverged_bitmap[page >> 5] & bit) { g_text_native_blocked++; return 0; } /* Fast path: a page never touched by a guarded write AND not runtime-dirty * still holds the pristine compiled image — native is safe with no compare. */ if (!(text_modified_bitmap[page >> 5] & bit) && !dirty_ram_is_dirty(phys)) return 1; /* Flagged (a guarded write that differed, an overlay-dirty page, or an * intentional runtime data patch): the compiled native code is valid IFF the * bytes at the entry still match the reference image. Decide by the ACTUAL * bytes, not the page-dirty heuristic — so a data-only change in a page that * also holds code (e.g. a translation string table blessed into the ref via * dirty_ram_text_bless) does not needlessly block a still-valid function and * route it to psx_unknown_dispatch. A genuine code overwrite still diverges. */ uint32_t n = 256; if (n > text_ref_hi - phys) n = text_ref_hi - phys; if (memcmp(ram + phys, text_ref_image + (phys - text_ref_lo), n) == 0) return 1; text_diverged_bitmap[page >> 5] |= bit; g_text_diverged_pages++; g_text_native_blocked++; return 0; } /* Validate the exact instruction ranges emitted for a static game function. * Each pair is {virtual/physical lo, byte len}; non-code gaps and mutable data * on the same page are intentionally absent. Unlike the legacy 256-byte probe, * a mismatch never poisons an unrelated 4 KB page forever: every decision is * made from the live bytes the native body will actually execute. * * exec_pc is the dispatch/resume address, kept for observability and future * use; ranges are validated IN FULL regardless of it. The former * [exec_pc, end) clip assumed a continuation "never fetches the patched * bytes" behind its resume PC — false for any entry with a backward edge: * a post-call continuation re-enters mid-function and loops back into the * clipped-away region, executing stale static code (CMR2 overlay * corruption, second locus 0x80016B34). A genuinely patched prologue now * blocks the whole entry to the interpreter — correct, merely slower. */ int dirty_ram_text_native_ok_ranges_from(const uint32_t *lo_len_pairs, uint32_t count, uint32_t exec_pc) { if (!text_ref_image || !lo_len_pairs || count == 0) return 0; if (dirty_ram_force_interp()) return 0; (void)exec_pc; int any = 0; for (uint32_t i = 0; i < count; i++) { uint32_t phys = lo_len_pairs[i * 2u] & 0x1FFFFFFFu; uint32_t len = lo_len_pairs[i * 2u + 1u]; if (len == 0 || phys < text_ref_lo || phys >= text_ref_hi || len > text_ref_hi - phys) { g_text_native_blocked++; return 0; } any = 1; /* Page-clean fast path: every page this range touches is neither * guard-modified nor runtime-dirty, so its bytes still equal the * reference image — skip the memcmp. This keeps the emitted * stale-static guards on inter-piece host transfers near-free on * the (overwhelmingly common) pristine pages. */ { uint32_t p0 = phys >> DIRTY_RAM_PAGE_SHIFT; uint32_t p1 = (phys + len - 1u) >> DIRTY_RAM_PAGE_SHIFT; uint32_t p; int clean = 1; for (p = p0; p <= p1; p++) { if ((text_modified_bitmap[p >> 5] & (1u << (p & 31u))) || dirty_ram_is_dirty(p << DIRTY_RAM_PAGE_SHIFT)) { clean = 0; break; } } if (clean) continue; } if (memcmp(ram + phys, text_ref_image + (phys - text_ref_lo), len) != 0) { uint32_t off = 0; const uint8_t *live = ram + phys; const uint8_t *ref = text_ref_image + (phys - text_ref_lo); while (off < len && live[off] == ref[off]) off++; g_text_exact_mismatches++; g_text_exact_last_range_lo = phys; g_text_exact_last_range_len = len; g_text_exact_last_mismatch = phys + off; g_text_exact_last_live = off < len ? live[off] : 0; g_text_exact_last_ref = off < len ? ref[off] : 0; /* Do not sticky-poison the page. A continuation on the same page * may still match its clipped ranges. */ g_text_native_blocked++; return 0; } } if (!any) { g_text_native_blocked++; return 0; } return 1; } /* Preserve the generated-code ABI used by existing game projects. */ int dirty_ram_text_native_ok_ranges(const uint32_t *lo_len_pairs, uint32_t count) { return dirty_ram_text_native_ok_ranges_from(lo_len_pairs, count, 0u); } void dirty_ram_text_exact_mismatch_stats(uint64_t *count, uint32_t out[5]) { if (count) *count = g_text_exact_mismatches; if (!out) return; out[0] = g_text_exact_last_range_lo; out[1] = g_text_exact_last_range_len; out[2] = g_text_exact_last_mismatch; out[3] = g_text_exact_last_live; out[4] = g_text_exact_last_ref; } /* Bless an INTENTIONAL data patch into the reference image so the text-divergence * guard does not mistake it for self-modifying code. The runtime's own * translation layer (text_xlate) patches string/glyph tables that share 4 KB * pages — and the guard's 256-byte compare window — with real compiled functions. * Without this, patching a data table diverges the page and blocks native * dispatch of every function in it (a real function fatally routes to * psx_unknown_dispatch). By updating text_ref_image to match the patched bytes, * the compare passes for the intentional change while a genuine game write of * DIFFERENT bytes still diverges and is still caught. Clears the sticky diverged * bit for affected pages so the guard re-evaluates against the updated reference. * Byte-for-byte identical writes are a no-op. */ void dirty_ram_text_bless(uint32_t phys, const uint8_t *bytes, uint32_t len) { if (!text_ref_image || !bytes || len == 0) return; if (phys >= text_ref_hi || phys + len <= text_ref_lo) return; /* out of range */ uint32_t lo = phys < text_ref_lo ? text_ref_lo : phys; uint32_t hi = phys + len > text_ref_hi ? text_ref_hi : phys + len; if (hi <= lo) return; uint8_t *ref = text_ref_image + (lo - text_ref_lo); const uint8_t *src = bytes + (lo - phys); if (memcmp(ref, src, hi - lo) == 0) return; /* already in sync */ memcpy(ref, src, hi - lo); /* Re-open the affected pages: clear the sticky diverged bit so the next * dispatch re-runs the compare against the now-updated reference. */ uint32_t first_page = lo >> DIRTY_RAM_PAGE_SHIFT; uint32_t last_page = (hi - 1u) >> DIRTY_RAM_PAGE_SHIFT; for (uint32_t page = first_page; page <= last_page; page++) text_diverged_bitmap[page >> 5] &= ~(1u << (page & 31u)); } uint64_t dirty_ram_text_native_blocked(void) { return g_text_native_blocked; } uint32_t dirty_ram_text_diverged_pages(void) { return g_text_diverged_pages; } uint32_t dirty_ram_text_modified_bitmap_word(uint32_t word_index) { if (word_index >= DIRTY_RAM_BITMAP_WORDS) return 0; return text_modified_bitmap[word_index]; } uint32_t dirty_ram_text_diverged_bitmap_word(uint32_t word_index) { if (word_index >= DIRTY_RAM_BITMAP_WORDS) return 0; return text_diverged_bitmap[word_index]; } void dirty_ram_mark_executable_range(uint32_t phys, uint32_t len) { if (len == 0 || phys >= RAM_LIVE) return; psx_kernel_bless_note_range(phys, len); uint32_t end = phys + len - 1u; if (end >= RAM_LIVE || end < phys) end = RAM_LIVE - 1u; uint32_t first_page = phys >> DIRTY_RAM_PAGE_SHIFT; uint32_t last_page = end >> DIRTY_RAM_PAGE_SHIFT; for (uint32_t page = first_page; page <= last_page; page++) { dirty_ram_bitmap[page >> 5] |= (1u << (page & 31u)); } g_dirty_ram_code_gen++; } /* Force-interp mode (tooling): PSX_FORCE_INTERP=1 makes ALL RAM above the kernel * window report dirty, so every dispatch into game/overlay text routes to the * dirty-RAM interpreter (the SAME path overlays take) instead of the compiled * image. Interp-path Δ-ruler enabler: lets the cyctest isolation loops be measured * native-INTERP vs Beetle. Consulted by every routing site that already calls * dirty_ram_is_dirty (top dispatch, dirty_ram_dispatch_inner gates, the * psx_dispatch_game_compiled gates) — no emitter/dispatch change needed. */ static int dirty_ram_force_interp(void) { static int s = -1; if (s < 0) { const char* e = getenv("PSX_FORCE_INTERP"); s = (e && e[0] && e[0] != '0'); } return s; } /* DIAGNOSTIC ONLY (class-B reproduction, NOT a fix — Rule -1): PSX_SHELLWIN_INTERP=1 * reports the BIOS shell-copy relocated RAM window [0x30000, 0x5AFFF] as dirty, so * the compiled dispatch (full_function_emitter.cpp:1380) routes those addresses * through the recovering dirty-RAM interp instead of normalize()->shell ROM. This * peels the class-A shell-window pc=0 wedge (func_1FC42090) so the *general* * class-B compiled exception-return pc=0 (in [0x5B000, 0x8F000)) can surface and be * captured. Must be reverted before any merge; it is a probe, not the fix. */ static int dirty_ram_shellwin_interp(void) { static int s = -1; if (s < 0) { #ifdef PSX_SHELLWIN_INTERP_DEFAULT s = 1; #else const char* e = getenv("PSX_SHELLWIN_INTERP"); s = (e && e[0] && e[0] != '0'); #endif } return s; } int dirty_ram_is_dirty(uint32_t phys) { /* Dirtiness belongs to the BYTES: a PC in a retail RAM mirror is asked * about the page it folds to (identity for every live-RAM offset). */ phys = psx_ram_map_read(phys); if (phys >= RAM_LIVE) return 0; if (dirty_ram_force_interp() && phys >= DIRTY_RAM_KERNEL_TRACK_BYTES) return 1; if (dirty_ram_shellwin_interp() && phys >= 0x00030000u && phys <= 0x0005AFFFu) return 1; /* Experimental fallback for overlays copied into their final location by * ordinary guest CPU stores rather than CD DMA. Dispatch OUTSIDE the static * image (either side of it) is treated as dynamic and validated by the * interpreter. Below-text RAM counts too: a high-loading boot EXE streams * its gameplay code into the RAM beneath itself (Klonoa loads at 0x180000 * and runs overlays from 0x10000+), and gating on the floor alone left * those pages unreachable by the interpreter. See dirty_ram_interp.h. */ if (phys_is_overlay_region(phys)) return 1; uint32_t page = phys >> DIRTY_RAM_PAGE_SHIFT; return (dirty_ram_bitmap[page >> 5] >> (page & 31u)) & 1u; } uint32_t dirty_ram_get_bitmap(void) { return dirty_ram_bitmap[0]; } uint32_t dirty_ram_get_bitmap_word(uint32_t word_index) { if (word_index >= DIRTY_RAM_BITMAP_WORDS) return 0; return dirty_ram_bitmap[word_index]; } /* Serialized (savestate / netplay digest) extent: the LIVE RAM's pages only, * so a retail session's dirty-bitmap section is unchanged by the 8 MiB host * backing. */ uint32_t dirty_ram_get_bitmap_word_count(void) { return DIRTY_RAM_LIVE_BITMAP_WORDS; } void dirty_ram_set_bitmap_words(const uint32_t* words, uint32_t count) { memset(dirty_ram_bitmap, 0, sizeof(dirty_ram_bitmap)); if (count > DIRTY_RAM_LIVE_BITMAP_WORDS) count = DIRTY_RAM_LIVE_BITMAP_WORDS; for (uint32_t i = 0; i < count; i++) dirty_ram_bitmap[i] = words[i]; /* Bitmap replace bypasses clean→dirty transitions; bump so interpreter * site caches keyed on g_dirty_ram_code_gen cannot survive a restore. */ g_dirty_ram_code_gen++; } /* ---- Inc3: watched overlay pages + per-page generation counters --------- * Pages covered by a registered overlay function's code range. The store path * (the single, audited RAM-write chokepoint — all CPU + DMA stores funnel * here) tests the watch bitmap and, on a hit, bumps that page's generation * counter. It does NOT eagerly invalidate (Inc1-D did): validity is now decided * lazily, per compiled entry, at dispatch time (overlay_loader.c §8). The * generation counter lets the loader cheaply detect "did any page covering this * entry's code change since I last validated it?" without hashing on the store * path. Monotonic: gen only increases, so a sum over an entry's pages is a * perfect change detector (no aliasing). * * The bitmap is almost always empty, so the per-store cost on the common path * is a single bitmap lookup. */ static uint32_t overlay_watch_bitmap[DIRTY_RAM_BITMAP_WORDS]; static uint32_t overlay_page_gen[DIRTY_RAM_PAGE_COUNT]; void dirty_ram_reset_for_boot(void) { memset(dirty_ram_bitmap, 0, sizeof(dirty_ram_bitmap)); memset(text_modified_bitmap, 0, sizeof(text_modified_bitmap)); memset(text_diverged_bitmap, 0, sizeof(text_diverged_bitmap)); g_text_diverged_pages = 0; memset(overlay_watch_bitmap, 0, sizeof(overlay_watch_bitmap)); memset(overlay_page_gen, 0, sizeof(overlay_page_gen)); memset(g_dirty_ram_exec_page_bitmap, 0, sizeof(g_dirty_ram_exec_page_bitmap)); memset(g_dirty_ram_exec_pc_bitmap, 0, sizeof(g_dirty_ram_exec_pc_bitmap)); dirty_ram_dispatch_evidence_clear(0u, DIRTY_RAM_EXEC_BITMAP_WORDS); g_dirty_ram_code_gen++; } void overlay_watch_set_range(uint32_t phys, uint32_t len) { if (len == 0 || phys >= RAM_LIVE) return; uint32_t end = phys + len - 1u; if (end >= RAM_LIVE || end < phys) end = RAM_LIVE - 1u; uint32_t fp = phys >> DIRTY_RAM_PAGE_SHIFT; uint32_t lp = end >> DIRTY_RAM_PAGE_SHIFT; for (uint32_t pg = fp; pg <= lp; pg++) overlay_watch_bitmap[pg >> 5] |= (1u << (pg & 31u)); } void overlay_watch_clear_range(uint32_t phys, uint32_t len) { if (len == 0 || phys >= RAM_LIVE) return; uint32_t end = phys + len - 1u; if (end >= RAM_LIVE || end < phys) end = RAM_LIVE - 1u; uint32_t fp = phys >> DIRTY_RAM_PAGE_SHIFT; uint32_t lp = end >> DIRTY_RAM_PAGE_SHIFT; for (uint32_t pg = fp; pg <= lp; pg++) overlay_watch_bitmap[pg >> 5] &= ~(1u << (pg & 31u)); } /* Sum of generation counters over the pages spanning [phys, phys+len). The * loader stores this at validation time and compares on dispatch; any change * means a watched page in the range was written. */ uint32_t overlay_watch_pagegen_sum(uint32_t phys, uint32_t len) { if (len == 0 || phys >= RAM_LIVE) return 0; uint32_t end = phys + len - 1u; if (end >= RAM_LIVE || end < phys) end = RAM_LIVE - 1u; uint32_t fp = phys >> DIRTY_RAM_PAGE_SHIFT; uint32_t lp = end >> DIRTY_RAM_PAGE_SHIFT; uint32_t sum = 0; for (uint32_t pg = fp; pg <= lp; pg++) sum += overlay_page_gen[pg]; return sum; } /* Savestate restores RAM via memcpy and never hits the store chokepoint that * bumps overlay_page_gen. Without this, ENTRY_VALID overlays keep the gen-gated * fast path and run native code against restored bytes they were not validated * for — hang / freeze after the restored frame presents. */ void dirty_ram_text_guard_resync_after_restore(void) { /* text_diverged_bitmap is sticky: once a page's entry bytes fail the * reference compare, native stays blocked forever. That is correct for * forward sim, but after a savestate/RB rewind the restored RAM may * match the reference again — leaving the pre-load sticky bit forces * dirty-interp where the peer (or the prior resim) still runs native, * forking MotK selfcheck warm #2vs#3 at matched clocks (win#118 class: * cold≡0, warm FAIL; post-span irq_resume also drifts). Drop both * host-only text-guard bitmaps; live writes re-arm modified, and the * next native_ok compare re-decides diverge against restored bytes. * * "Modified" is recomputed, not just forgotten. The page-clean fast * path in dirty_ram_text_native_ok_ranges_from trusts a clear bit as * "bytes still equal the reference" and skips the compare, but the * restored RAM may carry self-modified text (CMR2 class) that no live * write will ever re-flag: the bitmap is not part of the state image, * and the writes happened in the process that produced it. One pass * over the reference range (at most 2 MB, once per restore) puts every * mismatching page back under the exact compare. */ memset(text_modified_bitmap, 0, sizeof(text_modified_bitmap)); memset(text_diverged_bitmap, 0, sizeof(text_diverged_bitmap)); g_text_diverged_pages = 0; if (text_ref_image && text_ref_hi > text_ref_lo) { uint32_t p0 = text_ref_lo >> DIRTY_RAM_PAGE_SHIFT; uint32_t p1 = (text_ref_hi - 1u) >> DIRTY_RAM_PAGE_SHIFT; for (uint32_t p = p0; p <= p1; p++) { uint32_t lo = p << DIRTY_RAM_PAGE_SHIFT; uint32_t hi = lo + (1u << DIRTY_RAM_PAGE_SHIFT); if (lo < text_ref_lo) lo = text_ref_lo; if (hi > text_ref_hi) hi = text_ref_hi; if (hi > RAM_LIVE) hi = RAM_LIVE; if (hi <= lo) continue; if (memcmp(ram + lo, text_ref_image + (lo - text_ref_lo), hi - lo) != 0) text_modified_bitmap[p >> 5] |= (1u << (p & 31u)); } } } void overlay_watch_invalidate_after_ram_restore(void) { for (uint32_t pg = 0; pg < DIRTY_RAM_PAGE_COUNT; pg++) overlay_page_gen[pg]++; extern void overlay_loader_note_code_write(void); extern void overlay_loader_resync_validation_after_restore(void); overlay_loader_note_code_write(); dirty_ram_text_guard_resync_after_restore(); psx_kernel_bless_resync_after_restore(); overlay_loader_resync_validation_after_restore(); } static inline void overlay_watch_note_write(uint32_t phys, uint32_t size) { uint32_t pg = phys >> DIRTY_RAM_PAGE_SHIFT; if (pg >= (RAM_LIVE >> DIRTY_RAM_PAGE_SHIFT)) return; /* Never attach pre-write PC evidence to post-write bytes, including for * completely unknown/self-modifying code. This is deliberately a compact * page clear, not a capture: serializing snapshots from this universal * guest-store hook caused unbounded queues and multi-second stalls. CD DMA * and periodic coherent capture remain the durable variant boundaries. */ if ((g_dirty_ram_exec_page_bitmap[pg >> 5] >> (pg & 31u)) & 1u) { uint32_t bitmap_word = pg * (4096u / 4u / 32u); memset(&g_dirty_ram_exec_pc_bitmap[bitmap_word], 0, (4096u / 4u / 32u) * sizeof(uint32_t)); dirty_ram_dispatch_evidence_clear(bitmap_word, 4096u / 4u / 32u); g_dirty_ram_exec_page_bitmap[pg >> 5] &= ~(1u << (pg & 31u)); } if ((overlay_watch_bitmap[pg >> 5] >> (pg & 31u)) & 1u) { overlay_page_gen[pg]++; /* Also invalidate generation-aware negative overlay lookups: bytes in * a manifested code page may now match a previously absent variant. * Keep this separate from g_dirty_ram_code_gen: interpreter widescreen * classifiers use that epoch and must not churn on every watched-page * data write. */ extern void overlay_loader_note_code_write(void); overlay_loader_note_code_write(); /* Self-modification of a currently-executing native entry cannot be * recovered lazily (the next dispatch is too late) — the loader * blacklists that entry. Everything else is handled at dispatch. */ extern void overlay_loader_active_write_check(uint32_t phys, uint32_t size); overlay_loader_active_write_check(phys, size); } } /* Memory control registers: 0x1F801000..0x1F80103F (16 words) + 0x1F801060 (RAM size). * Includes expansion base/size, COM_DELAY, SPU_DELAY, CDROM_DELAY etc. */ static uint32_t mem_ctrl[16]; /* indices 0..15 → addresses 0x1F801000..0x1F80103C */ static uint32_t ram_size_reg; /* 0x1F801060 */ /* KSEG2 no-op access telemetry (see the guards in the accessors). */ uint64_t g_kseg2_ignored_reads; uint64_t g_kseg2_ignored_writes; /* Cache control register (KSEG2: 0xFFFE0130). */ static uint32_t cache_ctrl; /* Pointer to cpu->cop0[12] (SR). Set once at init. * Used by write functions to check the IsC (Isolate Cache) bit: while it is * set, a CPU store goes to the caches and never reaches the bus (isc_store). */ static const uint32_t *sr_ptr; /* Interrupt controller — non-static so hardware subsystems can set I_STAT bits. */ uint32_t i_stat; /* 0x1F801070 — interrupt status (AND-acknowledge semantics) */ uint32_t i_mask; /* 0x1F801074 — interrupt enable mask */ /* Shadow-diff device-access detector. run_shadow_diff arms g_shadow_mmio_watch * around its single authoritative (interpreter) probe pass; ANY MMIO touch bumps * g_shadow_mmio_hits, so the harness can detect a device-touching function and * SKIP the validation (native) pass — device I/O must never be double-executed * (one spurious card/SIO/DMA write corrupts hardware state). Zero cost when not * watching (adds 0). */ int g_shadow_mmio_watch = 0; uint64_t g_shadow_mmio_hits = 0; /* Always-on MMIO access counter (reads AND writes of the device page). The * idle-loop cycle skip (psx_cycles.c) requires "no MMIO touched since the * last interrupt check" before fast-forwarding time: an MMIO read can return * time-varying or side-effecting values (timer counters, CD response FIFO), * so any touch disqualifies the window. One increment per access. */ uint64_t g_mmio_access_count = 0; #define SHADOW_NOTE_MMIO() do { g_mmio_access_count++; \ g_shadow_mmio_hits += (uint64_t)g_shadow_mmio_watch; } while (0) /* Always-on guest store counter, bumped at the top of every raw store * (RAM, scratchpad, MMIO, even discarded KSEG2/IsC stores — over-counting * is conservative). Second gate of the idle-loop cycle skip: a window with * ANY store is not a pure poll loop. CPU stores and in-flight DMA writes * both funnel through the psx_write_*_raw chokepoints. */ uint64_t g_guest_store_count = 0; /* ---- Card protocol trace: tracks I_MASK bit 7 transitions ---- */ #define IMASK_TRACE_CAP 4096 typedef struct { uint32_t old_mask; uint32_t new_mask; uint32_t caller; /* g_debug_current_func_addr */ uint32_t store_pc; /* g_debug_last_store_pc — exact PC of the SW/SH */ uint8_t width; /* 8, 16, or 32 */ uint8_t bit7_set; /* 1 if this write SET bit 7 */ uint8_t bit7_clear; /* 1 if this write CLEARED bit 7 */ uint8_t in_exc; } ImaskTraceEntry; static ImaskTraceEntry imask_trace[IMASK_TRACE_CAP]; static int imask_trace_idx = 0; static int imask_trace_count = 0; static int imask_bit7_set_count = 0; static int imask_bit7_clear_count = 0; static void imask_trace_record(uint32_t old_val, uint32_t new_val, uint8_t width) { extern uint32_t g_debug_current_func_addr; extern uint32_t g_debug_last_store_pc; extern int psx_get_in_exception(void); ImaskTraceEntry *e = &imask_trace[imask_trace_idx]; e->old_mask = old_val; e->new_mask = new_val; e->caller = g_debug_current_func_addr; e->store_pc = g_debug_last_store_pc; e->width = width; e->bit7_set = (!(old_val & 0x80) && (new_val & 0x80)) ? 1 : 0; e->bit7_clear = ((old_val & 0x80) && !(new_val & 0x80)) ? 1 : 0; e->in_exc = (uint8_t)psx_get_in_exception(); if (e->bit7_set) imask_bit7_set_count++; if (e->bit7_clear) imask_bit7_clear_count++; imask_trace_idx = (imask_trace_idx + 1) % IMASK_TRACE_CAP; imask_trace_count++; } /* VBLANK-ack telemetry (Tomba 2 exception-reentry-storm diagnosis): counts how * many times an I_STAT write clears the VBLANK bit (the handler's ack). Read in * the freeze heartbeat against g_vblank_raise/deliver counts. */ uint64_t g_vblank_ack_count = 0; /* interrupts.c — CAUSE.IP2 mirrors the INTC line and must be recomputed at * every point (I_STAT & I_MASK) can change. Both writers below are such a * point: an ack can drop the line, a mask write can drop or raise it. */ extern void psx_irq_refresh_cause_ip2(void); static void interrupt_write_stat_masked(uint32_t val, uint32_t mask) { uint32_t ack_mask = mask & 0x7FFu; uint32_t before = i_stat; i_stat = (i_stat & ~ack_mask) | (i_stat & val & ack_mask); if ((before & 1u) && !(i_stat & 1u)) g_vblank_ack_count++; /* VBLANK bit 1->0 */ psx_irq_refresh_cause_ip2(); } static void interrupt_write_mask_masked(uint32_t val, uint32_t mask, uint8_t width) { uint32_t old = i_mask; uint32_t next = ((i_mask & ~mask) | (val & mask)) & 0x7FFu; /* INTC mask writes are owned by the guest, never by a device repair. */ i_mask = next; imask_trace_record(old, i_mask, width); { extern void sio_card_handoff_on_imask(uint32_t old_mask, uint32_t new_mask); sio_card_handoff_on_imask(old, i_mask); } psx_irq_refresh_cause_ip2(); } /* Getters for debug server */ int memory_get_imask_bit7_set_count(void) { return imask_bit7_set_count; } int memory_get_imask_bit7_clear_count(void) { return imask_bit7_clear_count; } const ImaskTraceEntry *memory_get_imask_trace(int *idx_out, int *count_out) { if (idx_out) *idx_out = imask_trace_idx; if (count_out) *count_out = imask_trace_count; return imask_trace; } /* Tier 1 write-trace hooks (implemented in debug_server.c). */ extern void debug_server_trace_write_check(uint32_t phys, uint32_t old_val, uint32_t new_val, uint8_t width); extern void debug_server_trace_mmio_write(uint32_t addr, uint32_t val, uint8_t width); extern void debug_server_trace_mmio_read(uint32_t addr, uint32_t val, uint8_t width); /* Targeted main-RAM read watch (debug_server.c). Flag gates the hot read path. */ extern int g_ram_read_watch_active; extern void debug_server_trace_ram_read_watch(uint32_t phys, uint32_t val); extern void debug_server_trace_entryint_write(uint32_t phys, uint32_t old_val, uint32_t new_val, uint8_t width); extern CPUState *debug_cpu_ptr; extern uint32_t g_debug_last_store_pc; /* Parity last-writer provenance (parity_trace.c): note every main-RAM write so * the watch-word last-writer table tracks the exact producing store. No-op * unless the parity ring is armed. */ extern void parity_trace_note_write(uint32_t addr, uint32_t width, uint32_t writer_pc); /* Effective writer PC for provenance: during a DMA transfer the last CPU-store * PC is stale/unrelated, so attribute DMA-sourced RAM writes to the PC that * kicked the DMA (dma.c). Matches the wtrace recorder's DMA attribution. */ extern int g_dma_exec_depth; extern uint32_t g_dma_initiator_pc; static inline uint32_t effective_store_pc(void) { return (g_dma_exec_depth > 0 && g_dma_initiator_pc) ? g_dma_initiator_pc : g_debug_last_store_pc; } /* Card-byte destination capture (Phase 3 audit). Always-on. */ extern int card_data_writes_check(uint32_t phys, uint32_t value, uint8_t width); static inline uint32_t read_ram_word(uint32_t phys) { return (uint32_t)ram[phys] | ((uint32_t)ram[phys + 1] << 8) | ((uint32_t)ram[phys + 2] << 16) | ((uint32_t)ram[phys + 3] << 24); } static inline uint16_t read_ram_half(uint32_t phys) { return (uint16_t)ram[phys] | ((uint16_t)ram[phys + 1] << 8); } /* SCPH-1001 store-PC keys for code that runs relocated (docs/SEGMENT_AWARE_CODE.md * §9). `pc` is the PC the CPU executes the store at, which the compiled BIOS * (BiosAddressModel::runtime_pc) and the interpreter both stamp; `rom` is the * store's address in the image. A RAM PC alone is ambiguous: another BIOS, or * game code once the region is reused, can run a store there. So the key * matches only SCPH-1001's own instruction: the active image is SCPH-1001 and * the RAM word at `pc` is still the ROM word it was copied from. That limits * each key to the one store its ROM-address key named before the re-key, now * whichever backend runs it. */ #include "psx_bios_known_images.h" static inline int scph1001_relocated_store(uint32_t pc, uint32_t rom) { if (g_debug_last_store_pc != pc) return 0; static uint32_t s_scph1001_crc = 0; if (!s_scph1001_crc) { const PsxKnownBiosImage* img = psx_known_bios_by_stem("SCPH1001"); s_scph1001_crc = img ? img->crc32 : 0xFFFFFFFFu; } if (psx_bios_image.image_crc32 != s_scph1001_crc) return 0; const uint32_t phys = pc & 0x1FFFFFFFu; const uint32_t off = rom & (BIOS_ROM_SIZE - 1u); if (phys + 4u > RAM_SIZE) return 0; const uint32_t rom_word = (uint32_t)bios_rom[off] | ((uint32_t)bios_rom[off + 1] << 8) | ((uint32_t)bios_rom[off + 2] << 16) | ((uint32_t)bios_rom[off + 3] << 24); return read_ram_word(phys) == rom_word; } /* SPU registers are now handled by spu.c */ void memory_set_sr_ptr(const uint32_t *p) { sr_ptr = p; } uint32_t memory_get_sr(void) { return sr_ptr ? *sr_ptr : 0; } static uint32_t s_bios_checksum = 0; uint32_t memory_get_bios_checksum(void) { return s_bios_checksum; } void memory_init(const char* bios_path) { psx_ram_apply_size_request(); memset(ram, 0, sizeof(ram)); memset(scratchpad, 0, sizeof(scratchpad)); /* Rematch re-enters without process exit — wipe sticky I/O regs that * live outside device *_init (I_STAT/I_MASK cleared in interrupts_init). */ memset(mem_ctrl, 0, sizeof(mem_ctrl)); ram_size_reg = 0; i_stat = 0; i_mask = 0; /* Host dirty/text/overlay bitmaps survive memset(ram) and fork dig0. */ dirty_ram_reset_for_boot(); /* Re-latch kbless window from the newly activated psx_bios_image. */ psx_kernel_bless_reset_for_boot(); FILE* f = fopen(bios_path, "rb"); if (!f) { fprintf(stderr, "FATAL: cannot open BIOS file: %s\n", bios_path); exit(1); } size_t n = fread(bios_rom, 1, BIOS_ROM_SIZE, f); fclose(f); if (n != BIOS_ROM_SIZE) { fprintf(stderr, "FATAL: BIOS file %s is %zu bytes (expected %d)\n", bios_path, n, BIOS_ROM_SIZE); exit(1); } s_bios_checksum = 0; for (uint32_t i = 0; i < BIOS_ROM_SIZE / 4; i++) s_bios_checksum += ((const uint32_t*)bios_rom)[i]; } /* Unmapped-register access INSIDE the I/O window (0x1F801000..0x1F803FFF): * real hardware open-buses these (reads return garbage, writes vanish; no * fault) and games genuinely hit them — Tomba2's late attract sweeps a wild * byte loop across the whole window (bzero/read over a 0xDF80xxxx pointer). * Beetle returns 0 / ignores. Match it: count + (already ring-traced by the * callers' mmio trace hooks) + open-bus. Genuinely unknown-DEVICE reads are * still observable via the always-on MMIO rings and these counters — probes * query the rings, per the ring-buffer doctrine. mmio_fatal is retired. */ uint64_t g_io_openbus_reads; uint64_t g_io_openbus_writes; static void mmio_fatal(uint32_t vaddr, uint32_t phys, const char* op) { static char reason[96]; snprintf(reason, sizeof(reason), "MMIO %s @ 0x%08X (phys 0x%08X)", op, vaddr, phys); fprintf(stderr, "%s\n", reason); fflush(stderr); FILE* cf = fopen("psx_crash.txt", "w"); if (cf) { fprintf(cf, "%s\n", reason); fclose(cf); } psx_fatal_halt(reason); } static void mmio_unimplemented(uint32_t addr, const char* op) { static char reason[96]; snprintf(reason, sizeof(reason), "UNIMPLEMENTED MMIO %s @ 0x%08X", op, addr); fprintf(stderr, "%s\n", reason); fflush(stderr); /* Also write to a crash file for capture when stderr is lost. */ FILE* cf = fopen("psx_crash.txt", "w"); if (cf) { fprintf(cf, "%s\n", reason); fclose(cf); } psx_fatal_halt(reason); } static void unmapped_fatal(uint32_t vaddr, uint32_t phys, const char* op) { /* On real PS1 hardware, reads from unmapped addresses return open bus * (typically the last value on the data bus, or 0xFFFFFFFF). * The BIOS intentionally probes unmapped regions (RAM size detection, * expansion hardware detection). Fatal abort would prevent normal boot. */ (void)vaddr; (void)phys; (void)op; } /* --- MMIO read/write helpers --- */ static uint32_t mmio_read32_impl(uint32_t addr) { SHADOW_NOTE_MMIO(); /* Memory control: 0x1F801000..0x1F801020 */ if (addr >= 0x1F801000u && addr <= 0x1F80103Cu) { return mem_ctrl[(addr - 0x1F801000u) >> 2]; } /* SIO: 0x1F801040..0x1F80105F */ if (addr >= 0x1F801040u && addr <= 0x1F80105Fu) { return sio_read(addr); } /* RAM size: 0x1F801060 */ if (addr == 0x1F801060u) { return ram_size_reg; } /* Interrupts: 0x1F801070, 0x1F801074 */ if (addr == 0x1F801070u) { sio_tick(0); return i_stat; } if (addr == 0x1F801074u) return i_mask; /* DMA: 0x1F801080..0x1F8010FF */ if (addr >= 0x1F801080u && addr <= 0x1F8010FFu) { return dma_read(addr); } /* Timers: 0x1F801100..0x1F80112F */ if (addr >= 0x1F801100u && addr <= 0x1F80112Fu) { return timers_read(addr); } /* CDROM: 0x1F801800..0x1F801803 */ if (addr >= 0x1F801800u && addr <= 0x1F801803u) { return cdrom_read(addr); } /* GPU: 0x1F801810 (GPUREAD), 0x1F801814 (GPUSTAT) */ if (addr == 0x1F801810u) return gpu_read_gpuread(); if (addr == 0x1F801814u) return gpu_read_gpustat(); /* MDEC: 0x1F801820, 0x1F801824 */ if (addr == 0x1F801820u || addr == 0x1F801824u) return mdec_read(addr); /* SPU: 0x1F801C00..0x1F801FFF */ if (addr >= 0x1F801C00u && addr <= 0x1F801FFFu) { return spu_read(addr); } /* Expansion 2 / POST: 0x1F802000..0x1F802FFF */ if (addr >= 0x1F802000u && addr <= 0x1F802FFFu) { return 0; } { /* open-bus (Beetle parity) */ g_io_openbus_reads++; return 0;; } return 0; } /* Thin wrapper: record the loaded value into the MMIO-READ trace ring AFTER the * single (side-effecting) read. Callers use mmio_read32; the body is _impl, so * the device read executes exactly once. */ static uint32_t mmio_read32(uint32_t addr) { psx_devices_mmio_sync(); uint32_t v = mmio_read32_impl(addr); debug_server_trace_mmio_read(addr, v, 4); return v; } static void mmio_write32(uint32_t addr, uint32_t val) { psx_devices_mmio_sync(); SHADOW_NOTE_MMIO(); debug_server_trace_mmio_write(addr, val, 4); /* Memory control: 0x1F801000..0x1F801020 */ if (addr >= 0x1F801000u && addr <= 0x1F80103Cu) { mem_ctrl[(addr - 0x1F801000u) >> 2] = val; return; } /* SIO: 0x1F801040..0x1F80105F */ if (addr >= 0x1F801040u && addr <= 0x1F80105Fu) { sio_write(addr, val); return; } /* RAM size: 0x1F801060 */ if (addr == 0x1F801060u) { ram_size_reg = val; return; } /* Interrupts: 0x1F801070, 0x1F801074 */ if (addr == 0x1F801070u) { interrupt_write_stat_masked(val, 0xFFFFFFFFu); return; } if (addr == 0x1F801074u) { interrupt_write_mask_masked(val, 0xFFFFFFFFu, 32); return; } /* DMA: 0x1F801080..0x1F8010FF */ if (addr >= 0x1F801080u && addr <= 0x1F8010FFu) { dma_write(addr, val); return; } /* Timers: 0x1F801100..0x1F80112F */ if (addr >= 0x1F801100u && addr <= 0x1F80112Fu) { timers_write(addr, val); return; } /* CDROM: 0x1F801800..0x1F801803 */ if (addr >= 0x1F801800u && addr <= 0x1F801803u) { cdrom_write(addr, val); return; } /* GPU GP0: 0x1F801810, GP1: 0x1F801814 */ if (addr == 0x1F801810u) { uint32_t src = addr; /* SCPH-1001 shell store to GP0. The shell runs relocated at * 0x80030000, so ROM 0xBFC38B1C executes at 0x80050B1C * (scph1001_relocated_store). */ if (debug_cpu_ptr && scph1001_relocated_store(0x80050B1Cu, 0xBFC38B1Cu)) { /* Word-aligned RAM key through the live geometry (retail: the * 0x1FFFFC fold, identical to the DMA/GPU source keys). */ src = psx_ram_canonical_offset(debug_cpu_ptr->gpr[4] - 4u) & ~3u; } gpu_set_gp0_source(src); gpu_write_gp0(val); return; } if (addr == 0x1F801814u) { gpu_write_gp1(val); return; } /* MDEC: 0x1F801820, 0x1F801824 */ if (addr == 0x1F801820u || addr == 0x1F801824u) { mdec_write(addr, val); return; } /* SPU: 0x1F801C00..0x1F801FFF */ if (addr >= 0x1F801C00u && addr <= 0x1F801FFFu) { spu_write(addr, val); return; } /* Expansion 2 / POST: 0x1F802000..0x1F802FFF */ if (addr >= 0x1F802000u && addr <= 0x1F802FFFu) { return; /* POST port — ignore */ } { /* open-bus (Beetle parity) */ g_io_openbus_writes++; return;; } } static uint16_t mmio_read16_impl(uint32_t addr) { SHADOW_NOTE_MMIO(); /* Memory control: 0x1F801000..0x1F80103C — halfword lane of the 32-bit * register (games touch EXP1/EXP2 config with sub-word accesses; Tomba2's * late attract byte-writes 0x1F801000). */ if (addr >= 0x1F801000u && addr <= 0x1F80103Fu) { uint32_t v = mem_ctrl[(addr - 0x1F801000u) >> 2]; return (uint16_t)(v >> (8u * (addr & 2u))); } if (addr >= 0x1F801060u && addr <= 0x1F801063u) { return (uint16_t)(ram_size_reg >> (8u * (addr & 2u))); } /* SIO: 0x1F801040..0x1F80105F */ if (addr >= 0x1F801040u && addr <= 0x1F80105Fu) { return (uint16_t)sio_read(addr); } /* Interrupts */ if (addr >= 0x1F801070u && addr <= 0x1F801072u) { sio_tick(0); uint32_t shift = (addr & 2u) ? 16u : 0u; return (uint16_t)(i_stat >> shift); } if (addr >= 0x1F801074u && addr <= 0x1F801076u) { uint32_t shift = (addr & 2u) ? 16u : 0u; return (uint16_t)(i_mask >> shift); } /* Timers: 0x1F801100..0x1F80112F */ if (addr >= 0x1F801100u && addr <= 0x1F80112Fu) { return (uint16_t)timers_read(addr); } /* DMA: 0x1F801080..0x1F8010FF */ if (addr >= 0x1F801080u && addr <= 0x1F8010FFu) { uint32_t val = dma_read(addr & ~3u); return (addr & 2) ? (uint16_t)(val >> 16) : (uint16_t)val; } /* MDEC: 0x1F801820..0x1F801827 */ if (addr >= 0x1F801820u && addr <= 0x1F801827u) { uint32_t val = mdec_read(addr & ~3u); return (addr & 2) ? (uint16_t)(val >> 16) : (uint16_t)val; } /* SPU: 0x1F801C00..0x1F801FFF */ if (addr >= 0x1F801C00u && addr <= 0x1F801FFFu) { return (uint16_t)spu_read(addr); } { /* open-bus (Beetle parity) */ g_io_openbus_reads++; return 0;; } return 0; } static uint16_t mmio_read16(uint32_t addr) { psx_devices_mmio_sync(); uint16_t v = mmio_read16_impl(addr); debug_server_trace_mmio_read(addr, (uint32_t)v, 2); return v; } static void mmio_write16(uint32_t addr, uint16_t val) { psx_devices_mmio_sync(); SHADOW_NOTE_MMIO(); debug_server_trace_mmio_write(addr, (uint32_t)val, 2); /* Memory control: 0x1F801000..0x1F80103C — halfword lane RMW. */ if (addr >= 0x1F801000u && addr <= 0x1F80103Fu) { uint32_t idx = (addr - 0x1F801000u) >> 2; uint32_t shift = 8u * (addr & 2u); mem_ctrl[idx] = (mem_ctrl[idx] & ~(0xFFFFu << shift)) | ((uint32_t)val << shift); return; } /* RAM size register: 0x1F801060 — halfword lane RMW. */ if (addr >= 0x1F801060u && addr <= 0x1F801063u) { uint32_t shift = 8u * (addr & 2u); ram_size_reg = (ram_size_reg & ~(0xFFFFu << shift)) | ((uint32_t)val << shift); return; } /* SIO: 0x1F801040..0x1F80105F */ if (addr >= 0x1F801040u && addr <= 0x1F80105Fu) { sio_write(addr, val); return; } /* Interrupts */ if (addr >= 0x1F801070u && addr <= 0x1F801072u) { uint32_t shift = (addr & 2u) ? 16u : 0u; interrupt_write_stat_masked((uint32_t)val << shift, 0xFFFFu << shift); return; } if (addr >= 0x1F801074u && addr <= 0x1F801076u) { uint32_t shift = (addr & 2u) ? 16u : 0u; interrupt_write_mask_masked((uint32_t)val << shift, 0xFFFFu << shift, 16); return; } /* Timers: 0x1F801100..0x1F80112F */ if (addr >= 0x1F801100u && addr <= 0x1F80112Fu) { timers_write(addr, val); return; } /* DMA: 0x1F801080..0x1F8010FF */ if (addr >= 0x1F801080u && addr <= 0x1F8010FFu) { uint32_t aligned = addr & ~3u; uint32_t shift = (addr & 2) ? 16u : 0u; uint32_t mask = 0xFFFFu << shift; dma_write_masked(aligned, (uint32_t)val << shift, mask); return; } /* MDEC: 0x1F801820..0x1F801827 */ if (addr >= 0x1F801820u && addr <= 0x1F801827u) { uint32_t aligned = addr & ~3u; uint32_t cur = mdec_read(aligned); if (addr & 2) cur = (cur & 0x0000FFFFu) | ((uint32_t)val << 16); else cur = (cur & 0xFFFF0000u) | (uint32_t)val; mdec_write(aligned, cur); return; } /* SPU: 0x1F801C00..0x1F801FFF */ if (addr >= 0x1F801C00u && addr <= 0x1F801FFFu) { spu_write(addr, val); return; } { /* open-bus (Beetle parity) */ g_io_openbus_writes++; return;; } } static uint8_t mmio_read8_impl(uint32_t addr) { SHADOW_NOTE_MMIO(); /* Memory control: 0x1F801000..0x1F80103C — byte lane of the 32-bit reg. */ if (addr >= 0x1F801000u && addr <= 0x1F80103Fu) { uint32_t v = mem_ctrl[(addr - 0x1F801000u) >> 2]; return (uint8_t)(v >> (8u * (addr & 3u))); } if (addr >= 0x1F801060u && addr <= 0x1F801063u) { return (uint8_t)(ram_size_reg >> (8u * (addr & 3u))); } /* Interrupts: 0x1F801070..0x1F801077 (I_STAT, I_MASK) */ if (addr >= 0x1F801070u && addr <= 0x1F801077u) { if (addr < 0x1F801074u) sio_tick(0); uint32_t val = (addr < 0x1F801074u) ? i_stat : i_mask; return (uint8_t)(val >> (8 * (addr & 3))); } /* SIO: 0x1F801040..0x1F80104F */ if (addr >= 0x1F801040u && addr <= 0x1F80104Fu) { return (uint8_t)sio_read(addr & ~3u); } /* DMA: 0x1F801080..0x1F8010FF — byte reads return the corresponding * byte of the 32-bit register. The BIOS shell reads DICR (0x1F8010F4) * as individual bytes during interrupt handling. */ if (addr >= 0x1F801080u && addr <= 0x1F8010FFu) { uint32_t aligned = addr & ~3u; uint32_t val = dma_read(aligned); return (uint8_t)(val >> (8 * (addr & 3))); } /* MDEC: 0x1F801820..0x1F801827 */ if (addr >= 0x1F801820u && addr <= 0x1F801827u) { uint32_t val = mdec_read(addr & ~3u); return (uint8_t)(val >> (8 * (addr & 3))); } /* CDROM: 0x1F801800..0x1F801803 */ if (addr >= 0x1F801800u && addr <= 0x1F801803u) { return (uint8_t)cdrom_read(addr); } /* Expansion 2 / POST: 0x1F802000..0x1F802FFF */ if (addr >= 0x1F802000u && addr <= 0x1F802FFFu) { return 0; } { /* open-bus (Beetle parity) */ g_io_openbus_reads++; return 0;; } return 0; } static uint8_t mmio_read8(uint32_t addr) { psx_devices_mmio_sync(); uint8_t v = mmio_read8_impl(addr); debug_server_trace_mmio_read(addr, (uint32_t)v, 1); return v; } static void mmio_write8(uint32_t addr, uint8_t val) { psx_devices_mmio_sync(); SHADOW_NOTE_MMIO(); debug_server_trace_mmio_write(addr, (uint32_t)val, 1); /* Memory control: 0x1F801000..0x1F80103C — byte lane RMW. Tomba2's late * attract byte-writes the EXP1 base register at 0x1F801000; only the * 32-bit path covered the block and the byte path fatal'd. */ if (addr >= 0x1F801000u && addr <= 0x1F80103Fu) { uint32_t idx = (addr - 0x1F801000u) >> 2; uint32_t shift = 8u * (addr & 3u); mem_ctrl[idx] = (mem_ctrl[idx] & ~(0xFFu << shift)) | ((uint32_t)val << shift); return; } /* RAM size register: 0x1F801060 — byte lane RMW (Tomba2 late attract). */ if (addr >= 0x1F801060u && addr <= 0x1F801063u) { uint32_t shift = 8u * (addr & 3u); ram_size_reg = (ram_size_reg & ~(0xFFu << shift)) | ((uint32_t)val << shift); return; } /* Interrupts: partial stores affect only the addressed byte lane. */ if (addr >= 0x1F801070u && addr <= 0x1F801073u) { uint32_t shift = 8u * (addr & 3u); interrupt_write_stat_masked((uint32_t)val << shift, 0xFFu << shift); return; } if (addr >= 0x1F801074u && addr <= 0x1F801077u) { uint32_t shift = 8u * (addr & 3u); interrupt_write_mask_masked((uint32_t)val << shift, 0xFFu << shift, 8); return; } /* SIO: 0x1F801040..0x1F80105F */ if (addr >= 0x1F801040u && addr <= 0x1F80105Fu) { sio_write(addr & ~3u, (uint32_t)val); return; } /* DMA: 0x1F801080..0x1F8010FF — byte writes update the corresponding * byte of the 32-bit register. Needed for DICR byte-level access. */ if (addr >= 0x1F801080u && addr <= 0x1F8010FFu) { uint32_t aligned = addr & ~3u; uint32_t shift = 8 * (addr & 3); uint32_t mask = 0xFFu << shift; dma_write_masked(aligned, (uint32_t)val << shift, mask); return; } /* Timers: 0x1F801100..0x1F80112F — byte writes update the addressed byte * lane of the 32-bit register. Byte stores to timer registers are valid * hardware accesses; mmio_write16/32 already route here via timers_write, so * write8 must too (otherwise a guest `sb` to a timer fails loud). */ if (addr >= 0x1F801100u && addr <= 0x1F80112Fu) { uint32_t aligned = addr & ~3u; uint32_t cur = timers_read(aligned); uint32_t shift = 8 * (addr & 3); cur = (cur & ~(0xFFu << shift)) | ((uint32_t)val << shift); timers_write(aligned, cur); return; } /* MDEC: 0x1F801820..0x1F801827 */ if (addr >= 0x1F801820u && addr <= 0x1F801827u) { uint32_t aligned = addr & ~3u; uint32_t cur = mdec_read(aligned); uint32_t shift = 8 * (addr & 3); cur = (cur & ~(0xFFu << shift)) | ((uint32_t)val << shift); mdec_write(aligned, cur); return; } /* CDROM: 0x1F801800..0x1F801803 */ if (addr >= 0x1F801800u && addr <= 0x1F801803u) { cdrom_write(addr, val); return; } /* Expansion 2 / POST: 0x1F802000..0x1F802FFF */ if (addr >= 0x1F802000u && addr <= 0x1F802FFFu) { return; } { /* open-bus (Beetle parity) */ g_io_openbus_writes++; return;; } } /* --- Read functions --- */ /* lockstep: 1 while a guest-direct memory op's REAL body runs, so nested ops * (device-emulation reads triggered by an MMIO write, etc.) are not recorded — * the shadow replay verifies writes without performing them, so it never sees * those device-internal accesses. Interrupt-handler ops are excluded too * (psx_get_in_exception): the replay runs only the block, never the handler. */ static int s_ls_op_active = 0; extern int g_ls_suppress_record; extern int g_dma_exec_depth; extern int psx_get_in_exception(void); static uint32_t psx_read_word_raw(uint32_t addr); /* data-shard capture feed (data_shards.c): record guest reads/writes while a * capture window is armed. Same exclusions as the lockstep hooks: never in * exception context (ISR work replays live), never under DMA (DMA writes * poison the window via ds_note_dma_write instead). */ uint32_t psx_read_word(uint32_t addr) { if (g_ls_mode == 2) return ls_read_hook(addr, 4, 0u); if (g_ds_recording && g_dma_exec_depth == 0 && !psx_get_in_exception()) ds_note_read(addr, 4); if (g_ls_mode != 1 || s_ls_op_active || g_ls_suppress_record || g_dma_exec_depth > 0) return psx_read_word_raw(addr); s_ls_op_active = 1; uint32_t v = psx_read_word_raw(addr); s_ls_op_active = 0; if (!psx_get_in_exception()) ls_read_hook(addr, 4, v); return v; } /* Physical address of a CPU/DMA main-RAM access. Fold KUSEG/KSEG0/KSEG1 first * (0x1FFFFFFF), then fold the 2nd-4th main-RAM mirrors: real hardware mirrors the * 2 MB DRAM across the WHOLE 0..0x7FFFFF physical window (Beetle libretro.cpp:874 * `A < 0x00800000` routes to main RAM; psx-spx "2048K RAM ... mirrored 4x"). A game * may legitimately place its stack at the top of that window — Tsumu Light computes * sp = (ramtop-8)|0x80000000 with its ramtop constant 0x00800000, giving sp=0x807FFFF8 * (top of the 4th mirror). Without this fold those accesses miss DRAM (`< RAM_SIZE` * fails) and hit open bus: stores drop, loads return 0, so the first saved return * address reads back 0 and `jr ra` derails to PC=0. Identity for phys < RAM_SIZE, so * normal-range RAM is byte-identical; MMIO/scratchpad/BIOS (>= 0x800000) untouched. */ static uint32_t psx_read_word_raw(uint32_t addr) { /* KSEG2 cache control — before physical translation. */ if (addr == 0xFFFE0130u) return cache_ctrl; /* KSEG2 (0xC0000000+): only cache control (0xFFFE0130, above where * applicable) exists there. Real hardware maps NOTHING else — Beetle * (cpu.cpp addr_mask[6..7]=0xFFFFFFFF) leaves KSEG2 addresses unmasked * so they fall to unmapped space and the access is a no-op. Our flat * 0x1FFFFFFF masking routed KSEG2 garbage onto LIVE registers: Tomba2's * attract runs a BIOS bzero over a wild 0xDF80xxxx pointer, which zeroed * the SIO/memctrl I/O block byte-by-byte and then hit the unmapped-MMIO * fatal (frame 9705). Ignore writes, read as 0, count for telemetry. */ if (addr >= 0xC0000000u) { g_kseg2_ignored_reads++; return 0; } uint32_t phys = psx_phys_addr(addr); if (phys < RAM_SIZE) { /* Host LE load — same bytes as the shift-or form; VLC/decode hot paths * issue millions of aligned main-RAM LWs per second. */ uint32_t v; memcpy(&v, &ram[phys], sizeof(v)); /* Targeted main-RAM read watch (debug). Flag is 0 in normal runs, so the * hot read path pays only a predictable branch. */ if (g_ram_read_watch_active) debug_server_trace_ram_read_watch(phys, v); return v; } { uint32_t off; if (mod_gpu_dma_memory_offset(phys, 4u, &off)) { uint32_t v; memcpy(&v, mod_gpu_dma_memory + off, sizeof(v)); return v; } } /* Expansion 1: 0x1F000000..0x1F7FFFFF — no device, open bus */ { uint32_t off; if (mod_memory_offset(phys, 4u, &off)) { uint32_t v; memcpy(&v, mod_memory + off, sizeof(v)); return v; } } if (phys >= 0x1F000000u && phys <= 0x1F7FFFFFu) { return 0xFFFFFFFFu; } if (phys >= 0x1F800000u && phys <= 0x1F8003FFu) { uint32_t off = phys - 0x1F800000u; return (uint32_t)scratchpad[off] | ((uint32_t)scratchpad[off + 1] << 8) | ((uint32_t)scratchpad[off + 2] << 16) | ((uint32_t)scratchpad[off + 3] << 24); } if (phys >= 0x1F801000u && phys <= 0x1F803FFFu) { return mmio_read32(phys); } if (phys >= 0x1FC00000u && phys <= 0x1FC7FFFFu) { uint32_t off = phys - 0x1FC00000u; return (uint32_t)bios_rom[off] | ((uint32_t)bios_rom[off + 1] << 8) | ((uint32_t)bios_rom[off + 2] << 16) | ((uint32_t)bios_rom[off + 3] << 24); } unmapped_fatal(addr, phys, "READ"); return 0; } /* ---- VSync callback-pointer provenance probe (MMX6 boot wedge) ------------- * The kernel VSync/RCnt callback-block pointer at phys 0x79D44 (KSEG0 0x80079D44) * is corrupted to 0x016F0110 at frame ~1188 by a write whose g_debug_last_store_pc * is STALE (so it is not a compiled/dirty-interp store — a DMA or runtime mem-op * with a wrong destination). This always-on ring records EVERY write to that word * at the unified RAM-write chokepoint, tagging CPU-vs-DMA via the dma.c exec flags, * so the real corrupting writer (and, if DMA, its channel + madr) is captured even * though the per-instruction store-PC tracker can't see it. Dump via `d44_ring`. */ #define D44_PHYS 0x00079D44u #define D44_RING_CAP 32u typedef struct { uint64_t seq; uint32_t val, old, store_pc; int32_t dma_depth, dma_ch; uint32_t dma_madr, dma_bcr, frame; } D44Entry; D44Entry g_d44_ring[D44_RING_CAP]; uint64_t g_d44_seq = 0; extern uint32_t g_debug_last_store_pc; extern int g_dma_exec_depth; /* >0 while a DMA is moving data (dma.c) */ extern int g_dma_cur_ch; /* channel of the in-flight DMA, else -1 */ extern uint32_t g_dma_cur_madr; /* current MADR of the in-flight DMA */ extern uint32_t g_dma_cur_bcr; /* BCR of the in-flight DMA */ extern uint64_t s_frame_count; static inline void d44_note(uint32_t phys, uint32_t old, uint32_t val) { if (phys != D44_PHYS) return; uint64_t i = g_d44_seq++; D44Entry *e = &g_d44_ring[i & (D44_RING_CAP - 1u)]; e->seq = i; e->val = val; e->old = old; e->store_pc = g_debug_last_store_pc; e->dma_depth = g_dma_exec_depth; e->dma_ch = g_dma_cur_ch; e->dma_madr = g_dma_cur_madr; e->dma_bcr = g_dma_cur_bcr; e->frame = (uint32_t)s_frame_count; } /* ---- Render-pass stores (render_pass.c, docs/RENDER_PASSES.md) ------------- * A render pass runs guest draw code in frozen time and afterwards restores * RAM, scratchpad, CPU, GPU and DMA state byte-for-byte. Its stores therefore * bypass every observer of the live timeline (code-page tracking, overlay * watch, write traces, card/parity hooks) and go straight to memory. MMIO is * an allow-list: the GPU (GP0, and GP1 DMA-direction / info queries), the GPU * and OTC DMA channels with DPCR/DICR, and I_STAT/I_MASK -- all of which the * pass restores. Everything else (SPU key-ons, CD, timers, SIO, MDEC, memory * control, other DMA channels) would escape the restore, so it is dropped and * counted instead. */ uint64_t g_render_pass_dropped_writes[RENDER_PASS_DROP_CLASSES]; /* SR.IsC (Isolate Cache, bit 16). While it is set, a CPU store goes to the * caches, never to the bus. This is Beetle PS_CPU::WriteMemory's IsC branch * (mednafen/psx/cpu.cpp:482-512), which runs before any address decode: * - I-cache enabled (BIU bit 11) with a tag-test, invalidate or lock mode * bit: the store rewrites the tag and valid bits of its line * (psx_icache_isc_store). FlushCache (A 44h) invalidates the I-cache this * way. * - I-cache enabled, no mode bit: Beetle writes an instruction word, which * the model does not hold. * - D-cache enabled and lock mode clear ((BIU & 0x81) == 0x80): the store * lands in the scratchpad, the D-cache array, at addr & 0x3FF, whatever * region addr is in. * Nothing else happens: an isolated store to RAM, MMIO or the BIU register * itself is dropped (Beetle routes 0xFFFE0130 through MemRW, libretro.cpp:1053, * which only non-isolated stores reach). DMA is not a CPU store; Beetle's DMA * writes RAM directly, so IsC does not apply while a transfer moves data. * Neither is a host store (psx_host_write_*: mods, FMV skip, debug pokes, * enhancement fills); like DMA it reaches memory whatever SR says. */ int g_host_store_depth; /* >0 inside psx_host_write_* */ static inline int cpu_store_isolated(void) { return sr_ptr && (*sr_ptr & 0x10000u) && g_dma_exec_depth == 0 && g_host_store_depth == 0; } static void isc_store(uint32_t addr, uint32_t val, uint32_t width) { /* The overlay shadow replay cannot redo this (lockstep.h). */ if (g_ls_mode == 1) ls_shadow_record_unreplayable(); psx_icache_isc_store(cache_ctrl, addr, val); if ((cache_ctrl & (PSX_BIU_DCACHE_ENABLE | PSX_BIU_LOCK)) == PSX_BIU_DCACHE_ENABLE) { uint32_t off = addr & 0x3FFu; #ifndef PSX_NO_DEBUG_TOOLS uint32_t old = 0; for (uint32_t i = 0; i < width; i++) old |= (uint32_t)scratchpad[(off + i) & 0x3FFu] << (8u * i); debug_server_trace_write_check(0x1F800000u + off, old, val, (uint8_t)width); #endif for (uint32_t i = 0; i < width; i++) scratchpad[(off + i) & 0x3FFu] = (uint8_t)(val >> (8u * i)); } } static void render_pass_mmio_write(uint32_t phys, uint32_t val, uint32_t width) { if (width == 4) mmio_write32(phys, val); else if (width == 2) mmio_write16(phys, (uint16_t)val); else mmio_write8(phys, (uint8_t)val); } /* The policy is render_pass_store_to (render_pass_plan.c, unit-tested by * render_pass_sandbox_test); this only wires it to memory.c's arrays. */ /* Mod arenas (Expansion-1 mod memory, the GPU-DMA aperture) are guest * memory a pass may draw into: a title that moves its primitive buffers there * (an extended draw distance) has its passes build their packets there. Each * arena page a pass writes is backed up on its first write and put back by * render_pass_mod_arenas_rollback(), with the rest of the pass restore. */ #define RP_MOD_PAGE 4096u #define RP_MOD_PAGES ((MOD_MEMORY_SIZE + PSX_MOD_GPU_DMA_APERTURE_SIZE) / RP_MOD_PAGE) typedef struct { uint8_t *page; uint32_t index; uint8_t data[RP_MOD_PAGE]; } RpModPage; static uint8_t s_rp_mod_bits[RP_MOD_PAGES / 8u]; static RpModPage *s_rp_mod_log = NULL; static uint32_t s_rp_mod_n = 0, s_rp_mod_cap = 0; static int s_rp_mod_lossy = 0; uint64_t g_render_pass_mod_pages = 0; /* pages journaled, total */ static int rp_mod_journal(uint8_t *base, uint32_t page_base, uint32_t off, uint32_t width) { for (uint32_t pg = off / RP_MOD_PAGE; pg <= (off + width - 1u) / RP_MOD_PAGE; pg++) { const uint32_t index = page_base + pg; const uint8_t bit = (uint8_t)(1u << (index & 7u)); if (s_rp_mod_bits[index >> 3] & bit) continue; if (s_rp_mod_n == s_rp_mod_cap) { uint32_t cap = s_rp_mod_cap ? s_rp_mod_cap * 2u : 64u; RpModPage *p = (RpModPage *)realloc(s_rp_mod_log, cap * sizeof *p); if (!p) { s_rp_mod_lossy = 1; return 0; } s_rp_mod_log = p; s_rp_mod_cap = cap; } s_rp_mod_bits[index >> 3] |= bit; s_rp_mod_log[s_rp_mod_n].page = base + pg * RP_MOD_PAGE; s_rp_mod_log[s_rp_mod_n].index = index; memcpy(s_rp_mod_log[s_rp_mod_n].data, base + pg * RP_MOD_PAGE, RP_MOD_PAGE); s_rp_mod_n++; g_render_pass_mod_pages++; } return 1; } /* 1 when the store landed in a mod arena (journaled and written). */ static int render_pass_mod_store(uint32_t addr, uint32_t val, uint32_t width) { uint32_t phys, off; uint8_t *base = NULL; uint32_t page_base = 0; if (addr >= 0xC0000000u) return 0; phys = addr & 0x1FFFFFFFu; if (phys < psx_ram_live_bytes()) return 0; if (phys < 0x00800000u && psx_ram_live_bytes() < 0x00800000u) return 0; /* mirrors */ if (mod_gpu_dma_memory_offset(phys, width, &off)) { base = mod_gpu_dma_memory; page_base = MOD_MEMORY_SIZE / RP_MOD_PAGE; } else if (mod_memory_offset(phys, width, &off)) { base = mod_memory; } else { return 0; } if (!rp_mod_journal(base, page_base, off, width)) return 1; /* lossy: drop */ for (uint32_t i = 0; i < width; i++) base[off + i] = (uint8_t)(val >> (8u * i)); return 1; } void render_pass_mod_arenas_rollback(void) { for (uint32_t i = s_rp_mod_n; i-- > 0;) { memcpy(s_rp_mod_log[i].page, s_rp_mod_log[i].data, RP_MOD_PAGE); s_rp_mod_bits[s_rp_mod_log[i].index >> 3] &= (uint8_t)~(1u << (s_rp_mod_log[i].index & 7u)); } s_rp_mod_n = 0; s_rp_mod_lossy = 0; } /* FNV-1a over the used part of both arenas (PSX_RENDER_PASS_VERIFY). */ uint64_t render_pass_mod_arenas_hash(void) { uint64_t h = 1469598103934665603ULL; for (uint32_t i = 0; i < mod_memory_used; i++) h = (h ^ mod_memory[i]) * 1099511628211ULL; for (uint32_t i = 0; i < mod_gpu_dma_memory_used; i++) h = (h ^ mod_gpu_dma_memory[i]) * 1099511628211ULL; return h; } static void render_pass_store(uint32_t addr, uint32_t val, uint32_t width) { RenderPassStoreTarget t; int cls; if (render_pass_mod_store(addr, val, width)) return; t.ram = ram; t.ram_size = psx_ram_live_bytes(); /* live geometry, as psx_ram_map_write */ t.scratchpad = scratchpad; t.scratchpad_size = SCRATCHPAD_SIZE; t.isolate_cache = cpu_store_isolated(); /* Same cache effect as outside a pass; the pass checkpoint restores the * I-cache tags and the scratchpad when it ends. */ if (t.isolate_cache) isc_store(addr, val, width); t.mmio_write = render_pass_mmio_write; cls = render_pass_store_to(&t, addr, val, width); if (cls >= 0) g_render_pass_dropped_writes[cls]++; } static void psx_write_word_raw(uint32_t addr, uint32_t val); void psx_write_word(uint32_t addr, uint32_t val) { extern void (*g_overlay_flush_pending_cycles)(void); if (g_overlay_flush_pending_cycles) g_overlay_flush_pending_cycles(); if (g_psx_render_pass_active) { render_pass_store(addr, val, 4); return; } if (g_ls_mode == 2) { ls_write_hook(addr, 4, val); return; } if (g_ds_recording) { if (g_dma_exec_depth > 0) ds_note_dma_write(); else if (!psx_get_in_exception()) ds_note_write(addr, 4); } if (g_ls_mode != 1 || s_ls_op_active || g_ls_suppress_record || g_dma_exec_depth > 0) { psx_write_word_raw(addr, val); return; } if (!psx_get_in_exception()) ls_write_hook(addr, 4, val); s_ls_op_active = 1; psx_write_word_raw(addr, val); s_ls_op_active = 0; } static void psx_write_word_raw(uint32_t addr, uint32_t val) { g_guest_store_count++; /* (pgxp) plain-store shadow invalidation retired: the PGXP engine * validates tracked words against the actual packet word on read, so an * overwritten word can never be believed (docs/ENHANCEMENTS.md G1). */ /* IsC first, before any decode (cpu_store_isolated). */ if (cpu_store_isolated()) { isc_store(addr, val, 4); return; } /* KSEG2 cache control — before physical translation. */ if (addr == 0xFFFE0130u) { cache_ctrl = val; return; } /* KSEG2 guard — see psx_read_word_raw. */ if (addr >= 0xC0000000u) { g_kseg2_ignored_writes++; return; } uint32_t phys = psx_phys_addr_store(addr); /* The generated BIOS mirrors its exception trampoline to 0x80000000 during * boot. On hardware this mirror copy is not visible in RAM; only the real * exception vector at 0x80000080 is. Tomba 2 later passes buffer=0 to the * BIOS card write routine, so stale mirror bytes at 0 corrupt the sector 63 * management write and leave the load menu stuck checking the card. * Keys for relocated SCPH-1001 code go through scph1001_relocated_store() * (runtime PC, ROM address); Kernel Part 2 runs at 0x500 from ROM * 0xBFC10000. */ if (fntrace_is_game_started() && phys < 0x10u && scph1001_relocated_store(0x00000F00u, 0xBFC10A00u)) return; /* BIOS helpers use RAM address zero as a tiny delay-loop scratch between * device-register polls. Treat these two dummy stores as non-visible; real * hardware / Beetle preserve Tomba 2's sector-63 card-management payload * when it passes buffer=0 to _card_write. */ if (fntrace_is_game_started() && phys == 0u) { switch (g_debug_last_store_pc) { case 0xBFC04E90u: /* ROM, runs in place */ case 0xBFC04EF0u: case 0xBFC05164u: case 0xBFC0D634u: case 0x80012434u: case 0x800125ACu: return; default: break; } /* SCPH-1001 shell, which runs relocated at 0x80030000. */ if (scph1001_relocated_store(0x80056EB4u, 0xBFC3EEB4u) || scph1001_relocated_store(0x800585E4u, 0xBFC405E4u) || scph1001_relocated_store(0x80058788u, 0xBFC40788u) || scph1001_relocated_store(0x80059C50u, 0xBFC41C50u)) return; } if (phys < RAM_SIZE) { /* Tomba 2 load-game card check: the BIOS card-manager cleanup helper * at kernel RAM 0x1C5C scans MARK events and re-arms any READY event * matching F0000011/{4,8000,100,200,2000}. In the recomp timing path it * can run after the card completion DeliverEvent but before the game's * TestEvent poll, consuming the public card event and leaving the UI * stuck at "Checking MEMORY CARD...". Let the real TestEvent consume * public MARK card events instead; keep this narrowly keyed to the * helper's status store and the EvCB layout. * * Ape Escape LOAD: this suppress is tip-only (post-483a0d4) and leaves * libcard parked after the 81 52 00 probe (A6C10 nested, B4E38=0, never * re-enables I_MASK bit7 / never TX 0x57). Opt out unless explicitly * enabled — Tomba can set PSX_TOMB_CARD_EVCB_PROTECT=1. */ { static int s_tomb_evcb_protect = -1; if (s_tomb_evcb_protect < 0) { const char *e = getenv("PSX_TOMB_CARD_EVCB_PROTECT"); s_tomb_evcb_protect = (e && e[0] == '1') ? 1 : 0; } if (s_tomb_evcb_protect && fntrace_is_game_started() && scph1001_relocated_store(0x00001CE4u, 0xBFC117E4u) && /* kernel */ val == 0x2000u && phys >= 4u && (phys + 8u) < RAM_SIZE && read_ram_word(phys) == 0x4000u && read_ram_word(phys - 4u) == 0xF0000011u && read_ram_word(phys + 8u) == 0x2000u) { uint32_t spec = read_ram_word(phys + 4u); if (spec == 0x00000004u || spec == 0x00008000u || spec == 0x00000100u || spec == 0x00000200u || spec == 0x00002000u) { return; } } } if (phys == D44_PHYS) d44_note(phys, read_ram_word(phys), val); #ifndef PSX_NO_DEBUG_TOOLS debug_server_trace_write_check(phys, read_ram_word(phys), val, 4); #endif parity_trace_note_write(phys, 4, effective_store_pc()); card_data_writes_check(phys, val, 4); dirty_ram_mark_kernel_write(phys); text_guard_note_write(phys, val, 4); overlay_watch_note_write(phys, 4); #ifdef PSX_COSIM { extern void cosim_note_ram_write(uint32_t,uint32_t); cosim_note_ram_write(phys, 4); } #endif ram[phys] = (uint8_t)(val); ram[phys + 1] = (uint8_t)(val >> 8); ram[phys + 2] = (uint8_t)(val >> 16); ram[phys + 3] = (uint8_t)(val >> 24); return; } { uint32_t off; if (mod_gpu_dma_memory_offset(phys, 4u, &off)) { memcpy(mod_gpu_dma_memory + off, &val, sizeof(val)); return; } } /* Expansion 1: 0x1F000000..0x1F7FFFFF — ignore writes */ { uint32_t off; if (mod_memory_offset(phys, 4u, &off)) { memcpy(mod_memory + off, &val, sizeof(val)); return; } } if (phys >= 0x1F000000u && phys <= 0x1F7FFFFFu) return; if (phys >= 0x1F800000u && phys <= 0x1F8003FFu) { uint32_t off = phys - 0x1F800000u; #ifndef PSX_NO_DEBUG_TOOLS debug_server_trace_write_check(phys, (uint32_t)scratchpad[off] | ((uint32_t)scratchpad[off + 1] << 8) | ((uint32_t)scratchpad[off + 2] << 16) | ((uint32_t)scratchpad[off + 3] << 24), val, 4); #endif scratchpad[off] = (uint8_t)(val); scratchpad[off + 1] = (uint8_t)(val >> 8); scratchpad[off + 2] = (uint8_t)(val >> 16); scratchpad[off + 3] = (uint8_t)(val >> 24); return; } if (phys >= 0x1F801000u && phys <= 0x1F803FFFu) { mmio_write32(phys, val); return; } if (phys >= 0x1FC00000u && phys <= 0x1FC7FFFFu) { /* ROM: silently ignore writes */ return; } unmapped_fatal(addr, phys, "WRITE"); } static uint16_t psx_read_half_raw(uint32_t addr); uint16_t psx_read_half(uint32_t addr) { if (g_ls_mode == 2) return (uint16_t)ls_read_hook(addr, 2, 0u); if (g_ds_recording && g_dma_exec_depth == 0 && !psx_get_in_exception()) ds_note_read(addr, 2); if (g_ls_mode != 1 || s_ls_op_active || g_ls_suppress_record || g_dma_exec_depth > 0) return psx_read_half_raw(addr); s_ls_op_active = 1; uint16_t v = psx_read_half_raw(addr); s_ls_op_active = 0; if (!psx_get_in_exception()) ls_read_hook(addr, 2, v); return v; } static uint16_t psx_read_half_raw(uint32_t addr) { /* KSEG2 guard — see psx_read_word_raw. */ if (addr >= 0xC0000000u) { g_kseg2_ignored_reads++; return 0; } uint32_t phys = psx_phys_addr(addr); if (phys < RAM_SIZE) { return (uint16_t)ram[phys] | ((uint16_t)ram[phys + 1] << 8); } { uint32_t off; if (mod_gpu_dma_memory_offset(phys, 2u, &off)) return (uint16_t)mod_gpu_dma_memory[off] | ((uint16_t)mod_gpu_dma_memory[off + 1u] << 8); } { uint32_t off; if (mod_memory_offset(phys, 2u, &off)) return (uint16_t)mod_memory[off] | ((uint16_t)mod_memory[off + 1u] << 8); } if (phys >= 0x1F000000u && phys <= 0x1F7FFFFFu) return 0xFFFFu; if (phys >= 0x1F800000u && phys <= 0x1F8003FFu) { uint32_t off = phys - 0x1F800000u; return (uint16_t)scratchpad[off] | ((uint16_t)scratchpad[off + 1] << 8); } if (phys >= 0x1F801000u && phys <= 0x1F803FFFu) { return mmio_read16(phys); } if (phys >= 0x1FC00000u && phys <= 0x1FC7FFFFu) { uint32_t off = phys - 0x1FC00000u; return (uint16_t)bios_rom[off] | ((uint16_t)bios_rom[off + 1] << 8); } unmapped_fatal(addr, phys, "READ"); return 0; } static void psx_write_half_raw(uint32_t addr, uint16_t val); void psx_write_half(uint32_t addr, uint16_t val) { extern void (*g_overlay_flush_pending_cycles)(void); if (g_overlay_flush_pending_cycles) g_overlay_flush_pending_cycles(); if (g_psx_render_pass_active) { render_pass_store(addr, val, 2); return; } if (g_ls_mode == 2) { ls_write_hook(addr, 2, val); return; } if (g_ds_recording) { if (g_dma_exec_depth > 0) ds_note_dma_write(); else if (!psx_get_in_exception()) ds_note_write(addr, 2); } if (g_ls_mode != 1 || s_ls_op_active || g_ls_suppress_record || g_dma_exec_depth > 0) { psx_write_half_raw(addr, val); return; } if (!psx_get_in_exception()) ls_write_hook(addr, 2, val); s_ls_op_active = 1; psx_write_half_raw(addr, val); s_ls_op_active = 0; } static void psx_write_half_raw(uint32_t addr, uint16_t val) { g_guest_store_count++; if (cpu_store_isolated()) { isc_store(addr, val, 2); return; } /* KSEG2 guard — see psx_read_word_raw. */ if (addr >= 0xC0000000u) { g_kseg2_ignored_writes++; return; } uint32_t phys = psx_phys_addr_store(addr); if (phys < RAM_SIZE) { #ifndef PSX_NO_DEBUG_TOOLS debug_server_trace_write_check(phys, (uint32_t)read_ram_half(phys), (uint32_t)val, 2); #endif parity_trace_note_write(phys, 2, effective_store_pc()); card_data_writes_check(phys, (uint32_t)val, 2); dirty_ram_mark_kernel_write(phys); text_guard_note_write(phys, (uint32_t)val, 2); overlay_watch_note_write(phys, 2); #ifdef PSX_COSIM { extern void cosim_note_ram_write(uint32_t,uint32_t); cosim_note_ram_write(phys, 2); } #endif ram[phys] = (uint8_t)(val); ram[phys + 1] = (uint8_t)(val >> 8); return; } { uint32_t off; if (mod_gpu_dma_memory_offset(phys, 2u, &off)) { mod_gpu_dma_memory[off] = (uint8_t)val; mod_gpu_dma_memory[off + 1u] = (uint8_t)(val >> 8); return; } } { uint32_t off; if (mod_memory_offset(phys, 2u, &off)) { mod_memory[off] = (uint8_t)val; mod_memory[off + 1u] = (uint8_t)(val >> 8); return; } } if (phys >= 0x1F000000u && phys <= 0x1F7FFFFFu) return; if (phys >= 0x1F800000u && phys <= 0x1F8003FFu) { uint32_t off = phys - 0x1F800000u; #ifndef PSX_NO_DEBUG_TOOLS debug_server_trace_write_check(phys, (uint32_t)scratchpad[off] | ((uint32_t)scratchpad[off + 1] << 8), (uint32_t)val, 2); #endif scratchpad[off] = (uint8_t)(val); scratchpad[off + 1] = (uint8_t)(val >> 8); return; } if (phys >= 0x1F801000u && phys <= 0x1F803FFFu) { mmio_write16(phys, val); return; } if (phys >= 0x1FC00000u && phys <= 0x1FC7FFFFu) { return; /* ROM: ignore */ } unmapped_fatal(addr, phys, "WRITE"); } static uint8_t psx_read_byte_raw(uint32_t addr); uint8_t psx_read_byte(uint32_t addr) { if (g_ls_mode == 2) return (uint8_t)ls_read_hook(addr, 1, 0u); if (g_ls_mode != 1 || s_ls_op_active || g_ls_suppress_record || g_dma_exec_depth > 0) return psx_read_byte_raw(addr); s_ls_op_active = 1; uint8_t v = psx_read_byte_raw(addr); s_ls_op_active = 0; if (!psx_get_in_exception()) ls_read_hook(addr, 1, v); return v; } static uint8_t psx_read_byte_raw(uint32_t addr) { /* KSEG2 guard — see psx_read_word_raw. */ if (addr >= 0xC0000000u) { g_kseg2_ignored_reads++; return 0; } uint32_t phys = psx_phys_addr(addr); if (phys < RAM_SIZE) { return ram[phys]; } { uint32_t off; if (mod_gpu_dma_memory_offset(phys, 1u, &off)) return mod_gpu_dma_memory[off]; } { uint32_t off; if (mod_memory_offset(phys, 1u, &off)) return mod_memory[off]; } if (phys >= 0x1F000000u && phys <= 0x1F7FFFFFu) return 0xFFu; if (phys >= 0x1F800000u && phys <= 0x1F8003FFu) { return scratchpad[phys - 0x1F800000u]; } if (phys >= 0x1F801000u && phys <= 0x1F803FFFu) { return mmio_read8(phys); } if (phys >= 0x1FC00000u && phys <= 0x1FC7FFFFu) { return bios_rom[phys - 0x1FC00000u]; } unmapped_fatal(addr, phys, "READ"); return 0; } /* ---- CPU guest-side data loads: faithful R3000A load-delay pipeline interlock ---- * The R3000A has no usable D-cache (it is repurposed as the 1 KB scratchpad), so a * CPU data load from main DRAM stalls the pipeline. Beetle (cpu.cpp ReadMemory, * 364-451) models this as: a +2 "fudge" iff the predecessor committed no load, the * region wait (main RAM = +3, libretro.cpp:884), and a completion cost (+2 CPU / * +1 LWC2); the (region+completion) becomes a per-register LDAbsorb "give-back" that * following instructions consume instead of their own +1 base (pipeline write-back * overlap). The §1 base + GPR_DEPRES + DO_LDS that bracket this run in psx_cyc.h. * * These functions own the WHOLE per-instruction interlock for a CPU load (they call * psx_cyc_base/deps/lds), so the emitters/interp invoke them in place of the prior * cpu->read_* call and emit NO separate psx_cyc_step for the load. They return the * raw value via the UNCHARGED psx_read_* (cpu->read_* is now uncharged too — the * data-access cost is charged exactly once, here). Keyed on the runtime effective * physical address (KUSEG/KSEG0/KSEG1 alias the same DRAM). * * DMACycleSteal residual: Beetle adds the (dynamic) DMACycleSteal to EVERY read * (libretro.cpp:868-869). That is non-zero only while a DMA channel is actively * stealing the bus; modeling it needs the live steal count threaded out of the DMA * controller, and it can't be isolated by a static ruler. It remains an unmodeled * dynamic axis; the per-region device waits below are the static, validatable piece. */ /* Runtime-only production cycle charge for data-load timing. Overlay DLLs * flush their local pending-cycle accumulator before entering these host * helpers, so this stays on the host side of that ABI boundary. */ #if defined(PSX_NO_DEBUG_TOOLS) && !defined(PSX_COSIM) && !STARVATION_RING_ENABLED extern uint64_t g_psx_cycle_fast_limit; extern int g_event_step_conservative; extern int g_ls_replay_active; static inline void psx_load_charge_cycles(uint32_t cycles) { if (g_ls_replay_active || cycles == 0u) return; uint64_t next = psx_cycle_count + (uint64_t)cycles; if (!g_event_step_conservative && g_psx_cycle_fast_limit != 0u && next >= psx_cycle_count && next <= g_psx_cycle_fast_limit) { psx_cycle_count = next; return; } psx_advance_cycles(cycles); } #else static inline void psx_load_charge_cycles(uint32_t cycles) { psx_advance_cycles(cycles); } #endif /* Beetle MemRW device-region READ wait (libretro.cpp:859-1131), the device-dependent * part of a load's access cost (added to the timestamp before the +completion). `size` * is the access width in bytes (1/2/4) — the SPU and CDC waits are width-dependent. * Reads only; writes are posted (~free) in MemRW. phys is the masked physical address. */ static inline uint32_t psx_mmio_read_wait(uint32_t phys, uint32_t size) { /* Main RAM, mirrored across phys 0..0x7FFFFF (libretro.cpp:874 `A < 0x00800000`). */ if (phys < 0x00800000u) return 3u; /* BIOS ROM (905) and Expansion 1 / PIO (1134): no extra device wait. */ if (phys >= 0x1FC00000u && phys <= 0x1FC7FFFFu) return 0u; if (phys >= 0x1F000000u && phys <= 0x1F7FFFFFu) return 0u; /* Hardware MMIO window 0x1F801000..0x1F802FFF (921). */ if (phys >= 0x1F801000u && phys <= 0x1F802FFFu) { /* Bisect gate (PSX_MMIO_WAIT=0): disable the device-region read waits * (the 9ae534d feature) to test whether they move the MMX6 cutscene * ordering. Read once. */ static int s_mw = -1; if (s_mw < 0) { const char* e = getenv("PSX_MMIO_WAIT"); s_mw = (e && e[0] == '0') ? 0 : 1; } if (!s_mw) return 0u; if (phys >= 0x1F801C00u && phys <= 0x1F801FFFu) /* SPU (929) */ return (size == 4u) ? 36u : 16u; if (phys >= 0x1F801800u && phys <= 0x1F80180Fu) /* CDC (979) */ return 6u * size; if (phys >= 0x1F801810u && phys <= 0x1F801817u) return 1u; /* GPU (994) */ if (phys >= 0x1F801820u && phys <= 0x1F801827u) return 1u; /* MDEC (1007) */ if (phys >= 0x1F801000u && phys <= 0x1F801023u) return 1u; /* SysControl (1020) */ if (phys >= 0x1F801040u && phys <= 0x1F80104Fu) return 1u; /* FrontIO/pad (1043) */ if (phys >= 0x1F801050u && phys <= 0x1F80105Fu) return 1u; /* SIO (1055) */ if (phys >= 0x1F801070u && phys <= 0x1F801077u) return 1u; /* IRQ (1094) */ if (phys >= 0x1F801080u && phys <= 0x1F8010FFu) return 1u; /* DMA (1106) */ if (phys >= 0x1F801100u && phys <= 0x1F80113Fu) return 1u; /* Timers (1119) */ return 0u; /* unmatched MMIO: Beetle adds no device wait */ } return 0u; /* unknown / open-bus region */ } /* Beetle ReadMemory data-access timing (cpu.cpp:369-448), after §1/deps/DO_LDS. * compl_cost = 2 (CPU load) / 1 (LWC2); arm_rt = GPR to arm as pending load, or * 0x20 = none (LWC2, dest is a GTE reg). size = access width in bytes (1/2/4). */ static inline void psx_cyc_readmem(CPUState* cpu, uint32_t phys, uint32_t size, uint32_t compl_cost, uint32_t arm_rt) { /* ReadMemory start (369-370): clear the current give-back slot. */ cpu->read_absorb[cpu->read_absorb_which] = 0u; cpu->read_absorb_which = 0u; /* Scratchpad (the D-cache): no wait, no give-back (414-422). */ if (phys >= 0x1F800000u && phys <= 0x1F8003FFu) { cpu->ld_absorb = 0u; cpu->ld_which_t = (uint8_t)arm_rt; return; } /* fudge (424): +2 iff the predecessor committed no load (read_fudge==0x20). * Combined with region+completion into one advance — deadline catch-up * replays exact event boundaries, so splitting the charge is only host cost. */ uint32_t region = psx_mmio_read_wait(phys, size); /* device-region wait */ uint32_t cost = region + compl_cost; /* LDAbsorb = region + completion */ uint32_t fudge = (uint32_t)((cpu->read_fudge >> 4) & 2u); cpu->ld_absorb = cost; psx_advance_cycles(fudge + cost); cpu->ld_which_t = (uint8_t)arm_rt; /* PROOF GATE (PSX_POLL_PROOF=N, default 0/off): a FLAT, non-absorbed extra N * cycles per main-RAM data read — replicates the historical "+6 cyc/main-RAM * read" fix (mmx6_memcard_invalid_rootcause) that lengthened MMX6's card * busy-poll so it outlasts the VBlank-paced async card op. Unlike the * region+completion above (which arms ld_absorb and gets given back in a * tight loop), this is pure added cost. If setting this makes the MMX6 save * load, the card regression is confirmed as poll-vs-op timing. TEMPORARY — * the faithful fix models the uncached data-read cost properly. */ if (phys < 0x00800000u) { static int s_pp = -1; if (s_pp < 0) { const char* e = getenv("PSX_POLL_PROOF"); s_pp = (e && e[0]) ? atoi(e) : 0; } if (s_pp > 0) psx_load_charge_cycles((uint32_t)s_pp); } } /* Resolve PSX_LOAD_DELAY once (shared with inlined psx_cyc.h helpers). */ int psx_load_delay_enabled(void) { if (g_psx_load_delay < 0) { const char* e = getenv("PSX_LOAD_DELAY"); g_psx_load_delay = (e && e[0] == '0') ? 0 : 1; } return g_psx_load_delay; } /* The interlock half of a load (§1+deps+(cancel)+DO_LDS+ReadMemory). Gated on * PSX_ENABLE_BLOCK_CYCLES so the Beetle-oracle build (cycles off) does a plain read. */ static inline void psx_cyc_load_timing(CPUState* cpu, uint32_t addr, uint32_t size, uint32_t rt, uint32_t reg_mask) { #ifdef PSX_ENABLE_BLOCK_CYCLES /* Bisect gate (PSX_LOAD_DELAY=0): disable the R3000A load-delay interlock * timing (the d8c4a8e/fade560/d597797 feature) to test whether it moves the * MMX6 cutscene ordering. Read once; default on. */ if (!psx_load_delay_enabled()) { (void)addr; (void)size; (void)rt; (void)reg_mask; return; } psx_cyc_base(cpu); psx_cyc_deps(cpu, reg_mask); if (cpu->ld_which_t == rt) cpu->ld_which_t = 0u; /* cancel pending load to same dest */ psx_cyc_lds(cpu); psx_cyc_readmem(cpu, addr & 0x1FFFFFFFu, size, 2u, rt); #else (void)cpu; (void)addr; (void)size; (void)rt; (void)reg_mask; #endif } /* Production value-read fast path for the overwhelmingly common main-RAM * load. Timing has already run before this helper is consulted, so a device * deadline (and any DMA it services) remains ordered before the value read. * * Keep this host-local: generated overlay DLLs flush their pending cycles * before entering psx_cyc_load_* and must not depend on runtime globals. Every * mode with observable read-side instrumentation falls back to psx_read_*. * The physical-range test is done before the 2 MiB mirror fold so MMIO, BIOS, * scratchpad, KSEG2/cache-control and open-bus behavior remain canonical. */ #if defined(PSX_NO_DEBUG_TOOLS) && !defined(PSX_COSIM) static inline int psx_cyc_main_ram_fast_addr(uint32_t addr, uint32_t width, uint32_t *phys_out) { if (g_ls_mode != 0 || g_ls_replay_active || g_ds_recording || g_ram_read_watch_active || g_dma_exec_depth > 0 || addr >= 0xC0000000u) return 0; uint32_t phys = addr & 0x1FFFFFFFu; if (phys >= 0x00800000u) return 0; { /* One live-mask load (psx_memory.h); size = mask + 1. */ const uint32_t mask = g_psx_ram_mask; phys &= mask; /* Aligned guest loads cannot cross this boundary, but fail closed for * a malformed/unaligned caller instead of introducing a host OOB read. */ if (phys > mask + 1u - width) return 0; } *phys_out = phys; return 1; } #endif /* Slow paths for the inlined helpers in psx_cyc.h (MMIO / lockstep / shards). */ uint32_t psx_cyc_load_word_slow(CPUState* cpu, uint32_t addr, uint32_t rt, uint32_t reg_mask) { psx_cyc_load_timing(cpu, addr, 4u, rt, reg_mask); return psx_read_word(addr); } uint16_t psx_cyc_load_half_slow(CPUState* cpu, uint32_t addr, uint32_t rt, uint32_t reg_mask) { psx_cyc_load_timing(cpu, addr, 2u, rt, reg_mask); return psx_read_half(addr); } void psx_cyc_load_word_timing_only(CPUState* cpu, uint32_t addr, uint32_t rt, uint32_t reg_mask) { psx_cyc_load_timing(cpu, addr, 4u, rt, reg_mask); } uint8_t psx_cyc_load_byte(CPUState* cpu, uint32_t addr, uint32_t rt, uint32_t reg_mask) { psx_cyc_load_timing(cpu, addr, 1u, rt, reg_mask); #if defined(PSX_NO_DEBUG_TOOLS) && !defined(PSX_COSIM) uint32_t phys; if (psx_cyc_main_ram_fast_addr(addr, 1u, &phys)) return ram[phys]; #endif return psx_read_byte(addr); } /* LWC2 (GTE load): §1/DO_LDS done by psx_cyc_step(cpu,0); the GTE deadline stall by * psx_gte_stall — both emitted before this call. 32-bit access, completion +1, no * LDWhich arm. */ uint32_t psx_cyc_lwc2_read(CPUState* cpu, uint32_t addr) { #ifdef PSX_ENABLE_BLOCK_CYCLES psx_cyc_readmem(cpu, addr & 0x1FFFFFFFu, 4u, 1u, 0x20u); #else (void)cpu; #endif #if defined(PSX_NO_DEBUG_TOOLS) && !defined(PSX_COSIM) uint32_t phys; if (psx_cyc_main_ram_fast_addr(addr, 4u, &phys)) { return (uint32_t)ram[phys] | ((uint32_t)ram[phys + 1] << 8) | ((uint32_t)ram[phys + 2] << 16) | ((uint32_t)ram[phys + 3] << 24); } #endif return psx_read_word(addr); } /* Deprecated uncharged passthroughs (the +4 flat wait-state model is gone; load * timing now lives in psx_cyc_load_*). Kept so any stray reference stays valid. */ uint32_t psx_guest_read_word(uint32_t addr) { return psx_read_word(addr); } uint16_t psx_guest_read_half(uint32_t addr) { return psx_read_half(addr); } uint8_t psx_guest_read_byte(uint32_t addr) { return psx_read_byte(addr); } static void psx_write_byte_raw(uint32_t addr, uint8_t val); void psx_write_byte(uint32_t addr, uint8_t val) { extern void (*g_overlay_flush_pending_cycles)(void); if (g_overlay_flush_pending_cycles) g_overlay_flush_pending_cycles(); if (g_psx_render_pass_active) { render_pass_store(addr, val, 1); return; } if (g_ls_mode == 2) { ls_write_hook(addr, 1, val); return; } if (g_ls_mode != 1 || s_ls_op_active || g_ls_suppress_record || g_dma_exec_depth > 0) { psx_write_byte_raw(addr, val); return; } if (!psx_get_in_exception()) ls_write_hook(addr, 1, val); s_ls_op_active = 1; psx_write_byte_raw(addr, val); s_ls_op_active = 0; } /* Host stores: the same paths as a guest store, but never cache-isolated * (cpu_store_isolated). */ void psx_host_write_word(uint32_t addr, uint32_t val) { g_host_store_depth++; psx_write_word(addr, val); g_host_store_depth--; } void psx_host_write_half(uint32_t addr, uint16_t val) { g_host_store_depth++; psx_write_half(addr, val); g_host_store_depth--; } void psx_host_write_byte(uint32_t addr, uint8_t val) { g_host_store_depth++; psx_write_byte(addr, val); g_host_store_depth--; } static void psx_write_byte_raw(uint32_t addr, uint8_t val) { g_guest_store_count++; if (cpu_store_isolated()) { isc_store(addr, val, 1); return; } /* KSEG2 guard — see psx_read_word_raw. */ if (addr >= 0xC0000000u) { g_kseg2_ignored_writes++; return; } uint32_t phys = psx_phys_addr_store(addr); if (phys < RAM_SIZE) { #ifndef PSX_NO_DEBUG_TOOLS debug_server_trace_write_check(phys, (uint32_t)ram[phys], (uint32_t)val, 1); #endif parity_trace_note_write(phys, 1, effective_store_pc()); card_data_writes_check(phys, (uint32_t)val, 1); dirty_ram_mark_kernel_write(phys); text_guard_note_write(phys, (uint32_t)val, 1); overlay_watch_note_write(phys, 1); #ifdef PSX_COSIM { extern void cosim_note_ram_write(uint32_t,uint32_t); cosim_note_ram_write(phys, 1); } #endif ram[phys] = val; return; } { uint32_t off; if (mod_gpu_dma_memory_offset(phys, 1u, &off)) { mod_gpu_dma_memory[off] = val; return; } } { uint32_t off; if (mod_memory_offset(phys, 1u, &off)) { mod_memory[off] = val; return; } } if (phys >= 0x1F000000u && phys <= 0x1F7FFFFFu) return; if (phys >= 0x1F800000u && phys <= 0x1F8003FFu) { #ifndef PSX_NO_DEBUG_TOOLS debug_server_trace_write_check(phys, (uint32_t)scratchpad[phys - 0x1F800000u], (uint32_t)val, 1); #endif scratchpad[phys - 0x1F800000u] = val; return; } if (phys >= 0x1F801000u && phys <= 0x1F803FFFu) { mmio_write8(phys, val); return; } if (phys >= 0x1FC00000u && phys <= 0x1FC7FFFFu) { return; /* ROM: ignore */ } unmapped_fatal(addr, phys, "WRITE"); }