fix(agc): export the stencil reference from pixel shaders with VK_EXT_shader_stencil_export

This commit is contained in:
Dean Galvin 2026-10-12 19:39:43 +13:00
parent d06c065657
commit 22d662eda7
22 changed files with 363 additions and 18 deletions

View File

@ -64,6 +64,7 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptr<Vulk
for (const bool flag : {pixel.wave32, pixel.hasPerspectiveCenterVgpr, pixel.perspectiveCentroid, pixel.posX, pixel.posY, pixel.posZ, pixel.posW, pixel.frontFace, pixel.ancillary, pixel.sampleShading, pixel.noPerspective, pixel.linearCentroid, pixel.pixelKillEnable, pixel.depthExportEnable, pixel.sampleMaskExportEnable, pixel.earlyZ, pixel.executeOnNoop}) mix(flag);
mix(static_cast<std::uint64_t>(pixel.conservativeZExport));
mix(pixel.orderedPixelShader);
mix(pixel.stencilRefExportEnable);
for (const auto value : pixel.targetOutputMode) mix(value);
for (const auto value : pixel.targetExportMapping) mix(value);
for (const auto value : pixel.targetExportPacking) mix(static_cast<std::uint64_t>(value));

View File

@ -98,7 +98,7 @@ bool Driver::sameDecode(const DrawDecode& a, const DrawDecode& b) {
if (std::memcmp(&s.viewport, &t.viewport, sizeof(VkViewport)) != 0 || std::memcmp(&s.scissor, &t.scissor, sizeof(VkRect2D)) != 0) return false;
const auto& p = a.pixel;
const auto& q = b.pixel;
if (p.interpolatorCount != q.interpolatorCount || p.interpolatorSettings != q.interpolatorSettings || p.wave32 != q.wave32 || p.inputAddr != q.inputAddr || p.hasPerspectiveCenterVgpr != q.hasPerspectiveCenterVgpr || p.perspectiveCentroid != q.perspectiveCentroid || p.posX != q.posX || p.posY != q.posY || p.posZ != q.posZ || p.posW != q.posW || p.frontFace != q.frontFace || p.ancillary != q.ancillary || p.sampleShading != q.sampleShading || p.noPerspective != q.noPerspective || p.linearCentroid != q.linearCentroid || p.pixelKillEnable != q.pixelKillEnable || p.depthExportEnable != q.depthExportEnable || p.sampleMaskExportEnable != q.sampleMaskExportEnable || p.earlyZ != q.earlyZ || p.executeOnNoop != q.executeOnNoop || p.conservativeZExport != q.conservativeZExport || p.orderedPixelShader != q.orderedPixelShader || p.targetOutputMode != q.targetOutputMode || p.targetExportMapping != q.targetExportMapping) return false;
if (p.interpolatorCount != q.interpolatorCount || p.interpolatorSettings != q.interpolatorSettings || p.wave32 != q.wave32 || p.inputAddr != q.inputAddr || p.hasPerspectiveCenterVgpr != q.hasPerspectiveCenterVgpr || p.perspectiveCentroid != q.perspectiveCentroid || p.posX != q.posX || p.posY != q.posY || p.posZ != q.posZ || p.posW != q.posW || p.frontFace != q.frontFace || p.ancillary != q.ancillary || p.sampleShading != q.sampleShading || p.noPerspective != q.noPerspective || p.linearCentroid != q.linearCentroid || p.pixelKillEnable != q.pixelKillEnable || p.depthExportEnable != q.depthExportEnable || p.sampleMaskExportEnable != q.sampleMaskExportEnable || p.earlyZ != q.earlyZ || p.executeOnNoop != q.executeOnNoop || p.conservativeZExport != q.conservativeZExport || p.orderedPixelShader != q.orderedPixelShader || p.stencilRefExportEnable != q.stencilRefExportEnable || p.targetOutputMode != q.targetOutputMode || p.targetExportMapping != q.targetExportMapping) return false;
if (a.roles != b.roles || a.programs.size() != b.programs.size()) return false;
for (std::size_t i = 0; i < a.programs.size(); ++i) {
const auto& x = a.programs[i];

View File

@ -1095,6 +1095,11 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) :
state->capabilities.push_back(spv::CapabilityFragmentShaderPixelInterlockEXT);
state->spirvExtensions.push_back("SPV_EXT_fragment_shader_interlock");
}
if (hasExtension(VK_EXT_SHADER_STENCIL_EXPORT_EXTENSION_NAME)) {
deviceExtensions.push_back(VK_EXT_SHADER_STENCIL_EXPORT_EXTENSION_NAME);
state->capabilities.push_back(spv::CapabilityStencilExportEXT);
state->spirvExtensions.push_back("SPV_EXT_shader_stencil_export");
}
if (state->shaderClock) {
deviceExtensions.push_back(VK_KHR_SHADER_CLOCK_EXTENSION_NAME);
state->capabilities.push_back(spv::CapabilityShaderClockKHR);

View File

@ -142,6 +142,7 @@ ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& con
const bool pixelKillEnable = ((shaderControl >> 6u) & 0x1u) != 0;
const bool depthExportEnable = (shaderControl & 0x1u) != 0;
const bool sampleMaskExportEnable = ((shaderControl >> 8u) & 0x1u) != 0;
const bool stencilRefExportEnable = ((shaderControl >> 1u) & 0x1u) != 0;
const auto zOrder = (shaderControl >> 4u) & 0x3u;
const auto loaded = [&](PixelInput input) { return (activeInputs & PixelInputBit(input)) != 0; };
return ShaderRecompiler::ShaderPixelStageInfo{
@ -163,12 +164,13 @@ ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& con
.pixelKillEnable = pixelKillEnable,
.depthExportEnable = depthExportEnable,
.sampleMaskExportEnable = sampleMaskExportEnable,
.earlyZ = zOrder == 1u && !pixelKillEnable && !depthExportEnable && !sampleMaskExportEnable,
.earlyZ = zOrder == 1u && !pixelKillEnable && !depthExportEnable && !sampleMaskExportEnable && !stencilRefExportEnable,
.executeOnNoop = ((shaderControl >> 10u) & 0x1u) != 0,
.conservativeZExport = static_cast<ShaderRecompiler::ConservativeZExport>(conservativeZExport),
.orderedPixelShader = ((shaderControl >> 16u) & 0x1u) != 0,
.targetOutputMode = targetOutputMode,
.targetExportMapping = nullProgram ? std::array<std::uint8_t, 8>{} : exportMappings
.targetExportMapping = nullProgram ? std::array<std::uint8_t, 8>{} : exportMappings,
.stencilRefExportEnable = stencilRefExportEnable
};
}

