From 5a6e2b8eb574a2b9acb6db84f67bae2fa3f8fa6b Mon Sep 17 00:00:00 2001 From: Michon <231849@buas.nl> Date: Tue, 29 Sep 2026 19:08:10 +0200 Subject: [PATCH 1/3] fix(shader): packed pixel ancillary as SampleId and Layer, gl_Layer via Geometry, Z export formats Bit-field extracts of the packed ancillary VGPR become gl_SampleID and gl_Layer reads. A pixel shader reading gl_Layer needs the Geometry capability instead of ShaderLayer and SPIR-V 1.5, and the Geometry capability is available whenever the geometryShader feature is. 32-bit SPI_SHADER_Z_FORMATs and 32_ABGR are accepted with the depth and sample-mask export enables. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../Execution/src/VulkanDevice.cpp | 4 ++ .../libSceAgcDriver/Graphics/src/State.cpp | 23 ++++-- .../Optimization/src/ConstantFolder.cpp | 70 +++++++++++++++++++ .../src/SpirvMemory/SpirvModuleSetup.cpp | 5 +- 4 files changed, 97 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 98d526ccb..c2309ce3b 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -852,6 +852,10 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u if (enabled.shaderImageGatherExtended) state->capabilities.push_back(spv::CapabilityImageGatherExtended); enabled.shaderResourceMinLod = available.shaderResourceMinLod; if (enabled.shaderResourceMinLod) state->capabilities.push_back(spv::CapabilityMinLod); + enabled.sampleRateShading = available.sampleRateShading; + enabled.geometryShader = available.geometryShader; + if (enabled.geometryShader) state->capabilities.push_back(spv::CapabilityGeometry); + enabled.shaderClipDistance = available.shaderClipDistance; if (enabled.shaderStorageImageWriteWithoutFormat) state->capabilities.push_back(spv::CapabilityStorageImageWriteWithoutFormat); if (enabled.shaderStorageImageReadWithoutFormat) state->capabilities.push_back(spv::CapabilityStorageImageReadWithoutFormat); // Bindless image tables index an image array with a wave-uniform runtime slot. diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 48b9a1e9c..c2c508b37 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -72,6 +72,19 @@ constexpr std::uint32_t ScreenOffsetMask = ~0x01ff01ffu; // Bits 26/27 (ZCLIP_NEAR/FAR_DISABLE) become depth clamping; bit 19 selects the [0, 1] clip space. constexpr std::uint32_t ClipControlMask = ~(0x80000u | 0x0c000000u); +bool zFormatSupported(std::uint32_t format) { + return format == 0u || format == 1u || format == 2u || format == 3u || format == 9u; +} + +std::uint32_t shaderControlMask(std::uint32_t zFormat) { + constexpr std::uint32_t zExportEnable = 0x1u; + constexpr std::uint32_t maskExportEnable = 0x100u; + auto mask = ShaderControlMask; + if (zFormat != 0u && zFormatSupported(zFormat)) mask &= ~zExportEnable; + if (zFormat == 9u) mask &= ~maskExportEnable; + return mask; +} + // Debug aid: APS5_IGNORE_DEPTH_TEST=1 renders depth- and stencil-tested draws without a depth // target as if their tests always passed (wrong occlusion, but the draws run), so stages that // depth-test everything can exercise the draw paths before depth targets exist. @@ -469,7 +482,7 @@ State DecodeState(const QueueState& queue) { Require((depthControl & 8u) == 0 || result.depth.has_value(), "depth bounds without a depth surface"); Require((depthControl & 0xc0000000u) == 0, "depth-conditional color writes are unsupported"); } - zero(cx, 0x203, ShaderControlMask, "depth export, shader coverage or ordered fragment execution"); + zero(cx, 0x203, shaderControlMask(read(cx, 0x1c4)), "depth export, shader coverage or ordered fragment execution"); zero(cx, 0x2dc, AlphaToCoverageMask, "alpha-to-coverage"); zero(cx, 0x2f8, ~0u, "multisampling or coverage conversion"); zero(cx, 0x292, ScanModeMask, "scan conversion mode"); @@ -515,7 +528,7 @@ State DecodeState(const QueueState& queue) { // CB_COLOR_CONTROL mode 0 disables color writes, which only matters when a target is written. if (const auto colorControl = read(cx, 0x202); !colorControlSupported(colorControl, result.hasColorTarget)) throw std::runtime_error(colorControlMessage(colorControl)); - zero(cx, 0x1c4, ~0u, "depth or sample-mask export"); + zero(cx, 0x1c4, zFormatSupported(read(cx, 0x1c4)) ? 0u : ~0u, "depth or sample-mask export"); const auto exportFormat = result.hasColorTarget ? read(cx, 0x1c5) : 0u; APS5_LOG_OUT_DEBUG("Export format=%u", exportFormat); // SPI_SHADER_POS_FORMAT: POS0 must be a 4-component position; later vectors carry the misc/clip @@ -758,7 +771,9 @@ std::string DrawRejection(const QueueState& queue, bool indexed) { if (auto reason = require((word & 8u) == 0 || surface, "depth bounds without a depth surface"); !reason.empty()) return reason; if (auto reason = require((word & 0xc0000000u) == 0, "depth-conditional color writes are unsupported"); !reason.empty()) return reason; } - if (auto reason = nonzero(cx, 0x203, ShaderControlMask, "depth export, shader coverage or ordered fragment execution"); !reason.empty()) return reason; + std::uint32_t zFormat = 0; + static_cast(value(cx, 0x1c4, zFormat)); + if (auto reason = nonzero(cx, 0x203, shaderControlMask(zFormat), "depth export, shader coverage or ordered fragment execution"); !reason.empty()) return reason; if (auto reason = nonzero(cx, 0x2dc, AlphaToCoverageMask, "alpha-to-coverage"); !reason.empty()) return reason; if (auto reason = nonzero(cx, 0x2f8, ~0u, "multisampling or coverage conversion"); !reason.empty()) return reason; if (auto reason = nonzero(cx, 0x292, ScanModeMask, "scan conversion mode"); !reason.empty()) return reason; @@ -775,7 +790,7 @@ std::string DrawRejection(const QueueState& queue, bool indexed) { if (auto reason = nonzero(cx, 0x204, ClipControlMask, "unsupported PA_CL_CLIP_CNTL flags"); !reason.empty()) return reason; 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, ~0u, "depth or sample-mask export"); !reason.empty()) return reason; + if (auto reason = nonzero(cx, 0x1c4, zFormatSupported(read(cx, 0x1c4)) ? 0u : ~0u, "depth or sample-mask export"); !reason.empty()) return reason; if (PixelProgramUnset(queue)) return NullPixelProgramRejection(queue); for (const auto offset : {0x1b3u, 0x1b4u, 0x1c5u}) { if (find(cx, offset) != cx.end()) continue; diff --git a/core/shader/recompiler/Optimization/src/ConstantFolder.cpp b/core/shader/recompiler/Optimization/src/ConstantFolder.cpp index 8f87d21de..ac7bcfdcd 100644 --- a/core/shader/recompiler/Optimization/src/ConstantFolder.cpp +++ b/core/shader/recompiler/Optimization/src/ConstantFolder.cpp @@ -2,8 +2,12 @@ #include "IntermediateRepresentation/IrBuilder.hpp" #include +#include #include #include +#include +#include +#include namespace ShaderRecompiler { namespace { @@ -181,12 +185,78 @@ bool foldCompositeExtract(IrBuilder& builder, IrValue& inst, IrOpcode construct, return false; } +void collectUses(IrValue& value, std::vector& uses) { + for (const IrUse& use : value.OperandUses()) { + if (use.user->Opcode() == IrOpcode::Identity) { + collectUses(*use.user, uses); + } else { + uses.push_back(use); + } + } +} + +bool lowerPackedAncillary(IrProgram& program, IrBuilder& builder, IrValue& ancillary) { + auto& kind = resolveArg(ancillary, 0); + if (!isImmediate(kind, IrType::U32) || static_cast(kind.ImmediateU32()) != StageInputKind::PackedAncillary) { + return false; + } + std::vector uses; + collectUses(ancillary, uses); + if (uses.empty()) { + return false; + } + IrBlock& block = *ancillary.Parent(); + const auto emit = [&](IrOpcode opcode, std::initializer_list arguments) -> IrValue& { + IrValue& created = program.CreateValue(opcode, IrType::U32); + for (IrValue* argument : arguments) { + created.AddArgument(argument); + } + block.InsertInstructionBefore(&ancillary, &created); + return created; + }; + std::array fields {}; + const auto field = [&](std::size_t index) -> IrValue& { + if (fields[index] == nullptr) { + const auto builtin = index == 0u ? StageInputKind::SampleId : StageInputKind::Layer; + fields[index] = &emit(IrOpcode::GetBuiltin, {&builder.Constant(static_cast(builtin)), &builder.Constant(0u)}); + } + return *fields[index]; + }; + constexpr std::array, 2> ranges {{{8u, 4u}, {16u, 13u}}}; + IrValue* packed = nullptr; + for (const IrUse& use : uses) { + IrValue& user = *use.user; + if ((user.Opcode() == IrOpcode::BitFieldUExtract || user.Opcode() == IrOpcode::BitFieldSExtract) && use.operand == 0u) { + auto& offset = resolveArg(user, 1); + auto& count = resolveArg(user, 2); + if (isImmediate(offset, IrType::U32) && isImmediate(count, IrType::U32) && count.ImmediateU32() != 0u) { + const auto range = std::ranges::find_if(ranges, [&](const auto& candidate) { + return offset.ImmediateU32() >= candidate.first && count.ImmediateU32() <= candidate.first + candidate.second - offset.ImmediateU32(); + }); + if (range != ranges.end()) { + user.ReplaceArgument(0, &field(static_cast(range - ranges.begin()))); + user.ReplaceArgument(1, &builder.Constant(offset.ImmediateU32() - range->first)); + continue; + } + } + } + if (packed == nullptr) { + IrValue& sample = emit(IrOpcode::ShiftLeftLogical32, {&field(0u), &builder.Constant(ranges[0].first)}); + IrValue& layer = emit(IrOpcode::ShiftLeftLogical32, {&field(1u), &builder.Constant(ranges[1].first)}); + packed = &emit(IrOpcode::BitwiseOr32, {&sample, &layer}); + } + user.ReplaceArgument(use.operand, packed); + } + return true; +} + } // namespace bool ConstantFolder::tryFoldValue(IrProgram& program, IrValue& value) const { IrBuilder builder(program); switch (value.Opcode()) { case IrOpcode::Phi: return foldPhi(value); + case IrOpcode::GetBuiltin: return lowerPackedAncillary(program, builder, value); case IrOpcode::SelectU1: case IrOpcode::SelectF32: case IrOpcode::SelectU32: return foldSelect(value); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp index 9b56e98ae..515f6d256 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp @@ -132,10 +132,13 @@ void DefineModule(SpirvEmitterState& state) { if (state.cullDistanceVariable != 0) { state.module.EmitCapability(spv::CapabilityCullDistance); } - if (state.layerVariable != 0 || InputVariableForKind(state, StageInputKind::Layer) != 0) { + if (state.layerVariable != 0) { state.module.RequireVersion(0x00010500u); state.module.EmitCapability(spv::CapabilityShaderLayer); } + if (InputVariableForKind(state, StageInputKind::Layer) != 0) { + state.module.EmitCapability(spv::CapabilityGeometry); + } if (state.viewportIndexVariable != 0) { state.module.RequireVersion(0x00010500u); state.module.EmitCapability(spv::CapabilityShaderViewportIndex); From 3d014251a54df7d006da1ab746eb6eaa4b5342d4 Mon Sep 17 00:00:00 2001 From: Michon <231849@buas.nl> Date: Sun, 4 Oct 2026 10:27:07 +0200 Subject: [PATCH 2/3] fix(shader): lower only exact sample id and layer field extracts of the packed pixel ancillary Any other live use of the packed register keeps throwing, as on main, instead of reading SampleId << 8 | Layer << 16 with the other bits zero. The field range check no longer underflows for an extract that starts past a field's end. Adds the pixel_ancillary recompiler test. Co-Authored-By: Claude Opus 5.5 (1M context) --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 8 +++ core/libs/tests/PixelAncillary.cpp | 64 +++++++++++++++++++ .../Optimization/src/ConstantFolder.cpp | 57 +++++++---------- 3 files changed, 95 insertions(+), 34 deletions(-) create mode 100644 core/libs/tests/PixelAncillary.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index b373847f0..4388e8574 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -758,6 +758,14 @@ if(BUILD_TESTING) set_tests_properties(image_sample_gather PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(pixel_ancillary_tests ${LIBS_INCLUDE_DIR}/tests/PixelAncillary.cpp) + target_link_libraries(pixel_ancillary_tests PRIVATE shader_recompiler_agc_driver libc) + configure_windows_unwind(pixel_ancillary_tests) + add_test(NAME pixel_ancillary COMMAND pixel_ancillary_tests) + if(WIN32) + set_tests_properties(pixel_ancillary PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_indexed_draw_tests ${LIBS_INCLUDE_DIR}/tests/GuestIndexedDraw.cpp) target_include_directories(guest_indexed_draw_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_indexed_draw_tests PRIVATE libSceAgc libSceAgcDriver libc) diff --git a/core/libs/tests/PixelAncillary.cpp b/core/libs/tests/PixelAncillary.cpp new file mode 100644 index 000000000..2d6f10dad --- /dev/null +++ b/core/libs/tests/PixelAncillary.cpp @@ -0,0 +1,64 @@ +#include "IntermediateRepresentation/IrBuilder.hpp" +#include "Optimization/ConstantFolder.hpp" +#include "Optimization/DeadCodeEliminator.hpp" +#include "Optimization/ShaderInfoCollector.hpp" +#include +#include +#include + +using namespace ShaderRecompiler; +static void Require(bool value) { if (!value) throw std::runtime_error("packed pixel ancillary regression"); } +static IrValue& Build(IrProgram& program, IrOpcode opcode, std::uint32_t offset, std::uint32_t count) { + program.Resources().stage = IrShaderStage::Pixel; + program.Resources().resourceTrackingComplete = true; + auto& block = program.CreateBlock(); + program.SetEntryBlock(block); + program.BlockOrder().push_back(&block); + IrBuilder builder(program); + builder.SetInsertionPoint(block); + auto& ancillary = builder.Emit(IrOpcode::GetBuiltin, IrType::U32, {&builder.Constant(static_cast(StageInputKind::PackedAncillary)), &builder.Constant(0u)}); + auto& user = opcode == IrOpcode::BitwiseOr32 ? builder.Emit(opcode, IrType::U32, {&ancillary, &builder.Constant(offset)}) + : builder.Emit(opcode, IrType::U32, {&ancillary, &builder.Constant(offset), &builder.Constant(count)}); + static_cast(builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&user})); + static_cast(builder.Emit(IrOpcode::Return, IrType::Void, {})); + return user; +} +static void Lower(IrProgram& program) { + ConstantFolder().Fold(program); + DeadCodeEliminator().RemoveIdentities(program); + DeadCodeEliminator().Eliminate(program); + const ShaderPixelInputInfo pixel {}; + ShaderInfoCollector().Collect(program, ShaderStageInputInfo {nullptr, &pixel, nullptr}); +} +static void Extract(IrOpcode opcode, std::uint32_t offset, std::uint32_t count, StageInputKind kind, std::uint32_t fieldOffset) { + IrProgram program; + auto& user = Build(program, opcode, offset, count); + Lower(program); + const IrValue* field = user.Argument(0)->Resolve(); + Require(field->Opcode() == IrOpcode::GetBuiltin); + Require(static_cast(field->Argument(0)->Resolve()->ImmediateU32()) == kind); + Require(user.Argument(1)->Resolve()->ImmediateU32() == fieldOffset); + Require(user.Argument(2)->Resolve()->ImmediateU32() == count); +} +static void Refused(IrOpcode opcode, std::uint32_t offset, std::uint32_t count) { + IrProgram program; + static_cast(Build(program, opcode, offset, count)); + try { + Lower(program); + } catch (const std::runtime_error& error) { + Require(std::string(error.what()).find("unsupported live use") != std::string::npos); + return; + } + Require(false); +} +int main() { + Extract(IrOpcode::BitFieldUExtract, 8u, 4u, StageInputKind::SampleId, 0u); + Extract(IrOpcode::BitFieldUExtract, 9u, 2u, StageInputKind::SampleId, 1u); + Extract(IrOpcode::BitFieldUExtract, 16u, 13u, StageInputKind::Layer, 0u); + Extract(IrOpcode::BitFieldSExtract, 20u, 9u, StageInputKind::Layer, 4u); + Refused(IrOpcode::BitwiseOr32, 1u, 0u); + Refused(IrOpcode::BitFieldUExtract, 2u, 4u); + Refused(IrOpcode::BitFieldUExtract, 10u, 4u); + Refused(IrOpcode::BitFieldUExtract, 13u, 2u); + Refused(IrOpcode::BitFieldUExtract, 16u, 14u); +} diff --git a/core/shader/recompiler/Optimization/src/ConstantFolder.cpp b/core/shader/recompiler/Optimization/src/ConstantFolder.cpp index ac7bcfdcd..a05a6c7a8 100644 --- a/core/shader/recompiler/Optimization/src/ConstantFolder.cpp +++ b/core/shader/recompiler/Optimization/src/ConstantFolder.cpp @@ -5,7 +5,6 @@ #include #include #include -#include #include #include @@ -202,52 +201,42 @@ bool lowerPackedAncillary(IrProgram& program, IrBuilder& builder, IrValue& ancil } std::vector uses; collectUses(ancillary, uses); - if (uses.empty()) { - return false; - } - IrBlock& block = *ancillary.Parent(); - const auto emit = [&](IrOpcode opcode, std::initializer_list arguments) -> IrValue& { - IrValue& created = program.CreateValue(opcode, IrType::U32); - for (IrValue* argument : arguments) { - created.AddArgument(argument); - } - block.InsertInstructionBefore(&ancillary, &created); - return created; - }; std::array fields {}; const auto field = [&](std::size_t index) -> IrValue& { if (fields[index] == nullptr) { const auto builtin = index == 0u ? StageInputKind::SampleId : StageInputKind::Layer; - fields[index] = &emit(IrOpcode::GetBuiltin, {&builder.Constant(static_cast(builtin)), &builder.Constant(0u)}); + IrValue& created = program.CreateValue(IrOpcode::GetBuiltin, IrType::U32); + created.AddArgument(&builder.Constant(static_cast(builtin))); + created.AddArgument(&builder.Constant(0u)); + ancillary.Parent()->InsertInstructionBefore(&ancillary, &created); + fields[index] = &created; } return *fields[index]; }; constexpr std::array, 2> ranges {{{8u, 4u}, {16u, 13u}}}; - IrValue* packed = nullptr; + bool lowered = false; for (const IrUse& use : uses) { IrValue& user = *use.user; - if ((user.Opcode() == IrOpcode::BitFieldUExtract || user.Opcode() == IrOpcode::BitFieldSExtract) && use.operand == 0u) { - auto& offset = resolveArg(user, 1); - auto& count = resolveArg(user, 2); - if (isImmediate(offset, IrType::U32) && isImmediate(count, IrType::U32) && count.ImmediateU32() != 0u) { - const auto range = std::ranges::find_if(ranges, [&](const auto& candidate) { - return offset.ImmediateU32() >= candidate.first && count.ImmediateU32() <= candidate.first + candidate.second - offset.ImmediateU32(); - }); - if (range != ranges.end()) { - user.ReplaceArgument(0, &field(static_cast(range - ranges.begin()))); - user.ReplaceArgument(1, &builder.Constant(offset.ImmediateU32() - range->first)); - continue; - } - } + if ((user.Opcode() != IrOpcode::BitFieldUExtract && user.Opcode() != IrOpcode::BitFieldSExtract) || use.operand != 0u) { + continue; } - if (packed == nullptr) { - IrValue& sample = emit(IrOpcode::ShiftLeftLogical32, {&field(0u), &builder.Constant(ranges[0].first)}); - IrValue& layer = emit(IrOpcode::ShiftLeftLogical32, {&field(1u), &builder.Constant(ranges[1].first)}); - packed = &emit(IrOpcode::BitwiseOr32, {&sample, &layer}); + auto& offset = resolveArg(user, 1); + auto& count = resolveArg(user, 2); + if (!isImmediate(offset, IrType::U32) || !isImmediate(count, IrType::U32) || count.ImmediateU32() == 0u) { + continue; } - user.ReplaceArgument(use.operand, packed); + const auto range = std::ranges::find_if(ranges, [&](const auto& candidate) { + return offset.ImmediateU32() >= candidate.first && count.ImmediateU32() <= candidate.second && + offset.ImmediateU32() - candidate.first <= candidate.second - count.ImmediateU32(); + }); + if (range == ranges.end()) { + continue; + } + user.ReplaceArgument(0, &field(static_cast(range - ranges.begin()))); + user.ReplaceArgument(1, &builder.Constant(offset.ImmediateU32() - range->first)); + lowered = true; } - return true; + return lowered; } } // namespace From 048d9982cda337c61150c635e4b5a71620018cbf Mon Sep 17 00:00:00 2001 From: Michon <231849@buas.nl> Date: Wed, 7 Oct 2026 10:09:51 +0200 Subject: [PATCH 3/3] fix(agc): the Z-format precheck gives no verdict for a missing SPI_SHADER_Z_FORMAT DrawRejection read 0x1c4 with read(), which throws when the register is absent, against the function's rule that a missing register gives no verdict there; it now reuses the value read with value() for the DB_SHADER_CONTROL check. Tests pin the Z-export rules: formats 1, 2, 3 and 9 with Z_EXPORT_ENABLE accepted, format 0 rejected, the sample-mask export accepted with 32_ABGR and rejected with 32_R, formats 4 to 8 rejected, and a missing 0x1c4 accepted. --- .../libSceAgcDriver/Graphics/src/State.cpp | 2 +- .../prx/libSceAgcDriver/tests/Graphics.cpp | 30 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index c2c508b37..18feef999 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -790,7 +790,7 @@ std::string DrawRejection(const QueueState& queue, bool indexed) { if (auto reason = nonzero(cx, 0x204, ClipControlMask, "unsupported PA_CL_CLIP_CNTL flags"); !reason.empty()) return reason; 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(read(cx, 0x1c4)) ? 0u : ~0u, "depth or sample-mask export"); !reason.empty()) return reason; + 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); for (const auto offset : {0x1b3u, 0x1b4u, 0x1c5u}) { if (find(cx, offset) != cx.end()) continue; diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 3cd226c5a..dca4b6f5b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -573,6 +573,35 @@ void DepthStencilTests() { expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "DCC 3D color targets"); } +// SPI_SHADER_Z_FORMAT (0x1c4) and the export enables of DB_SHADER_CONTROL (0x203): Z export needs a +// format with a depth channel (1, 2, 3 or 32_ABGR 9), the sample mask needs 32_ABGR, and the +// formats the export path does not lay out are refused. A missing 0x1c4 gives no verdict here. +void ZExportTests() { + constexpr std::uint32_t zExportEnable = 0x1u; + constexpr std::uint32_t maskExportEnable = 0x100u; + const auto withExports = [](std::uint32_t format, std::uint32_t exports) { + auto queue = makeState(); + queue.context[0x1b3] = 2; + queue.context[0x1b4] = 2; + queue.context[0x1c4] = format; + queue.context[0x203] = 0x800u | exports; + return queue; + }; + for (const std::uint32_t format : {1u, 2u, 3u, 9u}) { + const auto queue = withExports(format, zExportEnable); + Require(AgcDriver::Graphics::DrawRejection(queue, true).empty(), "a Z export with format " + std::to_string(format) + " was rejected"); + } + Require(!AgcDriver::Graphics::DrawRejection(withExports(0, zExportEnable), true).empty(), "a Z export without a Z format was accepted"); + Require(AgcDriver::Graphics::DrawRejection(withExports(9, zExportEnable | maskExportEnable), true).empty(), "a sample-mask export with 32_ABGR was rejected"); + Require(!AgcDriver::Graphics::DrawRejection(withExports(1, zExportEnable | maskExportEnable), true).empty(), "a sample-mask export with 32_R was accepted"); + for (std::uint32_t format = 4; format <= 8; ++format) { + Require(!AgcDriver::Graphics::DrawRejection(withExports(format, 0), true).empty(), "Z format " + std::to_string(format) + " was accepted"); + } + auto absent = withExports(0, 0); + absent.context.erase(0x1c4); + Require(AgcDriver::Graphics::DrawRejection(absent, true).empty(), "a missing SPI_SHADER_Z_FORMAT was rejected (or threw) in the precheck"); +} + void DepthBoundsBiasTests() { const auto bits = [](float value) { std::uint32_t word = 0; @@ -1907,6 +1936,7 @@ int main() { hardwareScreenOffsetTests(); DepthClipTests(); DepthStencilTests(); + ZExportTests(); DepthBoundsBiasTests(); DisabledColorTests(); CompactedExportTests();