155 Commits

Author SHA1 Message Date
chrg6
1c3fa7df2a Give Streamline constants and null tags on a frame without inputs while frame generation is on.
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>
2026-11-14 12:54:08 -05:00
chrg6
2dd363b86e Count the texture cache's short waits in seconds below 60 presents a second too.
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>
2026-11-14 11:33:02 -05:00
chrg6
712568d4fb Make the release 0.11.0: the release prepared and previewed as 0.10.2 takes this number, with its notes.
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>
2026-11-14 09:24:25 -05:00
chrg6
5eef43742d Compare textures up to 8 MiB by content before uploading them a second time, by default.
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>
2026-11-14 08:33:06 -05:00
chrg6
87bb179c78 Keep released textures for one minute and up to a thirty-second of the video memory budget.
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>
2026-11-14 08:33:01 -05:00
chrg6
6e57ef9619 Check three of this pass's decisions without a device: the automatic texture detail, the device made with DLSS Frame Generation on, and the mesh fallback's depth hold.
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>
2026-11-14 05:35:49 -05:00
chrg6
6ababe8275 Let the automatic texture detail choose full from 12 GiB cards on, and never on an integrated GPU.
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>
2026-11-14 05:12:10 -05:00
chrg6
190cdbb50f Say in the log what keeps the half-full blocks of video memory from going back to the driver.
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>
2026-11-14 04:30:25 -05:00
chrg6
f7a2d94813 Bring public main's video memory fix and 0.10.1's defaults into the 0.10.2 line.
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>
2026-11-14 04:20:32 -05:00
chrg6
6b95c867c8 Make the page write generations 64-bit, so a long session no longer wraps them and every texture, mirror and shadow check after the wrap answers wrong.
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>
2026-11-13 20:12:50 -05:00
chrg6
b04e7c85b2 Log shaders that index the texture pool with the earlier translator's length, and a texture they cannot reach.
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>
2026-11-13 18:41:45 -05:00
chrg6
a8cc1e694e Write the released textures' room census only while they may keep more than their bound.
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>
2026-11-13 18:41:40 -05:00
chrg6
8fc2228996 Time the hard-count batches of released textures, to measure what one costs.
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>
2026-11-13 18:41:40 -05:00
chrg6
c0f8a3224d Stop the VRAM pressure check from walking every texture and every released entry again when nothing can go.
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>
2026-11-13 18:41:40 -05:00
chrg6
28a69d23cc Write the GPU-drawn textures of a hard-count batch back in one upload batch before the drops.
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>
2026-11-13 18:41:40 -05:00
chrg6
48ea624389 Bring the stutter map's ini text, the checks in docs/building.md and the released-textures bound line in step with the code.
- 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>
2026-11-13 18:41:39 -05:00
chrg6
5354aecdc5 Name the term that set the released textures' size bound in its log line.
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>
2026-11-13 15:57:52 -05:00
chrg6
1fa8b61b87 Give back the ASTC decoder's grown scratch 5 s after its last large texture, not at the next upload.
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>
2026-11-13 15:57:52 -05:00
chrg6
00081be563 Split the released textures' take-backs and remakes by kind and size, and say what keeps them past their bound with room.
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>
2026-11-13 15:57:52 -05:00
chrg6
2969005edc Past the hard count of released textures, drop the cheapest ones first, in one walk per batch.
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>
2026-11-13 15:57:52 -05:00
epicmeme546
1dbecd464f Hand an overwritten render target's image on after 8 presents while the card is past its pressure share, and free the ones no texture took after 32.
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>
2026-11-13 11:49:48 -05:00
epicmeme546
217fb32f6b Let a render target that was drawn over give its image to the next target of its size, so video memory no longer fills up while the camera moves.
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>
2026-11-13 11:49:41 -05:00
chrg6
cc5c8f87ad Add shadow_pcf, which makes every depth-compare sampler filter linearly so each shadow test blends the 2x2 shadow map texels around it, and log the filters of the compare samplers made.
Through a nearest filter each of the game's shadow compares tests one texel, so shadow edges are stair steps of shadow map texels that crawl over the ground as the sun cascades follow the camera. The setting is off by default and read at start; with shadow_scale above 1 the samplers were already linear.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 08:36:46 -05:00
chrg6
fadd4a6d5e Copy large textures out of the game's memory on several threads at once: a level of 1 MiB or more goes to a small worker pool as bands of rows (par_jobs.c, tex_deswizzle.c tex_bl_copy_mt), and so does a large block copy for the GPU's ASTC decoder, so the upload's storage copy no longer runs on the drawing thread alone. The bytes are those of the one-thread copy; par_jobs = 0 turns it off.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 08:35:42 -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
b912d72b87 Remove the Video memory use row; its low behaviour is the only one now.
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>
2026-11-13 06:23:01 -05:00
Tobim6
1eadef609e Put textures and allocations past a full video memory heap only in another device-local heap, never in system memory.
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>
2026-11-13 05:09:47 -05:00
chrg6
ff8435ce34 Count the released textures' hold in seconds of play, not of the wall.
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>
2026-11-13 00:12:46 -05:00
chrg6
08ad7402f7 Keep released textures by how long they went unused instead of by a small size bound.
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>
2026-11-13 00:12:31 -05:00
chrg6
2820781fb7 Log the memory by owner every 3600 frames, and how often released textures come back.
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>
2026-11-12 23:21:17 -05:00
chrg6
2571e180c8 Check the texture pool's set against the device's descriptor limits, and name the slot a carried translation reads.
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>
2026-11-12 22:46:24 -05:00
Leyukaka
2388f1f581 Give the texture pool room for Ocarina's 33068 descriptors instead of 32768.
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.
2026-11-12 22:45:06 -05:00
chrg6
ec9dd522b1 Add texture compaction behind NX2NAVI_TEX_COMPACT, off by default.
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>
2026-11-12 22:33:29 -05:00
chrg6
d668a62854 Log how much of the allocator's blocks is holes, and how much of that a texture compaction could give back.
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>
2026-11-12 22:33:29 -05:00
chrg6
264dbe42cc Merge the stability and memory pass into the 0.10.0 line.
Conflicts: the settings key lists and section maps take both sides' entries.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-12 21:49:17 -05:00
chrg6
e3e4b2d650 Log what keeps the released textures above their size bound.
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>
2026-11-12 15:21:43 -05:00
Vasilis Manetas
677eb5c242 Keep an area load's texture staging inside the staging pool's blocks made at boot, instead of making about 1.1 GiB of new host memory in the load frame.
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).
2026-11-12 14:59:47 -05:00
chrg6
ac3c43ba4d Make a small plain-format texture at its lookup, beside the creation budget.
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>
2026-11-12 14:52:26 -05:00
chrg6
16b0b46155 Keep a released texture that shaders still sample out of the idle drops.
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>
2026-11-12 13:12:58 -05:00
chrg6
e3cf0710c5 Record a draw's own texture binding that got the stand-in, and a sampled pool slot that is not registered.
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>
2026-11-12 13:08:16 -05:00
FIocker
58efccd883 Scale the slope-scaled depth bias with the shadow map scale, so the larger maps keep the bias the game asked for.
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>
2026-11-12 12:39:53 -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
f45273bfe5 Say in the flight recorder's blank-slot events what the slot resolves to, and whether any shader reads the pool as a volume.
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>
2026-11-12 11:28:22 -05:00
chrg6
a310940003 Merge branch 'claude/gfx-layer-perf-test' into ra-perf-test 2026-11-12 11:01:54 -05:00
chrg6
277f01375a Keep an overwritten texture that is still used.
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>
2026-11-12 11:01:44 -05:00
chrg6
a389f2450f Add a switch that compares textures over 64 KiB by content before uploading them again, and hash eight chains at once.
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>
2026-11-12 10:49:54 -05:00
chrg6
65dacfa7c7 Ask the write watch once per run of regions in the texture pool's check, and pass released entries by.
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>
2026-11-12 10:49:54 -05:00
chrg6
5ec1008709 Make a newly registered slot's texture at its first lookup too.
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>
2026-11-12 10:33:59 -05:00
chrg6
a374e778fa Keep a released texture's image while a registered slot still names it.
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>
2026-11-12 10:25:41 -05:00
Tobim6
01d81811a1 NX2NAVI_LOG_TEX_NEW: log the textures made from a frame on, with what destroyed the same key before
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 16:22:32 +01:00