vk_exec_held_ready runs without sub_lock (vk_backend.c held_resume and submit_sync). It followed evq[s].head into the
continuation and read the wait in the continuation's private copy of the recording. Meanwhile another submitting
thread, inside vk_sync_submit, could run evo_resume and cont_free that continuation. A reused block then gave
vk_exec_event_met an event address pointing anywhere, so the unlocked reader could fault, not only read a stale state.
Now the holder of sub_lock publishes what the decision needs into a per-queue sequence lock (runtime/vk_evo_peek.h),
wherever a queue's first continuation, or that continuation's wait or stop, changes: in cont_push when the pushed one
becomes the first, in evo_resume after the head moves on, and after cont_walk stops at another wait. The copy holds
the wait by value. vk_exec_held_ready decides from a copy taken from one publish. It never waits for the writer:
after three tries that overlap a publish it answers "not ready", which is what losing that race answered before.
This fixes F-1-06 without the front-end lock. F-2-01's lock is still the answer for the other unlocked front-end
state.
Not reproduced before the change. Checks that passed: the new tests/evo_peek_check (one writer publishing 1,000,000
times while three readers copy: no copy mixed two publishes, and the decision cont_ready makes, with a stand-in for
vk_exec_event_met; with the sequence check removed it reports over a million mixed copies), pass_cc_check on every
runtime file (a header was added), no new -Wall -Wextra warnings in vk_exec.c, target_frames_check, oq_hold_check,
sync_hw_check, mesh_path_check and gpa_check.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
vk_astc_scratch_trim runs at every present and held the decoder's lock while vk_defer_buffer waited for cmd_lock.
The upload path takes the two locks the other way round: vk_upload_begin takes cmd_lock, then vk_astc_record and
scratch_get take the decoder's lock. A present that trimmed while another thread was between vk_upload_begin and
scratch_get therefore hung both threads. Without the hang, the present still waited for as long as an upload flush
held cmd_lock through its fence waits. vk_memory.c gets vk_defer_buffer_try, which defers only when it gets cmd_lock
at once. When the lock is busy the trim leaves the scratch as it is, and the next present tries again.
tests/astc_trim_check builds the real vk_astc.c with platform/common.c's recursive lock. It checks that the trim gives
way while another thread holds cmd_lock, that it goes through once the lock is free, and that the forced interleaving
ends. Before the change the check reported the deadlock (exit 2). Checks that passed: astc_trim_check, pass_cc_check
on every runtime file (vk.h changed), no new -Wall -Wextra warnings in vk_astc.c and vk_memory.c, and the pass's
test matrix (35 ok; the 6 failures are the baseline's).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The review of d84aa5fb and c05e6528 found that nothing at compile time tied perf.c FT_C_N to stutter_map.c SM_CAUSES:
they sit in different translation units, perf.c ft_frame passes stutter_frame n = FT_C_N, and the areas' by_cause
counts and cause_short are indexed against SM_CAUSES, so a cause added on one side only would mislabel the map without
an error (tests/stutter_hitch_check.c checks only its own names[]). stutter_map.h now defines STUTTER_CAUSES,
stutter_map.c sizes SM_CAUSES from it, and perf.c asserts FT_C_N == STUTTER_CAUSES beside the FT_C_* list. With
STUTTER_CAUSES set to 11 the assertion stops perf.c's compile.
The review also found that the header comment line of stutter_map.c listing the causes ran to 142 columns, where the
file wraps at 120; the paragraph is rewrapped without a change of words.
No change to what runs. perf.c, stutter_map.c and the other files that include stutter_map.h (romfs.c, vk_exec_draw.c,
vk_pipeline.c) compile with the dev flags, perf.c and stutter_map.c add no -Wall -Wextra warnings, and
stutter_hitch_check and stutter_map_check pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The review of b4e12f1c found that vk_device.c timed_alloc calls perf_mark_end after a failed vkAllocateMemory too, with
0 bytes, so the "device memory allocations" mark's count now takes in failed attempts, which vk_headroom.c on_allocate's
old mark (VMA's callback after an allocation was made) never did. The time of a failed attempt held the frame as well,
and VMA may try again with a smaller block after it, so the mark keeps it; the comment now says that failed attempts
count with their time and 0 bytes and that the count can pass the number of allocations made.
Comment only. vk_device.c compiles with the dev flags and adds no -Wall -Wextra warnings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The review of d84aa5fb found that perf.c perf_present's window line computed "render busy avg" as the window minus
wait_ns[PERF_PACE], and that since d84aa5fb the pacing sum also holds the acquire's wait of a game that acquires its
window images on another thread (perf_end_any). That wait can overlap the render thread's own work, so for such a game
"render busy" was understated and the "render thread blocked" shares could add up to more than 100%. perf_present now
keeps the other threads' pacing it folds in over the window (pace_any) and takes only the rest out for "render busy",
as before d84aa5fb; the pacing share and the "bound:" rule keep the whole sum, which is what makes the acquire's pacing
visible. A comment there says that the shares of such a game can pass 100% and that the one-time "nvn: the game
acquires" line tells when that applies. The line's format is unchanged.
The review also asked that perf_end_any's comment cite a function: it now names vk_lowlat.c vk_lowlat_pace's comment.
Diagnostics only: the window line is logged with trace=perf. Not reproduced (it needs a game that acquires on another
thread). perf.c compiles with the dev flags and adds no -Wall -Wextra warnings; perf_summary_check passes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The review of c05e6528 found two texts that said more than the code or the logs do.
- runtime/perf_census.h stated as fact that the replaying thread's write-watch queries wait for the walk's address-space
lock. The verifier's caveat stands: in the session the charge is based on, the presenting thread's 600-frame memory
report (vk_memory.c vk_mem_census) ran at the same frames as the walks, and the log cannot tell which of the two held
the 63 replay hitches up. The comment now says they sat beside such a walk, that the charge assumes the walk held them
up, and that a play-test with mem_census = 0 checks it.
- nx2navi.ini.example's mem_census paragraph said the "mem: process" line comes every 600 frames. Since c05e6528,
vk_memory.c heap_log leaves its own walk out at a frame where perf.c census_check walked (every 3600th frame, and a
commit census on growth that falls on a 600th), and the commit census lines stand in for it; the paragraph says so.
Comment and settings text only. perf.c compiles with the dev flags; census_charge_check passes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The review of c05e6528 found a window in perf.c ft_frame: it loads walk_t0 and then walk_t1, while perf_census_walk(1)
stores walk_t1 = 0 and then the new start. A walk that starts between the two loads leaves ft_frame with the previous
walk's start and an end of 0, so perf_census.h census_charge took that walk as running since then, the whole frame as
overlapped, and moved all its replay and unattributed time to "census". The window is a few nanoseconds, but the error
is a whole frame. A walk takes 100-260 ms, so census_charge now returns 0 for a walk seen running for longer than
CENSUS_WALK_MAX_NS (2 s).
tests/census_charge_check.c gets a torn read (a start 10 s back with an end of 0), a walk running for 1.9 s, which still
counts, and one running for 2.001 s, which does not; its random frames now reach walks more than 2 s old and check the
moved time exactly (the overlap capped by the frame's room and its replay and unattributed parts) instead of only its
bound. Built against the header before this change, the check fails the torn read, the 2.001 s walk and the first such
random frame; with it, it passes. docs/building.md names the new case.
Diagnostics only: the census part exists only with NX2NAVI_MEM_CENSUS = 1. Not reproduced in a run (the window is a
few nanoseconds). perf.c compiles with the dev flags and adds no -Wall -Wextra warnings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The review of c05e6528 found that vk_memory.c census_main printed the walk's start and end as kern_now_ns / 1e9, which
is plat_now_ns: seconds since Windows started. No other log line uses that clock (the stutter lines give seconds since
the first frame, the slow-frame and present lines frame numbers), so the walk could not be set beside the frames it held
up, which is what a play-test with mem_census = 0 needs to tell the walk from the presenting thread's report as the
cause of the 600N+1 hitches. census_main now reads span_frame, the frame vk_mem_census sets at every present, beside
each end of the walk and logs "the walk from frame F0 to frame F1, N ms": the numbers the "stutter: frame" and
"perf: slow frame" lines carry. The walk's length stays.
Diagnostics only: the line is logged with NX2NAVI_MEM_CENSUS = 1, which defaults to 0. Not reproduced (it needs a game
run). No tool or test reads the line. vk_memory.c compiles with the dev flags and adds no -Wall -Wextra warnings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The review found that the comment in vk_backend.c present_main and the message of 3236dc3c gave a cause that cannot
happen: the menu cannot turn trace=perf on between the two reads. rt_trace is set once at start (main.c parse_trace),
TRACE is a RESTART row in cfg.c metas, no menu code changes it, and the only other writer, ipc.c probe_sdk_client,
runs in a probe mode and changes only the IPC bit. The single read stays, since the stamps and both lines then follow
the same flags; the comment now says only that.
Comment only. vk_backend.c compiles with the dev flags and with -Wall -Wextra without new warnings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
present_main (d84aa5fb) takes the blit and present-call stamps only while trace=present or trace=perf is on, then
read rt_trace again to decide whether to log. The menu's performance log can turn trace=perf on between the two
reads; the first window would then add the difference of a stamp and 0, a wrapped figure. The flags are now read once
at the start of each present and used for the stamps and both lines.
Diagnostics only. vk_backend.c compiles with the dev flags and with -Wall -Wextra without new warnings;
perf_summary_check passes.
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>
A hitch is decided only after the two frames that follow it, and an episode closes only at the next frame that is not
a candidate (stutter_map.c cand_decide, episode_close). The exit's report summed the areas as they were, so a hitch in
the last two frames before the exit, or a run of long frames ending there, was never counted: the audit's probe
(Av/stutter_exit_probe.c with the real stutter_map.c) printed 0 hitches for a 150 ms frame followed by 0, 1 or 2
frames, and 1 only from 3 frames on. The periodic and focus-loss reports only count it later; the exit loses it.
stutter_report("exit") now adds to its printed totals what the run would have counted: the open episode as
episode_close counts it (a hitch, or two frames or more), and the candidates still waiting, where one over
SM_HITCH_SURE_NS counts whatever follows and each joins the episode of the frame before it as cand_decide joins it. A
waiting candidate under the sure line is left out, as it may be a change of pace. The episode's area decides play or
load, and its first frame's cause goes into the by-cause totals. exit_tail only reads this state: the exit's report
runs on another thread (perf.c perf_summary_flush from the window's close or the game's exit) while the presenting
thread, which owns ep, the candidates and the areas' play counts, may still be in stutter_frame; closing the episode
from there would race it. The per-area lines and the other reports are unchanged.
tests/stutter_hitch_check.c checks six tails (a 150 ms frame followed by 0, 1 or 2 frames, two 150 ms frames, two
with one frame after them, and 87/91/96 ms): the exit's report prints one hitch more than the areas hold, the same
count the run reaches once it goes on. All six printed one fewer before the change. stutter_hitch_check and
stutter_map_check pass; stutter_map.c compiles with the dev flags and with -Wall -Wextra without new warnings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
stutter_map.c stutter_report held sm_lock from its start to its end: through a qsort over up to SM_PIPES (4096)
pipeline entries and up to about 85 log lines. The replay thread takes sm_lock in stutter_miss, and the pipeline
workers in stutter_ready from inside vk_pipeline.c's P.lock, which the draw path takes too (ready_note), so a worker
waiting for sm_lock while it held P.lock could hold up a drawing thread for the length of a report. Since 7b0dbeb1 the
report runs every 5 minutes instead of 10 and at a focus loss in play, on the presenting thread.
The report now copies the areas (at most 128), the used pipeline entries and the two lost counts into one malloc'd
block under sm_lock, releases it, and sorts and logs from the copy; the static pointer list it sorted is gone with it,
so two reports at once (the exit's and a periodic one) no longer share it. The areas' play counts still belong to the
presenting thread; copying them under sm_lock does not change that. perf.c session_report's comment now says the map
is copied under its lock.
The output text is unchanged: a scratch harness that ran the same frames, misses and ready notes (6 areas, 300 draws)
through the old and the new stutter_map.c printed identical reports, 50 lines each. Not reproduced before the change
(the hold is estimated under a millisecond, through the log queue). stutter_hitch_check and stutter_map_check pass;
stutter_map.c and perf.c compile with the dev flags and with -Wall -Wextra without new warnings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
perf.c rt_knob reads "0" as off, "ab" as the A/B blocks and everything else as on, without a word: "cache_die = off",
"crash_filter_guard = false" and "one_per_core = 0 ; x" all read as 1 (the audit's probe, Fv/knob.exe, cut rt_knob out
with cfg.c: three 1s and no log line). A player who wrote such a value meant the opposite and had no way to learn it
from the log.
rt_knob now logs "cfg: NX2NAVI_<name> = '<value>' is not 0, 1 or ab: read as 1" the first time it reads such a value
for a switch. The value still reads as 1: reading words as 0 changes what a written setting does and waits for
approval with F-8. Most of the 109 call sites keep the result in a static but some read it per call, so the warned
names are kept in a table of 32 pointers (the names are string literals) filled by compare-and-swap; 0, 1 and ab pay
only one more string compare.
New tests/rt_knob_check.py cuts rt_knob and its table out of perf.c and builds them with stand-ins for the settings and
the log: 0, 1, ab, empty and unset read as before without a line; off, false and "0 ; a note" read as 1 and log once
each however often they are read. docs/building.md describes it. The fault was shown before the change by the audit's
probe; rt_knob_check passes, and perf.c compiles with the dev flags and with -Wall -Wextra without new warnings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An area's first visit (0.10.0 fresh session, nx2navi.1.log, frame 10113) took 421 ms, and the stutter line left 192 ms
of it as replay and 27.5 ms unattributed after the pipeline, translation and texture waits. The same frame made 78
device memory allocations of 4442.7 MiB, but the "device memory allocations" mark carried only a count and bytes:
vk_headroom.c on_allocate added it with zero time, since VMA's allocate callback runs after the allocation. At the
8-19 ms per 256 MiB block that vk_headroom.c measured, those allocations could be most of the frame's unexplained
time.
vk_device.c create_device now hands VMA a VmaVulkanFunctions whose only member is vkAllocateMemory = timed_alloc;
VMA takes the others from its static table (vk_vma.cpp VMA_STATIC_VULKAN_FUNCTIONS 1; its ImportVulkanFunctions
copies only the non-null members). timed_alloc brackets vkAllocateMemory with perf_mark_begin and perf_mark_end, which
cost one branch while the trace is off. on_allocate's zero-time mark is gone, and perf.c mark_text prints the
allocations' time, count, bytes and longest like the other kinds. The texture statistics' count of large allocations
is left as it is (adding the time there would have counted every allocation a second time).
Diagnostics only, no change to play. Not reproduced before the change (it needs an area load with trace=perf).
stutter_map_check, stutter_hitch_check, frame_step_check and mem_growth_check pass; every runtime file compiles with
the dev flags after the host.h comment change, and vk_device.c, vk_headroom.c and perf.c add no -Wall -Wextra
warnings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
vk_backend.c synced_held_count counts the game's fence waits that return while a submission stopped at an event wait
is still held, to decide whether the value should be signalled only after the held remainder (F-2-05). The window
acquire's sync is fenced with value 0 (nvn_api_obj.c nvnWindowAcquireTexture), so a game's wait on it reached the
counter as a wait for 0, which no held submission signals: 6 of the 12 "a game's wait for N returned with K
event-ordered submission(s) still held" lines in the two 0.10.0 fresh sessions were "wait for 0". The counter now
returns at once for value 0, before counting.
Not done here: counting only the waits for values at or after the oldest held submission, which needs its value kept
in vk_exec.c's continuations. Diagnostic only. Not reproduced before the change (it needs a game run).
vk_backend.c compiles with the dev flags and with -Wall -Wextra without new warnings; frame_step_check passes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With mem_census = 1 and trace = perf, vk_memory.c census_main walks the address space for 100-240 ms on its own
thread, holding the process's address-space lock. The replaying thread's write-watch queries wait for that lock
("mem: slow write-watch query: 20-34 ms ... 0 pages written"), and ft_frame charged the frame to "replay": in the
0.10.0 fresh session (nx2navi.1.log, 7,974,927 bytes) 63 of the stutter map's 66 replay hitches sat beside such a
walk, 49 of them at frames 600N+1..3. The presenting thread's own 600-frame memory report (10-17 ms, timed in
vk_mem_census since b81a9561) went to "unattributed". And at frame 3600N, census_check walked on the presenting thread
and the same frame's heap_log started census_main's second walk.
- perf.c perf_census_walk brackets census_main's walk (atomics); ft_frame moves the part of a frame over 25 ms (or a
stutter-map candidate) that overlapped the walk out of the replay part first, then out of the unattributed part,
into "census". The rule is runtime/perf_census.h census_charge, capped by the overlap and the frame.
- A new cause "report": vk_mem_census feeds the report's time (perf_report_part), and so do perf.c's memory
snapshots and its 600-frame sizes lines; the next frame takes it, so the overlap rule no longer has to infer it.
The cause list changes together: perf.c FT_C_* and ft_cause_name, stutter_map.c SM_CAUSES and cause_short,
tests/stutter_hitch_check.c names[].
- census_check records the frame it walked (perf_census_walked_frame); heap_log does not start census_main for that
frame, so frame 3600N walks once. A frame stamp, not a time window, since prof_commit_census can fail and leave
perf_note_wc uncalled.
- The "mem: process" line gives the walk's start and end on the run's clock and its length, so a play-test with
mem_census = 0 can tell the walk from the presenting thread's report as the cause of the 600N+1 hitches.
New tests/census_charge_check.py builds runtime/perf_census.h alone: no overlap, partial, contained and still-running
walks, the charge from replay first, capped by the overlap and the parts, and 100,000 random frames in which the census
part never passes the frame; docs/building.md describes it. Diagnostics only: NX2NAVI_MEM_CENSUS defaults to 0, and
nothing in gameplay, pacing or the save changes. Not reproduced before the change (it needs a game run). The pass's
known-issues table, which should say that hitch counts are taken with mem_census = 0 and trace off, is not in the
repository. census_charge_check, stutter_hitch_check and stutter_map_check pass; every runtime file compiles with the
dev flags, and perf.c, stutter_map.c and vk_memory.c add no -Wall -Wextra warnings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The perf lines could not see pacing. In the 0.10.0 fresh session (nx2navi.1.log, 7,974,927 bytes) all 666 windows read
"pacing 0%" and "bound: cpu" and all 70 slow-frame lines "pacing 0.0 drain 0.0", while 610 windows had a 16.x ms
interval at fps_limit 60 under the game's own limiter. The pacer's wait happened where perf does not count it:
vk_backend.c present_image's wait for the present thread to take the previous picture was untimed, perf_end drops
waits on any thread but the presenting and replay ones (a game may acquire its window image on another), and the
acquire's GPU wait was counted twice, as pacing around b->fence_wait and as a fence wait inside vk_sync_fence_wait
(F-7-04).
- host.h: PERF_HANDOFF, the presenting thread's wait to hand the present thread the next picture, timed in
present_image from under pl (where pending is read) and kept in PresentJob.handoff_ns. perf_end_any counts a wait
on any other guest thread into a sum that perf_present adds to the frame before ft_frame reads its parts.
- nvn_api_obj.c nvnWindowAcquireTexture: the pacer's sleep is timed with perf_end_any; the GPU wait counts only as
the fence wait it is; one line names the threads that acquire and present once the first present is known.
- vk_backend.c present_main: the TRACE=present line adds the present call's time (vkQueuePresentKHR, under the queue
lock, where an overlay's frame limiter sleeps) and the job's hand-off wait; with trace=perf, a line per 60 presents
gives the present thread's blit and submit, paced wait, present call (avg, max), time past target and the hand-off.
- perf.c: "handoff" in the window line's shares and the slow-frame line, "hand-off" in the worst-frame lines and as a
cause of its own for frames over 25 ms and the stutter map. The "bound:" rule stays as it is until play-test numbers
exist.
- The cause list changes together: perf.c FT_C_* and ft_cause_name, stutter_map.c SM_CAUSES and cause_short,
tests/stutter_hitch_check.c names[] (now sized from SM_CAUSES), and tools/stutter_map.py, whose cause and hitch
patterns now take a hyphen. flight.c's frames.tsv gets a handoff_ms column (blocked_names checked against PERF_N),
and tools/flight_view.py lists it.
- tools/perf_summary.py reads the hand-off share, the present-thread lines and the TRACE=present lines, and prints
percentiles of the present interval, paced wait, time past target, present call and hand-off.
New tests/perf_summary_check.py makes lines with the runtime's format strings (checked against the C sources) and
checks the percentiles, the window averages and the hand-off share, and that older lines without these fields still
parse; tests/stutter_map_check.py gets a hand-off hitch and area cause, which the old patterns dropped. Diagnostics
only: no wait, pacing or presentation change. Not reproduced before the change (it needs a game run with trace=perf).
perf_summary_check, stutter_map_check, stutter_hitch_check, flight_flash_check and frame_step_check pass; every
runtime file compiles with the dev flags after the host.h change, and the changed files add no -Wall -Wextra warnings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A session of 2026-11-13 grew from 683 to 1095 OS handles in 11 minutes, rising at every area load and never falling,
while the guest's own handles stayed at 116-122 and the threads flat; the VMA block count fell back while the handle
count did not. The log could not show it: the 600-frame "mem: sizes" line had only the guest's handles, and nothing
told which kind of handle grows.
kernel.c kern_sizes now adds "process handles" (plat_handle_count, one system call every 600 frames under trace=perf)
beside the guest's "handles". platform.h gets plat_handle_census: on Windows the system's extended handle table
filtered by our process id, with the type names from NtQueryObject's ObjectTypesInformation; on Linux the
/proc/self/fd links grouped by kind. It never opens, duplicates or names a handle (a name query can block on a pipe).
With NX2NAVI_HANDLE_CENSUS=1 (default 0, read at start) and trace=perf, perf.c starts one long-lived thread that the
600-frame report wakes; when the count grew by 16 or more since its last census it logs "mem: handles frame N: T in
all (+D since the last census): Event a (+x), ...". The census reads a few MiB of the system's table in 2-5 ms, off
the drawing threads; one thread for the run keeps a new thread's own handle out of what it counts. New cfg rows:
HANDLE_CENSUS in known[], sections[] (diagnostics) and metas[] ("0", "0|1", restart), and an nx2navi.ini.example
entry.
New tests/handle_census_check.py builds the platform layer alone: 20 events and 5 sections show as Event +20 and
Section +5, the total stays within 2 of GetProcessHandleCount, the figures return once they are closed, and kinds
past the room given go into the last entry; docs/building.md describes it. Not reproduced before the change (the
growth needs a game run). handle_census_check, sync_wake_check and the cfg row check pass; every runtime file compiles
with the dev flags, and perf.c, kernel.c, win32.c and cfg.c add no -Wall -Wextra warnings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
At 40 fps or less every frame is over 25 ms and so a hitch candidate. A frame that stood out opened an episode with
its hitch flag set, every following paced candidate joined it as a consecutive frame, and the cut after
SM_EPISODE_MAX frames applied only to episodes without a hitch. So the episode grew until the frame rate rose again:
a 150 ms frame followed by 300 frames at 30 fps counted one hitch of about 10 s, and five 120 ms hitches at a steady
30 fps counted as one.
cand_decide now keeps the episode up to its last frame that stood out (frame, summed time, frame count). When more
than SM_EPISODE_MAX paced frames follow that frame, the paced frames are a change of pace and the episode ends at the
frame that stood out and is counted with its time to there. An episode without such a frame is cut as before, and
episode_close is unchanged, so the 87/91/96 ms episode of 2f640fb8 keeps its 274 ms.
tests/stutter_hitch_check.c gets both cases: 150 ms then 300 frames at 30 fps counts one hitch of 150 ms, and five
120 ms hitches at 30 fps count five of 120 ms each. Both failed before the change (0 hitches counted while the run
lasted). stutter_hitch_check and stutter_map_check pass; stutter_map.c compiles with the dev flags and with -Wall
-Wextra without new warnings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rereading the fixes for the review showed lines past the 120 columns the files keep to: the RTSS paragraph of
docs/architecture.md ran one line to 176 columns after its hooks library sentence was replaced, rtss_limit.c had three
lines just over (the creations and removals counters, the removed line, find_rtss's kept-out line), and
tests/rtss_worker_check.c three. They are wrapped; no text, log line or check changes.
rtss_limit_check passes 27 of 27, check_content passes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The review found vk_fg.cpp's header saying both of frame generation's queues come from another family that can present
when the graphics family lacks them. vk_device.c create_device takes only the present queue from a family that can
present; the acquire queue, which needs no capability, is a second queue of that family or of any other family, or the
present queue itself when there is none. The header now says so.
Comment only: vk_fg.cpp compiles, check_content passes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The review found that frame_rate.c frame_step_cap_ns's unlimited branch handed rtss_limit_want "rtss_out ? rtss_out :
0", which is rtss_out itself: a kept-out reason (RTSS_OUT_*), or 0, which clears RTSS's limit. It now passes rtss_out,
and the comment says what each value does. Same want, same code: frame_rate.c compiles, check_content passes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The review found RtssIn.closed and its RTSS_WHY_CLOSED rule unused in the game: rtss_limit.c worker builds a poll's
view with the want alone, after it has skipped the poll under the lock once rtss_limit_restore ran, so the pure check's
closed case pinned a rule the worker never reaches. The field, the rule and the reason go; rtss_decide's comment says
that no poll decides after the exit and where that is done. The worker's skip stays as it was, and
tests/rtss_worker_check.c's restore_* scenarios (restore_inflight among them) keep covering it. The pure check's
closed_and_library becomes unset_and_library.
No decision changes: rtss_limit_check passes 27 of 27, check_content passes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The review found that A-11's fix stores the hold after every decision but also logs it then: when the want changed
while the worker wrote the profile (frame generation switched on during the first write), rtss_limit.c set_active
logged "RivaTuner Statistics Server does the limiting; the game's own limiter stands aside" while the game's limiter
was pacing the new want (rtss_limit_active compares the hold with the want), and the kept-out line followed one poll
later. set_active still stores the hold every time, so that a want going w, w2 and w again never finds an older hold,
but logs a hold only while the want is still the one decided for; a decision that RTSS does not hold the game is
logged as before. The next poll logs what holds the newer want.
tests/rtss_worker_check.c active_logged switches frame generation on inside the first SaveProfile (a hook in the fake
RTSS): no line may say RTSS does the limiting, the next poll logs the kept-out line, and a later limit is held and
logged. It fails on the code before this change. Log only; rtss_limit_check passes 27 of 27, active_race included.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The review found that docs/architecture.md's list of exceptions to the one-limiter rule leaves one out: when RTSS is
out of it for the run because its profile cannot be written (no administrator rights) or its hooks library does not
load, a limit the player set in RTSS itself (Global or the game's .cfg, a frame time limit, scanline sync) still holds
the game while the game's own limiter paces it. rtss_limit.c watch worked the second limiter out only for a kept-out
want, so neither the "two limiters" line nor anything else said so; only the "settings for the game" line showed the
values.
rtss_decide.h now has rtss_out_of_limit (kept out, or failed with RTSS_FAIL_READONLY or RTSS_FAIL_LIBRARY; a run the
frame count check gave up on is left out, since RTSS was seen not to hold the game), and rtss_two takes the failure.
watch gets S.fail and logs the two-limiter line for those runs too, and logs it again when the other limiter changes
(a failed run that then keeps RTSS out for low latency). The menu is unchanged: its two-limiter note (rtss_limit_state
4) stays for a kept-out want, and a read-only profile keeps its administrator note (state 2). Log only: no pacing
decision changes. The docs' exceptions sentence names the case and what the log and the menu show.
tests/rtss_worker_check.c readonly_two and library_two run it with a limit in Global; both fail on the code before
this change (no two-limiter line). rtss_limit_check.c's two() covers the failures. rtss_limit_check passes 26 of 26,
check_content passes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The review found docs/architecture.md claiming that a run kept out of RTSS from its first present never loads the hooks
library, its thread only reading RTSS's settings. That holds only for NX2NAVI_RTSS=0: once frame generation or low
latency is switched off, a limit is wanted, rtss_decide.h rtss_wants_hooks lets the poll load the library, and the
limit goes into the profile as usual (tests/rtss_worker_check.c kept_lowlat_then_limit). The paragraph now says that
the thread reads only while RTSS is kept out, that a later limit loads the library, and that NX2NAVI_RTSS=0 keeps the
thread read-only for the run; it also says the profile gets back what it held before the run changed it. The same claim
is corrected in rtss_limit.c's header and read_settings comments, in rtss_decide.h rtss_wants_hooks's comment, and in
find_rtss's start-up line for a kept-out first want, which now says the thread only reads while RTSS is kept out.
Docs, comments and one log line's wording: rtss_limit_check passes 24 of 24, check_content passes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The review found that numbering these lines had been set aside as the performance branch's work, while it is the part
of A-10 that may land on its own, and only this branch writes rtss_limit.c. A want that flips between a limit and kept
out (low latency before the first swapchain and during a rebuild, or switched in the menu) creates the profile and
removes it again at each flip; the play-test's live toggles item counts those lines. rtss_limit.c apply now ends the
created line with "(creation N this run)" and put_back the removed line with "(removal N this run)", counted under the
lock both run under. The settle rule that would stop the churn still waits for approval.
tests/rtss_worker_check.c churn_run also checks that the ten flips log creations 1 to 6 and removals 1 to 5 and no
more. rtss_limit_check passes 24 of 24.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs/architecture.md mentioned the pacer only inside its low latency paragraph and RTSS nowhere. It now has the pacer
(the limit counting drawn frames, the 240 ceiling, movie rates, the slots and their catch-up, the present thread's
wait and pace_smooth, the acquire's waits, the step snapped to the paced rate), the one-limiter rule as the goal with
the exceptions that stand today (pace_smooth under RTSS, DLSS Frame Generation's Reflex beside the pacer, no limit at
all from the pacer while RTSS holds it through movies and windows with no present, a limit set in RTSS itself while
RTSS is kept out), RTSS's part (the lookup on its thread, the profile write and read-back, when the pacer stands aside
and when it keeps the limit, the kept-out runs that only read RTSS's settings, the restore on orderly exits and what a
kill leaves), and how to measure pacing (the present and perf traces, perf_summary, PresentMon as the reference, the
runtime's own hitches). The changes that wait for approval are not described as done.
nx2navi.ini.example's rtss paragraph adds low latency, and the second limiter a limit set in RTSS itself makes with
rtss = 0, frame generation or low latency, which the log and the settings menu now point out. README.md's frame
generation item ends its sentence on RTSS with what it does and the same note.
Docs only; check_content passes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The worker read RTSS's shared memory, decided and wrote the profile in one loop over file statics, so its decisions
could be checked only against a running RTSS. rtss_decide.h now holds the parts that make no system call: rtss_parse
reads the shared memory's header and this process's entry (bounded by the memory's size and 4096 entries, as before),
rtss_decide takes one poll's view (the want, whether it is still current, a closed run, the hooks library, the entry)
and the state between polls (RtssState: the limit applied and the one refused, whether the run changed the profile,
the frame count, the failure) and returns what to do and whether RTSS holds the game, and rtss_poll carries that out
through the profile calls rtss_limit.c hands it (apply and put_back, which keep their I/O and log lines). The worker,
rtss_limit_restore and the menu state read the same state.
No decision changes: the order of the rules, the immediate write before RTSS lists the game, six polls over 1.2 times
the limit, a write per movie and per switch of low latency all stay as they are. The rules that wait for approval are
not in it (the pacer keeping movies and windows with no host present, Vulkan flags, settling and a fresh period
before standing aside, rounding the limit down, the state file).
tests/rtss_worker_check.c gains nine scenarios that pin those decisions against the real worker (a read-only profile,
a refused limit, the frame count rule at 144, 69, 73 fps and unlimited, RTSS quitting, a hooks library that does not
load, low latency switching, a movie); the logs of all 23 scenarios are the same, line for line, before and after this
change. tests/rtss_limit_check.c checks the same decisions on rtss_decide.h alone, with a fake RTSSSharedMemoryV2 for
the reader (a bad header, short entries, an entry past the memory or past the 4096th) and a fake profile behind
RtssOps; mutating the frame count's polls or slack, the refusal, the library failure or the clear of a refused limit
makes it fail. docs/building.md describes the check. Every runtime file compiles; -Wall adds no warning;
frame_step_check passes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
vk_dlssg.c said frame_rate.c divides the frame limit by the multiplier, but the limit counts the frames the game draws
and only a limit of display is divided (frame_rate.c limit_game_hz). vk_fg.cpp said FSR 3.1 frame generation applies
after a restart, but FRAMEGEN is live (gfx_present.c window_settings_live makes the swapchain again; only a device made
for DLSS Frame Generation waits for a restart, vk_fg_wanted), and that its two queues come from the graphics family,
while vk_device.c create_device takes them from another family that can present where the graphics family has one
queue. rtss_limit.c's header said the profile is put back "from every path that ends the process" and stays "until the
next run changes it", and named only the setting and frame generation as keeping RTSS out: it now names the exits that
skip the restore (a kill, the device-lost watchdog, a crash inside the crash handler), that the next run takes the
leftover limit for the player's own, low latency, the read-only thread of a kept-out run, and the lookup on the
worker. (rtss_limit_want's "both reasons need a restart" went with the change that starts the worker for a kept-out
first want.)
Comments only: the three files compile, and check_content passes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
rtss_limit.c writes and reads back only FramerateLimit and FramerateLimitDenominator, but RTSS's [Framerate] section
holds more that limits or paces the frames: a frame time limit (LimitTime over LimitTimeDenominator), scanline sync
(SyncScanline0/1), display sync, its own Reflex sleep and passive waiting. A player with any of them set in Global gets
an RTSS that holds the game to more than the game's limit, and no log said so.
While RTSS lists the game, the worker's reader (read_settings, the system's ini reader on Profiles\<exe>.cfg, else
Profiles\Global, read again only when either file's write time changes) now also takes SyncDisplay, ReflexSleep and
PassiveWait, and watch logs "RivaTuner Statistics Server's settings for the game (<file>): limit L/D, frame time limit
T/TD, scanline sync S0/S1, display sync X, Reflex sleep R, passive wait P" once per change, 40 lines at most. It reads
only, makes no call into RTSS's hooks library and changes nothing RTSS or the game does; whether the game should keep
RTSS out when one of these is set is left for approval.
Not reproduced before the change: it needs RTSS's settings in a play-test. tests/rtss_limit_check.c checks the text
with and without the modes and that a short buffer cuts it; tests/rtss_worker_check.c gains settings_logged (nothing
read while the game is not listed, then one line for the game's profile with what it took from Global, and one more
after the limit changes). Both pass, with a compile of every runtime file; -Wall adds no warning.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
rtss_limit_want ran start() on the thread that called it: the registry read of RTSS's folder, a file attribute query
under Program Files, the executable's name and the thread's creation. frame_step_cap_ns calls it from
nvnQueuePresentTexture, so a guest present paid for them once per run (or at the first present after frame
generation or low latency was switched off).
rtss_limit_want now only stores the want and, once, spawns the worker; the worker does the lookup and the "found" or
"not installed" line before its first poll (find_rtss) and ends when RTSS is not installed. rtss_limit_restore,
which may run on another thread, reads the names only once the worker has published them (ready). Pacing does not
change: the worker's first poll comes the time of the lookup later.
Not reproduced before the change (its cost was never measured). tests/rtss_worker_check.c gains start_off_present,
which counts the registry and file calls made on the thread that asked for the limit (3 before the change, 0 after),
and not_installed (the worker ends, the game's limiter paces, the exit has nothing to put back). The other scenarios
and a compile of rtss_limit.c with -Wall pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An RTSS without FramerateLimitDenominator gets a fractional limit rounded to whole frames (apply: 59.94 becomes 60),
but the line after the write named the rate the game asked for: "limits to 59.940 fps" while the profile held 60. A
play-test log would hide that RTSS limits above a 59.94 Hz monitor. When the written limit over its denominator
differs from the asked rate, the line now names both: "limits to 60/1 (60.000 fps; the game asked 59.940)". What is
written does not change.
tests/rtss_worker_check.c gains round_logged (an RTSS with the denominator: 59940/1000, logged as 59.940 fps) and
round_logged_whole (without it: 59.94 and 143.856 Hz logged as 60/1 and 144/1, a whole 60 as before). The second
failed before the change and passes after it; a compile of rtss_limit.c with -Wall passes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The worker checked that the want was still the one it had read and then stored active = 1, two steps on two atomics.
A rtss_limit_want between them (frame generation or low latency switched on, a movie, a new limit) stored the new want
and active = 0, and the worker's store then set active back to 1: frame_step_cap_ns returned 0 for the kept-out want
until the next poll, up to 500 ms in which nothing limited the game.
The worker now stores the want RTSS holds the game to (active_for, or RTSS_UNSET for none), and rtss_limit_active is
true only while it equals the newest want, so a change of the want stops matching at once and rtss_limit_want no
longer clears anything. The frame count check against the limit keeps its test that the want has not changed.
tests/rtss_worker_check.c gains active_race: the worker polls without pause while the want switches 400000 times
between 60 fps and frame generation, and after each switch to frame generation rtss_limit_active must read 0 until
the worker has seen it. Before the change 128440 switches read 1; after it none do. The other scenarios and a compile
of rtss_limit.c with -Wall pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With NX2NAVI_RTSS=0, frame generation or the driver's low latency, the game keeps RTSS out of its frame limit, but a
limit the player set in RTSS itself (its Global profile or the game's own: a frame rate limit, a frame time limit or
scanline sync) still holds the frames, on top of the game's limiter, the driver's or frame generation's. Nothing read
RTSS's settings in such a run: a first want that kept RTSS out never started its thread, and frame_step_cap_ns folded
the reasons into -1, so the log said "NX2NAVI_RTSS=0 or frame generation" for low latency too.
frame_step_cap_ns now passes the reason (rtss_decide.h RTSS_OUT_SETTING, RTSS_OUT_FRAMEGEN, RTSS_OUT_LOWLAT; the
unlimited branch too), and the kept-out line names it. A kept-out first want starts the worker as well. While RTSS is
kept out the worker never loads RTSS's hooks library and only reads: its shared memory, and once RTSS lists the game
the [Framerate] values of Profiles\<exe>.cfg, else Profiles\Global, with the system's ini reader, again only when
either file's write time changes. When one of them limits the game, the log names the file, the values and the other
limiter, once per change, and rtss_limit_state returns 4, which the frame rate row shows as a new note
(UI_MENU_FPS_RTSS_TWO, in English, Russian, Brazilian Portuguese and Simplified Chinese). A later want of a limit
(frame generation or low latency switched off) loads the hooks and hands RTSS the limit as before; NX2NAVI_RTSS=0
stays read-only for the run. Pacing does not change.
Not reproduced before the change: a stacked limit needs RTSS running in a play-test. tests/rtss_worker_check.c gains
three scenarios against the fake RTSS (a Global limit with NX2NAVI_RTSS=0, scanline sync in the game's profile under
low latency and then a limit, frame generation with no limit of RTSS's own), and tests/rtss_limit_check.c builds
rtss_decide.h alone and checks which settings hold the frames and their text. Both pass, with frame_step_check and a
compile of every runtime file; -Wall adds no warning.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
rtss_limit_restore put the profile back and set the want to unset, but the game presents on until the process ends
(the rest of the exit runs after it). The next frame_step_cap_ns called rtss_limit_want with the limit, which replaced
the unset want, and the worker's next poll applied it again: the profile kept the game's limit after the exit, and the
next run took it for the player's own. An exit before the first want, or while RTSS was kept out, found the worker not
started, and a want after it started the worker, whose first poll applied at once. A want the worker read before the
restore was applied after it too, since the worker reads the want before it takes its lock.
rtss_limit_restore now marks the run closed before anything else: rtss_limit_want returns at once, the worker skips
its polls once it holds the lock, and rtss_limit_active and rtss_limit_state read 0, so the game's own limiter paces
the frames left.
tests/rtss_limit_check.py builds tests/rtss_worker_check.c, which compiles the real rtss_limit.c against a fake RTSS
(its registry key, hooks library, profiles and shared memory) and runs each scenario in a process of its own: a want
after the restore, the player's own limit, a want read before the restore, and an exit before the first want or while
RTSS was kept out. All five failed before the change and pass after it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In the 2026-11-13 session three frames of 87, 91 and 96 ms after an area load each counted as a change of pace, as each was followed by one as long, and loads that only the texture pool saw (no area file read, so no scene line) were charged to play. Consecutive long frames are now one episode, counted once with their summed time, its line giving that time and the first frame's parts; a run of more than 8 long frames none of which stands out is still a change of pace. The texture pool's start and end of an area load mark an area load for the map, as the game's own loads do.
With trace = perf, the slow write-watch queries and freezes past the 200 lines they may log are counted and given every 60 frames, so a late area load still shows them; the session's lines had run out by frame 36,387.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In the 2026-11-13 session frames of 60-96 ms came 2 to 6 frames after each area load with the render thread waiting 59-86 ms for the GPU, and no line said what the GPU was doing. With trace = perf such a frame now gets a line at the next present: the timed parts that ended in its interval, their GPU time, the longest gap between two, and the longest parts with their kind (draws, copies into VRAM mirrors, pre-passes, uploads, presents) and their time in the queue. The completion thread keeps the last parts in a ring as it reads their timestamps; nothing waits for the GPU. Measurement only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The system line took the driver's device-local figure out of the process's commit, but the driver's write-combined views of that memory came to 0.2-1.1 GiB more, and that part went to "other": in the 2026-11-13 session other system memory grew 1,283 MiB in play while the process's own commit grew 307. With a walk of the address space (mem_census) less than a minute old, the gap at that walk is now a term of its own. The NVN recording copies, malloc'd, count among the C runtime heap's known parts with those unused for 5 minutes, and the line ends with the process's handle count, which rose at every area load in that session and never fell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A recording copy is retired only when the game adds its control memory again, and in the 2026-11-13 session the live copies grew from 9,138 (0.7 MiB) to 223,753 (58.3 MiB) in 7 minutes of play, about 38,000 more at each return to an area: the one real host-memory leak of that session. Each copy now carries the minute of its last use (made, called or submitted), and counters by minute give how many copies no submission used for 5 minutes, and their bytes, in the 600-frame sizes line, with no walk of the copies under the lock the recording threads take. Measurement only: retiring such copies needs this figure first.
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>
The report runs at the present every 3600 frames in every run (600 with trace = perf) and walks the texture entries, the pipelines and the C runtime heap's summary; the 600-frame frames of the 2026-11-13 session carried 10-17 ms nothing else accounted for, and the report's own comment said it took well under a millisecond. A line after it now gives the time of the vram lines, the holes line and the owners' lines with what follows them, so the next session shows whether the report needs to move off that thread.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The session of 2026-11-13 ended 8 s after its window lost the focus, after 7 minutes of play, with none of the game's exit lines: no frame-time summary of its last period, no stutter map of its last minute and no growth ranking at all, as the ranking came every 10 minutes. When the window loses the focus in play, the present thread now writes the frame-time summary so far (starting a new period), the stutter map and the growth ranking at its next present, at most once a minute; formatting only, nothing asks the device. The growth ranking comes every 2 minutes of play instead of 10, the stutter map every 5 instead of 10.
A process that ends through exit() rather than plat_exit now writes the log thread's queue last, after the frame-time summary's exit handler, instead of losing it.
The launcher logged a game's end only for a nonzero code, so the session's end with code 0 left no line anywhere. It now logs every end of a game it started, and when the game's log has no line of the game's own end (the window's close, the game's ExitProcess, a timed run's end, a FATAL or exit line), it notes the end there too.
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>
A session of 2026-11-13 met 46 pipelines late in adult Kokiri Forest (145 draws left out, mesh pipelines usable after up to 782 ms) and ended about 2 s later: the records waited for the optimised builds, and 23 of the 46 never reached the file, so the shared pipeline list folded from it would have missed them. A pipeline is now recorded as soon as a draw can use it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After a slow freeze the passes wait 250 ms, doubled for each further slow one, but the first fast freeze after a hold started the doubling over, so in the seconds after a load a 4-5 ms freeze came every 250 ms (Kakariko, 2026-11-13: five in a row, 16 frames apart), and freezes of 4-10 ms pushed frames of 25-52 ms over the stutter line. The doubling now starts over only after freeze_quiet_ms (default 5000; 0: the old rule) without a slow freeze. No pass runs while the texture pool makes an area's textures, when the frames are held anyway and one freeze took 32.4 ms, nor for freeze_load_hold_ms (default 3000; 0: off) after that or after an area's load. The regions that go idle meanwhile stay queued and are frozen later; their write-watch queries go on until then. A region is now queued once instead of again every freeze_idle submissions while the passes are held.
The rules are in runtime/freeze_hold.h, checked by tests/freeze_hold_check.py.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With mem_census = 1 the census trigger compared the private bytes with the last census, and Windows charges the video memory to them, so every gigabyte of textures started another 130-260 ms walk on the presenting thread: 22 of the 33 walks of the 2026-11-13 session came from that trigger, while the process's own commit grew 307 MiB in play. The trigger now reads the private bytes less the write-combined memory of the last walk (and what the driver's device-local figure grew since), compares them with the highest figure seen at a census, and waits 60 s after the last walk; the 3600-frame censuses stay.
On Windows one walk of the address space now gives both the commit census and the guest regions' committed bytes, which mem_guest_census walked a second time. The census counts the driver's write-combined views apart instead of as guest memory, counts as guest only the reserved guest regions and the loaded modules, and ranks the process's own allocations wherever they lie. The frame after a walk charges its time to a new "census" cause in the frame-time summary and the stutter map instead of "unattributed".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>