21 Commits

Author SHA1 Message Date
chrg6
5faa1687f4 Add quad_index, which draws an indexed quad list as two triangles per quad and closes an indexed line loop, from the game's own indices, and log the first such draw either way.
Vulkan has no quad or line-loop topology. Draws without indices were rewritten already; an indexed quad list went to the GPU with its indices under the triangle list topology, three at a time, joining corners of neighbouring quads, and an indexed loop lacked its closing line. Off by default and read at start; no log of Ocarina has shown such a draw so far, and the new line says when one comes. tests/index_rewrite_check.py checks the index lists.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 08:36:46 -05:00
chrg6
9d5f16bda4 Add oq_hold, which makes a samples-passed report whose draws the full occlusion query ring left uncounted repeat its record's last complete count, and log how often such reports happen.
A draw that finds every query slot still waiting for the completion thread goes uncounted, and the report after it said only what the other draws counted, at least 1: the game takes the object it tested for all but hidden for that frame. With the setting the report writes the larger of that and the last complete count reported into the same record. Off by default and read at start; the count of such reports is logged at the 1st, 10th, 100th and so on either way. tests/oq_hold_check.py checks the rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 08:36:46 -05:00
chrg6
cc5c8f87ad Add shadow_pcf, which makes every depth-compare sampler filter linearly so each shadow test blends the 2x2 shadow map texels around it, and log the filters of the compare samplers made.
Through a nearest filter each of the game's shadow compares tests one texel, so shadow edges are stair steps of shadow map texels that crawl over the ground as the sun cascades follow the camera. The setting is off by default and read at start; with shadow_scale above 1 the samplers were already linear.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 08:36:46 -05:00
chrg6
97c3bac3bd Keep a small buffer clear and the counter reports of the same record in the order of the stream.
A ClearBuffer or ReportValue is a fill the GPU makes where it stands in the stream, and guest memory has its bytes once the GPU got there. A counter report reaches guest memory as a CPU write after its submission's fence, and a render condition reads the record when it is replayed. So a report followed by a clear of the same record ended as the report (in one submission always, across two when the completion thread was behind the GPU), and a condition replayed after the clear read what memory held before the fill, or the 0 of the report the clear had overwritten.

A clear of at most 256 bytes into CPU-visible memory is now also applied at its place in the replay to the records it covers that a counter report went into: the covered bytes get the value in guest memory at once, reports replayed before the clear leave those bytes alone when their write comes, a condition reads them from memory, and a report replayed after the clear writes the whole record as before. The GPU's fill is recorded as before. Other small clears stay the GPU's fill alone: the GPU reads guest memory too, at once in an imported pool and through the next upload of the written page in a mirrored one, so a write ahead of it into a draw count or an atomic counter would reach passes that still want the old value. NX2NAVI_CLEAR_SMALL_CPU=0 turns it off.

Whether the game clears counter records is not known. The pass timing captures of 75 scripted runs show one buffer fill a frame and nothing else about it, and the frame dump does not list buffer fills. The periodic vk_usnap line now counts the small clears, the last one's size, value and address, how many covered a record with a report, the reports into bytes cleared before, and the report writes that left cleared bytes alone.

