From 72166215452da9bd89619f3dc8f2df04ddd332fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arvind=20Fr=C3=B8iland?= Date: Fri, 9 Oct 2026 14:05:52 +0900 Subject: [PATCH] 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 Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 11 +- .../Execution/src/ShaderDeviceProfile.cpp | 1 - .../Execution/src/VulkanDevice.cpp | 33 ++++-- .../Graphics/include/Context.hpp | 2 + .../Graphics/include/PlaceholderImages.hpp | 75 ++++++++++++ .../Graphics/src/PlaceholderImages.cpp | 111 ++++++++++++++++++ .../Graphics/src/ShaderResources.cpp | 12 +- .../tests/PlaceholderImages.cpp | 98 ++++++++++++++++ .../tests/ShaderDeviceProfile.cpp | 8 +- docs/dev/TechnicalDebt.md | 1 + 10 files changed, 334 insertions(+), 18 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/Graphics/include/PlaceholderImages.hpp create mode 100644 core/libs/prx/libSceAgcDriver/Graphics/src/PlaceholderImages.cpp create mode 100644 core/libs/prx/libSceAgcDriver/tests/PlaceholderImages.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 8357c6c20..7232e14a3 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -179,6 +179,7 @@ set(agcDriverSources ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/PipelineLibrary.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/PipelineCache.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/Resources.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/PlaceholderImages.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/Recorder.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/ParallelCompare.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/ShaderInputState.cpp @@ -572,6 +573,12 @@ target_link_libraries(agc_shader_disk_cache_tests PRIVATE shader_recompiler_agc_ set_target_properties(agc_shader_disk_cache_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_shader_disk_cache_tests) +add_test_executable(agc_placeholder_images_tests EXCLUDE_FROM_ALL tests/PlaceholderImages.cpp) +target_include_directories(agc_placeholder_images_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/3rdparty/Vulkan-Headers/include) +target_link_libraries(agc_placeholder_images_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_placeholder_images_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_placeholder_images_tests) + add_test_executable(agc_shader_device_profile_tests EXCLUDE_FROM_ALL tests/ShaderDeviceProfile.cpp Execution/src/ShaderDeviceProfile.cpp) target_include_directories(agc_shader_device_profile_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) target_link_libraries(agc_shader_device_profile_tests PRIVATE shader_recompiler_agc_driver libc) @@ -804,6 +811,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/PlaceholderImages.cpp Graphics/src/ShaderResources.cpp Graphics/src/ShaderAddressResources.cpp Graphics/src/GuestBufferMemory.cpp @@ -860,6 +868,7 @@ add_test_executable(agc_driver_bda_device_tests EXCLUDE_FROM_ALL tests/BdaContracts.cpp Execution/src/BdaFeatures.cpp Graphics/src/Resources.cpp + Graphics/src/PlaceholderImages.cpp Graphics/src/BufferAddress.cpp ) @@ -960,7 +969,7 @@ if(BUILD_TESTING) if(WIN32) set_tests_properties(agc_shader_bindless agc_shader_frontend_pair agc_shader_frontend_pair_off PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() - foreach(agcTest IN ITEMS agc_shared_write_tests agc_driver_tests agc_shader_memory_tests agc_structurizer_tests agc_spirv_module_tests agc_dpp_write_tests agc_masked_select_tests agc_constant_bit_test_tests agc_scalar_select_mask_tests agc_vop1_control_decode_tests agc_validate_program_tests agc_pass_hazards_tests agc_shader_disk_cache_tests agc_shader_device_profile_tests agc_shader_control_flow_tests agc_driver_flip_tests agc_driver_pm4_tests agc_driver_graphics_tests agc_driver_bda_device_tests agc_driver_mesh_tests agc_driver_cmask_fast_clear_tests agc_formatted_buffer_once_tests ${agcExecutionTests}) + foreach(agcTest IN ITEMS agc_shared_write_tests agc_driver_tests agc_shader_memory_tests agc_structurizer_tests agc_spirv_module_tests agc_dpp_write_tests agc_masked_select_tests agc_constant_bit_test_tests agc_scalar_select_mask_tests agc_vop1_control_decode_tests agc_validate_program_tests agc_pass_hazards_tests agc_shader_disk_cache_tests agc_shader_device_profile_tests agc_placeholder_images_tests agc_shader_control_flow_tests agc_driver_flip_tests agc_driver_pm4_tests agc_driver_graphics_tests agc_driver_bda_device_tests agc_driver_mesh_tests agc_driver_cmask_fast_clear_tests agc_formatted_buffer_once_tests ${agcExecutionTests}) if(agcTest STREQUAL "agc_driver_constant_store_tests" OR agcTest STREQUAL "agc_driver_flat_apertures_tests") continue() endif() diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/ShaderDeviceProfile.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/ShaderDeviceProfile.cpp index 5eec16c82..3d3794279 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/ShaderDeviceProfile.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/ShaderDeviceProfile.cpp @@ -34,7 +34,6 @@ ShaderDeviceProfile::ShaderDeviceProfile(const ShaderRecompiler::SpirvTarget& ta Require(target.bdaAbiVersion == ShaderRecompiler::BdaAbi::Version, "shader device profile has an incompatible BDA ABI"); Require(bda != nullptr && bda->bufferDeviceAddress == VK_TRUE && core.shaderInt64 == VK_TRUE, "shader runtime requires enabled bufferDeviceAddress and shaderInt64"); Require(bytes != nullptr && bytes->storageBuffer8BitAccess == VK_TRUE, "shader runtime requires enabled storageBuffer8BitAccess"); - Require(nullDescriptors, "shader runtime requires enabled nullDescriptor"); Require(core.vertexPipelineStoresAndAtomics == VK_TRUE && core.fragmentStoresAndAtomics == VK_TRUE, "shader runtime requires enabled graphics stores and atomics"); Require(limits.maxPushConstantsSize >= ShaderRecompiler::RuntimeAbi::PushConstantDwords * sizeof(std::uint32_t) && limits.maxBoundDescriptorSets > ShaderRecompiler::RuntimeAbi::DescriptorSet, "shader runtime ABI exceeds device limits"); Require(limits.maxStorageBufferRange >= sizeof(ShaderRecompiler::RuntimeAbi::ShaderData), "shader runtime ShaderData exceeds device limits"); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index b6ed20d7d..246837582 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -11,6 +11,7 @@ #include "prx/libSceAgcDriver/Execution/include/ShaderDeviceProfile.hpp" #include "prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp" #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include "prx/libSceAgcDriver/Graphics/include/PlaceholderImages.hpp" #include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" #include "prx/libSceAgcDriver/Execution/include/DisplayFormat.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" @@ -283,6 +284,7 @@ struct VulkanDevice::State { std::unique_ptr colorTransfer; std::shared_ptr bufferPool; std::unique_ptr emptyBuffer; + std::unique_ptr placeholderImages; std::unique_ptr textureCache; std::unique_ptr pipelineCache; std::unique_ptr descriptorCache; @@ -616,6 +618,7 @@ struct VulkanDevice::State { if (workgroupMemoryLayout != VK_NULL_HANDLE) reinterpret_cast(deviceProc(device, "vkDestroyDescriptorSetLayout"))(device, workgroupMemoryLayout, nullptr); descriptorCache.reset(); emptyBuffer.reset(); + placeholderImages.reset(); samplerCache.reset(); textureCache.reset(); detiler.reset(); @@ -1287,17 +1290,19 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u } bdaFeatures.pNext = &byteFeatures; VkPhysicalDeviceRobustness2FeaturesEXT robustness2{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ROBUSTNESS_2_FEATURES_EXT}; - Graphics::Require(hasExtension(VK_EXT_ROBUSTNESS_2_EXTENSION_NAME), "shader runtime requires VK_EXT_robustness2"); - VkPhysicalDeviceFeatures2 queried{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &robustness2}; - state->InstanceFunction("vkGetPhysicalDeviceFeatures2")(selected, &queried); - Graphics::Require(robustness2.nullDescriptor == VK_TRUE, "shader runtime requires nullDescriptor"); - robustness2 = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ROBUSTNESS_2_FEATURES_EXT}; - robustness2.nullDescriptor = VK_TRUE; - robustness2.pNext = bdaFeatures.pNext; - bdaFeatures.pNext = &robustness2; - deviceExtensions.push_back(VK_EXT_ROBUSTNESS_2_EXTENSION_NAME); - deviceInfo.enabledExtensionCount = static_cast(deviceExtensions.size()); - deviceInfo.ppEnabledExtensionNames = deviceExtensions.data(); + if (hasExtension(VK_EXT_ROBUSTNESS_2_EXTENSION_NAME)) { + VkPhysicalDeviceFeatures2 queried{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &robustness2}; + state->InstanceFunction("vkGetPhysicalDeviceFeatures2")(selected, &queried); + } + if (robustness2.nullDescriptor == VK_TRUE) { + robustness2 = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ROBUSTNESS_2_FEATURES_EXT}; + robustness2.nullDescriptor = VK_TRUE; + robustness2.pNext = bdaFeatures.pNext; + bdaFeatures.pNext = &robustness2; + deviceExtensions.push_back(VK_EXT_ROBUSTNESS_2_EXTENSION_NAME); + deviceInfo.enabledExtensionCount = static_cast(deviceExtensions.size()); + deviceInfo.ppEnabledExtensionNames = deviceExtensions.data(); + } deviceInfo.pNext = &bdaFeatures; auto shaderProfile = std::make_unique(buildTarget(), deviceInfo, state->properties.limits); check(state->InstanceFunction("vkCreateDevice")(selected, &deviceInfo, nullptr, &state->device), "vkCreateDevice"); @@ -1323,6 +1328,11 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u state->descriptorCache = std::make_unique(graphicsContext()); state->samplerCache = std::make_unique(); state->CreateWorkgroupMemoryLayout(); + if (!state->shaderProfile->NullDescriptors()) { + std::unique_lock gpuLock(GuestMemory::GpuMutex(), std::defer_lock); + if (!GuestMemory::GpuMutex().HeldByThisThread()) gpuLock.lock(); + state->placeholderImages = std::make_unique(graphicsContext()); + } state->recorder = std::make_unique(graphicsContext(), state->timelineSemaphores); state->recorder->Activate(); state->context = buildContext(); @@ -2758,6 +2768,7 @@ Graphics::Context VulkanDevice::buildContext() const { context.sampleRateShading = state->sampleRateShading; context.nullDescriptors = state->shaderProfile != nullptr && state->shaderProfile->NullDescriptors(); context.bufferInt64Atomics = state->bufferInt64Atomics; + context.placeholderImages = state->placeholderImages.get(); context.primitiveListRestart = state->primitiveListRestart; context.imageViewMinLod = state->imageViewMinLod; context.pipelineExecutableInfo = state->pipelineExecutableInfo; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp index 83a15159e..4b8cc7e4c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp @@ -24,6 +24,7 @@ class Recorder; class DescriptorCache; class SamplerCache; class ShaderResources; +class PlaceholderImages; inline void Require(bool condition, const std::string& reason) { if (!condition) throw std::runtime_error("AGC graphics: " + reason); @@ -159,6 +160,7 @@ struct Context { bool sampleRateShading = false; bool nullDescriptors = false; bool bufferInt64Atomics = false; + const PlaceholderImages* placeholderImages = nullptr; bool primitiveListRestart = false; bool imageViewMinLod = false; bool pipelineExecutableInfo = false; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/PlaceholderImages.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/PlaceholderImages.hpp new file mode 100644 index 000000000..7e9a4dc18 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/PlaceholderImages.hpp @@ -0,0 +1,75 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_PLACEHOLDERIMAGES_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_PLACEHOLDERIMAGES_HPP + +#include "prx/libSceAgcDriver/Graphics/include/Context.hpp" +#include "RuntimeAbi.hpp" +#include +#include + +namespace AgcDriver::Graphics { + +inline constexpr std::uint32_t PlaceholderDimensionsPerClass = (ShaderRecompiler::RuntimeAbi::FirstComparisonImageBinding - ShaderRecompiler::RuntimeAbi::FirstImageBinding) / 3u; +static_assert(ShaderRecompiler::RuntimeAbi::FirstStorageImageBinding == ShaderRecompiler::RuntimeAbi::FirstComparisonImageBinding + PlaceholderDimensionsPerClass); +static_assert(ShaderRecompiler::RuntimeAbi::ImageBindingCount == 8u * PlaceholderDimensionsPerClass && PlaceholderDimensionsPerClass == 7u); + +struct PlaceholderImageShape { + VkFormat format; + VkFormatFeatureFlags features; + VkImageUsageFlags usage; + VkImageAspectFlags aspect; + VkImageType type; + VkImageViewType viewType; + VkSampleCountFlagBits samples; +}; + +constexpr PlaceholderImageShape PlaceholderImageShapeOf(std::uint32_t heap) { + constexpr std::array formats{VK_FORMAT_R8G8B8A8_UNORM, VK_FORMAT_R32_UINT, VK_FORMAT_R32_SINT, VK_FORMAT_D16_UNORM, + VK_FORMAT_R8G8B8A8_UNORM, VK_FORMAT_R32_UINT, VK_FORMAT_R32_UINT, VK_FORMAT_R64_UINT}; + constexpr std::array types{VK_IMAGE_TYPE_1D, VK_IMAGE_TYPE_1D, VK_IMAGE_TYPE_2D, VK_IMAGE_TYPE_2D, + VK_IMAGE_TYPE_2D, VK_IMAGE_TYPE_2D, VK_IMAGE_TYPE_3D}; + constexpr std::array views{VK_IMAGE_VIEW_TYPE_1D, VK_IMAGE_VIEW_TYPE_1D_ARRAY, VK_IMAGE_VIEW_TYPE_2D, + VK_IMAGE_VIEW_TYPE_2D_ARRAY, VK_IMAGE_VIEW_TYPE_2D, VK_IMAGE_VIEW_TYPE_2D_ARRAY, + VK_IMAGE_VIEW_TYPE_3D}; + constexpr std::array samples{VK_SAMPLE_COUNT_1_BIT, VK_SAMPLE_COUNT_1_BIT, VK_SAMPLE_COUNT_1_BIT, + VK_SAMPLE_COUNT_1_BIT, VK_SAMPLE_COUNT_4_BIT, VK_SAMPLE_COUNT_4_BIT, + VK_SAMPLE_COUNT_1_BIT}; + const auto index = heap - ShaderRecompiler::RuntimeAbi::FirstImageBinding; + const auto kind = index / PlaceholderDimensionsPerClass; + const auto format = formats[kind]; + const auto dimension = index % PlaceholderDimensionsPerClass; + const bool storage = heap >= ShaderRecompiler::RuntimeAbi::FirstStorageImageBinding; + const bool atomic = kind >= 6u; + const VkFormatFeatureFlags access = atomic ? VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT | VK_FORMAT_FEATURE_STORAGE_IMAGE_ATOMIC_BIT + : storage ? VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT + : VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT; + return {format, + static_cast(access | VK_FORMAT_FEATURE_TRANSFER_DST_BIT), + static_cast((storage ? VK_IMAGE_USAGE_STORAGE_BIT : VK_IMAGE_USAGE_SAMPLED_BIT) | VK_IMAGE_USAGE_TRANSFER_DST_BIT), + static_cast(format == VK_FORMAT_D16_UNORM ? VK_IMAGE_ASPECT_DEPTH_BIT : VK_IMAGE_ASPECT_COLOR_BIT), + types[dimension], + views[dimension], + samples[dimension]}; +} + +class PlaceholderImages { +public: + explicit PlaceholderImages(const Context& context); + ~PlaceholderImages(); + PlaceholderImages(const PlaceholderImages&) = delete; + PlaceholderImages& operator=(const PlaceholderImages&) = delete; + VkImageView View(std::uint32_t heap) const; + +private: + struct Image { + VkImage image = VK_NULL_HANDLE; + VkDeviceMemory memory = VK_NULL_HANDLE; + VkImageView view = VK_NULL_HANDLE; + }; + void release() noexcept; + Context context; + std::array images{}; +}; + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PlaceholderImages.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PlaceholderImages.cpp new file mode 100644 index 000000000..cc352d0d9 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PlaceholderImages.cpp @@ -0,0 +1,111 @@ +#include "prx/libSceAgcDriver/Graphics/include/PlaceholderImages.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" +#include +#include + +namespace AgcDriver::Graphics { +namespace { + +bool Supported(const Context& context, const PlaceholderImageShape& shape) { + VkFormatProperties properties{}; + context.formatProperties(context.physical, shape.format, &properties); + if ((properties.optimalTilingFeatures & shape.features) != shape.features) return false; + VkImageFormatProperties limits{}; + if (context.imageFormatProperties(context.physical, shape.format, shape.type, VK_IMAGE_TILING_OPTIMAL, shape.usage, 0, &limits) != VK_SUCCESS) return false; + return (limits.sampleCounts & shape.samples) != 0; +} + +} + +PlaceholderImages::PlaceholderImages(const Context& context) : context(context) { + try { + std::vector toClear; + for (std::uint32_t index = 0; index < images.size(); ++index) { + const auto shape = PlaceholderImageShapeOf(ShaderRecompiler::RuntimeAbi::FirstImageBinding + index); + if (!Supported(context, shape)) continue; + auto& image = images[index]; + VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + info.imageType = shape.type; + info.format = shape.format; + info.extent = {1, 1, 1}; + info.mipLevels = 1; + info.arrayLayers = 1; + info.samples = shape.samples; + info.tiling = VK_IMAGE_TILING_OPTIMAL; + info.usage = shape.usage; + info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + Check(context.Function("vkCreateImage")(context.device, &info, nullptr, &image.image), "vkCreateImage placeholder"); + VkMemoryRequirements requirements{}; + context.Function("vkGetImageMemoryRequirements")(context.device, image.image, &requirements); + VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocation.allocationSize = requirements.size; + allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + Check(context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &image.memory), "vkAllocateMemory placeholder"); + Check(context.Function("vkBindImageMemory")(context.device, image.image, image.memory, 0), "vkBindImageMemory placeholder"); + VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; + viewInfo.image = image.image; + viewInfo.viewType = shape.viewType; + viewInfo.format = shape.format; + viewInfo.subresourceRange = {shape.aspect, 0, 1, 0, 1}; + Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &image.view), "vkCreateImageView placeholder"); + VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.image = image.image; + barrier.subresourceRange = viewInfo.subresourceRange; + toClear.push_back(barrier); + } + if (toClear.empty()) return; + CommandBatch batch(context); + const auto commands = batch.Handle(); + const auto pipelineBarrier = context.Function("vkCmdPipelineBarrier"); + pipelineBarrier(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, static_cast(toClear.size()), toClear.data()); + std::vector toGeneral; + for (const auto& barrier : toClear) { + if (barrier.subresourceRange.aspectMask == VK_IMAGE_ASPECT_DEPTH_BIT) { + const VkClearDepthStencilValue zero{0.0f, 0}; + context.Function("vkCmdClearDepthStencilImage")(commands, barrier.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &zero, 1, &barrier.subresourceRange); + } else { + const VkClearColorValue zero{}; + context.Function("vkCmdClearColorImage")(commands, barrier.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &zero, 1, &barrier.subresourceRange); + } + auto general = barrier; + general.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + general.dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT; + general.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + general.newLayout = VK_IMAGE_LAYOUT_GENERAL; + toGeneral.push_back(general); + } + pipelineBarrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr, static_cast(toGeneral.size()), toGeneral.data()); + batch.SubmitAndWait(); + } catch (...) { + release(); + throw; + } +} + +PlaceholderImages::~PlaceholderImages() { + release(); +} + +void PlaceholderImages::release() noexcept { + for (auto& image : images) { + if (image.view) context.Function("vkDestroyImageView")(context.device, image.view, nullptr); + if (image.image) context.Function("vkDestroyImage")(context.device, image.image, nullptr); + if (image.memory) context.Function("vkFreeMemory")(context.device, image.memory, nullptr); + image = {}; + } +} + +VkImageView PlaceholderImages::View(std::uint32_t heap) const { + const auto index = heap - ShaderRecompiler::RuntimeAbi::FirstImageBinding; + Require(heap >= ShaderRecompiler::RuntimeAbi::FirstImageBinding && index < images.size() && images[index].view != VK_NULL_HANDLE, + "the device has no placeholder image for typed image heap " + std::to_string(heap)); + return images[index].view; +} + +} diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 18a32727c..222b73753 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -3,6 +3,7 @@ #include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp" #include "ThreadOwned.hpp" #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include "prx/libSceAgcDriver/Graphics/include/PlaceholderImages.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include @@ -1484,6 +1485,11 @@ void ShaderResources::buildComplete() { images.reserve(imageCount); std::vector writes; writes.reserve(bindings.size()); + const auto absentImage = [this](std::uint32_t binding) { + constexpr auto bindingCount = static_cast(ShaderRecompiler::RuntimeAbi::Binding::Count); + const auto view = context.nullDescriptors ? VK_NULL_HANDLE : context.placeholderImages->View(binding % bindingCount); + return VkDescriptorImageInfo{VK_NULL_HANDLE, view, VK_IMAGE_LAYOUT_GENERAL}; + }; for (const auto& binding : bindings) { VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}; write.dstSet = _set; @@ -1501,7 +1507,7 @@ void ShaderResources::buildComplete() { case VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE: write.pImageInfo = images.data() + images.size(); for (const auto index : binding.imageAllocations) { - if (index == std::numeric_limits::max()) images.push_back({VK_NULL_HANDLE, VK_NULL_HANDLE, VK_IMAGE_LAYOUT_GENERAL}); + if (index == std::numeric_limits::max()) images.push_back(absentImage(binding.layout.binding)); else images.push_back({VK_NULL_HANDLE, textureFirstLayer[index] ? textures[index]->FirstLayerView() : textures[index]->View(), textures[index]->Layout()}); } Require(binding.imageAllocations.size() == binding.layout.descriptorCount, "descriptor allocations disagree with compact binding"); @@ -1509,7 +1515,7 @@ void ShaderResources::buildComplete() { case VK_DESCRIPTOR_TYPE_STORAGE_IMAGE: write.pImageInfo = images.data() + images.size(); for (const auto index : binding.imageAllocations) { - if (index == std::numeric_limits::max()) images.push_back({VK_NULL_HANDLE, VK_NULL_HANDLE, VK_IMAGE_LAYOUT_GENERAL}); + if (index == std::numeric_limits::max()) images.push_back(absentImage(binding.layout.binding)); else images.push_back({VK_NULL_HANDLE, storageAtomic64[index] ? storageTextures[index]->Atomic64View(storageMips[index], storageFirstLayer[index]) : storageAtomic[index] ? storageTextures[index]->AtomicView(storageMips[index], storageFirstLayer[index]) : storageTextures[index]->StorageView(storageMips[index], storageFirstLayer[index]), VK_IMAGE_LAYOUT_GENERAL}); } Require(binding.imageAllocations.size() == binding.layout.descriptorCount, "descriptor allocations disagree with compact binding"); @@ -2906,7 +2912,7 @@ void ShaderResources::addImageBinding(const ShaderRecompiler::DescriptorBinding& const bool sampledHeap = localBinding >= ShaderRecompiler::RuntimeAbi::FirstImageBinding && localBinding < ShaderRecompiler::RuntimeAbi::FirstStorageImageBinding; const bool storageHeap = localBinding >= ShaderRecompiler::RuntimeAbi::FirstStorageImageBinding && localBinding < static_cast(ShaderRecompiler::RuntimeAbi::Binding::Samplers); Require((binding.kind == ShaderRecompiler::DescriptorKind::SampledImage && sampledHeap) || (binding.kind == ShaderRecompiler::DescriptorKind::StorageImage && storageHeap) || (binding.kind == ShaderRecompiler::DescriptorKind::Sampler && heap == ShaderRecompiler::RuntimeAbi::Binding::Samplers), "resource class disagrees with typed descriptor heap"); - Require(binding.kind == ShaderRecompiler::DescriptorKind::Sampler || context.nullDescriptors, "typed image heaps require enabled nullDescriptor"); + Require(binding.kind == ShaderRecompiler::DescriptorKind::Sampler || context.nullDescriptors || context.placeholderImages != nullptr, "typed image heaps require nullDescriptor or placeholder images"); 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"); diff --git a/core/libs/prx/libSceAgcDriver/tests/PlaceholderImages.cpp b/core/libs/prx/libSceAgcDriver/tests/PlaceholderImages.cpp new file mode 100644 index 000000000..e00241e37 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/PlaceholderImages.cpp @@ -0,0 +1,98 @@ +#include "prx/libSceAgcDriver/Graphics/include/PlaceholderImages.hpp" +#include "IntermediateRepresentation/IrProgram.hpp" +#include +#include +#include +#include +#include +#include +#include + +using namespace ShaderRecompiler; + +namespace { + +void Require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +VkImageViewType ViewType(RdnaImageDimension dimension) { + switch (dimension) { + case RdnaImageDimension::Dim1D: return VK_IMAGE_VIEW_TYPE_1D; + case RdnaImageDimension::Dim1DArray: return VK_IMAGE_VIEW_TYPE_1D_ARRAY; + case RdnaImageDimension::Dim2D: + case RdnaImageDimension::Dim2DMsaa: return VK_IMAGE_VIEW_TYPE_2D; + case RdnaImageDimension::Dim2DArray: + case RdnaImageDimension::Dim2DMsaaArray: return VK_IMAGE_VIEW_TYPE_2D_ARRAY; + case RdnaImageDimension::Dim3D: return VK_IMAGE_VIEW_TYPE_3D; + default: throw std::runtime_error("unknown image dimension"); + } +} + +VkFormat Format(const ImageResource& image) { + if (image.depthCompare) return VK_FORMAT_D16_UNORM; + if (image.atomic64) return VK_FORMAT_R64_UINT; + switch (image.numericClass) { + case IrTextureNumericClass::Float: return VK_FORMAT_R8G8B8A8_UNORM; + case IrTextureNumericClass::Uint: return VK_FORMAT_R32_UINT; + case IrTextureNumericClass::Sint: return VK_FORMAT_R32_SINT; + default: throw std::runtime_error("unknown numeric class"); + } +} + +void CheckEveryHeap() { + constexpr std::array dimensions{RdnaImageDimension::Dim1D, RdnaImageDimension::Dim1DArray, RdnaImageDimension::Dim2D, RdnaImageDimension::Dim2DArray, + RdnaImageDimension::Dim2DMsaa, RdnaImageDimension::Dim2DMsaaArray, RdnaImageDimension::Dim3D}; + std::set heaps; + for (const auto resourceClass : {ImageResourceClass::Sampled, ImageResourceClass::Storage}) { + for (const auto numericClass : {IrTextureNumericClass::Float, IrTextureNumericClass::Uint, IrTextureNumericClass::Sint}) { + for (const bool depthCompare : {false, true}) { + for (const bool atomic : {false, true}) { + for (const bool atomic64 : {false, true}) { + if ((depthCompare && resourceClass == ImageResourceClass::Storage) || (atomic64 && !atomic)) continue; + for (const auto dimension : dimensions) { + ImageResource image; + image.resourceClass = resourceClass; + image.numericClass = numericClass; + image.dimension = dimension; + image.depthCompare = depthCompare; + image.atomic = atomic; + image.atomic64 = atomic64; + std::uint32_t heap = 0; + try { + heap = static_cast(DescriptorBindingForImage(image)); + } catch (const std::exception&) { + continue; + } + const auto shape = AgcDriver::Graphics::PlaceholderImageShapeOf(heap); + const auto where = " for heap " + std::to_string(heap); + const bool storage = resourceClass == ImageResourceClass::Storage; + const bool multisampled = dimension == RdnaImageDimension::Dim2DMsaa || dimension == RdnaImageDimension::Dim2DMsaaArray; + Require(((shape.usage & VK_IMAGE_USAGE_STORAGE_BIT) != 0) == storage && ((shape.usage & VK_IMAGE_USAGE_SAMPLED_BIT) != 0) == !storage, "placeholder usage disagrees with the resource class" + where); + Require((shape.aspect == VK_IMAGE_ASPECT_DEPTH_BIT) == depthCompare, "placeholder aspect disagrees with depth comparison" + where); + Require(shape.format == Format(image), "placeholder format disagrees with the numeric class" + where); + Require(shape.viewType == ViewType(dimension), "placeholder view type disagrees with the dimension" + where); + Require((shape.samples != VK_SAMPLE_COUNT_1_BIT) == multisampled, "placeholder sample count disagrees with multisampling" + where); + Require(((shape.features & VK_FORMAT_FEATURE_STORAGE_IMAGE_ATOMIC_BIT) != 0) == atomic, "placeholder format features disagree with atomics" + where); + heaps.insert(heap); + } + } + } + } + } + } + Require(heaps.size() == RuntimeAbi::ImageBindingCount, "not every typed image heap is reachable from an image resource: " + std::to_string(heaps.size())); +} + +} + +int main() { + try { + CheckEveryHeap(); + std::cout << "placeholder image tests passed\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDeviceProfile.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDeviceProfile.cpp index 11e9c1dd1..c5f9c8d5d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDeviceProfile.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDeviceProfile.cpp @@ -82,8 +82,12 @@ void CheckProfile() { 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"); + 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"); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index e6360c572..62f01fa77 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -348,6 +348,7 @@ Throughout the project, every function at every stage either **does exactly what - [Graphics pipeline libraries](../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp) (libSceAgcDriver): a pipeline is fast-linked at the draw and relinked with `VK_PIPELINE_CREATE_LINK_TIME_OPTIMIZATION_BIT_EXT` on a background thread, and binds the optimized one from the next draw on. Draws recorded before that use the fast link, whose vertex stage keeps the outputs the fragment stage does not read: on RADV (Radeon 660M, Mesa 26.2.4) 346 of 483 Astro's Playroom vertex shaders paired with a fragment stage that reads none of them used more VGPRs (median 8 more, up to 2.5 times), and on an RTX 4090 13 of 15 PPSA36082 vertex shaders up to 68 instead of 38 (#2242). The optimized link compiles to the same code as a whole pipeline - [sceAvPlayerGetVideoData](../../core/libs/prx/libSceAvPlayer/src/Source.cpp) (libSceAvPlayer) in sync mode `SCE_AVPLAYER_AV_SYNC_MODE_NONE` skips queued frames that the audio clock has already passed while the title pulls audio, so a title that requests video slower than the film's frame rate keeps the picture with the sound. Whether the console hands out every frame in this mode is unknown. PPSA12544 requests 7-19 frames per second for a 30 fps cinematic - [`APS5_THREAD_PRIORITY=rt`](../../core/libs/prx/libSceAgcDriver/Execution/src/ThreadPriority.cpp) (libSceAgcDriver) lowers the process's hard `RLIMIT_RTTIME` when it is above `APS5_THREAD_RTTIME_US` or rtkit's `RTTimeUSecMax`, because [rtkit](https://github.com/heftig/rtkit/blob/c295fa849f52b487be6433e69e08b46251950399/rtkit-daemon.c#L592-L637) (`verify_process_rttime`) reads the hard limit from `/proc//limits` and refuses real-time scheduling when it is higher. An unprivileged process cannot raise the hard limit again, so it stays lowered for the process and its threads even when rtkit then refuses the request (burst limit, user or start-time checks) and the thread throws. rtkit's `MaxRealtimePriority` and `RTTimeUSecMax` are read first, so an unreachable rtkit or a priority above its maximum leaves the limit untouched. The lowered limit only matters for threads that later run under a real-time policy +- On a device without VK_EXT_robustness2's `nullDescriptor`, such as MoltenVK, a null image descriptor in a typed heap is a [placeholder image](../../core/libs/prx/libSceAgcDriver/Graphics/src/PlaceholderImages.cpp) of that heap's type, 1×1 and cleared to zero, which every null descriptor of the heap shares. Reads give zeros, as `nullDescriptor` allows, but stores and atomics to it are kept, so a later read of a null storage image can return them, and a depth comparison compares against depth 0. A heap whose placeholder the device cannot create, for example 1D or 3D depth, 64-bit atomics without 64-bit image atomics, or multisampled storage without `shaderStorageImageMultisample`, throws when a null descriptor reaches it. - [libSceHttp2 asynchronous requests](../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2) never reach the network: `sceHttp2SendRequestAsync` accepts the request, queues a completion whose result is the library's network error and triggers the user event named by the kqueue option (equeue at offset 0, event id at 8, user data at 16, layout inferred from PPSA17221); `sceHttp2WaitAsync` returns that completion. The completion's `event_type` value is unknown and left 0. PPSA17221 completes its request object only after `sceHttp2SendRequestAsync` returns, and its handler for a synchronous failure calls `shared_from_this` before that, so a send must not fail synchronously. A kqueue option or user event that is missing, an extra option, and a wait with no completed operation throw - [system](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) runs no command. It follows FreeBSD 11's `lib/libc/stdlib/system.c` for a sandbox without `/bin/sh`: a null command returns 1 unconditionally, as that version does, and any command returns the wait status of a child that could not exec the shell and called `_exit(127)` (`0x7f00`). Whether the console's application sandbox has no shell, and whether `vfork` succeeds there (FreeBSD returns -1 when it fails), is unverified - [sceKernelWriteThrottlingStatus](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) reports no write throttling: the host SSD has no write budget, so the first qword of the 32-byte status is set to `UINT32_MAX` and the rest is zeroed. PPSA17221 reads only that qword: it multiplies it by 64 KiB and compares the product with byte thresholds up to 0x4AF00000 to pick one of five tiers. When the call fails the title assumes 0x4B000001 bytes, which selects the same top tier as `UINT32_MAX`. What the value measures on hardware (a remaining budget or a rate) and the meaning of the other three qwords are unknown