overlay: Inc1-D registration-lifetime write-invalidation (fixes OV-1)

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 <noreply@anthropic.com>
This commit is contained in:
Matthew Stanley
2026-06-07 15:11:06 -07:00
parent 42246da229
commit 478e2352c9
4 changed files with 207 additions and 10 deletions
+16
View File
@@ -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
+16
View File
@@ -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, &regions);
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);
}
+42
View File
@@ -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;
}
+133 -10
View File
@@ -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;