46 Commits

Author SHA1 Message Date
chrg6
7a1c220e06 Test held work against the event's value now when another thread asks without the submission lock, and read the held count and the submission count as atomics there.
vk_exec_held_ready runs without sub_lock (vk_backend.c held_resume, submit_sync). It decided from the published copy
since 688b2506, but tested the wait with vk_exec_event_met, which compares at the copy point of the submission being
recorded (vk_exec_xfer.c ev_mem, ev_sync_seq). That point is set by whichever thread holds the lock, so a thread asking
while another one recorded tested "at sync" against that other submission's copies. Its question is whether held work
can go on in a submission of its own, which takes its copies later: vk_exec_event_met_now tests the value in memory now
or the signals replayed so far. Under the lock nothing changes: evo_resume and cont_ready keep the copy-point test, and
vk_exec_record asks right after its own copy point, where the two agree.

evo_pending and vk_exec_submit_seq are written under the lock and were read as plain values by vk_exec_held,
vk_exec_held_ready and the held present's since and PH_MAX checks; they are now stored and loaded as atomics
(vk_exec_submit_seq_now), so the compiler cannot keep a stale copy in an unlocked reader.

Not reproduced before the change. Checks that passed: pass_cc_check on every runtime file (a header changed), no new
-Wall -Wextra warnings in vk_exec.c, vk_exec_xfer.c and vk_backend.c, evo_peek_check, target_frames_check and
oq_hold_check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 20:13:02 -05:00
chrg6
d9429b0c00 Break the memory report down past VMA, by owner in system memory, and by growth over a session.
Every report adds a line for video memory outside VMA (the driver's usage and budget per heap against VMA's blocks, and counts of pipelines by kind and by last use, stage libraries, compute pipelines, layouts and descriptor pools, with the swapchain, DLSS and FSR sizes), a line that splits the process's commit by owner (the game's heap and maps, device-local memory, VMA's host blocks, the C runtime heap with the shader caches, pipeline tables, texture cache, log queue and flight recorder in it, the driver's pipeline cache data and the rest), and a line naming what grew by more than 64 MiB since the report before and since play began. Every ten minutes of play and at exit a ranking line orders the categories by growth and marks those that only climbed; tests/mem_growth_check.py checks that bookkeeping.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 06:46:31 -05:00
chrg6
a464ffafbc Merge Tobim6's work for older graphics cards: mesh shaders emulated with compute, video memory kept within the heaps' budgets, staged mirror uploads and the submit wait that goes with them.
Conflicts: the settings key lists take both sides; frame generation keeps the camera-cut handling and logs its counters every 600 frames with the cuts among them; the pass-time mark moves into draw_viewport; mo_desc keeps the pose depth and notes the reader.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-12 12:14:58 -05:00
chrg6
b1e8c1427b Merge branch 'claude/pr-intake-6-43af31' into ra-all-test 2026-11-12 11:23:54 -05:00
chrg6
5f10a0012b Merge remote-tracking branch 'nx2navi/main' into ra-all-test 2026-11-12 11:23:49 -05:00
epicmeme546
322d7c0933 Run DLSS or FSR where the game's own upscaler runs, and fill a jittered pass's edge rows as soon as it ends.
With the HUD hidden the screen showed the scene at its render size instead of the upscaler's picture. The upscaler ran at the pass after the game's own upscaler, taken for the pass the HUD is drawn into. Without a HUD the game draws no such pass: the next one is the frame's last, which encodes the stand-in's scaled scene into the 8-bit output. The upscaler's picture went into that target and the last draw drew over it. A player's save with the HUD off showed blocks of render pixels, a picture jumping by a render pixel between frames, and the undrawn edge row in white, at every FSR mode and at Native AA; every test save until now had the HUD on. vk_dlaa_upscaler now runs the upscaler right after the stand-in of the game's upscaler wrote its output image, and puts the picture there (NX2NAVI_DLAA_AT_UPSCALER=0: as before).

The edge fill moves to the end of each pass that took the jitter (vk_dlaa_pass_end), for the pass's colour targets and depth, in one copy of eight regions per image. Filled only before the upscaler, the undrawn row had already been read by the water, the bloom and the anti-aliasing pass.

