feat(agc): accept conservative Z export in DB_SHADER_CONTROL

DecodeState, the draw precheck and DecodePixelStageInfo rejected every
nonzero CONSERVATIVE_Z_EXPORT (DB_SHADER_CONTROL bits 13-14).
EXPORT_LESS_THAN_Z (1) and EXPORT_GREATER_THAN_Z (2) promise that the
exported Z stays on one side of the interpolated Z. The value now
reaches the recompiler as a pixel stage field, and a pixel shader that
writes FragDepth declares DepthLess or DepthGreater next to
DepthReplacing, the SPIR-V modes RADV maps to these values. Vulkan lets
those modes only add a depth test of the interpolated Z before shading,
so a shader that keeps the promise renders the same. The reserved value
3 still throws.

The field is part of the recompile cache key, the draw key and the
serialized request (version 7), and ValidateShaders accepts both modes.
This commit is contained in:
Celegans12 2026-10-06 00:35:43 +02:00
parent 681f83e51e
commit 4a67b140fc
13 changed files with 117 additions and 7 deletions

View File

@ -62,6 +62,7 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptr<Vulk
for (std::uint32_t i = 0; i < pixel.interpolatorCount; ++i) mix(pixel.interpolatorSettings[i]);
mix(pixel.inputAddr);
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));
for (const auto value : pixel.targetOutputMode) mix(value);
for (const auto value : pixel.targetExportMapping) mix(value);
std::lock_guard cacheLock(drawCacheMutex);

View File

@ -81,7 +81,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.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.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

@ -118,8 +118,9 @@ ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& con
const auto shaderControl = read(context, dbShaderControl, RegisterBank::Context);
// Bits 9 and 11 are EXEC_ON_HIER_FAIL and ALPHA_TO_MASK_DISABLE on GFX10; neither changes what the
// recompiled pixel shader computes.
if (((shaderControl >> 13u) & 0x3u) != 0) {
throw std::runtime_error("AGC graphics: DB_SHADER_CONTROL.CONSERVATIVE_Z_EXPORT is unsupported");
const auto conservativeZExport = (shaderControl >> 13u) & 0x3u;
if (conservativeZExport == 3u) {
throw std::runtime_error("AGC graphics: DB_SHADER_CONTROL.CONSERVATIVE_Z_EXPORT uses the reserved value 3");
}
const auto colFormat = nullProgram ? readOr(context, spiShaderColFormat, RegisterBank::Context, 0u) : read(context, spiShaderColFormat, RegisterBank::Context);
std::array<std::uint8_t, 8> targetOutputMode{};
@ -152,6 +153,7 @@ ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& con
.sampleMaskExportEnable = sampleMaskExportEnable,
.earlyZ = zOrder == 1u && !pixelKillEnable && !depthExportEnable && !sampleMaskExportEnable,
.executeOnNoop = ((shaderControl >> 10u) & 0x1u) != 0,
.conservativeZExport = static_cast<ShaderRecompiler::ConservativeZExport>(conservativeZExport),
.targetOutputMode = targetOutputMode,
.targetExportMapping = exportMappings
};

View File

