A review of the DLSS and frame generation paths against NVIDIA's guides (the DLSS programming guide 310.6.0, the
DLSS-FG guide 310.7.0, Streamline 2.14.16's guides and headers).
What follows them: the texture bias (log2(render width / output width) - 1 on top of the game's own, at each change
of the mode), the jitter (Halton, 8 x (output / render)^2 phases and 16 at least, in render pixels within half a
pixel, the scene drawn with it and the motion vectors made without it), the creation flags (HDR, vectors at the
render size, auto-exposure, depth not inverted) and the constants Streamline gets (unjittered matrices, the jitter
in pixels, the vectors' scale of one over the render size).
What did not: vk_dlssg.c holds a change of generation's state for FG_SETTLE_FRAMES frames, so for up to 12 frames
after the depth, the motion vectors or the camera went away (a menu, a fade, a load) generation stayed on and
nx2sl_frame set no constants and tagged nothing. Streamline's guide asks for the constants at every frame and for
null tags where depth or motion vectors are not valid; it logged "Unable to find 'common' constants for frame" and
"Failed to find global tag 'kBufferTypeDepth'" and used the frame before's (in every play log with frame
generation). Such a frame now gets the last frame's constants with reset set and the three tags as null. Not run in
a game yet: nx2navi_dlssg.dll builds and exports its 15 functions.
What the guides leave open: the sign of the jitter offsets against the shift of the picture, which NVIDIA says to
find by trying, and the bias reaches only the tiled, trilinear samplers, not every texture of the scene. For both
there is a test switch now, read once: NX2NAVI_DLAA_JITTER_TEST=<x>,<y> tells the upscaler the jitter times two
factors, and NX2NAVI_DLSS_LOD_BIAS_TEST=<bias> puts a bias in the rule's place. Neither was measured: the runs
that would do it start the game.
The line about the presents a second (the commit before) came at 59 a second under a limit of 60: it comes under 56
now.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The cache counts its short waits in presents (PRESENTS(n): n presents at 60 a second) and scales them by the presents
a second. Above 60 it always did. Below 60 the rate stopped at 60, so the 120 presents a released texture stays out
of the VRAM pressure's reach (STALE_FRAMES) were 4 s at 30 presents a second and 5.7 s at 21. The game lets go of
textures by the second, as the player moves, so what that window holds grows with its length.
A tester's log (RTX 4070 SUPER, 2560x1440, DLSS Quality with frame generation, texture detail Full, 0.10.2-rc4): the
card went to 11,198 MiB against a budget of 10,600 while the frame rate fell from 63 to 21 fps. The released textures
held 2,644 MiB against their bound of 384, and 2,105 MiB of that was released too lately to go (442 MiB when the
pressure began).
On an RTX 5090 with the budget taken as 10,600 MiB and the tester's output settings, a walk through Hyrule Field at a
30 fps limit: the released-too-lately textures were all in their first 120 presents (the new census line below) and
held 383 and 429 MiB on average at the memory reports of two runs, against 292 MiB at 60 fps. With this change they
are in their first 60 presents and hold 209 MiB. Textures made again after a drop: 295 and 523 before, 511 after.
Not shown: the tester's 21 fps and a card that is really over its budget, which this card does not reach; the memory
in use as the driver counts it stayed at 9.8 GiB in the capped runs.
The rate now follows the presents down to 15 a second (vk_tex_release.h trl_rate_sample, trl_rate, trl_presents),
once eight seconds in a row were under 60, and never in a run's first eight seconds; one slow second among fast ones
still shortens nothing. Every wait PRESENTS counts follows it. NX2NAVI_TEX_RATE_LOW=0 stops the rate at 60 as before,
and one line says when the rate crosses 60. Values at 60 presents a second and above are the ones before.
The census of the released textures over their bound gets a second line: what the ones released too lately wait for
(their first presents after the release, content only the GPU made, the gap before a take-back with or without a
lookup still reaching them). It is what showed which wait held the memory.
tests/tex_release_check has the rate's checks and a table of the wait by presents a second; they fail against the
rule before.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The user asked for the release to go out as 0.11.0. No final 0.10.2 was published: only the previews 0.10.2-rc1,
-rc3 and -rc4 carry that name, and the launcher counts 0.11.0 as newer than each of them, so testers are offered it
on either update channel.
VERSION holds 0.11.0 and the notes move to docs/releases/v0.11.0.md, with a line for those on a preview. Every line
that named 0.10.2 as the release in which something changed names 0.11.0 now: comments in the runtime, the two log
lines that give a version (the removed low-end GPU mode in cfg.c, the page generations in vk_memory.c),
nx2navi.ini.example, docs/building.md, the labels of tests/tex_release_check.c and a comment of
tests/audio_params_check.c. AUDIT.md names the release 0.11.0 throughout and records the earlier name and the
previews once, in its status line. Nothing else changes.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
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>
The textures the game lets go of were kept for three minutes and could hold a quarter of the card's budget, up to
8 GiB with room. A run through Hyrule Field shows what that bought and what it cost (RTX 5090, 5120x2160 with DLSS
Performance, about three minutes of running; the game keeps 4.8 GiB of textures live and loads about 190 a second):
the released textures held 5.3 GiB on average and the video memory in use went from 11.1 to 17.9 GiB, while the
game took back 1,494 MiB of them within 10 s of the release, 171 MiB within a minute and 10 MiB later, beside 33,000
textures it loaded anew. A shorter hold alone gives nothing back: with 20 s the released textures fell to 0.5 GiB
and the free space inside the allocator's blocks rose to 4.9 GiB, since a block returns to the driver only when it
is empty. What the released textures hold at their fullest is what the session costs, so the bound is the lever.
The bound is now a thirty-second of the budget, 384 MiB to 1 GiB (vk_tex_release.h trl_cap_mb), still doubled while
the card and the commit have room, and the hold 60 s, 120 for content only the GPU made. The same run with the new
defaults: 13.2 GiB in use on average against 16.7, 13.8 at most against 17.9, 532 MiB of textures made again over
the run, 77 fps against 75. An 8 GiB card keeps 384 MiB where it kept up to 1.9 GiB.
tests/tex_release_check checks the bound by the budget and the sessions it simulates against the rule before.
Compaction (tex_compact) and 16 MiB blocks were tried on the same run and gave back 0.7 and 1.2 GiB; neither is
changed here.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Each decision moves into a header the runtime and a test both read, as vk_mesh_path.h does: vk_tex_detail.h (tex_detail_auto), vk_dlssg_device.h (dlssg_card_skipped, dlssg_device_create) and mesh_fb_hold.h (the hold's table). The runtime's behaviour is the same as before the move.
tests/tex_detail_check has the budgets from players' logs of 0.10.1 (a handheld's integrated GPU at 10,361 MiB now gets reduced, a 12 GiB card at 11,313 MiB full). tests/dlssg_device_check plays the laptop of the report with stand-ins for the interposer and the loader: the chosen Intel card is skipped, the NVIDIA card is made through the interposer, a card of another vendor is made by the loader without the interposer being asked, and a refusal falls through to the loader. tests/mesh_fb_hold_check holds a list for two presents after its colour draw was skipped, through a wait of 600 frames, and no longer once it is drawn or gone. Built against the rule 0.10.1 had, the first two and mesh_path_check fail (6, 13 and 3 checks).
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
auto chose full from 8,192 MiB of video memory budget, which is the cards of 10 GiB, and went by the budget alone. Two reports on 0.10.1 say that is too much. A tester filled a 24 GiB card to the room of an 8 GiB one and toured eleven areas at 2560x1440: with full, the driver counted more than the card had (the textures and the free space the allocator's blocks keep, which the pressure check leaves out), memory went to system memory, and the tour ran at 29.6 fps with a 1% low of 11.4; with reduced, 42.8 and 36.4, and nothing spilled. And a handheld's integrated GPU reported a budget of 10,361 MiB, which is a share of its 16 GiB of system memory: auto chose full and the game ended when a save was loaded, until its player set reduced.
Full now starts at 10,240 MiB of budget, the cards of 12 GiB (one reported 11,313 MiB in a player's log), and an integrated GPU gets reduced at most by auto. The log line says so when it applies, the settings menu's automatic entry names the level as before, and a level the player set stays.
Not run in the game. The file compiles and the content check passes.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
A play session at 5120x2160 held 3 to 5 GiB free inside 140 to 180 of the allocator's 64 MiB blocks, 48 to 85 of them at most half full, and the "mem: holes" line said only that 1 to 18 of those could be emptied by moving textures. A second line now counts the other half-full blocks by what stays in them: a VRAM mirror, a texture whose content only the GPU made, a texture a shader sampled lately, or a texture that cannot move for another reason, each with the blocks that kind alone keeps. Those figures say which change would give the memory back: mirrors placed apart from textures, or a wider rule for what a compaction may move. The line is written with the holes line, every 3,600 frames, from the table that line already builds.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Main gained three commits after 0.10.1's release commit: a render target that was drawn over gives its image to the next target of its size, such an image is handed on after 8 presents and freed after 32 while the card is past its pressure share, and 0.10.1 kept CPU helper threads on Automatic and Start next frame early on. The settings keep this line's defaults (Helpers work ahead 4, Helpers take small jobs 16), the comments that said both were off in 0.10.1 now say what 0.10.1 shipped, the texture log keeps both sides' counters, and 0.10.2's notes say which two defaults are new.
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>
A shader whose texture pool array has 32768 entries reads slot 32767 for every texture the game numbers from 32768 up
(Ocarina's pool has 33068), but nothing in the log said such a shader was in use: vk_texture_register warned only for
slots past VK_POOL_TEX (40960), which Ocarina never registers, and vk_pipeline.c spirv_pool_rebind accepts the shorter
arrays silently. gfx_shader.c pool_short_note now counts the shaders entering the memory cache with such an array
(carry_usable keeps carried ones out, so these come from a translator older than the runtime) and logs the first;
vk_texture_register logs once, with that count, when a slot between 32768 and 40959 is registered while one is in
use. The count is kept per shader in gfx_shader.c (gfx_shader_pool_short_seen) rather than per pipeline stage in
spirv_pool_rebind, which runs once per module built.
Diagnostics only. tests/carry_pool_check.py now also checks that pool_short_note counts only shaders of the earlier
length and logs once; every runtime file compiles, with no new warnings in gfx_shader.c and vk_texture.c.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sweep_pressure's second census, rel_census(cap, 1), ran whenever the released entries held more than their bound and
less than 1.25 times what they may keep. Its line says "(kept past it while the card has room)". Without room, or
without the hold, vk_tex_release.h trl_keep returns the bound itself, so keep equals cap. The census then also ran
for bytes between the bound and 1.25 times it, which drop_stale was taking down anyway, and the label claimed room the
card did not have. Now it runs only when keep > cap. The line stays for players: it is what tells why released
textures stay past their bound (a 2026-11-13 session held 7,150 MiB against 4,008 and never said why), and at most one
walk a minute, 60 a run, costs well under a millisecond. rel_census's comment says when the room census runs.
Checks that passed: pass_cc_check on vk_texture.c, no new -Wall -Wextra warnings, tex_release_check (trl_keep's rule).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Past RELEASED_HARD, sweep_hard walks every released entry (up to 16k), classes each, and drops up to HARD_BATCH of them
in one call, under the texture lock, on the thread that releases (a guest thread's finalize or the replay's
overwrite_others). The rule before dropped one entry per release, so the work now comes in bursts, and nothing
measured a burst.
Each call is now timed, and counts the entries it walked, the entries it dropped and the entries written back first.
A call of 2 ms or more logs "released textures past the hard count: a batch walked N entries and dropped M (K written
back) in X ms", 16 lines at most. The trace=perf "texture origins" line adds the calls of its window, their time and
the longest. The behaviour changes the finding proposes after a measurement (a cap on the GPU class in trl_hard_pick)
are not part of this commit.
Diagnostics only. Checks that passed: pass_cc_check on vk_texture.c, no new -Wall -Wextra warnings,
tex_release_check, tex_compact_check.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Under VRAM pressure or a standing mirror room request, sweep_pressure runs every eighth submission. evict_idle walked
the whole texture table and, when it found no idle candidate, returned before it set evict_wait, so the next check
walked it all again. The released-list loop before it started from the head each time and passed every stale entry it
could not drop (up to RELEASED_HARD) until PRESSURE_STEP drops.
evict_idle now keeps a wait of its own after an empty walk: no new walk for EVICT_EMPTY_WAIT (16) submissions. It is
a separate idle_empty_wait, because evict_disused, which sweep_pressure calls right after evict_idle when it freed too
little, stops while evict_wait is ahead, and sharing that wait would have stopped disuse eviction for as long as the
idle walk stayed empty. The released loop looks at up to EVICT_LOOK (1024) entries a check from a cursor (press_at)
that rel_unlink moves off a leaving entry, as for sweep_at and aged_at. It starts from the oldest again at the first
entry released less than STALE_FRAMES ago or at the list's end. The trace=perf eviction line counts the empty walks
and their time.
Not reproduced before the change: the 0.10.0 session had no VRAM pressure. Checks that passed: pass_cc_check on
vk_texture.c, no new -Wall -Wextra warnings, tex_release_check, tex_compact_check.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Past RELEASED_HARD, sweep_hard drops up to HARD_BATCH released entries in one walk (2969005e). drop_tex writes a
gpu_dirty entry back for DROP_HARD when the fence write-back would take it, and each write_back_n(&e, 1) call ended
in vk_upload_flush(1), a GPU round trip. A batch with many such entries waited once per entry, under tex_lock, on the
thread that releases, the replay's overwrite_others included. The rule before dropped one entry per release, so it
waited at most once.
Now sweep_hard gathers the picked entries the same way drop_tex decides on them: an entry whose storage the CPU
rewrote since the GPU drew into it loses gpu_dirty, and one wb_wanted takes is written back. All of them go back in
one write_back_n call, one upload batch with one wait. write_back_n clears gpu_dirty, so drop_tex then finds nothing
to write back. The same entries go, in the same order, and the entries written back are counted in n_drop_wb as
before.
Not reproduced before the change: the 0.10.0 session never reached RELEASED_HARD, and no check without a device
covers write_back_n. Checks that passed: pass_cc_check on vk_texture.c, no new -Wall -Wextra warnings,
tex_release_check, tex_compact_check.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- nx2navi.ini.example said the stutter map is written every 10 minutes and at exit; since 7b0dbeb1 it is every 5
minutes of play (stutter_map.c SM_REPORT_NS), at a focus loss in play and at exit, and since 2f640fb8 a run of
consecutive long frames counts as one hitch with their summed time. The paragraph now says both.
- docs/building.md had no paragraph for tests/freeze_hold_check.py (0e0cfe08), while each other header-alone check has
one: it now says what a change to runtime/freeze_hold.h or vk_memory.c freeze_pass must pass and what it checks.
- vk_texture.c released_cap_bytes named "a thirty-second of the installed memory" as the source of the bound even when
plat_mem_info gave no size and the bound fell back to 1024 MiB; that case now reads "the default without the
installed memory's size".
Text only. check_content passes; freeze_hold_check passes; vk_texture.c compiles with the dev flags and with -Wall
-Wextra with the one warning it had before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The line said "a quarter of the video memory budget" whenever the card's rule raised the bound, also when the smaller term was an eighth of the installed memory: on the RTX 5090 with 32 GB of the 2026-11-13 session the bound of 4,008 MiB came from the memory, not from the card's 7,854. The line now names the term that won, and the default's own.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A scratch grown past 32 MiB for a large texture went back to the usual size only when the next ASTC upload asked for one, so in the 2026-11-13 session the decoder held 224 MiB for the 70 s after the first area load and from a later load to the session's end. Each present now gives it back once no texture needed more for 5 s; nothing is made then, the next request makes the usual scratch, and the buffer is freed once the upload batches recorded so far completed, as before. The present only tries the decoder's lock, which can be held while VMA makes a block. astc_scratch_trim = 0 keeps the scratch until the next upload.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Whether a shorter hold could take some of the released textures' memory without a take-back finding its entry gone depends on what comes back late: the log said 452 entries (72 MiB) were taken back after a minute in the 2026-11-13 session, but not whether any was large or held content only the GPU made. A second line after the reuse line now gives the take-backs and the makes again after a drop by age for each kind (intact, overwritten, made by the GPU) and own memory (under 1 MiB or more), with the megabytes taken back after a minute; the kind of a dropped entry is kept with its key for the remake.
The census of what keeps the released textures over their bound ran only above a quarter more than they may keep, which with room on the card is twice the bound, so the session's 7,150 MiB against a bound of 4,008 MiB never got one. It now also runs while they are over the bound itself, at most once a minute and 60 times a run. Log lines only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Past RELEASED_HARD (16,384) the sweep dropped the first released entry in release order that was overwritten or unsampled for 2 s, render targets included, whose drop vk_texture.c already ties to a black frame, and it walked the list from its head for every drop. At the end of the 2026-11-13 session the count sat at 16,384 for a minute while 12,811 of 15,734 released entries owned no memory. The entries now go by class: overwritten ones, then ones that come back intact and are past their usual take-back gap, then other intact ones, and only then ones whose content only the GPU made; still only unsampled ones, and the one sampled longest ago when all were sampled lately, as before. One walk takes a batch down to 256 under the bound. tex_hard_order = 0 keeps the rule of 0.10.1.
The drops past the hard count get a reason of their own, so the texture origins line and the reuse line count them apart from the sweep's drops past RELEASED_MAX, with the classes they came from. The class rules are in vk_tex_release.h and checked by tests/tex_release_check.py.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
At 3840x2160 the targets moved in 16 presents hold 3.1 GiB, which a 16 GiB card does not have beside the 10 GiB of a town. A mark reaches its entry within three frames of the draw (four read-back copies, one every four submissions), so eight presents without a stamp are enough there. The early drop waits four times that: at ten presents it took the images the reuse was about to hand on (7,301 dropped, 7,672 made, 5,226 reused in one run); at 32 the same run made 3,338 and reused 8,004.
The walk from the bug capture at 3840x2160 with the budget taken as 13.2 GiB, a 16 GiB card's: 10.9 to 11.4 GiB in use, at most 1.6 GiB of released targets, no image that could not be made and none in system memory. A player's log from such a card at that size had 12.2 GiB of released targets and 22.2 GiB in use at 2 frames a second. Without the budget, at 3416x1440: 10.0 to 10.7 GiB, as before this change.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The game lays its render targets out anew in every frame with a linear allocator. A shadow map among them changes its size with the view (1141x1141 to 1917x1917 in Kokiri Forest, a texel or two a frame), and every target behind it then lies at a new address in each frame. The entries at the old addresses are marked overwritten and released, and their images are meant to go to the next textures of their parameters (create_tex). They did not: the game finalizes a moved target's texture object only when it needs the object again, until then the object's slot holds the entry, the pool stamps every GPU-owned entry a slot holds as used in each submission (pool_touch), and the slot names it (slot_names). By those two rules the image stayed, each frame made about a dozen new images, and the released ones waited 120 presents or more: 6.7 GiB at 3416x1440 and 12.2 GiB at 3840x2160 on top of the area's 10 GiB.
An overwritten entry is now judged by what drew with it or sampled it (samp_frame, sampled_lately) while the pool's marks tell what the shaders sample, in the image reuse and in drop_overwritten. A marked slot whose own entry is released stamps that entry too. The reuse looks at up to 1024 released entries of a parameter set instead of 128. Past the VRAM pressure's share of the budget by the driver's own figure, which counts the holes in VMA's blocks, overwritten entries nothing sampled go after 18 presents instead of 600.
Measured on an RX 7900 XT from a player's bug capture in Kokiri Forest (walking 28 m from the save, then laps, the game's own camera): 15.9 GiB of device memory in use before and 12.3 to 14.3 GiB on the laps, 6.9 GiB of it released targets, 6,970 images made and 645 reused; 9.7 to 10.6 GiB after, at most 2.2 GiB released, 3,609 made and 8,821 reused, no image that could not be made. With the budget taken as 12.5 GiB the early drop took 3,740 entries and the use stayed under 10.6 GiB.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
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>
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>
Released textures go once they have gone unused for the hold, and what a switched-off upscaler or SMAA held is freed, with no setting to keep them. A vram_use line left in a settings file is known and read by nothing. Memory use is to be decided by the card's and the system's budget instead.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On an RX 580 the second device-local heap (2 GiB) takes what the first has no room for. On a GPU with one device-local
heap (an RTX 3050 Laptop, 4 GiB, 16 GiB of RAM) the budget wrapper and the texture placement sent everything past the
heap's reserve to system memory instead: 4096 and more texture images and every other allocation, held in the
process's commit (21 GiB). The system's commit ran out (31730 of 32023 MB), host memory imports failed, a pool was
left without a GPU buffer and the GPU wrote through its address: device lost. Now an allocation leaves out a full heap
only when another device-local heap has room, and a texture's second step allocates as before, past the budget, where
the driver keeps it in video memory or moves it out itself, and NX2navi's own VRAM pressure drops textures.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A window minimized for minutes, a frozen game or a long load aged every released texture past its hold while nothing could take it back, so the first frames after it would make again the textures the frames just before it released. A gap between two submissions now counts at most a second.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A released texture now goes once nothing has used it for three minutes (six for content only the GPU made), measured
in wall seconds from a clock of the presents. In two players' sessions the game took released textures back within a
minute of the release or never, so the hold drops nothing it would ask for again. The size bound stays as the safety
net for a burst of area changes, and young textures may pass it, up to twice it, while the card and the system's
commit have room. vram_use defaults to low, which now means this hold; high turns it off and keeps the released
textures up to the bound as before, and balanced reads as low. The old low's values stay available as their own keys.
The reuse log line counts drops past the hold and splits its ages around it. tests/tex_release_check checks the rules
and the clock, and simulates sessions shaped by the logs against the old and new defaults.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every VMA allocation is counted under the file that made it until it is freed, so the new "mem: by owner" line
splits the device-local memory the driver reports into textures (live, GPU-drawn, idle, released, released too
lately to drop), pools and VRAM mirrors, upscalers, replay buffers, uniform snapshots, the device address table,
the ASTC decoder, present and post-processing, headroom spares, the free space inside VMA's blocks and what VMA
never allocated. "mem: host" gives the host-visible allocations, the process's commit and working set and the
game's heap. "vk: released textures" counts take-backs by how long the texture was released and textures made
again after a drop by a bound or a pressure, with the drop's reason.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The larger pool asks for 655,360 sampled images in one set where 524,288 were asked before. The start now logs the
device's limits for one set and one stage next to that count, warns when the count is over them, and a refused
layout ends the start with the limits named instead of a bare failure.
The log line for a slot registered past the pool names both slots a shader reads instead: the pool's last entry, and
the last of the old length for a translation carried over from the translator before. The comments say what they say
without an issue number.
The translator's identity changes with this pool size: after the update every install translates its shaders again
in the background, the earlier translations serve meanwhile, and a shader gets the ground fix once it is translated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The game makes a texture pool of 33068 descriptors, and the translator clamped every pool handle to 32767, so the textures registered from 32768 up were drawn with whatever slot 32767 held, often an empty image: ground went see-through after an area load until the area was loaded again. VK_POOL_TEX and the translator's TexturePoolLength go to 40960, and a slot registered past it is logged once.
With the setting on, textures no shader sampled for ten seconds are copied on the GPU out of the emptiest allocator
blocks into the holes of fuller ones, so the emptied blocks go back to the driver. A pass starts when the free space
inside the device-local blocks reaches NX2NAVI_TEX_COMPACT_MIN_MB (one block under VRAM pressure) and the block
table has blocks that hold nothing but such textures, with room elsewhere for what moves. It takes at most four
textures and NX2NAVI_TEX_COMPACT_FRAME_MB a frame, and none in a frame that loads an area, already made or uploaded
textures, or follows a long one. A released texture the size cap would drop is dropped instead of moved.
Only a texture whose image holds what the game's memory holds moves: uploaded, never written by the GPU, single
sampled, not depth. The entry takes the new image under the texture lock and its address generation moves, so every
slot, pool descriptor and memo resolves again. The old image and its views stay until the texture pool has taken
every set of its ring and the GPU is past the submissions recorded until then, as a retired texture's do.
Each pass logs what it moved and dropped, the blocks and bytes given back, and the time it took. A block emptied of
textures that the allocator keeps is left alone for ten minutes, and a pass that gave nothing back doubles the wait.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
A log without the perf trace said nothing about the free space inside the allocator's device-local blocks, which the
driver counts as in use. Every 3600 frames (600 with the perf trace) a "mem: holes" line now gives the blocks and the
allocations in them, and for the memory type the textures use most the blocks that hold a texture, the textures and
VRAM mirrors in them, the blocks at most half full, and those of them that hold nothing but textures that could be
copied elsewhere or dropped: emptying those is the most a compaction could return.
The counting is runtime/vk_tex_compact.h, which also chooses the blocks to empty, with tests/tex_compact_check.py.
A texture entry keeps the block its memory lies in, so the table is built without asking the allocator per entry.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The bound on the released textures' memory only takes entries that are old enough and that no lookup reaches. The texture pool stamps every GPU-owned entry that a slot's last fill resolved to in each submission, a finalized slot's released entry included, so such an entry never counts as idle: in a session at 5120x2160 the released entries held 9.2 and then 10.1 GiB against a bound of 4,008 MiB, and its overwritten render targets were logged as "last used 1 presents ago". Nothing said which rule held how much. When the released entries stay a quarter over the bound after the drop, one line now splits them: released too lately, sampled lately, shown by a pool slot and not sampled (and of those, the ones another target was drawn over), reached by another lookup, next to go. No rule changes.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The staging pool keeps 4 blocks of 64 MiB from its first use on a discrete GPU (2 on an integrated one, as before). A staging buffer or arena chunk that does not fit them takes the room of the kept staging buffers and idle arena chunks, then of the oldest upload batches in flight, which it waits for; an arena chunk a waited-for batch gives back is taken instead of a new one. Only when that makes no room (the open batch's own staging, staging buffers their callers hold, the blocks' fragmentation) does the pool grow by a block, and the log says so. The outermost vk_upload_end submits the open batch once it holds 64 MiB of staging, besides the NX2NAVI_UPLOAD_BATCH_MB limit; nothing is submitted or waited for once the device is lost. Entering Hyrule Field made 221 staging buffers (1,131 MiB) in one frame; on Linux each new 64 MiB block is a kernel allocation that tries 2-4 MiB pages first, and with the free 2 MiB blocks used up the kernel compacted memory inside those allocations: 30-360 ms each, and the load frame took up to 4.7 s instead of about 0.3 s (RX 6800 XT, amdgpu, 32 GB).
The budget spreads the streamed, compressed pictures over frames. A texture that is not block-compressed is as often the game's data, a height map or a table, and a draw that reads the stand-in in its place for a frame misplaces geometry or lighting. Up to 24 of them a present, of at most 4 MiB each, are made at once; pool_data_first = 0 leaves them to the budget.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
A finalized slot shows its released entry while that holds the texture, and the pool's check passes released entries by, so no lookup reached such an entry and it counted as idle while every frame sampled it. Past the released entries' size cap or count it was dropped, and the slot showed the stand-in until the game registered it again. An entry sampled within the stale time is not idle now, and the count sweep leaves one that was used.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
In towns, kerb, stair and wall-top edges are drawn untextured for one frame when the game registers a round of streamed textures, and no pool entry of those textures goes to the stand-in in such a frame. A binding written into a draw's own set, and a slot that is unregistered when its marks are read, left no event until now.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The slope term is a bias per unit of depth slope across one pixel of the target. A shadow map made k times larger on
each side (shadow_scale 2 to 4) has 1/k of the slope across each texel, so the game's factor alone gave k times less
bias on sloped surfaces than at the game's own size, which is what shows as acne. vk_exec_draw.c slope_bias
multiplies the factor by the pass's scale for both the direct draws (record_fixed) and the queued mesh draws; the
constant term counts the depth format's smallest steps and keeps its value. Nothing changes at shadow_scale 1.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
Ten slots a frame are noted as having no picture when their submission ends: the 128x72 and 64x36 volumes the game registers under new slot numbers in every frame. The events now tell a slot whose entry cannot be viewed in any kind a shader reads from one whose lookup found nothing.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
A texture another target was drawn over is marked overwritten, dropped 600 presents later, and offered to the image reuse first. Neither asked whether anything still reads it. In a player's recording the entry of a second descriptor at the scene target's address was dropped that way and made again in the same frame from a released entry there, whose bits are the scene in another format: one frame nearly black with its colours exchanged, brightness 17 against 102 around it. An overwritten entry a draw, a read or the pool's marks used within those 600 presents now stays, and the image reuse passes over any released entry used within the last 16; tex_overwritten_used = 0 drops them as before.
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 rule for a slot that shows another texture left out the slots that showed nothing when the game registered them: the game takes slots whose texture it finalized some time ago for its next textures and draws with them at once. Past the 16 a refresh makes, such a slot kept its stand-in until the next round. One of them was an effect's texture with a transparent ground, which drawn with the stand-in was a flat grey patch for one frame, at each of two rounds of registrations four frames apart in a player's recording. A slot registered since its last lookup that found a texture now counts with the shown ones, within the same 64 a present.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The game finalizes a texture object while another one it initialized at the same storage with the same parameters stays registered in the pool, and the entry the two share is released all the same. Two frames later its image went to the next texture of its parameters and the entry was dropped; the slot then had nothing to show, because the pool makes no entry for storage the CPU cannot read, and kept its stand-in until the game drew into the texture again. Ocarina's 384x384 shadow maps are such textures: a player's recording has four slots of them sampled as stand-ins in the frame after three of their images had gone to other shadow maps, and a house front without its shadow in that frame. The reuse now passes over a released entry that a slot which is not finalized names; tex_reuse_named = 1 restores the old choice.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>