On a copy of the player's save (HUD off), FSR 4 Ultra Performance, world slowed to 1/20, present screenshots 0.25 s apart: the picture moves 0.00 to 0.02 px between shots (old placement: a jump of one render pixel in 5 of 20), the change on edges is 1.9 against 16.9, and no shot has the white row (half had it, with HDR on). Native AA, Performance with HDR, and FSR 3.1 are steady and clean as well; the save with the HUD on is unchanged. A run through a stream: 0 of 106 shots with a bright bottom edge (20 of 102). Frame rate 58.3 fps against 58.6.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-12 11:14:16 -05:00
DeepDaddyTTV
5b7703e08e Record the draws rejected during preparation in a captured frame.
A draw can be left out before it opens a render pass, and the pass list of a frame capture then showed nothing of it.
passes.txt now ends with the rejected draws of the captured frame by mesh or vertex draw and reason, each with the
first program it met, and says so when there were none. A draw left out by NX2NAVI_SKIP_PROG or NX2NAVI_SKIP_SHADERS
is counted under a reason of its own. The frame to capture and its files are set together under the recorder's lock.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-12 10:40:53 -05:00
Tobim6
38a3824b8f Mesh emulation: cut the part at a pass's first emulated draw instead of splitting it, so the texture pool set stays.
A split hands the part to the queue and seals its texture pool set; the next part takes another set of the ring and
refreshes its entries first. The splits for the mesh stages made 5 parts a submission and 6 set switches a frame at
2.3 ms of replay each (Kokiri Forest on 0.7.1, an RX 580: replay 32-38 ms a frame, the replay thread blocked on fences
26-35% of the time, 79 waits for a set the GPU still used). A cut (vk_sync_cut) ends the command buffer without
submitting it: the part and its pre-pass command buffer wait in a chain and go to the queue, in order, with the next
part that does, so the set is the same one throughout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 16:22:37 +01:00
Tobim6
55ab90c66e Mesh emulation: the stages of a pass's emulated draws run before the pass, in a command buffer of their own.
Each emulated mesh draw ended the pass, ran its stages between four barriers and began the pass again: a pass of 20
emulated draws became 20 passes, and on an RX 580 6 to 7 ms of a frame's 30 ms of GPU time went to the gaps between
passes (gpu_passes in Kokiri Forest: 8.7-10 ms per frame outside the game's passes and dispatches, 6.3-7.7 ms of it in
273-306 gaps with barriers only; 71 emulated draws a frame, 36 of them indirect). Now the first emulated draw of a pass
ends the part there (vk_exec_split_now), so the pass begins a new part, and the stages of every emulated draw of that
pass, the indirect ones' ring entries included, go into the part's pre-pass command buffer (vk_sync_pre), which the
queue runs after the mirror prologue and before the part; the pass is not split again. NX2NAVI_MESH_EMU_BATCH=0 (Lama
patches: Mesh emulation before the pass) brings the split back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 16:22:35 +01: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
epicmeme546
ffa599ef33 Let the submitting thread go on past the replay workers' groups, with up to four groups in flight.
With NX2NAVI_REPLAY_PIPE=1 (off by default) the submitting thread hands a group's chunks to the workers, takes chunks
of it itself until none is left to take, and goes on with the ops after the group in a new command buffer after the
chunks' places in the part (vk_sync_append_later), also into the next group's front pass.
Up to four groups are handed out and not in their part yet; their chunks wait in one queue in the order of the stream.
The submitting thread waits for them only where the order needs it: for room, before a part goes to the GPU, before
copies and texture or buffer clears, where DLAA's run or the contact shadows' hook acts, and where a lookup would make
a texture entry or take back a retired one (the op goes again afterwards, as a chunk's does in turn).

What the chunks read stays as the stream had it while the submitting thread runs ahead: uniform snapshot drops count
from their place in the stream, a chunk reads only the inline uniform updates before its group, the HDR present's
source goes by stream position, the texture pool set syncs under the chunks' lock, each group keeps its own render
conditions, occlusion segments and closed query slots, and the queries after a group's last report go into the next
segment made after it. A group's first chunk runs its pass hooks after the group before began its pass; turns wait
for every earlier chunk, of earlier groups too. Locks the chunks take, the submitting thread takes while it is behind.

Draws keep the sampled-texture descriptors they resolved per draw context and reuse them without the texture lock
while their slot is unchanged; the workers waited for that lock at nearly every draw. Texture lookups that change
nothing no longer wait for their turn. The perf line says where the submitting thread waited, what the chunks took,
and which texture lookups waited; NX2NAVI_GPU_TIMELINE also shows the chunks and the waits.

Off by default because it does not pay yet: in Hyrule Field with 3 workers it shortens the replay (11.5 against 12.1
ms a frame) but not the frame (49.2 against 49.6 fps in the same batch). The GPU gets a group's commands only once its
last chunk is recorded, so the main thread's wait for the GPU after the frame's first submission grew by 0.8 ms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 04:39:12 -05:00
epicmeme546
9fe3dd3331 Replay workers record mesh draws, and stop waiting for each other at queries and per-object motion.
Mesh draws ended a group and stayed on the submitting thread: 119 a frame in Hyrule Field at about 13 us each, and the
groups around them were cut short. They are recorded in the chunks now: the entries filled ahead for the indirect ones
are the draw context's, the ring index is taken atomically and the ring is made before the chunks record, each chunk
counts the ops that break a run of them on its own, and the lazy lookups of the mesh and shading-rate entry points store
their pointer before the flag that says it is there. With the mesh fallback, and while NX2NAVI_GPU_PASSES copies the
workgroup counts, they stay on the submitting thread.

A chunk's closed occlusion query slots go into a list of its own and into their segments once the group is done, in
chunk order, instead of under a lock per query. The per-object motion check decides alone per draw context (its
variant memo is per context) and takes its lock only to list a draw. The occlusion query ring has 16384 slots: the
chunks' blocks are sized to their draws, and with 4096 the ring came round to slots not reset yet.

The perf line splits the submitting thread's time with workers on: ops outside groups by kind, the front pass, its own
chunks and the wait for the others, and which ops ended the groups.

Hyrule Field, RX 7900 XT, 3440x1440, FSR 4 Native AA, same batch: standing 51.7 fps with 3 workers against 43.6 with
the workers off (replay 11.2 against 15.1 ms); along the field route 48.9 against 44.7. Screenshots standing and along
the route match the replay without workers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 02:15:35 -05:00
epicmeme546
a0a7548d30 Replay workers take their occlusion query slots in the order of the stream.
The chunks of a parallel group took query slots from the ring as they recorded, so the slots of the group's chunks
interleaved. The completion thread reads and resets slots in runs that follow the ring, and with interleaved slots it
made four times the result calls and a reset per slot: the wait after a frame's first submission ended 3.5 ms after
the GPU had run it instead of 0.6 ms, and the replay time the workers saved went into that wait (44 fps with 3 workers
as without). The front pass now gives each chunk a block of slots, as many as its draws that count samples and render,
in the order of the stream; a segment's slots are taken in ring order. Standing in Hyrule Field, 3 workers: 49.8 fps
against 43.7 with the workers off.

NX2NAVI_GPU_TIMELINE=<frame> writes four frames from that present on to the log: each replay, each timed part's submit
and GPU run, the waits for the GPU and the workers' groups, so a run shows whether the GPU waits for the replay, a wait,
or the CPU behind one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 02:15:35 -05:00
epicmeme546
77ae0f658e Keep samples-passed counters inside parallel groups, and lock the shared tables only while a chunk records.
Samples-passed resets and reports ended most groups in Hyrule Field (the game reports twice without a reset there, so
counting stays on): the front pass now replays them in the order of the stream, makes each occlusion segment and
queues its completion callback in that order, and each worker follows the counting from its chunk's first op and adds
the queries it closes to the segment the stream gives them. Per-object motion takes a lock instead of waiting for the
earlier chunks: its list's order does not change the picture. The locks the chunks share are taken only inside a
chunk, as outside groups the submitting thread alone replays; taken always, they cost the replay about 1 ms a frame.
The perf line counts the draws by what ended their group.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 02:14:37 -05:00
epicmeme546
7534193b62 Record the large draw groups of a submission on several replay workers at once.
Stage 2 of NX2NAVI_REPLAY_WORKERS (default 0, the replay as before). A run of draws is cut into groups: a group ends
where the render targets change after its first draw or clear, and before an inline uniform update, a mesh draw and a
samples-passed counter op, which the submitting thread replays itself in the order of the stream. A group of 64 draws
or more is first walked by a front pass on the submitting thread: it applies the state and binds, settles the CPU side
of the timestamp reports and decides the render conditions in the order of the stream, and copies the replay state at
the first op of each chunk. The workers and the submitting thread then take the chunks in order and record each into a
command buffer of its own, which vk_sync_append puts into the part in the order of the stream.

The frame's pass hooks (contact shadows, DLSS) run in a group's first chunk, which the others start after. What still
depends on the order between chunks waits until every earlier chunk of the group is done: a texture lookup that would
make or take back an entry returns nothing and the chunk replays the op again in turn, and so do the contact shadows'
pass, per-object motion and the HDR pass's source. The tables the chunks share take locks (the shader table, the
descriptor lists, mirror use and GPU write notes, uniform snapshot drops, target frames, the DLAA camera votes and the
previous-frame motion notes); each chunk keeps its own occlusion query open, its own feedback memo and no pass marks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 02:14:37 -05:00
epicmeme546
4fc32e8d84 Hand the draw context down in the replay state instead of reading it from a thread-local at every call.
Reading the thread-local pointer in the functions that run per binding and per descriptor cost about 0.3 ms of
replay a frame in Hyrule Field (45.0 against 45.8 fps). VkExec.dc now names the context; the draw and look-ahead
entry points fill it from the calling thread's on first use, the functions that hold the state read it from there and
the few helpers without it take it as a parameter. A replay worker's copy of the state carries the worker's context.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 02:14:37 -05:00
epicmeme546
3010a637bf Let one replay worker record each long run of draws from a copy of the replay state, behind NX2NAVI_REPLAY_WORKERS.
Stage 1 of recording a submission on several threads. A submission's draws come in runs between ops that have to
stay in the replay's order on its own thread (dispatches, copies, texture and buffer clears, events, the game's
barriers, calls). With NX2NAVI_REPLAY_WORKERS=1 a run of 16 draws or more is recorded by a worker thread: it takes a
copy of the replay state, records the run into a command buffer of its own pool with a draw context of its own, ends
it behind a full barrier and hands the state back. vk_sync_append puts that command buffer into the part between the
replay's own pieces before and after it, so the GPU runs everything in the op stream's order. The submitting thread
waits for each run, so every side effect keeps the replay's order; this stage checks that a run recorded apart from
a copy of the state gives the same pictures. The default, 0, replays as before.

