mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-19 18:36:53 +02:00
9202bddad2
Co-authored-by: Juan Pablo Farias <juanpablo.profissional.a.f@gmail.com>
1936 lines
89 KiB
C++
1936 lines
89 KiB
C++
#include "graphics/shader/recompiler/ir/passes/ResourceMaterialization.h"
|
|
|
|
#include "common/assert.h"
|
|
#include "graphics/guest_gpu/gpu_format.h"
|
|
#include "graphics/shader/recompiler/BufferFormat.h"
|
|
#include "graphics/shader/recompiler/ir/ShaderIR.h"
|
|
#include "graphics/shader/shaderBindings.h"
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <bit>
|
|
#include <cstdio>
|
|
#include <fmt/format.h>
|
|
#include <functional>
|
|
#include <numeric>
|
|
#include <unordered_set>
|
|
|
|
namespace Libs::Graphics::ShaderRecompiler::IR {
|
|
namespace {
|
|
|
|
constexpr uint64_t AddressMask = 0x0000ffffffffffffull;
|
|
|
|
bool SpecializationFail(std::string_view message) {
|
|
std::fprintf(stderr, "shader resource specialization failed: %.*s\n",
|
|
static_cast<int>(message.size()), message.data());
|
|
return false;
|
|
}
|
|
|
|
Decoder::ImageDimension DescriptorDimension(const DescriptorValue& descriptor,
|
|
Decoder::ImageDimension requested) {
|
|
const bool is_array = requested == Decoder::ImageDimension::Dim1DArray ||
|
|
requested == Decoder::ImageDimension::Dim2DArray ||
|
|
requested == Decoder::ImageDimension::Dim2DMsaaArray;
|
|
switch (static_cast<Prospero::ImageType>((descriptor.dwords[3] >> 28u) & 0xfu)) {
|
|
case Prospero::ImageType::kColor1D: return Decoder::ImageDimension::Dim1D;
|
|
case Prospero::ImageType::kColor1DArray:
|
|
if (is_array) {
|
|
return Decoder::ImageDimension::Dim1DArray;
|
|
}
|
|
return Decoder::ImageDimension::Dim1D;
|
|
case Prospero::ImageType::kColor3D: return Decoder::ImageDimension::Dim3D;
|
|
case Prospero::ImageType::kCube: return Decoder::ImageDimension::Dim2DArray;
|
|
case Prospero::ImageType::kColor2DArray:
|
|
if (is_array) {
|
|
return Decoder::ImageDimension::Dim2DArray;
|
|
}
|
|
return Decoder::ImageDimension::Dim2D;
|
|
case Prospero::ImageType::kColor2DMsaaArray:
|
|
if (is_array) {
|
|
return Decoder::ImageDimension::Dim2DMsaaArray;
|
|
}
|
|
return Decoder::ImageDimension::Dim2DMsaa;
|
|
case Prospero::ImageType::kColor2D: return Decoder::ImageDimension::Dim2D;
|
|
case Prospero::ImageType::kColor2DMsaa: return Decoder::ImageDimension::Dim2DMsaa;
|
|
default: return Decoder::ImageDimension::Unknown;
|
|
}
|
|
}
|
|
|
|
bool NullImageDescriptor(const DescriptorValue& descriptor) {
|
|
return descriptor.dwords[0] == 0 && (descriptor.dwords[1] & 0xffu) == 0;
|
|
}
|
|
|
|
bool NullBufferDescriptor(Value value) {
|
|
const auto* handle = value.Resolve().TryInstruction();
|
|
if (handle == nullptr || handle->GetOpcode() != ValueOpcode::GetBufferResource ||
|
|
handle->NumArgs() != 4u) return false;
|
|
for (size_t index = 0; index < 4u; ++index)
|
|
if (handle->Arg(index).Resolve() != Value(0u)) return false;
|
|
return true;
|
|
}
|
|
|
|
bool ValidImageDescriptor(const DescriptorValue& descriptor, bool r128 = false) {
|
|
const auto& words = descriptor.dwords;
|
|
// Reject texture descriptors with nonzero reserved bits.
|
|
if ((words[1] & 0x20000000u) != 0u || (words[2] & 0x70003000u) != 0u ||
|
|
(!r128 && ((words[4] & 0xe000e000u) != 0u || (words[5] & 0xf9000000u) != 0u ||
|
|
(words[6] & 0x00007b00u) != 0u))) {
|
|
return false;
|
|
}
|
|
const auto type = static_cast<Prospero::ImageType>((descriptor.dwords[3] >> 28u) & 0xfu);
|
|
const auto format = static_cast<Prospero::BufferFormat>((descriptor.dwords[1] >> 20u) & 0x1ffu);
|
|
if (type < Prospero::ImageType::kColor1D || format == Prospero::BufferFormat::kInvalid ||
|
|
format > Prospero::BufferFormat::kBc7Srgb) {
|
|
return false;
|
|
}
|
|
if (r128 && type != Prospero::ImageType::kColor1D && type != Prospero::ImageType::kColor2D &&
|
|
type != Prospero::ImageType::kColor2DMsaa) {
|
|
return false;
|
|
}
|
|
if (!IsValidImageSwizzle(words[3] & 0xfffu)) return false;
|
|
const bool array = type == Prospero::ImageType::kColor1DArray ||
|
|
type == Prospero::ImageType::kColor2DArray ||
|
|
type == Prospero::ImageType::kColor2DMsaaArray ||
|
|
type == Prospero::ImageType::kCube;
|
|
if (array && ((words[4] >> 16u) & 0x1fffu) > (words[4] & 0x1fffu)) {
|
|
return false;
|
|
}
|
|
const auto base_level = (words[3] >> 12u) & 0xfu;
|
|
const auto last_level = (words[3] >> 16u) & 0xfu;
|
|
if (type == Prospero::ImageType::kColor2DMsaa ||
|
|
type == Prospero::ImageType::kColor2DMsaaArray) {
|
|
const auto max_mip = (descriptor.dwords[5] >> 4u) & 0xfu;
|
|
return base_level == 0 && last_level >= 1 && last_level <= 3 &&
|
|
(r128 || max_mip == last_level);
|
|
}
|
|
return base_level <= last_level;
|
|
}
|
|
|
|
uint32_t DescriptorImageSwizzle(const DescriptorValue& descriptor) {
|
|
return descriptor.dwords[3] & 0xfffu;
|
|
}
|
|
|
|
Prospero::BufferFormat ImageConversionFormat(Prospero::BufferFormat format) {
|
|
return Prospero::RemapTextureFormat(format) != format ? format
|
|
: Prospero::BufferFormat::kInvalid;
|
|
}
|
|
|
|
bool ValidatePackedUnormSampling(const ResourcePlan& program, const ResourceSnapshot& snapshot,
|
|
uint32_t image_index) {
|
|
ShaderTextureResource texture;
|
|
std::copy_n(snapshot.images[image_index].dwords.begin(), std::size(texture.fields),
|
|
texture.fields);
|
|
if (texture.Type() != Prospero::ImageType::kColor2D ||
|
|
texture.BaseLevel() != texture.LastLevel() || texture.MinLod() != 0u ||
|
|
texture.CornerSample()) {
|
|
return SpecializationFail("packed UNorm sampling requires a centered, single-mip 2D image");
|
|
}
|
|
for (const auto& pair: program.info.sampled_pairs) {
|
|
if (pair.image != image_index) continue;
|
|
if (pair.sampler >= snapshot.samplers.size() ||
|
|
program.descriptor_sources[program.info.samplers[pair.sampler].source].indirect_descriptor) {
|
|
return SpecializationFail("packed UNorm sampling requires a direct sampler");
|
|
}
|
|
ShaderSamplerResource sampler;
|
|
std::copy_n(snapshot.samplers[pair.sampler].dwords.begin(), std::size(sampler.fields),
|
|
sampler.fields);
|
|
const auto linear = static_cast<uint32_t>(Prospero::SamplerFilter::kBilinear);
|
|
const auto clamp = static_cast<uint32_t>(Prospero::SamplerClampMode::kClampLastTexel);
|
|
if (sampler.XyMagFilter() != linear || sampler.XyMinFilter() != linear ||
|
|
sampler.ClampX() != clamp || sampler.ClampY() != clamp ||
|
|
sampler.ForceUnormCoords() || sampler.MaxAnisoRatio() != 0u ||
|
|
sampler.FilterMode() != 0u || sampler.MinLod() != 0u || sampler.ForceSrgb() ||
|
|
sampler.TruncCoord() || (sampler.fields[0] & (1u << 19u)) != 0u) { // MC_COORD_TRUNC
|
|
return SpecializationFail(fmt::format(
|
|
"packed UNorm image {} requires normalized bilinear clamp sampling (sampler {})",
|
|
image_index, pair.sampler));
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
enum class SamplerClass : uint8_t { Float, Integer };
|
|
|
|
SamplerClass ClassifySampler(const ImageResource& image) {
|
|
return image.numeric_class == Prospero::TextureNumericClass::Uint ||
|
|
image.numeric_class == Prospero::TextureNumericClass::Sint
|
|
? SamplerClass::Integer : SamplerClass::Float;
|
|
}
|
|
|
|
bool DescriptorIsCube(const DescriptorValue& descriptor) {
|
|
return static_cast<Prospero::ImageType>((descriptor.dwords[3] >> 28u) & 0xfu) ==
|
|
Prospero::ImageType::kCube;
|
|
}
|
|
|
|
uint32_t ImageMipCount(const ImageResource& image, const DescriptorValue& descriptor) {
|
|
if (image.mip_mode != ImageMipMode::Dynamic || NullImageDescriptor(descriptor)) {
|
|
return 1;
|
|
}
|
|
const auto base = (descriptor.dwords[3] >> 12u) & 0xfu;
|
|
const auto last = (descriptor.dwords[3] >> 16u) & 0xfu;
|
|
return last - base + 1u;
|
|
}
|
|
|
|
bool DecodeBufferDescriptor(const DescriptorValue& descriptor, ShaderBufferResource& result) {
|
|
if (descriptor.dword_count != std::size(result.fields)) {
|
|
return false;
|
|
}
|
|
std::copy_n(descriptor.dwords.begin(), std::size(result.fields), result.fields);
|
|
return true;
|
|
}
|
|
|
|
const DescriptorSource* Source(const ResourcePlan& program, uint32_t source) {
|
|
if (source >= program.descriptor_sources.size()) {
|
|
return nullptr;
|
|
}
|
|
return &program.descriptor_sources[source];
|
|
}
|
|
|
|
void MarkCleanReads(const std::unordered_set<const Inst*>& planned_reads,
|
|
const DescriptorSource* source, Value extra = {}) {
|
|
if (source == nullptr && extra.IsEmpty()) {
|
|
return;
|
|
}
|
|
std::vector<Value> pending;
|
|
if (source != nullptr) {
|
|
pending.assign(source->dwords.begin(), source->dwords.begin() + source->dword_count);
|
|
}
|
|
if (!extra.IsEmpty()) pending.push_back(extra);
|
|
std::vector<const Inst*> visited;
|
|
while (!pending.empty()) {
|
|
auto* inst = pending.back().Resolve().TryInstruction();
|
|
pending.pop_back();
|
|
if (inst == nullptr || std::ranges::find(visited, inst) != visited.end()) {
|
|
continue;
|
|
}
|
|
visited.push_back(inst);
|
|
if (planned_reads.contains(inst)) {
|
|
auto flags = inst->Flags<SrtReadFlags>();
|
|
flags.clean = 1u;
|
|
inst->SetFlags(flags);
|
|
}
|
|
for (size_t arg = 0; arg < inst->NumArgs(); arg++) {
|
|
pending.push_back(inst->Arg(arg));
|
|
}
|
|
}
|
|
}
|
|
|
|
bool ReadTable(uint64_t base, uint64_t size, uint64_t offset,
|
|
const SrtRuntime& runtime, std::span<uint32_t> words) {
|
|
const auto count = std::min<uint64_t>(words.size(), offset < size ? (size - offset) / 4u : 0u);
|
|
std::ranges::fill(words.subspan(count), 0u);
|
|
if (count == 0u) {
|
|
return true;
|
|
}
|
|
if (base > AddressMask || offset > AddressMask - base) {
|
|
return false;
|
|
}
|
|
const auto address = base + offset;
|
|
const auto prefix = words.first(count);
|
|
return prefix.size_bytes() - 1u <= AddressMask - address &&
|
|
runtime.read_specialization_memory != nullptr &&
|
|
runtime.read_specialization_memory(runtime.userdata, address, prefix);
|
|
}
|
|
|
|
bool ReadScalarTable(uint64_t base, uint64_t size, uint64_t offset,
|
|
const SrtRuntime& runtime, std::span<uint32_t> words) {
|
|
return ReadTable(base & AddressMask & ~uint64_t {3}, size, offset & ~uint64_t {3}, runtime, words);
|
|
}
|
|
|
|
bool NormalizeIndirectBuffer(DescriptorValue& value, bool indexed, const BufferResource& resource) {
|
|
ShaderBufferResource descriptor;
|
|
if (!DecodeBufferDescriptor(value, descriptor)) return false;
|
|
const bool formatted_read = resource.formatted && resource.read && !resource.written;
|
|
if (descriptor.Type() != 0u || descriptor.Format() == Prospero::BufferFormat::kInvalid ||
|
|
(!formatted_read && (descriptor.Base48() == 0u || descriptor.GetSize() == 0u))) {
|
|
value.dwords.fill(0u);
|
|
return true;
|
|
}
|
|
if (descriptor.SwizzleEnabled() || descriptor.AddTid() ||
|
|
descriptor.OutOfBounds() != (indexed ? 0u : 3u)) {
|
|
return SpecializationFail(fmt::format(
|
|
"indirect buffers require exact linear index or byte bounds: "
|
|
"descriptor={:08x},{:08x},{:08x},{:08x} swizzle={} add_tid={} oob={} "
|
|
"indexed={} formatted={} read={} written={} max_extent={}",
|
|
value.dwords[0], value.dwords[1], value.dwords[2], value.dwords[3],
|
|
descriptor.SwizzleEnabled(), descriptor.AddTid(), descriptor.OutOfBounds(), indexed,
|
|
resource.formatted, resource.read, resource.written, resource.max_byte_extent));
|
|
}
|
|
if (resource.formatted) {
|
|
const auto format = Format::GetFormatInfo(descriptor.Format());
|
|
if (!indexed || format.type == Format::ComponentType::Unknown)
|
|
return SpecializationFail("indirect formatted buffers require a supported indexed format");
|
|
if (!formatted_read) {
|
|
const bool scalar = resource.max_byte_extent == 4u &&
|
|
(descriptor.Format() == Prospero::BufferFormat::k32UInt ||
|
|
descriptor.Format() == Prospero::BufferFormat::k8SInt);
|
|
const bool rgba16 = resource.max_byte_extent == 16u &&
|
|
descriptor.Format() == Prospero::BufferFormat::k16_16_16_16Float;
|
|
if ((!scalar && !rgba16) || descriptor.Stride() != format.byte_size ||
|
|
descriptor.DstSelXYZW() != (rgba16 ? DstSel(4, 5, 6, 7) : DstSel(4, 0, 0, 0)))
|
|
return SpecializationFail("indirect formatted stores require tight identity SInt8, UInt32 or RGBA16Float records");
|
|
}
|
|
if (formatted_read &&
|
|
(descriptor.Base48() == 0u || descriptor.Stride() == 0u || descriptor.NumRecords() == 0u)) {
|
|
// Every index is out of bounds. Preserve the format and swizzle constants.
|
|
value.dwords[1] &= ~(0x3fffu << 16u);
|
|
value.dwords[2] = 0u;
|
|
return true;
|
|
}
|
|
const auto alignment = std::min(4u, format.byte_size);
|
|
if (descriptor.Stride() < format.byte_size ||
|
|
(descriptor.Stride() & (alignment - 1u)) != 0u ||
|
|
(descriptor.Base48() & (alignment - 1u)) != 0u)
|
|
return SpecializationFail("indirect formatted buffers require aligned full-format records");
|
|
} else if ((!indexed && descriptor.Stride() != 1u) ||
|
|
(indexed && resource.max_byte_extent != 1u && descriptor.Stride() != 4u)) {
|
|
return SpecializationFail("indirect buffer stores require exact linear DWORD or byte bounds");
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool IsBoundedDescriptorTable(const ResourcePlan& program,
|
|
const DescriptorSource::IndirectDescriptor& indirect) {
|
|
const auto* table = Source(program, indirect.table_source);
|
|
return indirect.sources.empty() && !indirect.selector &&
|
|
(indirect.key_count.IsEmpty() || indirect.table_maximum) && indirect.workgroup_axis == UINT32_MAX &&
|
|
table != nullptr && table->dword_count == 4u;
|
|
}
|
|
|
|
bool EvaluateIndirectCount(const DescriptorSource::IndirectDescriptor& indirect,
|
|
const SrtRuntime& runtime, SrtWalker& clean, uint32_t& count) {
|
|
if (indirect.workgroup_axis != UINT32_MAX) {
|
|
if (runtime.workgroup_counts.size() != 3u || indirect.workgroup_axis >= 3u) return false;
|
|
count = std::ranges::find(runtime.workgroup_counts, 0u) != runtime.workgroup_counts.end()
|
|
? 0u : runtime.workgroup_counts[indirect.workgroup_axis];
|
|
return true;
|
|
}
|
|
if (!clean.Evaluate(indirect.key_count, count)) return false;
|
|
if (indirect.signed_count && std::bit_cast<int32_t>(count) < 0) count = 0;
|
|
return true;
|
|
}
|
|
|
|
bool ResolveSelectorKeys(const DescriptorSource::IndirectDescriptor& indirect,
|
|
const DescriptorValue& material_value, const SrtRuntime& runtime,
|
|
SrtWalker& clean, std::vector<uint32_t>& keys) {
|
|
const auto* selector = &*indirect.selector;
|
|
keys.clear();
|
|
const auto read_selector = [&](uint64_t base, uint64_t size, uint64_t offset,
|
|
std::span<uint32_t> words) {
|
|
return selector->global ? ReadTable(base, size, offset, runtime, words)
|
|
: ReadScalarTable(base, size, offset, runtime, words);
|
|
};
|
|
const auto read_keys = [&](uint64_t base, uint64_t size, uint64_t first,
|
|
uint64_t step, uint64_t count) {
|
|
keys.resize(count);
|
|
if (count == 0u) return true;
|
|
if (step == 4u) {
|
|
return read_selector(base, size, first, keys);
|
|
}
|
|
if (step < 4u) return false;
|
|
for (uint64_t copied = 0; copied < count;) {
|
|
const auto offset = first + copied * step;
|
|
// Scalar reads align each strided offset independently.
|
|
const auto bias = selector->global ? 0u : offset & 3u;
|
|
const auto length = std::min(count - copied,
|
|
1u + (uint64_t {MaxIndirectDescriptorProbes - 1u} * 4u - bias) / step);
|
|
keys.resize(count + (bias + (length - 1u) * step) / 4u + 1u);
|
|
const auto batch = std::span(keys).subspan(count);
|
|
if (!read_selector(base, size, offset, batch)) {
|
|
keys.resize(count);
|
|
return false;
|
|
}
|
|
for (uint64_t index = 0; index < length; ++index)
|
|
keys[copied + index] = batch[(bias + index * step) / 4u];
|
|
copied += length;
|
|
}
|
|
keys.resize(count);
|
|
return true;
|
|
};
|
|
if (selector->rows) {
|
|
const auto& rows = *selector->rows;
|
|
DescriptorValue count_value;
|
|
ShaderBufferResource counts, material;
|
|
if (!clean.EvaluateDescriptor(rows.source, count_value) ||
|
|
!DecodeBufferDescriptor(count_value, counts) ||
|
|
!DecodeBufferDescriptor(material_value, material) || selector->stride != 4u ||
|
|
selector->offset != 0u || rows.stride == 0u || (rows.stride & 3u) != 0u ||
|
|
counts.GetSize() / 4u > MaxIndirectDescriptorProbes) return false;
|
|
std::vector<uint32_t> lengths(counts.GetSize() / 4u);
|
|
if (!ReadScalarTable(counts.Base48(), counts.GetSize(), 0u, runtime, lengths)) return false;
|
|
uint32_t total = 0;
|
|
for (size_t row = 0; row < lengths.size(); ++row) {
|
|
const auto length = lengths[row];
|
|
if (length > MaxIndirectDescriptorProbes - total ||
|
|
(length != 0u && uint64_t {row} * rows.stride + uint64_t {length} * 4u >
|
|
UINT32_MAX + 1ull)) return false;
|
|
total += length;
|
|
}
|
|
keys.reserve(total);
|
|
std::vector<uint32_t> batch;
|
|
for (size_t row = 0; row < lengths.size();) {
|
|
if (lengths[row] == 0u) { ++row; continue; }
|
|
const auto first = uint64_t {row} * rows.stride / 4u;
|
|
auto end = first + lengths[row];
|
|
size_t next = row + 1u;
|
|
for (; next < lengths.size(); ++next) {
|
|
if (lengths[next] == 0u) continue;
|
|
const auto candidate_end =
|
|
std::max(end, uint64_t {next} * rows.stride / 4u + lengths[next]);
|
|
if (candidate_end - first > MaxIndirectDescriptorProbes) break;
|
|
end = candidate_end;
|
|
}
|
|
// Coalesce small gaps, but interpret only selected prefixes. The strict
|
|
// reader captures the entire fetched span for writable-alias rejection.
|
|
batch.resize(end - first);
|
|
if (!ReadScalarTable(material.Base48(), material.GetSize(), first * 4u, runtime, batch))
|
|
return false;
|
|
for (; row < next; ++row) {
|
|
if (lengths[row] == 0u) continue;
|
|
const auto begin = batch.begin() + (uint64_t {row} * rows.stride / 4u - first);
|
|
keys.insert(keys.end(), begin, begin + lengths[row]);
|
|
}
|
|
}
|
|
} else if (selector->bitset) {
|
|
ShaderBufferResource material;
|
|
std::array<uint32_t, 4> masks {};
|
|
uint32_t first = 0, count = 0;
|
|
if (!DecodeBufferDescriptor(material_value, material) ||
|
|
!clean.Evaluate(indirect.selector_first, first) ||
|
|
!EvaluateIndirectCount(indirect, runtime, clean, count) || count != masks.size() * 32u ||
|
|
selector->stride != sizeof(masks) || selector->offset != 0u ||
|
|
material.Type() != 0u || material.Stride() != sizeof(masks) ||
|
|
material.SwizzleEnabled() || material.AddTid() || material.OutOfBounds() != 0u ||
|
|
(material.Base48() & 3u) != 0u) return false;
|
|
if (material.Format() != Prospero::BufferFormat::kInvalid) {
|
|
const auto words = material.GetSize() / sizeof(uint32_t);
|
|
// Bitmap storage can grow with resolution while its candidate domain stays
|
|
// at 128 keys. Bound scratch memory, preserving each coherent read's capture.
|
|
for (uint64_t first_word = 0; first_word < words; first_word += keys.size()) {
|
|
keys.resize(std::min<uint64_t>(words - first_word, MaxIndirectDescriptorProbes));
|
|
if (!ReadScalarTable(material.Base48(), material.GetSize(), first_word * 4u,
|
|
runtime, keys)) return false;
|
|
for (size_t word = 0; word < keys.size(); ++word) masks[word % masks.size()] |= keys[word];
|
|
if (std::ranges::all_of(masks, [](uint32_t mask) { return mask == UINT32_MAX; })) break;
|
|
}
|
|
keys.clear();
|
|
for (uint32_t word = 0; word < masks.size(); ++word) {
|
|
auto mask = masks[word];
|
|
while (mask != 0u) {
|
|
const auto rank = word * 32u + std::countr_zero(mask);
|
|
keys.push_back(first + count - 1u - rank);
|
|
mask &= mask - 1u;
|
|
}
|
|
}
|
|
}
|
|
} else if (!selector->byte_ranges.empty()) {
|
|
if (material_value.dword_count != 2u) return false;
|
|
const auto base = (uint64_t {material_value.dwords[1]} << 32u) | material_value.dwords[0];
|
|
std::array<bool, 256> selected {};
|
|
for (const auto [offset, size]: selector->byte_ranges) {
|
|
if (size > MaxIndirectDescriptorProbes || ((offset | size) & 3u) != 0u) return false;
|
|
keys.resize(size / 4u);
|
|
if (!ReadScalarTable(base, UINT64_MAX, offset, runtime, keys)) return false;
|
|
for (const auto word: keys)
|
|
for (uint32_t shift = 0; shift < 32u; shift += 8u)
|
|
selected[(word >> shift) & 255u] = true;
|
|
}
|
|
keys.clear();
|
|
for (uint32_t key = 0; key < selected.size(); ++key)
|
|
if (selected[key]) keys.push_back(key);
|
|
} else if (!indirect.selector_first.IsEmpty()) {
|
|
ShaderBufferResource material;
|
|
uint64_t base = 0, size = UINT64_MAX;
|
|
uint32_t first = 0, count = 0;
|
|
if (material_value.dword_count == 2u) {
|
|
if (selector->stride == 0u) return false;
|
|
base = (uint64_t {material_value.dwords[1]} << 32u) | material_value.dwords[0];
|
|
} else {
|
|
if (!DecodeBufferDescriptor(material_value, material)) return false;
|
|
base = material.Base48();
|
|
size = material.GetSize();
|
|
}
|
|
if (!clean.Evaluate(indirect.selector_first, first) ||
|
|
!EvaluateIndirectCount(indirect, runtime, clean, count) || count > MaxIndirectDescriptorProbes)
|
|
return false;
|
|
// SMEM uses its instruction's byte stride; V# stride only supplies its bounds.
|
|
const auto step = selector->stride != 0u ? selector->stride : material.Stride();
|
|
if (selector->stride == 0u &&
|
|
(material.Type() != 0u || material.SwizzleEnabled() || material.AddTid() ||
|
|
material.OutOfBounds() != 0u || uint64_t {selector->offset} + 4u > step ||
|
|
uint64_t {first} + count > material.NumRecords())) return false;
|
|
const auto last = count != 0u ? uint64_t {first} + count - 1u : 0u;
|
|
if (last > UINT32_MAX) return false;
|
|
const auto end = count != 0u ? last * step + selector->offset + 4u : 0u;
|
|
if (end > uint64_t {UINT32_MAX} + 1u ||
|
|
(indirect.workgroup_axis == UINT32_MAX && end > size))
|
|
return false;
|
|
if (!read_keys(base, size, uint64_t {first} * step + selector->offset, step, count))
|
|
return false;
|
|
} else if (!indirect.selector_mask.IsEmpty()) {
|
|
uint32_t mask = 0;
|
|
uint32_t count = 0;
|
|
if (material_value.dword_count != 2u ||
|
|
!clean.Evaluate(indirect.selector_mask, mask) ||
|
|
!EvaluateIndirectCount(indirect, runtime, clean, count) || count == 0u || count > 32u) {
|
|
return false;
|
|
}
|
|
if (count < 32u) mask &= (1u << count) - 1u;
|
|
const auto material_base =
|
|
(static_cast<uint64_t>(material_value.dwords[1]) << 32u) | material_value.dwords[0];
|
|
keys.reserve(std::popcount(mask));
|
|
while (mask != 0u) {
|
|
const auto index = std::countr_zero(mask);
|
|
const auto offset = static_cast<uint64_t>(selector->offset) +
|
|
static_cast<uint64_t>(index) * selector->stride;
|
|
if (offset > UINT32_MAX) return false;
|
|
uint32_t key = 0;
|
|
if (!read_selector(material_base, UINT64_MAX, static_cast<uint32_t>(offset),
|
|
{&key, 1})) return false;
|
|
keys.push_back(key);
|
|
mask &= mask - 1u;
|
|
}
|
|
} else {
|
|
ShaderBufferResource material;
|
|
if (!DecodeBufferDescriptor(material_value, material) || selector->stride < 4u ||
|
|
!std::has_single_bit(selector->stride) || (selector->offset & 3u) != 0u)
|
|
return false;
|
|
// Empty selector bounds denote the complete U32 congruence class. SMEM
|
|
// adds its aligned immediate after the dynamic offset, without U32 wrap.
|
|
const auto domain_count = (uint64_t {1} << 32u) / selector->stride;
|
|
const auto available = material.GetSize() > selector->offset
|
|
? material.GetSize() - selector->offset : 0u;
|
|
const auto count = std::min(domain_count,
|
|
available < 4u ? 0u : (available - 4u) / selector->stride + 1u);
|
|
if (count > MaxIndirectDescriptorProbes ||
|
|
!read_keys(material.Base48(), material.GetSize(), selector->offset, selector->stride, count)) return false;
|
|
if (count < domain_count) keys.push_back(0u); // An out-of-bounds scalar read returns zero.
|
|
}
|
|
if (selector->byte_ranges.empty()) {
|
|
if (selector->bit_offset != 0u || selector->bit_mask != UINT32_MAX || selector->maximum != UINT32_MAX)
|
|
for (auto& key: keys)
|
|
key = std::min((key >> selector->bit_offset) & selector->bit_mask, selector->maximum);
|
|
std::ranges::sort(keys);
|
|
keys.erase(std::unique(keys.begin(), keys.end()), keys.end());
|
|
}
|
|
return true;
|
|
}
|
|
|
|
template <typename Specialization, typename Normalize>
|
|
bool MaterializeIndirectDescriptor(const ResourcePlan& program,
|
|
uint32_t source_index,
|
|
uint32_t resource_index, uint32_t dword_count, uint32_t read_dwords,
|
|
const SrtRuntime& runtime, SrtWalker& clean,
|
|
ResourceSnapshot& snapshot,
|
|
std::vector<DescriptorValue>& descriptors,
|
|
std::vector<Specialization>& specializations,
|
|
uint32_t maximum_resources, Normalize&& normalize) {
|
|
const auto& indirect = *program.descriptor_sources[source_index].indirect_descriptor;
|
|
const auto descriptor_bytes = read_dwords * sizeof(uint32_t);
|
|
const auto& sources = indirect.sources;
|
|
const auto* selector = indirect.selector ? &*indirect.selector : nullptr;
|
|
auto* selector_keys = selector != nullptr
|
|
? &program.selector_keys[program.selector_groups[source_index]] : nullptr;
|
|
auto& keys = selector_keys != nullptr ? selector_keys->values : program.material_keys;
|
|
const auto children_begin = descriptors.size();
|
|
const auto root_resource = specializations[resource_index];
|
|
const auto intern_candidate = [&](const DescriptorValue& candidate, uint32_t& ordinal) {
|
|
ordinal = 0u;
|
|
if (candidate == descriptors[resource_index]) return true;
|
|
const auto found = std::find(descriptors.begin() + children_begin, descriptors.end(), candidate);
|
|
ordinal = static_cast<uint32_t>(found - descriptors.begin() - children_begin + 1u);
|
|
if (found == descriptors.end()) {
|
|
if (descriptors.size() >= maximum_resources) return false;
|
|
descriptors.push_back(candidate);
|
|
auto child = root_resource;
|
|
child.indirect_root = resource_index;
|
|
specializations.push_back(child);
|
|
}
|
|
return true;
|
|
};
|
|
uint64_t table_base = 0;
|
|
uint64_t table_size = UINT64_MAX; // Scalar addresses have no buffer descriptor bounds.
|
|
DescriptorValue table_value;
|
|
ShaderBufferResource table;
|
|
const auto failed = [&](std::string_view reason, std::string_view details = {}) {
|
|
const auto* pointer = indirect.pointer_table ? &*indirect.pointer_table : nullptr;
|
|
return SpecializationFail(fmt::format(
|
|
"indirect {} resource={} source={} table_source={} words={:08x},{:08x},{:08x},{:08x} "
|
|
"base={:#x} size={} type={} runtime_stride={} records={} swizzle={} addtid={} oob={} "
|
|
"maximum={} offset={} immediate={} record_bytes={} child_count={} child_stride={} "
|
|
"child_offset={} indexed_pointer={} candidates={} capacity={} {}",
|
|
reason, resource_index, source_index, indirect.table_source,
|
|
table_value.dwords[0], table_value.dwords[1], table_value.dwords[2], table_value.dwords[3],
|
|
table_base, table_size, table.Type(), table.Stride(), table.NumRecords(),
|
|
table.SwizzleEnabled(), table.AddTid(), table.OutOfBounds(),
|
|
indirect.table_maximum ? fmt::format("{}", *indirect.table_maximum) : "none",
|
|
indirect.table_offset, indirect.table_immediate, indirect.table_record_bytes,
|
|
pointer != nullptr ? pointer->count : 1u, pointer != nullptr ? pointer->stride : 0u,
|
|
pointer != nullptr ? pointer->offset : 0u, pointer != nullptr && pointer->indexed,
|
|
descriptors.size(), maximum_resources, details));
|
|
};
|
|
const auto read_failed = [&](std::string_view reason, uint32_t row, uint32_t child,
|
|
uint64_t base, uint64_t offset, size_t bytes) {
|
|
return failed(reason, fmt::format("row={} child={} read_base={:#x} read_offset={:#x} "
|
|
"read_address={:#x} requested_bytes={}", row, child, base, offset,
|
|
(base & AddressMask & ~uint64_t {3}) + (offset & ~uint64_t {3}), bytes));
|
|
};
|
|
const auto normalization_failed = [&](uint64_t offset, uint32_t key, uint64_t step,
|
|
std::string_view key_kind) {
|
|
return failed("candidate", fmt::format("candidate_offset={:#x} {}={} stride={} step={}",
|
|
offset & ~uint64_t {3}, key_kind, key, indirect.table_stride, step));
|
|
};
|
|
if (sources.empty()) {
|
|
if (selector == nullptr) keys.clear();
|
|
DescriptorValue material_value;
|
|
if ((selector != nullptr && !selector_keys->resolved &&
|
|
!clean.EvaluateDescriptor(selector->source, material_value)) ||
|
|
!clean.EvaluateDescriptor(indirect.table_source, table_value)) {
|
|
return false;
|
|
}
|
|
if (table_value.dword_count == 2u) {
|
|
table_base = (static_cast<uint64_t>(table_value.dwords[1]) << 32u) | table_value.dwords[0];
|
|
} else if (DecodeBufferDescriptor(table_value, table)) {
|
|
table_base = table.Base48();
|
|
table_size = table.GetSize();
|
|
} else {
|
|
return false;
|
|
}
|
|
if (IsBoundedDescriptorTable(program, indirect)) {
|
|
if (!indirect.table_scalar && table.Format() == Prospero::BufferFormat::kInvalid) {
|
|
descriptors[resource_index] = {.dword_count = dword_count};
|
|
return true;
|
|
}
|
|
if (table_size > UINT32_MAX ||
|
|
(indirect.table_record_bytes != 0u &&
|
|
(table.Type() != 0u || table.SwizzleEnabled() || table.AddTid() ||
|
|
table.OutOfBounds() != 0u || (table_base & 3u) != 0u ||
|
|
(table.Stride() & 3u) != 0u || table.Stride() < indirect.table_record_bytes)))
|
|
return failed("table geometry");
|
|
const auto* pointer = indirect.pointer_table ? &*indirect.pointer_table : nullptr;
|
|
const bool indexed_pointer = pointer != nullptr && pointer->indexed;
|
|
const auto child_count = pointer != nullptr ? pointer->count : 1u;
|
|
uint64_t domain = indirect.table_maximum ? uint64_t {*indirect.table_maximum} + 1u
|
|
: uint64_t {1} << 32u;
|
|
if (!indirect.key_count.IsEmpty()) {
|
|
uint32_t key_count = 0u;
|
|
if (!EvaluateIndirectCount(indirect, runtime, clean, key_count)) return false;
|
|
domain = std::min(domain, uint64_t {key_count});
|
|
}
|
|
if (indexed_pointer &&
|
|
(!indirect.table_maximum || table.Type() != 0u || table.SwizzleEnabled() || table.AddTid() ||
|
|
table.OutOfBounds() != 0u || (table_base & 3u) != 0u || (table.Stride() & 3u) != 0u ||
|
|
uint64_t {indirect.table_immediate} + 8u > table.Stride() ||
|
|
domain > table.NumRecords()))
|
|
return failed("indexed pointer geometry", fmt::format("rows={}", domain));
|
|
const bool affine = indirect.table_stride != 0u || indexed_pointer;
|
|
const bool image_heap = dword_count == 8u && pointer == nullptr;
|
|
const uint64_t step = indexed_pointer ? table.Stride()
|
|
: indirect.table_maximum ? indirect.table_stride
|
|
: affine ? std::max(uint64_t {1} << std::countr_zero(indirect.table_stride), uint64_t {4})
|
|
: 4u;
|
|
const uint64_t first = ((indirect.table_maximum ? indirect.table_offset
|
|
: indirect.table_offset % step) & ~uint64_t {3}) +
|
|
(indirect.table_immediate & ~3u);
|
|
if (!indirect.table_maximum) domain /= step;
|
|
const auto count = table_size >= first + 4u
|
|
? std::min((table_size - first - 4u) / step + 1u, domain)
|
|
: 0u;
|
|
if (child_count == 0u || (!image_heap && count * child_count > MaxIndirectDescriptorProbes))
|
|
return failed("candidate count", fmt::format("rows={} limit={}", count, MaxIndirectDescriptorProbes));
|
|
// Affine SMEM offsets wrap in U32 before adding the aligned immediate.
|
|
// Image heaps retain one bulk snapshot and their absolute byte-offset mapping.
|
|
if (image_heap) {
|
|
keys.resize(table_size / 4u);
|
|
if (!ReadScalarTable(table_base, table_size, 0u, runtime, keys))
|
|
return read_failed("image heap read", 0u, 0u, table_base, 0u, keys.size() * sizeof(uint32_t));
|
|
}
|
|
const auto mapping_offset = snapshot.flattened_srt.size();
|
|
descriptors[resource_index] = {.dword_count = dword_count};
|
|
snapshot.flattened_srt.resize(mapping_offset + 2u, 0u);
|
|
for (uint32_t word = 0; word < count; ++word) {
|
|
const auto table_offset = affine ? first + word * step : uint64_t {word} * 4u;
|
|
uint64_t pointer_base = 0u;
|
|
if (pointer != nullptr) {
|
|
std::array<uint32_t, 2> address {};
|
|
if (!ReadScalarTable(table_base, table_size, table_offset, runtime, address))
|
|
return read_failed("parent pointer read", word, 0u, table_base, table_offset, sizeof(address));
|
|
pointer_base = (uint64_t {address[1]} << 32u) | address[0];
|
|
}
|
|
for (uint32_t child = 0; child < child_count; ++child) {
|
|
DescriptorValue candidate {.dword_count = dword_count};
|
|
if (pointer != nullptr) {
|
|
if (!ReadScalarTable(pointer_base, UINT64_MAX,
|
|
uint64_t {child} * pointer->stride + pointer->offset, runtime,
|
|
std::span(candidate.dwords).first(read_dwords)))
|
|
return read_failed("child descriptor read", word, child, pointer_base,
|
|
uint64_t {child} * pointer->stride + pointer->offset, descriptor_bytes);
|
|
} else if (!image_heap) {
|
|
if (!ReadScalarTable(table_base, table_size, table_offset, runtime,
|
|
std::span(candidate.dwords).first(read_dwords))) return false;
|
|
} else {
|
|
const auto table_word = table_offset / 4u;
|
|
std::copy_n(keys.begin() + table_word,
|
|
std::min<size_t>(read_dwords, keys.size() - table_word),
|
|
candidate.dwords.begin());
|
|
}
|
|
if (!normalize(candidate))
|
|
return normalization_failed(table_offset, word, step,
|
|
indirect.table_maximum ? "key" : "probe");
|
|
uint32_t ordinal;
|
|
if (!intern_candidate(candidate, ordinal)) return false;
|
|
if (ordinal == 0u) continue;
|
|
const auto mapping_word = image_heap ? table_offset / 4u : word * child_count + child;
|
|
snapshot.flattened_srt.resize(mapping_offset + 2u + mapping_word, 0u);
|
|
snapshot.flattened_srt[mapping_offset + 1u + mapping_word] = ordinal;
|
|
}
|
|
}
|
|
|
|
if (descriptors.size() == children_begin) {
|
|
snapshot.flattened_srt.resize(mapping_offset);
|
|
} else {
|
|
snapshot.flattened_srt[mapping_offset] =
|
|
static_cast<uint32_t>(snapshot.flattened_srt.size() - mapping_offset - 1u);
|
|
auto& root = specializations[resource_index];
|
|
root.indirect_root = resource_index;
|
|
root.indirect_mapping_offset = static_cast<uint32_t>(mapping_offset);
|
|
root.indirect_search_iterations = 0u;
|
|
}
|
|
return true;
|
|
}
|
|
if (selector == nullptr) {
|
|
uint32_t key_count = 0;
|
|
uint32_t first_key = 0;
|
|
if (!EvaluateIndirectCount(indirect, runtime, clean, key_count)) return false;
|
|
if (!indirect.selector_first.IsEmpty() && !clean.Evaluate(indirect.selector_first, first_key))
|
|
return false;
|
|
if (indirect.table_stride == 0u || key_count > MaxIndirectDescriptorProbes ||
|
|
(indirect.selector_first.IsEmpty() && key_count != 0u &&
|
|
uint64_t {indirect.table_offset} + indirect.table_immediate +
|
|
uint64_t {key_count - 1u} * indirect.table_stride + descriptor_bytes >
|
|
UINT32_MAX + 1ull)) {
|
|
return false;
|
|
}
|
|
keys.resize(key_count);
|
|
std::iota(keys.begin(), keys.end(), first_key);
|
|
if (!keys.empty() && keys.back() < keys.front())
|
|
std::rotate(keys.begin(), keys.begin() + (UINT32_MAX - first_key + 1ull), keys.end());
|
|
} else {
|
|
if (!indirect.selector_mask.IsEmpty() && table_value.dword_count != 2u) return false;
|
|
if (!selector_keys->resolved) {
|
|
if (!ResolveSelectorKeys(indirect, material_value, runtime, clean, keys)) return false;
|
|
selector_keys->resolved = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
const auto mapping_offset = snapshot.flattened_srt.size();
|
|
const auto key_count = sources.empty() ? keys.size() : sources.size();
|
|
snapshot.flattened_srt.resize(mapping_offset + 1u + key_count * 2u);
|
|
snapshot.flattened_srt[mapping_offset] = static_cast<uint32_t>(key_count);
|
|
for (uint32_t entry = 0; entry < key_count; ++entry) {
|
|
const auto key = sources.empty() ? keys[entry] : entry;
|
|
uint64_t table_offset = 0;
|
|
DescriptorValue candidate;
|
|
candidate.dword_count = dword_count;
|
|
if (sources.empty()) {
|
|
table_offset = uint64_t {(key * indirect.table_stride + indirect.table_offset) & ~3u} +
|
|
(indirect.table_immediate & ~3u);
|
|
if (!ReadScalarTable(table_base, table_size, table_offset, runtime,
|
|
std::span(candidate.dwords).first(read_dwords))) {
|
|
return read_failed("table descriptor read", key, 0u, table_base, table_offset, descriptor_bytes);
|
|
}
|
|
} else if (!clean.EvaluateDescriptor(sources[entry], candidate)) {
|
|
return false;
|
|
}
|
|
if (!normalize(candidate))
|
|
return normalization_failed(table_offset, key, indirect.table_stride, "key");
|
|
uint32_t ordinal = 0;
|
|
if (entry == 0) {
|
|
descriptors[resource_index] = candidate;
|
|
} else if (!intern_candidate(candidate, ordinal)) {
|
|
return false;
|
|
}
|
|
snapshot.flattened_srt[mapping_offset + 1u + entry * 2u] = key;
|
|
snapshot.flattened_srt[mapping_offset + 2u + entry * 2u] = ordinal;
|
|
}
|
|
if (descriptors.size() == children_begin) {
|
|
snapshot.flattened_srt.resize(mapping_offset);
|
|
} else {
|
|
auto& root = specializations[resource_index];
|
|
root.indirect_root = resource_index;
|
|
root.indirect_mapping_offset = static_cast<uint32_t>(mapping_offset);
|
|
root.indirect_search_iterations = std::bit_width(key_count);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
struct SamplerPlan {
|
|
struct Binding {
|
|
uint32_t source;
|
|
SamplerClass type;
|
|
};
|
|
std::array<std::array<uint32_t, 2>, ShaderInfo::MaxSamplers> mapping;
|
|
std::array<Binding, ShaderInfo::MaxSamplers> bindings;
|
|
uint32_t sampler_count = 0;
|
|
};
|
|
|
|
template <typename T, typename Keep>
|
|
void CompactImages(std::vector<T>& images, Keep&& keep) {
|
|
size_t count = 0;
|
|
for (size_t index = 0; index < images.size(); ++index) {
|
|
if (!keep(index)) continue;
|
|
if (count != index) images[count] = std::move(images[index]);
|
|
++count;
|
|
}
|
|
images.resize(count);
|
|
}
|
|
|
|
struct ImageRemap {
|
|
explicit ImageRemap(const ResourceSpecialization& specialization)
|
|
: indices(specialization.images.size()),
|
|
source_count(static_cast<uint32_t>(specialization.images.size())) {
|
|
for (uint32_t index = 0; index < source_count; index++) {
|
|
indices[index] = specialization.images[index].fmask ? UINT32_MAX : count++;
|
|
}
|
|
}
|
|
|
|
uint32_t operator[](uint32_t index) const {
|
|
EXIT_IF(index >= source_count);
|
|
return indices[index];
|
|
}
|
|
|
|
template <typename T>
|
|
void Apply(std::vector<T>& images) const {
|
|
EXIT_IF(images.size() != source_count);
|
|
if (count == source_count) {
|
|
return;
|
|
}
|
|
CompactImages(images, [&](size_t index) { return indices[index] != UINT32_MAX; });
|
|
}
|
|
|
|
private:
|
|
std::vector<uint32_t> indices;
|
|
uint32_t source_count;
|
|
uint32_t count = 0;
|
|
};
|
|
|
|
static bool BuildResourceSpecialization(const ResourcePlan& program, ResourceSnapshot& snapshot,
|
|
ResourceSpecialization& specialization) {
|
|
for (uint32_t i = 0; i < specialization.images.size(); i++) {
|
|
const auto& descriptor = snapshot.images[i];
|
|
auto& image = specialization.images[i];
|
|
const auto base_index = i < program.info.images.size() ? i : image.indirect_root;
|
|
if (base_index >= program.info.images.size()) {
|
|
return SpecializationFail(fmt::format("image resource {} has an invalid root", i));
|
|
}
|
|
const auto& base = program.info.images[base_index];
|
|
if (base.resource_class == ImageResourceClass::None ||
|
|
(base.atomic && base.resource_class != ImageResourceClass::Storage)) {
|
|
return SpecializationFail(fmt::format("image resource {} has an invalid class", i));
|
|
}
|
|
image.mip_count = ImageMipCount(base, descriptor);
|
|
if (NullImageDescriptor(descriptor)) {
|
|
image.numeric_class = base.atomic ? Prospero::TextureNumericClass::Uint
|
|
: Prospero::TextureNumericClass::Float;
|
|
image.dimension = Decoder::ImageDimension::Dim2D;
|
|
image.cube = false;
|
|
continue;
|
|
}
|
|
const auto descriptor_dimension = DescriptorDimension(descriptor, base.dimension);
|
|
if (descriptor_dimension == Decoder::ImageDimension::Unknown) {
|
|
return SpecializationFail(fmt::format(
|
|
"image descriptor {} has unsupported type {}: {:08x},{:08x},{:08x},{:08x},"
|
|
"{:08x},{:08x},{:08x},{:08x}",
|
|
i, (descriptor.dwords[3] >> 28u) & 0xfu, descriptor.dwords[0], descriptor.dwords[1],
|
|
descriptor.dwords[2], descriptor.dwords[3], descriptor.dwords[4],
|
|
descriptor.dwords[5], descriptor.dwords[6], descriptor.dwords[7]));
|
|
}
|
|
image.dimension = descriptor_dimension;
|
|
image.cube = DescriptorIsCube(descriptor);
|
|
const auto format =
|
|
static_cast<Prospero::BufferFormat>((descriptor.dwords[1] >> 20u) & 0x1ffu);
|
|
if (base.atomic &&
|
|
(base.atomic64 ? format != Prospero::BufferFormat::k32_32UInt
|
|
: format != Prospero::BufferFormat::k32UInt &&
|
|
format != Prospero::BufferFormat::k32SInt &&
|
|
format != Prospero::BufferFormat::k32Float)) {
|
|
return SpecializationFail(
|
|
fmt::format("atomic image descriptor {} uses unsupported format {}", i,
|
|
static_cast<uint32_t>(format)));
|
|
}
|
|
const bool storage = base.resource_class == ImageResourceClass::Storage;
|
|
image.fmask = Prospero::IsFmaskTextureFormat(format);
|
|
if (image.fmask) {
|
|
if (storage || base.depth_compare ||
|
|
image.indirect_root != ImageResource::NoIndirectImage ||
|
|
std::ranges::any_of(program.info.sampled_pairs,
|
|
[&](const auto& pair) { return pair.image == i; })) {
|
|
return SpecializationFail("FMASK requires a direct image load");
|
|
}
|
|
}
|
|
image.conversion_format = ImageConversionFormat(format);
|
|
if (storage || image.conversion_format != Prospero::BufferFormat::kInvalid) {
|
|
image.shader_swizzle = DescriptorImageSwizzle(descriptor);
|
|
}
|
|
const auto raw_sint_format = RawSignedStorageFormat(base, format);
|
|
const bool raw_sint_storage = raw_sint_format != Prospero::BufferFormat::kInvalid;
|
|
if (raw_sint_format == Prospero::BufferFormat::k16UInt) image.conversion_format = format;
|
|
image.numeric_class = Prospero::SampledTextureNumericClass(
|
|
Prospero::RemapTextureFormat(format));
|
|
if (!storage && format == Prospero::BufferFormat::k11_11_10UNorm) {
|
|
if (image.indirect_root != ImageResource::NoIndirectImage) {
|
|
return SpecializationFail("packed UNorm sampling requires a direct image");
|
|
}
|
|
if (std::ranges::any_of(program.info.sampled_pairs,
|
|
[&](const auto& pair) { return pair.image == i; }) &&
|
|
!ValidatePackedUnormSampling(program, snapshot, i)) return false;
|
|
}
|
|
if (storage) {
|
|
if ((!raw_sint_storage && image.numeric_class == Prospero::TextureNumericClass::Sint) ||
|
|
image.numeric_class == Prospero::TextureNumericClass::Unsupported) {
|
|
return SpecializationFail(
|
|
fmt::format("storage image descriptor {} uses unsupported format {}", i,
|
|
static_cast<uint32_t>(format)));
|
|
}
|
|
if (raw_sint_storage || base.atomic) {
|
|
image.numeric_class = Prospero::TextureNumericClass::Uint;
|
|
}
|
|
} else if (image.numeric_class == Prospero::TextureNumericClass::Unsupported ||
|
|
(base.depth_compare &&
|
|
image.numeric_class != Prospero::TextureNumericClass::Float)) {
|
|
auto message = fmt::format(
|
|
"sampled image descriptor {} uses unsupported format {}: root={} source={} "
|
|
"first_use_pc={:#x} r128={} words={:08x},{:08x},{:08x},{:08x},"
|
|
"{:08x},{:08x},{:08x},{:08x}",
|
|
i, static_cast<uint32_t>(format), base_index, base.source, base.first_use_pc,
|
|
base.r128, descriptor.dwords[0], descriptor.dwords[1], descriptor.dwords[2],
|
|
descriptor.dwords[3], descriptor.dwords[4], descriptor.dwords[5],
|
|
descriptor.dwords[6], descriptor.dwords[7]);
|
|
const auto* source = Source(program, base.source);
|
|
if (i >= program.info.images.size() && source != nullptr &&
|
|
source->indirect_descriptor &&
|
|
IsBoundedDescriptorTable(program, *source->indirect_descriptor) &&
|
|
!source->indirect_descriptor->pointer_table) {
|
|
const auto& indirect = *source->indirect_descriptor;
|
|
const size_t mapping = specialization.images[base_index].indirect_mapping_offset;
|
|
const auto& flat = snapshot.flattened_srt;
|
|
if (mapping < flat.size() && flat[mapping] <= flat.size() - mapping - 1u) {
|
|
uint32_t ordinal = 0;
|
|
for (size_t child = program.info.images.size(); child <= i; ++child)
|
|
ordinal += specialization.images[child].indirect_root == base_index;
|
|
uint32_t matches = 0;
|
|
uint64_t first_offset = 0;
|
|
for (uint32_t probe = 0; probe < flat[mapping]; ++probe) {
|
|
if (flat[mapping + 1u + probe] != ordinal) continue;
|
|
if (matches++ == 0u) first_offset = uint64_t {probe} * 4u;
|
|
}
|
|
message += fmt::format(" table_source={} ordinal={} dense_matches={} "
|
|
"first_candidate_offset={:#x}",
|
|
indirect.table_source, ordinal, matches, first_offset);
|
|
}
|
|
}
|
|
return SpecializationFail(message);
|
|
}
|
|
}
|
|
for (uint32_t root_index = 0; root_index < specialization.images.size(); root_index++) {
|
|
auto& root = specialization.images[root_index];
|
|
if (root.indirect_root != root_index) {
|
|
continue;
|
|
}
|
|
const auto key_count = root.indirect_mapping_offset < snapshot.flattened_srt.size()
|
|
? snapshot.flattened_srt[root.indirect_mapping_offset]
|
|
: 0u;
|
|
if (key_count == 0u ||
|
|
static_cast<size_t>(root.indirect_mapping_offset) + 1u +
|
|
static_cast<size_t>(key_count) * (root.indirect_search_iterations == 0u ? 1u : 2u) >
|
|
snapshot.flattened_srt.size()) {
|
|
return SpecializationFail("indirect image specialization has an invalid key mapping");
|
|
}
|
|
uint32_t exemplar = ImageResource::NoIndirectImage;
|
|
uint32_t resource_count = 0;
|
|
for (uint32_t resource = 0; resource < specialization.images.size(); resource++) {
|
|
if (specialization.images[resource].indirect_root != root_index) {
|
|
continue;
|
|
}
|
|
resource_count++;
|
|
if (exemplar == ImageResource::NoIndirectImage &&
|
|
!NullImageDescriptor(snapshot.images[resource])) {
|
|
exemplar = resource;
|
|
}
|
|
}
|
|
if (resource_count < 2u || exemplar == ImageResource::NoIndirectImage) {
|
|
return SpecializationFail("indirect image specialization has no typed candidate");
|
|
}
|
|
const auto& image_class = specialization.images[exemplar];
|
|
const auto& base = program.info.images[root_index];
|
|
const bool storage_swizzle = base.unfiltered_access_only &&
|
|
base.resource_class == ImageResourceClass::Storage;
|
|
const auto is_2d = [](Decoder::ImageDimension dimension) {
|
|
return dimension == Decoder::ImageDimension::Dim2D ||
|
|
dimension == Decoder::ImageDimension::Dim2DArray;
|
|
};
|
|
const auto is_1d_or_2d = [](Decoder::ImageDimension dimension) {
|
|
return dimension == Decoder::ImageDimension::Dim1D ||
|
|
dimension == Decoder::ImageDimension::Dim2D;
|
|
};
|
|
for (uint32_t candidate = 0; candidate < specialization.images.size(); candidate++) {
|
|
auto& image = specialization.images[candidate];
|
|
if (image.indirect_root != root_index) {
|
|
continue;
|
|
}
|
|
if (NullImageDescriptor(snapshot.images[candidate])) {
|
|
image.numeric_class = image_class.numeric_class;
|
|
image.dimension = image_class.dimension;
|
|
image.mip_count = image_class.mip_count;
|
|
image.conversion_format = image_class.conversion_format;
|
|
image.shader_swizzle = image_class.shader_swizzle;
|
|
image.cube = image_class.cube;
|
|
}
|
|
const bool same_coordinates = image.dimension == image_class.dimension &&
|
|
image.cube == image_class.cube;
|
|
const bool unfiltered_coordinates = base.unfiltered_access_only &&
|
|
base.dimension == Decoder::ImageDimension::Dim2D &&
|
|
!image.cube && !image_class.cube && is_1d_or_2d(image.dimension) &&
|
|
is_1d_or_2d(image_class.dimension);
|
|
if ((!same_coordinates && !(is_2d(image.dimension) && is_2d(image_class.dimension)) &&
|
|
!unfiltered_coordinates) ||
|
|
image.mip_count != image_class.mip_count ||
|
|
image.conversion_format != image_class.conversion_format ||
|
|
(!storage_swizzle && image.shader_swizzle != image_class.shader_swizzle)) {
|
|
return SpecializationFail(
|
|
fmt::format("indirect image table at pc 0x{:08x} has incompatible candidates",
|
|
program.info.images[root_index].first_use_pc));
|
|
}
|
|
}
|
|
}
|
|
CompactImages(snapshot.images, [&](size_t index) { return !specialization.images[index].fmask; });
|
|
return true;
|
|
}
|
|
|
|
bool BuildSamplerPlan(const ShaderInfo& base, SamplerPlan& plan) {
|
|
if (base.samplers.size() > plan.mapping.size()) {
|
|
return false;
|
|
}
|
|
std::array<uint8_t, ShaderInfo::MaxSamplers> usage {};
|
|
plan.sampler_count = static_cast<uint32_t>(base.samplers.size());
|
|
for (const auto& pair: base.sampled_pairs) {
|
|
if (pair.image >= base.images.size() || pair.sampler >= base.samplers.size()) {
|
|
return false;
|
|
}
|
|
usage[pair.sampler] |= 1u << static_cast<uint32_t>(ClassifySampler(base.images[pair.image]));
|
|
for (const auto candidate: base.images[pair.image].indirect_resources) {
|
|
if (candidate >= base.images.size()) return false;
|
|
usage[pair.sampler] |= 1u << static_cast<uint32_t>(ClassifySampler(base.images[candidate]));
|
|
}
|
|
for (const auto candidate: base.samplers[pair.sampler].indirect_resources) {
|
|
if (candidate >= base.samplers.size()) return false;
|
|
usage[candidate] |= usage[pair.sampler];
|
|
}
|
|
}
|
|
for (uint32_t index = 0; index < base.samplers.size(); index++) {
|
|
auto& mapping = plan.mapping[index];
|
|
mapping.fill(UINT32_MAX);
|
|
const auto classes = usage[index] == 0u ? 1u : usage[index];
|
|
bool first = true;
|
|
for (uint32_t type = 0; type < mapping.size(); type++) {
|
|
if ((classes & (1u << type)) == 0u) continue;
|
|
const auto target = first ? index : plan.sampler_count++;
|
|
if (target >= ShaderInfo::MaxSamplers) return false;
|
|
mapping[type] = target;
|
|
plan.bindings[target] = {index, static_cast<SamplerClass>(type)};
|
|
first = false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
template <typename Predicate>
|
|
static std::vector<ResourceBlock> ResourceControlFlow(const Program& program, const Predicate& predicate) {
|
|
// The renderer checks captured scalar reads against final buffer and image write ranges.
|
|
if (program.has_address_writes) {
|
|
return {};
|
|
}
|
|
std::unordered_map<const Block*, uint32_t> indices;
|
|
for (uint32_t i = 0; i < program.blocks.size(); i++) {
|
|
if (!indices.emplace(program.blocks[i], i).second) {
|
|
return {};
|
|
}
|
|
}
|
|
std::vector<ResourceBlock> blocks(program.blocks.size());
|
|
for (uint32_t i = 0; i < blocks.size(); i++) {
|
|
auto& block = blocks[i];
|
|
const auto& info = *program.blocks[i];
|
|
const auto& terminator = info.terminator;
|
|
const std::array targets {terminator.true_block, terminator.false_block};
|
|
std::span<Block* const> successors;
|
|
switch (terminator.kind) {
|
|
case CFG::TerminatorKind::Branch: successors = std::span(targets).first(1); break;
|
|
case CFG::TerminatorKind::ConditionalBranch:
|
|
successors = targets;
|
|
block.condition = info.condition;
|
|
break;
|
|
case CFG::TerminatorKind::IndirectBranch: successors = info.ImmSuccessors(); break;
|
|
case CFG::TerminatorKind::Return: break;
|
|
default: return {};
|
|
}
|
|
for (const auto successor: successors) {
|
|
const auto found = indices.find(successor);
|
|
if (found == indices.end()) {
|
|
return {};
|
|
}
|
|
block.successors.push_back(found->second);
|
|
}
|
|
for (const auto& inst: *program.blocks[i]) {
|
|
const auto op = inst.GetOpcode();
|
|
if (op == ValueOpcode::ReadConst) {
|
|
block.srt_reads.push_back(inst.Arg(1).Resolve().U32());
|
|
continue;
|
|
}
|
|
const auto buffer = BufferAccessOf(op);
|
|
const auto image = ImageOpcodeInfoOf(op);
|
|
if (buffer == BufferAccess::None && image.access == ImageAccess::None) {
|
|
continue;
|
|
}
|
|
const auto& memory = program.memory_info.at(inst.Flags<MemoryFlags>().index);
|
|
if (memory.planning_only || memory.kind == ResourceKind::IndirectBuffer) {
|
|
continue;
|
|
}
|
|
if (buffer != BufferAccess::None) {
|
|
block.sources.push_back(program.info.buffers.at(memory.resource).source);
|
|
} else {
|
|
block.sources.push_back(program.info.images.at(memory.resource).source);
|
|
if (image.needs_sampler) {
|
|
block.sources.push_back(program.info.samplers.at(memory.sampler).source);
|
|
}
|
|
}
|
|
}
|
|
std::ranges::sort(block.sources);
|
|
block.sources.erase(std::unique(block.sources.begin(), block.sources.end()),
|
|
block.sources.end());
|
|
}
|
|
for (auto& block: blocks) block.condition = predicate(block.condition);
|
|
if (std::ranges::none_of(
|
|
blocks, [](const ResourceBlock& block) { return !block.condition.IsEmpty(); })) {
|
|
return {};
|
|
}
|
|
return blocks;
|
|
}
|
|
|
|
// ResolveComputeBufferFill proves complete workgroups and matching requested extents, so every
|
|
// dispatch-bound predicate accepted here is true for every invocation covered by the fill.
|
|
static bool IsFullDispatchPredicate(Value value, uint32_t depth = 0) {
|
|
value = value.Resolve();
|
|
if (value == Value(true)) return true;
|
|
const auto* inst = value.TryInstruction();
|
|
if (inst == nullptr || depth > 32) return false;
|
|
if (inst->GetOpcode() == ValueOpcode::LogicalAnd) {
|
|
return IsFullDispatchPredicate(inst->Arg(0), depth + 1) &&
|
|
IsFullDispatchPredicate(inst->Arg(1), depth + 1);
|
|
}
|
|
if (inst->GetOpcode() != ValueOpcode::ULessThan32) return false;
|
|
const auto* global = inst->Arg(0).Resolve().TryInstruction();
|
|
const auto* extent = inst->Arg(1).Resolve().TryInstruction();
|
|
if (global == nullptr || global->GetOpcode() != ValueOpcode::GetBuiltin ||
|
|
global->Arg(0).Resolve() != Value(static_cast<uint32_t>(StageInputKind::GlobalInvocationId)) ||
|
|
extent == nullptr || extent->GetOpcode() != ValueOpcode::GetDispatchThreadExtent)
|
|
return false;
|
|
const auto axis = global->Arg(1).Resolve();
|
|
return axis.IsImmediate() && axis.U32() < 3 && extent->Arg(0).Resolve() == axis;
|
|
}
|
|
|
|
static std::optional<std::array<uint64_t, 3>> FillIndex(Value value, uint32_t axis, Value guard) {
|
|
return AffineIndex<3>(value, [&](Value at) { return ResolveActiveU32(at, guard); },
|
|
[&](Value at) -> std::optional<size_t> {
|
|
const auto* inst = at.TryInstruction();
|
|
if (inst == nullptr || inst->GetOpcode() != ValueOpcode::GetBuiltin ||
|
|
inst->Arg(1) != Value(axis)) return {};
|
|
if (inst->Arg(0) == Value(static_cast<uint32_t>(StageInputKind::LocalInvocationId))) return 1u;
|
|
if (inst->Arg(0) == Value(static_cast<uint32_t>(StageInputKind::WorkgroupId))) return 2u;
|
|
return {};
|
|
});
|
|
}
|
|
|
|
static UniformFillPlan AnalyzeUniformFill(const Program& program) {
|
|
if (program.stage != ShaderType::Compute || program.blocks.empty() || program.info.uses_dma ||
|
|
!program.info.samplers.empty()) {
|
|
return {};
|
|
}
|
|
std::unordered_set<const Block*> visited;
|
|
const auto* block = program.blocks.front();
|
|
const Inst* store = nullptr;
|
|
for (;;) {
|
|
if (block == nullptr || !visited.insert(block).second) return {};
|
|
for (const auto& inst: *block) {
|
|
if (AddressOpcodeInfoOf(inst.GetOpcode()).access != AddressAccess::None) return {};
|
|
if (!inst.MayHaveSideEffects()) continue;
|
|
if (store != nullptr || (BufferAccessOf(inst.GetOpcode()) != BufferAccess::Write &&
|
|
inst.GetOpcode() != ValueOpcode::ImageWrite))
|
|
return {};
|
|
store = &inst;
|
|
}
|
|
const auto& term = block->terminator;
|
|
if (term.kind == CFG::TerminatorKind::Return) break;
|
|
if (term.kind != CFG::TerminatorKind::Branch) return {};
|
|
block = term.true_block;
|
|
}
|
|
if (store == nullptr || visited.size() != program.blocks.size()) return {};
|
|
for (const auto& buffer: program.info.buffers) {
|
|
if (buffer.read && (!buffer.scalar || buffer.written)) return {};
|
|
}
|
|
const auto& memory = program.memory_info.at(store->Flags<MemoryFlags>().index);
|
|
UniformFillPlan result;
|
|
result.fill.resource = memory.resource;
|
|
Value data;
|
|
Value guard(true);
|
|
if (store->GetOpcode() == ValueOpcode::ImageWrite) {
|
|
if (program.info.images.size() != 1 || memory.dmask != 1 || memory.data_bits != 32 ||
|
|
memory.image_has_mip || memory.image_sample_flags != 0 || memory.image_r128 ||
|
|
memory.image_dimension != Decoder::ImageDimension::Dim2DArray ||
|
|
store->Arg(3).Resolve() != Value(true)) return {};
|
|
const auto& image = program.info.images[memory.resource];
|
|
if (image.read || image.atomic || image.mip_mode != ImageMipMode::None) return {};
|
|
const auto* address = store->Arg(1).ResolveInstruction();
|
|
if (address == nullptr || address->GetOpcode() != ValueOpcode::MakeImageAddress) return {};
|
|
for (uint32_t axis = 0; axis < 3; ++axis) {
|
|
const auto index = FillIndex(address->Arg(axis), axis, guard);
|
|
if (!index || (*index)[0] != 0) return {};
|
|
if (axis < 2) {
|
|
if ((*index)[1] != 1 || (*index)[2] == 0) return {};
|
|
} else if ((*index)[1] != 0 || (*index)[2] != 1) {
|
|
return {};
|
|
}
|
|
result.fill.group_stride[axis] = static_cast<uint32_t>((*index)[2]);
|
|
}
|
|
const auto* values = store->Arg(2).ResolveInstruction();
|
|
if (values == nullptr || values->GetOpcode() != ValueOpcode::CompositeConstructU32x4)
|
|
return {};
|
|
result.fill.kind = UniformFillKind::Image;
|
|
result.fill.words = 1;
|
|
data = values->Arg(0);
|
|
} else {
|
|
if (!program.info.images.empty()) return {};
|
|
const auto op = store->GetOpcode();
|
|
constexpr std::array stores {ValueOpcode::StoreBufferU32, ValueOpcode::StoreBufferU32x2,
|
|
ValueOpcode::StoreBufferU32x3, ValueOpcode::StoreBufferU32x4};
|
|
const auto store_op = std::ranges::find(stores, op);
|
|
if (store_op == stores.end() || store->Arg(2).Resolve() != Value(0u) ||
|
|
store->Arg(3).Resolve() != Value(0u))
|
|
return {};
|
|
guard = store->Arg(5).Resolve();
|
|
if (!IsFullDispatchPredicate(guard)) return {};
|
|
if (!memory.formatted || memory.typed || !memory.idxen || memory.offen || memory.offset != 0 ||
|
|
memory.data_bits != 32 ||
|
|
memory.data_dwords != static_cast<uint32_t>(store_op - stores.begin() + 1))
|
|
return {};
|
|
const auto address = FillIndex(store->Arg(1), 0, guard);
|
|
if (!address || (*address)[0] != 0 || (*address)[1] != 1 || (*address)[2] == 0) return {};
|
|
result.fill.kind = UniformFillKind::Buffer;
|
|
result.fill.group_stride[0] = static_cast<uint32_t>((*address)[2]);
|
|
result.fill.words = memory.data_dwords;
|
|
data = store->Arg(4);
|
|
}
|
|
data = ResolveActiveU32(data, guard);
|
|
const auto* vector = data.TryInstruction();
|
|
constexpr std::array composites {ValueOpcode::CompositeConstructU32x2,
|
|
ValueOpcode::CompositeConstructU32x3,
|
|
ValueOpcode::CompositeConstructU32x4};
|
|
if (result.fill.words > 1 &&
|
|
(vector == nullptr || vector->GetOpcode() != composites[result.fill.words - 2]))
|
|
return {};
|
|
for (uint32_t i = 0; i < result.fill.words; ++i) {
|
|
const auto word = ResolveActiveU32(result.fill.words == 1 ? data : vector->Arg(i), guard);
|
|
if (word.GetType() != Type::U32 ||
|
|
!ValidateRuntimeValue(program, word, RuntimeValueType::Integer))
|
|
return {};
|
|
result.values[i] = word;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
ResourcePlan ExtractResourcePlan(const Program& program) {
|
|
ResourcePlan plan;
|
|
plan.stage = program.stage;
|
|
plan.shader_hash = program.shader_hash;
|
|
plan.user_data_base = program.user_data_base;
|
|
plan.user_data_count = program.user_data_count;
|
|
plan.info = program.info;
|
|
plan.memory_info = program.memory_info;
|
|
plan.source_reads = program.source_reads;
|
|
plan.srt_plan_complete = program.srt_plan_complete;
|
|
plan.resource_tracking_complete = program.resource_tracking_complete;
|
|
|
|
std::unordered_map<const Inst*, Inst*> cloned;
|
|
std::function<Value(Value)> Clone = [&](Value value) -> Value {
|
|
value = value.Resolve();
|
|
const auto* source = value.TryInstruction();
|
|
if (source == nullptr) {
|
|
return value;
|
|
}
|
|
if (source->GetOpcode() == ValueOpcode::ReadConst) {
|
|
const auto slot = source->Arg(1).Resolve();
|
|
EXIT_IF(!slot.IsImmediate() || slot.GetType() != Type::U32 ||
|
|
slot.U32() >= program.srt_reads.size());
|
|
return Clone(program.srt_reads[slot.U32()].value);
|
|
}
|
|
if (source->GetOpcode() == ValueOpcode::ReadFirstLane &&
|
|
ValidateRuntimeValue(program, source->Arg(0))) {
|
|
// Uniform values need no new EXEC context; preserve their shared evaluation memo.
|
|
return Clone(source->Arg(0));
|
|
}
|
|
if (source->GetOpcode() == ValueOpcode::Phi) {
|
|
const auto invariant = ResolveInvariantPhi(program, value);
|
|
if (!invariant.IsEmpty() && invariant != value) {
|
|
return Clone(invariant);
|
|
}
|
|
}
|
|
if (const auto found = cloned.find(source); found != cloned.end()) {
|
|
return Value(found->second);
|
|
}
|
|
if (source->GetOpcode() == ValueOpcode::LoadBufferU32) {
|
|
plan.requires_specialization_memory = true;
|
|
plan.capture_specialization_reads = true;
|
|
}
|
|
auto& target =
|
|
plan.value_storage.emplace_back(source->GetOpcode(), source->Flags<uint64_t>());
|
|
if (source->GetOpcode() == ValueOpcode::LoadAddressU32 ||
|
|
source->GetOpcode() == ValueOpcode::ReadConstBuffer ||
|
|
source->GetOpcode() == ValueOpcode::LoadBufferU32) {
|
|
target.SetFlags(SrtReadFlags {.index = source->Flags<MemoryFlags>().index});
|
|
}
|
|
cloned.emplace(source, &target);
|
|
if (source->GetOpcode() == ValueOpcode::Phi) {
|
|
for (size_t index = 0; index < source->NumArgs(); index++) {
|
|
target.AddPhiOperand(nullptr, Clone(source->Arg(index)));
|
|
}
|
|
} else {
|
|
for (size_t index = 0; index < source->NumArgs(); index++) {
|
|
target.SetArg(index, Clone(source->Arg(index)));
|
|
}
|
|
}
|
|
return Value(&target);
|
|
};
|
|
|
|
bool capture_indirect_reads = false;
|
|
plan.descriptor_sources.reserve(program.descriptor_sources.size());
|
|
for (const auto& source: program.descriptor_sources) {
|
|
auto& target = plan.descriptor_sources.emplace_back();
|
|
target.dword_count = source.dword_count;
|
|
target.indirect_descriptor = source.indirect_descriptor;
|
|
if (target.indirect_descriptor.has_value()) {
|
|
plan.requires_specialization_memory = true;
|
|
capture_indirect_reads = true;
|
|
target.indirect_descriptor->key_count = Clone(target.indirect_descriptor->key_count);
|
|
target.indirect_descriptor->selector_first =
|
|
Clone(target.indirect_descriptor->selector_first);
|
|
target.indirect_descriptor->selector_mask =
|
|
Clone(target.indirect_descriptor->selector_mask);
|
|
}
|
|
for (uint32_t dword = 0; dword < source.dword_count; dword++) {
|
|
target.dwords[dword] = Clone(source.dwords[dword]);
|
|
}
|
|
}
|
|
// Destination tables share keys when their complete selector domains agree.
|
|
// Resolve those domains lazily at dispatch time, without repeated guest reads.
|
|
plan.selector_groups.resize(plan.descriptor_sources.size(), UINT32_MAX);
|
|
for (uint32_t source = 0; source < plan.descriptor_sources.size(); ++source) {
|
|
const auto& descriptor = program.descriptor_sources[source].indirect_descriptor;
|
|
if (!descriptor || !descriptor->selector) continue;
|
|
const auto& a = *descriptor;
|
|
uint32_t group = 0;
|
|
for (; group < plan.selector_keys.size(); ++group) {
|
|
const auto& b = *program.descriptor_sources[plan.selector_keys[group].source].indirect_descriptor;
|
|
if (EquivalentSelectorDomain(program, a, b)) break;
|
|
}
|
|
if (group == plan.selector_keys.size()) plan.selector_keys.push_back({.source = source});
|
|
plan.selector_groups[source] = group;
|
|
}
|
|
plan.srt_reads.reserve(program.srt_reads.size());
|
|
for (const auto& read: program.srt_reads) {
|
|
plan.srt_reads.push_back({Clone(read.value), read.flat_offset});
|
|
}
|
|
// Retain integer control expressions with opaque varying leaves. Their identities
|
|
// matter when an EXEC factor also guards a selected value; never clone float work.
|
|
bool capture_control_reads = false;
|
|
bool uniform_control = false;
|
|
const auto control_value_count = plan.value_storage.size();
|
|
struct ControlValue {
|
|
Value value;
|
|
bool complete = false;
|
|
};
|
|
std::unordered_map<const Inst*, ControlValue> predicates;
|
|
std::function<Value(Value)> ClonePredicate = [&](Value value) -> Value {
|
|
value = value.Resolve();
|
|
if (value.IsEmpty()) return {};
|
|
const auto* inst = value.TryInstruction();
|
|
if (inst == nullptr) {
|
|
uniform_control |= value.GetType() == Type::U1;
|
|
return ValidateRuntimeValue(program, value, RuntimeValueType::Integer) ? value : Value {};
|
|
}
|
|
const auto [found, inserted] = predicates.try_emplace(inst);
|
|
auto& entry = found->second;
|
|
auto& result = entry.value;
|
|
const auto opaque = [&]() -> Value {
|
|
if (value.GetType() != Type::U1 && value.GetType() != Type::U32) return {};
|
|
return result = Value(&plan.value_storage.emplace_back(
|
|
value.GetType() == Type::U1 ? ValueOpcode::UndefU1 : ValueOpcode::UndefU32));
|
|
};
|
|
// A recursive projection remains opaque, including unstructured loop Phis.
|
|
if (!inserted) return entry.complete || !result.IsEmpty() ? result : opaque();
|
|
const auto finish = [&](Value projected) {
|
|
entry.complete = true;
|
|
return result = projected;
|
|
};
|
|
if (ValidateRuntimeValue(program, value, RuntimeValueType::Integer)) {
|
|
uniform_control = true;
|
|
capture_control_reads |=
|
|
!ValidateRuntimeValue(program, value, RuntimeValueType::ImmutableInteger);
|
|
return finish(Clone(value));
|
|
}
|
|
const auto op = inst->GetOpcode();
|
|
if (op == ValueOpcode::ConditionRef) return finish(ClonePredicate(inst->Arg(0)));
|
|
if (op == ValueOpcode::Phi) {
|
|
if (inst->Parent() == nullptr || inst->Parent()->terminator.loop_header ||
|
|
inst->NumPhiBlocks() != inst->NumArgs() || inst->NumArgs() == 0u ||
|
|
(value.GetType() != Type::U1 && value.GetType() != Type::U32)) return finish(opaque());
|
|
std::vector<uint32_t> predecessors;
|
|
for (size_t index = 0; index < inst->NumArgs(); ++index) {
|
|
const auto predecessor = std::ranges::find(program.blocks, inst->PhiBlock(index));
|
|
if (predecessor == program.blocks.end()) return finish(opaque());
|
|
predecessors.push_back(static_cast<uint32_t>(predecessor - program.blocks.begin()));
|
|
}
|
|
auto& node = plan.value_storage.emplace_back(op, inst->Flags<uint64_t>());
|
|
plan.control_phi_predecessors.emplace(&node, std::move(predecessors));
|
|
for (size_t index = 0; index < inst->NumArgs(); ++index)
|
|
node.AddPhiOperand(nullptr, ClonePredicate(inst->Arg(index)));
|
|
return finish(Value(&node));
|
|
}
|
|
if (op != ValueOpcode::LogicalAnd && op != ValueOpcode::LogicalOr &&
|
|
op != ValueOpcode::LogicalNot && op != ValueOpcode::IEqual32 &&
|
|
op != ValueOpcode::INotEqual32 && op != ValueOpcode::ULessThanEqual32 &&
|
|
op != ValueOpcode::SelectU1 && op != ValueOpcode::SelectU32) return finish(opaque());
|
|
auto& node = plan.value_storage.emplace_back(op);
|
|
for (size_t index = 0; index < inst->NumArgs(); ++index)
|
|
node.SetArg(index, ClonePredicate(inst->Arg(index)));
|
|
return finish(Value(&node));
|
|
};
|
|
plan.control_flow = ResourceControlFlow(program, ClonePredicate);
|
|
if (!uniform_control) {
|
|
plan.control_flow.clear();
|
|
plan.control_phi_predecessors.clear();
|
|
auto first = std::next(plan.value_storage.begin(), control_value_count);
|
|
for (auto it = first; it != plan.value_storage.end(); ++it) it->Invalidate();
|
|
plan.value_storage.erase(first, plan.value_storage.end());
|
|
}
|
|
plan.uniform_fill = program.uniform_fill.fill.kind != UniformFillKind::None
|
|
? program.uniform_fill : AnalyzeUniformFill(program);
|
|
for (auto& value: plan.uniform_fill.values) value = Clone(value);
|
|
plan.uniform_fill.pattern_size = Clone(plan.uniform_fill.pattern_size);
|
|
plan.uniform_fill.element_count = Clone(plan.uniform_fill.element_count);
|
|
plan.capture_specialization_reads |=
|
|
capture_indirect_reads || capture_control_reads || !plan.source_reads.empty();
|
|
if (plan.capture_specialization_reads) {
|
|
// Every former flat alias now targets its retained raw read. Mark those reads
|
|
// on the normalized graph, so discarded Phi/EXEC dependencies stay discarded.
|
|
std::unordered_set<const Inst*> planned_reads;
|
|
for (const auto& read: plan.srt_reads) {
|
|
auto* inst = read.value.ResolveInstruction();
|
|
EXIT_IF(inst == nullptr || (inst->GetOpcode() != ValueOpcode::LoadAddressU32 &&
|
|
inst->GetOpcode() != ValueOpcode::ReadConstBuffer));
|
|
planned_reads.insert(inst);
|
|
}
|
|
for (const auto& source: plan.descriptor_sources) {
|
|
if (!source.indirect_descriptor.has_value()) continue;
|
|
const auto& indirect = *source.indirect_descriptor;
|
|
if (indirect.selector) {
|
|
MarkCleanReads(planned_reads, Source(plan, indirect.selector->source));
|
|
if (indirect.selector->rows)
|
|
MarkCleanReads(planned_reads, Source(plan, indirect.selector->rows->source));
|
|
}
|
|
MarkCleanReads(planned_reads, nullptr, indirect.selector_mask);
|
|
MarkCleanReads(planned_reads, nullptr, indirect.selector_first);
|
|
MarkCleanReads(planned_reads, nullptr, indirect.key_count);
|
|
if (indirect.sources.empty()) {
|
|
MarkCleanReads(planned_reads, Source(plan, indirect.table_source));
|
|
} else {
|
|
for (const auto candidate: indirect.sources) {
|
|
MarkCleanReads(planned_reads, Source(plan, candidate));
|
|
}
|
|
}
|
|
}
|
|
// Clean writable addresses prove that shader writes cannot overlap captured reads.
|
|
for (const auto& buffer: plan.info.buffers) {
|
|
if (buffer.written) MarkCleanReads(planned_reads, Source(plan, buffer.source));
|
|
}
|
|
for (const auto& image: plan.info.images) {
|
|
if (image.written) MarkCleanReads(planned_reads, Source(plan, image.source));
|
|
}
|
|
}
|
|
if (capture_indirect_reads || !plan.source_reads.empty())
|
|
plan.resource_tracking_complete &= !program.has_address_writes;
|
|
CompileSrtWalker(plan);
|
|
return plan;
|
|
}
|
|
|
|
bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime,
|
|
ResourceSnapshot& snapshot, ResourceSpecialization& specialization) {
|
|
for (auto& keys: program.selector_keys) keys.resolved = false;
|
|
if (!program.resource_tracking_complete ||
|
|
((program.requires_specialization_memory || !program.source_reads.empty()) &&
|
|
runtime.read_specialization_memory == nullptr)) {
|
|
return false;
|
|
}
|
|
const auto uses_workgroup_count = [](const DescriptorSource& source) {
|
|
return source.indirect_descriptor && source.indirect_descriptor->workgroup_axis != UINT32_MAX;
|
|
};
|
|
const bool capture_reads = program.capture_specialization_reads || !program.source_reads.empty() ||
|
|
(runtime.indirect_args_address != 0u &&
|
|
std::ranges::any_of(program.descriptor_sources, uses_workgroup_count));
|
|
auto& reads = snapshot.specialization_reads;
|
|
reads = program.source_reads;
|
|
SrtReadCapture capture {runtime, reads};
|
|
auto observed = capture_reads ? capture.ObservedRuntime() : runtime;
|
|
SrtWalker clean(program, CleanRuntime(observed));
|
|
SrtWalker walker(program, observed,
|
|
capture_reads || program.requires_specialization_memory ? &clean : nullptr);
|
|
if (!walker.RefreshFlatBuffer(snapshot.flattened_srt)) {
|
|
return SpecializationFail(fmt::format("SRT refresh complete={} reads={} user_data={}",
|
|
program.srt_plan_complete, program.srt_reads.size(), runtime.user_data.size()));
|
|
}
|
|
const auto active = std::span<const uint8_t>(program.active_sources);
|
|
std::array<uint32_t, 3> workgroup_counts {};
|
|
if (runtime.indirect_args_address != 0u) {
|
|
for (uint32_t source = 0; source < program.descriptor_sources.size(); ++source) {
|
|
if ((!active.empty() && !active[source]) ||
|
|
!uses_workgroup_count(program.descriptor_sources[source])) continue;
|
|
if ((runtime.indirect_args_address & 3u) != 0u ||
|
|
!ReadTable(runtime.indirect_args_address, sizeof(workgroup_counts), 0u,
|
|
observed, workgroup_counts)) return false;
|
|
observed.workgroup_counts = workgroup_counts;
|
|
break;
|
|
}
|
|
}
|
|
snapshot.uniform_fill = {};
|
|
const auto& fill = program.uniform_fill;
|
|
auto words = fill.fill.words;
|
|
std::array<uint32_t, 4> stored {};
|
|
bool uniform_fill = words != 0;
|
|
if (uniform_fill && !fill.pattern_size.IsEmpty())
|
|
uniform_fill = clean.Evaluate(fill.pattern_size, words) && words >= 1 && words <= 4;
|
|
uint32_t elements = 0;
|
|
if (uniform_fill && !fill.element_count.IsEmpty())
|
|
uniform_fill = clean.Evaluate(fill.element_count, elements) && elements != 0;
|
|
for (uint32_t i = 0; i < words && uniform_fill; ++i) {
|
|
uniform_fill = clean.Evaluate(fill.values[i], stored[i]) && stored[i] == stored[0];
|
|
}
|
|
if (uniform_fill) {
|
|
snapshot.uniform_fill = fill.fill;
|
|
snapshot.uniform_fill.value = stored[0];
|
|
snapshot.uniform_fill.bounded_elements = elements;
|
|
}
|
|
const auto evaluate = [&](uint32_t source, DescriptorValue& value, bool written = false) {
|
|
if (source >= program.descriptor_sources.size()) {
|
|
return false;
|
|
}
|
|
if (active.empty() || active[source]) {
|
|
return (capture_reads && written ? clean : walker).EvaluateDescriptor(source, value) ||
|
|
SpecializationFail(fmt::format("descriptor evaluation source={}", source));
|
|
}
|
|
value = {};
|
|
value.dword_count = program.descriptor_sources[source].dword_count;
|
|
return true;
|
|
};
|
|
const auto resource_failed = [&](std::string_view kind, uint32_t resource, uint32_t source,
|
|
size_t candidates, uint32_t capacity) {
|
|
return SpecializationFail(fmt::format("indirect {} resource={} source={} candidates={} capacity={}",
|
|
kind, resource, source, candidates, capacity));
|
|
};
|
|
snapshot.buffers.resize(program.info.buffers.size());
|
|
specialization.buffers.assign(program.info.buffers.size(), {});
|
|
for (uint32_t i = 0; i < snapshot.buffers.size(); ++i) {
|
|
const auto base_index =
|
|
i < program.info.buffers.size() ? i : specialization.buffers[i].indirect_root;
|
|
if (base_index >= program.info.buffers.size()) return false;
|
|
const auto& base = program.info.buffers[base_index];
|
|
if (i < program.info.buffers.size()) {
|
|
const auto* source = Source(program, base.source);
|
|
if (source == nullptr) return false;
|
|
if (source->indirect_descriptor.has_value()) {
|
|
snapshot.buffers[i] = {.dword_count = 4u};
|
|
if (active.empty() || active[base.source]) {
|
|
if (!MaterializeIndirectDescriptor(
|
|
program, base.source, i, 4u, 4u, observed, clean, snapshot,
|
|
snapshot.buffers, specialization.buffers, UINT32_MAX,
|
|
[&](DescriptorValue& value) {
|
|
return NormalizeIndirectBuffer(
|
|
value, source->indirect_descriptor->indexed, base);
|
|
}))
|
|
return resource_failed("buffer", i, base.source, snapshot.buffers.size(), UINT32_MAX);
|
|
}
|
|
} else if (!evaluate(base.source, snapshot.buffers[i], base.written)) {
|
|
return false;
|
|
}
|
|
}
|
|
auto& descriptor_value = snapshot.buffers[i];
|
|
ShaderBufferResource descriptor;
|
|
if (!DecodeBufferDescriptor(descriptor_value, descriptor)) {
|
|
return SpecializationFail(fmt::format("buffer descriptor {} has invalid width", i));
|
|
}
|
|
if (descriptor.Type() != 0) {
|
|
descriptor_value.dwords.fill(0);
|
|
descriptor = {};
|
|
}
|
|
auto packed_stride = descriptor.PackedStride();
|
|
const auto stride = packed_stride & 0x3fffu;
|
|
const bool swizzle = stride != 0u && ((packed_stride >> 14u) & 1u) != 0u;
|
|
if (stride == 0u) {
|
|
packed_stride &= ~((1u << 14u) | (3u << 16u));
|
|
} else if (!swizzle) {
|
|
packed_stride &= ~(3u << 16u);
|
|
}
|
|
auto& buffer = specialization.buffers[i];
|
|
buffer.packed_stride = packed_stride;
|
|
buffer.descriptor_format =
|
|
base.formatted ? descriptor.Format() : Prospero::BufferFormat::kInvalid;
|
|
buffer.descriptor_swizzle = base.formatted ? descriptor.DstSelXYZW() : DstSel(4, 5, 6, 7);
|
|
buffer.zero_stride_oob = descriptor.OutOfBounds() == 0u && stride == 0u;
|
|
buffer.scalar_empty = base.scalar && descriptor.GetSize() == 0u;
|
|
}
|
|
snapshot.images.resize(program.info.images.size());
|
|
specialization.images.resize(program.info.images.size());
|
|
for (uint32_t i = 0; i < program.info.images.size(); ++i) {
|
|
const auto& image = program.info.images[i];
|
|
specialization.images[i] = {
|
|
.numeric_class = image.numeric_class,
|
|
.dimension = image.dimension,
|
|
.mip_count = image.mip_count,
|
|
.conversion_format = image.conversion_format,
|
|
.shader_swizzle = image.shader_swizzle,
|
|
.indirect_root = image.indirect_root,
|
|
.indirect_mapping_offset = image.indirect_mapping_offset,
|
|
.indirect_search_iterations = image.indirect_search_iterations,
|
|
.cube = image.cube,
|
|
};
|
|
const auto* source = Source(program, image.source);
|
|
if (source == nullptr) {
|
|
return false;
|
|
}
|
|
if (source->indirect_descriptor.has_value()) {
|
|
snapshot.images[i] = {.dword_count = 8u};
|
|
if (!active.empty() && !active[image.source]) {
|
|
continue;
|
|
}
|
|
const auto& indirect = *source->indirect_descriptor;
|
|
const bool bounded_table = IsBoundedDescriptorTable(program, indirect);
|
|
if (!MaterializeIndirectDescriptor(
|
|
program, image.source, i, 8u, image.r128 ? 4u : 8u, observed, clean, snapshot, snapshot.images,
|
|
specialization.images, UINT32_MAX, [&](DescriptorValue& value) {
|
|
// A broad heap also contains resources for other typed image operations.
|
|
if (NullImageDescriptor(value) || !ValidImageDescriptor(value, image.r128) ||
|
|
(bounded_table && DescriptorDimension(value, image.dimension) != image.dimension))
|
|
value.dwords.fill(0u);
|
|
return true;
|
|
})) {
|
|
return resource_failed("image", i, image.source, snapshot.images.size(), UINT32_MAX);
|
|
}
|
|
} else {
|
|
if (!evaluate(image.source, snapshot.images[i], image.written)) {
|
|
return false;
|
|
}
|
|
if (!ValidImageDescriptor(snapshot.images[i], image.r128)) {
|
|
snapshot.images[i].dwords.fill(0);
|
|
}
|
|
}
|
|
}
|
|
snapshot.samplers.resize(program.info.samplers.size());
|
|
specialization.samplers.assign(program.info.samplers.size(), {});
|
|
for (uint32_t i = 0; i < snapshot.samplers.size(); ++i) {
|
|
const auto base_index =
|
|
i < program.info.samplers.size() ? i : specialization.samplers[i].indirect_root;
|
|
if (base_index >= program.info.samplers.size()) return false;
|
|
if (i < program.info.samplers.size()) {
|
|
auto source = program.info.samplers[i].source;
|
|
const auto* descriptor = Source(program, source);
|
|
if (descriptor == nullptr) return false;
|
|
const auto* indirect = descriptor->indirect_descriptor ? &*descriptor->indirect_descriptor : nullptr;
|
|
if (indirect != nullptr && indirect->sources.empty()) {
|
|
snapshot.samplers[i] = {.dword_count = 4u};
|
|
if ((active.empty() || active[source]) && !MaterializeIndirectDescriptor(
|
|
program, source, i, 4u, 4u, observed, clean, snapshot, snapshot.samplers,
|
|
specialization.samplers, ShaderInfo::MaxSamplers,
|
|
[](DescriptorValue&) { return true; }))
|
|
return resource_failed("sampler", i, source, snapshot.samplers.size(), ShaderInfo::MaxSamplers);
|
|
} else {
|
|
if (indirect != nullptr) source = indirect->sources[0];
|
|
if (!evaluate(source, snapshot.samplers[i])) return false;
|
|
}
|
|
}
|
|
if (program.info.samplers[base_index].gather_lod) {
|
|
const auto control = snapshot.samplers[i].dwords[2];
|
|
const auto filter = (control >> 26u) & 3u;
|
|
// MipNone always selects the base level. Explicit point gathers currently require
|
|
// encoded-zero primary and secondary bias; linear primary-mip selection is unsupported.
|
|
if (filter > 1u || (filter == 1u && (control & 0xfffffu) != 0u)) {
|
|
return SpecializationFail(
|
|
"explicit-LOD gather requires mip filtering None or Point with zero LOD biases");
|
|
}
|
|
}
|
|
}
|
|
snapshot.user_data.assign(runtime.user_data.begin(), runtime.user_data.end());
|
|
return BuildResourceSpecialization(program, snapshot, specialization);
|
|
}
|
|
|
|
void ApplyResourceSpecialization(Program& program, const ResourceSpecialization& specialization) {
|
|
EXIT_IF(!program.resource_tracking_complete || program.shader_info_complete);
|
|
EXIT_IF(program.info.buffers.size() > specialization.buffers.size() ||
|
|
program.info.images.size() > specialization.images.size() ||
|
|
program.info.samplers.size() > specialization.samplers.size());
|
|
|
|
auto& buffers = program.info.buffers;
|
|
const auto original_buffer_count = buffers.size();
|
|
buffers.reserve(specialization.buffers.size());
|
|
for (uint32_t index = 0; index < specialization.buffers.size(); ++index) {
|
|
const auto& source = specialization.buffers[index];
|
|
if (index >= buffers.size()) {
|
|
EXIT_IF(source.indirect_root >= original_buffer_count);
|
|
buffers.push_back(buffers[source.indirect_root]);
|
|
}
|
|
auto& buffer = buffers[index];
|
|
buffer.packed_stride = source.packed_stride;
|
|
buffer.descriptor_format = source.descriptor_format;
|
|
buffer.descriptor_swizzle = source.descriptor_swizzle;
|
|
buffer.indirect_root = source.indirect_root;
|
|
buffer.indirect_mapping_offset = source.indirect_mapping_offset;
|
|
buffer.indirect_search_iterations = source.indirect_search_iterations;
|
|
buffer.indirect_resources.clear();
|
|
}
|
|
for (uint32_t index = 0; index < buffers.size(); ++index) {
|
|
const auto root = buffers[index].indirect_root;
|
|
if (root != BufferResource::NoIndirectBuffer) {
|
|
EXIT_IF(root >= buffers.size());
|
|
buffers[root].indirect_resources.push_back(index);
|
|
}
|
|
}
|
|
auto& images = program.info.images;
|
|
const auto original_image_count = images.size();
|
|
images.reserve(specialization.images.size());
|
|
for (uint32_t index = 0; index < specialization.images.size(); index++) {
|
|
const auto& source = specialization.images[index];
|
|
if (index >= images.size()) {
|
|
EXIT_IF(source.indirect_root >= original_image_count);
|
|
images.push_back(images[source.indirect_root]);
|
|
}
|
|
auto& image = images[index];
|
|
image.numeric_class = source.numeric_class;
|
|
image.dimension = source.dimension;
|
|
image.mip_count = source.mip_count;
|
|
image.conversion_format = source.conversion_format;
|
|
image.shader_swizzle = source.shader_swizzle;
|
|
image.indirect_root = source.indirect_root;
|
|
image.indirect_mapping_offset = source.indirect_mapping_offset;
|
|
image.indirect_search_iterations = source.indirect_search_iterations;
|
|
image.cube = source.cube;
|
|
image.indirect_resources.clear();
|
|
}
|
|
for (uint32_t index = 0; index < images.size(); index++) {
|
|
const auto root = images[index].indirect_root;
|
|
if (root != ImageResource::NoIndirectImage) {
|
|
EXIT_IF(root >= images.size());
|
|
images[root].indirect_resources.push_back(index);
|
|
}
|
|
}
|
|
|
|
auto& samplers = program.info.samplers;
|
|
auto& sampled_pairs = program.info.sampled_pairs;
|
|
const auto base_sampler_count = samplers.size();
|
|
samplers.reserve(specialization.samplers.size());
|
|
for (uint32_t index = 0; index < specialization.samplers.size(); ++index) {
|
|
const auto& source = specialization.samplers[index];
|
|
if (index >= samplers.size()) {
|
|
EXIT_IF(source.indirect_root >= base_sampler_count);
|
|
samplers.push_back(samplers[source.indirect_root]);
|
|
samplers.back().indirect_resources.clear();
|
|
}
|
|
samplers[index].indirect_root = source.indirect_root;
|
|
samplers[index].indirect_mapping_offset = source.indirect_mapping_offset;
|
|
samplers[index].indirect_search_iterations = source.indirect_search_iterations;
|
|
}
|
|
for (uint32_t index = 0; index < samplers.size(); ++index) {
|
|
const auto root = samplers[index].indirect_root;
|
|
if (root != SamplerResource::NoIndirectSampler) {
|
|
EXIT_IF(root >= samplers.size());
|
|
samplers[root].indirect_resources.push_back(index);
|
|
}
|
|
}
|
|
SamplerPlan sampler_plan;
|
|
EXIT_IF(!BuildSamplerPlan(program.info, sampler_plan));
|
|
const auto original_sampler_count = samplers.size();
|
|
samplers.reserve(sampler_plan.sampler_count);
|
|
for (uint32_t index = 0; index < sampler_plan.sampler_count; index++) {
|
|
const auto& binding = sampler_plan.bindings[index];
|
|
if (index >= original_sampler_count) {
|
|
samplers.push_back(samplers[binding.source]);
|
|
}
|
|
samplers[index].snapshot_index = binding.source;
|
|
samplers[index].force_point_filtering = binding.type == SamplerClass::Integer;
|
|
samplers[index].integer_border = binding.type != SamplerClass::Float;
|
|
}
|
|
for (uint32_t index = 0; index < sampler_plan.sampler_count; index++) {
|
|
const auto& binding = sampler_plan.bindings[index];
|
|
for (auto& candidate: samplers[index].indirect_resources) {
|
|
candidate = sampler_plan.mapping[candidate][static_cast<uint32_t>(binding.type)];
|
|
EXIT_IF(candidate == UINT32_MAX);
|
|
}
|
|
}
|
|
for (auto& pair: sampled_pairs) {
|
|
const auto type = static_cast<uint32_t>(ClassifySampler(images[pair.image]));
|
|
pair.sampler = sampler_plan.mapping[pair.sampler][type];
|
|
EXIT_IF(pair.sampler == UINT32_MAX);
|
|
samplers[pair.sampler].depth_compare |= images[pair.image].depth_compare;
|
|
for (const auto candidate: samplers[pair.sampler].indirect_resources)
|
|
samplers[candidate].depth_compare |= images[pair.image].depth_compare;
|
|
}
|
|
|
|
auto& memory_info = program.memory_info;
|
|
// Resolve empty scalar-table words before folding the indirect descriptors that use them.
|
|
for (auto* block: program.blocks) {
|
|
for (auto& inst: *block) {
|
|
if (inst.GetOpcode() != ValueOpcode::ReadConstBuffer) continue;
|
|
const auto& memory = memory_info[inst.Flags<MemoryFlags>().index];
|
|
if (memory.kind == ResourceKind::ScalarBuffer &&
|
|
specialization.buffers[memory.resource].scalar_empty &&
|
|
buffers[memory.resource].indirect_root == BufferResource::NoIndirectBuffer)
|
|
inst.ReplaceUsesWith(Value(0u));
|
|
}
|
|
}
|
|
const ImageRemap image_remap(specialization);
|
|
for (auto* block: program.blocks) {
|
|
for (auto it = block->begin(); it != block->end(); ++it) {
|
|
auto& inst = *it;
|
|
if (inst.GetOpcode() == ValueOpcode::GetImageResource) {
|
|
inst.SetFlags(image_remap[inst.Flags<uint32_t>()]);
|
|
continue;
|
|
}
|
|
if (BufferAccessOf(inst.GetOpcode()) == BufferAccess::Read) {
|
|
const auto& memory = memory_info[inst.Flags<MemoryFlags>().index];
|
|
const bool native_oob = memory.kind == ResourceKind::Buffer &&
|
|
specialization.buffers[memory.resource].zero_stride_oob &&
|
|
buffers[memory.resource].indirect_root == BufferResource::NoIndirectBuffer;
|
|
const bool null_indirect = memory.kind == ResourceKind::IndirectBuffer &&
|
|
NullBufferDescriptor(inst.Arg(0));
|
|
if (native_oob || null_indirect) {
|
|
// Bounds mode 0 checks offset >= stride, so zero stride
|
|
// makes every vector read out of bounds. An all-zero V# is unbound.
|
|
const auto count = BufferComponentCount(inst.GetOpcode());
|
|
std::array<Value, 4> values {Value(0u), Value(0u), Value(0u), Value(0u)};
|
|
if (native_oob && memory.formatted && !memory.typed) {
|
|
const auto& buffer = buffers[memory.resource];
|
|
const auto format = Format::GetFormatInfo(buffer.descriptor_format);
|
|
const bool d16 = memory.data_bits == 16u;
|
|
const auto components = d16 ? memory.component_count : count;
|
|
std::array<uint32_t, 4> constants {};
|
|
for (uint32_t component = 0; component < components; component++) {
|
|
if (format.type == Format::ComponentType::Unknown ||
|
|
GetDstSel(buffer.descriptor_swizzle, component) != 1u) continue;
|
|
auto one = Format::FormattedConstantBits(
|
|
format, Format::FormattedSourceKind::One);
|
|
if (d16 && one == 0x3f800000u) one = 0x3c00u;
|
|
constants[d16 ? component / 2u : component] |=
|
|
one << (d16 ? (component % 2u) * 16u : 0u);
|
|
}
|
|
for (uint32_t component = 0; component < count; component++) {
|
|
if (constants[component] == 0u) continue;
|
|
values[component] = Value(&*block->PrependNewInst(
|
|
it, ValueOpcode::SelectU32,
|
|
{inst.Arg(inst.NumArgs() - 1), Value(constants[component]), Value(0u)}));
|
|
}
|
|
}
|
|
Value result = values[0];
|
|
switch (inst.GetType()) {
|
|
case Type::U8: result = Value(uint8_t {0}); break;
|
|
case Type::U16: result = Value(uint16_t {0}); break;
|
|
case Type::U32x2:
|
|
result = Value(&*block->PrependNewInst(it, ValueOpcode::CompositeConstructU32x2,
|
|
{values[0], values[1]}));
|
|
break;
|
|
case Type::U32x3:
|
|
result = Value(&*block->PrependNewInst(it, ValueOpcode::CompositeConstructU32x3,
|
|
{values[0], values[1], values[2]}));
|
|
break;
|
|
case Type::U32x4:
|
|
result = Value(&*block->PrependNewInst(it, ValueOpcode::CompositeConstructU32x4,
|
|
{values[0], values[1], values[2], values[3]}));
|
|
break;
|
|
default: break;
|
|
}
|
|
inst.ReplaceUsesWith(result);
|
|
}
|
|
continue;
|
|
}
|
|
const auto image_opcode = ImageOpcodeInfoOf(inst.GetOpcode());
|
|
if (image_opcode.access == ImageAccess::None) {
|
|
continue;
|
|
}
|
|
const auto index = inst.Flags<MemoryFlags>().index;
|
|
EXIT_IF(index >= memory_info.size());
|
|
auto& memory = memory_info[index];
|
|
EXIT_IF(memory.resource >= images.size());
|
|
const auto& image = images[memory.resource];
|
|
if (specialization.images[memory.resource].fmask) {
|
|
EXIT_IF(inst.GetOpcode() != ValueOpcode::ImageRead || memory.data_bits != 32u);
|
|
// Vulkan MSAA stores each sample directly; FMASK's four-bit fragment indices
|
|
// therefore map each coverage sample to the same host sample.
|
|
constexpr uint32_t indices[] = {0x76543210u, 0xfedcba98u};
|
|
std::array<Value, 2> fragments;
|
|
for (uint32_t component = 0; component < fragments.size(); component++) {
|
|
const auto selected = block->PrependNewInst(
|
|
it, ValueOpcode::SelectU32, {inst.Arg(2), Value(indices[component]), Value(0u)});
|
|
fragments[component] = Value(&*selected);
|
|
}
|
|
const auto result = block->PrependNewInst(
|
|
it, ValueOpcode::CompositeConstructU32x4,
|
|
{fragments[0], fragments[1], Value(0u), Value(0u)});
|
|
inst.ReplaceUsesWith(Value(&*result));
|
|
continue;
|
|
}
|
|
if (image_opcode.needs_sampler &&
|
|
memory.sampler < original_sampler_count) {
|
|
const auto type = static_cast<uint32_t>(ClassifySampler(image));
|
|
memory.sampler = sampler_plan.mapping[memory.sampler][type];
|
|
EXIT_IF(memory.sampler == UINT32_MAX);
|
|
}
|
|
EXIT_IF(image.indirect_root == memory.resource &&
|
|
inst.GetOpcode() != ValueOpcode::ImageSampleRaw &&
|
|
inst.GetOpcode() != ValueOpcode::ImageGatherRaw &&
|
|
inst.GetOpcode() != ValueOpcode::ImageRead &&
|
|
inst.GetOpcode() != ValueOpcode::ImageWrite);
|
|
}
|
|
}
|
|
for (auto& memory: memory_info) {
|
|
if (memory.kind == ResourceKind::Image && !memory.planning_only) {
|
|
memory.resource = image_remap[memory.resource];
|
|
}
|
|
}
|
|
for (auto& buffer: buffers) {
|
|
if (buffer.image_alias != BufferResource::NoImageAlias) {
|
|
buffer.image_alias = image_remap[buffer.image_alias];
|
|
}
|
|
}
|
|
for (auto& pair: sampled_pairs) {
|
|
pair.image = image_remap[pair.image];
|
|
}
|
|
for (auto& image: images) {
|
|
if (image.indirect_root != ImageResource::NoIndirectImage) {
|
|
image.indirect_root = image_remap[image.indirect_root];
|
|
}
|
|
for (auto& resource: image.indirect_resources) {
|
|
resource = image_remap[resource];
|
|
}
|
|
}
|
|
image_remap.Apply(images);
|
|
}
|
|
|
|
} // namespace Libs::Graphics::ShaderRecompiler::IR
|