Checked with a build of the dev exe, the launcher tests, the device address table check, and a scratch link of vk_exec_xfer.c against stubs that replays reports and clears in both orders. Not run in the game.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-12 19:49:40 -05:00
chrg6
857d22fe4f Apply texture and sampler registrations between submissions, in the order the game made them.
A registration the game made while a submission was being replayed changed the slot table under that submission,
whose texture pool set is written as the replay goes and read when it runs. Such a registration now waits and takes
effect once the submission has gone to the queue; with the replay on its own thread it goes into the replay queue
ahead of the next item. NX2NAVI_REG_ORDER=0 restores the old behaviour. In a town session the white kerb, stair and
wall-top edges of single frames were gone with it; the log counts the registrations that waited.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-12 13:58:36 -05:00
chrg6
99bdecd725 Bring the graphics notes' checks up to the code: dual-source blend and depth compare in compute are done, and what the timestamp mismatch can change.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-12 09:31:49 -05:00
chrg6
518d55ed07 Draw lines as wide as the game sets them.
The game's line width was dropped at the call and every line drawn 1 pixel wide; the game draws two lines a frame at 4.5 pixels into a 144x144 mask. The width is now recorded with the draw state and set as dynamic state for draws of the line topology class, held to the device's range. Only pipelines of that class list the new dynamic state, so the others keep their entries in the pipeline caches and the driver's. line_width = 0 draws every line 1 pixel wide as before.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-12 09:31:04 -05:00
chrg6
677a69378f Fill the fragment inputs a mesh program does not write, with the point sprite's coordinate for point meshes.
A mesh pipeline whose fragment stage reads a location the mesh program never writes now gets a fragment module where
that input is a private variable set to (0, 0, 0, 1) at entry, or to (s, t, 0, 1) of gl_PointCoord when the mesh
program draws points. The fragment shaders recorded with two of the game's point-drawing mesh programs read location
5, which those programs never write; Vulkan leaves such an input undefined.

Checks: tests/fs_fill_inputs_check.py passes; 237 recorded fragment modules rewritten offline pass spirv-val; a
60 s Kakariko run with the change shows the area as before with no Vulkan errors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 19:38:18 -05:00
chrg6
e8b4d29f91 Give mesh output components outside the program's output map Maxwell's default of (0, 0, 0, 1).
A mesh program's header lists the components of each output vector it has; the fragment stage may read the others.
The native mesh entry function never wrote them, so they read as whatever the output held, and the fallback's vertex
stage wrote 0 for all of them, w included. Maxwell gives an unwritten component (0, 0, 0, 1), as the pipeline code
already pads a missing vertex output. Both paths now write that default: MeshSpirv.cs after copying each vertex's lines,
mesh_fb.c meshfb_vs with w as the bits of 1.0f whatever the type.