walk's handling of the ops that do not steer it moves into op_one, which the worker uses too. NX2NAVI_REPLAY_RUNS=1
logs every 600 frames how the draws fall into such runs and which ops break them. The draw replay's hot functions load
their recording context once instead of at every use.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 02:14:36 -05:00
chrg6
bfe88e503a Add one anti-aliasing setting: the game's SMAA, off, FXAA, SMAA at its Ultra preset, or TAA.
Ocarina anti-aliases every frame with its own SMAA in three compute passes after the bloom composite. The new aa
setting (nx2navi.ini aa, the Screen tab's Anti-aliasing row) keeps that by default. off, fxaa and smaa leave out the
game's edge and weight passes and replace its composition with a pass of our own that writes the same square-rooted
RGB9E5 words into the scene target: a plain copy, an edge smoothing written here, or the SMAA reference implementation
with its area and search textures (MIT). taa is DLSS or FSR 3.1 at the scene's size, which the row sets through the
upscaler; while either runs, the game's SMAA stays whatever aa says. The game's passes are known by a hash of their
guest code. The shader build now also rebuilds a header when a file its source includes changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 15:09:16 -05:00
tangerineturtle1
48b71a630d Jitter DLSS's scene on Ocarina's frame, with the shading draws against the copied depth jittered too.
Jitter was off on Ocarina because with it the ground and characters went white within seconds. The cause: the game draws its depth pre-pass into the scene depth, copies it into a second image of the same size, and shades the same geometry into the light target against the copy with the depth test EQUAL. Only the draws into the first image took the jitter, so the shading draws compared unjittered depth with jittered depth, EQUAL failed on nearly every pixel, and the light target kept its clear colour. Frame dumps with and without jitter show the shading passes darkening the light target from a mean of 133 to 13 without it and leaving it at 112 with it.

On Ocarina's frame every draw that projects through the camera (it binds the scene context at c3) into a D32F depth of the scene's size now takes the jitter, and full-screen passes drawn with a scene depth attached do not: the bloom composite DLSS reads would otherwise move the picture by the jitter a second time. The viewport is recorded again whenever a draw's jitter differs from the last one recorded. Jitter is on for Ocarina by default (dlaa_jitter = 0 still turns it off), and the DLAA log line counts jittered frames and draws.

Checked in Hyrule Field with the camera turning: the picture's brightness matches the run without jitter (mean 33 to 63 against 35 to 60; 144 to 188 before the fix), and in 20 motion-vector checks the warp error is lowest with the jitter applied as DLSS is told it (0.032 to 0.077), against 0.076 to 0.141 with its sign flipped and 0.059 to 0.106 left out, still cameras included: the scene moves by the jitter of that frame, in render pixels.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 15:09:16 -05:00
chrg6
295922c224 Sync the device address table's shadows once per guest submission that binds the table, and take out the port of pull request #353.
The author of pull request #353 closed it on 2026-11-11 with the words that they give no permission to use its code. Its port (3134f524) goes: the mark of a pipeline that declares the table (VkPipeInfo.gpa, vk_pipeline.c pipe_add) and of the part that draws with it (vk_mem_gpa_use in vk_exec_draw.c bind_resources and mo_emit), the sync of the table's pools in every such part's prologue (vk_mem_gpa_sync), the submission with a callback in the replay's place as it stood (vk_sync_probe_submit), and --probe-vkmem's read through the table with its shader (test_gpa_read, runtime/shaders/gpa_read.spvasm).

In its place, after pull request #551, which was written without that code: a shadow pool goes into the table and onto a list of the table's shadows (vk_memory.c shadow_table_add); the record of a binding of the table notes the guest submission (vk_exec_draw.c ssbo_info, vk_mem_shadow_table_read); and vk_mem_shadow_sync brings the CPU's writes into every shadow on the list once for that guest submission, before the part is submitted. Once is enough, because the copy-ins read the guest submission's write-watch snapshots, which a second sync would find unchanged. Measured by #551's author on Linux (RTX 4090, Kokiri Forest): 113 and 76 frames over 25 ms in 75 s against 217 and 429 with the port, 43 against 41 fps, the picture complete in both.

What this line had built on the port stays, on the new sync: the test that passes over a pool with nothing written (shadow_pool_clean), the upload of the same pages into a table pool's VRAM mirror, the write-back of stores through the table (NX2NAVI_SHADOW_TABLE_STORES, table_wb_cb, now asked once per guest submission that binds the table) and the counts of NX2NAVI_SHADOW_CHECK. The probes that record their own commands (the lost-device probe, --probe-vkmem's table parts) call vk_sync_submit_own, written anew. vk_mem_shadow_storage stays: one line that returns the mode. Finalized pools in the table were already kept for vk_mem_shutdown here.

