Merge pull request #1377 from gjonn/fix/agc-unnormalized-base-level-view

fix(agc): sample the base level of mipmapped views with unnormalized coordinates
This commit is contained in:
Adrià 2026-10-11 11:17:05 +02:00 committed by GitHub
commit 5ad2cb8cd0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 157 additions and 50 deletions

View File

@ -434,6 +434,7 @@ private:
std::uint64_t dispatchThreads = 0;
std::vector<std::shared_ptr<Texture>> textures;
std::vector<bool> textureFirstLayer;
std::vector<bool> textureBaseLevel;
std::vector<std::shared_ptr<StorageTexture>> storageTextures;
std::vector<std::uint32_t> storageMips;
std::vector<std::uint64_t> storageKeys;

View File

@ -13,6 +13,8 @@
#include <cstddef>
#include <map>
#include <memory>
#include <mutex>
#include <optional>
#include <span>
#include <tuple>
#include <utility>
@ -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<VkImageViewCreateInfo> 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;

View File

@ -1506,7 +1506,7 @@ void ShaderResources::buildComplete() {
write.pImageInfo = images.data() + images.size();
for (const auto index : binding.imageAllocations) {
if (index == std::numeric_limits<std::size_t>::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;
@ -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);
@ -3162,10 +3163,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<int>(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<int>(range.type)) + ")");
baseLevel = range.levels > 1u;
}
RequireFilterMinmax(context, texture->ViewFormat(), binding.imageSamplers[element], shaderSamplers);
RequireBorderSwizzle(resource.bcSwizzle, binding.imageSamplers[element], shaderSamplers);
@ -3185,6 +3190,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<int>(resource.tileMode), resource.dccAddress});
item.imageAllocations.push_back(textures.size() - 1);
}

View File

