perf(agc): build the shader pipeline libraries when graphics stages are resolved at registration

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Adrià Franch 2026-10-11 15:51:30 +02:00
parent d7e0f27d6b
commit 7f64d14772
12 changed files with 120 additions and 7 deletions

View File

@ -37,6 +37,9 @@ void DecodeGraphicsPrograms(DrawDecode& decoded, const QueueState& queue, const
struct PreparedGraphicsStage {
std::shared_ptr<const ShaderSnapshot> snapshot;
PreparedShaders::Entry entry;
ShaderRecompiler::ShaderStage stage = ShaderRecompiler::ShaderStage::Vertex;
std::uint32_t pushOffset = 0;
std::shared_ptr<const ShaderRecompiler::RecompileResult> registration;
};
std::vector<PreparedGraphicsStage> PrepareGraphicsStages(const DrawDecode& decoded, const ShaderRecompiler::SpirvTarget& target);

View File

@ -299,6 +299,7 @@ public:
};
IndirectDrawSupport DrawIndirectSupport() const;
void EnqueueDraw(const Graphics::State& graphics, const Pm4::DrawParameters& draw, std::span<const Graphics::CompiledShader> shaders, std::span<const Graphics::GuestMemorySnapshot> snapshots = {});
bool PrebuildGraphicsLibraries(const Graphics::ShaderStages& stages, std::span<const Graphics::CompiledShader> shaders);
private:
VulkanDevice(const PresentationWindow* window, bool compileOnly);

View File

