Merge pull request #4650 from Mouad-Scriptz-0/feat/agc-depth-range-remap

feat(agc): remap a viewport depth range the device cannot express into the vertex shader
This commit is contained in:
Dean authored and GitHub committed 2026-10-19 16:52:31 +13:00
commit 3ddbc4accc
35 files changed
+770 -38

No files matched your search

@@ -1777,6 +1777,8 @@ target_include_directories(agc_driver_color1555_target_tests PRIVATE $<TARGET_PR
target_include_directories(agc_driver_color565_target_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>)
target_include_directories(agc_driver_depth_range_remap_draw_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>)
foreach(stage IN ITEMS vert frag)
set(agcProvokingShaderSource ${CMAKE_CURRENT_SOURCE_DIR}/tests/shaders/ProvokingVertex.${stage})
set(agcProvokingShaderSpv ${agcTestShaderGeneratedRoot}/ProvokingVertex_${stage}.spv)
@@ -2834,6 +2836,20 @@ if(BUILD_TESTING)
endif()
endif()
add_test_executable(agc_depth_range_remap_tests EXCLUDE_FROM_ALL tests/DepthRangeRemap.cpp)
target_link_libraries(agc_depth_range_remap_tests PRIVATE shader_recompiler_agc_driver libc)
set_target_properties(agc_depth_range_remap_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_depth_range_remap_tests)
if(BUILD_TESTING)
set_target_properties(agc_depth_range_remap_tests PROPERTIES EXCLUDE_FROM_ALL OFF)
add_test(NAME agc_depth_range_remap COMMAND agc_depth_range_remap_tests)
set_tests_properties(agc_depth_range_remap PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1")
if(WIN32)
set_tests_properties(agc_depth_range_remap PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endif()
add_test_executable(agc_position_invariance_tests EXCLUDE_FROM_ALL tests/PositionInvariance.cpp)
target_link_libraries(agc_position_invariance_tests PRIVATE shader_recompiler_agc_driver libc)
set_target_properties(agc_position_invariance_tests PROPERTIES CXX_EXTENSIONS OFF)
@@ -160,8 +160,8 @@ private:
void reportDrawRelocation();
static bool sameVertexInfo(const ShaderRecompiler::ShaderVertexStageInfo& a, const ShaderRecompiler::ShaderVertexStageInfo& b);
static bool sameDecode(const DrawDecode& a, const DrawDecode& b);
std::shared_ptr<DrawDecode> decodeDraw(const QueueState& queue, const Submission& submission);
void resolveDrawDecode(const QueueState& queue, const Submission& submission, std::shared_ptr<const DrawDecode>& decode, bool registerKey, std::uint64_t drawKey, bool profile);
std::shared_ptr<DrawDecode> decodeDraw(const QueueState& queue, const Submission& submission, const Graphics::Context& context);
void resolveDrawDecode(const QueueState& queue, const Submission& submission, const Graphics::Context& context, std::shared_ptr<const DrawDecode>& decode, bool registerKey, std::uint64_t drawKey, bool profile);
void lookupDraw(const Submission& submission, const std::shared_ptr<VulkanDevice>& localDevice, const Graphics::State& graphics, const ShaderRecompiler::ShaderPixelStageInfo& pixel, const std::vector<DrawProgram>& programs, const std::vector<ShaderRecompiler::ProgramRole>& roles, const std::vector<std::optional<ShaderRecompiler::ShaderVertexStageInfo>>& vertexInfos, bool useDrawEntries, bool registerKey, bool profile, std::uint64_t& drawKey, std::shared_ptr<DrawEntry>& entry, std::vector<std::shared_ptr<DispatchVariant>>& matched, std::vector<std::vector<ShaderRecompiler::MemoryRegion>>& matchedRegions, bool& drawHit, bool& verifyHit, DrawPhaseTiming& phaseTiming, std::array<double, DrawDriverPhaseCount>& phaseMs, const std::shared_ptr<DrawEntry>& dataEntry, DrawDataCandidates& dataCandidates, std::vector<StageDataHit>& stageData);
std::shared_ptr<const ShaderRecompiler::RecompileResult> materializeDrawStage(std::size_t i, std::uint32_t pushOffset, const QueueState& queue, const Submission& submission, const std::vector<DrawProgram>& programs, const Graphics::State& graphics, const ShaderRecompiler::ShaderPixelStageInfo& pixel, const std::vector<std::optional<ShaderRecompiler::ShaderVertexStageInfo>>& vertexInfos, std::vector<ShaderRecompiler::MemoryRegion>& memory, const std::vector<ShaderRecompiler::LinkedProgram>& linked, const Pm4::DrawParameters& drawParameters, const std::shared_ptr<VulkanDevice>& localDevice, ShaderMemory& shaderMemory, std::vector<StageCapture>& stageCaptures, std::vector<bool>& recompiled, bool drawHit, const std::vector<std::shared_ptr<DispatchVariant>>& matched, const std::vector<std::vector<ShaderRecompiler::MemoryRegion>>& matchedRegions, bool profile, std::uint64_t dumpTarget, std::uint64_t dumpSlot1, std::uint64_t& captures, DrawPhaseTiming& phaseTiming, std::array<double, DrawDriverPhaseCount>& phaseMs, std::string& rejected);
static void recordStageDataPositions(DispatchVariant& variant, const ShaderRecompiler::ResourceCapture& capture, const std::vector<Graphics::DecodeRead>& decodeReads);
@@ -331,9 +331,7 @@ public:
void EnqueueDraw(const Graphics::State& graphics, const Pm4::DrawParameters& draw, std::span<const Graphics::CompiledShader> shaders, std::span<const Graphics::GuestMemorySnapshot> snapshots = {});
bool PrebuildGraphicsLibraries(const Graphics::ShaderStages& stages, std::span<const Graphics::CompiledShader> shaders);
private:
VulkanDevice(const PresentationWindow* window, bool compileOnly);
friend struct VulkanDeviceTestAccess;
public:
// The device's Graphics::Context: a copy of the one built at setup (its instance functions
// resolved then, its function table filled then), or with APS5_NO_CONTEXT_CACHE=1 built anew.
Graphics::Context graphicsContext() const;
@@ -341,6 +339,10 @@ private:
std::uint32_t computeSubgroupSize(const ShaderRecompiler::RecompileResult& shader) const;
VkPipelineShaderStageCreateFlags computeStageFlags(const ShaderRecompiler::RecompileResult& shader) const;
std::shared_ptr<ComputePipelineObjects> createComputePipeline(const Graphics::Context& context, const ShaderRecompiler::RecompileResult& shader, VkDescriptorSetLayout setLayout, std::span<const std::uint32_t> layoutKey, VkShaderStageFlags pushStages, std::uint64_t programAddress);
private:
VulkanDevice(const PresentationWindow* window, bool compileOnly);
friend struct VulkanDeviceTestAccess;
ShaderRecompiler::SpirvTarget buildTarget() const;
Graphics::Context buildContext() const;
// Body of Dispatch and DispatchIndirect: `arguments` 0 dispatches x, y, z groups.
@@ -8,6 +8,7 @@
#include "prx/libSceAgcDriver/Execution/include/Pm4.hpp"
#include "Optimization/ResourceProgram.hpp"
#include <algorithm>
#include <bit>
#include <cstdlib>
#include <cstdio>
#include <cstring>
@@ -162,7 +163,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe
if (program.merged) InitializeMerged(queue, program, program.mergedPointer, program.mergedPointerRequired, false);
}
if (verifyDrawDataHits()) {
const auto full = decodeDraw(queue, submission);
const auto full = decodeDraw(queue, submission, localDevice->graphicsContext());
if (!sameDecode(*refreshed, *full)) {
std::size_t differing = 0;
while (differing < full->programs.size() && differing < refreshed->programs.size() && full->programs[differing].userData == refreshed->programs[differing].userData) ++differing;
@@ -173,7 +174,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe
}
decode = std::move(refreshed);
}
resolveDrawDecode(queue, submission, decode, registerKey, drawKey, profile);
resolveDrawDecode(queue, submission, localDevice->graphicsContext(), decode, registerKey, drawKey, profile);
const auto& graphics = decode->state;
auto pixel = decode->pixel;
const bool writesPrograms = graphics.stages.mesh || drawParameters.indirect || !userPatches.empty();
@@ -246,6 +247,9 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe
decodeReads[i].clear();
vertexInfos[i] = Graphics::DecodeVertexStageInfo(program.binary.header, program.binary.headerAddress, program.userData, &decodeReads[i], false, 8u - program.firstUserSgpr);
if (vertexInfos[i]) vertexInfos[i]->paClVsOutCntl = PositionExportLayout(program.snapshot.get(), program.codeOffset, program.binary.code, readRegister(queue.context, 0x207), program.tailContinuation.get()) | graphics.layerExports;
if (vertexInfos[i]) {
vertexInfos[i]->depthRemapMode = graphics.depthRemap.mode;
}
};
if (!registerKey) {
for (std::size_t i = 0; i < programs.size(); ++i) decodeVertexInfo(i);
@@ -23,7 +23,7 @@ std::shared_ptr<const ShaderRecompiler::RecompileResult> Driver::materializeDraw
program.binary,
{waveSize, program.firstUserSgpr, program.userData, std::nullopt, program.binary.stage == Stage::Fragment ? std::optional(pixel) : std::nullopt, vertexInfos[i], memory, RegisteredFloatMode(*program.snapshot)},
localDevice->Target(),
{0, 0, pushOffset, (graphics.stages.mesh ? ShaderRecompiler::MeshDrawPushOffsetBytes : graphics.stages.tessellation && pushOffset < Graphics::PipelinePushSlotBytes ? ShaderRecompiler::TessellationLevelPushOffsetBytes : Graphics::PipelinePushSlotBytes) - pushOffset % Graphics::PipelinePushSlotBytes},
{0, 0, pushOffset, (graphics.stages.mesh ? ShaderRecompiler::MeshDrawPushOffsetBytes : graphics.stages.tessellation && pushOffset < Graphics::PipelinePushSlotBytes ? ShaderRecompiler::TessellationLevelPushOffsetBytes : graphics.depthRemap.mode != 0u ? ShaderRecompiler::DepthRemapPushOffsetBytes : Graphics::PipelinePushSlotBytes) - pushOffset % Graphics::PipelinePushSlotBytes},
ShaderRecompiler::GraphicsCompileContext{program.firstUserSgpr, linked, graphics.stages.mesh, graphics.stages.tessellation, {drawParameters.indexAddress, drawParameters.indexCount, drawParameters.indexSize, drawParameters.instanceCount}}
};
const auto waitedBefore = traceCapSync() || profile ? Graphics::Recorder::ThreadWaitedMs() : 0.0;
@@ -122,9 +122,10 @@ void DecodeGraphicsPrograms(DrawDecode& decoded, const QueueState& queue, const
}
}
std::shared_ptr<DrawDecode> Driver::decodeDraw(const QueueState& queue, const Submission& submission) {
std::shared_ptr<DrawDecode> Driver::decodeDraw(const QueueState& queue, const Submission& submission, const Graphics::Context& context) {
auto product = std::make_shared<DrawDecode>();
product->state = Graphics::DecodeState(queue, VertexWorkgroupRequired(queue, *submission.shaders));
Graphics::ApplyDepthRemap(context, product->state);
DecodeGraphicsPrograms(*product, queue, *submission.shaders, false, true);
product->pixel = Graphics::DecodePixelStageInfo(queue.context, Graphics::ExportMappings(product->state), Graphics::PixelProgramSkipped(queue));
product->pixel.targetExportPacking = Graphics::ExportPackings(product->state);
@@ -146,11 +147,11 @@ std::shared_ptr<DrawDecode> Driver::decodeDraw(const QueueState& queue, const Su
return product;
}
void Driver::resolveDrawDecode(const QueueState& queue, const Submission& submission, std::shared_ptr<const DrawDecode>& decode, bool registerKey, std::uint64_t drawKey, bool profile) {
void Driver::resolveDrawDecode(const QueueState& queue, const Submission& submission, const Graphics::Context& context, std::shared_ptr<const DrawDecode>& decode, bool registerKey, std::uint64_t drawKey, bool profile) {
if (decode == nullptr || verifyDrawRecipe()) {
std::vector<Graphics::RegisterRead> readLog;
const Graphics::RegisterReadLogScope logScope(verifyDrawRecipe() && registerKey ? &readLog : nullptr);
auto fresh = decodeDraw(queue, submission);
auto fresh = decodeDraw(queue, submission, context);
if (verifyDrawRecipe() && registerKey) {
std::uint64_t facadeMismatches = 0;
for (const auto read : readLog) {
@@ -214,6 +214,7 @@ void Driver::traceDrawKeyLookup(const std::array<std::uint64_t, 5>& components,
bool Driver::sameVertexInfo(const ShaderRecompiler::ShaderVertexStageInfo& a, const ShaderRecompiler::ShaderVertexStageInfo& b) {
if (a.resourcesNum != b.resourcesNum || a.fetchAttribReg != b.fetchAttribReg || a.fetchBufferReg != b.fetchBufferReg || a.fetchEmbedded != b.fetchEmbedded || a.paClVsOutCntl != b.paClVsOutCntl) return false;
if (a.depthRemapMode != b.depthRemapMode) return false;
for (std::uint32_t i = 0; i < a.resourcesNum && i < a.resources.size(); ++i) {
if (a.resources[i].fields != b.resources[i].fields) return false;
const auto& x = a.resourcesDst[i];
@@ -1331,7 +1331,7 @@ std::vector<PreparedGraphicsStage> PrepareGraphicsStages(const DrawDecode& decod
std::vector<PreparedGraphicsStage> prepared;
std::uint32_t pushOffset = 0;
const bool fixedSlots = target.fixedPushSlots;
const auto capacity = decoded.state.stages.mesh ? ShaderRecompiler::MeshDrawPushOffsetBytes : decoded.state.stages.tessellation ? ShaderRecompiler::TessellationLevelPushOffsetBytes : Graphics::PipelinePushSlotBytes;
const auto capacity = decoded.state.stages.mesh ? ShaderRecompiler::MeshDrawPushOffsetBytes : decoded.state.stages.tessellation ? ShaderRecompiler::TessellationLevelPushOffsetBytes : decoded.state.depthRemap.mode != 0u ? ShaderRecompiler::DepthRemapPushOffsetBytes : Graphics::PipelinePushSlotBytes;
for (std::size_t index = 0; index < decoded.programs.size(); ++index) {
if (decoded.roles[index] == ShaderRecompiler::ProgramRole::GeometryBack) continue;
const auto& program = decoded.programs[index];
@@ -3518,6 +3518,7 @@ ShaderRecompiler::SpirvTarget VulkanDevice::buildTarget() const {
target.float64DenormPreserve = state->float64DenormPreserve;
target.float16DenormPreserve = state->float16DenormPreserve;
target.spannedWaves = state->spannedWaves;
target.depthRangeRemap = !state->depthRangeUnrestricted;
target.fixedPushSlots = state->graphicsPipelineLibrary;
target.registrationModules = state->graphicsPipelineLibrary;
target.robustBufferAccess2 = state->robustBufferAccess2;
@@ -73,7 +73,7 @@ inline VkShaderStageFlags PushConstantStages(std::span<const CompiledShader> sha
VkShaderStageFlags result = 0;
for (const auto& shader : shaders) {
Require(shader.program != nullptr, "missing compiled shader");
if (!shader.program->pushConstants.empty() || shader.stage == ShaderRecompiler::ShaderStage::Mesh || shader.stage == ShaderRecompiler::ShaderStage::TessellationControl) result |= VulkanStage(shader.stage);
if (!shader.program->pushConstants.empty() || shader.program->readsDepthRemapPush || shader.stage == ShaderRecompiler::ShaderStage::Mesh || shader.stage == ShaderRecompiler::ShaderStage::TessellationControl) result |= VulkanStage(shader.stage);
}
return result;
}
@@ -125,6 +125,16 @@ struct State {
VkViewport viewport;
bool negativeOneToOne;
bool depthClamp = false;
float depthScale = 1;
float depthOffset = 0;
float depthClampMin = 0;
float depthClampMax = 1;
struct DepthRemap {
std::uint32_t mode = 0;
float scale = 1;
float offset = 0;
bool operator==(const DepthRemap&) const = default;
} depthRemap;
bool alphaToCoverage = false;
bool ditheredAlphaToCoverage = false;
bool perSampleShading = false;
@@ -150,6 +160,7 @@ inline constexpr std::uint32_t PositionExportLayoutBits = 0xffffu | (1u << 21u)
ShaderStages DecodeShaderStages(const QueueState& queue, bool vertexWorkgroup = false);
State DecodeState(const QueueState& queue, bool vertexWorkgroup = false);
void ApplyDepthRemap(const Context& context, State& state);
std::array<VkSampleLocationEXT, 32> SampleLocations(const State& state);
std::array<std::uint8_t, 32> PixelSampleLocations(const State& state, const ShaderRecompiler::ShaderPixelStageInfo& pixel);
VkExtent2D SampleLocationGrid(const State& state);
@@ -1632,6 +1632,15 @@ struct RecordedDraw {
std::vector<std::pair<std::uint32_t, std::shared_ptr<Pipeline>>> stencilReplay;
};
// A remapped viewport depth range reads its scale and offset from the draw's push block (the vertex shader's
// RemapPositionDepth), so draws that differ only in them share one pipeline; the draw's own state supplies them.
void patchDepthRemapPush(std::array<std::byte, PipelinePushConstantBytes>& block, const State& state) {
if (state.depthRemap.mode == 0u) return;
static_assert(ShaderRecompiler::DepthRemapPushOffsetBytes + ShaderRecompiler::DepthRemapPushBytes <= PipelinePushSlotBytes);
const std::array<float, ShaderRecompiler::DepthRemapPushBytes / 4> words{state.depthRemap.scale, state.depthRemap.offset};
std::memcpy(block.data() + ShaderRecompiler::DepthRemapPushOffsetBytes, words.data(), sizeof(words));
}
void recordStencilReplay(const Context& context, VkCommandBuffer commands, const State& state, const Pm4::DrawParameters& draw, const DrawInputs& inputs, std::span<const std::pair<std::uint32_t, std::shared_ptr<Pipeline>>> replay, std::span<const CompiledShader> shaders, const ShaderResources& resources, const std::array<std::byte, PipelinePushConstantBytes>* bytes = nullptr, VkShaderStageFlags stages = 0) {
State selected = state;
auto block = bytes != nullptr ? *bytes : AssemblePushConstants(shaders);
@@ -1639,6 +1648,7 @@ void recordStencilReplay(const Context& context, VkCommandBuffer commands, const
resources.PatchPushConstants(block);
stages = PushConstantStages(shaders);
}
patchDepthRemapPush(block, state);
const auto fragment = std::ranges::find_if(shaders, [](const CompiledShader& shader) { return shader.stage == ShaderRecompiler::ShaderStage::Fragment && !shader.program->stencilReferences.empty(); });
Require(fragment != shaders.end(), "stencil replay fragment stage is missing");
const auto referenceOffset = fragment->pushConstantOffset + fragment->program->stencilReferenceDword * sizeof(std::uint32_t);
@@ -1676,6 +1686,7 @@ void pushDrawConstants(const Pipeline& pipeline, VkCommandBuffer commands, const
resources.PatchPushConstants(block);
stages = PushConstantStages(shaders);
}
patchDepthRemapPush(block, state);
if (state.stages.tessellation) {
for (const auto& shader : shaders) Require(shader.program->pushConstants.empty() || shader.pushConstantOffset >= PipelinePushSlotBytes || shader.pushConstantOffset + shader.program->pushConstants.size() <= ShaderRecompiler::TessellationLevelPushOffsetBytes, "a stage's push constants overlap the tessellation levels");
const std::array<float, ShaderRecompiler::TessellationLevelPushBytes / 4> levels{state.stages.minTessellationLevel, state.stages.maxTessellationLevel};
@@ -35,7 +35,7 @@ static_assert(sizeof(VkStencilOpState) == 28, "VkStencilOpState changed: update
static_assert(sizeof(VkVertexInputBindingDescription) == 12, "VkVertexInputBindingDescription changed: update the recipe codec");
static_assert(sizeof(VkVertexInputAttributeDescription) == 16, "VkVertexInputAttributeDescription changed: update the recipe codec");
#if defined(__linux__) && defined(__x86_64__) && defined(__GLIBCXX__)
static_assert(sizeof(State) == 752, "State changed: update CanonicalPipelineState and the recipe codec");
static_assert(sizeof(State) == 784, "State changed: update CanonicalPipelineState and the recipe codec");
static_assert(sizeof(ShaderStages) == 112, "ShaderStages changed: update the recipe codec");
static_assert(sizeof(ShaderRecompiler::MeshConfiguration) == 40, "MeshConfiguration changed: update the recipe codec");
static_assert(sizeof(ShaderRecompiler::TessellationConfiguration) == 40, "TessellationConfiguration changed: update the recipe codec");
@@ -44,7 +44,7 @@ static_assert(sizeof(ShaderRecompiler::TessellationConfiguration) == 40, "Tessel
constexpr std::uint32_t FileMagic = 0x4c4a5041u;
// Bumped when the recipe codec, CanonicalPipelineState or the store key change in a way the
// layout guards above do not catch (a field renumbered or reinterpreted at the same size).
constexpr std::uint32_t FileFormat = 3u;
constexpr std::uint32_t FileFormat = 4u;
constexpr std::uint32_t ModuleRecord = 1u;
constexpr std::uint32_t GraphicsRecord = 2u;
constexpr std::uint32_t ComputeRecord = 3u;
@@ -216,6 +216,7 @@ void encodeState(Writer& writer, const State& state) {
writer.Bool(state.sampleLocationGrid);
writer.Bool(state.perSampleShading);
for (const auto mask : state.quadSampleMask) writer.Raw(mask);
writer.U32(state.depthRemap.mode);
}
void decodeState(Reader& reader, State& state) {
@@ -274,6 +275,7 @@ void decodeState(Reader& reader, State& state) {
state.sampleLocationGrid = reader.Bool();
state.perSampleShading = reader.Bool();
for (auto& mask : state.quadSampleMask) mask = reader.Raw<std::uint16_t>();
state.depthRemap.mode = reader.U32();
}
void encodeRecipe(Writer& writer, const GraphicsPipelineRecipe& recipe) {
@@ -1037,6 +1039,7 @@ State CanonicalPipelineState(const State& state) {
canonical.sampleLocationGrid = state.sampleLocationGrid;
canonical.perSampleShading = state.perSampleShading;
canonical.quadSampleMask = state.quadSampleMask;
canonical.depthRemap.mode = state.depthRemap.mode;
return canonical;
}
@@ -541,7 +541,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi
}
}
} else if (variable.storage == spv::StorageClassPushConstant) {
Require(!push && (!shader.pushConstants.empty() || mesh || (control && !rectListControl)) && (type[0] & 0xffffu) == spv::OpTypeStruct, "invalid push constant interface");
Require(!push && (!shader.pushConstants.empty() || shader.readsDepthRemapPush || mesh || (control && !rectListControl)) && (type[0] & 0xffffu) == spv::OpTypeStruct, "invalid push constant interface");
push = true;
Require(type.size() == 3 && module.decorations[typeId].block, "push constant variable must be a Block struct with exactly one member");
const auto offset = module.offsets.find({typeId, 0});
@@ -613,7 +613,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi
Require(shader.vertexAttributes.empty(), "vertex attribute metadata is invalid for this stage");
}
Require(descriptors.size() == shader.bindings.size(), "recompiler binding metadata contains undeclared resources");
Require(push == (!shader.pushConstants.empty() || mesh || (control && !rectListControl && !barycentricPatches)), "recompiler push constant metadata disagrees with SPIR-V");
Require(push == (!shader.pushConstants.empty() || shader.readsDepthRemapPush || mesh || (control && !rectListControl && !barycentricPatches)), "recompiler push constant metadata disagrees with SPIR-V");
for (const auto id : module.interface) Require(module.variables.contains(id), "entry point interface contains an unknown variable");
if (mesh) Require(module.primitiveIndices == state.stages.mesh->outputPrimitiveSize, "mesh shader does not export primitive indices");
if (stage == Stage::Vertex || mesh || evaluation || geometry) Require(module.position, "vertex shader does not export position");
@@ -1061,6 +1061,32 @@ ReversedBlend TranslateReversedBlend(std::uint32_t blendControl, std::uint8_t co
return {blend, routing->code};
}
void ApplyDepthRemap(const Context& context, State& state) {
const auto& viewport = state.viewport;
const bool inRange = viewport.minDepth >= 0 && viewport.minDepth <= 1 && viewport.maxDepth >= 0 && viewport.maxDepth <= 1;
if (context.depthRangeUnrestricted || inRange) return;
Require(state.stages.path == ShaderPath::Vertex && !state.stages.mesh && !state.stages.tessellation, "viewport depth outside [0, 1] is remapped on the vertex path only");
Require(context.depthClamp, "viewport depth outside [0, 1] needs depth clamping to stand in for the console's clamp");
const float clampMin = std::clamp(state.depthClampMin, 0.0f, 1.0f);
const float clampMax = std::clamp(state.depthClampMax, 0.0f, 1.0f);
const bool floatDepth = state.depth.has_value() && (state.depth->format == VK_FORMAT_D32_SFLOAT || state.depth->format == VK_FORMAT_D32_SFLOAT_S8_UINT);
if ((clampMin != state.depthClampMin || clampMax != state.depthClampMax) && floatDepth && (state.depthWrite || state.depthTest)) {
static bool reported = false;
if (!reported) {
reported = true;
std::fprintf(stderr, "[gpu] viewport depth clamp bounds [%g, %g] on a float depth target are clipped to [0, 1]: the device has no VK_EXT_depth_range_unrestricted\n", state.depthClampMin, state.depthClampMax);
}
}
const float range = clampMax - clampMin;
const float scale = range > 0.0f ? state.depthScale / range : state.depthScale;
const float offset = range > 0.0f ? (state.depthOffset - clampMin) / range : state.depthOffset;
state.depthRemap = {state.negativeOneToOne ? 2u : 1u, scale, offset};
state.viewport.minDepth = clampMin;
state.viewport.maxDepth = clampMax;
state.negativeOneToOne = false;
state.depthClamp = true;
}
State DecodeState(const QueueState& queue, bool vertexWorkgroup) {
if (auto reason = DepthMaintenanceRejection(queue); !reason.empty()) throw std::runtime_error(reason);
const auto& cx = queue.context;
@@ -1295,6 +1321,10 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) {
throw std::runtime_error(message.str());
}
Require(readFloat(cx, 0xb4) <= readFloat(cx, 0xb5), "inverted viewport depth clamp bounds");
result.depthScale = zs;
result.depthOffset = zo;
result.depthClampMin = readFloat(cx, 0xb4);
result.depthClampMax = readFloat(cx, 0xb5);
result.viewport = collapsed ? VkViewport{xo, yo, 1, 1, minDepth, maxDepth} : VkViewport{xo - xs, yo - ys, 2 * xs, 2 * ys, minDepth, maxDepth};
if (const auto renderControl = find(cx, 0x000); result.depth && renderControl != cx.end() && (renderControl->second & 1u) != 0) {
result.viewport.minDepth = result.depth->clearDepth;
@@ -0,0 +1,166 @@
#include "Recompiler.hpp"
#include "PipelineSpecialization.hpp"
#include "Optimization/ResourceProgram.hpp"
#include <spirv/unified1/spirv.hpp>
#include <array>
#include <bit>
#include <cstdint>
#include <iostream>
#include <map>
#include <set>
#include <span>
#include <stdexcept>
#include <string>
#include <vector>
namespace {
struct ModuleFacts {
std::map<std::uint32_t, std::uint32_t> specializationDefaults;
std::uint32_t clipDistanceLength = 0;
bool clipDistanceBuiltIn = false;
std::size_t multiplies = 0;
bool pushConstantVariable = false;
std::set<std::uint32_t> pushDwordLoads;
};
ModuleFacts inspect(std::span<const std::uint32_t> words) {
ModuleFacts facts;
std::map<std::uint32_t, std::uint32_t> specIds;
std::map<std::uint32_t, std::uint32_t> specValues;
std::map<std::uint32_t, std::uint32_t> constants;
std::map<std::uint32_t, std::uint32_t> arrayLengths;
std::map<std::uint32_t, std::uint32_t> pointerTargets;
std::map<std::uint32_t, std::uint32_t> variableTypes;
std::set<std::uint32_t> clipVariables;
std::set<std::uint32_t> pushVariables;
std::vector<std::uint32_t> pushIndexIds;
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);
const auto count = words[at] >> 16u;
if (op == spv::OpDecorate && count == 4 && words[at + 2] == spv::DecorationSpecId) specIds[words[at + 1]] = words[at + 3];
if (op == spv::OpDecorate && count == 4 && words[at + 2] == spv::DecorationBuiltIn && words[at + 3] == spv::BuiltInClipDistance) clipVariables.insert(words[at + 1]);
if (op == spv::OpSpecConstant && count == 4) specValues[words[at + 2]] = words[at + 3];
if (op == spv::OpConstant && count == 4) constants[words[at + 2]] = words[at + 3];
if (op == spv::OpTypeArray && count == 4) arrayLengths[words[at + 1]] = words[at + 3];
if (op == spv::OpTypePointer && count == 4) pointerTargets[words[at + 1]] = words[at + 3];
if (op == spv::OpVariable && count >= 4) variableTypes[words[at + 2]] = words[at + 1];
if (op == spv::OpVariable && count >= 4 && words[at + 3] == spv::StorageClassPushConstant) pushVariables.insert(words[at + 2]);
if (op == spv::OpAccessChain && count == 6 && pushVariables.contains(words[at + 3])) pushIndexIds.push_back(words[at + 5]);
if (op == spv::OpFMul) ++facts.multiplies;
}
for (const auto& [id, specId] : specIds) {
if (const auto value = specValues.find(id); value != specValues.end()) facts.specializationDefaults[specId] = value->second;
}
facts.pushConstantVariable = !pushVariables.empty();
for (const auto id : pushIndexIds) {
if (const auto value = constants.find(id); value != constants.end()) facts.pushDwordLoads.insert(value->second);
}
for (const auto variable : clipVariables) {
facts.clipDistanceBuiltIn = true;
const auto pointer = variableTypes.find(variable);
if (pointer == variableTypes.end()) continue;
const auto array = pointerTargets.find(pointer->second);
if (array == pointerTargets.end()) continue;
const auto length = arrayLengths.find(array->second);
if (length == arrayLengths.end()) continue;
const auto constant = constants.find(length->second);
if (constant != constants.end()) facts.clipDistanceLength = constant->second;
}
return facts;
}
ModuleFacts inspect(const ShaderRecompiler::RecompileResult& result) {
return inspect(result.spirv.Words());
}
constexpr std::array<std::uint32_t, 3> PositionExport{0xf80008cfu, 0u, 0xbf810000u};
ShaderRecompiler::RecompileRequest baseRequest(bool remap) {
using namespace ShaderRecompiler;
RecompileRequest request{};
request.shader = {ShaderStage::Vertex, 0x10000u, PositionExport, 0, {}};
request.context.waveSize = 64;
ShaderVertexStageInfo vertex{};
vertex.depthRemapMode = 2u;
request.context.vertex = vertex;
request.target.vulkanVersion = 0x00401000u;
request.target.spirvVersion = 0x00010300u;
request.target.subgroupSize = 64;
static constexpr std::array<std::uint32_t, 2> capabilities{spv::CapabilityShader, spv::CapabilityClipDistance};
request.target.supportedCapabilities = capabilities;
request.target.depthRangeRemap = remap;
request.layout.pushConstantSizeBytes = ShaderRecompiler::DepthRemapPushOffsetBytes;
request.useCache = false;
return request;
}
void require(bool condition, const char* what) {
if (!condition) throw std::runtime_error(what);
}
void preparedTransformTests() {
using namespace ShaderRecompiler;
auto request = baseRequest(true);
request.useCache = true;
const auto handle = PrepareShader(request);
const auto materialize = [&](std::uint32_t mode) {
auto moved = request;
moved.context.vertex->depthRemapMode = mode;
const auto invocation = PreparedShaderInvocation::TryCreate(moved, handle);
require(invocation.has_value(), "the prepared vertex shader did not match a request of its own mode");
return invocation->Materialize(*invocation->Capture(SrtRuntime{}));
};
const auto prepared = inspect(GetPreparedArtifact(*handle).spirv.Words());
require(prepared.specializationDefaults.contains(PipelineSpecialization::DepthRemapBase) && !prepared.specializationDefaults.contains(PipelineSpecialization::DepthRemapBase + 1u) && !prepared.specializationDefaults.contains(PipelineSpecialization::DepthRemapBase + 2u), "only the depth remap mode may be a specialization constant of the prepared module");
const auto first = materialize(2u);
require(materialize(2u)->PipelineVariantId() == first->PipelineVariantId(), "the same depth remap mode did not reuse its materialized vertex shader");
auto other = request;
other.context.vertex->depthRemapMode = 1u;
require(!PreparedShaderInvocation::TryCreate(other, handle).has_value(), "a prepared vertex shader matched a request of another depth remap mode");
}
void pushedTransformTests() {
using namespace ShaderRecompiler;
const auto remapped = Recompile(baseRequest(true));
const auto plain = Recompile(baseRequest(false));
const auto facts = inspect(remapped);
require(!facts.specializationDefaults.contains(PipelineSpecialization::DepthRemapBase + 1u) && !facts.specializationDefaults.contains(PipelineSpecialization::DepthRemapBase + 2u), "the depth remap scale or offset is a specialization constant");
require(facts.pushConstantVariable && facts.pushDwordLoads.contains(DepthRemapPushOffsetBytes / 4u) && facts.pushDwordLoads.contains(DepthRemapPushOffsetBytes / 4u + 1u), "the remapped vertex shader does not read the scale and offset from the push block");
require(!inspect(plain).pushConstantVariable, "a vertex shader without a depth remap gained a push constant block");
auto crowded = baseRequest(true);
crowded.layout.pushConstantSizeBytes = DepthRemapPushOffsetBytes + 4u;
bool refused = false;
try {
static_cast<void>(Recompile(crowded));
} catch (const std::runtime_error&) {
refused = true;
}
require(refused, "a remapped vertex shader with push data over the depth remap words was accepted");
}
}
int main() {
using namespace ShaderRecompiler;
try {
const auto remapped = inspect(Recompile(baseRequest(true)));
const auto plain = inspect(Recompile(baseRequest(false)));
require(remapped.clipDistanceBuiltIn && remapped.clipDistanceLength == plain.clipDistanceLength + 2u, "the remap did not add its two clip distances");
require(remapped.multiplies >= plain.multiplies + 2u, "the remap does not scale the clip z and offset it by w");
require(!plain.specializationDefaults.contains(PipelineSpecialization::DepthRemapBase) && !remapped.specializationDefaults.contains(PipelineSpecialization::DepthRemapBase + 3u), "unexpected specialization constants");
preparedTransformTests();
pushedTransformTests();
std::cout << "Depth range remap tests passed\n";
return 0;
} catch (const std::exception& error) {
std::cerr << "Depth range remap test failed: " << error.what() << '\n';
for (const bool remap : {true, false}) {
const auto facts = inspect(Recompile(baseRequest(remap)));
std::cerr << " remap=" << remap << ": spec ids";
for (const auto& [id, value] : facts.specializationDefaults) std::cerr << ' ' << id << '=' << value;
std::cerr << "; clip builtin " << facts.clipDistanceBuiltIn << " length " << facts.clipDistanceLength << "; fmul " << facts.multiplies << '\n';
}
return 1;
}
}
@@ -11,6 +11,7 @@
#include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp"
#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp"
#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp"
#include "ShaderDiskCache.hpp"
#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp"
#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp"
#include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp"
@@ -3901,6 +3902,57 @@ void quadPixelMaskTests() {
expectFailure([&] { AgcDriver::Graphics::ValidateSampleMask(context, varying, storing); }, "differs between the pixels of a quad");
}
void DepthRemapTests() {
auto queue = makeState();
queue.context[0x204] = 0;
queue.context[0x113] = std::bit_cast<std::uint32_t>(1.0f);
queue.context[0x114] = 0;
AgcDriver::Graphics::Context remapping{};
remapping.depthClamp = true;
auto state = AgcDriver::Graphics::DecodeState(queue);
Require(state.negativeOneToOne && state.viewport.minDepth == -1 && state.viewport.maxDepth == 1 && state.depthRemap.mode == 0, "a negative-one-to-one depth transform decoded with a remap");
AgcDriver::Graphics::ApplyDepthRemap(remapping, state);
Require(state.depthRemap.mode == 2 && state.depthRemap.scale == 1.0f && state.depthRemap.offset == 0.0f, "the remap did not take the viewport's depth scale and offset");
Require(state.viewport.minDepth == 0 && state.viewport.maxDepth == 1 && !state.negativeOneToOne && state.depthClamp, "the remapped viewport does not clamp at the console's bounds in zero-to-one clip space");
AgcDriver::Graphics::Context unrestricted{};
unrestricted.depthRangeUnrestricted = true;
auto kept = AgcDriver::Graphics::DecodeState(queue);
AgcDriver::Graphics::ApplyDepthRemap(unrestricted, kept);
Require(kept.depthRemap.mode == 0 && kept.viewport.minDepth == -1 && kept.negativeOneToOne && !kept.depthClamp, "an unrestricted device remapped the depth range");
AgcDriver::Graphics::Context noClamp{};
auto refused = AgcDriver::Graphics::DecodeState(queue);
expectFailure([&] { AgcDriver::Graphics::ApplyDepthRemap(noClamp, refused); }, "depth clamping");
queue.context[0x113] = std::bit_cast<std::uint32_t>(0.5f);
queue.context[0x114] = std::bit_cast<std::uint32_t>(0.5f);
auto inRange = AgcDriver::Graphics::DecodeState(queue);
AgcDriver::Graphics::ApplyDepthRemap(remapping, inRange);
Require(inRange.depthRemap.mode == 0 && inRange.negativeOneToOne && !inRange.depthClamp, "an in-range depth transform was remapped");
queue.context[0x204] = 0x80000u;
queue.context[0x113] = std::bit_cast<std::uint32_t>(2.0f);
queue.context[0x114] = std::bit_cast<std::uint32_t>(-0.5f);
queue.context[0xb4] = std::bit_cast<std::uint32_t>(0.25f);
queue.context[0xb5] = std::bit_cast<std::uint32_t>(0.75f);
auto bounded = AgcDriver::Graphics::DecodeState(queue);
AgcDriver::Graphics::ApplyDepthRemap(remapping, bounded);
Require(bounded.depthRemap.mode == 1 && bounded.depthRemap.scale == 4.0f && bounded.depthRemap.offset == -1.5f && bounded.viewport.minDepth == 0.25f && bounded.viewport.maxDepth == 0.75f, "a zero-to-one remap did not divide its transform by the clamp range it gives the viewport");
queue.context[0xb4] = std::bit_cast<std::uint32_t>(-1.0f);
queue.context[0xb5] = std::bit_cast<std::uint32_t>(2.0f);
auto clipped = AgcDriver::Graphics::DecodeState(queue);
AgcDriver::Graphics::ApplyDepthRemap(remapping, clipped);
Require(clipped.depthRemap.mode == 1 && clipped.viewport.minDepth == 0 && clipped.viewport.maxDepth == 1, "clamp bounds beyond [0, 1] were not clipped to the representable depth range");
Require(clipped.depthRemap.scale == 2.0f && clipped.depthRemap.offset == -0.5f, "clamp bounds clipped to [0, 1] changed the depth transform");
auto unorm = AgcDriver::Graphics::DecodeState(queue);
unorm.depth = AgcDriver::Graphics::DepthTarget{0x10000, 0, {64, 64}, VK_FORMAT_D16_UNORM, 0.0f, 0};
unorm.depthTest = unorm.depthWrite = true;
AgcDriver::Graphics::ApplyDepthRemap(remapping, unorm);
Require(unorm.viewport.minDepth == 0 && unorm.viewport.maxDepth == 1, "a normalized depth target refused clipped clamp bounds");
auto floating = AgcDriver::Graphics::DecodeState(queue);
floating.depth = AgcDriver::Graphics::DepthTarget{0x10000, 0, {64, 64}, VK_FORMAT_D32_SFLOAT, 0.0f, 0};
floating.depthWrite = true;
AgcDriver::Graphics::ApplyDepthRemap(remapping, floating);
Require(floating.depthRemap.mode == 1 && floating.viewport.minDepth == 0 && floating.viewport.maxDepth == 1 && floating.depthClamp, "a written float depth target refused clipped clamp bounds");
}
void DepthClipTests() {
auto queue = makeState();
const auto direct = AgcDriver::Graphics::DecodeState(queue);
@@ -5185,6 +5237,16 @@ void pushConstantTests() {
fragment.pushConstants.clear();
shaders[1].pushConstantOffset = 999;
Require(AgcDriver::Graphics::PushConstantStages(shaders) == VK_SHADER_STAGE_VERTEX_BIT, "empty push constants contributed a stage");
{
ShaderRecompiler::RecompileResult reading;
reading.readsDepthRemapPush = true;
std::vector<std::byte> encoded;
ShaderRecompiler::ShaderDiskCache::EncodeResult(reading, encoded);
ShaderRecompiler::RecompileResult replayed;
Require(ShaderRecompiler::ShaderDiskCache::DecodeResult(encoded, replayed), "a result that reads the depth remap push words does not decode");
const std::array<AgcDriver::Graphics::CompiledShader, 1> remapped{{{ShaderRecompiler::ShaderStage::Vertex, &replayed, 0}}};
Require(replayed.pushConstants.empty() && AgcDriver::Graphics::PushConstantStages(remapped) == VK_SHADER_STAGE_VERTEX_BIT, "a journalled vertex shader that reads the depth remap push words lost its push stage");
}
Require(AgcDriver::Graphics::AssemblePushConstants(shaders)[8] == std::byte{0}, "empty push constants were copied");
shaders[1].program = nullptr;
expectFailure([&] { AgcDriver::Graphics::AssemblePushConstants(shaders); }, "missing compiled shader");
@@ -9524,6 +9586,7 @@ int main(int argc, char** argv) {
hardwareScreenOffsetTests();
srgb8TargetTests();
DepthClipTests();
DepthRemapTests();
DepthStencilTests();
OneDimensionalColorTests();
htileDepthClearTests();
@@ -125,6 +125,7 @@ void requireSameResult(const RecompileResult& left, const RecompileResult& right
const std::string prefix = std::string(what) + ": ";
require(sameBindings(left.bindings, right.bindings), prefix + "bindings differ");
require(left.pushConstants == right.pushConstants, prefix + "push constants differ");
require(left.readsDepthRemapPush == right.readsDepthRemapPush, prefix + "depth remap push reads differ");
require(left.specialization == right.specialization, prefix + "specialization constants differ");
require(left.workgroupMemoryDwords == right.workgroupMemoryDwords, prefix + "workgroup memory stride differs");
require(left.poisonedSrtReads == right.poisonedSrtReads, prefix + "poisoned SRT read counts differ");
@@ -196,6 +197,7 @@ RecompileResult sampleResult() {
result.bindings[1].role = DescriptorRole::GuestBuffers;
result.bindings[1].imageShape.reset();
result.pushConstants = {std::byte{1}, std::byte{0xff}, std::byte{0}, std::byte{0x80}, std::byte{7}};
result.readsDepthRemapPush = true;
result.bdaAbiVersion = 3;
result.memoryOffsetDword = 7;
result.hostSubgroupSize = 32;
@@ -4731,7 +4731,7 @@ void verifyPixelRequestSerialization() {
minimal.context.pixel->layerExported = true;
minimal.context.pixel->ditheredAlphaToCoverage = true;
const auto encoded = serializer.Serialize(minimal);
require(requestPrefix(encoded, 8u) == "NVNQQSIAAAA=", "new requests did not use serialization version 34");
require(requestPrefix(encoded, 8u) == "NVNQQSMAAAA=", "new requests did not use serialization version 35");
const std::array<std::uint32_t, 2> fallbacks{0x40u, 0x1ac4u};
auto withFallbacks = minimal;
withFallbacks.context.linkedCallFallbacks = fallbacks;
@@ -4849,7 +4849,7 @@ void verifyPixelRequestSerialization() {
for (std::size_t bytes = 0; bytes < 45u; ++bytes) {
expectFailure([&] { static_cast<void>(serializer.Deserialize(requestPrefix(encoded, mappingOffset + bytes))); }, "truncated data", "a truncated pixel mapping, packing, dual-source, alpha-to-coverage, stencil reference export flag, primitive class, layer export, dither or front-face flag, quad pixel mask or quad sample mask was accepted");
}
for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQSMAAAA="}) {
for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQSQAAAA="}) {
expectFailure([&] { static_cast<void>(serializer.Deserialize(unsupported)); }, "serialization version", "an unsupported request version was accepted");
}
static constexpr std::string_view version18Request{
@@ -4999,6 +4999,7 @@ void verifyVertexInfoVersion19() {
require(std::equal(maskedBytes.begin(), maskedBytes.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u), sampledBytes.begin()) && sampledBytes[maskOffset + 1u] == 0x1u && sampledBytes[maskOffset + 5u] == 0x2u && sampledBytes[maskOffset + 9u] == 0x4u && sampledBytes[maskOffset + 13u] == 0x8u, "the quad sample masks are not the four words after the quad pixel mask");
auto version30 = sampledBytes;
version30.pop_back();
version30.pop_back();
version30.erase(version30.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes + 4u), version30.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes));
version30[4] = 30u;
const auto replay30 = serializer.Deserialize(encodeBase64(version30));
@@ -5006,31 +5007,36 @@ void verifyVertexInfoVersion19() {
auto spanned = sampledPixel;
spanned.target.spannedWaves = true;
auto version32 = decodeBase64(serializer.Serialize(spanned));
require(version32.back() == 1u, "the spanned-waves flag is not the last request byte");
require(version32.size() > 1u && version32.back() == 0u && version32[version32.size() - 2u] == 1u, "the spanned-waves flag is not the byte before the depth remap flag");
version32.pop_back();
version32.erase(version32.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes + 4u), version32.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes));
version32[4] = 32u;
const auto replay32 = serializer.Deserialize(encodeBase64(version32));
require(replay32.request.context.pixel.has_value() && !replay32.request.context.pixel->iteratesSamples && replay32.request.context.pixel->sampleLocations == std::array<std::uint8_t, 32>{} && replay32.request.target.spannedWaves && serializer.Serialize(replay32.request) == serializer.Serialize(spanned), "a version 32 request read pixel iteration or sample locations, or lost its spanned-waves flag");
auto version29 = sampledBytes;
version29.pop_back();
version29.pop_back();
version29.erase(version29.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes), version29.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes));
version29[4] = 29u;
const auto replay29 = serializer.Deserialize(encodeBase64(version29));
require(replay29.request.context.pixel.has_value() && replay29.request.context.pixel->quadSampleMask == std::array<std::uint16_t, 4>{0x1u, 0x2u, 0x4u, 0x8u} && replay29.request.context.pixel->reversedBlend == 0u && decodeBase64(serializer.Serialize(replay29.request)) == sampledBytes, "a version 29 request read a reversed-order blend code or misread the fields after the quad sample masks");
auto version28 = sampledBytes;
version28.pop_back();
version28.pop_back();
version28.erase(version28.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u), version28.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes));
version28[4] = 28u;
const auto replay28 = serializer.Deserialize(encodeBase64(version28));
require(replay28.request.context.pixel.has_value() && replay28.request.context.pixel->quadPixelMask == 0x2u && replay28.request.context.pixel->quadSampleMask == std::array<std::uint16_t, 4>{0xffffu, 0xffffu, 0xffffu, 0xffffu} && replay28.request.context.pixel->reversedBlend == 0u && decodeBase64(serializer.Serialize(replay28.request)) == maskedBytes, "a version 28 request read quad sample masks or a reversed-order blend code, or misread the fields after them");
auto version27 = maskedBytes;
version27.pop_back();
version27.pop_back();
version27.erase(version27.begin() + static_cast<std::ptrdiff_t>(maskOffset), version27.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes));
version27[4] = 27u;
const auto replay27 = serializer.Deserialize(encodeBase64(version27));
require(replay27.request.context.pixel.has_value() && replay27.request.context.pixel->quadPixelMask == 0xfu && replay27.request.context.pixel->reversedBlend == 0u && serializer.Serialize(replay27.request) == encodedPixel, "a version 27 request read a quad pixel mask or reversed-order blend code, or misread the fields after it");
auto legacyPixel = plainPixel;
legacyPixel.pop_back();
legacyPixel.pop_back();
legacyPixel.erase(legacyPixel.begin() + static_cast<std::ptrdiff_t>(allBitsOffset), legacyPixel.begin() + static_cast<std::ptrdiff_t>(allBitsOffset + 2u + sampleMaskBytes + 4u + iterationBytes));
legacyPixel[4] = 26u;
const auto replay26 = serializer.Deserialize(encodeBase64(legacyPixel));
@@ -5061,6 +5067,56 @@ void verifyVertexInfoVersion19() {
}
}
void verifyDepthRemapRequests() {
using namespace ShaderRecompiler;
const std::array<std::uint32_t, 1> code{0xbf810000u};
const RequestSerializer serializer;
RecompileRequest vertex{};
vertex.shader = {ShaderStage::Vertex, 0x10000u, code, 0, {}};
vertex.context.waveSize = 64;
vertex.context.vertex = ShaderVertexStageInfo{};
vertex.context.vertex->paClVsOutCntl = 0x89abcdefu;
const auto plain = serializer.Serialize(vertex);
vertex.target.depthRangeRemap = true;
vertex.context.vertex->depthRemapMode = 2u;
const auto remapped = serializer.Serialize(vertex);
const auto replay = serializer.Deserialize(remapped);
require(replay.request.target.depthRangeRemap && replay.request.context.vertex.has_value() && replay.request.context.vertex->paClVsOutCntl == 0x89abcdefu, "the depth remap target flag or the vertex info did not round-trip");
require(replay.request.context.vertex->depthRemapMode == 2u, "the depth remap mode did not round-trip");
require(serializer.Serialize(replay.request) == remapped, "a version 35 depth remap request changed on re-serialization");
const auto plainBytes = decodeBase64(plain);
const auto remappedBytes = decodeBase64(remapped);
require(plainBytes.size() == remappedBytes.size() && plainBytes.size() > 5u && std::equal(plainBytes.begin(), plainBytes.end() - 5, remappedBytes.begin()), "the depth remap flag and mode are not the last 5 bytes of a vertex request");
for (const std::uint8_t legacyVersion : {32u, 34u}) {
auto legacyBytes = remappedBytes;
legacyBytes[4] = legacyVersion;
legacyBytes.resize(legacyBytes.size() - 5u);
const auto legacy = serializer.Deserialize(encodeBase64(legacyBytes));
require(!legacy.request.target.depthRangeRemap && legacy.request.context.vertex.has_value() && legacy.request.context.vertex->depthRemapMode == 0u, ("a version " + std::to_string(legacyVersion) + " vertex request read depth remap fields").c_str());
require(serializer.Serialize(legacy.request) == plain, ("a version " + std::to_string(legacyVersion) + " vertex request misread the fields before the depth remap").c_str());
}
auto truncated = remappedBytes;
truncated.pop_back();
expectFailure([&] { static_cast<void>(serializer.Deserialize(encodeBase64(truncated))); }, "truncated data", "a truncated depth remap mode was accepted");
auto trailing = remappedBytes;
trailing.push_back(0u);
expectFailure([&] { static_cast<void>(serializer.Deserialize(encodeBase64(trailing))); }, "unexpected trailing data", "a byte after the depth remap fields was accepted");
RecompileRequest pixel{};
pixel.shader = {ShaderStage::Fragment, 0x20000u, code, 0, {}};
pixel.context.waveSize = 64;
pixel.context.pixel = ShaderPixelStageInfo{};
pixel.target.depthRangeRemap = true;
const auto pixelReplay = serializer.Deserialize(serializer.Serialize(pixel));
require(pixelReplay.request.target.depthRangeRemap && !pixelReplay.request.context.vertex.has_value(), "a pixel request lost the depth remap target flag or gained vertex info");
std::vector<std::uint64_t> key;
std::vector<std::uint64_t> other;
RecompileCacheKey::Build(vertex, key);
auto zeroToOne = vertex;
zeroToOne.context.vertex->depthRemapMode = 1u;
RecompileCacheKey::Build(zeroToOne, other);
require(key != other, "the depth remap mode did not change the shader cache key");
}
void verifyLegacyPixelRequests() {
using namespace ShaderRecompiler;
static constexpr std::array<std::string_view, 14> legacyPixelRequests{
@@ -7193,6 +7249,7 @@ int main(int argc, char** argv) {
verifyCanonicalVersion20And21Requests();
verifyPixelRequestSerialization();
verifyVertexInfoVersion19();
verifyDepthRemapRequests();
verifyPositionExportComponents();
verifyLegacyPixelRequests();
verifyPixelExportReplay();
@@ -0,0 +1,257 @@
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp"
#include "prx/libSceAgcDriver/Graphics/include/State.hpp"
#include "Recompiler.hpp"
#include "Optimization/ResourceProgram.hpp"
#include "DepthFastClearHarness.hpp"
#include "VulkanTestDevice.hpp"
#include <algorithm>
#include <array>
#include <bit>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <iostream>
#include <optional>
#include <span>
#include <stdexcept>
#include <string>
#include <vector>
namespace {
namespace Harness = DepthFastClearHarness;
using AgcDriver::Graphics::Require;
using ShaderRecompiler::ShaderStage;
struct Transform {
bool negativeOneToOne;
float scale;
float offset;
float clampMin;
float clampMax;
};
struct Case {
Transform transform;
float z;
bool drawn;
float probe;
bool covered;
bool remapOnly;
const char* what;
};
constexpr Transform Plain{false, 1.0f, 0.0f, 0.0f, 1.0f};
alignas(4096) std::array<std::array<float, Harness::Width * Harness::Height>, 32> Depths{};
alignas(256) std::array<std::array<std::array<float, 4>, 3>, 128> VertexSets{};
std::size_t vertexSets = 0;
// A vertex shader that reads no user data: the fullscreen triangle comes from the vertex index (v5), and the
// clip z is the literal at index 9. Its program carries no push data of its own.
alignas(256) std::array<std::uint32_t, 14> FetchlessCode{
0x36020a81u, 0x34020282u, 0x4a0202c1u, 0x7e020b01u, 0x2c040a81u, 0x34040482u, 0x4a0404c1u, 0x7e040b02u,
0x7e0602ffu, 0u, 0x7e0802f2u, 0xf80008cfu, 0x04030201u, 0xbf810000u,
};
std::uint32_t Draw(AgcDriver::VulkanDevice& device, std::uint64_t depth, const Transform& transform, bool remap, float z, bool write, bool fetchless = false, bool targetRemap = false, bool prepared = false) {
Require(vertexSets < VertexSets.size(), "the test ran out of vertex sets");
auto& vertices = VertexSets[vertexSets++];
vertices = {{{-1.0f, -1.0f, z, 1.0f}, {3.0f, -1.0f, z, 1.0f}, {-1.0f, 3.0f, z, 1.0f}}};
Harness::Pixels.fill(std::byte{0});
AgcDriver::Graphics::State state{};
state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 32u, 32u, std::nullopt, std::nullopt};
state.color = {reinterpret_cast<std::uintptr_t>(Harness::Pixels.data()), {Harness::Width, Harness::Height}, VK_FORMAT_R8G8B8A8_UNORM, Harness::Pixels.size(), 0xe4u};
state.colors = {state.color};
state.hasColorTarget = true;
state.depth = AgcDriver::Graphics::DepthTarget{depth, 0, {Harness::Width, Harness::Height}, VK_FORMAT_D32_SFLOAT, 1.0f, 0};
state.depthTest = true;
state.depthWrite = write;
state.depthCompare = write ? VK_COMPARE_OP_ALWAYS : VK_COMPARE_OP_LESS;
state.renderExtent = {Harness::Width, Harness::Height};
state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
const float minDepth = transform.negativeOneToOne ? transform.offset - transform.scale : transform.offset;
state.viewport = {0, static_cast<float>(Harness::Height), static_cast<float>(Harness::Width), -static_cast<float>(Harness::Height), minDepth, transform.offset + transform.scale};
state.negativeOneToOne = transform.negativeOneToOne;
state.depthScale = transform.scale;
state.depthOffset = transform.offset;
state.depthClampMin = transform.clampMin;
state.depthClampMax = transform.clampMax;
state.scissor = {{0, 0}, {Harness::Width, Harness::Height}};
state.cullMode = VK_CULL_MODE_NONE;
state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
state.blend.colorWriteMask = 15;
state.blends = {state.blend};
state.blendConstants = {};
auto context = device.graphicsContext();
context.depthRangeUnrestricted = !remap;
AgcDriver::Graphics::ApplyDepthRemap(context, state);
const bool inRange = minDepth >= 0.0f && transform.offset + transform.scale <= 1.0f;
Require((state.depthRemap.mode != 0u) == (remap && !inRange), "the draw did not take the depth path the test asked for");
auto target = device.Target();
target.depthRangeRemap = state.depthRemap.mode != 0u || targetRemap;
ShaderRecompiler::ShaderVertexStageInfo vertexInfo{};
vertexInfo.depthRemapMode = state.depthRemap.mode;
std::vector<std::uint32_t> vertexUserData(4, 0u);
if (fetchless) {
FetchlessCode[9] = std::bit_cast<std::uint32_t>(z);
} else {
const auto vertexBuffer = Harness::BufferDescriptor(vertices.data(), 16u, static_cast<std::uint32_t>(vertices.size()));
std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin());
}
const std::span<const std::uint32_t> vertexCode = fetchless ? std::span<const std::uint32_t>(FetchlessCode) : std::span<const std::uint32_t>(Harness::VertexCode);
const std::array<ShaderRecompiler::MemoryRegion, 1> vertexMemory{{{reinterpret_cast<std::uintptr_t>(vertexCode.data()), std::as_bytes(vertexCode)}}};
ShaderRecompiler::RecompileRequest vertex{
{ShaderStage::Vertex, reinterpret_cast<std::uintptr_t>(vertexCode.data()), vertexCode, 0, {}},
{32, 0, vertexUserData, std::nullopt, std::nullopt, vertexInfo, vertexMemory},
target,
{0, 0, 0, 64}
};
vertex.useCache = prepared;
std::shared_ptr<const ShaderRecompiler::RecompileResult> vertexHolder;
if (prepared) {
const auto handle = ShaderRecompiler::PrepareShader(vertex);
const auto invocation = ShaderRecompiler::PreparedShaderInvocation::TryCreate(vertex, handle);
Require(invocation.has_value(), "the prepared vertex shader did not match its own request");
vertexHolder = invocation->Materialize(*invocation->Capture(ShaderRecompiler::SrtRuntime{std::span<const std::uint32_t>(vertexUserData)}));
} else {
vertexHolder = std::make_shared<const ShaderRecompiler::RecompileResult>(ShaderRecompiler::Recompile(vertex));
}
const auto& vertexResult = *vertexHolder;
const auto vertexPush = static_cast<std::uint32_t>(vertexResult.pushConstants.size());
if (fetchless) Require(vertexPush == 0u, "the fetchless vertex shader carries push data of its own");
ShaderRecompiler::ShaderPixelStageInfo pixel{};
pixel.wave32 = true;
pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionX) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionY);
pixel.posX = true;
pixel.posY = true;
pixel.targetOutputMode[0] = 9;
pixel.targetExportMapping.fill(0xe4u);
std::vector<std::uint32_t> pixelUserData(4, 0u);
const std::array<ShaderRecompiler::MemoryRegion, 1> pixelMemory{{{reinterpret_cast<std::uintptr_t>(Harness::PixelCode.data()), std::as_bytes(std::span(Harness::PixelCode))}}};
ShaderRecompiler::RecompileRequest fragment{
{ShaderStage::Fragment, reinterpret_cast<std::uintptr_t>(Harness::PixelCode.data()), Harness::PixelCode, 0, {}},
{32, 0, pixelUserData, std::nullopt, pixel, 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)}
}};
const AgcDriver::Pm4::DrawParameters draw{0, static_cast<std::uint32_t>(vertices.size()), 0, 1, 0, false};
device.Draw(state, draw, shaders);
device.WaitIdle();
std::uint32_t covered = 0;
for (std::size_t index = 0; index < Harness::Pixels.size() / 4u; ++index) {
if (std::to_integer<std::uint8_t>(Harness::Pixels[index * 4u]) == 255u) ++covered;
}
return covered;
}
}
int main() {
try {
const auto device = OpenVulkanTestDevice();
if (!device) return VulkanTestSkipped;
const auto context = device->graphicsContext();
if (!context.depthClamp) {
std::puts("skipped, the device has no depth clamping");
return VulkanTestSkipped;
}
constexpr float Step = 1.0f / 64.0f;
constexpr Transform minecraft{true, 1.0f, 0.0f, -1.0f, 1.0f};
constexpr Transform widened{false, 2.0f, -0.5f, 0.0f, 1.0f};
constexpr Transform bounded{false, 2.0f, -0.5f, 0.25f, 0.75f};
constexpr std::array<Case, 13> cases{{
{minecraft, 0.5f - Step, true, 0.5f, false, false, "negative-one-to-one depth just below 0.5"},
{minecraft, 0.5f + Step, true, 0.5f, true, false, "negative-one-to-one depth just above 0.5"},
{minecraft, -1.5f, false, 0.5f, true, false, "negative-one-to-one clip z in front of the near plane"},
{minecraft, 1.5f, false, 0.5f, true, false, "negative-one-to-one clip z behind the far plane"},
{widened, 0.5f - Step / 2.0f, true, 0.5f, false, false, "scaled zero-to-one depth just below 0.5"},
{widened, 0.5f + Step / 2.0f, true, 0.5f, true, false, "scaled zero-to-one depth just above 0.5"},
{widened, -0.125f, false, 0.5f, true, false, "zero-to-one clip z in front of the near plane"},
{widened, 1.125f, false, 0.5f, true, false, "zero-to-one clip z behind the far plane"},
{bounded, 0.4f, true, 0.35f, false, false, "depth 0.3 inside clamp bounds [0.25, 0.75], probed at 0.35"},
{bounded, 0.4f, true, 0.25f, true, false, "depth 0.3 inside clamp bounds [0.25, 0.75], probed at 0.25"},
{bounded, 0.6f, true, 0.65f, true, false, "depth 0.7 inside clamp bounds [0.25, 0.75], probed at 0.65"},
{bounded, 0.2f, true, 0.2f, true, true, "depth -0.1 clamped to 0.25, probed at 0.2"},
{bounded, 0.8f, true, 0.8f, false, true, "depth 1.1 clamped to 0.75, probed at 0.8"},
}};
std::size_t surfaces = 0;
std::size_t ran = 0;
for (const bool remap : {true, false}) {
if (!remap && !context.depthRangeUnrestricted) continue;
for (const auto& test : cases) {
if (!remap && (test.remapOnly || (test.transform.negativeOneToOne && !context.depthClipControl))) continue;
Require(surfaces < Depths.size(), "the test ran out of depth surfaces");
const auto depth = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(Depths[surfaces++].data()));
const auto drawn = Draw(*device, depth, test.transform, remap, test.z, true);
const auto passed = Draw(*device, depth, Plain, false, test.probe, false);
const auto path = remap ? "remapped" : "unrestricted";
if (drawn != (test.drawn ? Harness::Covered : 0u) || passed != (test.covered ? Harness::Covered : 0u)) {
char detail[256];
std::snprintf(detail, sizeof(detail), "%s (%s path): the draw covered %u of %u pixels (expected %s) and the probe passed %u (expected %s)", test.what, path, drawn, Harness::Covered, test.drawn ? "all" : "none", passed, test.covered ? "all" : "none");
throw std::runtime_error(detail);
}
++ran;
}
}
// Scale and offset reach the vertex shader through the draw's push block, so two draws whose depth remap
// differs only in them build one pipeline between them: the second finds it in the store, and both
// store the depth they should.
{
constexpr Transform widerScale{false, 4.0f, -1.5f, 0.0f, 1.0f};
Require(surfaces + 2u <= Depths.size(), "the test ran out of depth surfaces");
const auto depthA = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(Depths[surfaces++].data()));
const auto depthB = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(Depths[surfaces++].data()));
const auto first = Draw(*device, depthA, widened, true, 0.5f + Step / 2.0f, true);
const auto before = AgcDriver::Graphics::PipelineStoreTotals();
const auto second = Draw(*device, depthB, widerScale, true, 0.5f + Step / 4.0f, true);
const auto after = AgcDriver::Graphics::PipelineStoreTotals();
Require(first == Harness::Covered && second == Harness::Covered, "a draw with a remapped depth did not cover the target");
Require(after.misses == before.misses && after.hits > before.hits, "a draw that differs only in the depth scale and offset built another pipeline");
for (const auto depth : {depthA, depthB}) {
Require(Draw(*device, depth, Plain, false, 0.5f, false) == Harness::Covered, "a depth remapped by a shared pipeline was stored below 0.5 + 1/64");
Require(Draw(*device, depth, Plain, false, 0.5f + 2.0f * Step, false) == 0u, "a depth remapped by a shared pipeline was stored above 0.5 + 1/64");
}
++ran;
}
// A remapped vertex shader with no push data of its own, and a fragment shader with none: the pipeline's
// push stages must still include the vertex stage (a non-GPL layout names them), or the scale and offset
// reach no one.
{
Require(surfaces < Depths.size(), "the test ran out of depth surfaces");
const auto depth = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(Depths[surfaces++].data()));
const auto drawn = Draw(*device, depth, widened, true, 0.5f + Step / 2.0f, true, true);
Require(drawn == Harness::Covered, "a fetchless remapped vertex shader did not cover the target");
Require(Draw(*device, depth, Plain, false, 0.5f, false) == Harness::Covered, "a fetchless remapped vertex shader stored a depth below 0.5 + 1/64, so its scale and offset did not arrive");
Require(Draw(*device, depth, Plain, false, 0.5f + 2.0f * Step, false) == 0u, "a fetchless remapped vertex shader stored a depth above 0.5 + 1/64");
++ran;
}
// On a device without VK_EXT_depth_range_unrestricted the target remaps every vertex shader, so an ordinary
// in-range draw (mode 0) with no push data of its own still carries the remap's push block: it must validate
// and draw, from a direct build and from a prepared one.
for (const bool prepared : {false, true}) {
Require(surfaces < Depths.size(), "the test ran out of depth surfaces");
const auto depth = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(Depths[surfaces++].data()));
const auto drawn = Draw(*device, depth, Plain, false, 0.5f + Step / 2.0f, true, true, true, prepared);
Require(drawn == Harness::Covered, prepared ? "a fetchless mode 0 vertex shader on a remapping target, prepared, did not cover the target" : "a fetchless mode 0 vertex shader on a remapping target did not cover the target");
Require(Draw(*device, depth, Plain, false, 0.5f, false) == Harness::Covered && Draw(*device, depth, Plain, false, 0.5f + 2.0f * Step, false) == 0u, "a fetchless mode 0 vertex shader on a remapping target stored the wrong depth");
++ran;
}
std::printf("depth range remap draw tests passed (%zu cases, unrestricted path %s)\n", ran, context.depthRangeUnrestricted ? "compared" : "absent");
return 0;
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
}
+2
View File
@@ -213,6 +213,7 @@ private:
append(key, value.fetchBufferReg);
append(key, value.fetchEmbedded);
append(key, value.paClVsOutCntl);
append(key, value.depthRemapMode);
if (value.resourcesNum > value.resources.size()) throw std::runtime_error("Shader cache: invalid vertex resource count");
append(key, value.resourcesNum);
for (std::uint32_t i = 0; i < value.resourcesNum; ++i) {
@@ -262,6 +263,7 @@ private:
append(key, value.srgbDecodeFormats);
append(key, value.narrowSubgroupClock);
append(key, value.float64DenormPreserve);
append(key, value.depthRangeRemap);
append(key, value.registrationModules);
append(key, value.registrationWordLimit);
append(key, value.vertexSubgroups);
@@ -794,7 +794,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const
std::string buffer;
Writer writer(buffer);
writer.WriteU32(0x41505335u);
writer.WriteU32(34u);
writer.WriteU32(35u);
writeShaderBinary(writer, request.shader);
writeGuestContext(writer, request.context);
writeSpirvTarget(writer, request.target);
@@ -820,6 +820,10 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const
writer.WriteBool(request.target.float16DenormPreserve);
writer.WriteU32Span(request.context.linkedCallFallbacks);
writer.WriteBool(request.target.spannedWaves);
writer.WriteBool(request.target.depthRangeRemap);
if (request.context.vertex.has_value()) {
writer.WriteU32(request.context.vertex->depthRemapMode);
}
return base64Encode(buffer);
}
@@ -828,7 +832,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 > 34u) throw std::runtime_error("unsupported recompile request serialization version");
if (version < 1u || version > 35u) 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);
@@ -865,6 +869,13 @@ DeserializedRequest RequestSerializer::Deserialize(std::string_view text) const
result.request.context.linkedCallFallbacks = result.linkedCallFallbacks;
}
if (version >= 32u) result.request.target.spannedWaves = reader.ReadBool();
if (version >= 35u) {
result.request.target.depthRangeRemap = reader.ReadBool();
if (result.vertex.has_value()) {
result.vertex->depthRemapMode = reader.ReadU32();
result.request.context.vertex = result.vertex;
}
}
if (version >= 21u && reader.Remaining() != 0u) throw std::runtime_error("RequestSerializer: unexpected trailing data");
return result;
}
@@ -69,6 +69,10 @@ struct ShaderStageRuntime {
}
};
struct ShaderDepthRemap {
std::uint32_t mode = 0;
};
struct ShaderClipSpaceTransform {
float scale[2] = {};
float offset[2] = {};
@@ -137,6 +141,7 @@ struct ShaderVertexInputInfo {
std::uint32_t hostSubgroupSize = 64;
std::uint32_t paClVsOutCntl = 0;
ShaderClipSpaceTransform clipSpace;
ShaderDepthRemap depthRemap;
ShaderMeshInputInfo mesh;
ShaderTessellationInputInfo tess;
bool fetchExternal = false;
@@ -30,6 +30,8 @@ inline constexpr std::uint32_t RasterPrimitive = 8233u;
inline constexpr std::uint32_t LayerExported = 8234u;
inline constexpr std::uint32_t StencilReferenceOffset = 8235u;
inline constexpr std::uint32_t ReversedBlend = 8236u;
inline constexpr std::uint32_t DepthRemapBase = 8240u;
inline constexpr std::uint32_t DepthRemapWords = 1u;
inline constexpr std::uint32_t HeapCountBase = 8256u;
inline constexpr std::uint32_t MipCountBase = 8320u;
inline constexpr std::uint32_t DescriptorIndexBase = 16384u;
+13 -1
View File
@@ -557,6 +557,7 @@ CompiledVariant emitVariant(const RecompileRequest& request, IrProgram& program)
targetOptions.nonConstantImageOffsets = request.target.nonConstantImageOffsets;
targetOptions.narrowSubgroupClock = request.target.narrowSubgroupClock;
targetOptions.float64DenormPreserve = request.target.float64DenormPreserve;
targetOptions.depthRangeRemap = request.target.depthRangeRemap;
targetOptions.registrationModules = registration;
targetOptions.fragmentDescriptorSet = request.target.registrationModules;
targetOptions.float16DenormPreserve = request.target.float16DenormPreserve;
@@ -935,6 +936,13 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil
if (variant.info.info.rasterPrimitive) moduleKey.push_back(static_cast<std::uint32_t>(request.context.pixel->rasterPrimitive));
if (variant.info.info.layerExported) moduleKey.push_back(request.context.pixel->layerExported ? 1u : 0u);
}
const bool depthRemap = request.target.depthRangeRemap && request.shader.stage == ShaderStage::Vertex && request.context.vertex.has_value();
if (depthRemap) {
if (request.context.vertex->depthRemapMode != 0u && request.layout.pushConstantOffsetBytes % (RuntimeAbi::PushConstantDwords * 4u) + request.layout.pushConstantSizeBytes > DepthRemapPushOffsetBytes) {
throw std::runtime_error("a remapped vertex shader's push data must end before the depth remap words");
}
moduleKey.push_back(request.context.vertex->depthRemapMode);
}
result.vertexAttributes.reserve(result.vertexInputs.size());
std::array<std::uint32_t, ShaderVertexStageInfo::MaxResources> vertexClasses{};
std::array<std::uint32_t, ShaderVertexStageInfo::MaxResources / RuntimeAbi::VertexSelectorsPerDword> runtimeSelectors{};
@@ -963,7 +971,7 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil
if (artifact.registrationModule) runtimeSelectors.at(input.location / RuntimeAbi::VertexSelectorsPerDword) |= (selectors & 0xfffu) << (input.location % RuntimeAbi::VertexSelectorsPerDword * 16u);
}
}
if (!plan->bindings.specialization.empty() || variant.bindings.layout.UsesPushData() || !artifact.vertexInputPatches.empty() || request.context.pixel) {
if (!plan->bindings.specialization.empty() || variant.bindings.layout.UsesPushData() || !artifact.vertexInputPatches.empty() || request.context.pixel || depthRemap) {
{
std::shared_lock lock(plan->mutex);
if (const auto found = plan->modules.find(moduleKey); found != plan->modules.end()) entry = found->second;
@@ -985,6 +993,9 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil
if (variant.info.info.rasterPrimitive) constants.push_back({PipelineSpecialization::RasterPrimitive, moduleKey[index++]});
if (variant.info.info.layerExported) constants.push_back({PipelineSpecialization::LayerExported, moduleKey[index++]});
}
if (depthRemap) {
for (std::uint32_t word = 0; word < PipelineSpecialization::DepthRemapWords; ++word) constants.push_back({PipelineSpecialization::DepthRemapBase + word, moduleKey[index++]});
}
if (!artifact.vertexInputPatches.empty()) {
for (const auto& input : result.vertexInputs) {
const auto selectors = moduleKey[index++];
@@ -1023,6 +1034,7 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil
result.workgroupMemoryDwords = WorkgroupMemoryStrideDwords(variant.info.info);
result.bindings = std::move(bindings.bindings);
result.pushConstants = std::move(bindings.pushConstants);
result.readsDepthRemapPush = depthRemap;
if (!artifact.stencilReferences.empty()) {
if (artifact.stencilReferenceDword * sizeof(std::uint32_t) != result.pushConstants.size() || result.pushConstants.size() + sizeof(std::uint32_t) > request.layout.pushConstantSizeBytes) throw std::runtime_error("stencil replay push constant lies outside the stage range");
result.pushConstants.resize(result.pushConstants.size() + sizeof(std::uint32_t));
+5
View File
@@ -211,6 +211,7 @@ struct ShaderVertexStageInfo {
std::uint32_t fetchBufferReg;
bool fetchEmbedded;
std::uint32_t paClVsOutCntl = 0;
std::uint32_t depthRemapMode = 0;
};
struct ShaderFloatMode {
@@ -286,6 +287,7 @@ struct SpirvTarget {
bool float16DenormPreserve = false;
bool spannedWaves = false;
bool robustBufferAccess2 = false;
bool depthRangeRemap = false;
};
struct BindingLayout {
@@ -342,6 +344,8 @@ inline constexpr std::uint32_t MeshDrawPushOffsetBytes = 104;
inline constexpr std::uint32_t MeshDrawPushBytes = 24;
inline constexpr std::uint32_t TessellationLevelPushOffsetBytes = 120;
inline constexpr std::uint32_t TessellationLevelPushBytes = 8;
inline constexpr std::uint32_t DepthRemapPushOffsetBytes = 104;
inline constexpr std::uint32_t DepthRemapPushBytes = 8;
inline constexpr std::uint32_t MeshArgumentAddressDword = 4;
inline constexpr std::uint32_t MeshArgumentIndexCountDword = 3;
inline constexpr std::uint32_t MeshArgumentFirstIndexDword = 4;
@@ -641,6 +645,7 @@ struct ShaderInvocation {
std::uint64_t specializationId = 0;
std::vector<DescriptorBinding> bindings;
std::vector<std::byte> pushConstants;
bool readsDepthRemapPush = false;
std::vector<VertexAttribute> vertexAttributes;
std::uint32_t poisonedSrtReads = 0;
std::vector<ImageTableEntryPoison> imageTablePoison;
+4 -2
View File
@@ -85,8 +85,8 @@ namespace ShaderRecompiler::ShaderDiskCache {
#if defined(__linux__) && defined(__x86_64__) && defined(__GLIBCXX__)
static_assert(sizeof(CompiledShaderArtifact) == 240, "CompiledShaderArtifact changed: update the artifact encoder");
static_assert(sizeof(ShaderInvocation) == 200, "ShaderInvocation changed: update the invocation encoder");
static_assert(sizeof(RecompileResult) == 472, "RecompileResult changed: update EncodeResult and DecodeResult");
static_assert(sizeof(ShaderInvocation) == 208, "ShaderInvocation changed: update the invocation encoder");
static_assert(sizeof(RecompileResult) == 480, "RecompileResult changed: update EncodeResult and DecodeResult");
static_assert(sizeof(DescriptorBinding) == 656, "DescriptorBinding changed: update the binding encoder");
static_assert(sizeof(VertexAttribute) == 32, "VertexAttribute changed: update the attribute encoder");
static_assert(sizeof(VertexInput) == 16, "VertexInput changed: update the vertex input encoder");
@@ -565,6 +565,7 @@ void encodeInvocation(Writer& writer, const ShaderInvocation& invocation) {
writer.List(invocation.bindings, encodeBinding);
writer.Value<std::uint64_t>(invocation.pushConstants.size());
for (const auto byte : invocation.pushConstants) writer.Value(static_cast<std::uint8_t>(byte));
writer.Value(invocation.readsDepthRemapPush);
writer.List(invocation.vertexAttributes, [](Writer& out, const VertexAttribute& attribute) {
out.Value(attribute.location);
out.Value(attribute.components);
@@ -604,6 +605,7 @@ void decodeInvocation(Reader& reader, ShaderInvocation& invocation) {
reader.Values(pushConstants);
invocation.pushConstants.resize(pushConstants.size());
if (!pushConstants.empty()) std::memcpy(invocation.pushConstants.data(), pushConstants.data(), pushConstants.size());
reader.Value(invocation.readsDepthRemapPush);
reader.List(invocation.vertexAttributes, 32, [](Reader& in, VertexAttribute& attribute) {
in.Value(attribute.location);
in.Value(attribute.components);
@@ -22,6 +22,7 @@ struct SpirvTargetOptions {
bool nonConstantImageOffsets = false;
bool narrowSubgroupClock = false;
bool float64DenormPreserve = false;
bool depthRangeRemap = false;
bool registrationModules = false;
bool fragmentDescriptorSet = false;
bool float16DenormPreserve = false;
@@ -105,6 +105,8 @@ struct SpirvEmitterState {
std::uint32_t spannedExchangeVariable = 0;
bool narrowSubgroupClock = false;
bool float64DenormPreserve = false;
bool depthRangeRemap = false;
std::uint32_t depthRemapClipIndex = 0;
bool registrationModules = false;
bool fragmentDescriptorSet = false;
bool float16DenormPreserve = false;
@@ -257,6 +257,7 @@ std::vector<std::uint32_t> SpirvEmitter::Emit(const IrProgram& program, const Sh
state.nonConstantImageOffsets = target.nonConstantImageOffsets;
state.narrowSubgroupClock = target.narrowSubgroupClock;
state.float64DenormPreserve = target.float64DenormPreserve;
state.depthRangeRemap = target.depthRangeRemap;
state.registrationModules = target.registrationModules;
state.fragmentDescriptorSet = target.fragmentDescriptorSet;
state.float16DenormPreserve = target.float16DenormPreserve;
@@ -327,6 +327,11 @@ void DefineOutputs(SpirvEmitterState& state) {
DefineMeshOutputs(state);
return;
}
const bool depthRemap = state.depthRangeRemap && state.program.Resources().stage == IrShaderStage::Vertex;
if (depthRemap) {
state.depthRemapClipIndex = clipDistanceCount;
clipDistanceCount += 2u;
}
const auto stage = state.program.Resources().stage;
const bool exportsPosition = std::ranges::any_of(state.outputs, [](const SpirvOutputBinding& output) { return output.kind == StageOutputKind::Position; });
if (exportsPosition && (stage == IrShaderStage::Vertex || stage == IrShaderStage::TessellationEvaluation) && clipDistanceCount + cullDistanceCount < MaxClipDistances) {
@@ -343,6 +348,7 @@ void DefineOutputs(SpirvEmitterState& state) {
}
return variable;
};
if (depthRemap) BuiltIn(state.clipDistanceVariable, F32ArrayType(state, clipDistanceCount), "gl_ClipDistance", spv::BuiltInClipDistance);
for (auto& binding : state.outputs) {
switch (binding.kind) {
case StageOutputKind::Position:
@@ -413,7 +419,7 @@ void DefineOutputs(SpirvEmitterState& state) {
void DefineDescriptors(SpirvEmitterState& state) {
const IrBindingLayout& layout = state.program.Metadata().bindings;
const IrShaderStage stage = state.program.Resources().stage;
if (layout.UsesPushData() || state.stencilReplay || stage == IrShaderStage::Mesh || stage == IrShaderStage::TessellationControl) {
if (layout.UsesPushData() || state.stencilReplay || stage == IrShaderStage::Mesh || stage == IrShaderStage::TessellationControl || (state.depthRangeRemap && stage == IrShaderStage::Vertex)) {
const auto type = PushConstantBlockType(state);
state.pushConstantVariable = state.module.DefineGlobalVariable(TypePointer(state, spv::StorageClassPushConstant, type), spv::StorageClassPushConstant);
state.module.AddName(type, "BufferResource");
@@ -571,6 +571,59 @@ std::uint32_t ConvertClipCoordinate(SpirvEmitterState& state, std::uint32_t coor
return ndc;
}
std::uint32_t RemapPositionDepth(SpirvEmitterState& state, std::uint32_t position) {
const auto& remap = state.inputInfo.vertex->depthRemap;
const auto f32 = TypeF32(state);
const auto u32 = TypeU32(state);
const auto boolean = TypeBool(state);
std::array<std::uint32_t, 4> components{};
for (std::uint32_t i = 0; i < 4u; i++) {
components.at(i) = state.module.AllocateId();
state.module.AddFunction(spv::OpCompositeExtract, f32, components.at(i), position, i);
}
const auto mode = state.module.SpecializationConstant(u32, PipelineSpecialization::DepthRemapBase, remap.mode);
if (state.pushConstantVariable == 0u) throw std::runtime_error("depth remap push constants were not declared");
const auto pushFloat = [&](std::uint32_t dword) {
const auto pointer = state.module.AllocateId();
state.module.AddFunction(spv::OpAccessChain, TypePushConstantElementPointer(state), pointer, state.pushConstantVariable, ConstantU32(state, 0u), ConstantU32(state, DepthRemapPushOffsetBytes / 4u + dword));
const auto bits = state.module.AllocateId();
state.module.AddFunction(spv::OpLoad, u32, bits, pointer);
const auto value = state.module.AllocateId();
state.module.AddFunction(spv::OpBitcast, f32, value, bits);
return value;
};
const auto scale = pushFloat(0u);
const auto offset = pushFloat(1u);
const auto scaled = state.module.AllocateId();
state.module.AddFunction(spv::OpFMul, f32, scaled, components[2], scale);
const auto shifted = state.module.AllocateId();
state.module.AddFunction(spv::OpFMul, f32, shifted, components[3], offset);
const auto remapped = state.module.AllocateId();
state.module.AddFunction(spv::OpFAdd, f32, remapped, scaled, shifted);
const auto active = state.module.AllocateId();
state.module.AddFunction(spv::OpINotEqual, boolean, active, mode, ConstantU32(state, 0u));
const auto z = Select(state, f32, active, remapped, components[2]);
const auto symmetric = state.module.AllocateId();
state.module.AddFunction(spv::OpIEqual, boolean, symmetric, mode, ConstantU32(state, 2u));
const auto nearSymmetric = state.module.AllocateId();
state.module.AddFunction(spv::OpFAdd, f32, nearSymmetric, components[3], components[2]);
const auto near = Select(state, f32, symmetric, nearSymmetric, components[2]);
const auto far = state.module.AllocateId();
state.module.AddFunction(spv::OpFSub, f32, far, components[3], components[2]);
const auto inside = ConstantF32Value(state, 1.0f);
const auto storeDistance = [&](std::uint32_t index, std::uint32_t distance) {
const auto pointer = state.module.AllocateId();
state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassOutput, f32), pointer, state.clipDistanceVariable, ConstantU32(state, index));
state.module.AddFunction(spv::OpStore, pointer, Select(state, f32, active, distance, inside));
};
if (state.clipDistanceVariable == 0u) throw std::runtime_error("depth remap clip distances were not declared");
storeDistance(state.depthRemapClipIndex, near);
storeDistance(state.depthRemapClipIndex + 1u, far);
const auto converted = state.module.AllocateId();
state.module.AddFunction(spv::OpCompositeConstruct, TypeF32Vector(state, 4u), converted, components[0], components[1], z, components[3]);
return converted;
}
std::uint32_t ConvertPositionToClipSpace(SpirvEmitterState& state, std::uint32_t position) {
if (state.inputInfo.vertex == nullptr) {
throw std::runtime_error("vertex input info is missing for clip-space conversion");
@@ -1588,6 +1641,9 @@ void EmitSetAttribute(SpirvValueEmitContext& ctx, const IrValue& inst) {
if (state.inputInfo.vertex->clipSpace.enabled) {
value = ConvertPositionToClipSpace(state, value);
}
if (state.depthRangeRemap && state.program.Resources().stage == IrShaderStage::Vertex) {
value = RemapPositionDepth(state, value);
}
}
if (exp.kind == ExportTargetKind::Position && state.zeroPositionClipIndex) {
StoreZeroPositionClipDistance(state, value);
@@ -174,6 +174,7 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues
vertexStorage.hostSubgroupSize = hostSubgroupSize;
vertexStorage.fetchEmbedded = vertex.fetchEmbedded;
vertexStorage.paClVsOutCntl = vertex.paClVsOutCntl;
vertexStorage.depthRemap = {vertex.depthRemapMode};
vertexStorage.fetchExternal = false;
vertexStorage.fetchAttribReg = static_cast<int>(vertex.fetchAttribReg);
vertexStorage.fetchBufferReg = static_cast<int>(vertex.fetchBufferReg);
@@ -207,11 +207,11 @@ int main() {
Require(request.graphics->draw.indexAddress == 0xabcdef3400u && request.graphics->draw.indexCount == 57u && request.graphics->draw.indexElementBytes == 4u && request.graphics->draw.instanceCount == 9u);
Require(request.target.float16DenormPreserve == preserve);
auto upgraded = bytes;
upgraded[4] = 34u;
upgraded[4] = 35u;
if (fixture.pixel) upgraded.insert(upgraded.begin() + 258u, {0u, 0u, 0u});
if (fixture.pixel) InsertPixelDefaultsAfterFrontFace(upgraded);
if (fixture.tessellation) upgraded.insert(upgraded.end() - 35u, 5u, 0u);
upgraded.insert(upgraded.end(), sizeof(std::uint64_t) + 1u, 0u);
upgraded.insert(upgraded.end(), sizeof(std::uint64_t) + 1u + 5u, 0u);
Require(serializer.Serialize(request) == EncodeBase64(upgraded));
auto truncated = bytes;
truncated.pop_back();
@@ -246,11 +246,11 @@ int main() {
layerRejected = std::string_view(error.what()) == "RequestSerializer: truncated data";
}
Require(layerRejected);
layered[4] = 34u;
layered[4] = 35u;
layered.insert(layered.begin() + 259u, {0u, 0u});
InsertPixelDefaultsAfterFrontFace(layered);
if (fixture.tessellation) layered.insert(layered.end() - 35u, 5u, 0u);
layered.insert(layered.end(), sizeof(std::uint64_t) + 1u, 0u);
layered.insert(layered.end(), sizeof(std::uint64_t) + 1u + 5u, 0u);
Require(serializer.Serialize(layerReplay.request) == EncodeBase64(layered));
}
}
@@ -271,10 +271,10 @@ int main() {
Require(replay.request.context.pixel->rasterPrimitive == ShaderRecompiler::RasterPrimitive::Rectangle);
Require(replay.request.context.vertex->paClVsOutCntl == 0x00050a05u);
Require(replay.request.target.float16DenormPreserve == preserve);
bytes[4] = 34u;
bytes[4] = 35u;
bytes.insert(bytes.begin() + 259u, {0u, 0u});
InsertPixelDefaultsAfterFrontFace(bytes);
bytes.insert(bytes.end(), sizeof(std::uint64_t) + 1u, 0u);
bytes.insert(bytes.end(), sizeof(std::uint64_t) + 1u + 5u, 0u);
Require(serializer.Serialize(replay.request) == EncodeBase64(bytes));
auto truncated23 = DecodeBase64(encoded23);
@@ -395,10 +395,10 @@ int main() {
Require(request.graphics->draw.indexAddress == 0xabcdef3400u && request.graphics->draw.indexCount == 57u && request.graphics->draw.indexElementBytes == 4u && request.graphics->draw.instanceCount == 9u);
Require(request.target.float16DenormPreserve == preserve);
auto upgraded = bytes;
upgraded[4] = 34u;
upgraded[4] = 35u;
if (fixture.pixel) upgraded.insert(upgraded.begin() + 259u, {0u, 0u});
if (fixture.pixel) InsertPixelDefaultsAfterFrontFace(upgraded);
upgraded.insert(upgraded.end(), sizeof(std::uint64_t) + 1u, 0u);
upgraded.insert(upgraded.end(), sizeof(std::uint64_t) + 1u + 5u, 0u);
Require(serializer.Serialize(request) == EncodeBase64(upgraded));
for (const bool dither : {false, true}) {
if (!fixture.pixel && dither) continue;
@@ -408,12 +408,12 @@ int main() {
const auto replay25 = serializer.Deserialize(EncodeBase64(main25));
Require(!fixture.pixel || (replay25.request.context.pixel->layerExported == layer && replay25.request.context.pixel->ditheredAlphaToCoverage == dither && replay25.request.context.pixel->reversedBlend == 0u));
Require(replay25.request.target.float16DenormPreserve == preserve && (!fixture.tessellation || replay25.request.graphics->tessellation->primitiveGroupSize == 37u));
main25[4] = 34u;
main25[4] = 35u;
if (fixture.pixel) {
main25.insert(main25.begin() + 260u, 0u);
InsertPixelDefaultsAfterFrontFace(main25);
}
main25.insert(main25.end(), sizeof(std::uint64_t) + 1u, 0u);
main25.insert(main25.end(), sizeof(std::uint64_t) + 1u + 5u, 0u);
Require(serializer.Serialize(replay25.request) == EncodeBase64(main25));
}
}
@@ -427,9 +427,9 @@ int main() {
const auto replay = serializer.Deserialize(encoded);
Require(replay.request.target.float16DenormPreserve == preserve);
auto bytes = DecodeBase64(encoded);
Require(bytes.size() >= 8u && bytes[4] == 34u && bytes[5] == 0u && bytes[6] == 0u && bytes[7] == 0u);
Require(bytes.size() >= 8u && bytes[4] == 35u && bytes[5] == 0u && bytes[6] == 0u && bytes[7] == 0u);
auto future = bytes;
future[4] = 35u;
future[4] = 36u;
bool futureRejected = false;
try {
static_cast<void>(serializer.Deserialize(EncodeBase64(future)));
+1
View File
@@ -187,3 +187,4 @@
- [Unclamped normalized color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - CB_COLOR_INFO targets with BLEND_CLAMP clear (bit 15) on UNORM and SNORM formats are accepted when blending is disabled (ENABLE clear or BLEND_BYPASS set). Vulkan converts unblended shader values and clamps to the normalized range automatically; blending into an unclamped normalized target remains unmodeled because Vulkan always clamps inputs prior to blending.
- [Thick SW_4KB_S_X volumes](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp) use AMD addrlib's GFX10 non-RB+ 16-pipe `GFX10_SW_4K_S3_X_PATINFO` equations for 1, 2, 4, 8 and 16-byte elements, including packed mip tails. The layout follows [AMD PAL c5e80007](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp); it has not been measured on console hardware. Other thick swizzle modes outside the supported families still throw. Mipmapped 3D color targets remain refused by the color-register decoder.
- [Viewport depth range remap](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - on a device without `VK_EXT_depth_range_unrestricted` a viewport depth range outside [0, 1] (Minecraft PPSA17221 draws its world with `PA_CL_VPORT_ZSCALE` 1, `ZOFFSET` 0 in negative-one-to-one clip space) is folded into the vertex shader: `RemapPositionDepth` scales clip z so z/w is the console's depth, the pipeline's depth clamp applies the console's `PA_SC_VPORT_ZMIN/ZMAX` clipped to [0, 1] (exact for a normalized depth target or none; Minecraft clamps its float depth target to [-1, 1], so a depth below 0 is stored as 0 there, which only a later test or read at exactly 0 can tell, and the first such draw is reported once), and two extra `gl_ClipDistance` elements restore the near and far planes depth clamping removes. The mode is a pipeline specialization constant (`PipelineSpecialization::DepthRemapBase`); scale and offset are read from the draw's push block (two words at byte 104, `DepthRemapPushOffsetBytes`, so a remapped draw's stage push data ends before it), so draws that differ only in them share one vertex module and one pipeline. The remap serves the vertex path only; mesh and tessellation draws with such a range still throw. Whether the console clamps before or after its depth bias and how it treats a clamp range narrower than the transform's are not measured.