shader: support returning 64-bit LDS max and compare-exchange

This commit is contained in:
nmzik 2026-10-13 14:48:38 +02:00
parent e998f21e71
commit 9e41d8320a
12 changed files with 188 additions and 31 deletions

View File

@ -322,6 +322,8 @@ inline constexpr auto EmitSharedAtomicDec32 = EmitSharedIncDec;
inline constexpr auto EmitSharedAtomicSMin32 = EmitAtomic32;
inline constexpr auto EmitSharedAtomicUMin32 = EmitAtomic32;
inline constexpr auto EmitSharedAtomicSMax32 = EmitAtomic32;
inline constexpr auto EmitSharedAtomicUMax64 = EmitSharedAtomic64;
inline constexpr auto EmitSharedAtomicCmpSwap64 = EmitSharedAtomic64;
inline constexpr auto EmitSharedAtomicUMax32 = EmitAtomic32;
inline constexpr auto EmitSharedAtomicAnd32 = EmitAtomic32;
inline constexpr auto EmitSharedAtomicOr32 = EmitAtomic32;

View File

@ -1240,32 +1240,48 @@ void EmitSharedAtomic64(ValueEmitContext& ctx, const IR::Inst& inst) {
auto& state = ctx.state;
const auto& mem = ctx.Memory(inst);
EnsureLdsStorage(state);
EmitIfCondition(state, ctx.Arg(inst, 2), [&]() {
const auto address = Binary(state, spv::OpBitwiseAnd, TypeU32(state),
ByteAddress(ctx, inst, mem), ConstantU32(state, 0xfff8u));
const auto index = Binary(state, spv::OpShiftRightLogical, TypeU32(state), address,
ConstantU32(state, 3u));
const auto in_bounds = Binary(state, spv::OpULessThan, TypeBool(state), index,
ConstantU32(state, LdsDwordCount(state) / 2u));
EmitIfCondition(state, in_bounds, [&]() {
auto native_index = index;
auto semantics = spv::MemorySemanticsWorkgroupMemoryMask;
if (state.lds_storage_class == spv::StorageClassStorageBuffer) {
native_index = EmitAddU32(state, index, EmitBinaryU32(
state, spv::OpShiftRightLogical, state.lds_base_dwords, ConstantU32(state, 1)));
semantics = spv::MemorySemanticsUniformMemoryMask;
}
const auto pointer = state.builder.AllocateId();
const auto address = Binary(state, spv::OpBitwiseAnd, TypeU32(state),
ByteAddress(ctx, inst, mem), ConstantU32(state, 0xfff8u));
const auto index = Binary(state, spv::OpShiftRightLogical, TypeU32(state), address,
ConstantU32(state, 3u));
const auto in_bounds = Binary(state, spv::OpULessThan, TypeBool(state), index,
ConstantU32(state, LdsDwordCount(state) / 2u));
const auto active = EmitLogicalAndBool(state, ctx.Arg(inst, inst.NumArgs() - 1), in_bounds);
const auto emit = [&]() {
auto native_index = index;
auto semantics = spv::MemorySemanticsWorkgroupMemoryMask;
if (state.lds_storage_class == spv::StorageClassStorageBuffer) {
native_index = EmitAddU32(state, index, EmitBinaryU32(
state, spv::OpShiftRightLogical, state.lds_base_dwords, ConstantU32(state, 1)));
semantics = spv::MemorySemanticsUniformMemoryMask;
}
const auto pointer = state.builder.AllocateId();
state.builder.AddFunction(
spv::OpAccessChain, TypePointer(state, state.lds_storage_class, TypeU64(state)),
pointer, state.lds_u64_variable, ConstantU32(state, 0), native_index);
const auto old = state.builder.AllocateId();
const auto opcode = SpirvAtomicOpcode(inst.GetOpcode());
const auto scope = ConstantU32(state, spv::ScopeWorkgroup);
const auto acquire_release =
ConstantU32(state, spv::MemorySemanticsAcquireReleaseMask | semantics);
if (opcode == spv::OpAtomicCompareExchange) {
// RDNA2 DS compares DATA and stores DATA2, opposite to BUFFER atomics.
state.builder.AddFunction(
spv::OpAccessChain, TypePointer(state, state.lds_storage_class, TypeU64(state)),
pointer, state.lds_u64_variable, ConstantU32(state, 0), native_index);
const auto value = ctx.Arg(inst, 1);
state.builder.AddFunction(
SpirvAtomicOpcode(inst.GetOpcode()), TypeU64(state), state.builder.AllocateId(),
pointer, ConstantU32(state, spv::ScopeWorkgroup),
ConstantU32(state, spv::MemorySemanticsAcquireReleaseMask | semantics), value);
});
});
opcode, TypeU64(state), old, pointer, scope, acquire_release,
ConstantU32(state, spv::MemorySemanticsAcquireMask | semantics),
ctx.Arg(inst, 2), ctx.Arg(inst, 1));
} else {
state.builder.AddFunction(opcode, TypeU64(state), old, pointer, scope,
acquire_release, ctx.Arg(inst, 1));
}
return old;
};
if (inst.HasUses()) {
ctx.Define(inst, EmitValueOrDefaultIfCondition(
state, active, TypeU64(state), ConstantU64(state, 0), emit));
} else {
EmitIfCondition(state, active, emit);
}
}
uint32_t EmitBufferFloatAtomic(ValueEmitContext& ctx, const IR::Inst& inst) {

View File

@ -464,6 +464,7 @@ uint32_t EncodeFormattedStoreComponent(EmitterState& state, const Format::Buffer
spv::Op SpirvAtomicOpcode(IR::ValueOpcode opcode) {
switch (opcode) {
case IR::ValueOpcode::SharedAtomicCmpSwap64:
case IR::ValueOpcode::ImageAtomicCompareSwap32:
case IR::ValueOpcode::BufferAtomicCmpSwap32: return spv::OpAtomicCompareExchange;
case IR::ValueOpcode::ImageAtomicSwap32:
@ -491,7 +492,8 @@ spv::Op SpirvAtomicOpcode(IR::ValueOpcode opcode) {
case IR::ValueOpcode::ImageAtomicUMax32:
case IR::ValueOpcode::ImageAtomicUMax64:
case IR::ValueOpcode::BufferAtomicUMax32:
case IR::ValueOpcode::SharedAtomicUMax32: return spv::OpAtomicUMax;
case IR::ValueOpcode::SharedAtomicUMax32:
case IR::ValueOpcode::SharedAtomicUMax64: return spv::OpAtomicUMax;
case IR::ValueOpcode::ImageAtomicAnd32:
case IR::ValueOpcode::ImageAtomicAnd64:
case IR::ValueOpcode::BufferAtomicAnd32:

View File

@ -116,7 +116,8 @@ constexpr MemoryOpcodeInfo DS_OPCODE_LIST[] = {
{0x3fu, Opcode::DS_ORDERED_COUNT, 1, 32},
{0x40u, Opcode::DS_ADD_U64, 2, 32}, {0x4au, Opcode::DS_OR_B64, 2, 32},
{0x4du, Opcode::DS_WRITE_B64, 2, 32}, {0x4eu, Opcode::DS_WRITE2_B64, 4, 32},
{0x4fu, Opcode::DS_WRITE2ST64_B64, 4, 32}, {0x76u, Opcode::DS_READ_B64, 2, 32},
{0x4fu, Opcode::DS_WRITE2ST64_B64, 4, 32}, {0x68u, Opcode::DS_MAX_RTN_U64, 2, 32},
{0x70u, Opcode::DS_CMPST_RTN_B64, 2, 32}, {0x76u, Opcode::DS_READ_B64, 2, 32},
{0x77u, Opcode::DS_READ2_B64, 4, 32}, {0x78u, Opcode::DS_READ2ST64_B64, 4, 32},
{0xa0u, Opcode::DS_WRITE_B8_D16_HI, 1, 8},
{0xa1u, Opcode::DS_WRITE_B16_D16_HI, 1, 16},
@ -192,6 +193,8 @@ bool IsDsAtomicOpcode(Opcode opcode) {
case Opcode::DS_MIN_RTN_U32:
case Opcode::DS_MAX_U32:
case Opcode::DS_MAX_RTN_U32:
case Opcode::DS_MAX_RTN_U64:
case Opcode::DS_CMPST_RTN_B64:
case Opcode::DS_AND_B32:
case Opcode::DS_AND_RTN_B32:
case Opcode::DS_OR_B32:
@ -211,6 +214,7 @@ uint32_t DsSourceCount(Opcode opcode) {
case Opcode::DS_WRITE2ST64_B32:
case Opcode::DS_WRITE2_B64:
case Opcode::DS_WRITE2ST64_B64:
case Opcode::DS_CMPST_RTN_B64:
case Opcode::DS_MSKOR_B32: return 3u;
case Opcode::DS_MIN_F32:
case Opcode::DS_MAX_F32:

View File

@ -708,6 +708,8 @@ std::string InstructionToString(const Instruction& inst) {
case Opcode::DS_MIN_RTN_U32:
case Opcode::DS_MAX_U32:
case Opcode::DS_MAX_RTN_U32:
case Opcode::DS_MAX_RTN_U64:
case Opcode::DS_CMPST_RTN_B64:
case Opcode::DS_AND_B32:
case Opcode::DS_AND_RTN_B32:
case Opcode::DS_OR_B32:

View File

@ -624,6 +624,8 @@ enum class Opcode {
DS_MIN_RTN_U32,
DS_MAX_U32,
DS_MAX_RTN_U32,
DS_MAX_RTN_U64,
DS_CMPST_RTN_B64,
DS_AND_B32,
DS_AND_RTN_B32,
DS_OR_B32,

View File

@ -578,11 +578,13 @@ void Translator::DS_ATOMIC(const Decoder::Instruction& inst, IR::ValueOpcode opc
bool returns_value) {
const auto memory = MemoryInfoFromDecoded(inst);
const auto address = ReadU32(inst.src0);
const IR::Value value =
memory.data_dwords == 2u ? IR::Value(ReadU64(inst.src1)) : IR::Value(ReadU32(inst.src1));
const auto read = [&](const Decoder::Operand& operand) -> IR::Value {
return memory.data_dwords == 2u ? IR::Value(ReadU64(operand)) : IR::Value(ReadU32(operand));
};
const auto value = read(inst.src1);
const auto flags = AddMemoryInfo(memory, inst.pc);
const auto result = inst.src_count == 3u
? ir.Emit(opcode, {address, value, ReadU32(inst.src2), ir.GetExec()}, flags)
? ir.Emit(opcode, {address, value, read(inst.src2), ir.GetExec()}, flags)
: ir.Emit(opcode, {address, value, ir.GetExec()}, flags);
if (returns_value) {
WriteOperand(inst.dst, result);
@ -1073,6 +1075,10 @@ void Translator::EmitMemory(const Decoder::Instruction& inst) {
return DS_ATOMIC(inst, IR::ValueOpcode::SharedAtomicUMax32, false);
case Decoder::Opcode::DS_MAX_RTN_U32:
return DS_ATOMIC(inst, IR::ValueOpcode::SharedAtomicUMax32, true);
case Decoder::Opcode::DS_MAX_RTN_U64:
return DS_ATOMIC(inst, IR::ValueOpcode::SharedAtomicUMax64, true);
case Decoder::Opcode::DS_CMPST_RTN_B64:
return DS_ATOMIC(inst, IR::ValueOpcode::SharedAtomicCmpSwap64, true);
case Decoder::Opcode::DS_AND_B32:
return DS_ATOMIC(inst, IR::ValueOpcode::SharedAtomicAnd32, false);
case Decoder::Opcode::DS_AND_RTN_B32:

View File

@ -948,9 +948,14 @@ void IncludeInstructionVectorRegisters(const Decoder::Instruction& inst, uint32_
switch (inst.opcode) {
case Decoder::Opcode::DS_ADD_U64:
case Decoder::Opcode::DS_OR_B64:
case Decoder::Opcode::DS_MAX_RTN_U64:
case Decoder::Opcode::DS_WRITE_B64:
case Decoder::Opcode::DS_WRITE_B96:
case Decoder::Opcode::DS_WRITE_B128: include_vector(inst.src1, inst.data_dwords); break;
case Decoder::Opcode::DS_CMPST_RTN_B64:
include_vector(inst.src1, 2u);
include_vector(inst.src2, 2u);
break;
case Decoder::Opcode::DS_WRITE2_B32:
case Decoder::Opcode::DS_WRITE2ST64_B32:
case Decoder::Opcode::DS_WRITE2_B64:

View File

@ -175,6 +175,8 @@ SharedAccess SharedAccessOf(ValueOpcode opcode) {
case ValueOpcode::SharedAtomicUMin32:
case ValueOpcode::SharedAtomicSMax32:
case ValueOpcode::SharedAtomicUMax32:
case ValueOpcode::SharedAtomicUMax64:
case ValueOpcode::SharedAtomicCmpSwap64:
case ValueOpcode::SharedAtomicAnd32:
case ValueOpcode::SharedAtomicOr32:
case ValueOpcode::SharedAtomicOr64:
@ -190,6 +192,8 @@ uint32_t SharedComponentCount(ValueOpcode opcode) {
switch (opcode) {
case ValueOpcode::SharedAtomicIAdd64:
case ValueOpcode::SharedAtomicOr64:
case ValueOpcode::SharedAtomicUMax64:
case ValueOpcode::SharedAtomicCmpSwap64:
case ValueOpcode::LoadSharedU32x2:
case ValueOpcode::WriteSharedU32x2: return 2u;
case ValueOpcode::LoadSharedU32x3:

View File

@ -303,6 +303,8 @@ VALUE_OPCODE(SharedAtomicSMin32, U32, U32, U32, U1)
VALUE_OPCODE(SharedAtomicUMin32, U32, U32, U32, U1)
VALUE_OPCODE(SharedAtomicSMax32, U32, U32, U32, U1)
VALUE_OPCODE(SharedAtomicUMax32, U32, U32, U32, U1)
VALUE_OPCODE(SharedAtomicUMax64, U64, U32, U64, U1)
VALUE_OPCODE(SharedAtomicCmpSwap64, U64, U32, U64, U64, U1)
VALUE_OPCODE(SharedAtomicAnd32, U32, U32, U32, U1)
VALUE_OPCODE(SharedAtomicOr32, U32, U32, U32, U1)
VALUE_OPCODE(SharedAtomicOr64, Void, U32, U64, U1)

View File

@ -20100,6 +20100,8 @@ CoverageClass ClassifyOpcode(ShaderOpcode opcode,
case Opcode::FLAT_STORE_DWORDX4:
case Opcode::DS_ADD_U32:
case Opcode::DS_ADD_U64:
case Opcode::DS_MAX_RTN_U64:
case Opcode::DS_CMPST_RTN_B64:
case Opcode::DS_ADD_RTN_U32:
case Opcode::DS_SUB_U32:
case Opcode::DS_SUB_RTN_U32:
@ -33184,6 +33186,90 @@ TestCase DsAtomic64Bounds(bool add) {
return test;
}
TestCase DsAtomicReturn64() {
using O = ShaderOpcode;
TestCase test;
test.name = "DsAtomicReturn64";
auto &code = test.code;
const auto pair = [&](u32 reg, uint64_t value) {
AppendVMovLiteral(&code, reg, static_cast<u32>(value));
AppendVMovLiteral(&code, reg + 1u, static_cast<u32>(value >> 32u));
};
const auto result = [&](u32 reg, uint64_t value) {
AppendStoreVgpr(&code, reg, static_cast<u32>(test.expected.size()));
test.expected.push_back(static_cast<u32>(value));
AppendStoreVgpr(&code, reg + 1u, static_cast<u32>(test.expected.size()));
test.expected.push_back(static_cast<u32>(value >> 32u));
};
AppendVMovU32(&code, 7, 0);
AppendVMovLiteral(&code, 6, 0x13579bdfu);
code.push_back(EncodeDs0(0x0d, 24));
code.push_back(EncodeDs1(0, 6, 7));
struct Case { uint64_t before, value; u32 address, offset; };
constexpr std::array cases{
Case{0x7fffffffffffffffull, 0x8000000000000000ull, 0, 8},
Case{0x8000000000000000ull, 0x7fffffffffffffffull, 0, 8},
Case{0x8000000000000001ull, 0x8000000000000002ull, 0x10007, 2},
Case{0xffffffffffffffffull, 0, 0, 8}};
for (const auto &item : cases) {
pair(2, item.before);
code.push_back(EncodeDs0(0x4d, 8));
code.push_back(EncodeDs1(0, 2, 7));
pair(4, item.value);
AppendVMovLiteral(&code, 0, item.address);
// CS23e96bf5029f62fd pc 0xec8: d9a00008 00000400. VDst aliases ADDR.
code.push_back(EncodeDs0(0x68, item.offset));
code.push_back(0x00000400u);
result(0, item.before);
code.push_back(EncodeDs0(0x76, 8));
code.push_back(EncodeDs1(8, 0, 7));
result(8, std::max(item.before, item.value));
}
struct CompareCase { uint64_t before, compare, replacement; };
constexpr std::array compare_cases{
CompareCase{0xfedcba9876543210ull, 0xfedcba9876543210ull, 0x0123456789abcdefull},
CompareCase{0xfedcba9876543210ull, 0x0edcba9876543210ull, UINT64_MAX},
CompareCase{0xfedcba9876543210ull, 0xfedcba9876543211ull, UINT64_MAX},
CompareCase{UINT64_MAX, UINT64_MAX, 0}};
AppendVMovU32(&code, 63, 0);
for (const auto &item : compare_cases) {
pair(4, item.before);
code.push_back(EncodeDs0(0x4d, 16));
code.push_back(EncodeDs1(0, 4, 7));
pair(0, item.compare);
pair(2, item.replacement);
// pc 0x1048: DATA compares, DATA2 replaces, VDst aliases DATA2.
code.insert(code.end(), {0xd9c00010u, 0x0202003fu});
result(2, item.before);
code.push_back(EncodeDs0(0x76, 16));
code.push_back(EncodeDs1(8, 0, 7));
result(8, item.before == item.compare ? item.replacement : item.before);
}
pair(4, UINT64_MAX);
AppendVMovU32(&code, 0, 24); // One DWORD remains: ignore the entire atomic.
code.push_back(EncodeDs0(0x68));
code.push_back(EncodeDs1(2, 4, 0));
result(2, 0);
pair(2, 0xabcdef0112345678ull);
AppendVMovU32(&code, 0, 0);
code.push_back(EncodeSMovB32(126, InlineU32(0)));
code.insert(code.end(), {0xd9a00008u, 0x02000400u});
code.push_back(EncodeSMovB32(126, InlineU32(1)));
result(2, 0xabcdef0112345678ull); // Inactive lanes retain both destination DWORDs.
code.push_back(EncodeDs0(0x36, 24));
code.push_back(EncodeDs1(6, 0, 7));
AppendStoreVgpr(&code, 6, static_cast<u32>(test.expected.size()));
test.expected.push_back(0x13579bdfu);
AppendEnd(&code);
test.compute_info.lds_size_dwords = 7;
test.opcodes = {O::V_MOV_B32, O::S_MOV_B32, O::DS_WRITE_B32, O::DS_WRITE_B64,
O::DS_MAX_RTN_U64, O::DS_CMPST_RTN_B64, O::DS_READ_B32, O::DS_READ_B64,
O::BUFFER_STORE_DWORD, O::S_ENDPGM};
test.required_spirv = {"OpAtomicUMax %ulong", "OpAtomicCompareExchange %ulong", "WorkgroupMemoryExplicitLayoutKHR", "Aliased"};
test.forbidden_spirv = {"OpAtomicLoad", "OpLoopMerge"};
return test;
}
TestCase DsAtomic64Contention(bool add, u32 wave_size) {
using O = ShaderOpcode;
TestCase test;
@ -39204,6 +39290,7 @@ std::vector<TestCase> MakeCases() {
cases.push_back(DsMaskedOrContention(wave_size));
}
AddCase(DsAtomicReturnVariants);
AddCase(DsAtomicReturn64);
for (bool add : {false, true}) {
cases.push_back(DsAtomic64Bounds(add));
for (u32 wave_size : {32, 64}) {
@ -44656,7 +44743,7 @@ int main(int argc, char **argv) {
RunCase(&vulkan, DsWideLdsPartialBounds());
RunCase(&vulkan, DsAtomic64Bounds(false));
RunCase(&vulkan, DsAtomic64Bounds(true));
for (auto test : {DsWideLdsPartialBounds(), DsAtomic64Bounds(false), DsAtomic64Bounds(true)}) {
for (auto test : {DsWideLdsPartialBounds(), DsAtomic64Bounds(false), DsAtomic64Bounds(true), DsAtomicReturn64()}) {
test.compute_info.lds_storage = true;
std::erase(test.required_spirv, "WorkgroupMemoryExplicitLayoutKHR");
test.required_spirv.push_back("StorageBuffer");
@ -44700,6 +44787,7 @@ int main(int argc, char **argv) {
RunCase(&vulkan, DsAtomic64Contention(add, wave_size));
}
}
RunCase(&vulkan, DsAtomicReturn64());
RunCase(&vulkan, BufferAtomicAndX2GlcAndExec());
RunCase(&vulkan, ImageAtomicSignedMinMax<false>());
RunCase(&vulkan, ImageAtomicSignedMinMax<true>());

View File

@ -7913,6 +7913,29 @@ void TestNewShaderRecompilerDsAddtidTranslation() {
CheckSpirvBinaryValidates(result.spirv);
}
void TestDsReturningAtomic64Decoder() {
using namespace ShaderRecompiler::Decoder;
// CS23e96bf5029f62fd: native DATA is the comparison and DATA2 the replacement.
const uint32_t max_words[] = {0xd9a00008u, 0x00000400u};
const uint32_t cmp_words[] = {0xd9c00010u, 0x0202003fu};
Instruction decoded;
DecodeInstruction(max_words, 0u, decoded);
Check(decoded.opcode == Opcode::DS_MAX_RTN_U64 &&
decoded.word_count == 2u && decoded.data_dwords == 2u &&
decoded.data_bits == 32u && decoded.src_count == 2u &&
decoded.src0.reg == 0u && decoded.src1.reg == 4u &&
decoded.dst.reg == 0u && decoded.offset == 8u && !decoded.gds,
"captured DS_MAX_RTN_U64 lost its QWORD width, byte offset, or aliases");
DecodeInstruction(cmp_words, 0u, decoded);
Check(decoded.opcode == Opcode::DS_CMPST_RTN_B64 &&
decoded.word_count == 2u && decoded.data_dwords == 2u &&
decoded.data_bits == 32u && decoded.src_count == 3u &&
decoded.src0.reg == 63u && decoded.src1.reg == 0u &&
decoded.src2.reg == 2u && decoded.dst.reg == 2u &&
decoded.offset == 16u && !decoded.gds,
"captured DS_CMPST_RTN_B64 lost its comparison/replacement pairs or aliases");
}
void TestDsFloatMinMaxDecoder() {
using namespace ShaderRecompiler::Decoder;
for (bool max_value : {false, true}) {
@ -15716,6 +15739,7 @@ int main() {
// here.
TestScalarAshrI64Decoder();
TestImageAtomicWidthDecoder();
TestDsReturningAtomic64Decoder();
TestDsFloatMinMaxDecoder();
TestNewShaderDecoderArchitecture();
TestImageAddressOperands();