#include "graphics/shader/recompiler/ir/ShaderIR.h" #include "graphics/shader/recompiler/ir/passes/ResourceMaterialization.h" #include #include #include #include #include namespace { void Check(bool value, const char *text) { if (!value) { std::fprintf(stderr, "ResourceMaterializationTests: failed: %s\n", text); std::abort(); } } bool RejectSpecializationRead(void *userdata, uint64_t, std::span) { ++*static_cast(userdata); return false; } Libs::Graphics::ShaderRecompiler::IR::Block & AddValueBlock(Libs::Graphics::ShaderRecompiler::IR::Program &program) { using namespace Libs::Graphics::ShaderRecompiler::IR; auto block = std::make_unique(); auto *result = block.get(); program.blocks.push_back(result); result->id = static_cast(program.blocks.size() - 1u); program.block_storage.push_back(std::move(block)); return *result; } Libs::Graphics::ShaderRecompiler::IR::Program SrtProgram(uint64_t address) { using namespace Libs::Graphics::ShaderRecompiler::IR; Program program; program.stage = Libs::Graphics::ShaderType::Compute; program.srt_plan_complete = true; program.resource_tracking_complete = true; auto &value_block = AddValueBlock(program); MemoryInfo memory; memory.kind = ResourceKind::ScalarAddress; memory.planning_only = true; program.memory_info.push_back(memory); const auto low = Value(static_cast(address)); const auto high = Value(static_cast(address >> 32u)); auto &handle = value_block.AppendNewInst(ValueOpcode::GetAddressResource, {low, high}); auto &raw = value_block.AppendNewInst( ValueOpcode::LoadAddressU32, {Value(&handle), Value(0u), Value(0u), Value(true)}); raw.SetFlags(MemoryFlags{.index = 0, .pc = 0x40}); program.srt_reads.push_back({Value(&raw), 0}); auto &srt = value_block.AppendNewInst(ValueOpcode::GetSrtResource); auto &flat = value_block.AppendNewInst(ValueOpcode::ReadConst, {Value(&srt), Value(0u)}); DescriptorSource source; source.dwords[0] = Value(&flat); source.dwords[1] = Value(0u); source.dword_count = 2; program.descriptor_sources.push_back(source); return program; } Libs::Graphics::ShaderRecompiler::IR::ResourcePlan UnbasedFlatPlan() { using namespace Libs::Graphics::ShaderRecompiler::IR; Program program; program.stage = Libs::Graphics::ShaderType::Compute; program.srt_plan_complete = true; program.resource_tracking_complete = true; AddValueBlock(program); program.info.uses_dma = true; return ExtractResourcePlan(program); } Libs::Graphics::ShaderRecompiler::IR::ResourcePlan UserDataBufferPlan() { using namespace Libs::Graphics::ShaderRecompiler::IR; Program program; program.stage = Libs::Graphics::ShaderType::Compute; program.srt_plan_complete = true; program.resource_tracking_complete = true; auto &value_block = AddValueBlock(program); auto &user_data = value_block.AppendNewInst( ValueOpcode::GetUserData, {Value(static_cast(0))}); DescriptorSource source; source.dwords[0] = Value(&user_data); source.dwords[1] = Value(0u); source.dwords[2] = Value(0u); source.dwords[3] = Value(0u); source.dword_count = 4; program.descriptor_sources.push_back(source); program.info.buffers.push_back({.source = 0}); return ExtractResourcePlan(program); } Libs::Graphics::ShaderRecompiler::IR::Program MixedSamplerProgram() { using namespace Libs::Graphics::ShaderRecompiler::IR; Program program; program.stage = Libs::Graphics::ShaderType::Compute; program.srt_plan_complete = true; program.resource_tracking_complete = true; auto &block = AddValueBlock(program); const auto AddSource = [&program](uint32_t dword_count) { DescriptorSource source; source.dword_count = dword_count; for (uint32_t i = 0; i < dword_count; i++) { source.dwords[i] = Value(0u); } program.descriptor_sources.push_back(source); return static_cast(program.descriptor_sources.size() - 1u); }; const auto image0 = AddSource(8); const auto image1 = AddSource(8); const auto sampler0 = AddSource(4); const auto sampler1 = AddSource(4); program.descriptor_sources[image1].dwords[0] = Value(1u); program.descriptor_sources[image1].dwords[1] = Value(static_cast( Libs::Graphics::Prospero::BufferFormat::k11_11_10UInt) << 20u); program.descriptor_sources[image1].dwords[3] = Value(static_cast( Libs::Graphics::Prospero::ImageType::kColor2D) << 28u); for (uint32_t index = 0; index < 2; ++index) { auto &value = block.AppendNewInst( ValueOpcode::GetUserData, {Value(static_cast(index))}); program.descriptor_sources[sampler0 + index].dwords[0] = Value(&value); } program.info.images.push_back( {.source = image0, .resource_class = ImageResourceClass::Sampled, .numeric_class = Libs::Graphics::Prospero::TextureNumericClass::Float, .dimension = Libs::Graphics::ShaderRecompiler::Decoder::ImageDimension::Dim2D}); program.info.images.push_back( {.source = image1, .resource_class = ImageResourceClass::Sampled, .numeric_class = Libs::Graphics::Prospero::TextureNumericClass::Float, .dimension = Libs::Graphics::ShaderRecompiler::Decoder::ImageDimension::Dim2D}); program.info.samplers.push_back({.source = sampler0}); program.info.samplers.push_back({.source = sampler1}); program.info.sampled_pairs.push_back({.image = 0, .sampler = 0}); program.info.sampled_pairs.push_back({.image = 0, .sampler = 1}); program.info.sampled_pairs.push_back({.image = 1, .sampler = 1}); return program; } void TestMappedSrtUsesDirectReaderByDefault() { using namespace Libs::Graphics::ShaderRecompiler::IR; const uint32_t dword = 0x12345678; auto plan = ExtractResourcePlan(SrtProgram(reinterpret_cast(&dword))); uint32_t specialization_reads = 0; const SrtRuntime runtime{.userdata = &specialization_reads, .read_specialization_memory = RejectSpecializationRead}; ResourceSnapshot snapshot; ResourceSpecialization specialization; Check(MaterializeResources(plan, runtime, snapshot, specialization), "mapped SRT stage materialization failed"); Check(specialization_reads == 0, "ordinary SRT read used the specialization reader"); Check(snapshot.flattened_srt.size() == 1 && snapshot.flattened_srt[0] == dword, "cache rematerialization did not use the direct reader by default"); } void TestIntegerRuntimeValueFollowsSrtReads() { using namespace Libs::Graphics::ShaderRecompiler::IR; auto plan = SrtProgram(0x10000); const auto root = plan.descriptor_sources.front().dwords[0]; Check(ValidateRuntimeValue(plan, root, RuntimeValueType::Integer), "integer SRT read was rejected"); Block values; auto &comparison = values.AppendNewInst(ValueOpcode::FPOrdLessThanEqual32, {Value::F32(1.f), Value::F32(0.f)}); auto &selection = values.AppendNewInst( ValueOpcode::SelectU32, {Value(&comparison), Value(1u), Value(0u)}); plan.srt_reads[0].value = Value(&selection); Check(ValidateRuntimeValue(plan, root), "ordinary SRT validation rejected a floating-point dependency"); Check(!ValidateRuntimeValue(plan, root, RuntimeValueType::Integer), "integer SRT validation missed a hidden floating-point dependency"); auto &first = values.AppendNewInst(ValueOpcode::ReadFirstLane, {root, Value(true)}); Check(!ValidateRuntimeValue(plan, Value(&first), RuntimeValueType::Integer), "read-first-lane lost integer-only SRT validation"); auto &active = values.AppendNewInst(ValueOpcode::ReadFirstLane, {Value(&selection), Value(&comparison)}); Check(!ValidateRuntimeValue(plan, Value(&active), RuntimeValueType::Integer), "floating-point execution mask was accepted as integer-only"); auto &lane = values.AppendNewInst( ValueOpcode::GetBuiltin, {Value(static_cast(StageInputKind::LocalInvocationId)), Value(0u)}); auto &mask = values.AppendNewInst(ValueOpcode::INotEqual32, {Value(&lane), Value(0u)}); selection.SetArg(0, Value(&mask)); active.SetArg(1, Value(&mask)); Check(ValidateRuntimeValue(plan, Value(&active), RuntimeValueType::Integer), "nonuniform integer execution mask was rejected"); auto &float_value = values.AppendNewInst(ValueOpcode::BitCastU32F32, {Value::F32(1.f)}); selection.SetArg(2, Value(&float_value)); Check(!ValidateRuntimeValue(plan, Value(&active), RuntimeValueType::Integer), "floating-point inactive arm was accepted as integer-only"); plan.srt_reads[0].value = Value(&first); Check(!ValidateRuntimeValue(plan, root, RuntimeValueType::Integer), "cyclic SRT read-first-lane dependency was accepted"); } void TestSrtAliasesRetainReadPolicy() { using namespace Libs::Graphics::ShaderRecompiler::IR; auto program = SrtProgram(0x1000u); auto &block = *program.blocks[0]; const auto append_read = [&](uint32_t address) { auto &handle = block.AppendNewInst(ValueOpcode::GetAddressResource, {Value(address), Value(0u)}); auto &read = block.AppendNewInst(ValueOpcode::LoadAddressU32, {Value(&handle), Value(0u), Value(0u), Value(true)}); // Different reads deliberately share MemoryInfo but have different guest PCs. read.SetFlags(MemoryFlags{.index = 0, .pc = address}); const auto slot = static_cast(program.srt_reads.size()); program.srt_reads.push_back({Value(&read), slot}); auto &srt = block.AppendNewInst(ValueOpcode::GetSrtResource); return Value(&block.AppendNewInst(ValueOpcode::ReadConst, {Value(&srt), Value(slot)})); }; const auto ordinary = append_read(0x2000u); const auto pointer = append_read(0x3000u); program.srt_reads[0].value.Instruction()->Arg(0).Instruction()->SetArg(0, pointer); auto &mask = block.AppendNewInst(ValueOpcode::INotEqual32, {ordinary, Value(0u)}); auto &first = block.AppendNewInst(ValueOpcode::ReadFirstLane, {Value(9u), Value(&mask)}); program.descriptor_sources.push_back({.dwords = {Value(&first), Value(0u)}, .dword_count = 2}); DescriptorSource indirect; indirect.indirect_descriptor.emplace(DescriptorSource::IndirectDescriptor{}).sources = {0, 1}; program.descriptor_sources.push_back(indirect); auto plan = ExtractResourcePlan(program); for (const auto &inst : plan.value_storage) { Check(inst.GetOpcode() != ValueOpcode::ReadConst && inst.GetOpcode() != ValueOpcode::GetSrtResource, "retained plan still contains flattened-read aliases"); } Check(plan.descriptor_sources[0].dwords[0] == plan.srt_reads[0].value && plan.srt_reads[0].value.Instruction()->Flags().clean == 1u && plan.srt_reads[1].value.Instruction()->Flags().clean == 0u && plan.srt_reads[2].value.Instruction()->Flags().clean == 1u, "alias normalization lost read identity or retained discarded EXEC provenance"); struct Reads { uint32_t strict = 0; uint32_t ordinary = 0; bool dirty = false; } reads; const SrtRuntime runtime{ .read_memory = +[](void *data, uint64_t address, std::span words) { ++static_cast(data)->ordinary; if (address == 0x2000u) words[0] = 0x2222u; else if (address == 0x3000u) words[0] = 0x8000u; else if (address == 0x8000u) words[0] = 0xeeeeu; else return false; return true; }, .userdata = &reads, .read_specialization_memory = +[](void *data, uint64_t address, std::span words) { auto &reads = *static_cast(data); ++reads.strict; if (reads.dirty) return false; if (address == 0x3000u) words[0] = 0x1000u; else if (address == 0x1000u) words[0] = 0x1111u; else return false; return true; }}; std::vector flat; DescriptorValue descriptor; { SrtWalker clean(plan, CleanRuntime(runtime)); SrtWalker walker(plan, runtime, &clean); Check(walker.RefreshFlatBuffer(flat) && flat == std::vector{0x1111u, 0x2222u, 0x1000u} && walker.EvaluateDescriptor(0, descriptor) && descriptor.dwords[0] == 0x1111u && reads.strict == 2 && reads.ordinary == 1, "normalized aliases changed nested strict reads or repeated a shared read"); } Check(!SrtWalker(plan, runtime).RefreshFlatBuffer(flat), "strict flat read accepted a missing clean evaluator"); auto no_reader = runtime; no_reader.read_specialization_memory = nullptr; { SrtWalker clean(plan, CleanRuntime(no_reader)); Check(!SrtWalker(plan, no_reader, &clean).RefreshFlatBuffer(flat), "strict flat read accepted a missing strict reader"); } Check(SrtWalker(plan, runtime).EvaluateDescriptor(0, descriptor) && descriptor.dwords[0] == 0xeeeeu && reads.strict == 2 && reads.ordinary == 3, "direct descriptor evaluation without a clean evaluator changed read domains"); reads.dirty = true; { SrtWalker clean(plan, CleanRuntime(runtime)); Check(!SrtWalker(plan, runtime, &clean).RefreshFlatBuffer(flat) && reads.strict == 3 && reads.ordinary == 3, "dirty strict pointer fell back to an ordinary read"); } } void TestUniformVectorDescriptorRead() { using namespace Libs::Graphics::ShaderRecompiler::IR; Program program; program.stage = Libs::Graphics::ShaderType::Compute; program.srt_plan_complete = program.resource_tracking_complete = true; auto &block = AddValueBlock(program); auto &handle = block.AppendNewInst(ValueOpcode::GetBufferResource, {Value(0x1000u), Value(4u << 16u), Value(1u), Value(0x16204u)}); program.memory_info.push_back({.kind = ResourceKind::Buffer}); auto &count = block.AppendNewInst(ValueOpcode::LoadBufferU32, {Value(&handle), Value(0u), Value(0u), Value(0u), Value(true)}); count.SetFlags(MemoryFlags{.index = 0}); // The captured indirect kernel shares one read across sibling scalar lane reads, // enclosed by a different EXEC mask. Its resource plan must share that read too. auto &lane = block.AppendNewInst(ValueOpcode::GetBuiltin, {Value(static_cast(StageInputKind::LocalInvocationId)), Value(0u)}); auto &active = block.AppendNewInst(ValueOpcode::ULessThan32, {Value(&lane), Value(32u)}); auto &first = block.AppendNewInst(ValueOpcode::ReadFirstLane, {Value(&count), Value(true)}); auto &next = block.AppendNewInst(ValueOpcode::IAdd32, {Value(&count), Value(1u)}); auto &second = block.AppendNewInst(ValueOpcode::ReadFirstLane, {Value(&next), Value(true)}); auto &sum = block.AppendNewInst(ValueOpcode::IAdd32, {Value(&first), Value(&second)}); auto &small = block.AppendNewInst(ValueOpcode::ULessThanEqual32, {Value(&count), Value(72u)}); auto &selected = block.AppendNewInst(ValueOpcode::SelectU32, {Value(&small), Value(&sum), Value(&count)}); auto &masked = block.AppendNewInst(ValueOpcode::SelectU32, {Value(&active), Value(&selected), Value(0u)}); auto &records = block.AppendNewInst(ValueOpcode::ReadFirstLane, {Value(&masked), Value(&active)}); DescriptorSource source; source.dword_count = 4; source.dwords = {Value(0x2000u), Value(4u << 16u), Value(&records), Value(0x16204u)}; program.descriptor_sources.push_back(source); program.info.buffers.push_back({.source = 0, .written = true}); Check(ValidateRuntimeValue(program, Value(&count)), "uniform DWORD count was rejected"); struct Reads { uint32_t value = 72; uint32_t strict = 0; uint32_t ordinary = 0; bool clean = true; } reads; const SrtRuntime runtime{ .read_memory = [](void *data, uint64_t, std::span words) { ++static_cast(data)->ordinary; words[0] = 999; return true; }, .userdata = &reads, .read_specialization_memory = [](void *data, uint64_t address, std::span words) { auto &reads = *static_cast(data); ++reads.strict; if (!reads.clean || address != 0x1000u || words.size() != 1) return false; words[0] = reads.value; return true; }}; ResourceSnapshot snapshot; ResourceSpecialization specialization; for (const bool written : {false, true}) { program.info.buffers[0].written = written; auto plan = ExtractResourcePlan(program); Check(plan.control_flow.empty() && plan.requires_specialization_memory && plan.capture_specialization_reads, "vector descriptor read depended on incidental control-flow capture"); reads = {}; Check(MaterializeResources(plan, runtime, snapshot, specialization) && snapshot.buffers[0].dwords[2] == 145 && reads.strict == 1 && reads.ordinary == 0 && snapshot.specialization_reads == std::vector>{{0x1000u, 4u}}, "vector descriptor input was not read and captured exactly once"); reads.clean = false; reads.strict = 0; Check(!MaterializeResources(plan, runtime, snapshot, specialization) && reads.strict == 1 && reads.ordinary == 0, "dirty vector descriptor input fell back to an ordinary memory read"); } count.SetArg(4, Value(false)); auto &inactive = block.AppendNewInst(ValueOpcode::ReadFirstLane, {Value(&count), Value(false)}); program.descriptor_sources.push_back({.dwords = {Value(&inactive)}, .dword_count = 1}); reads.strict = 0; uint32_t result = 99; { const auto plan = ExtractResourcePlan(program); Check(SrtWalker(plan, runtime).Evaluate(plan.descriptor_sources.back().dwords[0], result) && result == 0 && reads.strict == 0 && reads.ordinary == 0, "literal false EXEC read vector memory"); } count.SetArg(4, Value(true)); handle.SetArg(3, Value(0x204u)); program.descriptor_sources.back().dwords[0] = Value(&count); { const auto plan = ExtractResourcePlan(program); Check(SrtWalker(plan, runtime).Evaluate(plan.descriptor_sources.back().dwords[0], result) && result == 0 && reads.strict == 0 && reads.ordinary == 0, "invalid vector buffer format read memory"); } handle.SetArg(3, Value(0x16204u)); count.SetArg(1, Value(&lane)); Check(!ValidateRuntimeValue(program, Value(&count)), "varying vector address was treated as a uniform descriptor read"); } void TestExactReciprocalDescriptorArithmetic() { using namespace Libs::Graphics::ShaderRecompiler::IR; Program program; auto &block = AddValueBlock(program); for (const float divisor : {64.f, 128.f, 256.f, 512.f, std::numeric_limits::min(), std::bit_cast(253u << 23u)}) { auto &reciprocal = block.AppendNewInst(ValueOpcode::FPRecipIFlag32, {Value::F32(divisor)}); uint32_t result = 0; Check(SrtWalker(program, {}).Evaluate(Value(&reciprocal), result) && result == std::bit_cast(1.f / divisor), "power-of-two reciprocal was not exact"); } for (const float divisor : {0.f, 3.f, -64.f, std::numeric_limits::infinity(), std::numeric_limits::denorm_min(), std::bit_cast(254u << 23u)}) { auto &reciprocal = block.AppendNewInst(ValueOpcode::FPRecipIFlag32, {Value::F32(divisor)}); uint32_t result = 0; Check(!SrtWalker(program, {}).Evaluate(Value(&reciprocal), result), "unsupported reciprocal rounding or exceptional input was accepted"); } } void TestUnbasedFlatCacheHitMaterializes() { using namespace Libs::Graphics::ShaderRecompiler::IR; auto plan = UnbasedFlatPlan(); ResourceSnapshot snapshot; ResourceSpecialization specialization; Check(MaterializeResources(plan, {}, snapshot, specialization), "unbased FLAT stage materialization failed"); Check(snapshot.buffers.empty() && snapshot.images.empty(), "unbased FLAT plan produced unexpected descriptors"); } void TestWrittenDescriptorPredicateReads(bool memory_condition) { using namespace Libs::Graphics::ShaderRecompiler::IR; Program program; program.stage = Libs::Graphics::ShaderType::Compute; program.user_data_count = 1; program.srt_plan_complete = true; program.resource_tracking_complete = true; auto &block = AddValueBlock(program); program.memory_info.push_back({.kind = ResourceKind::ScalarAddress}); auto &handle = block.AppendNewInst(ValueOpcode::GetAddressResource, {Value(0x1000u), Value(0u)}); auto &offset = block.AppendNewInst(ValueOpcode::GetUserData, {Value(static_cast(0))}); auto &read = block.AppendNewInst(ValueOpcode::LoadAddressU32, {Value(&handle), Value(&offset), Value(0u), Value(false)}); read.SetFlags(MemoryFlags{.index = 0}); DescriptorSource source; source.dwords = {Value(&read), Value(0u), Value(4u), Value(0u)}; source.dword_count = 4; program.descriptor_sources.push_back(source); program.info.buffers.push_back({.source = 0, .written = true}); // Only memory-derived host decisions need clean, disjoint descriptor reads. auto &condition = block.AppendNewInst(ValueOpcode::IEqual32, {memory_condition ? Value(&read) : Value(&offset), Value(memory_condition ? 0x8000u : 4u)}); auto &store_block = AddValueBlock(program); AddValueBlock(program); program.blocks[0]->condition = Value(&condition); program.blocks[0]->terminator.kind = Libs::Graphics::ShaderRecompiler::CFG::TerminatorKind::ConditionalBranch; program.blocks[0]->terminator.true_block = program.blocks[1]; program.blocks[0]->terminator.false_block = program.blocks[2]; program.blocks[1]->id = 1; program.blocks[1]->terminator.kind = Libs::Graphics::ShaderRecompiler::CFG::TerminatorKind::Return; program.blocks[2]->id = 2; program.blocks[2]->terminator.kind = Libs::Graphics::ShaderRecompiler::CFG::TerminatorKind::Return; program.memory_info.push_back({.kind = ResourceKind::Buffer, .resource = 0}); auto &output = store_block.AppendNewInst(ValueOpcode::GetBufferResource, {source.dwords[0], source.dwords[1], source.dwords[2], source.dwords[3]}); store_block.AppendNewInst(ValueOpcode::StoreBufferU32, {Value(&output), Value(0u), Value(0u), Value(0u), Value(1u), Value(true)}) .SetFlags(MemoryFlags{.index = 1}); auto plan = ExtractResourcePlan(program); Check(plan.capture_specialization_reads == memory_condition && !plan.control_flow.empty(), "conditional writable descriptor used the wrong memory dependency policy"); struct Reads { uint32_t ordinary = 0; uint32_t strict = 0; bool clean = false; } reads; const std::array user_data{4u}; const SrtRuntime runtime{ .user_data = user_data, .read_memory = +[](void *data, uint64_t, std::span words) { ++static_cast(data)->ordinary; words[0] = 0x8000u; return true; }, .userdata = &reads, .read_specialization_memory = +[](void *data, uint64_t address, std::span words) { auto &reads = *static_cast(data); ++reads.strict; if (!reads.clean || address != 0x1004u) return false; words[0] = 0x8000u; return true; }}; ResourceSnapshot snapshot; ResourceSpecialization specialization; for (const bool clean : {false, true}) { reads = {.clean = clean}; // A failed predicate explores both edges and the writable descriptor retries its clean read. const bool expected = !memory_condition || clean; Check(MaterializeResources(plan, runtime, snapshot, specialization) == expected && reads.ordinary == (memory_condition ? 0u : 1u) && reads.strict == (memory_condition ? (clean ? 1u : 2u) : 0u), "writable descriptor changed reader policy or bypassed a dirty memory predicate"); if (!expected) continue; Check(snapshot.buffers[0].dwords[0] == 0x8000u && snapshot.specialization_reads == (memory_condition ? std::vector>{{0x1004u, 4u}} : std::vector>{}), "writable descriptor lost its address or captured an immutable predicate"); } } void TestFailedMaterializationRejectsStage() { using namespace Libs::Graphics::ShaderRecompiler::IR; auto plan = UserDataBufferPlan(); ResourceSnapshot snapshot; ResourceSpecialization specialization; Check(!MaterializeResources(plan, {}, snapshot, specialization), "missing runtime user data did not reject the cached stage"); } void TestFiniteImageRefreshReusesScalarReads() { using namespace Libs::Graphics::ShaderRecompiler::IR; Program program; program.stage = Libs::Graphics::ShaderType::Compute; program.srt_plan_complete = true; program.resource_tracking_complete = true; auto &block = AddValueBlock(program); program.memory_info.push_back({.kind = ResourceKind::ScalarAddress}); auto &handle = block.AppendNewInst(ValueOpcode::GetAddressResource, {Value(0x1000u), Value(0u)}); auto &srt = block.AppendNewInst(ValueOpcode::GetSrtResource); for (uint32_t index = 0; index < 3; ++index) { auto &read = block.AppendNewInst(ValueOpcode::LoadAddressU32, {Value(&handle), Value(index * 4u), Value(0u), Value(true)}); read.SetFlags(MemoryFlags{.index = 0}); program.srt_reads.push_back({Value(&read), index}); auto &flat = block.AppendNewInst(ValueOpcode::ReadConst, {Value(&srt), Value(index)}); DescriptorSource source; source.dword_count = 8; source.dwords.fill(Value(0u)); source.dwords[0] = Value(&flat); source.dwords[1] = Value(static_cast( Libs::Graphics::Prospero::BufferFormat::k32_32_32_32Float) << 20u); source.dwords[3] = Value(Libs::Graphics::DstSel(4, 5, 6, 7) | (static_cast(Libs::Graphics::Prospero::ImageType::kColor2D) << 28u)); program.descriptor_sources.push_back(source); } DescriptorSource root; root.dword_count = 8; root.dwords.fill(Value(0u)); root.indirect_descriptor.emplace(DescriptorSource::IndirectDescriptor{}).sources = {0, 1, 2, 1}; program.descriptor_sources.push_back(root); program.info.images.push_back({ .source = 3, .resource_class = ImageResourceClass::Sampled, .numeric_class = Libs::Graphics::Prospero::TextureNumericClass::Float, .dimension = Libs::Graphics::ShaderRecompiler::Decoder::ImageDimension::Dim2D}); auto plan = ExtractResourcePlan(program); struct Reads { std::array words{0x100u, 0x200u, 0x200u}; std::array counts{}; uint32_t ordinary = 0; } reads; const SrtRuntime runtime{ .read_memory = +[](void *data, uint64_t, std::span) { ++static_cast(data)->ordinary; return false; }, .userdata = &reads, .read_specialization_memory = +[](void *data, uint64_t address, std::span words) { if (words.size() != 1 || address < 0x1000u || address >= 0x100cu || (address & 3u) != 0) return false; auto &reads = *static_cast(data); const auto index = (address - 0x1000u) / 4u; ++reads.counts[index]; words[0] = reads.words[index]; return true; }}; ResourceSnapshot snapshot; ResourceSpecialization specialization; const auto capacities = [&] { return std::array{snapshot.images.capacity(), snapshot.flattened_srt.capacity(), snapshot.specialization_reads.capacity(), specialization.images.capacity()}; }; const auto check_mapping = [&](std::array ordinals) { const auto offset = specialization.images[0].indirect_mapping_offset; Check(snapshot.images.size() == 2 && snapshot.flattened_srt[offset] == 4, "finite image candidates were not deduplicated"); for (uint32_t key = 0; key < ordinals.size(); ++key) { Check(snapshot.flattened_srt[offset + 1u + key * 2u] == key && snapshot.flattened_srt[offset + 2u + key * 2u] == ordinals[key], "finite image selector mapping is stale or incorrect"); } }; Check(MaterializeResources(plan, runtime, snapshot, specialization), "finite image materialization failed"); Check(reads.ordinary == 0 && reads.counts == std::array{1, 1, 1} && snapshot.specialization_reads.size() == 3, "finite image candidates repeated scalar reads or bypassed clean provenance"); check_mapping({0, 1, 1, 1}); const auto warm_capacities = capacities(); reads.words = {0x300u, 0x300u, 0x400u}; Check(MaterializeResources(plan, runtime, snapshot, specialization), "finite image refresh failed after descriptor changes"); Check(reads.ordinary == 0 && reads.counts == std::array{2, 2, 2} && snapshot.specialization_reads.size() == 3 && snapshot.images[0].dwords[0] == 0x300u && snapshot.images[1].dwords[0] == 0x400u, "finite image refresh retained old scalar values or repeated reads"); check_mapping({0, 0, 1, 0}); Check(capacities() == warm_capacities, "finite image refresh grew reusable resource storage after warmup"); } void TestMixedSamplerVariantsShareRuntimeDescriptor() { using namespace Libs::Graphics::ShaderRecompiler::IR; auto program = MixedSamplerProgram(); auto plan = ExtractResourcePlan(program); std::array user_data{0x11111111u, 0x22222222u}; const SrtRuntime runtime{.user_data = user_data}; ResourceSnapshot snapshot; ResourceSpecialization specialization; Check(MaterializeResources(plan, runtime, snapshot, specialization), "mixed sampler materialization failed"); ApplyResourceSpecialization(program, specialization); const auto &samplers = program.info.samplers; Check(snapshot.samplers.size() == 2 && samplers.size() == 3 && samplers[0].snapshot_index == 0 && samplers[1].snapshot_index == 1 && samplers[2].snapshot_index == 1 && samplers[0].source == plan.info.samplers[0].source && samplers[1].source == plan.info.samplers[1].source && samplers[2].source == samplers[1].source && !samplers[1].force_point_filtering && samplers[2].force_point_filtering && program.info.sampled_pairs[2].sampler == 2, "native sampler variants lost their source identity or binding order"); const auto capacity = snapshot.samplers.capacity(); user_data[1] = 0x33333333u; Check(MaterializeResources(plan, runtime, snapshot, specialization) && snapshot.samplers.size() == 2 && snapshot.samplers.capacity() == capacity && snapshot.samplers[samplers[0].snapshot_index].dwords[0] == user_data[0] && snapshot.samplers[samplers[1].snapshot_index].dwords[0] == user_data[1] && snapshot.samplers[samplers[2].snapshot_index].dwords[0] == user_data[1], "sampler variants retained stale or duplicated descriptors after refresh"); } } // namespace namespace Common { int DbgExitHandler(const char *, int, std::string_view) { std::abort(); } int DbgExitHandler(const char *, int, fmt::text_style, std::string_view) { std::abort(); } int DbgExitIfHandler(const char *, const char *, int) { return 1; } void DbgExit(int) { std::abort(); } } // namespace Common int main() { TestMappedSrtUsesDirectReaderByDefault(); TestIntegerRuntimeValueFollowsSrtReads(); TestSrtAliasesRetainReadPolicy(); TestUniformVectorDescriptorRead(); TestExactReciprocalDescriptorArithmetic(); TestUnbasedFlatCacheHitMaterializes(); for (const bool memory_condition : {false, true}) TestWrittenDescriptorPredicateReads(memory_condition); TestFailedMaterializationRejectsStage(); TestFiniteImageRefreshReusesScalarReads(); TestMixedSamplerVariantsShareRuntimeDescriptor(); std::puts("ResourceMaterializationTests: all cases passed"); return 0; } // Keep this focused standalone target self-contained by amalgamating its small // typed-IR implementation set. #include "graphics/shader/recompiler/ir/Block.cpp" #include "graphics/shader/recompiler/ir/Program.cpp" #include "graphics/shader/recompiler/ir/Type.cpp" #include "graphics/shader/recompiler/ir/Value.cpp" #include "graphics/shader/recompiler/ir/opcodes/ValueOpcodes.cpp"