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) <noreply@anthropic.com>
This commit is contained in:
Henrik RydgårdandClaude Opus 5.5 committed 2026-10-12 12:15:56 +02:00
1 parent ecad07b3b1
commit ae68a7e73f
8 files changed
+103

No files matched your search

+17
View File
@@ -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 },
{}
};
+1
View File
@@ -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", },
+67
View File
@@ -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<u8> 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;
+1
View File
@@ -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.
+7
View File
@@ -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);
+3
View File
@@ -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.
+6
View File
@@ -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);
+1
View File
@@ -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;