diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 44f27630d..effa7dcc2 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -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 "$" -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:$") + 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) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index 5cdd8fa57..3cd41ef7d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -371,16 +372,19 @@ void append(std::vector& 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 pipelineKey(const Context& context, const State& state, const VertexInputLayout& input, const ShaderResources& resources, std::span shaders, VkImageLayout attachmentLayout) { +void pipelineKey(std::vector& key, const Context& context, const State& state, const VertexInputLayout& input, const ShaderResources& resources, std::span shaders, VkImageLayout attachmentLayout) { using Stage = ShaderRecompiler::ShaderStage; - std::vector 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 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& key) { - std::uint64_t hash = 14695981039346656037ull; - for (const auto byte : key) { - hash ^= static_cast(byte); - hash *= 1099511628211ull; - } - return hash; + return std::hash{}(std::string_view(reinterpret_cast(key.data()), key.size())); } struct PipelineStore { @@ -475,6 +473,7 @@ struct PipelineStore { std::shared_ptr pipeline; }; std::mutex mutex; + std::vector scratchKey; // Least recently used first. std::list entries; std::unordered_map::iterator> index; @@ -526,7 +525,8 @@ std::shared_ptr 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(context, state, vertexInput, resources, shaders, attachmentLayout); diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 33b372100..ddd72d4e4 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -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 #include #include +#include #include #include #include @@ -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::begin(TRIANGLE_vert_SPV), std::end(TRIANGLE_vert_SPV)); + fragment.spirv = std::vector(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 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(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());