fix(agc): serialize pipeline cache snapshots

This commit is contained in:
MarcZX100 2026-10-12 22:10:20 +00:00 committed by Dean Galvin
parent ce5c839533
commit 4307e3cb54
10 changed files with 92 additions and 18 deletions

View File

@ -94,7 +94,7 @@ bool CoreVulkanCapability(std::uint32_t capability) {
}
}
void CreateComputePipeline(VkDevice device, PFN_vkCreateComputePipelines create, VkPipelineCache cache, VkShaderModule module, VkPipelineLayout layout, VkPipelineCreateFlags flags, VkPipelineShaderStageCreateFlags stageFlags, const VkSpecializationInfo* specialization, std::uint32_t subgroupSize, VkPipeline* pipeline) {
void CreateComputePipeline(VkDevice device, PFN_vkCreateComputePipelines create, VkPipelineCache cache, VkShaderModule module, VkPipelineLayout layout, VkPipelineCreateFlags flags, VkPipelineShaderStageCreateFlags stageFlags, const VkSpecializationInfo* specialization, std::uint32_t subgroupSize, VkPipeline* pipeline, Graphics::PipelineCacheSynchronization* synchronization = nullptr) {
VkComputePipelineCreateInfo info{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO};
info.flags = flags;
info.stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
@ -107,6 +107,7 @@ void CreateComputePipeline(VkDevice device, PFN_vkCreateComputePipelines create,
subgroup.requiredSubgroupSize = subgroupSize;
if (subgroupSize != 0) info.stage.pNext = &subgroup;
info.layout = layout;
auto cacheLock = synchronization != nullptr ? synchronization->LockForPipelineUse() : Graphics::PipelineCacheSynchronization::PipelineUseLock{};
check(create(device, cache, 1, &info, nullptr, pipeline), "vkCreateComputePipelines");
}
@ -3120,6 +3121,7 @@ Graphics::Context VulkanDevice::buildContext() const {
state->depthClamp,
state->hostImportAlignment
};
context.pipelineCacheSynchronization = state->pipelineCache ? &state->pipelineCache->Synchronization() : nullptr;
context.dmaBufImport = state->dmaBufImport;
context.recorder = state->recorder.get();
context.descriptorCache = state->descriptorCache.get();
@ -4275,7 +4277,7 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec
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);
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);

View File

