From 0258bd61eacd9b60b4d49e96027c54cc7afa8f37 Mon Sep 17 00:00:00 2001 From: nmzik Date: Sun, 4 Oct 2026 05:04:44 +0400 Subject: [PATCH] shader: preserve complete clear recognition with dispatch EXEC bounds Co-authored-by: TheCruZ Co-authored-by: Mac --- .../ir/passes/ResourceMaterialization.cpp | 55 ++++++++++++++++--- tests/ShaderRecompilerComputeTests.cpp | 21 +++++++ 2 files changed, 67 insertions(+), 9 deletions(-) diff --git a/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp b/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp index 5005c203f..c63434529 100644 --- a/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp +++ b/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp @@ -708,11 +708,45 @@ static std::vector ResourceControlFlow(const Program& program, co return blocks; } +// ResolveComputeBufferFill proves complete workgroups and matching requested extents, so every +// dispatch-bound predicate accepted here is true for every invocation covered by the fill. +static bool IsFullDispatchPredicate(Value value, uint32_t depth = 0) { + value = value.Resolve(); + if (value == Value(true)) return true; + const auto* inst = value.TryInstruction(); + if (inst == nullptr || depth > 32) return false; + if (inst->GetOpcode() == ValueOpcode::LogicalAnd) { + return IsFullDispatchPredicate(inst->Arg(0), depth + 1) && + IsFullDispatchPredicate(inst->Arg(1), depth + 1); + } + if (inst->GetOpcode() != ValueOpcode::ULessThan32) return false; + const auto* global = inst->Arg(0).Resolve().TryInstruction(); + const auto* extent = inst->Arg(1).Resolve().TryInstruction(); + if (global == nullptr || global->GetOpcode() != ValueOpcode::GetBuiltin || + global->Arg(0).Resolve() != Value(static_cast(StageInputKind::GlobalInvocationId)) || + extent == nullptr || extent->GetOpcode() != ValueOpcode::GetDispatchThreadExtent) + return false; + const auto axis = global->Arg(1).Resolve(); + return axis.IsImmediate() && axis.U32() < 3 && extent->Arg(0).Resolve() == axis; +} + +static Value FillValue(Value value, Value guard) { + for (uint32_t depth = 0; depth <= 32; ++depth) { + value = value.Resolve(); + const auto* inst = value.TryInstruction(); + if (inst == nullptr || inst->GetOpcode() != ValueOpcode::SelectU32 || + inst->Arg(0).Resolve() != guard) + return value; + value = inst->Arg(1); + } + return {}; +} + // Nonnegative affine coefficients for constant, local and workgroup coordinates. Reject modular // arithmetic that could wrap; runtime coverage also bounds the largest invocation index. -static std::optional> FillIndex(Value value, uint32_t axis, +static std::optional> FillIndex(Value value, uint32_t axis, Value guard, uint32_t depth = 0) { - value = value.Resolve(); + value = FillValue(value, guard); if (depth > 32 || value.GetType() != Type::U32) { return {}; } @@ -736,8 +770,8 @@ static std::optional> FillIndex(Value value, uint32_t ax op != ValueOpcode::ShiftLeftLogical32) { return {}; } - auto left = FillIndex(inst->Arg(0), axis, depth + 1); - auto right = FillIndex(inst->Arg(1), axis, depth + 1); + auto left = FillIndex(inst->Arg(0), axis, guard, depth + 1); + auto right = FillIndex(inst->Arg(1), axis, guard, depth + 1); if (!left || !right) { return {}; } @@ -793,6 +827,7 @@ static UniformFillPlan AnalyzeUniformFill(const Program& program) { UniformFillPlan result; result.fill.resource = memory.resource; Value data; + Value guard(true); if (store->GetOpcode() == ValueOpcode::ImageWrite) { if (program.info.images.size() != 1 || memory.dmask != 1 || memory.data_bits != 32 || memory.image_has_mip || memory.image_sample_flags != 0 || memory.image_r128 || @@ -803,7 +838,7 @@ static UniformFillPlan AnalyzeUniformFill(const Program& program) { const auto* address = store->Arg(1).ResolveInstruction(); if (address == nullptr || address->GetOpcode() != ValueOpcode::MakeImageAddress) return {}; for (uint32_t axis = 0; axis < 3; ++axis) { - const auto index = FillIndex(address->Arg(axis), axis); + const auto index = FillIndex(address->Arg(axis), axis, guard); if (!index || (*index)[0] != 0) return {}; if (axis < 2) { if ((*index)[1] != 1 || (*index)[2] == 0) return {}; @@ -825,20 +860,22 @@ static UniformFillPlan AnalyzeUniformFill(const Program& program) { 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)) + store->Arg(3).Resolve() != Value(0u)) return {}; + guard = store->Arg(5).Resolve(); + if (!IsFullDispatchPredicate(guard)) return {}; 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), 0); + const auto address = FillIndex(store->Arg(1), 0, guard); if (!address || (*address)[0] != 0 || (*address)[1] != 1 || (*address)[2] == 0) return {}; result.fill.kind = UniformFillKind::Buffer; result.fill.group_stride[0] = static_cast((*address)[2]); result.fill.words = memory.data_dwords; data = store->Arg(4); } - data = data.Resolve(); + data = FillValue(data, guard); const auto* vector = data.TryInstruction(); constexpr std::array composites {ValueOpcode::CompositeConstructU32x2, ValueOpcode::CompositeConstructU32x3, @@ -847,7 +884,7 @@ static UniformFillPlan AnalyzeUniformFill(const Program& program) { (vector == nullptr || vector->GetOpcode() != composites[result.fill.words - 2])) return {}; for (uint32_t i = 0; i < result.fill.words; ++i) { - const auto word = result.fill.words == 1 ? data : vector->Arg(i); + const auto word = FillValue(result.fill.words == 1 ? data : vector->Arg(i), guard); if (word.GetType() != Type::U32 || !ValidateRuntimeValue(program, word, RuntimeValueType::Integer)) return {}; diff --git a/tests/ShaderRecompilerComputeTests.cpp b/tests/ShaderRecompilerComputeTests.cpp index d4e03fcf3..0cee638d8 100644 --- a/tests/ShaderRecompilerComputeTests.cpp +++ b/tests/ShaderRecompilerComputeTests.cpp @@ -33173,6 +33173,15 @@ void CheckPs5GameExampleImageClearRuntimeShape() { packed_clear == 0xff000000u, "exact Prospero runtime binding did not resolve to a complete clear"); + auto narrow_exec = code; + narrow_exec.insert(narrow_exec.end() - 3, + EncodeSop1(0x24u, 10, InlineU32(1))); // S_AND_SAVEEXEC_B64. + runtime_resources = Compile(narrow_exec); + Require("Ps5GameExampleImageClear", "guest EXEC narrowing", + !ResolveComputeBufferFill(compute, 64, 1, 1, 0x61u, descriptor, + packed_clear, size), + "a guest predicate narrowing the store was accepted as a complete clear"); + auto wrong_index = code; wrong_index[0] = (wrong_index[0] & ~(0x3ffu << 16u)) | (0x347u << 16u); runtime_resources = Compile(wrong_index); @@ -33188,6 +33197,18 @@ void CheckPs5GameExampleImageClearRuntimeShape() { packed_clear, size), "non-uniform uint4 data was replaced with a color clear"); runtime_resources = positive; + compute.dispatch_threads_num[0] = 63; + Require("Ps5GameExampleImageClear", "mismatched requested extent", + !ResolveComputeBufferFill(compute, 64, 1, 1, 0x61u, descriptor, + packed_clear, size), + "a dispatch exceeding its requested extent was classified as a complete clear"); + runtime_resources.buffers[0].dwords[2] = 65; + compute.dispatch_threads_num[0] = 65; + Require("Ps5GameExampleImageClear", "partial final workgroup", + !ResolveComputeBufferFill(compute, 65, 1, 1, 0x61u, descriptor, + packed_clear, size), + "a nondivisible thread dispatch was classified as a complete clear"); + runtime_resources = positive; compute.dispatch_threads_num[0] = 32; Require("Ps5GameExampleImageClear", "partial dispatch", !ResolveComputeBufferFill(compute, 32, 1, 1, 0x61u, descriptor,