perf(agc): resolve a sampled element repeating the one before it to the same texture

A build's sampled image elements often repeat the element just before them
in plan order (the same eight descriptor words). Each still took its own
stage-A record (decode, collect, DCC key read, cache entry) and its own
stage-B lookup.

The lookups run in plan order and nothing runs between two consecutive ones,
so a repeated element's lookup would find the state the previous lookup just
left: the entry it validated or made, current against the same collect. Such
an element now binds the previous element's texture (when the depth-compare
flag matches too, which is part of the cache key), and stage A gives it a
record of the decoded descriptor only. An element repeating an earlier but
not the immediately preceding one still takes its own lookup, as does any
element after a storage image lookup.
This commit is contained in:
Dean Galvin 2026-10-10 23:08:32 +13:00
parent 0209c64245
commit 63fb5779b3
3 changed files with 52 additions and 3 deletions

View File

@ -335,6 +335,12 @@ private:
std::uint64_t entryGeneration = 0;
};
std::vector<ImageRecord> imageRecords;
struct PreviousSampled {
std::array<std::uint32_t, 8> words{};
bool depthCompare = false;
std::shared_ptr<Texture> texture;
};
PreviousSampled previousSampled;
// The next record resolveImageBinding consumes (records follow the deferredImages order).
std::size_t nextImageRecord = 0;
// Stage B: the record's texture when the fastRevalidate predicate proves it current under the

View File

@ -1421,8 +1421,10 @@ 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.
previousSampled = {};
for (auto& source : samplerSources) source.singleLevelImage = source.mipmappedImage = false;
for (const auto& deferred : deferredImages) resolveImageBinding(*deferred.binding, bindings[deferred.index], std::span<const std::shared_ptr<Sampler>>(samplers).subspan(deferred.firstSampler, deferred.samplerCount));
previousSampled = {};
deferredImages.clear();
applyAnisoOverride();
// The records served their purpose: each holds the cache entry's objects as of stage A,
@ -2940,8 +2942,23 @@ bool ShaderResources::precollectImages() {
const auto elementWords = binding.guestDescriptor.size() / binding.count;
for (std::uint32_t element = 0; element < binding.count; ++element) {
const auto words = std::span<const std::uint32_t>(binding.guestDescriptor).subspan(static_cast<std::size_t>(element) * elementWords, elementWords);
const bool sampled = binding.kind == ShaderRecompiler::DescriptorKind::SampledImage;
if (sampled && !imageRecords.empty()) {
const auto& previous = imageRecords.back();
if (previous.sampled && previous.decoded && words.size() == 8 && std::equal(words.begin(), words.end(), previous.words.begin()) && (binding.imageDepthCompare.empty() || !binding.imageDepthCompare.at(element))) {
ImageRecord repeated;
repeated.sampled = true;
repeated.decoded = previous.decoded;
repeated.words = previous.words;
repeated.resource = previous.resource;
repeated.guestBytes = previous.guestBytes;
repeated.components = previous.components;
imageRecords.push_back(repeated);
continue;
}
}
ImageRecord record;
record.sampled = binding.kind == ShaderRecompiler::DescriptorKind::SampledImage;
record.sampled = sampled;
if (words[0] != 0u || (words[1] & 0xffu) != 0u) {
const auto* memo = words.size() == 8 ? &decodedImage(context.device, words) : nullptr;
record.resource = memo != nullptr ? memo->resource : DecodeTextureResource(words);
@ -3064,11 +3081,13 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind
const VkComponentMapping components = ViewComponents(resource);
const auto guestBytes = record != nullptr && record->decoded ? record->guestBytes : DescribeSurface(resource).guestBytes;
std::shared_ptr<Texture> texture;
if (record != nullptr && record->texture != nullptr) {
const bool depthCompare = !binding.imageDepthCompare.empty() && binding.imageDepthCompare.at(element);
if (previousSampled.texture != nullptr && previousSampled.depthCompare == depthCompare && words.size() == 8 && std::equal(words.begin(), words.end(), previousSampled.words.begin())) texture = previousSampled.texture;
if (texture == nullptr && record != nullptr && record->texture != nullptr) {
texture = fastTexture(*record);
(texture != nullptr ? counters.fastHits : counters.fastMisses).fetch_add(1, std::memory_order_relaxed);
}
if (texture == nullptr) texture = cachedTexture(context, words, resource, components, guestBytes, !binding.imageDepthCompare.empty() && binding.imageDepthCompare.at(element));
if (texture == nullptr) texture = cachedTexture(context, words, resource, components, guestBytes, depthCompare);
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;
@ -3085,6 +3104,11 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind
(singleLevel ? source.singleLevelImage : source.mipmappedImage) = true;
}
}
if (words.size() == 8) {
std::copy(words.begin(), words.end(), previousSampled.words.begin());
previousSampled.depthCompare = depthCompare;
previousSampled.texture = texture;
}
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<int>(resource.tileMode), resource.dccAddress});
@ -3094,6 +3118,7 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind
reportTextureCounters();
return;
}
previousSampled = {};
// Consecutive identical storage descriptors address successive mips of one texture (dynamic-mip
// storage writes).
std::uint32_t mipOffset = 0;

View File

@ -3763,6 +3763,24 @@ void imageMemoTests(const Device& device, Recorder& recorder) {
Require(sample(first) == remade, "the remade texture is not the cache's");
const auto replaced = sample(second);
Require(replaced != other && sample(second) == replaced && sample(first) == remade, "the cache does not answer with the replacements");
const auto firstWords = linearTextureWords(reinterpret_cast<std::uint64_t>(first), width, height, 56);
const auto secondWords = linearTextureWords(reinterpret_cast<std::uint64_t>(second), width, height, 56);
const auto bindElements = [&](std::initializer_list<std::array<std::uint32_t, 8>> elements, std::vector<bool> depthCompare) {
program.bindings[0].count = static_cast<std::uint32_t>(elements.size());
program.bindings[0].guestDescriptor.clear();
for (const auto& words : elements) program.bindings[0].guestDescriptor.insert(program.bindings[0].guestDescriptor.end(), words.begin(), words.end());
program.bindings[0].imageDepthCompare = std::move(depthCompare);
program.bindings[0].imageSamplers.assign(elements.size(), 0u);
ShaderResources resources(context, compiled);
Require(resources.SampledTextures().size() == elements.size(), "the build has the wrong number of sampled textures");
kept.insert(kept.end(), resources.SampledTextures().begin(), resources.SampledTextures().end());
return resources.SampledTextures();
};
const auto repeated = bindElements({firstWords, firstWords, secondWords, firstWords}, {});
Require(repeated[0] == remade && repeated[1] == remade && repeated[2] == replaced && repeated[3] == remade, "repeated elements of one build do not bind the cache's textures");
program.bindings[0].count = 1;
program.bindings[0].imageDepthCompare.clear();
program.bindings[0].imageSamplers.assign(1, 0u);
recorder.Submit();
device.WaitQueue();
recorder.Sync();