diff --git a/src/graphics/shader/recompiler/ir/ShaderIR.h b/src/graphics/shader/recompiler/ir/ShaderIR.h index 694cd94b1..400e1b311 100644 --- a/src/graphics/shader/recompiler/ir/ShaderIR.h +++ b/src/graphics/shader/recompiler/ir/ShaderIR.h @@ -493,6 +493,7 @@ struct DescriptorSource { uint32_t table_stride = 0; uint32_t workgroup_axis = UINT32_MAX; uint32_t selector_shift = 0; + uint32_t selector_bits = UINT32_MAX; Value key_count; Value selector_first; Value selector_mask; diff --git a/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp b/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp index ec8762f66..a814a3326 100644 --- a/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp +++ b/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp @@ -306,7 +306,8 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p ShaderBufferResource material; uint32_t first = 0, count = 0; if (!DecodeBufferDescriptor(material_value, material) || material.Type() != 0u || - (indirect.selector_shift != 0u && (material.Base48() & 3u) != 0u) || + ((indirect.selector_shift != 0u || indirect.selector_bits != UINT32_MAX) && + (material.Base48() & 3u) != 0u) || material.SwizzleEnabled() || material.AddTid() || material.OutOfBounds() != 0u || uint64_t {indirect.selector_offset} + 4u > material.Stride() || @@ -362,8 +363,8 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p if (indirect.selector_shift == 0u) keys.push_back(0u); } if (indirect.material_source != UINT32_MAX) { - if (indirect.selector_shift != 0u) { - for (auto& key: keys) key >>= indirect.selector_shift; + for (auto& key: keys) key = (key >> indirect.selector_shift) & indirect.selector_bits; + if (indirect.selector_shift != 0u || indirect.selector_bits != UINT32_MAX) { keys.push_back(0u); // An out-of-range material load returns zero. } std::ranges::sort(keys); diff --git a/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp b/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp index c64ac4cee..f0035bfc0 100644 --- a/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp +++ b/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp @@ -835,7 +835,7 @@ private: const auto& b = *descriptor.indirect_descriptor; if (a.material_source != b.material_source || a.table_source != b.table_source || a.selector_stride != b.selector_stride || a.selector_offset != b.selector_offset || - a.selector_shift != b.selector_shift || + a.selector_shift != b.selector_shift || a.selector_bits != b.selector_bits || a.table_offset != b.table_offset || a.table_stride != b.table_stride || a.workgroup_axis != b.workgroup_axis || a.sources != b.sources || !EquivalentValue(m_program, a.key_count, b.key_count) || @@ -968,11 +968,24 @@ private: return stride != 0u && value.GetType() == Type::U32 && !value.IsImmediate(); } - const Inst* MaterialKeyShift(Value key, uint32_t& amount) const { - const auto* shift = key.Resolve().TryInstruction(); - return shift != nullptr && shift->GetOpcode() == ValueOpcode::ShiftRightLogical32 && - ImmediateU32(shift->Arg(1), amount) && amount != 0u && amount < 32u - ? shift : nullptr; + Value MaterialKey(Value key, DescriptorSource::IndirectDescriptor& indirect) const { + indirect.selector_shift = 0u; + indirect.selector_bits = UINT32_MAX; + const auto* inst = key.Resolve().TryInstruction(); + uint32_t immediate; + if (inst != nullptr && inst->GetOpcode() == ValueOpcode::BitwiseAnd32) { + if (ImmediateU32(inst->Arg(1), immediate)) key = inst->Arg(0); + else if (ImmediateU32(inst->Arg(0), immediate)) key = inst->Arg(1); + else return key; + indirect.selector_bits = immediate; + inst = key.Resolve().TryInstruction(); + } + if (inst != nullptr && inst->GetOpcode() == ValueOpcode::ShiftRightLogical32 && + ImmediateU32(inst->Arg(1), immediate) && immediate != 0u && immediate < 32u) { + indirect.selector_shift = immediate; + key = inst->Arg(0); + } + return key.Resolve(); } enum class LaneQuantifier { Any, All }; @@ -1647,10 +1660,9 @@ private: DescriptorSource& material_source) { const auto* selected = UniformizedMaterialValue(key, image); if (selected == nullptr) return false; - uint32_t amount = 0; - const auto* shift = MaterialKeyShift(selected->Arg(1), amount); - if (shift == nullptr) return false; - const auto* loaded = shift->Arg(0).Resolve().TryInstruction(); + const auto value = MaterialKey(selected->Arg(1), indirect); + if (indirect.selector_shift == 0u && indirect.selector_bits == UINT32_MAX) return false; + const auto* loaded = value.TryInstruction(); if (loaded == nullptr || loaded->GetOpcode() != ValueOpcode::SelectU32 || !EquivalentValue(m_program, selected->Arg(0), loaded->Arg(0))) return false; const auto* read = loaded->Arg(1).Resolve().TryInstruction(); @@ -1668,7 +1680,6 @@ private: indirect.selector_first = Value(0u); indirect.key_count = material_source.dwords[2]; indirect.selector_offset = memory.offset; - indirect.selector_shift = amount; return true; } @@ -1690,9 +1701,8 @@ private: // The descriptor also supplies dimensions to shader arithmetic. Keep its reads; // only the image handle is projected onto the bounded workgroup key. plan.reads.fill(nullptr); - } else if (table_stride != 32u) { - return false; } else if (table_source.dword_count == 2u) { + if (table_stride != 32u) return false; const auto* selector = key.Resolve().TryInstruction(); const bool bitscan = selector != nullptr && selector->GetOpcode() == ValueOpcode::FindILsb32 && selector->NumArgs() == 1u && !m_shader_writes && @@ -1710,13 +1720,11 @@ private: if ((table_offset & 3u) != 0u || (bitscan && table_offset > UINT32_MAX - (32u * 32u - 1u))) return false; } else if (!MatchUniformizedBufferKey(key, handle, indirect, material_source)) { - const auto* key_shift = MaterialKeyShift(key, indirect.selector_shift); - auto* material_read = - (key_shift != nullptr ? key_shift->Arg(0) : key).Resolve().TryInstruction(); + auto* material_read = MaterialKey(key, indirect).TryInstruction(); uint32_t material_memory_index = 0; const auto* memory = material_read != nullptr ? ScalarReadMemory(*material_read, material_memory_index) : nullptr; - if (table_offset != 0u || memory == nullptr || memory->kind != ResourceKind::ScalarBuffer || + if (memory == nullptr || memory->kind != ResourceKind::ScalarBuffer || memory->offset > INT32_MAX || !MemoryIndexBelongsTo(material_memory_index, *material_read)) { return false; } @@ -1735,6 +1743,12 @@ private: } indirect.material_source = InternSource(material_source); } + // This plan combines shader byte-offset additions with the scalar immediate. + // A nonzero combined offset is exact only when neither can cross U32 wrap. + const auto maximum_key = (UINT32_MAX >> indirect.selector_shift) & indirect.selector_bits; + if (indirect.workgroup_axis == UINT32_MAX && table_source.dword_count == 4u && + table_offset != 0u && uint64_t {maximum_key} * table_stride + table_offset + 28u > UINT32_MAX) + return false; indirect.table_source = InternSource(table_source); DescriptorSource image_source; image_source.dword_count = 8u; @@ -2032,6 +2046,8 @@ private: DescriptorSource descriptor; MakeSource(*handle, inst.GetOpcode() == ValueOpcode::StoreBufferU32 ? 4u : 8u, false, false, memory.resource * 4u, descriptor, flags.pc); + uint32_t bad_dword = 0; + if (ValidateSource(descriptor, bad_dword)) continue; IndirectDescriptorPlan plan; if (TryMakeIndirectImage(*handle, descriptor, plan) || TryMakeFiniteImage(*handle, plan) || TryMakeIndirectBuffer(*handle, descriptor, plan)) { diff --git a/tests/ResourceTrackingTests.cpp b/tests/ResourceTrackingTests.cpp index 96537b46e..fa87c9dc6 100644 --- a/tests/ResourceTrackingTests.cpp +++ b/tests/ResourceTrackingTests.cpp @@ -208,7 +208,8 @@ bool ReadLinearTestMemory(void *userdata, uint64_t address, std::span std::unique_ptr MakeIndirectImageFixture(bool malformed, uint32_t material_immediate = 4, bool memory_backed_material = false, uint32_t member_offset = 0, - uint32_t material_stride = 224) { + uint32_t material_stride = 224, uint32_t selector_bits = UINT32_MAX, + uint32_t table_stride = 32, uint32_t table_offset = 0) { auto fixture = std::make_unique(); std::array material_words; std::array heap_words; @@ -257,17 +258,19 @@ MakeIndirectImageFixture(bool malformed, uint32_t material_immediate = 4, MemoryInfo material_scalar; material_scalar.kind = ResourceKind::ScalarBuffer; material_scalar.offset = material_immediate; - const auto key = + auto key = fixture->Emit(ValueOpcode::ReadConstBuffer, {material, member}, fixture->AddMemory(material_scalar, 0x10d8)); + if (selector_bits != UINT32_MAX) + key = fixture->Emit(ValueOpcode::BitwiseAnd32, {key, Value(selector_bits)}); const auto heap_offset = - fixture->Emit(ValueOpcode::ShiftLeftLogical32, {key, Value(5u)}); + fixture->Emit(ValueOpcode::IMul32, {key, Value(table_stride)}); std::array image_words; MemoryInfo heap_scalar; heap_scalar.kind = ResourceKind::ScalarBuffer; for (uint32_t dword = 0; dword < image_words.size(); dword++) { auto component = heap_scalar; - component.offset = dword * sizeof(uint32_t); + component.offset = table_offset + dword * sizeof(uint32_t); if (malformed && dword == image_words.size() - 1u) { component.offset += sizeof(uint32_t); } @@ -579,6 +582,43 @@ void TestInvariantIndirectImageMaterialization() { split_memory.watched_reads == 1u, "indirect scalar offsets did not align and bound their components independently"); } + + // The shader packs flags above a 15-bit cube-image key in the final DWORD of + // each 64-byte material record; the image occupies bytes 16..47 of its record. + auto packed = MakeIndirectImageFixture(false, 60u, false, 0u, 64u, 0x7fffu, 48u, 16u); + packed->PlanAndTrack(); + const auto packed_plan = ExtractResourcePlan(packed->program); + LinearTestMemory packed_memory; + std::array packed_data{0x1000u, 64u << 16u, 3u, 0u, + 0x2000u, 48u << 16u, 2u, 0u}; + for (uint32_t i = 0; i < 3u; ++i) + packed_memory.words[(i * 64u + 60u) / 4u] = (0xffffu << 15u) | (i & 1u); + for (uint32_t i = 0; i < 2u; ++i) { + auto descriptor = image_descriptor; + descriptor[0] += i; + std::copy(descriptor.begin(), descriptor.end(), + packed_memory.words.begin() + (0x1000u + i * 48u + 16u) / 4u); + } + SrtRuntime packed_runtime{.user_data = packed_data, .userdata = &packed_memory, + .read_specialization_memory = ReadLinearTestMemory}; + Check(MaterializeResources(packed_plan, packed_runtime, snapshot, specialization) && + snapshot.images.size() == 2u && snapshot.images[0].dwords == image_descriptor && + snapshot.images[1].dwords[0] == image_descriptor[0] + 1u && + snapshot.flattened_srt[specialization.images[0].indirect_mapping_offset] == 2u, + "packed material flags changed the table key or 48-byte descriptor addressing"); + packed_memory.words[60u / 4u] = 0xffff8002u; + packed_memory.fail_address = 0x2070u; + Check(MaterializeResources(packed_plan, packed_runtime, snapshot, specialization) && + snapshot.images.size() == 3u && + std::ranges::all_of(snapshot.images[2].dwords, [](uint32_t word) { return word == 0u; }), + "masked out-of-range descriptor did not return zero without reading past table bounds"); + packed_memory.fail_address = 0x205cu; + Check(!MaterializeResources(packed_plan, packed_runtime, snapshot, specialization), + "unreadable in-range DWORD in a 48-byte descriptor record was accepted"); + auto overflowing = MakeIndirectImageFixture(false, 60u, false, 0u, 64u, + UINT32_MAX, 48u, 16u); + CheckFatal([&] { overflowing->PlanAndTrack(); }, "not a valid runtime value", + "unbounded table offset conflated shader U32 wrap with scalar immediate addition"); } void TestGuardedDirectImageTable() { @@ -1529,6 +1569,9 @@ void TestUniformScalarBufferImage() { fixture.PlanAndTrack(); const auto plan = ExtractResourcePlan(fixture.program); + Check(std::ranges::none_of(plan.descriptor_sources, [](const DescriptorSource& source) { + return source.indirect_descriptor.has_value(); + }), "draw-uniform scalar image was expanded to GPU-selected candidates"); std::array user_data{0x1000u, 0u, 0x100u, 0u, 0x2000u, 0u, 0x100u, 0u, 0u}; LinearTestMemory memory;