From 9584bc7b4eed981dd8a75c04b410284afd211a03 Mon Sep 17 00:00:00 2001 From: TibixDev Date: Wed, 7 Oct 2026 16:43:02 +0300 Subject: [PATCH 1/5] refactor(agc): retain immutable resource binding plans Retain descriptor layouts, allocation counts and buffer patch positions per shader variant. Resolve live addresses on each resource build and preserve independent resource lifetimes. Bound plan retention and bypass unknown variant identities. Cover changing addresses, array bindings, push offsets, eviction and plan lifetime. Supply the sampled-image metadata required by the resource-build benchmark. Fedora 44 Release driver, graphics and PM4 tests pass. RX 6600 validation passes; llvmpipe reports an upstream write-after-write warning. The isolated resource-build benchmark does not establish a clear gain. AI-assisted: yes. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 3 + .../Graphics/include/BindingPlan.hpp | 77 +++++++ .../Graphics/include/ShaderResources.hpp | 25 +- .../Graphics/src/BindingPlan.cpp | 174 ++++++++++++++ .../Graphics/src/ShaderResources.cpp | 215 +++++------------- .../prx/libSceAgcDriver/tests/Graphics.cpp | 84 +++++++ .../prx/libSceAgcDriver/tests/Recorder.cpp | 106 ++++++++- 7 files changed, 506 insertions(+), 178 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/Graphics/include/BindingPlan.hpp create mode 100644 core/libs/prx/libSceAgcDriver/Graphics/src/BindingPlan.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 40c7b76296..96b7d72ea7 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -139,6 +139,7 @@ set(agcDriverSources ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/Resources.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/Recorder.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/ShaderInputState.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/BindingPlan.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/ShaderResources.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/ShaderAddressResources.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/GuestBufferMemory.cpp @@ -441,6 +442,7 @@ add_test_executable(agc_driver_graphics_tests EXCLUDE_FROM_ALL Graphics/src/ColorTargetLayout.cpp Graphics/src/ColorTargetTransfer.cpp Graphics/src/Resources.cpp + Graphics/src/BindingPlan.cpp Graphics/src/ShaderResources.cpp Graphics/src/ShaderAddressResources.cpp Graphics/src/GuestBufferMemory.cpp @@ -597,6 +599,7 @@ if(AGC_BUILD_VISUAL_TEST) Graphics/src/Pipeline.cpp Graphics/src/PipelineCache.cpp Graphics/src/Resources.cpp + Graphics/src/BindingPlan.cpp Graphics/src/ShaderResources.cpp Graphics/src/ShaderAddressResources.cpp Graphics/src/GuestBufferMemory.cpp diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/BindingPlan.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/BindingPlan.hpp new file mode 100644 index 0000000000..28d80434b9 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/BindingPlan.hpp @@ -0,0 +1,77 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_BINDINGPLAN_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_BINDINGPLAN_HPP + +#include "prx/libSceAgcDriver/Graphics/include/Shaders.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver::Graphics { + +class BindingPlan { +public: + BindingPlan(const Context& context, std::span shaders); + BindingPlan(const BindingPlan&) = delete; + BindingPlan& operator=(const BindingPlan&) = delete; + + struct Binding { + VkDescriptorSetLayoutBinding layout; + std::size_t shader; + std::size_t source; + ShaderRecompiler::DescriptorRole role; + std::ranges::iota_view allocations; + std::ranges::iota_view imageAllocations; + std::size_t firstSampler = 0; + std::size_t samplerCount = 0; + }; + struct Buffer { + bool written = true; + bool atomic = false; + std::int32_t pushByte = -1; + std::int64_t dataAllocation = -1; + std::uint32_t dataByte = 0; + }; + + std::vector bindings; + std::vector buffers; + std::vector layout; + std::vector layoutKey; + std::vector descriptorSizes; + std::uint32_t sampledImages = 0; + std::uint32_t storageImages = 0; + std::uint32_t samplers = 0; +}; + +class BindingPlanCache { +public: + explicit BindingPlanCache(const Context& context, std::size_t capacity = 1024); + std::shared_ptr Get(std::span shaders); + std::size_t Size() const; + +private: + struct StageKey { + std::uint64_t variant = 0; + ShaderRecompiler::ShaderStage stage{}; + std::uint32_t pushOffset = 0; + auto operator<=>(const StageKey&) const = default; + }; + using Key = std::array; + struct Entry { + std::shared_ptr plan; + std::list::iterator order; + }; + Context context; + std::size_t capacity; + mutable std::mutex mutex; + std::list order; + std::map entries; +}; + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index 29e48af836..c226187c8c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -2,6 +2,7 @@ #define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_SHADERRESOURCES_HPP #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" +#include "prx/libSceAgcDriver/Graphics/include/BindingPlan.hpp" #include "prx/libSceAgcDriver/Graphics/include/BdaResources.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" #include "prx/libSceAgcDriver/Graphics/include/Sampler.hpp" @@ -73,6 +74,7 @@ class DescriptorCache { std::uint64_t pools = 0; }; Stats Counters() const; + std::shared_ptr Plan(std::span shaders) { return plans.Get(shaders); } private: Context context; @@ -83,6 +85,7 @@ class DescriptorCache { std::map, VkDescriptorSetLayout> layouts; std::vector pools; Stats stats; + BindingPlanCache plans; }; class ShaderResources { @@ -149,7 +152,8 @@ class ShaderResources { // a draw for which neither holds (Draw.cpp). bool WritesMemory() const; bool ReadsImage(const StorageTexture* image) const; - const std::vector& LayoutKey() const { return layoutKey; } + const std::vector& LayoutKey() const { return bindingPlan->layoutKey; } + const BindingPlan& Plan() const { return *bindingPlan; } // Debug aid: each bound guest resource with the fraction of sampled bytes that are nonzero. std::string Describe() const; @@ -261,11 +265,7 @@ class ShaderResources { }; void writeDataWords(VkCommandBuffer commands, std::size_t allocation, std::span words) const; - struct Binding { - VkDescriptorSetLayoutBinding layout; - std::vector allocations; - std::vector imageAllocations; - }; + using Binding = BindingPlan::Binding; // A host-imported buffer region the set reads in place, with its import's identity at build time. struct DirectRegion { @@ -292,7 +292,7 @@ class ShaderResources { std::size_t addDataBuffer(std::span words); // Stage A: the layout entry of an image binding (samplers are taken at once, the sampler cache // locks itself); stage B looks the sampled textures and storage images up (resolveImageBinding). - void addImageBinding(const ShaderRecompiler::DescriptorBinding& binding, VkShaderStageFlags flags); + void addImageBinding(const ShaderRecompiler::DescriptorBinding& binding, std::size_t index); // Stage A: one record per planned image element (ImageRecord), in plan order: the decoded // descriptor and its surface size (computed once for the build), the write watch walked so stage // B's collects are memo hits (APS5_NO_PRECOLLECT=1 skips the pass entirely), and for a sampled @@ -326,7 +326,7 @@ class ShaderResources { // Stage B: the record's texture when the fastRevalidate predicate proves it current under the // lock and the cache still holds it; null sends the element to cachedTexture. std::shared_ptr fastTexture(const ImageRecord& record); - void resolveImageBinding(const ShaderRecompiler::DescriptorBinding& binding, Binding& item, std::span> shaderSamplers); + void resolveImageBinding(const ShaderRecompiler::DescriptorBinding& binding, std::span> shaderSamplers); void forgetDeferredInputs(); void release() noexcept; void prepareAddressBindings(std::span shaders, std::span snapshots); @@ -387,7 +387,7 @@ class ShaderResources { // Today's per-element walk (APS5_NO_EPOCH_REVALIDATE=1). bool fastRevalidateEach(); Context context; - std::vector layoutKey; + std::shared_ptr bindingPlan; GuestBufferMemory guestMemory; std::unique_ptr bda; bool usesBda = false; @@ -433,17 +433,12 @@ class ShaderResources { // Build state carried from stage A to stage B: the bindings in plan order, the image bindings // still to look up (index into `bindings`; the DescriptorBinding lives in the compiled shader), // the descriptor counts the set was sized for, and the compute stage of a deferred build. - std::vector bindings; + std::span bindings; struct DeferredImages { const ShaderRecompiler::DescriptorBinding* binding; std::size_t index; - std::size_t firstSampler = 0; - std::size_t samplerCount = 0; }; std::vector deferredImages; - std::uint64_t storageBuffers = 0; - std::uint32_t plannedSampledImages = 0; - std::uint32_t plannedStorageImages = 0; // The compute constructor's shader and captured regions: the caller's objects, valid only until // the build (Complete() for a deferred one) is done, and reset then (forgetDeferredInputs). CompiledShader deferredCompute{}; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/BindingPlan.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/BindingPlan.cpp new file mode 100644 index 0000000000..329bdf9d22 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/BindingPlan.cpp @@ -0,0 +1,174 @@ +#include "prx/libSceAgcDriver/Graphics/include/BindingPlan.hpp" +#include +#include +#include + +namespace AgcDriver::Graphics { + +namespace { + +const char* roleName(ShaderRecompiler::DescriptorRole role) { + switch (role) { + case ShaderRecompiler::DescriptorRole::GuestBuffers: return "GuestBuffers"; + case ShaderRecompiler::DescriptorRole::GuestImages: return "GuestImages"; + case ShaderRecompiler::DescriptorRole::GuestSamplers: return "GuestSamplers"; + case ShaderRecompiler::DescriptorRole::Gds: return "Gds"; + case ShaderRecompiler::DescriptorRole::BdaPagetable: return "BdaPagetable"; + case ShaderRecompiler::DescriptorRole::FaultBuffer: return "FaultBuffer"; + case ShaderRecompiler::DescriptorRole::FlattenedSrt: return "FlattenedSrt"; + case ShaderRecompiler::DescriptorRole::ShaderData: return "ShaderData"; + } + throw std::runtime_error("AGC graphics: unknown descriptor role"); +} + +const char* kindName(ShaderRecompiler::DescriptorKind kind) { + switch (kind) { + case ShaderRecompiler::DescriptorKind::UniformBuffer: return "UniformBuffer"; + case ShaderRecompiler::DescriptorKind::StorageBuffer: return "StorageBuffer"; + case ShaderRecompiler::DescriptorKind::UniformTexelBuffer: return "UniformTexelBuffer"; + case ShaderRecompiler::DescriptorKind::StorageTexelBuffer: return "StorageTexelBuffer"; + case ShaderRecompiler::DescriptorKind::SampledImage: return "SampledImage"; + case ShaderRecompiler::DescriptorKind::StorageImage: return "StorageImage"; + case ShaderRecompiler::DescriptorKind::Sampler: return "Sampler"; + } + throw std::runtime_error("AGC graphics: unknown descriptor kind"); +} + +} + +BindingPlan::BindingPlan(const Context& context, std::span shaders) { + using Role = ShaderRecompiler::DescriptorRole; + using Kind = ShaderRecompiler::DescriptorKind; + Require(!shaders.empty() && context.limits.maxBoundDescriptorSets >= 1, "shader descriptor set exceeds device limits"); + std::set occupied; + for (std::size_t shaderIndex = 0; shaderIndex < shaders.size(); ++shaderIndex) { + const auto& shader = shaders[shaderIndex]; + Require(shader.program != nullptr, "missing compiled shader"); + const auto flags = VulkanStage(shader.stage); + std::uint64_t stageBuffers = 0, stageSampled = 0, stageStorage = 0, stageSamplers = 0; + const auto firstBuffer = buffers.size(); + const auto firstBinding = bindings.size(); + const auto firstSampler = samplers; + std::int64_t shaderData = -1; + for (std::size_t source = 0; source < shader.program->bindings.size(); ++source) { + const auto& binding = shader.program->bindings[source]; + Require(binding.descriptorSet == 0, "unexpected descriptor set: every shader resource must use descriptor set zero"); + Require(occupied.insert(binding.binding).second, "duplicate shader binding"); + Require(binding.count != 0, "empty descriptor binding"); + Binding item{{binding.binding, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, binding.count, flags, nullptr}, shaderIndex, source, binding.role, {}, {}}; + if (binding.role == Role::GuestImages || binding.role == Role::GuestSamplers) { + if (binding.kind == Kind::SampledImage || binding.kind == Kind::StorageImage) Require(stageBuffers + stageSampled + stageStorage + binding.count <= context.limits.maxPerStageResources, "shader descriptors exceed per-stage limits"); + const bool sampled = binding.kind == Kind::SampledImage; + const bool storage = binding.kind == Kind::StorageImage; + const bool sampler = binding.kind == Kind::Sampler; + if (!(sampled || storage || sampler)) Require(false, std::string("unsupported descriptor kind ") + kindName(binding.kind) + " for role " + roleName(binding.role)); + if (storage) Require(binding.role == Role::GuestImages, "storage image binding has a non-image role"); + else Require(((sampled || storage) && binding.role == Role::GuestImages) || (sampler && binding.role == Role::GuestSamplers), "guest image descriptor role disagrees with its kind"); + Require(binding.guestDescriptor.size() % binding.count == 0, "guest image descriptor size is not a multiple of the binding count"); + const auto words = binding.guestDescriptor.size() / binding.count; + if (sampled) { + Require(words == 8, "guest texture descriptor must contain 8 dwords"); + Require(binding.imageShape.has_value(), "guest image binding is missing an image shape"); + stageSampled += binding.count; + Require(stageSampled <= context.limits.maxPerStageDescriptorSampledImages, "shader sampled-image descriptors exceed per-stage limits"); + Require(static_cast(sampledImages) + binding.count <= context.limits.maxDescriptorSetSampledImages, "pipeline sampled-image descriptors exceed device limits"); + item.layout.descriptorType = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE; + item.imageAllocations = std::ranges::iota_view{sampledImages, static_cast(sampledImages) + binding.count}; + sampledImages += binding.count; + } else if (storage) { + Require(words == 8, "guest storage image descriptors must contain 8 dwords each"); + stageStorage += binding.count; + Require(stageStorage <= context.limits.maxPerStageDescriptorStorageImages, "shader storage-image descriptors exceed per-stage limits"); + Require(static_cast(storageImages) + binding.count <= context.limits.maxDescriptorSetStorageImages, "pipeline storage-image descriptors exceed device limits"); + item.layout.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; + item.imageAllocations = std::ranges::iota_view{storageImages, static_cast(storageImages) + binding.count}; + storageImages += binding.count; + } else { + Require(words == 4, "guest sampler descriptor must contain 4 dwords"); + stageSamplers += binding.count; + Require(stageSamplers <= context.limits.maxPerStageDescriptorSamplers, "shader sampler descriptors exceed per-stage limits"); + Require(binding.samplerDepthCompare.size() == binding.count, "guest sampler binding is missing depth comparison metadata"); + Require(binding.samplerUnnormalized.empty() || binding.samplerUnnormalized.size() == binding.count, "guest sampler binding has unnormalized coordinate metadata of another size"); + Require(static_cast(samplers) + binding.count <= context.limits.maxDescriptorSetSamplers, "pipeline sampler descriptors exceed device limits"); + item.layout.descriptorType = VK_DESCRIPTOR_TYPE_SAMPLER; + item.imageAllocations = std::ranges::iota_view{samplers, static_cast(samplers) + binding.count}; + samplers += binding.count; + } + } else { + const bool address = binding.role == Role::BdaPagetable || binding.role == Role::FaultBuffer; + if (!(address || binding.role == Role::GuestBuffers || binding.role == Role::ShaderData || binding.role == Role::FlattenedSrt || binding.role == Role::Gds)) Require(false, std::string("unsupported descriptor role ") + roleName(binding.role)); + if (binding.kind != Kind::StorageBuffer) Require(false, std::string("unsupported descriptor kind ") + kindName(binding.kind) + " for role " + roleName(binding.role) + ": only StorageBuffer is supported"); + Require(!binding.readOnly, "read-only descriptors are unsupported because the recompiler emits no NonWritable decoration"); + stageBuffers += binding.count; + Require(stageBuffers <= context.limits.maxPerStageDescriptorStorageBuffers && stageBuffers <= context.limits.maxPerStageResources, "shader descriptors exceed per-stage limits"); + Require(buffers.size() + static_cast(binding.count) <= context.limits.maxDescriptorSetStorageBuffers, "pipeline descriptors exceed device limits"); + item.allocations = std::ranges::iota_view{buffers.size(), buffers.size() + binding.count}; + if (binding.role == Role::GuestBuffers) { + Require(binding.guestDescriptor.size() == static_cast(binding.count) * 4, "guest buffer descriptor must contain four DWORDs per array element"); + for (std::uint32_t element = 0; element < binding.count; ++element) { + Buffer buffer; + buffer.written = element >= binding.bufferWritten.size() || binding.bufferWritten[element]; + buffer.atomic = element < binding.bufferAtomic.size() && binding.bufferAtomic[element]; + const auto position = static_cast(shader.program->memoryOffsetDword) * 4 + element; + Require(position <= std::numeric_limits::max(), "guest buffer offset exceeds shader data address space"); + if (!shader.program->pushConstants.empty()) { + Require(position < shader.program->pushConstants.size(), "guest buffer offset lies outside the shader's push constants"); + Require(position + shader.pushConstantOffset <= std::numeric_limits::max(), "guest buffer push offset exceeds address space"); + buffer.pushByte = static_cast(position + shader.pushConstantOffset); + } else buffer.dataByte = static_cast(position); + buffers.push_back(buffer); + } + } else { + Require(binding.count == 1, "shader data and flattened SRT descriptors must not be arrays"); + if (binding.role == Role::Gds) Require(binding.guestDescriptor.empty(), "invalid GDS descriptor contract"); + else if (!address) Require(!binding.guestDescriptor.empty(), "empty shader data descriptor"); + if (binding.role == Role::ShaderData) shaderData = static_cast(buffers.size()); + buffers.push_back({}); + } + } + Require(stageBuffers + stageSampled + stageStorage <= context.limits.maxPerStageResources, "shader descriptors exceed per-stage limits"); + layout.push_back(item.layout); + layoutKey.insert(layoutKey.end(), {item.layout.binding, static_cast(item.layout.descriptorType), item.layout.descriptorCount, flags}); + bindings.push_back(item); + } + for (auto index = firstBuffer; index < buffers.size(); ++index) buffers[index].dataAllocation = shaderData; + for (auto index = firstBinding; index < bindings.size(); ++index) { + bindings[index].firstSampler = firstSampler; + bindings[index].samplerCount = samplers - firstSampler; + } + } + if (!buffers.empty()) descriptorSizes.push_back({VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, static_cast(buffers.size())}); + if (sampledImages != 0) descriptorSizes.push_back({VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, sampledImages}); + if (storageImages != 0) descriptorSizes.push_back({VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, storageImages}); + if (samplers != 0) descriptorSizes.push_back({VK_DESCRIPTOR_TYPE_SAMPLER, samplers}); +} + +BindingPlanCache::BindingPlanCache(const Context& context, std::size_t capacity) : context(context), capacity(capacity) { + Require(capacity != 0, "binding plan cache capacity is zero"); +} + +std::shared_ptr BindingPlanCache::Get(std::span shaders) { + if (shaders.size() > Key{}.size() || std::any_of(shaders.begin(), shaders.end(), [](const auto& shader) { return shader.program == nullptr || shader.program->variantId == 0; })) return std::make_shared(context, shaders); + Key key{}; + for (std::size_t i = 0; i < shaders.size(); ++i) key[i] = {shaders[i].program->variantId, shaders[i].stage, shaders[i].pushConstantOffset}; + std::lock_guard lock(mutex); + if (const auto found = entries.find(key); found != entries.end()) { + order.splice(order.begin(), order, found->second.order); + return found->second.plan; + } + auto plan = std::make_shared(context, shaders); + order.push_front(key); + entries.emplace(key, Entry{plan, order.begin()}); + if (entries.size() > capacity) { + entries.erase(order.back()); + order.pop_back(); + } + return plan; +} + +std::size_t BindingPlanCache::Size() const { + std::lock_guard lock(mutex); + return entries.size(); +} + +} diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index b15233be59..101154b3d4 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -25,7 +25,6 @@ #include #include #include -#include #include #include #include "prx/libc/include/GuestAllocations.hpp" @@ -710,19 +709,6 @@ const char* roleName(ShaderRecompiler::DescriptorRole role) { throw std::runtime_error("AGC graphics: unknown descriptor role"); } -const char* kindName(ShaderRecompiler::DescriptorKind kind) { - switch (kind) { - case ShaderRecompiler::DescriptorKind::UniformBuffer: return "UniformBuffer"; - case ShaderRecompiler::DescriptorKind::StorageBuffer: return "StorageBuffer"; - case ShaderRecompiler::DescriptorKind::UniformTexelBuffer: return "UniformTexelBuffer"; - case ShaderRecompiler::DescriptorKind::StorageTexelBuffer: return "StorageTexelBuffer"; - case ShaderRecompiler::DescriptorKind::SampledImage: return "SampledImage"; - case ShaderRecompiler::DescriptorKind::StorageImage: return "StorageImage"; - case ShaderRecompiler::DescriptorKind::Sampler: return "Sampler"; - } - throw std::runtime_error("AGC graphics: unknown descriptor kind"); -} - // Consecutive identical storage descriptors address successive mips of one texture (dynamic-mip // storage writes), so the second and later ones share the first one's image lookup. // APS5_NO_STORAGE_DEDUPE=1 looks each one up (and refreshes the image) separately as before. @@ -894,96 +880,51 @@ void ShaderResources::buildPrepare(std::span shaders, cons if (++profile.builds % 1000 == 0) profile.merge(); } try { - Require(!shaders.empty() && context.limits.maxBoundDescriptorSets >= 1, "shader descriptor set exceeds device limits"); - std::set occupied; - for (const auto& shader : shaders) { - Require(shader.program != nullptr, "missing compiled shader"); - const VkShaderStageFlags flags = VulkanStage(shader.stage); - std::uint64_t stageStorageBuffers = 0; - std::uint64_t stageResources = 0; - std::vector offsetsInData; - std::int64_t shaderData = -1; - const auto firstSampler = samplers.size(); - const auto firstDeferred = deferredImages.size(); - for (const auto& binding : shader.program->bindings) { - Require(binding.descriptorSet == 0, "unexpected descriptor set: every shader resource must use descriptor set zero"); - Require(occupied.insert(binding.binding).second, "duplicate shader binding"); - const bool addressRole = binding.role == ShaderRecompiler::DescriptorRole::BdaPagetable || binding.role == ShaderRecompiler::DescriptorRole::FaultBuffer; - const bool bufferRole = addressRole || binding.role == ShaderRecompiler::DescriptorRole::GuestBuffers || binding.role == ShaderRecompiler::DescriptorRole::ShaderData || binding.role == ShaderRecompiler::DescriptorRole::FlattenedSrt || binding.role == ShaderRecompiler::DescriptorRole::Gds; - const bool imageRole = binding.role == ShaderRecompiler::DescriptorRole::GuestImages || binding.role == ShaderRecompiler::DescriptorRole::GuestSamplers; - if (imageRole) { - if (binding.kind == ShaderRecompiler::DescriptorKind::SampledImage || binding.kind == ShaderRecompiler::DescriptorKind::StorageImage) stageResources += binding.count; - Require(stageResources <= context.limits.maxPerStageResources, "shader descriptors exceed per-stage limits"); - addImageBinding(binding, flags); - continue; - } - if (!bufferRole) Require(false, std::string("unsupported descriptor role ") + roleName(binding.role)); - if (binding.kind != ShaderRecompiler::DescriptorKind::StorageBuffer) Require(false, std::string("unsupported descriptor kind ") + kindName(binding.kind) + " for role " + roleName(binding.role) + ": only StorageBuffer is supported"); - Require(!binding.readOnly, "read-only descriptors are unsupported because the recompiler emits no NonWritable decoration"); - Require(binding.count != 0, "empty descriptor binding"); - stageStorageBuffers += binding.count; - stageResources += binding.count; - storageBuffers += binding.count; - Require(stageStorageBuffers <= context.limits.maxPerStageDescriptorStorageBuffers && stageResources <= context.limits.maxPerStageResources, "shader descriptors exceed per-stage limits"); - Binding item{{binding.binding, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, binding.count, flags, nullptr}, {}}; - if (binding.role == ShaderRecompiler::DescriptorRole::GuestBuffers) { - Require(binding.guestDescriptor.size() == static_cast(binding.count) * 4, "guest buffer descriptor must contain four DWORDs per array element"); - for (std::uint32_t element = 0; element < binding.count; ++element) { - // An element the recompiler did not classify (a producer without the - // vector) counts as written, like imageWritten below. - const bool written = element >= binding.bufferWritten.size() || binding.bufferWritten[element]; - const bool atomic = element < binding.bufferAtomic.size() && binding.bufferAtomic[element]; - const auto index = addGuestBuffer(std::span(binding.guestDescriptor).subspan(static_cast(element) * 4, 4), target, indexAddress, indexBytes, written, atomic); - const auto& push = shader.program->pushConstants; - if (!push.empty()) { - const auto position = shader.program->memoryOffsetDword * 4u + element; - Require(position < push.size(), "guest buffer offset lies outside the shader's push constants"); - allocations[index].pushByte = static_cast(shader.pushConstantOffset + position); - } else { - allocations[index].dataByte = shader.program->memoryOffsetDword * 4u + element; - offsetsInData.push_back(index); - } - item.allocations.push_back(index); - } - } else if (addressRole) { - item.allocations.push_back(allocations.size()); - allocations.push_back({0, 0, false, nullptr, binding.role}); - } else if (binding.role == ShaderRecompiler::DescriptorRole::Gds) { - Require(binding.count == 1 && binding.guestDescriptor.empty(), "invalid GDS descriptor contract"); - const auto gds = Pm4::GdsAddress(); - guestMemory.AddWritable(gds, Pm4::GdsBytes, true); - item.allocations.push_back(allocations.size()); - allocations.push_back({gds, Pm4::GdsBytes, true, nullptr, ShaderRecompiler::DescriptorRole::ShaderData, true}); - } else { - Require(binding.count == 1, "shader data and flattened SRT descriptors must not be arrays"); - Require(!binding.guestDescriptor.empty(), "empty shader data descriptor"); - item.allocations.push_back(addDataBuffer(binding.guestDescriptor)); - if (binding.role == ShaderRecompiler::DescriptorRole::ShaderData) shaderData = static_cast(item.allocations.back()); - } - bindings.push_back(std::move(item)); + bindingPlan = context.descriptorCache != nullptr ? context.descriptorCache->Plan(shaders) : std::make_shared(context, shaders); + bindings = bindingPlan->bindings; + allocations.reserve(bindingPlan->buffers.size()); + textures.reserve(bindingPlan->sampledImages); + storageTextures.reserve(bindingPlan->storageImages); + samplers.reserve(bindingPlan->samplers); + for (std::size_t index = 0; index < bindings.size(); ++index) { + const auto& operation = bindings[index]; + const auto& binding = shaders[operation.shader].program->bindings[operation.source]; + if (operation.layout.descriptorType != VK_DESCRIPTOR_TYPE_STORAGE_BUFFER) { + addImageBinding(binding, index); + continue; } - for (const auto index : offsetsInData) allocations[index].dataAllocation = shaderData; - for (auto index = firstDeferred; index < deferredImages.size(); ++index) { - deferredImages[index].firstSampler = firstSampler; - deferredImages[index].samplerCount = samplers.size() - firstSampler; + if (operation.role == ShaderRecompiler::DescriptorRole::GuestBuffers) { + Require(binding.guestDescriptor.size() == operation.allocations.size() * 4, "guest buffer descriptor must contain four DWORDs per array element"); + for (std::size_t element = 0; element < operation.allocations.size(); ++element) { + const auto& buffer = bindingPlan->buffers[operation.allocations[element]]; + const auto allocated = addGuestBuffer(std::span(binding.guestDescriptor).subspan(element * 4, 4), target, indexAddress, indexBytes, buffer.written, buffer.atomic); + allocations[allocated].pushByte = buffer.pushByte; + allocations[allocated].dataByte = buffer.dataByte; + allocations[allocated].dataAllocation = buffer.dataAllocation; + } + } else if (operation.role == ShaderRecompiler::DescriptorRole::Gds) { + Require(binding.count == 1 && binding.guestDescriptor.empty(), "invalid GDS descriptor contract"); + const auto gds = Pm4::GdsAddress(); + guestMemory.AddWritable(gds, Pm4::GdsBytes, true); + allocations.push_back({gds, Pm4::GdsBytes, true, nullptr, ShaderRecompiler::DescriptorRole::ShaderData, true}); + } else if (operation.role == ShaderRecompiler::DescriptorRole::BdaPagetable || operation.role == ShaderRecompiler::DescriptorRole::FaultBuffer) { + allocations.push_back({0, 0, false, nullptr, operation.role}); + } else { + Require(!binding.guestDescriptor.empty(), "empty shader data descriptor"); + addDataBuffer(binding.guestDescriptor); } } - Require(storageBuffers <= context.limits.maxDescriptorSetStorageBuffers, "pipeline descriptors exceed device limits"); timing.bindingsMs = phase(BuildPhase::Bindings); // For every build, locked ones included: their stage B then takes the fast path too, and the // collects cost the same wherever they run. if (precollectImages()) phase(BuildPhase::Precollect); guestMemory.UploadPrepare(usesBda); timing.uploadMs = phase(BuildPhase::Upload); - std::vector description; - for (const auto& binding : bindings) { - description.push_back(binding.layout); - layoutKey.insert(layoutKey.end(), {binding.layout.binding, static_cast(binding.layout.descriptorType), binding.layout.descriptorCount, binding.layout.stageFlags}); - } + const auto& description = bindingPlan->layout; static const bool noLayoutCache = std::getenv("APS5_NO_LAYOUT_CACHE") != nullptr; static const bool noPoolCache = std::getenv("APS5_NO_POOL_CACHE") != nullptr; if (context.descriptorCache != nullptr && !noLayoutCache) { - _layout = context.descriptorCache->Layout(layoutKey, description); + _layout = context.descriptorCache->Layout(bindingPlan->layoutKey, description); } else { VkDescriptorSetLayoutCreateInfo info{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO}; info.bindingCount = static_cast(description.size()); @@ -992,13 +933,7 @@ void ShaderResources::buildPrepare(std::span shaders, cons ownsLayout = true; } if (!bindings.empty()) { - // The set is sized from the plan: every image element becomes exactly one descriptor - // when stage B looks it up. - std::vector sizes; - if (storageBuffers != 0) sizes.push_back({VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, static_cast(storageBuffers)}); - if (plannedSampledImages != 0) sizes.push_back({VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, plannedSampledImages}); - if (plannedStorageImages != 0) sizes.push_back({VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, plannedStorageImages}); - if (!samplers.empty()) sizes.push_back({VK_DESCRIPTOR_TYPE_SAMPLER, static_cast(samplers.size())}); + const auto& sizes = bindingPlan->descriptorSizes; if (context.descriptorCache != nullptr && !noPoolCache) { const auto allocated = context.descriptorCache->Allocate(_layout, sizes); _set = allocated.set; @@ -1034,7 +969,10 @@ void ShaderResources::buildComplete() { try { // The lookups run in plan order: Revalidate walks the bindings the same way, and consecutive // storage elements of one mip chain share the previous element's image. - for (const auto& deferred : deferredImages) resolveImageBinding(*deferred.binding, bindings[deferred.index], std::span>(samplers).subspan(deferred.firstSampler, deferred.samplerCount)); + for (const auto& deferred : deferredImages) { + const auto& operation = bindings[deferred.index]; + resolveImageBinding(*deferred.binding, std::span>(samplers).subspan(operation.firstSampler, operation.samplerCount)); + } deferredImages.clear(); // The records served their purpose: each holds the cache entry's objects as of stage A, // which would otherwise keep a replaced texture or an evicted storage image (and its device @@ -1042,7 +980,7 @@ void ShaderResources::buildComplete() { imageRecords.clear(); imageRecords.shrink_to_fit(); nextImageRecord = 0; - Require(textures.size() == plannedSampledImages && storageTextures.size() == plannedStorageImages, "image lookups disagree with the descriptor plan"); + Require(textures.size() == bindingPlan->sampledImages && storageTextures.size() == bindingPlan->storageImages, "image lookups disagree with the descriptor plan"); timing.bindingsMs += phase(BuildPhase::Images); guestMemory.UploadFinish(usesBda); timing.uploadMs += phase(BuildPhase::Upload); @@ -1052,16 +990,10 @@ void ShaderResources::buildComplete() { if (_set != VK_NULL_HANDLE) { // One update call for the whole set: the info arrays are sized up front so every write's // pointer into them stays valid until the call. - std::size_t bufferCount = 0; - std::size_t imageCount = 0; - for (const auto& binding : bindings) { - bufferCount += binding.allocations.size(); - imageCount += binding.imageAllocations.size(); - } std::vector buffers; std::vector images; - buffers.reserve(bufferCount); - images.reserve(imageCount); + buffers.reserve(bindingPlan->buffers.size()); + images.reserve(bindingPlan->sampledImages + bindingPlan->storageImages + bindingPlan->samplers); std::vector writes; writes.reserve(bindings.size()); for (const auto& binding : bindings) { @@ -2099,7 +2031,7 @@ ResourceCache& SharedResourceCache() { return *cache; } -DescriptorCache::DescriptorCache(const Context& context) : context(context), destroyLayout(context.Function("vkDestroyDescriptorSetLayout")), destroyPool(context.Function("vkDestroyDescriptorPool")), freeSets(context.Function("vkFreeDescriptorSets")) {} +DescriptorCache::DescriptorCache(const Context& context) : context(context), destroyLayout(context.Function("vkDestroyDescriptorSetLayout")), destroyPool(context.Function("vkDestroyDescriptorPool")), freeSets(context.Function("vkFreeDescriptorSets")), plans(context) {} DescriptorCache::~DescriptorCache() { for (const auto pool : pools) destroyPool(context.device, pool, nullptr); @@ -2363,43 +2295,12 @@ void ShaderResources::PrecollectSurfaces() const { for (const auto& range : describedRanges) GuestMemory::CollectWrites(range.address, static_cast(range.bytes)); } -void ShaderResources::addImageBinding(const ShaderRecompiler::DescriptorBinding& binding, VkShaderStageFlags flags) { - Require(binding.count != 0, "empty descriptor binding"); - if (binding.kind == ShaderRecompiler::DescriptorKind::StorageImage) { - // Storage images are guest textures the shader writes (looked up in stage B). - Require(binding.role == ShaderRecompiler::DescriptorRole::GuestImages, "storage image binding has a non-image role"); - Require(binding.guestDescriptor.size() == static_cast(binding.count) * 8u, "guest storage image descriptors must contain 8 dwords each"); - Require(context.detiler != nullptr, "device texture detiler is unavailable"); - Require(binding.count <= context.limits.maxPerStageDescriptorStorageImages, "shader storage-image descriptors exceed per-stage limits"); - bindings.push_back({{binding.binding, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, binding.count, flags, nullptr}, {}, {}}); - plannedStorageImages += binding.count; - deferredImages.push_back({&binding, bindings.size() - 1}); - return; - } - const bool sampledImage = binding.kind == ShaderRecompiler::DescriptorKind::SampledImage; - const bool samplerKind = binding.kind == ShaderRecompiler::DescriptorKind::Sampler; - if (!(sampledImage || samplerKind)) Require(false, std::string("unsupported descriptor kind ") + kindName(binding.kind) + " for role " + roleName(binding.role)); - Require((sampledImage && binding.role == ShaderRecompiler::DescriptorRole::GuestImages) || (samplerKind && binding.role == ShaderRecompiler::DescriptorRole::GuestSamplers), "guest image descriptor role disagrees with its kind"); - Require(binding.guestDescriptor.size() % binding.count == 0, "guest image descriptor size is not a multiple of the binding count"); - const auto elementWords = binding.guestDescriptor.size() / binding.count; - - Binding item{{binding.binding, sampledImage ? VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE : VK_DESCRIPTOR_TYPE_SAMPLER, binding.count, flags, nullptr}, {}, {}}; - - if (sampledImage) { - Require(elementWords == 8, "guest texture descriptor must contain 8 dwords"); - Require(binding.imageShape.has_value(), "guest image binding is missing an image shape"); - Require(context.detiler != nullptr, "device texture detiler is unavailable"); - Require(context.textureCache != nullptr, "device texture cache is unavailable"); - Require(binding.count <= context.limits.maxPerStageDescriptorSampledImages, "shader sampled-image descriptors exceed per-stage limits"); - plannedSampledImages += binding.count; - Require(plannedSampledImages <= context.limits.maxDescriptorSetSampledImages, "pipeline sampled-image descriptors exceed device limits"); - } else { - Require(elementWords == 4, "guest sampler descriptor must contain 4 dwords"); - Require(binding.count <= context.limits.maxPerStageDescriptorSamplers, "shader sampler descriptors exceed per-stage limits"); - Require(binding.samplerDepthCompare.size() == binding.count, "guest sampler binding is missing depth comparison metadata"); - Require(binding.samplerUnnormalized.empty() || binding.samplerUnnormalized.size() == binding.count, "guest sampler binding has unnormalized coordinate metadata of another size"); - for (std::uint32_t element = 0; element < binding.count; ++element) { - const auto words = std::span(binding.guestDescriptor).subspan(static_cast(element) * elementWords, elementWords); +void ShaderResources::addImageBinding(const ShaderRecompiler::DescriptorBinding& binding, std::size_t index) { + const auto& operation = bindings[index]; + if (operation.layout.descriptorType == VK_DESCRIPTOR_TYPE_SAMPLER) { + Require(binding.guestDescriptor.size() == operation.imageAllocations.size() * 4, "guest sampler descriptor must contain 4 dwords"); + for (std::size_t element = 0; element < operation.imageAllocations.size(); ++element) { + const auto words = std::span(binding.guestDescriptor).subspan(element * 4, 4); const bool compareEnable = binding.samplerDepthCompare.at(element); const bool unnormalized = element < binding.samplerUnnormalized.size() && binding.samplerUnnormalized[element]; static const bool noSamplerCache = std::getenv("APS5_NO_SAMPLER_CACHE") != nullptr; @@ -2410,13 +2311,13 @@ void ShaderResources::addImageBinding(const ShaderRecompiler::DescriptorBinding& resource.compareEnable = compareEnable; samplers.push_back(std::make_shared(context, resource)); } - item.imageAllocations.push_back(samplers.size() - 1); } - Require(samplers.size() <= context.limits.maxDescriptorSetSamplers, "pipeline sampler descriptors exceed device limits"); + return; } - - bindings.push_back(std::move(item)); - if (sampledImage) deferredImages.push_back({&binding, bindings.size() - 1}); + Require(binding.guestDescriptor.size() == operation.imageAllocations.size() * 8, "guest texture descriptor must contain 8 dwords"); + Require(context.detiler != nullptr, "device texture detiler is unavailable"); + if (operation.layout.descriptorType == VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE) Require(context.textureCache != nullptr, "device texture cache is unavailable"); + deferredImages.push_back({&binding, index}); } namespace { @@ -2545,7 +2446,7 @@ std::shared_ptr ShaderResources::fastTexture(const ImageRecord& record) return record.texture; } -void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBinding& binding, Binding& item, std::span> shaderSamplers) { +void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBinding& binding, std::span> shaderSamplers) { auto& counters = TextureCounts(); // The element's stage-A record, when the pass ran (records follow the plan order exactly). const auto nextRecord = [&]() -> const ImageRecord* { return nextImageRecord < imageRecords.size() ? &imageRecords[nextImageRecord++] : nullptr; }; @@ -2575,7 +2476,6 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind textures.push_back(std::move(texture)); textureFirstLayer.push_back(firstLayer); describedRanges.push_back({"texture", resource.baseAddress, guestBytes, resource.width, resource.height, resource.format, static_cast(resource.tileMode), resource.dccAddress}); - item.imageAllocations.push_back(textures.size() - 1); } // The line is due even when every element took the fast path (cachedTexture reports too). reportTextureCounters(); @@ -2607,7 +2507,6 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind storageAtomic.push_back(element < binding.imageAtomic.size() && binding.imageAtomic[element]); storageAtomic64.push_back(element < binding.imageAtomic64.size() && binding.imageAtomic64[element]); describedRanges.push_back({"storage", resource.baseAddress, guestBytes, resource.width, resource.height, resource.format, static_cast(resource.tileMode), resource.dccAddress}); - item.imageAllocations.push_back(storageTextures.size() - 1); } } @@ -2778,12 +2677,8 @@ std::shared_ptr ShaderResources::PrepareDrawBindi } if (selected.empty()) return {}; Require(context.descriptorCache != nullptr, "draw snapshots require a descriptor cache"); - std::map counts; - for (const auto& binding : bindings) counts[binding.layout.descriptorType] += binding.layout.descriptorCount; - std::vector sizes; - for (const auto& [type, count] : counts) sizes.push_back({type, count}); result->cache = context.descriptorCache; - result->allocation = result->cache->Allocate(_layout, sizes); + result->allocation = result->cache->Allocate(_layout, bindingPlan->descriptorSizes); Require(result->allocation.set != VK_NULL_HANDLE, "draw snapshot descriptor allocation failed"); std::vector copies; for (const auto& binding : bindings) { diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 681f13da2f..1de455a9d5 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -1073,6 +1073,10 @@ VKAPI_ATTR VkResult VKAPI_CALL mockAllocateDescriptorSets(VkDevice, const VkDesc return VK_SUCCESS; } +VKAPI_ATTR VkResult VKAPI_CALL mockFreeDescriptorSets(VkDevice, VkDescriptorPool, std::uint32_t, const VkDescriptorSet*) { + return VK_SUCCESS; +} + VKAPI_ATTR void VKAPI_CALL mockUpdateDescriptorSets(VkDevice, std::uint32_t count, const VkWriteDescriptorSet* writes, std::uint32_t copyCount, const VkCopyDescriptorSet*) { Require(copyCount == 0, "descriptor copies are not expected"); for (std::uint32_t i = 0; i < count; ++i) { @@ -1170,6 +1174,7 @@ PFN_vkVoidFunction VKAPI_CALL mockProc(VkDevice, const char* name) { {"vkCreateDescriptorPool", reinterpret_cast(mockCreateDescriptorPool)}, {"vkDestroyDescriptorPool", reinterpret_cast(mockDestroyDescriptorPool)}, {"vkAllocateDescriptorSets", reinterpret_cast(mockAllocateDescriptorSets)}, + {"vkFreeDescriptorSets", reinterpret_cast(mockFreeDescriptorSets)}, {"vkUpdateDescriptorSets", reinterpret_cast(mockUpdateDescriptorSets)}, {"vkCmdBindDescriptorSets", reinterpret_cast(mockCmdBindDescriptorSets)}, {"vkCreatePipelineLayout", reinterpret_cast(mockCreatePipelineLayout)}, @@ -1303,6 +1308,82 @@ void pushConstantTests() { expectFailure([&] { AgcDriver::Graphics::AssemblePushConstants(shaders); }, "missing compiled shader"); } +void bindingPlanTests() { + using namespace AgcDriver::Graphics; + mock = MockVulkan{}; + auto context = mockContext(); + context.limits.maxPerStageDescriptorSampledImages = 16; + context.limits.maxDescriptorSetSampledImages = 16; + context.limits.maxPerStageDescriptorSamplers = 16; + context.limits.maxDescriptorSetSamplers = 16; + BindingPlanCache cache(context, 2); + ShaderRecompiler::RecompileResult vertex; + ShaderRecompiler::RecompileResult fragment; + vertex.variantId = 1; + fragment.variantId = 2; + vertex.bindings.push_back(makeBinding(Role::GuestBuffers, 0, 2, join(vsharp(guestFirst.data(), 16), vsharp(guestSecond.data(), 32)))); + vertex.bindings.front().bufferWritten = {false, true}; + vertex.bindings.front().bufferAtomic = {false, true}; + vertex.bindings.push_back(makeBinding(Role::ShaderData, 5, 1, {7, 8, 9})); + fragment.bindings.push_back(makeBinding(Role::FlattenedSrt, 43, 1, {1, 2})); + std::array shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &fragment, 8}}}; + const auto first = cache.Get(shaders); + Require(first->bindings.size() == 3 && first->buffers.size() == 4, "binding plan lost array elements"); + Require(first->bindings[0].allocations[0] == 0 && first->bindings[0].allocations[1] == 1 && first->bindings[1].allocations.front() == 2, "binding plan allocation ranges overlap"); + Require(first->buffers[0].dataAllocation == 2 && !first->buffers[0].written && first->buffers[1].written && first->buffers[1].atomic, "binding plan lost shader data offsets or access flags"); + vertex.bindings.front().guestDescriptor = join(vsharp(guestThird.data(), 8), vsharp(guestFirst.data(), 16)); + vertex.bindings[1].guestDescriptor = {10, 11, 12}; + Require(cache.Get(shaders) == first, "live addresses or constants replaced the binding plan"); + vertex.variantId = 3; + vertex.pushConstants.resize(8); + shaders.front().pushConstantOffset = 16; + const auto pushed = cache.Get(shaders); + Require(pushed != first && pushed->buffers[0].pushByte == 16 && pushed->buffers[1].pushByte == 17, "push patch positions were reused across incompatible plans"); + shaders.front().pushConstantOffset = 24; + Require(cache.Get(shaders)->buffers[0].pushByte == 24 && cache.Size() == 2, "binding plan cache ignored push offsets or its capacity"); + Require(first->buffers[0].pushByte == -1 && first->bindings[2].layout.binding == 43, "eviction invalidated a retained binding plan"); + vertex.variantId = 0; + const auto uncached = cache.Get(shaders); + vertex.bindings[1].binding = 6; + Require(cache.Get(shaders) != uncached && cache.Get(shaders)->bindings[1].layout.binding == 6, "unknown shader variants reused a stale layout"); + ShaderRecompiler::RecompileResult images; + auto sampled = makeBinding(Role::GuestImages, 1, 2, std::vector(16)); + sampled.kind = Kind::SampledImage; + sampled.imageShape = ShaderRecompiler::DescriptorImageShape::Image2D; + images.bindings.push_back(sampled); + auto sampler = makeBinding(Role::GuestSamplers, 4, 1, std::vector(4)); + sampler.kind = Kind::Sampler; + sampler.samplerDepthCompare = {false}; + images.bindings.push_back(sampler); + sampled.binding = 7; + images.bindings.push_back(sampled); + const CompiledShader imageShader{ShaderRecompiler::ShaderStage::Fragment, &images, 0}; + const auto imagePlan = cache.Get(std::span(&imageShader, 1)); + Require(imagePlan->sampledImages == 4 && imagePlan->samplers == 1 && imagePlan->bindings[2].imageAllocations.front() == 2 && imagePlan->bindings[1].imageAllocations.front() == 0, "image and sampler allocation ranges were conflated"); + context.limits.maxPerStageDescriptorSampledImages = 3; + expectFailure([&] { BindingPlan plan(context, std::span(&imageShader, 1)); }, "per-stage limits"); + { + DescriptorCache descriptors(context); + context.descriptorCache = &descriptors; + ShaderRecompiler::RecompileResult program; + program.variantId = 20; + program.pushConstants.resize(4); + program.bindings.push_back(makeBinding(Role::GuestBuffers, 0, 1, vsharp(guestFirst.data(), 16))); + program.bindings.front().bufferWritten = {false}; + const CompiledShader shader{ShaderRecompiler::ShaderStage::Compute, &program, 0}; + ShaderResources before(context, shader); + const auto beforeWrite = findWrite(0); + program.bindings.front().guestDescriptor = vsharp(guestSecond.data(), 32); + mock.writes.clear(); + ShaderResources after(context, shader); + const auto afterWrite = findWrite(0); + Require(&before.Plan() == &after.Plan(), "resource builds did not share invariant binding structure"); + Require(beforeWrite.buffers.front().range == 16 && afterWrite.buffers.front().range == 32, "resource plan froze a runtime buffer range"); + Require(sameBytes(bufferBytes(beforeWrite.buffers.front().buffer), guestFirst.data(), 16) && sameBytes(bufferBytes(afterWrite.buffers.front().buffer), guestSecond.data(), 32), "later materialization changed an earlier draw's data"); + } + Require(mock.live == 0, "binding plan resources leaked Vulkan objects"); +} + void resourceTests() { mock = MockVulkan{}; const auto context = mockContext(); @@ -1439,6 +1520,8 @@ void resourceTests() { mock = MockVulkan{}; auto limited = mockContext(); limited.limits.maxPerStageResources = limit; + limited.limits.maxPerStageDescriptorStorageImages = 1; + limited.limits.maxDescriptorSetStorageImages = 1; expectFailure([&] { AgcDriver::Graphics::ShaderResources resources(limited, vertex, fragment, state.color, 0, 0); }, reason); Require(mock.live == 0, "failed shader resources leaked Vulkan objects"); }; @@ -2276,6 +2359,7 @@ int main() { PixelInputLayoutTests(); InitialContextTests(); pushConstantTests(); + bindingPlanTests(); resourceTests(); misalignedShaderDataTests(); debugBranchTests(); diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 61d55d2918..b8c01e9b7b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -3,9 +3,11 @@ #include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" #include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" +#include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" #include "prx/libSceAgcDriver/Graphics/include/Shaders.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureCache.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" #include "prx/libSceAgcDriver/Graphics/include/UnitShadow.hpp" #include "prx/libSceAgcDriver/Execution/include/BdaFeatures.hpp" @@ -188,6 +190,7 @@ class Device { context.formatProperties = function("vkGetPhysicalDeviceFormatProperties"); context.Function("vkGetDeviceQueue")(context.device, family, 0, &context.queue); VkCommandPoolCreateInfo pool{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO}; + pool.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; pool.queueFamilyIndex = family; Check(context.Function("vkCreateCommandPool")(context.device, &pool, nullptr, &context.pool), "vkCreateCommandPool"); } catch (...) { @@ -1137,6 +1140,86 @@ void misalignedSnapshotTests(const Device& device, Recorder& recorder) { recorder.Activate(); } +void resourceBuildBenchmark(const Device& device) { + std::unique_lock gpu(GpuMutex()); + auto context = device.GetContext(); + context.hostImportAlignment = 0; + context.dmaBufImport = false; + DeviceFunctions functions; + FillDeviceFunctions(context, functions); + context.functions = &functions; + DescriptorCache cache(context); + context.descriptorCache = &cache; + TextureDetiler detiler(context); + context.detiler = &detiler; + TextureCache textureCache(context); + context.textureCache = &textureCache; + context.bufferPool = std::make_shared(context); + Recorder recorder(context); + recorder.Activate(); + constexpr std::size_t bytes = 16 * 16384; + void* memory = AllocateWatched(bytes, 65536); + Require(memory != nullptr, "resource benchmark requires watched memory"); + struct Cleanup { + Context& context; + Recorder& recorder; + void* memory; + ~Cleanup() { recorder.Sync(); ClearCachedTextures(context.device); ReleaseWatched(memory, bytes); } + } cleanup{context, recorder, memory}; + std::memset(memory, 0x71, bytes); + const auto address = reinterpret_cast(memory); + for (const std::uint32_t imageCount : {0u, 8u}) { + ShaderRecompiler::RecompileResult program; + program.variantId = 1000000 + imageCount; + program.pushConstants.resize(16); + for (std::uint32_t i = 0; i < 8; ++i) { + ShaderRecompiler::DescriptorBinding binding{}; + binding.kind = ShaderRecompiler::DescriptorKind::StorageBuffer; + binding.role = ShaderRecompiler::DescriptorRole::GuestBuffers; + binding.binding = i; + binding.count = 1; + binding.bufferWritten = {false}; + const auto base = address + i * 256; + binding.guestDescriptor = {static_cast(base), static_cast(base >> 32), 256, 0x31016fac}; + program.bindings.push_back(std::move(binding)); + } + for (std::uint32_t i = 0; i < imageCount; ++i) { + ShaderRecompiler::DescriptorBinding binding{}; + binding.kind = ShaderRecompiler::DescriptorKind::SampledImage; + binding.role = ShaderRecompiler::DescriptorRole::GuestImages; + binding.binding = 8 + i; + binding.count = 1; + binding.imageShape = ShaderRecompiler::DescriptorImageShape::Image2D; + binding.imageSamplers = {0}; + const auto base = address + (i + 1) * 16384; + binding.guestDescriptor = {static_cast(base >> 8), static_cast(base >> 40) | (56u << 20) | (3u << 30), 15u | (63u << 14), 0x90000fac, 0, 0, 0, 0}; + program.bindings.push_back(std::move(binding)); + } + const CompiledShader shader{ShaderRecompiler::ShaderStage::Fragment, &program, 0}; + ColorTarget target{}; + std::array times{}; + std::uint64_t checksum = 0; + for (std::size_t pass = 0; pass <= times.size(); ++pass) { + const auto started = std::chrono::steady_clock::now(); + for (unsigned batch = 0; batch < 32; ++batch) { + for (unsigned draw = 0; draw < 32; ++draw) { + const auto base = address + ((batch * 32 + draw) % 8) * 256; + program.bindings.front().guestDescriptor[0] = static_cast(base); + program.bindings.front().guestDescriptor[1] = static_cast(base >> 32); + ShaderResources resources(context, std::span(&shader, 1), target, 0, 0); + checksum += resources.LayoutKey().size(); + } + recorder.Sync(); + gpu.unlock(); + gpu.lock(); + } + if (pass != 0) times[pass - 1] = std::chrono::duration(std::chrono::steady_clock::now() - started).count() / 1024; + } + std::sort(times.begin(), times.end()); + std::cout << "Resource build (" << imageCount << " textures): median " << times[4] << " us, p95 " << times[8] << " us, minimum " << times[0] << " us, checksum " << checksum << '\n'; + } +} + void drawSnapshotReuseTests(const Device& device, Recorder& recorder) { const auto& context = device.GetContext(); constexpr std::size_t bytes = 65536; @@ -1198,12 +1281,17 @@ void drawSnapshotReuseTests(const Device& device, Recorder& recorder) { Require(contents.size() == elementBytes && std::all_of(contents.begin(), contents.end(), [&](std::byte value) { return value == expected; }), "a draw snapshot does not hold the guest bytes of its draw"); return buffer; }; + const bool watched = AgcDriver::GuestMemory::Watched(element, elementBytes); const auto first = snapshot(std::byte{0x11}); - Require(snapshot(std::byte{0x11}) == first, "an unchanged draw input was copied again"); + const auto repeated = snapshot(std::byte{0x11}); + Require(!watched || repeated == first, "an unchanged watched draw input was copied again"); + Require(watched || repeated != first, "an unwatched imported input reused an unvalidated snapshot"); std::memset(reinterpret_cast(element), 0x22, elementBytes); const auto afterCpu = snapshot(std::byte{0x22}); Require(afterCpu != first, "a draw snapshot outlived a CPU store to its range"); - Require(snapshot(std::byte{0x22}) == afterCpu, "the recopied draw input was not kept"); + const auto repeatedCpu = snapshot(std::byte{0x22}); + Require(!watched || repeatedCpu == afterCpu, "the recopied watched draw input was not kept"); + Require(watched || repeatedCpu != afterCpu, "an unwatched imported input was kept after a CPU store"); std::memset(reinterpret_cast(element), 0x33, elementBytes); AgcDriver::GuestMemory::MarkWritten(element, 4); const auto afterStore = snapshot(std::byte{0x33}); @@ -2812,13 +2900,25 @@ void keysFillTests(const Device& device, Recorder& recorder) { recorder.Sync(); } -int main() { +int main(int argc, char** argv) { try { Device device; + if (argc == 2 && std::string_view(argv[1]) == "--benchmark-resource-build") { + resourceBuildBenchmark(device); + return 0; + } std::lock_guard gpu(GpuMutex()); std::cout << "host imports " << (PrepareImportWatch(device.GetContext()) == ImportWatch::Unwatch ? "are compared" : "stay watched") << '\n'; Recorder recorder(device.GetContext()); recorder.Activate(); + if (argc == 2 && std::string_view(argv[1]) == "--binding-plan-only") { + resourceReadTests(device, recorder); + drawSnapshotReuseTests(device, recorder); + drawInputReuseTests(device, recorder); + dataRefreshTests(device, recorder); + std::cout << "Binding plan resources, snapshots, and live input tests passed\n"; + return 0; + } readTrackingTests(device, recorder); writeSettledTests(device, recorder); completionCountTests(device, recorder); From 60e98883f6bbc92212e0b8b6f53ad81fce5ae01f Mon Sep 17 00:00:00 2001 From: TibixDev Date: Wed, 7 Oct 2026 18:44:56 +0300 Subject: [PATCH 2/5] perf(shader): retain immutable descriptor access metadata Prepare binding reflection with each compiled variant, share its access flags across materialized results, and populate only live descriptor and push data. Persist the reflection in versioned shader-cache entries. Add ordering, filtering, lifetime, and cache round-trip coverage. Written with AI assistance. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 7 +- .../src/Driver/Memory/WrittenBuffers.cpp | 4 +- .../Graphics/src/BindingPlan.cpp | 6 +- .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 2 +- .../Graphics/src/ShaderResources.cpp | 36 ++-- .../tests/BindingMaterialization.cpp | 158 ++++++++++++++++++ .../prx/libSceAgcDriver/tests/Graphics.cpp | 7 +- .../prx/libSceAgcDriver/tests/Recorder.cpp | 8 +- .../libSceAgcDriver/tests/ShaderDiskCache.cpp | 19 ++- .../libSceAgcDriver/tests/ShaderMemory.cpp | 2 +- .../tests/execution/EmulatedColorCompare.cpp | 2 +- .../tests/execution/ImageSampleReduction.cpp | 2 +- .../libSceAgcDriver/tools/ShaderReplay.cpp | 2 +- .../Optimization/DescriptorBindingBuilder.hpp | 6 + .../src/DescriptorBindingBuilder.cpp | 121 +++++++++----- core/shader/recompiler/Recompiler.cpp | 23 +-- core/shader/recompiler/Recompiler.hpp | 30 ++-- core/shader/recompiler/ShaderDiskCache.cpp | 37 ++-- core/shader/recompiler/ShaderDiskCache.hpp | 2 +- 19 files changed, 344 insertions(+), 130 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/BindingMaterialization.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 96b7d72ea7..4c43578218 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -251,6 +251,11 @@ target_link_libraries(agc_shader_memory_tests PRIVATE shader_recompiler_agc_driv set_target_properties(agc_shader_memory_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_shader_memory_tests) +add_test_executable(agc_binding_materialization_tests EXCLUDE_FROM_ALL tests/BindingMaterialization.cpp) +target_link_libraries(agc_binding_materialization_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_binding_materialization_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_binding_materialization_tests) + add_test_executable(agc_write_tracking_tests EXCLUDE_FROM_ALL tests/WriteTracking.cpp Execution/src/GuestMemory.cpp @@ -539,7 +544,7 @@ foreach(agcExecutionTestSource IN LISTS agcExecutionTestSources) endforeach() if(BUILD_TESTING) - foreach(agcTest IN ITEMS agc_driver_tests agc_shader_memory_tests agc_structurizer_tests agc_spirv_module_tests agc_dpp_write_tests agc_masked_select_tests agc_vop1_control_decode_tests agc_validate_program_tests agc_shader_disk_cache_tests agc_shader_control_flow_tests agc_driver_flip_tests agc_driver_pm4_tests agc_driver_graphics_tests agc_driver_bda_device_tests ${agcExecutionTests}) + foreach(agcTest IN ITEMS agc_driver_tests agc_shader_memory_tests agc_structurizer_tests agc_spirv_module_tests agc_dpp_write_tests agc_masked_select_tests agc_vop1_control_decode_tests agc_validate_program_tests agc_shader_disk_cache_tests agc_shader_control_flow_tests agc_driver_flip_tests agc_driver_pm4_tests agc_driver_graphics_tests agc_driver_bda_device_tests ${agcExecutionTests} agc_binding_materialization_tests) string(REGEX REPLACE "_tests$" "" agcTestName "${agcTest}") set_target_properties(${agcTest} PROPERTIES EXCLUDE_FROM_ALL OFF) add_test(NAME ${agcTestName} COMMAND ${agcTest}) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WrittenBuffers.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WrittenBuffers.cpp index b9b65463ff..4178b7404e 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WrittenBuffers.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WrittenBuffers.cpp @@ -8,14 +8,14 @@ void Driver::forEachWrittenBuffer(const ShaderRecompiler::RecompileResult& compi for (const auto& binding : compiled.bindings) { if (binding.role != ShaderRecompiler::DescriptorRole::GuestBuffers) continue; for (std::uint32_t element = 0; element < binding.count; ++element) { - const bool written = element >= binding.bufferWritten.size() || binding.bufferWritten[element]; + const bool written = element >= binding.Usage().bufferWritten.size() || binding.Usage().bufferWritten[element]; if (!written || binding.guestDescriptor.size() < (static_cast(element) + 1) * 4) continue; const auto words = std::span(binding.guestDescriptor).subspan(static_cast(element) * 4, 4); const ShaderRecompiler::ShaderBufferResource descriptor{{words[0], words[1], words[2], words[3]}}; const auto base = descriptor.Base48(); const auto size = descriptor.GetSize(); if (base == 0 || size == 0) continue; - visit(element, base, base + size, element < binding.bufferAtomic.size() && binding.bufferAtomic[element]); + visit(element, base, base + size, element < binding.Usage().bufferAtomic.size() && binding.Usage().bufferAtomic[element]); } } } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/BindingPlan.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/BindingPlan.cpp index 329bdf9d22..1fd63a4493 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/BindingPlan.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/BindingPlan.cpp @@ -87,7 +87,7 @@ BindingPlan::BindingPlan(const Context& context, std::span Require(words == 4, "guest sampler descriptor must contain 4 dwords"); stageSamplers += binding.count; Require(stageSamplers <= context.limits.maxPerStageDescriptorSamplers, "shader sampler descriptors exceed per-stage limits"); - Require(binding.samplerDepthCompare.size() == binding.count, "guest sampler binding is missing depth comparison metadata"); + Require(binding.Usage().samplerDepthCompare.size() == binding.count, "guest sampler binding is missing depth comparison metadata"); Require(binding.samplerUnnormalized.empty() || binding.samplerUnnormalized.size() == binding.count, "guest sampler binding has unnormalized coordinate metadata of another size"); Require(static_cast(samplers) + binding.count <= context.limits.maxDescriptorSetSamplers, "pipeline sampler descriptors exceed device limits"); item.layout.descriptorType = VK_DESCRIPTOR_TYPE_SAMPLER; @@ -107,8 +107,8 @@ BindingPlan::BindingPlan(const Context& context, std::span Require(binding.guestDescriptor.size() == static_cast(binding.count) * 4, "guest buffer descriptor must contain four DWORDs per array element"); for (std::uint32_t element = 0; element < binding.count; ++element) { Buffer buffer; - buffer.written = element >= binding.bufferWritten.size() || binding.bufferWritten[element]; - buffer.atomic = element < binding.bufferAtomic.size() && binding.bufferAtomic[element]; + buffer.written = element >= binding.Usage().bufferWritten.size() || binding.Usage().bufferWritten[element]; + buffer.atomic = element < binding.Usage().bufferAtomic.size() && binding.Usage().bufferAtomic[element]; const auto position = static_cast(shader.program->memoryOffsetDword) * 4 + element; Require(position <= std::numeric_limits::max(), "guest buffer offset exceeds shader data address space"); if (!shader.program->pushConstants.empty()) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 29a48ebe9d..642b50cb45 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -612,7 +612,7 @@ void CheckBufferAliases(std::span shaders, const ColorTarg const auto size = descriptor.GetSize(); if (size == 0 || address == 0) continue; const auto element = offset / 4; - const bool written = element >= binding.bufferWritten.size() || binding.bufferWritten[element]; + const bool written = element >= binding.Usage().bufferWritten.size() || binding.Usage().bufferWritten[element]; Require(!overlap(address, size, target.address, target.bytes), "shader buffer aliases the render target"); Require(!written || !overlap(address, size, indexAddress, indexBytes), "writable shader buffer aliases the index buffer"); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 101154b3d4..ae362f731a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -1147,7 +1147,7 @@ std::vector ShaderResources::ContentKey(const CompiledShader& sha if (movableBuffers && binding.role == ShaderRecompiler::DescriptorRole::GuestBuffers && binding.guestDescriptor.size() == static_cast(binding.count) * 4u) { for (std::uint32_t element = 0; element < binding.count; ++element) { const auto* words = binding.guestDescriptor.data() + static_cast(element) * 4u; - const bool written = element >= binding.bufferWritten.size() || binding.bufferWritten[element]; + const bool written = element >= binding.Usage().bufferWritten.size() || binding.Usage().bufferWritten[element]; const bool empty = words[2] == 0 || (words[0] == 0 && (words[1] & 0xffffu) == 0); if (written || empty) key.insert(key.end(), words, words + 4); else key.insert(key.end(), {0u, words[1] & 0xffff0000u, 0u, words[3]}); @@ -1155,17 +1155,17 @@ std::vector ShaderResources::ContentKey(const CompiledShader& sha } else if ((dataWords && !movableBuffers) || !DataRole(binding.role)) { key.insert(key.end(), binding.guestDescriptor.begin(), binding.guestDescriptor.end()); } - packBits(binding.imageWritten); - packBits(binding.samplerDepthCompare); - packBits(binding.imageDepthCompare); - packBits(binding.imageAtomic); + packBits(binding.Usage().imageWritten); + packBits(binding.Usage().samplerDepthCompare); + packBits(binding.Usage().imageDepthCompare); + packBits(binding.Usage().imageAtomic); packBits(binding.samplerUnnormalized); packBits(binding.imageUnnormalized); // Read-only elements are bound without a write set: an object built for one written set // must not serve a build with another (the variant implies it, this makes it explicit). - packBits(binding.bufferWritten); - key.push_back(static_cast(binding.imageSamplers.size())); - key.insert(key.end(), binding.imageSamplers.begin(), binding.imageSamplers.end()); + packBits(binding.Usage().bufferWritten); + key.push_back(static_cast(binding.Usage().imageSamplers.size())); + key.insert(key.end(), binding.Usage().imageSamplers.begin(), binding.Usage().imageSamplers.end()); } return key; } @@ -1658,7 +1658,7 @@ bool ShaderResources::Revalidate(std::span shaders, ProofR const auto words = std::span(binding.guestDescriptor).subspan(static_cast(element) * elementWords, elementWords); const auto resource = DecodeTextureResource(words); const VkComponentMapping components = ViewComponents(resource); - if (textureIndex >= textures.size() || cachedTexture(context, words, resource, components, 0, !binding.imageDepthCompare.empty() && binding.imageDepthCompare.at(element)) != textures[textureIndex]) return false; + if (textureIndex >= textures.size() || cachedTexture(context, words, resource, components, 0, !binding.Usage().imageDepthCompare.empty() && binding.Usage().imageDepthCompare.at(element)) != textures[textureIndex]) return false; ++textureIndex; } } else if (binding.kind == ShaderRecompiler::DescriptorKind::StorageImage) { @@ -2301,7 +2301,7 @@ void ShaderResources::addImageBinding(const ShaderRecompiler::DescriptorBinding& Require(binding.guestDescriptor.size() == operation.imageAllocations.size() * 4, "guest sampler descriptor must contain 4 dwords"); for (std::size_t element = 0; element < operation.imageAllocations.size(); ++element) { const auto words = std::span(binding.guestDescriptor).subspan(element * 4, 4); - const bool compareEnable = binding.samplerDepthCompare.at(element); + const bool compareEnable = binding.Usage().samplerDepthCompare.at(element); const bool unnormalized = element < binding.samplerUnnormalized.size() && binding.samplerUnnormalized[element]; static const bool noSamplerCache = std::getenv("APS5_NO_SAMPLER_CACHE") != nullptr; if (context.samplerCache != nullptr && !noSamplerCache) { @@ -2368,7 +2368,7 @@ bool ShaderResources::precollectImages() { record.guestBytes = DescribeSurface(record.resource).guestBytes; record.generation = GuestMemory::CollectWrites(record.resource.baseAddress, static_cast(record.guestBytes)); record.decoded = true; - if (record.sampled && !noRecords && words.size() == 8 && (binding.imageDepthCompare.empty() || !binding.imageDepthCompare.at(element))) { + if (record.sampled && !noRecords && words.size() == 8 && (binding.Usage().imageDepthCompare.empty() || !binding.Usage().imageDepthCompare.at(element))) { std::copy(words.begin(), words.end(), record.words.begin()); record.components = ViewComponents(record.resource); record.keys = TextureClearKeys(record.resource, record.guestBytes); @@ -2451,7 +2451,7 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind // The element's stage-A record, when the pass ran (records follow the plan order exactly). const auto nextRecord = [&]() -> const ImageRecord* { return nextImageRecord < imageRecords.size() ? &imageRecords[nextImageRecord++] : nullptr; }; if (binding.kind == ShaderRecompiler::DescriptorKind::SampledImage) { - Require(binding.imageSamplers.size() == binding.count, "guest sampled image binding is missing its image-sampler pairs"); + Require(binding.Usage().imageSamplers.size() == binding.count, "guest sampled image binding is missing its image-sampler pairs"); const auto elementWords = binding.guestDescriptor.size() / binding.count; for (std::uint32_t element = 0; element < binding.count; ++element) { const auto words = std::span(binding.guestDescriptor).subspan(static_cast(element) * elementWords, elementWords); @@ -2466,13 +2466,13 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind texture = fastTexture(*record); (texture != nullptr ? counters.fastHits : counters.fastMisses).fetch_add(1, std::memory_order_relaxed); } - if (texture == nullptr) texture = cachedTexture(context, words, resource, components, guestBytes, !binding.imageDepthCompare.empty() && binding.imageDepthCompare.at(element)); + if (texture == nullptr) texture = cachedTexture(context, words, resource, components, guestBytes, !binding.Usage().imageDepthCompare.empty() && binding.Usage().imageDepthCompare.at(element)); if (element < binding.imageUnnormalized.size() && binding.imageUnnormalized[element]) { const auto range = texture->SampledViewRange(firstLayer); const bool singleLevel = range.levels == 1u && range.layers == 1u && resource.baseLevel == 0u && EffectiveMinLod(resource) == 0.0f; if (!singleLevel || (range.type != VK_IMAGE_VIEW_TYPE_1D && range.type != VK_IMAGE_VIEW_TYPE_2D)) throw std::runtime_error("AGC graphics: guest texture sampled with unnormalized coordinates is not a single-level, single-layer 1D or 2D view starting at mip 0, which is not implemented (base level " + std::to_string(resource.baseLevel) + ", levels " + std::to_string(range.levels) + ", layers " + std::to_string(range.layers) + ", view type " + std::to_string(static_cast(range.type)) + ")"); } - RequireFilterMinmax(context, texture->ViewFormat(), binding.imageSamplers[element], shaderSamplers); + RequireFilterMinmax(context, texture->ViewFormat(), binding.Usage().imageSamplers[element], shaderSamplers); textures.push_back(std::move(texture)); textureFirstLayer.push_back(firstLayer); describedRanges.push_back({"texture", resource.baseAddress, guestBytes, resource.width, resource.height, resource.format, static_cast(resource.tileMode), resource.dccAddress}); @@ -2503,9 +2503,9 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind storageKeys.push_back(resource.dccAddress); storageFirstLayer.push_back(firstLayer); // Images the shader only reads have nothing to store back. - storageWritten.push_back(element >= binding.imageWritten.size() || binding.imageWritten[element]); - storageAtomic.push_back(element < binding.imageAtomic.size() && binding.imageAtomic[element]); - storageAtomic64.push_back(element < binding.imageAtomic64.size() && binding.imageAtomic64[element]); + storageWritten.push_back(element >= binding.Usage().imageWritten.size() || binding.Usage().imageWritten[element]); + storageAtomic.push_back(element < binding.Usage().imageAtomic.size() && binding.Usage().imageAtomic[element]); + storageAtomic64.push_back(element < binding.Usage().imageAtomic64.size() && binding.Usage().imageAtomic64[element]); describedRanges.push_back({"storage", resource.baseAddress, guestBytes, resource.width, resource.height, resource.format, static_cast(resource.tileMode), resource.dccAddress}); } } @@ -2620,7 +2620,7 @@ std::optional> ShaderResources::MovedR return std::nullopt; } if (item.address == address && item.size == size) continue; - const bool written = element >= binding.bufferWritten.size() || binding.bufferWritten[element]; + const bool written = element >= binding.Usage().bufferWritten.size() || binding.Usage().bufferWritten[element]; if (written || empty || item.written || size > context.limits.maxStorageBufferRange) return std::nullopt; const auto begin = address - item.adjustment; const auto bytes = static_cast(size) + item.adjustment; diff --git a/core/libs/prx/libSceAgcDriver/tests/BindingMaterialization.cpp b/core/libs/prx/libSceAgcDriver/tests/BindingMaterialization.cpp new file mode 100644 index 0000000000..dd020e1f8b --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/BindingMaterialization.cpp @@ -0,0 +1,158 @@ +#include "Optimization/DescriptorBindingBuilder.hpp" +#include "ShaderDiskCache.hpp" +#include +#include +#include +#include + +namespace { +using namespace ShaderRecompiler; +void require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} +template void rejects(TAction&& function) { + bool failed = false; + try { function(); } catch (const std::exception&) { failed = true; } + require(failed, "invalid binding input was accepted"); +} +struct Fixture { + ShaderInfo info; + ResourceSnapshot snapshot; + BindingAllocationResult plan; + DescriptorBindingBuilder builder; + Fixture(bool push) { + info.buffers = {{.read = true}, {.written = true}, {.atomic = true}}; + info.images = {{.dimension = RdnaImageDimension::Dim2D, .read = true}, {.dimension = RdnaImageDimension::Dim2D, .atomic = true, .depthCompare = true}}; + info.samplers = {{.forcePointFiltering = true}, {.depthCompare = true}}; + info.images[1].atomic64 = true; + info.samplers[0].uses = SamplerUseExplicitLod; + info.sampledPairs = {{0, 0, 0x10}}; + snapshot.buffers = {{{10,11,12,13},4}, {{20,21,22,23},4}, {{30,31,32,33},4}}; + snapshot.images = {{{40,41,42,43,44,45,46,47},8}, {{50,51,52,53,54,55,56,57},8}}; + snapshot.samplers = {{{60,61,0xffffffffu,63},4}, {{70,71,72,73},4}}; + snapshot.userData = {100,101,102,103}; + snapshot.flattenedSrt = {200,201,202}; + plan.layout.userDataRegisters = {19,16}; + plan.layout.memoryOffsetDword = 2; + plan.layout.memoryOffsetCount = 4; + plan.layout.dispatchThreadLimit = true; + plan.layout.descriptors = {{DescriptorBindingKind::Buffers,{2,0,1}}, {static_cast(FirstImageBinding),{1,0}}, {DescriptorBindingKind::Samplers,{1,0}}, {DescriptorBindingKind::FlattenedSrt,{}}, {DescriptorBindingKind::Gds,{}}, {DescriptorBindingKind::BdaPagetable,{}}, {DescriptorBindingKind::FaultBuffer,{}}}; + if (push) plan.layout.pushDataStartDword = 4; + else plan.layout.descriptors.push_back({DescriptorBindingKind::ShaderData,{}}); + builder.Prepare(plan, info, IrShaderStage::Pixel); + } + MaterializedBindings Materialize() const { return builder.Materialize(plan, info, 16, snapshot, {5,6,7}); } +}; +void materialization(bool push) { + Fixture f(push); + for (const auto& binding : f.plan.bindings) require(binding.guestDescriptor.empty(), "reflection retains live descriptor words"); + require(f.plan.pushConstants.empty(), "reflection retains push data"); + auto a = f.Materialize(); + require(a.bindings[0].Usage().bufferAtomic == std::vector{true,false,false}, "buffer atomic metadata changed"); + require(a.bindings[0].Usage().bufferWritten == std::vector{true,false,true}, "buffer write proof changed"); + require(a.bindings[1].Usage().imageWritten == std::vector{true,false}, "image atomic write proof changed"); + require(a.bindings[1].Usage().imageDepthCompare == std::vector{true,false}, "image comparison metadata changed"); + require(a.bindings[1].Usage().imageAtomic == std::vector{true,false}, "image atomic metadata changed"); + require(a.bindings[1].Usage().imageAtomic64 == std::vector{true,false}, "64-bit image atomic metadata changed"); + require(a.bindings[1].Usage().imageSamplers == std::vector{0,2}, "image-sampler mapping ignored binding order"); + require(a.bindings[1].imageShape == DescriptorImageShape::Image2D, "image shape changed"); + require(a.bindings[2].Usage().samplerDepthCompare == std::vector{true,false}, "sampler comparison metadata changed"); + require(a.bindings[0].guestDescriptor == std::vector{30,31,32,33,10,11,12,13,20,21,22,23}, "buffer resource ordering changed"); + require(a.bindings[1].guestDescriptor == std::vector{50,51,52,53,54,55,56,57,40,41,42,43,44,45,46,47}, "image resource ordering changed"); + require(a.bindings[2].guestDescriptor == std::vector{70,71,72,73,60,61,0xf50fffffu,63}, "point filtering or sampler ordering changed"); + require(a.bindings[3].guestDescriptor == f.snapshot.flattenedSrt, "flattened SRT changed"); + const std::vector expectedData{103,100,0,5,6,7}; + if (push) { + require(a.pushConstants.size() == expectedData.size() * 4, "push data size changed"); + require(std::memcmp(a.pushConstants.data(), expectedData.data(), a.pushConstants.size()) == 0, "push data contents changed"); + } else { + require(a.pushConstants.empty(), "buffer shader data also pushed"); + require(a.bindings.back().guestDescriptor == expectedData, "buffer shader data changed"); + } + for (std::size_t i = 4; i < 7; ++i) require(a.bindings[i].guestDescriptor.empty(), "synthetic binding has guest descriptor words"); + f.snapshot.buffers[2].dwords[0] = 999; + f.snapshot.userData[3] = 888; + f.snapshot.flattenedSrt[0] = 777; + f.snapshot.samplers[0].dwords[2] = 0; + auto b = f.Materialize(); + require(b.bindings[0].guestDescriptor[0] == 999 && a.bindings[0].guestDescriptor[0] == 30, "draw descriptors alias mutable storage"); + require(b.bindings[3].guestDescriptor[0] == 777 && a.bindings[3].guestDescriptor[0] == 200, "flattened SRT snapshots alias"); + require(b.bindings[2].guestDescriptor[6] == 0x01000000u, "point filtering adds a mip filter without mipmaps"); + for (std::size_t i = 0; i < 3; ++i) require(a.bindings[i].usage == b.bindings[i].usage && a.bindings[i].usage == f.plan.bindings[i].usage, "immutable reflection was rebuilt"); + f.plan.bindings.clear(); + require(a.bindings[0].Usage().bufferWritten == std::vector{true,false,true}, "retained draw lost reflection owner"); + rejects([&] { (void)f.Materialize(); }); +} +void changingSamplerCoordinates() { + Fixture f(true); + auto normalized = f.Materialize(); + f.snapshot.samplers[0].dwords[0] |= 1u << 15u; + auto unnormalized = f.Materialize(); + require(normalized.bindings[1].imageUnnormalized == std::vector{false,false}, "retained image coordinate metadata changed"); + require(normalized.bindings[2].samplerUnnormalized == std::vector{false,false}, "retained sampler coordinate metadata changed"); + require(unnormalized.bindings[1].imageUnnormalized == std::vector{false,true}, "live image coordinate metadata not materialized"); + require(unnormalized.bindings[2].samplerUnnormalized == std::vector{false,true}, "live sampler coordinate metadata not materialized"); + require(normalized.bindings[1].usage == unnormalized.bindings[1].usage && normalized.bindings[2].usage == unnormalized.bindings[2].usage, "sampler change rebuilt immutable metadata"); + f.builder.Populate(f.plan, f.info, IrShaderStage::Pixel, 16, f.snapshot, {5,6,7}); + auto repeated = f.Materialize(); + require(repeated.bindings[1].imageUnnormalized == unnormalized.bindings[1].imageUnnormalized && repeated.bindings[2].samplerUnnormalized == unnormalized.bindings[2].samplerUnnormalized, "rematerialization appended coordinate metadata"); + f.snapshot.samplers[1].dwords[0] |= 1u << 15u; + rejects([&] { (void)f.Materialize(); }); +} +void cacheRoundTrip() { + Fixture f(true); + CompiledVariant variant; + variant.bindings = f.plan; + variant.info.info = f.info; + variant.info.stage = IrShaderStage::Pixel; + variant.info.userDataBase = 16; + const std::array key{std::byte{42}}; + const auto bytes = ShaderDiskCache::EncodeEntry(key, variant); + CompiledVariant decoded; + require(ShaderDiskCache::DecodeEntry(bytes, key, decoded) == ShaderDiskCache::LoadStatus::Loaded, "prepared reflection cache entry rejected"); + auto result = f.builder.Materialize(decoded.bindings, decoded.info.info, decoded.info.userDataBase, f.snapshot, {5,6,7}); + auto expected = f.Materialize(); + require(result.pushConstants == expected.pushConstants, "cached reflection changed push data"); + require(result.bindings.size() == expected.bindings.size(), "cached reflection changed binding count"); + for (std::size_t i = 0; i < result.bindings.size(); ++i) { + const auto& a = result.bindings[i]; + const auto& b = expected.bindings[i]; + require(a.guestDescriptor == b.guestDescriptor && a.kind == b.kind && a.role == b.role && a.binding == b.binding && a.count == b.count && a.imageShape == b.imageShape, "cached reflection changed descriptors"); + require(a.Usage().bufferWritten == b.Usage().bufferWritten && a.Usage().bufferAtomic == b.Usage().bufferAtomic && a.Usage().imageWritten == b.Usage().imageWritten && a.Usage().imageAtomic == b.Usage().imageAtomic && a.Usage().imageDepthCompare == b.Usage().imageDepthCompare && a.Usage().samplerDepthCompare == b.Usage().samplerDepthCompare, "cached reflection changed access metadata"); + require(decoded.bindings.bindings[i].guestDescriptor.empty(), "cached reflection retained live words"); + require(a.Usage().imageAtomic64 == b.Usage().imageAtomic64 && a.Usage().imageSamplers == b.Usage().imageSamplers && a.samplerUnnormalized == b.samplerUnnormalized && a.imageUnnormalized == b.imageUnnormalized, "cached reflection lost merged image metadata"); + } +} +void malformed() { + Fixture f(true); + f.snapshot.buffers[0].dwordCount = 3; + rejects([&] { (void)f.Materialize(); }); + f.snapshot.buffers[0].dwordCount = 4; + f.snapshot.images[0].dwordCount = 4; + rejects([&] { (void)f.Materialize(); }); + f.snapshot.images[0].dwordCount = 8; + f.snapshot.flattenedSrt.clear(); + rejects([&] { (void)f.Materialize(); }); + f.snapshot.flattenedSrt = {0}; + rejects([&] { (void)f.builder.Materialize(f.plan, f.info, 16, f.snapshot, {}); }); + f.snapshot.userData.clear(); + rejects([&] { (void)f.Materialize(); }); + f.plan.layout.descriptors.push_back({DescriptorBindingKind::ShaderData,{}}); + rejects([&] { f.builder.Prepare(f.plan, f.info, IrShaderStage::Pixel); }); + DescriptorBinding unproved{}; + require(unproved.Usage().bufferWritten.empty(), "missing access metadata invents a write proof"); +} +} +int main() { + try { + materialization(true); + materialization(false); + malformed(); + cacheRoundTrip(); + changingSamplerCoordinates(); + std::cout << "binding materialization tests passed\n"; + } catch (const std::exception& e) { + std::cerr << e.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 1de455a9d5..84ef4e5710 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -1322,8 +1322,7 @@ void bindingPlanTests() { vertex.variantId = 1; fragment.variantId = 2; vertex.bindings.push_back(makeBinding(Role::GuestBuffers, 0, 2, join(vsharp(guestFirst.data(), 16), vsharp(guestSecond.data(), 32)))); - vertex.bindings.front().bufferWritten = {false, true}; - vertex.bindings.front().bufferAtomic = {false, true}; + vertex.bindings.front().usage = std::make_shared(ShaderRecompiler::DescriptorBindingUsage{.bufferAtomic = {false, true}, .bufferWritten = {false, true}}); vertex.bindings.push_back(makeBinding(Role::ShaderData, 5, 1, {7, 8, 9})); fragment.bindings.push_back(makeBinding(Role::FlattenedSrt, 43, 1, {1, 2})); std::array shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &fragment, 8}}}; @@ -1353,7 +1352,7 @@ void bindingPlanTests() { images.bindings.push_back(sampled); auto sampler = makeBinding(Role::GuestSamplers, 4, 1, std::vector(4)); sampler.kind = Kind::Sampler; - sampler.samplerDepthCompare = {false}; + sampler.usage = std::make_shared(ShaderRecompiler::DescriptorBindingUsage{.samplerDepthCompare = {false}}); images.bindings.push_back(sampler); sampled.binding = 7; images.bindings.push_back(sampled); @@ -1369,7 +1368,7 @@ void bindingPlanTests() { program.variantId = 20; program.pushConstants.resize(4); program.bindings.push_back(makeBinding(Role::GuestBuffers, 0, 1, vsharp(guestFirst.data(), 16))); - program.bindings.front().bufferWritten = {false}; + program.bindings.front().usage = std::make_shared(ShaderRecompiler::DescriptorBindingUsage{.bufferWritten = {false}}); const CompiledShader shader{ShaderRecompiler::ShaderStage::Compute, &program, 0}; ShaderResources before(context, shader); const auto beforeWrite = findWrite(0); diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index b8c01e9b7b..43a049cef2 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -996,7 +996,7 @@ void resourceReadTests(const Device& device, Recorder& recorder) { binding.binding = 0; binding.count = 1; binding.guestDescriptor = {static_cast(element), static_cast(element >> 32u) & 0xffffu, elementBytes, 0x31000000u}; - binding.bufferWritten = {false}; + binding.usage = std::make_shared(ShaderRecompiler::DescriptorBindingUsage{.bufferWritten = {false}}); program.bindings.push_back(binding); const CompiledShader compute{ShaderRecompiler::ShaderStage::Compute, &program, 0}; { @@ -1112,7 +1112,7 @@ void misalignedSnapshotTests(const Device& device, Recorder& recorder) { binding.count = 2; for (const auto word : words(address + outer, outerBytes)) binding.guestDescriptor.push_back(word); for (const auto word : words(address + offset, elementBytes)) binding.guestDescriptor.push_back(word); - binding.bufferWritten = {false, false}; + binding.usage = std::make_shared(ShaderRecompiler::DescriptorBindingUsage{.bufferWritten = {false, false}}); program.bindings.push_back(binding); program.pushConstants.resize(16); program.memoryOffsetDword = 0; @@ -1178,7 +1178,7 @@ void resourceBuildBenchmark(const Device& device) { binding.role = ShaderRecompiler::DescriptorRole::GuestBuffers; binding.binding = i; binding.count = 1; - binding.bufferWritten = {false}; + binding.usage = std::make_shared(ShaderRecompiler::DescriptorBindingUsage{.bufferWritten = {false}}); const auto base = address + i * 256; binding.guestDescriptor = {static_cast(base), static_cast(base >> 32), 256, 0x31016fac}; program.bindings.push_back(std::move(binding)); @@ -1263,7 +1263,7 @@ void drawSnapshotReuseTests(const Device& device, Recorder& recorder) { binding.binding = 0; binding.count = 1; binding.guestDescriptor = {static_cast(element), static_cast(element >> 32u) & 0xffffu, elementBytes, 0x31000000u}; - binding.bufferWritten = {false}; + binding.usage = std::make_shared(ShaderRecompiler::DescriptorBindingUsage{.bufferWritten = {false}}); program.bindings.push_back(binding); const CompiledShader compute{ShaderRecompiler::ShaderStage::Compute, &program, 0}; { diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index 11af875da1..f9d7b7368f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -33,7 +33,7 @@ void setEnvironment(const char* name, const std::string& value) { } bool sameBinding(const DescriptorBinding& left, const DescriptorBinding& right) { - return left.kind == right.kind && left.role == right.role && left.descriptorSet == right.descriptorSet && left.binding == right.binding && left.count == right.count && left.guestDescriptor == right.guestDescriptor && left.readOnly == right.readOnly && left.imageShape == right.imageShape && left.samplerDepthCompare == right.samplerDepthCompare && left.imageWritten == right.imageWritten && left.imageDepthCompare == right.imageDepthCompare && left.imageAtomic == right.imageAtomic && left.bufferAtomic == right.bufferAtomic && left.bufferWritten == right.bufferWritten && left.samplerUnnormalized == right.samplerUnnormalized && left.imageUnnormalized == right.imageUnnormalized && left.imageSamplers == right.imageSamplers; + return left.kind == right.kind && left.role == right.role && left.descriptorSet == right.descriptorSet && left.binding == right.binding && left.count == right.count && left.guestDescriptor == right.guestDescriptor && left.readOnly == right.readOnly && left.imageShape == right.imageShape && left.Usage().samplerDepthCompare == right.Usage().samplerDepthCompare && left.Usage().imageWritten == right.Usage().imageWritten && left.Usage().imageDepthCompare == right.Usage().imageDepthCompare && left.Usage().imageAtomic == right.Usage().imageAtomic && left.Usage().imageAtomic64 == right.Usage().imageAtomic64 && left.Usage().bufferAtomic == right.Usage().bufferAtomic && left.Usage().bufferWritten == right.Usage().bufferWritten && left.samplerUnnormalized == right.samplerUnnormalized && left.imageUnnormalized == right.imageUnnormalized && left.Usage().imageSamplers == right.Usage().imageSamplers; } bool sameBindings(const std::vector& left, const std::vector& right) { @@ -91,15 +91,18 @@ DescriptorBinding sampleBinding(std::uint32_t seed) { binding.guestDescriptor = {0x11111111u * seed, 0xdeadbeefu, 0x80000000u, 7u, 0u, 0xffffffffu, 42u, seed}; binding.readOnly = seed % 2 == 0; binding.imageShape = DescriptorImageShape::Image2DArray; - binding.samplerDepthCompare = {true, false, true}; - binding.imageDepthCompare = {false, true, false}; - binding.imageWritten = {false, true, true}; - binding.imageAtomic = {false, true, false}; - binding.bufferAtomic = {true}; - binding.bufferWritten = {false, false, true, true, false}; + binding.usage = std::make_shared(DescriptorBindingUsage{ + .samplerDepthCompare = {true, false, true}, + .imageWritten = {false, true, true}, + .imageDepthCompare = {false, true, false}, + .imageAtomic = {false, true, false}, + .imageAtomic64 = {true, false, true}, + .bufferAtomic = {true}, + .bufferWritten = {false, false, true, true, false}, + .imageSamplers = {0x5u, 0u, 0x80000000u} + }); binding.samplerUnnormalized = {false, true, true}; binding.imageUnnormalized = {true, false, seed % 2 == 0}; - binding.imageSamplers = {0x5u, 0u, 0x80000000u}; return binding; } diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 7a2e71bca6..c2f36766c7 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -318,7 +318,7 @@ void verifyBindlessTable() { if (binding.kind != DescriptorKind::SampledImage) continue; sampled = true; require(binding.count == direct + slots - 1u && binding.guestDescriptor.size() == 8u * binding.count, "bindless: the sampled image binding does not hold the table slots"); - require(std::none_of(binding.imageWritten.begin(), binding.imageWritten.end(), [](bool written) { return written; }), "bindless: a table slot is marked written"); + require(std::none_of(binding.Usage().imageWritten.begin(), binding.Usage().imageWritten.end(), [](bool written) { return written; }), "bindless: a table slot is marked written"); } require(sampled && flattened, "bindless: the bindings lack the image array or the flattened SRT"); struct SpirvScan { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp index c664f1d296..d0dc1e4b25 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp @@ -205,7 +205,7 @@ void Run(AgcDriver::VulkanDevice& device, const Sampler& sampler, const char* na bool BindsDepthCompare(const ShaderRecompiler::RecompileResult& result) { return std::any_of(result.bindings.begin(), result.bindings.end(), [](const ShaderRecompiler::DescriptorBinding& binding) { - return std::any_of(binding.imageDepthCompare.begin(), binding.imageDepthCompare.end(), [](bool compare) { return compare; }); + return std::any_of(binding.Usage().imageDepthCompare.begin(), binding.Usage().imageDepthCompare.end(), [](bool compare) { return compare; }); }); } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleReduction.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleReduction.cpp index 648a88e528..e4c9daba0c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleReduction.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleReduction.cpp @@ -237,7 +237,7 @@ std::uint32_t PairedSamplers(const ShaderRecompiler::RecompileResult& result, st for (const auto& binding : result.bindings) { if (binding.kind != ShaderRecompiler::DescriptorKind::SampledImage) continue; for (std::uint32_t element = 0; element < binding.count; ++element) { - if (((binding.guestDescriptor.at(element * 8u + 1u) >> 20u) & 0x1ffu) == format) return binding.imageSamplers.at(element); + if (((binding.guestDescriptor.at(element * 8u + 1u) >> 20u) & 0x1ffu) == format) return binding.Usage().imageSamplers.at(element); } } Require(false, "image sample reduction: no sampled image of format " + std::to_string(format)); diff --git a/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp b/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp index b62ee64a71..8fedef2c49 100644 --- a/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp +++ b/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp @@ -127,7 +127,7 @@ bool Replay(const char* path) { if (binding.role != ShaderRecompiler::DescriptorRole::GuestBuffers) continue; for (std::size_t i = 0; i + 4 <= binding.guestDescriptor.size(); i += 4) { const auto* v = binding.guestDescriptor.data() + i; - const bool written = i / 4 >= binding.bufferWritten.size() || binding.bufferWritten[i / 4]; + const bool written = i / 4 >= binding.Usage().bufferWritten.size() || binding.Usage().bufferWritten[i / 4]; std::printf(" buffer %zu: V# %08x %08x %08x %08x%s\n", i / 4, v[0], v[1], v[2], v[3], written ? " written" : ""); } } diff --git a/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp b/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp index cae8055d89..510c08d56a 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp @@ -8,10 +8,16 @@ namespace ShaderRecompiler { +struct MaterializedBindings { + std::vector bindings; + std::vector pushConstants; +}; std::uint32_t PointFilteredSamplerWord(std::uint32_t word0, std::uint32_t filter); class DescriptorBindingBuilder { public: + void Prepare(BindingAllocationResult& allocation, const ShaderInfo& info, IrShaderStage stage) const; + [[nodiscard]] MaterializedBindings Materialize(const BindingAllocationResult& allocation, const ShaderInfo& info, std::uint32_t userDataBase, const ResourceSnapshot& snapshot, const std::array& partialThreads) const; void Populate(BindingAllocationResult& allocation, const IrProgram& program, const ResourceSnapshot& snapshot, const std::array& partialThreads) const; void Populate(BindingAllocationResult& allocation, const ShaderInfo& info, IrShaderStage stage, std::uint32_t userDataBase, const ResourceSnapshot& snapshot, const std::array& partialThreads) const; }; diff --git a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp index 6cd17b6b38..5bd37fb67e 100644 --- a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp +++ b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp @@ -299,58 +299,95 @@ void DescriptorBindingBuilder::Populate(BindingAllocationResult& allocation, con } void DescriptorBindingBuilder::Populate(BindingAllocationResult& allocation, const ShaderInfo& info, IrShaderStage stage, std::uint32_t userDataBase, const ResourceSnapshot& snapshot, const std::array& partialThreads) const { - const IrBindingLayout& layout = allocation.layout; - const std::vector shaderData = ShaderDataDwordsFor(layout, userDataBase, snapshot, partialThreads); - const UnnormalizedProof unnormalized = ProveUnnormalized(info, snapshot); - const std::vector samplerElements = SamplerElements(layout, info); + Prepare(allocation, info, stage); + auto result = Materialize(allocation, info, userDataBase, snapshot, partialThreads); + allocation.bindings = std::move(result.bindings); + allocation.pushConstants = std::move(result.pushConstants); +} +void DescriptorBindingBuilder::Prepare(BindingAllocationResult& allocation, const ShaderInfo& info, IrShaderStage stage) const { + const auto samplerElements = SamplerElements(allocation.layout, info); std::vector bindings; - bindings.reserve(layout.descriptors.size()); - std::size_t writtenHere = 0; - std::size_t readOnlyHere = 0; - for (const IrDescriptorBinding& logical : layout.descriptors) { + bindings.reserve(allocation.layout.descriptors.size()); + for (const IrDescriptorBinding& logical : allocation.layout.descriptors) { DescriptorBinding physical; physical.descriptorSet = 0u; physical.binding = NativeBinding(stage, logical.kind); physical.count = logical.resources.empty() ? 1u : static_cast(logical.resources.size()); physical.kind = PhysicalKindFor(logical.kind); physical.role = RoleFor(logical.kind); - physical.readOnly = false; - + DescriptorBindingUsage usage; switch (physical.role) { case DescriptorRole::GuestBuffers: - physical.guestDescriptor = GuestBuffersDescriptor(logical.resources, snapshot); for (const std::uint32_t resource : logical.resources) { const auto& buffer = info.buffers.at(resource); - physical.bufferAtomic.push_back(buffer.atomic); - // The tracker merges every buffer access of a source into its resource - // (ResourceTracker::Merge), so an element without a store or atomic is read-only - // over its whole extent; stores through pointers (BDA) never bind a V#. - physical.bufferWritten.push_back(buffer.written || buffer.atomic); - if (buffer.written || buffer.atomic) ++writtenHere; - else ++readOnlyHere; + usage.bufferAtomic.push_back(buffer.atomic); + usage.bufferWritten.push_back(buffer.written || buffer.atomic); } break; case DescriptorRole::GuestImages: - physical.guestDescriptor = GuestImagesDescriptor(logical.resources, snapshot); physical.imageShape = ImageShapeFor(info.images, logical.resources); for (const std::uint32_t resource : logical.resources) { const auto& image = info.images.at(resource); - physical.imageWritten.push_back(image.written || image.atomic); - physical.imageDepthCompare.push_back(image.depthCompare); - physical.imageAtomic.push_back(image.atomic); - physical.imageAtomic64.push_back(image.atomic64); - physical.imageUnnormalized.push_back(unnormalized.images.at(resource)); - physical.imageSamplers.push_back(ImageSamplerMask(info, samplerElements, resource)); + usage.imageWritten.push_back(image.written || image.atomic); + usage.imageDepthCompare.push_back(image.depthCompare); + usage.imageAtomic.push_back(image.atomic); + usage.imageAtomic64.push_back(image.atomic64); + usage.imageSamplers.push_back(ImageSamplerMask(info, samplerElements, resource)); + } + break; + case DescriptorRole::GuestSamplers: + for (const std::uint32_t resource : logical.resources) { + usage.samplerDepthCompare.push_back(info.samplers.at(resource).depthCompare); } break; + case DescriptorRole::ShaderData: + if (allocation.layout.UsesPushData()) { + fail("DescriptorBindingBuilder::Prepare shader-data binding must not exist when push data is used"); + } + break; + case DescriptorRole::FlattenedSrt: + case DescriptorRole::Gds: + case DescriptorRole::BdaPagetable: + case DescriptorRole::FaultBuffer: + break; + } + if (physical.role == DescriptorRole::GuestBuffers || physical.role == DescriptorRole::GuestImages || physical.role == DescriptorRole::GuestSamplers) { + physical.usage = std::make_shared(std::move(usage)); + } + bindings.push_back(std::move(physical)); + } + allocation.bindings = std::move(bindings); + allocation.pushConstants.clear(); +} + +MaterializedBindings DescriptorBindingBuilder::Materialize(const BindingAllocationResult& allocation, const ShaderInfo& info, std::uint32_t userDataBase, const ResourceSnapshot& snapshot, const std::array& partialThreads) const { + const IrBindingLayout& layout = allocation.layout; + if (allocation.bindings.size() != layout.descriptors.size()) { + fail("DescriptorBindingBuilder::Materialize binding reflection does not match the layout"); + } + const std::vector shaderData = ShaderDataDwordsFor(layout, userDataBase, snapshot, partialThreads); + const auto unnormalized = ProveUnnormalized(info, snapshot); + MaterializedBindings result; + result.bindings = allocation.bindings; + for (std::size_t i = 0; i < result.bindings.size(); ++i) { + const auto& logical = layout.descriptors[i]; + auto& physical = result.bindings[i]; + physical.samplerUnnormalized.clear(); + physical.imageUnnormalized.clear(); + switch (physical.role) { + case DescriptorRole::GuestBuffers: + physical.guestDescriptor = GuestBuffersDescriptor(logical.resources, snapshot); + break; + case DescriptorRole::GuestImages: + physical.guestDescriptor = GuestImagesDescriptor(logical.resources, snapshot); + for (const auto resource : logical.resources) physical.imageUnnormalized.push_back(unnormalized.images.at(resource)); + break; case DescriptorRole::GuestSamplers: physical.guestDescriptor = GuestSamplersDescriptor(logical.resources, snapshot); for (std::size_t element = 0; element < logical.resources.size(); ++element) { - const auto& sampler = info.samplers.at(logical.resources[element]); - physical.samplerDepthCompare.push_back(sampler.depthCompare); physical.samplerUnnormalized.push_back(unnormalized.samplers.at(logical.resources[element])); - if (sampler.forcePointFiltering) { + if (info.samplers.at(logical.resources[element]).forcePointFiltering) { auto& filter = physical.guestDescriptor.at(element * 4u + 2u); filter = PointFilteredSamplerWord(physical.guestDescriptor.at(element * 4u), filter); } @@ -358,14 +395,11 @@ void DescriptorBindingBuilder::Populate(BindingAllocationResult& allocation, con break; case DescriptorRole::FlattenedSrt: if (snapshot.flattenedSrt.empty()) { - fail("DescriptorBindingBuilder::Populate flattened SRT snapshot is empty"); + fail("DescriptorBindingBuilder::Materialize flattened SRT snapshot is empty"); } physical.guestDescriptor = snapshot.flattenedSrt; break; case DescriptorRole::ShaderData: - if (layout.UsesPushData()) { - fail("DescriptorBindingBuilder::Populate shader-data binding must not exist when push data is used"); - } physical.guestDescriptor = shaderData; break; case DescriptorRole::Gds: @@ -373,29 +407,30 @@ void DescriptorBindingBuilder::Populate(BindingAllocationResult& allocation, con case DescriptorRole::FaultBuffer: break; } - if (physical.role == DescriptorRole::GuestBuffers || physical.role == DescriptorRole::GuestImages || physical.role == DescriptorRole::GuestSamplers) { if (physical.count == 0u || physical.guestDescriptor.size() % physical.count != 0u) { - fail("DescriptorBindingBuilder::Populate guest descriptor size is not a multiple of the binding count"); + fail("DescriptorBindingBuilder::Materialize guest descriptor size is not a multiple of the binding count"); } } - - bindings.push_back(std::move(physical)); } - if (bufferWrittenTraceEnabled()) { - // Cumulative counts include every Populate of the process (the replay tool populates - // each program twice), so the per-call counts are the ones to sum per program. + std::size_t writtenHere = 0; + std::size_t readOnlyHere = 0; + for (const auto& binding : result.bindings) { + for (bool written : binding.Usage().bufferWritten) { + if (written) ++writtenHere; + else ++readOnlyHere; + } + } const auto written = bufferWrittenCounts.written.fetch_add(writtenHere) + writtenHere; const auto readOnly = bufferWrittenCounts.readOnly.fetch_add(readOnlyHere) + readOnlyHere; std::fprintf(stderr, "[bindings] guest buffer elements: %zu written, %zu read-only (total so far: %llu / %llu)\n", writtenHere, readOnlyHere, written, readOnly); } - allocation.bindings = std::move(bindings); - allocation.pushConstants.clear(); if (layout.UsesPushData()) { - allocation.pushConstants.resize(static_cast(shaderData.size()) * sizeof(std::uint32_t)); - std::memcpy(allocation.pushConstants.data(), shaderData.data(), allocation.pushConstants.size()); + result.pushConstants.resize(static_cast(shaderData.size()) * sizeof(std::uint32_t)); + std::memcpy(result.pushConstants.data(), shaderData.data(), result.pushConstants.size()); } + return result; } } diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 9f817d1439..356141b60d 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -304,7 +304,7 @@ std::uint64_t nextVariantId() { return variants.fetch_add(1, std::memory_order_relaxed) + 1; } -CompiledVariant compileVariant(const RecompileRequest& request, IrProgram program, const ResourceSnapshot& resourceSnapshot, const ResourceSpecialization& resourceSpecialization, std::exception_ptr* emissionFailure = nullptr) { +CompiledVariant compileVariant(const RecompileRequest& request, IrProgram program, const ResourceSpecialization& resourceSpecialization, std::exception_ptr* emissionFailure = nullptr) { const auto inputInfo = RequestInputInfo(request); constexpr DeadCodeEliminator deadCodeEliminator; constexpr ResourceMaterializer resourceMaterializer; @@ -320,7 +320,7 @@ CompiledVariant compileVariant(const RecompileRequest& request, IrProgram progra auto bindings = bindingAllocator.Allocate(program, request.layout); constexpr DescriptorBindingBuilder descriptorBindingBuilder; - descriptorBindingBuilder.Populate(bindings, program, resourceSnapshot, partialThreads(request)); + descriptorBindingBuilder.Prepare(bindings, program.Info(), program.Resources().stage); SpirvTargetOptions targetOptions {}; targetOptions.vulkanVersion = request.target.vulkanVersion; @@ -371,18 +371,13 @@ CompiledVariant compileVariant(const RecompileRequest& request, IrProgram progra result.bindings.clear(); result.pushConstants.clear(); for (auto& attribute : result.vertexAttributes) attribute.resource = {}; - bindings.bindings.clear(); bindings.pushConstants.clear(); return {resourceSpecialization, request.layout, std::move(program).TakeCompiledInfo(), std::move(bindings), std::move(result)}; } RecompileResult materializeResult(const CompiledVariant& variant, const RecompileRequest& request, const ResourceSnapshot& snapshot) { auto result = variant.result; - BindingAllocationResult bindings; - bindings.layout = variant.bindings.layout; - bindings.pushConstantOffsetBytes = variant.bindings.pushConstantOffsetBytes; - bindings.pushConstantSizeBytes = variant.bindings.pushConstantSizeBytes; - DescriptorBindingBuilder{}.Populate(bindings, variant.info.info, variant.info.stage, variant.info.userDataBase, snapshot, partialThreads(request)); + auto bindings = DescriptorBindingBuilder{}.Materialize(variant.bindings, variant.info.info, variant.info.userDataBase, snapshot, partialThreads(request)); result.bindings = std::move(bindings.bindings); result.pushConstants = std::move(bindings.pushConstants); for (auto& attribute : result.vertexAttributes) { @@ -396,7 +391,7 @@ bool sameLayout(const BindingLayout& left, const BindingLayout& right) { return left.descriptorSet == right.descriptorSet && left.firstBinding == right.firstBinding && left.pushConstantOffsetBytes == right.pushConstantOffsetBytes && left.pushConstantSizeBytes == right.pushConstantSizeBytes; } -std::shared_ptr findOrCompileVariant(SourceEntry& source, const RecompileRequest& request, const ResourceSnapshot& snapshot, const ResourceSpecialization& specialization, bool& cacheHit) { +std::shared_ptr findOrCompileVariant(SourceEntry& source, const RecompileRequest& request, const ResourceSpecialization& specialization, bool& cacheHit) { for (const auto& candidate : source.variants) { if (sameLayout(candidate->layout, request.layout) && candidate->specialization == specialization) { cacheHit = true; @@ -425,7 +420,7 @@ std::shared_ptr findOrCompileVariant(SourceEntry& source, source.program.reset(); std::exception_ptr emissionFailure; try { - variant = std::make_shared(compileVariant(request, std::move(program), snapshot, specialization, &emissionFailure)); + variant = std::make_shared(compileVariant(request, std::move(program), specialization, &emissionFailure)); } catch (...) { if (emissionFailure != nullptr && FailureMemo()) source.emissionFailures.push_back({request.shader.codeAddress, request.layout, specialization, emissionFailure}); throw; @@ -443,7 +438,7 @@ RecompileResult materializeVariant(SourceEntry& source, const RecompileRequest& bool cacheHit = false; { std::lock_guard lock(source.mutex); - variant = findOrCompileVariant(source, request, snapshot, specialization, cacheHit); + variant = findOrCompileVariant(source, request, specialization, cacheHit); } auto result = materializeResult(*variant, request, snapshot); result.cacheHit = cacheHit; @@ -461,7 +456,7 @@ RecompileResult RecompileImpl(const RecompileRequest& request) { auto program = PrepareResourceProgram(request); const auto plan = materializer.ExtractPlan(program); materializer.Materialize(plan, runtime, snapshot, specialization); - const auto variant = compileVariant(request, std::move(program), snapshot, specialization); + const auto variant = compileVariant(request, std::move(program), specialization); return materializeResult(variant, request, snapshot); } const auto source = getSource(request); @@ -561,7 +556,7 @@ std::shared_ptr materializeMemoized(SourceEntry& source, auto& counters = resultMemoCounters(); { std::lock_guard lock(source.mutex); - variant = findOrCompileVariant(source, request, snapshot, specialization, cacheHit); + variant = findOrCompileVariant(source, request, specialization, cacheHit); index = (variant->result.variantId * 0x9e3779b97f4a7c15ull) ^ hash; const auto found = source.memoIndex.find(index); if (found != source.memoIndex.end() && found->second->variantId == variant->result.variantId && found->second->hash == hash) { @@ -610,7 +605,7 @@ std::shared_ptr materializeMemoized(SourceEntry& source, std::shared_ptr RecompileImpl(const RecompileRequest& request, const ResourceCapture& capture, bool* memoHit) { if (!request.useCache || capture.source == nullptr) { auto program = PrepareResourceProgram(request); - const auto variant = compileVariant(request, std::move(program), capture.snapshot, capture.specialization); + const auto variant = compileVariant(request, std::move(program), capture.specialization); return std::make_shared(materializeResult(variant, request, capture.snapshot)); } if (!ResultMemo()) return std::make_shared(materializeVariant(*capture.source, request, capture.snapshot, capture.specialization)); diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 7eddc28134..001fabbfcb 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -306,15 +306,7 @@ enum class DescriptorRole { ShaderData }; -struct DescriptorBinding { - DescriptorKind kind; - DescriptorRole role; - std::uint32_t descriptorSet; - std::uint32_t binding; - std::uint32_t count; - std::vector guestDescriptor; - bool readOnly = false; - std::optional imageShape; +struct DescriptorBindingUsage { std::vector samplerDepthCompare; // Guest image elements the shader stores to (or updates atomically); the others are only read. std::vector imageWritten; @@ -331,9 +323,27 @@ struct DescriptorBinding { // skip the write-back and the pending-write note for the element; an element beyond the vector // (a producer that does not fill it) must be treated as written. std::vector bufferWritten; + std::vector imageSamplers; +}; + +struct DescriptorBinding { + DescriptorKind kind; + DescriptorRole role; + std::uint32_t descriptorSet; + std::uint32_t binding; + std::uint32_t count; + std::vector guestDescriptor; + bool readOnly = false; + std::optional imageShape; + std::shared_ptr usage; std::vector samplerUnnormalized; std::vector imageUnnormalized; - std::vector imageSamplers; + + [[nodiscard]] const DescriptorBindingUsage& Usage() const { + if (usage) return *usage; + static const DescriptorBindingUsage empty; + return empty; + } }; struct VertexAttribute { diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index ed87d113e6..956c341b9c 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -64,7 +64,8 @@ namespace ShaderRecompiler::ShaderDiskCache { #if defined(__linux__) && defined(__x86_64__) && defined(__GLIBCXX__) static_assert(sizeof(RecompileResult) == 176, "RecompileResult changed: update EncodeResult and DecodeResult"); -static_assert(sizeof(DescriptorBinding) == 448, "DescriptorBinding changed: update the binding encoder"); +static_assert(sizeof(DescriptorBinding) == 160, "DescriptorBinding changed: update the binding encoder"); +static_assert(sizeof(DescriptorBindingUsage) == 304, "DescriptorBindingUsage changed: update the binding encoder"); static_assert(sizeof(VertexAttribute) == 28, "VertexAttribute changed: update the attribute encoder"); static_assert(sizeof(FragmentParameter) == 12, "FragmentParameter changed: update the parameter encoder"); static_assert(sizeof(CompiledShaderInfo) == 304, "CompiledShaderInfo changed: update the info encoder"); @@ -245,16 +246,16 @@ void encodeBinding(Writer& writer, const DescriptorBinding& binding) { writer.Value(binding.readOnly); writer.Value(binding.imageShape.has_value()); writer.Value(binding.imageShape.value_or(DescriptorImageShape::Image1D)); - writer.Flags(binding.samplerDepthCompare); - writer.Flags(binding.imageWritten); - writer.Flags(binding.imageDepthCompare); - writer.Flags(binding.imageAtomic); - writer.Flags(binding.imageAtomic64); - writer.Flags(binding.bufferAtomic); - writer.Flags(binding.bufferWritten); + writer.Flags(binding.Usage().samplerDepthCompare); + writer.Flags(binding.Usage().imageWritten); + writer.Flags(binding.Usage().imageDepthCompare); + writer.Flags(binding.Usage().imageAtomic); + writer.Flags(binding.Usage().imageAtomic64); + writer.Flags(binding.Usage().bufferAtomic); + writer.Flags(binding.Usage().bufferWritten); writer.Flags(binding.samplerUnnormalized); writer.Flags(binding.imageUnnormalized); - writer.Values(std::span(binding.imageSamplers)); + writer.Values(std::span(binding.Usage().imageSamplers)); } void decodeBinding(Reader& reader, DescriptorBinding& binding) { @@ -268,16 +269,18 @@ void decodeBinding(Reader& reader, DescriptorBinding& binding) { const bool hasShape = reader.Get(); const auto shape = reader.Get(); binding.imageShape = hasShape ? std::optional(shape) : std::nullopt; - reader.Flags(binding.samplerDepthCompare); - reader.Flags(binding.imageWritten); - reader.Flags(binding.imageDepthCompare); - reader.Flags(binding.imageAtomic); - reader.Flags(binding.imageAtomic64); - reader.Flags(binding.bufferAtomic); - reader.Flags(binding.bufferWritten); + DescriptorBindingUsage usage; + reader.Flags(usage.samplerDepthCompare); + reader.Flags(usage.imageWritten); + reader.Flags(usage.imageDepthCompare); + reader.Flags(usage.imageAtomic); + reader.Flags(usage.imageAtomic64); + reader.Flags(usage.bufferAtomic); + reader.Flags(usage.bufferWritten); reader.Flags(binding.samplerUnnormalized); reader.Flags(binding.imageUnnormalized); - reader.Values(binding.imageSamplers); + reader.Values(usage.imageSamplers); + binding.usage = std::make_shared(std::move(usage)); } void encodeResult(Writer& writer, const RecompileResult& result) { diff --git a/core/shader/recompiler/ShaderDiskCache.hpp b/core/shader/recompiler/ShaderDiskCache.hpp index 01a2e20bbf..29e244b46d 100644 --- a/core/shader/recompiler/ShaderDiskCache.hpp +++ b/core/shader/recompiler/ShaderDiskCache.hpp @@ -12,7 +12,7 @@ namespace ShaderRecompiler::ShaderDiskCache { -inline constexpr std::uint32_t FormatVersion = 7; +inline constexpr std::uint32_t FormatVersion = 8; enum class LoadStatus { Loaded, From a8b2195c5c2d80799749526632668f35863dd77b Mon Sep 17 00:00:00 2001 From: TibixDev Date: Thu, 8 Oct 2026 10:34:28 +0300 Subject: [PATCH 3/5] test(shader): retain sampler usage in the resource fixture Adapt the corrected binding-plan benchmark to immutable descriptor usage metadata. AI-assisted: yes. --- core/libs/prx/libSceAgcDriver/tests/Recorder.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 43a049cef2..6fa75e839b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -1190,7 +1190,7 @@ void resourceBuildBenchmark(const Device& device) { binding.binding = 8 + i; binding.count = 1; binding.imageShape = ShaderRecompiler::DescriptorImageShape::Image2D; - binding.imageSamplers = {0}; + binding.usage = std::make_shared(ShaderRecompiler::DescriptorBindingUsage{.imageSamplers = {0}}); const auto base = address + (i + 1) * 16384; binding.guestDescriptor = {static_cast(base >> 8), static_cast(base >> 40) | (56u << 20) | (3u << 30), 15u | (63u << 14), 0x90000fac, 0, 0, 0, 0}; program.bindings.push_back(std::move(binding)); From 6c122a800a8a8b1626f29e0faca50633ee6dd514 Mon Sep 17 00:00:00 2001 From: TibixDev Date: Thu, 8 Oct 2026 10:35:33 +0300 Subject: [PATCH 4/5] test(agc): order consecutive shader-data fixture writes Use the transfer barrier already present in the retained-buffer fixture between two direct RefreshData calls. AI-assisted: yes. --- core/libs/prx/libSceAgcDriver/tests/Recorder.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 6fa75e839b..42a1285cad 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -2536,6 +2536,7 @@ void dataRefreshTests(const Device& device, Recorder& recorder) { Require(!resources.DataWordsDiffer(original) && resources.DataWordsDiffer(live), "the per-word compare disagrees with the words"); Require(resources.RefreshData(recorder.Commands(), live, &recorder), "a refresh with different words recorded nothing"); Require(resources.DataWordsHash() == ShaderResources::DataWordsHash(live) && !resources.DataWordsDiffer(live), "the refreshed template's hash is not the patched words'"); + RecordMemoryBarrier(device.GetContext(), recorder.Commands(), VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); Require(resources.RefreshData(recorder.Commands(), original, &recorder), "the refresh back recorded nothing"); Require(resources.DataWordsHash() == ShaderResources::DataWordsHash(original), "the refreshed template's hash is not the original words'"); Require(!resources.RefreshData(recorder.Commands(), original, &recorder), "a refresh with equal words recorded"); From 63763c6a0ada57b7bdd1d24dc50e8bf2ba517923 Mon Sep 17 00:00:00 2001 From: TibixDev Date: Thu, 8 Oct 2026 11:10:10 +0300 Subject: [PATCH 5/5] fix(shader): validate sampler usage before SPIR-V emission --- .../prx/libSceAgcDriver/tests/ShaderMemory.cpp | 2 +- .../Optimization/DescriptorBindingBuilder.hpp | 1 + .../Optimization/src/DescriptorBindingBuilder.cpp | 4 ++++ core/shader/recompiler/Recompiler.cpp | 15 ++++++++------- 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index c2f36766c7..e4d0065824 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -41,7 +41,7 @@ void expectFailure(TAction action, const char* expected, const char* message) { try { action(); } catch (const std::runtime_error& error) { - require(std::string(error.what()).find(expected) != std::string::npos, "unexpected failure reason"); + if (std::string(error.what()).find(expected) == std::string::npos) throw std::runtime_error(std::string(message) + ": expected " + expected + ", got " + error.what()); return; } throw std::runtime_error(message); diff --git a/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp b/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp index 510c08d56a..c331158863 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp @@ -16,6 +16,7 @@ std::uint32_t PointFilteredSamplerWord(std::uint32_t word0, std::uint32_t filter class DescriptorBindingBuilder { public: + void ValidateSamplers(const ShaderInfo& info, const ResourceSnapshot& snapshot) const; void Prepare(BindingAllocationResult& allocation, const ShaderInfo& info, IrShaderStage stage) const; [[nodiscard]] MaterializedBindings Materialize(const BindingAllocationResult& allocation, const ShaderInfo& info, std::uint32_t userDataBase, const ResourceSnapshot& snapshot, const std::array& partialThreads) const; void Populate(BindingAllocationResult& allocation, const IrProgram& program, const ResourceSnapshot& snapshot, const std::array& partialThreads) const; diff --git a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp index 5bd37fb67e..4aa25fca51 100644 --- a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp +++ b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp @@ -294,6 +294,10 @@ std::uint32_t PointFilteredSamplerWord(std::uint32_t word0, std::uint32_t filter return (filter & ~(0xffu << 20u)) | (1u << 24u) | (mipmapped ? 1u << 26u : 0u); } +void DescriptorBindingBuilder::ValidateSamplers(const ShaderInfo& info, const ResourceSnapshot& snapshot) const { + static_cast(ProveUnnormalized(info, snapshot)); +} + void DescriptorBindingBuilder::Populate(BindingAllocationResult& allocation, const IrProgram& program, const ResourceSnapshot& snapshot, const std::array& partialThreads) const { Populate(allocation, program.Info(), program.Resources().stage, program.Resources().userDataBase, snapshot, partialThreads); } diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 356141b60d..1e456cc553 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -304,7 +304,7 @@ std::uint64_t nextVariantId() { return variants.fetch_add(1, std::memory_order_relaxed) + 1; } -CompiledVariant compileVariant(const RecompileRequest& request, IrProgram program, const ResourceSpecialization& resourceSpecialization, std::exception_ptr* emissionFailure = nullptr) { +CompiledVariant compileVariant(const RecompileRequest& request, IrProgram program, const ResourceSnapshot& resourceSnapshot, const ResourceSpecialization& resourceSpecialization, std::exception_ptr* emissionFailure = nullptr) { const auto inputInfo = RequestInputInfo(request); constexpr DeadCodeEliminator deadCodeEliminator; constexpr ResourceMaterializer resourceMaterializer; @@ -320,6 +320,7 @@ CompiledVariant compileVariant(const RecompileRequest& request, IrProgram progra auto bindings = bindingAllocator.Allocate(program, request.layout); constexpr DescriptorBindingBuilder descriptorBindingBuilder; + descriptorBindingBuilder.ValidateSamplers(program.Info(), resourceSnapshot); descriptorBindingBuilder.Prepare(bindings, program.Info(), program.Resources().stage); SpirvTargetOptions targetOptions {}; @@ -391,7 +392,7 @@ bool sameLayout(const BindingLayout& left, const BindingLayout& right) { return left.descriptorSet == right.descriptorSet && left.firstBinding == right.firstBinding && left.pushConstantOffsetBytes == right.pushConstantOffsetBytes && left.pushConstantSizeBytes == right.pushConstantSizeBytes; } -std::shared_ptr findOrCompileVariant(SourceEntry& source, const RecompileRequest& request, const ResourceSpecialization& specialization, bool& cacheHit) { +std::shared_ptr findOrCompileVariant(SourceEntry& source, const RecompileRequest& request, const ResourceSnapshot& snapshot, const ResourceSpecialization& specialization, bool& cacheHit) { for (const auto& candidate : source.variants) { if (sameLayout(candidate->layout, request.layout) && candidate->specialization == specialization) { cacheHit = true; @@ -420,7 +421,7 @@ std::shared_ptr findOrCompileVariant(SourceEntry& source, source.program.reset(); std::exception_ptr emissionFailure; try { - variant = std::make_shared(compileVariant(request, std::move(program), specialization, &emissionFailure)); + variant = std::make_shared(compileVariant(request, std::move(program), snapshot, specialization, &emissionFailure)); } catch (...) { if (emissionFailure != nullptr && FailureMemo()) source.emissionFailures.push_back({request.shader.codeAddress, request.layout, specialization, emissionFailure}); throw; @@ -438,7 +439,7 @@ RecompileResult materializeVariant(SourceEntry& source, const RecompileRequest& bool cacheHit = false; { std::lock_guard lock(source.mutex); - variant = findOrCompileVariant(source, request, specialization, cacheHit); + variant = findOrCompileVariant(source, request, snapshot, specialization, cacheHit); } auto result = materializeResult(*variant, request, snapshot); result.cacheHit = cacheHit; @@ -456,7 +457,7 @@ RecompileResult RecompileImpl(const RecompileRequest& request) { auto program = PrepareResourceProgram(request); const auto plan = materializer.ExtractPlan(program); materializer.Materialize(plan, runtime, snapshot, specialization); - const auto variant = compileVariant(request, std::move(program), specialization); + const auto variant = compileVariant(request, std::move(program), snapshot, specialization); return materializeResult(variant, request, snapshot); } const auto source = getSource(request); @@ -556,7 +557,7 @@ std::shared_ptr materializeMemoized(SourceEntry& source, auto& counters = resultMemoCounters(); { std::lock_guard lock(source.mutex); - variant = findOrCompileVariant(source, request, specialization, cacheHit); + variant = findOrCompileVariant(source, request, snapshot, specialization, cacheHit); index = (variant->result.variantId * 0x9e3779b97f4a7c15ull) ^ hash; const auto found = source.memoIndex.find(index); if (found != source.memoIndex.end() && found->second->variantId == variant->result.variantId && found->second->hash == hash) { @@ -605,7 +606,7 @@ std::shared_ptr materializeMemoized(SourceEntry& source, std::shared_ptr RecompileImpl(const RecompileRequest& request, const ResourceCapture& capture, bool* memoHit) { if (!request.useCache || capture.source == nullptr) { auto program = PrepareResourceProgram(request); - const auto variant = compileVariant(request, std::move(program), capture.specialization); + const auto variant = compileVariant(request, std::move(program), capture.snapshot, capture.specialization); return std::make_shared(materializeResult(variant, request, capture.snapshot)); } if (!ResultMemo()) return std::make_shared(materializeVariant(*capture.source, request, capture.snapshot, capture.specialization));