--probe-vkmem has a check of its own for the sync (test_table_sync): the shadow of a pool that is never bound keeps its old word through a CPU write and a guest submission that does not bind the table, and holds the new one after a guest submission that does. It passes with NX2NAVI_SHADOW=1 on Windows (RTX 5090), as do the store check with NX2NAVI_SHADOW_TABLE_STORES=1 and the probe without shadow storage; --probe-devlost ends as before. Not run: the game, with or without shadow storage, and anything on Linux.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-11 06:10:09 -05:00
chrg6
6c991d628d Start a texture the CPU does not write, made over storage the CPU writes, with that storage's bytes.
Two descriptors can name one CPU-written storage: one whose content the cache takes from the CPU, and a second with a texture flag for which it does not. The second has no upload of its own and its slot shows the first one's image while that entry is live. Once the first is released, a lookup of the second's slot makes it an entry of its own, start_at destroys the released first one, and the new image starts cleared and is never uploaded; the slot shows it from then on, also after the first descriptor is registered again. The same happens when both are released and the texture pool refresh looks the second's slot up before the next texture arrives in that memory.

With tex_split_fill = 1 (the default) start_at gives such an entry the storage's bytes: by a copy from the released CPU-written entry it is about to destroy when that image is current and alias_compatible allows, else by an upload from the storage. The image then follows the storage as a CPU-written texture's does until the GPU or WriteTexels writes it; 2 fills it once, 0 is the behaviour before. Depth, stencil and multisample images, pools without CPU access and storage over 32 MiB are left out, and an entry that is to be filled gets a new image instead of a released entry's, since a reused image goes through its first transition after the upload batch. The perf trace counts the copies, the fills with their bytes and time, the fills made again after a write and the entries left empty.

