mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-19 18:36:53 +02:00
shader: share nested pointer-table image and sampler selection
Co-authored-by: fxpw <fxpw@outlook.com>
This commit is contained in:
@@ -826,8 +826,13 @@ std::array<ImageSampler, 2> LoadImageSamplers(ValueEmitContext& ctx, const IR::I
|
||||
const auto* handle = inst.Arg(1).ResolveInstruction();
|
||||
if (handle == nullptr || handle->GetOpcode() != IR::ValueOpcode::GetSamplerResource)
|
||||
ctx.Fail(inst, "has invalid finite sampler key provenance");
|
||||
auto selected = ctx.Def(handle->Arg(0));
|
||||
if (samplers[index].indirect_root != IR::SamplerResource::NoIndirectSampler)
|
||||
selected = EmitIndirectResourceIndex(state, selected,
|
||||
samplers[index].indirect_mapping_offset,
|
||||
samplers[index].indirect_search_iterations, 0u);
|
||||
array_index = EmitIndexSwitch(
|
||||
state, ctx.Def(handle->Arg(0)), static_cast<uint32_t>(candidates.size()),
|
||||
state, selected, static_cast<uint32_t>(candidates.size()),
|
||||
TypeU32(state), [&](uint32_t ordinal) { return ConstantU32(state, candidates[ordinal]); });
|
||||
}
|
||||
sampler = {LoadSamplerDescriptor(state, index, array_index), array_index != 0u};
|
||||
|
||||
@@ -181,10 +181,15 @@ RawSignedStorageFormat(const ImageResource& image, Prospero::BufferFormat format
|
||||
}
|
||||
|
||||
struct SamplerResource {
|
||||
static constexpr uint32_t NoIndirectSampler = UINT32_MAX;
|
||||
|
||||
uint32_t source = 0;
|
||||
uint32_t first_use_pc = 0;
|
||||
// Native filtering/border variants share the original sampler's runtime descriptor.
|
||||
uint32_t snapshot_index = 0;
|
||||
uint32_t indirect_root = NoIndirectSampler;
|
||||
uint32_t indirect_mapping_offset = 0;
|
||||
uint32_t indirect_search_iterations = 0;
|
||||
std::vector<uint32_t> indirect_resources;
|
||||
bool force_point_filtering = false;
|
||||
bool depth_compare = false;
|
||||
@@ -520,6 +525,12 @@ struct ShaderInfo {
|
||||
|
||||
struct DescriptorSource {
|
||||
struct IndirectDescriptor {
|
||||
struct PointerTable {
|
||||
uint32_t maximum = 0;
|
||||
uint32_t offset = 0;
|
||||
|
||||
bool operator==(const PointerTable&) const = default;
|
||||
};
|
||||
struct SelectorRead {
|
||||
struct CountedRows {
|
||||
uint32_t source = UINT32_MAX;
|
||||
@@ -542,6 +553,7 @@ struct DescriptorSource {
|
||||
bool operator==(const SelectorRead& other) const = default;
|
||||
};
|
||||
std::optional<SelectorRead> selector;
|
||||
std::optional<PointerTable> pointer_table;
|
||||
uint32_t table_source = 0;
|
||||
uint32_t table_offset = 0;
|
||||
uint32_t table_immediate = 0;
|
||||
|
||||
@@ -127,7 +127,7 @@ bool ValidatePackedUnormSampling(const ResourcePlan& program, const ResourceSnap
|
||||
for (const auto& pair: program.info.sampled_pairs) {
|
||||
if (pair.image != image_index) continue;
|
||||
if (pair.sampler >= snapshot.samplers.size() ||
|
||||
!program.info.samplers[pair.sampler].indirect_resources.empty()) {
|
||||
program.descriptor_sources[program.info.samplers[pair.sampler].source].indirect_descriptor) {
|
||||
return SpecializationFail("packed UNorm sampling requires a direct sampler");
|
||||
}
|
||||
ShaderSamplerResource sampler;
|
||||
@@ -367,11 +367,16 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
|
||||
(table.Stride() & 3u) != 0u || table.Stride() < indirect.table_record_bytes)))
|
||||
return false;
|
||||
const bool affine = indirect.table_stride != 0u;
|
||||
const uint64_t step = affine ? uint64_t {1} << std::countr_zero(indirect.table_stride) : 4u;
|
||||
const uint64_t first = ((indirect.table_offset % step) & ~uint64_t {3}) +
|
||||
const auto* pointer = indirect.pointer_table ? &*indirect.pointer_table : nullptr;
|
||||
const uint64_t step = pointer != nullptr ? indirect.table_stride
|
||||
: affine ? uint64_t {1} << std::countr_zero(indirect.table_stride) : 4u;
|
||||
const uint64_t first = ((pointer != nullptr ? indirect.table_offset
|
||||
: indirect.table_offset % step) & ~uint64_t {3}) +
|
||||
(indirect.table_immediate & ~3u);
|
||||
const uint64_t domain = pointer != nullptr ? uint64_t {pointer->maximum} + 1u
|
||||
: (uint64_t {1} << 32u) / step;
|
||||
const auto count = table_size >= first + 4u
|
||||
? std::min((table_size - first - 4u) / step + 1u, (uint64_t {1} << 32u) / step)
|
||||
? std::min((table_size - first - 4u) / step + 1u, domain)
|
||||
: 0u;
|
||||
if (affine && count > MaxIndirectDescriptorProbes) return false;
|
||||
// Affine SMEM offsets wrap in U32 before adding the aligned immediate.
|
||||
@@ -385,7 +390,13 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
|
||||
snapshot.flattened_srt.resize(mapping_offset + 2u, 0u);
|
||||
for (uint32_t word = 0; word < count; ++word) {
|
||||
DescriptorValue candidate {.dword_count = dword_count};
|
||||
if (affine) {
|
||||
if (pointer != nullptr) {
|
||||
std::array<uint32_t, 2> address {};
|
||||
if (!ReadScalarTable(table_base, table_size, first + word * step, runtime, address) ||
|
||||
!ReadScalarTable((uint64_t {address[1]} << 32u) | address[0], UINT64_MAX,
|
||||
pointer->offset, runtime,
|
||||
std::span(candidate.dwords).first(dword_count))) return false;
|
||||
} else if (affine) {
|
||||
if (!ReadScalarTable(table_base, table_size, first + word * step, runtime,
|
||||
std::span(candidate.dwords).first(dword_count))) return false;
|
||||
} else {
|
||||
@@ -1437,18 +1448,28 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
|
||||
}
|
||||
}
|
||||
snapshot.samplers.resize(program.info.samplers.size());
|
||||
for (uint32_t i = 0; i < program.info.samplers.size(); ++i) {
|
||||
auto source = program.info.samplers[i].source;
|
||||
if (source >= program.descriptor_sources.size()) return false;
|
||||
const auto& descriptor = program.descriptor_sources[source];
|
||||
if (descriptor.indirect_descriptor) {
|
||||
if (descriptor.indirect_descriptor->sources.empty()) return false;
|
||||
source = descriptor.indirect_descriptor->sources[0];
|
||||
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, *indirect, i, 4u, observed, clean, snapshot, snapshot.samplers,
|
||||
specialization.samplers, ShaderInfo::MaxSamplers,
|
||||
[](DescriptorValue&) { return true; })) return false;
|
||||
} else {
|
||||
if (indirect != nullptr) source = indirect->sources[0];
|
||||
if (!evaluate(source, snapshot.samplers[i])) return false;
|
||||
}
|
||||
}
|
||||
if (!evaluate(source, snapshot.samplers[i])) {
|
||||
return false;
|
||||
}
|
||||
if (program.info.samplers[i].gather_lod) {
|
||||
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
|
||||
@@ -1466,7 +1487,8 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
|
||||
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.images.size() > specialization.images.size() ||
|
||||
program.info.samplers.size() > specialization.samplers.size());
|
||||
|
||||
auto& buffers = program.info.buffers;
|
||||
const auto original_buffer_count = buffers.size();
|
||||
@@ -1522,10 +1544,30 @@ void ApplyResourceSpecialization(Program& program, const ResourceSpecialization&
|
||||
}
|
||||
}
|
||||
|
||||
SamplerPlan sampler_plan;
|
||||
EXIT_IF(!BuildSamplerPlan(program.info, sampler_plan));
|
||||
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++) {
|
||||
|
||||
@@ -34,8 +34,16 @@ struct ResourceSpecialization {
|
||||
bool operator==(const Image&) const = default;
|
||||
};
|
||||
|
||||
struct Sampler {
|
||||
uint32_t indirect_root = SamplerResource::NoIndirectSampler;
|
||||
uint32_t indirect_mapping_offset = 0;
|
||||
uint32_t indirect_search_iterations = 0;
|
||||
bool operator==(const Sampler&) const = default;
|
||||
};
|
||||
|
||||
std::vector<Buffer> buffers;
|
||||
std::vector<Image> images;
|
||||
std::vector<Sampler> samplers;
|
||||
|
||||
bool operator==(const ResourceSpecialization&) const = default;
|
||||
};
|
||||
|
||||
@@ -45,10 +45,13 @@ uint32_t PossibleU32Bits(Value value, const auto& normalize) {
|
||||
if (inst->NumArgs() != 0u) bits = merged;
|
||||
break;
|
||||
}
|
||||
case ValueOpcode::ShiftLeftLogical32: {
|
||||
case ValueOpcode::ShiftLeftLogical32:
|
||||
case ValueOpcode::ShiftRightLogical32: {
|
||||
const auto shift = inst->Arg(1).Resolve();
|
||||
if (shift.IsImmediate() && shift.GetType() == Type::U32)
|
||||
bits = arg(0) << (shift.U32() & 31u);
|
||||
if (shift.IsImmediate() && shift.GetType() == Type::U32) {
|
||||
bits = inst->GetOpcode() == ValueOpcode::ShiftLeftLogical32
|
||||
? arg(0) << (shift.U32() & 31u) : arg(0) >> (shift.U32() & 31u);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: break;
|
||||
@@ -397,6 +400,13 @@ public:
|
||||
std::vector<std::pair<const IndirectDescriptorPlan*, Value>> buffer_selections;
|
||||
for (const auto& plan: m_indirect_descriptors) {
|
||||
auto key = plan.key;
|
||||
if (m_sources[plan.source].indirect_descriptor->pointer_table) {
|
||||
auto* block = plan.handle->Parent();
|
||||
const auto where = std::ranges::find_if(block->Instructions(),
|
||||
[&](const Inst& inst) { return &inst == plan.handle; });
|
||||
key = Value(&*block->PrependNewInst(
|
||||
where, ValueOpcode::ShiftLeftLogical32, {key, Value(2u)}));
|
||||
}
|
||||
if (plan.table_read != nullptr) {
|
||||
const auto* anchor = plan.table_indexed ? plan.handle : plan.table_read;
|
||||
auto* block = anchor->Parent();
|
||||
@@ -964,7 +974,8 @@ private:
|
||||
if (current.indirect_descriptor.has_value()) {
|
||||
const auto& a = *current.indirect_descriptor;
|
||||
const auto& b = *descriptor.indirect_descriptor;
|
||||
if (a.selector != b.selector || a.table_source != b.table_source ||
|
||||
if (a.selector != b.selector || a.pointer_table != b.pointer_table ||
|
||||
a.table_source != b.table_source ||
|
||||
a.table_offset != b.table_offset || a.table_immediate != b.table_immediate ||
|
||||
a.table_stride != b.table_stride || a.indexed != b.indexed ||
|
||||
a.workgroup_axis != b.workgroup_axis || a.sources != b.sources ||
|
||||
@@ -2713,6 +2724,74 @@ private:
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TryMakeNestedDescriptor(Inst& handle, const DescriptorSource& descriptor,
|
||||
IndirectDescriptorPlan& plan) {
|
||||
if (handle.GetOpcode() != ValueOpcode::GetImageResource &&
|
||||
handle.GetOpcode() != ValueOpcode::GetSamplerResource) return false;
|
||||
const Inst* pointer = nullptr;
|
||||
uint32_t descriptor_offset = 0;
|
||||
for (uint32_t word = 0; word < handle.NumArgs(); ++word) {
|
||||
const auto* read = descriptor.dwords[word].Resolve().TryInstruction();
|
||||
uint32_t index, offset;
|
||||
const auto* memory = read != nullptr ? ScalarReadMemory(*read, index) : nullptr;
|
||||
if (memory == nullptr || memory->kind != ResourceKind::ScalarAddress ||
|
||||
memory->offset > INT32_MAX || !ImmediateU32(read->Arg(1), offset) ||
|
||||
!AvailableBefore(*read, handle)) return false;
|
||||
const auto* address = read->Arg(0).Resolve().TryInstruction();
|
||||
const uint64_t current_offset = uint64_t {offset & ~3u} + (memory->offset & ~3u);
|
||||
if (address == nullptr || address->GetOpcode() != ValueOpcode::GetAddressResource ||
|
||||
address->NumArgs() != 2u || current_offset > UINT32_MAX ||
|
||||
(pointer != nullptr && (!EquivalentValue(m_program, Value(pointer), Value(address)) ||
|
||||
current_offset != uint64_t {descriptor_offset} + word * 4u)))
|
||||
return false;
|
||||
if (word == 0u) descriptor_offset = static_cast<uint32_t>(current_offset);
|
||||
pointer = address;
|
||||
}
|
||||
// An out-of-bounds pointer pair is zero. Guest mappings exclude the first
|
||||
// 16 KiB page, so these pointed reads have the existing null-image semantics.
|
||||
if (pointer == nullptr || uint64_t {descriptor_offset} + handle.NumArgs() * 4u > 0x4000u)
|
||||
return false;
|
||||
const auto* low = pointer->Arg(0).Resolve().TryInstruction();
|
||||
const auto* high = pointer->Arg(1).Resolve().TryInstruction();
|
||||
uint32_t low_index, high_index;
|
||||
const auto* low_memory = low != nullptr ? ScalarReadMemory(*low, low_index) : nullptr;
|
||||
const auto* high_memory = high != nullptr ? ScalarReadMemory(*high, high_index) : nullptr;
|
||||
if (low_memory == nullptr || high_memory == nullptr ||
|
||||
low_memory->kind != ResourceKind::ScalarBuffer ||
|
||||
high_memory->kind != ResourceKind::ScalarBuffer ||
|
||||
low_memory->offset > INT32_MAX || high_memory->offset > INT32_MAX ||
|
||||
(low_memory->offset & ~3u) + 4u != (high_memory->offset & ~3u) ||
|
||||
!EquivalentValue(m_program, low->Arg(0), high->Arg(0)) ||
|
||||
!EquivalentValue(m_program, low->Arg(1), high->Arg(1)) ||
|
||||
!AvailableBefore(*low, *pointer) || !AvailableBefore(*high, *pointer)) return false;
|
||||
Value key;
|
||||
DescriptorSource::IndirectDescriptor indirect;
|
||||
if (!MatchTableOffset(low->Arg(1), key, indirect.table_offset, indirect.table_stride) ||
|
||||
indirect.table_stride < 8u || (indirect.table_stride & 3u) != 0u) return false;
|
||||
const auto maximum = SelectorMaximum(key);
|
||||
if (maximum > UINT32_MAX / 4u ||
|
||||
uint64_t {maximum} * indirect.table_stride + indirect.table_offset > UINT32_MAX ||
|
||||
(key.TryInstruction() != nullptr && !AvailableBefore(*key.Instruction(), handle)))
|
||||
return false;
|
||||
DescriptorSource table_source;
|
||||
if (!MakeRuntimeTableSource(*low, table_source) || table_source.dword_count != 4u)
|
||||
return false;
|
||||
indirect.table_source = InternSource(table_source);
|
||||
indirect.table_immediate = low_memory->offset & ~3u;
|
||||
indirect.table_scalar = true;
|
||||
indirect.pointer_table = {{maximum, descriptor_offset}};
|
||||
DescriptorSource source;
|
||||
source.dword_count = handle.NumArgs();
|
||||
source.dwords.fill(Value(0u));
|
||||
std::copy_n(table_source.dwords.begin(), 4u, source.dwords.begin());
|
||||
source.indirect_descriptor = indirect;
|
||||
plan.handle = &handle;
|
||||
plan.source = InternSource(source);
|
||||
plan.key = key;
|
||||
plan.roots = source.dwords;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TryMakeIndirectImage(Inst& handle, const DescriptorSource& descriptor,
|
||||
IndirectDescriptorPlan& plan) {
|
||||
if (handle.GetOpcode() != ValueOpcode::GetImageResource || handle.NumArgs() != 8u) return false;
|
||||
@@ -3019,6 +3098,9 @@ private:
|
||||
return SelectorMaximum(inst->Arg(arg), bounds, depth + 1u, guard);
|
||||
};
|
||||
switch (inst->GetOpcode()) {
|
||||
case ValueOpcode::ReadLane:
|
||||
// ReadLane ignores EXEC; only context-free bounds on every source lane apply.
|
||||
return PossibleU32Bits(inst->Arg(0));
|
||||
case ValueOpcode::Phi: {
|
||||
const auto initial = InitialCandidateMask(value);
|
||||
if (!initial.IsEmpty()) return SelectorMaximum(initial, bounds, depth + 1u, guard);
|
||||
@@ -3240,10 +3322,11 @@ private:
|
||||
uint32_t bad_dword = 0;
|
||||
if (ValidateSource(descriptor, bad_dword)) return;
|
||||
IndirectDescriptorPlan plan;
|
||||
if (sampler ? TryMakeFiniteDescriptor(*handle, plan)
|
||||
if (TryMakeNestedDescriptor(*handle, descriptor, plan) ||
|
||||
(sampler ? TryMakeFiniteDescriptor(*handle, plan)
|
||||
: (TryMakeIndirectImage(*handle, descriptor, plan) ||
|
||||
TryMakeFiniteDescriptor(*handle, plan) ||
|
||||
TryMakeIndirectBuffer(*handle, descriptor, plan))) {
|
||||
TryMakeIndirectBuffer(*handle, descriptor, plan)))) {
|
||||
for (const auto& previous: m_indirect_descriptors) {
|
||||
if (plan.reads[0] != nullptr && previous.reads[0] == plan.reads[0] &&
|
||||
!EquivalentValue(m_program, previous.key, plan.key))
|
||||
@@ -3389,7 +3472,7 @@ private:
|
||||
const auto* descriptor = Source(source);
|
||||
if (descriptor != nullptr && descriptor->indirect_descriptor) {
|
||||
const auto& sources = descriptor->indirect_descriptor->sources;
|
||||
if (sources.empty()) Fail(pc, "finite sampler has no candidates");
|
||||
if (sources.empty()) return root;
|
||||
std::vector<uint32_t> resources {root};
|
||||
for (size_t i = 1; i < sources.size(); ++i) {
|
||||
const auto candidate = AddSampler(sources[i], pc);
|
||||
|
||||
@@ -561,6 +561,201 @@ void TestWaterfallImageTable() {
|
||||
"waterfall without all descriptor equality witnesses was accepted");
|
||||
}
|
||||
|
||||
void TestNestedPointerImageTable() {
|
||||
namespace CFG = Libs::Graphics::ShaderRecompiler::CFG;
|
||||
const auto make = [](uint32_t variant = 0u) {
|
||||
auto fixture = std::make_unique<Fixture>();
|
||||
std::array<Value, 4> table_words, key_words;
|
||||
for (uint32_t word = 0; word < 4u; ++word) {
|
||||
table_words[word] = fixture->UserData(word);
|
||||
key_words[word] = fixture->UserData(word + 4u);
|
||||
}
|
||||
const auto table = fixture->Buffer(table_words);
|
||||
const auto keys = fixture->Buffer(key_words);
|
||||
Inst* carried = nullptr;
|
||||
auto* entry = fixture->block;
|
||||
if (variant == 6u) {
|
||||
auto* loop = fixture->AddBlock();
|
||||
auto* exit = fixture->AddBlock();
|
||||
entry->AddBranch(loop);
|
||||
entry->terminator = {.kind = CFG::TerminatorKind::Branch,
|
||||
.true_block = loop};
|
||||
loop->AddBranch(loop);
|
||||
loop->AddBranch(exit);
|
||||
loop->terminator = {.kind = CFG::TerminatorKind::ConditionalBranch,
|
||||
.true_block = loop, .false_block = exit};
|
||||
exit->terminator.kind = CFG::TerminatorKind::Return;
|
||||
fixture->block = loop;
|
||||
carried = &loop->AppendNewInst(ValueOpcode::Phi, {}, uint64_t(Type::U32));
|
||||
}
|
||||
const auto active = fixture->Emit(ValueOpcode::INotEqual32,
|
||||
{fixture->Emit(ValueOpcode::LaneId), Value(0u)});
|
||||
MemoryInfo key_memory;
|
||||
key_memory.kind = ResourceKind::Buffer;
|
||||
key_memory.offen = true;
|
||||
auto key = fixture->Emit(ValueOpcode::LoadBufferU32,
|
||||
{keys, Value(0u), fixture->Emit(ValueOpcode::LaneId), Value(0u), Value(true)},
|
||||
fixture->AddMemory(key_memory, 0x100u));
|
||||
if (carried != nullptr) {
|
||||
carried->AddPhiOperand(entry, Value(0u));
|
||||
carried->AddPhiOperand(fixture->block,
|
||||
fixture->Emit(ValueOpcode::SelectU32, {active, key, Value(carried)}));
|
||||
fixture->block->condition = active;
|
||||
key = Value(carried);
|
||||
}
|
||||
const auto unbounded_key = key;
|
||||
if (variant != 1u)
|
||||
key = fixture->Emit(ValueOpcode::ShiftRightLogical32, {key, Value(variant == 4u ? 15u : 16u)});
|
||||
if (variant == 5u || variant == 6u)
|
||||
key = fixture->Emit(ValueOpcode::SelectU32,
|
||||
{active, key, variant == 5u ? unbounded_key : Value(0u)});
|
||||
key = fixture->Emit(ValueOpcode::ReadLane, {key, Value(0u)});
|
||||
const auto offset = fixture->Emit(ValueOpcode::IMul32, {key, Value(592u)});
|
||||
std::array<Value, 2> pointer;
|
||||
for (uint32_t word = 0; word < 2u; ++word) {
|
||||
MemoryInfo memory;
|
||||
memory.kind = ResourceKind::ScalarBuffer;
|
||||
memory.offset = word * (variant == 2u ? 8u : 4u);
|
||||
pointer[word] = fixture->Emit(ValueOpcode::ReadConstBuffer, {table, offset},
|
||||
fixture->AddMemory(memory, 0x1f4u));
|
||||
}
|
||||
const auto address = fixture->Address(pointer[0], pointer[1]);
|
||||
const auto load = [&](uint32_t first, uint32_t width) {
|
||||
if (width == 8u && variant >= 7u && variant <= 9u)
|
||||
first = variant == 7u ? 0x4000u : variant == 8u ? 0x3ff0u : 0x3fe0u;
|
||||
if (width == 4u && variant >= 9u) first = variant == 9u ? 0x3ff0u : 0x4000u;
|
||||
std::array<Value, 8> words;
|
||||
for (uint32_t word = 0; word < width; ++word) {
|
||||
MemoryInfo memory;
|
||||
memory.kind = ResourceKind::ScalarAddress;
|
||||
memory.offset = first + word * 4u;
|
||||
if (variant == 3u && width == 8u && word == 7u) memory.offset += 4u;
|
||||
words[word] = fixture->Emit(ValueOpcode::LoadAddressU32,
|
||||
{address, Value(0u), Value(0u), Value(true)},
|
||||
fixture->AddMemory(memory, first == 0x3d0u ? 0x1110u : 0x1118u));
|
||||
}
|
||||
return words;
|
||||
};
|
||||
const auto image = fixture->Image(load(0x3d0u, 8u));
|
||||
const auto sampler_words = load(0x3f8u, 4u);
|
||||
const auto sampler = fixture->Sampler(
|
||||
{sampler_words[0], sampler_words[1], sampler_words[2], sampler_words[3]});
|
||||
MemoryInfo memory;
|
||||
memory.kind = ResourceKind::Image;
|
||||
memory.image_dimension = Decoder::ImageDimension::Dim2D;
|
||||
fixture->Emit(ValueOpcode::ImageSampleRaw,
|
||||
{image, sampler, fixture->ImageAddress(), variant == 5u ? active : Value(true)},
|
||||
fixture->AddMemory(memory, 0x1128u));
|
||||
return fixture;
|
||||
};
|
||||
auto fixture = make();
|
||||
fixture->PlanAndTrack();
|
||||
const auto plan = ExtractResourcePlan(fixture->program);
|
||||
Check(plan.info.images.size() == 1u && plan.info.samplers.size() == 1u,
|
||||
"nested pointer descriptors were expanded before their runtime table was available");
|
||||
for (const auto source : {plan.info.images[0].source, plan.info.samplers[0].source}) {
|
||||
const auto& descriptor = plan.descriptor_sources[source];
|
||||
const auto& indirect = descriptor.indirect_descriptor;
|
||||
Check(indirect && !indirect->selector && indirect->pointer_table &&
|
||||
indirect->pointer_table->maximum == 65535u && indirect->table_stride == 592u &&
|
||||
indirect->pointer_table->offset == (descriptor.dword_count == 8u ? 0x3d0u : 0x3f8u),
|
||||
"nested pointer descriptors lost their row geometry or captured the GPU selector payload");
|
||||
}
|
||||
|
||||
constexpr uint32_t table = 0x2000u, objects = 0x4000u, stride = 592u, count = 6u;
|
||||
LinearTestMemory memory;
|
||||
memory.words.resize((objects + count * 0x430u - memory.base) / 4u);
|
||||
memory.fail_address = 0x1800u; // The GPU selector's contents are unavailable to the CPU.
|
||||
const auto at = [&](uint32_t address) { return memory.words.begin() + (address - memory.base) / 4u; };
|
||||
for (uint32_t row = 0; row < count; ++row) {
|
||||
const auto object = objects + row * 0x430u;
|
||||
*at(table + row * stride) = object;
|
||||
const std::array<uint32_t, 8> image{10u + row,
|
||||
uint32_t(Libs::Graphics::Prospero::BufferFormat::k32Float) << 20u, 0u,
|
||||
Libs::Graphics::DstSel(4, 0, 0, 1) | (9u << 28u), 0u, 0u, 0u, 0u};
|
||||
std::copy(image.begin(), image.end(), at(object + 0x3d0u));
|
||||
const std::array<uint32_t, 4> sampler{row, 0x00fff000u, 0u, 0u};
|
||||
std::copy(sampler.begin(), sampler.end(), at(object + 0x3f8u));
|
||||
}
|
||||
std::array<uint32_t, 8> user_data{table, stride << 16u, count, 0x5204u,
|
||||
0x1800u, 0u, 4u, 0x30005204u};
|
||||
const SrtRuntime runtime{.user_data = user_data, .userdata = &memory,
|
||||
.read_specialization_memory = ReadLinearTestMemory};
|
||||
ResourceSnapshot snapshot;
|
||||
ResourceSpecialization specialization;
|
||||
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
|
||||
snapshot.images.size() == count + 1u && snapshot.samplers.size() == count + 1u,
|
||||
"bounded nested pointer table lost image/sampler candidates or read the GPU selector");
|
||||
for (uint32_t row = 0; row < count; ++row)
|
||||
Check(snapshot.images[row + 1u].dwords[0] == 10u + row &&
|
||||
snapshot.samplers[row + 1u].dwords[0] == row,
|
||||
"nested pointer table mixed rows or pointee members");
|
||||
const auto populated = specialization;
|
||||
const auto reads = memory.reads;
|
||||
user_data[2] = 0u;
|
||||
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
|
||||
snapshot.images.size() == 1u && snapshot.samplers.size() == 1u &&
|
||||
memory.reads == reads && specialization != populated,
|
||||
"empty nested pointer table retained candidates or read an unavailable row");
|
||||
user_data[2] = count;
|
||||
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
|
||||
specialization == populated,
|
||||
"empty-to-positive pointer table refresh did not restore its specialization");
|
||||
*at(table) = objects + 0x430u;
|
||||
*at(objects + 0x430u + 0x3d0u) = 42u;
|
||||
*at(objects + 0x430u + 0x3f8u) = 7u;
|
||||
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
|
||||
snapshot.images.size() == count && snapshot.samplers.size() == count &&
|
||||
snapshot.images[1].dwords[0] == 42u && snapshot.samplers[1].dwords[0] == 7u,
|
||||
"nested descriptor refresh retained stale pointers or failed to deduplicate new pointees");
|
||||
*at(table) = objects;
|
||||
|
||||
// A final low DWORD is in bounds; the high pointer DWORD is independently zero-filled.
|
||||
user_data[1] = 1u << 16u;
|
||||
user_data[2] = count * stride + 4u;
|
||||
memory.watched_address = table + count * stride;
|
||||
*at(static_cast<uint32_t>(memory.watched_address)) = objects;
|
||||
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
|
||||
memory.watched_reads != 0u && memory.watched_dwords == 1u &&
|
||||
snapshot.images.size() == count + 1u && snapshot.samplers.size() == count + 1u,
|
||||
"partial pointer row was skipped or read beyond its scalar buffer bounds");
|
||||
const auto mapping = specialization.images[0].indirect_mapping_offset;
|
||||
Check(snapshot.flattened_srt[mapping] == count + 1u &&
|
||||
snapshot.flattened_srt[mapping + count + 1u] == 1u,
|
||||
"partial pointer row lost its low-word pointee or dense key mapping");
|
||||
user_data[1] = stride << 16u;
|
||||
user_data[2] = count;
|
||||
memory.fail_address = objects + 0x3d0u;
|
||||
Check(!MaterializeResources(plan, runtime, snapshot, specialization),
|
||||
"failed nested image pointee read was treated as a null descriptor");
|
||||
memory.fail_address = 0x1800u;
|
||||
*at(table) = 0u;
|
||||
Check(!MaterializeResources(plan, runtime, snapshot, specialization),
|
||||
"unreadable null pointer row bypassed the strict pointee reader");
|
||||
*at(table) = objects;
|
||||
|
||||
auto capped = make(4u);
|
||||
capped->PlanAndTrack();
|
||||
const auto capped_plan = ExtractResourcePlan(capped->program);
|
||||
user_data[2] = 65537u;
|
||||
const auto before_cap = memory.reads;
|
||||
Check(!MaterializeResources(capped_plan, runtime, snapshot, specialization) &&
|
||||
memory.reads == before_cap,
|
||||
"oversized nested pointer table performed reads before enforcing its candidate cap");
|
||||
user_data[2] = count;
|
||||
auto cyclic = make(6u);
|
||||
cyclic->PlanAndTrack();
|
||||
Check(MaterializeResources(ExtractResourcePlan(cyclic->program), runtime, snapshot, specialization),
|
||||
"context-free shift bound rejected a cyclic source or a bounded Select arm");
|
||||
auto boundary = make(9u);
|
||||
boundary->PlanAndTrack(); // Both descriptor spans end exactly at the unmappable first-page boundary.
|
||||
for (const uint32_t variant : {1u, 2u, 3u, 5u, 7u, 8u, 10u}) {
|
||||
auto invalid = make(variant);
|
||||
CheckFatal([&] { invalid->PlanAndTrack(); }, "not a valid runtime value",
|
||||
"nested pointer planning accepted wrapping keys, mismatched words, lane-local bounds or mappable null-pointee bytes");
|
||||
}
|
||||
}
|
||||
|
||||
void TestGuardedDirectImageTable() {
|
||||
namespace CFG = Libs::Graphics::ShaderRecompiler::CFG;
|
||||
enum class Guard {
|
||||
@@ -6248,6 +6443,7 @@ int main() {
|
||||
Run("unbounded material image keys", [] { TestInvariantIndirectImageMaterialization(false); });
|
||||
Run("bounded image view eligibility", TestBoundedImageViewEligibility);
|
||||
Run("waterfall image table", TestWaterfallImageTable);
|
||||
Run("nested pointer image table", TestNestedPointerImageTable);
|
||||
Run("guarded direct image table", TestGuardedDirectImageTable);
|
||||
Run("bounded compute image loop", TestBoundedComputeImageLoop);
|
||||
Run("uniformized material image keys", TestUniformizedMaterialImageKeys);
|
||||
|
||||
@@ -37549,6 +37549,115 @@ void CheckPointerImageTable(VulkanHarness &vulkan) {
|
||||
}
|
||||
}
|
||||
|
||||
void CheckNestedPointerImageTable(VulkanHarness &vulkan) {
|
||||
using O = ShaderOpcode;
|
||||
constexpr u32 keys_address = 128, table = 256, stride = 592, objects = 4096;
|
||||
constexpr std::array keys{5u, 0u, 3u, 1u, 4u, 2u};
|
||||
for (const u32 wave : {32u, 64u}) {
|
||||
TestCase test;
|
||||
test.name = "NestedPointerImageSamplerTable";
|
||||
test.initial.resize((objects + keys.size() * 0x430u) / 4u, 0);
|
||||
std::fill_n(test.initial.begin(), keys.size(), 0xdeadbeefu);
|
||||
std::fill_n(test.initial.begin() + keys_address / 4u, keys.size(), UINT32_MAX);
|
||||
for (u32 row = 0; row < keys.size(); ++row) {
|
||||
const u32 object = objects + row * 0x430u;
|
||||
test.initial[(table + row * stride) / 4u] = object;
|
||||
auto image = MakeSampledTextureData(Prospero::BufferFormat::k32Float);
|
||||
image[0] = 10u + row;
|
||||
image[1] |= 1u << 30u; // Two texels: zero, then the row's value.
|
||||
image[3] = DstSel(4, 0, 0, 1) |
|
||||
(static_cast<u32>(Prospero::ImageType::kColor2D) << 28u);
|
||||
std::copy_n(image.begin(), 8, test.initial.begin() + (object + 0x3d0u) / 4u);
|
||||
const std::array sampler{0u, 0x00fff000u, (row & 1u) ? 0x0a500000u : 0u, 0u};
|
||||
std::copy(sampler.begin(), sampler.end(),
|
||||
test.initial.begin() + (object + 0x3f8u) / 4u);
|
||||
}
|
||||
auto records = MakeStructuredStorageBufferData(stride, keys.size());
|
||||
records[0] = table;
|
||||
std::copy_n(records.begin(), 4, test.user_data.begin() + 32);
|
||||
auto key_buffer = MakeStructuredStorageBufferData(0, keys.size() * 4u);
|
||||
key_buffer[0] = keys_address;
|
||||
std::copy_n(key_buffer.begin(), 4, test.user_data.begin() + 4);
|
||||
const auto output = MakeStructuredStorageBufferData(0, keys.size() * 4u);
|
||||
std::copy_n(output.begin(), 4, test.user_data.begin() + 48);
|
||||
test.bda_mappings = {{0u, 0u}};
|
||||
test.has_user_data = test.has_compute_info = true;
|
||||
test.compute_info.wave_size = wave;
|
||||
test.compute_info.threads_num[0] = test.compute_info.threads_num[1] =
|
||||
test.compute_info.threads_num[2] = 1;
|
||||
test.compute_info.group_id[0] = true;
|
||||
test.compute_info.workgroup_register = 44;
|
||||
test.dispatch_x = keys.size();
|
||||
auto &code = test.code;
|
||||
code.push_back(EncodeSop2(0x1e, 23, 44, InlineU32(2)));
|
||||
code.push_back(EncodeVop1(0x01, 30, 23));
|
||||
code.insert(code.end(), {EncodeMubuf0(0x0c), EncodeMubuf1(4, 1, 30),
|
||||
EncodeVop2(0x16, 6, InlineU32(16), 4)});
|
||||
AppendVop3(&code, 0x360, 22, Vgpr(6), InlineU32(0));
|
||||
code.insert(code.end(), {EncodeSop2(0x26, 23, 22, 255), stride,
|
||||
EncodeSmem0(0x09, 20, 16), EncodeSmem1(0, 23),
|
||||
EncodeSmem0(0x03, 8, 10), EncodeSmem1(0x3d0, 125),
|
||||
EncodeSmem0(0x02, 16, 10), EncodeSmem1(0x3f8, 125)});
|
||||
AppendVMovLiteral(&code, 20, std::bit_cast<u32>(0.5f));
|
||||
AppendVMovLiteral(&code, 21, std::bit_cast<u32>(0.5f));
|
||||
code.insert(code.end(), {EncodeMimg0(0x27, 1), EncodeMimg1(5, 20, 2, 4)});
|
||||
AppendBufferStoreDword(&code, 5, 30);
|
||||
AppendEnd(&code);
|
||||
test.opcodes = {O::S_LSHL_B32, O::V_MOV_B32, O::BUFFER_LOAD_DWORD,
|
||||
O::V_LSHRREV_B32, O::V_READLANE_B32, O::S_MUL_I32,
|
||||
O::S_BUFFER_LOAD_DWORDX2, O::S_LOAD_DWORDX8, O::S_LOAD_DWORDX4,
|
||||
O::IMAGE_SAMPLE, O::BUFFER_STORE_DWORD, O::S_ENDPGM};
|
||||
const auto compiled = CompileCase(test, vulkan.SubgroupSize());
|
||||
for (const auto &value : compiled.resources.buffers) {
|
||||
ShaderBufferResource descriptor{};
|
||||
std::copy_n(value.dwords.begin(), 4, descriptor.fields);
|
||||
test.storage_buffer_ranges.emplace_back(descriptor.Base48(),
|
||||
std::max(4u, u32(descriptor.GetSize())));
|
||||
}
|
||||
std::vector<VulkanHarness::Image> images;
|
||||
for (const auto &descriptor : compiled.resources.images)
|
||||
images.push_back(vulkan.CreateImage2D(test.name, 2, 1, vk::Format::eR32Sfloat,
|
||||
vk::ImageUsageFlagBits::eSampled,
|
||||
{0u, std::bit_cast<u32>(float(descriptor.dwords[0]))}, 1,
|
||||
vk::ImageLayout::eShaderReadOnlyOptimal));
|
||||
std::vector<vk::Sampler> samplers;
|
||||
for (const auto &resource : compiled.program.info.samplers) {
|
||||
ShaderSamplerResource descriptor{};
|
||||
std::copy_n(compiled.resources.samplers[resource.snapshot_index].dwords.begin(),
|
||||
4, descriptor.fields);
|
||||
samplers.push_back(vulkan.CreateSampler(test.name, 1,
|
||||
descriptor.XyMagFilter() == 1 ? vk::Filter::eLinear : vk::Filter::eNearest));
|
||||
}
|
||||
TestCase producer;
|
||||
producer.name = "NestedPointerSelectorProducer";
|
||||
producer.initial = test.initial;
|
||||
producer.has_compute_info = true;
|
||||
producer.compute_info.threads_num[0] = producer.compute_info.threads_num[1] =
|
||||
producer.compute_info.threads_num[2] = 1;
|
||||
producer.compute_info.wave_size = 32;
|
||||
for (u32 group = 0; group < keys.size(); ++group) {
|
||||
AppendVMovLiteral(&producer.code, 1, (keys[group] << 16u) | 0xbeefu);
|
||||
AppendVMovLiteral(&producer.code, 2, keys_address + group * 4u);
|
||||
AppendBufferStoreDword(&producer.code, 1, 2);
|
||||
}
|
||||
AppendEnd(&producer.code);
|
||||
const auto producer_shader = CompileCase(producer, vulkan.SubgroupSize());
|
||||
auto buffer = vulkan.CreateStorageBuffer(test.name, test.initial, test.initial.size(), true);
|
||||
vulkan.Dispatch(producer, producer_shader, buffer);
|
||||
vulkan.Dispatch(test, compiled, buffer, nullptr, nullptr, nullptr, nullptr,
|
||||
nullptr, images, samplers);
|
||||
std::vector<u32> expected;
|
||||
for (const auto key : keys)
|
||||
expected.push_back(std::bit_cast<u32>(float(10u + key) * ((key & 1u) ? 0.5f : 1.0f)));
|
||||
CompareWords(test, "GPU-selected nested image and sampler", expected,
|
||||
vulkan.ReadBuffer(test.name, buffer, expected.size()));
|
||||
vulkan.DestroyBuffer(&buffer);
|
||||
for (const auto sampler : samplers) vulkan.Device().destroySampler(sampler);
|
||||
for (auto &image : images) vulkan.DestroyImage(&image);
|
||||
std::printf("[compute] %s wave=%u ok\n", test.name, wave);
|
||||
}
|
||||
}
|
||||
|
||||
void CheckGuardedRecordImageTable(VulkanHarness &vulkan) {
|
||||
using O = ShaderOpcode;
|
||||
constexpr u32 table = 256u;
|
||||
@@ -46364,6 +46473,7 @@ int main(int argc, char **argv) {
|
||||
VulkanHarness vulkan;
|
||||
CheckCountedRowImageSelector(vulkan);
|
||||
CheckPointerImageTable(vulkan);
|
||||
CheckNestedPointerImageTable(vulkan);
|
||||
CheckGuardedRecordImageTable(vulkan);
|
||||
CheckIndirectImageKeySwitch(vulkan);
|
||||
CheckFiniteInlineSamplerPhi(vulkan);
|
||||
@@ -46536,6 +46646,7 @@ int main(int argc, char **argv) {
|
||||
CheckComputeThreadDimensions(vulkan);
|
||||
CheckCountedRowImageSelector(vulkan);
|
||||
CheckPointerImageTable(vulkan);
|
||||
CheckNestedPointerImageTable(vulkan);
|
||||
CheckGuardedRecordImageTable(vulkan);
|
||||
CheckIndirectImageKeySwitch(vulkan);
|
||||
CheckDynamicSampleOffsets(vulkan, 32u);
|
||||
|
||||
Reference in New Issue
Block a user