mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
main
355 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2dd363b86e |
Count the texture cache's short waits in seconds below 60 presents a second too.
The cache counts its short waits in presents (PRESENTS(n): n presents at 60 a second) and scales them by the presents a second. Above 60 it always did. Below 60 the rate stopped at 60, so the 120 presents a released texture stays out of the VRAM pressure's reach (STALE_FRAMES) were 4 s at 30 presents a second and 5.7 s at 21. The game lets go of textures by the second, as the player moves, so what that window holds grows with its length. A tester's log (RTX 4070 SUPER, 2560x1440, DLSS Quality with frame generation, texture detail Full, 0.10.2-rc4): the card went to 11,198 MiB against a budget of 10,600 while the frame rate fell from 63 to 21 fps. The released textures held 2,644 MiB against their bound of 384, and 2,105 MiB of that was released too lately to go (442 MiB when the pressure began). On an RTX 5090 with the budget taken as 10,600 MiB and the tester's output settings, a walk through Hyrule Field at a 30 fps limit: the released-too-lately textures were all in their first 120 presents (the new census line below) and held 383 and 429 MiB on average at the memory reports of two runs, against 292 MiB at 60 fps. With this change they are in their first 60 presents and hold 209 MiB. Textures made again after a drop: 295 and 523 before, 511 after. Not shown: the tester's 21 fps and a card that is really over its budget, which this card does not reach; the memory in use as the driver counts it stayed at 9.8 GiB in the capped runs. The rate now follows the presents down to 15 a second (vk_tex_release.h trl_rate_sample, trl_rate, trl_presents), once eight seconds in a row were under 60, and never in a run's first eight seconds; one slow second among fast ones still shortens nothing. Every wait PRESENTS counts follows it. NX2NAVI_TEX_RATE_LOW=0 stops the rate at 60 as before, and one line says when the rate crosses 60. Values at 60 presents a second and above are the ones before. The census of the released textures over their bound gets a second line: what the ones released too lately wait for (their first presents after the release, content only the GPU made, the gap before a take-back with or without a lookup still reaching them). It is what showed which wait held the memory. tests/tex_release_check has the rate's checks and a table of the wait by presents a second; they fail against the rule before. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
7432f7375c |
Ask for the C runtime heap's summary before play only.
The memory report's system line asked for the heap's summary at every report until one was slow. The summary holds the heap's lock, and every malloc of every thread with it, while it adds the heap up: 54 and 70 ms in two sessions on an RTX 5090 PC, and the frame of the first report, 30 seconds into each session, took 55 to 82 ms, the longest frame of play in both. One slow summary switched the question off, so every session paid for it once. It is now asked for before play only (the boot and the title screen), and at every report with mem_census = 1, which already accepts a late frame for the walk of the address space. In play the line gives what the runtime knows it allocated, the same figure from one report to the next. The rule is in runtime/vk_mem_heap.h with tests/mem_heap_check, which fails against the rule before. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com> |
||
|
|
87bb179c78 |
Keep released textures for one minute and up to a thirty-second of the video memory budget.
The textures the game lets go of were kept for three minutes and could hold a quarter of the card's budget, up to 8 GiB with room. A run through Hyrule Field shows what that bought and what it cost (RTX 5090, 5120x2160 with DLSS Performance, about three minutes of running; the game keeps 4.8 GiB of textures live and loads about 190 a second): the released textures held 5.3 GiB on average and the video memory in use went from 11.1 to 17.9 GiB, while the game took back 1,494 MiB of them within 10 s of the release, 171 MiB within a minute and 10 MiB later, beside 33,000 textures it loaded anew. A shorter hold alone gives nothing back: with 20 s the released textures fell to 0.5 GiB and the free space inside the allocator's blocks rose to 4.9 GiB, since a block returns to the driver only when it is empty. What the released textures hold at their fullest is what the session costs, so the bound is the lever. The bound is now a thirty-second of the budget, 384 MiB to 1 GiB (vk_tex_release.h trl_cap_mb), still doubled while the card and the commit have room, and the hold 60 s, 120 for content only the GPU made. The same run with the new defaults: 13.2 GiB in use on average against 16.7, 13.8 at most against 17.9, 532 MiB of textures made again over the run, 77 fps against 75. An 8 GiB card keeps 384 MiB where it kept up to 1.9 GiB. tests/tex_release_check checks the bound by the budget and the sessions it simulates against the rule before. Compaction (tex_compact) and 16 MiB blocks were tried on the same run and gave back 0.7 and 1.2 GiB; neither is changed here. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com> |
||
|
|
1d0327acd1 |
Run a parallel group's pass hooks once, at its first chunk's first pass.
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> |
||
|
|
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> |
||
|
|
6e57ef9619 |
Check three of this pass's decisions without a device: the automatic texture detail, the device made with DLSS Frame Generation on, and the mesh fallback's depth hold.
Each decision moves into a header the runtime and a test both read, as vk_mesh_path.h does: vk_tex_detail.h (tex_detail_auto), vk_dlssg_device.h (dlssg_card_skipped, dlssg_device_create) and mesh_fb_hold.h (the hold's table). The runtime's behaviour is the same as before the move. tests/tex_detail_check has the budgets from players' logs of 0.10.1 (a handheld's integrated GPU at 10,361 MiB now gets reduced, a 12 GiB card at 11,313 MiB full). tests/dlssg_device_check plays the laptop of the report with stand-ins for the interposer and the loader: the chosen Intel card is skipped, the NVIDIA card is made through the interposer, a card of another vendor is made by the loader without the interposer being asked, and a refusal falls through to the loader. tests/mesh_fb_hold_check holds a list for two presents after its colour draw was skipped, through a wait of 600 frames, and no longer once it is drawn or gone. Built against the rule 0.10.1 had, the first two and mesh_path_check fail (6, 13 and 3 checks). Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com> |
||
|
|
bbeecce88c |
Never take the mesh emulation on an NVIDIA card, where it ended the game inside the driver.
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> |
||
|
|
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> |
||
|
|
8c9011d56f |
Read low_end_gpu's and shader_carry's off words the same in the low-end mode and in make_game.py as in the game's other readers.
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> |
||
|
|
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> |
||
|
|
358713fbf2 |
Trap unsigned wrap-around in tests/dlssg_pace_check.py, as its comment on overflows promised.
The review found that the comment said an overflow in the period arithmetic fails the check, but -fsanitize=undefined does not trap unsigned wrap-around, and every period and time in runtime/dlssg_pace.h is a uint64_t. The check now builds with -fsanitize=undefined,unsigned-integer-overflow and the same -fsanitize-trap list, and the comment says why. It passes; a uint64_t subtraction that wraps traps under these flags and runs on under the old ones. 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> |
||
|
|
2bf32649af |
Name the game's frame-rate ceiling as a period of its own, frame_step_ceiling_ns, for a pacer that hands the limit over.
When Reflex holds the frame limit with DLSS Frame Generation, the game's pacer is to keep only the rate the game's code survives (240 a second; the quick menu read address 0 at 640 to 700), so that a limiter that lets go, or one that counts other frames than the game's, cannot run the game past it. frame_step_ceiling_ns gives that period from LIMIT_FREE_MAX_HZ, or LIMIT_MAX_HZ for a build with the fps60 or realtime patch set; frame_step_cap_ns is left as it is. tests/frame_step_check.c checks 4166666 ns for a game stepping by the clock; frame_step_check passes. 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> |
||
|
|
c19f75e505 |
Check that a crash on RivaTuner Statistics Server's own thread, with no line in the state file, returns at once.
The review found that no test ran rtss_limit_crash's thread gate: where the state file holds no line of the run, rtss_limit_crash puts the profile back through rtss_limit_restore, except on the worker's own thread, which may hold the lock that restore tries for a second; only the static check in tests/rtss_limit_check.py looked at it. tests/rtss_worker_check.c crash_worker_thread now makes the state file unwritable and calls rtss_limit_crash from inside SaveProfile on the worker thread, with the lock held: it must return within 200 ms with no call into the fake hooks library (a new count of every call), close the run, and let no later want reach the profile. With the gate removed from a copy of rtss_limit.c, the scenario fails. Checks that passed: tests/rtss_limit_check.py, tools/check_content.py --staged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
9f45d7448f |
Leave rtss-profile.txt alone when it is there but does not read, so the lines of other executables in the folder stay.
The review found that rtss_limit.c state_put took a state file plat_file_read could not read for a missing one: a file another program held open (a scan) was rewritten with this profile's line alone, or deleted when the run removed its line, and the lines of other executables that share the folder (dev or probe builds) were lost. state_put now returns 0 when the read gave nothing but plat_file_size finds the file with bytes in it; the run then goes on as with a file that cannot be written (one log line, and a crash puts the profile back as an orderly exit does). An empty file is still rewritten. tests/rtss_worker_check.c state_locked checks a file holding another executable's line that does not read: the run limits as before, logs it, and the file stays as it was through the exit. Not reproduced before the change. Checks that passed: tests/rtss_limit_check.py, pass_cc_check.py --wall on rtss_limit.c, tools/check_content.py --staged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
da822abec3 |
Keep the state file's line ahead of RivaTuner Statistics Server's profile, and keep what a run owes the profile after a put back that did not land.
The review found three ways rtss-profile.txt could fall behind the profile. Every write after a run's first one saved the profile before the line, so a kill between the two left a line whose value the profile no longer held, and the next start took the game's limit for the player's (rtss_restore_plan LEAVE). A put back whose write did not land kept the line, but rtss_put_back_done cleared S.changed, so a later limit took the run's first-write path again and read the game's own limit from the profile as the value to put back, overwriting the true one in the line. And put_back removed the line after DeleteProfile without checking that the profile went away. The line now carries the value the run wrote before the newest one (rtss_decide.h RtssSaved prev_limit and prev_den, rtss1 with two more fields; the file came with this branch and was never released). rtss_limit.c apply writes the new value into the line before the profile (record_before) and drops the previous one once the profile holds the new one, or returns to it when RTSS refused it (record_after), and rtss_restore_plan takes a profile holding either value for the game's. A new owed flag keeps had_profile, old_limit and old_denominator from the run's first write until a put back lands: a later first write keeps them and the line's values, a failed first write leaves the line alone, and rtss_limit_restore tries the put back again at the exit. put_back and restore_left check that a deleted profile is gone before they remove the line, and restore_left's lines name the value the profile held. tests/rtss_limit_check.c covers the new fields and the plan for either value; tests/rtss_worker_check.c checks the line before and after a write (state_record), a run killed inside the save of its second limit and the next start removing the profile (kill_restart_mid_write), a put back that did not land followed by a limit and the exit (put_back_owed), and a removal that did not land (put_back_undeleted). Lines over 120 columns from the earlier fixes are wrapped. Not reproduced before the change. Checks that passed: tests/rtss_limit_check.py (44 of 44), tests/exit_order_check.py, tests/fatal_exit_check.py, pass_cc_check.py --wall on rtss_limit.c, 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> |
||
|
|
ad84e0a1c0 |
Say what RivaTuner Statistics Server does with a movie at the default limit of 60, where the profile keeps the limit.
The review found that rtss_decide.h rtss_target writes no limit only while a movie runs faster than the play limit, so
at the default limit of 60 a 60 fps movie leaves 60 in RTSS's profile and RTSS holds the movie beside the game's pacer.
Its comment said RTSS at the movie's rate would space out the frames that catch up after a late frame, and README.md
("one limiter at a time") and docs/architecture.md read as if every movie under RTSS were the pacer's alone. The
approved rule writes nothing to the profile at movies when the limit is 60 or more, so the code stays: rtss_target's
and rtss_pace_period's comments now say that at the movie's own rate RTSS keeps holding it and that whether its
limiter lets the frames that catch up through is not measured, README.md drops "one limiter at a time", and
docs/architecture.md lists the movie at 60 among the exceptions to one limiter. tests/rtss_limit_check.c pins a movie
at 60 under a limit of 60: no write, the profile keeps 60, held again after it.
No behaviour changes. Checks that passed: tests/rtss_limit_check.py, tools/check_content.py --staged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
60694532a1 |
Round a fractional limit down for a RivaTuner Statistics Server without the denominator field, so "display" never holds the game above the refresh.
apply() rounded the millihertz to the nearest whole frame where RTSS has no FramerateLimitDenominator, so "display" on a 59.94 Hz monitor wrote 60 and on a 143.856 Hz one 144: above the refresh, one frame more than the monitor shows every 17 s and every 7 s. Only "display" asks a fractional rate under RTSS (fixed limits and movie rates are whole, frame generation keeps RTSS out), so the new rtss_decide.h rtss_limit_value rounds every fraction down when the denominator is missing (59/1, 143/1), and keeps a whole rate over 1 and a fraction over 1000 where RTSS knows the denominator. VRR and present-wait handling stay to be measured. Not reproduced before the change. rtss_limit_check (T15: rtss_limit_value at 59940, 60000, 143856, 0 and 240000 with and without the denominator) and rtss_worker_check (round_logged_whole now 59/1 and 143/1, round_logged) pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
4d4b97b043 |
Let a want reach RivaTuner Statistics Server's profile only once it stayed two polls, and write nothing before the first host present.
A want that flips between a limit and kept-out created and removed RTSS's profile at each flip: with a driver's low latency vk_lowlat_on() is 0 before the first swapchain and during every swapchain rebuild, so a warm start without the boot screen wrote the profile and removed it a poll later, and a poll that fell into a rebuild did the same (lane A's probe: 6 creations and 5 removals for a want flipping each poll, 1 and 1 at start before RTSS hooked the game). rtss_decide now counts the polls in a row a want stayed: a limit goes into the profile and a kept-out want puts it back only after RTSS_SETTLE_POLLS (2), while standing aside still ends at once (rtss_limit_active compares the want), and nothing is written before the first host present (RTSS_WHY_NO_PRESENT). A limit change reaches the profile up to a second later; the game's limiter holds the new limit meanwhile. The created and removed lines were numbered before. Not reproduced before the change. rtss_limit_check (T8: kept out for one poll and for two, a second reason, a limit again; T9: 20 flips after a written limit give no write or put back, 20 from the start none, flips every two polls act; no write while no host present) and rtss_worker_check (churn_run and every scenario that now settles) pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
f7899e5443 |
Let the game's limiter stand aside only once RivaTuner Statistics Server is seen holding the game, and give up on RTSS at 1.05 times the limit over three fresh periods.
The worker stood aside as soon as RTSS listed the process and the profile read back, before RTSS had seen a Vulkan present (with InjectionDelay=15000 it lists the game some seconds after start) or read the new profile. Its frame count check took the newest period whatever its age: a period frozen at 144 fps after the limit went to 60 and the window was minimised failed RTSS for the run, and 1.15 times the limit was never caught (lane A's probe: listed_no_api, stale_fps, over115). rtss_decide now parses the entry's flags and stands aside only when the profile held the target at the poll before, RTSS lists the game with the Vulkan API value (flags & 0xFFFF == 0xA), RTSS_UPDATE_ASKED is clear, a new period was seen within max(2.5 s, 2.5 times its length), no movie plays and a host present went out within 200 ms. Only a new period after a poll that stood aside is checked: three in a row over 1.05 times the limit put the profile back for the run, and a stale period counts neither way. The worker reads the poll time and the newest host present (gfx_window_hosted_ns), and logs each change of why the game's limiter paces, up to 100 lines. Not reproduced before the change. rtss_limit_check (the flags at +264; T2 injection delay through not listed, no API, the update flag, no period and holding; T3 quit; T6 the frame count at 144, 69, 63 fps, a frozen period and a newer want; the idle case) and rtss_worker_check (over_run, over_slack_run, new api_run and idle_run, and every scenario that now holds from the poll after the write) pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
04f4d44747 |
Keep RivaTuner Statistics Server's profile as it was in rtss-profile.txt, so a start after a kill or crash puts it back, and leave it to that start at a crash.
An exit that skipped rtss_limit_restore (Task Manager, a hang, lost_wait_exit, a kill during the pipeline save) left the game's limit in Profiles\<exe>.cfg, and the next run's first apply took it for the player's own: that run then put the stale limit back at its close, and a profile the game had made stayed for good (lane A's probe: kill1, then kill2 'held a limit of 60/1'). The crash handler and the fatal error's end also called into RTSSHooks64.dll from a process that had failed (P-13). rtss_limit.c now keeps one line per profile in rtss-profile.txt next to the executable (rtss_decide.h RtssSaved: whether the profile existed, its old limit and denominator, the value the run wrote), written atomically before the run's first change of the profile, updated with each value written, and removed once the profile is back (put_back; also when the first write did not stick). On the first poll with the hooks library a start that finds a line of its profile puts the profile back first (restore_left, rtss_restore_plan): the old limit, or no profile, unless the player changed or removed it since, which the written value tells. main.c crash_handler and dispatch.c fatal_end call the new rtss_limit_crash, which closes the run and leaves the profile to that start with no call into RTSS's library; only when the file could not be written does it put the profile back as before, and never from the worker's own thread. A file that cannot be written leaves the run as it was, with one line in the log. Profiles frozen by earlier versions cannot be told from the player's own. Not reproduced before the change. rtss_limit_check (T12: the line's format and parse, the file's rewrite, each plan; the static check that both crash paths go through rtss_limit_crash), rtss_worker_check (state_record, state_player, state_unwritable, crash_recorded, and kill_restart with its player and edited variants, each a killed run and the next start in processes of their own), fatal_exit_check and exit_order_check pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |