From fda4aa90c9f81fa3b5160150b3bee926b6923955 Mon Sep 17 00:00:00 2001 From: AlxcBTW <187763517+AlxcBTW@users.noreply.github.com> Date: Sat, 10 Oct 2026 22:53:22 +0200 Subject: [PATCH] fix(shader): let image writes, explicit-LOD gathers and size queries pass the masked-lane walk Following the ImageRead case, every image operation was checked against the two lists the masked-lane walk uses: accesses guarded by a trailing exec operand, and lane-local values. - ImageWrite carries its exec last like StoreBuffer, and EmitWriteOp stores only inside an if on it (bindless tables and FMASK refuse the store). It joins the exec-guarded list. - A gather with an LOD of its own (gather4_l, gather4_lz) or outside a pixel shader reads only its lane's address, like the explicit-LOD samples the walk already follows. A pixel gather without one still keeps the select, since on the GPU it takes its LOD from the quad. - ImageQueryDimensions returns the size of its lane's level of the image and is lane-local. Left out on purpose: - ImageQueryLod and implicit-LOD samples read their quad. - ImageBvhIntersectRay has a trailing exec operand, but its emitter ignores it and records faults in every lane. - Image, buffer, shared and address atomics are guarded by their exec in the emitter too, but the test from 60c65521 pins them as keeping the select, and no refused shader has needed them so far. Co-Authored-By: Claude Opus 5.5 --- .../libSceAgcDriver/tests/MaskedSelect.cpp | 34 +++++++++++++++++-- .../src/MaskedSelectEliminator.cpp | 4 ++- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp b/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp index ecf3a581e4..17c06cd64a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp @@ -592,7 +592,7 @@ int main() { return b.eliminate() == 0u && !removed(written); }); - const auto sample = [](IrShaderStage stage, std::uint32_t sampleFlags) { + const auto sample = [](IrShaderStage stage, std::uint32_t sampleFlags, IrOpcode opcode = IrOpcode::ImageSampleRaw) { Builder b; b.program.Resources().stage = stage; MemoryInfo memory; @@ -608,7 +608,7 @@ int main() { MemoryFlags flags{0u, 0u}; std::uint64_t bits = 0; std::memcpy(&bits, &flags, sizeof(flags)); - auto& sampled = b.emit(IrOpcode::ImageSampleRaw, IrType::U32x4, {&image, &sampler, &address}, bits); + auto& sampled = b.emit(opcode, IrType::U32x4, {&image, &sampler, &address}, bits); b.keep(b.select(exec, b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&sampled, &b.constant(0u)}), old)); b.eliminate(); return removed(written); @@ -622,6 +622,26 @@ int main() { return sample(IrShaderStage::Pixel, RdnaImageSampleFlagLevelZero) && sample(IrShaderStage::Mesh, 0u); }); + passed &= run("a pixel gather without an LOD of its own takes it from the quad and keeps the select", [&] { + return !sample(IrShaderStage::Pixel, 0u, IrOpcode::ImageGatherRaw) && !sample(IrShaderStage::Pixel, RdnaImageSampleFlagBias, IrOpcode::ImageGatherRaw); + }); + + passed &= run("a gather with an explicit LOD reads only its lane and drops the select", [&] { + return sample(IrShaderStage::Pixel, RdnaImageSampleFlagLevelZero, IrOpcode::ImageGatherRaw) && sample(IrShaderStage::Pixel, RdnaImageSampleFlagLod, IrOpcode::ImageGatherRaw) && + sample(IrShaderStage::Mesh, 0u, IrOpcode::ImageGatherRaw); + }); + + passed &= run("an image size query reads only its lane's level and drops the select", [] { + Builder b; + auto& exec = b.mask(16u); + auto& old = b.lane(); + auto& written = b.select(exec, b.add(old, 1u), old); + auto& address = b.emit(IrOpcode::MakeImageAddress, IrType::ImageAddress, {&written}); + auto& size = b.emit(IrOpcode::ImageQueryDimensions, IrType::U32x4, {&b.emit(IrOpcode::GetImageResource, IrType::ImageResource, {}), &address}); + b.keep(b.select(exec, b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&size, &b.constant(0u)}), old)); + return b.eliminate() == 1u && removed(written); + }); + const auto guarded = [](IrOpcode opcode, bool sameExec, bool asActive) { Builder b; auto& wide = b.mask(48u); @@ -643,6 +663,12 @@ int main() { b.keep(b.select(exec, b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&read, &b.constant(0u)}), old)); break; } + case IrOpcode::ImageWrite: { + auto& address = b.emit(IrOpcode::MakeImageAddress, IrType::ImageAddress, {&old, &old}); + auto& texel = b.emit(IrOpcode::CompositeConstructU32x4, IrType::U32x4, {&written, &old, &old, &old}); + b.emit(opcode, IrType::Void, {&b.emit(IrOpcode::GetImageResource, IrType::ImageResource, {}), &address, &texel, &active}); + break; + } default: b.emit(opcode, IrType::Void, {&b.emit(IrOpcode::CompositeConstructU32x4, IrType::U32x4, {&written, &old, &old, &old}), &active}); break; @@ -659,6 +685,10 @@ int main() { return !guarded(IrOpcode::WriteSharedU32, false, false) && !guarded(IrOpcode::LoadBufferU32, false, false) && !guarded(IrOpcode::ImageRead, false, false) && !guarded(IrOpcode::SetAttribute, false, false); }); + passed &= run("an image write under the write's exec drops the select, under a wider exec or as its exec keeps it", [&] { + return guarded(IrOpcode::ImageWrite, true, false) && !guarded(IrOpcode::ImageWrite, false, false) && !guarded(IrOpcode::ImageWrite, true, true); + }); + passed &= run("a value used as a store's exec keeps the select", [&] { return !guarded(IrOpcode::WriteSharedU32, true, true); }); diff --git a/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp b/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp index 30331f633b..e500ead2ae 100644 --- a/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp +++ b/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp @@ -171,6 +171,7 @@ bool isLaneLocal(IrOpcode opcode) { case IrOpcode::FPFrexpMant64: case IrOpcode::FPFrexpExp64: case IrOpcode::MakeImageAddress: + case IrOpcode::ImageQueryDimensions: return true; default: return false; @@ -205,6 +206,7 @@ bool readsOnlyWhereActive(IrOpcode opcode) { case IrOpcode::WriteSharedU32x4: case IrOpcode::SetAttribute: case IrOpcode::ImageRead: + case IrOpcode::ImageWrite: return true; default: return false; @@ -212,7 +214,7 @@ bool readsOnlyWhereActive(IrOpcode opcode) { } bool isExplicitLodSample(const IrProgram& program, const IrValue& value) { - if (value.Opcode() != IrOpcode::ImageSampleRaw) return false; + if (value.Opcode() != IrOpcode::ImageSampleRaw && value.Opcode() != IrOpcode::ImageGatherRaw) return false; if (program.Resources().stage != IrShaderStage::Pixel) return true; const auto index = value.Flags().index; if (index >= program.Resources().memoryInfo.size()) return false;