Files
psxrecomp/runtime/include/dirty_ram_interp.h
Kevin 3cbe6c1cde kernel-bless: publish install-slot ranges so patched BIOS bodies run native
The BIOS exception handler ran interpreted for the life of every process,
1.22 billion instructions and 44.6% of all interpreted work in Breath of
Fire III, even though it is compiled and even though the stub that
unblessed it sat at exactly the address bios/SCPH1001.toml already
declared as an install slot. Two mechanisms existed and did not compose:
the emitter hooked the declared slot PC, while the runtime's bless
verifier memcmp'd the whole body including that slot, so the body was
MISMATCH forever and native code never reached the hook.

Three parts, none of which pays off alone:

1. Publish the declared ranges to the runtime. The emitter emits a
   PsxKernelPatchRange table next to the kernel body table and routes it
   through the backend descriptor; the bless verifier compares a body in
   segments that skip them. CLEAN now means "every byte outside every
   declared range still matches ROM", which is the claim the native code
   actually depends on.

2. Generalise the slot model to what the patches are. The old model
   assumed a 4-word lui/addiu/jalr/nop stub written over ROM zeros and
   fired on `word != 0`. Measured against both images, only one of five
   observed shapes fits: patchers also plant a `jr` over real ROM
   instructions, rewrite an 11-word prologue and insert an `mfc0 Cause`,
   and NOP out a driver routine. A slot is now a range with a length and
   an explicit `resume` (jalr / fallthrough / none), detected by
   comparing every word against its ROM-baked value.

3. Hand back from the interpreter at the range end. Kernel page 0 is
   permanently dirty, because the handler saves registers there on every
   exception, so straight-line interpretation would run the whole
   function after the patch. The emitter registers the resume PC as a
   continuation key and dirty_ram_interp surfaces there.

A PC inside a declared range must never be a native dispatch key: the
compiled bytes there are not what executes. Seven pre-existing
jal-return continuations violated that and wedged the boot at frame 0 in
an infinite dispatch loop (OpenBIOS RAM 0x357C). The emitter now drops
continuation keys inside a range so dispatch misses through to the
interpreter, and refuses to emit if a range covers a function entry,
which would silently remove a callable function instead.

Both profiles declare their measured ranges, and SCPH1001.toml finally
pins [program.image] sha256 to v2.2 (CRC32 37157331) so a v2.0 dump
refuses to generate rather than producing phantom discovery gaps.

Measured on Breath of Fire III (SLPS-00990, Psy-Q SDK 3.70), headless,
matched frames, PSX_KERNEL_PATCH_RANGES=0 for the OFF side:

                      OpenBIOS            SCPH-1001 v2.2
  bless mismatch      21 -> 0             74 -> 0
  kernel bodies       -87.7%/frame        -97.8%/frame
  all interp insns    -42.6%/frame        -94.2%/frame

The residual kernel-body work is exactly declared-words x entries on
every range, so nothing leaks past the hook. The A0/B0/C0 vector
trampolines are unchanged (+0.2%, noise): the profile excludes them by
design and they are a separate job. Card read traces are byte-identical
across the A/B on both images, all 32 entries, which exercises the two
ranges that replace the kernel's card handler.

Rule 18 holds throughout: the guest's patched instructions still execute
as written on the dirty-RAM interpreter. Only the code around them
changes backend. Nothing is synthesised.

Tests: runtime/tests/test_kernel_patch_ranges.c (new, the verifier's
accept/reject behaviour and the hand-back predicate) and the install-slot
range invariants in recompiler/tests/bios_address_model_test.cpp.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 16:38:34 -04:00

385 lines
20 KiB
C

