Translate float atomic adds, signed RED offsets, calls and subgroup operations correctly.

Patch 0023 turns a global RED or ATOM .F32 add, which the translator dropped, into a compare-and-swap loop with a float
add once GlobalToStorage knows its buffer; the light and fog accumulators of Ocarina use them, and their loss lit
Kokiri Forest's fog as if nothing shaded it. Patch 0024 reads RED's 20-bit offset as signed, as ATOM already did.
Patch 0025 keeps the block after a CAL as its successor, so code after a call is no longer dropped. Patch 0026 keeps
ballot, shuffle and vote operations where they are, so they see every lane. build_xlate.py now expects 25 patches.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
chrg6
2026-11-06 21:43:49 -05:00
parent 12fbb31451
commit 85f401669f
5 changed files with 98 additions and 2 deletions

View File

@@ -99,8 +99,8 @@ def prepare_sources() -> Path:
for junk in list(src.glob("src/*/obj")) + list(src.glob("src/*/bin")):
shutil.rmtree(junk)
patches = sorted((XLATE / "patches").glob("*.patch"))
if len(patches) != 21:
sys.exit(f"expected 21 patches, found {len(patches)}")
if len(patches) != 25:
sys.exit(f"expected 25 patches, found {len(patches)}")
for patch in patches:
# git apply works outside a repository when given a directory; --check first so a failure names the patch.
for extra in (["--check"], []):

View File

@@ -0,0 +1,48 @@
--- a/src/Ryujinx.Graphics.Shader/Instructions/InstEmitMemory.cs
+++ b/src/Ryujinx.Graphics.Shader/Instructions/InstEmitMemory.cs
@@ -217,6 +217,13 @@
{
res = context.AtomicAdd(storageKind, e0, e1, value);
}
+ else if (type == AtomSize.F32FtzRn && storageKind == StorageKind.GlobalMemory)
+ {
+ // NX2NAVI: a float add (RED/ATOM .F32.FTZ.RN), which accumulators of light and fog use, was
+ // dropped here. It is marked FP32 and becomes a compare-and-swap loop with a float add once
+ // GlobalToStorage knows its buffer (TryGenerateStorageOp).
+ res = context.Add(Instruction.AtomicAdd | Instruction.FP32, storageKind, Local(), e0, e1, value);
+ }
else
{
context.TranslatorContext.GpuAccessor.Log($"Invalid reduction type: {type}.");
--- a/src/Ryujinx.Graphics.Shader/Translation/Optimizations/GlobalToStorage.cs
+++ b/src/Ryujinx.Graphics.Shader/Translation/Optimizations/GlobalToStorage.cs
@@ -448,6 +448,13 @@
return false;
}
+ // NX2NAVI: a float atomic add has no inline form (the SPIR-V backend only adds integers): it takes the
+ // helper path, whose TryGenerateStorageOp writes a compare-and-swap loop
+ if (operation.Inst == (Instruction.AtomicAdd | Instruction.FP32))
+ {
+ return false;
+ }
+
return (operation.Inst != Instruction.AtomicMaxS32 &&
operation.Inst != Instruction.AtomicMinS32) || targetLanguage == TargetLanguage.Spirv;
}
@@ -928,6 +935,15 @@
case Instruction.AtomicAdd:
resultValue = context.AtomicAdd(StorageKind.StorageBuffer, binding, Const(0), wordOffset, value);
break;
+ case Instruction.AtomicAdd | Instruction.FP32:
+ // NX2NAVI: the word is read, the float sum swapped in if the word is still the one read,
+ // and the loop repeats otherwise (InstEmitMemory EmitAtomicOp marks these)
+ resultValue = GenerateAtomicCasLoop(context, wordOffset, binding, (memValue) =>
+ {
+ return context.FPAdd(memValue, value);
+ });
+
+ break;
case Instruction.AtomicAnd:
resultValue = context.AtomicAnd(StorageKind.StorageBuffer, binding, Const(0), wordOffset, value);
break;

View File

@@ -0,0 +1,15 @@
--- a/src/Ryujinx.Graphics.Shader/Instructions/InstEmitMemory.cs
+++ b/src/Ryujinx.Graphics.Shader/Instructions/InstEmitMemory.cs
@@ -134,7 +134,11 @@
{
InstRed op = context.GetOp<InstRed>();
- (Operand addrLow, Operand addrHigh) = Get40BitsAddress(context, new Register(op.SrcA, RegisterType.Gpr), op.E, op.Imm20);
+ // NX2NAVI: the 20-bit offset is signed, as Atom reads it; unsigned, a reduction at -12 (the culling's group
+ // counts written before a record) landed 1 MiB further on
+ int sOffset = (op.Imm20 << 12) >> 12;
+
+ (Operand addrLow, Operand addrHigh) = Get40BitsAddress(context, new Register(op.SrcA, RegisterType.Gpr), op.E, sOffset);
EmitAtomicOp(context, StorageKind.GlobalMemory, (AtomOp)op.RedOp, op.RedSize, addrLow, addrHigh, GetDest(op.SrcB));
}

View File

@@ -0,0 +1,14 @@
--- a/src/Ryujinx.Graphics.Shader/Decoders/Decoder.cs
+++ b/src/Ryujinx.Graphics.Shader/Decoders/Decoder.cs
@@ -500,7 +500,10 @@
public static bool IsUnconditionalBranch(ref InstOp op)
{
- return IsUnconditional(ref op) && op.Props.HasFlag(InstProps.Bra);
+ // NX2NAVI: a call returns to the next instruction, so the block after a CAL is its successor. Taken for
+ // a branch, that block lost its only predecessor and was dropped (AmpereLowering emits CAL for SM86
+ // CALL/RET; Ocarina's culling writes its draw lists after one).
+ return IsUnconditional(ref op) && op.Props.HasFlag(InstProps.Bra) && op.Name != InstName.Cal;
}
private static bool IsUnconditional(ref InstOp op)

View File

@@ -0,0 +1,19 @@
--- a/src/Ryujinx.Graphics.Shader/StructuredIr/AstOptimizer.cs
+++ b/src/Ryujinx.Graphics.Shader/StructuredIr/AstOptimizer.cs
@@ -69,6 +69,16 @@
case Instruction.AtomicXor:
case Instruction.Call:
case Instruction.ImageAtomic:
+ // NX2NAVI: a subgroup operation reads the other lanes, so it must stay where it was: moved into a
+ // branch it sees only the lanes that took the branch (Ocarina's culling counted 1 of 32)
+ case Instruction.Ballot:
+ case Instruction.Shuffle:
+ case Instruction.ShuffleDown:
+ case Instruction.ShuffleUp:
+ case Instruction.ShuffleXor:
+ case Instruction.VoteAll:
+ case Instruction.VoteAllEqual:
+ case Instruction.VoteAny:
return true;
}
}