From 478e2352c9ef1d80e58df7eaf614f6df324d7428 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Sun, 7 Jun 2026 15:11:06 -0700 Subject: [PATCH] overlay: Inc1-D registration-lifetime write-invalidation (fixes OV-1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A registered overlay function is only callable while the RAM it was compiled from is unchanged. Adds coarse, region-level write-invalidation: - overlay_loader: each loaded DLL gets a region record {base,size,generation, valid}; dyntab entries carry their region index. dyntab_lookup returns a function only if its region is still valid (else counts stale_blocked and falls through). Invalidation is O(1) (flip valid=0) — no open-addressing deletion. DLL stays loaded; only callability is revoked (Compiled != Registered != Callable). - memory.c: a watched-overlay-pages bitmap, set when a region registers; one bitmap test added to the RAM-store branch of psx_write_{word,half,byte} (the single audited store chokepoint — all CPU + DMA writes funnel here). A write into a watched page calls overlay_loader_invalidate_at(), which flips the region invalid and stops watching it. - Counters via overlay_loader_status: loads, invalidations, unregistered_funcs, dispatch_native, dispatch_interp_fallback, stale_blocked, last_write_{pc, addr,size}, regions. Coarse by design: ANY write into a registered region invalidates the WHOLE region (over-invalidation is safe — it falls back to the interpreter, which self-heals). Per-function/dependency-range narrowing waits for the Inc2 manifest. After invalidation the region is not auto-reloaded this session (re-registering a CRC-matched cache is also Inc2); the new content runs interpreted, which is correct. Co-Authored-By: Claude Opus 4.8 --- runtime/include/overlay_loader.h | 16 ++++ runtime/src/debug_server.c | 16 ++++ runtime/src/memory.c | 42 +++++++++ runtime/src/overlay_loader.c | 143 ++++++++++++++++++++++++++++--- 4 files changed, 207 insertions(+), 10 deletions(-) diff --git a/runtime/include/overlay_loader.h b/runtime/include/overlay_loader.h index 2f9734d8..043ad28c 100644 --- a/runtime/include/overlay_loader.h +++ b/runtime/include/overlay_loader.h @@ -51,6 +51,22 @@ void overlay_loader_get_status(int *active, int *registered, * the overlay_loader_status TCP command — no stderr logging (Rule 3). */ const char *overlay_loader_last_msg(void); +/* Inc1-D: invalidate the registered overlay region containing `phys`. Called + * from the psx_write_* store path (memory.c) when a write lands on a watched + * overlay page — the region's registered functions stop being callable and the + * new RAM content falls back to the interpreter. */ +void overlay_loader_invalidate_at(uint32_t phys, uint32_t size); + +/* Inc1-D counters, surfaced via overlay_loader_status. */ +void overlay_loader_get_counters(uint32_t *loads, uint32_t *invalidations, + uint32_t *unregistered, + uint64_t *disp_native, uint64_t *disp_interp, + uint64_t *stale_blocked, + uint32_t *last_write_pc, + uint32_t *last_write_addr, + uint32_t *last_write_size, + int *regions); + #ifdef __cplusplus } #endif diff --git a/runtime/src/debug_server.c b/runtime/src/debug_server.c index 0b6d3730..78824f66 100644 --- a/runtime/src/debug_server.c +++ b/runtime/src/debug_server.c @@ -7293,6 +7293,22 @@ static void handle_overlay_loader_status(int id, const char *json) "%s\"0x%08X\"", i ? "," : "", checked[i]); n += snprintf(buf + n, sizeof(buf) - n, "]"); } + /* Inc1-D registration-lifetime counters. */ + { + uint32_t loads=0, invs=0, unreg=0, lw_pc=0, lw_addr=0, lw_size=0; + uint64_t dnat=0, dint=0, stale=0; int regions=0; + overlay_loader_get_counters(&loads, &invs, &unreg, &dnat, &dint, + &stale, &lw_pc, &lw_addr, &lw_size, ®ions); + n += snprintf(buf + n, sizeof(buf) - n, + ",\"regions\":%d,\"loads\":%u,\"invalidations\":%u," + "\"unregistered_funcs\":%u,\"dispatch_native\":%llu," + "\"dispatch_interp_fallback\":%llu,\"stale_blocked\":%llu," + "\"last_write_pc\":\"0x%08X\",\"last_write_addr\":\"0x%08X\"," + "\"last_write_size\":%u", + regions, loads, invs, unreg, + (unsigned long long)dnat, (unsigned long long)dint, + (unsigned long long)stale, lw_pc, lw_addr, lw_size); + } snprintf(buf + n, sizeof(buf) - n, "}\n"); send_fmt("%s", buf); } diff --git a/runtime/src/memory.c b/runtime/src/memory.c index 9e44335a..83d79766 100644 --- a/runtime/src/memory.c +++ b/runtime/src/memory.c @@ -106,6 +106,45 @@ void dirty_ram_set_bitmap_words(const uint32_t* words, uint32_t count) { dirty_ram_bitmap[i] = words[i]; } +/* ---- Inc1-D: watched overlay pages ------------------------------------- + * Pages covered by a registered overlay DLL. A write into a watched page + * invalidates that overlay's registration so the new RAM content falls back to + * the interpreter instead of executing stale native code (OV-1). The bitmap is + * almost always empty, so the per-store cost is a single bitmap lookup. + * Set/cleared by the overlay loader; tested on the psx_write_* store path — + * the single, audited RAM-write chokepoint (all CPU + DMA stores funnel here). + */ +static uint32_t overlay_watch_bitmap[DIRTY_RAM_BITMAP_WORDS]; + +void overlay_watch_set_range(uint32_t phys, uint32_t len) { + if (len == 0 || phys >= RAM_SIZE) return; + uint32_t end = phys + len - 1u; + if (end >= RAM_SIZE || end < phys) end = RAM_SIZE - 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_SIZE) return; + uint32_t end = phys + len - 1u; + if (end >= RAM_SIZE || end < phys) end = RAM_SIZE - 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)); +} + +static inline void overlay_watch_note_write(uint32_t phys, uint32_t size) { + uint32_t pg = phys >> DIRTY_RAM_PAGE_SHIFT; + if (pg >= DIRTY_RAM_PAGE_COUNT) return; + if ((overlay_watch_bitmap[pg >> 5] >> (pg & 31u)) & 1u) { + extern void overlay_loader_invalidate_at(uint32_t phys, uint32_t size); + overlay_loader_invalidate_at(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 */ @@ -552,6 +591,7 @@ void psx_write_word(uint32_t addr, uint32_t val) { debug_server_trace_write_check(phys, read_ram_word(phys), val, 4); card_data_writes_check(phys, val, 4); dirty_ram_mark_kernel_write(phys); + overlay_watch_note_write(phys, 4); ram[phys] = (uint8_t)(val); ram[phys + 1] = (uint8_t)(val >> 8); ram[phys + 2] = (uint8_t)(val >> 16); @@ -616,6 +656,7 @@ void psx_write_half(uint32_t addr, uint16_t val) { debug_server_trace_write_check(phys, (uint32_t)read_ram_half(phys), (uint32_t)val, 2); card_data_writes_check(phys, (uint32_t)val, 2); dirty_ram_mark_kernel_write(phys); + overlay_watch_note_write(phys, 2); ram[phys] = (uint8_t)(val); ram[phys + 1] = (uint8_t)(val >> 8); return; @@ -669,6 +710,7 @@ void psx_write_byte(uint32_t addr, uint8_t val) { debug_server_trace_write_check(phys, (uint32_t)ram[phys], (uint32_t)val, 1); card_data_writes_check(phys, (uint32_t)val, 1); dirty_ram_mark_kernel_write(phys); + overlay_watch_note_write(phys, 1); ram[phys] = val; return; } diff --git a/runtime/src/overlay_loader.c b/runtime/src/overlay_loader.c index c8e8901a..96b9dd4f 100644 --- a/runtime/src/overlay_loader.c +++ b/runtime/src/overlay_loader.c @@ -29,12 +29,42 @@ typedef void (*OverlayFn)(CPUState *); typedef struct { uint32_t addr; /* physical address, 0 = empty */ OverlayFn fn; + int region; /* index into s_regions, -1 = none */ } DynEntry; static DynEntry s_table[DYNTAB_CAP]; static int s_count = 0; -static void dyntab_insert(uint32_t phys, OverlayFn fn) +/* ---- Registered overlay regions (Inc1-D: registration lifetime) --------- */ +/* A registration is only callable while the RAM it was compiled from is + * unchanged. Each region carries a validity flag; a write into the region's + * watched pages flips it invalid. We do NOT delete dyntab entries (messy with + * open addressing) — lookup consults the region's validity, so an invalidated + * region's functions become non-callable in O(1). Invariant: + * Compiled (DLL loaded) != Registered (in dyntab) != Callable (region valid). */ +#define MAX_OVL_REGIONS 32 +typedef struct { + uint32_t base; /* phys region start */ + uint32_t size; /* bytes (page-aligned span) */ + uint32_t generation; /* bumped on each invalidation */ + int valid; /* 1 = functions callable */ + int func_count; /* functions registered for this region */ +} OvlRegion; +static OvlRegion s_regions[MAX_OVL_REGIONS]; +static int s_nregions = 0; + +/* Counters / last-write info, surfaced via overlay_loader_status. */ +static uint32_t s_loads = 0; +static uint32_t s_invalidations = 0; +static uint32_t s_unregistered = 0; +static uint64_t s_disp_native = 0; +static uint64_t s_disp_interp = 0; +static uint64_t s_stale_blocked = 0; +static uint32_t s_last_write_pc = 0; +static uint32_t s_last_write_addr = 0; +static uint32_t s_last_write_size = 0; + +static void dyntab_insert(uint32_t phys, OverlayFn fn, int region) { uint32_t h = (phys * 2654435761u) & DYNTAB_MASK; uint32_t i; @@ -42,8 +72,9 @@ static void dyntab_insert(uint32_t phys, OverlayFn fn) uint32_t idx = (h + i) & DYNTAB_MASK; if (s_table[idx].addr == 0 || s_table[idx].addr == phys) { if (s_table[idx].addr == 0) s_count++; - s_table[idx].addr = phys; - s_table[idx].fn = fn; + s_table[idx].addr = phys; + s_table[idx].fn = fn; + s_table[idx].region = region; return; } } @@ -57,7 +88,17 @@ static OverlayFn dyntab_lookup(uint32_t phys) for (i = 0; i < DYNTAB_CAP; i++) { uint32_t idx = (h + i) & DYNTAB_MASK; if (s_table[idx].addr == 0) return NULL; - if (s_table[idx].addr == phys) return s_table[idx].fn; + if (s_table[idx].addr == phys) { + int r = s_table[idx].region; + /* A registered function whose source region was overwritten is + * NOT callable — block it and let the interpreter handle the new + * content (it self-heals via recapture). */ + if (r >= 0 && r < s_nregions && !s_regions[r].valid) { + s_stale_blocked++; + return NULL; + } + return s_table[idx].fn; + } } return NULL; } @@ -140,7 +181,7 @@ static void init_callbacks(void) /* ---- DLL loading and export enumeration -------------------------------- */ #ifdef _WIN32 -static int load_overlay_dll(const char *dll_path, uint32_t load_addr_virt) +static int load_overlay_dll(const char *dll_path, uint32_t load_addr_virt, int region) { HMODULE dll = LoadLibraryA(dll_path); if (!dll) { @@ -190,7 +231,7 @@ static int load_overlay_dll(const char *dll_path, uint32_t load_addr_virt) OverlayFn fn = (OverlayFn)(base + funcs[ord]); uint32_t phys = addr & 0x1FFFFFFFu; - dyntab_insert(phys, fn); + dyntab_insert(phys, fn, region); registered++; } @@ -198,8 +239,9 @@ static int load_overlay_dll(const char *dll_path, uint32_t load_addr_virt) return registered; } #else -static int load_overlay_dll(const char *dll_path, uint32_t load_addr_virt) +static int load_overlay_dll(const char *dll_path, uint32_t load_addr_virt, int region) { + (void)region; void *dll = dlopen(dll_path, RTLD_NOW | RTLD_LOCAL); if (!dll) { loader_log("dlopen(%s) failed: %s", dll_path, dlerror()); @@ -317,14 +359,73 @@ static void try_load_region(uint32_t phys) strncpy(dll_path, found_path, sizeof(dll_path) - 1); s_last_file_found = 1; - load_overlay_dll(dll_path, 0x80000000u | (region_start & 0x1FFFFFFFu)); + + if (s_nregions >= MAX_OVL_REGIONS) return; + int region = s_nregions; + s_regions[region].base = region_start; + s_regions[region].size = region_size; + s_regions[region].generation = 0; + s_regions[region].valid = 1; + s_regions[region].func_count = 0; + + int registered = load_overlay_dll( + dll_path, 0x80000000u | (region_start & 0x1FFFFFFFu), region); + if (registered <= 0) { + /* Nothing registered — don't keep an empty/watched region. The + * already-checked mark stays so we don't retry the same miss. */ + return; + } + s_regions[region].func_count = registered; + s_nregions++; + s_loads++; + + /* Watch this region's RAM pages: a later write invalidates the + * registration (Inc1-D). overlay_watch_set_range lives in memory.c, on + * the single psx_write_* store chokepoint. */ + extern void overlay_watch_set_range(uint32_t phys, uint32_t len); + overlay_watch_set_range(region_start, region_size); +} + +/* Invalidate the registered region containing `phys` (called from the store + * path in memory.c when a write lands on a watched overlay page). Coarse: + * the whole region's registrations become non-callable. The DLL stays loaded; + * only callability (region validity) is revoked. The already-checked mark is + * left in place so the stale DLL is not auto-reloaded for changed bytes — the + * new content falls back to the interpreter (re-registration of a matching + * cache is a later increment). */ +void overlay_loader_invalidate_at(uint32_t phys, uint32_t size) +{ + extern uint32_t g_debug_last_store_pc; + uint32_t p = phys & 0x1FFFFFFFu; + int r; + for (r = 0; r < s_nregions; r++) { + if (!s_regions[r].valid) continue; + if (p < s_regions[r].base) continue; + if (p >= s_regions[r].base + s_regions[r].size) continue; + + s_regions[r].valid = 0; + s_regions[r].generation++; + s_invalidations++; + s_unregistered += (uint32_t)s_regions[r].func_count; + s_last_write_pc = g_debug_last_store_pc; + s_last_write_addr = phys; + s_last_write_size = size; + + /* Stop watching: one invalidation per registration is enough. */ + extern void overlay_watch_clear_range(uint32_t phys, uint32_t len); + overlay_watch_clear_range(s_regions[r].base, s_regions[r].size); + loader_log("invalidated region 0x%08X (+%u) on write 0x%08X -> %d funcs", + s_regions[r].base, s_regions[r].size, phys, + s_regions[r].func_count); + return; + } } int overlay_loader_dispatch(CPUState *cpu, uint32_t addr) { uint32_t phys = addr & 0x1FFFFFFFu; OverlayFn fn = dyntab_lookup(phys); - if (fn) { fn(cpu); return 1; } + if (fn) { s_disp_native++; fn(cpu); return 1; } /* First miss for this region: check if a DLL is cached. */ if (s_active && phys >= 0x98000u) @@ -332,11 +433,33 @@ int overlay_loader_dispatch(CPUState *cpu, uint32_t addr) /* Retry after potential DLL load. */ fn = dyntab_lookup(phys); - if (fn) { fn(cpu); return 1; } + if (fn) { s_disp_native++; fn(cpu); return 1; } + s_disp_interp++; return 0; } +void overlay_loader_get_counters(uint32_t *loads, uint32_t *invalidations, + uint32_t *unregistered, + uint64_t *disp_native, uint64_t *disp_interp, + uint64_t *stale_blocked, + uint32_t *last_write_pc, + uint32_t *last_write_addr, + uint32_t *last_write_size, + int *regions) +{ + if (loads) *loads = s_loads; + if (invalidations) *invalidations = s_invalidations; + if (unregistered) *unregistered = s_unregistered; + if (disp_native) *disp_native = s_disp_native; + if (disp_interp) *disp_interp = s_disp_interp; + if (stale_blocked) *stale_blocked = s_stale_blocked; + if (last_write_pc) *last_write_pc = s_last_write_pc; + if (last_write_addr) *last_write_addr = s_last_write_addr; + if (last_write_size) *last_write_size = s_last_write_size; + if (regions) *regions = s_nregions; +} + int overlay_loader_registered_count(void) { return s_count;