diff --git a/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp b/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp index 3f37c5f55..f8017b837 100644 --- a/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp +++ b/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp @@ -249,6 +249,19 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p const auto descriptor_bytes = dword_count * sizeof(uint32_t); const auto& sources = indirect.sources; auto& keys = program.material_keys; + const auto read_keys = [&](const ShaderBufferResource& material, uint64_t first, + uint64_t step, uint64_t count) { + keys.resize(count); + if (step == 4u) { + return ReadScalarTable(material.Base48(), material.GetSize(), first, runtime, keys); + } + for (auto& key: keys) { + if (!ReadScalarTable(material.Base48(), material.GetSize(), first, runtime, {&key, 1})) + return false; + first += step; + } + return true; + }; uint64_t table_base = 0; uint64_t table_size = UINT64_MAX; // Scalar addresses have no buffer descriptor bounds. if (sources.empty()) { @@ -297,20 +310,8 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p indirect.selector_offset + 4u > uint64_t {UINT32_MAX} + 1u) return false; - if (material.Stride() == 4u && indirect.selector_offset == 0u) { - keys.resize(count); - if (!ReadScalarTable(material.Base48(), material.GetSize(), uint64_t {first} * 4u, - runtime, keys)) return false; - } else { - keys.reserve(count); - for (uint64_t index = first; index < uint64_t {first} + count; ++index) { - uint32_t key = 0; - const auto offset = index * material.Stride() + indirect.selector_offset; - if (!ReadScalarTable(material.Base48(), material.GetSize(), offset, runtime, - {&key, 1})) return false; - keys.push_back(key); - } - } + if (!read_keys(material, uint64_t {first} * material.Stride() + indirect.selector_offset, + material.Stride(), count)) return false; } else if (!indirect.selector_mask.IsEmpty()) { uint32_t mask = 0; uint32_t count = 0; @@ -336,8 +337,7 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p } } else { ShaderBufferResource material; - if (!DecodeBufferDescriptor(material_value, material) || table_value.dword_count != 4u || - material.Stride() != indirect.selector_stride) { + if (!DecodeBufferDescriptor(material_value, material) || table_value.dword_count != 4u) { return false; } // The first aligned offset includes the immediate added after shader U32 arithmetic. @@ -350,15 +350,8 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p if (probe_count > MaxIndirectDescriptorProbes) { return false; } - keys.reserve(static_cast(probe_count) + 1u); - keys.push_back(0u); - for (uint64_t probe = 0, offset = first; probe < probe_count; ++probe, offset += step) { - uint32_t key = 0; - if (!ReadScalarTable(material.Base48(), size, offset, runtime, {&key, 1})) { - return false; - } - keys.push_back(key); - } + if (!read_keys(material, first, step, probe_count)) return false; + if (indirect.selector_shift == 0u) keys.push_back(0u); } if (indirect.material_source != UINT32_MAX) { if (indirect.selector_shift != 0u) { @@ -1011,8 +1004,7 @@ ResourcePlan ExtractResourcePlan(const Program& program) { if (!source.indirect_descriptor.has_value()) continue; const auto& indirect = *source.indirect_descriptor; plan.requires_specialization_memory = true; - capture_indirect_reads |= !indirect.selector_mask.IsEmpty() || - !indirect.selector_first.IsEmpty() || !indirect.sources.empty(); + capture_indirect_reads |= indirect.material_source != UINT32_MAX || !indirect.sources.empty(); MarkCleanFlatSlots(plan, Source(plan, indirect.material_source), plan.clean_flat_slots, indirect.selector_mask); MarkCleanFlatSlots(plan, nullptr, plan.clean_flat_slots, indirect.selector_first); diff --git a/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp b/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp index 6ed792ef8..ed6d98236 100644 --- a/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp +++ b/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp @@ -923,38 +923,39 @@ private: return ValidateSource(descriptor, bad_dword); } - bool MatchMaterialOffset(Value value, Value& selector, uint32_t& stride, - uint32_t& offset) const { - value = value.Resolve(); - offset = 0; - auto* candidate = value.TryInstruction(); - if (candidate != nullptr && candidate->GetOpcode() == ValueOpcode::IAdd32 && - candidate->NumArgs() == 2u) { - uint32_t immediate = 0; - if (ImmediateU32(candidate->Arg(0), immediate)) { - value = candidate->Arg(1).Resolve(); - } else if (ImmediateU32(candidate->Arg(1), immediate)) { - value = candidate->Arg(0).Resolve(); + bool MatchMaterialOffset(Value value, uint32_t& stride, uint32_t& offset) const { + stride = 1u; + offset = 0u; + value = value.Resolve(); + while (const auto* inst = value.TryInstruction()) { + if (inst->NumArgs() != 2u) break; + uint32_t constant = 0; + Value remaining; + if (inst->GetOpcode() == ValueOpcode::ShiftLeftLogical32 && + ImmediateU32(inst->Arg(1), constant) && constant < 32u) { + stride *= 1u << constant; + remaining = inst->Arg(0); + } else if (inst->GetOpcode() == ValueOpcode::IAdd32 || + inst->GetOpcode() == ValueOpcode::IMul32) { + if (ImmediateU32(inst->Arg(0), constant)) remaining = inst->Arg(1); + else if (ImmediateU32(inst->Arg(1), constant)) remaining = inst->Arg(0); + else break; + if (inst->GetOpcode() == ValueOpcode::IAdd32) offset += stride * constant; + else stride *= constant; } else { - return false; + break; } - offset = immediate; + value = remaining.Resolve(); } - const auto* multiply = value.TryInstruction(); - if (multiply == nullptr || multiply->GetOpcode() != ValueOpcode::IMul32 || - multiply->NumArgs() != 2u) { - return false; - } - if (ImmediateU32(multiply->Arg(0), stride)) { - selector = multiply->Arg(1).Resolve(); - } else if (ImmediateU32(multiply->Arg(1), stride)) { - selector = multiply->Arg(0).Resolve(); - } else { - return false; - } - const auto* selector_inst = selector.TryInstruction(); - return stride != 0u && selector_inst != nullptr && - selector_inst->GetOpcode() == ValueOpcode::ReadFirstLane; + // Treat the remaining GPU-derived scalar as any U32 value. + 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; } enum class LaneQuantifier { Any, All }; @@ -1558,10 +1559,9 @@ private: DescriptorSource& material_source) { const auto* selected = UniformizedMaterialValue(key, image); if (selected == nullptr) return false; - const auto* shift = selected->Arg(1).Resolve().TryInstruction(); uint32_t amount = 0; - if (shift == nullptr || shift->GetOpcode() != ValueOpcode::ShiftRightLogical32 || - !ImmediateU32(shift->Arg(1), amount) || amount == 0u || amount >= 32u) return false; + const auto* shift = MaterialKeyShift(selected->Arg(1), amount); + if (shift == nullptr) return false; const auto* loaded = shift->Arg(0).Resolve().TryInstruction(); if (loaded == nullptr || loaded->GetOpcode() != ValueOpcode::SelectU32 || !EquivalentValue(m_program, selected->Arg(0), loaded->Arg(0))) return false; @@ -1611,7 +1611,9 @@ private: if ((table_offset & 3u) != 0u || (bitscan && table_offset > UINT32_MAX - (32u * 32u - 1u))) return false; } else if (!MatchUniformizedBufferKey(key, handle, indirect, material_source)) { - auto* material_read = key.Resolve().TryInstruction(); + const auto* key_shift = MaterialKeyShift(key, indirect.selector_shift); + auto* material_read = + (key_shift != nullptr ? key_shift->Arg(0) : key).Resolve().TryInstruction(); uint32_t material_memory_index = 0; const auto* memory = material_read != nullptr ? ScalarReadMemory(*material_read, material_memory_index) : nullptr; @@ -1619,8 +1621,7 @@ private: memory->offset > INT32_MAX || !MemoryIndexBelongsTo(material_memory_index, *material_read)) { return false; } - Value selector; - if (!MatchMaterialOffset(material_read->Arg(1), selector, indirect.selector_stride, + if (!MatchMaterialOffset(material_read->Arg(1), indirect.selector_stride, indirect.selector_offset)) { return false; } @@ -1628,8 +1629,11 @@ private: indirect.selector_offset = (static_cast(indirect.selector_offset % step) & ~3u) + (memory->offset & ~3u); const auto* shift = plan.reads[0]->Arg(1).Resolve().TryInstruction(); - const std::array material_users {shift}; - if (!UsesOnly(*material_read, material_users) || !UsesOnly(*shift, plan.reads)) { + const std::array material_users {key_shift != nullptr ? key_shift : shift}; + const std::array key_users {shift}; + if (!UsesOnly(*material_read, material_users) || + (key_shift != nullptr && !UsesOnly(*key_shift, key_users)) || + !UsesOnly(*shift, plan.reads)) { return false; } const auto* material_handle = material_read->Arg(0).Resolve().TryInstruction(); diff --git a/tests/ShaderRecompilerComputeTests.cpp b/tests/ShaderRecompilerComputeTests.cpp index 00b639eeb..a82a466f9 100644 --- a/tests/ShaderRecompilerComputeTests.cpp +++ b/tests/ShaderRecompilerComputeTests.cpp @@ -35604,9 +35604,9 @@ void CheckImageSamplerSpecialization() { "a shared float/unsigned/signed sampler lost its border or filtering " "class in the compiled bindings, instruction or descriptor snapshot"); - { + for (const bool scalar : {false, true}) { using namespace ShaderRecompiler::IR; - constexpr const char *name = "GpuMaterialImageDomain"; + const char *name = scalar ? "ScalarMaterialImageDomain" : "GpuMaterialImageDomain"; constexpr u32 count = 70, table_base = 0x1000, material_base = 0x2000; std::vector memory(4096); for (u32 i = 0; i < count; ++i) { @@ -35628,29 +35628,44 @@ void CheckImageSamplerSpecialization() { std::copy_n(output.begin(), 4, user_data.begin() + 48); user_data[48] = 0x3000u; std::vector code{EncodeVopc(0xc5, InlineU32(0), 0), - EncodeSop1(0x24, 24, 106), EncodeSopp(0x08, 0), - EncodeSop1(0x04, 26, 126), EncodeVop1(0x01, 16, InlineU32(1))}; - // An earlier descriptor waterfall separates the EXECZ witness from this material load. - const auto previous_loop = code.size(); - code.insert(code.end(), {EncodeSop1(0x04, 28, 126), - EncodeVop1(0x02, 22, Vgpr(0)), EncodeVopc(0xd2, 22, 0), - EncodeVop1(0x01, 16, InlineU32(0)), - EncodeSop1(0x04, 126, 28), EncodeVopc(0xd5, InlineU32(0), 16)}); - code.push_back(EncodeSopp(0x09, static_cast(previous_loop - code.size() - 1u))); - code.insert(code.end(), {EncodeSop1(0x04, 126, 26), - EncodeMubuf0(0x0c, 0, true, false), EncodeMubuf1(15, 1, 0), - EncodeVop2(0x16, 15, InlineU32(4), 15), - EncodeVop1(0x01, 16, InlineU32(1))}); + EncodeSop1(0x24, 24, 106), EncodeSopp(0x08, 0)}; + size_t scalar_shift = 0; + if (scalar) { + code.insert(code.end(), {EncodeVop1(0x02, 20, Vgpr(0)), + EncodeSop2(0x00, 20, 20, 21), + EncodeSop2(0x26, 20, 20, InlineU32(3)), + EncodeSop2(0x1e, 20, 20, InlineU32(2)), + EncodeSop2(0x00, 20, 20, InlineU32(8)), + EncodeSmem0(0x08, 20, 2), EncodeSmem1(0, 20)}); + scalar_shift = code.size(); + code.push_back(EncodeSop2(0x20, 20, 20, InlineU32(4))); + } else { + code.insert(code.end(), {EncodeSop1(0x04, 26, 126), EncodeVop1(0x01, 16, InlineU32(1))}); + // An earlier descriptor waterfall separates the EXECZ witness from the material load. + const auto previous_loop = code.size(); + code.insert(code.end(), {EncodeSop1(0x04, 28, 126), + EncodeVop1(0x02, 22, Vgpr(0)), EncodeVopc(0xd2, 22, 0), + EncodeVop1(0x01, 16, InlineU32(0)), + EncodeSop1(0x04, 126, 28), EncodeVopc(0xd5, InlineU32(0), 16)}); + code.push_back(EncodeSopp(0x09, static_cast(previous_loop - code.size() - 1u))); + code.insert(code.end(), {EncodeSop1(0x04, 126, 26), + EncodeMubuf0(0x0c, 0, true, false), EncodeMubuf1(15, 1, 0), + EncodeVop2(0x16, 15, InlineU32(4), 15), + EncodeVop1(0x01, 16, InlineU32(1))}); + } const auto image_loop = code.size(); - code.insert(code.end(), {EncodeSop1(0x04, 28, 126), - EncodeVop1(0x02, 20, Vgpr(15)), EncodeVopc(0xd2, 20, 15), - EncodeSop2(0x1e, 20, 20, InlineU32(5)), + if (!scalar) + code.insert(code.end(), {EncodeSop1(0x04, 28, 126), + EncodeVop1(0x02, 20, Vgpr(15)), EncodeVopc(0xd2, 20, 15)}); + code.insert(code.end(), {EncodeSop2(0x1e, 20, 20, InlineU32(5)), EncodeSmem0(0x0b, 12), EncodeSmem1(0, 20), EncodeMimg0(0x24, 1), EncodeMimg1(4, 0, 3, 2)}); AppendBufferStoreDword(&code, 4, 0); - code.insert(code.end(), {EncodeVop1(0x01, 16, InlineU32(0)), - EncodeSop1(0x04, 126, 28), EncodeVopc(0xd5, InlineU32(0), 16)}); - code.push_back(EncodeSopp(0x09, static_cast(image_loop - code.size() - 1u))); + if (!scalar) { + code.insert(code.end(), {EncodeVop1(0x01, 16, InlineU32(0)), + EncodeSop1(0x04, 126, 28), EncodeVopc(0xd5, InlineU32(0), 16)}); + code.push_back(EncodeSopp(0x09, static_cast(image_loop - code.size() - 1u))); + } code[2] = EncodeSopp(0x08, static_cast(code.size() - 3u)); AppendEnd(&code); ShaderComputeInputInfo compute{}; @@ -35695,21 +35710,34 @@ void CheckImageSamplerSpecialization() { "descriptor addresses were frozen or entered the shader specialization"); auto compiled = ShaderRecompiler::CompileProgram(std::move(translated), options, specialization); ValidateSpirv(name, compiled.spirv); - user_data[4] += 1u; - Require(name, "misaligned material rejection", - !MaterializeResources(plan, runtime, snapshot, specialization), - "unaligned raw buffer base was treated as separately aligned scalar memory"); - user_data[4] -= 1u; + if (!scalar) { + user_data[4] += 1u; + Require(name, "misaligned material rejection", + !MaterializeResources(plan, runtime, snapshot, specialization), + "unaligned raw buffer base was treated as separately aligned scalar memory"); + user_data[4] -= 1u; + } auto dirty = runtime; dirty.read_specialization_memory = +[](void*, uint64_t, std::span) { return false; }; Require(name, "dirty material rejection", !MaterializeResources(plan, dirty, snapshot, specialization), "GPU-selected texture materialization bypassed strict read provenance"); #if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS - ExpectFatal("MaterialImageWithoutEntryWitness", [&] { - code[2] = EncodeSopp(0x00, 0); // S_NOP removes the original nonempty-EXEC witness. - (void)ShaderRecompiler::TranslateProgram(code, options); - }); + if (scalar) { + ExpectFatal("ScalarMaterialNumericConsumer", [&] { + code.insert(code.begin() + scalar_shift, EncodeVop1(0x01, 17, 20)); + code.pop_back(); + AppendBufferStoreDword(&code, 17, 0); + code[2] = EncodeSopp(0x08, static_cast(code.size() - 3u)); + AppendEnd(&code); + (void)ShaderRecompiler::TranslateProgram(code, options); + }); + } else { + ExpectFatal("MaterialImageWithoutEntryWitness", [&] { + code[2] = EncodeSopp(0x00, 0); // S_NOP removes the original nonempty-EXEC witness. + (void)ShaderRecompiler::TranslateProgram(code, options); + }); + } #endif }