From ca214b2c8caf3f7e761daba1833be12c53cacf9c Mon Sep 17 00:00:00 2001 From: Shane Michael Mathews Date: Sun, 26 Jul 2026 17:14:13 -0400 Subject: [PATCH] fix(gs): correct depth write for all AFAIL alpha-fail modes The GS documents a four-way truth table for what TEST.AFAIL does when the alpha test fails: KEEP writes neither color nor depth, FB_ONLY writes color only (no Z), ZB_ONLY writes depth only, and RGB_ONLY writes RGB while preserving destination alpha (no Z). The old rasterizer used a single writeFramebuffer flag to stand in for both the color and depth disposition, and writePixel bailed out early only when that flag was false. Every case that survived the early-out then fell through to an unconditional depth write gated only by ZMSK. That made pass, FB_ONLY, and RGB_ONLY all write depth on alpha-fail, against the table, while ZB_ONLY was folded into the same branch as KEEP and wrote neither color nor its required Z. Give AlphaTestResult an independent writeDepth field so each AFAIL case states its own color and depth disposition, and gate the two WriteVram calls in writePixel on writeFramebuffer and writeDepth separately instead of bailing out on a single flag. Each case now matches the table: FB_ONLY and RGB_ONLY stop writing Z on alpha-fail, and ZB_ONLY gains its previously-missing depth-only write. A passing fragment still writes both. The depth test itself is unchanged; a fragment that fails it still writes nothing. Add depth-behavior pins for all three affected AFAIL modes, each with ZMSK=0, ZTST=ALWAYS, and a Z page distinct from the color page so the depth path is actually exercised: ZB_ONLY asserts depth is written on alpha-fail, and FB_ONLY and RGB_ONLY siblings assert depth is left untouched. The existing FB_ONLY and RGB_ONLY framebuffer tests are unchanged. --- ps2xRuntime/src/lib/ps2_gs_rasterizer.cpp | 21 ++- ps2xTest/src/ps2_gs_tests.cpp | 184 ++++++++++++++++++++++ 2 files changed, 197 insertions(+), 8 deletions(-) diff --git a/ps2xRuntime/src/lib/ps2_gs_rasterizer.cpp b/ps2xRuntime/src/lib/ps2_gs_rasterizer.cpp index 9c143559e..85537e61d 100644 --- a/ps2xRuntime/src/lib/ps2_gs_rasterizer.cpp +++ b/ps2xRuntime/src/lib/ps2_gs_rasterizer.cpp @@ -136,25 +136,27 @@ namespace { bool writeFramebuffer; bool preserveDestinationAlpha; + bool writeDepth; }; AlphaTestResult classifyAlphaTest(uint64_t testReg, uint8_t alpha) { const bool pass = passesAlphaTest(testReg, alpha); if (pass) - return {true, false}; + return {true, false, true}; // TEST.AFAIL controls what happens when the alpha comparison fails. switch (static_cast((testReg >> 12) & 0x3u)) { case 1: // FB_ONLY - return {true, false}; + return {true, false, false}; + case 2: // ZB_ONLY + return {false, false, true}; case 3: // RGB_ONLY - return {true, true}; + return {true, true, false}; case 0: // KEEP - case 2: // ZB_ONLY default: - return {false, false}; + return {false, false, false}; } } @@ -417,7 +419,7 @@ void GSRasterizer::writePixel(GS *gs, int x, int y, int z, uint8_t r, uint8_t g, const AlphaTestResult alphaTest = classifyAlphaTest(ctx.test, a); - if (!alphaTest.writeFramebuffer) + if (!alphaTest.writeFramebuffer && !alphaTest.writeDepth) return; u8* vram = gs->m_vram; @@ -544,9 +546,12 @@ void GSRasterizer::writePixel(GS *gs, int x, int y, int z, uint8_t r, uint8_t g, pixel = Rgba8888ToRgba5551(pixel); } - gs->WriteVram(fpsm, fbp, fbw, x, y, pixel); + if (alphaTest.writeFramebuffer) + { + gs->WriteVram(fpsm, fbp, fbw, x, y, pixel); + } - if (!zmask) + if (alphaTest.writeDepth && !zmask) { gs->WriteVram(zpsm, zbp, fbw, x, y, z); } diff --git a/ps2xTest/src/ps2_gs_tests.cpp b/ps2xTest/src/ps2_gs_tests.cpp index 4c3e8ec1c..b501c1881 100644 --- a/ps2xTest/src/ps2_gs_tests.cpp +++ b/ps2xTest/src/ps2_gs_tests.cpp @@ -3,6 +3,7 @@ #include "ps2_runtime.h" #include "ps2_stubs.h" #include "ps2_syscalls.h" +#include "runtime/ps2_gs_common.h" #include "runtime/ps2_gs_gpu.h" #include "runtime/ps2_gs_memory.h" #include "runtime/ps2_gs_psmct32.h" @@ -3068,6 +3069,189 @@ void register_ps2_gs_tests() "AFAIL=RGB_ONLY should update RGB while preserving destination alpha"); }); + tc.Run("GS alpha test AFAIL Z-buffer-only writes depth but not color", [](TestCase &t) + { + std::vector vram(PS2_GS_VRAM_SIZE, 0u); + GS gs; + gs.init(vram.data(), static_cast(vram.size()), nullptr); + + constexpr uint32_t kFbw = 1u; + constexpr uint32_t kZbp = 1u; // ZBUF page, distinct from the color page + const uint32_t zBlock = GSInternal::framePageBaseToBlock(kZbp); + + constexpr uint32_t kColorSentinel = 0xAB030201u; + constexpr uint32_t kZSentinel = 0x11111111u; + constexpr uint32_t kDrawnZ = 0x12345678u; + + std::memcpy(vram.data(), &kColorSentinel, sizeof(kColorSentinel)); + gs.WriteVram(GS_PSM_Z32, zBlock, kFbw, 0u, 0u, kZSentinel); + + constexpr uint64_t kFrame = + (0ull << 0) | + (1ull << 16) | + (static_cast(GS_PSM_CT32) << 24); + const uint64_t kZbuf = static_cast(kZbp) << 0; // ZBP=1, PSM nibble=0 (Z32), ZMSK=0 + constexpr uint64_t kScissor = + (0ull << 0) | + (0ull << 16) | + (0ull << 32) | + (0ull << 48); + constexpr uint64_t kTest = + 1ull | // ATE + (5ull << 1) | // ATST = GEQUAL + (0x80ull << 4) | // AREF + (2ull << 12) | // AFAIL = ZB_ONLY + (1ull << 17); // ZTST = ALWAYS + constexpr uint64_t kPrim = + static_cast(GS_PRIM_POINT); + constexpr uint64_t kRgbaq = + (0x12ull << 0) | + (0x34ull << 8) | + (0x56ull << 16) | + (0x00ull << 24) | + (0x3F800000ull << 32); // q = 1.0f + const uint64_t kXyz2 = static_cast(kDrawnZ) << 32; + + gs.writeRegister(GS_REG_FRAME_1, kFrame); + gs.writeRegister(GS_REG_ZBUF_1, kZbuf); + gs.writeRegister(GS_REG_SCISSOR_1, kScissor); + gs.writeRegister(GS_REG_TEST_1, kTest); + gs.writeRegister(GS_REG_PRIM, kPrim); + gs.writeRegister(GS_REG_RGBAQ, kRgbaq); + gs.writeRegister(GS_REG_XYZ2, kXyz2); + + uint32_t pixel = 0u; + std::memcpy(&pixel, vram.data(), sizeof(pixel)); + t.Equals(pixel, kColorSentinel, + "AFAIL=ZB_ONLY must not write the framebuffer when the alpha test fails"); + + const uint32_t depthResult = gs.ReadVram(GS_PSM_Z32, zBlock, kFbw, 0u, 0u); + t.Equals(depthResult, kDrawnZ, + "AFAIL=ZB_ONLY must still write depth when the alpha test fails"); + }); + + tc.Run("GS alpha test AFAIL framebuffer-only leaves the depth buffer unwritten", [](TestCase &t) + { + std::vector vram(PS2_GS_VRAM_SIZE, 0u); + GS gs; + gs.init(vram.data(), static_cast(vram.size()), nullptr); + + constexpr uint32_t kFbw = 1u; + constexpr uint32_t kZbp = 1u; // ZBUF page, distinct from the color page + const uint32_t zBlock = GSInternal::framePageBaseToBlock(kZbp); + + constexpr uint32_t kColorSentinel = 0xAB030201u; + constexpr uint32_t kZSentinel = 0x11111111u; + constexpr uint32_t kDrawnZ = 0x12345678u; + + std::memcpy(vram.data(), &kColorSentinel, sizeof(kColorSentinel)); + gs.WriteVram(GS_PSM_Z32, zBlock, kFbw, 0u, 0u, kZSentinel); + + constexpr uint64_t kFrame = + (0ull << 0) | + (1ull << 16) | + (static_cast(GS_PSM_CT32) << 24); + const uint64_t kZbuf = static_cast(kZbp) << 0; // ZBP=1, PSM nibble=0 (Z32), ZMSK=0 + constexpr uint64_t kScissor = + (0ull << 0) | + (0ull << 16) | + (0ull << 32) | + (0ull << 48); + constexpr uint64_t kTest = + 1ull | // ATE + (5ull << 1) | // ATST = GEQUAL + (0x80ull << 4) | // AREF + (1ull << 12) | // AFAIL = FB_ONLY + (1ull << 17); // ZTST = ALWAYS + constexpr uint64_t kPrim = + static_cast(GS_PRIM_POINT); + constexpr uint64_t kRgbaq = + (0x12ull << 0) | + (0x34ull << 8) | + (0x56ull << 16) | + (0x00ull << 24) | + (0x3F800000ull << 32); // q = 1.0f + const uint64_t kXyz2 = static_cast(kDrawnZ) << 32; + + gs.writeRegister(GS_REG_FRAME_1, kFrame); + gs.writeRegister(GS_REG_ZBUF_1, kZbuf); + gs.writeRegister(GS_REG_SCISSOR_1, kScissor); + gs.writeRegister(GS_REG_TEST_1, kTest); + gs.writeRegister(GS_REG_PRIM, kPrim); + gs.writeRegister(GS_REG_RGBAQ, kRgbaq); + gs.writeRegister(GS_REG_XYZ2, kXyz2); + + uint32_t pixel = 0u; + std::memcpy(&pixel, vram.data(), sizeof(pixel)); + t.Equals(pixel, 0x00563412u, + "AFAIL=FB_ONLY must write the framebuffer when the alpha test fails"); + + const uint32_t depthResult = gs.ReadVram(GS_PSM_Z32, zBlock, kFbw, 0u, 0u); + t.Equals(depthResult, kZSentinel, + "AFAIL=FB_ONLY must NOT write depth when the alpha test fails"); + }); + + tc.Run("GS alpha test AFAIL RGB-only leaves the depth buffer unwritten", [](TestCase &t) + { + std::vector vram(PS2_GS_VRAM_SIZE, 0u); + GS gs; + gs.init(vram.data(), static_cast(vram.size()), nullptr); + + constexpr uint32_t kFbw = 1u; + constexpr uint32_t kZbp = 1u; // ZBUF page, distinct from the color page + const uint32_t zBlock = GSInternal::framePageBaseToBlock(kZbp); + + constexpr uint32_t kColorSentinel = 0xAB030201u; + constexpr uint32_t kZSentinel = 0x11111111u; + constexpr uint32_t kDrawnZ = 0x12345678u; + + std::memcpy(vram.data(), &kColorSentinel, sizeof(kColorSentinel)); + gs.WriteVram(GS_PSM_Z32, zBlock, kFbw, 0u, 0u, kZSentinel); + + constexpr uint64_t kFrame = + (0ull << 0) | + (1ull << 16) | + (static_cast(GS_PSM_CT32) << 24); + const uint64_t kZbuf = static_cast(kZbp) << 0; // ZBP=1, PSM nibble=0 (Z32), ZMSK=0 + constexpr uint64_t kScissor = + (0ull << 0) | + (0ull << 16) | + (0ull << 32) | + (0ull << 48); + constexpr uint64_t kTest = + 1ull | // ATE + (5ull << 1) | // ATST = GEQUAL + (0x80ull << 4) | // AREF + (3ull << 12) | // AFAIL = RGB_ONLY + (1ull << 17); // ZTST = ALWAYS + constexpr uint64_t kPrim = + static_cast(GS_PRIM_POINT); + constexpr uint64_t kRgbaq = + (0x12ull << 0) | + (0x34ull << 8) | + (0x56ull << 16) | + (0x00ull << 24) | + (0x3F800000ull << 32); // q = 1.0f + const uint64_t kXyz2 = static_cast(kDrawnZ) << 32; + + gs.writeRegister(GS_REG_FRAME_1, kFrame); + gs.writeRegister(GS_REG_ZBUF_1, kZbuf); + gs.writeRegister(GS_REG_SCISSOR_1, kScissor); + gs.writeRegister(GS_REG_TEST_1, kTest); + gs.writeRegister(GS_REG_PRIM, kPrim); + gs.writeRegister(GS_REG_RGBAQ, kRgbaq); + gs.writeRegister(GS_REG_XYZ2, kXyz2); + + uint32_t pixel = 0u; + std::memcpy(&pixel, vram.data(), sizeof(pixel)); + t.Equals(pixel, 0xAB563412u, + "AFAIL=RGB_ONLY must write RGB and preserve destination alpha when the alpha test fails"); + + const uint32_t depthResult = gs.ReadVram(GS_PSM_Z32, zBlock, kFbw, 0u, 0u); + t.Equals(depthResult, kZSentinel, + "AFAIL=RGB_ONLY must NOT write depth when the alpha test fails"); + }); + tc.Run("GS triangle fan subpixel quad fills rows without interior holes", [](TestCase &t) { std::vector vram(PS2_GS_VRAM_SIZE, 0u);