From ae68a7e73f337c18e2d8a94f14a606fa3961ac7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 12 Oct 2026 10:32:55 +0200 Subject: [PATCH] Coded Arms: Contagion: Read back the stencil the crosshair checks The crosshair turns red, and lock-on works, when stencil bit 3 is set at the centre of the frame, which the game tests by reading the pixel from VRAM with the CPU (#17936). Hook that and read back the stencil of that row into the alpha bytes in RAM. The alpha the hardware backends keep doesn't follow stencil everywhere, so reading color isn't enough. Adds PerformReadbackStencilToMemory for this (8888 only for now). Co-Authored-By: Claude Opus 5.5 (1M context) --- Core/HLE/ReplaceTables.cpp | 17 +++++++ Core/MIPS/MIPSAnalyst.cpp | 1 + GPU/Common/FramebufferManagerCommon.cpp | 67 +++++++++++++++++++++++++ GPU/Common/FramebufferManagerCommon.h | 1 + GPU/GPUCommon.cpp | 7 +++ GPU/GPUCommon.h | 3 ++ GPU/Software/SoftGpu.cpp | 6 +++ GPU/Software/SoftGpu.h | 1 + 8 files changed, 103 insertions(+) diff --git a/Core/HLE/ReplaceTables.cpp b/Core/HLE/ReplaceTables.cpp index 06521cbdca..5c977f4d7f 100644 --- a/Core/HLE/ReplaceTables.cpp +++ b/Core/HLE/ReplaceTables.cpp @@ -1467,6 +1467,22 @@ static int Hook_everybodysgolf_spinner_background() { return 0; } +static int Hook_codedarms_stencil_check() { + // The crosshair turns red, and lock-on works, when stencil bit 3 is set at the centre of the frame + // just rendered, which the game tests by reading the pixel from VRAM (#17936). Hooked at the load, + // where v0 is the pixel's address. Read back the stencil of the row it's in (the game's stride is + // 512). Only stencil: the alpha the hardware backends keep doesn't follow it everywhere. + const u32 addr = currentMIPS->r[MIPS_REG_V0]; + if (!Memory::IsVRAMAddress(addr)) { + return 0; + } + const u32 row = addr & ~0x7FF; + if (Memory::IsValidRange(row, 0x800)) { + gpu->PerformReadbackStencilToMemory(row, 0x800); + } + return 0; +} + static int Hook_katamari_render_check() { if (!Memory::IsValidRange(currentMIPS->r[MIPS_REG_A0] + 0x3C, 8)) { return 0; @@ -1804,6 +1820,7 @@ static const ReplacementTableEntry entries[] = { { "everybodystennis_copy_portrait", &Hook_everybodys_copy_portrait, 0, REPFLAG_HOOKENTER, 0x54 }, { "everybodysgolf_copy_portrait", &Hook_everybodysgolf_copy_portrait, 0, REPFLAG_HOOKENTER, 0x50 }, { "everybodysgolf_spinner_background", &Hook_everybodysgolf_spinner_background, 0, REPFLAG_HOOKENTER, 0x138 }, + { "codedarms_stencil_check", &Hook_codedarms_stencil_check, 0, REPFLAG_HOOKENTER, 0x8C }, {} }; diff --git a/Core/MIPS/MIPSAnalyst.cpp b/Core/MIPS/MIPSAnalyst.cpp index 1d25ce177d..fc87bc6701 100644 --- a/Core/MIPS/MIPSAnalyst.cpp +++ b/Core/MIPS/MIPSAnalyst.cpp @@ -482,6 +482,7 @@ static const HardHashTableEntry hardcodedHashes[] = { { 0xeabb9c1b4f83d2b4, 52, "memset_jak", }, // Crisis Core (US), Jak and Daxter (this is a slow memset and needs to have slow timing) { 0xeb0f7bf63d52ece9, 88, "strncat", }, { 0xeb8c0834d8bbc28c, 416, "fmodf", }, + { 0xec9ad8df944a770b, 180, "codedarms_stencil_check", }, // Coded Arms: Contagion (EUR) { 0xed8918f378e9a563, 628, "sd_gundam_g_generation_download_frame", }, // SD Gundam G Generation Overworld { 0xedbbe9bf9fbceca8, 172, "dl_write_viewport2", }, { 0xedc3f476221f96e6, 148, "tanf", }, diff --git a/GPU/Common/FramebufferManagerCommon.cpp b/GPU/Common/FramebufferManagerCommon.cpp index 5818264215..7c35b73809 100644 --- a/GPU/Common/FramebufferManagerCommon.cpp +++ b/GPU/Common/FramebufferManagerCommon.cpp @@ -3487,6 +3487,73 @@ bool FramebufferManagerCommon::ReadbackStencilbuffer(Draw::Framebuffer *fbo, int return draw_->CopyFramebufferToMemory(fbo, Draw::Aspect::DEPTH_BIT, x, y, w, h, Draw::DataFormat::S8, pixels, pixelsStride, mode, "ReadbackStencilbufferSync"); } +bool FramebufferManagerCommon::ReadbackStencilToMemory(u32 addr, int size) { + if (!useBufferedRendering_) { + return false; + } + addr &= 0x3FFFFFFF; + if (Memory::IsVRAMAddress(addr)) + addr &= 0x041FFFFF; + + VirtualFramebuffer *vfb = nullptr; + for (auto v : vfbs_) { + if (!v->fbo || v->fb_stride == 0 || v->fb_format != GE_FORMAT_8888) { + continue; + } + if (addr >= v->fb_address && addr + size <= v->fb_address + v->BufferByteSize(RASTER_COLOR)) { + if (!vfb || v->colorBindSeq > vfb->colorBindSeq) { + vfb = v; + } + } + } + if (!vfb) { + return false; + } + + // Whole rows only. + const u32 byteStride = vfb->fb_stride * 4; + const u32 offset = addr - vfb->fb_address; + if (offset % byteStride != 0 || size % byteStride != 0) { + return false; + } + const int y = offset / byteStride; + const int h = std::min((int)(size / byteStride), (int)vfb->height - y); + const int w = std::min((int)vfb->fb_stride, (int)vfb->width); + if (w <= 0 || h <= 0) { + return false; + } + + FlushBeforeCopy(); + + const int scale = vfb->renderScaleFactor; + const int sw = w * scale; + const int sh = h * scale; + std::vector stencil(sw * sh); + bool ok = draw_->CopyFramebufferToMemory(vfb->fbo, Draw::Aspect::STENCIL_BIT, 0, y * scale, sw, sh, Draw::DataFormat::S8, stencil.data(), sw, Draw::ReadbackMode::BLOCK, "ReadbackStencilToMemory"); + if (!ok) { + ok = ReadbackStencilbuffer(vfb->fbo, 0, y * scale, sw, sh, stencil.data(), sw, Draw::ReadbackMode::BLOCK); + } + draw_->Invalidate(InvalidationFlags::CACHED_RENDER_STATE); + textureCache_->ForgetLastTexture(); + RebindFramebuffer("RebindFramebuffer - ReadbackStencilToMemory"); + gpuStats.perFrame.numBlockingReadbacks++; + if (!ok) { + return false; + } + + // PSP stencil is the alpha byte, so that's all that changes. + u8 *dest = Memory::GetPointerWriteUnchecked(vfb->fb_address + offset); + for (int row = 0; row < h; row++) { + const u8 *src = &stencil[row * scale * sw]; + u8 *dst = dest + row * byteStride; + for (int x = 0; x < w; x++) { + dst[x * 4 + 3] = src[x * scale]; + } + } + NotifyMemInfo(MemBlockFlags::WRITE, vfb->fb_address + offset, h * byteStride, "ReadbackStencil"); + return true; +} + void FramebufferManagerCommon::ReadFramebufferToMemory(VirtualFramebuffer *vfb, int x, int y, int w, int h, RasterChannel channel, Draw::ReadbackMode mode) { if (!vfb || !vfb->fbo) { return; diff --git a/GPU/Common/FramebufferManagerCommon.h b/GPU/Common/FramebufferManagerCommon.h index f8e5373f21..137f4ebc39 100644 --- a/GPU/Common/FramebufferManagerCommon.h +++ b/GPU/Common/FramebufferManagerCommon.h @@ -351,6 +351,7 @@ public: void UpdateFromMemory(u32 addr, int size); void ApplyClearToMemory(int x1, int y1, int x2, int y2, u32 clearColor); bool PerformWriteStencilFromMemory(u32 addr, int size, WriteStencil flags); + bool ReadbackStencilToMemory(u32 addr, int size); void WriteStencilFromFramebufferAlpha(VirtualFramebuffer *dst); // We changed our depth mode, gotta start over. diff --git a/GPU/GPUCommon.cpp b/GPU/GPUCommon.cpp index 408b2e5aee..011521fa29 100644 --- a/GPU/GPUCommon.cpp +++ b/GPU/GPUCommon.cpp @@ -2358,6 +2358,13 @@ void GPUCommon::PerformWriteFormattedFromMemory(u32 addr, int size, int frameWid InvalidateCache(addr, size, GPU_INVALIDATE_SAFE); } +bool GPUCommon::PerformReadbackStencilToMemory(u32 dest, int size) { + if (Memory::IsVRAMAddress(dest) && framebufferManager_->MayIntersectFramebufferColor(dest)) { + return framebufferManager_->ReadbackStencilToMemory(dest, size); + } + return false; +} + bool GPUCommon::PerformWriteStencilFromMemory(u32 dest, int size, WriteStencil flags) { if (framebufferManager_->MayIntersectFramebufferColor(dest)) { framebufferManager_->PerformWriteStencilFromMemory(dest, size, flags); diff --git a/GPU/GPUCommon.h b/GPU/GPUCommon.h index d78d417277..04a57ba4e2 100644 --- a/GPU/GPUCommon.h +++ b/GPU/GPUCommon.h @@ -168,6 +168,9 @@ public: // 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); 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. + virtual bool PerformReadbackStencilToMemory(u32 dest, int size); // Tells the texture cache that a block copy moved pixels, so it can carry "this is video" from // the source to the destination. Cheap and safe to call for any copy. diff --git a/GPU/Software/SoftGpu.cpp b/GPU/Software/SoftGpu.cpp index 1eb0f2d0bb..9375185f8c 100644 --- a/GPU/Software/SoftGpu.cpp +++ b/GPU/Software/SoftGpu.cpp @@ -1493,6 +1493,12 @@ bool SoftGPU::PerformReadbackToMemory(u32 dest, int size) return false; } +bool SoftGPU::PerformReadbackStencilToMemory(u32 dest, int size) { + // Stencil is already in RAM, once any queued drawing is done. + drawEngine_->transformUnit.FlushIfOverlap(this, "readback", false, dest, size, size, 1); + return false; +} + bool SoftGPU::PerformWriteColorFromMemory(u32 dest, int size) { drawEngine_->transformUnit.FlushIfOverlap(this, "upload", true, dest, size, size, 1); diff --git a/GPU/Software/SoftGpu.h b/GPU/Software/SoftGpu.h index cb61d33551..baae40a276 100644 --- a/GPU/Software/SoftGpu.h +++ b/GPU/Software/SoftGpu.h @@ -176,6 +176,7 @@ public: bool PerformReadbackToMemory(u32 dest, int size) override; bool PerformWriteColorFromMemory(u32 dest, int size) override; bool PerformWriteStencilFromMemory(u32 dest, int size, WriteStencil flags) override; + bool PerformReadbackStencilToMemory(u32 dest, int size) override; void DeviceLost() override; void DeviceRestore(Draw::DrawContext *draw) override;