feat(shader): emulate smooth barycentrics on the mesh path

A pixel shader that reads barycentrics on a device without
VK_KHR_fragment_shader_barycentric used to need a vertex shader so a
generated geometry stage could follow it, which a mesh pipeline cannot
have. The mesh program is now compiled a second time with the layout
its pixel shader needs: it writes its clip position as a varying and the
three corner positions of each primitive as per-primitive outputs, and
the pixel shader solves for the weights, for both smooth and linear
interpolation.

The layout is part of the mesh cache key, the stored artifact and the
captured request. Without VK_EXT_mesh_shader the draw still throws, now
saying so. Per-vertex parameters on the mesh path still throw.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
sussyandClaude Sonnet 5.5 authored and Adrià Franch committed 2026-10-19 17:26:37 +02:00
1 parent cded26c687
commit dd41527ce2
21 files changed
+728 -82

No files matched your search

@@ -163,7 +163,7 @@ private:
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);
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, const std::optional<ShaderRecompiler::MeshBarycentricLayout>& meshBarycentrics);
static void recordStageDataPositions(DispatchVariant& variant, const ShaderRecompiler::ResourceCapture& capture, const std::vector<Graphics::DecodeRead>& decodeReads);
void cacheDrawStages(bool useDrawEntries, bool drawHit, const Pm4::DrawParameters& drawParameters, const std::optional<Graphics::IndirectDrawPath>& indirectCpu, const std::vector<DrawProgram>& programs, const std::vector<StageCapture>& stageCaptures, const std::vector<std::optional<ShaderRecompiler::ShaderVertexStageInfo>>& vertexInfos, const std::vector<std::vector<Graphics::DecodeRead>>& decodeReads, bool verifyHit, const std::vector<std::shared_ptr<DispatchVariant>>& matched, std::vector<std::shared_ptr<DispatchVariant>>& fresh, std::uint64_t drawKey, bool registerKey, const std::shared_ptr<const DrawDecode>& decode, DrawPhaseTiming& phaseTiming, const std::vector<bool>& reused, const std::shared_ptr<DrawEntry>& entry, std::uint64_t shapeKey);
static bool drawPrecheck();
@@ -380,6 +380,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe
};
std::optional<Graphics::IndirectDrawPath> indirectCpu;
std::vector<std::uint32_t> pushOffsets(programs.size(), 0);
std::optional<ShaderRecompiler::MeshBarycentricLayout> meshBarycentrics;
std::vector<std::size_t> resultIndex(programs.size(), 0);
for (std::size_t i = 0; i < programs.size(); ++i) {
if (roles[i] == Role::GeometryBack) continue;
@@ -407,7 +408,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe
break;
}
}
compiledStages.push_back(materializeDrawStage(i, pushCursorBytes, queue, submission, programs, graphics, pixel, vertexInfos, memory, linked, drawParameters, localDevice, *shaderMemory, stageCaptures, recompiled, drawHit, matched, matchedRegions, profile, dumpTarget, dumpSlot1, captures, phaseTiming, phaseMs, rejected));
compiledStages.push_back(materializeDrawStage(i, pushCursorBytes, queue, submission, programs, graphics, pixel, vertexInfos, memory, linked, drawParameters, localDevice, *shaderMemory, stageCaptures, recompiled, drawHit, matched, matchedRegions, profile, dumpTarget, dumpSlot1, captures, phaseTiming, phaseMs, rejected, meshBarycentrics));
if (!rejected.empty()) return DrawVerdict::Rejected;
programResults[i] = compiledStages.back().get();
if (candidate != dataCandidates[i].end()) verifyDataStage(i, *candidate->first);
@@ -471,7 +472,21 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe
phaseTiming.Phase(DrawRowRectList);
};
if (graphics.rectList) buildRectList();
if (!programs.empty() && programResults.back() != nullptr && programResults.back()->barycentricEmulation.active) {
if (!programs.empty() && programResults.back() != nullptr && programResults.back()->barycentricEmulation.active && graphics.stages.path == Graphics::ShaderPath::Geometry) {
require(localDevice->Target().mesh.has_value(), "a pixel shader that reads barycentrics without VK_KHR_fragment_shader_barycentric needs barycentric emulation on the mesh path, which requires VK_EXT_mesh_shader; this device has no mesh shaders");
require(!graphics.rectList && graphics.stages.mesh && programs.size() == 2 && programResults[0] != nullptr && programResults[1] != nullptr && stages.size() == 2, "a pixel shader that reads barycentrics needs exactly one mesh program before it to emulate them");
meshBarycentrics = ShaderRecompiler::MeshBarycentricLayoutOf(*programResults[1]);
if (programResults[0]->meshBarycentrics != meshBarycentrics) {
const auto pushBytes = programResults[0]->pushConstants.size();
const auto exports = programResults[0]->parameterExports;
compiledStages.push_back(materializeDrawStage(0, pushOffsets[0], queue, submission, programs, graphics, pixel, vertexInfos, memory, linked, drawParameters, localDevice, *shaderMemory, stageCaptures, recompiled, drawHit, matched, matchedRegions, profile, dumpTarget, dumpSlot1, captures, phaseTiming, phaseMs, rejected, meshBarycentrics));
if (!rejected.empty()) return DrawVerdict::Rejected;
programResults[0] = compiledStages.back().get();
stages[0].program = programResults[0];
require(programResults[0]->pushConstants.size() == pushBytes && programResults[0]->parameterExports == exports, "pairing the mesh program with its pixel shader changed the push constants or the parameter exports the pixel shader was built for");
}
require(programResults[0]->meshBarycentrics == meshBarycentrics, "the mesh program was compiled for a different barycentric layout than its pixel shader");
} else if (!programs.empty() && programResults.back() != nullptr && programResults.back()->barycentricEmulation.active) {
require(!graphics.rectList && graphics.stages.path == Graphics::ShaderPath::Vertex && programs.size() == 2 && programResults[0] != nullptr && stages.size() == 2, "a pixel shader that reads barycentrics without VK_KHR_fragment_shader_barycentric needs a vertex shader before it");
const auto geometryLimits = localDevice->GeometryLimits();
if (geometryLimits || !localDevice->Target().tessellation) {
@@ -573,7 +588,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe
auto& result = results[resultIndex[programIndex]];
const auto pushBytes = result.pushConstants.size();
decodeVertexInfo(programIndex);
result = *materializeDrawStage(programIndex, pushOffsets[programIndex], queue, submission, programs, graphics, pixel, vertexInfos, memory, linked, programs[programIndex].binary.stage == Stage::TessellationControl || programs[programIndex].binary.stage == Stage::TessellationEvaluation ? direct : drawParameters, localDevice, *shaderMemory, stageCaptures, recompiled, drawHit, matched, matchedRegions, profile, dumpTarget, dumpSlot1, captures, phaseTiming, phaseMs, rejected);
result = *materializeDrawStage(programIndex, pushOffsets[programIndex], queue, submission, programs, graphics, pixel, vertexInfos, memory, linked, programs[programIndex].binary.stage == Stage::TessellationControl || programs[programIndex].binary.stage == Stage::TessellationEvaluation ? direct : drawParameters, localDevice, *shaderMemory, stageCaptures, recompiled, drawHit, matched, matchedRegions, profile, dumpTarget, dumpSlot1, captures, phaseTiming, phaseMs, rejected, meshBarycentrics);
if (!rejected.empty()) return DrawVerdict::Rejected;
require(result.pushConstants.size() == pushBytes, "patched program changed its push constant layout");
}
@@ -13,7 +13,7 @@
namespace AgcDriver::DriverDetail {
std::shared_ptr<const ShaderRecompiler::RecompileResult> Driver::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) {
std::shared_ptr<const ShaderRecompiler::RecompileResult> Driver::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, const std::optional<ShaderRecompiler::MeshBarycentricLayout>& meshBarycentrics) {
PerformanceTimer timing("Shader.DrawStage");
using Stage = ShaderRecompiler::ShaderStage;
phaseTiming.Phase(DrawRowVectors);
@@ -24,7 +24,7 @@ std::shared_ptr<const ShaderRecompiler::RecompileResult> Driver::materializeDraw
{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.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}}
ShaderRecompiler::GraphicsCompileContext{program.firstUserSgpr, linked, graphics.stages.mesh, graphics.stages.tessellation, {drawParameters.indexAddress, drawParameters.indexCount, drawParameters.indexSize, drawParameters.instanceCount}, program.binary.stage == Stage::Mesh ? meshBarycentrics : std::nullopt}
};
const auto waitedBefore = traceCapSync() || profile ? Graphics::Recorder::ThreadWaitedMs() : 0.0;
auto invocation = InvocationFor(*program.snapshot, program.codeOffset, request);
@@ -51,6 +51,8 @@ struct Module {
std::uint32_t primitiveIndices = 0;
bool fragmentBarycentric = false;
bool layerOutput = false;
bool meshCapability = false;
std::uint32_t primitiveInputs = 0;
std::map<std::uint32_t, std::vector<std::uint32_t>> modes;
const std::vector<std::uint32_t>& Type(std::uint32_t id) const {
@@ -75,17 +77,17 @@ struct Module {
throw std::runtime_error("AGC graphics: unsupported SPIR-V interface type");
}
std::uint32_t AddLocations(std::map<std::uint32_t, std::string>& locations, std::uint32_t location, std::uint32_t typeId, bool patch, std::uint32_t depth = 0) const {
std::uint32_t AddLocations(std::map<std::uint32_t, std::string>& locations, std::uint32_t location, std::uint32_t typeId, bool patch, bool primitive = false, std::uint32_t depth = 0) const {
Require(depth < 8 && location < 256, "graphics interface exceeds supported locations");
const auto& type = Type(typeId);
if ((type[0] & 0xffffu) == spv::OpTypeArray) {
Require(type.size() == 4, "malformed interface array");
const auto count = constants.find(type[3]);
Require(count != constants.end() && count->second != 0 && count->second <= 256, "invalid interface array length");
for (std::uint32_t i = 0; i < count->second; ++i) location = AddLocations(locations, location, type[2], patch, depth + 1);
for (std::uint32_t i = 0; i < count->second; ++i) location = AddLocations(locations, location, type[2], patch, primitive, depth + 1);
return location;
}
Require(locations.emplace(location | (patch ? 0x10000u : 0u), std::string(patch ? "patch:" : "vertex:") + Signature(typeId)).second, "duplicate shader interface location");
Require(locations.emplace(location | (patch ? 0x10000u : 0u), std::string(patch ? "patch:" : primitive ? "primitive:" : "vertex:") + Signature(typeId)).second, "duplicate shader interface location");
return location + 1;
}
@@ -154,7 +156,7 @@ struct Module {
}
};
Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing, bool imageInt64Atomics, bool geometryShader, bool sampleRateShading, bool bufferInt64Atomics, bool shaderClipDistance, bool shaderCullDistance, bool viewportIndexLayer, bool runtimeDescriptorArray, bool demoteToHelperInvocation, bool shaderTerminateInvocation) {
Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing, bool imageInt64Atomics, bool geometryShader, bool sampleRateShading, bool bufferInt64Atomics, bool shaderClipDistance, bool shaderCullDistance, bool viewportIndexLayer, bool runtimeDescriptorArray, bool demoteToHelperInvocation, bool shaderTerminateInvocation, std::span<const std::uint32_t> primitiveLocations = {}) {
using Stage = ShaderRecompiler::ShaderStage;
Require(compiled.program != nullptr, "missing compiled shader");
const auto& shader = *compiled.program;
@@ -238,8 +240,9 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi
(control || evaluation) &&
capability == spv::CapabilityTessellation;
if (fragment && capability == spv::CapabilityMeshShadingEXT) module.meshCapability = true;
const bool isMeshCapability =
mesh &&
(mesh || (fragment && !primitiveLocations.empty())) &&
capability == spv::CapabilityMeshShadingEXT;
const bool isGeometryCapability =
@@ -342,7 +345,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi
terminateExtension = true;
break;
}
Require(extension == "SPV_KHR_float_controls" || (imageInt64Atomics && extension == "SPV_EXT_shader_image_int64") || (viewportIndexLayer && extension == "SPV_EXT_shader_viewport_index_layer") || (fragment && demoteToHelperInvocation && extension == "SPV_EXT_demote_to_helper_invocation") || (mesh && (extension == "SPV_EXT_mesh_shader" || extension == "SPV_KHR_physical_storage_buffer")) || ((descriptorIndexing || runtimeDescriptorArray) && extension == "SPV_EXT_descriptor_indexing") || (shader.bdaAbiVersion == ShaderRecompiler::BdaAbi::Version && (extension == "SPV_KHR_physical_storage_buffer" || extension == "SPV_KHR_8bit_storage")), "unsupported SPIR-V extension");
Require(extension == "SPV_KHR_float_controls" || (imageInt64Atomics && extension == "SPV_EXT_shader_image_int64") || (viewportIndexLayer && extension == "SPV_EXT_shader_viewport_index_layer") || (fragment && demoteToHelperInvocation && extension == "SPV_EXT_demote_to_helper_invocation") || (mesh && (extension == "SPV_EXT_mesh_shader" || extension == "SPV_KHR_physical_storage_buffer")) || (fragment && !primitiveLocations.empty() && extension == "SPV_EXT_mesh_shader") || ((descriptorIndexing || runtimeDescriptorArray) && extension == "SPV_EXT_descriptor_indexing") || (shader.bdaAbiVersion == ShaderRecompiler::BdaAbi::Version && (extension == "SPV_KHR_physical_storage_buffer" || extension == "SPV_KHR_8bit_storage")), "unsupported SPIR-V extension");
break;
}
case spv::OpTerminateInvocation:
@@ -529,10 +532,13 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi
module.secondSource = module.Signature(typeId);
} else if (decoration.location) {
Require(!vertexArray || (outer[0] & 0xffffu) == spv::OpTypeArray, "per-vertex interface lacks a control-point or mesh-output dimension");
Require(!decoration.perPrimitive, "per-primitive user outputs are unsupported");
Require(!decoration.perPrimitive || ((mesh && output) || (fragment && input)), "per-primitive user interface is unsupported for this stage");
Require(!decoration.perPrimitive || !decoration.patch, "a per-primitive interface cannot be a patch");
Require(!decoration.perPrimitive || std::ranges::find(primitiveLocations, *decoration.location) != primitiveLocations.end(), "per-primitive user interface at a location the barycentric emulation does not reserve");
Require(!decoration.builtin, "shader input cannot have both location and built-in decorations");
auto& locations = variable.storage == spv::StorageClassInput ? module.inputs : module.outputs;
module.AddLocations(locations, *decoration.location, typeId, decoration.patch);
module.AddLocations(locations, *decoration.location, typeId, decoration.patch, decoration.perPrimitive);
if (decoration.perPrimitive && fragment) module.primitiveInputs++;
} else if (decoration.builtin) {
module.Builtin(*decoration.builtin, typeId, variable.storage, stage, subgroup);
} else {
@@ -619,6 +625,8 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi
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 (fragment) Require(module.primitiveInputs == 0 || !primitiveLocations.empty(), "per-primitive pixel inputs need the barycentric emulation layout");
if (fragment) Require(!module.meshCapability || module.primitiveInputs != 0, "a pixel shader declares mesh shading without a per-primitive input");
if (stage == Stage::Vertex || mesh || evaluation || geometry) Require(module.position, "vertex shader does not export position");
return module;
}
@@ -657,13 +665,21 @@ std::set<std::uint32_t> ValidateShaders(std::span<const CompiledShader> shaders,
}
const std::array<Stage, 4> tessStages{Stage::Local, Stage::TessellationControl, Stage::TessellationEvaluation, Stage::Fragment};
static_cast<void>(AssemblePushConstants(shaders));
std::vector<std::uint32_t> primitiveLocations;
if (mesh && !shaders.empty() && shaders.back().program != nullptr && shaders.back().program->barycentricEmulation.active) {
const auto& last = *shaders.back().program;
const auto layout = ShaderRecompiler::LayoutBarycentricEmulation(last.fragmentParameters, last.barycentricEmulation, true);
if (layout.positionsLocation != ShaderRecompiler::BarycentricEmulationLayout::NoLocation) {
for (std::uint32_t corner = 0; corner < 3u; ++corner) primitiveLocations.push_back(layout.positionsLocation + corner);
}
}
Module previous;
for (std::size_t i = 0; i < shaders.size(); ++i) {
const auto expected = state.rectList || patches ? (i == 0 ? Stage::Vertex : tessStages[i]) : tessellation ? tessStages[i] : geometry && i == 1 ? Stage::Geometry : i == shaders.size() - 1 ? Stage::Fragment : state.stages.path == ShaderPath::Geometry ? Stage::Mesh : Stage::Vertex;
Require(shaders[i].program != nullptr, "missing compiled shader");
Require(shaders[i].stage == expected, "graphics stage order disagrees");
for (const auto& binding : shaders[i].program->bindings) Require(binding.descriptorSet == ShaderRecompiler::RuntimeAbi::DescriptorSet || (shaders[i].stage == ShaderRecompiler::ShaderStage::Fragment && binding.descriptorSet == ShaderRecompiler::RuntimeAbi::FragmentDescriptorSet), "graphics resource uses a descriptor set other than its stage's");
const auto current = Inspect(shaders[i], state, subgroup, fragmentShaderBarycentric, descriptorIndexing, imageInt64Atomics, geometryShader, sampleRateShading, bufferInt64Atomics, shaderClipDistance, shaderCullDistance, viewportIndexLayer, runtimeDescriptorArray, demoteToHelperInvocation, shaderTerminateInvocation);
const auto current = Inspect(shaders[i], state, subgroup, fragmentShaderBarycentric, descriptorIndexing, imageInt64Atomics, geometryShader, sampleRateShading, bufferInt64Atomics, shaderClipDistance, shaderCullDistance, viewportIndexLayer, runtimeDescriptorArray, demoteToHelperInvocation, shaderTerminateInvocation, primitiveLocations);
if (i != 0) {
for (const auto& [location, signature] : current.inputs) {
const auto output = previous.outputs.find(location);
@@ -144,6 +144,7 @@ void requireSameResult(const RecompileResult& left, const RecompileResult& right
require(a.location == b.location && a.components == b.components && a.resource.fields == b.resource.fields && a.fetchIndex == b.fetchIndex && a.formatComponents == b.formatComponents, prefix + "vertex attribute differs");
}
require(left.barycentricEmulation.active == right.barycentricEmulation.active && left.barycentricEmulation.smooth == right.barycentricEmulation.smooth && left.barycentricEmulation.linear == right.barycentricEmulation.linear, prefix + "barycentric emulation differs");
require(left.meshBarycentrics == right.meshBarycentrics, prefix + "mesh barycentric layout differs");
}
void requireSameVariant(const CompiledVariant& left, const CompiledVariant& right, const char* what) {
@@ -224,6 +225,7 @@ RecompileResult sampleResult() {
result.poisonedSrtReads = 3;
result.shaderDataUserWords = {2u, 0xffffffffu, 9u};
result.barycentricEmulation = {true, false, true};
result.meshBarycentrics = MeshBarycentricLayout{{true, true, false}, {FragmentParameter{0, 2, false, true}, FragmentParameter{3, 3, false, false}}};
result.variantId = 99;
result.imageTablePoison = {{{1u, 2u, 3u, 4u, 5u, 6u, 7u, 8u}, 8u, 2u, 6u}, {{9u, 0u, 0u, 0u, 0u, 0u, 0u, 0u}, 4u, 0xffffffffu, 19u}};
result.imageTableRanges = {{0x1000u, 0x200u}, {0x5000000000ull, 48u}};
@@ -610,6 +612,18 @@ void verifyArtifactStorageIsolation() {
require(ShaderDiskCache::EncodeEntry(key, variant) != before, "the stored artifact ignores vertex input metadata");
}
void verifyMeshBarycentricRequestSerialization() {
for (const auto& layout : {std::optional<MeshBarycentricLayout>{}, std::optional<MeshBarycentricLayout>{MeshBarycentricLayout{{true, true, true}, {FragmentParameter{0, 2, false, true}, FragmentParameter{4, 4, false, false}}}}}) {
SampleRequest sample;
sample.request.shader.stage = ShaderStage::Mesh;
sample.request.context.compute.reset();
sample.request.graphics = GraphicsCompileContext{0, {}, MeshConfiguration{4u, 4u, 12u, 12u, 4u, 64u, 1024u, 0u, 4u}, std::nullopt, {}, layout};
const auto text = RequestSerializer{}.Serialize(sample.request);
const auto restored = RequestSerializer{}.Deserialize(text);
require(restored.request.graphics.has_value() && restored.request.graphics->meshBarycentrics == layout, "a captured mesh request lost its barycentric layout");
}
}
void verifyKeySensitivity() {
SampleRequest base;
const auto key = base.Key();
@@ -678,6 +692,40 @@ void verifyKeySensitivity() {
require(changed.first != astro.first && changed.second != astro.second, std::string("the key ignores ") + what);
}
const auto meshKeys = [&](const std::optional<MeshBarycentricLayout>& layout) {
SampleRequest sample;
sample.request.shader.stage = ShaderStage::Mesh;
sample.request.context.compute.reset();
sample.request.graphics = GraphicsCompileContext{0, {}, MeshConfiguration{4u, 4u, 12u, 12u, 4u, 64u, 1024u, 0u, 4u}, std::nullopt, {}, layout};
return std::pair{sample.Key(), RecompileCacheKey::ContextHash(sample.request)};
};
const std::vector<std::pair<const char*, std::optional<MeshBarycentricLayout>>> meshLayouts{
{"no mesh barycentric layout", std::nullopt},
{"smooth mesh barycentrics", MeshBarycentricLayout{{true, true, false}, {}}},
{"linear mesh barycentrics", MeshBarycentricLayout{{true, false, true}, {}}},
{"smooth and linear mesh barycentrics", MeshBarycentricLayout{{true, true, true}, {}}},
{"a per-vertex mesh parameter", MeshBarycentricLayout{{true, true, false}, {FragmentParameter{0, 0, false, true}}}},
{"another per-vertex mesh parameter source", MeshBarycentricLayout{{true, true, false}, {FragmentParameter{0, 1, false, true}}}},
{"a flat mesh parameter", MeshBarycentricLayout{{true, true, false}, {FragmentParameter{0, 0, false, false}}}},
};
for (std::size_t i = 0; i < meshLayouts.size(); ++i) {
require(meshKeys(meshLayouts[i].second) == meshKeys(meshLayouts[i].second), std::string("the key of ") + meshLayouts[i].first + " is not deterministic");
for (std::size_t j = i + 1; j < meshLayouts.size(); ++j) {
const auto left = meshKeys(meshLayouts[i].second);
const auto right = meshKeys(meshLayouts[j].second);
require(left.first != right.first && left.second != right.second, std::string("the mesh key confuses ") + meshLayouts[i].first + " with " + meshLayouts[j].first);
}
}
const auto pixelKeys = [&](bool meshPath) {
SampleRequest sample;
sample.request.shader.stage = ShaderStage::Fragment;
sample.request.context.compute.reset();
sample.request.graphics = GraphicsCompileContext{0, {}, meshPath ? std::optional(MeshConfiguration{4u, 4u, 12u, 12u, 4u, 64u, 1024u, 0u, 4u}) : std::nullopt, std::nullopt, {}, std::nullopt};
return std::pair{sample.Key(), RecompileCacheKey::ContextHash(sample.request)};
};
require(pixelKeys(true).first != pixelKeys(false).first && pixelKeys(true).second != pixelKeys(false).second, "a pixel shader on the mesh path shares a key with the vertex path");
SampleRequest moved;
moved.userData[0] ^= 0x10000u;
moved.request.shader.codeAddress += 0x1000000u;
@@ -2288,6 +2336,7 @@ int main(int argc, char** argv) {
verifyPrebuiltPack(directory / "packs");
verifyArtifactStorageIsolation();
verifyKeySensitivity();
verifyMeshBarycentricRequestSerialization();
verifyFloatModeSerialization();
verifyPlanRoundTrip();
verifyPlanKey();
@@ -0,0 +1,273 @@
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
#include "Recompiler.hpp"
#include "VulkanTestDevice.hpp"
#include <algorithm>
#include <array>
#include <cmath>
#include <cstdio>
#include <iostream>
#include <span>
#include <string>
#include <string_view>
#include <vector>
namespace {
using AgcDriver::Graphics::Require;
using ShaderRecompiler::PixelInput;
using ShaderRecompiler::PixelInputBit;
using ShaderRecompiler::ShaderStage;
constexpr std::uint32_t Width = 64;
constexpr std::uint32_t Height = 48;
constexpr float Unwritten = -7.0f;
alignas(256) std::array<float, Width * Height * 4> Pixels{};
alignas(256) constexpr std::array<std::uint32_t, 104> MeshCode{
0x8f6a9003, 0x94fe6ac1, 0xbf88000b, 0xd7650006, 0x000100c1, 0xd7660006, 0x00020cc1, 0x93ebff03,
0x00040018, 0xd7460006, 0x04190c6b, 0x340c0c82, 0xd8340000, 0x00000506, 0xbf8cc07f, 0xbefe04c1,
0xbf8a0000, 0x938dff02, 0x00090016, 0x9382ff03, 0x00040018, 0x938cff03, 0x00080008, 0xd7650009,
0x000100c1, 0xd7660009, 0x000212c1, 0xd746000a, 0x04250c02, 0x7da8120c, 0xbf88002e, 0x361600ff,
0x0000ffff, 0x2c180090, 0x361a02ff, 0x0000ffff, 0xd8d80000, 0x0b00000b, 0xd8d80000, 0x0c00000c,
0xd8d80000, 0x0d00000d, 0xbf8cc07f, 0xe0382000, 0x8002100b, 0xe0382010, 0x8002140b, 0xe0382000,
0x8002180c, 0xe0382010, 0x80021c0c, 0xe0382000, 0x8002200d, 0xe0382010, 0x8002240d, 0x161c1483,
0x161e14ff, 0x00000060, 0xbf8c3f70, 0xdb7c0400, 0x0000100f, 0xdb7c0410, 0x0000140f, 0xdb7c0420,
0x0000180f, 0xdb7c0430, 0x00001c0f, 0xdb7c0440, 0x0000200f, 0xdb7c0450, 0x0000240f, 0x4a501c81,
0x4a521c82, 0x3450508a, 0x34525294, 0xd772002a, 0x04a6510e, 0xbf8cc07f, 0xbefe04c1, 0xbf8a0000,
0x930e830d, 0xbf078002, 0xbf850003, 0x8f0f8c0d, 0x887c0f0e, 0xbf900009, 0x7da8140d, 0xbf880002,
0xf8000941, 0x0000002a, 0xbefe04c1, 0x7da8140e, 0xbf88000a, 0x34561485, 0xdbfc0400, 0x2c00002b,
0xdbfc0410, 0x3000002b, 0xbf8cc07f, 0xf80008cf, 0x2f2e2d2c, 0xf800020f, 0x33323130, 0xbf810000,
};
alignas(256) constexpr std::array<std::uint32_t, 3> BarycentricPixelCode{
0xf800180f, 0x01000100, 0xbf810000,
};
struct Vertex {
std::array<float, 4> position;
std::array<float, 4> color;
};
struct Corner {
float x;
float y;
float w;
};
constexpr std::array<Corner, 3> Corners{{{-0.8f, -0.8f, 1.0f}, {0.9f, -0.6f, 2.5f}, {-0.5f, 0.85f, 0.5f}}};
alignas(256) std::array<Vertex, 3> Vertices{};
std::array<std::uint32_t, 4> VertexBufferDescriptor(const void* vertices, std::uint32_t count) {
const auto address = reinterpret_cast<std::uintptr_t>(vertices);
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu) | (32u << 16u), count, 0x01016facu};
}
constexpr ShaderRecompiler::MeshConfiguration SmallSubgroup{4u, 4u, 12u, 12u, 4u, 64u, 1024u, 0u, 4u};
template<typename TAction>
void ExpectRejected(TAction action, const std::string& reason) {
try {
action();
} catch (const std::exception& error) {
Require(std::string_view(error.what()).find(reason) != std::string_view::npos, std::string("the validator rejected the draw for another reason than \"") + reason + "\": " + error.what());
return;
}
Require(false, "the validator accepted a draw it must reject (" + reason + ")");
}
std::uint32_t DecorationWord(const ShaderRecompiler::RecompileResult& result, spv::Decoration decoration) {
const auto& words = result.spirv.Words();
for (std::size_t cursor = 5; cursor < words.size();) {
const auto count = words[cursor] >> 16u;
if ((words[cursor] & 0xffffu) == spv::OpDecorate && count == 3u && words[cursor + 2] == decoration) return static_cast<std::uint32_t>(cursor + 2);
cursor += count;
}
return 0u;
}
void CheckValidator(AgcDriver::VulkanDevice& device, const AgcDriver::Pm4::DrawParameters& draw, const AgcDriver::Graphics::State& state, const ShaderRecompiler::RecompileResult& meshResult, const ShaderRecompiler::RecompileResult& pixelResult, std::uint32_t pixelOffset) {
const auto rejected = [&](const ShaderRecompiler::RecompileResult& mesh, const ShaderRecompiler::RecompileResult& pixel, const std::string& reason) {
const std::array<AgcDriver::Graphics::CompiledShader, 2> shaders{{{ShaderStage::Mesh, &mesh, 0}, {ShaderStage::Fragment, &pixel, pixelOffset}}};
ExpectRejected([&] { device.Draw(state, draw, shaders); }, reason);
};
auto unreserved = pixelResult;
unreserved.barycentricEmulation.active = false;
rejected(meshResult, unreserved, "per-primitive");
auto shifted = pixelResult;
shifted.fragmentParameters.push_back({0u, 0u, false, false});
rejected(meshResult, shifted, "per-primitive");
auto plain = pixelResult;
const auto decorate = DecorationWord(plain, spv::DecorationPerPrimitiveEXT);
Require(decorate != 0u, "the pixel module has no per-primitive input");
plain.spirv[decorate] = spv::DecorationFlat;
rejected(meshResult, plain, "mesh shading");
}
ShaderRecompiler::RecompileResult Draw(AgcDriver::VulkanDevice& device, std::uint32_t inputs) {
Pixels.fill(Unwritten);
auto target = device.Target();
target.fragmentShaderBarycentricEnabled = false;
const AgcDriver::Pm4::DrawParameters draw{0, static_cast<std::uint32_t>(Vertices.size()), 0, 1, 0, false};
std::vector<std::uint32_t> meshUserData(12, 0u);
const auto index = AgcDriver::Graphics::MeshIndexBufferDescriptor(draw);
std::copy(index.begin(), index.end(), meshUserData.begin() + ShaderRecompiler::MeshIndexBufferUserWord);
const auto vertexBuffer = VertexBufferDescriptor(Vertices.data(), static_cast<std::uint32_t>(Vertices.size()));
std::copy(vertexBuffer.begin(), vertexBuffer.end(), meshUserData.begin() + 8);
const std::array<ShaderRecompiler::MemoryRegion, 1> meshMemory{{{reinterpret_cast<std::uintptr_t>(MeshCode.data()), std::as_bytes(std::span(MeshCode))}}};
ShaderRecompiler::RecompileRequest mesh{
{ShaderStage::Mesh, reinterpret_cast<std::uintptr_t>(MeshCode.data()), MeshCode, 0, {}},
{64u, 0, meshUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, meshMemory},
target,
{0, 0, 0, ShaderRecompiler::MeshDrawPushOffsetBytes},
ShaderRecompiler::GraphicsCompileContext{0, {}, SmallSubgroup, std::nullopt, {draw.indexAddress, draw.indexCount, draw.indexSize, draw.instanceCount}}
};
mesh.useCache = false;
const auto meshResult = ShaderRecompiler::Recompile(mesh);
const auto meshPush = static_cast<std::uint32_t>(meshResult.pushConstants.size());
ShaderRecompiler::ShaderPixelStageInfo pixel{};
pixel.interpolatorCount = 1;
pixel.inputAddr = inputs;
pixel.hasPerspectiveCenterVgpr = (inputs & PixelInputBit(PixelInput::PerspectiveCenter)) != 0u;
pixel.noPerspective = (inputs & PixelInputBit(PixelInput::LinearCenter)) != 0u;
pixel.executeOnNoop = true;
pixel.targetOutputMode[0] = 9;
pixel.targetExportMapping.fill(0xe4u);
const std::vector<std::uint32_t> pixelUserData;
const std::array<ShaderRecompiler::MemoryRegion, 1> pixelMemory{{{reinterpret_cast<std::uintptr_t>(BarycentricPixelCode.data()), std::as_bytes(std::span(BarycentricPixelCode))}}};
ShaderRecompiler::RecompileRequest fragment{
{ShaderStage::Fragment, reinterpret_cast<std::uintptr_t>(BarycentricPixelCode.data()), BarycentricPixelCode, 0, {}},
{64u, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory},
target,
PixelPushLayout(meshPush, target),
ShaderRecompiler::GraphicsCompileContext{0, {}, SmallSubgroup, std::nullopt, {draw.indexAddress, draw.indexCount, draw.indexSize, draw.instanceCount}}
};
fragment.useCache = false;
auto pixelResult = ShaderRecompiler::Recompile(fragment);
Require(pixelResult.barycentricEmulation.active, "the pixel shader did not emulate barycentrics");
mesh.graphics->meshBarycentrics = ShaderRecompiler::MeshBarycentricLayoutOf(pixelResult);
const auto pairedMesh = ShaderRecompiler::Recompile(mesh);
Require(pairedMesh.meshBarycentrics == mesh.graphics->meshBarycentrics, "the paired mesh does not record the layout it was compiled for");
Require(pairedMesh.pushConstants.size() == meshPush && pairedMesh.parameterExports == meshResult.parameterExports, "the paired mesh changed the push constants or exports the pixel shader was built for");
const std::array<AgcDriver::Graphics::CompiledShader, 2> shaders{{
{ShaderStage::Mesh, &pairedMesh, 0},
{ShaderStage::Fragment, &pixelResult, PixelPushOffset(meshPush, target)}
}};
AgcDriver::Graphics::State state{};
state.stages = {AgcDriver::Graphics::ShaderPath::Geometry, 0x20u, 64u, 64u, SmallSubgroup, std::nullopt};
state.color = {reinterpret_cast<std::uintptr_t>(Pixels.data()), {Width, Height}, VK_FORMAT_R32G32B32A32_SFLOAT, sizeof(Pixels), 0xe4u, AgcDriver::Graphics::ColorTileMode::Linear, 16u};
state.colors = {state.color};
state.hasColorTarget = true;
state.renderExtent = {Width, Height};
state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
state.viewport = {0, static_cast<float>(Height), static_cast<float>(Width), -static_cast<float>(Height), 0, 1};
state.negativeOneToOne = false;
state.scissor = {{0, 0}, {Width, Height}};
state.cullMode = VK_CULL_MODE_NONE;
state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
state.blend.colorWriteMask = 15;
state.blends = {state.blend};
state.blendConstants = {};
CheckValidator(device, draw, state, pairedMesh, pixelResult, PixelPushOffset(meshPush, target));
device.Draw(state, draw, shaders);
device.WaitIdle();
return pixelResult;
}
struct Expected {
bool inside;
std::array<float, 3> linear;
std::array<float, 3> perspective;
};
Expected Barycentrics(std::uint32_t x, std::uint32_t y, bool snapped) {
std::array<std::array<double, 2>, 3> window{};
for (std::size_t i = 0; i < Corners.size(); ++i) window[i] = {(Corners[i].x + 1.0) * Width / 2.0, (1.0 - Corners[i].y) * Height / 2.0};
if (snapped) {
for (auto& corner : window) {
for (auto& coordinate : corner) coordinate = std::round(coordinate * 256.0) / 256.0;
}
}
const double px = x + 0.5;
const double py = y + 0.5;
const auto edge = [](const std::array<double, 2>& a, const std::array<double, 2>& b, double cx, double cy) { return (b[0] - a[0]) * (cy - a[1]) - (b[1] - a[1]) * (cx - a[0]); };
const double area = edge(window[0], window[1], window[2][0], window[2][1]);
const std::array<double, 3> b{edge(window[1], window[2], px, py) / area, edge(window[2], window[0], px, py) / area, edge(window[0], window[1], px, py) / area};
Expected expected{std::min({b[0], b[1], b[2]}) > 0.02, {}, {}};
double sum = 0.0;
for (std::size_t i = 0; i < 3; ++i) sum += b[i] / Corners[i].w;
for (std::size_t i = 0; i < 3; ++i) {
expected.linear[i] = static_cast<float>(b[i]);
expected.perspective[i] = static_cast<float>(b[i] / Corners[i].w / sum);
}
return expected;
}
bool MatchesEither(float value, float unsnapped, float snapped) {
return std::fabs(value - unsnapped) <= 1e-4f || std::fabs(value - snapped) <= 1e-4f;
}
float Pixel(std::uint32_t x, std::uint32_t y, std::uint32_t channel) {
return Pixels[(y * Width + x) * 4u + channel];
}
std::uint32_t CheckBarycentrics(const std::string& what, bool perspective) {
std::uint32_t checked = 0;
for (std::uint32_t y = 0; y < Height; ++y) {
for (std::uint32_t x = 0; x < Width; ++x) {
const auto expected = Barycentrics(x, y, false);
if (!expected.inside) continue;
const auto snapped = Barycentrics(x, y, true);
const auto& weights = perspective ? expected.perspective : expected.linear;
const auto& snappedWeights = perspective ? snapped.perspective : snapped.linear;
for (std::uint32_t channel = 0; channel < 4; ++channel) {
const auto value = Pixel(x, y, channel);
const auto reference = weights[1u + (channel & 1u)];
const auto snappedReference = snappedWeights[1u + (channel & 1u)];
Require(MatchesEither(value, reference, snappedReference), what + ": pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") channel " + std::to_string(channel) + " is " + std::to_string(value) + ", expected " + std::to_string(reference) + " or " + std::to_string(snappedReference));
}
++checked;
}
}
Require(checked > 200u, what + ": too few interior pixels were checked");
return checked;
}
}
int main() {
try {
const auto device = OpenVulkanTestDevice();
if (!device) return VulkanTestSkipped;
if (!device->Target().mesh.has_value()) {
std::puts("skipped, the device has no VK_EXT_mesh_shader");
return VulkanTestSkipped;
}
if (device->DeviceName().starts_with("llvmpipe")) {
std::puts("skipped, the device has no mesh shader compiler that handles these programs (llvmpipe)");
return VulkanTestSkipped;
}
for (std::size_t i = 0; i < Corners.size(); ++i) {
const auto& corner = Corners[i];
Vertices[i] = {{corner.x * corner.w, corner.y * corner.w, 0.5f * corner.w, corner.w}, {0.0f, 0.0f, 0.0f, 1.0f}};
}
const auto perspective = PixelInputBit(PixelInput::PerspectiveCenter);
const auto linear = PixelInputBit(PixelInput::LinearCenter);
auto result = Draw(*device, perspective);
Require(result.barycentricEmulation.smooth && !result.barycentricEmulation.linear, "reading PERSP_CENTER I/J did not emulate perspective barycentrics");
const auto smooth = CheckBarycentrics("mesh PERSP_CENTER I/J", true);
result = Draw(*device, linear);
Require(!result.barycentricEmulation.smooth && result.barycentricEmulation.linear, "reading LINEAR_CENTER I/J did not emulate linear barycentrics");
const auto noPerspective = CheckBarycentrics("mesh LINEAR_CENTER I/J", false);
std::printf("mesh barycentric emulation tests passed: %u perspective and %u linear I/J pixels\n", smooth, noPerspective);
return 0;
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
}
+2
View File
@@ -236,6 +236,8 @@ function(add_shader_recompiler target runtimeTarget)
DebugProbeActive
DepthBitsTextureWidth
HashBytes
LayoutBarycentricEmulation
MeshBarycentricLayoutOf
ImageTableAbi::PoisonCode
ImageTableAbi::PoisonReasonName
ImageTableAbi::TableHeader
+16
View File
@@ -102,11 +102,27 @@ private:
}
static void appendMesh(std::vector<std::uint64_t>& key, const RecompileRequest& request) {
if (request.shader.stage == ShaderStage::Fragment) {
append(key, request.graphics && request.graphics->mesh);
return;
}
if (request.shader.stage != ShaderStage::Mesh) return;
const auto* mesh = request.graphics && request.graphics->mesh ? &*request.graphics->mesh : nullptr;
append(key, mesh != nullptr);
if (mesh == nullptr) return;
for (const auto value : {mesh->inputPrimitive, mesh->primitivesPerGroup, mesh->verticesPerGroup, mesh->maxVertices, mesh->maxPrimitives, mesh->threadsPerGroup, mesh->ldsSizeDwords, mesh->provokingVertex, mesh->esgsItemSize, mesh->outputPrimitiveSize}) append(key, value);
const auto& barycentrics = request.graphics->meshBarycentrics;
append(key, barycentrics.has_value());
if (!barycentrics.has_value()) return;
append(key, barycentrics->emulation.active);
append(key, barycentrics->emulation.smooth);
append(key, barycentrics->emulation.linear);
append(key, barycentrics->parameters.size());
for (const auto& parameter : barycentrics->parameters) {
append(key, parameter.location);
append(key, parameter.sourceLocation);
append(key, parameter.perVertex);
}
}
static void appendTessellation(std::vector<std::uint64_t>& key, const RecompileRequest& request) {
@@ -749,6 +749,19 @@ void writeGraphicsCompileContext(Writer& writer, const GraphicsCompileContext& g
writer.WriteU32(graphics.draw.indexCount);
writer.WriteU32(graphics.draw.indexElementBytes);
writer.WriteU32(graphics.draw.instanceCount);
writer.WriteBool(graphics.meshBarycentrics.has_value());
if (graphics.meshBarycentrics.has_value()) {
const auto& barycentrics = *graphics.meshBarycentrics;
writer.WriteBool(barycentrics.emulation.active);
writer.WriteBool(barycentrics.emulation.smooth);
writer.WriteBool(barycentrics.emulation.linear);
writer.WriteU64(barycentrics.parameters.size());
for (const auto& parameter : barycentrics.parameters) {
writer.WriteU32(parameter.location);
writer.WriteU32(parameter.sourceLocation);
writer.WriteBool(parameter.perVertex);
}
}
}
GraphicsCompileContext readGraphicsCompileContext(Reader& reader, DeserializedGraphicsCompileContext& storage, std::uint32_t version) {
@@ -787,6 +800,22 @@ GraphicsCompileContext readGraphicsCompileContext(Reader& reader, DeserializedGr
storage.draw.indexElementBytes = reader.ReadU32();
storage.draw.instanceCount = reader.ReadU32();
graphics.draw = storage.draw;
if (version >= 38u && reader.ReadBool()) {
MeshBarycentricLayout barycentrics;
barycentrics.emulation.active = reader.ReadBool();
barycentrics.emulation.smooth = reader.ReadBool();
barycentrics.emulation.linear = reader.ReadBool();
const auto count = reader.ReadU64();
reader.Require(count, 9u);
for (std::uint64_t i = 0; i < count; i++) {
FragmentParameter parameter{};
parameter.location = reader.ReadU32();
parameter.sourceLocation = reader.ReadU32();
parameter.perVertex = reader.ReadBool();
barycentrics.parameters.push_back(parameter);
}
graphics.meshBarycentrics = std::move(barycentrics);
}
return graphics;
}
@@ -796,7 +825,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const
std::string buffer;
Writer writer(buffer);
writer.WriteU32(0x41505335u);
writer.WriteU32(37u);
writer.WriteU32(38u);
writeShaderBinary(writer, request.shader);
writeGuestContext(writer, request.context);
writeSpirvTarget(writer, request.target);
@@ -835,7 +864,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 > 37u) throw std::runtime_error("unsupported recompile request serialization version");
if (version < 1u || version > 38u) 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);
+23
View File
@@ -571,6 +571,17 @@ CompiledVariant emitVariant(const RecompileRequest& request, IrProgram& program)
targetOptions.spannedWaves = request.target.spannedWaves;
targetOptions.robustBufferAccess2 = request.target.robustBufferAccess2;
targetOptions.float32FlushToZero = request.target.float32FlushToZero;
targetOptions.meshBarycentricPixel = request.shader.stage == ShaderStage::Fragment && request.graphics && request.graphics->mesh;
if (request.shader.stage == ShaderStage::Mesh && request.graphics && request.graphics->meshBarycentrics) {
const auto& paired = *request.graphics->meshBarycentrics;
const auto emulated = DescribeEmulatedInterface(paired.parameters, paired.emulation, true);
const auto& layout = emulated.layout;
targetOptions.meshBarycentricVertex = true;
targetOptions.barycentricSmoothLocation = layout.smoothLocation;
targetOptions.barycentricLinearLocation = layout.linearLocation;
targetOptions.barycentricPositionsLocation = layout.positionsLocation;
targetOptions.meshExportedLocations = emulated.passed;
}
constexpr SpirvEmitter spirvEmitter;
CompiledShaderArtifact result;
@@ -591,6 +602,7 @@ CompiledVariant emitVariant(const RecompileRequest& request, IrProgram& program)
result.spirv = spirvEmitter.Emit(program, inputInfo, bindings, targetOptions);
}
result.registrationModule = registration;
if (request.shader.stage == ShaderStage::Mesh && request.graphics) result.meshBarycentrics = request.graphics->meshBarycentrics;
result.stencilReplaySafe = (request.shader.stage == ShaderStage::Vertex || stencilReplay) && StencilReplaySafe(program);
if (stencilReplay) {
result.stencilReferences = StencilReferenceValues(program, request.context.pixel.has_value() && request.context.pixel->stencilOperationValueExport ? 8u : 0u);
@@ -935,6 +947,7 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil
.parameterExports = artifact.parameterExports,
.fragmentParameters = artifact.fragmentParameters,
.barycentricEmulation = artifact.barycentricEmulation,
.meshBarycentrics = artifact.meshBarycentrics,
.stencilReferences = artifact.stencilReferences,
.stencilReplaySafe = artifact.stencilReplaySafe,
.stencilReferenceDword = artifact.stencilReferenceDword,
@@ -1094,6 +1107,16 @@ bool sameLayout(const BindingLayout& left, const BindingLayout& right) {
}
std::optional<MeshBarycentricLayout> MeshBarycentricLayoutOf(const RecompileResult& fragment) {
if (!fragment.barycentricEmulation.active) return std::nullopt;
MeshBarycentricLayout layout;
layout.emulation = fragment.barycentricEmulation;
for (const auto& parameter : fragment.fragmentParameters) {
layout.parameters.push_back({parameter.location, parameter.sourceLocation, false, parameter.perVertex});
}
return layout;
}
struct PreparedInterface {
static CompiledVariant Compile(SourceEntry& source, const RecompileRequest& request, ShaderPreparationContext& preparation, std::exception_ptr* emissionFailure) {
if (preparation.interfaceSource.get() != &source || preparation.interfaceProgram->Resources().shaderHash != request.shader.codeAddress) {
+46 -17
View File
@@ -365,12 +365,37 @@ struct GraphicsDrawParameters {
std::uint32_t instanceCount;
};
struct FragmentParameter {
std::uint32_t location;
std::uint32_t sourceLocation;
bool flat;
bool perVertex;
bool custom = false;
bool flatP10 = false;
bool vertexOrder = false;
bool operator==(const FragmentParameter&) const = default;
};
struct BarycentricEmulation {
bool active = false;
bool smooth = false;
bool linear = false;
bool operator==(const BarycentricEmulation&) const = default;
};
struct MeshBarycentricLayout {
BarycentricEmulation emulation;
std::vector<FragmentParameter> parameters;
bool operator==(const MeshBarycentricLayout&) const = default;
};
struct GraphicsCompileContext {
std::uint32_t firstUserSgpr;
std::span<const LinkedProgram> linkedPrograms;
std::optional<MeshConfiguration> mesh;
std::optional<TessellationConfiguration> tessellation;
GraphicsDrawParameters draw;
std::optional<MeshBarycentricLayout> meshBarycentrics;
};
struct RecompileRequest {
@@ -474,30 +499,31 @@ struct VertexAttribute {
std::uint32_t formatComponents = 0;
};
struct FragmentParameter {
std::uint32_t location;
std::uint32_t sourceLocation;
bool flat;
bool perVertex;
bool custom = false;
bool flatP10 = false;
bool vertexOrder = false;
};
struct BarycentricEmulation {
bool active = false;
bool smooth = false;
bool linear = false;
};
struct BarycentricEmulationLayout {
static constexpr std::uint32_t NoLocation = 0xffffffffu;
std::uint32_t smoothLocation = NoLocation;
std::uint32_t linearLocation = NoLocation;
std::uint32_t positionsLocation = NoLocation;
std::vector<std::pair<std::uint32_t, std::uint32_t>> perVertexLocations;
};
[[nodiscard]] BarycentricEmulationLayout LayoutBarycentricEmulation(std::span<const FragmentParameter> parameters, const BarycentricEmulation& emulation);
[[nodiscard]] BarycentricEmulationLayout LayoutBarycentricEmulation(std::span<const FragmentParameter> parameters, const BarycentricEmulation& emulation, bool meshPositions = false);
struct EmulatedPerVertexParameter {
std::uint32_t sourceLocation;
std::uint32_t location;
};
struct EmulatedInterface {
std::vector<std::uint32_t> passed;
std::vector<bool> passedFlat;
std::vector<EmulatedPerVertexParameter> perVertex;
BarycentricEmulationLayout layout;
std::uint32_t barycentrics = 0;
std::uint32_t sources = 0;
};
[[nodiscard]] EmulatedInterface DescribeEmulatedInterface(std::span<const FragmentParameter> parameters, const BarycentricEmulation& emulation, bool meshPositions = false);
// A 128-bit hash of a module's words (SharedSpirv::ContentHash), zero for an empty module. The
// mixing is pure integer arithmetic over the words in order, so the value is the same on every run,
@@ -636,6 +662,7 @@ struct CompiledShaderArtifact {
std::vector<std::uint32_t> parameterExports;
std::vector<FragmentParameter> fragmentParameters;
BarycentricEmulation barycentricEmulation;
std::optional<MeshBarycentricLayout> meshBarycentrics;
std::vector<std::uint32_t> stencilReferences;
bool stencilReplaySafe = false;
std::uint32_t stencilReferenceDword = 0xffffffffu;
@@ -696,6 +723,8 @@ struct RecompileResult : CompiledShaderArtifact, ShaderInvocation {
}
};
[[nodiscard]] std::optional<MeshBarycentricLayout> MeshBarycentricLayoutOf(const RecompileResult& fragment);
[[nodiscard]] RecompileResult Recompile(const RecompileRequest& request);
struct ResourceCapture;
+28 -2
View File
@@ -84,9 +84,9 @@ std::filesystem::path ShaderCacheDirectory() {
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(CompiledShaderArtifact) == 280, "CompiledShaderArtifact changed: update the artifact encoder");
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(RecompileResult) == 520, "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");
@@ -504,6 +504,17 @@ void encodeArtifact(Writer& writer, const CompiledShaderArtifact& result) {
writer.Value(result.barycentricEmulation.active);
writer.Value(result.barycentricEmulation.smooth);
writer.Value(result.barycentricEmulation.linear);
writer.Value(result.meshBarycentrics.has_value());
if (result.meshBarycentrics) {
writer.Value(result.meshBarycentrics->emulation.active);
writer.Value(result.meshBarycentrics->emulation.smooth);
writer.Value(result.meshBarycentrics->emulation.linear);
writer.List(result.meshBarycentrics->parameters, [](Writer& out, const FragmentParameter& parameter) {
out.Value(parameter.location);
out.Value(parameter.sourceLocation);
out.Value(parameter.perVertex);
});
}
writer.Values(std::span<const std::uint32_t>(result.stencilReferences));
writer.Value(result.stencilReplaySafe);
writer.Value(result.stencilReferenceDword);
@@ -551,6 +562,21 @@ void decodeArtifact(Reader& reader, CompiledShaderArtifact& result) {
reader.Value(result.barycentricEmulation.active);
reader.Value(result.barycentricEmulation.smooth);
reader.Value(result.barycentricEmulation.linear);
bool hasMeshBarycentrics = false;
reader.Value(hasMeshBarycentrics);
result.meshBarycentrics.reset();
if (hasMeshBarycentrics) {
MeshBarycentricLayout layout;
reader.Value(layout.emulation.active);
reader.Value(layout.emulation.smooth);
reader.Value(layout.emulation.linear);
reader.List(layout.parameters, 9, [](Reader& in, FragmentParameter& parameter) {
in.Value(parameter.location);
in.Value(parameter.sourceLocation);
in.Value(parameter.perVertex);
});
result.meshBarycentrics = std::move(layout);
}
reader.Values(result.stencilReferences);
reader.Value(result.stencilReplaySafe);
reader.Value(result.stencilReferenceDword);
+1 -1
View File
@@ -13,7 +13,7 @@
namespace ShaderRecompiler::ShaderDiskCache {
inline constexpr std::uint32_t FormatVersion = 40;
inline constexpr std::uint32_t FormatVersion = 41;
enum class LoadStatus {
Loaded,
@@ -29,6 +29,12 @@ struct SpirvTargetOptions {
bool spannedWaves = false;
bool robustBufferAccess2 = false;
bool float32FlushToZero = false;
bool meshBarycentricPixel = false;
bool meshBarycentricVertex = false;
std::uint32_t barycentricSmoothLocation = 0xffffffffu;
std::uint32_t barycentricLinearLocation = 0xffffffffu;
std::uint32_t barycentricPositionsLocation = 0xffffffffu;
std::vector<std::uint32_t> meshExportedLocations;
};
struct SpirvEmitInfo {
@@ -112,6 +112,18 @@ struct SpirvEmitterState {
bool float16DenormPreserve = false;
bool robustBufferAccess2 = false;
bool float32FlushToZero = false;
bool meshBarycentricPixel = false;
bool meshBarycentricVertex = false;
std::uint32_t barycentricSmoothLocation = 0xffffffffu;
std::uint32_t barycentricLinearLocation = 0xffffffffu;
std::uint32_t barycentricPositionsLocation = 0xffffffffu;
std::vector<std::uint32_t> meshExportedLocations;
std::uint32_t meshPositionStaging = 0;
std::uint32_t meshClipSmooth = 0;
std::uint32_t meshClipLinear = 0;
std::uint32_t meshFlatPositions = 0;
std::uint32_t pixelFlatPositions = 0;
std::map<std::pair<std::uint32_t, std::uint32_t>, std::uint32_t> solvedBarycentrics;
std::unordered_set<const IrValue*> sharedLaneValues;
std::uint32_t laneHalf = 0;
std::uint32_t spirvVersion = 0x00010300u;
@@ -20,41 +20,14 @@ void require(bool condition, const std::string& message) {
if (!condition) throw std::runtime_error("Barycentric geometry SPIR-V: " + message);
}
struct PerVertexParameter {
std::uint32_t sourceLocation;
std::uint32_t location;
};
struct EmulatedInterface {
std::vector<std::uint32_t> passed;
std::vector<bool> passedFlat;
std::vector<PerVertexParameter> perVertex;
BarycentricEmulationLayout layout;
std::uint32_t barycentrics = 0;
std::uint32_t sources = 0;
};
using PerVertexParameter = EmulatedPerVertexParameter;
EmulatedInterface CollectEmulatedInterface(const RecompileResult& vertex, const RecompileResult& fragment) {
require(fragment.barycentricEmulation.active, "the fragment shader does not emulate barycentrics");
const auto exported = [&](std::uint32_t location) { return std::find(vertex.parameterExports.begin(), vertex.parameterExports.end(), location) != vertex.parameterExports.end(); };
EmulatedInterface emulated;
emulated.layout = LayoutBarycentricEmulation(fragment.fragmentParameters, fragment.barycentricEmulation);
for (const auto& parameter : fragment.fragmentParameters) {
require(exported(parameter.sourceLocation), "fragment parameter " + std::to_string(parameter.location) + " is not exported by the vertex shader");
if (!parameter.perVertex) {
emulated.passed.push_back(parameter.sourceLocation);
emulated.passedFlat.push_back(parameter.flat);
continue;
}
const auto relocated = std::find_if(emulated.layout.perVertexLocations.begin(), emulated.layout.perVertexLocations.end(), [&](const auto& entry) { return entry.first == parameter.location; });
require(relocated != emulated.layout.perVertexLocations.end(), "per-vertex parameter " + std::to_string(parameter.location) + " has no emulated location");
emulated.perVertex.push_back({parameter.sourceLocation, relocated->second});
require(std::find(vertex.parameterExports.begin(), vertex.parameterExports.end(), parameter.sourceLocation) != vertex.parameterExports.end(), "fragment parameter " + std::to_string(parameter.location) + " is not exported by the vertex shader");
}
emulated.barycentrics = (emulated.layout.smoothLocation != BarycentricEmulationLayout::NoLocation ? 1u : 0u) + (emulated.layout.linearLocation != BarycentricEmulationLayout::NoLocation ? 1u : 0u);
std::set<std::uint32_t> sources(emulated.passed.begin(), emulated.passed.end());
for (const auto& parameter : emulated.perVertex) sources.insert(parameter.sourceLocation);
emulated.sources = static_cast<std::uint32_t>(sources.size());
return emulated;
return DescribeEmulatedInterface(fragment.fragmentParameters, fragment.barycentricEmulation);
}
class BarycentricGeometryEmitter {
@@ -378,7 +351,7 @@ private:
}
BarycentricEmulationLayout LayoutBarycentricEmulation(std::span<const FragmentParameter> parameters, const BarycentricEmulation& emulation) {
BarycentricEmulationLayout LayoutBarycentricEmulation(std::span<const FragmentParameter> parameters, const BarycentricEmulation& emulation, bool meshPositions) {
BarycentricEmulationLayout layout;
if (!emulation.active) return layout;
std::array<bool, ParameterLocations> used {};
@@ -396,12 +369,33 @@ BarycentricEmulationLayout LayoutBarycentricEmulation(std::span<const FragmentPa
};
if (emulation.smooth) layout.smoothLocation = take(1u);
if (emulation.linear) layout.linearLocation = take(1u);
if (meshPositions) layout.positionsLocation = take(3u);
for (const auto& parameter : parameters) {
if (parameter.perVertex) layout.perVertexLocations.emplace_back(parameter.location, take(3u));
}
return layout;
}
EmulatedInterface DescribeEmulatedInterface(std::span<const FragmentParameter> parameters, const BarycentricEmulation& emulation, bool meshPositions) {
EmulatedInterface emulated;
emulated.layout = LayoutBarycentricEmulation(parameters, emulation, meshPositions);
for (const auto& parameter : parameters) {
if (!parameter.perVertex) {
emulated.passed.push_back(parameter.sourceLocation);
emulated.passedFlat.push_back(parameter.flat);
continue;
}
const auto relocated = std::find_if(emulated.layout.perVertexLocations.begin(), emulated.layout.perVertexLocations.end(), [&](const auto& entry) { return entry.first == parameter.location; });
require(relocated != emulated.layout.perVertexLocations.end(), "per-vertex parameter " + std::to_string(parameter.location) + " has no emulated location");
emulated.perVertex.push_back({parameter.sourceLocation, relocated->second});
}
emulated.barycentrics = (emulated.layout.smoothLocation != BarycentricEmulationLayout::NoLocation ? 1u : 0u) + (emulated.layout.linearLocation != BarycentricEmulationLayout::NoLocation ? 1u : 0u);
std::set<std::uint32_t> sources(emulated.passed.begin(), emulated.passed.end());
for (const auto& parameter : emulated.perVertex) sources.insert(parameter.sourceLocation);
emulated.sources = static_cast<std::uint32_t>(sources.size());
return emulated;
}
RecompileResult BuildBarycentricGeometryShader(const RecompileResult& vertex, const RecompileResult& fragment, const SpirvTarget& target, const std::optional<GeometryStageLimits>& limits) {
require(fragment.barycentricEmulation.active, "the fragment shader does not emulate barycentrics");
require(limits.has_value(), "geometry shaders are unavailable");
@@ -260,6 +260,12 @@ std::vector<std::uint32_t> SpirvEmitter::Emit(const IrProgram& program, const Sh
state.depthRangeRemap = target.depthRangeRemap;
state.registrationModules = target.registrationModules;
state.fragmentDescriptorSet = target.fragmentDescriptorSet;
state.meshBarycentricPixel = target.meshBarycentricPixel;
state.meshBarycentricVertex = target.meshBarycentricVertex;
state.barycentricSmoothLocation = target.barycentricSmoothLocation;
state.barycentricLinearLocation = target.barycentricLinearLocation;
state.barycentricPositionsLocation = target.barycentricPositionsLocation;
state.meshExportedLocations = target.meshExportedLocations;
state.float16DenormPreserve = target.float16DenormPreserve;
state.robustBufferAccess2 = target.robustBufferAccess2;
state.float32FlushToZero = target.float32FlushToZero;
@@ -218,7 +218,17 @@ void DefineInputs(SpirvEmitterState& state) {
const auto reads = [&](StageInputKind kind) {
return std::any_of(state.inputs.begin(), state.inputs.end(), [&](const SpirvInputBinding& input) { return input.kind == kind; });
};
emulation = LayoutBarycentricEmulation(parameters, {true, reads(StageInputKind::BaryCoordSmooth), reads(StageInputKind::BaryCoordNoPerspective)});
emulation = LayoutBarycentricEmulation(parameters, {true, reads(StageInputKind::BaryCoordSmooth), reads(StageInputKind::BaryCoordNoPerspective)}, state.meshBarycentricPixel);
if (state.meshBarycentricPixel && std::any_of(parameters.begin(), parameters.end(), [](const FragmentParameter& parameter) { return parameter.perVertex; })) {
throw std::runtime_error("SPIR-V module emission failed: per-vertex parameters are not emulated on the mesh path");
}
if (state.meshBarycentricPixel && emulation.positionsLocation != BarycentricEmulationLayout::NoLocation) {
state.pixelFlatPositions = DefineInterfaceVariable(state, state.module.Type(spv::OpTypeArray, TypeF32Vector(state, 4u), ConstantU32(state, 3u)), spv::StorageClassInput, "meshBarycentricPositions");
state.module.AddAnnotation(spv::OpDecorate, state.pixelFlatPositions, spv::DecorationLocation, emulation.positionsLocation);
state.module.AddAnnotation(spv::OpDecorate, state.pixelFlatPositions, spv::DecorationPerPrimitiveEXT);
state.module.EmitCapability(spv::CapabilityMeshShadingEXT);
state.module.EmitExtension("SPV_EXT_mesh_shader");
}
}
for (auto& input : state.inputs) {
if (pixelStage && input.kind == StageInputKind::Parameter) {
@@ -27,6 +27,48 @@ bool UserDataDwordIndex(const SpirvEmitterState& state, ScalarReg reg, std::uint
return true;
}
bool MeshBarycentricPixel(const SpirvEmitterState& state) {
return state.meshBarycentricPixel && state.program.Metadata().barycentricEmulation && state.pixelFlatPositions != 0u;
}
std::uint32_t EmitSolvedBarycentric(SpirvEmitterState& state, StageInputKind kind) {
const auto cached = state.solvedBarycentrics.find({state.currentLabel, static_cast<std::uint32_t>(kind)});
if (cached != state.solvedBarycentrics.end()) return cached->second;
const auto vec3 = TypeF32Vector(state, 3u);
const auto varying = state.module.AllocateId();
state.module.AddFunction(spv::OpLoad, vec3, varying, InputVariableForKind(state, kind));
std::array<std::uint32_t, 3> corner {};
for (std::uint32_t index = 0; index < 3u; index++) {
const auto pointer = state.module.AllocateId();
const auto position = state.module.AllocateId();
corner.at(index) = state.module.AllocateId();
state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassInput, TypeF32Vector(state, 4u)), pointer, state.pixelFlatPositions, ConstantU32(state, index));
state.module.AddFunction(spv::OpLoad, TypeF32Vector(state, 4u), position, pointer);
state.module.AddFunction(spv::OpVectorShuffle, vec3, corner.at(index), position, position, 0u, 1u, 3u);
}
const auto cross = [&](std::uint32_t left, std::uint32_t right) {
const auto result = state.module.AllocateId();
state.module.AddFunction(spv::OpExtInst, vec3, result, GlslStd450(state), GLSLstd450Cross, left, right);
return result;
};
const auto dot = [&](std::uint32_t left, std::uint32_t right) {
const auto result = state.module.AllocateId();
state.module.AddFunction(spv::OpDot, TypeF32(state), result, left, right);
return result;
};
const auto determinant = dot(corner[0], cross(corner[1], corner[2]));
std::array<std::uint32_t, 3> weight {};
const std::array<std::array<std::uint32_t, 2>, 3> rows{{{1u, 2u}, {2u, 0u}, {0u, 1u}}};
for (std::uint32_t index = 0; index < 3u; index++) {
weight.at(index) = state.module.AllocateId();
state.module.AddFunction(spv::OpFDiv, TypeF32(state), weight.at(index), dot(varying, cross(corner[rows[index][0]], corner[rows[index][1]])), determinant);
}
const auto solved = state.module.AllocateId();
state.module.AddFunction(spv::OpCompositeConstruct, vec3, solved, weight[0], weight[1], weight[2]);
state.solvedBarycentrics[{state.currentLabel, static_cast<std::uint32_t>(kind)}] = solved;
return solved;
}
const ShaderWorkgroupInputInfo* ShaderWorkgroupInputFor(const SpirvEmitterState& state) {
switch (state.program.Resources().stage) {
case IrShaderStage::Compute:
@@ -182,6 +224,13 @@ std::uint32_t EmitBuiltinU32(SpirvEmitterState& state, StageInputKind kind, std:
state.module.AddFunction(spv::OpBitcast, TypeU32(state), bits, value);
return bits;
}
if ((kind == StageInputKind::BaryCoordSmooth || kind == StageInputKind::BaryCoordNoPerspective) && MeshBarycentricPixel(state)) {
const auto value = state.module.AllocateId();
const auto bits = state.module.AllocateId();
state.module.AddFunction(spv::OpCompositeExtract, TypeF32(state), value, EmitSolvedBarycentric(state, kind), component + 1u);
state.module.AddFunction(spv::OpBitcast, TypeU32(state), bits, value);
return bits;
}
if (kind == StageInputKind::BaryCoordSmooth || kind == StageInputKind::BaryCoordNoPerspective) {
const auto pointer = state.module.AllocateId();
const auto value = state.module.AllocateId();
@@ -228,12 +277,17 @@ std::uint32_t EmitAttributeValue(SpirvEmitterState& state, std::uint32_t attr, s
throw std::runtime_error("per-vertex attribute interpolation lacks its barycentric input");
}
std::uint32_t sum = 0;
const auto solved = MeshBarycentricPixel(state) ? EmitSolvedBarycentric(state, barycentricKind) : 0u;
for (std::uint32_t vertex = 0; vertex < 3u; vertex++) {
const auto pointer = state.module.AllocateId();
const auto weight = state.module.AllocateId();
const auto product = state.module.AllocateId();
state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassInput, TypeF32(state)), pointer, barycentric, ConstantU32(state, vertex));
state.module.AddFunction(spv::OpLoad, TypeF32(state), weight, pointer);
if (solved != 0u) {
state.module.AddFunction(spv::OpCompositeExtract, TypeF32(state), weight, solved, vertex);
} else {
state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassInput, TypeF32(state)), pointer, barycentric, ConstantU32(state, vertex));
state.module.AddFunction(spv::OpLoad, TypeF32(state), weight, pointer);
}
state.module.AddFunction(spv::OpFMul, TypeF32(state), product, loadPerVertex(vertex), weight);
if (vertex == 0u) {
sum = product;
@@ -876,10 +930,13 @@ void DefineMeshOutputs(SpirvEmitterState& state) {
throw std::runtime_error("unsupported mesh output kind");
}
const auto type = MeshOutputType(state, output.kind);
const auto layerBlock = output.kind == StageOutputKind::Layer ? state.module.DecoratedType(spv::OpTypeStruct, {{spv::OpMemberDecorate, {0u, spv::DecorationBuiltIn, spv::BuiltInLayer}}, {spv::OpMemberDecorate, {0u, spv::DecorationPerPrimitiveEXT}}, {spv::OpDecorate, {spv::DecorationBlock}}}, type) : 0u;
output.variableId = MeshArray(state, spv::StorageClassOutput, output.kind == StageOutputKind::Layer ? layerBlock : type, output.kind == StageOutputKind::Layer ? mesh.maxPrimitives : mesh.maxVertices);
const bool shared = output.kind == StageOutputKind::Layer;
output.meshDataVariable = MeshArray(state, shared ? spv::StorageClassWorkgroup : spv::StorageClassPrivate, type, shared ? mesh.maxVertices : state.laneCount);
if (state.meshBarycentricVertex && output.kind == StageOutputKind::Parameter && std::ranges::find(state.meshExportedLocations, output.location) == state.meshExportedLocations.end()) {
continue;
}
const auto layerBlock = output.kind == StageOutputKind::Layer ? state.module.DecoratedType(spv::OpTypeStruct, {{spv::OpMemberDecorate, {0u, spv::DecorationBuiltIn, spv::BuiltInLayer}}, {spv::OpMemberDecorate, {0u, spv::DecorationPerPrimitiveEXT}}, {spv::OpDecorate, {spv::DecorationBlock}}}, type) : 0u;
output.variableId = MeshArray(state, spv::StorageClassOutput, output.kind == StageOutputKind::Layer ? layerBlock : type, output.kind == StageOutputKind::Layer ? mesh.maxPrimitives : mesh.maxVertices);
state.interfaceVariables.push_back(output.variableId);
state.module.AddName(output.variableId, output.debugName);
if (output.kind == StageOutputKind::Parameter) {
@@ -891,6 +948,33 @@ void DefineMeshOutputs(SpirvEmitterState& state) {
state.module.AddAnnotation(spv::OpDecorate, output.variableId, spv::DecorationInvariant);
}
}
if (state.meshBarycentricVertex) {
if (std::ranges::none_of(state.outputs, [](const SpirvOutputBinding& output) { return output.kind == StageOutputKind::Position; })) {
throw std::runtime_error("barycentric emulation on the mesh path needs the mesh program to export a position");
}
const auto vec3 = TypeF32Vector(state, 3u);
state.meshPositionStaging = MeshArray(state, spv::StorageClassWorkgroup, TypeF32Vector(state, 4u), mesh.maxVertices);
const auto defineClip = [&](std::uint32_t location, bool linear, const char* name) {
if (location == BarycentricEmulationLayout::NoLocation) return 0u;
const auto variable = MeshArray(state, spv::StorageClassOutput, vec3, mesh.maxVertices);
state.interfaceVariables.push_back(variable);
state.module.AddName(variable, name);
state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationLocation, location);
if (linear) state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationNoPerspective);
return variable;
};
state.meshClipSmooth = defineClip(state.barycentricSmoothLocation, false, "meshBarycentricClipSmooth");
state.meshClipLinear = defineClip(state.barycentricLinearLocation, true, "meshBarycentricClipLinear");
if (state.barycentricPositionsLocation == BarycentricEmulationLayout::NoLocation) {
throw std::runtime_error("barycentric emulation on the mesh path has no location for the corner positions");
}
const auto corners = state.module.Type(spv::OpTypeArray, TypeF32Vector(state, 4u), ConstantU32(state, 3u));
state.meshFlatPositions = MeshArray(state, spv::StorageClassOutput, corners, mesh.maxPrimitives);
state.interfaceVariables.push_back(state.meshFlatPositions);
state.module.AddName(state.meshFlatPositions, "meshBarycentricPositions");
state.module.AddAnnotation(spv::OpDecorate, state.meshFlatPositions, spv::DecorationLocation, state.barycentricPositionsLocation);
state.module.AddAnnotation(spv::OpDecorate, state.meshFlatPositions, spv::DecorationPerPrimitiveEXT);
}
state.meshAllocation = MeshArray(state, spv::StorageClassWorkgroup, TypeU32(state), 2u);
state.meshPrimitiveData = MeshArray(state, spv::StorageClassPrivate, TypeU32(state), state.laneCount);
state.meshPrimitives = MeshArray(state, spv::StorageClassOutput, TypeU32Vector(state, mesh.outputPrimitiveSize), mesh.maxPrimitives);
@@ -967,14 +1051,15 @@ void EmitMeshEntryPoint(SpirvEmitterState& state) {
const auto anyVertex = state.module.AllocateId();
state.module.AddFunction(spv::OpExtInst, TypeU32(state), anyVertex, GlslStd450(state), GLSLstd450UMax, vertices, ConstantU32(state, 1u));
const auto lastVertex = EmitBinaryU32(state, spv::OpISub, anyVertex, ConstantU32(state, 1u));
for (std::uint32_t half = 0; half < state.laneCount; half++) {
const bool emulating = state.meshBarycentricVertex;
const auto emitVertexPart = [&](std::uint32_t half) {
state.laneHalf = half;
const auto index = EmitLocalInvocationIndex(state);
const auto isVertex = state.module.AllocateId();
state.module.AddFunction(spv::OpULessThan, TypeBool(state), isVertex, index, vertices);
EmitIfCondition(state, isVertex, [&]() {
for (const auto& output : state.outputs) {
if (output.kind == StageOutputKind::Layer) {
if (output.kind == StageOutputKind::Layer || output.variableId == 0u) {
continue;
}
const auto type = MeshOutputType(state, output.kind);
@@ -982,7 +1067,24 @@ void EmitMeshEntryPoint(SpirvEmitterState& state) {
const auto pointer = IsMeshDistance(output.kind) ? MeshDistancePointer(state, output, index) : MeshElement(state, output.variableId, spv::StorageClassOutput, type, index);
state.module.AddFunction(spv::OpStore, pointer, value);
}
if (!emulating) {
return;
}
const auto position = std::ranges::find_if(state.outputs, [](const SpirvOutputBinding& output) { return output.kind == StageOutputKind::Position; });
const auto value = MeshLoad(state, position->meshDataVariable, spv::StorageClassPrivate, TypeF32Vector(state, 4u), ConstantU32(state, half));
state.module.AddFunction(spv::OpStore, MeshElement(state, state.meshPositionStaging, spv::StorageClassWorkgroup, TypeF32Vector(state, 4u), index), value);
const auto clip = state.module.AllocateId();
state.module.AddFunction(spv::OpVectorShuffle, TypeF32Vector(state, 3u), clip, value, value, 0u, 1u, 3u);
for (const auto variable : {state.meshClipSmooth, state.meshClipLinear}) {
if (variable != 0u) {
state.module.AddFunction(spv::OpStore, MeshElement(state, variable, spv::StorageClassOutput, TypeF32Vector(state, 3u), index), clip);
}
}
});
};
const auto emitPrimitivePart = [&](std::uint32_t half) {
state.laneHalf = half;
const auto index = EmitLocalInvocationIndex(state);
const auto isPrimitive = state.module.AllocateId();
state.module.AddFunction(spv::OpULessThan, TypeBool(state), isPrimitive, index, primitives);
EmitIfCondition(state, isPrimitive, [&]() {
@@ -1011,7 +1113,32 @@ void EmitMeshEntryPoint(SpirvEmitterState& state) {
state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassOutput, TypeU32(state)), pointer, output.variableId, index, ConstantU32(state, 0u));
state.module.AddFunction(spv::OpStore, pointer, layer);
}
if (emulating) {
const auto corners = state.module.Type(spv::OpTypeArray, TypeF32Vector(state, 4u), ConstantU32(state, 3u));
std::array<std::uint32_t, 3> staged {};
for (std::uint32_t component = 0; component < 3u; component++) {
staged.at(component) = MeshLoad(state, state.meshPositionStaging, spv::StorageClassWorkgroup, TypeF32Vector(state, 4u), vertex.at(component));
}
const auto array = state.module.AllocateId();
state.module.AddFunction(spv::OpCompositeConstruct, corners, array, staged[0], staged[1], staged[2]);
state.module.AddFunction(spv::OpStore, MeshElement(state, state.meshFlatPositions, spv::StorageClassOutput, corners, index), array);
}
});
};
if (!emulating) {
for (std::uint32_t half = 0; half < state.laneCount; half++) {
emitVertexPart(half);
emitPrimitivePart(half);
}
} else {
for (std::uint32_t half = 0; half < state.laneCount; half++) {
emitVertexPart(half);
}
state.laneHalf = 0;
state.module.AddFunction(spv::OpControlBarrier, ConstantU32(state, spv::ScopeWorkgroup), ConstantU32(state, spv::ScopeWorkgroup), ConstantU32(state, spv::MemorySemanticsAcquireReleaseMask | spv::MemorySemanticsWorkgroupMemoryMask));
for (std::uint32_t half = 0; half < state.laneCount; half++) {
emitPrimitivePart(half);
}
}
state.laneHalf = 0;
state.module.AddFunction(spv::OpReturn);
@@ -45,6 +45,13 @@ void Require(bool condition) {
if (!condition) std::abort();
}
void InsertMeshBarycentricFlag(std::vector<std::uint8_t>& bytes) {
constexpr std::array<std::uint8_t, 20> draw{0x00, 0x34, 0xef, 0xcd, 0xab, 0x00, 0x00, 0x00, 57u, 0u, 0u, 0u, 4u, 0u, 0u, 0u, 9u, 0u, 0u, 0u};
const auto at = std::search(bytes.begin(), bytes.end(), draw.begin(), draw.end());
Require(at != bytes.end());
bytes.insert(at + static_cast<std::ptrdiff_t>(draw.size()), 0u);
}
void InsertPixelDefaultsAfterFrontFace(std::vector<std::uint8_t>& bytes) {
bytes.insert(bytes.begin() + 261u, {0x0fu, 0xffu, 0xffu, 0u, 0u, 0xffu, 0xffu, 0u, 0u, 0xffu, 0xffu, 0u, 0u, 0xffu, 0xffu, 0u, 0u, 0u, 0u, 0u, 0u});
bytes.insert(bytes.begin() + 282u, 35u, 0u);
@@ -207,7 +214,8 @@ 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] = 37u;
upgraded[4] = 38u;
InsertMeshBarycentricFlag(upgraded);
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);
@@ -246,7 +254,8 @@ int main() {
layerRejected = std::string_view(error.what()) == "RequestSerializer: truncated data";
}
Require(layerRejected);
layered[4] = 37u;
layered[4] = 38u;
InsertMeshBarycentricFlag(layered);
layered.insert(layered.begin() + 259u, {0u, 0u});
InsertPixelDefaultsAfterFrontFace(layered);
if (fixture.tessellation) layered.insert(layered.end() - 35u, 5u, 0u);
@@ -271,7 +280,8 @@ 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] = 37u;
bytes[4] = 38u;
InsertMeshBarycentricFlag(bytes);
bytes.insert(bytes.begin() + 259u, {0u, 0u});
InsertPixelDefaultsAfterFrontFace(bytes);
bytes.insert(bytes.end(), sizeof(std::uint64_t) + 1u + 5u + 1u, 0u);
@@ -395,7 +405,8 @@ 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] = 37u;
upgraded[4] = 38u;
InsertMeshBarycentricFlag(upgraded);
if (fixture.pixel) upgraded.insert(upgraded.begin() + 259u, {0u, 0u});
if (fixture.pixel) InsertPixelDefaultsAfterFrontFace(upgraded);
upgraded.insert(upgraded.end(), sizeof(std::uint64_t) + 1u + 5u + 1u, 0u);
@@ -408,7 +419,8 @@ 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] = 37u;
main25[4] = 38u;
InsertMeshBarycentricFlag(main25);
if (fixture.pixel) {
main25.insert(main25.begin() + 260u, 0u);
InsertPixelDefaultsAfterFrontFace(main25);
@@ -428,9 +440,9 @@ int main() {
const auto replay = serializer.Deserialize(encoded);
Require(replay.request.target.float16DenormPreserve == preserve && replay.request.target.float32FlushToZero == !preserve);
auto bytes = DecodeBase64(encoded);
Require(bytes.size() >= 8u && bytes[4] == 37u && bytes[5] == 0u && bytes[6] == 0u && bytes[7] == 0u);
Require(bytes.size() >= 8u && bytes[4] == 38u && bytes[5] == 0u && bytes[6] == 0u && bytes[7] == 0u);
auto future = bytes;
future[4] = 38u;
future[4] = 39u;
bool futureRejected = false;
try {
static_cast<void>(serializer.Deserialize(EncodeBase64(future)));
+1
View File
@@ -30,6 +30,7 @@
- [sceAgcDriverGetEqContextId](../../../core/libs/prx/libSceAgcDriver/Eq/src/Query.cpp) (libSceAgcDriver) - returns `ident`: an end-of-pipe interrupt is delivered only to the registration whose id equals the raising queue, so `ident` is that context's id (titles register 0 or 1 for graphics and `0x20 + 8 * pipe + queue` for compute). The console's value is not known; KytyPS5 returns the event's `data`, which it fills with the interrupt's context id, whereas `data` here is the EV_CLEAR trigger count. Other event filters throw
- [User data registers](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp) (AGC driver) - a `SPI_SHADER_USER_DATA_*` or `COMPUTE_USER_DATA_*` register the title never wrote reads as 0 when a stage's RSRC2 count covers it, as Kyty (zeroed `UserSgprInfo`) and SharpEmu (missing registers read 0) do, and as AMD PAL and Mesa initialize them; the console's value is not confirmed
- [SET_PREDICATION](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (AGC driver) - boolean predication follows the AMD PM4 encoding [Mesa](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/common/sid.h) uses: operation 3 reads 64 bits and 4 reads 32 bits, and bit 8 runs predicated packets when the value is non-zero (clear: when it is zero); the bit 12 hint is ignored. The address must have its low 4 bits clear, as `sceAgcDcbSetPredication` writes it. Query predication (Z-pass, primitive count), a predicated COND_EXEC, predicated command buffer chains and flips in predicated command buffers throw
- [Barycentrics without mesh shaders](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp) (AGC driver) - a pixel shader that reads barycentrics on the mesh path is emulated only on a device with `VK_EXT_mesh_shader`; a device with neither it nor `VK_KHR_fragment_shader_barycentric` (reported on Linux RADV with a GCN 5 GPU) throws that the device has no mesh shaders, because a geometry stage cannot follow a mesh stage and no other route is implemented. `VK_AMD_shader_explicit_vertex_parameter`, which a GCN 5 device on RADV exposes, is an unexplored option.
- [NGG vertex programs on the mesh path](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - an unregistered vertex program with `PRIMGEN_EN` that uses LDS or `s_barrier` runs on the geometry (mesh) path. Each input primitive is assumed to yield at most one output primitive, the subgroup is bounded by `GE_MAX_OUTPUT_PER_SUBGROUP` as a vertex budget, and the GS/ES VGPR count checks are skipped, because the merged inputs keep their positions. Taken from the code Mesa's ACO emits for NGG culling and from PPSA59322 and PPSA99010 draws, not measured on hardware. Registered programs keep the vertex path
- [VGT_SHADER_STAGES_EN 0x02002008](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - this one value, `ES_EN` = 1 without `HS_EN` on a non-patch draw, is drawn as the vertex path. AMD's CIK register reference calls `ES_EN` = 1 `ES_STAGE_DS` (the ES stage runs the tessellation evaluation shader), which does not explain it on a non-patch draw. Seen only on PPSA29413 and not verified on the console; other vertex-path values with `ES_EN` set still throw.
- [Strided buffer overhang](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - a strided buffer descriptor that ends less than one stride past the end of the readable registered allocation holding its base is bound only up to that allocation's end, even when another readable allocation starts right there. This assumes the bytes past the end are the rounded-up `num_records` of the last record (RADV rounds it up), which no fetch inside the buffer reads; a guest that really keeps a vertex split across two adjacent allocations would read zeros for the cut tail. Taken from the RetroArch Dolphin core case in #2912, not measured on hardware.