diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 8ef01a306..583e96a21 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -164,6 +164,7 @@ set(agcDriverSources ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/ColorTargetLayout.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/ColorTargetTransfer.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/Pipeline.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/PipelineLibrary.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/PipelineCache.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/Resources.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/Recorder.cpp @@ -657,6 +658,14 @@ 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) + add_test(NAME agc_pipeline_library COMMAND agc_driver_recorder_tests --pipeline-library-only) + add_test(NAME agc_pipeline_library_off COMMAND agc_driver_recorder_tests --pipeline-library-only) + set_tests_properties(agc_pipeline_library agc_pipeline_library_off PROPERTIES SKIP_RETURN_CODE 77) + set_tests_properties(agc_pipeline_library_off PROPERTIES ENVIRONMENT "APS5_NO_GPL=1") + if(WIN32) + set_tests_properties(agc_pipeline_library agc_pipeline_library_off PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() set_tests_properties(agc_pipeline_cache PROPERTIES SKIP_RETURN_CODE 77) if(WIN32) set_tests_properties(agc_pipeline_cache PROPERTIES ENVIRONMENT_MODIFICATION @@ -722,6 +731,7 @@ add_test_executable(agc_driver_graphics_tests EXCLUDE_FROM_ALL Graphics/src/Draw.cpp Graphics/src/DepthSurface.cpp Graphics/src/Pipeline.cpp + Graphics/src/PipelineLibrary.cpp Graphics/src/State.cpp Graphics/src/ColorTargetLayout.cpp Graphics/src/ColorTargetTransfer.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 0ffe73788..75cab0ab9 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -222,6 +222,7 @@ struct VulkanDevice::State { bool bufferInt64Atomics = false; bool primitiveListRestart = false; bool depthClipControl = false; + bool graphicsPipelineLibrary = false; bool imageViewMinLod = false; bool pipelineExecutableInfo = false; bool maintenance8 = false; @@ -1031,6 +1032,20 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u } nonSeamlessCubeFeatures = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_NON_SEAMLESS_CUBE_MAP_FEATURES_EXT}; nonSeamlessCubeFeatures.nonSeamlessCubeMap = VK_TRUE; + VkPhysicalDeviceGraphicsPipelineLibraryFeaturesEXT libraryFeatures{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_GRAPHICS_PIPELINE_LIBRARY_FEATURES_EXT}; + VkPhysicalDeviceExtendedDynamicStateFeaturesEXT dynamicStateFeatures{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTENDED_DYNAMIC_STATE_FEATURES_EXT}; + if (std::getenv("APS5_NO_GPL") == nullptr && hasExtension(VK_KHR_PIPELINE_LIBRARY_EXTENSION_NAME) && hasExtension(VK_EXT_GRAPHICS_PIPELINE_LIBRARY_EXTENSION_NAME) && hasExtension(VK_EXT_EXTENDED_DYNAMIC_STATE_EXTENSION_NAME)) { + libraryFeatures.pNext = &dynamicStateFeatures; + VkPhysicalDeviceFeatures2 features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &libraryFeatures}; + state->InstanceFunction("vkGetPhysicalDeviceFeatures2")(selected, &features); + VkPhysicalDeviceGraphicsPipelineLibraryPropertiesEXT libraryProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_GRAPHICS_PIPELINE_LIBRARY_PROPERTIES_EXT}; + VkPhysicalDeviceProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &libraryProperties}; + state->InstanceFunction("vkGetPhysicalDeviceProperties2")(selected, &properties); + state->graphicsPipelineLibrary = libraryFeatures.graphicsPipelineLibrary == VK_TRUE && dynamicStateFeatures.extendedDynamicState == VK_TRUE && libraryProperties.graphicsPipelineLibraryFastLinking == VK_TRUE; + if (state->graphicsPipelineLibrary) deviceExtensions.insert(deviceExtensions.end(), {VK_KHR_PIPELINE_LIBRARY_EXTENSION_NAME, VK_EXT_GRAPHICS_PIPELINE_LIBRARY_EXTENSION_NAME, VK_EXT_EXTENDED_DYNAMIC_STATE_EXTENSION_NAME}); + } + libraryFeatures = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_GRAPHICS_PIPELINE_LIBRARY_FEATURES_EXT, &dynamicStateFeatures, VK_TRUE}; + dynamicStateFeatures = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTENDED_DYNAMIC_STATE_FEATURES_EXT, nullptr, VK_TRUE}; if (state->meshShader) { deviceExtensions.insert(deviceExtensions.end(), meshExtensions.begin(), meshExtensions.end()); state->capabilities.push_back(5283); @@ -1056,7 +1071,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u enabled.tessellationShader = available.tessellationShader; state->tessellationShader = enabled.tessellationShader == VK_TRUE; if (state->tessellationShader) state->capabilities.push_back(3); - APS5_LOG_OUT("Vulkan features tessellationAvailable=%u mesh=%u depthClip=%u depthRangeUnrestricted=%u", static_cast(state->tessellationShader), static_cast(state->meshShader), static_cast(state->depthClipControl), static_cast(state->depthRangeUnrestricted)); + APS5_LOG_OUT("Vulkan features tessellationAvailable=%u mesh=%u depthClip=%u depthRangeUnrestricted=%u pipelineLibrary=%u", static_cast(state->tessellationShader), static_cast(state->meshShader), static_cast(state->depthClipControl), static_cast(state->depthRangeUnrestricted), static_cast(state->graphicsPipelineLibrary)); require(available.samplerAnisotropy && available.textureCompressionBC, "device lacks sampler anisotropy or BC texture compression support required for texture sampling"); enabled.samplerAnisotropy = VK_TRUE; enabled.textureCompressionBC = VK_TRUE; @@ -1138,6 +1153,10 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u depthClipFeatures.pNext = const_cast(deviceInfo.pNext); deviceInfo.pNext = &depthClipFeatures; } + if (state->graphicsPipelineLibrary) { + dynamicStateFeatures.pNext = const_cast(deviceInfo.pNext); + deviceInfo.pNext = &libraryFeatures; + } if (state->primitiveListRestart) { listRestartFeatures.pNext = const_cast(deviceInfo.pNext); deviceInfo.pNext = &listRestartFeatures; @@ -2687,6 +2706,7 @@ Graphics::Context VulkanDevice::buildContext() const { context.nonSeamlessCubeMap = state->nonSeamlessCubeMap; context.conservativeRasterization = state->conservativeRasterization; context.provokingVertexLast = state->provokingVertexLast; + context.graphicsPipelineLibrary = state->graphicsPipelineLibrary; context.provokingVertexModePerPipeline = state->provokingVertexModePerPipeline; context.drawIndirectCount = state->drawIndirectCount; context.occlusionQueryPrecise = state->occlusionQueryPrecise; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp index c3c341ea6..2c731d714 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp @@ -56,6 +56,17 @@ struct DeviceFunctions { PFN_vkCmdSetScissor cmdSetScissor = nullptr; PFN_vkCmdSetDepthBounds cmdSetDepthBounds = nullptr; PFN_vkCmdSetDepthBias cmdSetDepthBias = nullptr; + PFN_vkCmdSetCullModeEXT cmdSetCullMode = nullptr; + PFN_vkCmdSetFrontFaceEXT cmdSetFrontFace = nullptr; + PFN_vkCmdSetDepthTestEnableEXT cmdSetDepthTestEnable = nullptr; + PFN_vkCmdSetDepthWriteEnableEXT cmdSetDepthWriteEnable = nullptr; + PFN_vkCmdSetDepthCompareOpEXT cmdSetDepthCompareOp = nullptr; + PFN_vkCmdSetDepthBoundsTestEnableEXT cmdSetDepthBoundsTestEnable = nullptr; + PFN_vkCmdSetStencilTestEnableEXT cmdSetStencilTestEnable = nullptr; + PFN_vkCmdSetStencilOpEXT cmdSetStencilOp = nullptr; + PFN_vkCmdSetStencilCompareMask cmdSetStencilCompareMask = nullptr; + PFN_vkCmdSetStencilWriteMask cmdSetStencilWriteMask = nullptr; + PFN_vkCmdSetStencilReference cmdSetStencilReference = nullptr; PFN_vkCmdBindVertexBuffers cmdBindVertexBuffers = nullptr; PFN_vkCmdBindIndexBuffer cmdBindIndexBuffer = nullptr; PFN_vkCmdDraw cmdDraw = nullptr; @@ -148,6 +159,7 @@ struct Context { bool primitiveListRestart = false; bool imageViewMinLod = false; bool pipelineExecutableInfo = false; + bool graphicsPipelineLibrary = false; std::uint32_t srgbDecodeFormats = 0; bool provokingVertexLast = false; bool provokingVertexModePerPipeline = false; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp index 4e41fa4e1..87ad53736 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp @@ -62,6 +62,7 @@ private: std::shared_ptr framebuffer; }; void release() noexcept; + void setLibraryState(VkCommandBuffer commands, const State& state) const; Context context; std::vector _modules; VkPipelineLayout layout = VK_NULL_HANDLE; @@ -72,6 +73,7 @@ private: std::size_t colorAttachments = 0; bool depthBounds = false; bool depthBias = false; + bool libraries = false; std::vector framebuffers; }; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp new file mode 100644 index 000000000..5452c34c9 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp @@ -0,0 +1,37 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_PIPELINELIBRARY_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_PIPELINELIBRARY_HPP + +#include "prx/libSceAgcDriver/Graphics/include/Context.hpp" +#include +#include +#include +#include +#include + +namespace AgcDriver::Graphics { + +struct PipelineLibraryKeys { + std::vector vertexInput; + std::vector preRasterization; + std::vector fragmentShader; + std::vector fragmentOutput; + std::vector renderPass; + std::vector layout; +}; + +std::span PipelineLibraryDynamicStates(); + +VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPipelineCreateInfo& info, const VkRenderPassCreateInfo& pass, const VkPipelineLayoutCreateInfo& layout, const PipelineLibraryKeys& keys); + +void ClearPipelineLibraries(VkDevice device); + +struct PipelineLibraryCounters { + std::array built{}; + std::uint64_t linked = 0; +}; + +PipelineLibraryCounters PipelineLibraryCountersOf(VkDevice device); + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index e6718791d..44b14f5b3 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -1,6 +1,7 @@ #include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" #include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp" +#include "prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp" #include "prx/libSceAgcDriver/Graphics/include/PipelineSpecialization.hpp" #include "prx/libSceAgcDriver/Graphics/include/VertexInput.hpp" #include @@ -108,6 +109,65 @@ void ValidateViewport(const Context& context, const VkViewport& viewport) { Require(std::min(viewport.y, viewport.y + viewport.height) >= context.limits.viewportBoundsRange[0] && std::max(viewport.y, viewport.y + viewport.height) <= context.limits.viewportBoundsRange[1], "viewport Y exceeds device bounds"); } +namespace { + +template +void appendKey(std::vector& key, const TValue& value) { + static_assert(std::is_trivially_copyable_v); + const auto bytes = std::as_bytes(std::span(&value, 1)); + key.insert(key.end(), bytes.begin(), bytes.end()); +} + +bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State& state, const VertexInputLayout& input, const ShaderResources& resources, std::span shaders, VkImageLayout attachmentLayout) { + using Stage = ShaderRecompiler::ShaderStage; + if (!context.graphicsPipelineLibrary || context.pipelineExecutableInfo || state.depthBiasPerFace || state.rectList) return false; + keys = {}; + for (const auto& shader : shaders) { + if (shader.stage == Stage::Geometry || shader.program->PipelineVariantId() == 0) return false; + auto& key = shader.stage == Stage::Fragment ? keys.fragmentShader : keys.preRasterization; + appendKey(key, shader.stage); + appendKey(key, shader.program->PipelineVariantId()); + appendKey(key, shader.pushConstantOffset); + } + for (const auto& binding : input.bindings) { + appendKey(keys.vertexInput, binding.binding); + appendKey(keys.vertexInput, binding.stride); + appendKey(keys.vertexInput, binding.inputRate); + } + appendKey(keys.vertexInput, input.attributes.size()); + for (const auto& attribute : input.attributes) { + appendKey(keys.vertexInput, attribute.location); + appendKey(keys.vertexInput, attribute.binding); + appendKey(keys.vertexInput, attribute.format); + appendKey(keys.vertexInput, attribute.offset); + } + appendKey(keys.vertexInput, state.topology); + appendKey(keys.vertexInput, state.primitiveRestart); + appendKey(keys.preRasterization, state.provokingVertexMode); + appendKey(keys.preRasterization, state.negativeOneToOne); + appendKey(keys.preRasterization, state.depthClamp && context.depthClamp); + appendKey(keys.preRasterization, state.conservativeRasterization); + appendKey(keys.preRasterization, state.depth.has_value() && state.depthBias); + appendKey(keys.preRasterization, state.stages.tessellation.has_value()); + if (state.stages.tessellation) appendKey(keys.preRasterization, state.stages.tessellation->inputControlPoints); + appendKey(keys.fragmentShader, state.depth.has_value()); + for (const auto& blend : state.blends) appendKey(keys.fragmentOutput, blend); + for (const auto value : state.blendConstants) appendKey(keys.fragmentOutput, value); + appendKey(keys.renderPass, attachmentLayout); + appendKey(keys.renderPass, state.blends.size()); + for (const auto& color : state.colors) { + appendKey(keys.renderPass, color.format); + appendKey(keys.renderPass, color.exportIndex); + } + appendKey(keys.renderPass, state.depth.has_value()); + if (state.depth) appendKey(keys.renderPass, state.depth->format); + appendKey(keys.layout, PushConstantStages(shaders)); + for (const auto word : resources.LayoutKey()) appendKey(keys.layout, word); + return true; +} + +} + Pipeline::Pipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const ShaderResources& resources, std::span shaders, VkImageLayout attachmentLayout) : context(context), _modules(shaders.size()), attachments(state.colors.size() + (state.depth ? 1u : 0u)), colorAttachments(state.colors.size()), depthBounds(state.depth.has_value() && state.depthBoundsTest), depthBias(state.depth.has_value() && state.depthBias) { PerformanceTimer timing("Vulkan.GraphicsPipeline"); // A cached pipeline may outlive its device's teardown (see ClearCachedPipelines); it must not keep @@ -221,9 +281,12 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput if (state.negativeOneToOne) viewports.pNext = &depthClip; viewports.viewportCount = 1; viewports.scissorCount = 1; + PipelineLibraryKeys keys; + libraries = libraryKeys(keys, context, state, vertexInput, resources, shaders, attachmentLayout); std::vector dynamicStates{VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}; if (depthBounds) dynamicStates.push_back(VK_DYNAMIC_STATE_DEPTH_BOUNDS); if (depthBias) dynamicStates.push_back(VK_DYNAMIC_STATE_DEPTH_BIAS); + if (libraries) dynamicStates.assign(PipelineLibraryDynamicStates().begin(), PipelineLibraryDynamicStates().end()); VkPipelineDynamicStateCreateInfo dynamic{VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO}; dynamic.dynamicStateCount = static_cast(dynamicStates.size()); dynamic.pDynamicStates = dynamicStates.data(); @@ -278,7 +341,8 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput pipelineInfo.layout = layout; pipelineInfo.renderPass = renderPass; timing.Mark("modules_and_state"); - Check(context.Function("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &pipeline), "vkCreateGraphicsPipelines"); + if (libraries) pipeline = LinkPipelineFromLibraries(context, pipelineInfo, passInfo, layoutInfo, keys); + else Check(context.Function("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &pipeline), "vkCreateGraphicsPipelines"); if (state.depthBiasPerFace) { raster.cullMode = VK_CULL_MODE_FRONT_BIT; Check(context.Function("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &backFaces), "vkCreateGraphicsPipelines"); @@ -382,11 +446,36 @@ void Pipeline::Continue(VkCommandBuffer commands, const State& state) const { context.Resolved(&DeviceFunctions::cmdBindPipeline, "vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); context.Resolved(&DeviceFunctions::cmdSetViewport, "vkCmdSetViewport")(commands, 0, 1, &state.viewport); context.Resolved(&DeviceFunctions::cmdSetScissor, "vkCmdSetScissor")(commands, 0, 1, &state.scissor); + if (libraries) { + setLibraryState(commands, state); + return; + } if (depthBias) context.Resolved(&DeviceFunctions::cmdSetDepthBias, "vkCmdSetDepthBias")(commands, state.depthBiasConstant, state.depthBiasClamp, state.depthBiasSlope); if (!depthBounds) return; context.Resolved(&DeviceFunctions::cmdSetDepthBounds, "vkCmdSetDepthBounds")(commands, state.minDepthBounds, state.maxDepthBounds); } +void Pipeline::setLibraryState(VkCommandBuffer commands, const State& state) const { + const bool depth = state.depth.has_value(); + context.Resolved(&DeviceFunctions::cmdSetCullMode, "vkCmdSetCullModeEXT")(commands, state.cullMode); + context.Resolved(&DeviceFunctions::cmdSetFrontFace, "vkCmdSetFrontFaceEXT")(commands, state.frontFace); + context.Resolved(&DeviceFunctions::cmdSetDepthBias, "vkCmdSetDepthBias")(commands, depthBias ? state.depthBiasConstant : 0.0f, depthBias ? state.depthBiasClamp : 0.0f, depthBias ? state.depthBiasSlope : 0.0f); + context.Resolved(&DeviceFunctions::cmdSetDepthBounds, "vkCmdSetDepthBounds")(commands, depthBounds ? state.minDepthBounds : 0.0f, depthBounds ? state.maxDepthBounds : 1.0f); + context.Resolved(&DeviceFunctions::cmdSetDepthTestEnable, "vkCmdSetDepthTestEnableEXT")(commands, depth && state.depthTest); + context.Resolved(&DeviceFunctions::cmdSetDepthWriteEnable, "vkCmdSetDepthWriteEnableEXT")(commands, depth && state.depthWrite); + context.Resolved(&DeviceFunctions::cmdSetDepthCompareOp, "vkCmdSetDepthCompareOpEXT")(commands, depth ? state.depthCompare : VK_COMPARE_OP_ALWAYS); + context.Resolved(&DeviceFunctions::cmdSetDepthBoundsTestEnable, "vkCmdSetDepthBoundsTestEnableEXT")(commands, depthBounds); + context.Resolved(&DeviceFunctions::cmdSetStencilTestEnable, "vkCmdSetStencilTestEnableEXT")(commands, depth && state.stencilTest); + const auto stencil = [&](VkStencilFaceFlags face, const VkStencilOpState& op) { + context.Resolved(&DeviceFunctions::cmdSetStencilOp, "vkCmdSetStencilOpEXT")(commands, face, op.failOp, op.passOp, op.depthFailOp, op.compareOp); + context.Resolved(&DeviceFunctions::cmdSetStencilCompareMask, "vkCmdSetStencilCompareMask")(commands, face, op.compareMask); + context.Resolved(&DeviceFunctions::cmdSetStencilWriteMask, "vkCmdSetStencilWriteMask")(commands, face, op.writeMask); + context.Resolved(&DeviceFunctions::cmdSetStencilReference, "vkCmdSetStencilReference")(commands, face, op.reference); + }; + stencil(VK_STENCIL_FACE_FRONT_BIT, depth ? state.stencilFront : VkStencilOpState{}); + stencil(VK_STENCIL_FACE_BACK_BIT, depth ? state.stencilBack : VkStencilOpState{}); +} + void Pipeline::ContinueBackFaces(VkCommandBuffer commands, const State& state) const { Require(backFaces != VK_NULL_HANDLE, "the pipeline draws both faces in one pass"); context.Resolved(&DeviceFunctions::cmdBindPipeline, "vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, backFaces); @@ -625,6 +714,7 @@ void ClearCachedPipelines(VkDevice device) { store.index.erase(it->hash); it = store.entries.erase(it); } + ClearPipelineLibraries(device); } } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp new file mode 100644 index 000000000..4a08571f3 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp @@ -0,0 +1,193 @@ +#include "prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp" +#include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" +#include +#include +#include +#include + +namespace AgcDriver::Graphics { + +namespace { + +constexpr std::array dynamicStates{ + VK_DYNAMIC_STATE_VIEWPORT, + VK_DYNAMIC_STATE_SCISSOR, + VK_DYNAMIC_STATE_DEPTH_BIAS, + VK_DYNAMIC_STATE_DEPTH_BOUNDS, + VK_DYNAMIC_STATE_STENCIL_COMPARE_MASK, + VK_DYNAMIC_STATE_STENCIL_WRITE_MASK, + VK_DYNAMIC_STATE_STENCIL_REFERENCE, + VK_DYNAMIC_STATE_CULL_MODE_EXT, + VK_DYNAMIC_STATE_FRONT_FACE_EXT, + VK_DYNAMIC_STATE_DEPTH_TEST_ENABLE_EXT, + VK_DYNAMIC_STATE_DEPTH_WRITE_ENABLE_EXT, + VK_DYNAMIC_STATE_DEPTH_COMPARE_OP_EXT, + VK_DYNAMIC_STATE_DEPTH_BOUNDS_TEST_ENABLE_EXT, + VK_DYNAMIC_STATE_STENCIL_TEST_ENABLE_EXT, + VK_DYNAMIC_STATE_STENCIL_OP_EXT, +}; + +using Key = std::vector; + +struct DeviceLibraries { + PFN_vkDestroyPipeline destroyPipeline = nullptr; + PFN_vkDestroyPipelineLayout destroyLayout = nullptr; + PFN_vkDestroyRenderPass destroyRenderPass = nullptr; + std::map renderPasses; + std::map layouts; + std::array, 4> parts; + PipelineLibraryCounters counters; +}; + +struct LibraryStore { + std::mutex mutex; + std::map devices; +}; + +LibraryStore& Libraries() { + static auto* store = new LibraryStore(); + return *store; +} + +Key Combined(const Key& part, const Key& renderPass, const Key& layout) { + Key key; + const auto append = [&](const Key& value) { + const auto size = value.size(); + const auto bytes = std::as_bytes(std::span(&size, 1)); + key.insert(key.end(), bytes.begin(), bytes.end()); + key.insert(key.end(), value.begin(), value.end()); + }; + append(part); + append(renderPass); + append(layout); + return key; +} + +} + +std::span PipelineLibraryDynamicStates() { + return dynamicStates; +} + +VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPipelineCreateInfo& info, const VkRenderPassCreateInfo& pass, const VkPipelineLayoutCreateInfo& layout, const PipelineLibraryKeys& keys) { + Require(info.pNext == nullptr && info.renderPass != VK_NULL_HANDLE && info.pDynamicState != nullptr, "pipeline library parts need the render pass path and the library dynamic states"); + auto& store = Libraries(); + std::lock_guard lock(store.mutex); + auto& device = store.devices[context.device]; + if (device.destroyPipeline == nullptr) { + device.destroyPipeline = context.Function("vkDestroyPipeline"); + device.destroyLayout = context.Function("vkDestroyPipelineLayout"); + device.destroyRenderPass = context.Function("vkDestroyRenderPass"); + } + auto renderPass = device.renderPasses.find(keys.renderPass); + if (renderPass == device.renderPasses.end()) { + VkRenderPass handle = VK_NULL_HANDLE; + Check(context.Function("vkCreateRenderPass")(context.device, &pass, nullptr, &handle), "vkCreateRenderPass library"); + renderPass = device.renderPasses.emplace(keys.renderPass, handle).first; + } + auto pipelineLayout = device.layouts.find(keys.layout); + if (pipelineLayout == device.layouts.end()) { + VkPipelineLayout handle = VK_NULL_HANDLE; + Check(context.Function("vkCreatePipelineLayout")(context.device, &layout, nullptr, &handle), "vkCreatePipelineLayout library"); + pipelineLayout = device.layouts.emplace(keys.layout, handle).first; + } + std::vector preRasterization; + const VkPipelineShaderStageCreateInfo* fragment = nullptr; + bool mesh = false; + for (std::uint32_t index = 0; index < info.stageCount; ++index) { + const auto& stage = info.pStages[index]; + if (stage.stage == VK_SHADER_STAGE_FRAGMENT_BIT) fragment = &stage; + else preRasterization.push_back(stage); + mesh |= stage.stage == VK_SHADER_STAGE_MESH_BIT_EXT; + } + Require(fragment != nullptr && !preRasterization.empty(), "a pipeline built from libraries needs a fragment and a pre-rasterization stage"); + constexpr std::array flags{ + VK_GRAPHICS_PIPELINE_LIBRARY_VERTEX_INPUT_INTERFACE_BIT_EXT, + VK_GRAPHICS_PIPELINE_LIBRARY_PRE_RASTERIZATION_SHADERS_BIT_EXT, + VK_GRAPHICS_PIPELINE_LIBRARY_FRAGMENT_SHADER_BIT_EXT, + VK_GRAPHICS_PIPELINE_LIBRARY_FRAGMENT_OUTPUT_INTERFACE_BIT_EXT, + }; + const std::array partKeys{&keys.vertexInput, &keys.preRasterization, &keys.fragmentShader, &keys.fragmentOutput}; + std::vector libraries; + for (std::size_t part = 0; part < flags.size(); ++part) { + if (part == 0 && mesh) continue; + const auto key = Combined(*partKeys[part], keys.renderPass, keys.layout); + auto& cache = device.parts[part]; + if (const auto found = cache.find(key); found != cache.end()) { + libraries.push_back(found->second); + continue; + } + PerformanceTimer timing("Vulkan.GraphicsPipelineLibrary"); + VkGraphicsPipelineLibraryCreateInfoEXT library{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_LIBRARY_CREATE_INFO_EXT}; + library.flags = flags[part]; + VkGraphicsPipelineCreateInfo create{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO, &library}; + create.flags = VK_PIPELINE_CREATE_LIBRARY_BIT_KHR; + create.pDynamicState = info.pDynamicState; + create.layout = pipelineLayout->second; + create.renderPass = renderPass->second; + create.subpass = info.subpass; + switch (flags[part]) { + case VK_GRAPHICS_PIPELINE_LIBRARY_VERTEX_INPUT_INTERFACE_BIT_EXT: + create.pVertexInputState = info.pVertexInputState; + create.pInputAssemblyState = info.pInputAssemblyState; + create.layout = VK_NULL_HANDLE; + create.renderPass = VK_NULL_HANDLE; + break; + case VK_GRAPHICS_PIPELINE_LIBRARY_PRE_RASTERIZATION_SHADERS_BIT_EXT: + create.stageCount = static_cast(preRasterization.size()); + create.pStages = preRasterization.data(); + create.pViewportState = info.pViewportState; + create.pRasterizationState = info.pRasterizationState; + create.pTessellationState = info.pTessellationState; + break; + case VK_GRAPHICS_PIPELINE_LIBRARY_FRAGMENT_SHADER_BIT_EXT: + create.stageCount = 1; + create.pStages = fragment; + create.pMultisampleState = info.pMultisampleState; + create.pDepthStencilState = info.pDepthStencilState; + break; + default: + create.pMultisampleState = info.pMultisampleState; + create.pColorBlendState = info.pColorBlendState; + break; + } + VkPipeline handle = VK_NULL_HANDLE; + Check(context.Function("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &create, nullptr, &handle), "vkCreateGraphicsPipelines library"); + cache.emplace(key, handle); + ++device.counters.built[part]; + libraries.push_back(handle); + } + PerformanceTimer timing("Vulkan.GraphicsPipelineLink"); + VkPipelineLibraryCreateInfoKHR linked{VK_STRUCTURE_TYPE_PIPELINE_LIBRARY_CREATE_INFO_KHR}; + linked.libraryCount = static_cast(libraries.size()); + linked.pLibraries = libraries.data(); + VkGraphicsPipelineCreateInfo create{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO, &linked}; + create.layout = info.layout; + VkPipeline pipeline = VK_NULL_HANDLE; + Check(context.Function("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &create, nullptr, &pipeline), "vkCreateGraphicsPipelines link"); + ++device.counters.linked; + return pipeline; +} + +void ClearPipelineLibraries(VkDevice device) { + auto& store = Libraries(); + std::lock_guard lock(store.mutex); + const auto found = store.devices.find(device); + if (found == store.devices.end()) return; + auto& libraries = found->second; + for (auto& part : libraries.parts) { + for (const auto& [key, handle] : part) libraries.destroyPipeline(device, handle, nullptr); + } + for (const auto& [key, handle] : libraries.layouts) libraries.destroyLayout(device, handle, nullptr); + for (const auto& [key, handle] : libraries.renderPasses) libraries.destroyRenderPass(device, handle, nullptr); + store.devices.erase(found); +} + +PipelineLibraryCounters PipelineLibraryCountersOf(VkDevice device) { + auto& store = Libraries(); + std::lock_guard lock(store.mutex); + const auto found = store.devices.find(device); + return found == store.devices.end() ? PipelineLibraryCounters{} : found->second.counters; +} + +} diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp index 336cbc301..a21d2952b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp @@ -166,6 +166,19 @@ void FillDeviceFunctions(const Context& context, DeviceFunctions& functions) { functions.cmdSetScissor = context.Function("vkCmdSetScissor"); functions.cmdSetDepthBounds = context.Function("vkCmdSetDepthBounds"); functions.cmdSetDepthBias = context.Function("vkCmdSetDepthBias"); + if (context.graphicsPipelineLibrary) { + functions.cmdSetCullMode = context.Function("vkCmdSetCullModeEXT"); + functions.cmdSetFrontFace = context.Function("vkCmdSetFrontFaceEXT"); + functions.cmdSetDepthTestEnable = context.Function("vkCmdSetDepthTestEnableEXT"); + functions.cmdSetDepthWriteEnable = context.Function("vkCmdSetDepthWriteEnableEXT"); + functions.cmdSetDepthCompareOp = context.Function("vkCmdSetDepthCompareOpEXT"); + functions.cmdSetDepthBoundsTestEnable = context.Function("vkCmdSetDepthBoundsTestEnableEXT"); + functions.cmdSetStencilTestEnable = context.Function("vkCmdSetStencilTestEnableEXT"); + functions.cmdSetStencilOp = context.Function("vkCmdSetStencilOpEXT"); + functions.cmdSetStencilCompareMask = context.Function("vkCmdSetStencilCompareMask"); + functions.cmdSetStencilWriteMask = context.Function("vkCmdSetStencilWriteMask"); + functions.cmdSetStencilReference = context.Function("vkCmdSetStencilReference"); + } functions.cmdBindVertexBuffers = context.Function("vkCmdBindVertexBuffers"); functions.cmdBindIndexBuffer = context.Function("vkCmdBindIndexBuffer"); functions.cmdDraw = context.Function("vkCmdDraw"); diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index f9c681cce..df3a3bfbc 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -2,6 +2,7 @@ #include "Triangle_vert_spv.h" #include "prx/libSceAgcDriver/Graphics/include/VertexInput.hpp" #include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp" +#include "prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" #include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" @@ -194,6 +195,22 @@ public: minLod.pNext = address.pNext; address.pNext = &minLod; } + VkPhysicalDeviceExtendedDynamicStateFeaturesEXT dynamicState{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTENDED_DYNAMIC_STATE_FEATURES_EXT}; + VkPhysicalDeviceGraphicsPipelineLibraryFeaturesEXT library{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_GRAPHICS_PIPELINE_LIBRARY_FEATURES_EXT, &dynamicState}; + if (std::getenv("APS5_NO_GPL") == nullptr && hasExtension(VK_KHR_PIPELINE_LIBRARY_EXTENSION_NAME) && hasExtension(VK_EXT_GRAPHICS_PIPELINE_LIBRARY_EXTENSION_NAME) && hasExtension(VK_EXT_EXTENDED_DYNAMIC_STATE_EXTENSION_NAME)) { + VkPhysicalDeviceFeatures2 features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &library}; + function("vkGetPhysicalDeviceFeatures2")(context.physical, &features); + VkPhysicalDeviceGraphicsPipelineLibraryPropertiesEXT libraryProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_GRAPHICS_PIPELINE_LIBRARY_PROPERTIES_EXT}; + VkPhysicalDeviceProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &libraryProperties}; + function("vkGetPhysicalDeviceProperties2")(context.physical, &properties); + context.graphicsPipelineLibrary = library.graphicsPipelineLibrary == VK_TRUE && dynamicState.extendedDynamicState == VK_TRUE && libraryProperties.graphicsPipelineLibraryFastLinking == VK_TRUE; + } + library = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_GRAPHICS_PIPELINE_LIBRARY_FEATURES_EXT, &dynamicState, VK_TRUE}; + dynamicState = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTENDED_DYNAMIC_STATE_FEATURES_EXT, address.pNext, VK_TRUE}; + if (context.graphicsPipelineLibrary) { + extensionsEnabled.insert(extensionsEnabled.end(), {VK_KHR_PIPELINE_LIBRARY_EXTENSION_NAME, VK_EXT_GRAPHICS_PIPELINE_LIBRARY_EXTENSION_NAME, VK_EXT_EXTENDED_DYNAMIC_STATE_EXTENSION_NAME}); + address.pNext = &library; + } VkDeviceCreateInfo device{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO, &address}; device.queueCreateInfoCount = 1; device.pQueueCreateInfos = &queue; @@ -3749,6 +3766,60 @@ void denormalClearTests(const Device& device, Recorder& recorder) { recorder.Sync(); } +void pipelineLibraryTests(const Device& device) { + std::lock_guard gpu(GpuMutex()); + const auto& context = device.GetContext(); + struct Cleanup { + VkDevice device; + ~Cleanup() { ClearCachedPipelines(device); } + } cleanup{context.device}; + ClearCachedPipelines(context.device); + ShaderRecompiler::RecompileResult vertex, fragment; + vertex.variantId = 11; + fragment.variantId = 12; + 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, static_cast(state.colors.size()), 0, 0); + VertexInputLayout input; + const auto lookup = [&] { return CachedPipeline(context, state, input, resources, shaders, VK_IMAGE_LAYOUT_GENERAL); }; + const auto counters = [&] { return PipelineLibraryCountersOf(context.device); }; + const auto first = lookup(); + if (!context.graphicsPipelineLibrary) { + Require(counters().linked == 0, "a pipeline was linked from libraries without VK_EXT_graphics_pipeline_library"); + std::cout << "graphics pipeline libraries unavailable or disabled: pipelines are created whole\n"; + return; + } + const auto built = counters(); + Require(built.linked == 1 && built.built == std::array{1, 1, 1, 1}, "the first pipeline was not linked from four new libraries"); + state.blends.front().blendEnable = VK_TRUE; + state.blends.front().srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA; + state.blends.front().dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; + const auto blended = lookup(); + Require(blended != first && counters().built == std::array{1, 1, 1, 2}, "a blend change rebuilt more than the fragment output library"); + state.cullMode = VK_CULL_MODE_BACK_BIT; + state.frontFace = VK_FRONT_FACE_CLOCKWISE; + const auto culled = lookup(); + Require(culled != blended && counters().built == std::array{1, 1, 1, 2}, "a cull mode change rebuilt a library instead of setting dynamic state"); + input.bindings.push_back({0, 16, VK_VERTEX_INPUT_RATE_VERTEX}); + input.attributes.push_back({0, 0, VK_FORMAT_R32G32B32A32_SFLOAT, 0}); + static_cast(lookup()); + Require(counters().built == std::array{2, 1, 1, 2} && counters().linked == 4, "a vertex input change rebuilt more than the vertex input library"); + fragment.variantId = 13; + static_cast(lookup()); + Require(counters().built == std::array{2, 1, 2, 2}, "a new pixel shader rebuilt more than the fragment shader library"); + ClearCachedPipelines(context.device); + Require(counters().linked == 0, "clearing the pipelines kept the device's libraries"); + std::cout << "Pipeline library reuse tests passed\n"; +} + int main(int argc, char** argv) { try { std::unique_ptr created; @@ -3760,6 +3831,10 @@ int main(int argc, char** argv) { return 77; } Device& device = *created; + if (argc == 2 && std::string_view(argv[1]) == "--pipeline-library-only") { + pipelineLibraryTests(device); + return 0; + } 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; diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index e332ca105..292e34f67 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -70,6 +70,7 @@ flowchart LR - A compute shader whose LDS plus its lock dword exceeds the host's `maxComputeSharedMemorySize` is not given a `Workgroup` array. `Recompile` sets `ShaderInfo::sharedMemoryBytes` for it and the SPIR-V backend addresses the LDS in a coherent storage buffer at descriptor set 1 (`WorkgroupMemoryDescriptorSet`), with a slice of `RecompileResult::workgroupMemoryDwords` per workgroup, indexed by `WorkgroupID` and `NumWorkGroups`. `VulkanDevice` keeps one such buffer, sized from the dispatch dimensions, and binds it after the resource set; programs that fit keep using shared memory. - `Driver::RegisterShader` splits shader preparation in two. `PlanRegistered` runs on the registering guest thread: it validates the registers and entry point, decodes the code and builds the recompile request, so invalid registrations still fail there. The RDNA to IR translation and resource plan (`PrepareShader`) run on a background pool of `hardware_concurrency() / 2` detached threads (at least 2, 64 MiB stacks). - The snapshot is published as pending. `SourceHandleFor`, `InvocationFor` and `ShaderPreparationTransaction::Edit` / `Read` wait for the pending job before reading `PreparedShaders`, and rethrow its failure. `APS5_SYNC_SHADER_PREPARE=1` prepares on the registering thread instead. +- A draw's `Graphics::Pipeline` comes from `CachedPipeline`. When the device has `VK_EXT_graphics_pipeline_library` with fast linking and `VK_EXT_extended_dynamic_state`, the pipeline is linked from four libraries (vertex input, pre-rasterization shaders, fragment shader, fragment output) kept per device in [`PipelineLibrary.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp). A library built for one pipeline is reused by the next: another blend or vertex layout builds only that part, and cull mode, front face, depth and stencil state are dynamic, so changing them only links. Pipelines with per-face depth bias, rect lists, geometry shaders or a stage without a variant id, and devices without the extensions (or `APS5_NO_GPL=1`), are created whole. ### Shader MODE register access diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 9d42e11b0..b81484f3a 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -272,6 +272,7 @@ Throughout the project, every function at every stage either **does exactly what - On Linux, [direct memory](../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) lives in one sparse memfd at file offset = physical address, so the [GPU imports](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) can share one udmabuf buffer object per 64 MiB of it. Released direct memory is not punched out of the file, because a live import of its chunk pins the pages and the GPU would keep reading the old ones: its pages stay committed (the process keeps the high-water mark of the direct memory it used) and are zeroed when the range is allocated again. - [Per-face depth bias](../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver): Vulkan has one depth bias per draw, so a draw that rasterizes both faces with different front and back bias (PA_SU_SC_MODE_CNTL, PA_SU_POLY_OFFSET_FRONT/BACK_*) is recorded twice, front faces with the back faces culled and then back faces with the front faces culled. The vertex stages run twice, so a vertex shader that writes memory writes twice, and all front faces land before all back faces instead of in primitive order, which differs only for blending or stencil updates that depend on the order. - [Compute LDS beyond the device's shared-memory limit](../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) (libSceAgcDriver) lives in a coherent storage buffer with one slice of `LDS bytes / 4 + 1` dwords per workgroup (the last dword is the atomic lock), shared by all such dispatches and grown in 1 MiB steps up to `maxStorageBufferRange`; a dispatch whose slices do not fit throws. It is slower than shared memory and its cost was not measured. An indirect dispatch of such a program always reads its group counts on the CPU. Its results are pinned against the in-limit semantics in `agc_driver_lds_beyond_device_limit`, not measured on hardware. +- [Graphics pipeline libraries](../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp) (libSceAgcDriver) are fast-linked only, never relinked with `VK_PIPELINE_CREATE_LINK_TIME_OPTIMIZATION_BIT_EXT`. A vertex stage then keeps the outputs its fragment stage does not read: on an RTX 4090 (NVIDIA 615.71) 13 of 15 PPSA36082 vertex shaders paired with a pixel shader that reads none of them used up to 68 registers instead of 38 and a binary up to 21 % larger, while pairs that read every output compiled to the same code as a whole pipeline (#2242). The libraries are created against a render pass of the draw's attachment formats, so a shader drawn to targets of other formats builds its libraries again - [sceAvPlayerGetVideoData](../../core/libs/prx/libSceAvPlayer/src/Source.cpp) (libSceAvPlayer) in sync mode `SCE_AVPLAYER_AV_SYNC_MODE_NONE` skips queued frames that the audio clock has already passed while the title pulls audio, so a title that requests video slower than the film's frame rate keeps the picture with the sound. Whether the console hands out every frame in this mode is unknown. PPSA12544 requests 7-19 frames per second for a 30 fps cinematic - [libSceHttp2 asynchronous requests](../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2) never reach the network: `sceHttp2SendRequestAsync` accepts the request, queues a completion whose result is the library's network error and triggers the user event named by the kqueue option (equeue at offset 0, event id at 8, user data at 16, layout inferred from PPSA17221); `sceHttp2WaitAsync` returns that completion. The completion's `event_type` value is unknown and left 0. PPSA17221 completes its request object only after `sceHttp2SendRequestAsync` returns, and its handler for a synchronous failure calls `shared_from_this` before that, so a send must not fail synchronously. A kqueue option or user event that is missing, an extra option, and a wait with no completed operation throw - [system](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) runs no command. It follows FreeBSD 11's `lib/libc/stdlib/system.c` for a sandbox without `/bin/sh`: a null command returns 1 unconditionally, as that version does, and any command returns the wait status of a child that could not exec the shell and called `_exit(127)` (`0x7f00`). Whether the console's application sandbox has no shell, and whether `vfork` succeeds there (FreeBSD returns -1 when it fails), is unverified