Merge pull request #5989 from oneandonlydean/carry4897/compute-pipeline-unlocked

perf(agc): create compute pipelines before taking the GPU lock
This commit is contained in:
Dean authored and GitHub committed 2026-10-15 09:34:45 +13:00
commit e6ef803ce0
5 files changed
+263 -67

No files matched your search

@@ -25,6 +25,7 @@ class StorageTexture;
// 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).
@@ -168,6 +169,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);
@@ -234,7 +239,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<PreparedDispatch> PrepareDispatch(const ShaderRecompiler::RecompileResult& shader, std::span<const Graphics::GuestMemorySnapshot> snapshots, std::uint64_t dispatchThreads = 0);
std::shared_ptr<PreparedDispatch> PrepareDispatch(const ShaderRecompiler::RecompileResult& shader, std::span<const Graphics::GuestMemorySnapshot> 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<const double, 5> PreparePhaseMs(const PreparedDispatch& prepared);
@@ -317,6 +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<const Graphics::GuestMemorySnapshot> snapshots, std::uint64_t programAddress, std::shared_ptr<PreparedDispatch> prepared, std::shared_ptr<const Recipe>* recipe);
// The stage-A pre-sync over `surfaces` (see PrepareDispatch): the serial waited for, 0 none.
std::uint64_t presync(std::span<const std::pair<std::uint64_t, std::uint64_t>> surfaces);
std::shared_ptr<ComputePipelineObjects> computePipeline(const Graphics::Context& context, const ShaderRecompiler::RecompileResult& shader, VkDescriptorSetLayout setLayout, std::span<const std::uint32_t> 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);
@@ -373,7 +373,7 @@ void Driver::dispatch(QueueState& queue, std::span<const std::uint32_t> packet,
std::shared_ptr<PreparedDispatch> 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;
}
@@ -43,6 +43,7 @@
#include <chrono>
#include <charconv>
#include <condition_variable>
#include <future>
#include <fstream>
#include <cctype>
#include <cstdlib>
@@ -353,6 +354,7 @@ struct VulkanDevice::State {
// references to its objects.
std::mutex computePipelinesMutex;
std::map<std::uint64_t, std::shared_ptr<ComputePipelineObjects>> computePipelines;
std::map<std::uint64_t, std::shared_future<std::shared_ptr<ComputePipelineObjects>>> computePipelinesBuilding;
std::unique_ptr<Graphics::GpuColorTransfer> colorTransfer;
std::shared_ptr<Graphics::BufferPool> bufferPool;
std::unique_ptr<Graphics::Buffer> 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.
@@ -2275,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<std::uint64_t> computePipelinesCreated{0};
std::atomic<std::uint64_t> 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)};
}
@@ -3555,6 +3567,8 @@ 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<ComputePipelineObjects> objects;
std::vector<std::uint32_t> describedLayoutKey;
};
struct RecipeHit {
@@ -3993,7 +4007,7 @@ std::uint64_t VulkanDevice::presync(std::span<const std::pair<std::uint64_t, std
return serial;
}
std::shared_ptr<PreparedDispatch> VulkanDevice::PrepareDispatch(const ShaderRecompiler::RecompileResult& shader, std::span<const Graphics::GuestMemorySnapshot> snapshots, std::uint64_t dispatchThreads) {
std::shared_ptr<PreparedDispatch> VulkanDevice::PrepareDispatch(const ShaderRecompiler::RecompileResult& shader, std::span<const Graphics::GuestMemorySnapshot> 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 +4061,18 @@ std::shared_ptr<PreparedDispatch> VulkanDevice::PrepareDispatch(const ShaderReco
phase(PreparedDispatch::PreparePresync);
}
if (profile) prepared->prepareMs = std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - start).count();
const auto& built = cached != nullptr ? *cached : *prepared->resources;
const std::array<Graphics::CompiledShader, 1> shaders{{compute}};
const auto pushStages = Graphics::PushConstantStages(shaders);
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;
}
@@ -4324,6 +4350,94 @@ void VulkanDevice::recordDispatch(RecordedDispatch& record) {
recordStep(PhaseRecordCompletion);
}
std::shared_ptr<ComputePipelineObjects> VulkanDevice::computePipeline(const Graphics::Context& context, const ShaderRecompiler::RecompileResult& shader, VkDescriptorSetLayout setLayout, std::span<const std::uint32_t> 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<std::shared_ptr<ComputePipelineObjects>> building;
if (pipelineKey != 0) {
std::shared_future<std::shared_ptr<ComputePipelineObjects>> 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<ComputePipelineObjects> 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<ComputePipelineObjects> objects;
try {
const auto start = std::chrono::steady_clock::now();
objects = std::make_shared<ComputePipelineObjects>();
objects->device = state->device;
objects->destroyModule = state->DeviceFunction<PFN_vkDestroyShaderModule>("vkDestroyShaderModule");
objects->destroyLayout = state->DeviceFunction<PFN_vkDestroyPipelineLayout>("vkDestroyPipelineLayout");
objects->destroyPipeline = state->DeviceFunction<PFN_vkDestroyPipeline>("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<PFN_vkCreateShaderModule>("vkCreateShaderModule")(state->device, &moduleInfo, nullptr, &objects->module), "vkCreateShaderModule");
const std::array<VkDescriptorSetLayout, 2> 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;
layoutInfo.pSetLayouts = setLayouts.data();
layoutInfo.pushConstantRangeCount = pushStages != 0 ? 1 : 0;
layoutInfo.pPushConstantRanges = pushStages != 0 ? &push : nullptr;
check(state->DeviceFunction<PFN_vkCreatePipelineLayout>("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;
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<VkDescriptorType>(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}}});
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<unsigned long long>(programAddress));
CreateComputePipeline(state->device, state->DeviceFunction<PFN_vkCreateComputePipelines>("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<double, std::milli>(std::chrono::steady_clock::now() - start).count(), static_cast<unsigned long long>(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<const Graphics::GuestMemorySnapshot> snapshots, std::uint64_t programAddress, std::shared_ptr<PreparedDispatch> prepared, std::shared_ptr<const Recipe>* recipeOut) {
state->recorder->BoundKeptBytes();
ShaderRecompiler::RuntimeAbi::RequireVersion(shader.runtimeAbiVersion);
@@ -4347,8 +4461,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<ComputePipelineObjects> objects;
if (pipelineKey != 0) {
std::shared_ptr<ComputePipelineObjects> 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;
}
@@ -4411,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<double, std::milli>(std::chrono::steady_clock::now() - completeStart).count();
timer.add(PhaseResourcesComplete, completeWall);
@@ -4485,73 +4600,15 @@ 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<unsigned long long>(d.cacheHits), static_cast<unsigned long long>(d.cacheMisses), static_cast<unsigned long long>(d.cacheInvalidated), state->resourceCache.Size(), static_cast<unsigned long long>(d.templateRefreshed), d.templateRevalidateMs, static_cast<unsigned long long>(d.templateSameWords), static_cast<unsigned long long>(ResourceCache::Finds()), static_cast<unsigned long long>(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<unsigned long long>(d.procLookups), d.profiledDispatches != 0 ? static_cast<double>(d.procLookups) / static_cast<double>(d.profiledDispatches) : 0.0, static_cast<unsigned long long>(d.preBarriersRecorded), static_cast<unsigned long long>(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<unsigned long long>(d.procLookups), d.profiledDispatches != 0 ? static_cast<double>(d.procLookups) / static_cast<double>(d.profiledDispatches) : 0.0, static_cast<unsigned long long>(d.preBarriersRecorded), static_cast<unsigned long long>(d.preBarriersSkipped), static_cast<unsigned long long>(computePipelinesCreated.load(std::memory_order_relaxed)), static_cast<unsigned long long>(computePipelinesCreatedLocked.load(std::memory_order_relaxed)));
reportRecipes();
}
timing.Mark("shader_resources");
timer.phase(PhaseResources);
if (objects == nullptr) {
objects = std::make_shared<ComputePipelineObjects>();
objects->device = state->device;
objects->destroyModule = state->DeviceFunction<PFN_vkDestroyShaderModule>("vkDestroyShaderModule");
objects->destroyLayout = state->DeviceFunction<PFN_vkDestroyPipelineLayout>("vkDestroyPipelineLayout");
objects->destroyPipeline = state->DeviceFunction<PFN_vkDestroyPipeline>("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<PFN_vkCreateShaderModule>("vkCreateShaderModule")(state->device, &moduleInfo, nullptr, &objects->module), "vkCreateShaderModule");
timing.Mark("validate_shader_module");
const std::array<VkDescriptorSetLayout, 2> 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<PFN_vkCreatePipelineLayout>("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<unsigned long long>(programAddress));
CreateComputePipeline(state->device, state->DeviceFunction<PFN_vkCreateComputePipelines>("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);
const auto layouts = resources->SetLayouts();
objects = computePipeline(context, shader, layouts.main, layouts.mainKey, 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
@@ -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 <array>
#include <chrono>
#include <cstdint>
#include <initializer_list>
#include <iostream>
#include <mutex>
#include <stdexcept>
#include <string>
#include <thread>
#include <vector>
namespace {
void Require(bool condition, const std::string& message) {
if (!condition) throw std::runtime_error(message);
}
alignas(256) std::array<std::uint32_t, 12> ReaderCode{0xf4040100u, 0xfa000000u, 0xf4080201u, 0xfa000000u, 0xbf8cc07fu, 0xf4000182u, 0xfa000000u, 0xbf8cc07fu, 0x7e000206u, 0xe0700000u, 0x80020000u, 0xbf810000u};
alignas(256) std::array<std::uint32_t, 13> NewReaderCode{0xbf800000u, 0xf4040100u, 0xfa000000u, 0xf4080201u, 0xfa000000u, 0xbf8cc07fu, 0xf4000182u, 0xfa000000u, 0xbf8cc07fu, 0x7e000206u, 0xe0700000u, 0x80020000u, 0xbf810000u};
struct Slot {
alignas(256) std::array<std::uint32_t, 4> value{};
alignas(256) std::array<std::uint32_t, 2> table{};
alignas(256) std::array<std::uint32_t, 4> output{};
alignas(256) std::array<std::uint32_t, 4> outputDescriptor{};
alignas(256) volatile std::uint32_t done = 0;
};
std::array<Slot, 2> Slots;
std::uint32_t Low(const volatile void* pointer) {
return static_cast<std::uint32_t>(reinterpret_cast<std::uintptr_t>(pointer));
}
std::uint32_t High(const volatile void* pointer) {
return static_cast<std::uint32_t>(static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(pointer)) >> 32u);
}
void SetShaderRegister(std::vector<std::uint32_t>& words, std::uint32_t offset, std::uint32_t value) {
words.insert(words.end(), {0xc0017600u, offset, value});
}
void Dispatch(std::vector<std::uint32_t>& words, const void* code, std::initializer_list<std::uint32_t> userData) {
const auto address = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(code));
SetShaderRegister(words, 0x20c, static_cast<std::uint32_t>(address >> 8u));
SetShaderRegister(words, 0x20d, static_cast<std::uint32_t>(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<std::uint32_t>(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<std::uint32_t>& 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<std::uint32_t>(sizeof(slot.output)), 0x31016facu};
std::vector<std::uint32_t> 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<std::uint32_t>(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;
}
}
@@ -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 <cstdlib>
#include <cstring>
#include <iostream>
#include <mutex>
#include <span>
#include <string>
#include <vector>
@@ -143,7 +145,15 @@ void Dispatch(AgcDriver::VulkanDevice& device, std::span<const std::uint32_t> co
};
request.useCache = false;
const auto result = ShaderRecompiler::Recompile(request);
device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(code.data()));
const auto before = AgcDriver::VulkanDevice::ComputePipelineCounts();
auto prepared = device.PrepareDispatch(result, {}, Threads, reinterpret_cast<std::uintptr_t>(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<std::uintptr_t>(code.data()), std::move(prepared));
}
Require(AgcDriver::VulkanDevice::ComputePipelineCounts().created == after.created, "global loads: the dispatch created a second pipeline");
device.WaitIdle();
}