View File

@ -137,7 +137,7 @@ struct Module {
Require(value == spv::BuiltInPosition && signature == "f32x4" && !position, "unsupported or duplicate vertex built-in output");
position = true;
} else {
Require((storage == spv::StorageClassInput && ((value == spv::BuiltInFragCoord && signature == "f32x4") || ((value == spv::BuiltInFrontFacing || value == spv::BuiltInHelperInvocation) && signature == "bool") || ((value == spv::BuiltInSampleId || value == spv::BuiltInLayer) && signature == "i32"))) || (storage == spv::StorageClassOutput && ((value == spv::BuiltInFragDepth && signature == "f32") || (value == spv::BuiltInSampleMask && (signature == "i32[1]" || signature == "u32[1]")))), "unsupported fragment built-in");
Require((storage == spv::StorageClassInput && ((value == spv::BuiltInFragCoord && signature == "f32x4") || ((value == spv::BuiltInFrontFacing || value == spv::BuiltInHelperInvocation) && signature == "bool") || ((value == spv::BuiltInSampleId || value == spv::BuiltInLayer) && signature == "i32"))) || (storage == spv::StorageClassOutput && ((value == spv::BuiltInFragDepth && signature == "f32") || (value == spv::BuiltInFragStencilRefEXT && signature == "i32") || (value == spv::BuiltInSampleMask && (signature == "i32[1]" || signature == "u32[1]")))), "unsupported fragment built-in");
}
}
};
@ -232,6 +232,10 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi
fragment &&
capability == spv::CapabilityFragmentShaderPixelInterlockEXT;
const bool isStencilExportCapability =
fragment &&
capability == spv::CapabilityStencilExportEXT;
// Enabled unconditionally or by the device setup in VulkanDevice.
const bool isFeatureCapability =
capability == spv::CapabilitySampled1D ||
@ -273,6 +277,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi
isMeshCapability ||
isGeometryCapability ||
isInterlockCapability ||
isStencilExportCapability ||
isFeatureCapability ||
isDescriptorIndexingCapability ||
((imageInt64Atomics || bufferInt64Atomics) && capability == spv::CapabilityInt64Atomics) ||
@ -293,6 +298,10 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi
Require(fragment, "SPV_EXT_fragment_shader_interlock requires a fragment shader");
break;
}
if (extension == "SPV_EXT_shader_stencil_export") {
Require(fragment, "SPV_EXT_shader_stencil_export requires a fragment shader");
break;
}
if (extension == "SPV_KHR_fragment_shader_barycentric") {
Require(fragment && fragmentShaderBarycentric, "SPV_KHR_fragment_shader_barycentric requires enabled fragmentShaderBarycentric in a fragment shader");
break;
@ -420,7 +429,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi
mode(spv::ExecutionModeOutputVertices, {3u});
Require(module.modes.size() == 4, "unsupported geometry execution mode");
} else if (fragment) {
for (const auto& [name, operands] : module.modes) Require(operands.empty() && (name == spv::ExecutionModeOriginUpperLeft || name == spv::ExecutionModeEarlyFragmentTests || name == spv::ExecutionModeDepthReplacing || name == spv::ExecutionModeDepthLess || name == spv::ExecutionModeDepthGreater || name == spv::ExecutionModePixelInterlockOrderedEXT), "unsupported fragment execution mode");
for (const auto& [name, operands] : module.modes) Require(operands.empty() && (name == spv::ExecutionModeOriginUpperLeft || name == spv::ExecutionModeEarlyFragmentTests || name == spv::ExecutionModeDepthReplacing || name == spv::ExecutionModeDepthLess || name == spv::ExecutionModeDepthGreater || name == spv::ExecutionModePixelInterlockOrderedEXT || name == spv::ExecutionModeStencilRefReplacingEXT), "unsupported fragment execution mode");
} else Require(module.modes.empty(), "unsupported vertex execution mode");
std::set<std::pair<std::uint32_t, std::uint32_t>> descriptors;
bool push = false;

View File

