From 98405e8f25b71f5e8f84edda6c1fbf7ed6f06df7 Mon Sep 17 00:00:00 2001 From: alfalfa-star Date: Wed, 7 Oct 2026 15:31:39 +0200 Subject: [PATCH] fix(agc): run draws whose pixel stage cannot run without its program A depth-only pass can leave a stale pixel program address in SPI_SHADER_PGM_LO/HI_PS while never setting the pixel registers it does not need. The draw then failed with "missing register at DWORD 0x1b3" (SPI_PS_INPUT_ENA), although the hardware never launches that program: with no color written (CB_TARGET_MASK & CB_SHADER_MASK == 0), no depth, stencil or mask export (SPI_SHADER_Z_FORMAT 0 and those enables of DB_SHADER_CONTROL clear), and neither KILL_ENABLE, EXEC_ON_HIER_FAIL, EXEC_ON_NOOP nor EXEC_IF_OVERLAPPED set, the pixel shader has no effect and is not executed. PixelProgramUnset becomes PixelProgramSkipped: true for a zero program address as before, and for such an inert pixel stage. Both use the driver's null pixel program; the precheck and the decode agree. PPSA14632 (LEGO Horizon Adventures) issues an HTILE expand this way (DB_RENDER_CONTROL DEPTH/STENCIL_COMPRESS_DISABLE, CB disabled, depth test and writes off) once per run. Co-Authored-By: Claude Opus 5.5 --- .../Execution/src/Driver/Draw/DrawDecode.cpp | 5 +++-- .../Graphics/include/State.hpp | 2 +- .../libSceAgcDriver/Graphics/src/State.cpp | 14 +++++++++--- .../prx/libSceAgcDriver/tests/Graphics.cpp | 22 +++++++++++++++++-- 4 files changed, 35 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp index f3d2e0120..90a99190d 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp @@ -15,8 +15,9 @@ std::shared_ptr Driver::decodeDraw(const QueueState& queue, const Su require((high & ~0xffu) == 0, "reserved graphics program address bits are set"); return (static_cast(readRegister(queue.shader, base)) << 8u) | (static_cast(high) << 40u); }; + const bool pixelSkipped = Graphics::PixelProgramSkipped(queue); const auto prepare = [&](std::uint64_t address, std::uint8_t type, Stage stage, std::uint32_t rsrc2, std::uint32_t userDataBase) { - const bool nullPixel = address == 0 && stage == Stage::Fragment; + const bool nullPixel = stage == Stage::Fragment && (address == 0 || pixelSkipped); if (nullPixel) address = NullPixelProgramAddress(); auto it = submission.shaders->upper_bound(address); require(it != submission.shaders->begin(), "graphics program does not belong to a registered shader"); @@ -89,7 +90,7 @@ std::shared_ptr Driver::decodeDraw(const QueueState& queue, const Su } else { append(0xc8, 2, Stage::Vertex, 0x8b, 0x8c, Role::Main); } - const bool nullPixel = Graphics::PixelProgramUnset(queue); + const bool nullPixel = pixelSkipped; if (nullPixel) { const auto rejection = Graphics::NullPixelProgramRejection(queue); require(rejection.empty(), rejection.c_str()); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp index 8367436d9..64ff85b69 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -111,7 +111,7 @@ std::optional DecodeColorMetadataPass(const QueueState& queue // this precheck covers, evaluated without exceptions before the draw is decoded; empty when they // pass (DecodeState still checks everything). A register a rule needs that is absent is no verdict. std::string DrawRejection(const QueueState& queue, bool indexed); -bool PixelProgramUnset(const QueueState& queue); +bool PixelProgramSkipped(const QueueState& queue); std::string NullPixelProgramRejection(const QueueState& queue); // The recording facade of the draw decoders (design_cpu_final M8, step 8a): every register read diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 53288a665..625a065db 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -65,6 +65,7 @@ constexpr std::uint32_t DepthControlMask = ~0x007007f0u; // EXEC_ON_HIER_FAIL / EXEC_ON_NOOP / EXEC_IF_OVERLAPPED (bits 9, 10, 17) only force the pixel shader // to run, which it always does here. constexpr std::uint32_t ShaderControlMask = ~(0x0000f870u | 0x00020600u); +constexpr std::uint32_t PixelStageRunsMask = 0x00020747u; constexpr std::uint32_t AlphaToCoverageMask = ~0x0001ff00u; constexpr std::uint32_t ScanModeMask = ~2u; constexpr std::uint32_t ScanControlMask = ~0x06023fffu; @@ -791,7 +792,7 @@ std::string DrawRejection(const QueueState& queue, bool indexed) { std::uint32_t targetMask = 0, shaderMask = 0; if (value(cx, 0x8e, targetMask) && value(cx, 0x8f, shaderMask) && value(cx, 0x202, word) && !colorControlSupported(word, (targetMask & shaderMask) != 0)) return colorControlMessage(word); if (auto reason = nonzero(cx, 0x1c4, zFormatSupported(zFormat) ? 0u : ~0u, "depth or sample-mask export"); !reason.empty()) return reason; - if (PixelProgramUnset(queue)) return NullPixelProgramRejection(queue); + if (PixelProgramSkipped(queue)) return NullPixelProgramRejection(queue); for (const auto offset : {0x1b3u, 0x1b4u, 0x1c5u}) { if (find(cx, offset) != cx.end()) continue; char text[64]; @@ -801,10 +802,17 @@ std::string DrawRejection(const QueueState& queue, bool indexed) { return {}; } -bool PixelProgramUnset(const QueueState& queue) { +bool PixelProgramSkipped(const QueueState& queue) { const auto low = find(queue.shader, 0x008, RegisterBank::Shader); const auto high = find(queue.shader, 0x009, RegisterBank::Shader); - return low != queue.shader.end() && high != queue.shader.end() && low->second == 0 && high->second == 0; + if (low != queue.shader.end() && high != queue.shader.end() && low->second == 0 && high->second == 0) return true; + const auto& cx = queue.context; + const auto targetMask = find(cx, 0x8e); + const auto shaderMask = find(cx, 0x8f); + const auto zFormat = find(cx, 0x1c4); + const auto shaderControl = find(cx, 0x203); + if (targetMask == cx.end() || shaderMask == cx.end() || zFormat == cx.end() || shaderControl == cx.end()) return false; + return (targetMask->second & shaderMask->second) == 0 && zFormat->second == 0 && (shaderControl->second & PixelStageRunsMask) == 0; } std::string NullPixelProgramRejection(const QueueState& queue) { diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 2d45534e2..f0ccab637 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -206,11 +206,11 @@ void stateTests() { queue.context.erase(0x1c5); queue.shader[0x008] = 0x100; queue.shader[0x009] = 0; - Require(!AgcDriver::Graphics::PixelProgramUnset(queue), "a pixel program address was read as unset"); + Require(!AgcDriver::Graphics::PixelProgramSkipped(queue), "a pixel program address was read as unset"); Require(AgcDriver::Graphics::DrawRejection(queue, true).find("missing register at DWORD 0x1b3") != std::string::npos, "a real pixel program without SPI_PS_INPUT_ENA was accepted"); expectFailure([&] { AgcDriver::Graphics::DecodePixelStageInfo(queue.context, std::array{0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u}); }, "missing register"); queue.shader[0x008] = 0; - Require(AgcDriver::Graphics::PixelProgramUnset(queue), "a zero pixel program address was not read as unset"); + Require(AgcDriver::Graphics::PixelProgramSkipped(queue), "a zero pixel program address was not read as unset"); Require(AgcDriver::Graphics::DrawRejection(queue, true).find("writes color") != std::string::npos, "a draw without a pixel program that writes color was accepted"); queue.context[0x8e] = 0; Require(AgcDriver::Graphics::DrawRejection(queue, true).empty(), "a depth-only draw without a pixel program was rejected"); @@ -219,6 +219,24 @@ void stateTests() { const auto unset = AgcDriver::Graphics::DecodePixelStageInfo(queue.context, AgcDriver::Graphics::ExportMappings(state), true); Require(unset.inputAddr == ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PerspectiveCenter), "unset pixel inputs of the null program did not read as PERSP_CENTER_ENA"); for (const auto mode : unset.targetOutputMode) Require(mode == 0, "an unset SPI_SHADER_COL_FORMAT exported a color"); + queue.shader[0x008] = 0x100; + queue.context[0x1c4] = 0; + queue.context[0x203] = 0; + Require(AgcDriver::Graphics::PixelProgramSkipped(queue), "a pixel program that writes nothing and cannot run was not skipped"); + Require(AgcDriver::Graphics::DrawRejection(queue, true).empty(), "a depth-only draw whose pixel program cannot run was rejected"); + for (const auto [control, what] : {std::pair{0x400u, "EXEC_ON_NOOP"}, std::pair{0x40u, "KILL_ENABLE"}}) { + queue.context[0x203] = control; + Require(!AgcDriver::Graphics::PixelProgramSkipped(queue), std::string("a pixel program with ") + what + " was skipped"); + Require(AgcDriver::Graphics::DrawRejection(queue, true).find("missing register at DWORD 0x1b3") != std::string::npos, std::string("a pixel program with ") + what + " ran without SPI_PS_INPUT_ENA"); + } + queue.context[0x203] = 1; + Require(!AgcDriver::Graphics::PixelProgramSkipped(queue), "a pixel program with Z_EXPORT_ENABLE was skipped"); + queue.context[0x203] = 0; + queue.context[0x1c4] = 1; + Require(!AgcDriver::Graphics::PixelProgramSkipped(queue), "a pixel program exporting depth was skipped"); + queue.context[0x1c4] = 0; + queue.context[0x8e] = 0xf; + Require(!AgcDriver::Graphics::PixelProgramSkipped(queue), "a pixel program writing color was skipped"); } void hardwareScreenOffsetTests() {