@ -9,6 +9,8 @@
#include <cstdint>
#include <cstdio>
#include <memory>
#include <mutex>
#include <shared_mutex>
#include <stdexcept>
#include <string>
#include <vector>
@ -28,6 +30,19 @@ class SamplerCache;
class ShaderResources;
class PlaceholderImages;
class PipelineCacheSynchronization {
public:
using PipelineUseLock = std::shared_lock<std::shared_mutex>;
using SnapshotLock = std::unique_lock<std::shared_mutex>;
PipelineUseLock LockForPipelineUse() const { return PipelineUseLock(mutex); }
SnapshotLock LockForSnapshot() const { return SnapshotLock(mutex); }
SnapshotLock TryLockForSnapshot() const { return SnapshotLock(mutex, std::try_to_lock); }
private:
mutable std::shared_mutex mutex;
};
inline void Require(bool condition, const std::string& reason) {
if (!condition) throw std::runtime_error("AGC graphics: " + reason);
}
@ -176,6 +191,11 @@ struct Context {
bool provokingVertexModePerPipeline = false;
bool singlePassStorage = false;
VkSampleCountFlags sampleLocationSampleCounts = 0;
PipelineCacheSynchronization* pipelineCacheSynchronization = nullptr;
PipelineCacheSynchronization::PipelineUseLock LockPipelineCacheForUse() const {
return pipelineCacheSynchronization != nullptr ? pipelineCacheSynchronization->LockForPipelineUse() : PipelineCacheSynchronization::PipelineUseLock{};
}
template<typename TFunction>
TFunction Function(const char* name) const {

View File

@ -21,6 +21,7 @@ public:
PipelineCache(const PipelineCache&) = delete;
PipelineCache& operator=(const PipelineCache&) = delete;
VkPipelineCache Handle() const { return cache; }
PipelineCacheSynchronization& Synchronization() { return synchronization; }
private:
void load(std::vector<std::byte>& initialData);
@ -30,6 +31,7 @@ private:
Context context;
VkPhysicalDeviceProperties properties{};
VkPipelineCache cache = VK_NULL_HANDLE;
PipelineCacheSynchronization synchronization;
std::filesystem::path path;
std::size_t savedBytes = 0;
std::mutex mutex;

View File

@ -35,7 +35,10 @@ GpuColorTransfer::GpuColorTransfer(const Context& context) : context(context) {
pipelineInfo.stage.module = module;
pipelineInfo.stage.pName = "main";
pipelineInfo.layout = pipelineLayout;
Check(context.Function<PFN_vkCreateComputePipelines>("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &pipeline), "vkCreateComputePipelines color transfer");
{
auto pipelineCacheLock = context.LockPipelineCacheForUse();
Check(context.Function<PFN_vkCreateComputePipelines>("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &pipeline), "vkCreateComputePipelines color transfer");
}
context.Function<PFN_vkDestroyShaderModule>("vkDestroyShaderModule")(context.device, module, nullptr);
module = VK_NULL_HANDLE;
const VkDescriptorPoolSize size{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 3};

View File

@ -499,13 +499,18 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput
return;
}
if (libraries) pipeline = LinkPipelineFromLibraries(context, pipelineInfo, rendering, layoutInfo, keys, &optimized, false, _modules);
else Check(context.Function<PFN_vkCreateGraphicsPipelines>("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &pipeline), "vkCreateGraphicsPipelines");
else {
auto pipelineCacheLock = context.LockPipelineCacheForUse();
Check(context.Function<PFN_vkCreateGraphicsPipelines>("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &pipeline), "vkCreateGraphicsPipelines");
}
if (alternateAlpha) {
blend.pAttachments = secondChannels.data();
auto pipelineCacheLock = context.LockPipelineCacheForUse();
Check(context.Function<PFN_vkCreateGraphicsPipelines>("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &secondChannel), "vkCreateGraphicsPipelines");
}
if (state.depthBiasPerFace) {
raster.cullMode = VK_CULL_MODE_FRONT_BIT;
auto pipelineCacheLock = context.LockPipelineCacheForUse();
Check(context.Function<PFN_vkCreateGraphicsPipelines>("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &backFaces), "vkCreateGraphicsPipelines");
}
for (const auto module : _modules) context.Function<PFN_vkDestroyShaderModule>("vkDestroyShaderModule")(context.device, module, nullptr);

View File

@ -106,11 +106,15 @@ void PipelineCache::load(std::vector<std::byte>& initialData) {
void PipelineCache::save(bool final) {
const auto getData = context.Function<PFN_vkGetPipelineCacheData>("vkGetPipelineCacheData");
std::size_t size = 0;
if (getData(context.device, cache, &size, nullptr) != VK_SUCCESS || size == 0 || size == savedBytes) return;
std::vector<std::byte> file(sizeof(FileHeader) + size);
const auto result = getData(context.device, cache, &size, file.data() + sizeof(FileHeader));
if (result != VK_SUCCESS && result != VK_INCOMPLETE) return;
file.resize(sizeof(FileHeader) + size);
std::vector<std::byte> file;
{
auto synchronizationLock = synchronization.LockForSnapshot();
if (getData(context.device, cache, &size, nullptr) != VK_SUCCESS || size == 0 || size == savedBytes) return;
file.resize(sizeof(FileHeader) + size);
const auto result = getData(context.device, cache, &size, file.data() + sizeof(FileHeader));
if (result != VK_SUCCESS && result != VK_INCOMPLETE) return;
file.resize(sizeof(FileHeader) + size);
}
const FileHeader header{FileMagic, FileFormat, size, ShaderRecompiler::HashBytes(std::span(file).subspan(sizeof(FileHeader)))};
std::memcpy(file.data(), &header, sizeof(header));
const auto started = std::chrono::steady_clock::now();

View File

@ -205,7 +205,11 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip
}
VkPipeline handle = VK_NULL_HANDLE;
if (partsOnly) lock.unlock();
const auto created = context.Function<PFN_vkCreateGraphicsPipelines>("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &create, nullptr, &handle);
VkResult created;
{
auto pipelineCacheLock = context.LockPipelineCacheForUse();
created = context.Function<PFN_vkCreateGraphicsPipelines>("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &create, nullptr, &handle);
}
if (partsOnly) lock.lock();
Check(created, "vkCreateGraphicsPipelines library");
if (const auto raced = cache.find(key); raced != cache.end()) {
@ -234,7 +238,10 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip
VkGraphicsPipelineCreateInfo create{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO, &linked};
create.layout = info.layout;
VkPipeline pipeline = VK_NULL_HANDLE;
Check(context.Function<PFN_vkCreateGraphicsPipelines>("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &create, nullptr, &pipeline), "vkCreateGraphicsPipelines link");
{
auto pipelineCacheLock = context.LockPipelineCacheForUse();
Check(context.Function<PFN_vkCreateGraphicsPipelines>("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &create, nullptr, &pipeline), "vkCreateGraphicsPipelines link");
}
++device.counters.linked;
if (optimize && optimized != nullptr) {
auto slot = std::make_shared<OptimizedPipeline>();
@ -243,8 +250,9 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip
*optimized = slot;
const auto createPipelines = context.Function<PFN_vkCreateGraphicsPipelines>("vkCreateGraphicsPipelines");
const auto cache = context.pipelineCache;
const auto cacheSynchronization = context.pipelineCacheSynchronization;
const auto libraryLayout = fullLayout;
enqueue(store, context.device, [slot, createPipelines, cache, libraryLayout, libraries, &store, devicePointer = &device] {
enqueue(store, context.device, [slot, createPipelines, cache, cacheSynchronization, libraryLayout, libraries, &store, devicePointer = &device] {
if (slot->released.load()) return;
VkPipelineLibraryCreateInfoKHR linked{VK_STRUCTURE_TYPE_PIPELINE_LIBRARY_CREATE_INFO_KHR};
linked.libraryCount = static_cast<std::uint32_t>(libraries.size());
@ -253,7 +261,11 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip
create.flags = VK_PIPELINE_CREATE_LINK_TIME_OPTIMIZATION_BIT_EXT;
create.layout = libraryLayout;
VkPipeline result = VK_NULL_HANDLE;
if (createPipelines(slot->device, cache, 1, &create, nullptr, &result) != VK_SUCCESS) return;
const auto pipelineCreationResult = [&] {
auto pipelineCacheLock = cacheSynchronization != nullptr ? cacheSynchronization->LockForPipelineUse() : PipelineCacheSynchronization::PipelineUseLock{};
return createPipelines(slot->device, cache, 1, &create, nullptr, &result);
}();
if (pipelineCreationResult != VK_SUCCESS) return;
slot->handle.store(result);
if (slot->released.load()) {
if (const auto handle = slot->handle.exchange(VK_NULL_HANDLE)) slot->destroy(slot->device, handle, nullptr);

View File

@ -1998,7 +1998,10 @@ bool Recorder::gpuSampleCounter() {
pipelineInfo.stage.module = module;
pipelineInfo.stage.pName = "main";
pipelineInfo.layout = sampleLayout;
Check(context.Function<PFN_vkCreateComputePipelines>("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &samplePipeline), "vkCreateComputePipelines sample counter");
{
auto pipelineCacheLock = context.LockPipelineCacheForUse();
Check(context.Function<PFN_vkCreateComputePipelines>("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &samplePipeline), "vkCreateComputePipelines sample counter");
}
context.Function<PFN_vkDestroyShaderModule>("vkDestroyShaderModule")(context.device, module, nullptr);
module = VK_NULL_HANDLE;
const VkPushConstantRange dumpPush{VK_SHADER_STAGE_COMPUTE_BIT, 0, SampleDumpPushBytes};
@ -2009,7 +2012,10 @@ bool Recorder::gpuSampleCounter() {
Check(context.Function<PFN_vkCreateShaderModule>("vkCreateShaderModule")(context.device, &moduleInfo, nullptr, &module), "vkCreateShaderModule sample dumps");
pipelineInfo.stage.module = module;
pipelineInfo.layout = dumpLayout;
Check(context.Function<PFN_vkCreateComputePipelines>("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &dumpPipeline), "vkCreateComputePipelines sample dumps");
{
auto pipelineCacheLock = context.LockPipelineCacheForUse();
Check(context.Function<PFN_vkCreateComputePipelines>("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &dumpPipeline), "vkCreateComputePipelines sample dumps");
}
context.Function<PFN_vkDestroyShaderModule>("vkDestroyShaderModule")(context.device, module, nullptr);
module = VK_NULL_HANDLE;
} catch (const std::exception& error) {
@ -2286,7 +2292,10 @@ bool Recorder::RecordMeshArguments(VkCommandBuffer commands, VkDeviceAddress rec
pipelineInfo.stage.module = module;
pipelineInfo.stage.pName = "main";
pipelineInfo.layout = meshArgumentLayout;
Check(context.Function<PFN_vkCreateComputePipelines>("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &meshArgumentPipeline), "vkCreateComputePipelines mesh arguments");
{
auto pipelineCacheLock = context.LockPipelineCacheForUse();
Check(context.Function<PFN_vkCreateComputePipelines>("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &meshArgumentPipeline), "vkCreateComputePipelines mesh arguments");
}
context.Function<PFN_vkDestroyShaderModule>("vkDestroyShaderModule")(context.device, module, nullptr);
meshArgumentState = 1;
} catch (const std::exception& error) {

View File

@ -181,7 +181,10 @@ VkPipeline TextureDetiler::pipeline(TextureTileMode tileMode, std::uint32_t elem
createInfo.stage = stage;
createInfo.layout = image ? imagePipelineLayout : pipelineLayout;
VkPipeline result = VK_NULL_HANDLE;
Check(context.Function<PFN_vkCreateComputePipelines>("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &createInfo, nullptr, &result), "vkCreateComputePipelines");
{
auto pipelineCacheLock = context.LockPipelineCacheForUse();
Check(context.Function<PFN_vkCreateComputePipelines>("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &createInfo, nullptr, &result), "vkCreateComputePipelines");
}
pipelines.emplace_back(key, result);
return result;
}
@ -299,7 +302,11 @@ void TextureDetiler::DispatchCmaskClear(VkCommandBuffer commands, VkBuffer cmask
pipelineInfo.stage.module = cmaskModule;
pipelineInfo.stage.pName = "main";
pipelineInfo.layout = cmaskPipelineLayout;
const auto created = context.Function<PFN_vkCreateComputePipelines>("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &cmaskPipeline);
VkResult created;
{
auto pipelineCacheLock = context.LockPipelineCacheForUse();
created = context.Function<PFN_vkCreateComputePipelines>("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &cmaskPipeline);
}
context.Function<PFN_vkDestroyShaderModule>("vkDestroyShaderModule")(context.device, cmaskModule, nullptr);
Check(created, "vkCreateComputePipelines CMASK clear");
cmaskErrors = std::make_unique<Buffer>(context, 4, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT);

View File

@ -5628,6 +5628,16 @@ int main(int argc, char** argv) {
std::cout << "address-space device teardown releases cached guest allocations\n";
return 0;
}
{
AgcDriver::Graphics::PipelineCacheSynchronization synchronization;
auto pipelineUse = synchronization.LockForPipelineUse();
bool snapshotAcquired = false;
std::thread snapshot([&] { snapshotAcquired = synchronization.TryLockForSnapshot().owns_lock(); });
snapshot.join();
Require(!snapshotAcquired, "pipeline cache snapshot overlapped pipeline creation");
pipelineUse.unlock();
Require(synchronization.TryLockForSnapshot().owns_lock(), "pipeline cache snapshot stayed blocked after pipeline creation finished");
}
if (argc == 2 && std::string_view(argv[1]) == "--descriptor-layout-flags") {
descriptorLayoutFlagTests();
std::cout << "Descriptor layout native and captured binding flags passed\n";