From df92a08990ba42f6d474ccc47119e37a47b6cbdb Mon Sep 17 00:00:00 2001 From: nmzik Date: Thu, 15 Oct 2026 15:50:21 +0200 Subject: [PATCH] shader: report descriptor materialization rejection context --- .../ir/passes/ResourceMaterialization.cpp | 72 +++++++++++++------ tests/ResourceTrackingTests.cpp | 10 +++ 2 files changed, 61 insertions(+), 21 deletions(-) diff --git a/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp b/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp index d0ed1859f..320d54b90 100644 --- a/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp +++ b/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp @@ -549,26 +549,44 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p }; 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 SpecializationFail(fmt::format( - "indirect candidate resource={} table_source={} table_base={:#x} table_size={} " - "candidate_offset={:#x} {}={} stride={} step={} maximum={} offset={} immediate={}", - resource_index, indirect.table_source, table_base & AddressMask & ~uint64_t {3}, table_size, - offset & ~uint64_t {3}, key_kind, key, indirect.table_stride, step, - indirect.table_maximum ? fmt::format("{}", *indirect.table_maximum) : "none", - indirect.table_offset, indirect.table_immediate)); + 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; - DescriptorValue table_value; if ((selector != nullptr && !selector_keys->resolved && !clean.EvaluateDescriptor(selector->source, material_value)) || !clean.EvaluateDescriptor(indirect.table_source, table_value)) { return false; } - ShaderBufferResource table; if (table_value.dword_count == 2u) { table_base = (static_cast(table_value.dwords[1]) << 32u) | table_value.dwords[0]; } else if (DecodeBufferDescriptor(table_value, table)) { @@ -587,7 +605,7 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p (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 false; + 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; @@ -595,7 +613,7 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p (!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() || - *indirect.table_maximum >= table.NumRecords())) return false; + *indirect.table_maximum >= table.NumRecords())) return failed("indexed pointer geometry"); 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() @@ -611,12 +629,13 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p ? std::min((table_size - first - 4u) / step + 1u, domain) : 0u; if (child_count == 0u || (!image_heap && count * child_count > MaxIndirectDescriptorProbes)) - return false; + 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 false; + 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}; @@ -626,7 +645,8 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p uint64_t pointer_base = 0u; if (pointer != nullptr) { std::array address {}; - if (!ReadScalarTable(table_base, table_size, table_offset, runtime, address)) return false; + 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) { @@ -634,7 +654,9 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p 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 false; + 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; @@ -708,7 +730,7 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p (indirect.table_immediate & ~3u); if (!ReadScalarTable(table_base, table_size, table_offset, runtime, std::span(candidate.dwords).first(read_dwords))) { - return false; + return read_failed("table descriptor read", key, 0u, table_base, table_offset, descriptor_bytes); } } else if (!clean.EvaluateDescriptor(sources[entry], candidate)) { return false; @@ -1469,7 +1491,8 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime SrtWalker walker(program, observed, capture_reads || program.requires_specialization_memory ? &clean : nullptr); if (!walker.RefreshFlatBuffer(snapshot.flattened_srt)) { - return false; + 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(program.active_sources); std::array workgroup_counts {}; @@ -1501,12 +1524,18 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime return false; } if (active.empty() || active[source]) { - return (capture_reads && written ? clean : walker).EvaluateDescriptor(source, value); + 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) { @@ -1527,7 +1556,7 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime return NormalizeIndirectBuffer( value, source->indirect_descriptor->indexed, base); })) - return false; + return resource_failed("buffer", i, base.source, snapshot.buffers.size(), UINT32_MAX); } } else if (!evaluate(base.source, snapshot.buffers[i], base.written)) { return false; @@ -1593,7 +1622,7 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime value.dwords.fill(0u); return true; })) { - return false; + return resource_failed("image", i, image.source, snapshot.images.size(), UINT32_MAX); } } else { if (!evaluate(image.source, snapshot.images[i], image.written)) { @@ -1620,7 +1649,8 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime if ((active.empty() || active[source]) && !MaterializeIndirectDescriptor( program, source, i, 4u, 4u, observed, clean, snapshot, snapshot.samplers, specialization.samplers, ShaderInfo::MaxSamplers, - [](DescriptorValue&) { return true; })) return false; + [](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; diff --git a/tests/ResourceTrackingTests.cpp b/tests/ResourceTrackingTests.cpp index 15ce25202..3ad64f396 100644 --- a/tests/ResourceTrackingTests.cpp +++ b/tests/ResourceTrackingTests.cpp @@ -950,6 +950,9 @@ void TestNestedPointerImageTable() { 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 = objects + 0x3f8u; + Check(!MaterializeResources(plan, runtime, snapshot, specialization), + "failed nested sampler pointee read was treated as a null descriptor"); memory.fail_address = 0x1800u; *at(table) = 0u; Check(!MaterializeResources(plan, runtime, snapshot, specialization), @@ -1154,6 +1157,10 @@ void TestIndexedPointerImageTable() { Check(!MaterializeResources(plan, runtime, snapshot, specialization) && memory.reads == reads, "incomplete indexed parent domain became null images or performed partial candidate reads"); } + user_data[2] = UINT32_MAX; + const auto oversized_reads = memory.reads; + Check(!MaterializeResources(plan, runtime, snapshot, specialization) && memory.reads == oversized_reads, + "oversized indexed parent table performed candidate reads"); user_data[2] = 2u; for (const uint32_t bad_stride : {0u, 4u, 161u}) { user_data[1] = 1u | (bad_stride << 16u); @@ -1169,6 +1176,9 @@ void TestIndexedPointerImageTable() { "indexed pointer table accepted unsupported RDNA2 geometry before reading candidates"); user_data[word] &= ~(1u << bit); } + memory.fail_address = high + table + stride; + Check(!MaterializeResources(plan, runtime, snapshot, specialization), + "unreadable indexed parent pointer was treated as a null image"); memory.fail_address = high + objects + 2u * 368u + 64u; Check(!MaterializeResources(plan, runtime, snapshot, specialization), "unreadable nested child was silently converted to a null image");