--probe-vkmem makes both sequences with the three values and reads the second descriptor's image back: zeros with 0, the storage's bytes with 1, and with 2 the bytes of the texture that was in the memory before when both descriptors were let go of. One case makes the entry inside a recorded submission, through a hook that lets the probe stand in for a replay, and there the content comes by the copy.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-11 05:57:23 -05:00
chrg6
892859cad0 Fill the records of a run of indirect mesh draws in one gap, so their pass is not cut at each of them.
An indirect mesh draw reads its workgroup count through a ring entry that a buffer copy fills, and a copy cannot be
recorded inside a pass: every such draw ended its pass, 45 times a frame in a village. The first draw of a run now
fills the entries of the mesh draws that follow it with only state, draws, counter reports and render conditions
between them, and those draws stay in the pass (mesh_ahead, default 1; 0 is the old way, ab switches in A/B blocks).
An entry is taken only by the draw it was filled for, at the same record address, while no other op was replayed
since. Counter reports count as staying in the pass because the replay closes their query inside it and writes the
value from the completion thread; the game reports one after most of these draws, and without that only 38% of the
draws were served.

Measured in the village: 42 of the 45 draws a frame are served by an entry filled ahead; gaps with these copies
41 to 3 a frame (0.22 to 0.04 ms of GPU time); passes and dispatches 248 to 208 a frame (302 to 254 in Hyrule
Field). With the piece release of the commit before, one run each: village 142.6 fps and GPU busy 3.87 ms (138.0
and 135.4 fps, 4.15 ms without this change), Hyrule Field 136.8 fps and 4.34 ms (131.5 and 131.3, 4.70 ms). In A/B
blocks of one run with the narrower rule (counter reports still ending a run) the GPU's busy time went from 4.81 to
4.64 ms.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 16:44:02 -05:00
chrg6
e8f5f8e01e Merge branch 'main' of https://github.com/tangerineturtle1/NX2navi into test-all-fixes
# Conflicts:
#	runtime/cfg.c
2026-11-10 14:27:47 -05:00
chrg6
61c4c873d5 Merge branch 'claude/pipeline-stutter-110e1b' into test-all-fixes
# Conflicts:
#	docs/building.md
2026-11-10 14:18:29 -05:00
ordinarybob
ce206a9239 Order passes and dispatches by the game's own barriers on NVIDIA graphics cards, which takes about a tenth off the GPU's time for a frame.
Every pass begin and every compute dispatch recorded a full barrier, which makes the GPU finish everything before each of them. With barrier_game = 1 (NX2NAVI_BARRIER_GAME) a pass after an earlier pass gets a barrier for its attachments only and a dispatch after other dispatches gets none; a change between drawing and compute keeps the full barrier, and so do the game's own barriers and the replay's copies, clears, fills and uniform updates. On an RTX 5090 at 2560x1440 the GPU's time for a frame fell from 5.4 to 4.9 ms standing in Kokiri Forest (two in-run A/B runs, the control's blocks differ by 0.7 %) and from 5.1 to 4.4 ms on a walk through it. The frame rate did not change there, because the replay thread is the limit on that machine; it should rise where the GPU is.

The default is 1 on NVIDIA graphics cards only, and 0 on every other vendor's: the game placed its barriers for an NVIDIA GPU, Vulkan asks for more of them than the game recorded, and no AMD or Intel driver has run this. 34 screenshots across the A and B blocks of one run show the same picture on NVIDIA. NX2NAVI_TRACE=perf logs the barriers a frame by kind.

From pull request #389, which had it on for every vendor; the default by vendor and the settings text are added here. The validation layer's synchronization check could not be run on this machine.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 14:08:48 -05:00
epicmeme546
da0d0be700 Read occlusion query results in batches, and wait in the submit call for the game's signal that the frame's data is written.
Both are about the time between two frames. Ocarina waits for the previous frame's fence before it lets the next frame's GPU work start: its main thread signals an event from the CPU, and the main queue's commands begin with a wait for it.

The completion thread ran a submission's callbacks before it let the submission's waiters go, and the occlusion counts were among them: one vkGetQueryPoolResults and one vkResetQueryPool per reported query, about a hundred queries a frame in Hyrule Field. On an RX 7900 XT (26.10.1) each call goes into the kernel, 3.9 ms of every frame, so the fence was reached 4 to 6 ms after the GPU had finished with every game thread waiting. Slots come from the ring in order, so one call now reads up to 256 slots from the one asked for, the available results are kept for the segments that follow, and the slots taken are reset in runs (vk_exec_xfer.c oq_take, NX2NAVI_OQ_BATCH). Standing in Hyrule Field at 3440x1440 with FSR: 52.7 fps against 44.6, the longest frame of the measured minute 46 ms against 55; the completion thread spends 0.3 ms a frame in the callback.

