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));