From 919fac13d048097f1823d7ef9333351d8e850541 Mon Sep 17 00:00:00 2001 From: Nawid3333 <102778966+Nawid3333@users.noreply.github.com> Date: Thu, 22 Oct 2026 01:17:31 +0200 Subject: [PATCH] shader: read lane 0 in V_READFIRSTLANE when EXEC is zero instead of an undefined lane --- .../backend/spirv/spirvEmitterProgram.cpp | 39 ++++++++----------- tests/ShaderRecompilerComputeTests.cpp | 36 +++++++++++++++++ 2 files changed, 53 insertions(+), 22 deletions(-) diff --git a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterProgram.cpp b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterProgram.cpp index cd93c7671..cd9e65595 100644 --- a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterProgram.cpp +++ b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterProgram.cpp @@ -493,29 +493,24 @@ uint32_t ValueEmitContext::Ballot(IR::Value predicate) { } uint32_t ValueEmitContext::FirstLane(uint32_t ballot) { - if (other_half == nullptr) { - const auto result = state.builder.AllocateId(); - state.builder.AddFunction(spv::OpGroupNonUniformBallotFindLSB, TypeU32(state), result, - ConstantU32(state, spv::ScopeSubgroup), ballot); - return result; + // An empty ballot (EXEC zero) gives lane 0, which V_READFIRSTLANE reads on hardware. FindILsb + // returns -1 for zero, unlike BallotFindLSB, whose result is then undefined. + const auto first = [&](uint32_t word) { + const auto bits = state.builder.AllocateId(); + state.builder.AddFunction(spv::OpCompositeExtract, TypeU32(state), bits, ballot, word); + return std::pair {bits, EmitGlsl(state, bits)}; + }; + const auto [low, low_first] = first(0); + if (state.program.wave_size == 32u) { + return EmitGlsl(state, low_first, ConstantU32(state, 0)); } - const auto low = state.builder.AllocateId(); - const auto high = state.builder.AllocateId(); - const auto low_first = state.builder.AllocateId(); - const auto high_first = state.builder.AllocateId(); - const auto low_active = state.builder.AllocateId(); - const auto result = state.builder.AllocateId(); - state.builder.AddFunction(spv::OpCompositeExtract, TypeU32(state), low, ballot, 0); - state.builder.AddFunction(spv::OpCompositeExtract, TypeU32(state), high, ballot, 1); - state.builder.AddFunction(spv::OpExtInst, TypeU32(state), low_first, GlslStd450(state), - GLSLstd450FindILsb, low); - state.builder.AddFunction(spv::OpExtInst, TypeU32(state), high_first, GlslStd450(state), - GLSLstd450FindILsb, high); - state.builder.AddFunction(spv::OpINotEqual, TypeBool(state), low_active, low, - ConstantU32(state, 0)); - state.builder.AddFunction(spv::OpSelect, TypeU32(state), result, low_active, low_first, - EmitAddU32(state, high_first, ConstantU32(state, 32))); - return result; + const auto high_first = first(1).second; + const auto high_lane = EmitGlsl( + state, EmitBinaryU32(state, spv::OpBitwiseOr, high_first, ConstantU32(state, 32)), + ConstantU32(state, 0)); + return Select(state, TypeU32(state), + Binary(state, spv::OpINotEqual, TypeBool(state), low, ConstantU32(state, 0)), + low_first, high_lane); } uint32_t ValueEmitContext::Shuffle(const IR::Inst& inst, size_t index, uint32_t lane) { diff --git a/tests/ShaderRecompilerComputeTests.cpp b/tests/ShaderRecompilerComputeTests.cpp index 1febaa64c..aedee8f18 100644 --- a/tests/ShaderRecompilerComputeTests.cpp +++ b/tests/ShaderRecompilerComputeTests.cpp @@ -23006,6 +23006,38 @@ TestCase ScalarZeroBitOps() { return test; } +// V_READFIRSTLANE reads the first EXEC lane, or lane 0 when EXEC is zero; v1 = 100 + lane. +TestCase VectorReadFirstLaneExecMasks(u32 wave_size) { + using O = ShaderOpcode; + TestCase test; + test.name = wave_size == 32 ? "VectorReadFirstLaneExecMasksWave32" + : "VectorReadFirstLaneExecMasksWave64"; + auto &code = test.code; + AppendVMovU32(&code, 1, 100); + code.push_back(EncodeVop2(0x25, 1, Vgpr(0), 1)); + code.push_back(EncodeSop1(0x04, 10, 126)); // s_mov_b64 s[10:11], exec + const uint64_t masks[] = {0, 1ull << 5, 0x80000000ull, 1ull << 32, 1ull << 63, 0xffff000000000000ull}; + for (const uint64_t mask : masks) { + if (wave_size == 32 && mask > UINT32_MAX) continue; + AppendSMovLiteral(&code, 126, u32(mask)); + if (wave_size == 64) AppendSMovLiteral(&code, 127, u32(mask >> 32)); + code.push_back(EncodeVop1(0x02, 20, Vgpr(1))); + code.push_back(EncodeSop1(0x04, 126, 10)); + AppendStoreSgpr(&code, 20, u32(test.expected.size())); + test.expected.push_back(100 + (mask == 0 ? 0 : std::countr_zero(mask))); + } + AppendEnd(&code); + test.initial.resize(test.expected.size()); + test.opcodes = {O::V_MOV_B32, O::V_ADD_NC_U32, O::S_MOV_B32, O::S_MOV_B64, + O::V_READFIRSTLANE_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; + test.compute_info.wave_size = wave_size; + test.compute_info.threads_num[0] = wave_size; + test.compute_info.threads_num[1] = test.compute_info.threads_num[2] = 1; + test.compute_info.thread_ids_num = 1; + test.has_compute_info = true; + return test; +} + TestCase ScalarSextMasks(u32 bits, u32 wave_size) { using O = ShaderOpcode; TestCase test; @@ -43094,6 +43126,8 @@ std::vector MakeCases() { cases.push_back(VectorDppRowShare(64)); cases.push_back(VectorDppMirrorAndRotate(32)); cases.push_back(VectorDppMirrorAndRotate(64)); + cases.push_back(VectorReadFirstLaneExecMasks(32)); + cases.push_back(VectorReadFirstLaneExecMasks(64)); cases.push_back(ComputeWave64Minimum(false)); cases.push_back(ComputeWave64Minimum(true)); AddCase(VectorDppBankMaskPreservesDestination); @@ -49186,6 +49220,8 @@ int main(int argc, char **argv) { RunCase(&vulkan, VectorDppRowShare(64)); RunCase(&vulkan, VectorDppMirrorAndRotate(32)); RunCase(&vulkan, VectorDppMirrorAndRotate(64)); + RunCase(&vulkan, VectorReadFirstLaneExecMasks(32)); + RunCase(&vulkan, VectorReadFirstLaneExecMasks(64)); RunCase(&vulkan, VectorDppBankMaskPreservesDestination()); RunCase(&vulkan, VectorDppBoundsControlZeroPreservesDestination()); return 0;