mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 10:19:47 -04:00
Guard static text dispatch with live RAM divergence
Packed games can rewrite code over their own loaded EXE text after static recompilation. A dirty-page bit alone is too coarse for dispatch decisions: it prevents stale native execution, but it also cannot distinguish data writes in a text page from instruction bytes that actually diverged from the compiled image.\n\nRegister the boot EXE image as a reference and let memory writes mark text pages whose bytes differ from that image. Static game dispatch now asks whether the target remains native-safe; diverged pages fall through to execute live RAM through the dirty interpreter, while matching code bytes can still use the compiled path.\n\nConstraint: Runtime-installed or rewritten RAM code must execute the program's own instructions, not HLE or stale static code\nRejected: Keep the existing dirty-page-only gate | too coarse for packed/self-modifying main text and data writes sharing code pages\nConfidence: medium\nScope-risk: moderate\nDirective: Do not bypass dirty_ram_text_native_ok() at game compiled dispatch sites without proving RAM still matches the compiled image\nTested: Local Tekken 3 branch using this mechanism reaches playable fights; clean upstream worktree compiles changed main/memory/dirty files before hitting an unrelated existing macOS overlay_loader LONG/InterlockedExchange build error\nNot-tested: Full upstream macOS build completion; non-macOS build matrix
This commit is contained in:
@@ -87,6 +87,11 @@ uint32_t dirty_ram_get_bitmap(void);
|
||||
uint32_t dirty_ram_get_bitmap_word(uint32_t word_index);
|
||||
uint32_t dirty_ram_get_bitmap_word_count(void);
|
||||
void dirty_ram_mark_executable_range(uint32_t phys, uint32_t len);
|
||||
void dirty_ram_register_text_image(uint32_t phys_lo, const uint8_t *bytes,
|
||||
uint32_t len);
|
||||
int dirty_ram_text_native_ok(uint32_t phys);
|
||||
uint64_t dirty_ram_text_native_blocked(void);
|
||||
uint32_t dirty_ram_text_diverged_pages(void);
|
||||
|
||||
/* Counters for visibility / TCP debug. Increment in interpreter; expose
|
||||
* via debug_server.c if helpful. */
|
||||
|
||||
@@ -1010,12 +1010,10 @@ int dirty_ram_xprobe_json(char *out, int cap) {
|
||||
#ifdef PSX_HAS_GAME_DISPATCH
|
||||
static int interp_enter_compiled(CPUState *cpu, uint32_t target) {
|
||||
if (target == 0x8001A954u) site_note(&g_site_interp);
|
||||
/* Decline if the target page is dirty: an overlay overwrote it after the
|
||||
* game-start baseline, so the compiled function is STALE. Returning 0 lets the
|
||||
* JAL/JALR handler fall through to is_local_dirty_target -> local-flow interp of
|
||||
* the live overlay, instead of running stale compiled code (the same staleness
|
||||
* the dirty_ram_dispatch_inner gate guards). Clean targets run compiled. */
|
||||
if (dirty_ram_is_dirty(target & 0x1FFFFFFFu)) return 0;
|
||||
/* Decline when the target page no longer matches the static game image.
|
||||
* Returning 0 lets the JAL/JALR handler fall through to local-flow interp
|
||||
* of the live RAM bytes instead of running stale compiled code. */
|
||||
if (!dirty_ram_text_native_ok(target & 0x1FFFFFFFu)) return 0;
|
||||
if (psx_mixed_owner_enabled()
|
||||
&& interp_host_stack_used() > psx_mixed_stack_watermark()) {
|
||||
cpu->pc = target;
|
||||
@@ -2062,15 +2060,10 @@ static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_
|
||||
|
||||
#ifdef PSX_HAS_GAME_DISPATCH
|
||||
xprobe_event(cpu->gpr[31], XOP_DD, XSITE_DD, addr, 0u, cpu->gpr[29], cpu->gpr[31], 0);
|
||||
/* Run the statically-compiled game function ONLY for a CLEAN page (RAM matches
|
||||
* the compiled image). A dirty page in the game-text region means an overlay
|
||||
* overwrote it after the game-start baseline (dirty_ram_clear_image_baseline),
|
||||
* so the compiled function is STALE and we must fall through to interpret the
|
||||
* live overlay. Without this gate the loader thread's psx_dispatch(0x8001DB38)
|
||||
* ran the stale compiled func_8001DB38 instead of the START.BIN-loader overlay
|
||||
* (Tomba 2 Whoopee-Camp splash). Clean text still runs compiled (the fast path,
|
||||
* unchanged for non-overlaying games once their text is baselined). */
|
||||
if (!dirty_ram_is_dirty(phys)) {
|
||||
/* Run the statically-compiled game function only while the target is still
|
||||
* native-safe. Dirty overlay pages and pages whose text bytes diverged from
|
||||
* the original EXE image fall through to interpret the live RAM bytes. */
|
||||
if (dirty_ram_text_native_ok(phys)) {
|
||||
g_mixed_depth++;
|
||||
{
|
||||
ls_func_enter(addr, cpu);
|
||||
|
||||
@@ -83,6 +83,9 @@ extern "C" uint64_t gte_get_exec_count(void);
|
||||
extern "C" void memory_init(const char* bios_path);
|
||||
extern "C" void memory_set_sr_ptr(const uint32_t *p);
|
||||
extern "C" uint32_t memory_get_bios_checksum(void);
|
||||
extern "C" void dirty_ram_register_text_image(uint32_t phys_lo,
|
||||
const uint8_t *bytes,
|
||||
uint32_t len);
|
||||
|
||||
/* dma.c */
|
||||
extern "C" void dma_init(void);
|
||||
@@ -2075,6 +2078,34 @@ int main(int argc, char** argv) {
|
||||
fast_boot = gc.runtime.fast_boot;
|
||||
bios_hle = gc.runtime.bios_hle;
|
||||
bios_hle_keep_intro = gc.runtime.bios_hle_keep_intro;
|
||||
/* Let the dispatch layer distinguish "dirty because text was
|
||||
* loaded" from "diverged because runtime wrote different code over
|
||||
* the original EXE image". Packed/self-modifying games can rewrite
|
||||
* their own text; those pages must execute from live RAM, not stale
|
||||
* static native code. Best effort: without the local EXE file, the
|
||||
* guard remains on the existing dirty-page behavior. */
|
||||
if (!gc.exe_path.empty()) {
|
||||
std::ifstream ef(gc.exe_path, std::ios::binary | std::ios::ate);
|
||||
if (ef) {
|
||||
std::streamsize sz = ef.tellg();
|
||||
if (sz > 2048) {
|
||||
uint32_t img_len = (uint32_t)(sz - 2048);
|
||||
uint8_t *img = (uint8_t *)std::malloc(img_len);
|
||||
if (img) {
|
||||
ef.seekg(2048, std::ios::beg);
|
||||
if (ef.read((char *)img, img_len)) {
|
||||
dirty_ram_register_text_image(
|
||||
gc.load_address & 0x1FFFFFFFu, img, img_len);
|
||||
std::fprintf(stdout,
|
||||
"psxrecomp: text image guard armed (0x%08X..0x%08X)\n",
|
||||
gc.load_address, gc.load_address + img_len);
|
||||
} else {
|
||||
std::free(img);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/* HLE-tier scheduler subsystem replacement default (env
|
||||
* PSX_HLE_SCHEDULER still wins; latched at first dispatch). */
|
||||
psx_hle_scheduler_set_default(gc.runtime.hle_scheduler ? 1 : 0);
|
||||
|
||||
@@ -87,6 +87,8 @@ static inline void dirty_ram_mark_kernel_write(uint32_t phys) {
|
||||
dirty_ram_mark_page(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
|
||||
@@ -109,6 +111,78 @@ void dirty_ram_clear_image_baseline(void) {
|
||||
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. */
|
||||
static const 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;
|
||||
|
||||
void dirty_ram_register_text_image(uint32_t phys_lo, const uint8_t *bytes,
|
||||
uint32_t len) {
|
||||
if (!bytes || len == 0 || phys_lo >= RAM_SIZE) return;
|
||||
if (len > RAM_SIZE - phys_lo) len = RAM_SIZE - phys_lo;
|
||||
text_ref_image = bytes;
|
||||
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;
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
int dirty_ram_text_native_ok(uint32_t phys) {
|
||||
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;
|
||||
}
|
||||
if (!(text_modified_bitmap[page >> 5] & bit))
|
||||
return !dirty_ram_is_dirty(phys);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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; }
|
||||
|
||||
void dirty_ram_mark_executable_range(uint32_t phys, uint32_t len) {
|
||||
if (len == 0 || phys >= RAM_SIZE) return;
|
||||
uint32_t end = phys + len - 1u;
|
||||
@@ -938,6 +1012,7 @@ static void psx_write_word_raw(uint32_t addr, uint32_t val) {
|
||||
parity_trace_note_write(phys, 4, g_debug_last_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); }
|
||||
@@ -1030,6 +1105,7 @@ static void psx_write_half_raw(uint32_t addr, uint16_t val) {
|
||||
parity_trace_note_write(phys, 2, g_debug_last_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); }
|
||||
@@ -1257,6 +1333,7 @@ static void psx_write_byte_raw(uint32_t addr, uint8_t val) {
|
||||
parity_trace_note_write(phys, 1, g_debug_last_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); }
|
||||
|
||||
@@ -2282,10 +2282,9 @@ static int psx_sljit_call_inner(CPUState *cpu, uint32_t target, uint32_t return_
|
||||
int check_contract) {
|
||||
uint32_t site_sp = cpu->gpr[29]; /* sp at the call (after the delay slot) */
|
||||
#ifdef PSX_HAS_GAME_DISPATCH
|
||||
/* Skip the compiled game function if its page is dirty (an overlay overwrote it
|
||||
* after the game-start baseline -> compiled code is stale); fall through to the
|
||||
* native/interp paths which run the live overlay. Clean pages run compiled. */
|
||||
if (!dirty_ram_is_dirty(target & 0x1FFFFFFFu)) {
|
||||
/* Skip the compiled game function when its target is no longer native-safe;
|
||||
* fall through to the native/interp paths that run the live RAM bytes. */
|
||||
if (dirty_ram_text_native_ok(target & 0x1FFFFFFFu)) {
|
||||
extern int psx_dispatch_game_compiled(CPUState *cpu, uint32_t addr);
|
||||
cpu->pc = 0;
|
||||
if (psx_dispatch_game_compiled(cpu, target)) {
|
||||
|
||||
Reference in New Issue
Block a user