@ -368,7 +368,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi
mode(spv::ExecutionModeVertexOrderCw, {});
Require(module.modes.size() == 3, "unsupported tessellation-evaluation execution mode");
} else if (fragment) {
for (const auto& [name, operands] : module.modes) Require(operands.empty() && (name == spv::ExecutionModeOriginUpperLeft || name == spv::ExecutionModeEarlyFragmentTests), "unsupported fragment execution mode");
for (const auto& [name, operands] : module.modes) Require(operands.empty() && (name == spv::ExecutionModeOriginUpperLeft || name == spv::ExecutionModeEarlyFragmentTests || name == spv::ExecutionModeDepthLess || name == spv::ExecutionModeDepthGreater), "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

@ -64,7 +64,7 @@ constexpr std::uint32_t LayerExports = (1u << 18u) | (1u << 19u) | (1u << 21u) |
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 = ~(0x00009870u | 0x00020600u);
constexpr std::uint32_t ShaderControlMask = ~(0x0000f870u | 0x00020600u);
constexpr std::uint32_t AlphaToCoverageMask = ~0x0001ff00u;
constexpr std::uint32_t ScanModeMask = ~2u;
constexpr std::uint32_t ScanControlMask = ~0x06003fffu;

View File

@ -6,6 +6,7 @@
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp"
#include "ControlFlow/RequestSerializer.hpp"
#include "CacheKey.hpp"
#include <spirv/unified1/spirv.hpp>
#include <algorithm>
#include <array>
@ -1283,6 +1284,7 @@ void misalignedShaderDataTests() {
struct ModuleShape {
bool fragment = false;
std::optional<std::uint32_t> fragmentMode;
bool push = false;
std::uint32_t pushLength = 32;
std::uint32_t pushStride = 4;
@ -1446,6 +1448,7 @@ std::vector<std::uint32_t> makeModule(const ModuleShape& shape) {
else emit(words, spv::OpEntryPoint, {shape.fragment ? spv::ExecutionModelFragment : spv::ExecutionModelVertex, main, 0x6e69616du, 0, output});
words[entryPointOffset] += static_cast<std::uint32_t>(extraInterface.size()) << 16u;
words.insert(words.end(), extraInterface.begin(), extraInterface.end());
if (shape.fragmentMode) emit(words, spv::OpExecutionMode, {main, *shape.fragmentMode});
if (shape.fragment) emit(words, spv::OpExecutionMode, {main, spv::ExecutionModeOriginUpperLeft});
words.insert(words.end(), annotations.begin(), annotations.end());
words.insert(words.end(), declarations.begin(), declarations.end());
@ -1563,6 +1566,81 @@ std::vector<LocatedInput> locatedInputs(std::span<const std::uint32_t> words) {
return result;
}
constexpr std::array<std::uint32_t, 3> DepthExportCode{0xf8001881u, 2u, 0xbf810000u};
AgcDriver::QueueState depthExportQueue(std::uint32_t shaderControl) {
auto queue = makeState();
queue.context[0x1b3] = 0x402u;
queue.context[0x1b4] = 0x402u;
queue.context[0x203] = shaderControl;
return queue;
}
ShaderRecompiler::RecompileRequest depthExportRequest(std::uint32_t shaderControl) {
const auto queue = depthExportQueue(shaderControl);
ShaderRecompiler::RecompileRequest request{};
request.shader = {ShaderRecompiler::ShaderStage::Fragment, 0x30000u, DepthExportCode, 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.layout.pushConstantSizeBytes = 128;
request.useCache = false;
return request;
}
std::set<std::uint32_t> depthModes(const ShaderRecompiler::RecompileRequest& request) {
const auto result = ShaderRecompiler::Recompile(request);
const auto& words = result.spirv.Words();
std::set<std::uint32_t> modes;
for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) {
if (static_cast<spv::Op>(words[at] & 0xffffu) != spv::OpExecutionMode) continue;
const auto mode = words[at + 2];
if (mode == spv::ExecutionModeDepthReplacing || mode == spv::ExecutionModeDepthGreater || mode == spv::ExecutionModeDepthLess || mode == spv::ExecutionModeDepthUnchanged) modes.insert(mode);
}
return modes;
}
void conservativeZExportTests() {
const std::array<std::pair<std::uint32_t, std::set<std::uint32_t>>, 4> cases{{
{0x0021u, {spv::ExecutionModeDepthReplacing}},
{0x2021u, {spv::ExecutionModeDepthReplacing, spv::ExecutionModeDepthLess}},
{0x4021u, {spv::ExecutionModeDepthReplacing, spv::ExecutionModeDepthGreater}},
{0x2020u, {}}
}};
const ShaderRecompiler::RequestSerializer serializer;
std::set<std::vector<std::uint64_t>> keys;
for (const auto& [shaderControl, expected] : cases) {
const auto request = depthExportRequest(shaderControl);
const auto replayed = serializer.Deserialize(serializer.Serialize(request));
Require(depthModes(request) == expected && depthModes(replayed.request) == expected, "DB_SHADER_CONTROL " + std::to_string(shaderControl) + " declared the wrong depth execution modes");
std::vector<std::uint64_t> key;
ShaderRecompiler::RecompileCacheKey::Build(request, key);
keys.insert(key);
}
Require(keys.size() == cases.size(), "the recompile cache key ignores CONSERVATIVE_Z_EXPORT");
for (const auto shaderControl : {0x2020u, 0x4020u}) {
const auto queue = depthExportQueue(shaderControl);
static_cast<void>(AgcDriver::Graphics::DecodeState(queue));
Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck rejected DB_SHADER_CONTROL " + std::to_string(shaderControl));
}
expectFailure([] { static_cast<void>(depthExportRequest(0x6021u)); }, "CONSERVATIVE_Z_EXPORT");
AgcDriver::Graphics::State state{};
state.stages.path = AgcDriver::Graphics::ShaderPath::Vertex;
ShaderRecompiler::RecompileResult vertex;
vertex.spirv = makeModule({});
ShaderRecompiler::RecompileResult pixel;
const std::array<AgcDriver::Graphics::CompiledShader, 2> shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &pixel, 0}}};
const VkPhysicalDeviceSubgroupProperties subgroup{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES};
for (const auto mode : {spv::ExecutionModeDepthLess, spv::ExecutionModeDepthGreater}) {
pixel.spirv = makeModule({.fragment = true, .fragmentMode = mode});
AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false);
}
pixel.spirv = makeModule({.fragment = true, .fragmentMode = spv::ExecutionModeDepthUnchanged});
expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false); }, "unsupported fragment execution mode");
}
void pixelParameterSlotTests() {
using AgcDriver::Graphics::CompiledShader;
const VkPhysicalDeviceSubgroupProperties subgroup{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES};
@ -1922,6 +2000,7 @@ int main() {
DepthClipTests();
DepthStencilTests();
DepthBoundsBiasTests();
conservativeZExportTests();
DisabledColorTests();
CompactedExportTests();
metadataPassTests();

View File

@ -140,6 +140,7 @@ private:
append(key, value.sampleMaskExportEnable);
append(key, value.earlyZ);
append(key, value.executeOnNoop);
append(key, value.conservativeZExport);
append(key, value.targetOutputMode);
append(key, value.targetExportMapping);
}

View File

@ -227,6 +227,14 @@ ProgramRole readProgramRole(Reader& reader) {
return static_cast<ProgramRole>(value);
}
ConservativeZExport readConservativeZExport(Reader& reader) {
const auto value = reader.ReadU8();
if (value > static_cast<std::uint8_t>(ConservativeZExport::GreaterThanZ)) {
throw std::runtime_error("RequestSerializer: invalid ConservativeZExport value");
}
return static_cast<ConservativeZExport>(value);
}
void writeShaderBinary(Writer& writer, const ShaderBinary& binary) {
writer.WriteU8(static_cast<std::uint8_t>(binary.stage));
writer.WriteU64(binary.codeAddress);
@ -296,6 +304,7 @@ void writePixelInfo(Writer& writer, const ShaderPixelStageInfo& info) {
writer.WriteBool(info.sampleMaskExportEnable);
writer.WriteBool(info.earlyZ);
writer.WriteBool(info.executeOnNoop);
writer.WriteU8(static_cast<std::uint8_t>(info.conservativeZExport));
for (const std::uint8_t value : info.targetOutputMode) {
writer.WriteU8(value);
}
@ -328,6 +337,7 @@ ShaderPixelStageInfo readPixelInfo(Reader& reader, std::uint32_t version) {
info.sampleMaskExportEnable = reader.ReadBool();
info.earlyZ = reader.ReadBool();
info.executeOnNoop = reader.ReadBool();
if (version >= 7u) info.conservativeZExport = readConservativeZExport(reader);
for (std::uint8_t& value : info.targetOutputMode) {
value = reader.ReadU8();
}
@ -677,7 +687,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const
std::string buffer;
Writer writer(buffer);
writer.WriteU32(0x41505335u);
writer.WriteU32(6u);
writer.WriteU32(7u);
writeShaderBinary(writer, request.shader);
writeGuestContext(writer, request.context);
writeSpirvTarget(writer, request.target);
@ -701,7 +711,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 > 6u) throw std::runtime_error("unsupported recompile request serialization version");
if (version < 1u || version > 7u) 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

@ -2,6 +2,7 @@
#define CORE_SHADER_RECOMPILIER_OPTIMIZATION_INCLUDE_OPTIMIZATION_SHADERSTAGEINPUTINFO_HPP
#include "IntermediateRepresentation/IrMetadata.hpp"
#include "Recompiler.hpp"
#include <array>
#include <cstdint>
#include <limits>
@ -183,6 +184,7 @@ struct ShaderPixelInputInfo {
bool psSampleShading = false;
bool psEarlyZ = false;
bool psExecuteOnNoop = false;
ConservativeZExport psConservativeZExport = ConservativeZExport::AnyZ;
ShaderStageRuntime stage;
bool HasPositionInput() const {

View File

@ -98,6 +98,12 @@ constexpr std::uint32_t PixelInputVgpr(std::uint32_t inputAddr, PixelInput input
return vgpr;
}
enum class ConservativeZExport : std::uint8_t {
AnyZ,
LessThanZ,
GreaterThanZ
};
struct ShaderPixelStageInfo {
std::uint32_t interpolatorCount;
std::array<std::uint32_t, 32> interpolatorSettings;
@ -119,6 +125,7 @@ struct ShaderPixelStageInfo {
bool sampleMaskExportEnable;
bool earlyZ;
bool executeOnNoop;
ConservativeZExport conservativeZExport;
std::array<std::uint8_t, 8> targetOutputMode;
std::array<std::uint8_t, 8> targetExportMapping;
};

View File

@ -197,6 +197,12 @@ void DefineModule(SpirvEmitterState& state) {
state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeOriginUpperLeft);
if (state.depthVariable != 0) {
state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeDepthReplacing);
if (pixel.psConservativeZExport == ConservativeZExport::LessThanZ) {
state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeDepthLess);
}
if (pixel.psConservativeZExport == ConservativeZExport::GreaterThanZ) {
state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeDepthGreater);
}
}
if (pixel.psEarlyZ && !pixel.psPixelKillEnable && !pixel.psDepthExportEnable && !pixel.psSampleMaskExportEnable) {
state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeEarlyFragmentTests);

View File

@ -137,6 +137,7 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues
pixelStorage.psSampleShading = pixel.sampleShading;
pixelStorage.psEarlyZ = pixel.earlyZ;
pixelStorage.psExecuteOnNoop = pixel.executeOnNoop;
pixelStorage.psConservativeZExport = pixel.conservativeZExport;
ShaderStageInputInfo result;
result.pixel = &pixelStorage;
return result;

View File

@ -114,6 +114,7 @@ Throughout the project, every function at every stage either **does exactly what
- [v_rcp, v_rsq, v_sqrt, v_log, v_exp, v_sin and v_cos](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (f32 and f16) use the driver's approximation for finite results, not the console's: the low bits can differ. Only NaN, invalid, zero and infinite results and the exact sin/cos zeros are chosen on the bits. v_sin_f32/v_cos_f32 with inputs beyond ±256 cycles were not compared with the console.
- `--to-intel` does not lower RDPRU/MCOMMIT; the [matcher](../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp) fails the relink instead. SHA-1 and SHA-256 instructions with a RIP-relative operand fail the relink.
- 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 VEX/0F38/0F3A RIP-relative operands, data-to-code references (relocations, FDEs, jump tables) or segment sizes, so every substitution keeps the instruction length.
- [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.
- The Linux placement of the `--to-intel` stubs in the [ELF patcher](../../core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp) is covered only by a synthetic test.
- The libc [SSE4a trap emulation](../../core/libs/prx/libc/src/specifics/windows/Sse4aEmulation.hpp) on Windows is superseded by `--to-intel` and remains only until the relinked title has been verified without it.
- `--to-intel` guest module trampolines are covered only by a synthetic relinker test; no game title has been verified with them on Linux or Windows.