From 9aeeea848f5fd52f3cce3a9603d02d1e774c1169 Mon Sep 17 00:00:00 2001 From: Mouhand-Kaddo Date: Mon, 12 Oct 2026 16:07:06 +0400 Subject: [PATCH 1/2] fix(agc): retain used descriptor sets in pipeline libraries --- .../Graphics/include/PipelineLibrary.hpp | 2 + .../libSceAgcDriver/Graphics/src/Pipeline.cpp | 11 ++- .../Graphics/src/PipelineLibrary.cpp | 13 ++-- .../prx/libSceAgcDriver/tests/Graphics.cpp | 74 +++++++++++++++++++ 4 files changed, 93 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp index 1d340829c..b2ca74e46 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp @@ -21,6 +21,8 @@ struct PipelineLibraryKeys { std::vector layout; std::vector preRasterizationLayout; std::vector fragmentLayout; + std::uint32_t preRasterizationSets = 0; + std::uint32_t fragmentSets = 0; }; std::span PipelineLibraryDynamicStates(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index 37f58e0fa..bf3ff3bbc 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -182,6 +182,13 @@ bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State& appendKey(key, shader.stage); appendKey(key, shader.program->PipelineVariantId()); appendKey(key, shader.pushConstantOffset); + if (resources.split) { + auto& sets = shader.stage == Stage::Fragment ? keys.fragmentSets : keys.preRasterizationSets; + for (const auto& binding : shader.program->bindings) { + Require(binding.descriptorSet < 2u, "split pipeline library descriptor set exceeds two slots"); + sets |= 1u << binding.descriptorSet; + } + } } for (const auto& binding : input.bindings) { appendKey(keys.vertexInput, binding.binding); @@ -246,8 +253,8 @@ bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State& } }; splitKey(keys.layout, 3u); - splitKey(keys.preRasterizationLayout, 1u); - splitKey(keys.fragmentLayout, 2u); + splitKey(keys.preRasterizationLayout, keys.preRasterizationSets); + splitKey(keys.fragmentLayout, keys.fragmentSets); return true; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp index db9134959..627f09c67 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp @@ -104,6 +104,8 @@ std::span PipelineLibraryDynamicStates() { VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPipelineCreateInfo& info, const VkPipelineRenderingCreateInfoKHR& rendering, const VkPipelineLayoutCreateInfo& layout, const PipelineLibraryKeys& keys, std::shared_ptr* optimized, bool partsOnly, std::span modules) { Require(info.renderPass == VK_NULL_HANDLE && info.pDynamicState != nullptr, "pipeline library parts need dynamic rendering and the library dynamic states"); + const bool split = (layout.flags & VK_PIPELINE_LAYOUT_CREATE_INDEPENDENT_SETS_BIT_EXT) != 0; + Require(!split || (layout.setLayoutCount == 2u && layout.pSetLayouts != nullptr && ((keys.preRasterizationSets | keys.fragmentSets) & ~3u) == 0u), "split pipeline library layout requires two descriptor set slots and a valid mask"); auto& store = Libraries(); std::unique_lock lock(store.mutex); auto& device = store.devices[context.device]; @@ -112,22 +114,23 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip device.destroyLayout = context.Function("vkDestroyPipelineLayout"); device.destroyModule = context.Function("vkDestroyShaderModule"); } - const auto layoutFor = [&](const Key& key, std::optional set) { + const auto layoutFor = [&](const Key& key, std::optional mask) { auto found = device.layouts.find(key); if (found != device.layouts.end()) return found->second; auto info = layout; std::array sets{VK_NULL_HANDLE, VK_NULL_HANDLE}; - if (set) { - sets.at(*set) = layout.pSetLayouts[*set]; + if (mask.has_value()) { + for (std::uint32_t set = 0; set < sets.size(); ++set) { + if ((*mask & (1u << set)) != 0u) sets[set] = layout.pSetLayouts[set]; + } info.pSetLayouts = sets.data(); } VkPipelineLayout handle = VK_NULL_HANDLE; Check(context.Function("vkCreatePipelineLayout")(context.device, &info, nullptr, &handle), "vkCreatePipelineLayout library"); return device.layouts.emplace(key, handle).first->second; }; - const bool split = (layout.flags & VK_PIPELINE_LAYOUT_CREATE_INDEPENDENT_SETS_BIT_EXT) != 0; const auto fullLayout = layoutFor(keys.layout, std::nullopt); - const std::array partLayouts{VK_NULL_HANDLE, split ? layoutFor(keys.preRasterizationLayout, 0u) : fullLayout, split ? layoutFor(keys.fragmentLayout, 1u) : fullLayout, VK_NULL_HANDLE}; + const std::array partLayouts{VK_NULL_HANDLE, split ? layoutFor(keys.preRasterizationLayout, keys.preRasterizationSets) : fullLayout, split ? layoutFor(keys.fragmentLayout, keys.fragmentSets) : fullLayout, VK_NULL_HANDLE}; std::vector preRasterization; const VkPipelineShaderStageCreateInfo* fragment = nullptr; bool mesh = false; diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 935796066..5aba36b8a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -2233,6 +2233,10 @@ struct MockVulkan { std::uint32_t boundFirst = 0; VkPipelineBindPoint boundPoint = VK_PIPELINE_BIND_POINT_MAX_ENUM; std::map pipelineLayoutPushConstantSize; + std::map> pipelineLayoutSets; + std::map pipelineLayoutFlags; + std::map> pipelineLayoutPushRanges; + std::map libraryLayouts; std::uint32_t pipelineCreateCount = 0; std::vector> pipelineSpecializations; VkPipeline boundPipeline = VK_NULL_HANDLE; @@ -2391,6 +2395,19 @@ VKAPI_ATTR VkDeviceAddress VKAPI_CALL mockGetBufferDeviceAddress(VkDevice, const VKAPI_ATTR VkResult VKAPI_CALL mockCreatePipelineLayout(VkDevice, const VkPipelineLayoutCreateInfo* info, const VkAllocationCallbacks*, VkPipelineLayout* layout) { *layout = makeHandle(); mock.pipelineLayoutPushConstantSize[*layout] = info->pushConstantRangeCount > 0 ? info->pPushConstantRanges[0].size : 0; + if (info->setLayoutCount != 0) mock.pipelineLayoutSets[*layout].assign(info->pSetLayouts, info->pSetLayouts + info->setLayoutCount); + mock.pipelineLayoutFlags[*layout] = info->flags; + if (info->pushConstantRangeCount != 0) mock.pipelineLayoutPushRanges[*layout].assign(info->pPushConstantRanges, info->pPushConstantRanges + info->pushConstantRangeCount); + ++mock.live; + return VK_SUCCESS; +} + +VKAPI_ATTR VkResult VKAPI_CALL mockCreateGraphicsPipelines(VkDevice, VkPipelineCache, std::uint32_t count, const VkGraphicsPipelineCreateInfo* infos, const VkAllocationCallbacks*, VkPipeline* pipelines) { + Require(count == 1 && (infos[0].flags & VK_PIPELINE_CREATE_LIBRARY_BIT_KHR) != 0, "mock expects a graphics pipeline library"); + const auto* library = static_cast(infos[0].pNext); + Require(library != nullptr && library->sType == VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_LIBRARY_CREATE_INFO_EXT, "missing graphics pipeline library description"); + mock.libraryLayouts[library->flags] = infos[0].layout; + *pipelines = makeHandle(); ++mock.live; return VK_SUCCESS; } @@ -2504,6 +2521,7 @@ PFN_vkVoidFunction VKAPI_CALL mockProc(VkDevice, const char* name) { {"vkResetDescriptorPool", reinterpret_cast(mockResetDescriptorPool)}, {"vkUpdateDescriptorSets", reinterpret_cast(mockUpdateDescriptorSets)}, {"vkCmdBindDescriptorSets", reinterpret_cast(mockCmdBindDescriptorSets)}, + {"vkCreateGraphicsPipelines", reinterpret_cast(mockCreateGraphicsPipelines)}, {"vkCreatePipelineLayout", reinterpret_cast(mockCreatePipelineLayout)}, {"vkDestroyPipelineLayout", reinterpret_cast(mockDestroyPipelineLayout)}, {"vkCreateShaderModule", reinterpret_cast(mockCreateShaderModule)}, @@ -3360,6 +3378,61 @@ std::vector makeModule(const ModuleShape& shape) { return words; } +void pipelineLibrarySetTests() { + using namespace AgcDriver::Graphics; + for (const auto mask : {0u, 1u, 2u, 3u}) { + mock = MockVulkan{}; + auto context = mockContext(); + context.graphicsPipelineLibrary = true; + context.limits.framebufferNoAttachmentsSampleCounts = VK_SAMPLE_COUNT_1_BIT; + State state{}; + ShaderRecompiler::RecompileResult vertex, fragment; + vertex.spirv = makeModule({}); + fragment.spirv = makeModule({.fragment = true}); + vertex.variantId = 1; + fragment.variantId = 2; + for (std::uint32_t set = 0; set < 2; ++set) { + ShaderRecompiler::DescriptorBinding binding{}; + binding.descriptorSet = set; + if ((mask & (1u << set)) != 0u) fragment.bindings.push_back(binding); + else vertex.bindings.push_back(binding); + } + const std::array shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &fragment, PipelinePushSlotBytes}}}; + std::array mainKey{11}, fragmentKey{22}; + PipelineSetLayouts resources{makeHandle(), makeHandle(), true, mainKey, fragmentKey}; + const auto prebuild = [&] { Require(Pipeline::Prebuild(context, state, VertexInputLayout{}, resources, shaders), "graphics pipeline library prebuild was skipped"); }; + prebuild(); + for (const auto part : {VK_GRAPHICS_PIPELINE_LIBRARY_PRE_RASTERIZATION_SHADERS_BIT_EXT, VK_GRAPHICS_PIPELINE_LIBRARY_FRAGMENT_SHADER_BIT_EXT}) { + const auto layout = mock.libraryLayouts.at(part); + const auto expectedMask = part == VK_GRAPHICS_PIPELINE_LIBRARY_FRAGMENT_SHADER_BIT_EXT ? mask : mask ^ 3u; + const std::vector expected{(expectedMask & 1u) != 0u ? resources.main : VK_NULL_HANDLE, (expectedMask & 2u) != 0u ? resources.fragment : VK_NULL_HANDLE}; + Require(mock.pipelineLayoutSets.at(layout) == expected, "shader library lost a used set or retained an unused set"); + Require(mock.pipelineLayoutFlags.at(layout) == VK_PIPELINE_LAYOUT_CREATE_INDEPENDENT_SETS_BIT_EXT, "shader library lost independent sets"); + const auto& pushes = mock.pipelineLayoutPushRanges.at(layout); + Require(pushes.size() == 2 && pushes[0].offset == 0 && pushes[0].size == PipelinePushSlotBytes && pushes[0].stageFlags == VK_SHADER_STAGE_VERTEX_BIT && pushes[1].offset == PipelinePushSlotBytes && pushes[1].size == PipelinePushSlotBytes && pushes[1].stageFlags == VK_SHADER_STAGE_FRAGMENT_BIT, "shader library changed push constant slots"); + } + auto before = PipelineLibraryCountersOf(context.device).built; + prebuild(); + Require(PipelineLibraryCountersOf(context.device).built == before, "identical shader libraries were rebuilt"); + for (std::uint32_t set = 0; set < 2; ++set) { + if (set == 0) { ++mainKey[0]; resources.main = makeHandle(); } + else { ++fragmentKey[0]; resources.fragment = makeHandle(); } + prebuild(); + const auto after = PipelineLibraryCountersOf(context.device).built; + Require(after[0] == before[0] && after[3] == before[3], "descriptor layout change rebuilt interface libraries"); + Require(after[1] == before[1] + ((mask & (1u << set)) == 0u ? 1u : 0u), "pre-rasterization library reuse ignored descriptor set usage"); + Require(after[2] == before[2] + ((mask & (1u << set)) != 0u ? 1u : 0u), "fragment library reuse ignored descriptor set usage"); + before = after; + } + ShaderRecompiler::DescriptorBinding invalid{}; + invalid.descriptorSet = 32; + fragment.bindings.push_back(invalid); + expectFailure(prebuild, "descriptor set exceeds two slots"); + ClearPipelineLibraries(context.device); + Require(mock.live == 0, "shader library tests leaked Vulkan objects"); + } +} + void barycentricEmulationTests() { using namespace ShaderRecompiler; using namespace AgcDriver::Graphics; @@ -4959,6 +5032,7 @@ int main(int argc, char** argv) { } RunGuestLeaseWaitTests(); stateTests(); + pipelineLibrarySetTests(); hdrComponentSwapTests(); depthMaintenanceTests(); hardwareScreenOffsetTests(); From 4f5f6d671cdb0ffd3a9c43eedbdf03e433e80ebe Mon Sep 17 00:00:00 2001 From: Mouhand Kaddo <73709175+Mouhand-Kaddo@users.noreply.github.com> Date: Tue, 13 Oct 2026 00:43:16 +0400 Subject: [PATCH 2/2] test(agc): ignore null shader modules in graphics mock Pipeline libraries take ownership of shader modules during prebuild. Treat null handles as a no-op so moved-from modules do not decrement the live-object count twice. --- core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 5aba36b8a..5fd498260 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -2422,8 +2422,8 @@ VKAPI_ATTR VkResult VKAPI_CALL mockCreateShaderModule(VkDevice, const VkShaderMo return VK_SUCCESS; } -VKAPI_ATTR void VKAPI_CALL mockDestroyShaderModule(VkDevice, VkShaderModule, const VkAllocationCallbacks*) { - --mock.live; +VKAPI_ATTR void VKAPI_CALL mockDestroyShaderModule(VkDevice, VkShaderModule module, const VkAllocationCallbacks*) { + if (module != VK_NULL_HANDLE) --mock.live; } VKAPI_ATTR VkResult VKAPI_CALL mockCreateComputePipelines(VkDevice, VkPipelineCache, std::uint32_t count, const VkComputePipelineCreateInfo* infos, const VkAllocationCallbacks*, VkPipeline* pipelines) {