When the CPU's signal came a millisecond or two after the main queue's submit call, the submission took its copies, stopped at the wait and was held; the render thread polled the event, presented, and the present replayed the frame under a value the game's fence does not stand for. The next frame then skipped its wait for the GPU and was replayed inside its submit call, and frames of 23 and 15 ms took turns. A submission that begins with a wait for an event the CPU has signalled before now waits for the signal in the submit call, before its copies are taken, 10 ms at most (vk_exec.c vk_exec_gate_wait, NX2NAVI_GATE_WAIT_MS): 54.5 fps and a longest frame of 33 ms in the same place, 58.6 fps with the recorder thread on. With a camera sweep the pictures went out 21.0 ms apart on average with a 99th percentile of 55 ms, against 25.4 and 64. A village went from 48.6 to 53.7 fps and a graveyard from 56.5 to 64.2. 900 consecutive frames of a still scene show no dark or changed frame, with the recorder thread and without.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 13:37:12 -05:00
chrg6
bfcefe6fa2 A frame no longer stops for the pipelines of an area's first frame: passes the game renders frame after frame stay per-frame through a loading screen, and the waits that remain share 50 ms a frame.
A draw whose pipeline is still building is skipped when its pass is rendered every frame and waits otherwise, so that a lookup table or a volume generated once is not left empty. "Every frame" meant "in the last four frames", so after a loading screen or a full-screen menu every pass of the first frame read as new: each draw with a new pipeline waited on the replay thread, up to 30 s each, and a mesh draw, whose pipeline has no fast link, for its whole compile. A player's 0.7.1 log shows it at three area changes: frames of 954, 1015 and 684 ms with 853, 960 and 663 ms of pipeline wait, six mesh draws waiting 52 to 660 ms in passes whose targets had been rendered in thousands of frames.

The target history (now runtime/vk_target_frames.h) also counts the frames each target was rendered in, for as long as it is the same guest texture. From eight frames on its pass is per-frame however long ago the last one was, and its draws are skipped and built by the workers like any other (pipe_skip_steady, on by default). A target rendered for the first time still waits, now from one pool of 50 ms a presented frame that the full-screen draws share (pipe_frame_wait_ms; oneshot_budget = 0 gives the old wait back), a mesh draw 8 ms at most, and the wait for a one-shot draw's shader comes from the same pool (gfx_shader_await takes a limit). A full-screen draw with no picture left to hold waits 40 ms more instead of 500. One frame so waits under 100 ms for pipelines in all.

Every 600 frames with a miss the log says how many draws found a shader or pipeline missing, how long they held the replay thread in all and in the worst frame, by the kind of wait, and how many were skipped at its end.

tests/target_frames_check.py replays the area change of that log against the header. Not yet run in the game.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 12:02:12 -05:00
chrg6
0e16e58cf3 Merge the shader stutter fixes into the combined test line.
Eight commits: a draw that misses its pipeline has the rest of its submission read ahead, the library workers link
the pipelines of skipped draws, a pipeline whose fragment shader reads a target of its own pass is fast-linked like
any other, and diagnostics. The merge was clean. Their before and after matrix had not finished when this was merged.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 11:05:17 -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
chrg6
fdf2fb89d3 A draw that misses its pipeline has the rest of its submission read ahead, and the stage libraries of the pipelines the later draws need are queued at once.
The next draw with a new pipeline was only found when the replay reached it, so the draws of an area's first frame,
or of a turn that brings several new materials into view, paid for their pipelines one after another while the
workers that could have built them together sat idle. The submission is recorded before it is replayed: the first
draw of a submission that finds a shader or pipeline missing now has the ops after it read (vk_exec.c vk_exec_ahead)
with the binds that decide a draw's shaders and shader libraries applied to a copy of the replay state. Each draw
there looks its shaders up as the real draw will, which also queues a shader that is not in memory, and names its
state to vk_pipeline_hint, which queues the pre-rasterization and fragment shader libraries that are missing. No
pipeline is entered and no key recorded, so a wrong guess costs two libraries at most. The draw's own lookup then
links on its thread when both libraries are there.

On a first visit to Kokiri Forest a scan read 6,900 ops in 0.78 ms on average (the longest 5.5 ms; a scan ends
after 3 ms), and the scans of the load and the walk named 377 states without a pipeline and queued 104 libraries
for them.

pipe_ahead = 0 (NX2NAVI_PIPE_AHEAD) turns it off.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 10:00:20 -05:00
chrg6
71eb5fbe26 Make an area's textures at once after a load instead of over the first seconds, which showed the ground and walls black.
The texture pool creates 16 entries a refresh and stops after 6 ms a frame, so that textures streaming in do not make
long frames. An area load leaves thousands of slots at once (4,936 loading the test save), and at that rate they came
in over the frames after the load screen: the scene's first frames showed a quarter of the picture as black stand-ins,
for 22 presented frames on a free machine and for about three seconds with the CPU busy.

While a refresh leaves pool_load_backlog slots or more (512; 0 never), the pool is loading: the count limit is off, a
frame's refreshes may take pool_load_ms (50), and frame_hold leaves out the presents of frames drawn meanwhile (up to
2 s), so the picture before the load stays until fewer than 64 slots are left. Measured with the flight recorder's
thumbnails of every present: the 4,847 textures are made in one frame of 345 ms, 30 frames before the scene fades in,
and the black share of the picture goes from the fade straight to its final value. The streaming limits are as before
below the backlog.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 06:55:13 -05:00
epicmeme546
d1bebd34cd Keep the previous picture on screen instead of showing a frame that lacks draws, and show the game's own VRAM in the overlay.
A draw whose shader or pipeline is still building is left out of its frame, and the frame was shown without it: a black or unlit picture with the interface intact when it was a lighting pass, a light going out, or ground missing for a frame. In a player's session a full-screen draw was skipped after its 50 ms and the frame came out black as a boulder hit.

