diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index f2d9e6690ff5..476d738b21ed 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -332,7 +332,6 @@ set(agcDriverSources ${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/PipelineBinaryStore.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/PipelineJournal.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/RuntimeSwitches.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/Resources.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 05a35f151ec7..384bd0c139ae 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -51,8 +51,6 @@ #include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureCache.hpp" #include "prx/libSceAgcDriver/Graphics/include/PipelineCache.hpp" -#include "prx/libSceAgcDriver/Graphics/include/PipelineBinaryStore.hpp" -#include "ShaderCacheDirectory.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" #include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" @@ -200,7 +198,7 @@ bool CoreVulkanCapability(std::uint32_t capability) { } } -void CreateComputePipeline(VkDevice device, PFN_vkCreateComputePipelines create, VkPipelineCache cache, VkShaderModule module, VkPipelineLayout layout, VkPipelineCreateFlags flags, VkPipelineShaderStageCreateFlags stageFlags, const VkSpecializationInfo* specialization, std::uint32_t subgroupSize, VkPipeline* pipeline, Graphics::PipelineCacheSynchronization* synchronization = nullptr, const Graphics::PipelineBinaryStore* binaries = nullptr, std::span spirv = {}, std::span layoutKey = {}) { +void CreateComputePipeline(VkDevice device, PFN_vkCreateComputePipelines create, VkPipelineCache cache, VkShaderModule module, VkPipelineLayout layout, VkPipelineCreateFlags flags, VkPipelineShaderStageCreateFlags stageFlags, const VkSpecializationInfo* specialization, std::uint32_t subgroupSize, VkPipeline* pipeline, Graphics::PipelineCacheSynchronization* synchronization = nullptr) { VkComputePipelineCreateInfo info{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; info.flags = flags; info.stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; @@ -213,11 +211,8 @@ void CreateComputePipeline(VkDevice device, PFN_vkCreateComputePipelines create, subgroup.requiredSubgroupSize = subgroupSize; if (subgroupSize != 0) info.stage.pNext = &subgroup; info.layout = layout; - if (binaries != nullptr) binaries->CreateComputePipeline(info, spirv, layoutKey, pipeline); - else { - auto cacheLock = synchronization != nullptr ? synchronization->LockForPipelineUse() : Graphics::PipelineCacheSynchronization::PipelineUseLock{}; - check(create(device, cache, 1, &info, nullptr, pipeline), "vkCreateComputePipelines"); - } + auto cacheLock = synchronization != nullptr ? synchronization->LockForPipelineUse() : Graphics::PipelineCacheSynchronization::PipelineUseLock{}; + check(create(device, cache, 1, &info, nullptr, pipeline), "vkCreateComputePipelines"); PerformanceCounters::CountPipeline(); } @@ -335,7 +330,6 @@ struct ComputePipelineObjects { VkPipelineLayout layout = VK_NULL_HANDLE; VkPipeline pipeline = VK_NULL_HANDLE; VkDescriptorSetLayout resourcesLayout = VK_NULL_HANDLE; - std::vector binaryLayoutKey; std::mutex splitMutex; std::map, VkPipeline> splitPipelines; ~ComputePipelineObjects() { @@ -469,7 +463,6 @@ struct VulkanDevice::State { bool pipelineExecutableInfo = false; bool maintenance8 = false; bool maintenance5 = false; - bool pipelineBinaries = false; bool fillModeNonSolid = false; std::uint32_t srgbDecodeFormats = 0; std::uint32_t srgbCopyFormats = 0; @@ -555,7 +548,6 @@ struct VulkanDevice::State { std::unique_ptr placeholderImages; std::unique_ptr textureCache; std::unique_ptr pipelineCache; - std::unique_ptr pipelineBinaryStore; std::unique_ptr descriptorCache; std::unique_ptr vertexInputs; std::unique_ptr samplerCache; @@ -981,7 +973,6 @@ struct VulkanDevice::State { scaler.reset(); hdrScaler.reset(); pipelineCache.reset(); - pipelineBinaryStore.reset(); context.bufferPool.reset(); bufferPool.reset(); const auto destroyFence = reinterpret_cast(deviceProc(device, "vkDestroyFence")); @@ -1753,27 +1744,14 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : } } VkPhysicalDeviceMaintenance5FeaturesKHR maintenance5Features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MAINTENANCE_5_FEATURES_KHR}; - const bool wantPipelineBinaries = hasExtension(VK_KHR_PIPELINE_BINARY_EXTENSION_NAME) && std::getenv("APS5_NO_PIPELINE_BINARIES") == nullptr && !ShaderRecompiler::ShaderCacheDirectory().empty() && - Graphics::PipelineBinaryStore::LayersCompatible(selected, state->InstanceFunction("vkEnumerateDeviceLayerProperties")); - constexpr std::array maintenanceExtensions{VK_KHR_MAINTENANCE_5_EXTENSION_NAME, VK_KHR_DYNAMIC_RENDERING_EXTENSION_NAME, VK_KHR_DEPTH_STENCIL_RESOLVE_EXTENSION_NAME, VK_KHR_CREATE_RENDERPASS_2_EXTENSION_NAME}; - if ((state->graphicsPipelineLibrary || wantPipelineBinaries) && std::all_of(maintenanceExtensions.begin(), maintenanceExtensions.end(), hasExtension)) { + if (state->graphicsPipelineLibrary && hasExtension(VK_KHR_MAINTENANCE_5_EXTENSION_NAME)) { VkPhysicalDeviceFeatures2 features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &maintenance5Features}; state->InstanceFunction("vkGetPhysicalDeviceFeatures2")(selected, &features); state->maintenance5 = maintenance5Features.maintenance5 == VK_TRUE; - if (state->maintenance5) { - deviceExtensions.push_back(VK_KHR_MAINTENANCE_5_EXTENSION_NAME); - if (!libraryDevice) deviceExtensions.insert(deviceExtensions.end(), maintenanceExtensions.begin() + 1, maintenanceExtensions.end()); - } + if (state->maintenance5) deviceExtensions.push_back(VK_KHR_MAINTENANCE_5_EXTENSION_NAME); } maintenance5Features = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MAINTENANCE_5_FEATURES_KHR}; maintenance5Features.maintenance5 = VK_TRUE; - VkPhysicalDevicePipelineBinaryFeaturesKHR binaryFeatures{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PIPELINE_BINARY_FEATURES_KHR}; - if (state->maintenance5 && wantPipelineBinaries) { - VkPhysicalDeviceFeatures2 features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &binaryFeatures}; - state->InstanceFunction("vkGetPhysicalDeviceFeatures2")(selected, &features); - state->pipelineBinaries = binaryFeatures.pipelineBinaries == VK_TRUE; - if (state->pipelineBinaries) deviceExtensions.push_back(VK_KHR_PIPELINE_BINARY_EXTENSION_NAME); - } renderingFeatures = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DYNAMIC_RENDERING_FEATURES_KHR, nullptr, VK_TRUE}; 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, &renderingFeatures, VK_TRUE}; @@ -1998,10 +1976,6 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : maintenance5Features.pNext = const_cast(deviceInfo.pNext); deviceInfo.pNext = &maintenance5Features; } - if (state->pipelineBinaries) { - binaryFeatures.pNext = const_cast(deviceInfo.pNext); - deviceInfo.pNext = &binaryFeatures; - } if (state->maintenance8) { maintenance8Features.pNext = const_cast(deviceInfo.pNext); deviceInfo.pNext = &maintenance8Features; @@ -2265,7 +2239,6 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : state->bufferPool = std::make_shared(graphicsContext()); state->emptyBuffer = std::make_unique(graphicsContext(), Graphics::EmptyBufferBytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); state->pipelineCache = std::make_unique(graphicsContext(), state->properties, window != nullptr || compileOnly); - if (state->pipelineBinaries) state->pipelineBinaryStore = std::make_unique(graphicsContext(), state->properties); state->detiler = std::make_unique(graphicsContext()); state->textureCache = std::make_unique(graphicsContext()); state->colorTransfer = std::make_unique(graphicsContext()); @@ -5483,8 +5456,6 @@ std::shared_ptr VulkanDevice::createComputePipeline(cons objects->destroyPipeline = state->DeviceFunction("vkDestroyPipeline"); objects->module = Graphics::CreateShaderModule(state->DeviceFunction("vkCreateShaderModule"), state->device, state->graphicsPipelineLibrary, shader.spirv.data(), shader.spirv.size() * sizeof(std::uint32_t), "vkCreateShaderModule"); objects->resourcesLayout = setLayout; - objects->binaryLayoutKey.assign(layoutKey.begin(), layoutKey.end()); - objects->binaryLayoutKey.insert(objects->binaryLayoutKey.end(), {shader.workgroupMemoryDwords != 0 ? 1u : 0u, pushStages, pushStages != 0 ? Graphics::ComputePushConstantBytes : 0u, state->graphicsPipelineLibrary ? 1u : 0u}); const std::array setLayouts{setLayout, shader.workgroupMemoryDwords != 0 ? state->workgroupMemoryLayout : VK_NULL_HANDLE}; const VkPushConstantRange push{VK_SHADER_STAGE_COMPUTE_BIT, 0, Graphics::ComputePushConstantBytes}; VkPipelineLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; @@ -5524,7 +5495,7 @@ std::shared_ptr VulkanDevice::createComputePipeline(cons if (!path.empty()) std::fprintf(stderr, "[compute-capture] %s\n", path.string().c_str()); } if (profile && shader.spirv.size() > 100000) std::fprintf(stderr, "[dispatch] creating a pipeline for %zu SPIR-V words (program 0x%llx)\n", shader.spirv.size(), static_cast(programAddress)); - CreateComputePipeline(state->device, state->DeviceFunction("vkCreateComputePipelines"), context.pipelineCache, objects->module, objects->layout, pipelineFlags, stageFlags, specialization.Info(), subgroupSize, &objects->pipeline, context.pipelineCacheSynchronization, state->probingFloatControls ? nullptr : state->pipelineBinaryStore.get(), std::span(shader.spirv.data(), shader.spirv.size()), objects->binaryLayoutKey); + CreateComputePipeline(state->device, state->DeviceFunction("vkCreateComputePipelines"), context.pipelineCache, objects->module, objects->layout, pipelineFlags, stageFlags, specialization.Info(), subgroupSize, &objects->pipeline, context.pipelineCacheSynchronization); objects->destroyModule(objects->device, objects->module, nullptr); objects->module = VK_NULL_HANDLE; if (!state->probingFloatControls) { @@ -5546,7 +5517,7 @@ VkPipeline VulkanDevice::splitComputePipeline(const Graphics::Context& context, VkShaderModule module = Graphics::CreateShaderModule(state->DeviceFunction("vkCreateShaderModule"), state->device, state->graphicsPipelineLibrary, shader.spirv.data(), shader.spirv.size() * sizeof(std::uint32_t), "vkCreateShaderModule"); VkPipeline pipeline = VK_NULL_HANDLE; try { - CreateComputePipeline(state->device, state->DeviceFunction("vkCreateComputePipelines"), context.pipelineCache, module, objects.layout, computePipelineFlags(), computeStageFlags(shader), specialization.Info(), computeSubgroupSize(shader), &pipeline, context.pipelineCacheSynchronization, state->pipelineBinaryStore.get(), std::span(shader.spirv.data(), shader.spirv.size()), objects.binaryLayoutKey); + CreateComputePipeline(state->device, state->DeviceFunction("vkCreateComputePipelines"), context.pipelineCache, module, objects.layout, computePipelineFlags(), computeStageFlags(shader), specialization.Info(), computeSubgroupSize(shader), &pipeline, context.pipelineCacheSynchronization); } catch (...) { objects.destroyModule(objects.device, module, nullptr); throw; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineBinaryStore.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineBinaryStore.hpp deleted file mode 100644 index fea02cb217ce..000000000000 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineBinaryStore.hpp +++ /dev/null @@ -1,40 +0,0 @@ -#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_PIPELINEBINARYSTORE_HPP -#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_PIPELINEBINARYSTORE_HPP - -#include "prx/libSceAgcDriver/Graphics/include/Context.hpp" -#include -#include - -namespace AgcDriver::Graphics { - -class PipelineBinaryStore { -public: - static bool LayersCompatible(VkPhysicalDevice physical, PFN_vkEnumerateDeviceLayerProperties enumerate); - PipelineBinaryStore(const Context& context, const VkPhysicalDeviceProperties& properties); - std::vector Key(const VkComputePipelineCreateInfo& info, std::span spirv, std::span layout) const; - void CreateComputePipeline(const VkComputePipelineCreateInfo& info, std::span spirv, std::span layout, VkPipeline* pipeline) const; - -private: - enum class RestoreResult { Missing, Restored, BelowThreshold, CaptureFailed }; - RestoreResult restore(const VkComputePipelineCreateInfo& info, std::span key, VkPipeline* pipeline) const; - RestoreResult restoreFile(const VkComputePipelineCreateInfo& info, std::span key, VkPipeline* pipeline, const std::filesystem::path& path) const; - bool capture(VkPipeline pipeline, std::span key) const; - void rememberThreshold(std::span key, bool captureFailed = false) const; - std::filesystem::path filePath(std::span key) const; - - Context context; - VkPhysicalDeviceProperties properties{}; - VkPipelineBinaryKeyKHR globalKey{VK_STRUCTURE_TYPE_PIPELINE_BINARY_KEY_KHR}; - std::filesystem::path directory; - std::size_t minimumWords = 100000; - std::uint64_t minimumMilliseconds = 1000; - PFN_vkGetPipelineKeyKHR getKey = nullptr; - PFN_vkCreatePipelineBinariesKHR createBinaries = nullptr; - PFN_vkDestroyPipelineBinaryKHR destroyBinary = nullptr; - PFN_vkGetPipelineBinaryDataKHR getData = nullptr; - PFN_vkReleaseCapturedPipelineDataKHR releaseData = nullptr; -}; - -} - -#endif diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineCache.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineCache.hpp index 6c25cb307ab3..f1074b7e2d44 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineCache.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineCache.hpp @@ -47,7 +47,7 @@ class PipelineCache { std::span Payload() const; }; - File readFile(const std::filesystem::path& source) const; + File readFile() const; std::optional readHeader() const; void load(std::vector& initialData); void save(bool final, bool waitForPipelines); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineBinaryStore.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineBinaryStore.cpp deleted file mode 100644 index d66d44b4190c..000000000000 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineBinaryStore.cpp +++ /dev/null @@ -1,359 +0,0 @@ -#include "prx/libSceAgcDriver/Graphics/include/PipelineBinaryStore.hpp" -#include "ShaderCacheDirectory.hpp" -#include -#include -#include -#include -#include -#include -#include - -namespace AgcDriver::Graphics { -namespace { - -constexpr std::uint64_t FileMagic = 0x3159425035535041ull; -constexpr std::size_t MaximumFileBytes = 256u * 1024u * 1024u; -constexpr std::uint32_t MaximumBinaries = 64; -constexpr std::uint64_t CaptureFailureMarker = 1; - -std::uint64_t configured(const char* name, std::uint64_t fallback) { - const auto* value = std::getenv(name); - if (value == nullptr) return fallback; - std::uint64_t result = 0; - const auto end = value + std::strlen(value); - const auto parsed = std::from_chars(value, end, result); - return parsed.ec == std::errc{} && parsed.ptr == end ? result : fallback; -} - -void number(std::vector& output, std::uint64_t value) { - for (unsigned i = 0; i < 8; ++i) output.push_back(static_cast(value >> (i * 8u))); -} - -void bytes(std::vector& output, std::span value) { - number(output, value.size()); - output.insert(output.end(), value.begin(), value.end()); -} - -bool validKey(const VkPipelineBinaryKeyKHR& key) { - return key.keySize != 0 && key.keySize <= VK_MAX_PIPELINE_BINARY_KEY_SIZE_KHR; -} - -void binaryKey(std::vector& output, const VkPipelineBinaryKeyKHR& key) { - bytes(output, std::as_bytes(std::span(key.key, key.keySize))); -} - -struct InvalidFile {}; - -class Reader { -public: - explicit Reader(std::span data) : data(data) {} - std::uint64_t Number() { - if (data.size() < 8) throw InvalidFile{}; - std::uint64_t value = 0; - for (unsigned i = 0; i < 8; ++i) value |= static_cast(data[i]) << (i * 8u); - data = data.subspan(8); - return value; - } - std::span Bytes() { - const auto length = Number(); - if (length > data.size()) throw InvalidFile{}; - const auto result = data.first(static_cast(length)); - data = data.subspan(static_cast(length)); - return result; - } - bool Empty() const { return data.empty(); } - -private: - std::span data; -}; - -struct BinaryHandles { - VkDevice device; - PFN_vkDestroyPipelineBinaryKHR destroy; - std::vector handles; - ~BinaryHandles() { - for (const auto handle : handles) if (handle != VK_NULL_HANDLE) destroy(device, handle, nullptr); - } -}; - -} - -bool PipelineBinaryStore::LayersCompatible(VkPhysicalDevice physical, PFN_vkEnumerateDeviceLayerProperties enumerate) { - if (enumerate == nullptr) { - std::fprintf(stderr, "[pipeline-binary] disabled: active layer enumeration unavailable; using the driver pipeline cache\n"); - return false; - } - VkResult result = VK_INCOMPLETE; - for (unsigned attempt = 0; attempt < 3; ++attempt) { - std::uint32_t count = 0; - result = enumerate(physical, &count, nullptr); - if (result == VK_INCOMPLETE) continue; - if (result != VK_SUCCESS) break; - if (count == 0) return true; - std::vector layers(count); - result = enumerate(physical, &count, layers.data()); - if (result == VK_INCOMPLETE) continue; - if (result != VK_SUCCESS) break; - layers.resize(count); - for (const auto& layer : layers) { - if (std::strcmp(layer.layerName, "VK_LAYER_KHRONOS_validation") != 0 && std::strcmp(layer.layerName, "VK_LAYER_LUNARG_gfxreconstruct") != 0) continue; - if (layer.specVersion >= VK_MAKE_API_VERSION(0, 1, 3, 294)) continue; - std::fprintf(stderr, "[pipeline-binary] disabled: active layer %s reports Vulkan %u.%u.%u (< 1.3.294); using the driver pipeline cache\n", - layer.layerName, VK_API_VERSION_MAJOR(layer.specVersion), VK_API_VERSION_MINOR(layer.specVersion), VK_API_VERSION_PATCH(layer.specVersion)); - return false; - } - return true; - } - if (result == VK_INCOMPLETE) { - std::fprintf(stderr, "[pipeline-binary] disabled: active layer enumeration remained VK_INCOMPLETE after 3 attempts; using the driver pipeline cache\n"); - } else { - std::fprintf(stderr, "[pipeline-binary] disabled: active layer enumeration failed (VkResult %d); using the driver pipeline cache\n", static_cast(result)); - } - return false; -} - -PipelineBinaryStore::PipelineBinaryStore(const Context& context, const VkPhysicalDeviceProperties& properties) : context(context), properties(properties) { - if (std::getenv("APS5_NO_PIPELINE_BINARIES") != nullptr) return; - minimumWords = static_cast(std::min(configured("APS5_PIPELINE_BINARY_MIN_WORDS", minimumWords), std::numeric_limits::max())); - minimumMilliseconds = configured("APS5_PIPELINE_BINARY_MIN_MS", minimumMilliseconds); - const auto root = ShaderRecompiler::ShaderCacheDirectory(); - if (root.empty()) return; - getKey = reinterpret_cast(context.deviceProc(context.device, "vkGetPipelineKeyKHR")); - createBinaries = reinterpret_cast(context.deviceProc(context.device, "vkCreatePipelineBinariesKHR")); - destroyBinary = reinterpret_cast(context.deviceProc(context.device, "vkDestroyPipelineBinaryKHR")); - getData = reinterpret_cast(context.deviceProc(context.device, "vkGetPipelineBinaryDataKHR")); - releaseData = reinterpret_cast(context.deviceProc(context.device, "vkReleaseCapturedPipelineDataKHR")); - if (getKey == nullptr || createBinaries == nullptr || destroyBinary == nullptr || getData == nullptr || releaseData == nullptr) return; - if (getKey(context.device, nullptr, &globalKey) != VK_SUCCESS || !validKey(globalKey)) return; - directory = root / "driver" / "binaries"; -} - -std::vector PipelineBinaryStore::Key(const VkComputePipelineCreateInfo& info, std::span spirv, std::span layout) const { - if (directory.empty() || spirv.empty() || layout.empty() || info.pNext != nullptr || info.stage.pName == nullptr || info.basePipelineHandle != VK_NULL_HANDLE || - (info.flags & (VK_PIPELINE_CREATE_DERIVATIVE_BIT | VK_PIPELINE_CREATE_ALLOW_DERIVATIVES_BIT)) != 0) return {}; - std::uint32_t subgroupSize = 0; - if (info.stage.pNext != nullptr) { - const auto* subgroup = static_cast(info.stage.pNext); - if (subgroup->sType != VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_REQUIRED_SUBGROUP_SIZE_CREATE_INFO || subgroup->pNext != nullptr) return {}; - subgroupSize = subgroup->requiredSubgroupSize; - } - VkPipelineCreateInfoKHR pipelineInfo{VK_STRUCTURE_TYPE_PIPELINE_CREATE_INFO_KHR, const_cast(&info)}; - VkPipelineBinaryKeyKHR pipelineKey{VK_STRUCTURE_TYPE_PIPELINE_BINARY_KEY_KHR}; - if (getKey(context.device, &pipelineInfo, &pipelineKey) != VK_SUCCESS || !validKey(pipelineKey)) return {}; - std::vector key; - number(key, 1); - number(key, properties.vendorID); - number(key, properties.deviceID); - number(key, properties.driverVersion); - bytes(key, std::as_bytes(std::span(properties.pipelineCacheUUID))); - binaryKey(key, globalKey); - binaryKey(key, pipelineKey); - bytes(key, std::as_bytes(spirv)); - bytes(key, std::as_bytes(layout)); - number(key, info.flags); - number(key, static_cast(info.basePipelineIndex)); - number(key, info.stage.flags); - number(key, info.stage.stage); - number(key, subgroupSize); - bytes(key, std::as_bytes(std::span(info.stage.pName, std::strlen(info.stage.pName)))); - const auto* specialization = info.stage.pSpecializationInfo; - number(key, specialization != nullptr); - if (specialization != nullptr) { - if ((specialization->mapEntryCount != 0 && specialization->pMapEntries == nullptr) || (specialization->dataSize != 0 && specialization->pData == nullptr)) return {}; - number(key, specialization->mapEntryCount); - for (std::uint32_t i = 0; i < specialization->mapEntryCount; ++i) { - const auto& entry = specialization->pMapEntries[i]; - if (entry.offset > specialization->dataSize || entry.size > specialization->dataSize - entry.offset) return {}; - number(key, entry.constantID); - number(key, entry.offset); - number(key, entry.size); - } - bytes(key, {static_cast(specialization->pData), specialization->dataSize}); - } - return key; -} - -std::filesystem::path PipelineBinaryStore::filePath(std::span key) const { - char name[32]; - std::snprintf(name, sizeof(name), "%016llx.bin", static_cast(ShaderRecompiler::HashBytes(key))); - return directory / name; -} - -PipelineBinaryStore::RestoreResult PipelineBinaryStore::restore(const VkComputePipelineCreateInfo& info, std::span key, VkPipeline* pipeline) const { - auto path = filePath(key); - const auto result = restoreFile(info, key, pipeline, path); - if (result != RestoreResult::Missing) return result; - path += ".threshold"; - return restoreFile(info, key, pipeline, path); -} - -PipelineBinaryStore::RestoreResult PipelineBinaryStore::restoreFile(const VkComputePipelineCreateInfo& info, std::span key, VkPipeline* pipeline, const std::filesystem::path& path) const try { - const auto lookupStarted = std::chrono::steady_clock::now(); - std::error_code error; -#ifdef _WIN32 - const auto native = ShaderRecompiler::NativeCachePath_nid_no_patch(path, error); - if (error) return RestoreResult::Missing; -#else - const auto& native = path; -#endif - const auto size = std::filesystem::file_size(native, error); - if (error || size > MaximumFileBytes) return RestoreResult::Missing; - std::vector file; - if (!ShaderRecompiler::ReadWholeFile(path, file) || file.size() < 8) return RestoreResult::Missing; - const auto payload = std::span(file).first(file.size() - 8); - Reader checksum{std::span(file).last(8)}; - if (checksum.Number() != ShaderRecompiler::HashBytes(payload)) throw InvalidFile{}; - Reader reader(payload); - if (reader.Number() != FileMagic || !std::ranges::equal(reader.Bytes(), key)) throw InvalidFile{}; - const auto count = reader.Number(); - if (count == 0) { - const auto threshold = reader.Number(); - const auto kind = reader.Empty() ? 0 : reader.Number(); - if (!reader.Empty()) throw InvalidFile{}; - if (kind == CaptureFailureMarker) return RestoreResult::CaptureFailed; - return minimumMilliseconds >= threshold ? RestoreResult::BelowThreshold : RestoreResult::Missing; - } - if (count > MaximumBinaries) throw InvalidFile{}; - std::vector keys(static_cast(count), {VK_STRUCTURE_TYPE_PIPELINE_BINARY_KEY_KHR}); - std::vector data(static_cast(count)); - for (std::size_t i = 0; i < count; ++i) { - const auto storedKey = reader.Bytes(); - if (storedKey.empty() || storedKey.size() > VK_MAX_PIPELINE_BINARY_KEY_SIZE_KHR) throw InvalidFile{}; - keys[i].keySize = static_cast(storedKey.size()); - std::memcpy(keys[i].key, storedKey.data(), storedKey.size()); - const auto storedData = reader.Bytes(); - if (storedData.empty()) throw InvalidFile{}; - data[i] = {storedData.size(), const_cast(storedData.data())}; - } - if (!reader.Empty()) throw InvalidFile{}; - BinaryHandles binaries{context.device, destroyBinary, std::vector(static_cast(count))}; - VkPipelineBinaryKeysAndDataKHR keysAndData{static_cast(count), keys.data(), data.data()}; - VkPipelineBinaryCreateInfoKHR binaryInfo{VK_STRUCTURE_TYPE_PIPELINE_BINARY_CREATE_INFO_KHR}; - binaryInfo.pKeysAndDataInfo = &keysAndData; - VkPipelineBinaryHandlesInfoKHR handles{VK_STRUCTURE_TYPE_PIPELINE_BINARY_HANDLES_INFO_KHR, nullptr, static_cast(count), binaries.handles.data()}; - const auto imported = createBinaries(context.device, &binaryInfo, nullptr, &handles); - if (imported != VK_SUCCESS || handles.pipelineBinaryCount != count || std::ranges::find(binaries.handles, VK_NULL_HANDLE) != binaries.handles.end()) { - std::fprintf(stderr, "[pipeline-binary] import rejected key=%016llx result=%d; rebuilding\n", static_cast(ShaderRecompiler::HashBytes(key)), static_cast(imported)); - return RestoreResult::Missing; - } - VkPipelineBinaryInfoKHR binary{VK_STRUCTURE_TYPE_PIPELINE_BINARY_INFO_KHR, nullptr, static_cast(count), binaries.handles.data()}; - auto restoredInfo = info; - restoredInfo.pNext = &binary; - const auto started = std::chrono::steady_clock::now(); - const auto result = context.Function("vkCreateComputePipelines")(context.device, VK_NULL_HANDLE, 1, &restoredInfo, nullptr, pipeline); - if (result != VK_SUCCESS) { - if (*pipeline != VK_NULL_HANDLE) context.Function("vkDestroyPipeline")(context.device, *pipeline, nullptr); - *pipeline = VK_NULL_HANDLE; - std::fprintf(stderr, "[pipeline-binary] restore rejected key=%016llx result=%d; rebuilding\n", static_cast(ShaderRecompiler::HashBytes(key)), static_cast(result)); - return RestoreResult::Missing; - } - std::fprintf(stderr, "[pipeline-binary] loaded key=%016llx bytes=%zu create_ms=%.3f total_ms=%.3f\n", static_cast(ShaderRecompiler::HashBytes(key)), file.size(), std::chrono::duration(std::chrono::steady_clock::now() - started).count(), std::chrono::duration(std::chrono::steady_clock::now() - lookupStarted).count()); - return RestoreResult::Restored; -} catch (const InvalidFile&) { - std::fprintf(stderr, "[pipeline-binary] invalid store key=%016llx; rebuilding\n", static_cast(ShaderRecompiler::HashBytes(key))); - return RestoreResult::Missing; -} - -bool PipelineBinaryStore::capture(VkPipeline pipeline, std::span key) const { - VkPipelineBinaryCreateInfoKHR info{VK_STRUCTURE_TYPE_PIPELINE_BINARY_CREATE_INFO_KHR}; - info.pipeline = pipeline; - VkPipelineBinaryHandlesInfoKHR output{VK_STRUCTURE_TYPE_PIPELINE_BINARY_HANDLES_INFO_KHR}; - if (createBinaries(context.device, &info, nullptr, &output) != VK_SUCCESS || output.pipelineBinaryCount == 0 || output.pipelineBinaryCount > MaximumBinaries) return false; - BinaryHandles binaries{context.device, destroyBinary, std::vector(output.pipelineBinaryCount)}; - const auto count = output.pipelineBinaryCount; - output.pPipelineBinaries = binaries.handles.data(); - if (createBinaries(context.device, &info, nullptr, &output) != VK_SUCCESS || output.pipelineBinaryCount != count || std::ranges::find(binaries.handles, VK_NULL_HANDLE) != binaries.handles.end()) return false; - std::vector file; - number(file, FileMagic); - bytes(file, key); - number(file, count); - for (const auto binary : binaries.handles) { - VkPipelineBinaryDataInfoKHR dataInfo{VK_STRUCTURE_TYPE_PIPELINE_BINARY_DATA_INFO_KHR, nullptr, binary}; - VkPipelineBinaryKeyKHR binaryId{VK_STRUCTURE_TYPE_PIPELINE_BINARY_KEY_KHR}; - std::size_t size = 0; - if (getData(context.device, &dataInfo, &binaryId, &size, nullptr) != VK_SUCCESS || !validKey(binaryId) || size == 0 || size > MaximumFileBytes) return false; - std::vector data(size); - if (getData(context.device, &dataInfo, &binaryId, &size, data.data()) != VK_SUCCESS || !validKey(binaryId) || size == 0 || size > data.size()) return false; - data.resize(size); - if (file.size() > MaximumFileBytes - data.size() || MaximumFileBytes - file.size() - data.size() < 64) return false; - binaryKey(file, binaryId); - bytes(file, data); - } - number(file, ShaderRecompiler::HashBytes(file)); - const auto path = filePath(key); - const bool stored = ShaderRecompiler::WriteFileAtomically(path, file); - if (stored) std::fprintf(stderr, "[pipeline-binary] saved key=%016llx bytes=%zu path=%s\n", static_cast(ShaderRecompiler::HashBytes(key)), file.size(), path.string().c_str()); - else std::fprintf(stderr, "[pipeline-binary] cannot save key=%016llx path=%s\n", static_cast(ShaderRecompiler::HashBytes(key)), path.string().c_str()); - return stored; -} - -void PipelineBinaryStore::rememberThreshold(std::span key, bool captureFailed) const try { - std::vector file; - number(file, FileMagic); - bytes(file, key); - number(file, 0); - number(file, minimumMilliseconds); - if (captureFailed) number(file, CaptureFailureMarker); - number(file, ShaderRecompiler::HashBytes(file)); - auto path = filePath(key); - path += ".threshold"; - if (file.size() <= MaximumFileBytes && ShaderRecompiler::WriteFileAtomically(path, file) && captureFailed) { - std::fprintf(stderr, "[pipeline-binary] capture failed key=%016llx; marked for driver cache builds\n", static_cast(ShaderRecompiler::HashBytes(key))); - } -} catch (const std::bad_alloc&) { - std::fputs("[pipeline-binary] marker skipped: host memory exhausted\n", stderr); -} - -void PipelineBinaryStore::CreateComputePipeline(const VkComputePipelineCreateInfo& info, std::span spirv, std::span layout, VkPipeline* pipeline) const { - *pipeline = VK_NULL_HANDLE; - const auto create = context.Function("vkCreateComputePipelines"); - std::vector key; - try { - if (spirv.size() >= minimumWords) key = Key(info, spirv, layout); - if (!key.empty()) { - const auto result = restore(info, key, pipeline); - if (result == RestoreResult::Restored) return; - if (result == RestoreResult::BelowThreshold || result == RestoreResult::CaptureFailed) key.clear(); - } - } catch (const std::bad_alloc&) { - key.clear(); - std::fputs("[pipeline-binary] lookup skipped: host memory exhausted\n", stderr); - } - if (!key.empty()) { - auto captureInfo = info; - VkPipelineCreateFlags2CreateInfoKHR flags{VK_STRUCTURE_TYPE_PIPELINE_CREATE_FLAGS_2_CREATE_INFO_KHR, nullptr, static_cast(info.flags) | VK_PIPELINE_CREATE_2_CAPTURE_DATA_BIT_KHR}; - captureInfo.pNext = &flags; - const auto started = std::chrono::steady_clock::now(); - const auto result = create(context.device, VK_NULL_HANDLE, 1, &captureInfo, nullptr, pipeline); - const auto milliseconds = std::chrono::duration(std::chrono::steady_clock::now() - started).count(); - if (result == VK_SUCCESS) { - struct Release { - const PipelineBinaryStore& store; - VkPipeline pipeline; - ~Release() { - VkReleaseCapturedPipelineDataInfoKHR release{VK_STRUCTURE_TYPE_RELEASE_CAPTURED_PIPELINE_DATA_INFO_KHR, nullptr, pipeline}; - store.releaseData(store.context.device, &release, nullptr); - } - } release{*this, *pipeline}; - std::fprintf(stderr, "[pipeline-binary] built key=%016llx words=%zu build_ms=%.3f\n", static_cast(ShaderRecompiler::HashBytes(key)), spirv.size(), milliseconds); - try { - if (milliseconds >= static_cast(minimumMilliseconds)) { - if (!capture(*pipeline, key)) rememberThreshold(key, true); - } - else rememberThreshold(key); - } catch (const std::bad_alloc&) { - std::fputs("[pipeline-binary] capture skipped: host memory exhausted\n", stderr); - if (milliseconds >= static_cast(minimumMilliseconds)) rememberThreshold(key, true); - } - return; - } - if (*pipeline != VK_NULL_HANDLE) context.Function("vkDestroyPipeline")(context.device, *pipeline, nullptr); - *pipeline = VK_NULL_HANDLE; - std::fprintf(stderr, "[pipeline-binary] capture build rejected key=%016llx result=%d; rebuilding without capture\n", static_cast(ShaderRecompiler::HashBytes(key)), static_cast(result)); - rememberThreshold(key, true); - } - auto lock = context.pipelineCacheSynchronization != nullptr ? context.pipelineCacheSynchronization->LockForPipelineUse() : PipelineCacheSynchronization::PipelineUseLock{}; - Check(create(context.device, context.pipelineCache, 1, &info, nullptr, pipeline), "vkCreateComputePipelines"); -} - -} diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp index 9baf224595e7..862e59483df1 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp @@ -1,5 +1,6 @@ #include "prx/libSceAgcDriver/Graphics/include/PipelineCache.hpp" #include "ShaderCacheDirectory.hpp" +#include "ShaderDiskCache.hpp" #include #include #include @@ -52,9 +53,11 @@ PipelineCache::PipelineCache(const Context& context, const VkPhysicalDevicePrope std::snprintf(digits, sizeof(digits), "%02x", static_cast(byte)); uuid += digits; } + char version[17]; + std::snprintf(version, sizeof(version), "%016llx", static_cast(ShaderRecompiler::ShaderDiskCache::SourceVersion())); char name[160]; std::snprintf(name, sizeof(name), "vk-pipelines-%04x-%04x-%08x-%s.bin", properties.vendorID, properties.deviceID, properties.driverVersion, uuid.c_str()); - path = directory / "driver" / name; + path = directory / version / name; } std::vector initialData; if (!path.empty()) { @@ -101,19 +104,19 @@ std::span PipelineCache::File::Payload() const { return std::span(contents).subspan(sizeof(FileHeader)); } -PipelineCache::File PipelineCache::readFile(const std::filesystem::path& source) const { +PipelineCache::File PipelineCache::readFile() const { File file; - if (source.empty()) { + if (path.empty()) { file.state = FileState::Missing; return file; } - if (!ShaderRecompiler::ReadWholeFile(source, file.contents)) { + if (!ShaderRecompiler::ReadWholeFile(path, file.contents)) { std::error_code error; #ifdef _WIN32 - const auto native = ShaderRecompiler::NativeCachePath_nid_no_patch(source, error); + const auto native = ShaderRecompiler::NativeCachePath_nid_no_patch(path, error); const bool exists = !error && std::filesystem::exists(native, error); #else - const bool exists = std::filesystem::exists(source, error); + const bool exists = std::filesystem::exists(path, error); #endif file.state = exists || error ? FileState::Unreadable : FileState::Missing; return file; @@ -181,37 +184,7 @@ std::optional PipelineCache::readHeader() const { } void PipelineCache::load(std::vector& initialData) { - auto file = readFile(path); - bool imported = false; - if (file.state == FileState::Missing) { - std::error_code error; - const auto root = path.parent_path().parent_path(); -#ifdef _WIN32 - const auto native = ShaderRecompiler::NativeCachePath_nid_no_patch(root, error); -#else - const auto& native = root; -#endif - std::vector> candidates; - if (!error) { - for (std::filesystem::directory_iterator it(native, error), end; !error && it != end; it.increment(error)) { - const auto name = it->path().filename().string(); - if (name.size() != 16 || name.find_first_not_of("0123456789abcdef") != std::string::npos) continue; - const auto candidate = it->path() / path.filename(); - const auto time = std::filesystem::last_write_time(candidate, error); - if (!error) candidates.emplace_back(time, candidate); - error.clear(); - } - } - std::ranges::sort(candidates, std::greater{}); - for (const auto& [time, candidate] : candidates) { - auto legacy = readFile(candidate); - if (legacy.state != FileState::Valid) continue; - file = std::move(legacy); - imported = true; - std::fprintf(stderr, "[pipeline-cache] imported compatible legacy cache from %s\n", candidate.string().c_str()); - break; - } - } + auto file = readFile(); if (file.state == FileState::Missing) return; if (file.state == FileState::Unreadable) { std::fprintf(stderr, "[pipeline-cache] cannot read %s; starting empty\n", path.string().c_str()); @@ -224,9 +197,9 @@ void PipelineCache::load(std::vector& initialData) { initialData = std::move(file.contents); initialData.erase(initialData.begin(), initialData.begin() + sizeof(FileHeader)); loadedBytes = initialData.size(); - settledBytes = imported ? 0 : initialData.size(); - settledFile = imported ? std::nullopt : std::optional(file.header); - holdsFile = !imported; + settledBytes = initialData.size(); + settledFile = file.header; + holdsFile = true; std::fprintf(stderr, "[pipeline-cache] loaded %.1f KiB from %s\n", static_cast(initialData.size()) / 1024.0, path.string().c_str()); } @@ -242,15 +215,14 @@ void PipelineCache::save(bool final, bool waitForPipelines) try { if (getData(context.device, cache, &liveBytes, nullptr) != VK_SUCCESS || liveBytes == 0) return; } const auto onDisk = readHeader(); - if (!final && !waitForPipelines && liveBytes == settledBytes && onDisk == settledFile) return; + if (liveBytes == settledBytes && onDisk == settledFile) return; const bool fileUnchanged = holdsFile && settledFile.has_value() && onDisk == settledFile; const auto started = std::chrono::steady_clock::now(); - const auto settle = [&](const std::optional& file, bool includesFile = false) { + const auto settle = [&](const std::optional& file) { settledBytes = liveBytes; settledFile = file; - holdsFile = includesFile; }; - auto disk = fileUnchanged ? File{} : readFile(path); + auto disk = fileUnchanged ? File{} : readFile(); if (disk.state == FileState::Unreadable) { std::fprintf(stderr, "[pipeline-cache] cannot read %s; not saving over it\n", path.string().c_str()); return; @@ -300,18 +272,13 @@ void PipelineCache::save(bool final, bool waitForPipelines) try { auto synchronizationLock = source == cache ? snapshot() : PipelineCacheSynchronization::SnapshotLock{}; if (source == cache && !synchronizationLock.owns_lock()) return; if (getData(context.device, source, &size, nullptr) != VK_SUCCESS || size == 0) return; - bool complete = false; - for (unsigned attempt = 0; attempt < 3; ++attempt) { - file.resize(sizeof(FileHeader) + size); - const auto capacity = size; - const auto result = getData(context.device, source, &size, file.data() + sizeof(FileHeader)); - if (result == VK_SUCCESS && size <= capacity) { - complete = true; - break; - } - if (result != VK_INCOMPLETE || getData(context.device, source, &size, nullptr) != VK_SUCCESS) return; + if (merged != VK_NULL_HANDLE && size == disk.header.dataBytes) { + settle(disk.header); + return; } - if (!complete) return; + file.resize(sizeof(FileHeader) + size); + const auto result = getData(context.device, source, &size, file.data() + sizeof(FileHeader)); + if (result != VK_SUCCESS && result != VK_INCOMPLETE) return; file.resize(sizeof(FileHeader) + size); } destroyMerged.Reset(); @@ -321,16 +288,12 @@ void PipelineCache::save(bool final, bool waitForPipelines) try { return; } const FileHeader header{FileMagic, FileFormat, size, ShaderRecompiler::HashBytes(std::span(file).subspan(sizeof(FileHeader)))}; - if ((fileUnchanged && header == *settledFile) || (mergedWithFile && header == disk.header)) { - settle(header, source == cache); - return; - } std::memcpy(file.data(), &header, sizeof(header)); if (!ShaderRecompiler::WriteFileAtomically(path, file)) { std::fprintf(stderr, "[pipeline-cache] cannot write %s\n", path.string().c_str()); return; } - settle(header, source == cache); + settle(header); if (final || profiling()) std::fprintf(stderr, "[pipeline-cache] saved %.1f KiB to %s in %.1f ms%s%s\n", static_cast(size) / 1024.0, path.string().c_str(), std::chrono::duration(std::chrono::steady_clock::now() - started).count(), mergedWithFile ? " (merged with the file)" : "", final ? " (teardown)" : ""); } catch (const std::bad_alloc&) { std::fputs("[pipeline-cache] save skipped: host memory exhausted\n", stderr); diff --git a/core/libs/prx/libSceAgcDriver/tests/PipelineBinaryRun.cmake b/core/libs/prx/libSceAgcDriver/tests/PipelineBinaryRun.cmake deleted file mode 100644 index 54a7b2c1a5d7..000000000000 --- a/core/libs/prx/libSceAgcDriver/tests/PipelineBinaryRun.cmake +++ /dev/null @@ -1,38 +0,0 @@ -string(RANDOM LENGTH 16 ALPHABET 0123456789abcdef suffix) -set(root "${CMAKE_CURRENT_BINARY_DIR}/pipeline-binary-${suffix}") -foreach(pass capture restore corrupt) - if(pass STREQUAL "corrupt") - file(GLOB stores "${root}/driver/binaries/*.bin") - foreach(store IN LISTS stores) - file(WRITE "${store}" "truncated") - endforeach() - endif() - execute_process(COMMAND "${TEST}" --pipeline-binary-only "${root}" - RESULT_VARIABLE result OUTPUT_VARIABLE output ERROR_VARIABLE errors TIMEOUT 15) - message("${pass}: ${output}${errors}") - if(result EQUAL 77) - file(REMOVE_RECURSE "${root}") - message("skipped, no usable Vulkan device or the device lacks VK_KHR_pipeline_binary") - return() - endif() - if(NOT result EQUAL 0) - message(FATAL_ERROR "${pass} failed: ${result}") - endif() - if("${output}${errors}" MATCHES "VUID-") - message(FATAL_ERROR "${pass} added Vulkan validation errors") - endif() - if(pass STREQUAL "restore") - string(REGEX MATCHALL "\\[pipeline-binary\\] loaded" loads "${errors}") - list(LENGTH loads count) - if(NOT count EQUAL 2 OR errors MATCHES "\\[pipeline-binary\\] built") - message(FATAL_ERROR "the new process did not restore both pipelines") - endif() - else() - string(REGEX MATCHALL "\\[pipeline-binary\\] saved" saves "${errors}") - list(LENGTH saves count) - if(NOT count EQUAL 2) - message(FATAL_ERROR "${pass} did not persist both pipelines") - endif() - endif() -endforeach() -file(REMOVE_RECURSE "${root}") diff --git a/core/libs/prx/libSceAgcDriver/tests/PipelineBinaryStore.cpp b/core/libs/prx/libSceAgcDriver/tests/PipelineBinaryStore.cpp deleted file mode 100644 index 9d22a0f6bbac..000000000000 --- a/core/libs/prx/libSceAgcDriver/tests/PipelineBinaryStore.cpp +++ /dev/null @@ -1,410 +0,0 @@ -#include "prx/libSceAgcDriver/Graphics/include/PipelineBinaryStore.hpp" -#include "ShaderCacheDirectory.hpp" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -using namespace AgcDriver::Graphics; - -namespace { - -unsigned cold = 0; -unsigned restored = 0; -unsigned released = 0; -unsigned legacyCreates = 0; -unsigned captures = 0; -bool reject = false; -bool captureFailure = false; -bool missing = false; -bool unsupported = false; -bool oversized = false; -std::uint8_t pipelineKey = 7; -std::uint8_t global = 1; -std::uintptr_t next = 1; -std::map binaries; -std::function duringCapture; - -void Check(bool value, const char* message) { - if (!value) throw std::runtime_error(message); -} - -std::vector activeLayers; -VkResult layerCountResult = VK_SUCCESS; -VkResult layerDataResult = VK_SUCCESS; -unsigned incompleteLayerReads = 0; - -VKAPI_ATTR VkResult VKAPI_CALL EnumerateLayers(VkPhysicalDevice physical, std::uint32_t* count, VkLayerProperties* properties) { - Check(physical == reinterpret_cast(1), "layer guard lost the physical device"); - if (properties == nullptr) { - *count = static_cast(activeLayers.size()); - return layerCountResult; - } - const auto written = std::min(*count, static_cast(activeLayers.size())); - std::copy_n(activeLayers.begin(), written, properties); - *count = written; - if (incompleteLayerReads != 0) { - --incompleteLayerReads; - return VK_INCOMPLETE; - } - return layerDataResult; -} - -void LayerCompatibilityTests() { - const auto physical = reinterpret_cast(1); - Check(PipelineBinaryStore::LayersCompatible(physical, EnumerateLayers), "no active layers disabled pipeline binaries"); - struct Case { - const char* name; - std::uint32_t version; - bool allowed; - }; - const Case cases[]{ - {"VK_LAYER_KHRONOS_validation", VK_MAKE_API_VERSION(0, 1, 3, 275), false}, - {"VK_LAYER_KHRONOS_validation", VK_MAKE_API_VERSION(0, 1, 3, 293), false}, - {"VK_LAYER_KHRONOS_validation", VK_MAKE_API_VERSION(0, 1, 3, 294), true}, - {"VK_LAYER_KHRONOS_validation", VK_MAKE_API_VERSION(0, 1, 4, 363), true}, - {"VK_LAYER_LUNARG_gfxreconstruct", VK_MAKE_API_VERSION(0, 1, 3, 280), false}, - {"VK_LAYER_VALVE_steam_overlay_64", VK_MAKE_API_VERSION(0, 1, 3, 207), true}, - }; - for (const auto& test : cases) { - VkLayerProperties layer{}; - std::strcpy(layer.layerName, test.name); - layer.specVersion = test.version; - activeLayers = {layer}; - Check(PipelineBinaryStore::LayersCompatible(physical, EnumerateLayers) == test.allowed, test.name); - } - activeLayers[0].specVersion = VK_MAKE_API_VERSION(0, 1, 4, 363); - VkLayerProperties oldValidation{}; - std::strcpy(oldValidation.layerName, "VK_LAYER_KHRONOS_validation"); - oldValidation.specVersion = VK_MAKE_API_VERSION(0, 1, 3, 275); - activeLayers.push_back(oldValidation); - Check(!PipelineBinaryStore::LayersCompatible(physical, EnumerateLayers), "mixed layers hid old validation"); - activeLayers.pop_back(); - for (const auto result : {VK_ERROR_INITIALIZATION_FAILED, VK_INCOMPLETE}) { - layerCountResult = result; - Check(!PipelineBinaryStore::LayersCompatible(physical, EnumerateLayers), "failed or incomplete layer count was accepted"); - layerCountResult = VK_SUCCESS; - layerDataResult = result; - Check(!PipelineBinaryStore::LayersCompatible(physical, EnumerateLayers), "failed or incomplete layer data was accepted"); - layerDataResult = VK_SUCCESS; - } - activeLayers.push_back(oldValidation); - incompleteLayerReads = 1; - Check(!PipelineBinaryStore::LayersCompatible(physical, EnumerateLayers), "incomplete read hid old validation"); - activeLayers.pop_back(); - incompleteLayerReads = 1; - Check(PipelineBinaryStore::LayersCompatible(physical, EnumerateLayers), "transient incomplete read disabled compatible layers"); - Check(!PipelineBinaryStore::LayersCompatible(physical, nullptr), "missing layer enumerator was accepted"); -} - -VKAPI_ATTR VkResult VKAPI_CALL Key(VkDevice, const VkPipelineCreateInfoKHR* info, VkPipelineBinaryKeyKHR* key) { - key->keySize = 1; - key->key[0] = info == nullptr ? global : pipelineKey; - return VK_SUCCESS; -} - -VKAPI_ATTR VkResult VKAPI_CALL Create(VkDevice, VkPipelineCache cache, std::uint32_t count, const VkComputePipelineCreateInfo* info, const VkAllocationCallbacks*, VkPipeline* pipeline) { - Check(count == 1, "expected one pipeline"); - const auto* chain = static_cast(info->pNext); - bool capture = false; - bool restore = false; - while (chain != nullptr) { - if (chain->sType == VK_STRUCTURE_TYPE_PIPELINE_CREATE_FLAGS_2_CREATE_INFO_KHR) capture = (reinterpret_cast(chain)->flags & VK_PIPELINE_CREATE_2_CAPTURE_DATA_BIT_KHR) != 0; - if (chain->sType == VK_STRUCTURE_TYPE_PIPELINE_BINARY_INFO_KHR) { - const auto& binary = *reinterpret_cast(chain); - Check(binary.binaryCount == 2, "lost pipeline binary count"); - Check(binaries.at(binary.pPipelineBinaries[0]) == 3 && binaries.at(binary.pPipelineBinaries[1]) == 4, "lost pipeline binary order"); - restore = true; - } - chain = chain->pNext; - } - if (capture || restore) Check(cache == VK_NULL_HANDLE, "binary pipeline used a legacy cache"); - else { - Check(cache == reinterpret_cast(2), "legacy build lost its driver cache"); - ++legacyCreates; - } - if (restore && reject) return VK_ERROR_UNKNOWN; - if (capture) ++captures; - if (capture && duringCapture) { - auto callback = std::move(duringCapture); - duringCapture = {}; - callback(); - } - if (capture && captureFailure) return VK_ERROR_UNKNOWN; - if (restore) ++restored; - else ++cold; - *pipeline = reinterpret_cast(next++); - return VK_SUCCESS; -} - -VKAPI_ATTR VkResult VKAPI_CALL Binary(VkDevice, const VkPipelineBinaryCreateInfoKHR* info, const VkAllocationCallbacks*, VkPipelineBinaryHandlesInfoKHR* output) { - if (missing) return VK_PIPELINE_BINARY_MISSING_KHR; - if (output->pPipelineBinaries == nullptr) { - output->pipelineBinaryCount = 2; - return VK_SUCCESS; - } - Check(output->pipelineBinaryCount == 2, "wrong binary capacity"); - for (unsigned i = 0; i < 2; ++i) { - const auto value = static_cast(3 + i); - if (info->pKeysAndDataInfo != nullptr) { - const auto& data = *info->pKeysAndDataInfo; - Check(data.binaryCount == 2 && data.pPipelineBinaryKeys[i].keySize == 1 && data.pPipelineBinaryKeys[i].key[0] == value, "wrong imported binary key"); - Check(data.pPipelineBinaryData[i].dataSize == 1 && *static_cast(data.pPipelineBinaryData[i].pData) == value, "wrong imported binary data"); - } - output->pPipelineBinaries[i] = reinterpret_cast(next++); - binaries.emplace(output->pPipelineBinaries[i], value); - } - return VK_SUCCESS; -} - -VKAPI_ATTR void VKAPI_CALL DestroyBinary(VkDevice, VkPipelineBinaryKHR binary, const VkAllocationCallbacks*) { - Check(binaries.erase(binary) == 1, "unknown binary destroyed"); -} - -VKAPI_ATTR void VKAPI_CALL DestroyPipeline(VkDevice, VkPipeline, const VkAllocationCallbacks*) {} - -VKAPI_ATTR VkResult VKAPI_CALL Data(VkDevice, const VkPipelineBinaryDataInfoKHR* info, VkPipelineBinaryKeyKHR* key, std::size_t* size, void* data) { - key->keySize = 1; - key->key[0] = binaries.at(info->pipelineBinary); - *size = oversized ? 256u * 1024u * 1024u + 1u : 1; - if (data != nullptr) *static_cast(data) = key->key[0]; - return VK_SUCCESS; -} - -VKAPI_ATTR VkResult VKAPI_CALL Release(VkDevice, const VkReleaseCapturedPipelineDataInfoKHR*, const VkAllocationCallbacks*) { - ++released; - return VK_SUCCESS; -} - -VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL Resolve(VkDevice, const char* name) { - if (unsupported && std::strcmp(name, "vkGetPipelineKeyKHR") == 0) return nullptr; - if (std::strcmp(name, "vkGetPipelineKeyKHR") == 0) return reinterpret_cast(Key); - if (std::strcmp(name, "vkCreateComputePipelines") == 0) return reinterpret_cast(Create); - if (std::strcmp(name, "vkCreatePipelineBinariesKHR") == 0) return reinterpret_cast(Binary); - if (std::strcmp(name, "vkDestroyPipelineBinaryKHR") == 0) return reinterpret_cast(DestroyBinary); - if (std::strcmp(name, "vkDestroyPipeline") == 0) return reinterpret_cast(DestroyPipeline); - if (std::strcmp(name, "vkGetPipelineBinaryDataKHR") == 0) return reinterpret_cast(Data); - if (std::strcmp(name, "vkReleaseCapturedPipelineDataKHR") == 0) return reinterpret_cast(Release); - return nullptr; -} - -void Environment(const char* name, const char* value) { -#ifdef _WIN32 - _putenv_s(name, value); -#else - if (*value == 0) unsetenv(name); - else setenv(name, value, 1); -#endif -} - -} - -int main() try { - LayerCompatibilityTests(); - const auto root = std::filesystem::temp_directory_path() / ("aps5-binary-test-" + std::to_string(std::random_device{}())); - std::filesystem::create_directories(root); - struct Cleanup { - std::filesystem::path path; - ~Cleanup() { std::error_code error; std::filesystem::remove_all(path, error); } - } cleanup{root}; - Environment("ANYPS5_SHADER_CACHE_DIR", root.string().c_str()); - Environment("ANYPS5_NO_SHADER_CACHE", "0"); - Environment("APS5_NO_PIPELINE_BINARIES", ""); - Environment("APS5_PIPELINE_BINARY_MIN_WORDS", "0"); - Environment("APS5_PIPELINE_BINARY_MIN_MS", "0"); - Context context{}; - context.device = reinterpret_cast(1); - context.deviceProc = Resolve; - context.pipelineCache = reinterpret_cast(2); - VkPhysicalDeviceProperties properties{}; - properties.vendorID = 1; - properties.deviceID = 2; - properties.driverVersion = 3; - std::array spirv{0x07230203, 0x10000, 0, 1, 0}; - std::array layout{1, 2, 3, 4}; - VkComputePipelineCreateInfo info{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; - info.stage = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, nullptr, 0, VK_SHADER_STAGE_COMPUTE_BIT, reinterpret_cast(1), "main", nullptr}; - info.layout = reinterpret_cast(1); - PipelineBinaryStore store(context, properties); - const auto key = store.Key(info, spirv, layout); - Check(!key.empty() && store.Key(info, spirv, layout) == key, "unstable recipe key"); - ++spirv[2]; - Check(store.Key(info, spirv, layout) != key, "SPIR-V is absent from key"); - --spirv[2]; - ++layout[0]; - Check(store.Key(info, spirv, layout) != key, "layout is absent from key"); - --layout[0]; - info.flags = VK_PIPELINE_CREATE_DISABLE_OPTIMIZATION_BIT; - Check(store.Key(info, spirv, layout) != key, "flags are absent from key"); - info.flags = 0; - info.stage.flags = VK_PIPELINE_SHADER_STAGE_CREATE_REQUIRE_FULL_SUBGROUPS_BIT; - Check(store.Key(info, spirv, layout) != key, "stage flags are absent from key"); - info.stage.flags = 0; - info.stage.pName = "other"; - Check(store.Key(info, spirv, layout) != key, "entry point is absent from key"); - info.stage.pName = "main"; - VkSpecializationMapEntry entry{7, 0, 4}; - std::uint32_t value = 1; - VkSpecializationInfo spec{1, &entry, sizeof(value), &value}; - info.stage.pSpecializationInfo = &spec; - const auto specialized = store.Key(info, spirv, layout); - Check(specialized != key, "specialization is absent from key"); - ++value; - Check(store.Key(info, spirv, layout) != specialized, "specialization data is absent from key"); - --value; - ++entry.constantID; - Check(store.Key(info, spirv, layout) != specialized, "specialization ID is absent from key"); - info.stage.pSpecializationInfo = nullptr; - VkPipelineShaderStageRequiredSubgroupSizeCreateInfo subgroup{VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_REQUIRED_SUBGROUP_SIZE_CREATE_INFO, nullptr, 32}; - info.stage.pNext = &subgroup; - const auto grouped = store.Key(info, spirv, layout); - Check(grouped != key, "subgroup size is absent from key"); - subgroup.requiredSubgroupSize = 64; - Check(store.Key(info, spirv, layout) != grouped, "subgroup size change is absent from key"); - info.stage.pNext = nullptr; - VkBaseInStructure unknown{VK_STRUCTURE_TYPE_APPLICATION_INFO, nullptr}; - info.pNext = &unknown; - Check(store.Key(info, spirv, layout).empty(), "unknown pipeline chain accepted"); - info.pNext = nullptr; - info.stage.pNext = &unknown; - Check(store.Key(info, spirv, layout).empty(), "unknown stage chain accepted"); - info.stage.pNext = nullptr; - VkPipeline pipeline = VK_NULL_HANDLE; - store.CreateComputePipeline(info, spirv, layout, &pipeline); - Check(cold == 1 && restored == 0 && released == 1 && binaries.empty(), "cold capture failed or leaked"); - { - PipelineBinaryStore reopened(context, properties); - reopened.CreateComputePipeline(info, spirv, layout, &pipeline); - } - Check(cold == 1 && restored == 1 && binaries.empty(), "store did not survive recreation"); - reject = true; - store.CreateComputePipeline(info, spirv, layout, &pipeline); - Check(cold == 2 && binaries.empty(), "rejected binary did not fall back"); - reject = false; - missing = true; - store.CreateComputePipeline(info, spirv, layout, &pipeline); - Check(cold == 3 && binaries.empty(), "missing binary did not fall back"); - missing = false; - ++global; - PipelineBinaryStore incompatible(context, properties); - Check(incompatible.Key(info, spirv, layout) != key, "global device key is absent"); - incompatible.CreateComputePipeline(info, spirv, layout, &pipeline); - Check(cold == 4, "incompatible global key reused a binary"); - ++properties.driverVersion; - PipelineBinaryStore newDriver(context, properties); - Check(newDriver.Key(info, spirv, layout) != incompatible.Key(info, spirv, layout), "driver fingerprint is absent"); - for (const auto& file : std::filesystem::recursive_directory_iterator(root)) { - if (!file.is_regular_file()) continue; - std::fstream stream(file.path(), std::ios::in | std::ios::out | std::ios::binary); - stream.put('X'); - } - store.CreateComputePipeline(info, spirv, layout, &pipeline); - Check(cold == 5, "corrupt binary did not fall back"); - captureFailure = true; - ++spirv[2]; - store.CreateComputePipeline(info, spirv, layout, &pipeline); - Check(cold == 6, "failed capture did not retry without capture"); - captureFailure = false; - ++spirv[2]; - Environment("APS5_NO_PIPELINE_BINARIES", "1"); - PipelineBinaryStore disabled(context, properties); - disabled.CreateComputePipeline(info, spirv, layout, &pipeline); - Check(cold == 7 && binaries.empty(), "opt-out did not use legacy build"); - Environment("APS5_NO_PIPELINE_BINARIES", ""); - unsupported = true; - PipelineBinaryStore unavailable(context, properties); - unavailable.CreateComputePipeline(info, spirv, layout, &pipeline); - Check(cold == 8 && binaries.empty(), "unsupported extension did not use legacy build"); - unsupported = false; - Environment("APS5_PIPELINE_BINARY_MIN_WORDS", "100"); - PipelineBinaryStore small(context, properties); - small.CreateComputePipeline(info, spirv, layout, &pipeline); - Check(cold == 9 && released == 5, "size threshold captured a small pipeline"); - Environment("APS5_PIPELINE_BINARY_MIN_WORDS", "0"); - Environment("APS5_PIPELINE_BINARY_MIN_MS", "100000"); - PipelineBinaryStore fast(context, properties); - fast.CreateComputePipeline(info, spirv, layout, &pipeline); - Check(cold == 10 && released == 6 && binaries.empty(), "time threshold did not release capture data"); - const auto filesBefore = std::distance(std::filesystem::recursive_directory_iterator(root), std::filesystem::recursive_directory_iterator{}); - const auto legacyBefore = legacyCreates; - PipelineBinaryStore fastReopened(context, properties); - fastReopened.CreateComputePipeline(info, spirv, layout, &pipeline); - Check(legacyCreates == legacyBefore + 1 && released == 6, "time-threshold rejection did not survive recreation and use the driver cache"); - const auto filesAfter = std::distance(std::filesystem::recursive_directory_iterator(root), std::filesystem::recursive_directory_iterator{}); - Check(cold == 11 && filesBefore == filesAfter, "time threshold persisted a fast pipeline"); - Environment("APS5_PIPELINE_BINARY_MIN_MS", "0"); - PipelineBinaryStore retryThreshold(context, properties); - retryThreshold.CreateComputePipeline(info, spirv, layout, &pipeline); - Check(cold == 12 && released == 7 && binaries.empty(), "lowering the time threshold did not retry capture"); - const auto beforePipelineKey = store.Key(info, spirv, layout); - ++pipelineKey; - Check(store.Key(info, spirv, layout) != beforePipelineKey, "Vulkan pipeline key is absent"); - properties.vendorID += 1; - PipelineBinaryStore newVendor(context, properties); - Check(newVendor.Key(info, spirv, layout) != fast.Key(info, spirv, layout), "vendor fingerprint is absent"); - properties.deviceID += 1; - PipelineBinaryStore newDevice(context, properties); - Check(newDevice.Key(info, spirv, layout) != newVendor.Key(info, spirv, layout), "device fingerprint is absent"); - properties.pipelineCacheUUID[0] = 1; - PipelineBinaryStore newUuid(context, properties); - Check(newUuid.Key(info, spirv, layout) != newDevice.Key(info, spirv, layout), "UUID fingerprint is absent"); - Environment("APS5_PIPELINE_BINARY_MIN_MS", "100000"); - PipelineBinaryStore pendingMarker(context, properties); - Environment("APS5_PIPELINE_BINARY_MIN_MS", "0"); - PipelineBinaryStore competingCapture(context, properties); - duringCapture = [&] { - VkPipeline competingPipeline = VK_NULL_HANDLE; - competingCapture.CreateComputePipeline(info, spirv, layout, &competingPipeline); - Check(competingPipeline != VK_NULL_HANDLE, "competing capture did not create a pipeline"); - }; - pendingMarker.CreateComputePipeline(info, spirv, layout, &pipeline); - const auto restoredBeforeMarker = restored; - const auto coldBeforeMarker = cold; - for (const auto threshold : {"100000", "200000"}) { - Environment("APS5_PIPELINE_BINARY_MIN_MS", threshold); - PipelineBinaryStore afterMarker(context, properties); - afterMarker.CreateComputePipeline(info, spirv, layout, &pipeline); - } - Check(restored == restoredBeforeMarker + 2 && cold == coldBeforeMarker && binaries.empty(), "pending threshold marker erased a competing captured binary"); - bool failuresRemembered = true; - for (const auto failure : {"extraction failure", "oversized binary", "rejected capture build"}) { - ++spirv[2]; - Environment("APS5_PIPELINE_BINARY_MIN_MS", "1"); - PipelineBinaryStore failing(context, properties); - const auto recipe = failing.Key(info, spirv, layout); - char name[32]; - std::snprintf(name, sizeof(name), "%016llx.bin", static_cast(ShaderRecompiler::HashBytes(recipe))); - const auto binaryFile = root / "driver" / "binaries" / name; - missing = std::strcmp(failure, "extraction failure") == 0; - oversized = std::strcmp(failure, "oversized binary") == 0; - captureFailure = std::strcmp(failure, "rejected capture build") == 0; - const auto capturesBefore = captures; - duringCapture = [] { std::this_thread::sleep_for(std::chrono::milliseconds(5)); }; - failing.CreateComputePipeline(info, spirv, layout, &pipeline); - Check(pipeline != VK_NULL_HANDLE && captures == capturesBefore + 1 && !std::filesystem::exists(binaryFile) && binaries.empty(), "failure case did not build without a stored binary"); - missing = false; - oversized = false; - captureFailure = false; - const auto legacyBeforeFailure = legacyCreates; - PipelineBinaryStore reopenedFailure(context, properties); - reopenedFailure.CreateComputePipeline(info, spirv, layout, &pipeline); - const bool ordinary = pipeline != VK_NULL_HANDLE && legacyCreates == legacyBeforeFailure + 1 && captures == capturesBefore + 1; - Environment("APS5_PIPELINE_BINARY_MIN_MS", "0"); - PipelineBinaryStore loweredFailure(context, properties); - loweredFailure.CreateComputePipeline(info, spirv, layout, &pipeline); - const bool loweredOrdinary = pipeline != VK_NULL_HANDLE && legacyCreates == legacyBeforeFailure + 2 && captures == capturesBefore + 1; - std::fprintf(stderr, "%s: %s survives recreation and a lowered time threshold\n", ordinary && loweredOrdinary ? "PASS" : "FAIL", failure); - failuresRemembered = failuresRemembered && ordinary && loweredOrdinary; - } - Check(failuresRemembered, "capture failures did not retain the ordinary driver cache path"); - std::puts("pipeline binary keys, round-trip, compatibility, corruption, rejection and fallback passed"); -} catch (const std::exception& error) { - std::fprintf(stderr, "%s\n", error.what()); - return 1; -} diff --git a/core/libs/prx/libSceAgcDriver/tests/PipelineCacheFailures.cpp b/core/libs/prx/libSceAgcDriver/tests/PipelineCacheFailures.cpp index 8dd46beb9681..7e13f9881cf9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/PipelineCacheFailures.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/PipelineCacheFailures.cpp @@ -36,11 +36,8 @@ std::uintptr_t next = 1; std::atomic armed{false}; std::atomic failStartup{false}; std::atomic queries{0}; -std::atomic sizeQueries{0}; -std::atomic snapshots{0}; std::atomic faults{0}; std::size_t initialBytes = 64; -unsigned incompleteSnapshots = 0; std::vector Payload() { std::vector bytes(initialBytes); @@ -80,15 +77,8 @@ VKAPI_ATTR VkResult VKAPI_CALL Data(VkDevice, VkPipelineCache cache, std::size_t std::lock_guard lock(mutex); const auto& bytes = caches.at(cache); if (output != nullptr) { - ++snapshots; Check(*size >= bytes.size(), "cache snapshot buffer is too small"); std::memcpy(output, bytes.data(), bytes.size()); - if (incompleteSnapshots != 0) { - --incompleteSnapshots; - return VK_INCOMPLETE; - } - } else { - ++sizeQueries; } *size = bytes.size(); if (output == nullptr && armed && ++queries == 2) { @@ -122,9 +112,9 @@ void Grow(VkPipelineCache cache, std::size_t bytes = 128) { } int main(int argc, char** argv) { - Check(argc == 2, "expected startup, now, busy, destructor, worker, read-retry, same-size, incomplete or periodic"); + Check(argc == 2, "expected startup, now, busy, destructor, worker or read-retry"); const std::string_view mode(argv[1]); - Check(mode == "startup" || mode == "now" || mode == "busy" || mode == "destructor" || mode == "worker" || mode == "read-retry" || mode == "same-size" || mode == "incomplete" || mode == "periodic", "unknown mode"); + Check(mode == "startup" || mode == "now" || mode == "busy" || mode == "destructor" || mode == "worker" || mode == "read-retry", "unknown mode"); #ifndef _WIN32 if (mode == "read-retry") return 77; #endif @@ -171,91 +161,6 @@ int main(int argc, char** argv) { std::filesystem::path file; for (const auto& entry : std::filesystem::recursive_directory_iterator(root)) if (entry.path().extension() == ".bin") file = entry.path(); Check(!file.empty(), "initial save created no cache file"); - if (mode == "periodic") { - const auto before = Read(file); - cache.reset(); - cache = std::make_unique(context, properties); - sizeQueries = 0; - snapshots = 0; - cache->SaveUnlessBusy(); - Check(sizeQueries == 1 && snapshots == 0, "an unchanged reopened periodic save copied the driver cache"); - { - std::lock_guard lock(mutex); - caches.at(cache->Handle()).back() = std::byte{17}; - } - cache->SaveUnlessBusy(); - Check(sizeQueries == 2 && snapshots == 0, "an equal-size periodic save copied the driver cache"); - Check(Read(file) == before, "the periodic save wrote an equal-size content change"); - cache->SaveNow(); - Check(snapshots != 0 && Read(file) != before, "SaveNow did not persist the equal-size change after a periodic save"); - const auto saved = Read(file); - { - std::lock_guard lock(mutex); - caches.at(cache->Handle()).back() = std::byte{18}; - } - snapshots = 0; - cache.reset(); - Check(snapshots != 0 && Read(file) != saved, "the final save did not persist an equal-size content change"); - const auto migratedBytes = Read(file); - const auto legacy = root / "0123456789abcdef" / file.filename(); - std::filesystem::create_directories(legacy.parent_path()); - std::filesystem::rename(file, legacy); - { - PipelineCache migrated(context, properties); - snapshots = 0; - migrated.SaveUnlessBusy(); - Check(snapshots != 0 && std::filesystem::exists(file), "the first periodic save did not write the imported legacy cache"); - Check(Read(file) == migratedBytes, "the periodic legacy import changed the cache data"); - } - Check(caches.empty(), "the periodic save test leaked Vulkan objects"); - std::puts("reopened periodic saves query only the size; legacy imports, SaveNow and teardown persist changes"); - return 0; - } - if (mode == "same-size" || mode == "incomplete") { - auto before = Read(file); - { - std::lock_guard lock(mutex); - caches.at(cache->Handle()).back() = std::byte{17}; - } - if (mode == "incomplete") { - incompleteSnapshots = 10; - cache->SaveNow(); - Check(Read(file) == before, "an incomplete snapshot replaced the cache"); - incompleteSnapshots = 0; - } - cache->SaveNow(); - Check(Read(file).size() == before.size() && Read(file) != before, "equal-size cache content change was not saved"); - { - PipelineCache writer(context, properties); - { - std::lock_guard lock(mutex); - caches.at(writer.Handle()).back() = std::byte{18}; - } - writer.SaveNow(); - } - before = Read(file); - cache->SaveNow(); - Check(Read(file).size() == before.size() && Read(file) != before, "equal-size merged cache content change was not saved"); - Check(file.parent_path().filename() == "driver", "driver cache remains tied to the recompiler source version"); - const auto saved = Read(file); - cache.reset(); - const auto legacy = root / "0123456789abcdef" / file.filename(); - std::filesystem::create_directories(legacy.parent_path()); - std::filesystem::rename(file, legacy); - { - PipelineCache migrated(context, properties); - migrated.SaveNow(); - Check(Read(file) == saved, "a compatible cache from a previous source version was not imported"); - } - ++properties.driverVersion; - { - PipelineCache differentDriver(context, properties); - std::lock_guard lock(mutex); - Check(caches.at(differentDriver.Handle()) == Payload(), "a different driver imported incompatible legacy data"); - } - std::puts("equal-size snapshots and merges persist content changes, incomplete snapshots preserve the file"); - return 0; - } #ifdef _WIN32 if (mode == "read-retry") { { diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 4b5b79c58a5b..9f9ca656a674 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -48,7 +48,7 @@ #include "VertexRangeProducer_frag_spv.h" #include "prx/libSceAgcDriver/Graphics/include/ColorTargetTransfer.hpp" #include "BdaAbi.hpp" -#include "prx/libSceAgcDriver/Graphics/include/PipelineBinaryStore.hpp" +#include "ShaderCacheSourceVersion.hpp" #include #ifdef __APPLE__ #include @@ -415,7 +415,7 @@ void deviceSelectionTests() { // them, as the driver creates its own; the recorder's batches need a real queue and real fences. class Device { public: - explicit Device(bool settleOnOtherFamily = false, bool pipelineBinaries = false) { + explicit Device(bool settleOnOtherFamily = false) { #ifdef _WIN32 library = SDL_LoadObject("vulkan-1.dll"); #elif defined(__APPLE__) @@ -601,35 +601,6 @@ class Device { robustness.pNext = address.pNext; address.pNext = &robustness; } - VkPhysicalDeviceMaintenance5FeaturesKHR maintenance{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MAINTENANCE_5_FEATURES_KHR}; - VkPhysicalDevicePipelineBinaryFeaturesKHR binaryFeatures{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PIPELINE_BINARY_FEATURES_KHR}; - VkDevicePipelineBinaryInternalCacheControlKHR internalCache{VK_STRUCTURE_TYPE_DEVICE_PIPELINE_BINARY_INTERNAL_CACHE_CONTROL_KHR}; - const std::array binaryDependencies{VK_KHR_MAINTENANCE_5_EXTENSION_NAME, VK_KHR_DYNAMIC_RENDERING_EXTENSION_NAME, VK_KHR_DEPTH_STENCIL_RESOLVE_EXTENSION_NAME, VK_KHR_CREATE_RENDERPASS_2_EXTENSION_NAME}; - const bool wantPipelineBinaries = pipelineBinaries && hasExtension(VK_KHR_PIPELINE_BINARY_EXTENSION_NAME) && std::ranges::all_of(binaryDependencies, hasExtension) && - PipelineBinaryStore::LayersCompatible(context.physical, function("vkEnumerateDeviceLayerProperties")); - if (wantPipelineBinaries) { - maintenance.pNext = &binaryFeatures; - VkPhysicalDeviceFeatures2 features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &maintenance}; - function("vkGetPhysicalDeviceFeatures2")(context.physical, &features); - binaryEnabled = maintenance.maintenance5 == VK_TRUE && binaryFeatures.pipelineBinaries == VK_TRUE; - if (binaryEnabled) { - for (const auto* extension : binaryDependencies) { - if (std::ranges::none_of(extensionsEnabled, [&](const char* enabled) { return std::strcmp(enabled, extension) == 0; })) extensionsEnabled.push_back(extension); - } - extensionsEnabled.push_back(VK_KHR_PIPELINE_BINARY_EXTENSION_NAME); - binaryFeatures.pNext = address.pNext; - address.pNext = &maintenance; - VkPhysicalDevicePipelineBinaryPropertiesKHR properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PIPELINE_BINARY_PROPERTIES_KHR}; - VkPhysicalDeviceProperties2 queried{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &properties}; - function("vkGetPhysicalDeviceProperties2")(context.physical, &queried); - if (properties.pipelineBinaryInternalCacheControl == VK_TRUE) { - internalCache.disableInternalCache = VK_TRUE; - internalCache.pNext = address.pNext; - address.pNext = &internalCache; - } - std::cout << "pipeline binary internal_cache_disabled=" << internalCache.disableInternalCache << " driver=" << queried.properties.driverVersion << '\n'; - } - } VkDeviceCreateInfo device{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO, &address}; const std::array queueInfos{queue, settle}; device.queueCreateInfoCount = settleFamily != noSettleFamily && settleFamily != family ? 2 : 1; @@ -671,7 +642,6 @@ class Device { ~Device() { release(); } bool NullDescriptors() const { return nullDescriptors; } - bool PipelineBinaries() const { return binaryEnabled; } const Context& GetContext() const { return context; } bool Graphics() const { return graphics; } bool Timestamps() const { @@ -730,7 +700,6 @@ class Device { VkInstance instance = VK_NULL_HANDLE; Context context{}; bool graphics = false; - bool binaryEnabled = false; std::mutex settleMutex; bool nullDescriptors = false; bool memoryBudget = false; @@ -11381,7 +11350,8 @@ void pipelineCacheHeaderTests(const Device& device) { } } removeDirectory{cleanupRoot}; auto directory = root; - const std::string version = "driver"; + char version[17]; + std::snprintf(version, sizeof(version), "%016llx", static_cast(ShaderRecompiler::ShaderDiskCache::Generated::SourceVersion)); while ((directory / version).native().size() < 220) { const auto length = (directory / version).native().size(); const auto remaining = 245 > length ? 245 - length : 1; @@ -11441,50 +11411,6 @@ void pipelineCacheHeaderTests(const Device& device) { std::cout << "long-path pipeline cache header reused without reloading\n"; } -int pipelineBinaryTests(const Device& device) { - if (!device.PipelineBinaries()) { - std::cout << "skipped, the device lacks VK_KHR_pipeline_binary\n"; - return 77; - } - const auto& context = device.GetContext(); - struct Objects { - const Context& context; - VkPipelineLayout layout = VK_NULL_HANDLE; - VkShaderModule module = VK_NULL_HANDLE; - VkPipeline pipeline = VK_NULL_HANDLE; - ~Objects() { - if (pipeline != VK_NULL_HANDLE) context.Function("vkDestroyPipeline")(context.device, pipeline, nullptr); - if (module != VK_NULL_HANDLE) context.Function("vkDestroyShaderModule")(context.device, module, nullptr); - if (layout != VK_NULL_HANDLE) context.Function("vkDestroyPipelineLayout")(context.device, layout, nullptr); - } - } objects{context}; - VkPipelineLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; - Check(context.Function("vkCreatePipelineLayout")(context.device, &layoutInfo, nullptr, &objects.layout), "vkCreatePipelineLayout"); - const std::array layoutKey{0, 0}; - PipelineBinaryStore store(context, device.Properties()); - for (const auto width : {4u, 8u}) { - const std::array spirv{0x07230203u, 0x00010000u, 0u, 6u, 0u, - 0x00020011u, 1u, 0x0003000eu, 0u, 1u, - 0x0005000fu, 5u, 4u, 0x6e69616du, 0u, - 0x00060010u, 4u, 17u, width, 1u, 1u, - 0x00020013u, 1u, 0x00030021u, 2u, 1u, - 0x00050036u, 1u, 4u, 0u, 2u, 0x000200f8u, 5u, 0x000100fdu, 0x00010038u}; - VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; - moduleInfo.codeSize = spirv.size() * sizeof(std::uint32_t); - moduleInfo.pCode = spirv.data(); - Check(context.Function("vkCreateShaderModule")(context.device, &moduleInfo, nullptr, &objects.module), "vkCreateShaderModule"); - VkComputePipelineCreateInfo info{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; - info.stage = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, nullptr, 0, VK_SHADER_STAGE_COMPUTE_BIT, objects.module, "main", nullptr}; - info.layout = objects.layout; - store.CreateComputePipeline(info, spirv, layoutKey, &objects.pipeline); - context.Function("vkDestroyPipeline")(context.device, objects.pipeline, nullptr); - objects.pipeline = VK_NULL_HANDLE; - context.Function("vkDestroyShaderModule")(context.device, objects.module, nullptr); - objects.module = VK_NULL_HANDLE; - } - return 0; -} - int pipelineCacheFileTests(const Device& device) { const auto& context = device.GetContext(); const auto directory = std::filesystem::temp_directory_path() / ("aps5-pipeline-cache-file-test-" + std::to_string(std::random_device{}())); @@ -11567,7 +11493,8 @@ int pipelineCacheFileTests(const Device& device) { }; const auto cacheFile = [&] { for (const auto& entry : std::filesystem::directory_iterator(directory)) Require(entry.is_directory(), "a pipeline cache file lives in the shader cache root"); - const std::string version = "driver"; + char version[17]; + std::snprintf(version, sizeof(version), "%016llx", static_cast(ShaderRecompiler::ShaderDiskCache::Generated::SourceVersion)); const auto versionDirectory = directory / version; Require(std::filesystem::is_directory(versionDirectory), "the current shader cache version has no directory"); std::vector found; @@ -12613,32 +12540,15 @@ int main(int argc, char** argv) { std::cout << "queued write group tests passed\n"; return 0; } - const bool binaryTest = argc == 3 && std::string_view(argv[1]) == "--pipeline-binary-only"; - if (binaryTest) { -#ifdef _WIN32 - _putenv_s("ANYPS5_SHADER_CACHE_DIR", argv[2]); - _putenv_s("ANYPS5_NO_SHADER_CACHE", "0"); - _putenv_s("APS5_PIPELINE_BINARY_MIN_WORDS", "0"); - _putenv_s("APS5_PIPELINE_BINARY_MIN_MS", "0"); - _putenv_s("APS5_NO_PIPELINE_BINARIES", ""); -#else - setenv("ANYPS5_SHADER_CACHE_DIR", argv[2], 1); - setenv("ANYPS5_NO_SHADER_CACHE", "0", 1); - setenv("APS5_PIPELINE_BINARY_MIN_WORDS", "0", 1); - setenv("APS5_PIPELINE_BINARY_MIN_MS", "0", 1); - unsetenv("APS5_NO_PIPELINE_BINARIES"); -#endif - } std::unique_ptr created; try { - created = std::make_unique(argc == 2 && (std::string_view(argv[1]) == "--import-watch-other-family-only" || std::string_view(argv[1]) == "--deferred-settle-other-family-only" || std::string_view(argv[1]) == "--host-import-lifetime-other-family-only" || std::string_view(argv[1]) == "--settle-write-order-other-family-only"), binaryTest); + created = std::make_unique(argc == 2 && (std::string_view(argv[1]) == "--import-watch-other-family-only" || std::string_view(argv[1]) == "--deferred-settle-other-family-only" || std::string_view(argv[1]) == "--host-import-lifetime-other-family-only" || std::string_view(argv[1]) == "--settle-write-order-other-family-only")); } catch (const std::exception& error) { if (std::getenv("ANYPS5_REQUIRE_VULKAN") != nullptr) throw; std::cout << "skipped, no usable Vulkan device: " << error.what() << '\n'; return 77; } Device& device = *created; - if (binaryTest) return pipelineBinaryTests(device); if (argc == 2 && std::string_view(argv[1]) == "--host-import-lifetime-only") return RunHostImportLifetimeTests(device.GetContext()); if (argc == 2 && std::string_view(argv[1]) == "--host-import-mutation-race-only") return RunHostImportMutationRaceTests(device.GetContext()); if (argc == 2 && (std::string_view(argv[1]) == "--gpu-timing-complete-only" || std::string_view(argv[1]) == "--gpu-timing-incomplete-only")) { diff --git a/core/libs/prx/libSceAgcDriver/tests/cmake/agc_driver_recorder_tests.cmake b/core/libs/prx/libSceAgcDriver/tests/cmake/agc_driver_recorder_tests.cmake index 32dd06fa414e..a238e74936c0 100644 --- a/core/libs/prx/libSceAgcDriver/tests/cmake/agc_driver_recorder_tests.cmake +++ b/core/libs/prx/libSceAgcDriver/tests/cmake/agc_driver_recorder_tests.cmake @@ -53,11 +53,6 @@ if(BUILD_TESTING) set_tests_properties(agc_pipeline_cache_header PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() - add_test(NAME agc_pipeline_binary_vulkan COMMAND ${CMAKE_COMMAND} -DTEST=$ -P ${CMAKE_CURRENT_SOURCE_DIR}/tests/PipelineBinaryRun.cmake) - set_tests_properties(agc_pipeline_binary_vulkan PROPERTIES TIMEOUT 25 SKIP_REGULAR_EXPRESSION "skipped, no usable Vulkan device or the device lacks VK_KHR_pipeline_binary") - if(WIN32) - set_tests_properties(agc_pipeline_binary_vulkan PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") - endif() 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) diff --git a/core/libs/prx/libSceAgcDriver/tests/cmake/agc_pipeline_binary_store_tests.cmake b/core/libs/prx/libSceAgcDriver/tests/cmake/agc_pipeline_binary_store_tests.cmake deleted file mode 100644 index 155b4a21c67a..000000000000 --- a/core/libs/prx/libSceAgcDriver/tests/cmake/agc_pipeline_binary_store_tests.cmake +++ /dev/null @@ -1,14 +0,0 @@ -add_test_executable(agc_pipeline_binary_store_tests EXCLUDE_FROM_ALL tests/PipelineBinaryStore.cpp Graphics/src/PipelineBinaryStore.cpp) -target_include_directories(agc_pipeline_binary_store_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/core/shader/recompiler ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) -target_link_libraries(agc_pipeline_binary_store_tests PRIVATE shader_recompiler_agc_driver libc) -set_target_properties(agc_pipeline_binary_store_tests PROPERTIES CXX_EXTENSIONS OFF) -configure_windows_unwind(agc_pipeline_binary_store_tests) - -if(BUILD_TESTING) - set_target_properties(agc_pipeline_binary_store_tests PROPERTIES EXCLUDE_FROM_ALL OFF) - add_test(NAME agc_pipeline_binary_store COMMAND agc_pipeline_binary_store_tests) - set_tests_properties(agc_pipeline_binary_store PROPERTIES TIMEOUT 25) - if(WIN32) - set_tests_properties(agc_pipeline_binary_store PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") - endif() -endif() diff --git a/core/libs/prx/libSceAgcDriver/tests/cmake/agc_pipeline_cache_allocation_tests.cmake b/core/libs/prx/libSceAgcDriver/tests/cmake/agc_pipeline_cache_allocation_tests.cmake index 68a864e3818c..ef7a969e695e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/cmake/agc_pipeline_cache_allocation_tests.cmake +++ b/core/libs/prx/libSceAgcDriver/tests/cmake/agc_pipeline_cache_allocation_tests.cmake @@ -7,7 +7,7 @@ configure_windows_unwind(agc_pipeline_cache_allocation_tests) if(BUILD_TESTING) set_target_properties(agc_pipeline_cache_allocation_tests PROPERTIES EXCLUDE_FROM_ALL OFF) - foreach(mode startup now busy destructor worker same-size incomplete periodic) + foreach(mode startup now busy destructor worker) add_test(NAME agc_pipeline_cache_allocation_${mode} COMMAND agc_pipeline_cache_allocation_tests ${mode}) set_tests_properties(agc_pipeline_cache_allocation_${mode} PROPERTIES TIMEOUT 25) if(WIN32) diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index e273454bc192..92a2e5d7b0c3 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -137,7 +137,6 @@ flowchart LR - [`Recompiler.cpp`](../../core/shader/recompiler/Recompiler.cpp) runs the stages in this order. With `ANYPS5_ENABLE_SPIRV_TOOLS`, the SPIR-V is also validated and optimized with SPIRV-Tools. - `ShaderRecompiler::Recompile` keeps compiled variants in memory, and `ShaderDiskCache` stores them on disk so later runs reuse them. Before the writable cache, a miss is also looked up in read-only prebuilt packs and loose entry folders under `shader_cache/prebuilt`; pack entries are ordinary encoded cache entries behind a sorted key-hash index, validated by magic, compiler source version and index checksum when mapped, and a miss or rejected entry falls through to the writable cache and the compiler. Cache format 35 also stores the finite stencil-reference set, its replay-safety proof and reserved push-data word in the artifact and binding layouts. Entries from formats 31 to 34 are rejected. - The Vulkan pipelines are journaled as well ([`PipelineJournal.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp)): a graphics pipeline the store builds (`CachedPipeline`) or a compute pipeline a dispatch builds is appended to `pipelines.journal` next to the shader cache entries, as the materialized result of each stage (the specialized SPIR-V, bindings and push constants, through `ShaderDiskCache::EncodeResult`) plus the recipe the pipeline was built from (`GraphicsPipelineRecipe`: the stages' identities, vertex input layout, descriptor set layout keys and the state fields the `Pipeline` constructor reads, `CanonicalPipelineState`). When a `VulkanDevice` is created, the journal is loaded and its pipelines are built on a pool of `hardware_concurrency() / 2` threads (`APS5_PIPELINE_JOURNAL_THREADS`) while the title loads. The load parses the file a record at a time and keeps it open, and a replay thread reads an entry's modules from it when it builds that entry (a record that no longer reads back as loaded rejects the entry), so the replay holds the modules of the entries being built rather than the file, so the first draw or dispatch of a configuration seen on an earlier run finds its pipeline resident. The pipeline stores key on `RecompileResult::Identity`, a hash of the module words, specialization constants, subgroup width and bindings that is the same on every run (`SharedSpirv::ContentHash`, computed once per shared module), rather than on the process-lifetime variant ids, and a graphics pipeline is built outside the store's mutex, so the replay threads and the queue workers build in parallel. A draw that arrives before its replayed entry builds the pipeline itself; the first inserted wins. A replayed entry enters the store at its least recently used end and a full store takes none, so the title's own pipelines are never evicted for a replayed one, and the optimized relink of a replayed pipeline (`OptimizedPipeline::Schedule`) waits for the first draw that binds it, so the relink thread serves the title's pipelines first. A device created without a window starts its replay at its first draw or dispatch rather than at creation, since `Driver::AttachWindow` usually replaces it before the title draws. The journal records a pipeline once (modules and recipes are deduplicated by content), keeps at most `APS5_PIPELINE_JOURNAL_MAX_MIB` (1024), replays the last `APS5_PIPELINE_JOURNAL_ENTRIES` recorded (4096, the pipeline store's largest bound: a replayed pipeline that finds the store full grows its bound from 1024 instead of being dropped), and is disabled with `APS5_NO_PIPELINE_JOURNAL=1` or without a shader cache directory. The file header names the device (`PipelineJournalDeviceFingerprint`, the properties the driver's pipeline cache file is named by), so a journal written for another GPU or driver is started over rather than replayed; appends and reads hold `pipelines.journal.lock` against another process sharing the directory, and a device cancels its replay before it is replaced (`PrepareForReplacement`) or destroyed, so a replaced headless device never inserts entries the window's device then sweeps. `APS5_PROFILE_DRAW=1` prints a `[pipeline-journal]` summary when the replay finishes and at teardown. -- The driver's own pipeline caches outlive source updates: the `vkGetPipelineCacheData` blob is kept in `/driver/` instead of the per-source-version directory (a compatible blob from the old location is imported once; the driver still validates it), and a compute pipeline whose recipe is at least `APS5_PIPELINE_BINARY_MIN_WORDS` (100000) words is stored by `PipelineBinaryStore` with `VK_KHR_pipeline_binary` under `driver/binaries/`, keyed by the full recipe (SPIR-V, specialization, layout, flags, subgroup size), the driver fingerprint and the extension's global key. A matching entry creates the pipeline from the binary; otherwise it is built with capture (a null `pipelineCache`, as `VUID-vkCreateComputePipelines-pNext-09617` requires) and stored when it took at least `APS5_PIPELINE_BINARY_MIN_MS` (1000), or a `.bin.threshold` marker remembers the recipe. A missing, corrupt, rejected or mismatched entry falls back to a normal build through the driver cache. `APS5_NO_PIPELINE_BINARIES` turns the store off, and it stays off with a `VK_LAYER_KHRONOS_validation` or `VK_LAYER_LUNARG_gfxreconstruct` older than Vulkan 1.3.294. - 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). - A thread that needs the GPU mutex, the committer's next item or the end of the committer's queue spins before it blocks (`HandoffSpin.hpp`): `GpuMutexType::lock` retries `try_lock` for the window, the committer polls its item count, and `DrawPipeline::Drain` polls it for twice the window, so a hand-off that completes within the window costs no kernel wake-up. The window is 150 us when the process may run on more than 8 logical processors (its affinity mask, via `CpuTopology`) and 0 otherwise, where the spinning threads would compete with the holder; `APS5_HANDOFF_SPIN_US=` sets it (0 turns it off, at most 10000). Ordering and the lock's semantics are unchanged. diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index f0110fa14f61..8ecfba5ad43b 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -317,7 +317,6 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_NO_PERF_FRONTEND_PAIR` | presence | `core/shader/recompiler/Recompiler.cpp` | | disables the recompiler's frontend-pair reuse; the shader disk cache treats it as output-neutral | | `APS5_NO_PERF_LAYOUT_PAIR` | presence | `core/shader/recompiler/Recompiler.cpp` | | disables the recompiler's layout-pair reuse; the shader disk cache treats it as output-neutral | | `APS5_NO_PIPELINED_INDIRECT` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp` | | drains the draw pipeline at every CPU-path indirect draw instead of queuing it behind the pipeline | -| `APS5_NO_PIPELINE_BINARIES` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/PipelineBinaryStore.cpp`, `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | disables explicit compute pipeline binary capture and restore; keeps the legacy Vulkan pipeline cache | | `APS5_NO_PIPELINE_CACHE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp` | | builds a pipeline per draw instead of reusing cached ones | | `APS5_NO_PIPELINE_JOURNAL` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp` | | keeps the pipeline journal off; 0 leaves it on, and it is also off while the shader disk cache is disabled | | `APS5_NO_PIPELINE_LTO` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp` | | disables background link-time optimization of graphics pipeline libraries | @@ -378,8 +377,6 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_NV_DIAGNOSTICS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | creates the device with VK_NV_device_diagnostics_config and VK_NV_device_diagnostic_checkpoints where supported, for Nsight Aftermath crash dumps | | `APS5_PACKET_EPOCH` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitStatistics.cpp` | | makes queue workers bump the collect epoch before every packet | | `APS5_PIPELINED_DRAWS` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp` | 8 | depth of the queue-0 draw pipeline: 0 records draws inline, 1 selects the default depth of 8, and any other value is the depth capped at 256 | -| `APS5_PIPELINE_BINARY_MIN_MS` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/PipelineBinaryStore.cpp` | 1000 | minimum capture build time in milliseconds for saving a compute binary; 0 saves every successful capture | -| `APS5_PIPELINE_BINARY_MIN_WORDS` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/PipelineBinaryStore.cpp` | 100000 | minimum SPIR-V word count for compute binary lookup and capture; 0 admits every size | | `APS5_PIPELINE_DISPATCH` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp` | | records an eligible dispatch as an item in the queue-0 pipeline instead of running it inline; an empty value or one starting with `0` leaves it off | | `APS5_PIPELINE_DRAIN_FILLS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp` | | runs queue-0 buffer fills through the draw pipeline to keep their order; any value stores them in place instead | | `APS5_PIPELINE_DRAIN_LABELS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp` | | answers a queue-0 wait from the draw pipeline's own pending labels while it is busy; any value drains the pipeline at the wait | diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index fbbeb61e6a27..4f9a9a69f8f0 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -112,8 +112,6 @@ ### Functional -- [Persistent compute pipeline binaries](../../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineBinaryStore.cpp): driver updates and GPUs other than the tested RTX 4080 remain unverified; binary persistence does not fix NVIDIA issue #11106. Only compute is captured; shaders below the word threshold use the legacy cache even when their build is slow. Files above 256 MiB or with more than 64 binaries are not stored. Both driver stores lack disk eviction. The journal still binds materialized SPIR-V to its source version and driver fingerprint; retaining it across driver changes needs device-feature and ABI revalidation. A source update must regenerate current shaders before selecting a binary by content. - - [Written staging window on discrete GPUs](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver) - a written buffer element of a dispatch that is not swept is staged device-local up to 8 MiB on a discrete GPU (`Context::discreteGpu`) and up to 2 MiB otherwise; `APS5_WRITTEN_SHADOW_MAX_KIB` replaces either bound. The 8 MiB figure comes from one title on one NVIDIA Windows system (PPSA09911: a 2.5 and a 3.3 MiB skin-cache output written in place cost 55 ms of GPU time per frame, 1.8 ms of extra staging copies once staged); the point where the two copies of a larger element cost more than its in-place stores, and whether AMD or Linux drivers move it, has not been measured. The window is per element and per use, so a kernel that streams once through an element inside the window pays the copy-in and the copy-back on every dispatch. - [FS table calls on macOS](../../../core/libs/prx/libSceAgcDriver/Execution/include/Driver/DriverMemory.hpp) (libSceAgcDriver) - macOS keeps the console's 0xfe0000000 for itself, so the driver block, and the FS table at 0xfe0040000 inside it, moves to 0xee0000000 there; `sceAgc*StaticBuffer` and `AgcDriverStaticTable_nid_postfix` use the moved address. A shader that builds the table address from two `s_mov_b32` literals still loads from 0xfe0040000: `ReadTableCallees` (`Driver/Shaders/ShaderRegistry.cpp`) reads that address unchanged, and the shader also loads from it at run time, so those dispatches cannot link on macOS. #6160 lists titles that build this address. `ComputeFsTableCalls` is skipped on macOS as it is under `ANYPS5_ENABLE_ASAN`. Closes when: the driver rewrites that pointer in the shader code for both the callee lookup and the code the GPU runs, or the console address can be mapped on macOS. - [Frame capture](../../../core/libs/prx/libSceAgcDriver/Execution/src/FrameRecorder.cpp) (libSceAgcDriver) throws on macOS. Section views, aliases of one direct memory block and file views replay as separate private memory, so a store through one alias is not seen through another. Only write-watched memory records CPU stores during the frames: private memory on Windows, where a store through one alias is recorded only at that alias; registered ranges on Linux, including direct memory unless mapped more than once (libkernel leaves those views unwatched, so no store through any of them is recorded). Title image pages are recorded only when changed and holding a shader header or its arrays. Linux omits no-access pages (Windows records them unreadable) and records file mapping pages past the file's end as unreadable. A capture starts only at a frame boundary where the driver idles within 10 s (five tries), and stops when GPU work in flight at a delta waits on a later CPU store. diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index c7a115ff2a89..90882881ded1 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -454,15 +454,11 @@ HDR output is available on Windows only. Titles see it when the monitor showing ### Shader cache -Recompiled shaders and driver pipeline data are kept in a `shader_cache/` directory beside the output executable. Set `ANYPS5_SHADER_CACHE_DIR` to use another directory, or set `ANYPS5_NO_SHADER_CACHE` to any non-empty value other than `0` to disable both. Shaders are stored in a subdirectory named after the recompiler's source version; subdirectories of other versions unused for 14 days are removed the first time a run looks up a shader. Driver data lives separately in `driver/`, outside that cleanup. Its `vk-pipelines-*.bin` files name the vendor, device, raw driver version and pipeline-cache UUID. When no current file exists, the newest compatible file from an older source-version directory is imported. Updates and rebuilds can therefore reuse driver data when the newly generated SPIR-V and pipeline inputs are unchanged; the shader cache still rejects artifacts from another recompiler version. - -Devices supporting `VK_KHR_pipeline_binary` also save individual compute pipeline binaries in `driver/binaries/`. Pipeline binaries are disabled in favor of the driver pipeline cache when an active Khronos validation or gfxreconstruct layer reports Vulkan below 1.3.294, or active device layers cannot be enumerated reliably. Their content keys cover SPIR-V, specialization entries and bytes, entry point, layout recipe, pipeline and stage flags, subgroup size, the driver fingerprint, and Vulkan's global and pipeline keys. The stored recipe is compared in full before import. Unknown inputs, corrupt files, compatibility changes and driver rejection cause a normal build. By default, shaders with at least 100000 SPIR-V words are built with capture enabled, and their binaries are saved when the build takes at least 1000 ms. `APS5_PIPELINE_BINARY_MIN_WORDS` and `APS5_PIPELINE_BINARY_MIN_MS` change these thresholds; `0` admits every size or duration. A build below the time threshold records that decision for the matching recipe; later builds use the legacy driver cache. Lowering the time threshold retries capture for below-threshold markers. A rejected capture build or a costly build whose binaries cannot be stored records a capture-failure marker; later builds use the legacy driver cache even when the time threshold is lowered. A changed driver fingerprint retries capture. Set `APS5_NO_PIPELINE_BINARIES=1` to use only the legacy driver cache. Split workgroup-base specialization variants have separate binary keys. - -`[pipeline-binary] built` reports the key and build time, `saved` confirms persistence, and `loaded` reports pipeline creation and total reload milliseconds. A rebuild that produces identical pipeline inputs should produce the same key and a `loaded` line on the next run. A driver or device-configuration change can require a new build. Both driver stores have no disk eviction limit. Deleting the directory is safe; it is rebuilt on the next run. +Recompiled shaders and the Vulkan pipeline cache are kept in a `shader_cache/` directory beside the output executable, so later runs skip recompilation. Set `ANYPS5_SHADER_CACHE_DIR` to use another directory, or set `ANYPS5_NO_SHADER_CACHE` to any non-empty value other than `0` to disable both caches. Shaders are stored in a subdirectory named after the recompiler's source version; subdirectories of other versions unused for 14 days are removed the first time a run looks up a shader. The pipeline cache is one `vk-pipelines-*.bin` file per GPU and driver version in the same subdirectory, so it goes with the shaders it was built from. Entries of the current version are never removed and the directory has no size limit, so a title that compiles many shaders can add several GB within minutes. Deleting the directory is safe: it is rebuilt on the next run. ### Pipeline journal -Besides the recompiled shaders, the source-version subdirectory holds `pipelines.journal`: every Vulkan graphics pipeline the pipeline store kept and every compute pipeline a run built, with the specialized shader modules they were built from. The next run of the title builds those pipelines on background threads while it loads, so a draw or dispatch the earlier run already made does not compile its pipeline when it is first reached. The journal still belongs to the recompiler source version, GPU and driver that wrote it: a run with another version starts it over. Compatible driver data in `driver/`, including compute binaries, can serve pipelines recorded or generated by the current run. Two runs sharing the directory take turns on `pipelines.journal.lock`; a run that cannot lock it records nothing. `APS5_NO_PIPELINE_JOURNAL=1` disables recording and replay; `APS5_PIPELINE_JOURNAL_THREADS` sets the replay threads (half the CPU threads by default; `0` records without replaying), `APS5_PIPELINE_JOURNAL_ENTRIES` how many pipelines are replayed (4096, the last recorded) and `APS5_PIPELINE_STORE_ENTRIES` how many graphics pipelines stay resident (by default the store grows from 1024 up to 4096 to hold the replay and the pipelines that keep coming back), and `APS5_PIPELINE_JOURNAL_MAX_MIB` (1024) the file size at which recording stops. Deleting the file loses nothing but the head start. With `APS5_PROFILE_DRAW=1` a `[pipeline-journal]` line reports what was loaded, replayed and recorded. +Besides the recompiled shaders, the shader cache directory holds `pipelines.journal`: every Vulkan graphics pipeline the pipeline store kept and every compute pipeline a run built, with the specialized shader modules they were built from. The next run of the title builds those pipelines on background threads while it loads, so a draw or dispatch the earlier run already made does not compile its pipeline when it is first reached. The journal belongs to the GPU and driver that wrote it, like the driver's pipeline cache file next to it: a run on another GPU or driver version starts it over, and the driver's own pipeline cache decides how fast each pipeline is rebuilt. Two runs sharing the directory take turns on `pipelines.journal.lock`; a run that cannot lock it records nothing. `APS5_NO_PIPELINE_JOURNAL=1` disables recording and replay; `APS5_PIPELINE_JOURNAL_THREADS` sets the replay threads (half the CPU threads by default; `0` records without replaying), `APS5_PIPELINE_JOURNAL_ENTRIES` how many pipelines are replayed (4096, the last recorded) and `APS5_PIPELINE_STORE_ENTRIES` how many graphics pipelines stay resident (by default the store grows from 1024 up to 4096 to hold the replay and the pipelines that keep coming back), and `APS5_PIPELINE_JOURNAL_MAX_MIB` (1024) the file size at which recording stops. Deleting the file loses nothing but the head start. With `APS5_PROFILE_DRAW=1` a `[pipeline-journal]` line reports what was loaded, replayed and recorded. ### Draw pipeline