mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
main
156 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
712568d4fb |
Make the release 0.11.0: the release prepared and previewed as 0.10.2 takes this number, with its notes.
The user asked for the release to go out as 0.11.0. No final 0.10.2 was published: only the previews 0.10.2-rc1, -rc3 and -rc4 carry that name, and the launcher counts 0.11.0 as newer than each of them, so testers are offered it on either update channel. VERSION holds 0.11.0 and the notes move to docs/releases/v0.11.0.md, with a line for those on a preview. Every line that named 0.10.2 as the release in which something changed names 0.11.0 now: comments in the runtime, the two log lines that give a version (the removed low-end GPU mode in cfg.c, the page generations in vk_memory.c), nx2navi.ini.example, docs/building.md, the labels of tests/tex_release_check.c and a comment of tests/audio_params_check.c. AUDIT.md names the release 0.11.0 throughout and records the earlier name and the previews once, in its status line. Nothing else changes. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com> |
||
|
|
7a8bbad47e |
Remove the low-end GPU mode from the settings.
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> |
||
|
|
1ae00ed802 |
Log dynamic resolution's target against the one the log last named, from the start line on.
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> |
||
|
|
4fe1339189 |
Say in nx2navi.ini.example's header which settings cut a comment after the value and which take the switch words.
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> |
||
|
|
b6709da852 |
Read a comment after a setting's value, and off, false and no for a switch, as players mean them in nx2navi.ini.
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>
|
||
|
|
1c26907abe |
Aim dynamic resolution at the frame limit's frame time by default and hold each step for a time, not a frame count.
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> |
||
|
|
5c9e480f90 |
Swing the shake key's stick by the game's updates instead of the clock, two updates a side at any frame rate.
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> |
||
|
|
5f9374e1d9 |
Keep the fraction of a frame the ocarina recorder's clock was losing above 60 fps, in a new patch set orecord.
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> |
||
|
|
a40afdd86d |
Log a splitter framing fault met while the update is applied as the rest of the section left out, not as a refused update.
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>
|
||
|
|
9770d409d5 |
Say what NX2NAVI_AUDIO_STRICT, the audio_bad log lines and the docs really do, as the review of the audio change found them overstated.
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>
|
||
|
|
7e7fed639e |
Refuse or leave out the audio values the game sends that the renderer cannot use, instead of ending the game on them.
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> |
||
|
|
b0baa981ff |
Keep a recording copy that one synchronous submit call gives twice in place both times, instead of cloning the second occurrence as if another thread's submission were in flight.
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> |
||
|
|
618cfc01f4 |
Run a frame's bookkeeping that is still owed on the present thread before the frame's blit, so the blit never reads the upscaler state of a frame vk_dlaa_frame has not closed.
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> |
||
|
|
58fad9128d |
Give a synchronous submission of a recording copy that another thread's submission still replays a clone of its own for its deferred binds, instead of rewriting the copy under that replay.
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>
|
||
|
|
51c9261db3 |
Close a frame's replay state under the submission lock: at the present when the lock is free, else first thing in the next submission, so another thread's replay never sees it reset halfway.
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> |
||
|
|
a789bd8fb0 |
Put a held present out under a lock of its own that submitting threads only try, so that it goes out once, whole and before the next present.
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>
|
||
|
|
4506988335 |
Make upscaler_idle_check fail where a place that lets something go frees it at once.
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> |
||
|
|
61f08a177d |
Try an FSR 3.1 context creation once more after the GPU is past the contexts let go of, instead of failing the run.
The review of 09027478 found that FSR 3.1 lost its fallback for a refused creation. Up to 0.10.1, vk_ffx.cpp context_for waited for the device and destroyed the other render size's or the least recently used context before create_counted, so their video memory was free for the new one. Since 09027478 they only go into g_dead, and the drain in vk_upscaler_let_go destroys those whose last upscale the GPU is past. When it is still behind and the library refuses the new context for want of memory, context_for returned nullptr, vk_ffx_upscale 0, and evaluate's dl_fail turned DLAA off for the run. DLSS (evaluate) and FSR 4 (context_for) already wait for the GPU to pass what they let go of and try again. context_for now does the same when the creation is refused and FSR 3.1's or FSR 4's list of contexts let go of is not empty: vk_upscaler_gpu_wait (a timeline wait for those submissions, not the device's idle; the device with upscaler_idle_wait = 1), vk_ffx_dead_take, and a second create_counted when something was destroyed. A line says the refusal, the wait and the outcome. upscaler_idle_check now also fails when vk_dlaa.c evaluate or vk_ffx.cpp or vk_ffx4.cpp context_for has no vk_upscaler_gpu_wait (a scratch copy without the retry fails it), and docs/building.md says so. Not reproduced before the change (found by review). Checks that passed: vk_ffx.cpp 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> |
||
|
|
ed87d30628 |
Check from the sources that no upscaler waits for the device where it lets something go.
tests/upscaler_idle_check.py reads vk_dlaa.c, vk_ffx.cpp and vk_ffx4.cpp with their comments and strings blanked and
fails when vkDeviceWaitIdle is called outside vk_upscaler_device_idle and the --probe-ffx-cache path, when
vk_upscaler_device_idle is called outside vk_upscaler_settle and vk_upscaler_gpu_wait, or when one of the ten places
that let something go does not call vk_upscaler_let_go, through which NX2NAVI_UPSCALER_IDLE_WAIT=1 brings the old wait
back. It also checks that switch's sections[] entry and nx2navi.ini.example line (cfg_rows_check has its known[] name
and metas[] row), that perf.c names every PERF_M_* kind (a kind without a name prints a null string on a slow frame's
line), and that pb_images notes PB.used after the last call that clears PB.
On this branch it passes. On the base of this branch's work (stability-pass,
|
||
|
|
86e6d62d35 |
Add a list of what an upscaler lets go of, released once the GPU is past it, and its unit check.
runtime/vk_up_defer.h holds, per owner, a DLSS feature, an FSR context or an image frame generation can still read, each with the timeline value of the last submission that named it and an optional present count. upd_take releases, in the order they were put, the entries whose value the GPU has reached and whose present count has passed; nothing in it waits or locks, so the owner checks at its own calls on the thread that makes its other upscaler calls. It is the piece the next commits use to take the upscalers' device-idle waits off the replaying thread: those waits stall the present's flip, the completion thread and upload submits with the queue lock held, at every pause-screen size, reload, window size or mode change. Nothing uses it yet. tests/up_defer_check.py builds tests/up_defer_check.c with the header as C and as C++ (vk_ffx.cpp and vk_ffx4.cpp include it) with -Wall -Wextra -Werror and checks both gates, the order, a full list, a null handle and a release that puts. It passed, and failed with each of three faults put into a copy of the header. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
a8d18ef474 |
Ask an armed region's page state under the freeze lock when a fault finds its range gone, so that a late write is not taken for a frozen page's and a read is not passed on past an arm.
The review found two windows in memory.c lazy_fault, where a fault holding frz_lock and lz_lock finds its address neither armed nor in flight. It returned 0 there, and access_fault then asked for the page's state without a lock. A write whose range a flight gave back while the write waited for frz_lock went on to thaw_fault, whose thaw released every armed range of the 2 MiB region (lazy_drop_region_locked), each fetched under the lock. And after a release that sets no hold (mem_lazy_skip's give-back of another buffer's range), the completion thread could arm the page again between that 0 and access_fault's query, so a read saw an inaccessible page and went to the crash handler; that one predates the ranges in flight. Now a fault that finds a flight there waits for it as before. Otherwise it releases lz_lock, asks for the page's state while it still holds frz_lock, under which no arm runs, and runs the access again when the page allows it: a read on a readable page, a write on a read-write one. A write to a frozen page still goes on to thaw_fault, and an address that is not ours to the crash handler. The new late_write scenario of tests/lazy_fetch_check.py has a thread write a range whose release starts and ends while the write waits for frz_lock: the write runs again and the region's other range stays armed. With the old return of 0 it fails, the other range released. The read's window has no check: the arm would have to land between two instructions. Not reproduced in the game before the change; lazy_fetch_check passed in all its scenarios. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
da60e0d327 |
Make wb_lazy_unlocked = 0 bring back 0.10.1's locking in evictions and releases as well, which the review found it did not.
With the setting at 0, frz_lock was held across every fault's and release's fetch again and mem_thaw released under the lock, but two parts of the new code stayed. vk_memory.c mirror_evict and hot_check still fetched their victims' armed ranges before taking pool_lock again, and a release still put up to 16 copies in one submission. Those are the parts that run only with a device and have no check without one, so a device loss caused by them was the one case the switch could not take back. Now vk_memory.c reads the setting too (evict_unlocked, once on the replay thread, where both run): at 0 neither drops pool_lock, and the release under it fetches everything, victim by victim, as before. And memory.c lazy_take_batch_locked takes one segment at a time at 0, so each is fetched on its own. What stays at 0 is the table of ranges in flight: a touch of a range being fetched waits for that fetch and runs again (a write no longer goes on to thaw_fault and releases its region), the hold is set as the range goes out, and mem_lazy_touches counts the range. memory.c "Ranges in flight", nx2navi.ini.example and docs/building.md say so. tests/lazy_fetch_check.py gains locked_release, the release scenario at 0: three fetches of one copy each, where the batched release makes two of up to two copies. It fails with the cap taken out. The eviction's side has a compile check only. Not reproduced before the change; lazy_fetch_check passed in all its scenarios. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
0eee849948 |
Check that a thaw fetches the armed ranges of its regions without the freeze lock, which the review found untested.
memory.c mem_thaw calls lazy_release_regions before it takes frz_lock, so the copy a file read's thaw needs (the buffer's armed ranges with stale bytes) no longer holds up the other threads' faults, freezes, thaws and arms. All 20 scenarios of tests/lazy_fetch_check.py passed with that call taken out: the thaw scenario reaches the same end state through thaw_begin_locked's lazy_drop_region_locked under the lock. The new thaw_unlocked scenario has a thread thaw a page of a range armed with stale bytes while the fetch takes 300 ms, and freezes another region meanwhile: the freeze returns at once, and took 300 ms with the call taken out. locked_thaw runs it with NX2NAVI_WB_LAZY_UNLOCKED=0, where the freeze waits for the fetch as in 0.10.1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
92121a11e8 |
Check that an arm keeps its region marked while a range in flight there ends, which the review found untested.
memory.c mem_lazy_arm marks the region it protects in lz_arm_r, so that lazy_flags_locked keeps lzr set when a release's flight in the same region ends during the arm's clean test. Without that mark the flight's end cleared lzr, lazy_fault let a read of the arming range go, and access_fault passed it to the crash handler. All 20 scenarios of tests/lazy_fetch_check.py passed with the mark taken out. The new arm_flight scenario has a release fetch one range for 100 ms, an arm of another range of the region wait 400 ms in its clean test, and a read of that range fault after the flight ended: it passes, and exits with an access violation when lazy_flags_locked ignores lz_arm_r. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
2e077edc9d |
Fetch the lazy write-back's ranges without the freeze lock, so a touch of GPU-written memory holds up only the threads that touch that range, with wb_lazy_unlocked = 0 to go back.
A CPU touch of an armed range ran its fetch (vk_memory.c lazy_fetch: a GPU copy after everything handed over, and a wait of up to 10 s) inside the fault handler under frz_lock. Every other fault, thaw, freeze and arm waited for the whole copy meanwhile: the replay's copies of guest memory (mem_copy_readable, vk_memory.c mirror_capture) and its freeze pass, the completion thread's arms, file reads and pool finalizes. A write that lost the race to the fetch went to thaw_fault after it and released every other armed range of its 2 MiB region, a second fetch. mirror_evict and hot_check fetched their victims' ranges under the exclusive pool_lock, which the completion thread's arms and every pool lookup wait for. This is AUDIT F-4-05 (C-12); the design's probe of the unchanged code had an unrelated mem_copy_readable, arm and freeze each wait 299 ms behind a 300 ms fetch, and the losing write 378.8 ms with two fetches. A range now leaves the armed list into a table of ranges in flight (memory.c "Ranges in flight", lz_fly) in the same lz_lock hold, and stays there, its pages inaccessible, until its bytes are down and its pages read-write again (lazy_fly_run), without frz_lock. A fault on a range in flight waits for that entry alone on lz_lock's condition and runs the access again, the losing write included; a thaw, a mapping change, a release of its buffer, an arm, a freeze and mem_lazy_touches see it. Every store to lzr is made under lz_lock from the list, the entries and the region an arm is protecting (lz_arm_r, which replaces the previous commit's restore). An entry starts under frz_lock and lz_lock and ends under lz_lock alone, so a holder of frz_lock may wait for one: its fetch takes only the queue lock, which no frz_lock taker holds. mem_thaw releases what is armed in its regions before it takes frz_lock (lazy_release_regions), so file reads and pool finalizes fetch without it; a mapping change's thaw still fetches under it, as its page calls follow. The holds a fault sets (lz_hot, lz_hots) are now set as the range goes into flight, so an arm meanwhile finds them. The fetch takes several copies in one submission and one wait (MemLazyCopy), and the back end has four slots of command pool, command buffer and fence, so fetches of different ranges run at once; a release takes up to 16 armed ranges out together. mirror_evict and hot_check now fetch what only their victims' mirrors hold before taking pool_lock exclusively, and then, under the lock, release only what was armed since (usually nothing) before the mirror leaves the list and the device address table names the imported buffer again. The design moved the release after the lock instead, which would have had the table change before the fetch: a shader of a submission still running reads through the table as it changes, and the fetch waits for every submission handed over. A victim that vk_pool_fini took off the list meanwhile is left to it; hot_check copies its log line's fields under the lock, as its records may be freed once the lock drops. The new setting wb_lazy_unlocked (NX2NAVI_WB_LAZY_UNLOCKED, default 1, restart) set to 0 keeps frz_lock from the take-out to the end of each flight and leaves mem_thaw's releases to the thaw, as in 0.10.1; the table, the slots and the eviction order stay. The "mem: lazy write-back" line counts segments fetched; a second line gives the touches that waited for a range in flight, the longest such wait and the most ranges in flight at once; "mem: sizes" counts fetches that found every slot busy. The frz_lock and lz_lock comments say what holds what now (B-12). mem_lazy_touches has callers outside memory.c (camera_speed.c, nvn_api_cmd.c ctl_fingerprint), which now skip a range in flight instead of faulting on it and waiting. Not reproduced in the game before the change. tests\lazy_fetch_check.py now runs 20 scenarios as their own processes: arm races, no stall of freezes, copies, arms, thaws and releases of other ranges during a fetch, joins with frz_lock held elsewhere, a losing write that keeps the region's other range armed, two fetches at once, a thaw, a mapping change and a release meeting a range in flight, an arm refused over one, the foreign give-back, the chunk mode, the old wait with wb_lazy_unlocked = 0 (joins and losing writes as well), the report lines, 80 touching threads past the 64 entries, and a freeze refused; all pass, in six runs. Taking the join before frz_lock out fails the join scenario. cfg_rows_check reports 0 problems; every runtime file compiles, and memory.c and vk_memory.c add no -Wall -Wextra warning; the test matrix passes 75 of 77, installer_test and worktree_test failing for the environment as before. vk_memory.c's slots and the eviction's order have no check without a device. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
f586c291fa |
Give the longest lazy fetch and the longest wait of a fault for the freeze lock on the "mem: lazy write-back" line.
A fetch of lazily written pages runs inside the fault handler under frz_lock (memory.c lazy_gone_locked and lazy_piece_fetch_locked call vk_memory.c lazy_fetch, a GPU copy after everything handed over and a wait for it), so every other fault, thaw, freeze and arm waits for the whole copy. The line gave only the fetches' sum: the 0.10.0 session's 848 fetches took 415.8 ms in all, and how long one took, or how long another thread waited behind it, was not known. Both fetch sites now keep the longest fetch, and lazy_fault keeps the longest time a fault waited to take frz_lock; the report line prints them as "the longest X ms, faults waited up to Y ms for the freeze lock" and starts both over. No tool parses the line. tests\lazy_fetch_check.py's new report scenario fetches for 30 and 120 ms and has a third fault wait 60 ms for frz_lock: the line reads 120.29 ms and 60.03 ms, and the next one 0.00 for both. arm_race, arm_race_slow, wb_lazy_check and heap_lazy_copy_check passed; memory.c compiles without a warning under -Wall -Wextra. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
ef0673fba0 |
Mark a region for the lazy write-back before its pages become inaccessible, so that a read during an arm is not passed to the crash handler.
mem_lazy_arm made a range's pages inaccessible (lazy_protect_locked: the protect, then the back end's clean test) and set the region's lzr mark only after the segment was in the list. lazy_fault returns 0 at once for an unmarked region, without taking frz_lock, so a CPU read in that window found the page inaccessible in access_fault and went to the crash handler as an access violation. A write survived only because thaw_fault waits for frz_lock. Arms come from the completion thread (vk_memory.c vk_gpu_written_back) when a submission's fence completes, which is when the game's own fence wait returns and it reads the results; the window lasts as long as the protect and the clean test, which can take tens of milliseconds while the driver pins memory. The mark is now set before the protect, after the cheap refusals, and put back when the protect or the clean test refuses the arm. A fault from the protect on finds the mark, waits for frz_lock and then finds the segment, or the pages given back. Every store to lzr is made under frz_lock, which the arm holds, so putting it back is exact. Not reproduced in the game before the change. The new tests\lazy_fetch_check.py reproduces it with the real Windows fault handler: a thread reads a page while it is armed and given back 20,000 times (arm_race: an access violation in 5 of 6 runs before the change), and once with a clean test of 100 ms (arm_race_slow: an access violation in every run). Both pass in every run since. wb_lazy_check, heap_lazy_copy_check, freeze_hold_check and mem_growth_check passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
5ee960f811 |
Add NX2NAVI_GEN_SEED, a test switch that starts the page write generations at a given value, so a play-test can cross 2^32 in seconds instead of 15 to 66 hours.
Whether the 64-bit generations hold past 2^32 in the game, with every texture check, mirror sync and shadow copy of a real scene, could not be seen in play: reaching 2^32 takes 15 to 66 hours. gen_wrap_check shows it for the generation code alone. NX2NAVI_GEN_SEED=<n> (nx2navi.ini gen_seed, diagnostics, default 0) makes the run's first generation n + 1: with 4294966000 a run passes 2^32 within its first seconds, and with 1099511627000 the low 32 bits pass through 0 again. The first gen_next, before anything is stamped, reads the value (gen_seed). It is parsed with strtoull, decimal or 0x hexadecimal: strtoul stops at 2^32 - 1 where unsigned long is 32 bits, as on Windows, and would turn a seed past 32 bits into 4294967295 without a word. It is capped at 2^62, far from the 64-bit end, and only ever raises the counter, since a lower value would leave stamps newer than it that read as written at every check. A value that is no number, or negative, is logged and ignored; a seed in use is logged as "mem: page generations start at <n> (NX2NAVI_GEN_SEED, a test switch)". Without the switch the only cost is one compare a generation that is true once. cfg.c gets its metas row (0, 0..4611686018427387904, read at start), its known[] name and its sections[] entry, each on a line of its own next to the other memory test switches (STAGING_FAIL_AT, VRAM_BALLAST_MB); nx2navi.ini.example gets a commented gen_seed entry under [diagnostics]. The settings menu does not show it. tests/gen_wrap_check runs the generation code a second way: the counter left at 0 and the seed taken from NX2NAVI_GEN_SEED, for 4294967288, 0xfffffffff8, 0x7ffffffffffffff8 (capped), 12abc, -8, 0 and unset, each with the start and the log line expected of it, then the same compares across the boundary. With strtoul in place of strtoull two of those runs fail. Checks that passed: gen_wrap_check, gen_width_check, cfg_rows_check (0 problems), pass_cc_check on vk_memory.c and cfg.c, no new -Wall -Wextra warnings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
6b95c867c8 |
Make the page write generations 64-bit, so a long session no longer wraps them and every texture, mirror and shadow check after the wrap answers wrong.
vk_memory.c handed out page write generations from a 32-bit counter (gen_now) and every consumer compared them as 32-bit values, with 0 meaning "never stamped". At 124 to 336 generations a frame (a 0.10.0 log with trace = perf) the counter wrapped after about 66 h of play at 60 fps, 27.6 h at 144 and 15 to 17 h at 240. Past the wrap a stamp made before it compared newer than every later check and one made after it older: textures and VRAM mirrors missed the CPU's writes (stale textures, and the lazy fetch copying older mirror bytes over a CPU write), pages nothing wrote read as written at every check (a hash or upload every time, and every such page of a mirrored pool copied up at each sync), and the generation handed out at the wrap was 0. At 64 bits nothing wraps: 2^64 lasts millions of years at 336 a frame and 240 fps. Below 2^32 every value and every compare is the same as before. What changed: gen_now, both tables (gen_chunk, cgen_chunk), reg_max and creg_max, PoolRec's synced_gen, mgen and sgen, dirty_gen and DIRTY_UNSYNCED, VSample.mgen, every local and parameter that holds a generation in vk_memory.c, vk_texture.c (Tex.wgen and gpu_gen, PEnt.since, storage_rewritten and its callers, sweep_hard's write-back check included) and tex_probe.c, and vk.h's vk_mem_changed, vk_mem_changed_snap and vk_mem_unchanged(_why)_locked. The verify mode's mismatch line prints the generations with %llu. The readers that take no gen_lock (lazy_clean, cpu_wrote_since, verify_cb, vk_mem_sizes) stay lock-free at 64 bits: the slots, mgen, gen_now and a new chunk pointer are now stored atomically (relaxed, or release where a reader acquires), the same instructions on x86-64, and a static assertion holds that 64-bit atomics are lock-free. No lock, wait or thread changes. gen_next, which hands out every generation, no longer warns at 0xC0000000 and up or at the wrap, which cannot come; it logs one line when the count reaches 2^32, where earlier versions ran out, with the frame, the hours and the rate. Cost: the tables take 8 bytes a page instead of 4 (512 KiB more per 256 MiB of guest address space stamped, both tables together, allocated as before when a chunk is first stamped), reg_max and creg_max 2 MiB each instead of 1, Tex and PoolRec 8 bytes more, PEnt 8 more. The page walks read 8 bytes a slot; a micro-benchmark of the walk measured no difference. Not reproduced in the game before the change: the wrong answers were reproduced by compiling vk_memory.c's generation code with the counter seeded below 2^32. tests/gen_wrap_check now runs that code, with the readers outside its section, across 2^32, 2^40 and 2^63 and checks that the compares answer as below the boundary; on the code before the change 19 of its checks fail at 2^32 and the other two seeds do not fit the counter. The new tests/gen_width_check compiles vk_memory.c, vk_texture.c and tex_probe.c with -Wshorten-64-to-32, -Wformat and -Wincompatible-pointer-types as errors, so a 32-bit local left behind is found (shown by reverting single locals, fields and formats). Checks that passed: gen_wrap_check, gen_width_check, pass_cc_check of every runtime file, no new -Wall -Wextra warnings in the three files, tex_release_check, tex_compact_check, wb_lazy_check, gpa_check, mem_growth_check, mem_budget_check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
0131b1b263 |
Stop a launcher release and a build kit on an nx2navi_dlssg.dll that lacks an export runtime/dlssg/nx2_sl.h declares, before they download anything.
The review found that nothing on the release path ran the new export check: tools/make_launcher.py build_app and tools/make_kit.py copied whatever --dlssg DLL they were given, and the release script still named the DLL of 0.8.0, which lacks nx2sl_reflex2. Shipped that way, Reflex would never hold the frame limit while players still got this branch's other changes. make_launcher.py check_dlssg and make_kit.py main now run build_dlssg.check on the --dlssg DLL beside the translator's check, before anything is resolved, downloaded or deleted; build_dlssg.check also stops on a missing file and on a file that is no DLL, with a message instead of a traceback. tests/dlssg_exports_check.py adds a DLL with every declared export (it passes), the two-export DLL, a missing file and two files that are no DLL (they stop, naming why), make_launcher.check_dlssg on both DLLs, and that both scripts' main makes the check before streamline_pin.resolve and shutil.rmtree; it fails when the call is taken out of make_launcher.py main. The DLL built from this branch passes make_launcher.check_dlssg; the DLL of 0.8.0 is refused, naming nx2sl_reflex2. docs/launcher.md and docs/building.md "Checks" say so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
9f4f51f010 |
Fail tools/build_dlssg.py when nx2navi_dlssg.dll lacks an export nx2_sl.h declares, and let it check a DLL built earlier.
The runtime takes a missing optional export (nx2sl_reflex2, nx2sl_frames_max) for an older DLL and leaves the feature out with one log line, so a release that ships a DLL built from older sources than its runtime loses Reflex's frame limit without an error; the release script took the DLL of 0.8.0 from a fixed path. After the build, build_dlssg.py now reads every PFN_nx2sl_<name> in runtime/dlssg/nx2_sl.h and the DLL's export directory (a small PE reader of its own, PE32 and PE32+) and exits naming each export that is missing; --check <dll> makes the same check without building. tests/dlssg_exports_check.py builds a DLL with two of three declared exports with MSYS2's clang and checks that the third is named, and that nx2_sl.h's own declarations are read in full; docs/building.md "Checks" and docs/launcher.md name both. The DLL built from this branch with the pinned Streamline 2.14.16 SDK passes (15 exports); the one built before nx2sl_reflex2 fails, naming it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
2d6f9f07b2 |
Decide who holds the frame limit with DLSS Frame Generation in a header of its own, runtime/dlssg_pace.h, with a check.
Reflex can hold the frame limit where each frame starts (nx2sl_reflex2), but the driver only takes a limit through Streamline, the SDK does not say whether its minimum interval counts the frames the game draws or every frame on screen, and nothing tells the game that the limit holds. dlssg_pace_step decides from what the presenting guest thread sees alone: the limit Reflex should be given (the drawn period, 0 during a movie, before the first host present, after 200 ms without one, or when not allowed), and whether Reflex holds this frame's limit, which it does only once Streamline took exactly that limit. Over windows of 60 presents Reflex held to one period and multiplier, over half sooner than three quarters of the period means nothing holds the game and the pacer takes the limit for the run; over half the multiplier periods apart means the driver counted the frames on screen, and Reflex is given the period over the multiplier from then on, or the pacer the limit if it happens again. Being pure, the decision is checked without a GPU: tests/dlssg_pace_check.py builds it with undefined-behaviour traps and drives it through each case; four mutations of the header (the window's majority, the idle bound, a host present stamped after the clock read, the applied-limit test) each fail it. docs/building.md "Checks" names it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
405a75f3b1 |
Say in docs/architecture.md when a crash still calls into RivaTuner Statistics Server, and list the new state-file checks in docs/building.md.
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> |
||
|
|
1744681f1a |
Give up on RivaTuner Statistics Server for the run when it hooks the game with the limit for 30 s and never shows it holds it.
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> |
||
|
|
a7b4f0c7cb |
Put back a profile a run that did not close left in RivaTuner Statistics Server, also at a start that keeps RTSS out.
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> |
||
|
|
80b96a748d |
Describe RivaTuner Statistics Server's hand-off, the pacer's fallbacks and rtss-profile.txt as they behave now.
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> |
||
|
|
1f547853e3 |
Put back the backslash that had turned into a tab in docs/building.md's tex_compact_check command.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
6b051eea58 |
List kern_ap_stub_ymm as dispatched in check_isa.py, so that a --cpu v2 build passes its instruction check again.
kernel.c's preemption stub has two bodies since
|
||
|
|
56db0d485c |
Make check_platform.py skip the branches that only a Windows build compiles, so that it passes on the runtime and guards it again.
tools/check_platform.py scanned the raw text of each shared runtime file and knew nothing of preprocessor branches, so the Win32 use that input.c, mods_probe.c, rtss_limit.c, vk_cache_file.h, vk_dlaa.c, vk_dlssg.c, vk_dump.c, vk_ffx.cpp, vk_ffx4.cpp, vk_fg.cpp, vk_fg4.cpp and vk_pipeline.c keep behind #ifdef _WIN32 made it fail on every run, at
|
||
|
|
78bfb83b2b |
Ask the shipped translator itself for its texture pool length before make_launcher packages it.
The review of the packaging guard (b4c21998) found that D-1's optional behaviour probe was left out without a reason: check_built compares only nx2navi_xlate.id with the checkout's sources, so an old exe beside a current .id still passed, and while the exe build record waits (G-12), a probe of the exe is the only landable check on the exe itself. It also passes the release scripts' folder, as that translator declares 40960 entries and clamps to 40959. tools/xlate_smoke.py gains pool_check(exe): it sends a hand-assembled fragment shader that reads a texture handle from an input (IPA R4 = a[0x80]; TXQ.B R0, R4; EXIT), which the translator reads from the texture pool array, and refuses an exe that does not run or answer, whose res.json texture pool entry (pool 1) has another array length than tools/xlate/Program.cs TexturePoolLength, or whose SPIR-V lacks that array or a GLSL.std.450 UMin by length - 1 (spirv_clamps reads the module itself, no disassembler needed). xlate_smoke.py runs it last and takes --exe. make_launcher.check_xlate runs pool_check on the exe after check_built on Windows, still before anything is downloaded, built or deleted. Not reproduced before the change beyond the audit's probe. On this PC, pool_check passes on work\xlate_6866a13e (and make_launcher.check_xlate on that folder), refuses the 0.9.2 translator (identity 4d051299) with "declares the texture pool with [32768] entries, where its sources say 40960", also through check_xlate beside a current .id, and xlate_smoke.py --exe passes on work\xlate_6866a13e. tests/xlate_package_check.py now checks spirv_clamps on a module it assembles, that pool_check and check_xlate refuse an exe that does not run beside a matching .id, and that a translator built in build/xlate passes when there is one; it passes. docs/building.md names pool_check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
75b1e0b5d9 |
Say that pipeline-binaries.bin is cut back also at a malformed record, and check the cut itself.
The review of the torn-file fix (43ac5ae3) found that pb_index also stops at a malformed record in the middle of the file, so pb_init cuts the file back to it and drops the whole records after it, while its log line and the comment above the cut spoke only of a record half written. It also found that tests/pb_torn_check.py covered only the index walk, not the keep-and-cut that is the fix. The decision after the walk moves from pb_init into pb_keep(path, good, len), which returns the length to go on with (len when the records fill the file, good after cutting it back) or 0 when the file cannot be cut and starts empty; its log line now reads "<path> ends inside a record or holds a malformed one at byte <good>; <n> bytes from there dropped", and the comment and the start-empty reason say the same. Behaviour is unchanged. pb_torn_check.py now also cuts pb_keep out of vk_pipeline.c and runs it on real files, with plat_file_truncate a stand-in that cuts with the C library: a torn file is cut to its whole records and logged, a second walk reaches the end of what is left, a record appended after the cut is read back with the others; a malformed record in the middle is cut off with what follows; a whole file is left as it is, unlogged; and a file that cannot be cut gives 0 and stays as it was. The check passes, vk_pipeline.c compiles, and its -Wall -Wextra warnings are the four it had, none in pb_keep or pb_init. docs/building.md names pb_keep and what the check now covers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
6c6e7b3974 |
Serve a refused carried translation once the current translator has failed the shader.
The review of the carry refusal (a2de7eb7) found that it also took the old translation away from programs the current
translator cannot translate. Before it, carry_find served the carried translation before any translation ran; after
it, a carried translation with the earlier texture pool length was refused, the translation ran and failed, and get_one
marked the key failed for the session (xlate_worker marked its wait failed), so the shader's draws were skipped, or a
compute dispatch left out, for the whole session, where 0.10.1 drew them with only the textures numbered from 32768 up
wrong. That contradicted docs/architecture.md ("A program the new translator refuses keeps its old translation") and
the carry pass's "its previous translation stays in use".
gfx_shader.c carry_last now runs where a translation failed (get_one, xlate_worker, and get_async_one's synchronous
fallback): carry_find with the new short_ok argument set passes carry_usable by, so the refused translation enters the
memory cache, where the lookups' memory step serves it from then on; the key is not marked failed and the wait is
cleared as for a success. The first four are logged with their key. Every other caller passes short_ok 0.
pool_short_note still counts such a shader for vk_texture_register's warning but no longer logs it as a translator in
use older than the runtime, and the refusal's log lines say that a shader the current translator fails keeps its old
translation. With this, the carry pass's "its previous translation stays in use" is true again for these programs, and
keeping their previous entries (the pass does not prune a failed input's) is what this needs.
Not reproduced before the change. tests/carry_pool_check.py now also cuts carry_last out of gfx_shader.c and checks that
it serves the refused translation into the memory cache, which the memory lookup then finds, serves a usable one
unlogged, returns NULL with none and still refuses another stage, and that pool_short_note counts a carried shader
without logging it; it passes, every runtime file compiles, and gfx_shader.c has the same 7 -Wall -Wextra warnings as
on stability-pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
acfc0ac24c |
Make excl_mt check the exclusive monitor as it is: a compare-and-swap of the value loaded.
The test expected a store exclusive to fail after another thread changed the value with exclusive pairs and changed it back (ABA), as the console's monitor would. runtime/cpu.h's monitor (EXCL ldxN/stxN, ldxp128/stxp128) compares the memory with the value loaded, so a value restored to the same bits passes by design, and the test has failed since both came in with the base import. It now checks both cases the helpers define: one change fails the store (status 1), a change and its undoing does not (status 0), for the 32-bit, 64-bit and 128-bit forms, with the control and the contention runs as before. Its docstring, a comment and docs/building.md's row say that the monitor does not see the change-and-restore case. No runtime change. The test passes under .venv: the one-change cases fail the store, the two-change cases succeed, and every contention counter is exact. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
79c41ca76a |
Keep the whole records of a pipeline-binaries.bin that ends inside a record, and cut a failed append back out.
With NX2NAVI_PIPE_BINARIES=1, a process ended during the autosave's append (a kill, a crash, a fatal on another thread) or a disk that filled left the file ending inside a record, and pb_init then started empty, dropping every whole record (up to the 1 GB limit). After a failed append pb_flush also left the torn bytes in the file while file_size stayed, so every later record's offset was off by them. The index walk moves into pb_index, which returns where the last whole record ends; pb_init keeps the index of those records and cuts the file back to that offset with plat_file_truncate after it closed its handle, and starts empty only when the cut fails. pb_flush closes the read handle, cuts the file back to file_size and opens it again when an append fails (plat_file_truncate opens the file alone, so it fails while another handle is open); pb_read refuses to read when that reopen failed. Not reproduced before the change; the option is off by default. tests/pb_torn_check.py (new) cuts the record layout and pb_index out of vk_pipeline.c and walks files of whole records, with a last record cut short at many lengths and with a malformed record; vk_pipeline.c compiles with no new warnings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
f3d8f08689 |
Make every packaging script refuse a translator not built from the checkout's sources, early.
build_xlate.py --id-only wrote an nx2navi_xlate.id for whatever exe sat in the folder, from the sources as they are, so make_launcher's guard (check_xlate, which compares only the .id) passed an old exe beside a fresh .id. Nothing calls the option; it is gone, and only a build writes an .id. make_release.check_translator compared build/xlate's .id with build_xlate.identity(), which hashes the tree the last build prepared there, so a patch or vendor change since that build passed unseen; make_appimage.py and make_kit.py checked nothing. The fresh-prepare identity moves from make_launcher into build_xlate as source_identity(), and build_xlate.check_built(folder, exe) refuses a missing exe or .id and an .id that differs from it. make_launcher, make_release, make_appimage and make_kit stop on its reason, each before its downloads, builds or deletions: make_launcher's check moves from build_app to the top of main, before the --out folder is deleted and the toolchain lock is resolved. A translator folder built from the current sources, as the release scripts pass it (work\xlate_6866a13e), passes as before. Not reproduced before the change beyond the audit's probes. tests/xlate_package_check.py (new) checks a matching folder, missing files, another identity and a stale build (refused although identity() over its own tree equals its .id), that --id-only is refused, and that make_launcher, make_release and make_kit stop on a foreign translator, the kit before any download; check_built passes on work\xlate_6866a13e. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
72047d12af |
Stop sending a request again to a new translator child after the first one gave no answer for the read timeout.
plat_child_read returned -1 both for a broken pipe and for a child that sent nothing for PLAT_CHILD_READ_MS (120 s), and svc_request retried any failure once on a fresh child. The translator is deterministic, so a program it hangs on hangs the second child too: a compute shader, which the replay thread looks up synchronously (vk_exec_draw.c shader_for), held the game for about 4 minutes instead of 2. plat_child_read now returns -2 on the timeout (win32.c and linux.c), child_exchange_raw returns 1, 0 for a broken pipe or a child that could not start, and -1 for the timeout, and svc_request stops the child and gives -1 without asking again; a dead child is still restarted once. translate_one fails such a request with its own reason, keeps its job and code for diagnosis and deletes whatever the stopped child wrote. The carry pass's child runs at idle priority, so a busy machine can starve it that long: carry_exchange does not resend after a timeout either, and carry_translate counts it as a failed input, not as an unavailable translator, so one slow program does not count towards the three that stop the pass. platform.h lets a build set PLAT_CHILD_READ_MS. Not reproduced before the change (no translator hang appears in the available logs). tests/child_timeout_check.py (new) builds the real child and request functions with a 300 ms timeout: a silent child gives -2 and costs one timeout with one child started, an exiting child is restarted once, an answering child gives its line. Every runtime file compiles, with no new warnings in gfx_shader.c and win32.c. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
46c764a434 |
Stop serving carried translations whose texture pool array has the earlier translator's length.
Installs updated from 0.10.0 or 0.10.1 hold translations of the 0.9.2 translator, which declares the texture pool with 32768 entries and clamps every handle to slot 32767. The carry lookups (gfx_shader.c carry_load, carry_find_iid) served such a translation for the whole session, so every texture the game numbers from 32768 up (Ocarina's pool has 33068) was drawn with whatever slot 32767 held: the see-through ground of issues #569 and #824 stayed on those installs after the update. carry_usable now refuses a previous translator's translation that has a texture pool array (reflection pool 1) of 32768 entries; the lookup goes on to the next previous identity and then to a translation under the current translator, so the boot prewarm translates the shaders it names on the boot screen and the rest are translated at first use. The first refusal is logged, and the saves and the exit log how many there were. get_one now checks its failed keys before the previous translators' disk entries, so a shader whose translation failed does not read a refused entry from the disk at every call. Not reproduced before the change. tests/carry_pool_check.py (new) cuts the carry lookups out of gfx_shader.c and checks that an earlier-length translation is freed and passed over, also in the owned branch, while others are served; gfx_shader.c compiles with no new warnings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
301445b314 |
Name the four checks this branch added in docs/building.md's "Checks".
The review found that astc_trim_check, evo_peek_check, heap_lazy_copy_check and gen_wrap_check were in neither the pass's test matrix nor the docs, so a later change to the code they cover would not know to run them. Each now has a sentence in the "A change to X passes python tests\Y.py" form: vk_astc.c's scratch trim with vk_defer_buffer_try, vk_evo_peek.h with vk_exec.c vk_exec_held_ready, memory.c mem_copy_readable with the lazy heap, and vk_memory.c's page write generations. Docs only. Checks that passed: check_content --staged; the four checks pass with system Python. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
28f420bf03 |
Take a census walk seen running for over 2 s as a torn read and charge nothing to it.
The review of c05e6528 found a window in perf.c ft_frame: it loads walk_t0 and then walk_t1, while perf_census_walk(1) stores walk_t1 = 0 and then the new start. A walk that starts between the two loads leaves ft_frame with the previous walk's start and an end of 0, so perf_census.h census_charge took that walk as running since then, the whole frame as overlapped, and moved all its replay and unattributed time to "census". The window is a few nanoseconds, but the error is a whole frame. A walk takes 100-260 ms, so census_charge now returns 0 for a walk seen running for longer than CENSUS_WALK_MAX_NS (2 s). tests/census_charge_check.c gets a torn read (a start 10 s back with an end of 0), a walk running for 1.9 s, which still counts, and one running for 2.001 s, which does not; its random frames now reach walks more than 2 s old and check the moved time exactly (the overlap capped by the frame's room and its replay and unattributed parts) instead of only its bound. Built against the header before this change, the check fails the torn read, the 2.001 s walk and the first such random frame; with it, it passes. docs/building.md names the new case. Diagnostics only: the census part exists only with NX2NAVI_MEM_CENSUS = 1. Not reproduced in a run (the window is a few nanoseconds). perf.c compiles with the dev flags and adds no -Wall -Wextra warnings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
48ea624389 |
Bring the stutter map's ini text, the checks in docs/building.md and the released-textures bound line in step with the code.
- nx2navi.ini.example said the stutter map is written every 10 minutes and at exit; since |