Merge pull request #22943 from hrydgard/psx-graphics

POPS: Upload images the emulator writes to VRAM with the CPU
This commit is contained in:
Henrik Rydgård authored and GitHub committed 2026-11-05 17:19:15 +01:00
commit 4866f356b5
7 files changed
+92 -3

No files matched your search

+72 -1
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;
@@ -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<HLEHelperThread> &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<std::pair<u32, u32>> 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;
}
+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;
+4 -1
View File
@@ -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.