Checked: all 520 task and mesh programs of an installed shader cache translate and pass spirv-val (vulkan1.3); 500
mesh modules gained about 730 bytes each on average, so many of the game's mesh programs have partly listed vectors.
--probe-meshfb passes (fallback pictures equal the mesh pipeline's), and its module part converts all 520 new
translations with 0 failures, all 1,020 modules pass spirv-val and their compute pipelines build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 19:38:17 -05:00
epicmeme546
e38f42a959 Test for NaN on the bits of the number on AMD's driver for RX 5000 and 6000 cards, so that no shader compiler can leave a test out.
The reports of black and white blocks on Navi, on Link next to her and on bright surfaces seen from inside a house come from RX 5000 and 6000 cards, whose Windows driver compiles with "AMD proprietary shader compiler" (vk_caps.amd_sc); an RX 7900 XT (LLPC) draws the same scenes clean. The game's shaders test for NaN with OpIsNan in 48,717 of an install's 54,959 translated modules, 26 tests each on average, and the translator's guard that keeps NaN out of every float colour output, so that one bad texel does not spread through blur and bloom into blocks, is such a test too. SignedZeroInfNanPreserve asks the driver to keep those tests; whether that compiler does is not known.

With that driver, vk_pipeline.c spirv_nan_bits now turns every 32-bit isnan, scalar or vector, into (bits & 0x7fffffff) > 0x7f800000 on the number's bits, and a float select with a constant whose bits are a NaN into a select of the bits (issue #156), when the module is created. NX2NAVI_VK_NAN_BITS=1 does it on every driver, 0 on none. Run over every module of an install's shader cache, the rewrite changed 1,281,675 tests and 57 selects with every output well formed (each id defined once, below the bound, the uint types declared once); --selftest checks it on a small module, and Link's house with it forced on (RX 7900 XT) draws as without it. Not yet tried on an RX 5000 or 6000 card.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 13:08:03 -05:00
chrg6
07721c0058 Test an event wait against the value the event had when the submission took its copies, so a frame is no longer drawn from data the game had not finished writing.
Each frame the game sets one of two events to 0 from the CPU, submits its worker queues and the main queue, whose first command waits for that event to be 1, and signals 1 with nvnEventSignal once it has written what the frame's commands read. The recording of a submission takes the VRAM mirror sync and the uniform snapshot at its start and tested the wait a moment later, so a signal that arrived in between passed the wait and the frame was drawn from copies taken before the signal: single frames with the whole scene's shadowing missing (6 to 7 of 255 brighter, the sky and the interface unchanged) or a black scene, more of them the busier the CPU.

Every nvnEventSignal is now numbered and keeps the value it replaced, the recording notes the number before it takes its copies, and a signal with a later number does not count for that submission. The submission stops at the wait like any wait reached before its signal and goes on at once in a submission of its own, whose copies are taken after the signal. NX2NAVI_EVENT_AT_SYNC=0 gives the earlier test. NX2NAVI_EVO_TRACE=1 logs the order in which every frame's submissions, waits and signals were replayed, which is how the frames were found.

Measured with the flight recorder's thumbnails, 1,800 presented frames a run, standing in Hyrule Field by day with the lazy write-back on, DLSS Performance, frame generation 4x and HDR: the build before this change showed 34 single-frame spikes with eight busy threads beside the game, 20 with four and 8 with six, most of them in frames whose signal came during the replay; with this change two runs with eight busy threads showed 0 and 0, with 667 and 724 such frames. A session of the user's with the lazy write-back off ran 25,200 frames without a spike in its three automatic dumps while the change stopped 4,744 wait tests.

Not measured: runs of this same build with the switch off (the two tried failed for other reasons, a device loss during the boot beside another game and a load with cold caches), any run with the lazy write-back off under load, the frame rate and GPU time against the switch off, and the differential tests, which this change does not touch. The flicker with the lazy write-back off that was reported has not been reproduced in a capture; its mechanism is the same signal and occurs there too.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 10:26:51 -05:00
Chris Toph
47d9a2d502 Seal the texture pool set when an early replay part is submitted.
A replay part handed to the queue early kept reading the bindless pool set while later parts of the same submission wrote new entries into it. That updates descriptors a pending command buffer reads, and on RADV a texture whose memory was mapped after the part was submitted is absent from its page tables. This caused a gfxhub read fault in the 128x128 pass of the first world frame in Kokiri Forest.

Seal the set when a part goes, take another ring set at the next sync that has entries to write, and keep recording the same part while every other set belongs to the submission. Document the descriptor lifetime contract in maxwell.md 4.9.

Fixes #257.
2026-11-09 20:52:40 -05:00
Chris Toph
b88439e2e0 Reflect every location in vertex and fragment interface arrays. 2026-11-09 20:51:04 -05:00
chrg6
66927b8f94 Ask every driver but NVIDIA's to keep NaN and infinity in the game's shaders, which should bring Navi's orb back on AMD GPUs.
The game's shaders rely on IEEE NaN and infinity results: unordered comparisons compile to x > c || isnan(x), a
zero-length normalisation gives an infinity that a later test catches, integer masks travel through float selects,
and the translator's own NaN guards are isnan tests. Our SPIR-V never declared SignedZeroInfNanPreserve, which lets a
driver assume floats are never NaN or infinite. NVIDIA's driver keeps the IEEE results anyway; AMD's compilers do not
(issue #156: a NaN-bit select read as a signed integer gave 2^32; #41 and #110: black and white blocks on Navi and on
Link next to her). The player report: AMD GPUs draw Navi's sparkles but not her orb, whose light sits at her own
centre.

vk_pipeline.c spirv_nan_preserve now adds the capability, SPV_KHR_float_controls and the mode for 32-bit floats (16
and 64 where the module declares them and the driver can) to every game shader module. NX2NAVI_VK_NAN_PRESERVE: unset
on every driver except NVIDIA's own, 1 everywhere, 0 off. --selftest checks the rewrite; the rewritten modules of 600
translated game shaders pass spirv-val for Vulkan 1.3. Not yet seen on an AMD GPU.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 11:17:08 -05:00
chrg6
e7f6a00407 Translate every shader, keep atomic loops from spinning on stale values, harden pool teardown, and update the README.
Shaders: a switch whose index is clamped against a register that a MOV of an immediate set before it now finds its
jump table (AmpereLowering.cs BrxTargets), so all 36,031 of the game's shader variants translate and no draw is skipped
for a failed one. The compare-and-swap loop behind storage-buffer atomics (patch 0033) takes the value a failed swap
returns as its next expected value instead of loading the word again, which on some drivers kept returning a stale
copy.

Memory: an unused physical pool outlives the submission being recorded, as virtual pages may still reach it through
device addresses; an image whose creation failed is not tried again for 2 s.

The installer and Linux texts name this game.
The README has the project icon, the current status, features, requirements and known issues.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 18:03:20 -05:00
chrg6
37c06e6894 Sample terrain with the right mip levels, keep NaNs out of min, max and clamp, and give shadow gathers their own tap offsets.
The runtime's TXD gradient transpose (gfx_shader.c patch_txd_transpose) is off for Switch 2 titles: the SM86 lowering
already orders the interleaved derivatives for the translator, so transposing again swapped them back. The
translator's float min, max and clamp are the NaN-safe forms (patch 0031), as Maxwell FMNMX and saturate behave, so a
NaN no longer spreads through lighting and bloom on drivers that pass it on. Depth-compare gathers with per-tap offsets
are split into one gather per tap like the others (patch 0032), where all four taps took the first tap's offset.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 16:28:53 -05:00
chrg6
ee6fbaf0bb Zero shader locals with DLSS and DLAA on too, which removes the speckled floor tiles there.
With DLAA on, function-local variables stayed undefined, and NVIDIA's driver filled the game's clustered-light reads
with other warp lanes' values: floors around Link lit in 4x8-pixel tiles. The setting was also part of every shader
cache key, so shaders prepared with DLSS off missed with it on. Zeroing is now on for every setting; DLSS still finds
the game's FXAA by its zeroed hash and keeps its camera motion. A log line now says when DLSS replaces the game's
scaling draw.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 13:31:06 -05:00
chrg6
1d7bc90848 Fix shadows, colour writes and present pacing on drivers that read Vulkan's undefined cases differently.
A depth-compare read through a sampler whose compare is off now gets the slot's comparing sampler, as the Switch
compares there and only NVIDIA's driver did so on its own. Colour components a fragment shader does not write are
masked out, as Maxwell leaves them alone. Without mailbox presentation the swapchain takes relaxed FIFO, or
immediate, so a late frame no longer halves the frame rate. NX2NAVI_VK_NOOPT=1 builds pipelines without the
driver's optimiser, for a driver whose compiler fails on one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 10:59:59 -05:00
chrg6
1b39aabd37 Describe what the code does in comments and docs.
Comments say what the code does and why, docs/maxwell.md keeps the Maxwell facts, and fetch_refs.sh clones only the checkout the build reads. Code is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 05:04:40 -05:00
chrg6
618b9ba4e5 Fix Ocarina's white dialogue frames, the fountain floor speckle and the button prompt text
The texture pool descriptors are now a ring of sets, one per submission, brought up to date from a change log, and the first submission of each frame waits for its GPU work before the game goes on: the GPU ran late and part of a frame read data of the next one, and distant characters drew NaN that bloom spread over the frame. The window acquire also waits for the image's previous frame. Function locals start at zero unless DLAA is on, which removes the per-warp light tiles on the Great Fairy Fountain floor and Link's dark, speckled look. Texture slots follow only live entries of their own target, so the volumetric fog gets its 3D volume. The pool refresh walks only the slots that changed.

The translator implements PRMT (patch 0021), which it only logged before; the button prompts render again. HFMA2.RELU no longer depends on it, HMMA.F16 rounds once and HMMA.F32 is lowered, checked against GPU vectors with a new Maxwell warp emulator.

The frame dump gains a re-arming white-frame trigger, a trigger on a depth target's first pass, NaN pixels in magenta, image handles and view targets, and depth copies of sampled depth textures.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 05:04:39 -05:00
Valiantsin Dzerakh
0478c13e45 Import the base tree (MIT, see LICENSE) 2026-11-07 05:04:38 -05:00