From 0c9f9c05a8c99c8dda4ec71275c9b777bbec6f1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Fri, 9 Oct 2026 17:41:30 +0200 Subject: [PATCH 1/2] GE dump recording: Track every VRAM block a range touches Both DirtyVRAM and GetTargetFlags counted blocks from the size alone, so a range starting partway into a block missed its last block: a write there didn't mark it dirty, and a snapshot didn't check it. And GetTargetFlags marked the partly covered last block clean, since its `i < blocks` was always true. Either way a later snapshot could be flagged unchanged while the dump didn't have the data. Memcpy and memset also marked partly covered blocks clean. Checked against the recorder's own copy of what the dump delivered: recording from savestates, 4511 of Cars Race-O-Rama's unchanged snapshots and 103 of MX vs ATV Reflex's didn't match VRAM (small textures at unaligned addresses), and none do now. Co-Authored-By: Claude Opus 5.5 --- GPU/Debugger/Record.cpp | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/GPU/Debugger/Record.cpp b/GPU/Debugger/Record.cpp index 4a0f7fab37..f2b07cdaac 100644 --- a/GPU/Debugger/Record.cpp +++ b/GPU/Debugger/Record.cpp @@ -102,8 +102,25 @@ void Recorder::DirtyAllVRAM(DirtyVRAMFlag flag) { } void Recorder::DirtyVRAM(u32 start, u32 sz, DirtyVRAMFlag flag) { - u32 count = (sz + DIRTY_VRAM_ROUND) >> DIRTY_VRAM_SHIFT; + if (sz == 0) + return; + // Every block the range touches, also when it starts partway into one. Only CLEAN (the dump has the data) + // leaves out partly covered blocks at the ends, since the rest of those isn't in the dump. u32 first = (start >> DIRTY_VRAM_SHIFT) & DIRTY_VRAM_MASK; + u32 count = ((start & DIRTY_VRAM_ROUND) + sz + DIRTY_VRAM_ROUND) >> DIRTY_VRAM_SHIFT; + if (flag == DirtyVRAMFlag::CLEAN) { + if ((start & DIRTY_VRAM_ROUND) != 0) { + first++; + count--; + } + if (count > 0 && ((start + sz) & DIRTY_VRAM_ROUND) != 0) + count--; + // Past the end of VRAM, only what's inside is in the dump. + if (first + count > DIRTY_VRAM_SIZE) + count = first < DIRTY_VRAM_SIZE ? DIRTY_VRAM_SIZE - first : 0; + if (count == 0) + return; + } if (first + count > DIRTY_VRAM_SIZE) { DirtyAllVRAM(flag); return; @@ -365,7 +382,8 @@ u32 Recorder::GetTargetFlags(u32 addr, u32 sizeInRAM) { bool isDirtyVRAM = false; bool isDrawnVRAM = false; uint32_t start = (addr >> DIRTY_VRAM_SHIFT) & DIRTY_VRAM_MASK; - uint32_t blocks = (sizeInRAM + DIRTY_VRAM_ROUND) >> DIRTY_VRAM_SHIFT; + // Every block the range touches, also when it starts partway into one. + uint32_t blocks = sizeInRAM == 0 ? 0 : ((addr & DIRTY_VRAM_ROUND) + sizeInRAM + DIRTY_VRAM_ROUND) >> DIRTY_VRAM_SHIFT; if (start + blocks >= DIRTY_VRAM_SIZE) return 0; bool startEven = (addr & DIRTY_VRAM_ROUND) == 0; @@ -376,9 +394,9 @@ u32 Recorder::GetTargetFlags(u32 addr, u32 sizeInRAM) { isDirtyVRAM = isDirtyVRAM || flag != DirtyVRAMFlag::CLEAN; isDrawnVRAM = isDrawnVRAM || flag == DirtyVRAMFlag::DRAWN; - // Mark the VRAM clean now that it's been copied to VRAM. + // Mark the VRAM clean now that it's been copied to the dump, except partly covered blocks at the ends. if (flag == DirtyVRAMFlag::UNKNOWN || flag == DirtyVRAMFlag::DIRTY) { - if ((i > 0 || startEven) && (i < blocks || endEven)) + if ((i > 0 || startEven) && (i + 1 < blocks || endEven)) dirtyVRAM[start + i] = DirtyVRAMFlag::CLEAN; } } From e705d62f0bfbcf470390398ec95b26c6852e72ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Fri, 9 Oct 2026 17:34:49 +0200 Subject: [PATCH 2/2] GL: Only set GL_DEPTH_STENCIL_TEXTURE_MODE where stencil texturing exists It needs GL 4.3 or GLES 3.1. On Apple's GL 4.1 every depth texture bind (Cars Race-O-Rama's depal from depth, for one) raised GL_INVALID_ENUM, which then showed up at whatever checked glGetError next. Without the mode a depth-stencil texture samples depth, so nothing else changes. Co-Authored-By: Claude Opus 5.5 --- Common/GPU/OpenGL/GLQueueRunner.cpp | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/Common/GPU/OpenGL/GLQueueRunner.cpp b/Common/GPU/OpenGL/GLQueueRunner.cpp index 6976e73ac1..3af2f4fd2f 100644 --- a/Common/GPU/OpenGL/GLQueueRunner.cpp +++ b/Common/GPU/OpenGL/GLQueueRunner.cpp @@ -898,6 +898,9 @@ void GLQueueRunner::PerformRenderPass(const GLRStep &step, bool first, bool last }; GLRect2D scissorRc = { -1, -1, -1, -1 }; + // GL_DEPTH_STENCIL_TEXTURE_MODE needs GL 4.3 or GLES 3.1. Without it a depth-stencil texture samples depth, + // and setting the mode is an error (Apple's GL 4.1). + const bool stencilTexturing = gl_extensions.ARB_stencil_texturing || (gl_extensions.IsGLES && gl_extensions.VersionGEThan(3, 1)); CHECK_GL_ERROR_IF_DEBUG(); auto &commands = step.commands; @@ -1219,15 +1222,19 @@ void GLQueueRunner::PerformRenderPass(const GLRStep &step, bool first, bool last glBindTexture(GL_TEXTURE_2D, c.bind_fb_texture.framebuffer->z_stencil_texture.texture); curTex[slot] = &c.bind_fb_texture.framebuffer->z_stencil_texture; } - // This should be uncommon, so always set the mode. - glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_STENCIL_TEXTURE_MODE, GL_DEPTH_COMPONENT); + // This should be uncommon, so always set the mode (where it exists). + if (stencilTexturing) { + glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_STENCIL_TEXTURE_MODE, GL_DEPTH_COMPONENT); + } } else if (c.bind_fb_texture.aspect == GL_STENCIL_BUFFER_BIT) { if (curTex[slot] != &c.bind_fb_texture.framebuffer->z_stencil_texture) { glBindTexture(GL_TEXTURE_2D, c.bind_fb_texture.framebuffer->z_stencil_texture.texture); curTex[slot] = &c.bind_fb_texture.framebuffer->z_stencil_texture; } - // This should be uncommon, so always set the mode. - glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_STENCIL_TEXTURE_MODE, GL_STENCIL_INDEX); + // This should be uncommon, so always set the mode (where it exists). + if (stencilTexturing) { + glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_STENCIL_TEXTURE_MODE, GL_STENCIL_INDEX); + } } else { curTex[slot] = nullptr; }