/* dirty_ram_interp.h — interpret-on-dispatch for install-at-runtime RAM.
*
* See CLAUDE.md Rule 18 and docs/dynamic_handler_install.md for the full
* rationale. The PS1 BIOS dynamically writes 4-instruction dispatch stubs
* into kernel RAM (notably RAM 0xCF0 for the SIO data-byte handler). A
* static recompiler can't see those bytes at compile time, so a small MIPS
* interpreter here runs them at dispatch time on the same CPUState.
*
* Scope: this is NOT a fallback for code the recompiler failed to translate.
* It runs only against PCs in pages that have been written-to since boot.
* Static-recompiled code continues to handle ROM-resident code and game
* RAM. See docs/dynamic_handler_install.md for the inline note about a
* potential future migration to runtime JIT (Option B).
*/
#ifndef PSXRECOMP_DIRTY_RAM_INTERP_H
#define PSXRECOMP_DIRTY_RAM_INTERP_H
#include <stdint.h>
#include "cpu_state.h"
#ifdef __cplusplus
extern "C" {
#endif
/* Returns 1 if `addr` lies in a dirty kernel-RAM page and the interpreter
* ran a basic block at that PC. Returns 0 if `addr` is clean (caller must
* fall back to the static dispatch table). On return with 1, cpu->pc is
* either 0 (block ended on jr $ra style return) or the target of a tail
* jump that the dispatch trampoline should re-enter.
*
* `stop_addr` is the dispatch loop's return contract (psx_dispatch_call's
* return_addr; 0 for plain tail dispatches). The interpreter's overlay
* local-flow chaining MUST NOT run past it: when a transfer (or fall-
* through) reaches stop_addr, the interpreter exits with cpu->pc ==
* stop_addr so the dispatch loop returns into the suspended native
* caller. Skipping this check lets the interp execute the native
* caller's tail itself; the suspended native host frame then resumes and
* double-executes that tail against a moved guest stack, restoring
* garbage callee-saved registers (the pig-throw blue-screen/MMIO-fatal
* corruption, 2026-06-10). */
int dirty_ram_dispatch(CPUState* cpu, uint32_t addr, uint32_t stop_addr);
/* Retire a deferred R3000A load-delay writeback (see dirty_ram_interp.c). The
* interpreter defers a load's destination-register write past the delay-slot
* instruction, as hardware does; call this anywhere control leaves the
* interpreter or an exception is delivered, so no register write is dropped. */
void dirty_ram_ld_delay_flush(CPUState* cpu);
/* Drop a deferred load-delay writeback WITHOUT applying it. Required on
* savestate/RB restore: the snap already has architectural GPRs, and a
* host-static pending load from the pre-load timeline must not stomp them
* on the next interp step (selfcheck MotK v0=countdown vs v0=1 forks). */
void dirty_ram_ld_delay_discard(void);
/* Re-anchor host-only dirty IRQ pump ambient (entry-poll stride + 4096-insn
* gap) after snap restore. Not in boot_state — peers that drifted through FMV
* otherwise entered the post-FMV dirty wait on opposite poll phases. */
void dirty_ram_irq_ambient_resync_after_restore(void);
/* Overlay-cache windows — the address ranges eligible for capture, offline
* recompilation, and per-entry-validated native execution (Rule 18 code that
* does not exist in any compile-time image):
*
* Kernel RAM [0x00000, 0x10000): the BIOS part-2 image relocated to RAM
* plus install-at-runtime stubs (e.g. the SIO data-byte stub at 0xCF0).
* Dirty-tracked per CPU store (dirty_ram_mark_kernel_write).
* Overlay region [OVERLAY_REGION_FLOOR, RAM_SIZE): game overlays loaded by
* CD DMA (dirty_ram_mark_executable_range).
*
* Main-EXE text [0x10000, OVERLAY_REGION_FLOOR): CLEAN pages are statically
* recompiled and never captured. But games that discard their boot/init code
* load gameplay overlays OVER this region (Tomba 2: hot gameplay code at
* 0x1DA88/0x24548/0x3116C, well below its 0x38800 floor). Post-baseline
* (dirty_ram_clear_image_baseline) a DIRTY page here is by definition live
* overlay code — the compiled image is stale for it — so dirty boot-text
* pages ARE a capture/candidate window (2026-07-06; before this they could
* only interpret, which dominated wall time at ~300ns/insn).
*
* The interpreter's LOCAL-FLOW gates (is_local_dirty_target /
* phys_is_overlay_flow_region) use DIRTY_RAM_KERNEL_WINDOW_END alone: only
* the kernel window stays per-block (it runs in exception context where the
* verified dispatch cadence is delicate). Native coverage for the kernel
* window comes from the overlay loader, not from interp chaining. */
#define DIRTY_RAM_KERNEL_WINDOW_END 0x00010000u
/* The overlay-region floor is the END of THIS game's statically-recompiled
* main-EXE text (phys). Everything at/above it is runtime-loaded overlay code,
* eligible for in-interpreter local-flow chaining and overlay-cache capture.
*
* It is a RUNTIME value (g_overlay_region_floor), NOT a compile-time constant,
* because it differs per game: Tomba 1's text ends at 0x98000 (0x10000+0x88000)
* but Tomba 2's boot EXE is only 0x28800 (text ends at 0x38800). A hardcoded
* 0x98000 misclassified Tomba 2's overlays (loaded at 0x85000+) as main-EXE
* text, forcing them onto the slow block-by-block dispatch path AND through the
* bail-prone non-local-call contract — the Whoopee-Camp splash freeze. main.cpp
* sets g_overlay_region_floor = (load_address + text_size) & 0x1FFFFFFF at game
* load; OVERLAY_REGION_FLOOR_DEFAULT applies for BIOS-only runs. */
#define OVERLAY_REGION_FLOOR_DEFAULT 0x00098000u
extern uint32_t g_overlay_region_floor;
#define OVERLAY_REGION_FLOOR (g_overlay_region_floor)
/* The text-image BASE (phys) = this game's main-EXE load address. The floor
* above only bounds the text from ABOVE; on its own it encodes the assumption
* that the boot EXE sits at the bottom of usable RAM, so that [KERNEL_END,
* FLOOR) is text and [FLOOR, RAM) is overlay. That holds for a load_address of
* 0x80010000 (text base == KERNEL_END, so the region below text is empty) but
* is FALSE for a boot EXE that loads HIGH and streams its gameplay code into
* the RAM BELOW itself: Klonoa loads at 0x80180000 (text 0x180000-0x18B000,
* overlays at 0x10000-0x130000), Street Fighter Alpha 3 at 0x80113B00,
* Bomberman Fantasy Race at 0x8003004C. For those the sub-text RAM was
* misclassified as main-EXE text — clear_image_baseline() wiped its dirty bits
* and the dispatch admit-heuristic refused it, so a JALR into an overlay page
* the CD DMA'd there fell through to psx_unknown_dispatch and fail-fast
* exit(1) (Klonoa, frame 687, target 0x80123D00).
*
* The overlay region is therefore BOTH sides of the text image, not just above
* it. main.cpp pins g_text_image_lo = load_address & 0x1FFFFFFF at game load;
* the default equals DIRTY_RAM_KERNEL_WINDOW_END so the below-text clause is
* empty for BIOS-only runs and for every bottom-loading game. */
extern uint32_t g_text_image_lo;
/* 1 iff phys is runtime-loaded overlay RAM rather than main-EXE text — either
* side of the text image. Identical to `phys >= FLOOR` whenever the game loads
* at the bottom of RAM (g_text_image_lo == KERNEL_WINDOW_END). */
static inline int phys_is_overlay_region(uint32_t phys) {
return phys >= g_overlay_region_floor ||
(phys >= DIRTY_RAM_KERNEL_WINDOW_END && phys < g_text_image_lo);
}
/* Test whether a given physical kernel-RAM address is in a page that was
* written-to since boot. Defined in memory.c. */
int dirty_ram_is_dirty(uint32_t phys);
/* Kernel-image bless (memory.c). A dispatch key in the relocated kernel
* window (RAM [0x500,0x8500), the BIOS's boot-time copy of ROM 0x1FC10000+)
* may run its statically-recompiled native function IFF the live RAM bytes
* of the code reachable from it (emitter-supplied body extent) byte-match
* the ROM source. Lazily verified, cached per entry, invalidated precisely
* on writes into the body. Runtime-patched bodies (pad/SIO install stubs)
* never verify and keep interpreting — faithful either way. */
int psx_kernel_bless_dispatchable(uint32_t phys);
/* True when a declared kernel patch range ENDS at this RAM address. The
* emitter registered that PC as a continuation key, so the interpreter hands
* straight-line flow back to static dispatch there and only the guest's
* patched words interpret (memory.c psx_bios_kernel_patch_ranges). */
int psx_kernel_patch_range_ends_at(uint32_t phys);
void psx_kernel_bless_note_range(uint32_t phys, uint32_t len);
void psx_kernel_bless_stats(uint64_t out[8]);
void psx_kernel_bless_resync_after_restore(void);
/* Soft-return rematch / BIOS switch: drop latched SCPH↔OpenBIOS window +
* CLEAN/MISMATCH so the next kbless_on() re-reads psx_bios_image. */
void psx_kernel_bless_reset_for_boot(void);
/* Capture/candidate window membership. Kernel window and overlay region are
* unconditional; main-EXE text [KERNEL_WINDOW_END, FLOOR) is included only
* when the page is dirty — i.e. a gameplay overlay overwrote the boot text
* after the game-start baseline, so the bytes there are runtime overlay code
* (see the window model note above). Clean boot text never enters the window
* (it runs compiled; capturing it would violate the static-first design). */
static inline int overlay_cache_window_contains(uint32_t phys) {
return phys < DIRTY_RAM_KERNEL_WINDOW_END
|| phys >= OVERLAY_REGION_FLOOR
|| dirty_ram_is_dirty(phys);
}
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_set_bitmap_words(const uint32_t* words, uint32_t count);
/* Rematch / session_reboot: wipe host dirty tracking so dig0 matches a cold
* process (memory_init clears RAM but used to leave these bitmaps sticky). */
void dirty_ram_reset_for_boot(void);
/* After bulk RAM restore (savestate): bump overlay page gens + lazy-miss epoch
* so native overlays re-hash against restored bytes; also drop sticky
* text_diverged/modified bitmaps (host-only — restored RAM may match ref). */
void overlay_watch_invalidate_after_ram_restore(void);
void dirty_ram_text_guard_resync_after_restore(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);
/* Exact CFG ranges; exec_pc clips ranges that end before the resume PC. */
int dirty_ram_text_native_ok_ranges_from(const uint32_t *lo_len_pairs,
uint32_t count,
uint32_t exec_pc);
int dirty_ram_text_native_ok_ranges(const uint32_t *lo_len_pairs,
uint32_t count);
int dirty_ram_text_image_registered(void);
/* Bless an intentional runtime data patch (e.g. text_xlate string/glyph tables)
* into the text reference image so it is not mistaken for self-modifying code. */
void dirty_ram_text_bless(uint32_t phys, const uint8_t *bytes, uint32_t len);
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. */
extern uint64_t g_dirty_ram_blocks_run; /* basic blocks interpreted */
extern uint64_t g_dirty_ram_insns_run; /* instructions interpreted */
extern uint64_t g_dirty_window_dispatches; /* interp dispatches inside a
* capture window (autocapture
* pressure signal, step 2.8) */
extern uint64_t g_dirty_ram_aborts; /* unsupported-opcode aborts */
extern uint64_t g_dirty_ram_guard_yields; /* long dirty loops yielded */
extern uint64_t g_dirty_ram_unsupported_midblock;
extern uint32_t g_dirty_ram_last_unsupported_entry;
extern uint32_t g_dirty_ram_last_unsupported_entry_ra;
extern uint32_t g_dirty_ram_last_unsupported_entry_sp;
extern uint32_t g_dirty_ram_last_unsupported_insns;
extern uint32_t g_dirty_ram_last_unsupported_pc;
extern uint32_t g_dirty_ram_last_unsupported_insn;
extern const char *g_dirty_ram_last_unsupported_reason;
/* Per-entry-PC counters. Aggregate counters above hide which install
* stubs actually fire — a single noisy spurious-dispatch site can mask
* a legitimate handler that never runs. This open-addressed table keys
* on the entry PC of each interpreted block. */
/* 65536 -> 262144 (2026-07-03): Tomba2's Trolley attract demo interprets a
* working set past 64K distinct PCs; a full table degraded every lookup to a
* full-table scan (0.3 fps host burn in pc_table_get_or_insert_in) and
* silently truncated overlay-capture seed lists (MAX_CAPTURE_PCS aliases
* this). Probe length is bounded in pc_table_get_or_insert_in either way. */
#define DIRTY_RAM_PC_TABLE_SIZE 262144
typedef struct {
uint32_t pc; /* entry PC, 0 = empty slot */
uint64_t hits; /* number of times dispatched here */
uint64_t insns; /* total instructions executed across hits */
uint64_t entry_hits; /* dispatches that arrived from NATIVE code (call /
* dispatch loop), not from the interpreter's own
* block-to-block chaining. `hits` conflates the two:
* below the local-flow floor every taken branch
* re-dispatches, so chain blocks dominate. entry_hits
* is the evidence stream for interior-alias seeds. */
/* Enrichment (2026-09-05, BoF3): make a bare interpreted PC explainable
* from a session-long per_pc snapshot alone, with no offline join and no
* live-ring window. Stamped only on EXTERNAL entries (arrived from native
* dispatch, addr != chain target), which is where the value is. */
uint32_t occ_crc; /* tier 1: psx_overlay_resident_crc_at(pc) at the last
* external entry -- the manifest CRC of the static
* variant whose code ranges span this PC (DLL path:
* last hash taken). Disambiguates a mixed band
* (0x801D0C00 carries BATTLE/ETC/SCENARIO/WORLD
* occupants) to the one actually loaded, which the
* offline enrich_pcs join cannot. 0 = nothing compiled
* spans this PC (BIOS / kernel / boot EXE). */
uint8_t occ_ok; /* 1 = that variant validated at the time (interior
* gap inside live native code: an Axis B seed);
* 0 = it is resident but CRC-missing (data inside the
* code range rewritten, or a different section than
* the one compiled) -- the whole band runs
* interpreted and no seed will fix it. */
uint32_t last_ext_ra;/* tier 2: gpr[31] (caller RA) at the most recent
* external entry — names the call site that reaches a
* function-pointer-table interior. This is the §9 (LOGO
* effect-handler tables) diagnosis made durable and
* session-long instead of ring-bounded; the full
* call/jalr/jr transfer split stays in the fp-log ring. */
} DirtyRamPcEntry;
extern DirtyRamPcEntry g_dirty_ram_pc_table[DIRTY_RAM_PC_TABLE_SIZE];
/* Every aligned main-RAM word is a possible instruction PC. Execution
* coverage only needs presence, not a hit count, so record it in a direct
* bitmap instead of probing a large hash table for every retired instruction.
* This covers all 524,288 RAM words (the old 262K-entry hash could saturate)
* and is also the execution-verified seed source used by overlay_capture. */
#define DIRTY_RAM_EXEC_WORD_COUNT ((2u * 1024u * 1024u) / 4u)
#define DIRTY_RAM_EXEC_BITMAP_WORDS ((DIRTY_RAM_EXEC_WORD_COUNT + 31u) / 32u)
extern uint32_t g_dirty_ram_exec_pc_bitmap[DIRTY_RAM_EXEC_BITMAP_WORDS];
/* One bit per 4 KiB page. RAM writes use this as a one-test stale-evidence
* guard; they clear that page's capture bits rather than serializing from the
* universal store hot path. */
#define DIRTY_RAM_EXEC_PAGE_BITMAP_WORDS 16u
extern uint32_t g_dirty_ram_exec_page_bitmap[DIRTY_RAM_EXEC_PAGE_BITMAP_WORDS];
/* Presence-only companion for interpreted block/dispatch entries. The richer
* counter table remains for telemetry, while capture can snapshot/reset this
* compact evidence independently at overlay-generation boundaries. */
extern uint32_t g_dirty_ram_dispatch_pc_bitmap[DIRTY_RAM_EXEC_BITMAP_WORDS];
/* Block-entry ring buffer. Records every dispatch into dirty RAM with the
* caller's RA at entry, plus argument context — answers
* "who tried to JALR into this RAM stub, with what args".
* Always-on, eviction keeps memory bounded; callers query the window of
* interest (CLAUDE.md global rule on ring buffers).
*
* Limitation: this captures dispatches into RAM-resident code only. ROM
* recompiled-C → recompiled-C calls (direct C function calls in
* generated dispatch table) are NOT captured here. For BIOS shell
* investigation that's fine — shell code lives in RAM (0x800XXXXX).
*
* `ra` is cpu->gpr[31] at dispatch time. For normal JAL-style calls,
* (ra - 8) gives the caller's PC. For J/JR/tail-calls it's only a
* heuristic — treat ra as "ra_callsite_guess", not authoritative. */
#ifdef PSX_NO_DEBUG_TOOLS
/* Keep ABI-visible one-element sentinels so crash/debug stubs still link, while
* Release does not reserve the diagnostic rings' ~227 MiB of address space. */
#define DIRTY_RAM_BLOCK_LOG_CAP 1u
#else
#define DIRTY_RAM_BLOCK_LOG_CAP (1u << 22) /* 4M entries (~224 MiB at 56 B each).
* At ~580K dispatches/s during boot, this retains ~7s of history; at
* ~10K/s during modal idle, ~400s. Sized for retroactive press-window
* analysis without the prior 16K ring's 28-ms eviction problem. */
#endif
typedef struct {
uint64_t seq; /* monotonic, unique per entry */
uint32_t target; /* entry PC (RAM address) */
uint32_t ra; /* cpu->gpr[31] at dispatch — caller's return target */
uint32_t a0; /* cpu->gpr[4] at dispatch */
uint32_t a1; /* cpu->gpr[5] at dispatch */
uint32_t a2; /* cpu->gpr[6] at dispatch */
uint32_t a3; /* cpu->gpr[7] at dispatch */
uint32_t t0; /* cpu->gpr[8] at dispatch */
uint32_t t1; /* cpu->gpr[9] at dispatch */
uint32_t t2; /* cpu->gpr[10] at dispatch */
uint32_t sp; /* cpu->gpr[29] at dispatch */
uint32_t frame; /* s_frame_count at the time of dispatch */
} DirtyRamBlockLogEntry;
extern DirtyRamBlockLogEntry g_dirty_ram_block_log[DIRTY_RAM_BLOCK_LOG_CAP];
extern uint64_t g_dirty_ram_block_log_seq;
#ifdef PSX_NO_DEBUG_TOOLS
#define DIRTY_RAM_FLOW_LOG_CAP 1u
#else
#define DIRTY_RAM_FLOW_LOG_CAP (1u << 16)
#endif
typedef struct {
uint64_t seq;
uint32_t pc;
uint32_t target;
uint32_t ra;
uint32_t a0;
uint32_t a1;
uint32_t a2;
uint32_t a3;
uint32_t sp;
uint32_t frame;
} DirtyRamFlowLogEntry;
extern DirtyRamFlowLogEntry g_dirty_ram_flow_log[DIRTY_RAM_FLOW_LOG_CAP];
extern uint64_t g_dirty_ram_flow_log_seq;
#ifdef PSX_NO_DEBUG_TOOLS
#define DIRTY_RAM_INSN_LOG_CAP 1u
#else
#define DIRTY_RAM_INSN_LOG_CAP (1u << 16)
#endif
typedef struct {
uint64_t seq;
uint32_t pc;
uint32_t insn;
uint32_t next_pc;
uint32_t target;
uint32_t before_s0;
uint32_t after_s0;
uint32_t sp;
uint32_t ra;
uint32_t v0;
uint32_t v1;
uint32_t a0;
uint32_t a1;
uint32_t a2;
uint32_t a3;
uint32_t t0;
uint32_t t1;
uint32_t t2;
/* Kernel scratch registers. $at is the assembler-temp that hand-written
* kernel asm uses to stash a base across a load-delay slot, and $k0/$k1
* are the exception handlers' only free registers — i.e. exactly the state
* that matters when debugging BIOS exception/handler code, which is what
* this interpreter mostly runs. */
uint32_t at;
uint32_t k0;
uint32_t k1;
uint32_t current_tcb;
uint32_t task_ptr;
uint32_t task_mode;
uint32_t task_submode;
uint32_t frame;
uint8_t transferred;
uint8_t pad[3];
} DirtyRamInsnLogEntry;
extern DirtyRamInsnLogEntry g_dirty_ram_insn_log[DIRTY_RAM_INSN_LOG_CAP];
extern uint64_t g_dirty_ram_insn_log_seq;
#ifdef __cplusplus
}
#endif
#endif /* PSXRECOMP_DIRTY_RAM_INTERP_H */