AnyPS5/core/libs/prx/libSceAgcDriver/tests/ShaderDeviceProfile.cpp

195 lines
12 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");
invalid([](auto& value) { value.robustness.nullDescriptor = VK_FALSE; }, "nullDescriptor");
invalid([](auto& value) { value.indexing.pNext = nullptr; }, "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;
}
}