The frame's present is now left out when draws were skipped (vk_backend.c frame_hold, NX2NAVI_FRAME_HOLD_MS, 120 by default, 0 off). The game runs on, so nothing waits. The hold comes from an allowance that fills again at an eighth of the clock's rate, so a long build or a run of them shows its frames as before and does not freeze the picture; a full-screen draw that would be skipped with the allowance spent waits on instead, 500 ms at most. Not with DLSS Frame Generation.

On a first walk through a village with no pipeline recorded, 21 of the 23 frames with skipped draws were held back; in the Lost Woods, 7 of 8.

The performance overlay: the VRAM line is the card's dedicated memory in use, which counts every program on it: a player read 14.5 GB as the game's while the game held 7.3 GB. The line's third cell now gives what this process holds on the device-local heaps (the driver's budget figure), in place of the GPU clock, which stays where the driver gives no budget.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 04:39:19 -05:00
chrg6
22103a57bf Merge main into the test line: the pull requests taken there and two render target fixes.
Main brought the fixes of eleven pull requests (the texture pool set sealed when an early replay part is submitted, every location reflected in interface arrays, RGB9E5 texels recognised for integer storage views, the upscaler a frame started with kept, GPU pass timestamps without vendor breadcrumbs, the Linux build, window and NGX fixes), render targets released when drawn over at another address, the vertex layout kept in the pipeline on newer AMD drivers, and the upscaler choice read once per evaluation.

Four hunks conflicted and take both sides: pool_sync returns for a sealed set before the per-frame creation budget's clock (vk_exec_draw.c); make_module keeps the test line's early_ok argument and main's module hash (vk_pipeline.c); a texture taken back counts as sampled again and records how long it was away, and the texture line logs the disuse counters and the overwritten render targets (vk_texture.c).

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-09 21:10:23 -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
chrg6
dc9f164afc Evict textures no shader sampled for 30 seconds while video memory or the system's commit runs low.
The texture pool refresh looks up every registered slot, so the textures of every area the game keeps loaded never
counted as idle and their memory only grew. Marked shaders now store into a buffer with one word per pool slot. Every
four submissions that recorded work, the replay copies it out and clears it, and the texture side notes when each
texture was last sampled. Under VRAM pressure or low commit, once idle textures are gone, live textures that come back
intact and were not sampled for 1800 presents go too. Their keys are held, so the pool refresh keeps the stand-in until
a shader samples that slot again, and the next refresh makes the texture again.

Fragment shaders that neither discard nor write depth, sample mask, stencil reference or memory keep early depth tests
(EarlyFragmentTests), so the marks do not take away early depth rejection. Alpha to coverage leaves the dynamic state
while marks are on, so each pipeline knows whether early tests are safe. Disuse eviction waits until every module that
indexes the pool was marked and two reads arrived. NX2NAVI_TEX_DISUSE=0 (tex_disuse = 0) turns it off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 1a4c896150a748f9275c3024e96af3055db94af4)
2026-11-09 14:39:07 -05:00
chrg6
1f7e9c975b Speed up the replay and fix what the perf pass found, and add a frame capture key for chasing rendering faults.
Performance: draws reuse the bindings that did not change and record only the dynamic states that differ from what
the command buffer holds; small uploads come from a staging arena; finished submissions are held briefly and handed to
the queue in batches; the recorder thread can keep a CPU core of its own; ASTC block-linear textures are deswizzled on
the GPU; dynamic resolution steps the upscaler mode to hold a frame-time target. Each has a setting in nx2navi.ini
(bind_reuse, dyn_delta, stg_arena, submit_batch, recorder_core, recorder_prio, astc_gpu_deswizzle, dyn_res) and the
staging cache now frees what it holds at shutdown.

Capture: NX2NAVI_DUMP=key saves the frame after each press of Insert, with mesh draws listed by task, mesh and fragment
shader, depth test and face culling, and mesh_prims.txt giving each mesh draw's primitive, clipping and fragment counts.
All of it is off unless NX2NAVI_DUMP is set.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 03:34:46 -05:00
chrg6
c71eeed305 Hold a present until the frame's held work is replayed, and sync held work that runs in an empty submission.
The event order (vk_exec.c) holds a submission at a wait whose signal has not been replayed. In Kokiri Forest dialogue several presents a second came with the frame's whole submission held (4 held submissions, nothing drawn since the last present): the present showed the picture from before, a flash of the whole picture, and the frame was drawn afterwards in the present thread's empty submission, next to the following frame. With the event order off (NX2NAVI_EVENT_ORDER=0) the same spot had no such frame in 3600.

present_held (vk_backend.c, synchronous replay only): at a present with held work, what can go on runs first in a submission of its own; if work is still held, the present waits and goes out after the submission that replays the last of it, at the next present, or after 8 submissions. present_held = 0 (NX2NAVI_PRESENT_HELD=0) turns it off.

held_sync (vk_exec.c vk_exec_record): a submission without commands of its own that resumes held work now gets the VRAM mirror sync before and the write-back after, and a uniform copy, like any submission. Before, that work read mirrors that had missed the CPU's latest writes, and its GPU writes were dropped from the write-back list by the next submission's vk_mem_mirror_begin. held_sync = 0 (NX2NAVI_HELD_SYNC=0) turns it off.

