mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
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 <noreply@anthropic.com>
This commit is contained in:
34
docs/releases/v0.5.0.md
Normal file
34
docs/releases/v0.5.0.md
Normal file
@@ -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.
|
||||
19
tools/xlate/patches/0033-cas-loop-atomic-read.patch
Normal file
19
tools/xlate/patches/0033-cas-loop-atomic-read.patch
Normal file
@@ -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<Operand, Operand> 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(
|
||||
@@ -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<Operand, Operand> 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;
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user