From dea80f34ca96644a4677da7cd2a575088e626e4c Mon Sep 17 00:00:00 2001 From: nmzik Date: Sat, 5 Sep 2026 10:34:30 +0400 Subject: [PATCH] shader: detect full buffer fills from uniform stores Co-authored-by: Techx3 <56268269+Techx3@users.noreply.github.com> --- .../host_gpu/renderer/renderCompute.cpp | 69 +++++---- .../shader/recompiler/ir/ResourceSnapshot.h | 10 ++ src/graphics/shader/recompiler/ir/ShaderIR.h | 10 +- .../ir/passes/ResourceMaterialization.cpp | 139 +++++++++++++++++- .../shader/recompiler/ir/passes/SrtWalker.cpp | 18 +++ .../shader/recompiler/ir/passes/SrtWalker.h | 2 + tests/ResourceTrackingTests.cpp | 108 +++++++++++++- tests/ShaderRecompilerComputeTests.cpp | 109 +++++++++++++- 8 files changed, 422 insertions(+), 43 deletions(-) diff --git a/src/graphics/host_gpu/renderer/renderCompute.cpp b/src/graphics/host_gpu/renderer/renderCompute.cpp index b0238875b..cc31c2a1b 100644 --- a/src/graphics/host_gpu/renderer/renderCompute.cpp +++ b/src/graphics/host_gpu/renderer/renderCompute.cpp @@ -25,6 +25,7 @@ #include "libs/errno.h" #include +#include #include #include #include @@ -82,46 +83,54 @@ bool ResolveComputeImageClear(const ShaderComputeInputInfo& input, uint32_t grou uint64_t& resolved_size) { const auto& program = *input.stage.program; const auto& resources = input.stage.resources; - if (program.info.buffers.size() != 1 || resources.buffers.size() != 1 || - !program.info.images.empty() || !program.info.samplers.empty() || program.info.uses_dma || - !resources.images.empty() || !resources.samplers.empty()) { + const auto& fill = resources.buffer_fill; + if (fill.element_size == 0 || fill.element_size > 16 || fill.element_size % 4 != 0 || + fill.buffer >= resources.buffers.size() || + resources.buffers.size() != program.info.buffers.size()) { return false; } - const auto& resource = program.info.buffers.front(); - const auto& raw = resources.buffers.front(); - const auto descriptor = DecodeNativeDescriptor(raw); + const auto& resource = program.info.buffers[fill.buffer]; + const auto& raw = resources.buffers[fill.buffer]; + const auto descriptor = DecodeNativeDescriptor(raw); + constexpr std::array formats { + Prospero::BufferFormat::k32UInt, Prospero::BufferFormat::k32_32UInt, + Prospero::BufferFormat::k32_32_32UInt, Prospero::BufferFormat::k32_32_32_32UInt}; if (!resource.formatted || !resource.written || resource.read || resource.atomic || - resource.scalar || resource.max_byte_extent != 16 || descriptor.Stride() != 16 || - descriptor.Format() != Prospero::BufferFormat::k32_32_32_32UInt || - descriptor.SwizzleEnabled() || descriptor.IndexStride() != 0 || descriptor.AddTid() || + resource.scalar || descriptor.Stride() != fill.element_size || + descriptor.Format() != formats[fill.element_size / 4 - 1] || descriptor.SwizzleEnabled() || + descriptor.IndexStride() != 0 || descriptor.AddTid() || resource.packed_stride != descriptor.PackedStride() || raw.dword_count != 4 || - program.user_data_base != 0 || resources.user_data.size() != 8) { + descriptor.Base48() == 0) { return false; } - for (uint32_t i = 0; i < raw.dword_count; i++) { - if (raw.dwords[i] != resources.user_data[i]) { + if (input.threads_num[0] == 0 || input.threads_num[0] != fill.group_stride || + input.threads_num[1] != 1 || input.threads_num[2] != 1 || group_x == 0 || group_y != 1 || + group_z != 1 || mode != (input.dispatch_thread_dimensions ? 0x61u : 0x41u)) { + return false; + } + const uint64_t invocations = input.dispatch_thread_dimensions + ? group_x + : static_cast(group_x) * input.threads_num[0]; + const auto size = BufferDescriptorSize(descriptor); + if (invocations != descriptor.NumRecords() || size == 0 || size > UINT32_MAX || + descriptor.Base48() > UINT64_MAX - size || + (input.dispatch_thread_dimensions && + (group_x % input.threads_num[0] != 0 || input.dispatch_threads_num[0] != group_x || + input.dispatch_threads_num[1] != 1 || input.dispatch_threads_num[2] != 1))) { + return false; + } + for (uint32_t i = 0; i < resources.buffers.size(); ++i) { + if (i == fill.buffer) continue; + const auto source = DecodeNativeDescriptor(resources.buffers[i]); + const auto bytes = BufferDescriptorSize(source); + if (source.Base48() > UINT64_MAX - bytes || + (source.Base48() < descriptor.Base48() + size && + descriptor.Base48() < source.Base48() + bytes)) { return false; } } - const uint32_t clear = resources.user_data[4]; - if (resources.user_data[5] != clear || resources.user_data[6] != clear || - resources.user_data[7] != clear) { - return false; - } - const bool full_dispatch = - input.dispatch_thread_dimensions && input.threads_num[0] == 64 && - input.threads_num[1] == 1 && input.threads_num[2] == 1 && group_x != 0 && group_y == 1 && - group_z == 1 && input.dispatch_threads_num[0] == group_x && - input.dispatch_threads_num[1] == 1 && input.dispatch_threads_num[2] == 1 && - input.group_id[0] && !input.group_id[1] && !input.group_id[2] && - input.thread_ids_num == 1 && input.wave_size == 64 && !input.tg_size_en && mode == 0x61u && - group_x % input.threads_num[0] == 0 && descriptor.NumRecords() == group_x; - const auto size = BufferDescriptorSize(descriptor); - if (!full_dispatch || size == 0) { - return false; - } resolved_descriptor = descriptor; - resolved_clear = clear; + resolved_clear = fill.value; resolved_size = size; return true; } diff --git a/src/graphics/shader/recompiler/ir/ResourceSnapshot.h b/src/graphics/shader/recompiler/ir/ResourceSnapshot.h index b74ea9d4e..db08b3ecf 100644 --- a/src/graphics/shader/recompiler/ir/ResourceSnapshot.h +++ b/src/graphics/shader/recompiler/ir/ResourceSnapshot.h @@ -16,12 +16,22 @@ struct DescriptorValue { } }; +struct BufferFill { + uint32_t buffer = 0; + uint32_t group_stride = 0; + uint32_t element_size = 0; + uint32_t value = 0; + + bool operator==(const BufferFill&) const = default; +}; + struct ResourceSnapshot { std::vector buffers; std::vector images; std::vector samplers; std::vector flattened_srt; std::vector user_data; + BufferFill buffer_fill; }; } // namespace Libs::Graphics::ShaderRecompiler::IR diff --git a/src/graphics/shader/recompiler/ir/ShaderIR.h b/src/graphics/shader/recompiler/ir/ShaderIR.h index fce7374d5..eb23ff179 100644 --- a/src/graphics/shader/recompiler/ir/ShaderIR.h +++ b/src/graphics/shader/recompiler/ir/ShaderIR.h @@ -490,8 +490,13 @@ struct CompiledShaderInfo { BindingLayout bindings; }; -// Immutable runtime resource analysis retained by the shader cache. It owns the descriptor/SRT -// and uniform condition values, plus resource-use edges, without retaining translated blocks. +struct BufferFillPlan { + BufferFill fill; + std::array values; +}; + +// Immutable runtime resource analysis retained by the shader cache. It owns descriptor/SRT, +// uniform condition and fill values without retaining translated blocks. struct ResourcePlan { ResourcePlan() = default; ~ResourcePlan(); @@ -516,6 +521,7 @@ struct ResourcePlan { bool srt_plan_complete = false; bool resource_tracking_complete = false; ShaderInfo info; + BufferFillPlan buffer_fill; }; struct Program: ResourcePlan { diff --git a/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp b/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp index 8f7e9f7a3..569bce60b 100644 --- a/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp +++ b/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp @@ -299,8 +299,19 @@ static bool MaterializeSnapshot(const ResourcePlan& program, const SrtRuntime& r return false; } - auto& next = snapshot.resources; - auto cursor = values.begin(); + auto& next = snapshot.resources; + const auto& fill = program.buffer_fill; + const auto words = fill.fill.element_size / sizeof(uint32_t); + std::array stored {}; + if (words != 0 && + EvaluateUniformValues(program, std::span(fill.values).first(words), runtime, + std::span(stored).first(words)) && + std::all_of(stored.begin(), stored.begin() + words, + [&](uint32_t value) { return value == stored[0]; })) { + next.buffer_fill = fill.fill; + next.buffer_fill.value = stored[0]; + } + auto cursor = values.begin(); next.buffers.assign(cursor, cursor + program.info.buffers.size()); cursor += program.info.buffers.size(); next.flattened_srt = std::move(flattened_srt); @@ -609,11 +620,11 @@ bool BuildSamplerPlan(const ShaderInfo& base, const Images& images, SamplerPlan& return true; } -static bool IntegerCondition(const ResourcePlan& program, Value condition) { - if (!ValidateRuntimeValue(program, condition)) { +static bool UniformIntegerValue(const ResourcePlan& program, Value root) { + if (!ValidateRuntimeValue(program, root)) { return false; } - std::vector pending {condition}; + std::vector pending {root}; std::unordered_set visited; while (!pending.empty()) { const auto value = pending.back().Resolve(); @@ -659,7 +670,7 @@ static std::vector ResourceControlFlow(const Program& program) { case CFG::TerminatorKind::Branch: successors.push_back(terminator.true_block); break; case CFG::TerminatorKind::ConditionalBranch: successors = {terminator.true_block, terminator.false_block}; - if (IntegerCondition(program, info.condition)) { + if (UniformIntegerValue(program, info.condition)) { block.condition = info.condition; } break; @@ -713,6 +724,118 @@ static std::vector ResourceControlFlow(const Program& program) { return blocks; } +// Nonnegative affine coefficients for constant, local X and workgroup X. Reject modular +// arithmetic that could wrap; runtime coverage also bounds the largest invocation index. +static std::optional> FillIndex(Value value, uint32_t depth = 0) { + value = value.Resolve(); + if (depth > 32 || value.GetType() != Type::U32) { + return {}; + } + if (value.IsImmediate()) { + return std::array {value.U32(), 0, 0}; + } + const auto* inst = value.TryInstruction(); + if (inst == nullptr) { + return {}; + } + const auto op = inst->GetOpcode(); + if (op == ValueOpcode::GetBuiltin && inst->Arg(1) == Value(0u)) { + if (inst->Arg(0) == Value(static_cast(StageInputKind::LocalInvocationId))) { + return std::array {0, 1, 0}; + } + if (inst->Arg(0) == Value(static_cast(StageInputKind::WorkgroupId))) { + return std::array {0, 0, 1}; + } + } + if (op != ValueOpcode::IAdd32 && op != ValueOpcode::IMul32 && + op != ValueOpcode::ShiftLeftLogical32) { + return {}; + } + auto left = FillIndex(inst->Arg(0), depth + 1); + auto right = FillIndex(inst->Arg(1), depth + 1); + if (!left || !right) { + return {}; + } + if (op == ValueOpcode::IMul32 && ((*right)[1] != 0 || (*right)[2] != 0)) { + std::swap(left, right); + } + if (op != ValueOpcode::IAdd32 && ((*right)[1] != 0 || (*right)[2] != 0)) { + return {}; + } + if (op == ValueOpcode::ShiftLeftLogical32) { + if ((*right)[0] >= 32) return {}; + (*right)[0] = uint64_t {1} << (*right)[0]; + } + for (uint32_t i = 0; i < left->size(); ++i) { + (*left)[i] = + op == ValueOpcode::IAdd32 ? (*left)[i] + (*right)[i] : (*left)[i] * (*right)[0]; + if ((*left)[i] > UINT32_MAX) return {}; + } + return left; +} + +static BufferFillPlan AnalyzeBufferFill(const Program& program) { + if (program.stage != ShaderType::Compute || program.blocks.empty() || + program.blocks.size() != program.block_info.size() || program.info.uses_dma || + !program.info.images.empty() || !program.info.samplers.empty()) { + return {}; + } + std::unordered_set visited; + uint32_t index = 0; + const Inst* store = nullptr; + for (;;) { + if (!visited.insert(index).second) return {}; + for (const auto& inst: *program.blocks[index]) { + if (AddressOpcodeInfoOf(inst.GetOpcode()).access != AddressAccess::None) return {}; + if (!inst.MayHaveSideEffects()) continue; + if (store != nullptr || BufferAccessOf(inst.GetOpcode()) != BufferAccess::Write) + return {}; + store = &inst; + } + const auto& term = program.block_info[index].terminator; + if (term.kind == CFG::TerminatorKind::Return) break; + if (term.kind != CFG::TerminatorKind::Branch) return {}; + const auto next = std::ranges::find(program.block_info, term.true_block, &BlockInfo::id); + if (next == program.block_info.end()) return {}; + index = static_cast(next - program.block_info.begin()); + } + if (store == nullptr || visited.size() != program.blocks.size()) return {}; + const auto op = store->GetOpcode(); + constexpr std::array stores {ValueOpcode::StoreBufferU32, ValueOpcode::StoreBufferU32x2, + ValueOpcode::StoreBufferU32x3, ValueOpcode::StoreBufferU32x4}; + const auto store_op = std::ranges::find(stores, op); + if (store_op == stores.end() || store->Arg(2).Resolve() != Value(0u) || + store->Arg(3).Resolve() != Value(0u) || store->Arg(5).Resolve() != Value(true)) + return {}; + const auto& memory = program.memory_info.at(store->Flags().index); + if (!memory.formatted || memory.typed || !memory.idxen || memory.offen || memory.offset != 0 || + memory.data_bits != 32 || + memory.data_dwords != static_cast(store_op - stores.begin() + 1)) + return {}; + const auto address = FillIndex(store->Arg(1)); + if (!address || (*address)[0] != 0 || (*address)[1] != 1 || (*address)[2] == 0) return {}; + for (const auto& buffer: program.info.buffers) { + if (buffer.read && (!buffer.scalar || buffer.written)) return {}; + } + BufferFillPlan result; + result.fill = {memory.resource, static_cast((*address)[2]), memory.data_dwords * 4, + 0}; + const auto data = store->Arg(4).Resolve(); + const auto* vector = data.TryInstruction(); + constexpr std::array composites {ValueOpcode::CompositeConstructU32x2, + ValueOpcode::CompositeConstructU32x3, + ValueOpcode::CompositeConstructU32x4}; + for (uint32_t i = 0; i < memory.data_dwords; ++i) { + if (memory.data_dwords > 1 && + (vector == nullptr || vector->GetOpcode() != composites[memory.data_dwords - 2])) + return {}; + const auto word = memory.data_dwords == 1 ? data : vector->Arg(i); + if (word.GetType() != Type::U32 || !UniformIntegerValue(program, word)) return {}; + result.values[i] = word; + } + return result; +} + ResourcePlan ExtractResourcePlan(const Program& program) { ResourcePlan plan; plan.stage = program.stage; @@ -772,6 +895,10 @@ ResourcePlan ExtractResourcePlan(const Program& program) { for (auto& block: plan.control_flow) { block.condition = Clone(block.condition); } + plan.buffer_fill = AnalyzeBufferFill(program); + for (uint32_t i = 0; i < plan.buffer_fill.fill.element_size / sizeof(uint32_t); ++i) { + plan.buffer_fill.values[i] = Clone(plan.buffer_fill.values[i]); + } plan.materialization_sources.reserve(plan.info.buffers.size() + plan.info.images.size() + plan.info.samplers.size()); for (const auto& buffer: plan.info.buffers) { diff --git a/src/graphics/shader/recompiler/ir/passes/SrtWalker.cpp b/src/graphics/shader/recompiler/ir/passes/SrtWalker.cpp index 3685224a3..3f64a2c32 100644 --- a/src/graphics/shader/recompiler/ir/passes/SrtWalker.cpp +++ b/src/graphics/shader/recompiler/ir/passes/SrtWalker.cpp @@ -1034,6 +1034,24 @@ void BuildSrtPlan(Program& program) { program.srt_plan_complete = true; } +bool EvaluateUniformValues(const ResourcePlan& program, std::span values, + const SrtRuntime& runtime, std::span results) { + if (values.size() != results.size()) { + return false; + } + auto clean = runtime; + clean.read_memory = runtime.read_specialization_memory != nullptr + ? runtime.read_specialization_memory + : +[](void*, uint64_t, uint32_t*) { return false; }; + Evaluator evaluator(program, clean); + for (size_t i = 0; i < values.size(); ++i) { + if (!evaluator.Evaluate(values[i], results[i])) { + return false; + } + } + return true; +} + bool EvaluateDescriptorSource(const ResourcePlan& program, uint32_t source, const SrtRuntime& runtime, DescriptorValue& result) { std::vector results; diff --git a/src/graphics/shader/recompiler/ir/passes/SrtWalker.h b/src/graphics/shader/recompiler/ir/passes/SrtWalker.h index c7a9163b5..c08aa168a 100644 --- a/src/graphics/shader/recompiler/ir/passes/SrtWalker.h +++ b/src/graphics/shader/recompiler/ir/passes/SrtWalker.h @@ -23,6 +23,8 @@ struct SrtRuntime { // dynamic offsets remain explicit and are never assigned a fake slot. void BuildSrtPlan(Program& program); bool ValidateRuntimeValue(const ResourcePlan& program, Value value); +bool EvaluateUniformValues(const ResourcePlan& program, std::span values, + const SrtRuntime& runtime, std::span results); bool EvaluateDescriptorSource(const ResourcePlan& program, uint32_t source, const SrtRuntime& runtime, DescriptorValue& result); diff --git a/tests/ResourceTrackingTests.cpp b/tests/ResourceTrackingTests.cpp index 0a8e39c2e..ca9ad335d 100644 --- a/tests/ResourceTrackingTests.cpp +++ b/tests/ResourceTrackingTests.cpp @@ -35,7 +35,7 @@ bool SameResourceSnapshot(const ResourceSnapshot &lhs, return lhs.buffers == rhs.buffers && lhs.images == rhs.images && lhs.samplers == rhs.samplers && lhs.flattened_srt == rhs.flattened_srt && - lhs.user_data == rhs.user_data; + lhs.user_data == rhs.user_data && lhs.buffer_fill == rhs.buffer_fill; } template @@ -457,6 +457,111 @@ void TestInvariantIndirectImageMaterialization() { "wrapped scalar immediate entered the invariant image proof"); } +void TestComputeBufferFill() { + struct Options { + bool scalar = false; + bool conditional = false; + bool shifted = false; + bool extra_store = false; + bool clean = false; + bool branch = false; + }; + const auto Run = [](Options options) { + Fixture fixture; + fixture.program.block_info[0].terminator.kind = + Libs::Graphics::ShaderRecompiler::CFG::TerminatorKind::Return; + if (options.branch) { + fixture.program.block_info[0].terminator.kind = Libs::Graphics:: + ShaderRecompiler::CFG::TerminatorKind::ConditionalBranch; + } + const auto buffer = + fixture.Buffer({fixture.UserData(0), fixture.UserData(1), + fixture.UserData(2), fixture.UserData(3)}); + const auto local = fixture.Emit( + ValueOpcode::GetBuiltin, + {Value(static_cast(StageInputKind::LocalInvocationId)), + Value(0u)}); + const auto group = fixture.Emit( + ValueOpcode::GetBuiltin, + {Value(static_cast(StageInputKind::WorkgroupId)), Value(0u)}); + auto index = + fixture.Emit(ValueOpcode::IAdd32, + {local, fixture.Emit(ValueOpcode::ShiftLeftLogical32, + {group, Value(6u)})}); + if (options.shifted) + index = fixture.Emit(ValueOpcode::IAdd32, {index, Value(1u)}); + Value value(0u); + TestMemory memory; + memory.words[0] = 0x40404040u; + if (options.scalar) { + const auto input = + fixture.Buffer({Value(static_cast(memory.base)), + Value(4u << 16), Value(1u), Value(0x14204u)}); + MemoryInfo load; + load.kind = ResourceKind::ScalarBuffer; + value = fixture.Emit(ValueOpcode::ReadConstBuffer, {input, Value(0u)}, + fixture.AddMemory(load, 8)); + } + MemoryInfo store; + store.kind = ResourceKind::Buffer; + store.formatted = true; + store.idxen = true; + const auto flags = fixture.AddMemory(store, 16); + const auto predicate = + options.conditional + ? fixture.Emit(ValueOpcode::ULessThan32, {local, Value(32u)}) + : Value(true); + const auto EmitStore = [&] { + fixture.Emit(ValueOpcode::StoreBufferU32, + {buffer, index, Value(0u), Value(0u), value, predicate}, + flags); + }; + EmitStore(); + if (options.extra_store) + EmitStore(); + fixture.PlanAndTrack(); + auto plan = ExtractResourcePlan(fixture.program); + std::array userdata{0x200000u, 4u << 16, 0x4000u, 0x14204u}; + ResourceSnapshot snapshot; + ResourceSpecialization specialization; + const auto Read = +[](void *data, uint64_t address, uint32_t *word) { + auto &memory = *static_cast(data); + if (address != memory.base) + return false; + ++memory.reads; + *word = memory.words[0]; + return true; + }; + Check(MaterializeResources( + plan, + {.user_data = userdata, + .read_memory = Read, + .userdata = &memory, + .read_specialization_memory = options.clean ? Read : nullptr}, + snapshot, specialization), + "fill fixture did not materialize"); + const bool expected = !options.conditional && !options.shifted && + !options.extra_store && !options.branch && + (!options.scalar || options.clean); + Check((snapshot.buffer_fill.element_size != 0) == expected, + "fill proof accepted an unsafe store or missed the real GTA3 clear"); + if (expected) { + Check(snapshot.buffer_fill.element_size == 4 && + snapshot.buffer_fill.group_stride == 64 && + snapshot.buffer_fill.value == + (options.scalar ? 0x40404040u : 0u), + "fill proof lost address coverage or the actual stored scalar"); + } + }; + Run({}); + Run({.scalar = true, .clean = true}); + Run({.scalar = true}); + Run({.conditional = true, .clean = true}); + Run({.shifted = true, .clean = true}); + Run({.extra_store = true, .clean = true}); + Run({.clean = true, .branch = true}); +} + void TestDenseBufferTracking() { Fixture fixture; std::array userdata; @@ -1650,6 +1755,7 @@ int main() { } }; Run("dense buffers", TestDenseBufferTracking); + Run("compute buffer fill", TestComputeBufferFill); Run("scalar/vector alias", TestScalarAndVectorBufferAlias); Run("runtime unsigned min", TestRuntimeUnsignedMinDescriptor); Run("images and samplers", TestImagesSamplersAndAliases); diff --git a/tests/ShaderRecompilerComputeTests.cpp b/tests/ShaderRecompilerComputeTests.cpp index 25548d540..35656e54f 100644 --- a/tests/ShaderRecompilerComputeTests.cpp +++ b/tests/ShaderRecompilerComputeTests.cpp @@ -22271,7 +22271,7 @@ std::vector MakeGraphicsCases() { void CheckPs5GameExampleImageClearRuntimeShape() { const auto MakeCode = [] { std::vector code; - AppendVop3(&code, 0x347u, 4, 8, InlineU32(6), Vgpr(0)); + AppendVop3(&code, 0x346u, 4, 8, InlineU32(6), Vgpr(0)); for (u32 i = 0; i < 4; i++) { code.push_back(EncodeVop1(0x01u, i, i + 4u)); } @@ -22300,6 +22300,8 @@ void CheckPs5GameExampleImageClearRuntimeShape() { compute.wave_size = 64; compute.thread_ids_num = 1; compute.workgroup_register = 8; + u32 scalar_clear = 0x40404040u; + bool clean_scalar = false; const auto Compile = [&](const std::vector &code) { ShaderRecompiler::CompileOptions options; @@ -22317,6 +22319,13 @@ void CheckPs5GameExampleImageClearRuntimeShape() { const ShaderRecompiler::IR::SrtRuntime runtime{ .user_data = options.user_data, .shader_base = reinterpret_cast(code.data()), + .userdata = &scalar_clear, + .read_specialization_memory = clean_scalar + ? +[](void *data, uint64_t address, uint32_t *word) { + if (address != reinterpret_cast(data)) return false; + *word = *static_cast(data); + return true; + } : nullptr, }; Require("Ps5GameExampleImageClear", "resource materialization", ShaderRecompiler::IR::MaterializeResources( @@ -22353,9 +22362,16 @@ void CheckPs5GameExampleImageClearRuntimeShape() { packed_clear == 0xff000000u, "exact Prospero runtime binding did not resolve to a complete clear"); - auto non_repeated = positive.resources; - non_repeated.user_data[7] ^= 1u; - compute.stage.resources = non_repeated; + auto wrong_index = code; + wrong_index[0] = (wrong_index[0] & ~(0x3ffu << 16u)) | (0x347u << 16u); + compute.stage.resources = Compile(wrong_index).resources; + Require("Ps5GameExampleImageClear", "add-before-shift address", + !ResolveComputeImageClear(compute, 64, 1, 1, 0x61u, descriptor, + packed_clear, size), + "V_ADD_LSHL was mistaken for V_LSHL_ADD contiguous coverage"); + + user_data[7] ^= 1u; + compute.stage.resources = Compile(code).resources; Require("Ps5GameExampleImageClear", "non-repeated clear", !ResolveComputeImageClear(compute, 64, 1, 1, 0x61u, descriptor, packed_clear, size), @@ -22366,6 +22382,91 @@ void CheckPs5GameExampleImageClearRuntimeShape() { !ResolveComputeImageClear(compute, 32, 1, 1, 0x61u, descriptor, packed_clear, size), "partial buffer coverage was classified as a complete clear"); + + // Same descriptor/dispatch as GTA3, with the stored value encoded in the + // shader. + user_data = {0x00200000u, 4u << 16u, 64u, 0x14204u}; + compute.workgroup_register = 4; + compute.dispatch_thread_dimensions = false; + std::vector scalar_code; + AppendVop3(&scalar_code, 0x346u, 0, 4, InlineU32(6), Vgpr(0)); + scalar_code.push_back(EncodeVop1(0x01u, 1, InlineU32(0))); + scalar_code.push_back(EncodeMubuf0(0x04u, 0, true, false)); + scalar_code.push_back(EncodeMubuf1(1, 0, 0)); + AppendEnd(&scalar_code); + auto scalar = Compile(scalar_code); + program.info = scalar.program.info; + compute.stage.resources = scalar.resources; + Require("Ps5GameExampleImageClear", "inline scalar fill", + ResolveComputeImageClear(compute, 1, 1, 1, 0x41u, descriptor, + packed_clear, size) && + descriptor.Base48() == 0x200000u && size == 256u && + packed_clear == 0u, + "GTA3's actual scalar store was not recognized from IR"); + Require("Ps5GameExampleImageClear", "excess dispatch", + !ResolveComputeImageClear(compute, 2, 1, 1, 0x41u, descriptor, + packed_clear, size), + "excess invocation coverage was accepted as a fill"); + auto alias = scalar.resources.buffers.front(); + compute.stage.resources.buffers.push_back(alias); + program.info.buffers.push_back({.read = true, .scalar = true}); + Require("Ps5GameExampleImageClear", "aliased scalar input", + !ResolveComputeImageClear(compute, 1, 1, 1, 0x41u, descriptor, + packed_clear, size), + "a scalar read aliasing the destination was accepted as uniform"); + program.info = scalar.program.info; + compute.stage.resources = scalar.resources; + compute.stage.resources.buffers[0].dwords[3] = + (static_cast(Prospero::BufferFormat::k32Float) << 12u) | 0x204u; + Require("Ps5GameExampleImageClear", "wrong store format", + !ResolveComputeImageClear(compute, 1, 1, 1, 0x41u, descriptor, + packed_clear, size), + "a format that changes the stored value was accepted as a uint fill"); + // A body change preserving every descriptor and dispatch field must change + // the result. + scalar_code[2] = EncodeVop1(0x01u, 1, InlineU32(7)); + compute.stage.resources = Compile(scalar_code).resources; + Require("Ps5GameExampleImageClear", "changed store value", + ResolveComputeImageClear(compute, 1, 1, 1, 0x41u, descriptor, + packed_clear, size) && + packed_clear == 7u, + "clear recognition ignored the shader's actual stored value"); + scalar_code[2] = EncodeVop1(0x01u, 1, Vgpr(0)); + compute.stage.resources = Compile(scalar_code).resources; + Require("Ps5GameExampleImageClear", "varying store value", + !ResolveComputeImageClear(compute, 1, 1, 1, 0x41u, descriptor, + packed_clear, size), + "a varying store with identical resources was treated as a fill"); + + const auto scalar_address = reinterpret_cast(&scalar_clear); + user_data[4] = static_cast(scalar_address); + user_data[5] = static_cast(scalar_address >> 32u) | (16u << 16u); + user_data[6] = 1; + user_data[7] = 0x4dfacu; + compute.workgroup_register = 8; + scalar_code.clear(); + AppendVop3(&scalar_code, 0x346u, 0, 8, InlineU32(6), Vgpr(0)); + scalar_code.push_back(EncodeSmem0(0x08u, 106, 2)); + scalar_code.push_back(EncodeSmem1(0, 125)); + scalar_code.push_back(EncodeVop1(0x01u, 1, 106)); + scalar_code.push_back(EncodeMubuf0(0x04u, 0, true, false)); + scalar_code.push_back(EncodeMubuf1(1, 0, 0)); + AppendEnd(&scalar_code); + clean_scalar = true; + auto loaded = Compile(scalar_code); + program.info = loaded.program.info; + compute.stage.resources = loaded.resources; + Require("Ps5GameExampleImageClear", "scalar-loaded fill", + ResolveComputeImageClear(compute, 1, 1, 1, 0x41u, descriptor, + packed_clear, size) && + packed_clear == scalar_clear && descriptor.Base48() == 0x200000u, + "GTA3's scalar input was confused with its destination"); + clean_scalar = false; + compute.stage.resources = Compile(scalar_code).resources; + Require("Ps5GameExampleImageClear", "unavailable clean scalar", + !ResolveComputeImageClear(compute, 1, 1, 1, 0x41u, descriptor, + packed_clear, size), + "a scalar without a clean reader was replaced by a fill"); std::printf("[host] %-32s ok\n", "Ps5GameExampleImageClear"); }