The sixteen settings the fixed look took away are settings again: the tone mapper choice, gamma correction, mid grey, match tone slope, tone slope, hue clip emulation, exposure, highlights, shadows, contrast, saturation, highlight saturation, blowout, flare, hue correction and colour grading. They have their rows in the settings table with the defaults and ranges they had, their rows in the Screen tab under HDR output's three brightness rows (none of them with the game's own HDR), their texts in the four languages, and their lines in the example ini. The tonemap library has the luminance mapper and the grading of the observer-response mapper again, and the support buffer carries five vec4 for it.
A player who changes nothing sees the picture as before. The observer-response mapper leaves each grading step out when its value is 1, and the slider values are divided, not multiplied by a rounded reciprocal, so 50 and 100 % arrive as exactly 1. Evaluated on the CPU in single precision, the shader with every default gave the same bits as the fixed one for 400,000 random scene colours, gains and whites, in HDR and for the SDR picture; the luminance mapper and the game's curve gave the same bits as before they were removed, and the observer-response mapper with its sliders moved stayed within 0.004 % of the pixel of its old result.
Gamma correction now counts with every tone mapper, 2.2 by default, which is what gave the fixed look its true blacks; before the fixed look it ran with the luminance mapper only and the other two showed the dark tones far above black. Its off value brings that lighter picture back for a player who asks for it. The game's own HDR does not take the setting and stays at 2.2.
Lines for these settings that an nx2navi.ini still holds from before the fixed look count again; the example ini and the release notes say so. Nothing of this was seen in the game.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The textures the game lets go of were kept for three minutes and could hold a quarter of the card's budget, up to
8 GiB with room. A run through Hyrule Field shows what that bought and what it cost (RTX 5090, 5120x2160 with DLSS
Performance, about three minutes of running; the game keeps 4.8 GiB of textures live and loads about 190 a second):
the released textures held 5.3 GiB on average and the video memory in use went from 11.1 to 17.9 GiB, while the
game took back 1,494 MiB of them within 10 s of the release, 171 MiB within a minute and 10 MiB later, beside 33,000
textures it loaded anew. A shorter hold alone gives nothing back: with 20 s the released textures fell to 0.5 GiB
and the free space inside the allocator's blocks rose to 4.9 GiB, since a block returns to the driver only when it
is empty. What the released textures hold at their fullest is what the session costs, so the bound is the lever.
The bound is now a thirty-second of the budget, 384 MiB to 1 GiB (vk_tex_release.h trl_cap_mb), still doubled while
the card and the commit have room, and the hold 60 s, 120 for content only the GPU made. The same run with the new
defaults: 13.2 GiB in use on average against 16.7, 13.8 at most against 17.9, 532 MiB of textures made again over
the run, 77 fps against 75. An 8 GiB card keeps 384 MiB where it kept up to 1.9 GiB.
tests/tex_release_check checks the bound by the budget and the sessions it simulates against the rule before.
Compaction (tex_compact) and 16 MiB blocks were tried on the same run and gave back 0.7 and 1.2 GiB; neither is
changed here.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The game draws a culled list of grass or leaves twice from one indirect record: into the depth target alone, then into the colour targets. On the mesh shader fallback each draw has compute modules and a pipeline of its own, made at its first draw, and the depth-only one is ready first. Drawn alone it left the list's depth in the scene with nothing drawn over it, so what lies behind failed the depth test there and the sky's colour showed through the ground in the shape of the grass for as long as the colour draw's pipeline built, which is seconds each on AMD's driver for Radeon RX 6000 and older, where the fallback is the default.
A colour draw skipped for its modules or pipeline now stamps its record's address with the present, and clears the stamp when it is drawn. The depth-only draw of a record stamped within the last two presents is skipped as well, counted like a draw whose own pipeline is building. A draw whose stages write memory is never held. mesh_fallback_hold = 0 draws the depth alone as before.
Not run: no card here takes the fallback by default, and the game is the user's to start. The changed file compiles and the settings table's check reports 0 problems.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The review found six texts that claimed more than the code does. NX2NAVI_AUDIO_STRICT=1 does not bring 0.10.1 back
exactly: it also ends the process on a limiter attack or release that is not a number and on a mix in use with a
negative buffer count, which 0.10.1 let through, so the cfg.c comment, nx2navi.ini.example, the audren.h audio_bad
comment and the test's comment now say it does so for most of these values. Its FATAL line described the non-strict
outcome ("its sound passes through unchanged", "the renderer goes on") and then ended the process; it now ends with
"NX2NAVI_AUDIO_STRICT=1 ends the process here instead". The audren.c header and docs/architecture.md said nothing the
game sends ends the process, while OpenAudioRenderer's parameters and the request's buffer descriptors still do; both
now say a value in the game's RequestUpdate does not, and name the two that still do. The first log line of a kind
gave "N such values so far", but bad_count counts every update and every render pass that meets any kind, so it now
reads "N times so far for all kinds together"; the renderer_close line and the bad_count comment say the same.
docs/building.md and the test's comment called 0x5299 the one result the SDK hands back; the SDK hands back 0x5299,
0x5499 and 0x5699 and aborts on any other. Texts only: no behaviour changes.
The first commit's message says any other result than 0x5299 makes the SDK abort; it should read any result but
0x5299, 0x5499 and 0x5699. The branch's history is kept, so this message carries the correction.
audio_params_check (the strict runs still end with "FATAL: audio:" and the switch's name), cfg_rows_check (0
problems) and check_content passed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The audio renderer checked the game's RequestUpdate with about 80 rt_fatal and need() sites in audren.c,
audio_mixer.c, audio_effects.c and audio_reverb.c, so a section size, a splitter or mix record or an effect setting
out of range (a reverb3d whose delays add up past 0.4 s, a limiter attack outside 0 to 1, a mix buffer index past its
mix, a filter coefficient that is not finite) ended the process. The tested route never reached one; dungeons and
bosses were never run. Now an update whose sections do not match the renderer is refused whole with 0x5299 before
anything of it is applied: the SDK's RequestUpdateAudioRenderer hands that result back to the game, which ignores it
and sends its next update, where any other result makes the SDK abort. A record with an id the renderer does not have
is left out of an update that is otherwise applied, a splitter destination id it does not have is stored as -1, which
both render loops skip, and an effect or sink whose values cannot be used is passed through or skipped in the render
pass, with every index checked before its first write. audio_bad logs the first of each kind per renderer with the
game frame and counts the rest. rt_fatal stays for host allocations, OpenAudioRenderer's parameters and the checks
the new ones turn into invariants. Two inputs that did not end the process before change too: a mix in use with a
negative buffer count is refused (it laid the mixes after it out before buffer 0), and a limiter attack or release
that is not a number passes the sound through instead of turning the mix into NaN. NX2NAVI_AUDIO_STRICT=1
(audio_strict, default 0) makes audio_bad end the process as before, for bug reports.
Not reproduced before the change in the game; crafted updates reproduced the fatals. The new
tests/audio_params_check sends 27 refused updates, 10 records and 38 effects and sinks the renderer cannot use
through renderer_handler and runs the strict switch; 15 deliberate breakages of the change each made it fail.
audio_mixer_check, cfg_rows_check, check_content, a compile of every runtime file with no new -Wall -Wextra warnings
in the changed ones and the stability pass's test matrix (the two baseline environment failures only) passed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The review of cd2fd454 found that with present_order = 1 the present thread could record and queue frame N's blit
before frame_end(N) ran: when the presenting thread's try of the submission lock failed, frame_end(N) waited for the
next submission, and that could be the present thread's own blit submission, which present_main makes only after
vk_present_blit. The blit reads what vk_dlaa_frame resets when the upscaler or frame generation is switched on or off
(gfx_present.c hudless_record through vk_dlaa_fg_hudless; vk_fg.cpp, vk_fg4.cpp and vk_dlssg.c through
vk_dlaa_fg_inputs, vk_dlaa_fg_views and vk_dlaa_fg_camera). vk_dlaa_frame drops those inputs at a switch because
handing them to frame generation ended an AMD process before, and in 0.10.1 vk_dlaa_frame(N) always ran before blit
N. So a switch frame inside an overlap window could hand frame generation the inputs the switch should have dropped.
Now present_image tells the present thread when it left the frame owed (PresentJob.owed), and present_main first
makes sure that frame's bookkeeping ran (present_frame_closed, vk_frame_owed.h fo_close): when a submission did it
meanwhile, done says so (it is now stored with release after each frame's bookkeeping and read with acquire, fo_ran)
and no lock is taken; otherwise the present thread takes the submission lock, waiting for a replay in progress
(vk_sync.c vk_sync_locked), and runs what is owed. frame_end(N) therefore always comes before blit N, as in 0.10.1.
The present thread is not one of the game's threads, and it waits for the same lock for the blit's submission a
moment later anyway. It waited for that lock with its busy flag set before too, and nothing that holds the lock waits
for the present thread, so no deadlock is added. The cost, only for a frame still owed when the
present thread takes it: the blit's recording no longer overlaps the replay it waits for, which can lengthen a drawing
thread's existing wait in vk_present_drain by that recording. The common frame, whose present got the lock, takes no
lock here. The log's frame bookkeeping line (exit, and trace = perf every 600 presents) counts these frames and the
time they took the present thread, the wait included.
tests/frame_owed_check now has a present thread that takes each frame from the presenting thread (one in waiting at
most), blits it and submits: no blit comes before its frame's bookkeeping. Its "old" argument leaves the catch-up out
and fails, as the branch had it: 2608 and 2706 blits before their frame's bookkeeping in two runs of 20,000 frames.
docs/building.md, docs/architecture.md and nx2navi.ini.example say where the bookkeeping runs.
Not reproduced before the change. Checks that passed: frame_owed_check (20,000 frames; 12,695 owed, 1,888 of them
caught up by the present thread, 0 blits before their bookkeeping, the longest present 0.003 ms), pass_cc_check of
every runtime file (headers changed), no new -Wall -Wextra warnings in vk_backend.c and vk_sync.c.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The submit path no longer relies on one guest thread at a time: replays take turns on the submission lock, a frame's
bookkeeping at its present runs between two replays (vk_frame_owed.h), a held present goes out once and in order
(vk_present_held.h), a second submission of a recording in flight gets a clone (nvn_submit_guard.h), and held work is
asked about from a published copy (vk_evo_peek.h). docs/architecture.md section 4 gets a bullet saying so, with the
switch and the log lines; the comment above nvn_api_core.c nvn_call_note now says what holds and that an overlap line
is a count, not a fault.
Checks that passed: pass_cc_check of nvn_api_core.c.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The review of 9071dfc3 found that nx2navi.ini.example, cfg.c's metas comment, the commit message and docs/architecture.md
said the rollback switch works "as up to 0.10.1", which it does not exactly. With 1, every place that lets something go
waits for the device again, C-5's queue-lock hazard with it, but what was let go of still goes through vk_dlaa.c's list
and vk_memory.c's deferred destruction, the motion vectors and the HUD-less picture keep their 8-present gate,
vk_upscaler_settle leaves what the submission being recorded names, stale_sizes waits once per call, and a refused DLSS
creation with features let go of still listed waits and tries again, where 0.10.1 turned DLAA off. Each difference is
the safer side. The ini line now says that the game waits at each change as up to 0.10.1 while what was let go of
still goes once the card is past it; cfg.c, vk_dlaa.c's "what the upscalers let go of" and docs/architecture.md say
the same. Comments and docs only.
Checks that passed: cfg_rows_check (0 problems), upscaler_idle_check, cfg.c and vk_dlaa.c compile with -Wall -Wextra
and no warning, tools/check_content.py --staged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
vk_ffx.cpp context_for (two places) and vk_ffx4.cpp context_for (three) waited for the whole device to be idle, with
the queue lock held, on the thread that replays the game's submissions, before they destroyed an upscale context of
another render size, the least recently used one for a new output size, or (FSR 4) every idle one after a refused
creation: a stall of the present's flip, the completion thread and upload submits at every pause-screen size, reload,
window size, dynamic resolution step or mode change with FSR on.
A context that goes is now let go of (retire_up): it goes into a list (vk_up_defer.h) with the timeline value of the
submission of its last upscale (Up.fence, set after each dispatch) and is destroyed on the same thread once the GPU is
past it, at the points where vk_dlaa.c takes its own list (every evaluation, every pass while none runs, and after
something is let go of). The 90-second trims, vk_ffx_trim_idle and vk_ffx_release_idle (the upscaler off, which no
longer needs retire_idle's wait) let go of theirs the same way. FSR 4's create descriptors, which the API asks to stay
live until the context is destroyed, move from the slot into a calloc'd Made that goes with the context, so the slot
can take a new context while the old one waits. A refused FSR 4 creation now waits on the timeline from the host for
the contexts let go of, no queue touched (vk_upscaler_gpu_wait), and tries again when any went; with
NX2NAVI_UPSCALER_IDLE_WAIT=1 that wait and every let go wait for the device as before. docs/architecture.md says
where the upscalers' releases go.
C-5 is narrowed: on the default path no upscaler waits for the device any more, so these waits no longer touch the
frame generation swapchain's own queues; vk_fg.cpp, vk_fg4.cpp, gfx_present.c, vk_hdr.c, vk_fsr.c and vk_color.c keep
theirs.
Not reproduced before the change (no FSR session log here). Every runtime file compiles, vk_dlaa.c, vk_ffx.cpp and
vk_ffx4.cpp with -Wall -Wextra and no warning; vk_ffx4.cpp's non-Windows stubs compile against vk_ffx.h;
up_defer_check passes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reflex's hold of the frame limit with DLSS-G is unmeasured here (no RTX 40 or 50), so a player needs a way back to
the pacing of 0.10.1 without an older DLL. vk_dlssg.c limit_allowed reads the setting through cfg_bool, again
whenever a setting changed (cfg_version), as vk_lowlat.c reads low_latency; with 0 the presenting guest thread asks
Reflex for no limit, the present thread hands Streamline 0, and the "frame rate:" line names NX2NAVI_REFLEX_LIMIT=0 as
the reason the game's own limiter holds it. The row sits beside FG_MULT in metas[], beside REFLEX in known[] and
beside LOW_LATENCY in sections[] (display), and nx2navi.ini.example explains it under framegen_multiplier.
cfg_rows_check reports 0 problems; vk_dlssg.c and cfg.c compile clean with the dev build's flags.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With DLSS-G, Reflex's sleep held each frame's start with no limit while the game's pacer still held the limit on the
present thread and at the window image's acquire (A-16, P-16): the pacer's wait came after the picture was drawn, so
the pad was read up to a period before the picture left, on top of Reflex's own wait for the GPU.
vk_dlssg.c vk_dlssg_pace, on the presenting guest thread in nvnQueuePresentTexture, asks dlssg_pace.h which limit Reflex
should hold and stores it; vk_dlssg_frame on the present thread hands a changed limit to Streamline before its present
(reflex_apply, under the present lock, which keeps Streamline's options calls on one thread at a time), as does each
new swapchain, which has neither mode nor limit. GfxBackend.frame_paced now returns the period the pacer still holds:
vk_backend.c vk_be_frame_paced gives the game's ceiling (frame_step_ceiling_ns, 240) while Reflex holds the limit,
0 where a driver's low latency holds it (vk_lowlat_pace, unchanged), else the period; during a movie the back ends get
0 and the pacer keeps the movie's period and catch-up slots as before. The pacer takes the limit back before the first
host present, after 200 ms without one, with a DLL older than nx2sl_reflex2 (one log line at start), for a limit
Streamline refused, and for the run when the frames show Reflex does not hold it; the "frame rate:" lines name who
holds it, the "dlssg: present N" line counts the frame starts Reflex held, and Reflex's sleep now counts as pacing
(perf_end_any, on whichever thread acquires). No wait is added on a drawing thread: the limiter's wait moves from the
acquire's sleep_until and the present thread's slot to Reflex's sleep a few lines later in the same acquire, and the
present thread's options call returns at once. docs/architecture.md names Reflex as a limiter. Not reproduced before
the change (no RTX 40 or 50 here); vk_dlssg.c, vk_backend.c and nvn_api_obj.c compile clean with the dev build's
flags, and dlssg_pace_check, fg_reset_check and frame_step_check pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The review found that docs/architecture.md said the ends of a crash and a fatal error make no call into RTSS's library,
while rtss_limit.c rtss_limit_crash puts the profile back as an orderly exit does where the state file could not be
written. The text now says that, that it never runs from RTSS's own thread, that the line takes each later value
before the profile does, and that a state file that cannot be read is left with the other executables' lines. Its
"one limiter" and README.md claims were corrected with the movie and unproven-hold fixes, and nx2navi.ini.example with
the kept-out restore. docs/building.md's description of tests/rtss_limit_check.py now names the checks added since:
the previous written value, a kill inside a save, put backs that do not land, an unreadable state file and a crash on
RTSS's own thread.
No behaviour changes. Checks that passed: tools/check_content.py --staged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The review found that nothing ended an RTSS hold that was never proven: when RTSS listed the game with the limit in
its profile but one sign never came (presents of another API value than Vulkan's, the update flag never cleared, a
period that never moved), the profile kept the limit and the game's pacer held the frames too for the whole run, two
limiters where the pacing rules allow one, with only a reason line in the log and nothing in the menu. Before the
hold had to be proven, RTSS alone held the game once it listed it. rtss_decide.h rtss_decide now counts the polls in a
row where RTSS lists the game, the profile holds a limit of this run (not "none"), a picture went out within 200 ms
and the hold is not shown; at RTSS_UNPROVEN_POLLS (60, 30 s) it puts the profile back and RTSS is out for the run
(RTSS_FAIL_UNPROVEN, "RivaTuner Statistics Server never showed it holds the game"). A minimised window, RTSS not
hooked, a want of no limit, a kept-out want or a poll that shows the hold start the count again. rtss_limit.c logs the
give-up, rtss_limit_state returns 3 for it, so the menu says RivaTuner does not limit the game and the game does, and
rtss_out_of_limit counts it, so a limit set in RTSS itself is reported as a second limiter. The comment above
rtss_decide, rewritten for the rule, is wrapped at 120 columns and no longer cites rtss_fresh, which does not exist.
tests/rtss_limit_check.c checks each sign that never comes (given up at the 60th poll, once, for the run), the polls
that do not count and a shown hold restarting the count; tests/rtss_worker_check.c unproven_run checks it against the
real worker. docs/architecture.md, docs/building.md and nx2navi.ini.example describe the rule and the remaining window
of two limiters before it.
This changes when the game takes the limit back from RTSS, beyond the approved rules: an RTSS whose hold never shows
in its shared memory (a version or setting that differs) is now left out after 30 s and the game's limiter holds
alone, as with rtss = 0, where it held the frames beside the game's limiter before. Not reproduced before the change.
Checks that passed: tests/rtss_limit_check.py (41 of 41), pass_cc_check.py --wall on rtss_limit.c,
tools/check_content.py --staged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The review found that the worker read rtss-profile.txt only on its first poll with the hooks library, which it loads
only for a limit, so a start with frame generation on, with the driver's low latency on from the first present, or
with NX2NAVI_RTSS=0 never put back a killed run's limit: RTSS then went on limiting the game at that value (counting
the generated frames too under frame generation), and the log and the menu reported it as the player's own limit
beside the game's limiter. rtss_limit.c worker now looks at the state file on its first poll that finds RTSS running
(left_start): a limit puts the profile back with the library it loads anyway, as before; frame generation and low
latency load the library for that one restore (restore_left, which now takes the line found); NX2NAVI_RTSS=0 keeps the
thread read-only and logs that the file holds the game's limit from a run that did not close and that a start with
RTSS let in puts it back. A restore the library could not be loaded for waits for the first poll with the library.
tests/rtss_worker_check.c adds kill_restart_framegen (the next start asks frame generation first: the profile is
removed, no limit written, the line gone) and kill_restart_setting (NX2NAVI_RTSS=0: no library loaded, the profile and
the line stay, the log says so). The rtss_limit.c header, docs/architecture.md, docs/building.md and
nx2navi.ini.example say so, and the ini text no longer says the game alone paces movies under RTSS.
Not reproduced before the change. Checks that passed: tests/rtss_limit_check.py (40 of 40),
pass_cc_check.py --wall on rtss_limit.c, tools/check_content.py --staged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The review found that rtss_decide.h rtss_target writes no limit only while a movie runs faster than the play limit, so
at the default limit of 60 a 60 fps movie leaves 60 in RTSS's profile and RTSS holds the movie beside the game's pacer.
Its comment said RTSS at the movie's rate would space out the frames that catch up after a late frame, and README.md
("one limiter at a time") and docs/architecture.md read as if every movie under RTSS were the pacer's alone. The
approved rule writes nothing to the profile at movies when the limit is 60 or more, so the code stays: rtss_target's
and rtss_pace_period's comments now say that at the movie's own rate RTSS keeps holding it and that whether its
limiter lets the frames that catch up through is not measured, README.md drops "one limiter at a time", and
docs/architecture.md lists the movie at 60 among the exceptions to one limiter. tests/rtss_limit_check.c pins a movie
at 60 under a limit of 60: no write, the profile keeps 60, held again after it.
No behaviour changes. Checks that passed: tests/rtss_limit_check.py, tools/check_content.py --staged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs/architecture.md said the pacer holds nothing while RTSS holds the limit, through movies and minimised windows,
that the pacer stands aside once RTSS lists the process, that RTSS fails at 1.2 times the limit over six polls, and
that a killed run leaves its limit for the next run to take as the player's. The one-limiter and RTSS paragraphs now
name the pacer's fallbacks (before the first host present, a movie, 200 ms with no host present, a limiter seen
letting frames through, any failure) and its 240 ceiling beside RTSS, the settle rule, a movie's profile, the proof
RTSS must give before the pacer stands aside, the 1.05 rule over three new periods, the rounding down, and the state
file with rtss_limit_crash. nx2navi.ini.example's rtss lines, README.md's frame generation item, docs/building.md's
rtss_limit_check and frame_step_check paragraphs and rtss_limit.c's header follow, and the comments at main.c
crash_handler's and dispatch.c fatal_end's rtss_limit_crash say when RTSS's library is still called there (no line in
the state file, never on RTSS's own thread). No code changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The review of the carry refusal (a2de7eb7) found that the "Translator updates" bullet still said a shader the new
translator has not translated yet uses the previous one's for the whole session, with no wait at the start or in play.
Since that commit, a previous translation whose texture pool array has the earlier length (32768 entries, the 0.9.2
translator's) is not used: such a shader is translated again, in the boot prewarm when the boot names it, otherwise at
first use, with its draws skipped meanwhile and a compute dispatch waiting for the translation. The bullet now says so,
names gfx_shader.c carry_usable, and says that a program the current translator fails keeps even such a translation
(carry_last). The bullet is rewrapped at 120 columns.
Documentation only; tools/check_content.py passes.
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 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>
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 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>
This takes back the three commits that added it (cd2f9f1, e60677e, a01910d): its runtime layer and Steam binding, the Co-op tab and what it drew over the game, the launcher's copy of Steam's library file, its settings, tests and docs. The tree is as it was before them, with the bug capture that came after kept: its note on screen no longer makes room for co-op's lines, and dbg_area, which the co-op had brought and the capture uses, stays.
The four settings of that co-op join the retired names, so a settings file that still has them logs no unknown key. A steam_api64.dll that the launcher put beside the game is left where it is: nothing loads it any more.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
A thread samples every two seconds the video memory budget against this process's use (vk_mem_vram) and the system's
commit headroom and free physical memory (plat_commit_info, plat_mem_info), and turns each side into a level: roomy,
normal, tight or critical, with five points of hysteresis upward and a 256 MiB floor. Changes are logged, and the
memory report's system line ends with both levels. MEM_BUDGET_TEST_VRAM_MB and MEM_BUDGET_TEST_RAM_MB pretend a side
is that small, so that what gives memory back can be tried on a large machine; the texture cache's rules are unchanged.
tests/mem_budget_check.py checks the levels and the hysteresis.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The shader memory cache held 777 MiB of SPIR-V for 7115 shaders for the whole session. A background thread now
compresses the SPIR-V of each shader no reader pinned for shader_compress_idle_s seconds (zstd level 1, 26% of the
bytes on the test install's 44,151 modules) and frees the plain copy; readers pin it through gfx_shader_spirv_pin and
get it decoded back. What the drawing thread compares at pipeline misses and draws, the module's FNV-1a and its Binding
decorations, is kept with the shader, so module_binding_missing, the water corrections and the DLSS, crumbs and skip
lists decode nothing. A compute pipeline created at a dispatch and a mesh shader's first pipeline key still decode on
that thread, and the memory report counts those decodes apart, with the SPIR-V as held against uncompressed and the
shaders held compressed. shader_compress = 0 keeps it plain.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every report adds a line for video memory outside VMA (the driver's usage and budget per heap against VMA's blocks, and counts of pipelines by kind and by last use, stage libraries, compute pipelines, layouts and descriptor pools, with the swapchain, DLSS and FSR sizes), a line that splits the process's commit by owner (the game's heap and maps, device-local memory, VMA's host blocks, the C runtime heap with the shader caches, pipeline tables, texture cache, log queue and flight recorder in it, the driver's pipeline cache data and the rest), and a line naming what grew by more than 64 MiB since the report before and since play began. Every ten minutes of play and at exit a ranking line orders the categories by growth and marks those that only climbed; tests/mem_growth_check.py checks that bookkeeping.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Released textures go once they have gone unused for the hold, and what a switched-off upscaler or SMAA held is freed, with no setting to keep them. A vram_use line left in a settings file is known and read by nothing. Memory use is to be decided by the card's and the system's budget instead.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
F8 now always writes a folder under logs/captures: the area, Link's position and rotation, the camera of the frame on screen and the game's own, the presented frame as a PNG, frame times around the press, the machine, the settings that differ from the defaults, the end of the log, the save, and the game's flags and texts. A note on screen names the capture. The flight recorder's F8 dump goes into the capture's folder.
NX2NAVI_CAPTURE_REPLAY starts a run on a capture's settings, teleports to its area, walks Link to where he stood and puts the free camera where the camera was; tools/capture_run.py prepares the profile from the capture's save, runs it and compares the two captures. docs/bug-capture.md has the files and the steps.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Up to four players, each in their own game and save, meet in a room. Rooms are Steam lobbies and the games talk through Steam's networking, so nobody forwards a port and nobody runs a server. The game signs in to Steam as its test app (480), so Steam shows the player as playing Spacewar. Off by default; the menu has a Co-op tab with Create a room, Join a room by its ID, Invite a friend, the players, chat and Leave, in English, Russian, Brazilian Portuguese and Simplified Chinese.
runtime/steam.c loads steam_api64.dll from beside the exe at run time and declares the C functions it calls itself; nothing of the Steamworks SDK is in the repository, and a player copies the file from a Steam game they have. runtime/coop.c is the room: the lobby's owner is the host, lets games of the same protocol and game build in and lists them, and every game takes positions, chat and progress only from the players on that list. runtime/coop_game.c reads Link from the Player actor and shares progress, items, songs and quest items through the game's own flag routines, each value only ever raised; small keys, rupees, ammunition and hearts stay each player's own, and the save is copied to coop-backup before anything of a room reaches it. The other players show as a name with a distance where their Link stands, projected with the upscaler's camera.
One Steam account cannot be in a lobby twice, and this PC has not the memory for two games, so the room and the game side were run with one game and tests/coop_peer.py as the second player over runtime/coop_local.c, a stand-in for Steam on 127.0.0.1 that only the environment turns on: the peer was let in, saw the game's Link in Hyrule Field, chat went both ways, the save was copied, one flag the save lacked was set and a flag the game does not share was refused. On Steam itself one game signed in as app 480, made a room, showed its ID and the player's Steam name on the Co-op tab and left. Not run: two Steam accounts in one room, an invite, a join by room ID over Steam, a host that leaves, a removal, and the menu's text probe on this build.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The hidden beta shared story progress through a relay a player had to run and never showed the other players. A new co-op replaces it. Gone are runtime/coop*.c and their headers, the menu's Co-op tab and its texts in four languages, tools/coop_relay.py, the co-op tests, docs/coop.md, the [coop] section of nx2navi.ini.example, the --probe-coop mode, the coop trace item, and Monocypher, which only this layer used.
The old settings stay known as retired keys, so an nx2navi.ini that still sets them logs no unknown key, and they stay on the list of private settings, so a bug report keeps a room's secret, a relay's address and a name out. The platform layer's UDP sockets and fs_save_snapshot stay for the new co-op; cfg_ini_now and fs_profile_path, which nothing else called, are removed.
Checked: the game builds and starts, the menu shows its four tabs, --probe-cfg and --probe-fs pass, and the launcher's private-settings test passes. tests/save_share_check.py builds without its co-op stub; it cannot run from this worktree because the folder's path is longer than Windows allows, on main too.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Frame dumps show the upscaler reads the bloom composite's output, which the game has already exposed, bloomed and
tonemapped to 0..1. With dlss_prebloom = 1 it runs at the bloom chain's first pass on the scene's linear light. Without
upscaling its picture goes back into the light target. With upscaling the lens flares and the bloom composite are
drawn at the picture's size and that picture replaces the game's upscaler's output. Off by default; with it off
nothing changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The row writes scene_share = 1 with dlaa = 0, which the start already sized the render-target heap for; widescreen.c now
reads the share live, says when the heap needs a restart for it (the row's note and the Restart count), and logs the
scene size, the picture size and how the scene is scaled once the fixed share takes over and at each change. The
stand-in copies a scene of the picture's size texel for texel with a nearest sampler, and the performance overlay shows
Ocarina's real scene size instead of the other builds' placeholders. scene_share gets its settings row and its ini
text; the row's texts are in all four menu languages.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Low, the default, lets released textures go after the hold and frees what switched-off upscalers and SMAA hold; High
keeps both for the session. The row shows Low for any setting but high, the old balanced among them. The descriptions
in all four languages no longer promise simpler surfaces or missing objects, since the default no longer trades them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Balanced, the default, is what the earlier commits made the default. High keeps what DLSS, frame generation's
HUD-less picture, FSR 1 and SMAA made after they are switched off, as before them. Low also sets, for the keys the
player leaves unset: released textures keep at most 1 GiB (tex_released_mb), textures give way from 70% of the video
memory budget (tex_vram_pressure) and the shared pipeline list is built at first draw (pipe_seed_boot = 0). Unsampled
live textures are not dropped in any of them: that eviction left the ground see-through for good. The values come
after the device profile and the low-end mode (cfg.c VRAMLOW); the row is in the Screen tab under Texture detail and
takes effect after a restart. The architecture notes describe the new memory report lines.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
The preparation and the boot build the stage libraries of single shaders. A mesh pipeline's pre-rasterization
library needs its mesh stage and its task stage together, and its fragment shader library depends on what that mesh
stage leaves unwritten, so only a pipeline record names what they are made of.
Every listed mesh pipeline the preparation builds and every recorded one the boot queues now notes its two shader
libraries, each key once. The preparation builds them on every core after the listed pipelines and before the
pipeline cache is saved, inside the marker that covers a driver dying on a listed pipeline. The boot builds them on
one thread at idle priority beside the game, behind the memory gate, with the rest of the library prebuild
(lib_prebuild = 0 leaves it out). Each is destroyed once built, which leaves it in the pipeline caches and the
driver's own, as for the single shaders' libraries.
A mesh pipeline of known stages that a draw meets with other targets, another blend state or sample count then finds
its libraries in the caches and is linked, where it was compiled whole. The recorded and listed mesh pipelines are
still built whole ahead of play, also on a machine that leaves the rest of the shared list to its first draw.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
A mesh pipeline (task and mesh stages: the foliage, the grass, the terrain) had no stage libraries. Each new one was
a whole build, 0.2 to 1.1 s the first time foliage of a kind appeared, and its draws waited or were left out of the
picture until it was done.
Where the graphics pipeline library is on, a mesh pipeline is now linked at its first use from three libraries and
its whole build follows behind, as for a vertex pipeline. Its pre-rasterization library holds the task and mesh
stages and is keyed by both shaders. No vertex input library is linked: a mesh pipeline has no such state. Its layout
is a stage-independent pair, the mesh and task stages' bindings in set 0 and the fragment stage's in set 1, so the
fragment shader library can be the one a vertex pipeline uses. That library's key now also carries what a mesh
pipeline's fragment module gets filled in and whether the shading rate is dynamic state, which the pre-rasterization
and the fragment shader state both hold. The fragment output library is shared as it is.
The fast link on the library workers, the wait of a draw for its libraries and the pipeline hints take mesh pipelines
in. The summary counts them by themselves: fast links and their time, how long the ones a draw missed took to become
usable, and the libraries made for them.
NX2NAVI_GPL_MESH=0 (gpl_mesh in nx2navi.ini) keeps the whole build. It is also kept without the pipeline library, on
a device that draws no mesh pipeline, and where mesh pipelines alone are built without the driver's optimiser; the
start logs which is in use and why. The mesh emulation of the low-end GPU mode is not touched.
A mesh pipeline with libraries is another pipeline to the driver than the one built before, so an install's recorded
mesh pipelines are compiled once more at the first start with this change.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
With the setting off the device is made exactly as before the mode existed, so the mode cannot change anything for a player who never turns it on. Turning it on in the menu then takes a restart, and the row says so; in a run that started with it on, changes still apply at once.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
A marker or a journal left by a start that died in the boot's builds was always read as a driver crash. Since the
whole warm those builds run for minutes, so a start ended from outside in that time (the task manager, a program that
stops the game, the power) switched the pipeline prebuild off for that driver for good, or put up to 32 variants on
the skip list that the driver had merely been building. Closing the window was already an orderly end and still is.
The crash handler now leaves a note while the boot's builds run. With the note the next start does what it did: it
skips the prebuild, or the variants the journal names. Without it the end is taken for a stopped start: the pipelines
are built again and the journal's variants are blamed on nothing. Three such ends in a row still skip the prebuild,
for a driver that ends the process without a fault the handler sees. The boot's journal has a file of its own, so the
preparation's keeps its meaning.
The whole warm waits prewarm_full_s seconds at most (600): when they are up the game starts, the builders step down
to the background's thread counts and priority, and the file says "done", so the boot screen has one turn per driver.
tests\boot_warm_check.py covers what a leftover marker means with and without the note.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
A start that finds the pipeline cache written by another device or driver has nothing built, and everything was
compiled beside the game: an 8-core integrated GPU drew fewer than 600 frames in its first 11 minutes. Such a start
now waits on the "Preparing shaders" screen, with counts, for every recorded pipeline, for the compute pipelines on
every core at normal priority, and, when a sample says the driver's own cache lacks the prepared stage libraries, for
all of them on every core. The memory gate still holds for the builds, and the library builds write the preparation's
journal while the start waits, so a variant the driver's compiler dies on is skipped from the next start on.
boot-warm-owed.txt beside the caches names the GPU and driver: a start closed during the warm is followed by one that
goes on with it, three at most, and "done" keeps later starts ordinary while the cache file still carries the earlier
driver's header. When the boot stops waiting (prewarm_wait_ms, or a minute without progress) the builders step down
to the background's thread counts and idle priority. prewarm_full = 0 turns the warm off.
With nothing in the driver's cache the probe took 425 s on an RTX 5090 with 16 CPU threads: 87 s for 5,775 recorded
pipelines, 19 s for 4,075 compute pipelines and 317 s for 42,231 prepared variants.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The stand-in for the game's upscaler enlarged the scene with a linear filter, which is softer than the console's picture. It now enlarges with the edge-adaptive filter the present already scales to the window with: the same shader, built a second time with a constant set so it squares what it filtered, as the linear pass does. The stored square-root values are filtered as before and the decode is the same. standin_filter = bilinear brings the linear pass back, and the setting is live. The linear pass also stays where nothing is enlarged and while DLSS or FSR 3.1 runs, since that upscaler scales the scene itself and its picture goes over the stand-in's. The movies do not go through this pass and are not changed.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The log thread wrote as soon as it woke, which outside a burst is once for every line: each line woke it and was a write of its own to the log file and the console. It now waits a quarter of a second from the first line it finds, or until the queue is half full, and writes what was queued by then at once, so the log is up to that much behind the game. What is being gathered stays in the queue, so a fatal error, a crash report and an exit write it out before their own lines as before; a game ended from outside loses those last lines. The queue's test checks the gathering, the half-full write and that a process which ends at once, by an exit or as a fatal error does, leaves every line in its file.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The game's scene upscaler samples the post pass's picture through an RGB9E5 texture at the scene target's address. On the console both are one memory; here they are two images, and the RGB9E5 one kept what it held when it was made. With NX2NAVI_NATIVE_TENSOR=1 the network therefore read an old picture or the target's R11G11B10F words as shared-exponent texels, the source of the infinities and noise it wrote. The stand-in already read the target's image for this reason. A network program's output image without storage use is now logged.
Only dispatches of tensor-core programs take the new path, and those run only with NX2NAVI_NATIVE_TENSOR=1, which stays 0 by default. The file compiles without warnings; no game run has checked the picture.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Choosing one of them while a temporal upscaler was set turned the upscaler off, and the picture fell back to bilinear scaling. vk_aa.c no longer keeps the game's SMAA while DLSS or FSR runs. The chosen pass replaces the game's composition as without them: in the native modes after the upscaler, and in the scaling modes before it. In the menu, a choice leaves the upscaler and frame generation alone, and the row notes that the upscaler's TAA runs too. Picking DLSS or FSR under Upscaling keeps the Anti-aliasing choice. The TAA entry, listed while no temporal mode is set, now sets FSR 4 Native AA where FSR 4 runs instead of forcing FSR 3.1. The overlay gets an Anti-aliasing line under Upscaling, for example "TAA + SMAA (Ultra)".
Checked on an RX 7900 XT in Hyrule Field with FSR 4.1 Native AA and Quality, and with FSR 3.1. The log has "the game's SMAA replaced by" smaa, off and fxaa while FSR 4.1.2 upscales, and the pictures are right.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>