diff --git a/Core/HLE/PopsBoot.cpp b/Core/HLE/PopsBoot.cpp index b74b6d3a99..bc4b410d3a 100644 --- a/Core/HLE/PopsBoot.cpp +++ b/Core/HLE/PopsBoot.cpp @@ -19,6 +19,7 @@ // This stands in for the parts of the POPS boot PPSSPP doesn't run: the kernel's init, and the // Media Engine the manager's ME loop runs on. See docs/POPS.md. +#include #include #include #include @@ -51,6 +52,8 @@ #include "Core/MemMapHelpers.h" #include "Core/Screenshot.h" #include "Core/System.h" +#include "GPU/GPU.h" +#include "GPU/GPUCommon.h" static bool g_popsBoot = false; static bool g_popsConvertedPBP = false; @@ -97,6 +100,8 @@ static const PopsManLayout POPSMAN_LAYOUTS[] = { { 0x1404, 0x030, 0, 0x3390, 0x33B0, 0x33B8, 0x0538, 0x05C4, 0x0138, 1 }, // 3.95-3.96 }; static int g_popsManLayout = 0; +// Where the emulator keeps its GP0 image transfer's rectangle, from its gp (0 if not found). +static u32 g_popsImageLoadState = 0; static const u32 SCE_POPSMAN_LOAD_MODULE_NID = 0x29B3FB24; @@ -109,6 +114,7 @@ void __PopsBootShutdown() { g_popsConvertedPBP = false; g_popsIcon0Usable = true; g_popsManLayout = 0; + g_popsImageLoadState = 0; g_popsPBPPath.clear(); // The kernel objects and memory are already gone with the kernel. if (g_popsInitThread) { @@ -138,7 +144,7 @@ static void DoHelperThread(PointerWrap &p, std::unique_ptr &thr } void __PopsBootDoState(PointerWrap &p) { - auto s = p.Section("PopsBoot", 0, 2); + auto s = p.Section("PopsBoot", 0, 3); if (!s) { if (p.mode == PointerWrap::MODE_READ) { __PopsBootShutdown(); @@ -158,6 +164,12 @@ void __PopsBootDoState(PointerWrap &p) { } else { g_popsManLayout = 0; } + if (s >= 3) { + Do(p, g_popsImageLoadState); + } else { + // Older states don't have the hook in the emulator's code. + g_popsImageLoadState = 0; + } } static const PopsManLayout &PopsMan() { @@ -303,10 +315,43 @@ static int __PopsReadIcon0(u32 file, u32 buf, u32 offset, u32 size) { return hleLogDebug(Log::sceKernel, (int)size); } +// The end of a row of a CPU-to-VRAM image transfer through the GP0 port, which the emulator writes +// pixel by pixel with the CPU. When it's the last row, the rectangle goes into the framebuffer. Its +// state: the rectangle (x0, y0, x1, y1), then the current x and y. +static void __PopsImageLoadRow() { + // The loop's registers are live across the call. + hleSkipDeadbeef(); + if (!g_popsImageLoadState) { + return; + } + const u32 state = currentMIPS->r[MIPS_REG_GP] + g_popsImageLoadState; + const u16 x0 = Memory::ReadUnchecked_U16(state), y0 = Memory::ReadUnchecked_U16(state + 2); + const u16 x1 = Memory::ReadUnchecked_U16(state + 4), y1 = Memory::ReadUnchecked_U16(state + 6); + const u16 y = Memory::ReadUnchecked_U16(state + 10); + if ((u16)(y + 1) != y1 || !gpu) { + return; + } + // VRAM is 1024x512 and the transfer wraps around its edges. + const int w = std::min((u16)(x1 - x0), (u16)1024), h = std::min((u16)(y1 - y0), (u16)512); + for (int ys = 0; ys < h;) { + const int sy = (y0 + ys) & 511; + const int sh = std::min(h - ys, 512 - sy); + for (int xs = 0; xs < w;) { + const int sx = (x0 + xs) & 1023; + const int sw = std::min(w - xs, 1024 - sx); + gpu->PerformWriteRectFromMemory(0x04000000 + (sy * 1024 + sx) * 2, 1024, sw, sh, GE_FORMAT_5551); + xs += sw; + } + ys += sh; + } + DEBUG_LOG(Log::sceKernel, "POPS: image load %d,%d %dx%d", x0, y0, w, h); +} + const HLEFunction PopsBootCalls[] = { { 0x13370006, &WrapI_UUU<__PopsDeflateDecompress>, "__PopsDeflateDecompress", 'i', "xxx" }, { 0x13370007, &WrapI_UUU<__PopsGetVersionKey>, "__PopsGetVersionKey", 'i', "xxx" }, { 0x13370008, &WrapI_UUUU<__PopsReadIcon0>, "__PopsReadIcon0", 'i', "xxxx" }, + { 0x13370009, &WrapV_V<__PopsImageLoadRow>, "__PopsImageLoadRow", 'v', "" }, }; void Register_PopsBootCalls() { @@ -440,6 +485,32 @@ void __PopsBootPatchEmulator(u32 textAddr, u32 textSize, u32 decodeStub) { } currentMIPS->InvalidateICacheRangeDeferred(textAddr, textSize); + // The GP0 port's image transfer ends its rows in two places (after the word's first pixel or its + // second): lhu rN, y1(gp); addiu rM, rY, 1; beq rM, rN; sh rM, y(gp), with y just after y1. The + // registers and offsets differ between versions (3.80 to 6.61), the shape doesn't. + std::vector> rowEnds; // address, y1 + for (u32 addr = textAddr; addr + 16 <= textAddr + textSize; addr += 4) { + const u32 lhu = Memory::ReadUnchecked_U32(addr); + const u32 addiu = Memory::ReadUnchecked_U32(addr + 4); + const u32 beq = Memory::ReadUnchecked_U32(addr + 8); + const u32 sh = Memory::ReadUnchecked_U32(addr + 12); + const u32 rN = (lhu >> 16) & 31, rM = (addiu >> 16) & 31; + const u32 y1 = lhu & 0xFFFF; + if ((lhu & 0xFFE00000) != 0x97800000 || (addiu & 0xFC00FFFF) != 0x24000001 || (beq >> 16) != (0x1000 | (rM << 5) | rN) || + sh != (0xA7800000 | (rM << 16) | ((y1 + 4) & 0xFFFF)) || y1 < 6) { + continue; + } + rowEnds.emplace_back(addr, y1); + } + if (rowEnds.size() == 2 && rowEnds[0].second == rowEnds[1].second) { + for (const auto &rowEnd : rowEnds) { + PatchTrampoline(rowEnd.first, "__PopsImageLoadRow", Memory::ReadUnchecked_U32(rowEnd.first), rowEnd.first + 8); + } + g_popsImageLoadState = rowEnds[0].second - 6; + } else { + WARN_LOG(Log::sceKernel, "POPS: found %d GP0 image transfer row ends, not 2 alike, hardware renderers may miss its pixels", (int)rowEnds.size()); + } + if (!g_popsConvertedPBP) { return; } diff --git a/GPU/Common/FramebufferManagerCommon.cpp b/GPU/Common/FramebufferManagerCommon.cpp index 3749c28f23..0a909fa5b3 100644 --- a/GPU/Common/FramebufferManagerCommon.cpp +++ b/GPU/Common/FramebufferManagerCommon.cpp @@ -1257,7 +1257,7 @@ void FramebufferManagerCommon::WriteRectFromMemory(u32 addr, int size, int strid VirtualFramebuffer *target = nullptr; for (auto vfb : vfbs_) { - if (!vfb->fbo || vfb->FbStrideInBytes() != byteStride || addr <= vfb->fb_address || addr >= vfb->fb_address + vfb->BufferByteSize(RASTER_COLOR)) { + if (!vfb->fbo || vfb->FbStrideInBytes() != byteStride || addr < vfb->fb_address || addr >= vfb->fb_address + vfb->BufferByteSize(RASTER_COLOR)) { continue; } if (!target || vfb->colorBindSeq > target->colorBindSeq) { diff --git a/GPU/GPUCommon.cpp b/GPU/GPUCommon.cpp index a3d3d78ea3..428ffc4c11 100644 --- a/GPU/GPUCommon.cpp +++ b/GPU/GPUCommon.cpp @@ -2686,6 +2686,14 @@ void GPUCommon::PerformWriteFormattedFromMemory(u32 addr, int size, int frameWid InvalidateCache(addr, size, GPU_INVALIDATE_SAFE); } +void GPUCommon::PerformWriteRectFromMemory(u32 addr, int stride, int width, int height, GEBufferFormat format) { + const int bpp = BufferFormatBytesPerPixel(format); + if (Memory::IsVRAMAddress(addr) && framebufferManager_) { + framebufferManager_->WriteRectFromMemory(addr, height * stride * bpp, stride, format, width); + } + InvalidateCache(addr, ((height - 1) * stride + width) * bpp, GPU_INVALIDATE_SAFE); +} + bool GPUCommon::PerformReadbackStencilToMemory(u32 dest, int size) { if (Memory::IsVRAMAddress(dest) && framebufferManager_->MayIntersectFramebufferColor(dest)) { return framebufferManager_->ReadbackStencilToMemory(dest, size); diff --git a/GPU/GPUCommon.h b/GPU/GPUCommon.h index 4346bf1a0f..e2f1f20c6f 100644 --- a/GPU/GPUCommon.h +++ b/GPU/GPUCommon.h @@ -174,6 +174,8 @@ public: // width is the stride. rectWidth, if set, is how many pixels of each row were written, which lets // a write that starts partway into a framebuffer row be uploaded without clobbering the rest. virtual void PerformWriteFormattedFromMemory(u32 addr, int size, int width, GEBufferFormat format, int rectWidth = 0); + // The CPU has written a width x height rectangle of pixels at addr, rows stride pixels apart. + virtual void PerformWriteRectFromMemory(u32 addr, int stride, int width, int height, GEBufferFormat format); virtual bool PerformWriteStencilFromMemory(u32 dest, int size, WriteStencil flags); // Reads the stencil of whole framebuffer rows into the alpha bytes in RAM, leaving the color alone. // For games that test stencil with the CPU. Only 8888 for now. diff --git a/GPU/Software/SoftGpu.cpp b/GPU/Software/SoftGpu.cpp index 4be76c783f..0ed0ec1fe3 100644 --- a/GPU/Software/SoftGpu.cpp +++ b/GPU/Software/SoftGpu.cpp @@ -1506,6 +1506,10 @@ void SoftGPU::PerformWriteFormattedFromMemory(u32 addr, int size, int width, GEB NoteVideoRange(addr, (u32)size); } +void SoftGPU::PerformWriteRectFromMemory(u32 addr, int stride, int width, int height, GEBufferFormat format) { + InvalidateCache(addr, ((height - 1) * stride + width) * BufferFormatBytesPerPixel(format), GPU_INVALIDATE_HINT); +} + bool SoftGPU::PerformMemoryCopy(u32 dest, u32 src, int size, GPUCopyFlag flags) { // Drawing can still be queued after a list stalled (FinishDeferred). drawEngine_->transformUnit.FlushIfOverlap(this, "memcpy", false, src, size, size, 1); diff --git a/GPU/Software/SoftGpu.h b/GPU/Software/SoftGpu.h index 4661faeb99..ff250d8eda 100644 --- a/GPU/Software/SoftGpu.h +++ b/GPU/Software/SoftGpu.h @@ -184,6 +184,7 @@ public: std::vector GetFramebufferList() const override { return std::vector(); } void InvalidateCache(u32 addr, int size, GPUInvalidationType type) override; void PerformWriteFormattedFromMemory(u32 addr, int size, int width, GEBufferFormat format, int rectWidth) override; + void PerformWriteRectFromMemory(u32 addr, int stride, int width, int height, GEBufferFormat format) override; bool PerformMemoryCopy(u32 dest, u32 src, int size, GPUCopyFlag flags = GPUCopyFlag::NONE) override; bool PerformMemorySet(u32 dest, u8 v, int size) override; bool PerformReadbackToMemory(u32 dest, int size) override; diff --git a/docs/POPS.md b/docs/POPS.md index b1ff7f0938..2a20feb9e5 100644 --- a/docs/POPS.md +++ b/docs/POPS.md @@ -43,7 +43,10 @@ without them. The emulator keeps the PS1's 1024x512 VRAM at `0x04000000` as one 16-bit framebuffer, and reads and writes it outside where it draws. It's a fixed framebuffer: created whole, from memory, and uploaded without -filtering. Its 24-bit video is unpacked three bytes per pixel into `0x04100000`, which is kept at 1x. It +filtering. An image the game sends to VRAM through the GP0 port rather than by DMA is written there by the +CPU, pixel by pixel, which the hardware renderers don't see: the end of each row of that loop is hooked, and +the finished rectangle is uploaded (Ridge Racer's title screen flag). The loop is found by its shape, which +is the same in every emulator from 3.80 to 6.61 and every model's, while its registers and offsets aren't. Its 24-bit video is unpacked three bytes per pixel into `0x04100000`, which is kept at 1x. It draws sprites on pixel centers, which above 1x read their neighbours' texels, so those are moved back half a pixel. And it builds video frames by ORing passes together, so draws whose logic op is done in the shader are drawn one at a time.