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:
commit
7fae9c8f00
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
Loading…
Reference in New Issue