From 7092dde6ee867f2f678773bac6b816061993674e Mon Sep 17 00:00:00 2001 From: milescoviello Date: Thu, 15 Oct 2026 22:24:26 +0000 Subject: [PATCH] fix(shader): emit nothing for a vertex s_barrier that precedes every memory access A vertex-path program with LDS, GDS or memory writes refused any s_barrier. NGG passthrough programs start with the s_barrier that orders GS_ALLOC_REQ, before any memory access, and some write buffers later. A barrier that runs before every load, store, atomic, image or LDS access of the program has nothing to order, so it emits no instruction, like the barrier of a program without memory access. Barriers after a memory access still throw. Co-Authored-By: Claude Opus 5.5 --- .../libSceAgcDriver/tests/VertexBarrier.cpp | 10 ++++++++++ .../SpirvBackend/src/SpirvFlowEmitter.cpp | 20 +++++++++++++++++++ docs/dev/debt/shader.md | 2 +- 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/VertexBarrier.cpp b/core/libs/prx/libSceAgcDriver/tests/VertexBarrier.cpp index 453fbbab75b0..2b0313c64d0b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/VertexBarrier.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/VertexBarrier.cpp @@ -11,6 +11,8 @@ namespace { constexpr std::array BarrierExport{0xbf8a0000u, 0xf80008cfu, 0u, 0xbf810000u}; constexpr std::array BarrierLdsExport{0xd8340000u, 0u, 0xbf8a0000u, 0xf80008cfu, 0u, 0xbf810000u}; +constexpr std::array LeadingBarrierLds{0xbf8a0000u, 0xd8340000u, 0u, 0xf80008cfu, 0u, 0xbf810000u}; +constexpr std::array ScalarBarrierLds{0xbe8003c1u, 0xbf8a0000u, 0xd8340000u, 0u, 0xf80008cfu, 0u, 0xbf810000u}; constexpr std::array VertexCapabilities{spv::CapabilityClipDistance}; @@ -42,6 +44,13 @@ void verifyBarrierWithoutMemory() { if (hasControlBarrier(result)) throw std::runtime_error("a vertex program's s_barrier became a workgroup OpControlBarrier, which the Vertex execution model does not allow"); } +void verifyBarrierBeforeLds() { + for (const auto code : {std::span(LeadingBarrierLds), std::span(ScalarBarrierLds)}) { + const auto result = ShaderRecompiler::Recompile(vertexRequest(code)); + if (hasControlBarrier(result)) throw std::runtime_error("an s_barrier before every memory access of a vertex program became a workgroup OpControlBarrier"); + } +} + void verifyBarrierWithLds() { try { (void)ShaderRecompiler::Recompile(vertexRequest(BarrierLdsExport)); @@ -57,6 +66,7 @@ void verifyBarrierWithLds() { int main() { try { verifyBarrierWithoutMemory(); + verifyBarrierBeforeLds(); verifyBarrierWithLds(); std::cout << "vertex barrier tests passed\n"; return 0; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp index b6d0b81edec2..a19022f3c788 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp @@ -809,6 +809,25 @@ bool CommunicatesThroughMemory(IrOpcode opcode) { return SharedAccessOf(opcode) != SharedAccess::None || buffer == BufferAccess::Write || buffer == BufferAccess::Atomic || address == AddressAccess::Write || address == AddressAccess::Atomic || image == ImageAccess::Write || image == ImageAccess::Atomic; } +bool AccessesMemory(IrOpcode opcode) { + return opcode == IrOpcode::ReadConst || opcode == IrOpcode::ReadConstBuffer || SharedAccessOf(opcode) != SharedAccess::None || BufferAccessOf(opcode) != BufferAccess::None || AddressOpcodeInfoOf(opcode).access != AddressAccess::None || ImageOpcodeInfoOf(opcode).access != ImageAccess::None; +} + +bool PrecedesEveryMemoryAccess(const IrProgram& program, const IrValue& barrier) { + const auto& blocks = program.BlockOrder(); + const IrBlock* block = blocks.empty() ? nullptr : blocks.front(); + for (std::size_t steps = 0; block != nullptr && steps < blocks.size(); ++steps) { + for (const auto* instruction : block->Instructions()) { + if (instruction == &barrier) return true; + if (AccessesMemory(instruction->Opcode())) return false; + } + if (block->Successors().size() != 1u) return false; + block = block->Successors().front(); + if (block->Predecessors().size() != 1u) return false; + } + return false; +} + } void EmitControlFlow(SpirvModule& module, const IrProgram& program) { @@ -865,6 +884,7 @@ void EmitVoid(SpirvValueEmitContext&) { void EmitBarrier(SpirvValueEmitContext& ctx, const IrValue& inst) { auto& state = ctx.state; if (state.program.Resources().stage == IrShaderStage::Vertex) { + if (PrecedesEveryMemoryAccess(state.program, inst)) return; for (const auto* block : state.program.BlockOrder()) { for (const auto* instruction : block->Instructions()) { if (CommunicatesThroughMemory(instruction->Opcode())) ctx.Fail(inst, "s_barrier in a vertex program that accesses LDS or GDS or writes memory is not implemented"); diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index c6c98363e016..2aaf0bfefb98 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -17,7 +17,7 @@ - [s_sethalt](../../../core/shader/recompiler/Translation/src/ScalarInstructions.cpp) (core/shader/recompiler) - with SIMM16[0] clear it is a no-op: the RDNA 2 ISA guide describes that value as resuming a halted wave, and a wave that executes it is not halted; measured on a gfx1035 (RDNA 2) wave: `s_sethalt 0` followed by a VALU op completes and gives the normal result. Only SIMM16[0] is read; with it set the instruction throws - [s_subvector_loop_begin and s_subvector_loop_end](../../../core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp) (core/shader/recompiler) - follow AMD's RDNA 2 instruction set reference and a gfx1036 (wave64) measurement, pinned in [SubvectorLoop.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/SubvectorLoop.cpp); `S0` in the end instruction's `elif(S0 == 0)` is read as its SGPR operand. A wave32 shader and a destination other than an SGPR throw because neither is measured. Measured on PC RDNA2 (Radeon 610M), not on a PS5. The [specialization pass](../../../core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp) keeps a constant-condition branch whose untaken target is a loop header, since folding it removes the loop's back edge, which SPIR-V requires exactly once per loop header (SPIRV-Tools rejects the module and the NVIDIA driver 615.78.08 crashes compiling it); a loop whose back-edge block becomes unreachable in another way is not handled - [Vertex entry merged wave info](../../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) (recompiler) - `s3` of every vertex-path shader is set to `waveSize | waveSize << 8` (vertex and primitive count of a full wave, wave index 0), or to `32 | 32 << 8` for a wave64 program on a 32-lane host subgroup, which runs it at one lane per invocation and holds no lanes 32-63. On a device whose `subgroupSupportedStages` lacks the vertex stage (lavapipe, MoltenVK), vertex and local programs run as one-lane waves: `s3` is `1 | 1 << 8`, the lane id is 0, a ballot is the invocation's own bit, a shuffle or broadcast returns the invocation's value, an elect is true and a subgroup reduction is the value itself, so no `GroupNonUniform` capability is declared. Layout taken from the prologue Mesa's ACO emits for NGG vertex shaders (`s_bfe_u64 exec` from `s3`), not measured on hardware. The driver gives every graphics program user data from `s8`, so the condition does not tell NGG from legacy vertex shaders: they just never read `s3`. A draw whose last wave is partial still reports a full wave, and so does a host wave with idle invocations (an RTX 5070 ran every vertex warp of a non-indexed triangle list with 30 of its 32 invocations). -- [s_barrier in vertex-path programs](../../../core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp) (recompiler) - emits no instruction when the program has no LDS or GDS access and no memory write or atomic, so no wave of the subgroup can observe another; the `GS_ALLOC_REQ` such a barrier orders on NGG passthrough programs is already dropped on the vertex path. The Vertex execution model has no workgroup barrier. Programs with LDS, GDS or memory writes throw. Not measured on hardware +- [s_barrier in vertex-path programs](../../../core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp) (recompiler) - emits no instruction when the program has no LDS or GDS access and no memory write or atomic, so no wave of the subgroup can observe another, or when the barrier runs before every memory access of the program (it is on the entry's straight-line path with no load, store, atomic, image or LDS access ahead of it), so it orders nothing; the `GS_ALLOC_REQ` such a barrier orders on NGG passthrough programs is already dropped on the vertex path. The Vertex execution model has no workgroup barrier. Other programs with LDS, GDS or memory writes throw. Not measured on hardware - [Vertex clip and cull distances](../../../core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp) (libSceAgcDriver) - `PA_CL_VS_OUT_CNTL` enable bit `s` of `CLIP_DIST_ENA`/`CULL_DIST_ENA` is taken to select component `s` of the `pos1`/`pos2` exports, with the Vulkan array index counted by the enabled bits below it, and `VS_OUT_CCDIST0_VEC_ENA` as bit 22; this follows the recompiler's existing position-export mapping, not hardware or documentation. The guest clip distances share one `gl_ClipDistance` array with the all-zero clip position distance, which takes the index after them. A vertex program whose own code has no `pos1`..`pos3` export is compiled and prepared without these bits (`PositionExportLayout`), so registration and draws agree on its artifact; an export in a subroutine it calls is not seen by that scan - [32_AR color exports](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - with `SPI_SHADER_COL_FORMAT` 3 the red value is export channel 0 and the alpha is export channel 1, the GFX10 layout of Mesa's [`emit_ps_color_export`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/3e32b0e1f132231b7a972c1b0b5b06c7e16cf8b9/src/amd/common/nir/ac_nir_lower_ps_late.c#L400-L411), ACO and radeonsi in Mesa 22.0 ([aco_instruction_selection.cpp](https://gitlab.freedesktop.org/mesa/mesa/-/blob/a82920af6efa4a7a66919885c6477fbd8fdc7b9d/src/amd/compiler/aco_instruction_selection.cpp#L11087-L11096), [si_shader_llvm_ps.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/a82920af6efa4a7a66919885c6477fbd8fdc7b9d/src/gallium/drivers/radeonsi/si_shader_llvm_ps.c#L323-L333)) and AMD's [LLPC](https://github.com/GPUOpen-Drivers/llpc/blob/40cb8d95ad8d6f7f1652e3fd47d39667594cce08/lgc/lowering/FragmentColorExport.cpp#L157-L168); before GFX10 the alpha was channel 3. Channels 2 and 3, which none of these compilers enables, are ignored; green and blue are 0 and the alpha of an export without channel 1 is 1.0, the defaults of the other formats, which no source gives. Not measured on hardware, neither on gfx1013 nor on a PC RDNA 2 GPU; pinned in [Pixel32ArExport.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/Pixel32ArExport.cpp)