Files
AnyPS5/core/libs/prx/libSceAgcDriver/tests/ShaderDeviceProfile.cpp
T
Arvind FrøilandandClaude Opus 5.5 7216621545 fix(agc): typed image heaps on devices without nullDescriptor
The typed descriptor heaps write a null descriptor for a null guest
image, and the device therefore required VK_EXT_robustness2's
nullDescriptor. MoltenVK has the extension but not the feature, so on
macOS no Vulkan device opened and every GPU test was skipped. The
feature is now enabled when the device has it. Without it, the device
creates a placeholder image per typed image heap: 1x1, cleared to zero,
of the heap's dimension, sample count, numeric class and access, and a
null guest image binds the placeholder of its heap. A heap whose
placeholder the device cannot create throws when a null image reaches
it. TechnicalDebt.md lists where a placeholder differs from a null
descriptor.

agc_placeholder_images checks every image resource the recompiler maps
to a heap against that heap's placeholder, so the two cannot drift
apart, and agc_shader_device_profile now expects a device without
nullDescriptor to be accepted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 10:32:49 +02:00

199 lines
13 KiB
C++

#include "prx/libSceAgcDriver/Execution/include/ShaderDeviceProfile.hpp"
#include "BdaAbi.hpp"
#include "Optimization/BindingAllocator.hpp"
#include "Optimization/ResourceMaterializer.hpp"
#include <spirv/unified1/spirv.hpp>
#include <algorithm>
#include <array>
#include <iostream>
#include <set>
#include <stdexcept>
#include <string>
#include <type_traits>
namespace {
using AgcDriver::Graphics::Require;
struct ProfileInput {
std::vector<std::uint32_t> capabilities{spv::CapabilityShader, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess};
std::array<std::string, 2> extensionStrings{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"};
std::array<std::string_view, 2> extensions{extensionStrings[0], extensionStrings[1]};
VkPhysicalDeviceRobustness2FeaturesEXT robustness{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ROBUSTNESS_2_FEATURES_EXT, nullptr, VK_FALSE, VK_FALSE, VK_TRUE};
VkPhysicalDeviceDescriptorIndexingFeatures indexing{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_INDEXING_FEATURES, &robustness};
VkPhysicalDevice8BitStorageFeatures bytes{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_8BIT_STORAGE_FEATURES, &indexing, VK_TRUE};
VkPhysicalDeviceBufferDeviceAddressFeatures bda{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_BUFFER_DEVICE_ADDRESS_FEATURES, &bytes, VK_TRUE};
VkPhysicalDeviceFeatures core{};
VkDeviceCreateInfo device{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO};
VkPhysicalDeviceLimits limits{};
ProfileInput() {
core.shaderInt64 = VK_TRUE;
core.vertexPipelineStoresAndAtomics = VK_TRUE;
core.fragmentStoresAndAtomics = VK_TRUE;
device.pEnabledFeatures = &core;
device.pNext = &bda;
limits.maxPushConstantsSize = 128u;
limits.maxBoundDescriptorSets = 1u;
limits.maxStorageBufferRange = sizeof(ShaderRecompiler::RuntimeAbi::ShaderData);
}
ShaderRecompiler::SpirvTarget Target() const {
ShaderRecompiler::SpirvTarget target{};
target.bdaAbiVersion = ShaderRecompiler::BdaAbi::Version;
target.supportedCapabilities = capabilities;
target.supportedExtensions = extensions;
return target;
}
};
template<typename TAction>
void Reject(TAction action, const char* expected) {
try {
action();
} catch (const std::runtime_error& error) {
Require(std::string(error.what()).find(expected) != std::string::npos, "unexpected profile validation error");
return;
}
throw std::runtime_error("invalid shader device profile was accepted");
}
void CheckProfile() {
ProfileInput input;
const AgcDriver::ShaderDeviceProfile profile(input.Target(), input.device, input.limits);
input.capabilities.clear();
input.extensionStrings[0] = "changed";
input.core.shaderInt64 = VK_FALSE;
input.robustness.nullDescriptor = VK_FALSE;
input.limits.maxPushConstantsSize = 0u;
const auto target = profile.Target();
Require(target.supportedCapabilities.size() == 4u && std::ranges::is_sorted(target.supportedCapabilities), "profile capabilities were not frozen");
Require(std::ranges::find(target.supportedExtensions, "SPV_KHR_physical_storage_buffer") != target.supportedExtensions.end(), "profile extension storage is borrowed");
Require(profile.Limits().maxPushConstantsSize == 128u, "profile limits were not frozen");
Require(profile.NullDescriptors(), "profile null descriptor support was not frozen");
static_assert(!std::is_copy_constructible_v<AgcDriver::ShaderDeviceProfile> && !std::is_move_constructible_v<AgcDriver::ShaderDeviceProfile>);
const auto invalid = [](auto mutate, const char* expected) {
ProfileInput candidate;
mutate(candidate);
Reject([&] { AgcDriver::ShaderDeviceProfile rejected(candidate.Target(), candidate.device, candidate.limits); }, expected);
};
invalid([](auto& value) { value.device.pEnabledFeatures = nullptr; }, "enabled core features");
invalid([](auto& value) { value.bda.bufferDeviceAddress = VK_FALSE; }, "bufferDeviceAddress");
invalid([](auto& value) { value.device.pNext = nullptr; }, "bufferDeviceAddress");
invalid([](auto& value) { value.core.shaderInt64 = VK_FALSE; }, "shaderInt64");
invalid([](auto& value) { value.bytes.storageBuffer8BitAccess = VK_FALSE; }, "storageBuffer8BitAccess");
ProfileInput withoutNull;
withoutNull.robustness.nullDescriptor = VK_FALSE;
Require(!AgcDriver::ShaderDeviceProfile(withoutNull.Target(), withoutNull.device, withoutNull.limits).NullDescriptors(), "a device without nullDescriptor was profiled with it");
ProfileInput withoutRobustness;
withoutRobustness.indexing.pNext = nullptr;
Require(!AgcDriver::ShaderDeviceProfile(withoutRobustness.Target(), withoutRobustness.device, withoutRobustness.limits).NullDescriptors(), "a device without VK_EXT_robustness2 was profiled with nullDescriptor");
invalid([](auto& value) { value.core.fragmentStoresAndAtomics = VK_FALSE; }, "graphics stores and atomics");
invalid([](auto& value) { value.limits.maxPushConstantsSize = 127u; }, "exceeds device limits");
invalid([](auto& value) { value.limits.maxBoundDescriptorSets = 0u; }, "exceeds device limits");
invalid([](auto& value) { --value.limits.maxStorageBufferRange; }, "ShaderData exceeds device limits");
invalid([](auto& value) { value.capabilities.clear(); }, "missing runtime capabilities");
invalid([](auto& value) { value.extensions[0] = "missing"; }, "missing runtime extensions");
invalid([](auto& value) { value.capabilities.push_back(spv::CapabilitySampledImageArrayDynamicIndexing); }, "shaderSampledImageArrayDynamicIndexing");
invalid([](auto& value) { value.capabilities.push_back(spv::CapabilityStorageImageArrayNonUniformIndexing); }, "shaderStorageImageArrayNonUniformIndexing");
invalid([](auto& value) { value.capabilities.push_back(spv::CapabilityStorageImageReadWithoutFormat); }, "shaderStorageImageReadWithoutFormat");
invalid([](auto& value) { value.capabilities.push_back(spv::CapabilityStorageImageMultisample); }, "shaderStorageImageMultisample");
invalid([](auto& value) { value.capabilities.push_back(spv::CapabilityFragmentBarycentricKHR); }, "fragmentShaderBarycentric");
invalid([](auto& value) { value.capabilities.push_back(spv::CapabilityMeshShadingEXT); }, "meshShader");
ProfileInput enabled;
enabled.capabilities.push_back(spv::CapabilitySampledImageArrayDynamicIndexing);
enabled.capabilities.push_back(spv::CapabilityStorageImageArrayNonUniformIndexing);
enabled.core.shaderSampledImageArrayDynamicIndexing = VK_TRUE;
enabled.indexing.shaderStorageImageArrayNonUniformIndexing = VK_TRUE;
const AgcDriver::ShaderDeviceProfile accepted(enabled.Target(), enabled.device, enabled.limits);
Require(accepted.Target().supportedCapabilities.size() == 6u, "enabled descriptor indexing was rejected");
}
void CheckAbi() {
using ShaderRecompiler::RuntimeAbi::Binding;
using ShaderRecompiler::RuntimeAbi::BindingNumber;
using ShaderRecompiler::RuntimeAbi::Stage;
std::set<std::uint32_t> bindings;
for (std::uint32_t stage = 0u; stage < ShaderRecompiler::RuntimeAbi::StageCount; ++stage) {
for (std::uint32_t binding = 0u; binding < static_cast<std::uint32_t>(Binding::Count); ++binding) {
Require(bindings.insert(BindingNumber(static_cast<Stage>(stage), static_cast<Binding>(binding))).second, "runtime ABI bindings overlap");
}
}
Require(BindingNumber(Stage::Main, Binding::ShaderData) == 62u && BindingNumber(Stage::Fragment, Binding::ShaderData) == 125u, "runtime ABI binding numbers changed");
Reject([] { BindingNumber(static_cast<Stage>(4u), Binding::Buffers); }, "invalid stage or binding");
Reject([] { BindingNumber(Stage::Main, Binding::Count); }, "invalid stage or binding");
Reject([] { ShaderRecompiler::RuntimeAbi::RequireVersion(0u); }, "incompatible version");
ShaderRecompiler::RuntimeAbi::RequireVersion(ShaderRecompiler::RuntimeAbi::Version);
}
void CheckHeaps() {
using namespace ShaderRecompiler;
const auto allocate = [](const ImageResource& image, std::uint32_t count, std::uint32_t samplers = 0u) {
IrProgram program;
program.Metadata().shaderInfoComplete = true;
program.Resources().info.images.assign(count, image);
program.Resources().info.samplers.resize(samplers);
return BindingAllocator{}.Allocate(program, {0u, 0u, 0u, 128u});
};
ImageResource image;
image.resourceClass = ImageResourceClass::Sampled;
image.numericClass = IrTextureNumericClass::Float;
image.dimension = RdnaImageDimension::Dim2D;
const auto single = allocate(image, 1u);
const auto sixteenSamplers = allocate(image, 1u, 16u);
Require(BindingAllocator{}.FindBinding(sixteenSamplers.layout, DescriptorBindingKind::Samplers).resources.size() == 32u, "sixteen logical samplers must fit with both descriptor variants");
const auto full = allocate(image, RuntimeAbi::SampledHeapCapacity, RuntimeAbi::SamplerHeapCapacity / 2u);
Require(single.layout.ShaderDataDwords() == full.layout.ShaderDataDwords() && single.layout.memoryOffsetDword == full.layout.memoryOffsetDword && !full.layout.UsesPushData(), "runtime layout depends on resource count");
Require(full.layout.memoryOffsetDword == 0u && full.layout.DispatchThreadLimitDword() == 0u && full.layout.ShaderDataDwords() == 0u, "direct image resources allocated runtime metadata");
Reject([&] { allocate(image, RuntimeAbi::SampledHeapCapacity + 1u); }, "heap capacity exceeded");
Require(BindingAllocator{}.FindBinding(allocate(image, 40u).layout, DescriptorBindingForImage(image)).resources.size() == 40u, "a sampled heap does not hold 40 images of one class");
Require(BindingAllocator{}.FindBinding(allocate(image, RuntimeAbi::SampledHeapCapacity).layout, DescriptorBindingForImage(image)).resources.size() == RuntimeAbi::SampledHeapCapacity, "a sampled heap does not hold a full class of images");
Require(ResourceMaterializer::BindlessSlots() == 16u, "bindless image tables changed size with the sampled heap");
Reject([&] { allocate(image, 1u, RuntimeAbi::SamplerHeapCapacity + 1u); }, "metadata capacity");
image.resourceClass = ImageResourceClass::Storage;
image.mipMode = ImageMipMode::DynamicStorage;
image.mipCount = RuntimeAbi::StorageMipSlots;
const auto storage = allocate(image, 1u);
Require(BindingAllocator{}.FindBinding(storage.layout, DescriptorBindingForImage(image)).resources.size() == image.mipCount, "storage heap did not reserve each mip");
const auto shared = allocate(image, 2u);
Require(BindingAllocator{}.FindBinding(shared.layout, DescriptorBindingForImage(image)).resources.size() == 2u * image.mipCount, "a storage heap did not hold two dynamic-mip images");
const auto capacity = RuntimeAbi::StorageHeapCapacity / image.mipCount;
const auto filled = allocate(image, capacity);
Require(BindingAllocator{}.FindBinding(filled.layout, DescriptorBindingForImage(image)).resources.size() == RuntimeAbi::StorageHeapCapacity, "a full storage heap was not allocated");
Reject([&] { allocate(image, capacity + 1u); }, "heap capacity exceeded");
Reject([&] { allocate(image, 0u, RuntimeAbi::SamplerHeapCapacity / 2u + 1u); }, "sampler pairs");
const std::array dimensions{RdnaImageDimension::Dim1D, RdnaImageDimension::Dim1DArray, RdnaImageDimension::Dim2D, RdnaImageDimension::Dim2DArray, RdnaImageDimension::Dim3D, RdnaImageDimension::Dim2DMsaa, RdnaImageDimension::Dim2DMsaaArray};
std::set<std::uint32_t> classes;
for (std::uint32_t group = 0u; group < 8u; ++group) {
for (const auto dimension : dimensions) {
image = {};
image.resourceClass = group < 4u ? ImageResourceClass::Sampled : ImageResourceClass::Storage;
image.numericClass = group == 1u || group >= 5u ? IrTextureNumericClass::Uint : group == 2u ? IrTextureNumericClass::Sint : IrTextureNumericClass::Float;
image.depthCompare = group == 3u;
image.atomic = group >= 6u;
image.atomic64 = group == 7u;
image.dimension = dimension;
const auto binding = DescriptorBindingForImage(image);
Require(classes.insert(static_cast<std::uint32_t>(binding)).second, "typed image classes overlap");
Require(RuntimeAbi::HeapCapacity(binding) == (group < 4u ? RuntimeAbi::SampledHeapCapacity : RuntimeAbi::StorageHeapCapacity), "typed image class has an invalid capacity");
}
}
Require(classes.size() == RuntimeAbi::ImageBindingCount && *classes.begin() == 1u && *classes.rbegin() == RuntimeAbi::ImageBindingCount, "typed image class mapping is incomplete");
Reject([] { RuntimeAbi::HeapCapacity(RuntimeAbi::Binding::ShaderData); }, "not a typed heap");
}
}
int main() {
try {
CheckAbi();
CheckHeaps();
CheckProfile();
std::cout << "shader runtime ABI and device profile tests passed\n";
return 0;
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
}