diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp index 0064178b9..9f12df608 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp @@ -158,6 +158,7 @@ public: void Resize(std::uint32_t width, std::uint32_t height); bool Presentable() const; bool PrimitiveListRestart() const; + bool SamplerFilterMinmax() const; // A presentation is a few steps so the presenter holds GuestMemory::GpuMutex only while it // touches the queue. Presentations are slots (FlipInFlight() + 1, each with its own command // buffer, fence, kept resident image and dump buffer): RetirePresents(keep) (no mutex) retires diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 6b50c2acd..3d848fde1 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -204,6 +204,7 @@ struct VulkanDevice::State { bool depthRangeUnrestricted = false; bool samplerAnisotropy = false; bool textureCompressionBC = false; + bool samplerFilterMinmax = false; // VK_KHR_timeline_semaphore enabled: the recorder's unlocked waits are available. bool timelineSemaphores = false; bool computeWave32 = false; @@ -763,6 +764,13 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u // drops those writes instead of faulting the device. const bool imageRobustness = hasExtension(VK_EXT_IMAGE_ROBUSTNESS_EXTENSION_NAME); if (imageRobustness) deviceExtensions.push_back(VK_EXT_IMAGE_ROBUSTNESS_EXTENSION_NAME); + if (hasExtension(VK_EXT_SAMPLER_FILTER_MINMAX_EXTENSION_NAME)) { + VkPhysicalDeviceSamplerFilterMinmaxPropertiesEXT minmaxProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SAMPLER_FILTER_MINMAX_PROPERTIES_EXT}; + VkPhysicalDeviceProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &minmaxProperties}; + state->InstanceFunction("vkGetPhysicalDeviceProperties2")(selected, &properties); + state->samplerFilterMinmax = minmaxProperties.filterMinmaxSingleComponentFormats == VK_TRUE && minmaxProperties.filterMinmaxImageComponentMapping == VK_TRUE; + if (state->samplerFilterMinmax) deviceExtensions.push_back(VK_EXT_SAMPLER_FILTER_MINMAX_EXTENSION_NAME); + } // Indirect draws with a GPU-side count (DRAW_INDIRECT_MULTI with count_indirect); a device // without it resolves such draws on the CPU. state->drawIndirectCount = hasExtension(VK_KHR_DRAW_INDIRECT_COUNT_EXTENSION_NAME); @@ -2341,6 +2349,10 @@ bool VulkanDevice::PrimitiveListRestart() const { return state->primitiveListRestart; } +bool VulkanDevice::SamplerFilterMinmax() const { + return state->samplerFilterMinmax; +} + Graphics::Context VulkanDevice::graphicsContext() const { static const bool noCache = std::getenv("APS5_NO_CONTEXT_CACHE") != nullptr; if (state->contextReady && !noCache) return state->context; @@ -2383,6 +2395,7 @@ Graphics::Context VulkanDevice::buildContext() const { context.multiDrawIndirect = state->multiDrawIndirect; context.depthBounds = state->depthBounds; context.depthBiasClamp = state->depthBiasClamp; + context.samplerFilterMinmax = state->samplerFilterMinmax; context.drawIndirectCount = state->drawIndirectCount; context.occlusionQueryPrecise = state->occlusionQueryPrecise; context.emptyBuffer = state->emptyBuffer ? state->emptyBuffer->Handle() : VK_NULL_HANDLE; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp index 238c05424..befeff74c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp @@ -123,6 +123,7 @@ struct Context { bool occlusionQueryPrecise = false; bool depthBounds = false; bool depthBiasClamp = false; + bool samplerFilterMinmax = false; VkBuffer emptyBuffer = VK_NULL_HANDLE; // The device's list of recorded dispatches whose copied written buffers await a CPU write-back // (VulkanDevice's State::copiedWriters; the draw counterpart is DrawCopiedWriters): an indirect diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestSamplerResource.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestSamplerResource.hpp index f8444bfdb..5312d9b5b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestSamplerResource.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestSamplerResource.hpp @@ -23,6 +23,7 @@ struct GuestSamplerResource { float maxLod; float lodBias; VkBorderColor borderColor; + VkSamplerReductionMode reductionMode = VK_SAMPLER_REDUCTION_MODE_WEIGHTED_AVERAGE_EXT; bool compareEnable = false; VkCompareOp compareOp = VK_COMPARE_OP_NEVER; bool unnormalizedCoordinates = false; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Sampler.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Sampler.hpp index 67450c4ac..564679119 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Sampler.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Sampler.hpp @@ -20,12 +20,14 @@ public: Sampler& operator=(const Sampler&) = delete; VkSampler Handle() const; + bool RequiresFilterMinmax() const; private: void release() noexcept; Context context; VkSampler sampler = VK_NULL_HANDLE; + bool requiresFilterMinmax = false; }; // One VkSampler per distinct S# (its 4 words plus the shader's depth-compare use and the @@ -57,6 +59,8 @@ private: std::uint64_t misses = 0; }; +void RequireFilterMinmax(const Context& context, VkFormat format, std::uint32_t samplerMask, std::span> samplers); + } #endif diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index 69d086ca4..9df5a3c1b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -326,7 +326,7 @@ private: // 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); + void resolveImageBinding(const ShaderRecompiler::DescriptorBinding& binding, Binding& item, std::span> shaderSamplers); void forgetDeferredInputs(); void release() noexcept; void prepareAddressBindings(std::span shaders, std::span snapshots); @@ -436,6 +436,8 @@ private: 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; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index 08009e94f..10fb690a3 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -75,6 +75,7 @@ public: // The layout the image is kept in while sampled. VkImageLayout Layout() const { return layout; } VkDeviceSize AllocationBytes() const { return allocationBytes; } + VkFormat ViewFormat() const { return viewFormat; } // Whether this texture is a view of a storage image (no snapshot of its own). bool ViewsStorageImage() const { return storageSource != nullptr; } const StorageTexture* StorageSource() const { return storageSource.get(); } @@ -100,6 +101,7 @@ private: ViewRange firstLayerRange{}; VkImageLayout layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; VkDeviceSize allocationBytes = 0; + VkFormat viewFormat = VK_FORMAT_UNDEFINED; std::shared_ptr source; std::shared_ptr storageSource; std::unique_ptr upload; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp index 5845b80fb..e70e48018 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp @@ -20,6 +20,15 @@ bool isAnisoFilter(std::uint32_t raw) { return raw == 2 || raw == 3; } +VkSamplerReductionMode toVkReductionMode(std::uint32_t raw) { + switch (raw) { + case 0: return VK_SAMPLER_REDUCTION_MODE_WEIGHTED_AVERAGE_EXT; + case 1: return VK_SAMPLER_REDUCTION_MODE_MIN_EXT; + case 2: return VK_SAMPLER_REDUCTION_MODE_MAX_EXT; + default: throw std::runtime_error("AGC graphics: guest sampler descriptor uses an unknown reduction filter mode " + std::to_string(raw)); + } +} + VkSamplerAddressMode toVkAddressMode(std::uint32_t raw) { switch (raw) { case 0: return VK_SAMPLER_ADDRESS_MODE_REPEAT; @@ -99,15 +108,17 @@ GuestSamplerResource DecodeSamplerResource(std::span words, static_cast(anisoThreshold); static_cast(anisoBias); Require(!disableCubeWrap, "guest sampler descriptor disables seamless cube filtering which is not implemented"); - Require(filterMode == 0, "guest sampler descriptor uses a reduction filter mode which is not implemented"); + const auto reductionMode = toVkReductionMode(filterMode); Require(!disableDegamma, "guest sampler descriptor disables degamma which is not implemented"); Require(lodBiasSec == 0, "guest sampler descriptor uses a secondary LOD bias which is not implemented"); Require(!pointPreclamp, "guest sampler descriptor uses point preclamping which is not implemented"); Require(!anisoOverride, "guest sampler descriptor uses an anisotropy override which is not implemented"); Require(!blendZeroPrt, "guest sampler descriptor uses PRT blend-zero which is not implemented"); if (mipFilter > 2u) Require(false, "guest sampler descriptor uses an unknown mip filter " + std::to_string(mipFilter)); + Require(mipFilter != 2u || reductionMode == VK_SAMPLER_REDUCTION_MODE_WEIGHTED_AVERAGE_EXT, "guest sampler descriptor combines a min or max reduction with a linear mip filter, which is not implemented"); const auto aniso = isAnisoFilter(xyMagFilter) || isAnisoFilter(xyMinFilter); + Require(!aniso || reductionMode == VK_SAMPLER_REDUCTION_MODE_WEIGHTED_AVERAGE_EXT, "guest sampler descriptor combines a min or max reduction with anisotropic filtering, which is not implemented"); auto anisoRatio = 1.0f; if (aniso) { if (maxAnisoRatio > 4u) Require(false, "guest sampler descriptor uses an unknown anisotropy ratio " + std::to_string(maxAnisoRatio)); @@ -147,6 +158,7 @@ GuestSamplerResource DecodeSamplerResource(std::span words, result.maxLod = maxLod; result.lodBias = toSignedLodBias(lodBiasRaw); result.borderColor = border; + result.reductionMode = reductionMode; const std::array compareOps{VK_COMPARE_OP_NEVER, VK_COMPARE_OP_LESS, VK_COMPARE_OP_EQUAL, VK_COMPARE_OP_LESS_OR_EQUAL, VK_COMPARE_OP_GREATER, VK_COMPARE_OP_NOT_EQUAL, VK_COMPARE_OP_GREATER_OR_EQUAL, VK_COMPARE_OP_ALWAYS}; result.compareOp = compareOps.at(depthCompareFunc); if (forceUnormCoords) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Sampler.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Sampler.cpp index b0bed507f..851ae3bf7 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Sampler.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Sampler.cpp @@ -1,6 +1,7 @@ #include "prx/libSceAgcDriver/Graphics/include/Sampler.hpp" #include #include +#include namespace AgcDriver::Graphics { @@ -11,6 +12,13 @@ namespace AgcDriver::Graphics { Require(!(descriptor.unnormalizedCoordinates && descriptor.compareEnable), "guest sampler descriptor with unnormalized coordinates enables depth comparison, which is not implemented"); VkSamplerCreateInfo info{VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO}; + VkSamplerReductionModeCreateInfoEXT reduction{VK_STRUCTURE_TYPE_SAMPLER_REDUCTION_MODE_CREATE_INFO_EXT, nullptr, descriptor.reductionMode}; + if (descriptor.reductionMode != VK_SAMPLER_REDUCTION_MODE_WEIGHTED_AVERAGE_EXT) { + Require(context.samplerFilterMinmax, "guest sampler descriptor uses a min or max reduction which the device does not support"); + Require(!descriptor.compareEnable, "guest sampler descriptor combines a min or max reduction with depth comparison, which is not implemented"); + info.pNext = &reduction; + requiresFilterMinmax = descriptor.magFilter == VK_FILTER_LINEAR || descriptor.minFilter == VK_FILTER_LINEAR; + } info.magFilter = descriptor.magFilter; info.minFilter = descriptor.minFilter; info.mipmapMode = descriptor.mipmapMode; @@ -41,6 +49,10 @@ namespace AgcDriver::Graphics { return sampler; } + bool Sampler::RequiresFilterMinmax() const { + return requiresFilterMinmax; + } + SamplerCache::SamplerCache(std::size_t capacity) : capacity(std::max(capacity, 1)) {} std::shared_ptr SamplerCache::Get(const Context& context, std::span words, bool compareEnable, bool unnormalizedProven) { @@ -67,4 +79,17 @@ namespace AgcDriver::Graphics { return sampler; } + void RequireFilterMinmax(const Context& context, VkFormat format, std::uint32_t samplerMask, std::span> samplers) { + bool filtered = false; + for (std::uint32_t element = 0; element < 32u; ++element) { + if (((samplerMask >> element) & 1u) == 0u) continue; + if (element >= samplers.size()) Require(false, "a sampled texture is paired with sampler element " + std::to_string(element) + ", which its shader does not bind"); + filtered = filtered || samplers[element]->RequiresFilterMinmax(); + } + if (!filtered) return; + VkFormatProperties properties{}; + context.formatProperties(context.physical, format, &properties); + if ((properties.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_MINMAX_BIT_EXT) == 0) Require(false, "a sampled texture whose format " + std::to_string(format) + " does not support min/max filtering is sampled through a min or max reduction sampler with linear filtering"); + } + } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 759380e79..5f4c021ab 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -899,6 +899,8 @@ void ShaderResources::buildPrepare(std::span shaders, cons std::uint64_t stageDescriptors = 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"); @@ -948,6 +950,10 @@ void ShaderResources::buildPrepare(std::span shaders, cons bindings.push_back(std::move(item)); } 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; + } } Require(storageBuffers <= context.limits.maxDescriptorSetStorageBuffers, "pipeline descriptors exceed device limits"); timing.bindingsMs = phase(BuildPhase::Bindings); @@ -1015,7 +1021,7 @@ 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]); + for (const auto& deferred : deferredImages) resolveImageBinding(*deferred.binding, bindings[deferred.index], std::span>(samplers).subspan(deferred.firstSampler, deferred.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 @@ -1213,6 +1219,8 @@ std::vector ShaderResources::ContentKey(const CompiledShader& sha // 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()); } return key; } @@ -2523,11 +2531,12 @@ std::shared_ptr ShaderResources::fastTexture(const ImageRecord& record) return record.texture; } -void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBinding& binding, Binding& item) { +void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBinding& binding, Binding& item, 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; }; if (binding.kind == ShaderRecompiler::DescriptorKind::SampledImage) { + Require(binding.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); @@ -2548,6 +2557,7 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind 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); 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}); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 513412318..695f83934 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -423,6 +423,7 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex viewInfo.image = image; viewInfo.viewType = ViewTypeFor(descriptor.dimension, viewLayerCount); viewInfo.format = vkFormat; + viewFormat = vkFormat; viewInfo.components = depthCompare ? VkComponentMapping{} : components; viewInfo.subresourceRange = {aspect, descriptor.baseLevel, viewLevelCount, descriptor.baseArray, viewLayerCount}; VkImageViewMinLodCreateInfoEXT minLod{VK_STRUCTURE_TYPE_IMAGE_VIEW_MIN_LOD_CREATE_INFO_EXT}; @@ -469,6 +470,7 @@ Texture::Texture(const Context& context, const std::shared_ptr& viewInfo.image = source->Image(); viewInfo.viewType = ViewTypeFor(descriptor.dimension, viewLayerCount); viewInfo.format = vkFormat; + viewFormat = vkFormat; viewInfo.components = components; viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, descriptor.baseLevel, viewLevelCount, descriptor.baseArray, viewLayerCount}; VkImageViewMinLodCreateInfoEXT minLod{VK_STRUCTURE_TYPE_IMAGE_VIEW_MIN_LOD_CREATE_INFO_EXT}; @@ -494,6 +496,7 @@ Texture::Texture(const Context& context, VkImage depthImage, VkFormat depthForma viewInfo.image = depthImage; viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; viewInfo.format = depthFormat; + viewFormat = depthFormat; viewInfo.components = components; viewInfo.subresourceRange = {aspect, 0, 1, 0, 1}; Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &view), "vkCreateImageView depth plane"); diff --git a/core/libs/prx/libSceAgcDriver/tests/GuestSamplerResource.cpp b/core/libs/prx/libSceAgcDriver/tests/GuestSamplerResource.cpp index 43e54c2f0..4a423ed0a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/GuestSamplerResource.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/GuestSamplerResource.cpp @@ -1,9 +1,14 @@ #include "GraphicsTests.hpp" +#include "Optimization/DescriptorBindingBuilder.hpp" #include "prx/libSceAgcDriver/Graphics/include/GuestSamplerResource.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Sampler.hpp" #include #include +#include +#include #include #include +#include namespace { @@ -84,6 +89,138 @@ bool nearlyEqual(float a, float b) { return std::fabs(a - b) < 0.001f; } +struct SamplerCapture { + bool created = false; + const void* next = nullptr; + VkSamplerReductionMode reductionMode = VK_SAMPLER_REDUCTION_MODE_MAX_ENUM; +}; +SamplerCapture samplerCapture; + +template +THandle fakeHandle(std::uint64_t value) { + if constexpr (std::is_pointer_v) return reinterpret_cast(static_cast(value)); + else return static_cast(value); +} + +VKAPI_ATTR VkResult VKAPI_CALL captureCreateSampler(VkDevice, const VkSamplerCreateInfo* info, const VkAllocationCallbacks*, VkSampler* sampler) { + samplerCapture.created = true; + samplerCapture.next = info->pNext; + const auto* reduction = static_cast(info->pNext); + if (reduction != nullptr && reduction->sType == VK_STRUCTURE_TYPE_SAMPLER_REDUCTION_MODE_CREATE_INFO_EXT && reduction->pNext == nullptr) samplerCapture.reductionMode = reduction->reductionMode; + *sampler = fakeHandle(0x5a); + return VK_SUCCESS; +} + +VKAPI_ATTR void VKAPI_CALL captureDestroySampler(VkDevice, VkSampler, const VkAllocationCallbacks*) {} + +PFN_vkVoidFunction VKAPI_CALL captureProc(VkDevice, const char* name) { + if (std::strcmp(name, "vkCreateSampler") == 0) return reinterpret_cast(captureCreateSampler); + if (std::strcmp(name, "vkDestroySampler") == 0) return reinterpret_cast(captureDestroySampler); + return nullptr; +} + +SamplerCapture createSampler(const Context& context, const GuestSamplerResource& resource) { + samplerCapture = {}; + const Sampler sampler(context, resource); + Require(sampler.Handle() != VK_NULL_HANDLE, "sampler test did not create a sampler"); + return samplerCapture; +} + +void RunSamplerReductionTests(const Fields& base) { + Context context{}; + context.limits.maxSamplerAnisotropy = 1.0f; + context.deviceProc = captureProc; + context.samplerFilterMinmax = true; + + const std::array capturedSampler{0x20000092u, 0x00fff000u, 0x05500000u, 0u}; + const auto captured = DecodeSamplerResource(capturedSampler); + Require(captured.reductionMode == VK_SAMPLER_REDUCTION_MODE_MIN_EXT, "captured min-reduction sampler decoded to another reduction mode"); + Require(captured.magFilter == VK_FILTER_LINEAR && captured.minFilter == VK_FILTER_LINEAR && captured.mipmapMode == VK_SAMPLER_MIPMAP_MODE_NEAREST, "captured min-reduction sampler filters decoded incorrectly"); + Require(captured.addressModeU == VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE && captured.addressModeV == VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE && captured.addressModeW == VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, "captured min-reduction sampler address modes decoded incorrectly"); + Require(!captured.anisotropyEnable && captured.compareOp == VK_COMPARE_OP_NEVER && nearlyEqual(captured.maxLod, 4095.0f / 256.0f), "captured min-reduction sampler fields decoded incorrectly"); + + const std::array modes{VK_SAMPLER_REDUCTION_MODE_WEIGHTED_AVERAGE_EXT, VK_SAMPLER_REDUCTION_MODE_MIN_EXT, VK_SAMPLER_REDUCTION_MODE_MAX_EXT}; + for (std::uint32_t mode = 0; mode < modes.size(); ++mode) { + for (const std::uint32_t filter : {0u, 1u}) { + Fields reduced = base; + reduced.filterMode = mode; + reduced.xyMagFilter = filter; + reduced.xyMinFilter = filter; + reduced.mipFilter = 1; + const auto decoded = DecodeSamplerResource(pack(reduced)); + Require(decoded.reductionMode == modes.at(mode), "sampler reduction mode decoded incorrectly"); + const auto created = createSampler(context, decoded); + Require(created.created, "sampler test did not reach vkCreateSampler"); + if (mode == 0u) Require(created.next == nullptr, "a weighted-average sampler chained a reduction mode"); + else Require(created.reductionMode == modes.at(mode), "a min or max sampler did not chain its reduction mode"); + const Sampler sampler(context, decoded); + Require(sampler.RequiresFilterMinmax() == (mode != 0u && filter == 1u), "sampler min/max format requirement is wrong"); + } + } + Fields noMip = base; + noMip.filterMode = 2; + noMip.mipFilter = 0; + Require(DecodeSamplerResource(pack(noMip)).reductionMode == VK_SAMPLER_REDUCTION_MODE_MAX_EXT, "a max sampler without mip filtering was not decoded"); + Fields linearMip = base; + linearMip.filterMode = 1; + rejectFields(linearMip, "min or max reduction with a linear mip filter"); + linearMip.xyMagFilter = 0; + linearMip.xyMinFilter = 0; + rejectFields(linearMip, "min or max reduction with a linear mip filter"); + + Fields badFilterMode = base; + badFilterMode.filterMode = 3; + rejectFields(badFilterMode, "unknown reduction filter mode 3"); + Fields anisoReduction = base; + anisoReduction.filterMode = 1; + anisoReduction.xyMagFilter = 3; + anisoReduction.xyMinFilter = 3; + anisoReduction.maxAnisoRatio = 2; + anisoReduction.mipFilter = 1; + rejectFields(anisoReduction, "min or max reduction with anisotropic filtering"); + anisoReduction.filterMode = 0; + Require(DecodeSamplerResource(pack(anisoReduction)).anisotropyEnable, "anisotropic weighted-average sampler was rejected"); + + auto compared = captured; + compared.compareEnable = true; + reject([&] { createSampler(context, compared); }, "min or max reduction with depth comparison"); + auto weightedCompare = DecodeSamplerResource(pack(base)); + weightedCompare.compareEnable = true; + Require(createSampler(context, weightedCompare).created, "weighted-average comparison sampler was rejected"); + context.samplerFilterMinmax = false; + reject([&] { createSampler(context, captured); }, "min or max reduction which the device does not support"); + Require(createSampler(context, DecodeSamplerResource(pack(base))).created, "weighted-average sampler needs min/max support"); + + context.formatProperties = [](VkPhysicalDevice, VkFormat format, VkFormatProperties* properties) { + *properties = {}; + if (format == VK_FORMAT_R32_SFLOAT) properties->optimalTilingFeatures = VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT | VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_MINMAX_BIT_EXT; + if (format == VK_FORMAT_R32G32_SFLOAT) properties->optimalTilingFeatures = VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT | VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT; + if (format == VK_FORMAT_R8G8B8A8_UNORM) properties->linearTilingFeatures = VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_MINMAX_BIT_EXT; + }; + context.samplerFilterMinmax = true; + Fields minPoint = base; + minPoint.filterMode = 1; + minPoint.xyMagFilter = 0; + minPoint.xyMinFilter = 0; + minPoint.mipFilter = 1; + const std::array samplers{std::make_shared(context, DecodeSamplerResource(pack(base))), std::make_shared(context, captured), std::make_shared(context, DecodeSamplerResource(pack(minPoint)))}; + RequireFilterMinmax(context, VK_FORMAT_R32_SFLOAT, 0b010u, samplers); + RequireFilterMinmax(context, VK_FORMAT_R8G8B8A8_UNORM, 0u, samplers); + RequireFilterMinmax(context, VK_FORMAT_R8G8B8A8_UNORM, 0b101u, samplers); + reject([&] { RequireFilterMinmax(context, VK_FORMAT_R8G8B8A8_UNORM, 0b010u, samplers); }, "format 37 does not support min/max filtering is sampled through a min or max reduction sampler"); + reject([&] { RequireFilterMinmax(context, VK_FORMAT_R32G32_SFLOAT, 0b011u, samplers); }, "does not support min/max filtering"); + reject([&] { RequireFilterMinmax(context, VK_FORMAT_R32_SFLOAT, 0b1000u, samplers); }, "sampler element 3, which its shader does not bind"); + + const auto pointFiltered = [](const std::array& words) { return ShaderRecompiler::PointFilteredSamplerWord(words[0], words[2]); }; + Require(pointFiltered(pack(base)) == 0x05000000u, "a point-filtered weighted-average sampler kept its bilinear or linear mip filter"); + Require(pointFiltered(pack(minPoint)) == pack(minPoint)[2], "a point-filtered min sampler that already point-samples changed"); + reject([&] { pointFiltered(capturedSampler); }, "needs point filtering"); + Fields maxLinearMip = minPoint; + maxLinearMip.filterMode = 2; + maxLinearMip.mipFilter = 2; + reject([&] { pointFiltered(pack(maxLinearMip)); }, "needs point filtering"); +} + } void RunGuestSamplerResourceTests() { @@ -239,9 +376,7 @@ void RunGuestSamplerResourceTests() { badCubeWrap.disableCubeWrap = true; rejectFields(badCubeWrap, "seamless cube filtering"); - Fields badFilterMode = base; - badFilterMode.filterMode = 1; - rejectFields(badFilterMode, "reduction filter mode"); + RunSamplerReductionTests(base); Fields badDegamma = base; badDegamma.disableDegamma = true; diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index 24da896c3..0adf42ad9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -31,7 +31,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; + 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; } bool sameBindings(const std::vector& left, const std::vector& right) { @@ -97,6 +97,7 @@ DescriptorBinding sampleBinding(std::uint32_t seed) { binding.bufferWritten = {false, false, true, true, false}; 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/execution/EmulatedColorCompare.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp index ac3b614f2..92443d6f9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp @@ -38,6 +38,7 @@ constexpr std::uint32_t BorderTable = 3; constexpr std::uint32_t FilterPoint = 0; constexpr std::uint32_t FilterBilinear = 1; constexpr std::uint32_t FilterAnisoBilinear = 3; +constexpr std::uint32_t ReductionMin = 1; constexpr std::array Red{0, 64, 128, 255}; alignas(256) std::array Input{}; @@ -53,6 +54,7 @@ struct Sampler { std::uint32_t clamp; std::uint32_t filter; std::uint32_t border = BorderBlack; + std::uint32_t reduction = 0; }; std::array BufferDescriptor(const void* data, std::uint32_t bytes) { @@ -73,7 +75,7 @@ std::array TextureDescriptor(std::uint32_t format) { std::array SamplerDescriptor(const Sampler& sampler) { return { - sampler.clamp | (sampler.clamp << 3u) | (ClampEdge << 6u) | (LessEqual << 12u), + sampler.clamp | (sampler.clamp << 3u) | (ClampEdge << 6u) | (LessEqual << 12u) | (sampler.reduction << 29u), 0u, (sampler.filter << 20u) | (sampler.filter << 22u), sampler.border << 30u, @@ -214,6 +216,7 @@ int main() { Reject(*device, Format8888UNorm, {ClampHalfBorder, FilterPoint}, "wrap, clamp-to-edge or clamp-to-border"); Reject(*device, Format8888UNorm, {ClampBorder, FilterPoint, BorderTable}, "border color table"); Reject(*device, Format8888UNorm, {ClampEdge, FilterAnisoBilinear}, "point or bilinear"); + Reject(*device, Format8888UNorm, {ClampEdge, FilterBilinear, BorderBlack, ReductionMin}, "min or max reduction"); std::puts("emulated color compare tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleReduction.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleReduction.cpp new file mode 100644 index 000000000..648a88e52 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleReduction.cpp @@ -0,0 +1,319 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Size = 4; +constexpr std::uint32_t Levels = 2; +constexpr std::uint32_t Format32SInt = 21; +constexpr std::uint32_t Format32Float = 22; +constexpr std::uint32_t Format8888UNorm = 56; +constexpr std::uint32_t Type2D = 9; +constexpr std::uint32_t ReductionMin = 1; +constexpr std::uint32_t ReductionMax = 2; +constexpr std::uint32_t FilterPoint = 0; +constexpr std::uint32_t FilterBilinear = 1; +constexpr std::uint32_t MipNone = 0; +constexpr std::uint32_t MipPoint = 1; +constexpr std::uint32_t MipLinear = 2; +constexpr float Margin = 0.05f; +constexpr std::array Level0{17, 11, 23, 14, 20, 25, 12, 19, 13, 22, 16, 24, 21, 10, 18, 15}; +constexpr std::array Level1{5, 30, 16.5f, 9}; + +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; +alignas(4096) std::array Texels{}; +alignas(4096) std::array Colors{}; + +alignas(256) constexpr std::array Code{ + 0x1614008c, 0xe03c1000, 0x8000010a, 0xbf8c3f70, 0xf0900108, 0x00820401, 0xbf8c3f70, + 0x34160082, 0xe0701000, 0x8001040b, 0xbf810000, 0xbf810000, 0xbf810000, +}; + +alignas(256) constexpr std::array GatherCode{ + 0x1614008c, 0xe03c1000, 0x8000010a, 0xbf8c3f70, 0xf11c0108, 0x00820401, 0xbf8c3f70, + 0x34160084, 0xe0781000, 0x8001040b, 0xbf810000, 0xbf810000, 0xbf810000, +}; + +alignas(256) constexpr std::array LoadedCode{ + 0x1614008c, 0xe03c1000, 0x8000010a, 0x36180083, 0x7e1a0280, 0xbf8c3f70, 0xf0900108, 0x00820401, 0xf0000108, 0x0005050c, + 0xbf8c3f70, 0x34160082, 0xe0701000, 0x8001040b, 0xe0701080, 0x8001050b, 0xbf810000, 0xbf810000, 0xbf810000, +}; + +alignas(256) constexpr std::array SampledCode{ + 0x1614008c, 0xe03c1000, 0x8000010a, 0xbf8c3f70, 0xf0900108, 0x00820401, 0xf0900108, 0x00850501, 0xbf8c3f70, + 0x34160082, 0xe0701000, 0x8001040b, 0xe0701080, 0x8001050b, 0xbf810000, 0xbf810000, 0xbf810000, +}; + +struct Sampler { + std::uint32_t reduction; + std::uint32_t filter; + std::uint32_t mip; +}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x01016facu}; +} + +std::array TextureDescriptor(std::uint32_t format, const void* texels = Texels.data(), std::uint32_t levels = Levels) { + const auto address = static_cast(reinterpret_cast(texels)); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (format << 20u) | (((Size - 1u) & 3u) << 30u), + ((Size - 1u) >> 2u) | ((Size - 1u) << 14u), + 0xfacu | ((levels - 1u) << 16u) | (Type2D << 28u), + 0u, + (levels - 1u) << 4u, + 0u, + 0u, + }; +} + +std::array SamplerDescriptor(const Sampler& sampler) { + return {0x92u | (sampler.reduction << 29u), 0x00fff000u, (sampler.filter << 20u) | (sampler.filter << 22u) | (1u << 24u) | (sampler.mip << 26u), 0u}; +} + +void FillTexels() { + const auto mips = AgcDriver::Graphics::ComputeMipLayout(AgcDriver::Graphics::TextureTileMode::kLinear, Format32Float, Size, Size, Levels); + Require(AgcDriver::Graphics::ComputeSurfaceSize(mips, 1) <= Texels.size(), "image sample reduction: the mip chain does not fit the texel storage"); + Texels.fill(0); + for (std::uint32_t level = 0; level < Levels; ++level) { + const auto& mip = mips[level]; + const auto& values = level == 0 ? std::span(Level0) : std::span(Level1); + for (std::uint32_t y = 0; y < mip.height; ++y) { + for (std::uint32_t x = 0; x < mip.width; ++x) { + const auto bits = std::bit_cast(values[y * mip.width + x]); + auto* texel = &Texels[mip.tiledOffset + static_cast(y) * mip.pitchBytes + x * 4u]; + for (std::uint32_t byte = 0; byte < 4u; ++byte) texel[byte] = static_cast(bits >> (byte * 8u)); + } + } + } +} + +std::uint8_t Red(int x, int y) { + const auto cx = static_cast(std::clamp(x, 0, static_cast(Size) - 1)); + const auto cy = static_cast(std::clamp(y, 0, static_cast(Size) - 1)); + return static_cast(17u * ((cy * 7u + cx * 5u) % 16u)); +} + +void FillColors() { + const auto mips = AgcDriver::Graphics::ComputeMipLayout(AgcDriver::Graphics::TextureTileMode::kLinear, Format8888UNorm, Size, Size, 1); + Require(AgcDriver::Graphics::ComputeSurfaceSize(mips, 1) <= Colors.size(), "image sample reduction: the 8_8_8_8 texture does not fit its storage"); + Colors.fill(0); + for (std::uint32_t y = 0; y < Size; ++y) { + for (std::uint32_t x = 0; x < Size; ++x) { + auto* texel = &Colors[mips[0].tiledOffset + static_cast(y) * mips[0].pitchBytes + x * 4u]; + texel[0] = Red(static_cast(x), static_cast(y)); + texel[3] = 255; + } + } +} + +bool Safe(float coordinate) { + for (std::uint32_t size = Size; size >= Size / 2; size /= 2) { + for (const float offset : {0.0f, -0.5f}) { + const float scaled = coordinate * static_cast(size) + offset; + const float fraction = scaled - std::floor(scaled); + if (fraction < Margin || fraction > 1.0f - Margin) return false; + } + } + return true; +} + +void FillInput() { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const float u = (static_cast((tid * 5u + 1u) % 12u) - 1.7f) / 8.0f; + const float v = (static_cast((tid * 7u + 3u) % 12u) - 1.7f) / 8.0f; + Require(Safe(u) && Safe(v), "image sample reduction: thread " + std::to_string(tid) + " samples too close to a texel boundary"); + Input[tid * 3u + 0u] = u; + Input[tid * 3u + 1u] = v; + Input[tid * 3u + 2u] = tid % 2u == 0u ? 0.25f : 0.75f; + } +} + +float Texel(std::uint32_t level, int x, int y) { + const int size = static_cast(Size >> level); + const auto cx = static_cast(std::clamp(x, 0, size - 1)); + const auto cy = static_cast(std::clamp(y, 0, size - 1)); + return level == 0 ? Level0[cy * Size + cx] : Level1[cy * (Size / 2) + cx]; +} + +float Expected(std::uint32_t tid, const Sampler& sampler) { + const float u = Input[tid * 3u + 0u]; + const float v = Input[tid * 3u + 1u]; + const float lod = Input[tid * 3u + 2u]; + std::vector footprint; + const auto gather = [&](std::uint32_t level) { + const float size = static_cast(Size >> level); + if (sampler.filter == FilterPoint) { + footprint.push_back(Texel(level, static_cast(std::floor(u * size)), static_cast(std::floor(v * size)))); + return; + } + const int x = static_cast(std::floor(u * size - 0.5f)); + const int y = static_cast(std::floor(v * size - 0.5f)); + for (int dy = 0; dy < 2; ++dy) { + for (int dx = 0; dx < 2; ++dx) footprint.push_back(Texel(level, x + dx, y + dy)); + } + }; + gather(sampler.mip == MipPoint && lod > 0.5f ? 1u : 0u); + return sampler.reduction == ReductionMin ? *std::min_element(footprint.begin(), footprint.end()) : *std::max_element(footprint.begin(), footprint.end()); +} + +std::array ExpectedGather(std::uint32_t tid) { + const int x = static_cast(std::floor(Input[tid * 3u + 0u] * static_cast(Size) - 0.5f)); + const int y = static_cast(std::floor(Input[tid * 3u + 1u] * static_cast(Size) - 0.5f)); + return {Texel(0, x, y + 1), Texel(0, x + 1, y + 1), Texel(0, x + 1, y), Texel(0, x, y)}; +} + +ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, std::uint32_t format, const Sampler& sampler, std::span code = Code) { + std::vector userData(28, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(sizeof(Input))); + const auto output = BufferDescriptor(Output.data(), static_cast(sizeof(Output))); + const auto texture = TextureDescriptor(format); + const auto samplerWords = SamplerDescriptor(sampler); + const auto colors = TextureDescriptor(Format8888UNorm, Colors.data(), 1); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + std::copy(texture.begin(), texture.end(), userData.begin() + 8); + std::copy(samplerWords.begin(), samplerWords.end(), userData.begin() + 16); + std::copy(colors.begin(), colors.end(), userData.begin() + 20); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + return ShaderRecompiler::Recompile(request); +} + +void Run(AgcDriver::VulkanDevice& device, const Sampler& sampler, const char* name) { + Output.fill(-1.0f); + const auto result = Compile(device, Format32Float, sampler); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(Code.data())); + device.WaitIdle(); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const float expected = Expected(tid, sampler); + Require(Output[tid] == expected, std::string(name) + ": thread " + std::to_string(tid) + " sampled " + std::to_string(Output[tid]) + ", expected " + std::to_string(expected)); + } +} + +void Gather(AgcDriver::VulkanDevice& device, const Sampler& sampler, const char* name) { + Output.fill(-1.0f); + const auto result = Compile(device, Format32Float, sampler, GatherCode); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(GatherCode.data())); + device.WaitIdle(); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto expected = ExpectedGather(tid); + for (std::uint32_t component = 0; component < 4u; ++component) { + const float sampled = Output[tid * 4u + component]; + Require(sampled == expected[component], std::string(name) + ": thread " + std::to_string(tid) + " component " + std::to_string(component) + " gathered " + std::to_string(sampled) + ", expected " + std::to_string(expected[component])); + } + } +} + +std::uint32_t PairedSamplers(const ShaderRecompiler::RecompileResult& result, std::uint32_t format) { + 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); + } + } + Require(false, "image sample reduction: no sampled image of format " + std::to_string(format)); + return 0; +} + +float ExpectedColor(std::uint32_t tid, bool sampled) { + if (!sampled) return static_cast(Red(static_cast(tid & 3u), 0)) / 255.0f; + const int x = static_cast(std::floor(Input[tid * 3u + 0u] * static_cast(Size) - 0.5f)); + const int y = static_cast(std::floor(Input[tid * 3u + 1u] * static_cast(Size) - 0.5f)); + return static_cast(std::min({Red(x, y), Red(x + 1, y), Red(x, y + 1), Red(x + 1, y + 1)})) / 255.0f; +} + +void RunWithColors(AgcDriver::VulkanDevice& device, const ShaderRecompiler::RecompileResult& result, std::span code, bool sampled, const char* name) { + const Sampler sampler{ReductionMin, FilterBilinear, MipPoint}; + Output.fill(-1.0f); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const float expected = Expected(tid, sampler); + Require(Output[tid] == expected, std::string(name) + ": thread " + std::to_string(tid) + " sampled " + std::to_string(Output[tid]) + " from 32_FLOAT, expected " + std::to_string(expected)); + const float color = ExpectedColor(tid, sampled); + Require(std::fabs(Output[Threads + tid] - color) <= 1.0f / 1024.0f, std::string(name) + ": thread " + std::to_string(tid) + " read " + std::to_string(Output[Threads + tid]) + " from 8_8_8_8, expected " + std::to_string(color)); + } +} + +void Reject(AgcDriver::VulkanDevice& device, std::uint32_t format, const Sampler& sampler, std::string_view reason) { + try { + static_cast(Compile(device, format, sampler)); + } catch (const std::exception& error) { + Require(std::string_view(error.what()).find(reason) != std::string_view::npos, std::string("unexpected rejection: ") + error.what()); + return; + } + Require(false, std::string("expected rejection: ") + std::string(reason)); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + FillTexels(); + FillColors(); + FillInput(); + const Sampler minBilinear{ReductionMin, FilterBilinear, MipPoint}; + const auto loaded = Compile(*device, Format32Float, minBilinear, LoadedCode); + Require(PairedSamplers(loaded, Format32Float) == 1u && PairedSamplers(loaded, Format8888UNorm) == 0u, "image_load of 8_8_8_8 next to a min sampler: wrong image-sampler pairs"); + const auto sampled = Compile(*device, Format32Float, minBilinear, SampledCode); + Require(PairedSamplers(sampled, Format32Float) == 1u && PairedSamplers(sampled, Format8888UNorm) == 1u, "8_8_8_8 sampled through a min sampler: wrong image-sampler pairs"); + static_cast(Compile(*device, Format32SInt, {ReductionMin, FilterPoint, MipPoint})); + Reject(*device, Format32SInt, {ReductionMin, FilterBilinear, MipPoint}, "needs point filtering"); + Reject(*device, Format32SInt, {ReductionMax, FilterPoint, MipLinear}, "needs point filtering"); + if (!device->SamplerFilterMinmax()) { + std::puts("skipped, the device has no min/max sampler reduction with component mapping"); + return VulkanTestSkipped; + } + Run(*device, {ReductionMin, FilterBilinear, MipPoint}, "min, bilinear, point mip"); + Run(*device, {ReductionMax, FilterBilinear, MipPoint}, "max, bilinear, point mip"); + Run(*device, {ReductionMin, FilterPoint, MipPoint}, "min, point, point mip"); + Run(*device, {ReductionMax, FilterPoint, MipPoint}, "max, point, point mip"); + Run(*device, {ReductionMin, FilterBilinear, MipNone}, "min, bilinear, no mip"); + Run(*device, {ReductionMax, FilterPoint, MipNone}, "max, point, no mip"); + Gather(*device, {ReductionMin, FilterBilinear, MipPoint}, "gather4_lz, min"); + Gather(*device, {ReductionMax, FilterBilinear, MipPoint}, "gather4_lz, max"); + RunWithColors(*device, loaded, LoadedCode, false, "min 32_FLOAT, image_load 8_8_8_8"); + try { + RunWithColors(*device, sampled, SampledCode, true, "min 32_FLOAT and 8_8_8_8"); + std::puts("the device filters 8_8_8_8 UNORM with min/max reduction"); + } catch (const std::exception& error) { + Require(std::string_view(error.what()).find("format 37 does not support min/max filtering is sampled through a min or max reduction sampler") != std::string_view::npos, std::string("min 32_FLOAT and 8_8_8_8: unexpected error: ") + error.what()); + } + std::puts("image sample reduction tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp b/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp index bac6a047a..cae8055d8 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp @@ -4,9 +4,12 @@ #include "IntermediateRepresentation/IrProgram.hpp" #include "Optimization/BindingAllocator.hpp" #include "Recompiler.hpp" +#include namespace ShaderRecompiler { +std::uint32_t PointFilteredSamplerWord(std::uint32_t word0, std::uint32_t filter); + class DescriptorBindingBuilder { public: 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 d88d1cb35..6791a87b8 100644 --- a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp +++ b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp @@ -240,6 +240,34 @@ UnnormalizedProof ProveUnnormalized(const ShaderInfo& info, const ResourceSnapsh return proof; } +std::vector SamplerElements(const IrBindingLayout& layout, const ShaderInfo& info) { + std::vector elements(info.samplers.size(), ShaderInfo::MaxSamplers); + for (const IrDescriptorBinding& logical : layout.descriptors) { + if (logical.kind != DescriptorBindingKind::Samplers) { + continue; + } + for (std::uint32_t element = 0; element < logical.resources.size() && element < ShaderInfo::MaxSamplers; element++) { + elements.at(logical.resources[element]) = element; + } + } + return elements; +} + +std::uint32_t ImageSamplerMask(const ShaderInfo& info, const std::vector& samplerElements, std::uint32_t resource) { + const std::uint32_t root = info.images.at(resource).indirectRoot; + std::uint32_t mask = 0; + for (const SampledResourcePair& pair : info.sampledPairs) { + if (pair.image != resource && pair.image != root) { + continue; + } + if (pair.sampler >= samplerElements.size() || samplerElements[pair.sampler] >= ShaderInfo::MaxSamplers) { + fail("DescriptorBindingBuilder::Populate sampled image pair names a sampler outside the first " + std::to_string(ShaderInfo::MaxSamplers) + " elements of the sampler binding"); + } + mask |= 1u << samplerElements[pair.sampler]; + } + return mask; +} + std::vector ShaderDataDwordsFor(const IrBindingLayout& layout, std::uint32_t userDataBase, const ResourceSnapshot& snapshot, const std::array& partialThreads) { std::vector result(layout.ShaderDataDwords(), 0u); for (std::size_t i = 0; i < layout.userDataRegisters.size(); i++) { @@ -260,6 +288,15 @@ std::vector ShaderDataDwordsFor(const IrBindingLayout& layout, st } +std::uint32_t PointFilteredSamplerWord(std::uint32_t word0, std::uint32_t filter) { + const bool reduced = ((word0 >> 29u) & 3u) != 0u; + if (reduced && (((filter >> 20u) & 0xfu) != 0u || ((filter >> 26u) & 3u) == 2u)) { + fail("DescriptorBindingBuilder: a min or max reduction sampler that filters between texels or mip levels samples an image that needs point filtering (sint, converted or depth-bits format), which is not implemented"); + } + const bool mipmapped = ((filter >> 26u) & 3u) != 0u; + return (filter & ~(0xffu << 20u)) | (1u << 24u) | (mipmapped ? 1u << 26u : 0u); +} + 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); } @@ -268,6 +305,7 @@ void DescriptorBindingBuilder::Populate(BindingAllocationResult& allocation, con 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); std::vector bindings; bindings.reserve(layout.descriptors.size()); @@ -305,6 +343,7 @@ void DescriptorBindingBuilder::Populate(BindingAllocationResult& allocation, con physical.imageDepthCompare.push_back(image.depthCompare); physical.imageAtomic.push_back(image.atomic); physical.imageUnnormalized.push_back(unnormalized.images.at(resource)); + physical.imageSamplers.push_back(ImageSamplerMask(info, samplerElements, resource)); } break; case DescriptorRole::GuestSamplers: @@ -315,8 +354,7 @@ void DescriptorBindingBuilder::Populate(BindingAllocationResult& allocation, con physical.samplerUnnormalized.push_back(unnormalized.samplers.at(logical.resources[element])); if (sampler.forcePointFiltering) { auto& filter = physical.guestDescriptor.at(element * 4u + 2u); - const bool mipmapped = ((filter >> 26u) & 3u) != 0u; - filter = (filter & ~(0xffu << 20u)) | (1u << 24u) | (mipmapped ? 1u << 26u : 0u); + filter = PointFilteredSamplerWord(physical.guestDescriptor.at(element * 4u), filter); } } break; diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index f88ed74a3..fcb675049 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -536,6 +536,7 @@ std::uint32_t emulatedCompareState(const IrResourcePlan& plan, const ResourceSna const auto clampY = (words[0] >> 3u) & 0x7u; const auto function = (words[0] >> 12u) & 0x7u; const bool unnormalized = ((words[0] >> 15u) & 0x1u) != 0u; + if (((words[0] >> 29u) & 0x3u) != 0u) throw std::runtime_error("comparison sampling of a color texture through a min or max reduction sampler is not implemented"); const auto lodBias = words[2] & 0x3fffu; const auto magFilter = (words[2] >> 20u) & 0x3u; const auto minFilter = (words[2] >> 22u) & 0x3u; diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index e3d69389d..8d8b10416 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -330,6 +330,7 @@ struct DescriptorBinding { std::vector bufferWritten; std::vector samplerUnnormalized; std::vector imageUnnormalized; + std::vector imageSamplers; }; struct VertexAttribute { diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index d060071fd..0f9f0d0dc 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -64,7 +64,7 @@ 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) == 384, "DescriptorBinding changed: update the binding encoder"); +static_assert(sizeof(DescriptorBinding) == 408, "DescriptorBinding 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"); @@ -253,6 +253,7 @@ void encodeBinding(Writer& writer, const DescriptorBinding& binding) { writer.Flags(binding.bufferWritten); writer.Flags(binding.samplerUnnormalized); writer.Flags(binding.imageUnnormalized); + writer.Values(std::span(binding.imageSamplers)); } void decodeBinding(Reader& reader, DescriptorBinding& binding) { @@ -274,6 +275,7 @@ void decodeBinding(Reader& reader, DescriptorBinding& binding) { reader.Flags(binding.bufferWritten); reader.Flags(binding.samplerUnnormalized); reader.Flags(binding.imageUnnormalized); + reader.Values(binding.imageSamplers); } void encodeResult(Writer& writer, const RecompileResult& result) { diff --git a/core/shader/recompiler/ShaderDiskCache.hpp b/core/shader/recompiler/ShaderDiskCache.hpp index 3abc5c257..a0af13f93 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 = 5; +inline constexpr std::uint32_t FormatVersion = 6; enum class LoadStatus { Loaded, diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 1d7e36087..b0eb65037 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -222,6 +222,8 @@ Throughout the project, every function at every stage either **does exactly what - [Converted unorm images](../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (10_11_11_UNORM, read and written through an R32_UINT view): samples, gathers, LOD queries, 16-bit data and a DST_SEL of the missing W channel throw. - [Converted unorm image loads](../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) divide by 2^n - 1 with the buffer format path's `OpFDiv`, which Vulkan allows to be up to 2.5 ULP off, so some drivers are not bit-exact. - [64-bit LDS atomics](../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_*_u64`, `ds_*_b64`, `ds_*_f64`) read and write the two dwords under a workgroup lock stored after the guest LDS, as LDS is a 32-bit array without 64-bit atomics. They are atomic with respect to each other only: 32-bit accesses to the same dwords issued concurrently are not ordered by the lock. 64-bit GDS atomics throw. `ds_min/max(_rtn)_f64` and `ds_cmpst(_rtn)_f64` follow the NaN and signed-zero rules measured for the f32 forms; the f64 forms were not measured on hardware. +- [Min/max S# reduction](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) - `FILTER_MODE` 1 and 2 use Vulkan's min and max reduction, assumed to reduce the same texels as the console's filter (those of non-zero weight). +- [Min/max S#](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver, shader recompiler) - throws with anisotropy, a linear mip filter, depth or color-texture compare, or bilinear filtering of an image the recompiler point-samples (sint, converted, depth-bits). - [wcsrtombs_s](../../core/libs/prx/libc/src/Multibyte.cpp) (libc) follows the console's `libc.prx`, but a runtime-constraint violation only returns its error: the default handler there writes the message and a newline to `stderr` before returning, which is not done here. Conversion uses the C locale of `wcrtomb` (`MB_CUR_MAX` 1), so the path where a multibyte character does not fit the remaining space is not reached - [collate::_Getcat](../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) builds the facet for the C locale only and throws for any other locale name. The console's `libc.prx` stores pointers to its collation state tables at offsets 0x10 and 0x18 of the facet; ours leaves them null, as only the facet's own functions read them. `do_transform` throws for input with embedded null characters, which the console drops depending on the output buffer size, and for results of 4095 characters or more, whose string storage uses the guest's over-aligned large allocation. - [Thread exit destructors](../../core/libs/prx/libSceLibcInternal/Export.cpp) (libSceLibcInternal) run when a libkernel thread finishes; on the main or a host thread only `_sceLibcInternalThreadDtors` runs them, as `exit` does, else they are dropped