A texture whose pages show a write is uploaded again unless a hash of its bytes says they are the same. The hash
covered textures up to 64 KiB; for larger ones it was a switch left off until measured in play (tex_same_big). A
run through Hyrule Field measured it (RTX 5090, about three minutes of running): without it 44,000 to 52,000 second
uploads of 33 to 47 GiB of storage in four runs of six, one 1536x1536 texture 29 times within 300 frames with the
same bytes each time, and 66 thousand million texels decoded on the GPU in a run; with it 214 to 592 second uploads,
24 GiB found unchanged, and 20 to 24 thousand million texels decoded, which is the first uploads. The GPU's upload
work fell from 0.41 to 0.14 ms a frame, and the second uploads' 2.9 s of the submitting thread are gone for about
as much hashing.
tex_same_big = 0 hashes only the small ones, as before.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
vk_memory.c handed out page write generations from a 32-bit counter (gen_now) and every consumer compared them as
32-bit values, with 0 meaning "never stamped". At 124 to 336 generations a frame (a 0.10.0 log with trace = perf) the
counter wrapped after about 66 h of play at 60 fps, 27.6 h at 144 and 15 to 17 h at 240. Past the wrap a stamp made
before it compared newer than every later check and one made after it older: textures and VRAM mirrors missed the
CPU's writes (stale textures, and the lazy fetch copying older mirror bytes over a CPU write), pages nothing wrote read
as written at every check (a hash or upload every time, and every such page of a mirrored pool copied up at each
sync), and the generation handed out at the wrap was 0. At 64 bits nothing wraps: 2^64 lasts millions of years at
336 a frame and 240 fps. Below 2^32 every value and every compare is the same as before.
What changed: gen_now, both tables (gen_chunk, cgen_chunk), reg_max and creg_max, PoolRec's synced_gen, mgen and sgen,
dirty_gen and DIRTY_UNSYNCED, VSample.mgen, every local and parameter that holds a generation in vk_memory.c,
vk_texture.c (Tex.wgen and gpu_gen, PEnt.since, storage_rewritten and its callers, sweep_hard's write-back check
included) and tex_probe.c, and vk.h's vk_mem_changed, vk_mem_changed_snap and vk_mem_unchanged(_why)_locked. The verify
mode's mismatch line prints the generations with %llu. The readers that take no gen_lock (lazy_clean, cpu_wrote_since,
verify_cb, vk_mem_sizes) stay lock-free at 64 bits: the slots, mgen, gen_now and a new chunk pointer are now stored
atomically (relaxed, or release where a reader acquires), the same instructions on x86-64, and a static assertion
holds that 64-bit atomics are lock-free. No lock, wait or thread changes.
gen_next, which hands out every generation, no longer warns at 0xC0000000 and up or at the wrap, which cannot come;
it logs one line when the count reaches 2^32, where earlier versions ran out, with the frame, the hours and the rate.
Cost: the tables take 8 bytes a page instead of 4 (512 KiB more per 256 MiB of guest address space stamped, both
tables together, allocated as before when a chunk is first stamped), reg_max and creg_max 2 MiB each instead of 1,
Tex and PoolRec 8 bytes more, PEnt 8 more. The page walks read 8 bytes a slot; a micro-benchmark of the walk measured no
difference.
Not reproduced in the game before the change: the wrong answers were reproduced by compiling vk_memory.c's generation
code with the counter seeded below 2^32. tests/gen_wrap_check now runs that code, with the readers outside its
section, across 2^32, 2^40 and 2^63 and checks that the compares answer as below the boundary; on the code before the
change 19 of its checks fail at 2^32 and the other two seeds do not fit the counter. The new tests/gen_width_check
compiles vk_memory.c, vk_texture.c and tex_probe.c with -Wshorten-64-to-32, -Wformat and -Wincompatible-pointer-types
as errors, so a 32-bit local left behind is found (shown by reverting single locals, fields and formats). Checks that
passed: gen_wrap_check, gen_width_check, pass_cc_check of every runtime file, no new -Wall -Wextra warnings in the
three files, tex_release_check, tex_compact_check, wb_lazy_check, gpa_check, mem_growth_check, mem_budget_check.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The table still named a new mirror at record time, as the pages' twin: parts already handed to the queue wrote
through the table into the new block before its first copy, the same early access the delayed read was made to stop.
The reads then moved to the mirror some submissions later by a second change that took a second record, cleared its
note before it knew the record was there, and until it ran left the table's readers on guest memory, without what
other shaders had written into the mirror through a buffer binding.
Nothing is changed when the mirror is made now. The prologue that holds the mirror's first copy also holds a store
into the pool's record, after the copy: what was queued before it reads and writes guest memory through the table as
if no mirror existed, what comes after reads the mirror and writes both. No second record, no second change, no
window in which the table is behind. gpa_late = 0 still changes the table at once, as up to 0.9.2.
--probe-vkmem makes a mirror in a recorded submission and reads the table's own memory back: the page names the
mirror with guest memory as the twin, and the mirror holds the pool's bytes. tests\gpa_check.py runs the store as the
GPU would, before and after. A table with no record left now says so once in the log.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The probe ended in FAIL on an unchanged tree, so none of its cases after the texture slots ran. Three of its expectations were older than the rule they test; the runtime does what its comments say in each.
A finalized slot of a CPU-written texture shows its own image while that image still holds the texture (stale_kept). The probe expected such a slot to resolve to nothing after its texture's release although another texture object kept the image current. It now expects the image with tex_stale_kept on and nothing with it off, and a case of its own covers the rule: after the finalize the slot shows the same image, bytes written to the storage afterwards do not reach it and nothing is uploaded; registered again, in a later submission, it holds the storage's bytes of then.
A slot follows another descriptor's live texture at its storage only when that texture has the slot's size (follow_size_ok). The probe put a 32x32 texture under a 64x64 slot and expected the slot to follow it. It now checks both: a 64x64 texture of another format is followed, and under a 32x32 one the slot gets an image of its own descriptor.
The texture detail cases and the case of an ASTC decode without scratch memory returned without a check whenever the image was neither RGBA8 nor BC3, which was meant for a device that samples ASTC itself. Since opaque ASTC textures are made as BC1 that held for every image of those cases: the probe printed their line and checked nothing. They read BC1 images now.
The probe passes with the default settings (188 GPU checks; 164 when it last passed), with tex_stale_kept = 0, tex_same_big = 1, pool_presnap_runs = 0, shadow storage, ASTC images as BC3 and as RGBA8, and texture detail reduced and low.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
While the player runs through a town the game moves about 32 textures to new storage every four or five frames, back to back in one block, and a frame after their first upload every page of the block shows a write again. A texture up to 64 KiB keeps a hash of its storage and is uploaded again only when its bytes changed: of those, 0 to 4 in a hundred are (9 of 440 in one recording of 600 frames). A larger one has no hash and 75 to 94 in a hundred are uploaded again, whatever the size and format: 3,034 second uploads in those 600 frames beside 4,233 first ones, the ASTC ones decoded and encoded on the GPU a second time. The frames of the second uploads take 0.9 to 2.5 ms longer than frames without uploads, and the frames after them wait 0.3 to 1.3 ms longer for the GPU (six recordings of one player).
tex_same_big = 1 has textures up to 8 MiB keep the hash too. It is off by default: the bytes are read once more at the first upload and once at the comparison, 15 us each for 300 KiB, which together is about what the second upload takes on the submitting thread (27 us), so what it gains is the GPU's work and whatever else a second upload costs its frame, and that is not measured in play. A texture whose comparisons found other bytes twice in a row takes no hash any more, and an area load takes none.
The hash runs eight chains side by side and folds them, 20 GB/s against 7 for the one chain on a Ryzen 7 9850X3D, which the small textures' comparisons get as well. Each step is still a bijection of its chain and of the word it takes.
The pool log's buckets line counts the hashed uploads and the comparisons when there are any. --probe-vkmem has a case that passes with the switch off and on: the same bytes written again, other bytes, and the same bytes after two rewrites in a row.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
At every submission that binds the texture pools the replay checks each listed texture for CPU writes, and takes the write-watch snapshot of the 2 MiB regions that have none yet as the list comes to them: one query a region, since the list is in no address order. Standing still that is 5 to 10 queries and the check takes 0.11 ms. While the player runs through Kakariko or Castle Town the game writes about 32 textures into new storage every four or five frames, a region the CPU wrote is not frozen again for 1,800 submissions, and the check asked for 100 to 500 regions one by one, 2.7 times a frame: 0.5 to 1.5 ms a check, about 3 us a query, in five sessions of one player. The regions are now collected for the whole list first and asked in one query per run of adjacent regions. On this machine 400 queries of 2 MiB take 727 us and one query over the same 800 MiB takes 360 us. The same regions are queried and the same pages stamped, all of them before the list is looked at, as each entry's were before its own look; a run across the edge of a watched reservation is left to the queries by range. pool_presnap_runs = 0 asks region by region as before.
A released entry stays in the list until it is destroyed, the check does nothing for it, and the stamp it compares stays as it was at the release, so one whose region took a write later missed the region check at every submission from then on and was fetched only to be passed by: 1,000 to 6,300 entries a check, with up to 8,192 released entries kept. The list now carries the released state, set at the release and cleared at the take-back, and the check passes such an entry by without touching it or asking for its regions.
The pool log's freshness line counts the released entries passed by, and its snapshot queries count the queries either way. --probe-vkmem has a case for the queries by run.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
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>
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>
Making a CPU-visible pool imports its pages (or copies them into a shadow), and the first sync of its mirror and the
texture checks read the write watch afterwards: a write the CPU made before must still be on record then. Nothing in
this line takes it away: the import asks the write watch nothing, and the only reader that resets it stamps the page
generations under the same lock. The self-test now writes two pages, makes the pool and finds both reported, and
prints how many of the pool's pages making it marks by itself.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
With shadow storage a write-back at a fence copies a page that has no base copy whole from the shadow into guest
memory. Every write the replay records gets base copies, so such a page means the pool the write was noted on was
finalized before the fence and another was made at its address: the new record's shadow is a copy of guest memory
from its own start, holds no byte of that write, and went over everything the CPU had written to the page since.
The page is now left alone when the record is younger than the submission whose fence it is, or when the CPU has
written the page since the shadow's last copy-in; the shadow line counts such pages.
--probe-vkmem has the case: with NX2NAVI_SHADOW=1 it fails without this change (0x11 over the CPU's 0x22, with
imports and with mirrors) and passes with it, as does the run without shadow storage.
Whether a game does finalize and remake a pool inside that window was not observed in play.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
When the decoder's scratch buffer could not be allocated, the upload logged it and still counted the texture as
uploaded. The image of a new texture then held nothing, and nothing uploaded it again until the game next wrote its
storage, which for most textures is never.
vk_astc_record now tells that failure (-1) from the ones that cannot change, and the upload leaves such a texture as
not uploaded: the lookups try it again every 30 frames until a decode succeeds. A WriteTexels whose decode has no
memory is still only logged.
The memory probe forces one failed scratch request and checks that the texture is left alone during its wait, is
uploaded by the first lookup after it and then holds its blocks.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
NX2NAVI_SHADOW_CHECK=1 logs with the shadow line, every 60 frames: the whole copy-ins that overwrote a store a
shader made through the device address table, and the pools they fell on; the write-backs that moved bytes into
a page the CPU had written since its copy-in; the bytes both sides changed since their base copy; what the
copy-ins cost by kind, and which pools and flag sets took them. The stores are found by content: a whole
copy-in leaves a hash of the page, and the next one compares the shadow page with it first.
NX2NAVI_SHADOW_TABLE_STORES=1 keeps such stores: a page found stored into gets a base copy, so copy-ins take
only the CPU's bytes, and the fence of every submission that uses the table writes the stores back to guest
memory. --probe-vkmem checks it with a GPU write that nothing notes. It is off by default. The overwrite it
removes was the suspected cause of the flicker on Linux and is not: standing outdoors (RTX 5090 with
NX2NAVI_SHADOW=1) it happens exactly four times a frame, on ten pages in pools of 20 and 44 KiB, with the copy
that precedes the frame's stores, and no frame came out wrong with it or without it in 1,800 frames each; the
hashes cost 1.8 ms a frame of the replay (67 against 75 fps); and a count the CPU resets before the write-back
has landed would keep counting, where the whole copy-in resets it every time.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Under shadow storage the copy-in reads each region's write-watch snapshot once per submission. The check ran
its two parts in one epoch, so the second part's copy-in did not see the CPU writes made after the first, and
the probe failed with NX2NAVI_SHADOW=1 ("GPU read of CPU-written heap differs"). Each part now starts a new
epoch, as a submission does.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
One conflict, in the build kit's notes, which keep both the shader cache item and the Texture detail text.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The probe takes the level from NX2NAVI_TEXTURE_DETAIL. It covers a 1024x1024 texture, a 256x256 array of three layers, and a 128x128 and an unmipped texture that stay whole, with the images as RGBA8 or BC3, and the sampler LOD clamp of a reduced texture. It passes at full, reduced and low on an RTX 5090, with NX2NAVI_ASTC_BC 0 and 1.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Whether a slot was stale was read from the released image's entry, which eviction, the released cap or a new image
at the address can destroy before the slot is looked up again; the slot then revived the texture from storage the
game had freed (the access violation in the block copy during a pool refresh). The flag now lives on the slot: a
release sets it on the slots registered with that descriptor, a registration clears it, and a finalized slot of a
CPU-written texture follows only live GPU content at its address.
From pull request #348 by TophC7, taken as it is.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
With shadow storage (Linux, and Windows builds under Wine) a CPU-visible pool is a host-visible copy with
no memory object of its own, and only pools that owned one went into the device address table: pointers
the game's shaders read from memory reached nothing. Shadow copies are now in the table. A shader that
reads through the table resolves no pool, so nothing queued the pool it reaches for the copy-in: the part
is marked where its pipeline is drawn or dispatched (VkPipeInfo.gpa, vk_mem_gpa_use), and its prologue
brings the shadow of every pool in the table up to date with the CPU's writes and queues the same pages
for those pools' VRAM mirrors (vk_memory.c vk_mem_gpa_sync). --probe-vkmem reads a word through the table
from a pool that is never bound, before and after a plain CPU write.
Pull request #353 by TophC7, ported onto the base copies of the black-world fix:
- taken as it is: the table registration, the mark in vk_exec_draw.c and vk_pipeline.c, the prologue
call and vk_sync_probe_submit in vk_sync.c (two hunks merged by hand), the probe's test;
- written again: vk_mem_gpa_sync, on top of the snapshot-based shadow_sync_pool; a lookup no longer
queues a pool for being in the table, which every shadow pool now is;
- narrowed: the sync runs only with shadow storage. Without it (Windows) a mirrored pool stays unsynced
for the table's readers as before, so the frame there is unchanged, and the probe makes the source's
mirror only with shadow storage and skips when the device has no address table;
- the probe's shader was an array of SPIR-V words in tex_probe.c; it is now the source
runtime/shaders/gpa_read.spvasm, built like the other runtime shaders;
- the exact re-upload count in the texture probe stays checked on Windows.
Files other sessions are changing: runtime/vk_memory.c (vk_pool_init's registration, mem_resolve's touch,
the new section before vk_mem_use).
The Windows files compile and the shader assembles and validates; no probe was run, and nothing was run on
Linux here.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This pull request fixed lighting that added up frame after frame under shadow storage on Linux: a GPU write
noted while a part was recorded kept that part's CPU inputs (the game's resets of its lighting accumulators)
out of the shadow. Its fix marked noted ranges as submitted in vk_memory.c; the base copies that came with
the black-world fix already move every CPU-changed byte in before the part runs, so that part is not taken
and no SHADOW_PENDING_INPUTS setting is added. Taken from it:
- tex_probe.c (--probe-vkmem): a range noted as a future GPU write still gets the CPU's bytes before the
GPU reads it. The pull request's other check, that a CPU write made after a GPU result loses to it until
the write-back, is left out: with base copies the later CPU write wins, as it does on the console.
- vk_dump.c: NX2NAVI_DUMP_SHADERS=<hash>,... copies images only for passes that use those shaders, and
NX2NAVI_DUMP_RAW=1 also saves each copied image's texel bytes.
- tools/fix_linux_ambient_power.py, as it is: patches one translated shader of a player's own cache so a
zero exponent no longer gives NaN. It holds two hashes and no shader data.
Pull request #264 by intellq. The Windows files compile; the probe was not run, and nothing was run on
Linux here.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
NX2NAVI_PROFILE_SLOW unwound threads in any module, and one being unloaded while the game started (a driver or vendor
DLL) had its unwind tables freed under the sampler. The exe and the system DLLs are walked freely; any other module is
held with GetModuleHandleEx for the step, and one that is unloading ends the chain. The block-linear copy probe gains
two cases of 256 KiB and more.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
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>