camfly moves the free camera by a distance and a turn in equal steps over a number of ticks, where camgo jumps: a
run can fly the view across an area without a route Link gets stuck on. A flight of 1.3 km round Hyrule Field showed
what it is not good for: the video memory in use stayed flat, the game loads and lets go of textures by where Link
is, not by where the camera looks.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
With Helpers take small jobs on (replay_small, with replay_pipe) the whole scene flashed for single frames under
DLSS, with the interface intact. The pass hooks keep what the last pass that ran them left: the upscaler's light
target is the one of the last scene pass before the output pass. A group's hooks run in its first chunk, and a pass
that chunk began again (after a barrier, or a texture lookup that waited for its turn) ran them a second time. With
the submitting thread already past the group, that came after the hooks of later passes and put the group's own
target back, so the upscaler took an earlier image of the frame for that frame. The log showed it: the light target
changed to another image and back at each flash.
The rule is in runtime/vk_pass_hooks.h, with tests/pass_hooks_check, which fails against the old rule.
Measured by replaying a bug capture in Kakariko (DLSS Performance, 3 workers, replay_pipe = 4, replay_small = 16,
100 s a run): 51 and 52 flash frames as installed, none with replay_small = 0, none with the helpers off, 52 with the
render target reuse off. Two copies of the installed build made the same way, one with this change and one without:
no flash frame in two runs with it, 373 in one run without.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The user asked for it: the mode no longer gains frame rate, and the mesh shader fallback covers the cards it was written for. Its switch in the settings menu's Game tab is gone with the rows under it, and so are their 57 text ids and strings.
low_end_gpu itself does nothing any more. It is still a known key, so an older nx2navi.ini gives no unknown-key warning, and while it is on the log says once that the mode was removed in 0.10.2. The preset that gave nine keys other defaults is gone, and so is the rule that read the mode's keys only while it was on: each of them (mesh_emulation, tex_clobber, foliage, vram_heap_limit, vram_reserve_mb, upload_batch_mb, mirror_stage, mirror_hot, submit_finish_gpu and the others) is an ordinary setting with the default of its row or of its reader, so a line a player wrote by hand keeps counting and a file with only the switch runs the game as it runs for everyone else. guest_opt in nx2navi.ini counts for the next rebuild without the switch too (tools/make_game.py).
The recorder thread read the switch itself: on an AMD card without mesh shaders the mode turned it on. It is off on every AMD card now unless vk_recorder = 1 is set.
nx2navi.ini.example lists the former mode's settings as single settings, the 0.10.2 notes have a Removed entry and name the foliage fix of the commit before this one, and the comments that said what the mode sets say so in the past tense. tests/cfg_parse_check has the new reading of the switch and of the keys without it; the test matrix is at 90 ok with the same two not ok as before (installer_test, worktree_test), and a clean build compiles 169 runtime files and links. Not run in the game.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The record was a fill of the whole record with 1 followed by fills and copies into parts of it, with no barrier between them: transfer writes to the same bytes, which the GPU may run in any order. On an RX 580 the vertex count, the first vertex or instance or the task count was the fill's 1 in about half the draws, and the foliage the mesh programs draw flashed, missing or white from the wrong slots. The record is now built as words first and written as fills of runs that do not overlap, with the count the GPU wrote copied into the words left out for it.
No vkCmdUpdateBuffer: it is not among the recorder thread's commands (vk_rec.h), so with the recorder on (an AMD card without mesh shaders in the low-end GPU mode) the call went to the driver from the replay thread while the recorder recorded the same command buffer, and amdvlk64.dll ended with an access violation at the first fallback draw.
Two commits by Tobim6 from the public repository (2d988e36 and 8ee0ca9d), applied to this line as one. Not run here: no card in this PC takes the fallback. The file compiles.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Each decision moves into a header the runtime and a test both read, as vk_mesh_path.h does: vk_tex_detail.h (tex_detail_auto), vk_dlssg_device.h (dlssg_card_skipped, dlssg_device_create) and mesh_fb_hold.h (the hold's table). The runtime's behaviour is the same as before the move.
tests/tex_detail_check has the budgets from players' logs of 0.10.1 (a handheld's integrated GPU at 10,361 MiB now gets reduced, a 12 GiB card at 11,313 MiB full). tests/dlssg_device_check plays the laptop of the report with stand-ins for the interposer and the loader: the chosen Intel card is skipped, the NVIDIA card is made through the interposer, a card of another vendor is made by the loader without the interposer being asked, and a refusal falls through to the loader. tests/mesh_fb_hold_check holds a list for two presents after its colour draw was skipped, through a wait of 600 frames, and no longer once it is drawn or gone. Built against the rule 0.10.1 had, the first two and mesh_path_check fail (6, 13 and 3 checks).
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The low-end GPU mode sets mesh_emulation = 1, and on a card without mesh shaders the emulation then draws the game's task and mesh programs in place of the mesh fallback. On NVIDIA's cards without mesh shaders that ended the game at the first mesh draws of an area with an access violation inside the driver, at nvoglv64.dll+0x10cdffe on a GTX 1050 Ti and on a GTX 1070 (both 0.10.1), and a Titan X (Pascal) report names the same mode. The GTX 1050 Ti's owner set mesh_emulation = 0 and played on through the fallback with the rest of the mode on.
The decision (vk_mesh_path.h) takes the card's vendor: an NVIDIA card is drawn for as if the emulation were not asked for, and the boot line says so. The replay and the pipeline code read the decided path from the device context where each read the setting itself. tests/mesh_path_check.c has the new cases.
Not run in the game: no GTX 10 card here. mesh_path_check passes and every file that includes the changed headers compiles.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Four players of 0.10.1 could not start the game again after changing settings: "no usable Vulkan device", exit code 70, until nx2navi.ini was edited or removed by hand. One log shows the case: a laptop with an RTX 3050 and Intel Iris Xe graphics, framegen = dlss and gpu_index = 1, the Intel one. With frame generation on, the device is made through Streamline's interposer, which asks it for VK_NV_low_latency2 and failed with VK_ERROR_EXTENSION_NOT_PRESENT, and nothing else was tried.
A chosen card of another vendor now counts as not usable while the interposer is in use, so the best usable card is taken, as for a chosen card that is gone. Where the card in use is still not an NVIDIA one, or the interposer fails to make the device for another reason, the loader makes it and frame generation is off for the run: the log says why and the settings menu shows it as unavailable, as it does when Streamline's files are missing.
Not run: no PC here has two cards of different vendors. The changed files compile.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
0.10.2 was to turn replay_pipe = 4 and replay_small = 16 on by default. A tester measured those two with five replay workers against the defaults on an RTX 3090, standing at eight places in Hyrule Field and at Lake Hylia with the default run repeated (within 3 %): slower at four of them (50.1 instead of 58.1 fps at one) and level or at most 7 % faster at the other four. The user decided to leave both at 0. CPU helper threads stay Automatic and Start next frame early stays on, as 0.10.1 shipped them; the notes and nx2navi.ini.example say so.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
auto chose full from 8,192 MiB of video memory budget, which is the cards of 10 GiB, and went by the budget alone. Two reports on 0.10.1 say that is too much. A tester filled a 24 GiB card to the room of an 8 GiB one and toured eleven areas at 2560x1440: with full, the driver counted more than the card had (the textures and the free space the allocator's blocks keep, which the pressure check leaves out), memory went to system memory, and the tour ran at 29.6 fps with a 1% low of 11.4; with reduced, 42.8 and 36.4, and nothing spilled. And a handheld's integrated GPU reported a budget of 10,361 MiB, which is a share of its 16 GiB of system memory: auto chose full and the game ended when a save was loaded, until its player set reduced.
Full now starts at 10,240 MiB of budget, the cards of 12 GiB (one reported 11,313 MiB in a player's log), and an integrated GPU gets reduced at most by auto. The log line says so when it applies, the settings menu's automatic entry names the level as before, and a level the player set stays.
Not run in the game. The file compiles and the content check passes.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
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 headroom thread compared a heap's use with the driver's budget alone, so a run with NX2NAVI_VRAM_BUDGET_MB kept its spare blocks where the card the cap stands for keeps none, and such a run understated a small card (the audit's F-3-07). With a cap set, the device-local use is now also compared with 85 % of the capped budget, the figure the mirrors and the textures go by. Without a cap nothing changes: each heap's own figure decides as before.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
A play session at 5120x2160 held 3 to 5 GiB free inside 140 to 180 of the allocator's 64 MiB blocks, 48 to 85 of them at most half full, and the "mem: holes" line said only that 1 to 18 of those could be emptied by moving textures. A second line now counts the other half-full blocks by what stays in them: a VRAM mirror, a texture whose content only the GPU made, a texture a shader sampled lately, or a texture that cannot move for another reason, each with the blocks that kind alone keeps. Those figures say which change would give the memory back: mirrors placed apart from textures, or a wider rule for what a compaction may move. The line is written with the holes line, every 3,600 frames, from the table that line already builds.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Main gained three commits after 0.10.1's release commit: a render target that was drawn over gives its image to the next target of its size, such an image is handed on after 8 presents and freed after 32 while the card is past its pressure share, and 0.10.1 kept CPU helper threads on Automatic and Start next frame early on. The settings keep this line's defaults (Helpers work ahead 4, Helpers take small jobs 16), the comments that said both were off in 0.10.1 now say what 0.10.1 shipped, the texture log keeps both sides' counters, and 0.10.2's notes say which two defaults are new.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The review found that dynres.c dynres_frame compared a changed target with a static that started at 0 and outlived a
restart: with fps_limit = display the first refresh-rate wander under 1 % after the start was logged, against the
comment's rule, and after dyn_res off and on a move back to the earlier run's last logged target said nothing, so the
start line and the live lines disagreed. A test of the menu's path found a third case: a limit or dyn_res_target
changed in the menu bumps cfg_version, dynres_read takes the new target first, and dynres_frame, which logged only
when its own reading differed from D.target_ns, never named it. The last logged target now lives in D (logged_ns),
the start line sets it, and dynres_frame logs any move of more than 1 % from it, naming the set target as "as set".
No lock and no wait; the target and the steps are what they were. The review's stale "(dyn_res_target 16.7 in
1..100)" in cfg.c misfit now reads 0..100, as the row's set has been since the commit that changed the default
target.
tests/dynres_check.py gains these cases (a menu limit, a menu target, a wander after the start, the earlier run's
target after off and on); they fail on the old dynres.c and pass. The -Wall compile of dynres.c and cfg.c shows no
warning, and cfg_rows_check reports 0 problems.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The review found that the header promised more than cfg.c does in two places. It said a blank then '#' or ';' after
any number starts a comment, but cfg.c cut_comment cuts only for a row with a set of values, so the settings that
take any number (gpu_index, compute_keep, tex_released_max, floor_probe, floor_scan) keep the comment in the value:
"compute_keep = 256 # less" reads the default 512. And it said a setting of 0 or 1 takes off, false and no, but
switch_word maps the words only for a row whose set is exactly 0|1; a setting with choices 0, 1 or 2 such as
async_preempt or tex_disuse reads "off" as its default, on, with no line in the log. The header now says the comment
is cut after the value of a setting with a list of choices or a range, names those five number settings among what
keeps its whole text, and gives the words to the settings whose only values are 0 and 1. docs/building.md's paragraph
on tests/cfg_parse_check.py now also names the low-end switch's case and make_game.py's tests added by the previous
commit. Comments and docs only; cfg_rows_check reports 0 problems.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The review of the commit that taught nx2navi.ini's readers off, false and no found three readers it left behind. cfg.c
lowend_of reads low_end_gpu through raw_get, which skips switch_word, and still took anything but "0" as on: with
"low_end_gpu = off" cfg_get, cfg_bool and the menu said off while cfg_low_end_active() was 1, so the mode's defaults
and its LOWEND_ONLY keys stayed in effect, and F-7's check, which now accepts the word, logged nothing. lowend_of now
takes the value through switch_of as cfg_bool does: 0 and the off words are off, anything else set is on (an
environment value "0 ; x" stays on, as cfg_bool reads it). tools/make_game.py read the file on its own too:
shader_carry took only "0" as off and kept a comment in the value, so with "shader_carry = off" or "shader_carry = 0 ;
note" an update skipped the full shader preparation and left the shaders to a game that no longer carries them over;
guest_opt_ini took low_end_gpu as on for anything but "" and "0". shader_carry now reads the switch as the game does
(the environment wins over the file and is taken as written, the file's last line with a value wins, a comment after
a blank is cut unless the value is in double quotes, make_game.py ini_choice, and 0, off, false and no in any case are
off); guest_opt_ini keeps its cut and takes the same words as off.
tests/cfg_parse_check gains a lowend case (low_end_gpu = Off, = 0 ; note and = yes from the file, FALSE and "0 ; x"
from the environment; two of them fail on the old lowend_of), and launcher_test's MakeGameTest two tests of
shader_carry's and guest_opt_ini's reading, which fail on the old make_game.py. cfg_rows_check reports 0 problems, and
the -Wall compile of cfg.c shows no warning.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A hand-edited "pace_smooth = 0 ; off" kept the comment in the value, which the readers took as on, "fps_limit = 144 #
monitor" gave 60, and "fullscreen = false" or "rtss = off" were on, since every reader took anything but "0" as on;
the log only named these lines (the check added for F-7). Now, for a key whose row has a set of values, cfg_load cuts
an unquoted value at the first blank followed by ';' or '#' after the '=' ("key = ; note" is unset) and logs the line
once; free text keeps its whole value (window_size, monitor, audio_device and keys without a row such as mods_dir,
pipeline_cache and the bindings, where a path or name may hold " #"), as do a value in double quotes and the
environment's values. A switch (a row whose set is 0|1) written as false, off or no reads as 0 and as true, on or yes
as 1 through cfg_get itself, so every reader agrees (cfg_bool, rt_knob, the readers that compare with "0" or parse a
number); cfg_bool and rt_knob also take those words for keys of other rows, and rt_knob no longer logs them. F-7's
check now names a switch value only when it is none of those, and a comment kept in a value only where it stays
(free text, the environment). The launcher's reader (launcher/gameini.py) cuts the same way for keys the caller
says have a set of values; its mods keys stay free text. The menu's save still writes a changed key's line again
without its comment.
This changes what existing files with such lines do, which is what was approved. tests/cfg_parse_check.py,
tests/rt_knob_check.py and launcher_test PlayerIniTest cover the cut, the words, free text, quotes and the
environment, and pass; cfg_rows_check reports 0 problems; the runtime compile check and the -Wall comparison of cfg.c
and perf.c show no new warnings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dynamic resolution's target was 16.7 ms unless set, whatever the frame limit, so under a 120 or 144 fps limit the GPU
could take up to 18 ms a frame at full size before a step down, and the game drew at about 55 fps instead of the
limit. Each step was held for 45 presented frames, 0.75 s at 60 fps but 0.19 s at 240, so the size changed four times
as often there. The target now defaults to 1000 / the rate the game draws at under the limit (frame_rate.c
frame_step_game_hz: the monitor's rate for display, the drawn frames with frame generation), read every frame so a
limit changed in the menu moves it, and 16.7 ms while that rate is unknown or unlimited; dyn_res_target set to 1 or
more still wins (its row's default is now 0). dyn_res_step keeps its key, range and default, now counted in 60ths of
a second of the clock, so 45 is 0.75 s at any frame rate and the same as before at 60 fps. The switch stays off by
default.
Not reproduced before the change (read from the code). The new tests/dynres_check.py builds dynres.c with stand-ins
and passes (24 failures on the old code); cfg_rows_check reports 0 problems; the runtime compile check and the -Wall
comparison of dynres.c show no new warnings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
shake_stick picked the side from the time since the press, 67 ms a side (two frames at the game's 30 fps), so above
30 fps each side lasted more of the game's updates: 8 at 120 fps and 16 at 240, slower swings than the game reads at
its own rate. The side now comes from the game's present count, which frame_rate.c already kept for the flight
recorder and now gives through frame_step_presents, two presents a side. The press still swings for 800 ms of the
clock and a hold keeps it going, as before; while no present comes (a load) the stick keeps its side.
Not reproduced before the change (read from the code). The new tests/shake_key_check.py builds shake_stick from
input.c and passes (it fails 118 times on the old code); the runtime compile check and the -Wall comparison of
input.c and frame_rate.c show no new warnings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The ocarina manager counts the length of each note and rest in whole 60 Hz frames and adds the time manager's frames
per update to that clock at four sites (main+0x32926c8, main+0x3293410, main+0x3296b08, main+0x3296da4) through
fcvtzu, which cuts the step toward zero. Above 60 fps the step is under one frame, so the clock never moved: a
recording kept its notes without their lengths (the recorder leaves a length of 0 out), the 360 and 120 frame limits
never came, and a recording ended after a fixed 60 frames of its timer instead of following the clock; at the 40 and
45 fps limits it counted one a frame. The set replaces each fcvtzu with nine words that add the fraction carried on
the patch page's +264, store what is left after the cut (NaN and negatives as 0) and convert the sum. Steps of one
and two frames give the game's count with the fraction left at 0, so 30 and 60 fps are unchanged. The words call
nothing and take no lock (one of the functions returns through an unsaved x30), and use only v16-v18, which the three
functions never name. The set is appended so that no existing set's index moves; ocarina_timing = 0 gives the game's
words.
Not reproduced before the change (read from the code). The generator's check of the original words passed against
the 1.1.1 main, the new tests/ocarina_timing_check.py passed (.venv, Unicorn), as did frame_step_check,
text_speed_check, sprint_check, movie_timeline_check, cfg_parse_check, launcher_test and cfg_rows_check.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The review found that audio_mixer.c splitters_walk used AUDIO_REFUSE for a framing fault in both of its walks. In the
apply walk (audio_mixer_update_routing, after audio_mixer_routing_check's walk passed) such a fault can only come from
the game rewriting its input during its own RequestUpdate. The update is taken by then: it is answered 0, its voices
and effects and the splitter records before the fault are applied, and the mix section is applied after it. Yet the
log said "RequestUpdate refused with 0x5299 ...; the renderer goes on with the last update it took". SPL_FAULT now
refuses the update in the check walk as before, and in the apply walk logs the fault with "the rest of the splitter
section is left out". The same double read of the mix section by audio_mixer_mix_layout_check and
audio_mixer_update_routing stays as it is; its comment now says the check holds for the apply only because the game
does not rewrite its input during its own RequestUpdate. docs/building.md's audio_params_check paragraph names the new
case and is wrapped again at 120 columns.
Not reproduced before the change: the game's sending thread waits in the IPC call, so neither case has a known way to
happen. A new case in tests/audio_params_check calls audio_mixer_update_routing with a splitter section whose first
destination record's magic is broken after a valid update. It checks that the splitter info before the fault and the
mix section are applied, that the destination record is not, and that the one log line names the rest of the section
left out. It failed on the code before this change ("RequestUpdate refused with 0x5299: splitter destination 0 ...
not SNDD"). audio_params_check (27 refused updates, 11 left-out records, 38 effects and sinks, both strict runs),
check_content, a compile of every runtime file and -Wall -Wextra on the audio files and cfg.c with no warnings passed.
Co-Authored-By: Claude Opus 5.5 <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 d2014496 found that vk_frame_hold_left loaded left_ns, last_ns and holding as three separate relaxed
loads while frame_hold_refill could store between them, so a reader on a replaying thread could pair a new left_ns
with an old last_ns and count one present interval twice (or, the other way round, not at all) for its decision, the
effect d2014496 meant to remove, now on the read side only; its comment presented the read as exact. Now
frame_hold_refill writes the pair between two steps of FH.seq (odd while it writes), and vk_frame_hold_left reads it
again when a read overlapped a refill. After FH_READ_TRIES such reads it goes with the last one, as vk_evo_peek.h
does, so a drawing thread never waits for the present; the comment says what such a read can get wrong. holding is
still stored after the refill, and a read between the two applies the previous frame's value to the microseconds
since that refill, which the comment says too. The decision is asked only for a full-screen draw whose pipeline is
still pending (vk_exec_draw.c).
Not reproduced before the change. Checks that passed: pass_cc_check of vk_backend.c with no new -Wall -Wextra
warnings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The review of cd2fd454 found that vk_sync.c submit calls vk_present_frames_owed before its replay timing starts
(t0 = perf_begin), so the owed bookkeeping a submitting thread runs was timed nowhere: a DLSS release in
vk_dlaa_frame (16 to 45 ms, F-4-02) would have shown as unattributed time in the frame-time and slow-frame lines, where
the design counts it as that thread's replay time. vk_present_frames_owed now times the frames it runs and adds them
with perf_replay. The present thread's catch-up before a blit (present_frame_closed) is not replay: it shows in the
present thread's own blit time and in the frame bookkeeping line.
Checks that passed: pass_cc_check of vk_backend.c with no new -Wall -Wextra warnings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The review of 6a0f123a found that nvn_submit_prepare took the same handle given twice in one nvnQueueSubmitCommands
call for another thread's submission in flight: the first occurrence takes the copy's busy count from 0 to 1 and
resolves in place, so nvn_copy_enter returns 0 for the second, and unless the copy was known to hold no deferred binds
(deferred 1; it is 0 at the first submission and 2 for any copy with deferred binds) the call cloned it, a malloc and
copy of up to 1-2 MiB and a pin walk under copy_lock shared. It also logged "thread N submits the recording ... while
another thread's submission of it is replayed", the line the play-test reads to tell whether F-2-03's remainder
happens in this game at all, so a call that repeats a handle would have given a false positive. The asynchronous path
already keeps such a repeat in place (nvn_submit_prepare_async: used_at != now).
Now runtime/nvn_submit_guard.h nvn_copy_again says whether the handle came earlier in the same call and resolves in
place there; nvn_submit_prepare asks it only after nvn_copy_enter found the copy busy and the copy may hold deferred
binds, so only that branch pays for the scan. Such an occurrence stays in place and keeps its busy count, which
nvn_submit_done leaves per occurrence as before. Another thread that submits the copy meanwhile still takes a clone.
tests/submit_clone_check gives the copy twice in a quarter of its calls (76,759 repeats kept in place in 800,000 calls,
no replay saw its handles change, the counts end at 0) and checks nvn_copy_again step by step: given twice with
nothing else in flight, in place both times; with another submission in flight, cloned both times.
docs/building.md says so.
Not reproduced before the change. Checks that passed: submit_clone_check, pass_cc_check of nvn_api_cmd.c and
nvn_api_core.c with no new -Wall -Wextra warnings.
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>
vk_sub_in_frame says which of its frame's submissions the one being recorded is; vk_memory.c sub_place files the mirror
pre-snapshot's statistics by it during the replay, on the replaying thread and its workers. vk_be_submit wrote it on
every submitting thread before submit_sync, outside sub_lock, so with the replay on the submitting threads another
thread entering a submit changed it under a replay in progress, and the replay filed its work at the other
submission's place. Each thread now notes the place in a thread-local (vk_be_submit, and replay_main for the
asynchronous replay), and vk_sync.c submit publishes it right after taking the lock for a submission with commands
(vk_sub_in_frame_publish, a weak no-op without the back end). Submissions without commands of their own (held work, the
present thread's blit) leave it as it was, as before. Performance heuristics only; no timing change.
Not reproduced before the change. Checks that passed: pass_cc_check on every runtime file (a header 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 changes leave three numbers a play-test needs: how many presents left their frame's bookkeeping to the
next submission because another thread held the submission lock (fo_owed_n) and how many frames a submission caught
up (fo_late_n); how many held presents another thread's submission put out (ph_out compares the thread with the one
that presented, now kept in PhJob.tid); and how often the presenting thread waited for PH.lock while one went out,
with the time in all and the longest (vk_present_held.h's counts). vk_backend.c order_log writes them on one line:
with the synchronous replay at exit (gfx_exit_log, "vk: over the run to frame N: frame bookkeeping: ..."), and with
NX2NAVI_TRACE=perf at every 600th present when a count moved since the last such line ("perf: since the last such
line, at frame N: ..."). With present_order = 0 the bookkeeping counts stay 0. The presenting thread's wait is also in
the slow-frame lines' "drain" since the lock commit. No timing change.
Checks that passed: pass_cc_check of vk_backend.c with no new -Wall -Wextra warnings, present_held_check,
perf_summary_check.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nvnQueueSubmitCommands resolves a copy's deferred binds at the submit call: the handles the game's arrays hold now are
written into the recording copy (nvn_api_cmd.c resolve_deferred). With the replay on the submitting thread, a second
guest thread that submitted the same copy while the first one replayed it, or while the first copied it for a held
continuation (vk_exec.c cont_new), rewrote those handles under that replay: the rest of it bound the next frame's
textures and samplers, and a held continuation could be copied torn (F-2-03's part left after the private copies).
Now each copy counts the synchronous submissions between their prepare and the end of their replay (copy_prefix.busy,
which shares the 8 bytes pins had, so the prefix keeps its 56 bytes). nvn_submit_prepare takes copy_lock shared once
for all handles: the first submission in flight resolves the copy in place, as before; one that finds another in
flight, unless the copy is known to hold no deferred binds, resolves a clone made under that lock, with its texture
pins counted, and nvn_submit_done frees it after the replay, as the asynchronous path does for a copy a queued
submission still reads. A copy the game releases while a submission of it is in flight becomes a zombie and is freed at
the next sweep after it ends (release_copies, zombie_sweep). The bind loop moved unchanged into
runtime/nvn_submit_guard.h (nvn_resolve_binds) so a check can build it; it reports a corrupt record or a call past 16
deep to resolve_deferred, which ends the run with the messages it had, and keeps the first six changes' trace line.
Being code the front end carries under its MIT header, the new header keeps that line and THIRD_PARTY_NOTICES.md's row
names it. A clone is logged the first four times and at each power of two ("submits the recording ... while another
thread's submission of it is replayed"). No timing change and no wait: the clone path runs only when two threads have
one copy in flight at once.
Not reproduced before the change; nobody knows yet whether the game ever submits one copy from two threads at once (the
new line will say). Checks that passed: the new tests/submit_clone_check (four threads submitting two copies 200,000
times each while their arrays change: no replay saw its handles change, both paths ran, the counts ended at 0; the
unguarded 0.10.1 shape through the same harness: about 450,000 of 800,000 replays saw them change; nvn_resolve_binds
step by step), copy_sweep_check (its stand-in prefix follows the new layout), pass_cc_check on every runtime file, no new
-Wall -Wextra warnings in nvn_api_cmd.c and nvn_api_core.c.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
present_image ran the frame's bookkeeping on the presenting thread without a lock: vk_exec_frame_log (the trace counts
and vk_exec_cond_frame_end's frame count, which the conditional draws go by), dynres_frame, vk_exec_barrier_log,
vk_dlaa_frame (the DLSS mode and its LOD bias, the frame's points, the swap image, the idle releases), vk_mvsurvey_frame,
vk_mvprev_frame (which swaps the two motion tables the draws insert into) and vk_gpu_passes_frame. All of it is state the
replay reads and writes as it records, under sub_lock. With the replay on the submitting threads the game presents on
one thread while another may be replaying (the overlap lines of the 0.10.0 fresh session, at area changes), so the
present reset it in the middle of that replay: a DLSS frame reset halfway, the motion tables swapped halfway (with
MVPREV_CHECK=1 an index out of bounds), the conditional draws counted against the next frame.
Now the bookkeeping is frame_end, and runtime/vk_frame_owed.h decides where it runs. present_image marks its frame owed
and runs it itself only when it gets sub_lock at once (vk_sync.c vk_sync_try_locked); otherwise vk_sync.c submit runs
every owed frame's, in frame order, right after it takes the lock and before its replay (vk_present_frames_owed), on
whichever thread submits next, the present thread's blit submission included. The presenting thread never waits for
another thread's replay for it: no new wait on a drawing thread; the thread that takes the lock next does the work it
would have done at the present, tens of microseconds normally. The hand-off, vk_texture_get with its upload flush and
vk_present_drain stay outside the lock, as frame_end must not wait for the present thread or submit; its callees were
checked for that. vk_sampler_aniso_live and vk_mem_census keep their place in present_image (their own locks). In the
asynchronous replay the other holder of sub_lock is the present thread's blit, and the same applies.
NX2NAVI_PRESENT_ORDER (present_order in [diagnostics], default 1, read at start) puts it back at the present with 0, as
in 0.10.1: metas, known and sections rows and an nx2navi.ini.example entry.
Not reproduced before the change. Checks that passed: the new tests/frame_owed_check (one presenting and three
replaying threads, some replays holding the lock 40 ms, over 20,000 frames: every frame's bookkeeping once, in order,
never during a replay and before any replay that took the lock after its present; the longest present under 1 ms with
19,000 presents made during a long hold), cfg_rows_check with 0 problems, pass_cc_check on every runtime file (headers
changed), no new -Wall -Wextra warnings in vk_backend.c, vk_sync.c, vk_exec.c and cfg.c.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With the replay on the submitting thread, a present that waits for event-ordered work (vk_backend.c PH) goes out after
the submission that replays the last of that work, at the next present, or after PH_MAX submissions. The game submits
from several threads while one of them presents, so that submission may be another thread's. PH had no lock: two
submitting threads could both pass ph_flush's check of PH.on and put one frame out twice; a thread reading PH's fields
while the presenting thread armed the next frame could put out a job mixed from two frames; and a held frame put out on
a submitting thread could reach the present thread after the presenting thread's next frame, a picture back in time.
runtime/vk_present_held.h now holds PH's job, its arming, putting out and counts behind PH.lock. Submitting threads
only try the lock (ph_flush_try) and go on when another thread holds it: that thread is putting a present out or arming
one, and the next submission, the next present or PH_MAX checks again, the bound there was before. The presenting
thread takes it for every synchronous present while held presents are on (ph_flush_next, ph_present), so the
synchronous present_image calls run one at a time and in the game's order. That is the one new wait, and only the
presenting thread makes it: only while another thread puts a held present out, for at most that present_image. It is
counted (lock_waits, lock_wait_ns, lock_wait_max) and timed as PERF_DRAIN. No path holds the lock across held_resume,
which is a replay. Neither 0.10.0 fresh log has a held present at all ("went out" and "presents that waited" lines),
so the wait is not expected in normal play. present_held = 0 turns held presents off, and with them every use of the
lock.
Not reproduced before the change. Checks that passed: the new tests/present_held_check (one presenting and three
submitting threads over 200,000 frames: every frame out once, in order and never two at once; the unlocked 0.10.1
shape through the same harness gave about 205,000 presents for 200,000 frames, 6,000 to 8,000 out of order or twice and
8,000 to 12,000 at the same time; step-by-step cases; a submitting thread gives up on a held lock at once and the
presenting thread's wait is counted), pass_cc_check on every runtime file, no new -Wall -Wextra warnings in
vk_backend.c.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nvnQueueSubmitCommands' q->submits++, vk_backend.c synced_held_count's two counts and vk_exec.c vk_exec_gate_wait's
window (GW) run on the calling guest thread without a lock, and the game submits and waits from several threads (five
in the 0.10.0 fresh session). A plain ++ there loses counts, and two threads could both reach a window's end and log
it, or reset it under each other. Now each count is an atomic add; the line is logged by the thread whose add reached
it, and the gate's window is taken with exchanges by the thread whose add made it 600, so adds that come in meanwhile
count in that window or the next. Nothing reads q->submits today; the counts only feed log lines.
Not reproduced before the change. Checks that passed: pass_cc_check of vk_exec.c, vk_backend.c and nvn_api_core.c with
no new -Wall -Wextra warnings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
setting_presents says how many more presents a new scene size counts as the result of a settings change (the resize
hold of vk_backend.c frame_hold). vk_dlaa_frame sets and counts it down, and vk_dlaa_resized_take tests and clears it
at the same present. With the replay on the submitting threads the frame's bookkeeping, vk_dlaa_frame among it, can run
on another thread than the presenting one (another thread's replay, or the next submission once the bookkeeping goes in
replay order), so a plain test-then-clear beside a plain decrement could take the hold twice or miss it. Now the take is
two exchanges, which give the outcome of the old two statements, and the count-down is a compare-and-swap that stops at
0, so it never goes below a clear made meanwhile.
Not reproduced before the change. Checks that passed: pass_cc_check of vk_dlaa.c with no new -Wall -Wextra warnings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
pending_draws (vk_exec_draw.c) counts the draws a frame left out because their shader or pipeline was still building;
vk_backend.c frame_hold takes it at the present to hold such a frame back. The comment said both happen on the replay
thread, which is true only with the replay on its own thread. With the replay on the submitting threads, draw_skipped
adds on the thread holding the submission lock (and its parallel chunks) while vk_exec_pending_draws_take reads and
clears it on the presenting thread, so a plain ++ and a read-then-clear could lose a count: a frame missing draws shown
instead of held, or the next frame held for nothing. The add, the take (an exchange) and the read that skips a link
wait are atomics now.
Not reproduced before the change. Checks that passed: pass_cc_check of vk_exec_draw.c with no new -Wall -Wextra
warnings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The replay asks whether a frame that skips a draw would be held (vk_exec_draw.c, vk_frame_hold_left) on whatever
thread holds the submission lock, and with the replay on the submitting thread that need not be the thread that
presents. vk_frame_hold_left refilled FH itself (frame_hold_refill), so a refill on a replaying thread beside the one in
frame_hold on the presenting thread wrote left_ns and last_ns from two threads: an interval counted twice or not at all,
which decides whether a frame with skipped draws is held. Now the reader computes the same arithmetic from the stored
fields (fh_left_at) and writes nothing; the limit is read once with a compare-and-swap whose winner logs it; left_ns,
last_ns and holding are stored and loaded as atomics. Refilling in two steps or one gives the same allowance, so the
single-thread case is unchanged.
Not reproduced before the change. Checks that passed: pass_cc_check of vk_backend.c with no new -Wall -Wextra
warnings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
vk_exec_held_ready runs without sub_lock (vk_backend.c held_resume, submit_sync). It decided from the published copy
since 688b2506, but tested the wait with vk_exec_event_met, which compares at the copy point of the submission being
recorded (vk_exec_xfer.c ev_mem, ev_sync_seq). That point is set by whichever thread holds the lock, so a thread asking
while another one recorded tested "at sync" against that other submission's copies. Its question is whether held work
can go on in a submission of its own, which takes its copies later: vk_exec_event_met_now tests the value in memory now
or the signals replayed so far. Under the lock nothing changes: evo_resume and cont_ready keep the copy-point test, and
vk_exec_record asks right after its own copy point, where the two agree.
evo_pending and vk_exec_submit_seq are written under the lock and were read as plain values by vk_exec_held,
vk_exec_held_ready and the held present's since and PH_MAX checks; they are now stored and loaded as atomics
(vk_exec_submit_seq_now), so the compiler cannot keep a stale copy in an unlocked reader.
Not reproduced before the change. Checks that passed: pass_cc_check on every runtime file (a header changed), no new
-Wall -Wextra warnings in vk_exec.c, vk_exec_xfer.c and vk_backend.c, evo_peek_check, target_frames_check and
oq_hold_check.
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>
The review of 9071dfc3 found that release_kept counted the features "released once the GPU is past their last frame"
as every DLSS feature left in the list of what was let go of, capped at the call's count, on the claim that older
entries had older fences and went first. That does not hold: an entry let go of earlier can have a later fence than
this call's (a size evaluated in a later submission and then freed), so the line could credit an earlier let-go to
this call. release_kept now keeps the handles of the features it lets go of and counts those still in the list after
vk_upscaler_let_go: a feature goes into the list once and leaves it only through its release, and no feature is made in
between, so such a handle is one of this call's. With upscaler_idle_wait = 1 that is 0 unless a feature was evaluated
in the submission being recorded, which no wait can pass yet. Only the log line changes.
Checks that passed: vk_dlaa.c compiles with -Wall -Wextra and no warning, upscaler_idle_check,
tools/check_content.py --staged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The review of 9071dfc3 found the comment stale: it said the new prebloom images were left at PB.used 0 by a clear of
the old ones, but retire_pb now keeps PB.used, and the PB.used = D.presents at the top of pb_images already covers the
new images on that path. The assignment after the images are made stays as the guard against any clear of PB before
it (pb_destroy would zero PB.used), which upscaler_idle_check holds. The comment now says that. A comment only: the
code is unchanged.
The review also noted that 77756b21 alone left a window between pb_destroy and that assignment in which PB.scene was
set with PB.used 0; 9071dfc3, where retire_pb takes pb_destroy's place on that path, closes it, so nothing changes
here for it.
Checks that passed: upscaler_idle_check, tools/check_content.py --staged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The review of 9071dfc3 found that PERF_M_UPWAIT, printed as "upscaler device waits", times two waits: vk_dlaa.c
vk_upscaler_device_idle's wait for the device, and vk_upscaler_gpu_wait's host wait on the timeline after a refused
DLSS or FSR creation, which touches no queue. host.h tied the mark to the first alone, and a play-test that expects no
"upscaler device waits" would have read the second, at a refused creation on a card short of video memory, as a wait
for the device. perf.c now names the mark "upscaler GPU waits", and host.h, vk_ffx.h and vk_dlaa.c say that both waits
are in it. What makes it a wait for the device still shows in the log: the rollback switch in the "cfg: ... sets:"
line, a full list ("a wait for the device") and a failed timeline wait ("waiting for the device instead") in lines of
their own.
Checks that passed: perf.c, vk_dlaa.c and vk_ffx.cpp compile with -Wall -Wextra and no warning, upscaler_idle_check (one
name per mark kind), tools/check_content.py --staged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The review of 2b9b3061 found that the check missed the dangerous regression. It only checked that each place that lets
something go calls vk_upscaler_let_go, so with destroy_size put back in vk_dlaa.c free_size, or hl_destroy in
hudless_from's size change, it still passed, while either frees images or a DLSS feature a frame still in flight uses.
The check now also fails when:
- a function that lets go of what the GPU may still use (vk_dlaa.c free_size, free_snap, stale_sizes, release_kept,
retire_idle, evaluate and the retire_size, retire_feat, retire_snap, retire_hl and retire_pb helpers; vk_ffx.cpp
context_for, trim, retire_up, vk_ffx_trim_idle and vk_ffx_release_idle; vk_ffx4.cpp context_for, vk_ffx4_trim,
retire_up and vk_ffx4_release_idle) calls a destroy that frees at once (destroy_size, destroy_snap, hl_destroy,
pb_destroy, feat_release, N.release, vkDestroyImageView, vmaDestroyImage; A.destroy in the FSR files);
- hudless_from, pb_images or snap_for call one of those before their first image2d call (after it, only their half-made
new images are destroyed);
- upd_take_all, which releases a whole list whatever its gates, is called outside teardown (vk_dlaa_release,
vk_ffx_release, vk_ffx4_release) and a full list's fallback (dead_put, the FSR files' retire_up), or there before
vk_upscaler_settle's wait for the device.
docs/building.md "Checks" says so.
Each of 19 mutations of scratch copies fails the check (destroy_size in free_size and retire_idle, hl_destroy in
hudless_from, pb_destroy in pb_images, destroy_snap in free_snap and snap_for, feat_release in release_kept, N.release in
stale_sizes, vmaDestroyImage in retire_size, upd_take_all in retire_hl, A.destroy in FSR 3.1's retire_up and in either
file's trim and release_idle, dead_put and FSR 4's retire_up without the settle, FSR 3.1 and FSR 4 without the retry);
the unchanged
sources pass, and stability-pass's sources fail with 46 problems. tools/check_content.py --staged passes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>