mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
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:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user