@ -119,9 +119,11 @@ bool zFormatSupported(std::uint32_t format) {
std::uint32_t shaderControlMask(std::uint32_t zFormat) {
constexpr std::uint32_t zExportEnable = 0x1u;
constexpr std::uint32_t stencilTestValExportEnable = 0x2u;
constexpr std::uint32_t maskExportEnable = 0x100u;
auto mask = ShaderControlMask;
if (zFormat != 0u && zFormatSupported(zFormat)) mask &= ~zExportEnable;
if (zFormat == 2u || zFormat == 9u) mask &= ~stencilTestValExportEnable;
if (zFormat == 9u) mask &= ~maskExportEnable;
return mask;
}
@ -154,7 +156,7 @@ bool depthPlanesAbsent(const Registers& cx) {
return z != cx.end() && stencil != cx.end() && (z->second & 3u) == 0 && (stencil->second & 1u) == 0;
}
VkStencilOpState stencilFace(std::uint32_t compare, std::uint32_t ops, std::uint32_t refMask, bool readOnly) {
VkStencilOpState stencilFace(std::uint32_t compare, std::uint32_t ops, std::uint32_t refMask, bool readOnly, bool testValExport) {
VkStencilOpState face{};
face.compareOp = static_cast<VkCompareOp>(compare);
face.compareMask = (refMask >> 8u) & 0xffu;
@ -172,6 +174,10 @@ VkStencilOpState stencilFace(std::uint32_t compare, std::uint32_t ops, std::uint
case 2:
case 3:
case 4: {
if (testValExport) {
if (op != 3) throw std::runtime_error("AGC graphics: stencil replacement by a value other than the test value is unsupported with a pixel shader that exports the stencil test value");
return VK_STENCIL_OP_REPLACE;
}
const auto value = op == 2 ? 0xffu : op == 3 ? test : opValue;
if (((reference ^ value) & fixed & face.writeMask) != 0) {
std::snprintf(detail, sizeof(detail), "AGC graphics: stencil replacement 0x%02x against test value 0x%02x (masks 0x%02x/0x%02x) is unsupported", value, test, face.compareMask, face.writeMask);
@ -203,7 +209,7 @@ VkStencilOpState stencilFace(std::uint32_t compare, std::uint32_t ops, std::uint
return face;
}
void decodeDepth(const Registers& cx, std::uint32_t depthControl, State& result) {
void decodeDepth(const Registers& cx, std::uint32_t depthControl, bool testValExport, State& result) {
zero(cx, 0x000, 0x00001f9cu, "depth copy, resummarize or decompress draws (DB_RENDER_CONTROL)");
const bool depthClear = (read(cx, 0x000) & 1u) != 0;
const bool stencilClear = (read(cx, 0x000) & 2u) != 0;
@ -223,6 +229,7 @@ void decodeDepth(const Registers& cx, std::uint32_t depthControl, State& result)
const bool depthReadOnly = (view & 0x01000000u) != 0;
const bool stencilReadOnly = (view & 0x02000000u) != 0;
Require(!stencilClear || (stencil && !stencilReadOnly), "stencil clear requires a writable stencil plane");
Require(!stencilClear || !testValExport, "stencil clear with a pixel shader that exports the stencil test value is unsupported");
Require(!depthClear || (zFormat != 0 && !depthReadOnly), "depth clear requires a writable depth plane");
DepthTarget depth{};
depth.samples = 1u << ((read(cx, 0x010) >> 2u) & 3u);
@ -274,8 +281,8 @@ void decodeDepth(const Registers& cx, std::uint32_t depthControl, State& result)
result.stencilBack = clear;
} else if (result.stencilTest) {
const auto ops = read(cx, 0x10b);
result.stencilFront = stencilFace((depthControl >> 8u) & 7u, ops, read(cx, 0x10c), stencilReadOnly);
result.stencilBack = (depthControl & 0x80u) != 0 ? stencilFace((depthControl >> 20u) & 7u, ops >> 12u, read(cx, 0x10d), stencilReadOnly) : result.stencilFront;
result.stencilFront = stencilFace((depthControl >> 8u) & 7u, ops, read(cx, 0x10c), stencilReadOnly, testValExport);
result.stencilBack = (depthControl & 0x80u) != 0 ? stencilFace((depthControl >> 20u) & 7u, ops >> 12u, read(cx, 0x10d), stencilReadOnly, testValExport) : result.stencilFront;
}
}
@ -667,7 +674,7 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) {
const bool clear = renderControl != cx.end() && (renderControl->second & 3u) != 0;
const auto tests = planeTests(cx, depthControl);
if (clear || (tests != 0 && depthSurfaceBound(cx))) {
decodeDepth(cx, depthControl, result);
decodeDepth(cx, depthControl, (read(cx, 0x203) & 2u) != 0 && !PixelProgramSkipped(queue), result);
} else if (depthPassThrough(depthControl) || ((depthControl & 0xbu) != 0 && (tests == 0 || depthPlanesAbsent(cx)))) {
static bool reported = false;
if (!reported) {

View File

@ -3688,6 +3688,131 @@ void orderedPixelShaderTests() {
expectFailure([&] { static_cast<void>(ShaderRecompiler::Recompile(orderedPixelRequest(0x30600u, plain))); }, "fragmentShaderPixelInterlock");
}
constexpr std::array<std::uint32_t, 4> StencilRefExportCode{0x7e0e02f2u, 0xf8001882u, 0x700u, 0xbf810000u};
constexpr std::array<std::uint32_t, 4> DepthStencilRefExportCode{0x7e0e02f2u, 0xf8001883u, 0x707u, 0xbf810000u};
AgcDriver::QueueState stencilRefExportQueue(std::uint32_t shaderControl, std::uint32_t zFormat) {
auto queue = makeState();
queue.context[0x1b3] = 0x300u;
queue.context[0x1b4] = 0x300u;
queue.context[0x1c4] = zFormat;
queue.context[0x203] = shaderControl;
return queue;
}
ShaderRecompiler::RecompileRequest stencilRefExportRequest(std::uint32_t shaderControl, std::span<const std::uint32_t> capabilities, std::span<const std::uint32_t> code = StencilRefExportCode) {
const auto queue = stencilRefExportQueue(shaderControl, 2u);
ShaderRecompiler::RecompileRequest request{};
request.shader = {ShaderRecompiler::ShaderStage::Fragment, 0x30000u, code, 0, {}};
request.context.waveSize = 64;
request.context.pixel = AgcDriver::Graphics::DecodePixelStageInfo(queue.context, IdentityExports);
request.target.vulkanVersion = 0x00401000u;
request.target.spirvVersion = 0x00010300u;
request.target.subgroupSize = 64;
request.target.supportedCapabilities = capabilities;
request.layout.pushConstantSizeBytes = 128;
request.useCache = false;
return request;
}
std::string stencilRefShape(std::span<const std::uint32_t> words) {
std::string shape;
std::uint32_t variable = 0;
for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) {
const auto op = static_cast<spv::Op>(words[at] & 0xffffu);
if (op == spv::OpCapability && words[at + 1] == spv::CapabilityStencilExportEXT) shape += "capability ";
if (op == spv::OpExtension && std::string_view(reinterpret_cast<const char*>(&words[at + 1])) == "SPV_EXT_shader_stencil_export") shape += "extension ";
if (op == spv::OpExecutionMode && words[at + 2] == spv::ExecutionModeEarlyFragmentTests) shape += "early ";
if (op == spv::OpExecutionMode && words[at + 2] == spv::ExecutionModeDepthReplacing) shape += "depth ";
if (op == spv::OpExecutionMode && words[at + 2] == spv::ExecutionModeStencilRefReplacingEXT) shape += "replacing ";
if (op == spv::OpDecorate && words[at + 2] == spv::DecorationBuiltIn && words[at + 3] == spv::BuiltInFragStencilRefEXT) {
variable = words[at + 1];
shape += "builtin ";
}
if (op == spv::OpStore && variable != 0 && words[at + 1] == variable) shape += "store ";
}
return shape;
}
void stencilRefExportTests() {
for (const auto shaderControl : {0x52u, 0x12u, 0x02u, 0x03u}) {
for (const auto zFormat : {2u, 9u}) {
const auto queue = stencilRefExportQueue(shaderControl, zFormat);
static_cast<void>(AgcDriver::Graphics::DecodeState(queue));
Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck rejected DB_SHADER_CONTROL " + std::to_string(shaderControl) + " with Z format " + std::to_string(zFormat));
const auto pixel = AgcDriver::Graphics::DecodePixelStageInfo(queue.context, IdentityExports);
Require(pixel.stencilRefExportEnable, "STENCIL_TEST_VAL_EXPORT_ENABLE did not reach the pixel stage");
Require(!pixel.earlyZ, "a pixel shader that exports the stencil reference was given early tests");
}
for (const auto zFormat : {0u, 1u, 3u, 7u}) {
const auto queue = stencilRefExportQueue(shaderControl, zFormat);
expectFailure([&] { static_cast<void>(AgcDriver::Graphics::DecodeState(queue)); }, "depth export, shader coverage or ordered fragment execution");
Require(!AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck accepted a stencil reference export with Z format " + std::to_string(zFormat));
}
}
for (const auto shaderControl : {0x54u, 0x56u}) {
const auto queue = stencilRefExportQueue(shaderControl, 2u);
expectFailure([&] { static_cast<void>(AgcDriver::Graphics::DecodeState(queue)); }, "depth export, shader coverage or ordered fragment execution");
Require(!AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck accepted STENCIL_OP_VAL_EXPORT_ENABLE in DB_SHADER_CONTROL " + std::to_string(shaderControl));
}
Require(!AgcDriver::Graphics::DecodePixelStageInfo(stencilRefExportQueue(0x50u, 2u).context, IdentityExports).stencilRefExportEnable, "a pixel stage without STENCIL_TEST_VAL_EXPORT_ENABLE exports the stencil reference");
const std::array<std::uint32_t, 2> stencilExport{spv::CapabilityShader, spv::CapabilityStencilExportEXT};
const std::array<std::uint32_t, 1> plain{spv::CapabilityShader};
const ShaderRecompiler::RequestSerializer serializer;
std::set<std::vector<std::uint64_t>> keys;
const std::array<std::pair<std::uint32_t, std::string_view>, 4> cases{{{0x52u, "capability extension replacing builtin store "}, {0x12u, "capability extension replacing builtin store "}, {0x50u, ""}, {0x10u, "early "}}};
for (const auto& [shaderControl, expected] : cases) {
const auto request = stencilRefExportRequest(shaderControl, stencilExport);
const auto replayed = serializer.Deserialize(serializer.Serialize(request));
Require(replayed.request.context.pixel->stencilRefExportEnable == ((shaderControl & 2u) != 0), "the serialized request lost STENCIL_TEST_VAL_EXPORT_ENABLE");
const auto result = ShaderRecompiler::Recompile(request);
Require(stencilRefShape(result.spirv.Words()) == expected && stencilRefShape(ShaderRecompiler::Recompile(replayed.request).spirv.Words()) == expected, "DB_SHADER_CONTROL " + std::to_string(shaderControl) + " declared the wrong stencil reference export: " + stencilRefShape(result.spirv.Words()));
std::vector<std::uint64_t> key;
ShaderRecompiler::RecompileCacheKey::Build(request, key);
keys.insert(key);
AgcDriver::Graphics::State state{};
state.stages.path = AgcDriver::Graphics::ShaderPath::Vertex;
ShaderRecompiler::RecompileResult vertex;
vertex.spirv = makeModule({});
const std::array<AgcDriver::Graphics::CompiledShader, 2> shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &result, 0}}};
AgcDriver::Graphics::ValidateShaders(shaders, state, VkPhysicalDeviceSubgroupProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES}, false);
}
Require(keys.size() == cases.size(), "the recompile cache key ignores STENCIL_TEST_VAL_EXPORT_ENABLE");
const auto both = stencilRefShape(ShaderRecompiler::Recompile(stencilRefExportRequest(0x53u, stencilExport, DepthStencilRefExportCode)).spirv.Words());
Require(both == "capability extension depth replacing builtin store ", "a depth and stencil reference export declared the wrong outputs: " + both);
const auto depthOnly = stencilRefShape(ShaderRecompiler::Recompile(stencilRefExportRequest(0x51u, stencilExport, DepthStencilRefExportCode)).spirv.Words());
Require(depthOnly == "depth ", "an MRTZ stencil channel without STENCIL_TEST_VAL_EXPORT_ENABLE declared the wrong outputs: " + depthOnly);
expectFailure([&] { static_cast<void>(ShaderRecompiler::Recompile(stencilRefExportRequest(0x52u, plain))); }, "VK_EXT_shader_stencil_export");
auto queue = stencilRefExportQueue(0x52u, 2u);
queue.context[0x000] = 0;
queue.context[0x002] = 0;
queue.context[0x007] = (1u << 16u) | 3u;
queue.context[0x00a] = 7;
queue.context[0x00b] = 0;
queue.context[0x010] = 0x22900983;
queue.context[0x011] = 0x20000181;
for (const auto offset : {0x012u, 0x014u}) queue.context[offset] = 0x100;
for (const auto offset : {0x013u, 0x015u}) queue.context[offset] = 0x200;
queue.context[0x10b] = 0x00030030;
queue.context[0x10c] = 0x05ffff02;
queue.context[0x10d] = 0x05ffff02;
queue.context[0x200] = 0x00200211;
auto state = AgcDriver::Graphics::DecodeState(queue);
Require(state.stencilTest && state.stencilFront.compareOp == VK_COMPARE_OP_EQUAL && state.stencilFront.passOp == VK_STENCIL_OP_REPLACE, "a stencil reference export changed the REPLACE_TEST decode");
for (const auto ops : {0x00020020u, 0x00040040u, 0x00000200u, 0x00000004u}) {
queue.context[0x10b] = ops;
expectFailure([&] { static_cast<void>(AgcDriver::Graphics::DecodeState(queue)); }, "exports the stencil test value");
}
queue.context[0x10b] = 0x00050050;
queue.context[0x10c] = 0x01ffff02;
state = AgcDriver::Graphics::DecodeState(queue);
Require(state.stencilFront.passOp == VK_STENCIL_OP_INCREMENT_AND_CLAMP, "a stencil reference export changed the increment decode");
queue.context[0x000] = 2;
expectFailure([&] { static_cast<void>(AgcDriver::Graphics::DecodeState(queue)); }, "stencil clear with a pixel shader that exports the stencil test value");
queue.context[0x203] = 0x50u;
static_cast<void>(AgcDriver::Graphics::DecodeState(queue));
}
void ProvokingVertexTests() {
using namespace AgcDriver::Graphics;
constexpr std::uint32_t lastVertex = 1u << 19u;
@ -4705,6 +4830,7 @@ int main(int argc, char** argv) {
DepthBoundsBiasTests();
conservativeZExportTests();
orderedPixelShaderTests();
stencilRefExportTests();
ProvokingVertexTests();
ProvokingVertexRenderPassTests();
ConservativeRasterizationTests();

View File

@ -1542,7 +1542,7 @@ void verifyPixelRequestSerialization() {
value.posZ, value.posW, value.frontFace, value.ancillary, value.sampleShading,
value.noPerspective, value.linearCentroid, value.pixelKillEnable, value.depthExportEnable,
value.sampleMaskExportEnable, value.earlyZ, value.executeOnNoop, value.conservativeZExport, value.orderedPixelShader,
value.targetOutputMode, value.targetExportMapping, value.alphaToCoverage);
value.targetOutputMode, value.targetExportMapping, value.alphaToCoverage, value.stencilRefExportEnable);
};
const std::array<std::uint32_t, 1> code{0xbf810000u};
const std::array<std::uint32_t, 3> userData{0x12345678u, 0xabcdef01u, 0x87654321u};
@ -1586,6 +1586,7 @@ void verifyPixelRequestSerialization() {
};
for (std::uint32_t i = 0; i < pixel.interpolatorSettings.size(); ++i) pixel.interpolatorSettings[i] = 0x10101010u + i;
pixel.alphaToCoverage = true;
pixel.stencilRefExportEnable = true;
const std::array<std::array<std::uint8_t, 8>, 3> mappings{{
{0x00u, 0xe4u, 0xc6u, 0x1bu, 0xffu, 0x80u, 0x55u, 0xaau},
{0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u},
@ -1614,6 +1615,10 @@ void verifyPixelRequestSerialization() {
withoutCoverage.context.pixel->alphaToCoverage = false;
RecompileCacheKey::Build(withoutCoverage, replayKey);
require(key != replayKey && RecompileCacheKey::ContextHash(request) != RecompileCacheKey::ContextHash(withoutCoverage), "alpha-to-coverage did not change the static shader identity");
auto withoutStencilRef = request;
withoutStencilRef.context.pixel->stencilRefExportEnable = false;
RecompileCacheKey::Build(withoutStencilRef, replayKey);
require(key != replayKey && RecompileCacheKey::ContextHash(request) != RecompileCacheKey::ContextHash(withoutStencilRef), "the stencil reference export did not change the static shader identity");
}
request.context.pixel.reset();
request.shader.stage = ShaderStage::Vertex;
@ -1625,12 +1630,12 @@ void verifyPixelRequestSerialization() {
minimal.context.waveSize = 64;
minimal.context.pixel = ShaderPixelStageInfo{};
const auto encoded = serializer.Serialize(minimal);
require(requestPrefix(encoded, 8u) == "NVNQQRAAAAA=", "new requests did not use serialization version 16");
require(requestPrefix(encoded, 8u) == "NVNQQREAAAA=", "new requests did not use serialization version 17");
constexpr std::size_t mappingOffset = 8u + 37u + 18u + 163u;
for (std::size_t bytes = 0; bytes < 18u; ++bytes) {
expectFailure([&] { static_cast<void>(serializer.Deserialize(requestPrefix(encoded, mappingOffset + bytes))); }, "truncated data", "a truncated pixel mapping, packing, dual-source or alpha-to-coverage flag was accepted");
for (std::size_t bytes = 0; bytes < 19u; ++bytes) {
expectFailure([&] { static_cast<void>(serializer.Deserialize(requestPrefix(encoded, mappingOffset + bytes))); }, "truncated data", "a truncated pixel mapping, packing, dual-source, alpha-to-coverage or stencil reference export flag was accepted");
}
for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQREAAAA="}) {
for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQRIAAAA="}) {
expectFailure([&] { static_cast<void>(serializer.Deserialize(unsupported)); }, "serialization version", "an unsupported request version was accepted");
}
}
@ -1720,6 +1725,7 @@ void verifyLegacyPixelRequests() {
require(pixel.targetExportPacking == std::array<ColorExportPacking, 8>{}, "a legacy request packed a color export");
require(!pixel.dualSourceBlend, "a legacy request enabled dual-source blending");
require(!pixel.alphaToCoverage, "a legacy request enabled alpha-to-coverage");
require(!pixel.stencilRefExportEnable, "a legacy request exported a stencil reference");
require(pixel.inputAddr == 2u && pixel.hasPerspectiveCenterVgpr && pixel.targetOutputMode[0] == 9u, "legacy pixel layout was misread");
require(pixel.conservativeZExport == (version >= 7u ? ConservativeZExport::GreaterThanZ : ConservativeZExport::AnyZ), "legacy conservative Z layout was misread");
require(!pixel.orderedPixelShader, "a legacy request became a primitive-ordered pixel shader");

View File

@ -0,0 +1,159 @@
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp"
#include "Recompiler.hpp"
#include "VulkanTestDevice.hpp"
#include <array>
#include <cstdio>
#include <functional>
#include <iostream>
#include <span>
#include <string>
#include <string_view>
#include <vector>
namespace {
using AgcDriver::Graphics::Require;
using ShaderRecompiler::ShaderStage;
constexpr std::uint32_t Width = 64;
constexpr std::uint32_t Height = 16;
constexpr std::uint32_t WaveSize = 64;
constexpr std::byte Kept{0x40};
alignas(256) std::array<std::byte, Width * Height * 4> Pixels{};
alignas(256) std::array<std::byte, 128 * 128 * 4> DepthMemory{};
alignas(256) std::array<std::byte, 256 * 256> StencilMemory{};
alignas(256) constexpr std::array<std::uint32_t, 6> VertexCode{
0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000,
};
alignas(256) constexpr std::array<std::uint32_t, 4> PlainPixelCode{
0x7e0e02f2, 0xf800180f, 0x07070707, 0xbf810000,
};
alignas(256) constexpr std::array<std::uint32_t, 7> ColumnPixelCode{
0x7e040f00, 0x7e0e02f2, 0xf8000082, 0x00000200, 0xf800180f, 0x07070707, 0xbf810000,
};
alignas(256) constexpr std::array<std::uint32_t, 7> RowPixelCode{
0x7e040f01, 0x7e0e02f2, 0xf8000082, 0x00000200, 0xf800180f, 0x07070707, 0xbf810000,
};
alignas(256) constexpr std::array<std::array<float, 4>, 3> Vertices{{
{-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f},
}};
std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) {
const auto address = reinterpret_cast<std::uintptr_t>(data);
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu};
}
ShaderRecompiler::ShaderPixelStageInfo PixelInfo(std::uint32_t shaderControl) {
const AgcDriver::Registers context{
{0x1b3, 0x300u}, {0x1b4, 0x300u}, {0x1b6, 0u}, {0x1c5, 9u}, {0x203, shaderControl},
};
return AgcDriver::Graphics::DecodePixelStageInfo(context, {0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u});
}
void Draw(AgcDriver::VulkanDevice& device, std::span<const std::uint32_t> pixelCode, std::uint32_t shaderControl, const VkStencilOpState& stencil) {
Pixels.fill(Kept);
const auto target = device.Target();
std::vector<std::uint32_t> vertexUserData(4, 0u);
const auto vertexBuffer = BufferDescriptor(Vertices.data(), 16u, static_cast<std::uint32_t>(Vertices.size()));
std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin());
const std::array<ShaderRecompiler::MemoryRegion, 1> vertexMemory{{{reinterpret_cast<std::uintptr_t>(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}};
ShaderRecompiler::RecompileRequest vertex{
{ShaderStage::Vertex, reinterpret_cast<std::uintptr_t>(VertexCode.data()), VertexCode, 0, {}},
{WaveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory},
target,
{0, 0, 0, 64}
};
vertex.useCache = false;
const auto vertexResult = ShaderRecompiler::Recompile(vertex);
const auto vertexPush = static_cast<std::uint32_t>(vertexResult.pushConstants.size());
const std::vector<std::uint32_t> pixelUserData(8, 0u);
const std::array<ShaderRecompiler::MemoryRegion, 1> pixelMemory{{{reinterpret_cast<std::uintptr_t>(pixelCode.data()), std::as_bytes(pixelCode)}}};
ShaderRecompiler::RecompileRequest fragment{
{ShaderStage::Fragment, reinterpret_cast<std::uintptr_t>(pixelCode.data()), pixelCode, 0, {}},
{WaveSize, 0, pixelUserData, std::nullopt, PixelInfo(shaderControl), std::nullopt, pixelMemory},
target,
PixelPushLayout(vertexPush, target)
};
fragment.useCache = false;
const auto pixelResult = ShaderRecompiler::Recompile(fragment);
const std::array<AgcDriver::Graphics::CompiledShader, 2> shaders{{
{ShaderStage::Vertex, &vertexResult, 0},
{ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)}
}};
AgcDriver::Graphics::State state{};
state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, WaveSize, WaveSize, std::nullopt, std::nullopt};
state.color = {reinterpret_cast<std::uintptr_t>(Pixels.data()), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, Pixels.size(), 0xe4u};
state.colors = {state.color};
state.hasColorTarget = true;
state.depth = AgcDriver::Graphics::DepthTarget{reinterpret_cast<std::uintptr_t>(DepthMemory.data()), reinterpret_cast<std::uintptr_t>(StencilMemory.data()), {Width, Height}, VK_FORMAT_D32_SFLOAT_S8_UINT, 1.0f, 0};
state.stencilTest = true;
state.stencilFront = stencil;
state.stencilBack = stencil;
state.renderExtent = {Width, Height};
state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
state.viewport = {0, static_cast<float>(Height), static_cast<float>(Width), -static_cast<float>(Height), 0, 1};
state.negativeOneToOne = false;
state.scissor = {{0, 0}, {Width, Height}};
state.cullMode = VK_CULL_MODE_NONE;
state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
state.blend.colorWriteMask = 15;
state.blends = {state.blend};
state.blendConstants = {};
const AgcDriver::Pm4::DrawParameters draw{0, static_cast<std::uint32_t>(Vertices.size()), 0, 1, 0, false};
device.Draw(state, draw, shaders);
device.WaitIdle();
}
void Check(const std::function<bool(std::uint32_t, std::uint32_t)>& drawn, std::string_view what) {
for (std::uint32_t y = 0; y < Height; ++y) {
for (std::uint32_t x = 0; x < Width; ++x) {
const auto expected = drawn(x, y) ? std::byte{255} : Kept;
const auto actual = Pixels[(y * Width + x) * 4u];
Require(actual == expected, std::string(what) + ": pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") is " + std::to_string(static_cast<unsigned>(actual)) + ", expected " + (drawn(x, y) ? "drawn" : "stencil-rejected"));
}
}
}
}
int main() {
try {
const auto device = OpenVulkanTestDevice();
if (!device) return VulkanTestSkipped;
if (!TargetHasCapability(device->Target(), spv::CapabilityStencilExportEXT)) {
std::puts("skipped, the device lacks VK_EXT_shader_stencil_export");
return VulkanTestSkipped;
}
constexpr std::uint32_t exported = 0x52u;
constexpr std::uint32_t plain = 0x50u;
const VkStencilOpState replace{VK_STENCIL_OP_KEEP, VK_STENCIL_OP_REPLACE, VK_STENCIL_OP_KEEP, VK_COMPARE_OP_ALWAYS, 0xffu, 0xffu, 0u};
const auto equal = [](std::uint32_t reference) { return VkStencilOpState{VK_STENCIL_OP_KEEP, VK_STENCIL_OP_KEEP, VK_STENCIL_OP_KEEP, VK_COMPARE_OP_EQUAL, 0xffu, 0u, reference}; };
Draw(*device, ColumnPixelCode, exported, replace);
Check([](std::uint32_t, std::uint32_t) { return true; }, "the draw that replaces the stencil with the exported column");
for (const auto column : {0u, 5u, 37u, 63u}) {
Draw(*device, PlainPixelCode, plain, equal(column));
Check([&](std::uint32_t x, std::uint32_t) { return x == column; }, "stencil equal to " + std::to_string(column) + " after the exported column was stored");
}
Draw(*device, ColumnPixelCode, exported, equal(200u));
Check([](std::uint32_t, std::uint32_t) { return true; }, "the exported column tested against the stored column");
Draw(*device, RowPixelCode, exported, equal(200u));
Check([](std::uint32_t x, std::uint32_t y) { return x == y; }, "the exported row tested against the stored column");
Draw(*device, ColumnPixelCode, plain, equal(9u));
Check([](std::uint32_t x, std::uint32_t) { return x == 9u; }, "a column export without STENCIL_TEST_VAL_EXPORT_ENABLE");
std::puts("stencil reference export tests passed");
return 0;
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
}

View File

@ -177,6 +177,7 @@ private:
append(key, value.conservativeZExport);
append(key, value.orderedPixelShader);
append(key, value.alphaToCoverage);
append(key, value.stencilRefExportEnable);
append(key, value.targetOutputMode);
}

View File

@ -325,6 +325,7 @@ void writePixelInfo(Writer& writer, const ShaderPixelStageInfo& info) {
}
writer.WriteBool(info.dualSourceBlend);
writer.WriteBool(info.alphaToCoverage);
writer.WriteBool(info.stencilRefExportEnable);
}
ShaderPixelStageInfo readPixelInfo(Reader& reader, std::uint32_t version) {
@ -373,6 +374,7 @@ ShaderPixelStageInfo readPixelInfo(Reader& reader, std::uint32_t version) {
}
if (version >= 15u) info.dualSourceBlend = reader.ReadBool();
if (version >= 16u) info.alphaToCoverage = reader.ReadBool();
if (version >= 17u) info.stencilRefExportEnable = reader.ReadBool();
if (version < 5u) {
const auto input = [](PixelInput value, bool present) { return present ? PixelInputBit(value) : 0u; };
info.inputAddr = input(PixelInput::PerspectiveSample, info.hasPerspectiveCenterVgpr && inputAddrOrCenterVgpr == 2u) | input(PixelInput::PerspectiveCenter, info.hasPerspectiveCenterVgpr) |
@ -734,7 +736,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const
std::string buffer;
Writer writer(buffer);
writer.WriteU32(0x41505335u);
writer.WriteU32(16u);
writer.WriteU32(17u);
writeShaderBinary(writer, request.shader);
writeGuestContext(writer, request.context);
writeSpirvTarget(writer, request.target);
@ -762,7 +764,7 @@ DeserializedRequest RequestSerializer::Deserialize(std::string_view text) const
Reader reader(decoded);
if (reader.ReadU32() != 0x41505335u) throw std::runtime_error("invalid recompile request signature");
const auto version = reader.ReadU32();
if (version < 1u || version > 16u) throw std::runtime_error("unsupported recompile request serialization version");
if (version < 1u || version > 17u) throw std::runtime_error("unsupported recompile request serialization version");
DeserializedRequest result{};
result.request.shader = readShaderBinary(reader, result.shaderCode, result.shaderHeader);
result.request.context = readGuestContext(reader, result, version);

View File

@ -51,6 +51,7 @@ enum class StageOutputKind {
Mrt,
Depth,
SampleMask,
StencilRef,
PointSize,
ClipDistance,
CullDistance,

View File

@ -167,6 +167,7 @@ struct ShaderPixelInputInfo {
bool psPixelKillEnable = false;
bool psDepthExportEnable = false;
bool psSampleMaskExportEnable = false;
bool psStencilRefExportEnable = false;
bool psSampleShading = false;
bool psEarlyZ = false;
bool psExecuteOnNoop = false;

View File

@ -408,6 +408,9 @@ void CollectOutputs(const IrProgram& program, ShaderStageInputInfo inputInfo, Sh
if (program.Resources().stage == IrShaderStage::Pixel && (exportInfo.en & 0x4u) != 0u && inputInfo.pixel->psSampleMaskExportEnable) {
AddOutput(info, StageOutputKind::SampleMask, 0, 0, "gl_SampleMask");
}
if (program.Resources().stage == IrShaderStage::Pixel && (exportInfo.en & 0x2u) != 0u && inputInfo.pixel->psStencilRefExportEnable) {
AddOutput(info, StageOutputKind::StencilRef, 0, 0, "gl_FragStencilRefARB");
}
continue;
}
if (exportInfo.en == 0u) {

View File

@ -141,6 +141,7 @@ struct ShaderPixelStageInfo {
std::array<ColorExportPacking, 8> targetExportPacking;
bool dualSourceBlend;
bool alphaToCoverage;
bool stencilRefExportEnable;
};
struct ShaderVertexBufferResource {

View File

@ -171,6 +171,7 @@ struct SpirvEmitterState {
std::uint32_t viewportIndexVariable = 0;
std::uint32_t depthVariable = 0;
std::uint32_t sampleMaskVariable = 0;
std::uint32_t stencilRefVariable = 0;
std::vector<SpirvInputBinding> inputs;
std::vector<SpirvOutputBinding> outputs;
std::vector<std::uint32_t> interfaceVariables;

View File

@ -359,6 +359,9 @@ void DefineOutputs(SpirvEmitterState& state) {
case StageOutputKind::SampleMask:
binding.variableId = BuiltIn(state.sampleMaskVariable, SampleMaskArrayType(state), "gl_SampleMask", spv::BuiltInSampleMask);
break;
case StageOutputKind::StencilRef:
binding.variableId = BuiltIn(state.stencilRefVariable, TypeI32(state), "gl_FragStencilRefARB", spv::BuiltInFragStencilRefEXT);
break;
case StageOutputKind::Parameter:
case StageOutputKind::Mrt: {
const bool uintOutput = binding.kind == StageOutputKind::Mrt && state.program.Resources().stage == IrShaderStage::Pixel && binding.index < std::size(PixelInfo(state).targetOutputMode) && PixelInfo(state).targetOutputMode[binding.index] == 7u;

View File

@ -185,7 +185,15 @@ void DefineModule(SpirvEmitterState& state) {
state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeDepthGreater);
}
}
if (pixel.psEarlyZ && !pixel.psPixelKillEnable && !pixel.psDepthExportEnable && !pixel.psSampleMaskExportEnable && !pixel.psAlphaToCoverage) {
if (state.stencilRefVariable != 0) {
if (std::find(state.supportedCapabilities.begin(), state.supportedCapabilities.end(), static_cast<std::uint32_t>(spv::CapabilityStencilExportEXT)) == state.supportedCapabilities.end()) {
throw std::runtime_error("a pixel shader that exports the stencil reference needs VK_EXT_shader_stencil_export, which the device lacks");
}
state.module.EmitCapability(spv::CapabilityStencilExportEXT);
state.module.EmitExtension("SPV_EXT_shader_stencil_export");
state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeStencilRefReplacingEXT);
}
if (pixel.psEarlyZ && !pixel.psPixelKillEnable && !pixel.psDepthExportEnable && !pixel.psSampleMaskExportEnable && !pixel.psStencilRefExportEnable && !pixel.psAlphaToCoverage) {
state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeEarlyFragmentTests);
}
if (pixel.psOrderedPixelShader) {

View File

@ -1298,6 +1298,12 @@ void EmitSetAttribute(SpirvValueEmitContext& ctx, const IrValue& inst) {
state.module.AddFunction(spv::OpBitcast, TypeF32(state), f32, raw);
state.module.AddFunction(spv::OpStore, state.depthVariable, f32);
}
if ((exp.en & 2u) != 0u && state.stencilRefVariable != 0u) {
const auto raw = ExportRawComponent(ctx, data, 1);
const auto value = state.module.AllocateId();
state.module.AddFunction(spv::OpBitcast, TypeI32(state), value, raw);
state.module.AddFunction(spv::OpStore, state.stencilRefVariable, value);
}
if ((exp.en & 4u) != 0u && state.sampleMaskVariable != 0u) {
const auto raw = ExportRawComponent(ctx, data, 2);
const auto value = state.module.AllocateId();

View File

@ -136,6 +136,7 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues
pixelStorage.psPixelKillEnable = pixel.pixelKillEnable;
pixelStorage.psDepthExportEnable = pixel.depthExportEnable;
pixelStorage.psSampleMaskExportEnable = pixel.sampleMaskExportEnable;
pixelStorage.psStencilRefExportEnable = pixel.stencilRefExportEnable;
pixelStorage.psSampleShading = pixel.sampleShading;
pixelStorage.psEarlyZ = pixel.earlyZ;
pixelStorage.psExecuteOnNoop = pixel.executeOnNoop;

View File

@ -541,6 +541,7 @@ Throughout the project, every function at every stage either **does exactly what
- The length-changing path of the [instruction rewriter](../../core/relinker/codegen/src/x86/X64InstructionRewriter.cpp) is not used by the [converter](../../core/relinker/codegen/src/Amd64OnlyConverter.cpp): it does not adjust data-to-code references (relocations, FDEs, jump tables) or segment sizes, so every substitution keeps the instruction length.
- In executables with a `PT_SCE_DYNLIBDATA` segment, the PS4 layout, the executable segment also holds read-only data after the code (CUSA00900: 37 MiB after its last function). The [relinker pipeline](../../core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp) therefore scans for syscalls and import call sites only up to the end of the PLT that follows the last function of the unwind table; code after the PLT without an unwind entry is not scanned. `--to-intel` and the `unused-filter=1` and `unused-filter=2` analyses are not limited this way.
- [DB_SHADER_CONTROL.CONSERVATIVE_Z_EXPORT](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp) (libSceAgcDriver) becomes `DepthLess`/`DepthGreater`. For a pixel shader that breaks the promise, the host driver decides whether its interpolated Z is depth-tested before shading; what the console does then was not measured.
- [DB_SHADER_CONTROL.STENCIL_TEST_VAL_EXPORT_ENABLE](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) becomes `FragStencilRefEXT`, taken from the second MRTZ channel of the 32_GR and 32_ABGR formats as Mesa's AMD drivers write it ([`emit_ps_mrtz_export`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/3e32b0e1f132231b7a972c1b0b5b06c7e16cf8b9/src/amd/common/nir/ac_nir_lower_ps_late.c#L194)); the console was not measured. The host value is the test value of both faces and what `REPLACE_TEST` writes, so a stencil operation that writes all ones or the operation value throws with it, as do a stencil clear draw, the UINT16_ABGR layout, `STENCIL_OP_VAL_EXPORT_ENABLE` and a device without `VK_EXT_shader_stencil_export` (NVIDIA's driver 610.57 offers none).
- [DB_SHADER_CONTROL.PRIMITIVE_ORDERED_PIXEL_SHADER](../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - the host pixel interlock spans the whole shader, not only up to `s_sendmsg` ORDERED_PS_DONE.
- [POPS collision SGPRs](../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - the collision wave ID and intrawave collision mask read 0, so a title skips its POPS wait and runs its per-collision loop once.
- [Image loads](../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) (shader recompiler) - an image_load reads a sampled binding, also of an image the shader stores to, so in a primitive-ordered pixel shader it misses an earlier overlapping primitive's image_store in the same draw.

View File

@ -13,6 +13,7 @@ LIMITATIONS = [
"skipped, the device lacks fragmentShaderPixelInterlock",
"skipped, the device lacks geometryShader",
"skipped, the device lacks shaderSubgroupClock",
"skipped, the device lacks VK_EXT_shader_stencil_export",
"skipped, the device does not import guest memory",
"skipped, the device imports no host memory",
"skipped, the device flushes f64 subnormals without DenormPreserve",