From 218ca18e4281dd242e56dd6cd8ca8cd4eb7f33cf Mon Sep 17 00:00:00 2001 From: JJenkx Date: Fri, 16 Oct 2026 16:34:01 -0400 Subject: [PATCH] fix(agc): render mesh layers within device limits --- .../include/PipelineSpecialization.hpp | 9 ++- .../Graphics/include/State.hpp | 2 + .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 22 ++++++++ .../libSceAgcDriver/Graphics/src/Pipeline.cpp | 6 +- .../Graphics/src/PipelineJournal.cpp | 6 ++ .../libSceAgcDriver/tests/MaskedSelect.cpp | 25 +++++++++ .../libSceAgcDriver/tests/ShaderDiskCache.cpp | 2 + .../tests/execution/LayeredVolumeDraw.cpp | 29 +++++++--- .../recompiler/PipelineSpecialization.hpp | 2 + core/shader/recompiler/Recompiler.cpp | 25 +++++++-- core/shader/recompiler/Recompiler.hpp | 1 + core/shader/recompiler/ShaderDiskCache.cpp | 2 + .../SpirvBackend/src/SpirvModuleEmitter.cpp | 18 +++++- core/shader/recompiler/StencilReplay.cpp | 55 +++++++++++++++++++ core/shader/recompiler/StencilReplay.hpp | 1 + docs/dev/debt/libSceAgcDriver.md | 2 + 16 files changed, 189 insertions(+), 18 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineSpecialization.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineSpecialization.hpp index 37ab9d1daa..d7628355bb 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineSpecialization.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineSpecialization.hpp @@ -52,9 +52,14 @@ public: } } - explicit PipelineSpecialization(const ShaderRecompiler::RecompileResult& shader) { + explicit PipelineSpecialization(const ShaderRecompiler::RecompileResult& shader, std::uint32_t layerBase = 0u, std::uint32_t layerCount = 0xffffffffu) { + values = shader.specialization; + for (auto& constant : values) { + if (constant.id == ShaderRecompiler::PipelineSpecialization::MeshLayerBase) constant.value = layerBase; + if (constant.id == ShaderRecompiler::PipelineSpecialization::MeshLayerCount) constant.value = layerCount; + } Validate(shader); - describe(shader.specialization); + describe(values); } PipelineSpecialization(const ShaderRecompiler::RecompileResult& shader, std::span replaced) : values(shader.specialization) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp index 7e403c1dd8..5e8075ecd2 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -146,6 +146,8 @@ struct State { std::uint32_t reversedBlend = 0; VkExtent2D renderExtent; std::uint32_t renderLayers = 1; + std::uint32_t meshLayerBase = 0; + std::uint32_t meshLayerCount = 0xffffffffu; std::uint32_t layerExports = 0; VkPrimitiveTopology topology; bool primitiveRestart = false; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index aae3553e12..0801ed7592 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -3748,6 +3748,28 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& drawTessellationGeometry(context, state, draw, shaders, snapshots, trace); return; } + if (state.stages.mesh && state.layerExports != 0u && state.renderLayers > context.meshLimits.maxMeshOutputLayers) { + const auto limit = context.meshLimits.maxMeshOutputLayers; + Require(limit != 0u && !state.depth, "mesh layer windows require a nonzero output limit and color-only layered rendering"); + Require(std::ranges::all_of(shaders, [](const CompiledShader& shader) { return shader.program != nullptr && shader.program->layerReplaySafe; }), "mesh layer windows refuse shaders with guest memory writes, clocks or unproven side effects"); + if (recipeOut != nullptr) recipeOut->reset(); + for (std::uint32_t base = 0; base < state.renderLayers;) { + auto window = state; + window.renderLayers = std::min(limit, state.renderLayers - base); + window.meshLayerBase = base; + window.meshLayerCount = window.renderLayers; + for (auto& color : window.colors) { + Require(color.layers == state.renderLayers, "mesh layer window targets have different layer counts"); + if (color.depth > 1u) color.depthSlice += base; + else color.arraySlice += base; + color.layers = window.renderLayers; + } + if (!window.colors.empty()) window.color = window.colors.front(); + Draw(context, window, draw, shaders, snapshots, nullptr, trace); + base += window.renderLayers; + } + return; + } static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; PreparedDraw prepared(profile); prepared.recipeOut = recipeOut; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index 0063fd6382..ba8c114f75 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -340,6 +340,8 @@ bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State& appendKey(key, identity.low); appendKey(key, identity.high); appendKey(key, shader.pushConstantOffset); + appendKey(key, state.meshLayerBase); + appendKey(key, state.meshLayerCount); if (resources.split) { auto& sets = shader.stage == Stage::Fragment ? keys.fragmentSets : keys.preRasterizationSets; for (const auto& binding : shader.program->bindings) { @@ -509,7 +511,7 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput stages[i].module = _modules[i]; stages[i].pName = "main"; if (shaders[i].stage == ShaderRecompiler::ShaderStage::Mesh && context.meshWave32 && shader.hostSubgroupSize == 32u) stages[i].pNext = &meshSubgroup; - specializations.emplace_back(shader); + specializations.emplace_back(shader, state.meshLayerBase, state.meshLayerCount); stages[i].pSpecializationInfo = specializations.back().Info(); } if (std::ranges::any_of(shaders, [](const CompiledShader& shader) { return shader.program != nullptr && !shader.program->stencilReferences.empty(); })) { @@ -1016,6 +1018,8 @@ void pipelineKey(std::vector& key, const Context& context, const Stat // Where the stage's push constants sit in the block (AssemblePushConstants). append(key, shader.pushConstantOffset); } + append(key, state.meshLayerBase); + append(key, state.meshLayerCount); append(key, PushConstantStages(shaders)); append(key, input.bindings.size()); for (const auto& binding : input.bindings) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp index e6718d9473..f414bcb33d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp @@ -180,6 +180,8 @@ void encodeState(Writer& writer, const State& state) { writer.U32(static_cast(state.stages.path)); writer.U32(state.stages.vertexWaveSize); writer.U32(state.stages.fragmentWaveSize); + writer.U32(state.meshLayerBase); + writer.U32(state.meshLayerCount); encodeOptional(writer, state.stages.mesh, encodeMesh); encodeOptional(writer, state.stages.tessellation, encodeTessellation); writer.U32(state.stages.hullWaveSize); @@ -244,6 +246,8 @@ void decodeState(Reader& reader, State& state) { state.stages.path = static_cast(reader.U32()); state.stages.vertexWaveSize = reader.U32(); state.stages.fragmentWaveSize = reader.U32(); + state.meshLayerBase = reader.U32(); + state.meshLayerCount = reader.U32(); if (reader.Bool()) decodeMesh(reader, state.stages.mesh.emplace()); if (reader.Bool()) decodeTessellation(reader, state.stages.tessellation.emplace()); state.stages.hullWaveSize = reader.U32(); @@ -1518,6 +1522,8 @@ State CanonicalPipelineState(const State& state) { canonical.stages.path = state.stages.path; canonical.stages.vertexWaveSize = state.stages.vertexWaveSize; canonical.stages.fragmentWaveSize = state.stages.fragmentWaveSize; + canonical.meshLayerBase = state.meshLayerBase; + canonical.meshLayerCount = state.meshLayerCount; canonical.stages.mesh = state.stages.mesh; canonical.stages.tessellation = state.stages.tessellation; canonical.stages.hullWaveSize = state.stages.hullWaveSize; diff --git a/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp b/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp index ed1d9185b3..14321ee8ca 100644 --- a/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp @@ -1737,5 +1737,30 @@ int main() { return small.removedSelects == 500u && smallKept == 500u && large.removedSelects == 4000u && largeKept == 4000u && small.steps != 0u && large.steps <= 9u * small.steps && large.steps < 90'000u; }); + passed &= run("layer replay permits local LDS but rejects GDS access", [] { + Builder b; + b.program.Resources().stage = IrShaderStage::Mesh; + MemoryInfo memory{}; + memory.kind = ResourceKind::Lds; + b.program.Resources().memoryInfo.push_back(memory); + b.emit(IrOpcode::StoreShared, IrType::Void, {&b.constant(0u), &b.constant(1u)}); + if (!LayerReplaySafe(b.program)) return false; + b.program.Resources().memoryInfo.front().kind = ResourceKind::Gds; + return !LayerReplaySafe(b.program); + }); + + passed &= run("layer replay rejects guest buffer stores and shader clocks", [] { + Builder written; + auto& buffer = written.emit(IrOpcode::GetBufferResource, IrType::BufferResource, {}); + written.emit(IrOpcode::StoreBuffer, IrType::Void, {&buffer, &written.constant(0u), &written.constant(1u)}); + if (LayerReplaySafe(written.program)) return false; + for (const auto opcode : {IrOpcode::ShaderClock, IrOpcode::RealtimeClock}) { + Builder clock; + clock.emit(opcode, IrType::U64, {}); + if (LayerReplaySafe(clock.program)) return false; + } + return true; + }); + return passed ? 0 : 1; } diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index 814b5a78f0..b62d1c8724 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -174,6 +174,7 @@ void requireSameArtifact(const CompiledShaderArtifact& left, const CompiledShade require(left.stencilReferences == right.stencilReferences, prefix + "stencil reference set differs"); require(left.stencilReplaySafe == right.stencilReplaySafe, prefix + "stencil replay proof differs"); require(left.bdaWrites == right.bdaWrites, prefix + "BDA write flag differs"); + require(left.layerReplaySafe == right.layerReplaySafe, prefix + "layer replay proof differs"); require(left.stencilReferenceDword == right.stencilReferenceDword, prefix + "stencil replay push offset differs"); require(left.fragmentParameters.size() == right.fragmentParameters.size(), prefix + "fragment parameter count differs"); for (std::size_t i = 0; i < left.fragmentParameters.size(); ++i) { @@ -287,6 +288,7 @@ RecompileResult sampleResult() { result.stencilReferences = {0u, 17u, 255u}; result.stencilReplaySafe = true; result.bdaWrites = true; + result.layerReplaySafe = true; result.stencilReferenceDword = 23u; result.poisonedSrtReads = 3; result.shaderDataUserWords = {2u, 0xffffffffu, 9u}; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/LayeredVolumeDraw.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LayeredVolumeDraw.cpp index 351c5de506..289cd49e25 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/LayeredVolumeDraw.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LayeredVolumeDraw.cpp @@ -33,10 +33,10 @@ using AgcDriver::Graphics::ColorTileMode; using AgcDriver::Graphics::Require; using ShaderRecompiler::ShaderStage; -constexpr std::uint32_t Width = 96; +constexpr std::uint32_t Width = 99; constexpr std::uint32_t Height = 16; -constexpr std::uint32_t Slices = 32; -constexpr std::uint32_t FirstSlice = 0; +constexpr std::uint32_t Slices = 34; +constexpr std::uint32_t FirstSlice = 1; constexpr std::uint32_t Layers = Slices - FirstSlice; constexpr std::uint32_t BandWidth = Width / Layers; constexpr std::uint8_t Kept = 0x40; @@ -184,6 +184,17 @@ void Run(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget if (mesh) geometry.graphics = ShaderRecompiler::GraphicsCompileContext{0, {}, Subgroup, std::nullopt, {draw.indexAddress, draw.indexCount, draw.indexSize, draw.instanceCount}}; geometry.useCache = false; const auto geometryResult = ShaderRecompiler::Recompile(geometry); + if (mesh) { + for (const auto required : {ShaderRecompiler::PipelineSpecialization::MeshLayerBase, ShaderRecompiler::PipelineSpecialization::MeshLayerCount}) { + bool found = false; + for (std::size_t at = 5; at < geometryResult.spirv.size();) { + const auto length = geometryResult.spirv[at] >> 16u; + if ((geometryResult.spirv[at] & 0xffffu) == 71u && length == 4u && geometryResult.spirv[at + 2u] == 1u && geometryResult.spirv[at + 3u] == required) found = true; + at += length; + } + Require(found, "mesh layer specialization was frozen: " + std::to_string(required)); + } + } const auto geometryPush = static_cast(geometryResult.pushConstants.size()); ShaderRecompiler::ShaderPixelStageInfo pixel{}; @@ -229,12 +240,7 @@ void Run(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget state.blendConstants = {}; if (mesh) { Require(DeclaresLayer(geometryResult.spirv), "the mesh program declares no Layer output"); - VkPhysicalDeviceSubgroupProperties subgroup{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES}; - subgroup.subgroupSize = target.subgroupSize; - subgroup.supportedStages = VK_SHADER_STAGE_ALL; - subgroup.supportedOperations = 0xffu; - subgroup.quadOperationsInAllStages = VK_TRUE; - AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false, false, false, false, false, false, false, false, true); + } device.Draw(state, draw, shaders); device.WaitIdle(); @@ -394,6 +400,7 @@ int main(int argc, char** argv) { MipTargets(*device); return 0; } + std::printf("graphics pipeline libraries: %s\n", device->GraphicsPipelineLibraries() ? "enabled" : "disabled"); BuildBands(); const GuestVolume volume((VolumeTarget(nullptr).bytes + 65535u) & ~std::size_t{65535u}); const auto color = VolumeTarget(volume.Data()); @@ -445,6 +452,10 @@ int main(int argc, char** argv) { const auto meshColor = VolumeTarget(meshVolume.Data()); Run(*device, meshColor, true, PixelCode, false); Check(meshVolume.Data(), meshColor, "mesh draw", WhiteBands); + const GuestVolume meshFieldVolume((VolumeTarget(nullptr).bytes + 65535u) & ~std::size_t{65535u}); + const auto meshFieldColor = VolumeTarget(meshFieldVolume.Data()); + Run(*device, meshFieldColor, true, LayerFieldPixelCode, true); + Check(meshFieldVolume.Data(), meshFieldColor, "mesh fragment layer input", LayerFieldBands); } else std::puts("mesh case skipped, the device has no VK_EXT_mesh_shader"); std::puts("layered volume draw tests passed"); diff --git a/core/shader/recompiler/PipelineSpecialization.hpp b/core/shader/recompiler/PipelineSpecialization.hpp index 12960b63df..73a87be347 100644 --- a/core/shader/recompiler/PipelineSpecialization.hpp +++ b/core/shader/recompiler/PipelineSpecialization.hpp @@ -33,6 +33,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 MeshLayerBase = 8237u; +inline constexpr std::uint32_t MeshLayerCount = 8238u; inline constexpr std::uint32_t DepthRemapBase = 8240u; inline constexpr std::uint32_t DepthRemapWords = 1u; inline constexpr std::uint32_t WorkgroupOffsetBase = 8241u; diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index fe989bcf5b..edf1047511 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -703,6 +703,7 @@ CompiledVariant emitVariant(const RecompileRequest& request, IrProgram& program) } result.registrationModule = registration; if (request.shader.stage == ShaderStage::Mesh && request.graphics) result.meshBarycentrics = request.graphics->meshBarycentrics; + result.layerReplaySafe = LayerReplaySafe(program); result.stencilReplaySafe = (request.shader.stage == ShaderStage::Vertex || stencilReplay) && StencilReplaySafe(program); result.bdaWrites = program.Info().bdaWrites; if (stencilReplay) { @@ -781,6 +782,7 @@ CompiledVariant compileVariant(const RecompileRequest& request, IrProgram progra struct SpecializedModule { std::uint64_t specializationId = 0; + std::vector specialization; std::vector bindings; bool pushData = false; SpirvContentHash hash; @@ -820,6 +822,11 @@ std::vector specializeWords(const CompiledShaderArtifact& artifac } const auto found = supplied.find(words[cursor + 3u]); if (found == supplied.end()) throw std::runtime_error("missing prepared specialization value"); + if (found->first == PipelineSpecialization::MeshLayerBase || found->first == PipelineSpecialization::MeshLayerCount) { + retained.insert(words[cursor + 1u]); + cursor += count; + continue; + } auto value = found->second; if (!artifact.stencilReferences.empty()) { if (found->first == PipelineSpecialization::StencilReferenceOffset) replaySlotBytes = value / PushData::DwordCount * NativePushSlotSize; @@ -834,9 +841,7 @@ std::vector specializeWords(const CompiledShaderArtifact& artifac for (std::size_t cursor = 5; cursor < words.size();) { const auto count = words[cursor] >> 16u; const auto op = static_cast(words[cursor] & 0xffffu); - if (op == spv::OpSpecConstant && count == 4u && retained.contains(words[cursor + 2u])) { - materialized.insert(materialized.end(), words.begin() + cursor, words.begin() + cursor + count); - } else if (op == spv::OpDecorate && count == 4u && words[cursor + 2u] == spv::DecorationSpecId && retained.contains(words[cursor + 1u])) { + if ((op == spv::OpSpecConstant && count == 4u && retained.contains(words[cursor + 2u])) || (op == spv::OpDecorate && count == 4u && words[cursor + 2u] == spv::DecorationSpecId && retained.contains(words[cursor + 1u]))) { materialized.insert(materialized.end(), words.begin() + cursor, words.begin() + cursor + count); } else if (op == spv::OpSpecConstant) { const auto found = count == 4u ? values.find(words[cursor + 2u]) : values.end(); @@ -853,7 +858,7 @@ std::vector specializeWords(const CompiledShaderArtifact& artifac } materialized = SpecializeFragmentOutputs(SpecializeSpirv(materialized), constants, specializedIds); #if ANYPS5_ENABLE_SPIRV_TOOLS - materialized = ValidateAndOptimizeSpirv(materialized, target.vulkanVersion, target.spirvVersion, target.nonConstantImageOffsets, true, true); + materialized = ValidateAndOptimizeSpirv(materialized, target.vulkanVersion, target.spirvVersion, target.nonConstantImageOffsets, true, retained.empty()); #endif std::map descriptorVariables; std::set usedDescriptors; @@ -894,6 +899,8 @@ std::vector specializeWords(const CompiledShaderArtifact& artifac SpecializedSpirv buildSpecializedModule(const CompiledShaderArtifact& artifact, std::span classes, std::span constants, const SpirvTarget& target) { auto materialized = specializeWords(artifact, classes, constants, target); auto module = std::make_shared(); + std::map supplied; + for (const auto& constant : constants) supplied.emplace(constant.id, constant.value); std::map bindingNumbers; std::unordered_set functionLdsBuffer; for (std::size_t cursor = 5; cursor < materialized.size();) { @@ -901,6 +908,10 @@ SpecializedSpirv buildSpecializedModule(const CompiledShaderArtifact& artifact, const auto op = static_cast(materialized[cursor] & 0xffffu); if (op == spv::OpDecorate && count == 4u && materialized[cursor + 2u] == spv::DecorationBinding) bindingNumbers.emplace(materialized[cursor + 1u], materialized[cursor + 3u]); if (op == spv::OpDecorate && count == 4u && materialized[cursor + 2u] == spv::DecorationDescriptorSet && (materialized[cursor + 3u] == PixelFunctionLdsDescriptorSet || materialized[cursor + 3u] == MeshWindowDescriptorSet)) functionLdsBuffer.insert(materialized[cursor + 1u]); + if (op == spv::OpDecorate && count == 4u && materialized[cursor + 2u] == spv::DecorationSpecId) { + const auto id = materialized[cursor + 3u]; + module->specialization.push_back({id, supplied.at(id)}); + } if (op == spv::OpVariable && count >= 4u && !functionLdsBuffer.contains(materialized[cursor + 2u])) { if (const auto found = bindingNumbers.find(materialized[cursor + 2u]); found != bindingNumbers.end()) module->bindings.push_back(found->second); module->pushData |= materialized[cursor + 3u] == spv::StorageClassPushConstant; @@ -1154,6 +1165,7 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil .stencilReferences = artifact.stencilReferences, .stencilReplaySafe = artifact.stencilReplaySafe, .bdaWrites = artifact.bdaWrites, + .layerReplaySafe = artifact.layerReplaySafe, .stencilReferenceDword = artifact.stencilReferenceDword, .variantId = artifact.variantId }; @@ -1217,7 +1229,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 || depthRemap) { + if (!plan->bindings.specialization.empty() || variant.bindings.layout.UsesPushData() || !artifact.vertexInputPatches.empty() || request.context.pixel || depthRemap || request.shader.stage == ShaderStage::Mesh) { { std::shared_lock lock(plan->mutex); if (const auto found = plan->modules.find(moduleKey); found != plan->modules.end()) entry = found->second; @@ -1229,6 +1241,8 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil } const auto specialize = [&] { auto constants = plan->bindings.specialization; + constants.push_back({PipelineSpecialization::MeshLayerBase, 0u}); + constants.push_back({PipelineSpecialization::MeshLayerCount, 0xffffffffu}); std::size_t index = 0; if (variant.bindings.layout.UsesPushData()) constants.push_back({PipelineSpecialization::PushDataOffset, moduleKey[index++]}); if (!artifact.stencilReferences.empty()) constants.push_back({PipelineSpecialization::StencilReferenceOffset, moduleKey[index++]}); @@ -1277,6 +1291,7 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil if (built.empty()) built = specialize().spirv; result.specializationId = module->specializationId; result.spirv = std::move(built); + result.specialization = module->specialization; bindingPlan = &entry->bindings; } else { result.spirv = artifact.spirv; diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 422432c8a0..de7eeae604 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -767,6 +767,7 @@ struct CompiledShaderArtifact { std::vector stencilReferences; bool stencilReplaySafe = false; bool bdaWrites = false; + bool layerReplaySafe = false; std::uint32_t stencilReferenceDword = 0xffffffffu; std::uint64_t variantId = 0; }; diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index 77fa67da41..3649a74326 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -571,6 +571,7 @@ void encodeArtifact(Writer& writer, const CompiledShaderArtifact& result) { writer.Values(std::span(result.stencilReferences)); writer.Value(result.stencilReplaySafe); writer.Value(result.bdaWrites); + writer.Value(result.layerReplaySafe); writer.Value(result.stencilReferenceDword); } @@ -640,6 +641,7 @@ void decodeArtifact(Reader& reader, CompiledShaderArtifact& result) { reader.Values(result.stencilReferences); reader.Value(result.stencilReplaySafe); reader.Value(result.bdaWrites); + reader.Value(result.layerReplaySafe); reader.Value(result.stencilReferenceDword); result.variantId = 0; } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index 44beabfca9..e9368332fd 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -248,6 +248,10 @@ std::uint32_t EmitBuiltinU32(SpirvEmitterState& state, StageInputKind kind, std: const auto bits = state.module.AllocateId(); state.module.AddFunction(spv::OpLoad, TypeI32(state), value, variable); state.module.AddFunction(spv::OpBitcast, TypeU32(state), bits, value); + if (kind == StageInputKind::Layer) { + const auto base = state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::MeshLayerBase, 0u); + return EmitBinaryU32(state, spv::OpIAdd, bits, base); + } return kind == StageInputKind::SampleId ? GuestSampleIndex(state, bits) : bits; } if (kind == StageInputKind::SampleMask) { @@ -1451,7 +1455,19 @@ void EmitMeshEntryPoint(SpirvEmitterState& state) { const auto layer = MeshLoad(state, output.meshDataVariable, spv::StorageClassWorkgroup, TypeU32(state), vertex.at(state.inputInfo.vertex->mesh.provokingVertex)); const auto pointer = state.module.AllocateId(); 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); + const auto base = state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::MeshLayerBase, 0u); + const auto count = state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::MeshLayerCount, 0xffffffffu); + const auto relative = EmitBinaryU32(state, spv::OpISub, layer, base); + const auto inside = state.module.AllocateId(); + const auto outside = state.module.AllocateId(); + const auto selected = state.module.AllocateId(); + const auto discarded = state.module.AllocateId(); + state.module.AddFunction(spv::OpULessThan, TypeBool(state), inside, relative, count); + state.module.AddFunction(spv::OpLogicalNot, TypeBool(state), outside, inside); + state.module.AddFunction(spv::OpSelect, TypeU32(state), selected, inside, relative, ConstantU32(state, 0u)); + state.module.AddFunction(spv::OpLogicalOr, TypeBool(state), discarded, culled, outside); + state.module.AddFunction(spv::OpStore, MeshElement(state, state.meshCull, spv::StorageClassOutput, TypeBool(state), index), discarded); + state.module.AddFunction(spv::OpStore, pointer, selected); } if (emulating) { const auto corners = state.module.Type(spv::OpTypeArray, TypeF32Vector(state, 4u), ConstantU32(state, 3u)); diff --git a/core/shader/recompiler/StencilReplay.cpp b/core/shader/recompiler/StencilReplay.cpp index 65146cbcff..432b8c85cf 100644 --- a/core/shader/recompiler/StencilReplay.cpp +++ b/core/shader/recompiler/StencilReplay.cpp @@ -848,6 +848,61 @@ bool StencilReferenceDefinedForKeptInvocations(const IrProgram& program) { return valid; } +bool LayerReplaySafe(const IrProgram& program) { + if (program.Info().bdaWrites || program.Info().usesDma) return false; + for (const auto& block : program.Blocks()) { + for (const auto* instruction : block->Instructions()) { + const auto opcode = instruction->Opcode(); + if (opcode == IrOpcode::ImageBvhIntersectRay || (BufferAccessOf(opcode) == BufferAccess::Write || BufferAccessOf(opcode) == BufferAccess::Atomic) || AddressOpcodeInfoOf(opcode).access == AddressAccess::Write || AddressOpcodeInfoOf(opcode).access == AddressAccess::Atomic || ImageOpcodeInfoOf(opcode).access == ImageAccess::Write || ImageOpcodeInfoOf(opcode).access == ImageAccess::Atomic) return false; + if (opcode == IrOpcode::UndefU1 || opcode == IrOpcode::UndefU8 || opcode == IrOpcode::UndefU16 || opcode == IrOpcode::UndefU32 || opcode == IrOpcode::UndefU64) return false; + if (opcode == IrOpcode::ShaderClock || opcode == IrOpcode::RealtimeClock) return false; + if (SharedAccessOf(opcode) != SharedAccess::None) { + const auto memory = instruction->Flags().index; + if (memory >= program.Resources().memoryInfo.size() || program.Resources().memoryInfo[memory].kind != ResourceKind::Lds) return false; + continue; + } + if (!instruction->MayHaveSideEffects()) continue; + switch (opcode) { + case IrOpcode::Barrier: + case IrOpcode::MeshAllocate: + case IrOpcode::Branch: + case IrOpcode::BranchConditional: + case IrOpcode::Return: + case IrOpcode::Unreachable: + case IrOpcode::Discard: + case IrOpcode::Reference: + case IrOpcode::ReferenceU32: + case IrOpcode::SetAttribute: + case IrOpcode::InterpolateHostP2: + case IrOpcode::SetRegister: + case IrOpcode::SetThreadBitScalarRegister: + case IrOpcode::SetScalarMaskTag: + case IrOpcode::SetScalarRegister: + case IrOpcode::SetVectorRegister: + case IrOpcode::StoreVectorWindow: + case IrOpcode::SetGotoVariable: + case IrOpcode::SetScc: + case IrOpcode::SetExec: + case IrOpcode::SetExecLo: + case IrOpcode::SetExecHi: + case IrOpcode::SetVcc: + case IrOpcode::SetVccLo: + case IrOpcode::SetVccHi: + case IrOpcode::SetM0: + case IrOpcode::Waitcnt: + case IrOpcode::InstPrefetch: + break; + case IrOpcode::Sendmsg: + if (instruction->ArgumentCount() != 0u) return false; + break; + default: + return false; + } + } + } + return true; +} + bool StencilReplaySafe(const IrProgram& program) { if (program.Info().sharedMemoryBytes != 0u || program.Info().bdaWrites || (program.Info().usesFaultBuffer && program.Resources().guardedSrtSlots.empty())) return false; if (program.Info().usesFaultBuffer && !FaultControlFlowAcyclic(program)) return false; diff --git a/core/shader/recompiler/StencilReplay.hpp b/core/shader/recompiler/StencilReplay.hpp index 5c998b0056..fcd16b2c03 100644 --- a/core/shader/recompiler/StencilReplay.hpp +++ b/core/shader/recompiler/StencilReplay.hpp @@ -8,6 +8,7 @@ namespace ShaderRecompiler { [[nodiscard]] std::vector StencilReferenceValues(const IrProgram& program); [[nodiscard]] std::vector StencilReferenceValues(const IrProgram& program, std::uint32_t shift); +[[nodiscard]] bool LayerReplaySafe(const IrProgram& program); [[nodiscard]] bool StencilReplaySafe(const IrProgram& program); [[nodiscard]] bool StencilReferenceDefinedForKeptInvocations(const IrProgram& program); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 5ab3c17e6d..e088f10e00 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -334,6 +334,8 @@ - [Adaptive staging](../../../core/libs/prx/libSceAgcDriver/Graphics/include/AdaptiveStaging.hpp) (libSceAgcDriver): whether a staged element pays for its two copies is decided by probing the frame GPU time, not from the element's accesses. The probe is measured on one title (PPSA03912, NVIDIA on Linux). It judges by the whole frame's GPU time, so a scene whose GPU load moves by itself can keep the slower mode until the next probe, 20 to 160 s later, and each probe runs a few frames in the slower mode. - [WriteChanged](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) (AGC driver) - a partly changed 16-byte write-back chunk is stored with one byte store per changed byte when at most `ChangedByteStoreLimit()` bytes changed, and with `MASKMOVDQU` otherwise. The limit is 16 (byte stores at every density) on a CPU whose vendor string is `AuthenticAMD` and 4 on any other. The cost of `MASKMOVDQU` is measured only on a Ryzen 9 7950X, where byte stores win up to about 10 changed bytes in a microbenchmark but PPSA16064 lost its gain when `MASKMOVDQU` was kept from 10 changed bytes on, and on a Core i9-14900KF, where byte stores win only at 3 or 4 changed bytes and are up to 2.6 times slower for dense chunks (#8930). Other AMD and Intel generations and other vendors are not measured. - [Host report](../../../core/libs/prx/libSceAgcDriver/Execution/src/HostReportVulkan.cpp) (libSceAgcDriver, `ANYPS5_HOST_REPORT=1`): run on Linux with a software Vulkan device only. Its Windows branches (`RtlGetVersion`, `GlobalMemoryStatusEx`, `USERNAME`, `COMPUTERNAME`, `USERPROFILE`) and its macOS branches are not run, and no hardware GPU's output is checked. The list of required Vulkan items in `HostReportVulkan.cpp` mirrors the checks of `VulkanDevice.cpp` by hand: a new hard requirement added there must be added to the list, or the start fails with the older single-item message. Closes when: the report is read on Windows, macOS and a hardware GPU. +- [Draw](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) partitions layered mesh color draws into attachment windows within the host `maxMeshOutputLayers` limit, remaps output layers and reconstructs the guest layer for fragment ancillary input. Replay requires shaders without guest memory writes, clocks, GDS access or unproven side effects; those draws still throw until mesh output can be captured once and rasterized separately. Layered depth rendering remains unsupported. Vulkan layer-limit semantics follow [Layer](https://docs.vulkan.org/refpages/latest/refpages/source/Layer.html); PS5 console behavior and native title results have not been validated for this path. + ### Opt-in draw paths