@ -445,6 +445,7 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex
Check(context.Function<PFN_vkCreateImageView>("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<StorageTexture>&
Check(context.Function<PFN_vkCreateImageView>("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<PFN_vkCreateImageView>("vkCreateImageView")(context.device, &*baseLevelInfo, nullptr, &baseLevelView), "vkCreateImageView base level");
return baseLevelView;
}
void Texture::release() noexcept {
upload.reset();
if (view) context.Function<PFN_vkDestroyImageView>("vkDestroyImageView")(context.device, view, nullptr);
view = VK_NULL_HANDLE;
if (firstLayerView) context.Function<PFN_vkDestroyImageView>("vkDestroyImageView")(context.device, firstLayerView, nullptr);
firstLayerView = VK_NULL_HANDLE;
if (baseLevelView) context.Function<PFN_vkDestroyImageView>("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;

View File

@ -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<DescriptorBinding>& left, const std::vector<DescriptorBinding>& 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;

View File

@ -2096,6 +2096,21 @@ void verifyUnusedUnnormalizedSampler() {
snapshot.images[0].dwords[3] = 0xa0000facu;
expectFailure([&] { static_cast<void>(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<bool>{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<bool>{false}, "unnormalized samplers: an image without live uses was flagged as exact at its base level");
require(atBaseLevel(offset) == std::vector<bool>{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<bool>{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<bool>{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<bool>{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<bool>{false} && normalized[1].samplerUnnormalized == std::vector<bool>{false}, "unnormalized samplers: a normalized S# was flagged");

View File

@ -47,6 +47,12 @@ alignas(256) constexpr std::array<std::uint32_t, 40> Code{
0x80001001, 0xe0701034, 0x80001101, 0xe0701038, 0x80001201, 0xe070103c, 0x80001301, 0xbf810000,
};
alignas(256) constexpr std::array<std::uint32_t, 18> LevelZeroCode{
0x34020086, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xbf8c3f70, 0xf09c0f08, 0x00610802,
0xbf8c3f70, 0xe0701010, 0x80000801, 0xe0701014, 0x80000901, 0xe0701018, 0x80000a01, 0xe070101c,
0x80000b01, 0xbf810000,
};
alignas(256) constexpr std::array<std::uint32_t, 45> 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<const std::uint32_t> code;
std::array<const char*, 3> instructions;
std::array<Offset, 3> 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<std::uint32_t, 34> ConstantCode{
@ -82,14 +90,15 @@ struct SamplerCase {
std::array<std::uint32_t, 4> words;
bool linear;
bool border;
bool atBaseLevel;
};
constexpr std::array<SamplerCase, 5> 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<Coordinate> 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<Coordinate> Coordinates(std::uint32_t level) {
const auto width = LevelWidth(level);
const auto height = LevelHeight(level);
std::vector<float> 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<float>(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<float>(width), static_cast<float>(width + 1), static_cast<float>(width) - 0.25f}) us.push_back(edge);
constexpr std::array<float, 3> fractions{0.5f, 0.0f, 0.75f};
std::vector<Coordinate> 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<float>((index * fractions.size() + fraction) % Height) + fractions[fraction]});
result.push_back({us[index], static_cast<float>((index * fractions.size() + fraction) % height) + fractions[fraction]});
}
}
for (std::int32_t x = 0; x < Width; ++x) {
const float v = static_cast<float>(x % Height) + 0.5f;
for (std::int32_t x = 0; x < width; ++x) {
const float v = static_cast<float>(x % height) + 0.5f;
result.push_back({static_cast<float>(x) - 1.0f, v});
result.push_back({static_cast<float>(x) + 2.0f, v});
}
@ -123,7 +142,7 @@ std::vector<Coordinate> Coordinates() {
std::vector<Coordinate> OffsetCoordinates() {
constexpr std::array<std::int32_t, 7> us{-32, -5, -1, 0, 1, 3, 31};
constexpr std::array<std::int32_t, 6> 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<Coordinate> OffsetCoordinates() {
return result;
}
std::array<double, 4> TexelOf(std::int32_t x, std::int32_t y) {
std::array<double, 4> 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<std::uint8_t> 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<std::uint64_t>(y) * mip.pitchBytes + x * 4u];
const auto value = TexelOf(static_cast<std::int32_t>(x), static_cast<std::int32_t>(y));
const auto value = TexelOf(level, static_cast<std::int32_t>(x), static_cast<std::int32_t>(y));
for (std::uint32_t component = 0; component < 4u; ++component) {
texel[component] = level == 0u ? static_cast<std::uint8_t>(value[component]) : OtherLevelTexel;
texel[component] = level <= 1u ? static_cast<std::uint8_t>(value[component]) : OtherLevelTexel;
}
}
}
@ -217,13 +237,15 @@ Samples Run(AgcDriver::VulkanDevice& device, const Program& program, const std::
return samples;
}
std::array<double, 4> 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<double, 4> 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<double, 4> Reference(const SamplerCase& sampler, const Coordinate& coordinate) {
if (!sampler.linear) return Fetch(sampler, static_cast<std::int32_t>(std::floor(coordinate.u)), static_cast<std::int32_t>(std::floor(coordinate.v)));
std::array<double, 4> Reference(const SamplerCase& sampler, std::uint32_t level, const Coordinate& coordinate) {
if (!sampler.linear) return Fetch(sampler, level, static_cast<std::int32_t>(std::floor(coordinate.u)), static_cast<std::int32_t>(std::floor(coordinate.v)));
const double u = static_cast<double>(coordinate.u) - 0.5;
const double v = static_cast<double>(coordinate.v) - 0.5;
const auto i = static_cast<std::int32_t>(std::floor(u));
@ -232,22 +254,22 @@ std::array<double, 4> Reference(const SamplerCase& sampler, const Coordinate& co
const double beta = v - j;
std::array<double, 4> 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<Coordinate>& coordinates, const Samples& samples) {
void Check(const Program& program, const SamplerCase& sampler, std::uint32_t level, const char* image, const std::vector<Coordinate>& 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<float>(du), coordinate.v + static_cast<float>(dv)});
const auto expected = Reference(sampler, level, {coordinate.u + static_cast<float>(du), coordinate.v + static_cast<float>(dv)});
for (std::uint32_t component = 0; component < 4u; ++component) {
const double value = static_cast<double>(std::bit_cast<float>(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<std::uint32_t, 8>& texture, const std::array<std::uint32_t, 4>& sampler, std::string_view reason, const char* what) {
void ExpectFailure(AgcDriver::VulkanDevice& device, const Program& program, const std::array<std::uint32_t, 8>& texture, const std::array<std::uint32_t, 4>& sampler, std::string_view reason, const char* what) {
const std::vector<Coordinate> one{{0.5f, 0.5f}};
try {
static_cast<void>(Run(device, Plain, texture, sampler, one));
static_cast<void>(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);

View File

@ -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<std::uint32_t>(SamplerUseExplicitLod | SamplerUseOffset)) == 0u && !sampler.depthCompare;
return (sampler.uses & SamplerUseOffset) != 0u && (sampler.uses & ~static_cast<std::uint32_t>(SamplerUseExplicitLod | SamplerUseOffset | SamplerUseNonZeroLod)) == 0u && !sampler.depthCompare;
}
struct SampledResourcePair {

View File

@ -160,10 +160,11 @@ const char* UnnormalizedUseReason(std::uint32_t uses) {
struct UnnormalizedProof {
std::vector<bool> samplers;
std::vector<bool> images;
std::vector<bool> imagesAtBaseLevel;
};
UnnormalizedProof ProveUnnormalized(const ShaderInfo& info, const ResourceSnapshot& snapshot) {
UnnormalizedProof proof{std::vector<bool>(info.samplers.size()), std::vector<bool>(info.images.size())};
UnnormalizedProof proof{std::vector<bool>(info.samplers.size()), std::vector<bool>(info.images.size()), std::vector<bool>(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<std::uint32_t>(SamplerUseExplicitLod | SamplerUseOffset);
const std::uint32_t unsupported = sampler.uses & ~static_cast<std::uint32_t>(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;

View File

@ -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<std::uint8_t>(use);
return static_cast<std::uint16_t>(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<std::uint8_t>(sampler.uses | SamplerUseOf(inst->Opcode(), memory.imageSampleFlags, program.Resources().stage));
sampler.uses = static_cast<std::uint16_t>(sampler.uses | SamplerUseOf(inst->Opcode(), memory.imageSampleFlags, program.Resources().stage));
}
}
}

View File

@ -368,6 +368,7 @@ struct DescriptorBinding {
std::vector<bool> samplerUnnormalized;
std::vector<bool> samplerIntegerBorder;
std::vector<bool> imageUnnormalized;
std::vector<bool> imageUnnormalizedBaseLevel;
std::vector<std::uint32_t> imageSamplers;
std::vector<bool> bufferRead;
};

View File

@ -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<const std::uint32_t>(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);

View File

@ -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,