POPS: Upload images the emulator writes to VRAM with the CPU

The PS1 emulator writes an image that comes through the GP0 data port
(rather than by DMA) into VRAM one pixel at a time with the CPU. The
hardware renderers never saw those pixels, so a later copy out of the PS1
VRAM framebuffer read stale texels.

The two row ends of that loop now go to an HLE call, which uploads the
finished rectangle with a new PerformWriteRectFromMemory().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Henrik RydgårdandClaude Opus 5.5 committed 2026-11-05 16:08:25 +01:00
1 parent 5b5465c78b
commit 05de369578
7 files changed
+72 -2

No files matched your search

+53
View File
@@ -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 <algorithm>
#include <cstring>
#include <memory>
#include <string>
@@ -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;
@@ -303,10 +306,43 @@ static int __PopsReadIcon0(u32 file, u32 buf, u32 offset, u32 size) {
return hleLogDebug(Log::sceKernel, (int)size);
}
// The emulator's state for a CPU-to-VRAM image transfer through the GP0 port, from its gp: the
// rectangle (x0, y0, x1, y1), then the current x and y.
static const u32 POPS_IMAGE_LOAD_STATE = 0x363C;
// The end of a row of such a transfer, which the emulator writes pixel by pixel with the CPU. When
// it's the last row, the rectangle goes into the framebuffer.
static void __PopsImageLoadRow() {
// The loop's registers are live across the call.
hleSkipDeadbeef();
const u32 state = currentMIPS->r[MIPS_REG_GP] + POPS_IMAGE_LOAD_STATE;
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 +476,23 @@ 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, a2, 1; beq rM, rN; sh rM, y(gp).
int imageLoadRows = 0;
for (u32 addr = textAddr; addr + 16 <= textAddr + textSize; addr += 4) {
const u32 op = Memory::ReadUnchecked_U32(addr);
const u32 next = Memory::ReadUnchecked_U32(addr + 4);
const bool first = op == 0x97983642 && next == 0x24C60001 && (Memory::ReadUnchecked_U32(addr + 8) >> 16) == 0x10D8 && Memory::ReadUnchecked_U32(addr + 12) == 0xA7863646;
const bool second = op == 0x97903642 && next == 0x24C40001 && (Memory::ReadUnchecked_U32(addr + 8) >> 16) == 0x1090 && Memory::ReadUnchecked_U32(addr + 12) == 0xA7843646;
if (first || second) {
PatchTrampoline(addr, "__PopsImageLoadRow", op, addr + 8);
imageLoadRows++;
}
}
if (imageLoadRows != 2) {
WARN_LOG(Log::sceKernel, "POPS: found %d of the 2 GP0 image transfer row ends, hardware renderers may miss its pixels", imageLoadRows);
}
if (!g_popsConvertedPBP) {
return;
}
+1 -1
View File
@@ -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) {
+8
View File
@@ -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);
+2
View File
@@ -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.
+4
View File
@@ -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);
+1
View File
@@ -184,6 +184,7 @@ public:
std::vector<const VirtualFramebuffer *> GetFramebufferList() const override { return std::vector<const VirtualFramebuffer *>(); }
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;
+3 -1
View File
@@ -43,7 +43,9 @@ 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). 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.