diff --git a/docs/TCP_COMMANDS.md b/docs/TCP_COMMANDS.md index 8bcca227..d48e2174 100644 --- a/docs/TCP_COMMANDS.md +++ b/docs/TCP_COMMANDS.md @@ -243,6 +243,43 @@ vector observation there. --- +## `dirty_ram_stats` `per_pc` — interpreted-PC table + +Snapshot of the open-addressed per-entry-PC table +(`g_dirty_ram_pc_table`, `dirty_ram_interp.c`): one object per interpreted +entry PC. Read-only; safe to poll while the game runs. + +- `pc` — physical entry PC. +- `hits` — dispatches here (interp block chaining conflated in). +- `insns` — instructions executed across those hits. +- `entries` — dispatches that arrived from NATIVE code (a call or a fresh + dispatch), not interp block-to-block chaining. `entries > 0` marks a real + interior entry point — the seed stream for overlay-capture aliasing. +- `occ_crc` / `occ_ok` — enrichment (2026-09-05): at the last external + entry, the manifest CRC of the static overlay variant whose code ranges + span this PC (`psx_overlay_resident_crc_at`; DLL-path titles get the last + residency hash instead), and whether it validated then. The CRC is the one + the variant was compiled from, so it maps straight back to a capture / + `.EMI` section offline. Read the pair as: `occ_ok = 1` — an interior gap + inside live native code, an alias seed will make it native; `occ_ok = 0` + with a CRC — the band's compiled occupant is resident but CRC-missing (data + inside the code range is rewritten at run time, or the section differs from + the compiled one), so the whole band runs interpreted and no seed helps; + `0x00000000` — nothing compiled spans this PC (BIOS / kernel / boot EXE). +- `ext_ra` — enrichment: `$ra` at the most recent external entry, i.e. the + caller that reached this interior. A PC reached only through a + function-pointer table shows the dispatcher's `ra` here, so the table's + call site is recoverable from the snapshot alone (the LOGO effect-handler + case) without a live `overlay_fp_log` window. `ext_ra == pc` means the + entry was a `jr ra` *return* from native code into an interpreted + continuation, not a call. + +The array is emitted inline and truncates before the trailing bitmap +diagnostics when the response buffer fills; a partial list is still valid JSON +(the cut lands between rows). Poll periodically to accumulate coverage. + +--- + ## Rule when the server can't answer your question If an inspection need isn't covered by the existing commands, **do not fall back to printf or log files**. Instead: diff --git a/runtime/include/dirty_ram_interp.h b/runtime/include/dirty_ram_interp.h index 515a9695..1fb60442 100644 --- a/runtime/include/dirty_ram_interp.h +++ b/runtime/include/dirty_ram_interp.h @@ -224,6 +224,30 @@ typedef struct { * 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 diff --git a/runtime/src/debug_server.c b/runtime/src/debug_server.c index ee94280e..48ab208f 100644 --- a/runtime/src/debug_server.c +++ b/runtime/src/debug_server.c @@ -2906,7 +2906,11 @@ static void handle_dirty_ram_stats(int id, const char *json) uint32_t out[5]); (void)json; - char buf[32 * 1024]; + /* 64 KiB (was 32): the per_pc array is emitted inline and each row grew by + * the occ_crc / ext_ra enrichment fields (2026-09-05). The tail reserve + * below still truncates the row list before the fixed diagnostics, so a + * larger buffer only means more per_pc rows fit before that cut. */ + char buf[64 * 1024]; int n = snprintf(buf, sizeof(buf), "{\"id\":%d,\"ok\":true,\"blocks_run\":%llu," "\"insns_run\":%llu,\"aborts\":%llu," @@ -2926,12 +2930,16 @@ static void handle_dirty_ram_stats(int id, const char *json) if (e->pc == 0 || e->hits == 0) continue; n += snprintf(buf + n, sizeof(buf) - n, "%s{\"pc\":\"0x%08X\",\"hits\":%llu,\"insns\":%llu," - "\"entries\":%llu}", + "\"entries\":%llu,\"occ_crc\":\"0x%08X\"," + "\"occ_ok\":%u,\"ext_ra\":\"0x%08X\"}", first ? "" : ",", (unsigned)e->pc, (unsigned long long)e->hits, (unsigned long long)e->insns, - (unsigned long long)e->entry_hits); + (unsigned long long)e->entry_hits, + (unsigned)e->occ_crc, + (unsigned)e->occ_ok, + (unsigned)e->last_ext_ra); first = 0; /* Reserve enough tail room for all bitmap/guard diagnostics below. */ if (n >= (int)sizeof(buf) - 2048) break; diff --git a/runtime/src/dirty_ram_interp.c b/runtime/src/dirty_ram_interp.c index 3da7fd6f..d8d218a4 100644 --- a/runtime/src/dirty_ram_interp.c +++ b/runtime/src/dirty_ram_interp.c @@ -2891,7 +2891,19 @@ static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_ * not a new entry. Anything else arrived from native code (a call or * a fresh dispatch) and is real interior-entry evidence for alias * seeding. */ - if (pc_entry && addr != g_dirty_interp_chain_target) pc_entry->entry_hits++; + if (pc_entry && addr != g_dirty_interp_chain_target) { + pc_entry->entry_hits++; + /* Enrichment, external entries only (this is a real native/dispatch + * arrival, not interp block chaining). occ_crc names which overlay is + * resident in this band; last_ext_ra names the caller that reached + * this interior. Both durable in the per_pc snapshot — no ring window, + * no offline join. See DirtyRamPcEntry. */ + extern uint32_t psx_overlay_resident_crc_at(uint32_t phys, int *valid); + int occ_ok = 0; + pc_entry->occ_crc = psx_overlay_resident_crc_at(phys, &occ_ok); + pc_entry->occ_ok = (uint8_t)occ_ok; + pc_entry->last_ext_ra = cpu->gpr[31]; + } g_dirty_interp_chain_target = 0; /* Block-entry ring buffer — answers "who tried to JALR into this RAM diff --git a/runtime/src/overlay_loader.c b/runtime/src/overlay_loader.c index a2b32480..e0b4bc27 100644 --- a/runtime/src/overlay_loader.c +++ b/runtime/src/overlay_loader.c @@ -504,6 +504,10 @@ static uint64_t s_diffgate_interp = 0; /* CPS interior re-entries sent to the /* interp because their candidate is */ /* still inside the diff verify budget */ static uint32_t s_last_crc = 0; +/* psx_overlay_resident_crc_at(phys, &valid) -- the occupant fingerprint the dirty + * interpreter stamps on each interpreted PC (DirtyRamPcEntry.occ_crc) -- is + * defined with the static-match cache below; s_last_crc is its DLL-path + * fallback. */ static uint32_t s_no_manifest = 0; /* exports skipped (no manifest range)*/ static uint64_t s_cand_overflow = 0; /* registrations dropped at CAND_CAP */ static uint64_t s_pair_aliases = 0; /* validated complete pairs deduped */ @@ -565,6 +569,8 @@ typedef struct { uint32_t expected_crc; uint32_t gen_sum; int matches; + uint32_t lo_min; /* span of the variant's code ranges (phys), for */ + uint32_t hi_max; /* psx_overlay_resident_crc_at() */ } StaticMatchCache; static StaticMatchCache s_static_match_cache[STATIC_MATCH_CACHE_CAP]; @@ -647,10 +653,62 @@ int psx_overlay_static_code_matches(const uint32_t *lo_len_pairs, entry->expected_crc = expected_crc; entry->gen_sum = gen_sum; entry->matches = matches; + uint32_t lo_min = 0xFFFFFFFFu, hi_max = 0; + for (uint32_t i = 0; i < count; i++) { + uint32_t lo = lo_len_pairs[i * 2u] & 0x1FFFFFFFu; + uint32_t hi = lo + lo_len_pairs[i * 2u + 1u]; + if (lo < lo_min) lo_min = lo; + if (hi > hi_max) hi_max = hi; + } + entry->lo_min = lo_min; + entry->hi_max = hi_max; } return matches; } +/* Occupant fingerprint for an interpreted PC (tier-1 enrichment, + * DirtyRamPcEntry.occ_crc / occ_ok). Scans the static-match cache for the + * variant whose code ranges span `phys`: + * - one that matched and whose pages are unchanged since (same + * page-generation test the dispatch fast path uses) -> its manifest CRC, + * *valid = 1. An interior the static walk missed, interpreted inside a + * validating variant: the normal Axis B seed. + * - otherwise the spanning variant most recently consulted -> its manifest + * CRC, *valid = 0. "Compiled for this band, resident, but NOT validating" + * -- a CRC-miss occupant (data inside the code range rewritten at run + * time, or a section that differs from the one compiled). This is the + * case a plain 0 would hide, and it is the one that explains a band that + * is compiled yet fully interpreted (SCENA16 at the title screen, + * 2026-09-05). + * - no spanning variant at all -> 0 / *valid = 0 (BIOS, kernel, boot EXE, + * or a band nothing was compiled for); DLL-path titles get the last + * residency hash as a hint instead. + * The CRC is the one the variant was compiled from, so it maps straight back + * to a capture / .EMI section offline. Called only on EXTERNAL interp + * entries, so a 4096-slot scan is fine. */ +uint32_t psx_overlay_resident_crc_at(uint32_t phys, int *valid) { + phys &= 0x1FFFFFFFu; + uint32_t stale = 0; + for (uint32_t i = 0; i < STATIC_MATCH_CACHE_CAP; i++) { + const StaticMatchCache *e = &s_static_match_cache[i]; + if (!e->ranges || phys < e->lo_min || phys >= e->hi_max) + continue; + if (e->matches) { + uint32_t gen_sum = 0; + for (uint32_t k = 0; k < e->count; k++) + gen_sum += overlay_watch_pagegen_sum(e->ranges[k * 2u] & 0x1FFFFFFFu, + e->ranges[k * 2u + 1u]); + if (gen_sum == e->gen_sum) { + if (valid) *valid = 1; + return e->expected_crc; + } + } + if (!stale) stale = e->expected_crc; + } + if (valid) *valid = 0; + return stale ? stale : s_last_crc; +} + void overlay_loader_static_match_stats(uint64_t *rehashes, uint64_t *crc_misses, uint64_t *gen_fastpath) { @@ -658,6 +716,8 @@ void overlay_loader_static_match_stats(uint64_t *rehashes, if (crc_misses) *crc_misses = s_static_match_crc_misses; if (gen_fastpath) *gen_fastpath = s_static_match_gen_fastpath; } +#else +uint32_t psx_overlay_resident_crc_at(uint32_t phys, int *valid) { (void)phys; if (valid) *valid = 0; return s_last_crc; } #endif /* ---- Per-DLL code-range manifest --------------------------------------- */