From 2b8f151b3f3d9e887ff1176440b1905dc3199c3a Mon Sep 17 00:00:00 2001 From: jweidenh <38249793+jweidenh@users.noreply.github.com> Date: Sun, 11 Oct 2026 12:09:51 -0400 Subject: [PATCH] test(agc): verify optimized pipeline draws and readback --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 18 +- .../Execution/include/VulkanDevice.hpp | 1 + .../execution/PipelineOptimizedReadback.cpp | 276 ++++++++++++++++++ 3 files changed, 294 insertions(+), 1 deletion(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/PipelineOptimizedReadback.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index e77f027d12e..20c42fc929d 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1038,7 +1038,7 @@ if(BUILD_TESTING) set_tests_properties(agc_shader_bindless agc_shader_srt_read_patching agc_shader_frontend_pair agc_shader_frontend_pair_off PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() foreach(agcTest IN ITEMS agc_shared_write_tests agc_driver_tests agc_shader_memory_tests agc_structurizer_tests agc_spirv_module_tests agc_dpp_write_tests agc_masked_select_tests agc_vector_window_tests agc_constant_bit_test_tests agc_scalar_select_mask_tests agc_vop1_control_decode_tests agc_validate_program_tests agc_pass_hazards_tests agc_shader_disk_cache_tests agc_shader_device_profile_tests agc_placeholder_images_tests agc_shader_control_flow_tests agc_driver_flip_tests agc_driver_pm4_tests agc_driver_graphics_tests agc_driver_bda_device_tests agc_driver_mesh_tests agc_driver_cmask_fast_clear_tests agc_formatted_buffer_once_tests ${agcExecutionTests}) - if(agcTest STREQUAL "agc_driver_constant_store_tests" OR agcTest STREQUAL "agc_driver_flat_apertures_tests") + if(agcTest STREQUAL "agc_driver_constant_store_tests" OR agcTest STREQUAL "agc_driver_flat_apertures_tests" OR agcTest STREQUAL "agc_driver_pipeline_optimized_readback_tests") continue() endif() string(REGEX REPLACE "_tests$" "" agcTestName "${agcTest}") @@ -1067,6 +1067,22 @@ if(BUILD_TESTING) set_tests_properties(agc_shared_write_partial_collect agc_shared_write_partial_collect_two_pass PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + set_target_properties(agc_driver_pipeline_optimized_readback_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + foreach(pipelineReadbackCase IN ITEMS d32 d16 color32 color64) + foreach(pipelineReadbackMode IN ITEMS default fast) + set(pipelineReadbackTest agc_driver_pipeline_optimized_readback_${pipelineReadbackCase}) + set(pipelineReadbackEnvironment "ANYPS5_NO_SHADER_CACHE=1") + if(pipelineReadbackMode STREQUAL "fast") + string(APPEND pipelineReadbackTest "_fast") + list(APPEND pipelineReadbackEnvironment "APS5_NO_PIPELINE_LTO=1") + endif() + add_test(NAME ${pipelineReadbackTest} COMMAND agc_driver_pipeline_optimized_readback_tests ${pipelineReadbackCase}) + set_tests_properties(${pipelineReadbackTest} PROPERTIES ENVIRONMENT "${pipelineReadbackEnvironment}" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + set_tests_properties(${pipelineReadbackTest} PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() + endforeach() + endforeach() add_test(NAME agc_driver_f64_denorm_preserve_spirv COMMAND agc_driver_f64_fma_subnormals_tests --verify-spv-denorm-mode) set_tests_properties(agc_driver_f64_denorm_preserve_spirv PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") if(WIN32) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp index 43358277502..fe9ea51175a 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp @@ -302,6 +302,7 @@ class VulkanDevice { private: VulkanDevice(const PresentationWindow* window, bool compileOnly); + friend struct VulkanDeviceTestAccess; // The device's Graphics::Context: a copy of the one built at setup (its instance functions // resolved then, its function table filled then), or with APS5_NO_CONTEXT_CACHE=1 built anew. Graphics::Context graphicsContext() const; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PipelineOptimizedReadback.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PipelineOptimizedReadback.cpp new file mode 100644 index 00000000000..e4e66f07f3f --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PipelineOptimizedReadback.cpp @@ -0,0 +1,276 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} + +namespace { + +using namespace AgcDriver::Graphics; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 64; +constexpr std::uint32_t Height = 16; +constexpr std::byte Kept{0x40}; +alignas(256) std::array Pixels{}; +alignas(256) std::array DepthMemory{}; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; +alignas(256) constexpr std::array MaskedPixelCode{ + 0x7e040f00, 0x36060481, 0x7da40680, 0x7e0e02f2, 0xf800180f, 0x07070707, 0xbf810000, +}; +alignas(256) constexpr std::array FullPixelCode{ + 0x7e0e02f2, 0xf800180f, 0x07070707, 0xbf810000, +}; +constexpr std::array, 3> Triangle{{ + {-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f} +}}; +constexpr std::array, 12> DepthTriangles{{ + {-1.0f, -1.0f, 0.25f, 1.0f}, {0.0f, -1.0f, 0.25f, 1.0f}, {-1.0f, 1.0f, 0.25f, 1.0f}, + {-1.0f, 1.0f, 0.25f, 1.0f}, {0.0f, -1.0f, 0.25f, 1.0f}, {0.0f, 1.0f, 0.25f, 1.0f}, + {0.0f, -1.0f, 0.75f, 1.0f}, {1.0f, -1.0f, 0.75f, 1.0f}, {0.0f, 1.0f, 0.75f, 1.0f}, + {0.0f, 1.0f, 0.75f, 1.0f}, {1.0f, -1.0f, 0.75f, 1.0f}, {1.0f, 1.0f, 0.75f, 1.0f} +}}; + +struct PipelineTrace { + PFN_vkGetDeviceProcAddr resolve = nullptr; + PFN_vkCreateGraphicsPipelines create = nullptr; + PFN_vkCmdBindPipeline bind = nullptr; + PFN_vkCmdDraw draw = nullptr; + DeviceFunctions functions{}; + std::mutex mutex; + std::condition_variable released; + bool allowOptimization = false; + std::vector fast; + std::vector optimized; + VkCommandBuffer boundCommands = VK_NULL_HANDLE; + VkPipeline bound = VK_NULL_HANDLE; + std::vector drawn; + + void ReleaseOptimization() { + { + std::lock_guard lock(mutex); + allowOptimization = true; + } + released.notify_all(); + } +}; + +PipelineTrace* Trace = nullptr; + +VKAPI_ATTR VkResult VKAPI_CALL CreatePipelines(VkDevice device, VkPipelineCache cache, std::uint32_t count, const VkGraphicsPipelineCreateInfo* infos, const VkAllocationCallbacks* allocator, VkPipeline* pipelines) { + const bool optimizing = std::any_of(infos, infos + count, [](const auto& info) { return (info.flags & VK_PIPELINE_CREATE_LINK_TIME_OPTIMIZATION_BIT_EXT) != 0; }); + if (optimizing) { + std::unique_lock lock(Trace->mutex); + Trace->released.wait(lock, [] { return Trace->allowOptimization; }); + } + const auto result = Trace->create(device, cache, count, infos, allocator, pipelines); + std::lock_guard lock(Trace->mutex); + for (std::uint32_t index = 0; index < count; ++index) { + if (pipelines[index] == VK_NULL_HANDLE || (infos[index].flags & VK_PIPELINE_CREATE_LIBRARY_BIT_KHR) != 0) continue; + auto& handles = (infos[index].flags & VK_PIPELINE_CREATE_LINK_TIME_OPTIMIZATION_BIT_EXT) != 0 ? Trace->optimized : Trace->fast; + handles.push_back(pipelines[index]); + } + return result; +} + +VKAPI_ATTR void VKAPI_CALL BindPipeline(VkCommandBuffer commands, VkPipelineBindPoint point, VkPipeline pipeline) { + if (point == VK_PIPELINE_BIND_POINT_GRAPHICS) { + Trace->boundCommands = commands; + Trace->bound = pipeline; + } + Trace->bind(commands, point, pipeline); +} + +VKAPI_ATTR void VKAPI_CALL DrawCommands(VkCommandBuffer commands, std::uint32_t vertices, std::uint32_t instances, std::uint32_t firstVertex, std::uint32_t firstInstance) { + Trace->drawn.push_back(Trace->boundCommands == commands ? Trace->bound : VK_NULL_HANDLE); + Trace->draw(commands, vertices, instances, firstVertex, firstInstance); +} + +VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL Resolve(VkDevice device, const char* name) { + if (std::strcmp(name, "vkCreateGraphicsPipelines") == 0) return reinterpret_cast(CreatePipelines); + if (std::strcmp(name, "vkCmdBindPipeline") == 0) return reinterpret_cast(BindPipeline); + if (std::strcmp(name, "vkCmdDraw") == 0) return reinterpret_cast(DrawCommands); + return Trace->resolve(device, name); +} + +struct OptimizationDrain { + VkDevice device; + ~OptimizationDrain() { + Trace->ReleaseOptimization(); + WaitForOptimizedPipelines(device); + } +}; + +std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu}; +} + +void CheckPixels(bool masked, std::string_view phase) { + for (std::uint32_t pixel = 0; pixel < Width * Height; ++pixel) { + const bool drawn = masked ? (pixel % Width) % 2u == 0u : pixel % Width < Width / 2u; + const auto expected = drawn ? std::byte{255} : Kept; + for (std::uint32_t channel = 0; channel < 4; ++channel) { + const auto actual = Pixels[pixel * 4u + channel]; + Require(actual == expected, std::string(phase) + ": pixel " + std::to_string(pixel) + " channel " + std::to_string(channel) + " is " + std::to_string(static_cast(actual)) + ", expected " + std::to_string(static_cast(expected))); + } + } +} + +void CheckDrawn(PipelineTrace& trace, bool optimized) { + std::lock_guard lock(trace.mutex); + const auto& expected = optimized ? trace.optimized : trace.fast; + Require(trace.drawn.size() == 1, "pipeline readback did not observe exactly one real draw"); + Require(std::find(expected.begin(), expected.end(), trace.drawn.front()) != expected.end(), optimized ? "readback draw did not bind an optimized pipeline" : "readback draw did not bind a fast-linked pipeline"); +} + +void Run(AgcDriver::VulkanDevice& device, Context context, PipelineTrace& trace, std::uint32_t waveSize, VkFormat depthFormat, bool optimize) { + const bool masked = depthFormat == VK_FORMAT_UNDEFINED; + const auto target = device.Target(); + const auto vertices = masked ? std::span>(Triangle) : std::span>(DepthTriangles); + const auto vertexBuffer = BufferDescriptor(vertices.data(), 16u, static_cast(vertices.size())); + const std::vector vertexUserData(vertexBuffer.begin(), vertexBuffer.end()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {waveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + const auto pixelCode = masked ? std::span(MaskedPixelCode) : std::span(FullPixelCode); + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.wave32 = waveSize == 32u; + pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionX) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionY); + pixel.posX = true; + pixel.posY = true; + pixel.targetOutputMode[0] = 9; + pixel.targetExportMapping.fill(0xe4u); + const std::vector pixelUserData(8, 0u); + const std::array pixelMemory{{{reinterpret_cast(pixelCode.data()), std::as_bytes(pixelCode)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, + {waveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} + }}; + State state{}; + state.stages = {ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt}; + state.color = {reinterpret_cast(Pixels.data()), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, Pixels.size(), 0xe4u}; + state.colors = {state.color}; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {Width, Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend.colorWriteMask = 15; + state.blends = {state.blend}; + if (!masked) { + state.depth = DepthTarget{reinterpret_cast(DepthMemory.data()), 0, {Width, Height}, depthFormat, 0.5f, 0}; + state.depthTest = true; + state.depthCompare = VK_COMPARE_OP_LESS; + } + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(vertices.size()), 0, 1, 0, false}; + const auto render = [&](bool optimized, std::string_view phase) { + Pixels.fill(Kept); + trace.drawn.clear(); + trace.bound = VK_NULL_HANDLE; + trace.boundCommands = VK_NULL_HANDLE; + AgcDriver::Graphics::Draw(context, state, draw, shaders); + device.WaitIdle(); + CheckDrawn(trace, optimized); + CheckPixels(masked, phase); + }; + render(false, "fast link"); + trace.ReleaseOptimization(); + WaitForOptimizedPipelines(context.device); + Require(PipelineLibraryCountersOf(context.device).linked == 1, "the readback pipeline was not linked from graphics libraries"); + Require(trace.optimized.size() == (optimize ? 1u : 0u), "the optimized pipeline creation count differs from the requested mode"); + render(optimize, "completed link"); + std::printf("pipeline readback passed: fast draws %u, optimized draws %u, optimized creations %zu\n", optimize ? 1u : 2u, optimize ? 1u : 0u, trace.optimized.size()); +} + +} + +int main(int argc, char** argv) { + try { + std::string_view mode = "d32"; + bool force = false; + for (int index = 1; index < argc; ++index) { + const std::string_view argument = argv[index]; + if (argument == "--force-lto") force = true; + else { + Require(argument == "color32" || argument == "color64" || argument == "d32" || argument == "d16", "unknown pipeline readback case"); + mode = argument; + } + } + PipelineTrace trace; + Trace = &trace; + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(*device); + if (!context.graphicsPipelineLibrary) { + std::puts("skipped, graphics pipeline libraries are unavailable or disabled"); + return VulkanTestSkipped; + } + const bool masked = mode == "color32" || mode == "color64"; + if (masked && device->Target().subgroupSize < 32u) { + std::puts("skipped, the masked pixel shader requires at least 32 subgroup lanes"); + return VulkanTestSkipped; + } + if (force) context.pipelineLinkTimeOptimization = true; + const bool optimize = context.pipelineLinkTimeOptimization && std::getenv("APS5_NO_PIPELINE_LTO") == nullptr; + std::printf("pipeline readback: %s, %.*s, LTO %s%s\n", device->DeviceName().c_str(), static_cast(mode.size()), mode.data(), optimize ? "enabled" : "disabled", force ? " (explicit test override)" : " (production policy)"); + trace.resolve = context.deviceProc; + trace.create = context.Function("vkCreateGraphicsPipelines"); + trace.bind = context.Function("vkCmdBindPipeline"); + trace.draw = context.Function("vkCmdDraw"); + trace.functions = context.functions != nullptr ? *context.functions : DeviceFunctions{}; + trace.functions.cmdBindPipeline = BindPipeline; + trace.functions.cmdDraw = DrawCommands; + context.functions = &trace.functions; + context.deviceProc = Resolve; + OptimizationDrain drain{context.device}; + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + Run(*device, context, trace, mode == "color32" ? 32u : 64u, masked ? VK_FORMAT_UNDEFINED : (mode == "d16" ? VK_FORMAT_D16_UNORM : VK_FORMAT_D32_SFLOAT), optimize); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +}