From 84582c10f157771b4cdb261c1a7614e4da70a95c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 5 Oct 2026 15:18:01 -0600 Subject: [PATCH 01/11] GPU: Add the GE's depth buffer layout in VRAM Where the GE stores each depth pixel, from the EDRAM address translation and the color format: with 32-bit color, address bits 5 up to log2(T) - 1 rotate left by one, then (T << 3) | 0x40 is XORed in (0x600, and bits 9 and 10 swapped, at T 0). The CPU sees the same mapping through VRAM's 0x04200000 and 0x04600000 mirrors. Measured in ppsspp-re geprobe exp8-10 (every pixel of 512x272 buffers at each T) and exp88; the unit test checks it against the model fitted there. Co-Authored-By: Claude Opus 5.5 (1M context) --- GPU/CMakeLists.txt | 1 + GPU/Common/DepthSwizzle.h | 62 ++++++++++++++++++++++++++ GPU/GPU.vcxproj | 1 + GPU/GPU.vcxproj.filters | 3 ++ UWP/GPU_UWP/GPU_UWP.vcxproj | 1 + UWP/GPU_UWP/GPU_UWP.vcxproj.filters | 1 + unittest/UnitTest.cpp | 67 +++++++++++++++++++++++++++++ 7 files changed, 136 insertions(+) create mode 100644 GPU/Common/DepthSwizzle.h diff --git a/GPU/CMakeLists.txt b/GPU/CMakeLists.txt index 2c48ce1ff7..81b942465e 100644 --- a/GPU/CMakeLists.txt +++ b/GPU/CMakeLists.txt @@ -80,6 +80,7 @@ set(GPU_SOURCES Common/DepthBufferCommon.cpp Common/DepthRaster.cpp Common/DepthRaster.h + Common/DepthSwizzle.h Common/TextureShaderCommon.cpp Common/TextureShaderCommon.h Common/DepalettizeShaderCommon.cpp diff --git a/GPU/Common/DepthSwizzle.h b/GPU/Common/DepthSwizzle.h new file mode 100644 index 0000000000..abaf69a52c --- /dev/null +++ b/GPU/Common/DepthSwizzle.h @@ -0,0 +1,62 @@ +// Copyright (c) 2012- PPSSPP Project. + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 2.0 or later versions. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License 2.0 for more details. + +// A copy of the GPL 2.0 should have been included with the program. +// If not, see http://www.gnu.org/licenses/ + +// Official git repository and contact information can be found at +// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. + +#pragma once + +#include + +// Where the GE stores a depth buffer in VRAM (ppsspp-re geprobe exp8-exp10, docs/ge-precision-and-depth-layout.md). +// The depth pixel at linear VRAM offset L (zbuf + (y * stride + x) * 2) is stored at Stored(L): with a 32-bit +// color buffer, address bits 5 to log2(T) - 1 rotate left by one (bits 9 and 10 swap at T 0), then +// (T << 3) | 0x40 is XORed in (0x600 at T 0). T is sceGeEdramSetAddrTranslation's value: 0, 0x200, 0x400, +// 0x800 or 0x1000. The offsets are absolute, independent of the buffer's address and stride. +// +// Bits 0-4 never change, so each 32-byte aligned run of 16 pixels is stored together. The CPU (and texturing) +// sees the same mapping through VRAM's mirrors: an access at 0x04200000 + L goes to Stored(L) of the 16-bit +// layout, at 0x04600000 + L to the 32-bit one's (exp88). +struct DepthLayout { + uint32_t rotMask = 0; + int rotShift = 0; + uint32_t xorBits = 0; + + uint32_t Stored(uint32_t offset) const { + const uint32_t f = offset & rotMask; + return ((offset & ~rotMask) | (((f << 1) | (f >> rotShift)) & rotMask)) ^ xorBits; + } +}; + +inline DepthLayout GetDepthLayout(uint32_t translation, bool color32) { + DepthLayout layout; + if (translation == 0) { + layout.xorBits = 0x600; + if (color32) { + layout.rotMask = 0x600; + layout.rotShift = 1; + } + return layout; + } + layout.xorBits = (translation << 3) | 0x40; + int top = 0; + while ((2u << top) <= translation) + top++; + // Bits 5 to top - 1. + if (color32 && top - 5 > 1) { + layout.rotMask = ((1u << (top - 5)) - 1) << 5; + layout.rotShift = top - 5 - 1; + } + return layout; +} diff --git a/GPU/GPU.vcxproj b/GPU/GPU.vcxproj index 0ca125648b..5ace859f45 100644 --- a/GPU/GPU.vcxproj +++ b/GPU/GPU.vcxproj @@ -256,6 +256,7 @@ + diff --git a/GPU/GPU.vcxproj.filters b/GPU/GPU.vcxproj.filters index 3c548501ce..f6251bcb9f 100644 --- a/GPU/GPU.vcxproj.filters +++ b/GPU/GPU.vcxproj.filters @@ -264,6 +264,9 @@ Common + + Common + Common diff --git a/UWP/GPU_UWP/GPU_UWP.vcxproj b/UWP/GPU_UWP/GPU_UWP.vcxproj index 1196f3355d..8c7bed96ca 100644 --- a/UWP/GPU_UWP/GPU_UWP.vcxproj +++ b/UWP/GPU_UWP/GPU_UWP.vcxproj @@ -86,6 +86,7 @@ + diff --git a/UWP/GPU_UWP/GPU_UWP.vcxproj.filters b/UWP/GPU_UWP/GPU_UWP.vcxproj.filters index 53d21d9d7b..bab77cdbfa 100644 --- a/UWP/GPU_UWP/GPU_UWP.vcxproj.filters +++ b/UWP/GPU_UWP/GPU_UWP.vcxproj.filters @@ -165,6 +165,7 @@ Debugger + diff --git a/unittest/UnitTest.cpp b/unittest/UnitTest.cpp index 44411c379d..d382e34b1f 100644 --- a/unittest/UnitTest.cpp +++ b/unittest/UnitTest.cpp @@ -115,6 +115,7 @@ #include "Core/HLE/sceCtrl.h" #include "Core/Util/PathUtil.h" #include "Core/MIPS/MIPSVFPUUtils.h" +#include "GPU/Common/DepthSwizzle.h" #include "GPU/Common/TextureDecoder.h" #include "GPU/Common/GPUStateUtils.h" #include "GPU/Math3D.h" @@ -2437,6 +2438,71 @@ static bool TestWrapText() { return true; } +// Against ppsspp-re geprobe's swz.py, the layout that fits exp8-exp10. +static bool TestDepthSwizzle() { + static const struct { + uint32_t translation; + bool color32; + uint32_t linear; + uint32_t stored; + } cases[] = { + { 0x0, false, 0x088000, 0x088600 }, + { 0x0, false, 0x08a0e2, 0x08a6e2 }, + { 0x0, false, 0x1ffffe, 0x1ff9fe }, + { 0x0, false, 0x14b9ac, 0x14bfac }, + { 0x0, false, 0x09a782, 0x09a182 }, + { 0x0, true, 0x088000, 0x088600 }, + { 0x0, true, 0x08a0e2, 0x08a6e2 }, + { 0x0, true, 0x1ffffe, 0x1ff9fe }, + { 0x0, true, 0x1944c8, 0x1944c8 }, + { 0x0, true, 0x03171e, 0x03111e }, + { 0x200, false, 0x088000, 0x089040 }, + { 0x200, false, 0x08a0e2, 0x08b0a2 }, + { 0x200, false, 0x1ffffe, 0x1fefbe }, + { 0x200, false, 0x04a2ca, 0x04b28a }, + { 0x200, false, 0x06063a, 0x06167a }, + { 0x200, true, 0x088000, 0x089040 }, + { 0x200, true, 0x08a0e2, 0x08b182 }, + { 0x200, true, 0x1ffffe, 0x1fefbe }, + { 0x200, true, 0x176772, 0x1776b2 }, + { 0x200, true, 0x03b640, 0x03a6c0 }, + { 0x400, false, 0x088000, 0x08a040 }, + { 0x400, false, 0x08a0e2, 0x0880a2 }, + { 0x400, false, 0x1ffffe, 0x1fdfbe }, + { 0x400, false, 0x0dbd9c, 0x0d9ddc }, + { 0x400, false, 0x026654, 0x024614 }, + { 0x400, true, 0x088000, 0x08a040 }, + { 0x400, true, 0x08a0e2, 0x088182 }, + { 0x400, true, 0x1ffffe, 0x1fdfbe }, + { 0x400, true, 0x058028, 0x05a008 }, + { 0x400, true, 0x1bc0d8, 0x1be1d8 }, + { 0x800, false, 0x088000, 0x08c040 }, + { 0x800, false, 0x08a0e2, 0x08e0a2 }, + { 0x800, false, 0x1ffffe, 0x1fbfbe }, + { 0x800, false, 0x1ac354, 0x1a8314 }, + { 0x800, false, 0x047882, 0x0438c2 }, + { 0x800, true, 0x088000, 0x08c040 }, + { 0x800, true, 0x08a0e2, 0x08e182 }, + { 0x800, true, 0x1ffffe, 0x1fbfbe }, + { 0x800, true, 0x0f6704, 0x0f2664 }, + { 0x800, true, 0x05ce3c, 0x058c3c }, + { 0x1000, false, 0x088000, 0x080040 }, + { 0x1000, false, 0x08a0e2, 0x0820a2 }, + { 0x1000, false, 0x1ffffe, 0x1f7fbe }, + { 0x1000, false, 0x1b2b52, 0x1bab12 }, + { 0x1000, false, 0x03c876, 0x034836 }, + { 0x1000, true, 0x088000, 0x080040 }, + { 0x1000, true, 0x08a0e2, 0x082182 }, + { 0x1000, true, 0x1ffffe, 0x1f7fbe }, + { 0x1000, true, 0x07ec5e, 0x0768fe }, + { 0x1000, true, 0x0e498c, 0x0ec36c }, + }; + for (const auto &c : cases) { + EXPECT_EQ_HEX(GetDepthLayout(c.translation, c.color32).Stored(c.linear), c.stored); + } + return true; +} + static bool TestSmallDataConvert() { float f[4] = { 1.0f / 255.0f, 2.0f / 255.0f, 3.0f / 255.0f, 4.0f / 255.f }; uint32_t result = Float4ToUint8x4_NoClamp(f); @@ -3191,6 +3257,7 @@ TestItem availableTests[] = { TEST_ITEM(MpegCsc), TEST_ITEM(SplineTessellation), TEST_ITEM(GEMath), + TEST_ITEM(DepthSwizzle), TEST_ITEM(Demangle), TEST_ITEM(TextureReplacer), TEST_ITEM(UITabOrder), From a11fd225538932d6d67a7e327818b0293be674b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 5 Oct 2026 15:27:15 -0600 Subject: [PATCH 02/11] SoftGPU: Store depth in VRAM as the GE does The software renderer kept depth linear in VRAM, where the GE stores it in the layout GPU/Common/DepthSwizzle.h describes, from the EDRAM address translation and the color format. So a texture or anything else sharing VRAM with a depth buffer saw depth land in the wrong places (the player's shadow in Silent Hill: Origins, #22415). Now every depth access goes through the layout: the generic pixel functions, the 2x2 depth test, the depth clear in 16-pixel runs, and the x86 pixel JIT, whose IDs carry the translation so the layout's constants are compiled in. It changes with the color format and sceGeEdramSetAddrTranslation, after a flush. Reading VRAM through its 0x04200000 and 0x04600000 mirrors undoes the layout, as the GE does: texturing from them (drawn from a linear copy made after a flush), and block transfers from or to them (Hayate no Gotoku restores its depth buffer through 0x04600000, #17878). Saved and debugger depth buffers read through the layout, so they're linear. Socom's lens flare dump now matches a PSP replay where it changes. About 2-3% more cycles in God of War, Tekken 6, Wipeout and Burnout. Co-Authored-By: Claude Opus 5.5 (1M context) --- GPU/GPUCommon.cpp | 6 +- GPU/GPUCommon.h | 6 +- GPU/Software/BinManager.cpp | 35 +++++++++++ GPU/Software/BinManager.h | 3 + GPU/Software/DrawPixel.h | 1 + GPU/Software/DrawPixelX86.cpp | 70 +++++++++++++--------- GPU/Software/FuncId.cpp | 9 +++ GPU/Software/FuncId.h | 13 ++-- GPU/Software/Rasterizer.cpp | 23 ++++++-- GPU/Software/SoftGpu.cpp | 101 +++++++++++++++++++++++++++----- GPU/Software/SoftGpu.h | 42 ++++++++++++- unittest/TestSoftwareGPUJit.cpp | 9 ++- 12 files changed, 259 insertions(+), 59 deletions(-) diff --git a/GPU/GPUCommon.cpp b/GPU/GPUCommon.cpp index 44d1852279..a512cc5033 100644 --- a/GPU/GPUCommon.cpp +++ b/GPU/GPUCommon.cpp @@ -2015,8 +2015,10 @@ void GPUCommon::DoBlockTransfer(u32 skipDrawReason) { // Can use GetPointerUnchecked because we checked the addresses above. We could also avoid them // entirely by walking a couple of pointers... - // Simple case: just a straight copy, no overlap or wrapping. - if (srcStride == dstStride && (u32)width == srcStride && !srcDstOverlap && srcValid && dstValid) { + if (CopyBlockTransfer(srcBasePtr, srcStride, srcX, srcY, dstBasePtr, dstStride, dstX, dstY, width, height, bpp)) { + // The backend copied it. + } else if (srcStride == dstStride && (u32)width == srcStride && !srcDstOverlap && srcValid && dstValid) { + // Simple case: just a straight copy, no overlap or wrapping. u32 srcLineStartAddr = srcBasePtr + (srcY * srcStride + srcX) * bpp; u32 dstLineStartAddr = dstBasePtr + (dstY * dstStride + dstX) * bpp; u32 bytesToCopy = width * height * bpp; diff --git a/GPU/GPUCommon.h b/GPU/GPUCommon.h index a0bcbf9403..56ab046a4b 100644 --- a/GPU/GPUCommon.h +++ b/GPU/GPUCommon.h @@ -144,7 +144,7 @@ public: // For example, a debugger is active. bool ShouldSplitOverGe() const; - uint32_t SetAddrTranslation(uint32_t value); + virtual uint32_t SetAddrTranslation(uint32_t value); uint32_t GetAddrTranslation(); virtual void SetDisplayFramebuffer(u32 framebuf, u32 stride, GEBufferFormat format) = 0; @@ -344,6 +344,10 @@ protected: void FastLoadBoneMatrix(u32 target); void FlushImm(); void DoBlockTransfer(u32 skipDrawReason); + // Does a block transfer's copy itself and returns true, when the backend needs to. + virtual bool CopyBlockTransfer(u32 srcBasePtr, u32 srcStride, int srcX, int srcY, u32 dstBasePtr, u32 dstStride, int dstX, int dstY, int width, int height, int bpp) { + return false; + } // TODO: Unify this. Vulkan and OpenGL are different due to how they buffer data. virtual void FinishDeferred() {} diff --git a/GPU/Software/BinManager.cpp b/GPU/Software/BinManager.cpp index 5d6b93c91e..c4d637b0cf 100644 --- a/GPU/Software/BinManager.cpp +++ b/GPU/Software/BinManager.cpp @@ -26,11 +26,13 @@ #include "Common/Thread/ThreadManager.h" #include "Common/Data/Text/StringWriter.h" #include "Common/TimeUtil.h" +#include "Core/MemMap.h" #include "Core/System.h" #include "GPU/Common/TextureDecoder.h" #include "GPU/Software/BinManager.h" #include "GPU/Software/Rasterizer.h" #include "GPU/Software/RasterizerRectangle.h" +#include "GPU/Software/SoftGpu.h" // Sometimes useful for debugging. static constexpr bool FORCE_SINGLE_THREAD = false; @@ -216,9 +218,42 @@ void BinManager::PushState() { } } states_[stateIndex_].samplerID.cached.clut = cluts_[clutIndex_].readable; + if (states_[stateIndex_].enableTextures) + DeswizzleMirrorTextures(states_[stateIndex_]); creatingState_ = false; } +// Texturing from VRAM's 0x04200000 and 0x04600000 mirrors reads it as the CPU does there: depth stored in the +// GE's layout comes out linear (16-bit and 32-bit color layouts). Such levels are drawn from a linear copy, +// made after everything queued is drawn. Since each such state flushes, one set of copies is enough. +void BinManager::DeswizzleMirrorTextures(RasterizerState &state) { + bool flushed = false; + for (int i = 0; i <= state.maxTexLevel; ++i) { + const uint32_t addr = state.texaddr[i]; + if (!state.texptr[i] || !Memory::IsDepthTexVRAMAddress(addr)) + continue; + if (!flushed) { + // Queued drawing may still write what this reads. This state is the last in the ring, which + // survives the flush. + Flush("depth view"); + flushed = true; + } + const DepthLayout layout = GetDepthLayout(depthbuf.translation, (addr & 0x00400000) != 0); + const uint32_t bytes = state.texbufw[i] * textureBitsPerPixel[state.samplerID.texfmt] / 8 * state.samplerID.cached.sizes[i].h; + std::vector &view = depthViews_[i]; + view.resize(bytes); + const uint32_t start = addr & 0x001FFFFF; + for (uint32_t pos = 0; pos < bytes; ) { + // Stored in runs of 32 aligned bytes. + const uint32_t offset = start + pos; + const uint32_t n = std::min(32 - (offset & 31), bytes - pos); + memcpy(view.data() + pos, depthbuf.vram + layout.Stored(offset), n); + pos += n; + } + state.texptr[i] = view.data(); + } +} + void BinManager::UpdateState() { PROFILE_THIS_SCOPE("bin_state"); auto jitGen = JitGeneration; diff --git a/GPU/Software/BinManager.h b/GPU/Software/BinManager.h index 94c9b2ef6c..59d9c4fdb0 100644 --- a/GPU/Software/BinManager.h +++ b/GPU/Software/BinManager.h @@ -338,6 +338,8 @@ private: bool pastStride_ = false; Rasterizer::RasterizerState selfTexState_; std::vector selfTexBuf_[8]; + // Linear copies of texture levels read through a swizzled VRAM mirror (DeswizzleMirrorTextures). + std::vector depthViews_[8]; uint32_t selfTexAddr_[8]{}; bool selfTexValid_ = false; uint32_t texFlushGen_ = 0; @@ -370,6 +372,7 @@ private: const Rasterizer::RasterizerState &SelfTextureSnapshot(const BinItem &item, const Rasterizer::RasterizerState &state); void OptimizePendingStates(uint16_t first, uint16_t last); void PushState(); + void DeswizzleMirrorTextures(Rasterizer::RasterizerState &state); template void AddFlags(F calculate); BinCoords Scissor(BinCoords range); diff --git a/GPU/Software/DrawPixel.h b/GPU/Software/DrawPixel.h index 729d96bedc..ac95def4b2 100644 --- a/GPU/Software/DrawPixel.h +++ b/GPU/Software/DrawPixel.h @@ -84,6 +84,7 @@ private: // Note: these may require a temporary reg. RegCache::Reg GetColorOff(const PixelFuncID &id); RegCache::Reg GetDepthOff(const PixelFuncID &id); + void EmitDepthPointer(const PixelFuncID &id, RegCache::Reg index, RegCache::Reg temp); RegCache::Reg GetDestStencil(const PixelFuncID &id); void WriteConstantPool(const PixelFuncID &id); diff --git a/GPU/Software/DrawPixelX86.cpp b/GPU/Software/DrawPixelX86.cpp index ee03dd76eb..812003b6f5 100644 --- a/GPU/Software/DrawPixelX86.cpp +++ b/GPU/Software/DrawPixelX86.cpp @@ -20,6 +20,7 @@ #if PPSSPP_ARCH(AMD64) #include "Common/x64Emitter.h" +#include "Common/BitSet.h" #include "Common/CPUDetect.h" #include "Common/LogReporting.h" #include "Common/Math/SIMDHeaders.h" @@ -148,22 +149,13 @@ RegCache::Reg PixelJitCache::GetColorOff(const PixelFuncID &id) { ADD(32, R(argXReg), R(argYReg)); // Now add the pointer for the color buffer. - if (loadDepthOff) { - _assert_msg_(Accessible(&fb.data, &depthbuf.data), "fb.data and depthbuf.data too far apart: %p %p (fb=%08x d=%08x)", fb.data, depthbuf.data, gstate.getFrameBufAddress(), gstate.getDepthBufAddress()); + if (loadDepthOff) depthTemp = regCache_.Alloc(RegCache::GEN_DEPTH_OFF); - if (RipAccessible(&fb.data) && RipAccessible(&depthbuf.data)) { - MOV(PTRBITS, R(argYReg), M(&fb.data)); - } else { - MOV(PTRBITS, R(depthTemp), ImmPtr(&fb.data)); - MOV(PTRBITS, R(argYReg), MatR(depthTemp)); - } + if (RipAccessible(&fb.data)) { + MOV(PTRBITS, R(argYReg), M(&fb.data)); } else { - if (RipAccessible(&fb.data)) { - MOV(PTRBITS, R(argYReg), M(&fb.data)); - } else { - MOV(PTRBITS, R(argYReg), ImmPtr(&fb.data)); - MOV(PTRBITS, R(argYReg), MatR(argYReg)); - } + MOV(PTRBITS, R(argYReg), ImmPtr(&fb.data)); + MOV(PTRBITS, R(argYReg), MatR(argYReg)); } LEA(PTRBITS, argYReg, MComplex(argYReg, argXReg, id.FBFormat() == GE_FORMAT_8888 ? 4 : 2, 0)); // With that, argYOff is now GEN_COLOR_OFF. @@ -174,12 +166,7 @@ RegCache::Reg PixelJitCache::GetColorOff(const PixelFuncID &id) { // Next, also calculate the depth offset, unless we won't need it at all. if (loadDepthOff) { - if (RipAccessible(&fb.data) && RipAccessible(&depthbuf.data)) { - MOV(PTRBITS, R(depthTemp), M(&depthbuf.data)); - } else { - MOV(PTRBITS, R(depthTemp), MAccessibleDisp(depthTemp, &fb.data, &depthbuf.data)); - } - LEA(PTRBITS, argXReg, MComplex(depthTemp, argXReg, 2, 0)); + EmitDepthPointer(id, argXReg, depthTemp); regCache_.Release(depthTemp, RegCache::GEN_DEPTH_OFF); // Okay, same deal - release as GEN_DEPTH_OFF and force retain it. @@ -269,13 +256,7 @@ RegCache::Reg PixelJitCache::GetDepthOff(const PixelFuncID &id) { regCache_.Unlock(argXReg, RegCache::GEN_ARG_X); X64Reg temp = regCache_.Alloc(RegCache::GEN_TEMP_HELPER); - if (RipAccessible(&depthbuf.data)) { - MOV(PTRBITS, R(temp), M(&depthbuf.data)); - } else { - MOV(PTRBITS, R(temp), ImmPtr(&depthbuf.data)); - MOV(PTRBITS, R(temp), MatR(temp)); - } - LEA(PTRBITS, r, MComplex(temp, r, 2, 0)); + EmitDepthPointer(id, r, temp); regCache_.Release(temp, RegCache::GEN_TEMP_HELPER); return r; @@ -283,6 +264,41 @@ RegCache::Reg PixelJitCache::GetDepthOff(const PixelFuncID &id) { return regCache_.Find(RegCache::GEN_DEPTH_OFF); } +// Turns a pixel index (y * stride + x) into a pointer to its depth, stored as the GE stores it +// (GPU/Common/DepthSwizzle.h). The layout's constants come from the ID. +void PixelJitCache::EmitDepthPointer(const PixelFuncID &id, X64Reg index, X64Reg temp) { + const DepthLayout layout = GetDepthLayout(DepthTranslationFromIndex(id.depthLayout), id.FBFormat() == GE_FORMAT_8888); + SHL(32, R(index), Imm8(1)); + if (RipAccessible(&depthbuf.base)) { + ADD(32, R(index), M(&depthbuf.base)); + } else { + MOV(PTRBITS, R(temp), ImmPtr(&depthbuf.base)); + ADD(32, R(index), MatR(temp)); + } + if (layout.rotMask != 0) { + // Rotate the field left by one: shifted to the bottom, its top bit carried around. + const int low = LeastSignificantSetBit(layout.rotMask); + const int width = layout.rotShift + 1; + MOV(32, R(temp), R(index)); + AND(32, R(index), Imm32(~layout.rotMask)); + SHR(32, R(temp), Imm8(low)); + AND(32, R(temp), Imm32((1 << width) - 1)); + SHL(32, R(temp), Imm8(1)); + BTR(32, R(temp), Imm8(width)); + ADC(32, R(temp), Imm8(0)); + SHL(32, R(temp), Imm8(low)); + OR(32, R(index), R(temp)); + } + XOR(32, R(index), Imm32(layout.xorBits)); + if (RipAccessible(&depthbuf.vram)) { + MOV(PTRBITS, R(temp), M(&depthbuf.vram)); + } else { + MOV(PTRBITS, R(temp), ImmPtr(&depthbuf.vram)); + MOV(PTRBITS, R(temp), MatR(temp)); + } + LEA(PTRBITS, index, MRegSum(temp, index)); +} + RegCache::Reg PixelJitCache::GetDestStencil(const PixelFuncID &id) { // Skip if 565, since stencil is fixed zero. diff --git a/GPU/Software/FuncId.cpp b/GPU/Software/FuncId.cpp index a525993e3c..a1d11b495c 100644 --- a/GPU/Software/FuncId.cpp +++ b/GPU/Software/FuncId.cpp @@ -22,6 +22,7 @@ #include "GPU/Common/TextureDecoder.h" #include "GPU/GPUState.h" #include "GPU/Software/FuncId.h" +#include "GPU/Software/SoftGpu.h" static_assert(sizeof(SamplerID) == sizeof(SamplerID::fullKey) + sizeof(SamplerID::cached) + sizeof(SamplerID::pad), "Bad sampler ID size"); static_assert(sizeof(PixelFuncID) == sizeof(PixelFuncID::fullKey) + sizeof(PixelFuncID::cached), "Bad pixel func ID size"); @@ -201,6 +202,8 @@ void ComputePixelFuncID(PixelFuncID *id) { } } + if (id->depthTestFunc != GE_COMP_ALWAYS || id->depthWrite) + id->depthLayout = DepthTranslationIndex(depthbuf.translation); if (id->useStandardStride && (id->depthTestFunc != GE_COMP_ALWAYS || id->depthWrite)) id->useStandardStride = gstate.DepthBufStride() == 512; @@ -446,6 +449,12 @@ std::string DescribePixelFuncID(const PixelFuncID &id) { else if (id.clearMode) desc = "INVALID:" + desc; + // DepthTranslationIndex() only gives 0 to 4. + if (id.depthLayout > 4) + desc = "INVALID:" + desc; + else if (id.depthLayout != 0) + desc += StringFromFormat("ZLayout%d:", (int)id.depthLayout); + if (desc.empty()) return "INVALID"; desc.resize(desc.size() - 1); diff --git a/GPU/Software/FuncId.h b/GPU/Software/FuncId.h index d9d8e51573..f557de0437 100644 --- a/GPU/Software/FuncId.h +++ b/GPU/Software/FuncId.h @@ -98,17 +98,20 @@ struct PixelFuncID { bool hasStencilTestMask : 1; bool dithering : 1; bool applyLogicOp : 1; - // 48 bits before applyFog. + // 48 bits. MSVC doesn't let a bit field straddle a byte, so each byte below is filled exactly. + uint8_t sFail : 3; + uint8_t zFail : 3; bool applyFog : 1; // Meaning: fb_stride == 512 && z_stride == 512 bool useStandardStride : 1; + uint8_t zPass : 3; // Meaning: maskRGB != 0 || maskA != 0 bool applyColorWriteMask : 1; - uint8_t sFail : 3; - uint8_t zFail : 3; - uint8_t zPass : 3; bool earlyZChecks : 1; - // 61 bits, 3 free. + // The EDRAM address translation, as DepthTranslationIndex(), which with fbFormat picks how depth is + // stored (GPU/Common/DepthSwizzle.h). + uint8_t depthLayout : 3; + // 64 bits. }; }; diff --git a/GPU/Software/Rasterizer.cpp b/GPU/Software/Rasterizer.cpp index 88e39f0dfa..cbc40a214f 100644 --- a/GPU/Software/Rasterizer.cpp +++ b/GPU/Software/Rasterizer.cpp @@ -718,6 +718,14 @@ static inline void ApplyTexturing(const RasterizerState &state, Vec4 *prim_ } } +// Depth at (x, y) and (x + 1, y). They're stored together unless x is last in a 16-pixel run. +static inline u32 ReadDepthPair(int x, int y, int stride) { + const uint32_t offset = depthbuf.base + (uint32_t)(x + y * stride) * 2; + if ((offset & 31) != 30) + return *(const u32 *)depthbuf.Get16Ptr(x, y, stride); + return depthbuf.Get16(x, y, stride) | ((u32)depthbuf.Get16(x + 1, y, stride) << 16); +} + static inline Vec4 SOFTRAST_CALL CheckDepthTestPassed4(const Vec4 &mask, GEComparison func, int x, int y, int stride, Vec4 z) { // Skip the depth buffer read if we're masked already. #if defined(_M_SSE) @@ -734,8 +742,8 @@ static inline Vec4 SOFTRAST_CALL CheckDepthTestPassed4(const Vec4 &mas // Read in the existing depth values. #if defined(_M_SSE) // Tried using flags from maskbits to skip dwords... seemed neutral. - __m128i refz = _mm_cvtsi32_si128(*(u32 *)depthbuf.Get16Ptr(x, y, stride)); - refz = _mm_unpacklo_epi32(refz, _mm_cvtsi32_si128(*(u32 *)depthbuf.Get16Ptr(x, y + 1, stride))); + __m128i refz = _mm_cvtsi32_si128(ReadDepthPair(x, y, stride)); + refz = _mm_unpacklo_epi32(refz, _mm_cvtsi32_si128(ReadDepthPair(x, y + 1, stride))); refz = _mm_unpacklo_epi16(refz, _mm_setzero_si128()); #else Vec4 refz(depthbuf.Get16(x, y, stride), depthbuf.Get16(x + 1, y, stride), depthbuf.Get16(x, y + 1, stride), depthbuf.Get16(x + 1, y + 1, stride)); @@ -1879,12 +1887,17 @@ void ClearRectangle(const VertexData &v0, const VertexData &v1, const BinCoords const u16 z = v1.screenpos.z; const int stride = pixelID.cached.depthbufStride; - // If both bytes of Z equal, we can just use memset directly which is faster. + // If both bytes of Z equal, we can just use memset directly which is faster. Depth is stored in runs of + // 16 pixels, each 32 bytes aligned. if ((z & 0xFF) == (z >> 8)) { DrawingCoords p = pprime; for (p.y = pprime.y; p.y <= pend.y; ++p.y) { - u16 *row = depthbuf.Get16Ptr(p.x, p.y, stride); - memset(row, z, w * 2); + for (int x = 0; x < w; ) { + const uint32_t offset = depthbuf.base + (uint32_t)(pprime.x + x + p.y * stride) * 2; + const int n = std::min(w - x, 16 - (int)((offset >> 1) & 15)); + memset(depthbuf.Get16Ptr(pprime.x + x, p.y, stride), z, n * 2); + x += n; + } } } else { DrawingCoords p = pprime; diff --git a/GPU/Software/SoftGpu.cpp b/GPU/Software/SoftGpu.cpp index d13c4a7a8d..d90794c644 100644 --- a/GPU/Software/SoftGpu.cpp +++ b/GPU/Software/SoftGpu.cpp @@ -18,6 +18,7 @@ #include #include "Common/System/Display.h" +#include "Common/Serialize/Serializer.h" #include "Common/GPU/OpenGL/GLFeatures.h" #include "GPU/GPUState.h" @@ -54,7 +55,7 @@ constexpr int FB_HEIGHT = 272; uint8_t clut[1024]; FormatBuffer fb; -FormatBuffer depthbuf; +DepthBuffer depthbuf; struct CommandInfo { uint64_t flags; @@ -397,7 +398,8 @@ SoftGPU::SoftGPU(GraphicsContext *gfxCtx, Draw::DrawContext *draw) : GPUCommon(gfxCtx, draw) { fb.data = Memory::GetPointerWriteOrException(0x44000000); // TODO: correct default address? - depthbuf.data = Memory::GetPointerWriteOrException(0x44000000); // TODO: correct default address? + depthbuf.vram = Memory::GetPointerWriteOrException(0x04000000); + UpdateDepthBuffer(); memset(softgpuCmdInfo, 0, sizeof(softgpuCmdInfo)); @@ -875,7 +877,10 @@ void SoftGPU::Execute_BlockTransferStart(u32 op, u32 diff) { const uint32_t dstSize = (height - 1) * (dstStride + width) * bpp; // Need to flush both source and target, so we overwrite properly. - if (Memory::IsValidRange(src, srcSize) && Memory::IsValidRange(dst, dstSize)) { + if (Memory::IsDepthTexVRAMAddress(srcBasePtr) || Memory::IsDepthTexVRAMAddress(dstBasePtr)) { + // Through the depth layout (CopyBlockTransfer), which the ranges don't follow. + drawEngine_->transformUnit.Flush(this, "blockxfer_depth"); + } else if (Memory::IsValidRange(src, srcSize) && Memory::IsValidRange(dst, dstSize)) { drawEngine_->transformUnit.FlushIfOverlap(this, "blockxfer", false, src, srcStride * bpp, width * bpp, height); drawEngine_->transformUnit.FlushIfOverlap(this, "blockxfer", true, dst, dstStride * bpp, width * bpp, height); } else { @@ -888,6 +893,47 @@ void SoftGPU::Execute_BlockTransferStart(u32 op, u32 diff) { MarkDirty(dst, dstSize, SoftGPUVRAMDirty::DIRTY | SoftGPUVRAMDirty::REALLY_DIRTY); } +// A block transfer from or to VRAM's 0x04200000 or 0x04600000 mirror goes through the depth layout there, as the +// CPU's accesses do: Hayate no Gotoku restores its depth buffer through 0x04600000 (#17878). +bool SoftGPU::CopyBlockTransfer(u32 srcBasePtr, u32 srcStride, int srcX, int srcY, u32 dstBasePtr, u32 dstStride, int dstX, int dstY, int width, int height, int bpp) { + if (!Memory::IsDepthTexVRAMAddress(srcBasePtr) && !Memory::IsDepthTexVRAMAddress(dstBasePtr)) + return false; + const DepthLayout layout16 = GetDepthLayout(GetAddrTranslation(), false); + const DepthLayout layout32 = GetDepthLayout(GetAddrTranslation(), true); + // The memory of each run of a row, which the layout keeps together within 32 aligned bytes. Past VRAM's end, a + // transfer wraps to its start, which isn't a swizzled mirror. + auto run = [&](u32 addr) -> u8 * { + if ((addr & 0x04800000) == 0x04800000) + addr &= ~0x00800000; + if (Memory::IsDepthTexVRAMAddress(addr)) + return depthbuf.vram + ((addr & 0x00400000) ? layout32 : layout16).Stored(addr & 0x001FFFFF); + return Memory::IsValidAddress(addr) ? Memory::GetPointerWriteUnchecked(addr) : nullptr; + }; + const u32 bytes = width * bpp; + std::vector row(bytes); + for (int y = 0; y < height; ++y) { + const u32 srcRow = srcBasePtr + ((y + srcY) * srcStride + srcX) * bpp; + const u32 dstRow = dstBasePtr + ((y + dstY) * dstStride + dstX) * bpp; + for (u32 pos = 0; pos < bytes; ) { + const u32 n = std::min(32 - ((srcRow + pos) & 31), bytes - pos); + const u8 *p = run(srcRow + pos); + if (p) + memcpy(row.data() + pos, p, n); + else + memset(row.data() + pos, 0, n); + pos += n; + } + for (u32 pos = 0; pos < bytes; ) { + const u32 n = std::min(32 - ((dstRow + pos) & 31), bytes - pos); + u8 *p = run(dstRow + pos); + if (p) + memcpy(p, row.data() + pos, n); + pos += n; + } + } + return true; +} + void SoftGPU::Execute_Prim(u32 op, u32 diff) { u32 count = op & 0xFFFF; // Upper bits are ignored. @@ -1082,8 +1128,11 @@ void SoftGPU::Execute_FramebufPtr(u32 op, u32 diff) { void SoftGPU::Execute_FramebufFormat(u32 op, u32 diff) { // We should flush, because ranges within bins may change. - if (diff) + if (diff) { drawEngine_->transformUnit.Flush(this, "framebuf"); + // The depth layout depends on the color format. + UpdateDepthBuffer(); + } } void SoftGPU::Execute_TexFlush(u32 op, u32 diff) { @@ -1091,15 +1140,42 @@ void SoftGPU::Execute_TexFlush(u32 op, u32 diff) { } void SoftGPU::Execute_ZbufPtr(u32 op, u32 diff) { - // We assume depthbuf.data won't change while we're drawing. + // We assume depthbuf won't change while we're drawing. if (diff) { drawEngine_->transformUnit.Flush(this, "depthbuf"); - // For the pointer, ignore memory mirrors. This also gives some buffer for draws that go outside. - // TODO: Confirm how wrapping is handled in drawing. Adjust if we ever handle VRAM mirrors more accurately. - depthbuf.data = Memory::GetPointerWriteOrException(gstate.getDepthBufAddress() & 0x041FFFF0); + UpdateDepthBuffer(); } } +// Call with nothing queued: the threads draw with depthbuf. +void SoftGPU::UpdateDepthBuffer() { + // The GE ignores the mirror bits of its own pointers (exp89). Draws that go outside reach the mirrors, + // which map back to VRAM. + depthbuf.base = gstate.getDepthBufAddress() & 0x001FFFF0; + depthbuf.translation = GetAddrTranslation(); + depthbuf.layout = GetDepthLayout(depthbuf.translation, gstate.FrameBufFormat() == GE_FORMAT_8888); +} + +void SoftGPU::DoState(PointerWrap &p) { + GPUCommon::DoState(p); + // The translation comes back without SetAddrTranslation. + if (p.mode == PointerWrap::MODE_READ) + UpdateDepthBuffer(); +} + +uint32_t SoftGPU::SetAddrTranslation(uint32_t value) { + if (value != GetAddrTranslation()) { + // Queued drawing uses the old layout. + drawEngine_->transformUnit.Flush(this, "translation"); + const uint32_t previous = GPUCommon::SetAddrTranslation(value); + UpdateDepthBuffer(); + dirtyFlags_ |= SoftDirty::PIXEL_BASIC; + drawEngine_->transformUnit.SetDirty(SoftDirty::PIXEL_BASIC); + return previous; + } + return GPUCommon::SetAddrTranslation(value); +} + void SoftGPU::Execute_VertexType(u32 op, u32 diff) { if ((diff & GE_VTYPE_THROUGH_MASK) != 0) { // This affects a lot of things, but some don't matter if it's off - so defer to when it's back on. @@ -1464,13 +1540,10 @@ bool SoftGPU::GetCurrentDepthbuffer(GPUDebugBuffer &buffer) { DrawingCoords size = GetTargetSize(gstate.DepthBufStride()); buffer.Allocate(size.x, size.y, GPU_DBG_FORMAT_16BIT); - const int depth = 2; - const u8 *src = depthbuf.data; - u8 *dst = buffer.GetData(); + u16 *dst = (u16 *)buffer.GetData(); for (int16_t y = 0; y < size.y; ++y) { - memcpy(dst, src, size.x * depth); - dst += size.x * depth; - src += gstate.DepthBufStride() * depth; + for (int16_t x = 0; x < size.x; ++x) + *dst++ = depthbuf.Get16(x, y, gstate.DepthBufStride()); } return true; } diff --git a/GPU/Software/SoftGpu.h b/GPU/Software/SoftGpu.h index 3fe79eb995..84ef4c6374 100644 --- a/GPU/Software/SoftGpu.h +++ b/GPU/Software/SoftGpu.h @@ -20,7 +20,7 @@ #include #include "GPU/GPUCommon.h" -#include "GPU/GPUCommon.h" +#include "GPU/Common/DepthSwizzle.h" #include "Common/GPU/thin3d.h" struct FormatBuffer { @@ -56,6 +56,40 @@ struct FormatBuffer { } }; +// The depth buffer, stored in VRAM as the GE stores it (GPU/Common/DepthSwizzle.h). +struct DepthBuffer { + // VRAM's start, and the buffer's offset in it. + u8 *vram = nullptr; + uint32_t base = 0; + DepthLayout layout; + // sceGeEdramSetAddrTranslation's value, which with the color format picks the layout. + uint32_t translation = 0x400; + + inline u16 *Get16Ptr(int x, int y, int stride) const { + return (u16 *)(vram + layout.Stored(base + (uint32_t)(x + y * stride) * 2)); + } + inline u16 Get16(int x, int y, int stride) const { + return *Get16Ptr(x, y, stride); + } + inline void Set16(int x, int y, int stride, u16 v) const { + *Get16Ptr(x, y, stride) = v; + } +}; + +// The index PixelFuncID::depthLayout keeps a translation as (0, 0x200, 0x400, 0x800, 0x1000). +inline int DepthTranslationIndex(uint32_t translation) { + int index = 0; + while (translation >= 0x200 && index < 4) { + translation >>= 1; + index++; + } + return index; +} + +inline uint32_t DepthTranslationFromIndex(int index) { + return index == 0 ? 0 : 0x100 << index; +} + enum class SoftDirty : uint64_t { NONE = 0, @@ -137,6 +171,9 @@ public: void UpdateCmdInfo() override {} void SetDisplayFramebuffer(u32 framebuf, u32 stride, GEBufferFormat format) override; + uint32_t SetAddrTranslation(uint32_t value) override; + bool CopyBlockTransfer(u32 srcBasePtr, u32 srcStride, int srcX, int srcY, u32 dstBasePtr, u32 dstStride, int dstX, int dstY, int width, int height, int bpp) override; + void DoState(PointerWrap &p) override; void SetCurFramebufferDirty(bool dirty) override {} void PrepareCopyDisplayToOutput(const DisplayLayoutConfig &config) override; void CopyDisplayToOutput(const DisplayLayoutConfig &config) override; @@ -184,6 +221,7 @@ public: void Execute_FramebufPtr(u32 op, u32 diff); void Execute_FramebufFormat(u32 op, u32 diff); void Execute_ZbufPtr(u32 op, u32 diff); + void UpdateDepthBuffer(); void Execute_TexFlush(u32 op, u32 diff); void Execute_VertexType(u32 op, u32 diff); @@ -247,7 +285,7 @@ private: // TODO: These shouldn't be global. extern uint8_t clut[1024]; extern FormatBuffer fb; -extern FormatBuffer depthbuf; +extern DepthBuffer depthbuf; // Type for the DarkStalkers stretch replacement. enum class DSStretch { diff --git a/unittest/TestSoftwareGPUJit.cpp b/unittest/TestSoftwareGPUJit.cpp index 79be7fdff1..764d9ea457 100644 --- a/unittest/TestSoftwareGPUJit.cpp +++ b/unittest/TestSoftwareGPUJit.cpp @@ -129,11 +129,14 @@ static bool TestPixelJit() { bool header = false; u32 *fb_data = new u32[512 * 2]; - u16 *zb_data = new u16[512 * 2]; + // Random IDs pick random depth layouts, which reach up to 0x8040 bytes past the pixel's offset. + const size_t zbSize = 0x10000; + u16 *zb_data = new u16[zbSize]; fb.as32 = fb_data; - depthbuf.as16 = zb_data; + depthbuf.vram = (u8 *)zb_data; + depthbuf.base = 0; memset(fb_data, 0, sizeof(u32) * 512 * 2); - memset(zb_data, 0, sizeof(u16) * 512 * 2); + memset(zb_data, 0, sizeof(u16) * zbSize); for (int i = 0; i < count; ) { PixelFuncID id; From ae324777644f2903e8a177455b6db6e0f8213fea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 5 Oct 2026 15:57:21 -0600 Subject: [PATCH 03/11] Core: Let 16-bit CPU accesses through the depth mirrors see the GE's layout With the software renderer, which now stores depth as the GE does, a 16-bit read or write at VRAM's 0x04200000 or 0x04600000 mirror goes to where the GE's 16-bit or 32-bit color layout puts that depth pixel (ppsspp-re geprobe exp88: the mirrors are the layout's exact inverse). Games access depth there with nothing else, so other widths stay plain. The accessors check the address, then whether the GPU core is the software renderer (CoreParameter); the hardware renderers keep depth plain and handle the layout their own way. The renderer keeps the layouts up to date with the EDRAM address translation. With it, the JITs send 16-bit loads and stores to the interpreter, and the IR passes don't change a load's width to or from 16 bits. The pspautotests that read depth through the mirrors now do so 16 bits at a time, re-recorded on hardware unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) --- Core/MIPS/ARM/ArmCompLoadStore.cpp | 4 ++++ Core/MIPS/ARM64/Arm64CompLoadStore.cpp | 4 ++++ Core/MIPS/IR/IRNativeCommon.cpp | 19 ++++++++++++++--- Core/MIPS/IR/IRPassSimplify.cpp | 8 ++++++-- Core/MIPS/x86/CompLoadStore.cpp | 4 ++++ Core/MemMap.cpp | 12 +++++++++++ Core/MemMap.h | 22 +++++++++++++++++++- Core/MemMapFunctions.cpp | 4 ++-- GPU/Common/DepthSwizzle.h | 28 ++++++++++++++++++++++++-- GPU/Software/SoftGpu.cpp | 3 +++ GPU/Software/SoftGpu.h | 14 ------------- pspautotests | 2 +- 12 files changed, 99 insertions(+), 25 deletions(-) diff --git a/Core/MIPS/ARM/ArmCompLoadStore.cpp b/Core/MIPS/ARM/ArmCompLoadStore.cpp index 8a2fed055f..249d4a9f69 100644 --- a/Core/MIPS/ARM/ArmCompLoadStore.cpp +++ b/Core/MIPS/ARM/ArmCompLoadStore.cpp @@ -284,6 +284,10 @@ namespace MIPSComp MIPSGPReg rt = _RT; MIPSGPReg rs = _RS; int o = op>>26; + // 16-bit accesses can go through the depth layout (Memory::DepthMirrored16). + if (Memory::DepthMirrorsActive() && (o == 33 || o == 37 || o == 41)) { + DISABLE; + } if (((op >> 29) & 1) == 0 && rt == MIPS_REG_ZERO) { // Don't load anything into $zr return; diff --git a/Core/MIPS/ARM64/Arm64CompLoadStore.cpp b/Core/MIPS/ARM64/Arm64CompLoadStore.cpp index 3b8ac2859d..f293b94329 100644 --- a/Core/MIPS/ARM64/Arm64CompLoadStore.cpp +++ b/Core/MIPS/ARM64/Arm64CompLoadStore.cpp @@ -279,6 +279,10 @@ namespace MIPSComp { MIPSGPReg rt = _RT; MIPSGPReg rs = _RS; int o = op >> 26; + // 16-bit accesses can go through the depth layout (Memory::DepthMirrored16). + if (Memory::DepthMirrorsActive() && (o == 33 || o == 37 || o == 41)) { + DISABLE; + } if (((op >> 29) & 1) == 0 && rt == MIPS_REG_ZERO) { // Don't load anything into $zr return; diff --git a/Core/MIPS/IR/IRNativeCommon.cpp b/Core/MIPS/IR/IRNativeCommon.cpp index 399089b8dc..07d0787c17 100644 --- a/Core/MIPS/IR/IRNativeCommon.cpp +++ b/Core/MIPS/IR/IRNativeCommon.cpp @@ -280,10 +280,17 @@ void IRNativeBackend::CompileIRInst(IRInst inst) { CompIR_Div(inst); break; - case IROp::Load8: - case IROp::Load8Ext: case IROp::Load16: case IROp::Load16Ext: + // 16-bit accesses can go through the depth layout (Memory::DepthMirrored16). + if (Memory::DepthMirrorsActive()) + CompIR_Generic(inst); + else + CompIR_Load(inst); + break; + + case IROp::Load8: + case IROp::Load8Ext: case IROp::Load32: case IROp::Load32Linked: CompIR_Load(inst); @@ -302,8 +309,14 @@ void IRNativeBackend::CompileIRInst(IRInst inst) { CompIR_VecLoad(inst); break; - case IROp::Store8: case IROp::Store16: + if (Memory::DepthMirrorsActive()) + CompIR_Generic(inst); + else + CompIR_Store(inst); + break; + + case IROp::Store8: case IROp::Store32: CompIR_Store(inst); break; diff --git a/Core/MIPS/IR/IRPassSimplify.cpp b/Core/MIPS/IR/IRPassSimplify.cpp index 11d3059dde..1961892748 100644 --- a/Core/MIPS/IR/IRPassSimplify.cpp +++ b/Core/MIPS/IR/IRPassSimplify.cpp @@ -6,6 +6,7 @@ #include "Common/Data/Convert/SmallDataConvert.h" #include "Common/Log.h" #include "Core/Config.h" +#include "Core/MemMap.h" #include "Core/MIPS/MIPSVFPUUtils.h" #include "Core/MIPS/IR/IRAnalysis.h" #include "Core/MIPS/IR/IRInterpreter.h" @@ -1357,7 +1358,10 @@ bool ReduceLoads(const IRWriter &in, IRWriter &out, const IROptions &opts) { for (int i = 0, n = (int)in.GetInstructions().size(); i < n; i++) { IRInst inst = in.GetInstructions()[i]; - if (inst.op == IROp::Load32 || inst.op == IROp::Load16 || inst.op == IROp::Load16Ext) { + // With the depth mirrors, a load's width decides whether it goes through the depth layout + // (Memory::DepthMirrored16), so 16-bit loads keep theirs, and no load becomes one. + const bool depthMirrors = Memory::DepthMirrorsActive(); + if (inst.op == IROp::Load32 || ((inst.op == IROp::Load16 || inst.op == IROp::Load16Ext) && !depthMirrors)) { int dest = IRDestGPR(GetIRMeta(inst)); for (int j = i + 1; j < n; j++) { const IRInstMeta laterInst = GetIRMeta(in.GetInstructions()[j]); @@ -1375,7 +1379,7 @@ bool ReduceLoads(const IRWriter &in, IRWriter &out, const IROptions &opts) { if (mask == 0xff) { nextSkip = j; } - } else if ((mask & 0xffff0000) == 0 && inst.op == IROp::Load32) { + } else if ((mask & 0xffff0000) == 0 && inst.op == IROp::Load32 && !depthMirrors) { inst.op = IROp::Load16; if (mask == 0xffff) { nextSkip = j; diff --git a/Core/MIPS/x86/CompLoadStore.cpp b/Core/MIPS/x86/CompLoadStore.cpp index d010cb3777..501f8f641c 100644 --- a/Core/MIPS/x86/CompLoadStore.cpp +++ b/Core/MIPS/x86/CompLoadStore.cpp @@ -302,6 +302,10 @@ namespace MIPSComp { MIPSGPReg rs = _RS; MIPSGPReg rt = _RT; int o = op>>26; + // 16-bit accesses can go through the depth layout (Memory::DepthMirrored16). + if (Memory::DepthMirrorsActive() && (o == 33 || o == 37 || o == 41)) { + DISABLE; + } if (((op >> 29) & 1) == 0 && rt == MIPS_REG_ZERO) { // Don't load anything into $zr return; diff --git a/Core/MemMap.cpp b/Core/MemMap.cpp index c925c541e2..2f2c0b40a8 100644 --- a/Core/MemMap.cpp +++ b/Core/MemMap.cpp @@ -82,6 +82,18 @@ static u8 *m_pUncachedVRAM[4]; // Required for HD Remasters to work properly. // This replaces RAM_NORMAL_SIZE at runtime. u32 g_MemorySize; +int g_depthTranslationIndex = DepthTranslationIndex(0x400); + +bool DepthMirrorsActive() { + return PSP_CoreParameter().gpuCore == GPUCORE_SOFTWARE; +} + +u32 DepthMirroredSlow(u32 address) { + if (!DepthMirrorsActive()) + return address; + const DepthLayout &layout = DEPTH_LAYOUTS[(address >> 22) & 1][g_depthTranslationIndex]; + return (address & 0x40000000) | 0x04000000 | layout.Stored(address & 0x001FFFFF); +} // Used to store the PSP model on game startup. u32 g_PSPModel; u32 g_UserPartitionSize; diff --git a/Core/MemMap.h b/Core/MemMap.h index 83fe8ec2a0..9bda44338d 100644 --- a/Core/MemMap.h +++ b/Core/MemMap.h @@ -29,6 +29,7 @@ #include "Common/CommonTypes.h" #include "Common/Swap.h" #include "Core/Opcode.h" +#include "GPU/Common/DepthSwizzle.h" // PPSSPP is very aggressive about trying to do memory accesses directly, for speed. // This can be a problem when debugging though, as stray memory reads and writes will @@ -192,7 +193,25 @@ inline float ReadUnchecked_Float(const u32 address) { #endif } -inline u16 ReadUnchecked_U16(const u32 address) { +// With the software renderer, which stores depth as the GE does, VRAM's 0x04200000 and 0x04600000 mirrors show it +// through the GE's 16-bit and 32-bit color layouts (GPU/Common/DepthSwizzle.h, ppsspp-re geprobe exp88). Only 16-bit +// accesses go through them: games read and write depth with nothing else. The layouts are fixed but for the EDRAM +// address translation, which the software renderer keeps here as DepthTranslationIndex(). +extern int g_depthTranslationIndex; + +// Whether the depth mirrors apply the layouts: only with the software renderer. Hardware renderers keep depth +// plain and handle the games that need the layout their own way. +bool DepthMirrorsActive(); +u32 DepthMirroredSlow(u32 address); + +inline u32 DepthMirrored16(u32 address) { + if ((address & 0xBFA00000) == 0x04200000) + return DepthMirroredSlow(address); + return address; +} + +inline u16 ReadUnchecked_U16(u32 address) { + address = DepthMirrored16(address); #ifdef MASKED_PSP_MEMORY return *(u16_le *)(base + (address & MEMVIEW32_MASK)); #else @@ -233,6 +252,7 @@ inline void WriteUnchecked_Float(float data, u32 address) { } inline void WriteUnchecked_U16(u16 data, u32 address) { + address = DepthMirrored16(address); #ifdef MASKED_PSP_MEMORY *(u16_le *)(base + (address & MEMVIEW32_MASK)) = data; #else diff --git a/Core/MemMapFunctions.cpp b/Core/MemMapFunctions.cpp index af5419b1e3..13456028ae 100644 --- a/Core/MemMapFunctions.cpp +++ b/Core/MemMapFunctions.cpp @@ -134,7 +134,7 @@ u8 ReadOrException_U8(const u32 address) { u16 ReadOrException_U16(const u32 address) { u16_le value = 0; - ReadMemoryOrException(value, address); + ReadMemoryOrException(value, DepthMirrored16(address)); return (u16)value; } @@ -155,7 +155,7 @@ void WriteOrException_U8(const u8 _Data, const u32 address) { } void WriteOrException_U16(const u16 _Data, const u32 address) { - WriteMemoryOrException(address, _Data); + WriteMemoryOrException(DepthMirrored16(address), _Data); } void WriteOrException_U32(const u32 _Data, const u32 address) { diff --git a/GPU/Common/DepthSwizzle.h b/GPU/Common/DepthSwizzle.h index abaf69a52c..e33fa3b6a3 100644 --- a/GPU/Common/DepthSwizzle.h +++ b/GPU/Common/DepthSwizzle.h @@ -33,13 +33,13 @@ struct DepthLayout { int rotShift = 0; uint32_t xorBits = 0; - uint32_t Stored(uint32_t offset) const { + constexpr uint32_t Stored(uint32_t offset) const { const uint32_t f = offset & rotMask; return ((offset & ~rotMask) | (((f << 1) | (f >> rotShift)) & rotMask)) ^ xorBits; } }; -inline DepthLayout GetDepthLayout(uint32_t translation, bool color32) { +constexpr DepthLayout GetDepthLayout(uint32_t translation, bool color32) { DepthLayout layout; if (translation == 0) { layout.xorBits = 0x600; @@ -60,3 +60,27 @@ inline DepthLayout GetDepthLayout(uint32_t translation, bool color32) { } return layout; } + +// The translations as an index 0 to 4: 0, 0x200, 0x400, 0x800, 0x1000. +constexpr int DepthTranslationIndex(uint32_t translation) { + int index = 0; + while (translation >= 0x200 && index < 4) { + translation >>= 1; + index++; + } + return index; +} + +constexpr uint32_t DepthTranslationFromIndex(int index) { + return index == 0 ? 0 : 0x100 << index; +} + +// Every layout there is, by [color32][translation index]. +inline constexpr DepthLayout DEPTH_LAYOUTS[2][5] = { + { GetDepthLayout(0, false), GetDepthLayout(0x200, false), GetDepthLayout(0x400, false), GetDepthLayout(0x800, false), GetDepthLayout(0x1000, false) }, + { GetDepthLayout(0, true), GetDepthLayout(0x200, true), GetDepthLayout(0x400, true), GetDepthLayout(0x800, true), GetDepthLayout(0x1000, true) }, +}; + +// A couple of exp8-exp10's measurements (TestDepthSwizzle has more). +static_assert(DEPTH_LAYOUTS[0][2].Stored(0x088000) == 0x08a040, "16-bit layout at the default translation"); +static_assert(DEPTH_LAYOUTS[1][0].Stored(0x03171e) == 0x03111e, "32-bit layout at translation 0"); diff --git a/GPU/Software/SoftGpu.cpp b/GPU/Software/SoftGpu.cpp index d90794c644..28e97ed811 100644 --- a/GPU/Software/SoftGpu.cpp +++ b/GPU/Software/SoftGpu.cpp @@ -475,6 +475,8 @@ void SoftGPU::DeviceRestore(Draw::DrawContext *draw) { } SoftGPU::~SoftGPU() { + // The firmware's default, until the next software renderer sets it. + Memory::g_depthTranslationIndex = DepthTranslationIndex(0x400); if (fbTex) { fbTex->Release(); fbTex = nullptr; @@ -1154,6 +1156,7 @@ void SoftGPU::UpdateDepthBuffer() { depthbuf.base = gstate.getDepthBufAddress() & 0x001FFFF0; depthbuf.translation = GetAddrTranslation(); depthbuf.layout = GetDepthLayout(depthbuf.translation, gstate.FrameBufFormat() == GE_FORMAT_8888); + Memory::g_depthTranslationIndex = DepthTranslationIndex(depthbuf.translation); } void SoftGPU::DoState(PointerWrap &p) { diff --git a/GPU/Software/SoftGpu.h b/GPU/Software/SoftGpu.h index 84ef4c6374..f7e41bba1c 100644 --- a/GPU/Software/SoftGpu.h +++ b/GPU/Software/SoftGpu.h @@ -76,20 +76,6 @@ struct DepthBuffer { } }; -// The index PixelFuncID::depthLayout keeps a translation as (0, 0x200, 0x400, 0x800, 0x1000). -inline int DepthTranslationIndex(uint32_t translation) { - int index = 0; - while (translation >= 0x200 && index < 4) { - translation >>= 1; - index++; - } - return index; -} - -inline uint32_t DepthTranslationFromIndex(int index) { - return index == 0 ? 0 : 0x100 << index; -} - enum class SoftDirty : uint64_t { NONE = 0, diff --git a/pspautotests b/pspautotests index 6729c819b0..c662bd80cd 160000 --- a/pspautotests +++ b/pspautotests @@ -1 +1 @@ -Subproject commit 6729c819b01c84c6585b6c6e1648e04c30b4711b +Subproject commit c662bd80cdb312df62086ff87e52174ede5bd3f0 From 2317ecbfbd10440e2f4aac419069a57a9b287484 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 5 Oct 2026 16:25:52 -0600 Subject: [PATCH 04/11] GE dump playback: Write recorded memory into the depth mirrors as the PSP does A dump records a texture at a swizzled VRAM mirror as the game sampled it, and the PSP's replayer writes it back with the CPU, which the PSP sends through the depth layout at any access width. PPSSPP's playback wrote it raw, so with depth stored in the GE's layout the texture came back permuted: IL-2's depth texture at 0x04710000 and the two Gensou Suikoden swizzle dumps. Texture copies, memcpys and memsets into a mirror now go through the layout, 32 aligned bytes at a time. Against PSP replays: both Suikoden dumps are now exact (from 1912 and 8083 pixels off), Star Wars #15832 is 67 off (from 2590) and IL-2 894 (from 70505, the rest from a write the next draw reads before it lands). Co-Authored-By: Claude Opus 5.5 (1M context) --- GPU/Debugger/Playback.cpp | 36 +++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/GPU/Debugger/Playback.cpp b/GPU/Debugger/Playback.cpp index 501c638728..0c49e28dcb 100644 --- a/GPU/Debugger/Playback.cpp +++ b/GPU/Debugger/Playback.cpp @@ -799,6 +799,36 @@ void DumpExecute::TransferSrc(u32 ptr, u32 sz) { execListQueue.push_back(((GE_CMD_TRANSFERSRC) << 24) | (psp & 0x00FFFFFF)); } +// The PSP replayer writes recorded memory with the CPU, and the PSP sends every CPU access to VRAM's 0x04200000 and +// 0x04600000 mirrors through the depth layout. With the software renderer PPSSPP's mirrors do that for 16-bit +// accesses only, so a write into one goes a run of 32 aligned bytes at a time, which the layout keeps together. +// IL-2's depth texture at 0x04710000 is recorded as the game sampled it, and only comes back so. +static void CopyToMemory(u32 addr, const u8 *data, u32 size) { + if (!Memory::DepthMirrorsActive() || !Memory::IsDepthTexVRAMAddress(addr)) { + Memory::MemcpyUnchecked(addr, data, size); + return; + } + for (u32 pos = 0; pos < size; ) { + const u32 a = addr + pos; + const u32 n = std::min(32 - (a & 31), size - pos); + Memory::MemcpyUnchecked(Memory::DepthMirrored16(a), data + pos, n); + pos += n; + } +} + +static void SetMemory(u32 addr, u8 value, u32 size) { + if (!Memory::DepthMirrorsActive() || !Memory::IsDepthTexVRAMAddress(addr)) { + Memory::Memset(addr, value, size); + return; + } + for (u32 pos = 0; pos < size; ) { + const u32 a = addr + pos; + const u32 n = std::min(32 - (a & 31), size - pos); + Memory::Memset(Memory::DepthMirrored16(a), value, n); + pos += n; + } +} + void DumpExecute::Memset(u32 ptr, u32 sz) { PROFILE_THIS_SCOPE("ReplayMemset"); struct MemsetCommand { @@ -816,7 +846,7 @@ void DumpExecute::Memset(u32 ptr, u32 sz) { // TODO: should probably do this as an operation. // The software renderer leaves the memset to the caller, like sceKernelMemset does. if (!gpu->PerformMemorySet(data->dest, (u8)data->value, data->sz)) - Memory::Memset(data->dest, (u8)data->value, data->sz); + SetMemory(data->dest, (u8)data->value, data->sz); } } @@ -829,7 +859,7 @@ void DumpExecute::Memcpy(u32 ptr, u32 sz) { if (Memory::IsVRAMAddress(execMemcpyDest)) { SyncStall(); gpu->Flush(); - Memory::MemcpyUnchecked(execMemcpyDest, pushbuf_.data() + ptr, sz); + CopyToMemory(execMemcpyDest, pushbuf_.data() + ptr, sz); NotifyMemInfo(MemBlockFlags::WRITE, execMemcpyDest, sz, "ReplayMemcpy"); gpu->PerformWriteColorFromMemory(execMemcpyDest, sz); } @@ -921,7 +951,7 @@ void DumpExecute::CopyAroundDrawn(u32 addr, const u8 *data, u32 size) { int64_t pos = start; auto copyTo = [&](int64_t until) { if (until > pos) { - Memory::MemcpyUnchecked(addr + (u32)(pos - start), data + (pos - start), (u32)(until - pos)); + CopyToMemory(addr + (u32)(pos - start), data + (pos - start), (u32)(until - pos)); NotifyMemInfo(MemBlockFlags::WRITE, addr + (u32)(pos - start), (u32)(until - pos), "ReplayTex"); } }; From 13c1b4407dcf460e4ec4e4641ea779a153c127dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 5 Oct 2026 16:25:52 -0600 Subject: [PATCH 05/11] frametests: Update IL-2's software reference Co-Authored-By: Claude Opus 5.5 (1M context) --- frametests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frametests b/frametests index a21764de0c..39e645170d 160000 --- a/frametests +++ b/frametests @@ -1 +1 @@ -Subproject commit a21764de0cce505241fb1f2a2277b844e4829115 +Subproject commit 39e645170db7b381a7e95e05a5f655cf5b3de496 From a4185143763c1a4a42001d3f2a580c3dcde45524 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Tue, 6 Oct 2026 12:57:58 -0600 Subject: [PATCH 06/11] GE dump playback: Load the initial CLUT, and show the last framebuffer when the display is off The recorder saves the CLUT the GE had loaded right after INIT, as a CLUT command with no LOADCLUT to follow, unlike the CLUTs it records at a LOADCLUT. Playback only pointed the CLUT address at it, so draws before the game's first LOADCLUT used whatever CLUT was loaded before: HotBrain (16131, ULUS10268) drew its save screen background black. Load it, and put the game's CLUT address back. A dump whose last DISPLAY turns the display off (address 0), or has none, now shows the last framebuffer drawn to (Auditorium 9213). The PSP replayer does both the same way. Co-Authored-By: Claude Opus 5.5 (1M context) --- GPU/Debugger/Playback.cpp | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/GPU/Debugger/Playback.cpp b/GPU/Debugger/Playback.cpp index 0c49e28dcb..1668ac9373 100644 --- a/GPU/Debugger/Playback.cpp +++ b/GPU/Debugger/Playback.cpp @@ -425,6 +425,10 @@ private: bool zTest_ = false; bool zWriteDisable_ = false; u32 clearMode_ = 0; + // Right after INIT, where a CLUT command is the CLUT the GE had loaded. + bool initialClut_ = false; + // Whether the last DISPLAY showed something (address 0 turns the display off). + bool haveDisplay_ = false; // The vertices recorded for the next draw. u32 lastVertsPtr_ = 0; u32 lastVertsSize_ = 0; @@ -463,6 +467,7 @@ void DumpExecute::SyncStall() { } void DumpExecute::Registers(u32 ptr, u32 sz) { + initialClut_ = false; if (execListBuf == 0) { u32 allocSize = LIST_BUF_SIZE; execListBuf = userMemory.Alloc(allocSize, true, "List buf"); @@ -620,6 +625,7 @@ void DumpExecute::Init(u32 ptr, u32 sz) { lastTex_[i] = 0; } lastBase_ = 0xFFFFFFFF; + initialClut_ = true; } // The drawing-space bounds of a through mode draw's vertices, or false if they aren't simple to read @@ -782,6 +788,16 @@ void DumpExecute::Clut(u32 ptr, u32 sz) { execListQueue.push_back((GE_CMD_CLUTADDRUPPER << 24) | ((psp >> 8) & 0x00FF0000)); execListQueue.push_back((GE_CMD_CLUTADDR << 24) | (psp & 0x00FFFFFF)); + if (initialClut_) { + // The CLUT the GE had loaded when the recording started (the recorder saves it right after + // INIT), with no LOADCLUT to follow, unlike the CLUTs recorded at a LOADCLUT: load it here, + // then put the game's CLUT address back. Without this, draws used whatever CLUT was loaded + // before (HotBrain 16131, ULUS10268, drew its save screen background black). + execListQueue.push_back((GE_CMD_LOADCLUT << 24) | ((sz / 32) & 0x3F)); + execListQueue.push_back(gstate.clutaddrupper); + execListQueue.push_back(gstate.clutaddr); + initialClut_ = false; + } } } @@ -973,6 +989,11 @@ void DumpExecute::Display(u32 ptr, u32 sz, bool allowFlip) { // Sync up drawing. SyncStall(); + // A display turned off shows nothing to compare, so the result falls back to the last framebuffer + // drawn to, as for a dump without a DISPLAY (Auditorium 9213). The PSP replayer does the same. + haveDisplay_ = disp->topaddr.ptr != 0; + if (!haveDisplay_) + return; __DisplaySetFramebuf(disp->topaddr.ptr, disp->linesize, disp->pixelFormat, 1); if (allowFlip) { __DisplaySetFramebuf(disp->topaddr.ptr, disp->linesize, disp->pixelFormat, 0); @@ -1094,6 +1115,12 @@ ReplayResult DumpExecute::Run() { } SubmitListEnd(); + if (!haveDisplay_ && fbWidth_ != 0) { + SyncStall(); + const u32 addr = 0x04000000 | (fbPtr_ & 0x001FFFF0); + __DisplaySetFramebuf(addr, fbWidth_ & 0x07FC, fbFormat_, 1); + __DisplaySetFramebuf(addr, fbWidth_ & 0x07FC, fbFormat_, 0); + } return ReplayResult::Done; } From eb59b00a37bc748b89b987cf8f77cb2848ed7a0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Tue, 6 Oct 2026 13:06:08 -0600 Subject: [PATCH 07/11] GE dump playback: Ignore a second INIT in a dump Before 71210f3fa2, a second dump request during a recording started it again, writing another INIT (and initial CLUT) into the dump: the GE state of that moment, which already includes the register writes the dump only has after it. Playback applied it, so the last draws ran with the wrong state: GOD EATER BURST (13950) and GOD EATER 2 (6343) came out black and Street Riders (14746) garbled. The recorder only writes an INIT when it starts, so skip any later one. The 15 such dumps in the GitHub issue set now match the PSP's replay, 10 of them exactly. Co-Authored-By: Claude Opus 5.5 (1M context) --- GPU/Debugger/Playback.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/GPU/Debugger/Playback.cpp b/GPU/Debugger/Playback.cpp index 1668ac9373..1014db3caf 100644 --- a/GPU/Debugger/Playback.cpp +++ b/GPU/Debugger/Playback.cpp @@ -1037,6 +1037,15 @@ ReplayResult DumpExecute::Run() { } const Command &cmd = commands_[i]; + // Only the first INIT is real. Before 71210f3fa2 a second dump request during a recording started + // it again, writing another INIT (and initial CLUT) into the dump: the GE state of that moment, + // which already includes register writes the dump only has after it. Applied, it ran the last + // draws with the wrong state (GOD EATER BURST 13950 came out black, Street Riders 14746 garbled). + if (cmd.type == CommandType::INIT && i != 0) { + if (i + 1 < commands_.size() && commands_[i + 1].type == CommandType::CLUT) + ++i; + continue; + } switch (cmd.type) { case CommandType::INIT: Init(cmd.ptr, cmd.sz); From 937053063ca936a3d168a27d838d523c99852bd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Tue, 6 Oct 2026 13:13:57 -0600 Subject: [PATCH 08/11] GE dump playback: Give unaligned CLUTs and textures an aligned copy Older recorders reused data at any offset in the dump, and Prince of Persia: Rival Swords (11587, 12943) has a CLUT stored one byte into the previous one. The GE ignores the low four bits of a CLUT or texture address, so the LOADCLUT read 15 bytes early and the palette channel-swap pass came out red. Map CLUTs, textures and transfer sources that aren't 16-byte aligned to a separate aligned copy. Also updates LocoRoco 12058's software reference, which now matches the PSP's replay exactly. Co-Authored-By: Claude Opus 5.5 (1M context) --- GPU/Debugger/Playback.cpp | 17 +++++++++++------ frametests | 2 +- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/GPU/Debugger/Playback.cpp b/GPU/Debugger/Playback.cpp index 1014db3caf..81b0d96cee 100644 --- a/GPU/Debugger/Playback.cpp +++ b/GPU/Debugger/Playback.cpp @@ -140,8 +140,8 @@ public: BufMapping(const std::vector &pushbuf) : pushbuf_(pushbuf) { } - // Returns a pointer to contiguous memory for this access, or else 0 (failure). - u32 Map(u32 bufpos, u32 sz, const std::function &flush); + // Returns a pointer to contiguous memory for this access, aligned to align bytes, or else 0 (failure). + u32 Map(u32 bufpos, u32 sz, const std::function &flush, u32 align = 1); // Clear and reset allocations made. void Reset() { @@ -229,10 +229,15 @@ protected: const std::vector &pushbuf_; }; -u32 BufMapping::Map(u32 bufpos, u32 sz, const std::function &flush) { +u32 BufMapping::Map(u32 bufpos, u32 sz, const std::function &flush, u32 align) { int slab1 = bufpos / SLAB_SIZE; int slab2 = (bufpos + sz - 1) / SLAB_SIZE; + // Older recorders could reuse data at any offset (a CLUT found one byte into another), but the GE + // ignores the low bits of a CLUT or texture address. The extra mappings are separate, aligned copies. + if ((bufpos & (align - 1)) != 0) + return MapExtra(bufpos, sz, flush); + if (slab1 == slab2) { // Shortcut in case it's simply the most recent slab. if (slabs_[lastSlab_].Matches(slab1 * SLAB_SIZE)) @@ -780,7 +785,7 @@ void DumpExecute::Clut(u32 ptr, u32 sz) { execClutAddr = 0; } else { - u32 psp = mapping_.Map(ptr, sz, std::bind(&DumpExecute::SyncStall, this)); + u32 psp = mapping_.Map(ptr, sz, std::bind(&DumpExecute::SyncStall, this), 16); if (psp == 0) { ERROR_LOG(Log::GeDebugger, "Unable to allocate for clut"); return; @@ -802,7 +807,7 @@ void DumpExecute::Clut(u32 ptr, u32 sz) { } void DumpExecute::TransferSrc(u32 ptr, u32 sz) { - u32 psp = mapping_.Map(ptr, sz, std::bind(&DumpExecute::SyncStall, this)); + u32 psp = mapping_.Map(ptr, sz, std::bind(&DumpExecute::SyncStall, this), 16); if (psp == 0) { ERROR_LOG(Log::GeDebugger, "Unable to allocate for transfer"); return; @@ -882,7 +887,7 @@ void DumpExecute::Memcpy(u32 ptr, u32 sz) { } void DumpExecute::Texture(int level, u32 ptr, u32 sz) { - u32 psp = mapping_.Map(ptr, sz, std::bind(&DumpExecute::SyncStall, this)); + u32 psp = mapping_.Map(ptr, sz, std::bind(&DumpExecute::SyncStall, this), 16); if (psp == 0) { ERROR_LOG(Log::GeDebugger, "Unable to allocate for texture"); return; diff --git a/frametests b/frametests index 39e645170d..4bccabd3d3 160000 --- a/frametests +++ b/frametests @@ -1 +1 @@ -Subproject commit 39e645170db7b381a7e95e05a5f655cf5b3de496 +Subproject commit 4bccabd3d3951fd7cebb01a23794807e5faa47d8 From a26ef51a0cecde5dd1785358a9bd18a6745eb3e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Tue, 6 Oct 2026 13:16:55 -0600 Subject: [PATCH 09/11] GE dump playback: Restore old-layout contexts from old dumps Dumps recorded before savedContextVersion 1 hold PPSSPP's old context layout, with the matrices as raw floats, but GEState::Restore picks the layout from savedContextVersion, which only a savestate sets. Playback restored them as the new layout, so the matrices became garbage and the scene vanished (4140 drew only its HUD). Tell the layouts apart by the END the new one ends with, as the PSP replayer does. Other callers are unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) --- GPU/Debugger/Playback.cpp | 13 ++++++++++++- GPU/GPUState.cpp | 6 +++++- GPU/GPUState.h | 2 ++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/GPU/Debugger/Playback.cpp b/GPU/Debugger/Playback.cpp index 81b0d96cee..9c636d5063 100644 --- a/GPU/Debugger/Playback.cpp +++ b/GPU/Debugger/Playback.cpp @@ -610,7 +610,18 @@ void DumpExecute::SubmitListEnd() { } void DumpExecute::Init(u32 ptr, u32 sz) { - gstate.Restore((u32_le *)(pushbuf_.data() + ptr)); + // Dumps from before savedContextVersion 1 hold PPSSPP's old context layout, with the matrices as raw + // floats; the new one ends its commands with an END. Restoring an old one as new turned its matrices + // into garbage and the scene disappeared (4140). The PSP replayer tells them apart the same way. + const u32_le *context = (const u32_le *)(pushbuf_.data() + ptr); + bool oldLayout = true; + for (int i = 17; i < 512; ++i) { + if (context[i] == GE_CMD_END << 24) { + oldLayout = false; + break; + } + } + gstate.Restore(context, oldLayout); ExecuteOnMain(Operation{ OpType::ReapplyGfxState }); fbPtr_ = gstate.fbptr & 0x00FFFFFF; fbWidth_ = gstate.fbwidth & 0x00FFFFFF; diff --git a/GPU/GPUState.cpp b/GPU/GPUState.cpp index 8b7de27977..c384df55df 100644 --- a/GPU/GPUState.cpp +++ b/GPU/GPUState.cpp @@ -205,6 +205,10 @@ void GEState::FastLoadBoneMatrix(u32 addr) { } void GEState::Restore(const u32_le *ptr) { + Restore(ptr, savedContextVersion == 0); +} + +void GEState::Restore(const u32_le *ptr, bool oldLayout) { // Not sure what the first 10 values are, exactly, but these seem right. gstate_c.vertexAddr = ptr[5]; gstate_c.indexAddr = ptr[6]; @@ -218,7 +222,7 @@ void GEState::Restore(const u32_le *ptr) { } } - if (savedContextVersion == 0) { + if (oldLayout) { if (Memory::IsValidAddress(getClutAddress())) loadclut = *cmds++; boneMatrixNumber = *cmds++; diff --git a/GPU/GPUState.h b/GPU/GPUState.h index 0422d67d89..7fe8bd8238 100644 --- a/GPU/GPUState.h +++ b/GPU/GPUState.h @@ -439,6 +439,8 @@ struct GEState { static void Reset(); void Save(u32_le *ptr); void Restore(const u32_le *ptr); + // oldLayout: PPSSPP's layout before savedContextVersion 1, with the matrices as raw floats. + void Restore(const u32_le *ptr, bool oldLayout); }; bool vertTypeIsSkinningEnabled(u32 vertType); From 75e79ef2db6fda85b02d569447cd55402b263b94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Tue, 6 Oct 2026 13:46:30 -0600 Subject: [PATCH 10/11] GE dump playback: Let the GE finish the list before reusing its buffer At the end of the 256 KB list buffer, playback jumped back to its start and kept writing, so when the GE was more than a lap behind, the next lap overwrote commands it hadn't reached. Warriors (14660) lost half its draws, and GTA LCS (15149) some. Finish the list, wait for it, and start a new one instead, as the PSP replayer does. Both now match the PSP's replay exactly. Co-Authored-By: Claude Opus 5.5 (1M context) --- GPU/Debugger/Playback.cpp | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/GPU/Debugger/Playback.cpp b/GPU/Debugger/Playback.cpp index 9c636d5063..a42192ce6e 100644 --- a/GPU/Debugger/Playback.cpp +++ b/GPU/Debugger/Playback.cpp @@ -498,22 +498,21 @@ void DumpExecute::Registers(u32 ptr, u32 sz) { // Validate space for jump. u32 allocSize = pendingSize + sz + 8; if (execListPos + allocSize >= execListBuf + LIST_BUF_SIZE) { - Memory::WriteUnchecked_U32((GE_CMD_BASE << 24) | ((execListBuf >> 8) & 0x00FF0000), execListPos); - Memory::WriteUnchecked_U32((GE_CMD_JUMP << 24) | (execListBuf & 0x00FFFFFF), execListPos + 4); + // Finish this list, wait for the GE to run it, and start a new one at the buffer's start. Jumping + // back instead let the next lap overwrite commands the GE hadn't reached yet: Warriors (14660) lost + // half its draws. The PSP replayer does the same. GE state, BASE included, carries over. + Memory::WriteUnchecked_U32(GE_CMD_FINISH << 24, execListPos); + Memory::WriteUnchecked_U32(GE_CMD_END << 24, execListPos + 4); + execListPos += 8; + SyncStall(); + ExecuteOnMain(Operation{ OpType::ListSync, execListID }); execListPos = execListBuf; - // The queued vertex, index and texture addresses were made for the previous base, which the jump - // just replaced (Tiger Woods ULUS10420 then read draw 21026's indices from 16 MB away). - if (lastBase_ != 0xFFFFFFFF && lastBase_ != (execListBuf & 0xFF000000)) { - Memory::WriteUnchecked_U32((GE_CMD_BASE << 24) | ((lastBase_ >> 8) & 0x00FF0000), execListPos); - execListPos += 4; - } else { - lastBase_ = execListBuf & 0xFF000000; - } - - // Don't continue until we've stalled. - // TODO: Is this really needed? It seems fine without it. - SyncStall(); + Memory::WriteUnchecked_U32(GE_CMD_NOP << 24, execListPos); + execListPos += 4; + gpu->EnableInterrupts(false); + execListID = ExecuteOnMain(Operation{ OpType::EnqueueList, execListBuf, execListPos }); + gpu->EnableInterrupts(true); } Memory::MemcpyUnchecked(execListPos, execListQueue.data(), pendingSize); From 50ba8e459bccb27ca451397c73cfa006c82c1614 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Tue, 6 Oct 2026 14:21:40 -0600 Subject: [PATCH 11/11] SoftGPU: Keep a not-black color test when dithering, fog or a logic op is on The software renderer drops a color test that only rejects black under blending that adds to the destination, since rejecting black then only skips adding nothing. With dithering on it doesn't: a rejected pixel keeps the destination, an accepted black one gets the dither added. Ridge Racer 2 (20839) adds a glow that way and came out one step darker wherever the dither matrix was negative; Burnout Legends' bloom (16119) too. Fog and logic ops would change the destination the same way. Co-Authored-By: Claude Opus 5.5 (1M context) --- GPU/Software/FuncId.cpp | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/GPU/Software/FuncId.cpp b/GPU/Software/FuncId.cpp index a1d11b495c..efec97eabc 100644 --- a/GPU/Software/FuncId.cpp +++ b/GPU/Software/FuncId.cpp @@ -183,17 +183,19 @@ void ComputePixelFuncID(PixelFuncID *id) { id->alphaBlendDst = (uint8_t)OptimizeAlphaFactor(gstate.getFixB()); } + id->applyLogicOp = gstate.isLogicOpEnabled() && gstate.getLogicOp() != GE_LOGIC_COPY; + id->applyFog = gstate.isFogEnabled() && !gstate.isModeThrough(); + if (id->colorTest && gstate.getColorTestFunction() == GE_COMP_NOTEQUAL && gstate.getColorTestRef() == 0 && gstate.getColorTestMask() == 0xFFFFFF) { if (!id->depthWrite && !id->stencilTest && id->alphaBlend && id->AlphaBlendEq() == GE_BLENDMODE_MUL_AND_ADD) { - // Might be a pointless color test (seen in Ridge Racer, for example.) - if (id->AlphaBlendDst() == PixelBlendFactor::ONE) + // Rejecting black then only skips adding nothing to the destination, unless dithering, fog + // or a logic op would still change it: Ridge Racer 2 (UCES00422) adds a glow with dithering, + // and its black pixels must not get the dither. + if (id->AlphaBlendDst() == PixelBlendFactor::ONE && !id->dithering && !id->applyFog && !id->applyLogicOp) id->colorTest = false; } } - id->applyLogicOp = gstate.isLogicOpEnabled() && gstate.getLogicOp() != GE_LOGIC_COPY; - id->applyFog = gstate.isFogEnabled() && !gstate.isModeThrough(); - id->earlyZChecks = id->DepthTestFunc() != GE_COMP_ALWAYS; if (id->stencilTest && id->earlyZChecks) { // Can't do them early if stencil might need to write.