From d0da862c87d1129e5f7fbf171b4bce6e9036f25e Mon Sep 17 00:00:00 2001 From: chrg6 Date: Sat, 7 Nov 2026 22:05:34 -0500 Subject: [PATCH] Release 0.5.0: fix the white haze in fog and the flickering foliage. The float atomic add's compare-and-swap loop (patch 0033) carried its expected value round the loop in a local, but the pass runs on SSA form and the optimizer's copy propagation takes every value as assigned once, so the loop compared against a stale value and the froxel fog added up wrong: Kokiri Forest was washed out white. Each attempt now reads the word with an atomic OR of 0, which also cannot return a stale cached copy. Writes through the device address table go to the guest copy for stores only (patch 0034). An atomic on the guest copy started from its stale value, so a counter the GPU reset with a buffer fill kept counting there, and the page's next sync copied the wrong count into the mirror: culled foliage drew garbage for a frame. Co-Authored-By: Claude Opus 5.5 --- VERSION | 2 +- docs/releases/v0.5.0.md | 34 ++++++++++ .../patches/0033-cas-loop-atomic-read.patch | 19 ++++++ .../0033-cas-loop-expected-value.patch | 28 -------- .../0034-device-address-twin-writes.patch | 67 ++++--------------- 5 files changed, 66 insertions(+), 84 deletions(-) create mode 100644 docs/releases/v0.5.0.md create mode 100644 tools/xlate/patches/0033-cas-loop-atomic-read.patch delete mode 100644 tools/xlate/patches/0033-cas-loop-expected-value.patch diff --git a/VERSION b/VERSION index 1d0ba9ea..8f0916f7 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.4.0 +0.5.0 diff --git a/docs/releases/v0.5.0.md b/docs/releases/v0.5.0.md new file mode 100644 index 00000000..8709c4e9 --- /dev/null +++ b/docs/releases/v0.5.0.md @@ -0,0 +1,34 @@ +A fix release for *The Legend of Zelda: Ocarina of Time* for Nintendo Switch 2 (game version 1.1.1): the fog and +lighting are back to the game's own. **No game files are included**: the setup builds the game on your PC from your own +dump. + +## Install or update + +- **Already have 0.4.0:** start the launcher; it offers 0.5.0 and updates. The first start after the update prepares + the shaders again (about a minute and a half on the boot screen). +- **New install:** download **`NX2navi-Setup-0.5.0.exe`** below, put it next to your `.nup` dump of the game and start + it. Windows SmartScreen warns on the first start because the setup is not signed: choose *More info* and *Run + anyway*. The launcher builds the game (10 to 25 minutes, once), prepares every shader and starts it. + +**Requirements:** Windows 10 or 11; an x86-64 CPU with SSE4.2; 16 GB of RAM; about 30 GB free for the game and 1.5 GB +for the launcher; a GPU with Vulkan 1.3, `VK_EXT_external_memory_host` and `VK_EXT_mesh_shader` (current NVIDIA and AMD +cards). + +## Fixed in this release + +- Kokiri Forest and other foggy areas are no longer washed out in a bright white and yellow haze: the fog the game + builds up on the GPU adds up correctly again (0.4.0 broke the shader loop that adds to it). +- Small white flowers and other foliage in the grass no longer flicker white from frame to frame. +- The trees stay fixed: no dark columns reaching the sky. + +## Known issues + +- Entering a new area can hold one frame for about 0.2 s while its textures are set up, and the first visit to an + area after an install can pause briefly while the graphics driver builds what it has not seen. +- A rare crash in the game's particle code. +- DLSS Frame Generation is not offered yet: AMD FSR 3.1 frame generation works on every GPU, NVIDIA's included. + +Report problems as issues, with the game's log (`logs\nx2navi.log` in the game folder) and what you were doing. + +NX2navi is an independent, non-commercial fan project, not affiliated with or endorsed by Nintendo. *The Legend of +Zelda*, *Ocarina of Time* and *Nintendo Switch* are trademarks of Nintendo. diff --git a/tools/xlate/patches/0033-cas-loop-atomic-read.patch b/tools/xlate/patches/0033-cas-loop-atomic-read.patch new file mode 100644 index 00000000..0279c25e --- /dev/null +++ b/tools/xlate/patches/0033-cas-loop-atomic-read.patch @@ -0,0 +1,19 @@ +--- a/src/Ryujinx.Graphics.Shader/Translation/Optimizations/GlobalToStorage.cs ++++ b/src/Ryujinx.Graphics.Shader/Translation/Optimizations/GlobalToStorage.cs +@@ -960,11 +960,15 @@ + + private static Operand GenerateAtomicCasLoop(EmitterContext context, Operand wordOffset, int binding, Func opCallback) + { ++ // NX2NAVI: each attempt reads the word with an atomic OR of 0, which returns its current value; a plain load ++ // could keep returning a stale cached copy (seen on AMD), so an invocation that lost the race never matched ++ // again and the loop did not end. The read stays inside the loop: this pass runs on SSA form, and the ++ // optimizer's copy propagation takes every value as assigned once, so no local can carry a value round it. + Operand lblLoopHead = Label(); + + context.MarkLabel(lblLoopHead); + +- Operand oldValue = context.Load(StorageKind.StorageBuffer, binding, Const(0), wordOffset); ++ Operand oldValue = context.AtomicOr(StorageKind.StorageBuffer, binding, Const(0), wordOffset, Const(0)); + Operand newValue = opCallback(oldValue); + + Operand casResult = context.AtomicCompareAndSwap( diff --git a/tools/xlate/patches/0033-cas-loop-expected-value.patch b/tools/xlate/patches/0033-cas-loop-expected-value.patch deleted file mode 100644 index eacdfb53..00000000 --- a/tools/xlate/patches/0033-cas-loop-expected-value.patch +++ /dev/null @@ -1,28 +0,0 @@ ---- a/src/Ryujinx.Graphics.Shader/Translation/Optimizations/GlobalToStorage.cs -+++ b/src/Ryujinx.Graphics.Shader/Translation/Optimizations/GlobalToStorage.cs -@@ -960,11 +960,16 @@ - - private static Operand GenerateAtomicCasLoop(EmitterContext context, Operand wordOffset, int binding, Func opCallback) - { -+ // NX2NAVI: the word is loaded once; a failed compare-and-swap returns the word's current value, which becomes -+ // the next attempt's expected value. A plain load in every attempt could keep returning a stale cached copy -+ // (seen on AMD), so an invocation that lost the race never matched again and the loop did not end. - Operand lblLoopHead = Label(); - -+ Operand oldValue = Local(); -+ context.Copy(oldValue, context.Load(StorageKind.StorageBuffer, binding, Const(0), wordOffset)); -+ - context.MarkLabel(lblLoopHead); - -- Operand oldValue = context.Load(StorageKind.StorageBuffer, binding, Const(0), wordOffset); - Operand newValue = opCallback(oldValue); - - Operand casResult = context.AtomicCompareAndSwap( -@@ -977,6 +982,7 @@ - - Operand casFail = context.ICompareNotEqual(casResult, oldValue); - -+ context.Copy(oldValue, casResult); - context.BranchIfTrue(lblLoopHead, casFail); - - return oldValue; diff --git a/tools/xlate/patches/0034-device-address-twin-writes.patch b/tools/xlate/patches/0034-device-address-twin-writes.patch index cacd6d39..5c1c8cef 100644 --- a/tools/xlate/patches/0034-device-address-twin-writes.patch +++ b/tools/xlate/patches/0034-device-address-twin-writes.patch @@ -1,45 +1,6 @@ --- a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Instructions.cs +++ b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Instructions.cs -@@ -1802,7 +1802,13 @@ - SpvInstruction one = context.Constant(context.TypeU32(), 1); - SpvInstruction zero = context.Constant(context.TypeU32(), 0); - -- return new OperationResult(varType, emitU(context.GetType(varType), elemPointer, one, zero, value)); -+ SpvInstruction result = emitU(context.GetType(varType), elemPointer, one, zero, value); -+ if (operation.StorageKind == StorageKind.GlobalMemory) // NX2NAVI: the twin (GetDeviceAddressTwinPointer) -+ { -+ emitU(context.GetType(varType), GlobalTwinPointer(context, operation), one, zero, value); -+ } -+ -+ return new OperationResult(varType, result); - } - - private static OperationResult GenerateAtomicMemoryCas(CodeGenContext context, AstOperation operation) -@@ -1815,7 +1821,22 @@ - SpvInstruction one = context.Constant(context.TypeU32(), 1); - SpvInstruction zero = context.Constant(context.TypeU32(), 0); - -- return new OperationResult(varType, context.AtomicCompareExchange(context.GetType(varType), elemPointer, one, zero, zero, value1, value0)); -+ SpvInstruction result = context.AtomicCompareExchange(context.GetType(varType), elemPointer, one, zero, zero, value1, value0); -+ if (operation.StorageKind == StorageKind.GlobalMemory) // NX2NAVI: the twin (GetDeviceAddressTwinPointer) -+ { -+ context.AtomicCompareExchange(context.GetType(varType), GlobalTwinPointer(context, operation), one, zero, zero, value1, value0); -+ } -+ -+ return new OperationResult(varType, result); -+ } -+ -+ private static SpvInstruction GlobalTwinPointer(CodeGenContext context, AstOperation operation) -+ { -+ return GetDeviceAddressTwinPointer( -+ context, -+ ((AstOperand)operation.GetSource(0)).Value, -+ context.Get(AggregateType.U32, operation.GetSource(1)), -+ context.Get(AggregateType.U32, operation.GetSource(2))); - } - - private static OperationResult GenerateLoadOrStore(CodeGenContext context, AstOperation operation, bool isStore) -@@ -1856,11 +1877,29 @@ +@@ -1856,11 +1856,25 @@ // second-level block, 0 for none (block 0 stays zero) // word 65552 second-level blocks of 4096 entries, one per 4 KiB page: the record of the buffer that holds // the page, 0 for none @@ -51,28 +12,24 @@ + // then the same for the page's twin, the pool's imported buffer in guest memory when the first is + // its VRAM mirror (low words first); the host never changes a record a page names, so a page entry + // read at any time gives a whole one -+ // The pointer addresses the 32-bit word at the address's 4-byte-aligned word. Stores and atomics also go to the -+ // twin (GetDeviceAddressTwinPointer): what the GPU writes into a mirror through the table is not recorded as a -+ // GPU write, so without it the CPU never saw it and a sync of a CPU-written page copied the stale guest page -+ // over it (runtime/vk_gpa.c). ++ // The pointer addresses the 32-bit word at the address's 4-byte-aligned word. Stores also go to the twin ++ // (GetDeviceAddressPointers): what the GPU writes into a mirror through the table is not recorded as a GPU write, ++ // so without it the CPU never saw it and a sync of a CPU-written page copied the stale guest page over it ++ // (runtime/vk_gpa.c). Atomics do not: the twin's word may be stale (a counter the GPU reset with a buffer fill ++ // went on counting from its old value in guest memory), and that page's next sync copied the wrong count into ++ // the mirror, so culled instance lists drew garbage for a frame (flickering foliage). private static SpvInstruction GetDeviceAddressPointer(CodeGenContext context, int binding, SpvInstruction low, SpvInstruction high) { + return GetDeviceAddressPointers(context, binding, low, high, out _); + } + -+ // The twin of an access's pointer: the guest copy of a mirrored page, else the scratch buffer. -+ private static SpvInstruction GetDeviceAddressTwinPointer(CodeGenContext context, int binding, SpvInstruction low, SpvInstruction high) -+ { -+ GetDeviceAddressPointers(context, binding, low, high, out SpvInstruction twin); -+ return twin; -+ } -+ ++ // twinPointer: the guest copy of a mirrored page, else the scratch buffer. + private static SpvInstruction GetDeviceAddressPointers(CodeGenContext context, int binding, SpvInstruction low, SpvInstruction high, out SpvInstruction twinPointer) + { SpvInstruction u32 = context.TypeU32(); SpvInstruction u64 = context.TypeInt(64, 0); SpvInstruction tableVar = context.StorageBuffers[binding]; -@@ -1879,16 +1918,26 @@ +@@ -1879,16 +1893,26 @@ SpvInstruction page = context.BitwiseAnd(u32, context.ShiftRightLogical(u32, low, U(12)), U(0xfff)); SpvInstruction record = Word(context.IAdd(u32, context.IAdd(u32, context.IMul(u32, block, U(4096)), U(65552)), page)); SpvInstruction mapped = context.INotEqual(context.TypeBool(), record, U(0)); @@ -101,7 +58,7 @@ } private static SpvInstruction LoadAligned(CodeGenContext context, SpvInstruction type, SpvInstruction pointer) -@@ -1912,7 +1961,7 @@ +@@ -1912,7 +1936,7 @@ SpvInstruction u32 = context.TypeU32(); SpvInstruction low = context.Get(AggregateType.U32, operation.GetSource(1)); SpvInstruction high = context.Get(AggregateType.U32, operation.GetSource(2)); @@ -110,7 +67,7 @@ StorageKind kind = operation.StorageKind; int bits = kind is StorageKind.GlobalMemoryS8 or StorageKind.GlobalMemoryU8 ? 8 : kind is StorageKind.GlobalMemoryS16 or StorageKind.GlobalMemoryU16 ? 16 : 32; -@@ -1921,7 +1970,9 @@ +@@ -1921,7 +1945,9 @@ { if (isStore) { @@ -121,7 +78,7 @@ return OperationResult.Invalid; } -@@ -1939,6 +1990,8 @@ +@@ -1939,6 +1965,8 @@ SpvInstruction semantics = context.Constant(u32, 0); context.AtomicAnd(u32, pointer, scope, semantics, context.Not(u32, mask)); context.AtomicOr(u32, pointer, scope, semantics, context.ShiftLeftLogical(u32, value, shift));