Merge pull request #1331 from TibixDev/pr/pipeline-key-storage

perf(pipeline): reuse key storage and hash keys in blocks
This commit is contained in:
Adrià 2026-10-10 03:00:46 +02:00 committed by GitHub
commit 7fae9c8f00
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 111 additions and 11 deletions

View File

@ -491,9 +491,26 @@ add_custom_command(
VERBATIM
)
foreach(agcTriangleStage IN ITEMS vert frag)
set(agcTriangleSource ${CMAKE_CURRENT_SOURCE_DIR}/tests/shaders/Triangle.${agcTriangleStage})
set(agcTriangleSpv ${agcTestShaderGeneratedRoot}/Triangle_${agcTriangleStage}.spv)
set(agcTriangleHeader ${agcTestShaderGeneratedRoot}/Triangle_${agcTriangleStage}_spv.h)
add_custom_command(
OUTPUT "${agcTriangleHeader}"
COMMAND ${CMAKE_COMMAND} -E make_directory "${agcTestShaderGeneratedRoot}"
COMMAND "$<TARGET_FILE:glslang-standalone>" -V --target-env vulkan1.1 -o "${agcTriangleSpv}" "${agcTriangleSource}"
COMMAND ${CMAKE_COMMAND} -DINPUT=${agcTriangleSpv} -DOUTPUT=${agcTriangleHeader}
-DSYMBOL=TRIANGLE_${agcTriangleStage}_SPV -P "${CMAKE_CURRENT_SOURCE_DIR}/embed_spirv.cmake"
DEPENDS "${agcTriangleSource}" glslang-standalone
VERBATIM
)
list(APPEND agcTriangleHeaders "${agcTriangleHeader}")
endforeach()
add_test_executable(agc_driver_recorder_tests EXCLUDE_FROM_ALL
tests/Recorder.cpp
tests/ResidentPresent.cpp
${agcTriangleHeaders}
${agcSampleLodShaderHeader}
${agcSampleArrayShaderHeader}
)
@ -514,6 +531,14 @@ target_link_libraries(agc_driver_recorder_tests PRIVATE
set_target_properties(agc_driver_recorder_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_driver_recorder_tests)
if(BUILD_TESTING)
set_target_properties(agc_driver_recorder_tests PROPERTIES EXCLUDE_FROM_ALL OFF)
add_test(NAME agc_pipeline_cache COMMAND agc_driver_recorder_tests --pipeline-cache-only)
if(WIN32)
set_tests_properties(agc_pipeline_cache PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endif()
add_test_executable(agc_recorder_kept_bytes_tests EXCLUDE_FROM_ALL tests/RecorderKeptBytes.cpp)
target_include_directories(agc_recorder_kept_bytes_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/core/shader/recompiler ${CMAKE_SOURCE_DIR}/3rdparty/Vulkan-Headers/include)

View File

@ -11,6 +11,7 @@
#include <cstdlib>
#include <list>
#include <mutex>
#include <string_view>
#include <type_traits>
#include <unordered_map>
#include <vector>
@ -371,16 +372,19 @@ void append(std::vector<std::byte>& key, const TValue& value) {
// (the recompiler could not identify it, so nothing else may share its pipeline). The rect-list
// control and evaluation stages are generated from the vertex and fragment results, which the key
// already names, so they carry no id of their own.
std::vector<std::byte> pipelineKey(const Context& context, const State& state, const VertexInputLayout& input, const ShaderResources& resources, std::span<const CompiledShader> shaders, VkImageLayout attachmentLayout) {
void pipelineKey(std::vector<std::byte>& key, const Context& context, const State& state, const VertexInputLayout& input, const ShaderResources& resources, std::span<const CompiledShader> shaders, VkImageLayout attachmentLayout) {
using Stage = ShaderRecompiler::ShaderStage;
std::vector<std::byte> key;
key.clear();
append(key, context.device);
append(key, attachmentLayout);
append(key, shaders.size());
for (const auto& shader : shaders) {
Require(shader.program != nullptr, "missing compiled shader");
const bool generated = state.rectList && (shader.stage == Stage::TessellationControl || shader.stage == Stage::TessellationEvaluation);
if (!generated && shader.program->PipelineVariantId() == 0) return {};
if (!generated && shader.program->PipelineVariantId() == 0) {
key.clear();
return;
}
append(key, shader.stage);
append(key, generated ? std::uint64_t{0} : shader.program->PipelineVariantId());
// Where the stage's push constants sit in the block (AssemblePushConstants).
@ -452,16 +456,10 @@ std::vector<std::byte> pipelineKey(const Context& context, const State& state, c
append(key, tessellation.partitioning);
append(key, tessellation.outputTopology);
}
return key;
}
std::uint64_t hashKey(const std::vector<std::byte>& key) {
std::uint64_t hash = 14695981039346656037ull;
for (const auto byte : key) {
hash ^= static_cast<std::uint8_t>(byte);
hash *= 1099511628211ull;
}
return hash;
return std::hash<std::string_view>{}(std::string_view(reinterpret_cast<const char*>(key.data()), key.size()));
}
struct PipelineStore {
@ -475,6 +473,7 @@ struct PipelineStore {
std::shared_ptr<Pipeline> pipeline;
};
std::mutex mutex;
std::vector<std::byte> scratchKey;
// Least recently used first.
std::list<Entry> entries;
std::unordered_map<std::uint64_t, std::list<Entry>::iterator> index;
@ -526,7 +525,8 @@ std::shared_ptr<Pipeline> CachedPipeline(const Context& context, const State& st
auto& store = Pipelines();
std::lock_guard lock(store.mutex);
reportPipelines(store);
const auto key = pipelineKey(context, state, vertexInput, resources, shaders, attachmentLayout);
auto& key = store.scratchKey;
pipelineKey(key, context, state, vertexInput, resources, shaders, attachmentLayout);
if (key.empty()) {
++store.uncached;
return std::make_shared<Pipeline>(context, state, vertexInput, resources, shaders, attachmentLayout);

View File

@ -1,3 +1,7 @@
#include "Triangle_frag_spv.h"
#include "Triangle_vert_spv.h"
#include "prx/libSceAgcDriver/Graphics/include/VertexInput.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
#include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp"
@ -31,6 +35,7 @@
#include <array>
#include <atomic>
#include <cmath>
#include <chrono>
#include <cstdlib>
#include <cstring>
#include <iostream>
@ -2901,6 +2906,71 @@ void atomicViewTests(const Device& device, Recorder& recorder) {
}
}
void pipelineCacheTests(const Device& device, bool benchmark) {
std::thread establishThreadedProcess([] {});
establishThreadedProcess.join();
std::lock_guard gpu(GpuMutex());
const auto& context = device.GetContext();
struct Cleanup {
VkDevice device;
~Cleanup() { ClearCachedPipelines(device); }
} cleanup{context.device};
ShaderRecompiler::RecompileResult vertex, fragment;
vertex.variantId = 1;
fragment.variantId = 2;
vertex.spirv = std::vector<std::uint32_t>(std::begin(TRIANGLE_vert_SPV), std::end(TRIANGLE_vert_SPV));
fragment.spirv = std::vector<std::uint32_t>(std::begin(TRIANGLE_frag_SPV), std::end(TRIANGLE_frag_SPV));
const std::array shaders{CompiledShader{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, CompiledShader{ShaderRecompiler::ShaderStage::Fragment, &fragment, 0}};
State state{};
state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
state.hasColorTarget = true;
state.color.format = VK_FORMAT_R8G8B8A8_UNORM;
state.colors.push_back(state.color);
state.blend.colorWriteMask = 15;
state.blends.push_back(state.blend);
ShaderResources resources(context, shaders, state.color, 0, 0);
const auto lookup = [&](const VertexInputLayout& input) {
return CachedPipeline(context, state, input, resources, shaders, VK_IMAGE_LAYOUT_GENERAL);
};
VertexInputLayout input;
const auto first = lookup(input);
Require(lookup(input) == first, "pipeline cache did not retain an identical key");
state.cullMode = VK_CULL_MODE_BACK_BIT;
Require(lookup(input) != first, "pipeline cache ignored changed raster state");
state.cullMode = 0;
for (const auto attributes : {0u, 8u, 32u}) {
Require(attributes <= context.limits.maxVertexInputBindings && attributes <= context.limits.maxVertexInputAttributes, "pipeline cache fixture exceeds vertex input limits");
input.bindings.clear();
input.attributes.clear();
for (std::uint32_t i = 0; i < attributes; ++i) {
input.bindings.push_back({i, 16, VK_VERTEX_INPUT_RATE_VERTEX});
input.attributes.push_back({i, i, VK_FORMAT_R32G32B32A32_SFLOAT, 0});
}
const auto expected = lookup(input);
if (attributes != 0) Require(expected != first, "pipeline cache ignored changed vertex input");
Require(lookup(input) == expected, "pipeline cache lost the larger key");
if (!benchmark) continue;
std::array<double, 9> times;
for (unsigned pass = 0; pass <= times.size(); ++pass) {
const auto started = std::chrono::steady_clock::now();
for (unsigned i = 0; i < 65536; ++i) Require(lookup(input) == expected, "pipeline cache benchmark missed a warmed key");
if (pass != 0) times[pass - 1] = std::chrono::duration<double, std::micro>(std::chrono::steady_clock::now() - started).count() / 65536;
}
std::ranges::sort(times);
std::cout << "Pipeline cache " << attributes << " attributes: median " << times[4] << " us/lookup, p95 pass " << times.back() << " us\n";
}
input = {};
Require(lookup(input) == first, "pipeline cache retained bytes from a larger key");
vertex.variantId = 0;
const auto unidentified = lookup(input);
Require(lookup(input) != unidentified, "pipeline cache retained an unidentified shader");
vertex.variantId = 1;
Require(lookup(input) == first, "an unidentified shader disturbed the retained pipeline");
ClearCachedPipelines(context.device);
Require(lookup(input) != first, "clearing the pipeline cache retained its old entry");
std::cout << "Pipeline cache key and retained lifetime tests passed\n";
}
void keysFillTests(const Device& device, Recorder& recorder) {
const auto& base = device.GetContext();
if (base.hostImportAlignment == 0) {
@ -3020,6 +3090,11 @@ void keysFillTests(const Device& device, Recorder& recorder) {
int main(int argc, char** argv) {
try {
Device device;
if (argc == 2 && (std::string_view(argv[1]) == "--benchmark-pipeline-cache" || std::string_view(argv[1]) == "--pipeline-cache-only")) {
pipelineCacheTests(device, std::string_view(argv[1]) == "--benchmark-pipeline-cache");
return 0;
}
std::lock_guard gpu(GpuMutex());
std::cout << "host imports " << (PrepareImportWatch(device.GetContext()) == ImportWatch::Unwatch ? "are compared" : "stay watched") << '\n';
Recorder recorder(device.GetContext());