@ -21,6 +21,7 @@
#include <deque>
#include <functional>
#include <list>
#include <atomic>
#include <pthread.h>
#include <stdexcept>
#include <thread>
@ -30,7 +31,8 @@ namespace AgcDriver::DriverDetail {
std::shared_ptr<const ShaderRecompiler::SourceHandle> PrepareShaderWithDiagnostics(const ShaderRecompiler::RecompileRequest& request, ShaderRecompiler::ShaderPreparationContext* preparation) {
try {
return ShaderRecompiler::PrepareShader(request, preparation);
auto handle = ShaderRecompiler::PrepareShader(request, preparation);
return handle;
} catch (...) {
try {
if (std::getenv("APS5_DUMP_SHADERS") != nullptr) static_cast<void>(Driver::dumpRequest(request.shader.codeAddress, request));
@ -860,7 +862,14 @@ std::vector<PreparedGraphicsStage> PrepareGraphicsStages(const DrawDecode& decod
}
const auto bytes = handle->artifact->bindings.pushConstantSizeBytes;
require(bytes <= room, "prepared graphics stages exceed the push constant block");
prepared.push_back({program.snapshot, {program.codeOffset, handle}});
std::shared_ptr<const ShaderRecompiler::RecompileResult> registration;
if (target.registrationModules) {
try {
registration = ShaderRecompiler::RegistrationResult(request, *handle);
} catch (const std::exception&) {
}
}
prepared.push_back({program.snapshot, {program.codeOffset, handle}, program.binary.stage, offset, std::move(registration)});
pushOffset += bytes;
}
transaction.Commit();
@ -930,6 +939,23 @@ void Driver::ResolveGraphicsStagesAbi(std::span<const Shader* const> stages, std
if (stage.snapshot->type == 1) continue;
ResolvePreparedGraphics(*stage.snapshot, {}, primitiveType == state.userConfig.end() ? 0u : primitiveType->second, localDevice->Target());
}
if (localDevice->Target().registrationModules && !prepared.empty() && std::ranges::all_of(prepared, [](const auto& stage) { return stage.registration != nullptr && (stage.stage == ShaderRecompiler::ShaderStage::Vertex || stage.stage == ShaderRecompiler::ShaderStage::Fragment); }) && prepared.back().stage == ShaderRecompiler::ShaderStage::Fragment) {
std::vector<std::shared_ptr<const ShaderRecompiler::RecompileResult>> results;
std::vector<std::pair<ShaderRecompiler::ShaderStage, std::uint32_t>> stages;
for (const auto& stage : prepared) {
results.push_back(stage.registration);
stages.emplace_back(stage.stage, stage.pushOffset);
}
SubmitRegistrationPreparation([localDevice, results = std::move(results), stages = std::move(stages), shaderStages = decoded.state.stages] {
std::vector<Graphics::CompiledShader> shaders;
for (std::size_t index = 0; index < results.size(); ++index) shaders.push_back({stages[index].first, results[index].get(), stages[index].second});
try {
static_cast<void>(localDevice->PrebuildGraphicsLibraries(shaderStages, shaders));
} catch (const std::exception& error) {
APS5_LOG_OUT("graphics library prebuild skipped: %s", error.what());
}
});
}
PreparedShaderState::GraphicsAbi abi{key, registry, {}};
for (const auto& stage : prepared) abi.stages.push_back(stage.snapshot);
auto& graphicsAbis = transaction.Edit(*owner).graphicsAbis;

View File

@ -3058,6 +3058,25 @@ void VulkanDevice::ColorMetadataPass(const Graphics::ColorMetadataPass& pass) {
Graphics::RunColorMetadataPass(graphicsContext(), pass);
}
bool VulkanDevice::PrebuildGraphicsLibraries(const Graphics::ShaderStages& stages, std::span<const Graphics::CompiledShader> shaders) {
PerformanceTimer timing("Vulkan.GraphicsPipelinePrebuild");
const auto context = graphicsContext();
if (!context.graphicsPipelineLibrary || context.descriptorCache == nullptr) return false;
Graphics::State state{};
state.stages = stages;
state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
state.color.format = VK_FORMAT_R8G8B8A8_UNORM;
state.color.exportIndex = 0;
state.colors.push_back(state.color);
state.hasColorTarget = true;
state.blend.colorWriteMask = 15;
state.blends.push_back(state.blend);
state.renderExtent = {1, 1};
const auto layouts = Graphics::DescribeSetLayouts(context, shaders);
return Graphics::Pipeline::Prebuild(context, state, Graphics::VertexInputLayout{}, layouts.View(), shaders);
}
void VulkanDevice::Draw(const Graphics::State& graphics, const Pm4::DrawParameters& draw, std::span<const Graphics::CompiledShader> shaders, std::span<const Graphics::GuestMemorySnapshot> snapshots, std::shared_ptr<const DrawRecipe>* recipe) {
for (const auto& shader : shaders) {
require(shader.program != nullptr, "missing compiled shader");

View File

@ -43,6 +43,7 @@ public:
Pipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const ShaderResources& resources, std::span<const CompiledShader> shaders, VkImageLayout attachmentLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
Pipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span<const CompiledShader> shaders, VkImageLayout attachmentLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
~Pipeline();
static bool Prebuild(const Context& context, const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span<const CompiledShader> shaders);
Pipeline(const Pipeline&) = delete;
Pipeline& operator=(const Pipeline&) = delete;
VkPipelineLayout Layout() const;

View File

@ -33,7 +33,7 @@ struct OptimizedPipeline {
void Release() noexcept;
};
VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPipelineCreateInfo& info, const VkPipelineRenderingCreateInfoKHR& rendering, const VkPipelineLayoutCreateInfo& layout, const PipelineLibraryKeys& keys, std::shared_ptr<OptimizedPipeline>* optimized = nullptr, std::span<VkShaderModule> modules = {});
VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPipelineCreateInfo& info, const VkPipelineRenderingCreateInfoKHR& rendering, const VkPipelineLayoutCreateInfo& layout, const PipelineLibraryKeys& keys, std::shared_ptr<OptimizedPipeline>* optimized = nullptr, std::span<VkShaderModule> modules = {}, bool partsOnly = false);
void WaitForOptimizedPipelines(VkDevice device);

View File

@ -152,6 +152,9 @@ bool ShadersWriteMemory(std::span<const CompiledShader> shaders) {
namespace {
thread_local bool prebuilding = false;
thread_local bool prebuilt = false;
VkShaderStageFlags pushStagesOf(const Context& context) {
return PreRasterizationPushStages(context.meshShader, context.tessellationShader, context.geometryShader);
}
@ -469,7 +472,18 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput
pipelineInfo.renderPass = renderPass;
if (dynamicRendering) pipelineInfo.pNext = &rendering;
timing.Mark("modules_and_state");
if (libraries) pipeline = LinkPipelineFromLibraries(context, pipelineInfo, rendering, layoutInfo, keys, &optimized, _modules);
if (prebuilding && !libraries) {
for (const auto module : _modules) context.Function<PFN_vkDestroyShaderModule>("vkDestroyShaderModule")(context.device, module, nullptr);
_modules.clear();
return;
}
if (libraries) pipeline = LinkPipelineFromLibraries(context, pipelineInfo, rendering, layoutInfo, keys, &optimized, _modules, prebuilding);
if (prebuilding) {
for (const auto module : _modules) context.Function<PFN_vkDestroyShaderModule>("vkDestroyShaderModule")(context.device, module, nullptr);
_modules.clear();
prebuilt = true;
return;
}
else Check(context.Function<PFN_vkCreateGraphicsPipelines>("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &pipeline), "vkCreateGraphicsPipelines");
if (alternateAlpha) {
blend.pAttachments = secondChannels.data();
@ -489,6 +503,15 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput
}
}
bool Pipeline::Prebuild(const Context& context, const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span<const CompiledShader> shaders) {
struct Scope {
Scope() { prebuilding = true; prebuilt = false; }
~Scope() { prebuilding = false; }
} scope;
const Pipeline pipeline(context, state, vertexInput, resources, shaders);
return prebuilt;
}
Pipeline::~Pipeline() {
release();
}

View File

@ -102,10 +102,10 @@ std::span<const VkDynamicState> PipelineLibraryDynamicStates() {
return dynamicStates;
}
VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPipelineCreateInfo& info, const VkPipelineRenderingCreateInfoKHR& rendering, const VkPipelineLayoutCreateInfo& layout, const PipelineLibraryKeys& keys, std::shared_ptr<OptimizedPipeline>* optimized, std::span<VkShaderModule> modules) {
VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPipelineCreateInfo& info, const VkPipelineRenderingCreateInfoKHR& rendering, const VkPipelineLayoutCreateInfo& layout, const PipelineLibraryKeys& keys, std::shared_ptr<OptimizedPipeline>* optimized, std::span<VkShaderModule> modules, bool partsOnly) {
Require(info.renderPass == VK_NULL_HANDLE && info.pDynamicState != nullptr, "pipeline library parts need dynamic rendering and the library dynamic states");
auto& store = Libraries();
std::lock_guard lock(store.mutex);
std::unique_lock lock(store.mutex);
auto& device = store.devices[context.device];
if (device.destroyPipeline == nullptr) {
device.destroyPipeline = context.Function<PFN_vkDestroyPipeline>("vkDestroyPipeline");
@ -193,7 +193,15 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip
break;
}
VkPipeline handle = VK_NULL_HANDLE;
Check(context.Function<PFN_vkCreateGraphicsPipelines>("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &create, nullptr, &handle), "vkCreateGraphicsPipelines library");
if (partsOnly) lock.unlock();
const auto 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()) {
device.destroyPipeline(context.device, handle, nullptr);
libraries.push_back(raced->second);
continue;
}
cache.emplace(key, handle);
if (optimize) {
for (std::uint32_t stage = 0; stage < create.stageCount; ++stage) {
@ -207,6 +215,7 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip
++device.counters.built[part];
libraries.push_back(handle);
}
if (partsOnly) return VK_NULL_HANDLE;
PerformanceTimer timing("Vulkan.GraphicsPipelineLink");
VkPipelineLibraryCreateInfoKHR linked{VK_STRUCTURE_TYPE_PIPELINE_LIBRARY_CREATE_INFO_KHR};
linked.libraryCount = static_cast<std::uint32_t>(libraries.size());

View File

@ -1,3 +1,4 @@
#include "prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp"
#include "VulkanTestDevice.hpp"
#include "prx/libSceAgcDriver/Execution/include/ShaderPreparation.hpp"
#include "SceShaders.hpp"
@ -130,6 +131,22 @@ void Check(AgcDriver::VulkanDevice& device, AgcDriver::Graphics::ShaderPath path
}
const auto stages = PrepareGraphicsStages(prepared, target);
Require(stages.size() == (tessellation ? 4u : 2u), "graphics preparation compiled the wrong stages");
if (!mesh && !tessellation && target.registrationModules) {
const auto prebuild = [&](const std::vector<PreparedGraphicsStage>& group) {
Require(group.front().registration != nullptr && group.back().registration != nullptr, "registration modules: a prepared stage has no registration result");
const std::array<AgcDriver::Graphics::CompiledShader, 2> shaders{{{group.front().stage, group.front().registration.get(), group.front().pushOffset}, {group.back().stage, group.back().registration.get(), group.back().pushOffset}}};
const auto before = AgcDriver::Graphics::PipelineLibraryCountersOf(device.Device());
Require(device.PrebuildGraphicsLibraries(prepared.state.stages, shaders), "registration modules: the shader libraries were not prebuilt");
const auto after = AgcDriver::Graphics::PipelineLibraryCountersOf(device.Device());
return std::pair{after.built[1] - before.built[1] + after.built[2] - before.built[2], after.linked - before.linked};
};
const auto [first, firstLinks] = prebuild(stages);
Require(first == 2u && firstLinks == 0u, "registration modules: the prebuild did not build exactly the two shader libraries without linking");
const auto again = PrepareGraphicsStages(prepared, target);
Require(again.front().registration != stages.front().registration, "registration modules: the second preparation reused the first result object");
const auto [second, secondLinks] = prebuild(again);
Require(second == 0u && secondLinks == 0u, "registration modules: another materialization of the same stages did not find the prebuilt libraries");
}
for (const auto& stage : stages) stage.snapshot->prepared->entries.push_back(stage.entry);
const auto& pixelArtifact = GetPreparedArtifact(*stages.back().entry.handle);
Require(!pixelArtifact.fragmentParameters.empty() && pixelArtifact.fragmentParameters.front().sourceLocation == 3u, "prepared fragment lost interpolant mapping");

View File

@ -236,6 +236,7 @@ function(add_shader_recompiler target runtimeTarget)
MatchesPreparedShader
GetPreparedArtifact
MaterializeShader
RegistrationResult
ResolveSource
ResourceMaterializer::SpecializationNanoseconds
RuntimeAbi::RequireVersion

View File

@ -70,6 +70,7 @@ private:
void BuildPreparedShaderKey(const RecompileRequest& request, std::vector<std::uint64_t>& key);
[[nodiscard]] bool MatchesPreparedShader(const RecompileRequest& request, const SourceHandle& handle, std::span<const std::uint64_t> key);
[[nodiscard]] const CompiledShaderArtifact& GetPreparedArtifact(const SourceHandle& handle);
[[nodiscard]] std::shared_ptr<const RecompileResult> RegistrationResult(const RecompileRequest& request, const SourceHandle& handle);
[[nodiscard]] std::shared_ptr<const RecompileResult> MaterializeShader(const RecompileRequest& request, const ResourceCapture& capture, const SourceHandle& handle);
[[nodiscard]] std::shared_ptr<const SourceHandle> ResolveSource(const RecompileRequest& request);
[[nodiscard]] std::shared_ptr<const ResourceCapture> CaptureResources(const RecompileRequest& request, const SrtRuntime& runtime, const SourceHandle& handle);

View File

@ -1216,6 +1216,18 @@ const CompiledShaderArtifact& GetPreparedArtifact(const SourceHandle& handle) {
return handle.artifact->artifact;
}
std::shared_ptr<const RecompileResult> RegistrationResult(const RecompileRequest& request, const SourceHandle& handle) {
if (!request.target.registrationModules) throw std::runtime_error("ShaderRecompiler: registration results need registration modules");
if (handle.artifact == nullptr || !MatchesPreparedShader(request, handle)) throw std::runtime_error("ShaderRecompiler: prepared artifact does not match the static ABI");
const auto& info = handle.artifact->info.info;
ResourceSnapshot snapshot;
snapshot.buffers.assign(info.buffers.size(), {{}, 4u});
snapshot.images.assign(info.images.size(), {{}, 8u});
snapshot.samplers.assign(info.samplers.size(), {{}, 4u});
snapshot.userData.assign(RuntimeAbi::UserDataCapacity, 0u);
return std::make_shared<RecompileResult>(materializeResult(*handle.artifact, request, snapshot));
}
std::shared_ptr<const RecompileResult> MaterializeShader(const RecompileRequest& request, const ResourceCapture& capture, const SourceHandle& handle) {
if (!MatchesPreparedShader(request, handle)) throw std::runtime_error("ShaderRecompiler: prepared artifact does not match the static ABI");
if (capture.source != handle.source || capture.plan != handle.source->plan) throw std::runtime_error("ShaderRecompiler: resource capture belongs to another prepared shader");