SaveState: Keep JIT blocks in module code a load doesn't change

Loading a state started the JIT over, so every block the game runs had to
be compiled again, which stutters. Now the loaded modules' code is
compared with the state's as RAM is loaded (a chunk that already matches
isn't copied either), and blocks entirely inside unchanged code stay.
Everything else is invalidated as before, and without a JIT, or with no
unchanged code, the JIT still starts over.

The JIT throws its blocks away if the loaded state needs code they weren't
compiled for (rounding mode switches, or no default prefixes at block
start). The other way round it keeps them, along with the flags they were
compiled under: before, a loaded hasSetRounding of false, or the reset of
lastSetRounding, had the next compile rebuild everything anyway.

In the three seconds after loading a state, Ridge Racer compiles 28 blocks
instead of 4823, Force Unleashed 31 instead of 7727. Frames rendered after
the load are identical either way.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Henrik RydgårdandClaude Opus 5.5 committed 2026-10-19 19:47:05 +02:00
1 parent 3ac908345c
commit 79d340f4fe
12 files changed
+206 -30

No files matched your search

+61 -4
View File
@@ -449,7 +449,60 @@ bool Reinit() {
return success;
}
static void DoMemoryVoid(PointerWrap &p, uint32_t start, uint32_t size) {
// Loads size bytes at start, comparing the parts of it in compare->ranges with what's there as it
// goes. A chunk that already matches doesn't need copying, and the matching parts are reported in
// compare->unchanged. The rest is a plain copy.
static void CopyComparing(uint8_t *dest, const uint8_t *src, u32 start, u32 size, RamCompare *compare) {
// In the size of the JIT's block lookup pages. One changed byte costs its chunk's blocks.
constexpr u32 CHUNK_SIZE = 1024;
// As offsets into the copy, in order, merged.
std::vector<std::pair<u32, u32>> ranges;
for (const auto &[address, length] : compare->ranges) {
const u32 rangeStart = std::max(address & 0x3FFFFFFF, start);
const u32 rangeEnd = std::min((address & 0x3FFFFFFF) + length, start + size);
if (rangeStart < rangeEnd) {
ranges.emplace_back(rangeStart - start, rangeEnd - start);
}
}
std::sort(ranges.begin(), ranges.end());
u32 pos = 0;
for (const auto &[rangeStart, rangeEnd] : ranges) {
const u32 from = std::max(rangeStart, pos);
if (from >= rangeEnd) {
continue;
}
if (from > pos) {
ParallelMemcpy(&g_threadManager, dest + pos, src + pos, from - pos);
}
u32 runStart = 0;
bool inRun = false;
for (u32 chunk = from; chunk < rangeEnd; chunk += CHUNK_SIZE) {
const u32 chunkSize = std::min(CHUNK_SIZE, rangeEnd - chunk);
const bool same = memcmp(dest + chunk, src + chunk, chunkSize) == 0;
if (same && !inRun) {
runStart = chunk;
inRun = true;
} else if (!same) {
memcpy(dest + chunk, src + chunk, chunkSize);
if (inRun) {
compare->unchanged.emplace_back(start + runStart, chunk - runStart);
inRun = false;
}
}
}
if (inRun) {
compare->unchanged.emplace_back(start + runStart, rangeEnd - runStart);
}
pos = rangeEnd;
}
if (pos < size) {
ParallelMemcpy(&g_threadManager, dest + pos, src + pos, size - pos);
}
}
static void DoMemoryVoid(PointerWrap &p, uint32_t start, uint32_t size, RamCompare *compare = nullptr) {
uint8_t *d = GetPointerWriteOrException(start);
uint8_t *&storage = *p.ptr;
@@ -463,7 +516,11 @@ static void DoMemoryVoid(PointerWrap &p, uint32_t start, uint32_t size) {
switch (p.mode) {
case PointerWrap::MODE_READ:
ParallelMemcpy(&g_threadManager, d, storage, size);
if (compare && !compare->ranges.empty()) {
CopyComparing(d, storage, start, size, compare);
} else {
ParallelMemcpy(&g_threadManager, d, storage, size);
}
break;
case PointerWrap::MODE_WRITE:
ParallelMemcpy(&g_threadManager, storage, d, size);
@@ -483,7 +540,7 @@ static void DoMemoryVoid(PointerWrap &p, uint32_t start, uint32_t size) {
storage += size;
}
void DoState(PointerWrap &p) {
void DoState(PointerWrap &p, RamCompare *compare) {
auto s = p.Section("Memory", 1, 3);
if (!s)
return;
@@ -529,7 +586,7 @@ void DoState(PointerWrap &p) {
}
}
DoMemoryVoid(p, PSP_GetKernelMemoryBase(), g_MemorySize);
DoMemoryVoid(p, PSP_GetKernelMemoryBase(), g_MemorySize, compare);
p.DoMarker("RAM");
DoMemoryVoid(p, PSP_GetVidMemBase(), VRAM_SIZE);