From 0fe4d66b7e6857a0d2369791963de5df481a17b5 Mon Sep 17 00:00:00 2001 From: Gjon Gojcevic Date: Wed, 7 Oct 2026 20:59:35 -0400 Subject: [PATCH 1/2] fix(agc): sample the base level of mipmapped views with unnormalized coordinates Vulkan only accepts unnormalized samplers on single-level views, while the guest binds full mip chains and samples their base level. Bind a lazily created one-level view of the descriptor's base mip instead of refusing mipmapped views and views that start above mip 0. --- .../Graphics/include/ShaderResources.hpp | 1 + .../Graphics/include/Texture.hpp | 7 +++++++ .../Graphics/src/ShaderResources.cpp | 11 +++++++--- .../libSceAgcDriver/Graphics/src/Texture.cpp | 21 +++++++++++++++++++ 4 files changed, 37 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index 43cc45c10..a4b6f50b6 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -436,6 +436,7 @@ private: std::uint64_t dispatchThreads = 0; std::vector> textures; std::vector textureFirstLayer; + std::vector textureBaseLevel; std::vector> storageTextures; std::vector storageMips; std::vector storageKeys; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index 6bc130edb..9f4c7765a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -13,6 +13,8 @@ #include #include #include +#include +#include #include #include #include @@ -79,6 +81,7 @@ public: VkImageView View() const; VkImageView FirstLayerView() const { return firstLayerView; } + VkImageView BaseLevelView() const; struct ViewRange { VkImageViewType type; std::uint32_t levels; @@ -101,6 +104,7 @@ public: private: void release() noexcept; void createFirstLayerView(const GuestTextureResource& descriptor, VkImageViewCreateInfo viewInfo); + void prepareBaseLevelView(const GuestTextureResource& descriptor, VkImageViewCreateInfo viewInfo); // Held by value: cached textures outlive the Context of the draw that created them. Context context; @@ -112,6 +116,9 @@ private: VkImageView firstLayerView = VK_NULL_HANDLE; ViewRange viewRange{}; ViewRange firstLayerRange{}; + std::optional baseLevelInfo; + mutable std::mutex baseLevelMutex; + mutable VkImageView baseLevelView = VK_NULL_HANDLE; VkImageLayout layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; VkDeviceSize allocationBytes = 0; VkDeviceSize countedBytes = 0; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 72b8204af..b8bfaad72 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -1506,7 +1506,7 @@ void ShaderResources::buildComplete() { write.pImageInfo = images.data() + images.size(); for (const auto index : binding.imageAllocations) { 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()}); + else images.push_back({VK_NULL_HANDLE, textureBaseLevel[index] ? textures[index]->BaseLevelView() : textureFirstLayer[index] ? textures[index]->FirstLayerView() : textures[index]->View(), textures[index]->Layout()}); } Require(binding.imageAllocations.size() == binding.layout.descriptorCount, "descriptor allocations disagree with compact binding"); break; @@ -3153,10 +3153,14 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind (texture != nullptr ? counters.fastHits : counters.fastMisses).fetch_add(1, std::memory_order_relaxed); } if (texture == nullptr) texture = cachedTexture(context, words, resource, components, guestBytes, depthCompare); + bool baseLevel = false; if (element < binding.imageUnnormalized.size() && binding.imageUnnormalized[element]) { const auto range = texture->SampledViewRange(firstLayer); - const bool singleLevel = range.levels == 1u && range.layers == 1u && resource.baseLevel == 0u && EffectiveMinLod(resource) == 0.0f; - if (!singleLevel || (range.type != VK_IMAGE_VIEW_TYPE_1D && range.type != VK_IMAGE_VIEW_TYPE_2D)) throw std::runtime_error("AGC graphics: guest texture sampled with unnormalized coordinates is not a single-level, single-layer 1D or 2D view starting at mip 0, which is not implemented (base level " + std::to_string(resource.baseLevel) + ", levels " + std::to_string(range.levels) + ", layers " + std::to_string(range.layers) + ", view type " + std::to_string(static_cast(range.type)) + ")"); + const bool exactAtBase = element < binding.imageUnnormalizedBaseLevel.size() && binding.imageUnnormalizedBaseLevel[element]; + const bool singleLevel = range.levels == 1u && resource.baseLevel == 0u; + const bool supported = range.layers == 1u && EffectiveMinLod(resource) == 0.0f && (singleLevel || exactAtBase); + if (!supported || (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-layer 1D or 2D view without a minimum LOD clamp, and is neither a single-level view at mip 0 nor read only by image_sample_lz through an S# without minimum LOD or bias, 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)) + ")"); + baseLevel = range.levels > 1u; } RequireFilterMinmax(context, texture->ViewFormat(), binding.imageSamplers[element], shaderSamplers); RequireBorderSwizzle(resource.bcSwizzle, binding.imageSamplers[element], shaderSamplers); @@ -3176,6 +3180,7 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind } textures.push_back(std::move(texture)); textureFirstLayer.push_back(firstLayer); + textureBaseLevel.push_back(baseLevel); describedRanges.push_back({"texture", resource.baseAddress, guestBytes, resource.width, resource.height, resource.format, static_cast(resource.tileMode), resource.dccAddress}); item.imageAllocations.push_back(textures.size() - 1); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index ee9ba01ad..6c75fc404 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -445,6 +445,7 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &view), "vkCreateImageView"); viewRange = {viewInfo.viewType, viewInfo.subresourceRange.levelCount, viewInfo.subresourceRange.layerCount}; createFirstLayerView(descriptor, viewInfo); + prepareBaseLevelView(descriptor, viewInfo); if (profile) { auto& totals = Profile(); totals.view += timer.lap(); @@ -646,6 +647,7 @@ Texture::Texture(const Context& context, const std::shared_ptr& Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &view), "vkCreateImageView storage view"); viewRange = {viewInfo.viewType, viewInfo.subresourceRange.levelCount, viewInfo.subresourceRange.layerCount}; createFirstLayerView(descriptor, viewInfo); + prepareBaseLevelView(descriptor, viewInfo); if (profile) { auto& totals = Profile(); totals.view += timer.lap(); @@ -683,12 +685,31 @@ void Texture::createFirstLayerView(const GuestTextureResource& descriptor, VkIma firstLayerRange = {viewInfo.viewType, viewInfo.subresourceRange.levelCount, viewInfo.subresourceRange.layerCount}; } +void Texture::prepareBaseLevelView(const GuestTextureResource& descriptor, VkImageViewCreateInfo viewInfo) { + if (viewInfo.subresourceRange.levelCount == 1u) return; + if (descriptor.dimension != TextureDimension::k1D && descriptor.dimension != TextureDimension::k2D && descriptor.dimension != TextureDimension::k2DArray) return; + viewInfo.pNext = nullptr; + viewInfo.viewType = descriptor.dimension == TextureDimension::k1D ? VK_IMAGE_VIEW_TYPE_1D : VK_IMAGE_VIEW_TYPE_2D; + viewInfo.subresourceRange.levelCount = 1; + viewInfo.subresourceRange.layerCount = 1; + baseLevelInfo = viewInfo; +} + +VkImageView Texture::BaseLevelView() const { + Require(baseLevelInfo.has_value(), "a base-level view needs a mipmapped 1D, 2D or 2D array texture"); + std::lock_guard lock(baseLevelMutex); + if (!baseLevelView) Check(context.Function("vkCreateImageView")(context.device, &*baseLevelInfo, nullptr, &baseLevelView), "vkCreateImageView base level"); + return baseLevelView; +} + void Texture::release() noexcept { upload.reset(); if (view) context.Function("vkDestroyImageView")(context.device, view, nullptr); view = VK_NULL_HANDLE; if (firstLayerView) context.Function("vkDestroyImageView")(context.device, firstLayerView, nullptr); firstLayerView = VK_NULL_HANDLE; + if (baseLevelView) context.Function("vkDestroyImageView")(context.device, baseLevelView, nullptr); + baseLevelView = VK_NULL_HANDLE; // The image and its memory go with the last holder: this texture, or the batch still uploading it. owned.reset(); image = VK_NULL_HANDLE; From ac1c3b775b05ffae3d8fd5da8f57a8e2dff85de2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 11 Oct 2026 11:07:20 +0200 Subject: [PATCH 2/2] fix(agc): read the base level of a mipmapped view through unnormalized coordinates only where the hardware does Multi-level views, and views starting above mip 0, are bound as their base level only when every use of the image is image_sample_lz through an unnormalized S# without minimum LOD or LOD bias, the case measured exact on RDNA2. Selected-LOD, implicit-LOD and offset uses keep throwing. The recompiler records selected-LOD uses (SamplerUseNonZeroLod) and proves the case per image (imageUnnormalizedBaseLevel); the shader disk cache format goes to 27. Co-Authored-By: Claude --- .../Graphics/src/ShaderResources.cpp | 1 + .../libSceAgcDriver/tests/ShaderDiskCache.cpp | 3 +- .../libSceAgcDriver/tests/ShaderMemory.cpp | 15 +++ .../execution/ImageSampleUnnormalized.cpp | 105 ++++++++++++------ .../IrMetadata/Resources.hpp | 7 +- .../src/DescriptorBindingBuilder.cpp | 16 ++- .../Optimization/src/ShaderInfoCollector.cpp | 9 +- core/shader/recompiler/Recompiler.hpp | 1 + core/shader/recompiler/ShaderDiskCache.cpp | 8 +- core/shader/recompiler/ShaderDiskCache.hpp | 2 +- 10 files changed, 120 insertions(+), 47 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index b8bfaad72..9ec3b927c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -1681,6 +1681,7 @@ void VisitContentKey(const CompiledShader& shader, bool dataWords, bool movableB packBits(binding.samplerUnnormalized); packBits(binding.samplerIntegerBorder); packBits(binding.imageUnnormalized); + packBits(binding.imageUnnormalizedBaseLevel); // 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); diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index fc10c39cb..fee50cb71 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -41,7 +41,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.bufferRead == right.bufferRead && left.samplerUnnormalized == right.samplerUnnormalized && left.samplerIntegerBorder == right.samplerIntegerBorder && left.imageUnnormalized == right.imageUnnormalized && left.imageSamplers == right.imageSamplers; + return left.kind == right.kind && left.role == right.role && left.descriptorSet == right.descriptorSet && left.binding == right.binding && left.count == right.count && left.guestDescriptor == right.guestDescriptor && left.readOnly == right.readOnly && left.imageShape == right.imageShape && left.samplerDepthCompare == right.samplerDepthCompare && left.imageWritten == right.imageWritten && left.imageDepthCompare == right.imageDepthCompare && left.imageAtomic == right.imageAtomic && left.bufferAtomic == right.bufferAtomic && left.bufferWritten == right.bufferWritten && left.bufferRead == right.bufferRead && left.samplerUnnormalized == right.samplerUnnormalized && left.samplerIntegerBorder == right.samplerIntegerBorder && left.imageUnnormalized == right.imageUnnormalized && left.imageUnnormalizedBaseLevel == right.imageUnnormalizedBaseLevel && left.imageSamplers == right.imageSamplers; } bool sameBindings(const std::vector& left, const std::vector& right) { @@ -121,6 +121,7 @@ DescriptorBinding sampleBinding(std::uint32_t seed) { binding.samplerUnnormalized = {false, true, true}; binding.samplerIntegerBorder = {true, false, seed % 2 == 1}; binding.imageUnnormalized = {true, false, seed % 2 == 0}; + binding.imageUnnormalizedBaseLevel = {false, true, seed % 3 == 0}; binding.imageSamplers = {0x5u, 0u, 0x80000000u}; binding.bufferRead = {true, false, false, true, true, false}; return binding; diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index a933ca489..3000968e3 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -2096,6 +2096,21 @@ void verifyUnusedUnnormalizedSampler() { snapshot.images[0].dwords[3] = 0xa0000facu; expectFailure([&] { static_cast(populate(foldVolume)); }, "takes only images an unnormalized S# can sample", "unnormalized texel offsets: an ordinary 3D view matched a constant-only mode"); snapshot.images[0].dwords[3] = 0x90000facu; + const auto atBaseLevel = [&](const ShaderInfo& shader) { return populate(shader)[0].imageUnnormalizedBaseLevel; }; + auto levelZero = info; + levelZero.samplers[0].uses = SamplerUseExplicitLod; + require(atBaseLevel(levelZero) == std::vector{true}, "unnormalized samplers: an image read only by image_sample_lz was not flagged as exact at its base level"); + require(atBaseLevel(info) == std::vector{false}, "unnormalized samplers: an image without live uses was flagged as exact at its base level"); + require(atBaseLevel(offset) == std::vector{false}, "unnormalized samplers: an image read with a texel offset was flagged as exact at its base level"); + auto selectedLod = levelZero; + selectedLod.samplers[0].uses |= SamplerUseNonZeroLod; + require(atBaseLevel(selectedLod) == std::vector{false}, "unnormalized samplers: an image also read at a selected LOD was flagged as exact at its base level"); + snapshot.samplers[0].dwords[1] = 0x00fff100u; + require(atBaseLevel(levelZero) == std::vector{false}, "unnormalized samplers: an S# with a minimum LOD was flagged as exact at the base level"); + snapshot.samplers[0].dwords[1] = 0x00fff000u; + snapshot.samplers[0].dwords[2] = 0x05500180u; + require(atBaseLevel(levelZero) == std::vector{false}, "unnormalized samplers: an S# with a LOD bias was flagged as exact at the base level"); + snapshot.samplers[0].dwords[2] = 0x05500000u; snapshot.samplers[0].dwords[0] = 0x00000092u; const auto normalized = populate(info); require(normalized[0].imageUnnormalized == std::vector{false} && normalized[1].samplerUnnormalized == std::vector{false}, "unnormalized samplers: a normalized S# was flagged"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleUnnormalized.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleUnnormalized.cpp index 7422e5e65..9a5394069 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleUnnormalized.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleUnnormalized.cpp @@ -47,6 +47,12 @@ alignas(256) constexpr std::array Code{ 0x80001001, 0xe0701034, 0x80001101, 0xe0701038, 0x80001201, 0xe070103c, 0x80001301, 0xbf810000, }; +alignas(256) constexpr std::array LevelZeroCode{ + 0x34020086, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xbf8c3f70, 0xf09c0f08, 0x00610802, + 0xbf8c3f70, 0xe0701010, 0x80000801, 0xe0701014, 0x80000901, 0xe0701018, 0x80000a01, 0xe070101c, + 0x80000b01, 0xbf810000, +}; + alignas(256) constexpr std::array OffsetCode{ 0x34020086, 0xe0301008, 0x80000201, 0xe0301000, 0x80000301, 0xe0301004, 0x80000401, 0xbf8c3f70, 0x7e0a02ff, 0x402ccccd, 0x7e2802ff, 0x0000023d, 0xf0dc0f08, 0x00610802, 0xf0d00f08, 0x00610c02, @@ -64,9 +70,11 @@ struct Program { std::span code; std::array instructions; std::array offsets; + std::size_t count = 3; }; const Program Plain{Code, {"image_sample_lz", "image_sample_l 2.7", "image_sample"}, {Offset::None, Offset::None, Offset::None}}; +const Program LevelZero{LevelZeroCode, {"image_sample_lz", "", ""}, {Offset::None, Offset::None, Offset::None}, 1}; const Program Offsets{OffsetCode, {"image_sample_lz_o", "image_sample_l_o 2.7", "image_sample_lz_o (-3, 2) NSA"}, {Offset::Lane, Offset::Lane, Offset::Constant}}; alignas(256) constexpr std::array ConstantCode{ @@ -82,14 +90,15 @@ struct SamplerCase { std::array words; bool linear; bool border; + bool atBaseLevel; }; constexpr std::array Samplers{{ - {"bilinear clamp-to-last-texel, point mips, MAX_LOD 0xfff", {0x00008092u, 0x00fff000u, 0x05500000u, 0u}, true, false}, - {"point clamp-to-last-texel, point mips", {0x00008092u, 0x00fff000u, 0x04000000u, 0u}, false, false}, - {"bilinear clamp-to-last-texel, linear mips, MIN_LOD 1, LOD bias 1.5", {0x00008092u, 0x00fff100u, 0x09500180u, 0u}, true, false}, - {"bilinear clamp-to-border, transparent black", {0x000080b6u, 0x00fff000u, 0x05500000u, 0u}, true, true}, - {"point clamp-to-border, no mips, transparent black", {0x000080b6u, 0u, 0u, 0u}, false, true}, + {"bilinear clamp-to-last-texel, point mips, MAX_LOD 0xfff", {0x00008092u, 0x00fff000u, 0x05500000u, 0u}, true, false, true}, + {"point clamp-to-last-texel, point mips", {0x00008092u, 0x00fff000u, 0x04000000u, 0u}, false, false, true}, + {"bilinear clamp-to-last-texel, linear mips, MIN_LOD 1, LOD bias 1.5", {0x00008092u, 0x00fff100u, 0x09500180u, 0u}, true, false, false}, + {"bilinear clamp-to-border, transparent black", {0x000080b6u, 0x00fff000u, 0x05500000u, 0u}, true, true, true}, + {"point clamp-to-border, no mips, transparent black", {0x000080b6u, 0u, 0u, 0u}, false, true, true}, }}; struct Coordinate { @@ -99,21 +108,31 @@ struct Coordinate { std::int32_t dv = 0; }; -std::vector Coordinates() { +std::int32_t LevelWidth(std::uint32_t level) { + return std::max(Width >> level, 1); +} + +std::int32_t LevelHeight(std::uint32_t level) { + return std::max(Height >> level, 1); +} + +std::vector Coordinates(std::uint32_t level) { + const auto width = LevelWidth(level); + const auto height = LevelHeight(level); std::vector us; - for (std::int32_t k = 0; k < Width; ++k) { + for (std::int32_t k = 0; k < width; ++k) { for (const float fraction : {0.5f, 0.0f, 0.25f, 0.75f}) us.push_back(static_cast(k) + fraction); } - for (const float edge : {-1.0f, -0.5f, 13.0f, 14.0f, 12.75f}) us.push_back(edge); + for (const float edge : {-1.0f, -0.5f, static_cast(width), static_cast(width + 1), static_cast(width) - 0.25f}) us.push_back(edge); constexpr std::array fractions{0.5f, 0.0f, 0.75f}; std::vector result; for (std::size_t index = 0; index < us.size(); ++index) { for (std::size_t fraction = 0; fraction < fractions.size(); ++fraction) { - result.push_back({us[index], static_cast((index * fractions.size() + fraction) % Height) + fractions[fraction]}); + result.push_back({us[index], static_cast((index * fractions.size() + fraction) % height) + fractions[fraction]}); } } - for (std::int32_t x = 0; x < Width; ++x) { - const float v = static_cast(x % Height) + 0.5f; + for (std::int32_t x = 0; x < width; ++x) { + const float v = static_cast(x % height) + 0.5f; result.push_back({static_cast(x) - 1.0f, v}); result.push_back({static_cast(x) + 2.0f, v}); } @@ -123,7 +142,7 @@ std::vector Coordinates() { std::vector OffsetCoordinates() { constexpr std::array us{-32, -5, -1, 0, 1, 3, 31}; constexpr std::array vs{-2, -1, 0, 2, -32, 31}; - auto result = Coordinates(); + auto result = Coordinates(0u); for (std::size_t index = 0; index < result.size(); ++index) { result[index].du = us[index % us.size()]; result[index].dv = vs[index % vs.size()]; @@ -131,7 +150,8 @@ std::vector OffsetCoordinates() { return result; } -std::array TexelOf(std::int32_t x, std::int32_t y) { +std::array TexelOf(std::uint32_t level, std::int32_t x, std::int32_t y) { + if (level == 1u) return {240.0 * ((x + y + 1) & 1), 32.0 * y + 48.0, 16.0 * x + 40.0, 200.0}; return {240.0 * ((x + y) & 1), 16.0 * x + 16.0, 32.0 * y + 32.0, 240.0}; } @@ -144,9 +164,9 @@ void FillTexture(std::span texels, std::uint32_t levels) { for (std::uint32_t y = 0; y < mip.height; ++y) { for (std::uint32_t x = 0; x < mip.width; ++x) { auto* texel = &texels[mip.tiledOffset + static_cast(y) * mip.pitchBytes + x * 4u]; - const auto value = TexelOf(static_cast(x), static_cast(y)); + const auto value = TexelOf(level, static_cast(x), static_cast(y)); for (std::uint32_t component = 0; component < 4u; ++component) { - texel[component] = level == 0u ? static_cast(value[component]) : OtherLevelTexel; + texel[component] = level <= 1u ? static_cast(value[component]) : OtherLevelTexel; } } } @@ -217,13 +237,15 @@ Samples Run(AgcDriver::VulkanDevice& device, const Program& program, const std:: return samples; } -std::array Fetch(const SamplerCase& sampler, std::int32_t x, std::int32_t y) { - if (sampler.border && (x < 0 || x >= Width || y < 0 || y >= Height)) return {0.0, 0.0, 0.0, 0.0}; - return TexelOf(std::clamp(x, 0, Width - 1), std::clamp(y, 0, Height - 1)); +std::array Fetch(const SamplerCase& sampler, std::uint32_t level, std::int32_t x, std::int32_t y) { + const auto width = LevelWidth(level); + const auto height = LevelHeight(level); + if (sampler.border && (x < 0 || x >= width || y < 0 || y >= height)) return {0.0, 0.0, 0.0, 0.0}; + return TexelOf(level, std::clamp(x, 0, width - 1), std::clamp(y, 0, height - 1)); } -std::array Reference(const SamplerCase& sampler, const Coordinate& coordinate) { - if (!sampler.linear) return Fetch(sampler, static_cast(std::floor(coordinate.u)), static_cast(std::floor(coordinate.v))); +std::array Reference(const SamplerCase& sampler, std::uint32_t level, const Coordinate& coordinate) { + if (!sampler.linear) return Fetch(sampler, level, static_cast(std::floor(coordinate.u)), static_cast(std::floor(coordinate.v))); const double u = static_cast(coordinate.u) - 0.5; const double v = static_cast(coordinate.v) - 0.5; const auto i = static_cast(std::floor(u)); @@ -232,22 +254,22 @@ std::array Reference(const SamplerCase& sampler, const Coordinate& co const double beta = v - j; std::array result{}; for (std::uint32_t component = 0; component < 4u; ++component) { - result[component] = (1.0 - alpha) * (1.0 - beta) * Fetch(sampler, i, j)[component] + alpha * (1.0 - beta) * Fetch(sampler, i + 1, j)[component] - + (1.0 - alpha) * beta * Fetch(sampler, i, j + 1)[component] + alpha * beta * Fetch(sampler, i + 1, j + 1)[component]; + result[component] = (1.0 - alpha) * (1.0 - beta) * Fetch(sampler, level, i, j)[component] + alpha * (1.0 - beta) * Fetch(sampler, level, i + 1, j)[component] + + (1.0 - alpha) * beta * Fetch(sampler, level, i, j + 1)[component] + alpha * beta * Fetch(sampler, level, i + 1, j + 1)[component]; } return result; } -void Check(const Program& program, const SamplerCase& sampler, const char* image, const std::vector& coordinates, const Samples& samples) { +void Check(const Program& program, const SamplerCase& sampler, std::uint32_t level, const char* image, const std::vector& coordinates, const Samples& samples) { Require(samples.size() == coordinates.size(), "image sample unnormalized: a dispatch lost samples"); const double tolerance = sampler.linear ? 0.5 : 0.25; for (std::size_t index = 0; index < coordinates.size(); ++index) { const auto& coordinate = coordinates[index]; - for (std::uint32_t instruction = 0; instruction < program.instructions.size(); ++instruction) { + for (std::uint32_t instruction = 0; instruction < program.count; ++instruction) { const auto offset = program.offsets[instruction]; const std::int32_t du = offset == Offset::Lane ? coordinate.du : offset == Offset::Constant ? ConstantOffsetU : 0; const std::int32_t dv = offset == Offset::Lane ? coordinate.dv : offset == Offset::Constant ? ConstantOffsetV : 0; - const auto expected = Reference(sampler, {coordinate.u + static_cast(du), coordinate.v + static_cast(dv)}); + const auto expected = Reference(sampler, level, {coordinate.u + static_cast(du), coordinate.v + static_cast(dv)}); for (std::uint32_t component = 0; component < 4u; ++component) { const double value = static_cast(std::bit_cast(samples[index][instruction * 4u + component])) * 255.0; Require(std::fabs(value - expected[component]) <= tolerance, std::string(program.instructions[instruction]) + ", " + sampler.name + ", " + image + ": (" + std::to_string(coordinate.u) + ", " + std::to_string(coordinate.v) + ") offset (" + std::to_string(du) + ", " + std::to_string(dv) + ") component " + std::to_string(component) + " is " + std::to_string(value) + ", expected " + std::to_string(expected[component])); @@ -256,10 +278,10 @@ void Check(const Program& program, const SamplerCase& sampler, const char* image } } -void ExpectFailure(AgcDriver::VulkanDevice& device, const std::array& texture, const std::array& sampler, std::string_view reason, const char* what) { +void ExpectFailure(AgcDriver::VulkanDevice& device, const Program& program, const std::array& texture, const std::array& sampler, std::string_view reason, const char* what) { const std::vector one{{0.5f, 0.5f}}; try { - static_cast(Run(device, Plain, texture, sampler, one)); + static_cast(Run(device, program, texture, sampler, one)); } catch (const std::exception& error) { Require(std::string_view(error.what()).find(reason) != std::string_view::npos, std::string(what) + ": unexpected error: " + error.what()); return; @@ -275,21 +297,36 @@ int main() { if (!device) return VulkanTestSkipped; FillTexture(SingleLevel, 1); FillTexture(MultiLevel, StorageLevels); - const auto coordinates = Coordinates(); + const auto coordinates = Coordinates(0u); + const auto levelOneCoordinates = Coordinates(1u); const auto single = TextureDescriptor(SingleLevel.data(), 0u, 0u); const auto multi = TextureDescriptor(MultiLevel.data(), 0u, StorageLevels - 1u); + const auto chain = TextureDescriptor(MultiLevel.data(), StorageLevels - 1u, StorageLevels - 1u); + auto fromLevelOne = chain; + fromLevelOne[3] |= 1u << 12u; + const bool driverSamplesUnnormalizedAboveMipZero = device->DeviceName().find("llvmpipe") == std::string::npos; const auto offsetCoordinates = OffsetCoordinates(); for (const auto& sampler : Samplers) { const auto singleSamples = Run(*device, Plain, single, sampler.words, coordinates); - Check(Plain, sampler, "1-level image", coordinates, singleSamples); + Check(Plain, sampler, 0u, "1-level image", coordinates, singleSamples); const auto multiSamples = Run(*device, Plain, multi, sampler.words, coordinates); - Check(Plain, sampler, "first level of a 4-level image", coordinates, multiSamples); + Check(Plain, sampler, 0u, "first level of a 4-level image", coordinates, multiSamples); Require(multiSamples == singleSamples, std::string(sampler.name) + ": the first level of a 4-level image does not sample like a 1-level image"); - Check(Offsets, sampler, "1-level image", offsetCoordinates, Run(*device, Offsets, single, sampler.words, offsetCoordinates)); + Check(Offsets, sampler, 0u, "1-level image", offsetCoordinates, Run(*device, Offsets, single, sampler.words, offsetCoordinates)); + if (!sampler.atBaseLevel) continue; + const auto levelZeroSingle = Run(*device, LevelZero, single, sampler.words, coordinates); + const auto levelZeroChain = Run(*device, LevelZero, chain, sampler.words, coordinates); + Check(LevelZero, sampler, 0u, "4-level view", coordinates, levelZeroChain); + Require(levelZeroChain == levelZeroSingle, std::string(sampler.name) + ": a 4-level view read by image_sample_lz does not sample its base level like a 1-level image"); + if (driverSamplesUnnormalizedAboveMipZero) Check(LevelZero, sampler, 1u, "3-level view starting at mip 1", levelOneCoordinates, Run(*device, LevelZero, fromLevelOne, sampler.words, levelOneCoordinates)); } - ExpectFailure(*device, TextureDescriptor(MultiLevel.data(), 2u, 2u), Samplers[0].words, "single-level", "a 3-level view"); - ExpectFailure(*device, single, {0x00008092u, 0x00fff000u, 0x05100000u, 0u}, "different minification", "unequal minification and magnification filters"); - ExpectFailure(*device, single, {0x00008090u, 0x00fff000u, 0x05500000u, 0u}, "clamp mode 0", "wrap on X"); + ExpectFailure(*device, Plain, TextureDescriptor(MultiLevel.data(), 2u, 2u), Samplers[0].words, "single-level", "a 3-level view read by a selected LOD"); + ExpectFailure(*device, Plain, chain, Samplers[0].words, "single-level", "a 4-level view read by image_sample_l and image_sample"); + ExpectFailure(*device, Plain, fromLevelOne, Samplers[0].words, "single-level", "a view starting at mip 1 read by image_sample_l and image_sample"); + ExpectFailure(*device, Offsets, chain, Samplers[0].words, "single-level", "a 4-level view read with texel offsets"); + ExpectFailure(*device, LevelZero, chain, Samplers[2].words, "single-level", "a 4-level view through an S# with a minimum LOD and a LOD bias"); + ExpectFailure(*device, Plain, single, {0x00008092u, 0x00fff000u, 0x05100000u, 0u}, "different minification", "unequal minification and magnification filters"); + ExpectFailure(*device, Plain, single, {0x00008090u, 0x00fff000u, 0x05500000u, 0u}, "clamp mode 0", "wrap on X"); const auto constantView = [&](std::uint32_t type, std::uint32_t format, std::uint32_t swizzle) { auto descriptor = TextureDescriptor(SingleLevel.data(), 0u, 0u); descriptor[1] = (descriptor[1] & ~(0x1ffu << 20u)) | (format << 20u); diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp index 27099a8ca..4137ad0e6 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp @@ -100,7 +100,7 @@ struct ImageResource { bool operator==(const ImageResource& other) const = default; }; -enum SamplerUse : std::uint8_t { +enum SamplerUse : std::uint16_t { SamplerUseExplicitLod = 1u << 0u, SamplerUseImplicitLod = 1u << 1u, SamplerUseGradient = 1u << 2u, @@ -109,6 +109,7 @@ enum SamplerUse : std::uint8_t { SamplerUseGather = 1u << 5u, SamplerUseQueryLod = 1u << 6u, SamplerUseAdjust = 1u << 7u, + SamplerUseNonZeroLod = 1u << 8u, }; inline bool ImageSampleExplicitLod(std::uint32_t flags, IrShaderStage stage) { @@ -123,14 +124,14 @@ struct SamplerResource { std::uint32_t copyOf = NoCopy; bool forcePointFiltering = false; bool depthCompare = false; - std::uint8_t uses = 0; + std::uint16_t uses = 0; bool foldTexelOffsetsOnly = false; bool operator==(const SamplerResource& other) const = default; }; inline bool FoldsTexelOffsets(const SamplerResource& sampler) { - return (sampler.uses & SamplerUseOffset) != 0u && (sampler.uses & ~static_cast(SamplerUseExplicitLod | SamplerUseOffset)) == 0u && !sampler.depthCompare; + return (sampler.uses & SamplerUseOffset) != 0u && (sampler.uses & ~static_cast(SamplerUseExplicitLod | SamplerUseOffset | SamplerUseNonZeroLod)) == 0u && !sampler.depthCompare; } struct SampledResourcePair { diff --git a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp index b2c07bab2..6b68b292a 100644 --- a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp +++ b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp @@ -160,10 +160,11 @@ const char* UnnormalizedUseReason(std::uint32_t uses) { struct UnnormalizedProof { std::vector samplers; std::vector images; + std::vector imagesAtBaseLevel; }; UnnormalizedProof ProveUnnormalized(const ShaderInfo& info, const ResourceSnapshot& snapshot) { - UnnormalizedProof proof{std::vector(info.samplers.size()), std::vector(info.images.size())}; + UnnormalizedProof proof{std::vector(info.samplers.size()), std::vector(info.images.size()), std::vector(info.images.size())}; for (std::uint32_t r = 0; r < info.samplers.size(); r++) { if (snapshot.samplers.at(r).dwordCount != 4u) fail("sampler descriptor must contain four dwords"); const auto& sampler = info.samplers[r]; @@ -173,7 +174,7 @@ UnnormalizedProof ProveUnnormalized(const ShaderInfo& info, const ResourceSnapsh } continue; } - const std::uint32_t unsupported = sampler.uses & ~static_cast(SamplerUseExplicitLod | SamplerUseOffset); + const std::uint32_t unsupported = sampler.uses & ~static_cast(SamplerUseExplicitLod | SamplerUseOffset | SamplerUseNonZeroLod); if (unsupported != 0u) { failUnnormalized(UnnormalizedUseReason(unsupported)); } @@ -194,6 +195,16 @@ UnnormalizedProof ProveUnnormalized(const ShaderInfo& info, const ResourceSnapsh } proof.samplers[r] = true; } + for (std::uint32_t resource = 0; resource < info.images.size(); resource++) { + bool exact = proof.images[resource]; + for (const auto& pair : info.sampledPairs) { + if (!exact) break; + if (pair.image != resource) continue; + const auto& words = snapshot.samplers.at(pair.sampler).dwords; + exact = proof.samplers[pair.sampler] && info.samplers.at(pair.sampler).uses == SamplerUseExplicitLod && (words[1] & 0xfffu) == 0u && (words[2] & 0x3fffu) == 0u; + } + proof.imagesAtBaseLevel[resource] = exact; + } return proof; } @@ -390,6 +401,7 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l physical.imageDepthCompare.push_back(mode.depthCompare); physical.imageAtomic.push_back(image.atomic); physical.imageUnnormalized.push_back(unnormalized.images.at(resource)); + physical.imageUnnormalizedBaseLevel.push_back(unnormalized.imagesAtBaseLevel.at(resource)); physical.imageSamplers.push_back(0u); physical.imageAtomic64.push_back(image.atomic64); if (layout.runtimeImageCount == 0u || image.indirectRoot == ImageResource::NoIndirectImage) continue; diff --git a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp index 672f01ef4..00bde0c18 100644 --- a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp +++ b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp @@ -457,7 +457,7 @@ void CollectOutputs(const IrProgram& program, ShaderStageInputInfo inputInfo, Sh } } -std::uint8_t SamplerUseOf(IrOpcode opcode, std::uint32_t flags, IrShaderStage stage) { +std::uint16_t SamplerUseOf(IrOpcode opcode, std::uint32_t flags, IrShaderStage stage) { if (opcode == IrOpcode::ImageGatherRaw) { return SamplerUseGather; } @@ -465,6 +465,9 @@ std::uint8_t SamplerUseOf(IrOpcode opcode, std::uint32_t flags, IrShaderStage st return SamplerUseQueryLod; } std::uint32_t use = (flags & RdnaImageSampleFlagDerivative) != 0u ? SamplerUseGradient : ImageSampleExplicitLod(flags, stage) ? SamplerUseExplicitLod : SamplerUseImplicitLod; + if (use == SamplerUseExplicitLod && (flags & RdnaImageSampleFlagLevelZero) == 0u) { + use |= SamplerUseNonZeroLod; + } if ((flags & RdnaImageSampleFlagOffset) != 0u) { use |= SamplerUseOffset; } @@ -474,7 +477,7 @@ std::uint8_t SamplerUseOf(IrOpcode opcode, std::uint32_t flags, IrShaderStage st if ((flags & RdnaImageSampleFlagAdjust) != 0u) { use |= SamplerUseAdjust; } - return static_cast(use); + return static_cast(use); } void CollectSamplerUses(const IrProgram& program, ShaderInfo& info) { @@ -496,7 +499,7 @@ void CollectSamplerUses(const IrProgram& program, ShaderInfo& info) { return Fail("image instruction references an unknown sampler"); } auto& sampler = info.samplers[memory.sampler]; - sampler.uses = static_cast(sampler.uses | SamplerUseOf(inst->Opcode(), memory.imageSampleFlags, program.Resources().stage)); + sampler.uses = static_cast(sampler.uses | SamplerUseOf(inst->Opcode(), memory.imageSampleFlags, program.Resources().stage)); } } } diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 668983cbe..deca904d9 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -368,6 +368,7 @@ struct DescriptorBinding { std::vector samplerUnnormalized; std::vector samplerIntegerBorder; std::vector imageUnnormalized; + std::vector imageUnnormalizedBaseLevel; std::vector imageSamplers; std::vector bufferRead; }; diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index a9bc8fb13..bd05440bc 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -67,7 +67,7 @@ namespace ShaderRecompiler::ShaderDiskCache { static_assert(sizeof(CompiledShaderArtifact) == 192, "CompiledShaderArtifact changed: update the artifact encoder"); static_assert(sizeof(ShaderInvocation) == 112, "ShaderInvocation changed: update the invocation encoder"); static_assert(sizeof(RecompileResult) == 312, "RecompileResult changed: update EncodeResult and DecodeResult"); -static_assert(sizeof(DescriptorBinding) == 528, "DescriptorBinding changed: update the binding encoder"); +static_assert(sizeof(DescriptorBinding) == 568, "DescriptorBinding changed: update the binding encoder"); static_assert(sizeof(VertexAttribute) == 32, "VertexAttribute changed: update the attribute encoder"); static_assert(sizeof(VertexInput) == 16, "VertexInput changed: update the vertex input encoder"); static_assert(sizeof(FragmentParameter) == 12, "FragmentParameter changed: update the parameter encoder"); @@ -75,7 +75,7 @@ static_assert(sizeof(CompiledShaderInfo) == 344, "CompiledShaderInfo changed: up static_assert(sizeof(ShaderInfo) == 232, "ShaderInfo changed: update the info encoder"); static_assert(sizeof(BufferResource) == 32, "BufferResource changed: update the info encoder"); static_assert(sizeof(ImageResource) == 120, "ImageResource changed: update the info encoder"); -static_assert(sizeof(SamplerResource) == 16, "SamplerResource changed: update the info encoder"); +static_assert(sizeof(SamplerResource) == 20, "SamplerResource changed: update the info encoder"); static_assert(sizeof(SampledResourcePair) == 12, "SampledResourcePair changed: update the info encoder"); static_assert(sizeof(StageInput) == 56, "StageInput changed: update the info encoder"); static_assert(sizeof(StageOutput) == 48, "StageOutput changed: update the info encoder"); @@ -256,6 +256,7 @@ void encodeBinding(Writer& writer, const DescriptorBinding& binding) { writer.Flags(binding.bufferWritten); writer.Flags(binding.samplerUnnormalized); writer.Flags(binding.imageUnnormalized); + writer.Flags(binding.imageUnnormalizedBaseLevel); writer.Flags(binding.samplerIntegerBorder); writer.Values(std::span(binding.imageSamplers)); writer.Flags(binding.bufferRead); @@ -281,6 +282,7 @@ void decodeBinding(Reader& reader, DescriptorBinding& binding) { reader.Flags(binding.bufferWritten); reader.Flags(binding.samplerUnnormalized); reader.Flags(binding.imageUnnormalized); + reader.Flags(binding.imageUnnormalizedBaseLevel); reader.Flags(binding.samplerIntegerBorder); reader.Values(binding.imageSamplers); reader.Flags(binding.bufferRead); @@ -608,7 +610,7 @@ void decodeInfo(Reader& reader, CompiledShaderInfo& compiled) { bool preparedImageModes = false; reader.Value(preparedImageModes); if (preparedImageModes) ResourceMaterializer::PrepareImageModes(info); - reader.List(info.samplers, 16, [](Reader& in, SamplerResource& sampler) { + reader.List(info.samplers, 17, [](Reader& in, SamplerResource& sampler) { in.Value(sampler.source); in.Value(sampler.firstUsePc); in.Value(sampler.copyOf); diff --git a/core/shader/recompiler/ShaderDiskCache.hpp b/core/shader/recompiler/ShaderDiskCache.hpp index df13ae723..9747d875b 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 = 26; +inline constexpr std::uint32_t FormatVersion = 27; enum class LoadStatus { Loaded,