mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
752f8ff2e7
docs/SEGMENT_AWARE_CODE.md §5.7. Overlay bytes are segment-free, but a PC is not: a KUSEG, KSEG0 or KSEG1 entry of the same word links, traps, fetches and stamps store PCs in its own segment, so each needs a shard compiled for that segment. Capture kept only the physical word, so every shard was compiled for KSEG0 and #417's gate interpreted the other segments forever. dirty_ram_interp.h adds one sibling bitmap per segment that maps RAM (g_dirty_ram_dispatch_seg_bitmap[3]; 768 KiB for the 8 MiB backing). dirty_ram_dispatch_inner sets the bit for the full PC next to the existing any-segment bit, once per interpreted dispatch. A PC in 0x20000000-0x7FFFFFFF or KSEG2 addresses no RAM in Beetle and gets no bit. dirty_ram_dispatch_evidence_clear() drops both together at every site that dropped dispatch evidence: boot reset, a store into an executed page, DMA preservation and the autocapture epoch. Snapshot jobs copy the siblings. Capture schema v3 adds, per region, "dispatch_entry_segments": {"kuseg": [...], "kseg0": [...], "kseg1": [...]} with each PC spelled in its segment. dispatch_entry_pcs keeps its v2 meaning (dispatched in any segment) and KSEG0 spelling, so existing readers are unaffected. A record without the field is KSEG0 only. Guest behaviour is unchanged: the bitmaps are host-only capture evidence and are not saved in savestates. overlay_capture_retry_test checks the v3 record through both writers (the background epoch and the synchronous final capture), including that a cleared epoch's segment does not outlive it. psx_ram_runtime_map_stubs.c defines the new bitmap, which memory.c's write paths now reference, so psx_ram_runtime_map_test still links on GNU toolchains. The disabled interpreter_perf_guards check follows the evidence clear (it would otherwise contradict overlay_segment_gate's wiring check). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
234 lines
9.2 KiB
C++
234 lines
9.2 KiB
C++
#include "code_provider.h"
|
|
#include "dirty_ram_interp.h"
|
|
#include "overlay_capture.h"
|
|
|
|
#include "psx_sdl.h"
|
|
|
|
#include <cstddef>
|
|
#include <cstdio>
|
|
#include <cstdint>
|
|
#include <cstdlib>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <iterator>
|
|
#include <string>
|
|
|
|
extern "C" int overlay_capture_test_write_state(void);
|
|
extern "C" unsigned overlay_capture_test_write_attempts(void);
|
|
extern "C" int overlay_capture_test_provider_pending(void);
|
|
|
|
extern "C" {
|
|
uint32_t g_dirty_ram_exec_pc_bitmap[DIRTY_RAM_EXEC_BITMAP_WORDS]{};
|
|
uint32_t g_dirty_ram_dispatch_pc_bitmap[DIRTY_RAM_EXEC_BITMAP_WORDS]{};
|
|
uint32_t g_dirty_ram_dispatch_seg_bitmap[PSX_CODE_SEGMENT_COUNT]
|
|
[DIRTY_RAM_EXEC_BITMAP_WORDS]{};
|
|
uint32_t g_dirty_ram_exec_page_bitmap[DIRTY_RAM_EXEC_PAGE_BITMAP_WORDS]{};
|
|
uint64_t g_dirty_ram_insns_run = 0;
|
|
uint64_t g_dirty_window_dispatches = 0;
|
|
uint64_t s_frame_count = 0;
|
|
uint32_t g_overlay_region_floor = 0x00010000u;
|
|
}
|
|
|
|
namespace {
|
|
|
|
uint8_t g_ram[2u * 1024u * 1024u]{};
|
|
uint32_t g_dirty_pages[DIRTY_RAM_EXEC_PAGE_BITMAP_WORDS]{};
|
|
int g_provider_calls = 0;
|
|
int g_provider_accepts = 0;
|
|
|
|
int provider_available() { return 1; }
|
|
int provider_request() {
|
|
++g_provider_calls;
|
|
if (g_provider_calls == 1) return 0; // inject one spawn failure
|
|
++g_provider_accepts;
|
|
return 1;
|
|
}
|
|
int provider_busy() { return 0; }
|
|
|
|
const CodeProvider kProvider = {
|
|
"capture-retry-test", provider_available, provider_request,
|
|
provider_busy, nullptr,
|
|
};
|
|
|
|
bool bit_is_set(const uint32_t *bitmap, uint32_t phys) {
|
|
const uint32_t word = phys >> 2;
|
|
return ((bitmap[word >> 5] >> (word & 31u)) & 1u) != 0;
|
|
}
|
|
|
|
/* An interpreted dispatch at `phys`, entered through segment `seg` (0 KUSEG,
|
|
* 1 KSEG0, 2 KSEG1), as dirty_ram_dispatch_inner records it. */
|
|
void set_exec(uint32_t phys, int seg = 1) {
|
|
const uint32_t word = phys >> 2;
|
|
g_dirty_ram_exec_pc_bitmap[word >> 5] |= 1u << (word & 31u);
|
|
g_dirty_ram_dispatch_pc_bitmap[word >> 5] |= 1u << (word & 31u);
|
|
g_dirty_ram_dispatch_seg_bitmap[seg][word >> 5] |= 1u << (word & 31u);
|
|
const uint32_t page = phys >> 12;
|
|
g_dirty_ram_exec_page_bitmap[page >> 5] |= 1u << (page & 31u);
|
|
g_dirty_pages[page >> 5] |= 1u << (page & 31u);
|
|
}
|
|
|
|
void set_dirty_page(uint32_t phys) {
|
|
const uint32_t page = phys >> 12;
|
|
g_dirty_pages[page >> 5] |= 1u << (page & 31u);
|
|
}
|
|
|
|
bool wait_for_failed_write() {
|
|
for (int i = 0; i < 200; ++i) {
|
|
overlay_autocapture_tick();
|
|
if (overlay_capture_test_write_state() == 0 &&
|
|
overlay_capture_test_write_attempts() >= 1u)
|
|
return true;
|
|
SDL_Delay(5);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool wait_for_provider_pending() {
|
|
for (int i = 0; i < 200; ++i) {
|
|
overlay_autocapture_tick();
|
|
if (overlay_capture_test_provider_pending()) return true;
|
|
SDL_Delay(5);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
std::string read_all(const std::filesystem::path &path) {
|
|
std::ifstream input(path, std::ios::binary);
|
|
return std::string(std::istreambuf_iterator<char>(input),
|
|
std::istreambuf_iterator<char>());
|
|
}
|
|
|
|
int fail(const char *message, const std::filesystem::path &root) {
|
|
std::fprintf(stderr, "FAIL: %s\n", message);
|
|
overlay_capture_wait_pending();
|
|
std::error_code ignored;
|
|
std::filesystem::remove_all(root, ignored);
|
|
SDL_Quit();
|
|
return 1;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
extern "C" uint8_t *memory_get_ram_ptr(void) { return g_ram; }
|
|
extern "C" uint32_t dirty_ram_get_bitmap_word_count(void) {
|
|
return DIRTY_RAM_EXEC_PAGE_BITMAP_WORDS;
|
|
}
|
|
extern "C" uint32_t dirty_ram_get_bitmap_word(uint32_t index) {
|
|
return index < DIRTY_RAM_EXEC_PAGE_BITMAP_WORDS ? g_dirty_pages[index] : 0u;
|
|
}
|
|
extern "C" int cdrom_load_in_progress(void) { return 0; }
|
|
extern "C" int fntrace_is_game_started(void) { return 1; }
|
|
extern "C" void overlay_loader_check_cache(uint32_t, uint32_t,
|
|
const uint8_t *) {}
|
|
extern "C" int overlay_loader_registered_count(void) { return 0; }
|
|
extern "C" const CodeProvider *code_provider_active(void) { return &kProvider; }
|
|
extern "C" uint32_t crc32_compute(const uint8_t *data, size_t size) {
|
|
uint32_t hash = 2166136261u;
|
|
for (size_t i = 0; i < size; ++i) hash = (hash ^ data[i]) * 16777619u;
|
|
return hash ? hash : 1u;
|
|
}
|
|
|
|
int main() {
|
|
/* psx_sdl_init: 0 on success under BOTH backends. SDL3's SDL.h no longer
|
|
* pulls SDL_main.h, and its SDL_Init returns bool (true = success), so
|
|
* the previous SDL_SetMainReady() + `SDL_Init(0) != 0` rotted twice over
|
|
* when the default backend moved to SDL3: it did not compile, and the
|
|
* check was inverted. SDL_SetMainReady is unnecessary here — this test
|
|
* never uses SDL_main. */
|
|
if (psx_sdl_init(0) != 0) {
|
|
std::fprintf(stderr, "FAIL: SDL_Init: %s\n", SDL_GetError());
|
|
return 1;
|
|
}
|
|
|
|
const auto root = std::filesystem::temp_directory_path() /
|
|
("psxrecomp-capture-retry-" + std::to_string(
|
|
static_cast<unsigned long long>(SDL_GetPerformanceCounter())));
|
|
const auto capture = root / "overlay_captures.json";
|
|
std::error_code ignored;
|
|
std::filesystem::remove_all(root, ignored);
|
|
|
|
overlay_capture_set_path(capture.string().c_str());
|
|
overlay_capture_set_enabled(1);
|
|
overlay_autocapture_set_enabled(1);
|
|
g_ram[0x10000] = 0x11;
|
|
overlay_capture_on_dma(0x10000u, 4u, &g_ram[0x10000]);
|
|
set_exec(0x10000u, 0); /* this epoch enters 0x10000 at KUSEG */
|
|
g_dirty_window_dispatches = 128u;
|
|
g_dirty_ram_insns_run = 100000u;
|
|
s_frame_count = 120u;
|
|
overlay_autocapture_tick();
|
|
|
|
if (!wait_for_failed_write())
|
|
return fail("failed periodic write was not retained", root);
|
|
if (bit_is_set(g_dirty_ram_exec_pc_bitmap, 0x10000u))
|
|
return fail("accepted snapshot did not begin a new live epoch", root);
|
|
|
|
/* This evidence belongs to the newer live epoch and must survive retrying
|
|
* the immutable old snapshot. */
|
|
set_exec(0x10004u);
|
|
std::filesystem::create_directories(root);
|
|
s_frame_count = 1000u;
|
|
overlay_autocapture_tick();
|
|
if (!wait_for_provider_pending())
|
|
return fail("durable retry did not retain failed provider request", root);
|
|
if (!bit_is_set(g_dirty_ram_exec_pc_bitmap, 0x10004u))
|
|
return fail("old snapshot retry erased newer live evidence", root);
|
|
|
|
const std::string first_epoch = read_all(capture);
|
|
if (first_epoch.find("0x80010000") == std::string::npos ||
|
|
first_epoch.find("0x80010004") != std::string::npos)
|
|
return fail("retried file did not preserve the immutable old epoch", root);
|
|
/* The background writer formats the segments it copied with the epoch. */
|
|
if (first_epoch.find("\"kuseg\": [\"0x00010000\"]") == std::string::npos)
|
|
return fail("the retried epoch lost its dispatch segments", root);
|
|
|
|
/* Retry only the provider start. No RAM snapshot or evidence clear should
|
|
* occur, and acceptance must be recorded exactly once. */
|
|
s_frame_count = 2000u;
|
|
overlay_autocapture_tick();
|
|
if (g_provider_calls != 2 || g_provider_accepts != 1 ||
|
|
overlay_capture_test_provider_pending())
|
|
return fail("provider spawn failure was not retried exactly once", root);
|
|
if (!bit_is_set(g_dirty_ram_exec_pc_bitmap, 0x10004u))
|
|
return fail("provider retry mutated the newer live epoch", root);
|
|
|
|
/* A dirty but never-executed neighbor must not inflate the final snapshot
|
|
* into a mutable multi-page region. This is the shutdown path that made
|
|
* long coverage sessions grow by gigabytes. */
|
|
set_dirty_page(0x11000u);
|
|
g_ram[0x11000] = 0x77;
|
|
/* Schema v3 (docs/SEGMENT_AWARE_CODE.md §5.7): each dispatch entry's
|
|
* segments, spelled in the segment. 0x10008 is entered at KUSEG and
|
|
* KSEG1, 0x1000C at KSEG1 only; dispatch_entry_pcs keeps its v2 KSEG0
|
|
* spelling of every entry. */
|
|
set_exec(0x10008u, 0);
|
|
set_exec(0x10008u, 2);
|
|
set_exec(0x1000Cu, 2);
|
|
/* 0x10000's KUSEG entry belonged to the first epoch, which the accepted
|
|
* snapshot cleared; now it is entered at KSEG0 only. */
|
|
set_exec(0x10000u, 1);
|
|
overlay_capture_write_json();
|
|
const std::string final_epoch = read_all(capture);
|
|
if (final_epoch.find("\"size\": 8196") != std::string::npos)
|
|
return fail("final snapshot included dirty unexecuted pages", root);
|
|
if (final_epoch.find("\"schema\": \"psxrecomp overlay capture v3\"") ==
|
|
std::string::npos)
|
|
return fail("capture is not schema v3", root);
|
|
if (final_epoch.find("\"dispatch_entry_pcs\": [\"0x80010000\", \"0x80010004\", "
|
|
"\"0x80010008\", \"0x8001000C\"]") == std::string::npos)
|
|
return fail("dispatch_entry_pcs lost its v2 meaning", root);
|
|
if (final_epoch.find("\"dispatch_entry_segments\": {"
|
|
"\"kuseg\": [\"0x00010008\"], "
|
|
"\"kseg0\": [\"0x80010000\", \"0x80010004\"], "
|
|
"\"kseg1\": [\"0xA0010008\", \"0xA001000C\"]}") ==
|
|
std::string::npos)
|
|
return fail("dispatch_entry_segments does not name each entry's segments "
|
|
"(or kept a cleared epoch's segment)", root);
|
|
|
|
overlay_capture_wait_pending();
|
|
std::filesystem::remove_all(root, ignored);
|
|
SDL_Quit();
|
|
std::puts("PASS: periodic capture and provider failures retain additive epochs");
|
|
return 0;
|
|
}
|