From 9398dcc76dc850c647d3af0300dafc5914130811 Mon Sep 17 00:00:00 2001 From: GTTeancum Date: Fri, 4 Sep 2026 09:15:31 -0400 Subject: [PATCH] Reject depth-failing triangles before texture shading --- ps2xRuntime/src/lib/gs/gs_cpu_backend.cpp | 19 ++- ps2xTest/cmake/CompareGsEarlyDepth.cmake | 37 ++++++ ps2xTest/src/ps2_gs_tests.cpp | 149 ++++++++++++++++++++++ 3 files changed, 204 insertions(+), 1 deletion(-) create mode 100644 ps2xTest/cmake/CompareGsEarlyDepth.cmake diff --git a/ps2xRuntime/src/lib/gs/gs_cpu_backend.cpp b/ps2xRuntime/src/lib/gs/gs_cpu_backend.cpp index 9c39ae2e7..77e879d65 100644 --- a/ps2xRuntime/src/lib/gs/gs_cpu_backend.cpp +++ b/ps2xRuntime/src/lib/gs/gs_cpu_backend.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -1224,6 +1225,12 @@ void GSCpuBackend::DrawTriangle(const GSPrimitiveBatch &batch) const float winding = (denom < 0.0f) ? -1.0f : 1.0f; const float invAbsDenom = 1.0f / std::fabs(denom); constexpr float kEdgeEpsilon = 1.0e-4f; + static const bool disableEarlyDepth = std::getenv("PS2X_GS_DISABLE_EARLY_DEPTH") != nullptr; + const uint32_t depthMethod = static_cast((ctx.test >> 17u) & 3u); + const bool earlyDepth = !disableEarlyDepth && ((ctx.test >> 16u) & 1u) != 0u && depthMethod >= 2u; + const uint32_t depthBase = GSInternal::framePageBaseToBlock(ctx.zbuf.zbp); + const uint32_t depthWidth = std::max(ctx.frame.fbw, 1u); + const auto &depthReader = m_readVramFuncs[ctx.zbuf.psm & 0x3Fu]; for (int y = minY; y <= maxY; ++y) { @@ -1240,6 +1247,16 @@ void GSCpuBackend::DrawTriangle(const GSPrimitiveBatch &batch) continue; double z = v0.z * w0 + v1.z * w1 + v2.z * w2; + const uint32_t pixelZ = static_cast(z + 0.5); + + // A failing depth test cannot store color or depth, regardless of + // alpha-test mode. Passing pixels retain the existing write path. + if (earlyDepth) + { + const uint32_t storedZ = depthReader(m_vram, depthBase, depthWidth, x, y); + if (pixelZ < storedZ || (depthMethod == 3u && pixelZ == storedZ)) + continue; + } uint8_t r, g, b, a; if (state.prim.iip) @@ -1302,7 +1319,7 @@ void GSCpuBackend::DrawTriangle(const GSPrimitiveBatch &batch) } const uint8_t fog = clampU8(static_cast(v0.fog * w0 + v1.fog * w1 + v2.fog * w2)); - WritePixel(state, x, y, static_cast(z + 0.5), r, g, b, a, fog); + WritePixel(state, x, y, pixelZ, r, g, b, a, fog); } } } diff --git a/ps2xTest/cmake/CompareGsEarlyDepth.cmake b/ps2xTest/cmake/CompareGsEarlyDepth.cmake new file mode 100644 index 000000000..efbad1fbf --- /dev/null +++ b/ps2xTest/cmake/CompareGsEarlyDepth.cmake @@ -0,0 +1,37 @@ +# Usage: cmake -DTEST_EXECUTABLE=/path/to/ps2x_tests -P CompareGsEarlyDepth.cmake +if(NOT DEFINED TEST_EXECUTABLE OR NOT EXISTS "${TEST_EXECUTABLE}") + message(FATAL_ERROR "TEST_EXECUTABLE must name the built ps2x_tests executable") +endif() +get_filename_component(source_root "${CMAKE_CURRENT_LIST_DIR}/../.." ABSOLUTE) + +foreach(mode IN ITEMS reference optimized) + if(mode STREQUAL "reference") + set(depth_setting "PS2X_GS_DISABLE_EARLY_DEPTH=1") + else() + set(depth_setting "--unset=PS2X_GS_DISABLE_EARLY_DEPTH") + endif() + execute_process( + COMMAND "${CMAKE_COMMAND}" -E env + --unset=PS2X_SKIP_CPU_RASTER + --unset=PS2X_SKIP_CPU_RASTER_BEFORE_PRESENT + --unset=PS2X_DEBUG_WHITE_WIREFRAME + --unset=PS2X_GS_VERIFY_EARLY_DEPTH_PRESENT + --unset=MINITEST_FILTER + "${depth_setting}" "${TEST_EXECUTABLE}" + WORKING_DIRECTORY "${source_root}" + RESULT_VARIABLE result OUTPUT_VARIABLE output ERROR_VARIABLE errors + TIMEOUT 60) + if(NOT result STREQUAL "0") + message(FATAL_ERROR "${mode} depth tests failed (${result}):\n${output}\n${errors}") + endif() + if(NOT output MATCHES "\\[gs-depth-differential\\] cases=128 hash=([0-9a-f]+)") + message(FATAL_ERROR "Missing ${mode} differential digest:\n${output}") + endif() + set(${mode}_hash "${CMAKE_MATCH_1}") + message(STATUS "${mode}: ${${mode}_hash}") +endforeach() + +if(NOT reference_hash STREQUAL optimized_hash) + message(FATAL_ERROR "Early depth rejection changed graphics memory") +endif() +message(STATUS "Early depth rejection preserves all 128 complete-VRAM results") diff --git a/ps2xTest/src/ps2_gs_tests.cpp b/ps2xTest/src/ps2_gs_tests.cpp index 8343775cb..c894fd323 100644 --- a/ps2xTest/src/ps2_gs_tests.cpp +++ b/ps2xTest/src/ps2_gs_tests.cpp @@ -4,6 +4,7 @@ #include "ps2_stubs.h" #include "ps2_syscalls.h" #include "runtime/gs/gs_frontend.h" +#include "runtime/gs/gs_cpu_backend.h" #include "runtime/ee_scheduler.h" #include "runtime/gs/ps2_gs_memory.h" #include "runtime/gs/ps2_gs_psmct32.h" @@ -3499,6 +3500,154 @@ void register_ps2_gs_tests() "REGION_REPEAT should calculate (U & UMSK) | UFIX"); }); + tc.Run("GS early depth preserves GEQUAL and GREATER stores across Z formats", [](TestCase &t) + { + for (uint8_t format : {GS_PSM_Z32, GS_PSM_Z24, GS_PSM_Z16, GS_PSM_Z16S}) + for (uint32_t method = 0u; method < 4u; ++method) + for (bool masked : {false, true}) + { + std::vector vram(PS2_GS_VRAM_SIZE, 0u); + GSCpuBackend backend; + backend.Initialize(vram.data(), static_cast(vram.size())); + GSPrimitiveBatch batch{}; + batch.vertexCount = 3u; + batch.state.prim.type = GS_PRIM_TRIANGLE; + batch.state.context.frame = {0u, 1u, GS_PSM_CT32, 0u}; + batch.state.context.zbuf = {1u, format, masked}; + batch.state.context.scissor = {0u, 7u, 0u, 7u}; + batch.state.context.test = (1ull << 16u) | (static_cast(method) << 17u); + batch.vertices[0].x = 0.0f; + batch.vertices[0].y = 0.0f; + batch.vertices[1].x = 8.0f; + batch.vertices[1].y = 0.0f; + batch.vertices[2].x = 0.0f; + batch.vertices[2].y = 8.0f; + for (auto &vertex : batch.vertices) + { + vertex.z = 256.0; + vertex.r = 0x12u; + vertex.g = 0x34u; + vertex.b = 0x56u; + vertex.a = 0x80u; + } + for (uint32_t x = 1u; x <= 3u; ++x) + { + backend.WriteVram(GS_PSM_CT32, 0u, 1u, x, 1u, 0xAABBCCDDu); + backend.WriteVram(format, 32u, 1u, x, 1u, 254u + x); + } + backend.Submit(batch); + for (uint32_t x = 1u; x <= 3u; ++x) + { + const uint32_t stored = 254u + x; + const bool passes = method == 1u || (method == 2u && 256u >= stored) || + (method == 3u && 256u > stored); + t.Equals(backend.ReadVram(GS_PSM_CT32, 0u, 1u, x, 1u), + passes ? 0x80563412u : 0xAABBCCDDu, + "depth comparison must preserve the expected color store"); + t.Equals(backend.ReadVram(format, 32u, 1u, x, 1u), + passes && !masked ? 256u : stored, + "depth rejection or ZMSK must preserve stored depth"); + } + } + }); + + tc.Run("GS early depth differential workload covers texture alpha fog and masks", [](TestCase &t) + { + std::vector vram(PS2_GS_VRAM_SIZE, 0u); + GSCpuBackend backend; + backend.Initialize(vram.data(), static_cast(vram.size())); + constexpr std::array frameFormats{ + GS_PSM_CT32, GS_PSM_CT24, GS_PSM_CT16, GS_PSM_CT16S}; + constexpr std::array depthFormats{ + GS_PSM_Z32, GS_PSM_Z24, GS_PSM_Z16, GS_PSM_Z16S}; + uint32_t random = 0xB16821F3u; + auto next = [&]() { random = random * 1664525u + 1013904223u; return random; }; + uint64_t hash = 14695981039346656037ull; + uint32_t changedCases = 0u; + for (uint32_t index = 0u; index < 128u; ++index) + { + std::fill(vram.begin(), vram.end(), 0x3Du); + GSPrimitiveBatch batch{}; + batch.vertexCount = 3u; + auto &state = batch.state; + auto &ctx = state.context; + state.prim.type = GS_PRIM_TRIANGLE; + state.prim.tme = true; + state.prim.iip = (index & 1u) != 0u; + state.prim.fst = (index & 2u) != 0u; + state.prim.abe = (index & 4u) != 0u; + state.prim.fge = (index & 8u) != 0u; + state.pabe = (index & 16u) != 0u; + state.colclamp = 1u; + state.fogR = 0x34u; + state.fogG = 0x56u; + state.fogB = 0x78u; + state.textureWidth = state.textureHeight = 16u; + state.linearFilter = (index & 32u) != 0u; + ctx.frame = {0u, 1u, frameFormats[index & 3u], (index & 16u) ? 0x005A005Au : 0u}; + ctx.zbuf = {32u, depthFormats[(index >> 2u) & 3u], (index & 8u) != 0u}; + ctx.scissor = {2u, 29u, 1u, 30u}; + ctx.tex0.tbp0 = 2048u; + ctx.tex0.tbw = 1u; + ctx.tex0.psm = GS_PSM_T8; + ctx.tex0.tw = ctx.tex0.th = 4u; + ctx.tex0.tcc = 1u; + ctx.tex0.tfx = static_cast(index & 3u); + ctx.tex0.cbp = 4096u; + ctx.tex0.cpsm = GS_PSM_CT32; + ctx.tex0.cld = 1u; + ctx.clamp = static_cast(index & 15u) | (15ull << 14u) | (15ull << 34u); + ctx.test = 1ull | ((index & 7ull) << 1u) | (0x70ull << 4u) | + (((index >> 3u) & 3ull) << 12u) | (1ull << 16u) | + (((index >> 5u) & 3ull) << 17u); + if ((index & 16u) != 0u) + ctx.test |= (1ull << 14u) | ((index & 1ull) << 15u); + ctx.alpha = 0x44ull | (0x60ull << 32u); + ctx.fba = index & 1u; + for (uint32_t y = 0u; y < 32u; ++y) + for (uint32_t x = 0u; x < 32u; ++x) + { + backend.WriteVram(ctx.frame.psm, 0u, 1u, x, y, next()); + backend.WriteVram(ctx.zbuf.psm, 1024u, 1u, x, y, 32766u + (next() % 5u)); + } + for (uint32_t y = 0u; y < 16u; ++y) + for (uint32_t x = 0u; x < 16u; ++x) + { + backend.WriteVram(GS_PSM_T8, 2048u, 1u, x, y, next() & 255u); + backend.WriteVram(GS_PSM_CT32, 4096u, 1u, x, y, next()); + } + const float xs[] = {-2.0f, 34.0f, 5.0f}; + const float ys[] = {2.0f, 7.0f, 35.0f}; + for (uint32_t vertexIndex = 0u; vertexIndex < 3u; ++vertexIndex) + { + auto &vertex = batch.vertices[vertexIndex]; + vertex.x = xs[vertexIndex]; + vertex.y = ys[vertexIndex]; + vertex.z = 32768.0 + (static_cast(vertexIndex) - 1) * 2.0; + vertex.q = 0.5f + static_cast(vertexIndex); + vertex.s = static_cast(vertexIndex); + vertex.t = 2.0f - vertex.s; + vertex.u = static_cast(vertexIndex * 8u * 16u); + vertex.v = static_cast((2u - vertexIndex) * 8u * 16u); + vertex.r = static_cast(next() >> 24u); + vertex.g = static_cast(next() >> 24u); + vertex.b = static_cast(next() >> 24u); + vertex.a = static_cast(next() >> 24u); + vertex.fog = static_cast(next() >> 24u); + } + const auto before = vram; + backend.Submit(batch); + if (before != vram) + ++changedCases; + for (uint8_t byte : vram) + hash = (hash ^ byte) * 1099511628211ull; + } + std::printf("[gs-depth-differential] cases=128 hash=%016llx\n", + static_cast(hash)); + t.IsTrue(changedCases > 0u && changedCases < 128u, + "the workload must exercise both stores and rejected draws"); + }); + tc.Run("GS STQ triangle interpolation divides homogeneous coordinates after DDA", [](TestCase &t) { std::vector vram(PS2_GS_VRAM_SIZE, 0u);