Not yet measured in play.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 03:34:46 -05:00
chrg6
e32fd15952 Take Ocarina's DLSS camera from the bytes the scene draws read, and log why a frame has none.
In Kokiri Forest dialogue (Saria, dlss = performance) about 1 frame in 20 had no camera, and DLSS started a new history twice for each: 24 to 26 camera resets per 300 frames. The camera block was looked up in the uniform copy of the submission DLSS ran in, which on those frames had none: the frame's work had been held past its present by the event order and ran in the present thread's empty submission. vk_dlaa_draw now keeps the camera rows as the first scene draw read them (vk_usnap_read), and mv_pass uses those. A frame with no scene-context draw at all keeps DLSS's history with zero motion, for up to 6 frames in a row.

Measured at the Saria spot: camera resets went from 24 to 26 per 300 frames to none in 3600 frames (the first frame only). The whole-picture flashes stayed: they come from the presents that go out with that work held, handled separately.

The log now explains frames without a camera, frames without the DLAA point (with the held submissions at that present), light target changes and a second scene after DLSS ran, plus a line every 300 DLSS frames with resets. dlaa_cam_keep = 0 (NX2NAVI_DLAA_CAM_KEEP=0) restores the old camera handling.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 03:34:46 -05:00
chrg6
3aa8ff56a2 Replay the game's queues in the order their events allow, so Link and the other characters are no longer drawn too dark.
Ocarina runs compute work on a second queue that trades results with the main one through events: the worker's command buffers wait (WaitEvent) for signals the main queue gives in its submission a few submits later, and the main queue waits for the worker's signals in turn. Replayed one submission after the other, the worker's steps ran before the work they wait for and read the previous frame's results, and the characters came out much too dark at any time of day (Kokiri Forest: about 13 waits a frame, 7 of them reached before their signal).

A top-level wait whose condition does not hold in replay order now stops its submission: the rest is held, with private copies of its recordings and the queue's state, and the queue's later submissions line up behind it. After each replayed signal the held work whose wait now holds goes on inside the command buffer being recorded, between full barriers, with its own state swapped in. Work still held after 36 submissions goes on regardless, as before (counted in the NX2NAVI_TRACE=perf line). The queue reaches the back end as gfx_submit_queue. Only with the replay on the submitting thread; NX2NAVI_EVENT_ORDER=0 turns it off.

Tested by play in adult Hyrule Field with the tree fix of the previous commit: Link lit normally at dusk and at noon, trees steady, lighting otherwise unchanged. The white specks in the grass remain (they predate both changes).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 15:51:47 -05:00
chrg6
9a8f74fe53 Refresh read-back ranges in the VRAM mirror, guard the boot pipeline prebuild against driver crashes and count out-of-order event waits.
Small ranges the game invalidates and reads back (the exposure meter) are copied from guest memory into the mirror at every submission that uses their pool, so a CPU write made between recording and the GPU's run is not lost; NX2NAVI_READBACK_SYNC=0 turns it off.

While the boot builds the recorded pipelines a marker file exists next to the pipeline cache. A start that finds it left over records the GPU and driver in pipeline-prewarm-skip.txt, and starts on them build those pipelines at their first draw instead.

CommandBufferWaitEvent is now recorded as OP_WAIT_EVENT. The replay waits for nothing but counts the waits it reaches before their signal, and how many submissions later that signal comes, in the NX2NAVI_TRACE=gfx frame line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 15:51:47 -05:00
chrg6
42ae87bca6 Runtime: power opt-out, faster fence wake-ups, feedback barrier, texture re-upload skip
- Opt the process out of Windows power throttling (execution speed and
  timer resolution) at startup; NX2NAVI_POWER_FULL=0 turns it off.
- Fence completion thread wakes waiters before collecting freed memory,
  so collection no longer delays them.
- Add a by-region barrier when a draw or clear writes an attachment that
  an earlier draw in the same pass sampled; count it and inline uniform
  updates in the per-frame stats.
- Hash small texture contents at upload and skip re-uploads whose bytes
  are unchanged (removes ~420 redundant uploads per frame in Kokiri
  Forest); NX2NAVI_TEX_SAME=0 turns it off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 06:04:57 -05:00
chrg6
99de7b943f Give each file of copied code one license line.
Each file that contains copied code names its copyright holder and license and points at THIRD_PARTY_NOTICES.md; zlib code keeps its full notice and the mark that it was altered. Comments and probe text only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 05:04:40 -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
chrg6
77d0c0b84e Port Ocarina of Time (Switch 2) to the recomp host
The NVN2 front end and the SM86 to Maxwell shader lowering (tools/xlate/AmpereLowering.cs, mesh and task stages,
bindless texture pools, BRX jump tables, the NVN2 compute c0 layout) with four translator patches (0017-0020) bring the
opening of the game to 58-60 fps on the Vulkan renderer.

This round fixes the haze: SM86 IPA is lowered as a plain Maxwell IPA.PASS (texture coordinates were corrected for
perspective twice); LDSM, TXD, both F2IP byte forms, HMMA, MOVM and HFMA2.RELU are lowered and spill mode has eight
uniform register groups; conditional rendering keeps the newest counter report per address and treats a report as
pending until its value is in guest memory, so occlusion tests and the depth clears around the depth copy behave; and
the upscaler stand-in decodes the post pass's sRGB picture before writing the linear target. Tensor-core programs keep
the stand-in unless NX2NAVI_NATIVE_TENSOR=1, because their lowering does not match the hardware yet.

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