From 7fae4549e3d86a1f5db1563792dc76ddf0a284c0 Mon Sep 17 00:00:00 2001 From: Karan Vora <52519664+karanvora2599@users.noreply.github.com> Date: Mon, 12 Oct 2026 21:20:45 -0400 Subject: [PATCH 1/3] perf(agc): create compute pipelines before taking the GPU lock PrepareDispatch now creates a missing compute pipeline once the descriptor set layout is known, so the driver's compile no longer runs under GuestMemory::GpuMutex. Other queues, presentation and label writes go on during it, and compiles from different queues overlap. Two queues that need the same new variant create it once. The pipeline is still created in dispatch() when the layout is only built under the lock, or with APS5_LOCKED_BUILD=1. The creation passes the context's PipelineCacheSynchronization to CreateComputePipeline. --- .../Execution/include/VulkanDevice.hpp | 5 +- .../src/Driver/Dispatch/Dispatch.cpp | 2 +- .../Execution/src/VulkanDevice.cpp | 166 +++++++++++------- 3 files changed, 108 insertions(+), 65 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp index f757498d0a..00907029e1 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp @@ -21,10 +21,12 @@ namespace AgcDriver { namespace Graphics { class StorageTexture; +class ShaderResources; } // Stage A of a dispatch's resource build (see VulkanDevice::PrepareDispatch); opaque to callers. struct PreparedDispatch; +struct ComputePipelineObjects; // The unlocked pre-checks of a recipe hit (see VulkanDevice::PrepareRecipe); opaque to callers. struct RecipeHit; // The record tail shared by an ordinary dispatch and a recipe hit (VulkanDevice::recordDispatch). @@ -234,7 +236,7 @@ public: // records. Null when nothing is prepared: the resource cache may serve the dispatch (its // Revalidate stays under the mutex), or APS5_LOCKED_BUILD=1 keeps the whole build under it as // before. `shader` and `snapshots` must outlive the dispatch. - std::shared_ptr PrepareDispatch(const ShaderRecompiler::RecompileResult& shader, std::span snapshots, std::uint64_t dispatchThreads = 0); + std::shared_ptr PrepareDispatch(const ShaderRecompiler::RecompileResult& shader, std::span snapshots, std::uint64_t dispatchThreads = 0, std::uint64_t programAddress = 0); // APS5_PROFILE_DRAW: the parts of a PrepareDispatch in milliseconds, in the order key, find, // precollect, presync, stage A (the driver's 'prepare:' rows). static std::span PreparePhaseMs(const PreparedDispatch& prepared); @@ -317,6 +319,7 @@ private: IndirectOutcome dispatch(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::span snapshots, std::uint64_t programAddress, std::shared_ptr prepared, std::shared_ptr* recipe); // The stage-A pre-sync over `surfaces` (see PrepareDispatch): the serial waited for, 0 none. std::uint64_t presync(std::span> surfaces); + std::shared_ptr computePipeline(const Graphics::Context& context, const ShaderRecompiler::RecompileResult& shader, const Graphics::ShaderResources& resources, VkShaderStageFlags pushStages, std::uint64_t programAddress); // A DISPATCH_INDIRECT's argument path (see DispatchIndirect): GPU-side from the host import, or // the counts read on the CPU (the record then continues as a direct dispatch). void decideIndirect(RecordedDispatch& record, IndirectOutcome& outcome, char* groupsText); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp index 9a23e42c8a..37388c94d3 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp @@ -372,7 +372,7 @@ void Driver::dispatch(QueueState& queue, std::span packet, std::shared_ptr prepared; if (recipeHit == nullptr || VulkanDevice::VerifyRecipes()) { try { - prepared = localDevice->PrepareDispatch(compiled, snapshots, dispatchThreads); + prepared = localDevice->PrepareDispatch(compiled, snapshots, dispatchThreads, address); } catch (const std::exception& error) { if (rethrow(error)) return; } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index eefe712a19..4128475a8f 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -43,6 +43,7 @@ #include #include #include +#include #include #include #include @@ -353,6 +354,7 @@ struct VulkanDevice::State { // references to its objects. std::mutex computePipelinesMutex; std::map> computePipelines; + std::map>> computePipelinesBuilding; std::unique_ptr colorTransfer; std::shared_ptr bufferPool; std::unique_ptr emptyBuffer; @@ -704,6 +706,7 @@ struct VulkanDevice::State { { std::lock_guard pipelines(computePipelinesMutex); computePipelines.clear(); + computePipelinesBuilding.clear(); } // Every ShaderResources (kept by the recorder or the resource cache) is gone now, so the // sets and samplers they borrowed can go. @@ -3555,6 +3558,7 @@ struct PreparedDispatch { // The stage-A pre-sync (see PrepareDispatch): the newest recorder serial waited for without the // mutex, whose batches the dispatch reaps under it before stage B (0: nothing waited for). std::uint64_t presyncSerial = 0; + std::shared_ptr objects; }; struct RecipeHit { @@ -3993,7 +3997,7 @@ std::uint64_t VulkanDevice::presync(std::span VulkanDevice::PrepareDispatch(const ShaderRecompiler::RecompileResult& shader, std::span snapshots, std::uint64_t dispatchThreads) { +std::shared_ptr VulkanDevice::PrepareDispatch(const ShaderRecompiler::RecompileResult& shader, std::span snapshots, std::uint64_t dispatchThreads, std::uint64_t programAddress) { ShaderRecompiler::RuntimeAbi::RequireVersion(shader.runtimeAbiVersion); // APS5_LOCKED_BUILD=1: the whole build under the mutex, as before the split. static const bool lockedBuild = std::getenv("APS5_LOCKED_BUILD") != nullptr; @@ -4047,6 +4051,12 @@ std::shared_ptr VulkanDevice::PrepareDispatch(const ShaderReco phase(PreparedDispatch::PreparePresync); } if (profile) prepared->prepareMs = std::chrono::duration(std::chrono::steady_clock::now() - start).count(); + const auto& built = cached != nullptr ? *cached : *prepared->resources; + const std::array shaders{{compute}}; + const auto pushStages = Graphics::PushConstantStages(shaders); + if (built.Layout() != VK_NULL_HANDLE && (pushStages == 0 || state->properties.limits.maxPushConstantsSize >= Graphics::ComputePushConstantBytes)) { + prepared->objects = computePipeline(context, shader, built, pushStages, programAddress); + } return prepared; } @@ -4324,6 +4334,95 @@ void VulkanDevice::recordDispatch(RecordedDispatch& record) { recordStep(PhaseRecordCompletion); } +std::shared_ptr VulkanDevice::computePipeline(const Graphics::Context& context, const ShaderRecompiler::RecompileResult& shader, const Graphics::ShaderResources& resources, VkShaderStageFlags pushStages, std::uint64_t programAddress) { + static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; + const std::uint64_t pipelineKey = shader.PipelineVariantId() != 0 ? (shader.PipelineVariantId() << 1u) | (pushStages != 0 ? 1u : 0u) : 0u; + std::promise> building; + if (pipelineKey != 0) { + std::shared_future> other; + { + std::lock_guard pipelines(state->computePipelinesMutex); + if (const auto found = state->computePipelines.find(pipelineKey); found != state->computePipelines.end()) return found->second; + if (const auto pending = state->computePipelinesBuilding.find(pipelineKey); pending != state->computePipelinesBuilding.end()) other = pending->second; + else state->computePipelinesBuilding.emplace(pipelineKey, building.get_future().share()); + } + if (other.valid()) return other.get(); + } + const auto finish = [&](std::shared_ptr built, std::exception_ptr error) { + if (pipelineKey == 0) return built; + { + std::lock_guard pipelines(state->computePipelinesMutex); + if (built != nullptr) built = state->computePipelines.try_emplace(pipelineKey, built).first->second; + state->computePipelinesBuilding.erase(pipelineKey); + } + if (error != nullptr) building.set_exception(error); + else building.set_value(built); + return built; + }; + std::shared_ptr objects; + try { + const auto start = std::chrono::steady_clock::now(); + objects = std::make_shared(); + objects->device = state->device; + objects->destroyModule = state->DeviceFunction("vkDestroyShaderModule"); + objects->destroyLayout = state->DeviceFunction("vkDestroyPipelineLayout"); + objects->destroyPipeline = state->DeviceFunction("vkDestroyPipeline"); + VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; + moduleInfo.codeSize = shader.spirv.size() * sizeof(std::uint32_t); + moduleInfo.pCode = shader.spirv.data(); + check(state->DeviceFunction("vkCreateShaderModule")(state->device, &moduleInfo, nullptr, &objects->module), "vkCreateShaderModule"); + const std::array setLayouts{resources.Layout(), shader.workgroupMemoryDwords != 0 ? state->workgroupMemoryLayout : VK_NULL_HANDLE}; + const VkPushConstantRange push{VK_SHADER_STAGE_COMPUTE_BIT, 0, Graphics::ComputePushConstantBytes}; + VkPipelineLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; + layoutInfo.setLayoutCount = shader.workgroupMemoryDwords != 0 ? ShaderRecompiler::WorkgroupMemoryDescriptorSet + 1 : 1; + layoutInfo.pSetLayouts = setLayouts.data(); + layoutInfo.pushConstantRangeCount = pushStages != 0 ? 1 : 0; + layoutInfo.pPushConstantRanges = pushStages != 0 ? &push : nullptr; + check(state->DeviceFunction("vkCreatePipelineLayout")(state->device, &layoutInfo, nullptr, &objects->layout), "vkCreatePipelineLayout"); + const VkPipelineCreateFlags pipelineFlags = (context.pipelineExecutableInfo ? VK_PIPELINE_CREATE_CAPTURE_STATISTICS_BIT_KHR : 0) | VK_PIPELINE_CREATE_DISPATCH_BASE_BIT; + const Graphics::PipelineSpecialization specialization(shader); + const auto subgroupSize = state->computeWave32 && (shader.hostSubgroupSize == 32u || shader.hostSubgroupSize > state->maxComputeSubgroupSize) ? std::min(shader.hostSubgroupSize, state->maxComputeSubgroupSize) : 0u; + if (const char* directory = std::getenv("APS5_DUMP_COMPUTE_PIPELINES"); directory != nullptr && *directory != '\0' && CaptureComputePipeline(shader.spirv.size())) { + ComputePipelineCapture capture; + capture.spirv = shader.spirv.Copy(); + ComputeDescriptorSetLayout resourcesLayout; + resourcesLayout.flags = resources.LayoutUpdateAfterBind() ? VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT : 0; + const auto layoutBindings = resources.LayoutBindings(); + const auto bindingFlags = resources.LayoutBindingFlags(); + require(layoutBindings.size() == bindingFlags.size(), "compute capture binding flag count differs from layout"); + for (std::size_t index = 0; index < layoutBindings.size(); ++index) { + const auto& binding = layoutBindings[index]; + require(binding.pImmutableSamplers == nullptr, "compute capture does not support immutable samplers"); + resourcesLayout.bindings.push_back({binding.binding, binding.descriptorType, binding.descriptorCount, binding.stageFlags, bindingFlags[index]}); + } + capture.sets.push_back(std::move(resourcesLayout)); + if (shader.workgroupMemoryDwords != 0) capture.sets.push_back({0, {{0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT, 0}}}); + if (pushStages != 0) capture.pushConstants.push_back(push); + for (const auto& value : shader.specialization) capture.specialization.push_back({value.id, value.value}); + capture.requiredSubgroupSize = subgroupSize; + capture.flags = pipelineFlags; + capture.deviceName = state->properties.deviceName; + capture.vendorId = state->properties.vendorID; + capture.deviceId = state->properties.deviceID; + capture.driverVersion = state->properties.driverVersion; + capture.apiVersion = state->properties.apiVersion; + std::copy_n(state->properties.pipelineCacheUUID, VK_UUID_SIZE, capture.pipelineCacheUuid.begin()); + const auto path = capture.Write(directory); + std::fprintf(stderr, "[compute-capture] %s\n", path.string().c_str()); + } + if (profile && shader.spirv.size() > 100000) std::fprintf(stderr, "[dispatch] creating a pipeline for %zu SPIR-V words (program 0x%llx)\n", shader.spirv.size(), static_cast(programAddress)); + CreateComputePipeline(state->device, state->DeviceFunction("vkCreateComputePipelines"), context.pipelineCache, objects->module, objects->layout, pipelineFlags, 0, specialization.Info(), subgroupSize, &objects->pipeline, context.pipelineCacheSynchronization); + objects->destroyModule(objects->device, objects->module, nullptr); + objects->module = VK_NULL_HANDLE; + Graphics::LogPipelineStatistics_nid_no_patch(context, objects->pipeline); + if (profile && shader.spirv.size() > 100000) std::fprintf(stderr, "[dispatch] created a pipeline for %zu SPIR-V words in %.0f ms (program 0x%llx)\n", shader.spirv.size(), std::chrono::duration(std::chrono::steady_clock::now() - start).count(), static_cast(programAddress)); + } catch (...) { + finish(nullptr, std::current_exception()); + throw; + } + return finish(std::move(objects), nullptr); +} + VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::span snapshots, std::uint64_t programAddress, std::shared_ptr prepared, std::shared_ptr* recipeOut) { state->recorder->BoundKeptBytes(); ShaderRecompiler::RuntimeAbi::RequireVersion(shader.runtimeAbiVersion); @@ -4347,8 +4446,8 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec // Pipelines are shared by dispatches of one compiled variant; descriptor set layouts built from the // same bindings are compatible, so the pipeline layout of the first dispatch serves them all. const std::uint64_t pipelineKey = shader.PipelineVariantId() != 0 ? (shader.PipelineVariantId() << 1u) | (pushStages != 0 ? 1u : 0u) : 0u; - std::shared_ptr objects; - if (pipelineKey != 0) { + std::shared_ptr objects = prepared != nullptr ? prepared->objects : nullptr; + if (objects == nullptr && pipelineKey != 0) { std::lock_guard pipelines(state->computePipelinesMutex); if (const auto found = state->computePipelines.find(pipelineKey); found != state->computePipelines.end()) objects = found->second; } @@ -4491,67 +4590,8 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec timing.Mark("shader_resources"); timer.phase(PhaseResources); if (objects == nullptr) { - objects = std::make_shared(); - objects->device = state->device; - objects->destroyModule = state->DeviceFunction("vkDestroyShaderModule"); - objects->destroyLayout = state->DeviceFunction("vkDestroyPipelineLayout"); - objects->destroyPipeline = state->DeviceFunction("vkDestroyPipeline"); - VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; - moduleInfo.codeSize = shader.spirv.size() * sizeof(std::uint32_t); - moduleInfo.pCode = shader.spirv.data(); - check(state->DeviceFunction("vkCreateShaderModule")(state->device, &moduleInfo, nullptr, &objects->module), "vkCreateShaderModule"); - timing.Mark("validate_shader_module"); - const std::array setLayouts{resources->Layout(), shader.workgroupMemoryDwords != 0 ? state->workgroupMemoryLayout : VK_NULL_HANDLE}; - const VkPushConstantRange push{VK_SHADER_STAGE_COMPUTE_BIT, 0, Graphics::ComputePushConstantBytes}; - VkPipelineLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; - layoutInfo.setLayoutCount = shader.workgroupMemoryDwords != 0 ? ShaderRecompiler::WorkgroupMemoryDescriptorSet + 1 : 1; - layoutInfo.pSetLayouts = setLayouts.data(); - layoutInfo.pushConstantRangeCount = pushStages != 0 ? 1 : 0; - layoutInfo.pPushConstantRanges = pushStages != 0 ? &push : nullptr; - check(state->DeviceFunction("vkCreatePipelineLayout")(state->device, &layoutInfo, nullptr, &objects->layout), "vkCreatePipelineLayout"); - const VkPipelineCreateFlags pipelineFlags = (context.pipelineExecutableInfo ? VK_PIPELINE_CREATE_CAPTURE_STATISTICS_BIT_KHR : 0) | VK_PIPELINE_CREATE_DISPATCH_BASE_BIT; - const Graphics::PipelineSpecialization specialization(shader); - const auto subgroupSize = state->computeWave32 && (shader.hostSubgroupSize == 32u || shader.hostSubgroupSize > state->maxComputeSubgroupSize) ? std::min(shader.hostSubgroupSize, state->maxComputeSubgroupSize) : 0u; - if (const char* directory = std::getenv("APS5_DUMP_COMPUTE_PIPELINES"); directory != nullptr && *directory != '\0' && CaptureComputePipeline(shader.spirv.size())) { - ComputePipelineCapture capture; - capture.spirv = shader.spirv.Copy(); - ComputeDescriptorSetLayout resourcesLayout; - resourcesLayout.flags = resources->LayoutUpdateAfterBind() ? VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT : 0; - const auto layoutBindings = resources->LayoutBindings(); - const auto bindingFlags = resources->LayoutBindingFlags(); - require(layoutBindings.size() == bindingFlags.size(), "compute capture binding flag count differs from layout"); - for (std::size_t index = 0; index < layoutBindings.size(); ++index) { - const auto& binding = layoutBindings[index]; - require(binding.pImmutableSamplers == nullptr, "compute capture does not support immutable samplers"); - resourcesLayout.bindings.push_back({binding.binding, binding.descriptorType, binding.descriptorCount, binding.stageFlags, bindingFlags[index]}); - } - capture.sets.push_back(std::move(resourcesLayout)); - if (shader.workgroupMemoryDwords != 0) capture.sets.push_back({0, {{0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT, 0}}}); - if (pushStages != 0) capture.pushConstants.push_back(push); - for (const auto& value : shader.specialization) capture.specialization.push_back({value.id, value.value}); - capture.requiredSubgroupSize = subgroupSize; - capture.flags = pipelineFlags; - capture.deviceName = state->properties.deviceName; - capture.vendorId = state->properties.vendorID; - capture.deviceId = state->properties.deviceID; - capture.driverVersion = state->properties.driverVersion; - capture.apiVersion = state->properties.apiVersion; - std::copy_n(state->properties.pipelineCacheUUID, VK_UUID_SIZE, capture.pipelineCacheUuid.begin()); - const auto path = capture.Write(directory); - std::fprintf(stderr, "[compute-capture] %s\n", path.string().c_str()); - } - if (profile && shader.spirv.size() > 100000) std::fprintf(stderr, "[dispatch] creating a pipeline for %zu SPIR-V words (program 0x%llx)\n", shader.spirv.size(), static_cast(programAddress)); - CreateComputePipeline(state->device, state->DeviceFunction("vkCreateComputePipelines"), context.pipelineCache, objects->module, objects->layout, pipelineFlags, 0, specialization.Info(), subgroupSize, &objects->pipeline, context.pipelineCacheSynchronization); - objects->destroyModule(objects->device, objects->module, nullptr); - objects->module = VK_NULL_HANDLE; - Graphics::LogPipelineStatistics_nid_no_patch(context, objects->pipeline); + objects = computePipeline(context, shader, *resources, pushStages, programAddress); timing.Mark("pipeline_create"); - if (pipelineKey != 0) { - // Find-or-insert: objects another dispatch of the variant mapped meanwhile serve this - // one too (this call's go with the batch that keeps them). - std::lock_guard pipelines(state->computePipelinesMutex); - objects = state->computePipelines.try_emplace(pipelineKey, objects).first->second; - } timer.phase(PhasePipeline); } // Recorded into the device's open batch: the CPU moves on to the next command while the GPU From ae5309e1c57020f6692a985f60263d2fee02b517 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Wed, 14 Oct 2026 21:05:28 +1300 Subject: [PATCH 2/3] test(agc): a new compute pipeline is created while another thread holds the GPU mutex --- .../Execution/include/VulkanDevice.hpp | 4 + .../Execution/src/VulkanDevice.cpp | 13 +- .../ComputePipelineOutsideGpuMutex.cpp | 123 ++++++++++++++++++ 3 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/ComputePipelineOutsideGpuMutex.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp index 00907029e1..740b9e8239 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp @@ -170,6 +170,10 @@ public: std::uint64_t aliased, created, noSource, shape, refused; }; static CopyAliasing CopyAliasCounts(); + struct ComputePipelineCreation { + std::uint64_t created, underGpuMutex; + }; + static ComputePipelineCreation ComputePipelineCounts(); void ResolveMemory(std::uint64_t address, std::size_t bytes, bool writable); void* Window() const; void Resize(std::uint32_t width, std::uint32_t height); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 4128475a8f..4f540d7810 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -2278,6 +2278,15 @@ VulkanDevice::CopyVerification VulkanDevice::CopyVerifyCounts() { return {copiesVerified.load(std::memory_order_relaxed), copySourceChanged.load(std::memory_order_relaxed), copyDestinationChanged.load(std::memory_order_relaxed), copyReaderMissed.load(std::memory_order_relaxed)}; } +namespace { +std::atomic computePipelinesCreated{0}; +std::atomic computePipelinesCreatedLocked{0}; +} + +VulkanDevice::ComputePipelineCreation VulkanDevice::ComputePipelineCounts() { + return {computePipelinesCreated.load(std::memory_order_relaxed), computePipelinesCreatedLocked.load(std::memory_order_relaxed)}; +} + VulkanDevice::CopyAliasing VulkanDevice::CopyAliasCounts() { return {copiesAliased.load(std::memory_order_relaxed), copyAliasCreated.load(std::memory_order_relaxed), copyAliasNoSource.load(std::memory_order_relaxed), copyAliasShape.load(std::memory_order_relaxed), copyAliasRefused.load(std::memory_order_relaxed)}; } @@ -4414,6 +4423,8 @@ std::shared_ptr VulkanDevice::computePipeline(const Grap CreateComputePipeline(state->device, state->DeviceFunction("vkCreateComputePipelines"), context.pipelineCache, objects->module, objects->layout, pipelineFlags, 0, specialization.Info(), subgroupSize, &objects->pipeline, context.pipelineCacheSynchronization); objects->destroyModule(objects->device, objects->module, nullptr); objects->module = VK_NULL_HANDLE; + computePipelinesCreated.fetch_add(1, std::memory_order_relaxed); + if (GuestMemory::GpuMutex().HeldByThisThread()) computePipelinesCreatedLocked.fetch_add(1, std::memory_order_relaxed); Graphics::LogPipelineStatistics_nid_no_patch(context, objects->pipeline); if (profile && shader.spirv.size() > 100000) std::fprintf(stderr, "[dispatch] created a pipeline for %zu SPIR-V words in %.0f ms (program 0x%llx)\n", shader.spirv.size(), std::chrono::duration(std::chrono::steady_clock::now() - start).count(), static_cast(programAddress)); } catch (...) { @@ -4584,7 +4595,7 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec if (profile && std::chrono::steady_clock::now() - d.cacheReport > std::chrono::seconds(10)) { d.cacheReport = std::chrono::steady_clock::now(); AgcDriver::ProfilePrint_nid_no_patch("[rescache] %llu hits, %llu misses, %llu invalidated, %zu entries (dispatch + draw); template hits: %llu refreshed the data buffers (revalidate %.1f ms), %llu had the same words; Find calls %llu, Touch calls %llu\n", static_cast(d.cacheHits), static_cast(d.cacheMisses), static_cast(d.cacheInvalidated), state->resourceCache.Size(), static_cast(d.templateRefreshed), d.templateRevalidateMs, static_cast(d.templateSameWords), static_cast(ResourceCache::Finds()), static_cast(ResourceCache::Touches())); - AgcDriver::ProfilePrint_nid_no_patch("[vk] deviceProc lookups inside the device call: %llu (%.2f per dispatch); pre-dispatch barriers recorded %llu, skipped %llu\n", static_cast(d.procLookups), d.profiledDispatches != 0 ? static_cast(d.procLookups) / static_cast(d.profiledDispatches) : 0.0, static_cast(d.preBarriersRecorded), static_cast(d.preBarriersSkipped)); + AgcDriver::ProfilePrint_nid_no_patch("[vk] deviceProc lookups inside the device call: %llu (%.2f per dispatch); pre-dispatch barriers recorded %llu, skipped %llu; compute pipelines created %llu (%llu under the GPU mutex)\n", static_cast(d.procLookups), d.profiledDispatches != 0 ? static_cast(d.procLookups) / static_cast(d.profiledDispatches) : 0.0, static_cast(d.preBarriersRecorded), static_cast(d.preBarriersSkipped), static_cast(computePipelinesCreated.load(std::memory_order_relaxed)), static_cast(computePipelinesCreatedLocked.load(std::memory_order_relaxed))); reportRecipes(); } timing.Mark("shader_resources"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ComputePipelineOutsideGpuMutex.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ComputePipelineOutsideGpuMutex.cpp new file mode 100644 index 0000000000..95513fdc4b --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ComputePipelineOutsideGpuMutex.cpp @@ -0,0 +1,123 @@ +#include "VulkanTestDevice.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Submit/include/Acb.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +void Require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +alignas(256) std::array ReaderCode{0xf4040100u, 0xfa000000u, 0xf4080201u, 0xfa000000u, 0xbf8cc07fu, 0xf4000182u, 0xfa000000u, 0xbf8cc07fu, 0x7e000206u, 0xe0700000u, 0x80020000u, 0xbf810000u}; +alignas(256) std::array NewReaderCode{0xbf800000u, 0xf4040100u, 0xfa000000u, 0xf4080201u, 0xfa000000u, 0xbf8cc07fu, 0xf4000182u, 0xfa000000u, 0xbf8cc07fu, 0x7e000206u, 0xe0700000u, 0x80020000u, 0xbf810000u}; + +struct Slot { + alignas(256) std::array value{}; + alignas(256) std::array table{}; + alignas(256) std::array output{}; + alignas(256) std::array outputDescriptor{}; + alignas(256) volatile std::uint32_t done = 0; +}; + +std::array Slots; + +std::uint32_t Low(const volatile void* pointer) { + return static_cast(reinterpret_cast(pointer)); +} + +std::uint32_t High(const volatile void* pointer) { + return static_cast(static_cast(reinterpret_cast(pointer)) >> 32u); +} + +void SetShaderRegister(std::vector& words, std::uint32_t offset, std::uint32_t value) { + words.insert(words.end(), {0xc0017600u, offset, value}); +} + +void Dispatch(std::vector& words, const void* code, std::initializer_list userData) { + const auto address = static_cast(reinterpret_cast(code)); + SetShaderRegister(words, 0x20c, static_cast(address >> 8u)); + SetShaderRegister(words, 0x20d, static_cast(address >> 40u) & 0xffu); + for (const std::uint32_t offset : {0x207u, 0x208u, 0x209u}) SetShaderRegister(words, offset, 1); + SetShaderRegister(words, 0x212, 0); + SetShaderRegister(words, 0x213, static_cast(userData.size()) << 1u); + std::uint32_t offset = 0x240; + for (const auto value : userData) SetShaderRegister(words, offset++, value); + words.insert(words.end(), {0xc0031500u, 1, 1, 1, 0x8041u}); +} + +void WriteData(std::vector& words, volatile std::uint32_t* target, std::uint32_t value) { + words.insert(words.end(), {0xc0033700u, 0x00100200u, Low(target), High(target), value}); +} + +void Submit(std::uint32_t queue, Slot& slot, const void* code, std::uint32_t value) { + slot.value[0] = value; + slot.table = {Low(slot.value.data()), High(slot.value.data())}; + slot.outputDescriptor = {Low(slot.output.data()), High(slot.output.data()) & 0xffffu, static_cast(sizeof(slot.output)), 0x31016facu}; + std::vector words; + Dispatch(words, code, {Low(slot.table.data()), High(slot.table.data()), Low(slot.outputDescriptor.data()), High(slot.outputDescriptor.data())}); + WriteData(words, &slot.done, 1); + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + Require(sceAgcDriverSubmitAcb(queue, &packet) == 0, "compute submit failed"); +} + +void WaitDone(const Slot& slot, const char* what) { + const auto start = std::chrono::steady_clock::now(); + while (slot.done == 0) { + Require(std::chrono::steady_clock::now() - start < std::chrono::seconds(60), what); + std::this_thread::sleep_for(std::chrono::microseconds(200)); + } +} + +} + +int main() { + using AgcDriver::VulkanDevice; + try { + auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + device.reset(); + Submit(0x28, Slots[0], ReaderCode.data(), 0x1234u); + WaitDone(Slots[0], "the first dispatch did not finish"); + Require(Slots[0].output[0] == 0x1234u, "the first dispatch stored " + std::to_string(Slots[0].output[0])); + + const auto before = VulkanDevice::ComputePipelineCounts(); + { + std::unique_lock gpuLock(AgcDriver::GuestMemory::GpuMutex()); + Submit(0x20, Slots[1], NewReaderCode.data(), 0x5678u); + const auto start = std::chrono::steady_clock::now(); + while (VulkanDevice::ComputePipelineCounts().created == before.created) { + Require(std::chrono::steady_clock::now() - start < std::chrono::seconds(60), "a dispatch of a new program did not create its pipeline while another thread held the GPU mutex"); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + Require(Slots[1].done == 0, "the dispatch finished while another thread held the GPU mutex"); + } + WaitDone(Slots[1], "the dispatch of the new program did not finish"); + Require(Slots[1].output[0] == 0x5678u, "the dispatch of the new program stored " + std::to_string(Slots[1].output[0])); + const auto after = VulkanDevice::ComputePipelineCounts(); + Require(after.underGpuMutex == before.underGpuMutex, "a compute pipeline was created under the GPU mutex"); + std::cout << "compute pipeline outside GPU mutex test passed\n"; + AgcDriverWaitIdle_nid_postfix(); + AgcDriverShutdown_nid_postfix(); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + try { + AgcDriverShutdown_nid_postfix(); + } catch (const std::exception& shutdownError) { + std::cerr << shutdownError.what() << '\n'; + } + return 1; + } +} From da47083cc0d5da1d4ec5321e8e213687354e8c28 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Wed, 14 Oct 2026 22:14:15 +1300 Subject: [PATCH 3/3] perf(agc): create address-table compute pipelines before the GPU lock from the described set layout --- .../Execution/include/VulkanDevice.hpp | 3 +- .../Execution/src/VulkanDevice.cpp | 32 +++++++++++-------- .../tests/execution/GlobalLoads.cpp | 12 ++++++- 3 files changed, 31 insertions(+), 16 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp index 740b9e8239..87800734a3 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp @@ -21,7 +21,6 @@ namespace AgcDriver { namespace Graphics { class StorageTexture; -class ShaderResources; } // Stage A of a dispatch's resource build (see VulkanDevice::PrepareDispatch); opaque to callers. @@ -323,7 +322,7 @@ private: IndirectOutcome dispatch(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::span snapshots, std::uint64_t programAddress, std::shared_ptr prepared, std::shared_ptr* recipe); // The stage-A pre-sync over `surfaces` (see PrepareDispatch): the serial waited for, 0 none. std::uint64_t presync(std::span> surfaces); - std::shared_ptr computePipeline(const Graphics::Context& context, const ShaderRecompiler::RecompileResult& shader, const Graphics::ShaderResources& resources, VkShaderStageFlags pushStages, std::uint64_t programAddress); + std::shared_ptr computePipeline(const Graphics::Context& context, const ShaderRecompiler::RecompileResult& shader, VkDescriptorSetLayout setLayout, std::span layoutKey, VkShaderStageFlags pushStages, std::uint64_t programAddress); // A DISPATCH_INDIRECT's argument path (see DispatchIndirect): GPU-side from the host import, or // the counts read on the CPU (the record then continues as a direct dispatch). void decideIndirect(RecordedDispatch& record, IndirectOutcome& outcome, char* groupsText); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 4f540d7810..dc1b079eb0 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -3568,6 +3568,7 @@ struct PreparedDispatch { // mutex, whose batches the dispatch reaps under it before stage B (0: nothing waited for). std::uint64_t presyncSerial = 0; std::shared_ptr objects; + std::vector describedLayoutKey; }; struct RecipeHit { @@ -4063,8 +4064,14 @@ std::shared_ptr VulkanDevice::PrepareDispatch(const ShaderReco const auto& built = cached != nullptr ? *cached : *prepared->resources; const std::array shaders{{compute}}; const auto pushStages = Graphics::PushConstantStages(shaders); - if (built.Layout() != VK_NULL_HANDLE && (pushStages == 0 || state->properties.limits.maxPushConstantsSize >= Graphics::ComputePushConstantBytes)) { - prepared->objects = computePipeline(context, shader, built, pushStages, programAddress); + if (pushStages != 0 && state->properties.limits.maxPushConstantsSize < Graphics::ComputePushConstantBytes) return prepared; + if (built.Layout() != VK_NULL_HANDLE) { + const auto layouts = built.SetLayouts(); + prepared->objects = computePipeline(context, shader, layouts.main, layouts.mainKey, pushStages, programAddress); + } else if (context.descriptorCache != nullptr) { + const auto described = Graphics::DescribeSetLayouts(context, shaders); + prepared->describedLayoutKey = described.mainKey; + prepared->objects = computePipeline(context, shader, described.layouts.main, described.mainKey, pushStages, programAddress); } return prepared; } @@ -4343,7 +4350,7 @@ void VulkanDevice::recordDispatch(RecordedDispatch& record) { recordStep(PhaseRecordCompletion); } -std::shared_ptr VulkanDevice::computePipeline(const Graphics::Context& context, const ShaderRecompiler::RecompileResult& shader, const Graphics::ShaderResources& resources, VkShaderStageFlags pushStages, std::uint64_t programAddress) { +std::shared_ptr VulkanDevice::computePipeline(const Graphics::Context& context, const ShaderRecompiler::RecompileResult& shader, VkDescriptorSetLayout setLayout, std::span layoutKey, VkShaderStageFlags pushStages, std::uint64_t programAddress) { static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; const std::uint64_t pipelineKey = shader.PipelineVariantId() != 0 ? (shader.PipelineVariantId() << 1u) | (pushStages != 0 ? 1u : 0u) : 0u; std::promise> building; @@ -4380,7 +4387,7 @@ std::shared_ptr VulkanDevice::computePipeline(const Grap moduleInfo.codeSize = shader.spirv.size() * sizeof(std::uint32_t); moduleInfo.pCode = shader.spirv.data(); check(state->DeviceFunction("vkCreateShaderModule")(state->device, &moduleInfo, nullptr, &objects->module), "vkCreateShaderModule"); - const std::array setLayouts{resources.Layout(), shader.workgroupMemoryDwords != 0 ? state->workgroupMemoryLayout : VK_NULL_HANDLE}; + const std::array setLayouts{setLayout, shader.workgroupMemoryDwords != 0 ? state->workgroupMemoryLayout : VK_NULL_HANDLE}; const VkPushConstantRange push{VK_SHADER_STAGE_COMPUTE_BIT, 0, Graphics::ComputePushConstantBytes}; VkPipelineLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; layoutInfo.setLayoutCount = shader.workgroupMemoryDwords != 0 ? ShaderRecompiler::WorkgroupMemoryDescriptorSet + 1 : 1; @@ -4395,14 +4402,11 @@ std::shared_ptr VulkanDevice::computePipeline(const Grap ComputePipelineCapture capture; capture.spirv = shader.spirv.Copy(); ComputeDescriptorSetLayout resourcesLayout; - resourcesLayout.flags = resources.LayoutUpdateAfterBind() ? VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT : 0; - const auto layoutBindings = resources.LayoutBindings(); - const auto bindingFlags = resources.LayoutBindingFlags(); - require(layoutBindings.size() == bindingFlags.size(), "compute capture binding flag count differs from layout"); - for (std::size_t index = 0; index < layoutBindings.size(); ++index) { - const auto& binding = layoutBindings[index]; - require(binding.pImmutableSamplers == nullptr, "compute capture does not support immutable samplers"); - resourcesLayout.bindings.push_back({binding.binding, binding.descriptorType, binding.descriptorCount, binding.stageFlags, bindingFlags[index]}); + const bool updateAfterBind = layoutKey.size() % 4u == 1u; + resourcesLayout.flags = updateAfterBind ? VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT : 0; + for (std::size_t index = 0; index + 4u <= layoutKey.size(); index += 4u) { + const auto type = static_cast(layoutKey[index + 1u]); + resourcesLayout.bindings.push_back({layoutKey[index], type, layoutKey[index + 2u], layoutKey[index + 3u], updateAfterBind && type == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER ? VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT : 0u}); } capture.sets.push_back(std::move(resourcesLayout)); if (shader.workgroupMemoryDwords != 0) capture.sets.push_back({0, {{0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT, 0}}}); @@ -4521,6 +4525,7 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec const auto before = profile ? resources->Timing() : Graphics::ShaderResources::BuildTiming{}; const auto completeStart = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; resources->Complete(); + require(prepared->describedLayoutKey.empty() || std::ranges::equal(resources->SetLayouts().mainKey, prepared->describedLayoutKey), "the set layout described for the dispatch's pipeline differs from its build"); if (profile) { const auto completeWall = std::chrono::duration(std::chrono::steady_clock::now() - completeStart).count(); timer.add(PhaseResourcesComplete, completeWall); @@ -4601,7 +4606,8 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec timing.Mark("shader_resources"); timer.phase(PhaseResources); if (objects == nullptr) { - objects = computePipeline(context, shader, *resources, pushStages, programAddress); + const auto layouts = resources->SetLayouts(); + objects = computePipeline(context, shader, layouts.main, layouts.mainKey, pushStages, programAddress); timing.Mark("pipeline_create"); timer.phase(PhasePipeline); } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/GlobalLoads.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/GlobalLoads.cpp index 0c288dbb13..b13c0f3fb7 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/GlobalLoads.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/GlobalLoads.cpp @@ -1,4 +1,5 @@ #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" #include "prx/libc/include/GuestAllocations.hpp" #include "Recompiler.hpp" @@ -16,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -143,7 +145,15 @@ void Dispatch(AgcDriver::VulkanDevice& device, std::span co }; request.useCache = false; const auto result = ShaderRecompiler::Recompile(request); - device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + const auto before = AgcDriver::VulkanDevice::ComputePipelineCounts(); + auto prepared = device.PrepareDispatch(result, {}, Threads, reinterpret_cast(code.data())); + const auto after = AgcDriver::VulkanDevice::ComputePipelineCounts(); + Require(after.created == before.created + 1 && after.underGpuMutex == before.underGpuMutex, "global loads: the address-table program's pipeline was not created before the GPU mutex"); + { + std::lock_guard gpuLock(AgcDriver::GuestMemory::GpuMutex()); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data()), std::move(prepared)); + } + Require(AgcDriver::VulkanDevice::ComputePipelineCounts().created == after.created, "global loads: the dispatch created a second pipeline"); device.WaitIdle(); }