diff --git a/docs/building.md b/docs/building.md index 32a15087..d72b6f94 100644 --- a/docs/building.md +++ b/docs/building.md @@ -792,6 +792,13 @@ A change to which entry of the upscaler stand-in's scratch images serves a new s and checks that a match is a hit, a ninth size takes a free entry or the one used longest ago instead of being refused, and two images of one frame never evict each other. No GPU, no game files. +A change to dynamic resolution's target or step hold (`runtime/dynres.c`) passes `python tests\dynres_check.py`, which +builds that file with stand-ins for the settings, the GPU's busy time, the clock and the game's rate under the limit: +the default target is the frame time of 60, 144 and 240 fps and 16.7 ms for an unknown or unlimited rate, a target +that is set wins, a limit changed while it runs moves the target with one log line, and each step is held for +`dyn_res_step` 60ths of a second at 60, 144 and 240 fps, down while the GPU is over the target and back up once it is +under three quarters of it. No GPU, no game files. + A change to the ASTC decoder's scratch trim (`runtime/vk_astc.c` `vk_astc_scratch_trim`, with the upload lock it only tries, `runtime/vk_memory.c` `vk_defer_buffer_try`) passes `python tests\astc_trim_check.py`, which builds `vk_astc.c` with the platform layer's recursive lock and the rest of the runtime stubbed: the trim gives way while another thread diff --git a/nx2navi.ini.example b/nx2navi.ini.example index 08c1b301..14a27ca3 100644 --- a/nx2navi.ini.example +++ b/nx2navi.ini.example @@ -177,10 +177,11 @@ # present, as before). # gate_wait_ms = 10 # Dynamic resolution, 0 or 1, applied at once: with a DLSS upscaling mode, the scene resolution follows the GPU time -# per frame toward dyn_res_target milliseconds (1 to 100), one DLSS step at a time, each held for dyn_res_step frames -# (8 to 600), from the DLSS mode set down to performance. Default: off. +# per frame toward dyn_res_target milliseconds (1 to 100; 0 is the time of a frame the game draws under fps_limit, +# 1000 / the limit, and 16.7 with fps_limit = unlimited), one DLSS step at a time, each held for dyn_res_step 60ths of +# a second (8 to 600; 45 is 0.75 s at any frame rate), from the DLSS mode set down to performance. Default: off. # dyn_res = 0 -# dyn_res_target = 16.7 +# dyn_res_target = 0 # dyn_res_step = 45 # Processors. On a processor whose dies have L3 caches of different sizes (AMD's two-die X3D processors, with 3D # V-Cache on one die) the game runs on the die with the larger cache when that die has 8 cores or more: 1 = on, diff --git a/runtime/cfg.c b/runtime/cfg.c index c02a670b..a6481433 100644 --- a/runtime/cfg.c +++ b/runtime/cfg.c @@ -331,8 +331,9 @@ static const CfgMeta metas[] = { {"TAIL_EARLY", "0", "0|1", RESTART}, {"REPLAY_SMALL", "0", "0..63", RESTART}, // vk_exec.c group_start {"FREE_CORES", "0", "0|1", RESTART}, {"TIMER_1MS", "1", "0|1", RESTART}, - // dynamic resolution (dynres.c): the scene follows the GPU time toward DYN_RES_TARGET milliseconds - {"DYN_RES", "0", "0|1", LIVE}, {"DYN_RES_TARGET", "16.7", "1..100", LIVE}, {"DYN_RES_STEP", "45", "8..600", LIVE}, + // dynamic resolution (dynres.c): the scene follows the GPU time toward DYN_RES_TARGET milliseconds (under 1: the + // frame limit's frame time), holding each step DYN_RES_STEP 60 Hz frames of the clock + {"DYN_RES", "0", "0|1", LIVE}, {"DYN_RES_TARGET", "0", "0..100", LIVE}, {"DYN_RES_STEP", "45", "8..600", LIVE}, // the recording thread: a core of its own (auto: with 12 or more logical processors) and the highest priority // (vk_exec.c rec_claim) {"RECORDER_CORE", "0", "auto|0|1", RESTART}, {"RECORDER_PRIO", "1", "0|1", RESTART}, {"REPLAY_TIME", "0", "0|1", RESTART}, {"LOCK_TIME", "0", "0|1", RESTART}, {"POOL_CREATE_BUDGET", "16", "1..4096", RESTART}, {"POOL_CREATE_MS", "6", "0..100", RESTART}, {"POOL_FINI_IDLE", "0", "0|1", RESTART}, {"FREEZE_BUDGET_US", "1000", "0..100000", RESTART}, {"POOL_LOAD_BACKLOG", "512", "0..65536", RESTART}, diff --git a/runtime/dynres.c b/runtime/dynres.c index 5a8d5a8d..39073ca9 100644 --- a/runtime/dynres.c +++ b/runtime/dynres.c @@ -7,11 +7,16 @@ // NX2NAVI_DLSS is one of those upscaling modes, which is then the sharpest step it uses; the lowest is performance, // or ultra performance when that is the player's own mode. While it runs it asks for GPU timestamps // (perf_gpu_timing_want; vk_sync.c times every submission part) and reads their running total (vk_gpu_busy_all) once -// per presented frame. It averages the busy time and moves one step at a time: down when the average is over -// NX2NAVI_DYN_RES_TARGET milliseconds (default 16.7, a 60 Hz frame) by 8 %, up when it is under 75 % of it, and holds -// each step for NX2NAVI_DYN_RES_STEP frames (default 45), because a size that changes every few frames is worse to look -// at than a steady one. The step in use goes to vk_dlaa.c dlss_mode as an override (dynres_override), which applies it -// as it applies the menu's choice; the player's setting itself never changes. +// per presented frame. It averages the busy time and moves one step at a time: down when the average is over the +// target by 8 %, up when it is under 75 % of it, and holds each step for NX2NAVI_DYN_RES_STEP 60 Hz frames of the clock +// (default 45, 750 ms), because a size that changes every few frames is worse to look at than a steady one. The target +// is NX2NAVI_DYN_RES_TARGET milliseconds when that is set (1 to 100), else the frame time of the limit the game draws +// at (frame_rate.c frame_step_game_hz: 1000 / the limit, the monitor's rate for display, the drawn frames with frame +// generation), and 16.7 ms, a 60 Hz frame, while that rate is unknown or unlimited. A fixed 16.7 ms target let the GPU +// take up to 18 ms a frame at full size under a 120 or 144 fps limit, and a hold of 45 presented frames lasted 0.19 s +// at 240 fps. The step in use goes to vk_dlaa.c dlss_mode as an override (dynres_override), which applies it as it +// applies the menu's choice; the player's setting itself never changes. +#include #include #include #include @@ -20,18 +25,21 @@ #include "cfg.h" #include "host.h" #include "vk.h" +#include "platform/platform.h" #define DYN_LEVELS 4 +#define DYN_TARGET_MS 16.7 // the target while the game's rate is unknown or unlimited: a 60 Hz frame static const char *const DYN_MODE[DYN_LEVELS] = {"quality", "balanced", "performance", "ultra_performance"}; static struct { int on; // dynamic resolution is acting now int base, low; // the player's step (the sharpest used) and the lowest one used int level; // the step in use - int settle; // frames the step has been held - int step; // frames to hold a step + uint64_t held_from; // plat_now_ns when the step in use was taken + uint64_t hold_ns; // how long to hold a step double avg_ns; // moving average of the GPU's busy time per frame - double target_ns; + double target_ms; // NX2NAVI_DYN_RES_TARGET; under 1: the frame time of the game's rate + double target_ns; // the target in use uint64_t last_busy; // vk_gpu_busy_all at the previous presented frame unsigned cfg_seen; // cfg_version the settings were read at char base_mode[32]; @@ -43,22 +51,31 @@ static int mode_index(const char *m) { return -1; } +// The target in ns: the setting's, else the frame time of the rate the game draws at under the limit. +static double target_now_ns(void) { + if (D.target_ms >= 1) return D.target_ms * 1e6; + double hz = frame_step_game_hz(); + return (hz > 0 ? 1000.0 / hz : DYN_TARGET_MS) * 1e6; +} + // The settings, again whenever one changed (the menu switches them live). static void dynres_read(void) { D.cfg_seen = cfg_version(); const char *m = cfg_value("NX2NAVI_DLSS"); int want = cfg_bool("NX2NAVI_DYN_RES"), base = mode_index(m); - double ms = cfg_double("NX2NAVI_DYN_RES_TARGET"); - D.target_ns = (ms >= 1 ? ms : 16.7) * 1e6; - D.step = cfg_int("NX2NAVI_DYN_RES_STEP"); - if (D.step < 8) D.step = 8; + D.target_ms = cfg_double("NX2NAVI_DYN_RES_TARGET"); + D.target_ns = target_now_ns(); + int step = cfg_int("NX2NAVI_DYN_RES_STEP"); + if (step < 8) step = 8; + D.hold_ns = (uint64_t)step * 1000000000ull / 60; int on = want && base >= 0; if (on && (!D.on || strcmp(m, D.base_mode))) { // (re)start at the player's step D.base = D.level = base; D.low = base > 2 ? base : 2; - D.settle = 0, D.avg_ns = 0, D.last_busy = vk_gpu_busy_all(); - rt_log("resolution: dynamic resolution on: target %.1f ms, DLSS %s down to %s", D.target_ns / 1e6, DYN_MODE[D.base], - DYN_MODE[D.low]); + D.held_from = plat_now_ns(), D.avg_ns = 0, D.last_busy = vk_gpu_busy_all(); + rt_log("resolution: dynamic resolution on: target %.1f ms (%s), each step held %.2f s, DLSS %s down to %s", + D.target_ns / 1e6, D.target_ms >= 1 ? "as set" : "the frame limit's", D.hold_ns / 1e9, + DYN_MODE[D.base], DYN_MODE[D.low]); } else if (!on && D.on) { rt_log("resolution: dynamic resolution off: DLSS %s as set", m); } else if (want && base < 0) { @@ -77,6 +94,16 @@ const char *dynres_override(void) { return D.on && D.level != D.base ? DYN_MODE[ void dynres_frame(void) { if (cfg_version() != D.cfg_seen) dynres_read(); if (!D.on) return; + double target = target_now_ns(); // the limit, the monitor's rate and frame generation change without a setting + if (target != D.target_ns) { // logged when it moves by more than 1 %: a measured refresh rate wanders a little + static double logged_ns; + static int said; + if (fabs(target - logged_ns) > logged_ns * 0.01 && said < 20) { + said++, logged_ns = target; + rt_log("resolution: dynamic resolution: target %.2f ms (the frame limit's)", target / 1e6); + } + D.target_ns = target; + } uint64_t now = vk_gpu_busy_all(), d = now - D.last_busy; D.last_busy = now; if (!d || d > 1000000000ull) return; // no timing this frame yet, or a stall that says nothing about the scene @@ -87,12 +114,12 @@ void dynres_frame(void) { rt_log("resolution: dynamic resolution: GPU %.2f ms a frame, DLSS %s, target %.2f ms", D.avg_ns / 1e6, DYN_MODE[D.level], D.target_ns / 1e6); } - if (++D.settle < D.step) return; + if (plat_now_ns() - D.held_from < D.hold_ns) return; int to = D.level; if (D.avg_ns > D.target_ns * 1.08 && D.level < D.low) to = D.level + 1; else if (D.avg_ns < D.target_ns * 0.75 && D.level > D.base) to = D.level - 1; if (to == D.level) return; rt_log("resolution: dynamic resolution: GPU %.2f ms a frame against %.2f ms: DLSS %s -> %s", D.avg_ns / 1e6, D.target_ns / 1e6, DYN_MODE[D.level], DYN_MODE[to]); - D.level = to, D.settle = 0; + D.level = to, D.held_from = plat_now_ns(); } diff --git a/tests/dynres_check.py b/tests/dynres_check.py new file mode 100644 index 00000000..6bf0ec5f --- /dev/null +++ b/tests/dynres_check.py @@ -0,0 +1,166 @@ +"""Dynamic resolution's target and step hold (runtime/dynres.c): builds dynres.c, cut by its text, with stand-ins for +the settings, the GPU's busy time, the clock and the game's rate under the limit (frame_rate.c frame_step_game_hz), +and checks that + + - the default target is the frame time of the game's rate (1000 / the limit: 60, 144 and 240 fps), 16.7 ms while + that rate is unknown or unlimited, and a target that is set wins at any rate; + - a limit changed while it runs moves the target on the next frame, with one log line; + - each step is held for dyn_res_step 60ths of a second of the clock at 60 and 240 fps (45: 0.75 s; 90: 1.5 s), down + while the GPU is over the target and back up once it is under three quarters of it. + +No game files, no GPU. Usage: python tests/dynres_check.py [--cc ] +""" +import argparse +import os +import subprocess +import sys +import tempfile +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +MSYS_BIN = Path(r'C:\msys64\ucrt64\bin') + +HARNESS = r''' +#include +#include +#include +#include +#include +#include +static unsigned ver = 1; +static const char *dlss = "quality", *dyn = "1"; +static double set_target_ms; // NX2NAVI_DYN_RES_TARGET as cfg_double reads it (0: the default) +static int set_step = 45; // NX2NAVI_DYN_RES_STEP +static double game_hz = 60; +static uint64_t busy = 1, now_ns = 1000000000ull; +static int timing, logs; +static char last_log[256]; +static unsigned cfg_version(void) { return ver; } +static const char *cfg_value(const char *k) { + return !strcmp(k, "NX2NAVI_DLSS") ? dlss : !strcmp(k, "NX2NAVI_DYN_RES") ? dyn : "0"; +} +static int cfg_bool(const char *k) { return strcmp(cfg_value(k), "0") != 0; } +static double cfg_double(const char *k) { return !strcmp(k, "NX2NAVI_DYN_RES_TARGET") ? set_target_ms : 0; } +static int cfg_int(const char *k) { return !strcmp(k, "NX2NAVI_DYN_RES_STEP") ? set_step : 0; } +static double frame_step_game_hz(void) { return game_hz; } +static uint64_t vk_gpu_busy_all(void) { return busy; } +static void perf_gpu_timing_want(int on) { timing = on; } +static uint64_t plat_now_ns(void) { return now_ns; } +static void rt_log(const char *fmt, ...) { + va_list a; + va_start(a, fmt); + vsnprintf(last_log, sizeof last_log, fmt, a); + va_end(a); + logs++; +} +%s +static int fails; +#define CHECK(cond, ...) do { if (!(cond)) { printf("FAIL: " __VA_ARGS__); putchar('\n'); fails++; } } while (0) +#define NEAR(a, b) (fabs((a) - (b)) <= (b) * 1e-6) + +// One presented frame at hz with the GPU busy gpu_ms; returns the step in use (0 quality .. 3 ultra performance). +static int frame(double hz, double gpu_ms) { + now_ns += (uint64_t)(1e9 / hz); + busy += (uint64_t)(gpu_ms * 1e6); + dynres_frame(); + return D.level; +} + +static void restart(void) { + dyn = "0", ver++, frame(60, 1); + dyn = "1", ver++, frame(60, 1); +} + +// Frames at hz with the GPU busy gpu_ms until the step changes (at most limit frames): their count, -1 for none. +static int frames_to_change(double hz, double gpu_ms, int limit) { + int from = D.level; + for (int k = 1; k <= limit; k++) + if (frame(hz, gpu_ms) != from) return k; + return -1; +} + +int main(void) { + // The default target follows the game's rate. + static const double rates[] = {60, 144, 240, 0}; + for (int i = 0; i < 4; i++) { + game_hz = rates[i]; + restart(); + double want = rates[i] > 0 ? 1e9 / rates[i] : 16.7e6; + CHECK(NEAR(D.target_ns, want), "at %%.0f fps the target is %%.3f ms, want %%.3f", rates[i], D.target_ns / 1e6, + want / 1e6); + CHECK(strstr(last_log, "dynamic resolution on") && strstr(last_log, "the frame limit's"), "start line: %%s", + last_log); + } + CHECK(timing, "GPU timestamps asked for while it runs"); + // A target that is set wins. + set_target_ms = 10, game_hz = 240; + restart(); + frame(240, 1); + CHECK(NEAR(D.target_ns, 10e6) && strstr(last_log, "as set"), "a set target: %%.3f ms (%%s)", D.target_ns / 1e6, + last_log); + set_target_ms = 0; + // A limit changed while it runs: the next frame's target, one line. + game_hz = 60; + restart(); + int before = logs; + game_hz = 144; + frame(144, 1); + CHECK(NEAR(D.target_ns, 1e9 / 144) && logs == before + 1 && strstr(last_log, "6.94"), "live limit: %%.3f ms, %%d " + "lines (%%s)", D.target_ns / 1e6, logs - before, last_log); + before = logs; + for (int k = 0; k < 100; k++) frame(144, 1); + CHECK(logs == before, "a steady limit logged %%d lines", logs - before); + // Each step is held for a time, whatever the frame rate. + static const struct { double hz; int step; double hold_s; } holds[] = {{60, 45, 0.75}, {240, 45, 0.75}, + {240, 90, 1.5}, {144, 8, 8 / 60.0}}; + for (int i = 0; i < 4; i++) { + double hz = holds[i].hz; + game_hz = hz, set_step = holds[i].step; + restart(); + double target = 1000 / hz; + int want = (int)ceil(holds[i].hold_s * hz); + int n = frames_to_change(hz, target * 2, 100000); + CHECK(D.level == 1 && n >= want - 1 && n <= want + 1, "%%.0f fps, step %%d: down after %%d frames, want %%d", + hz, holds[i].step, n, want); + n = frames_to_change(hz, target * 2, 100000); + CHECK(D.level == 2 && n >= want - 1 && n <= want + 1, "%%.0f fps: the next step after %%d frames, want %%d", hz, + n, want); + n = frames_to_change(hz, target * 2, want * 3); + CHECK(n < 0 && D.level == 2, "%%.0f fps: below performance (%%d)", hz, D.level); + n = frames_to_change(hz, target * 0.5, 100000); // the average needs a few frames to fall under 75 %% + CHECK(D.level == 1 && n >= 1 && n <= want + 40, "%%.0f fps: up after %%d frames", hz, n); + n = frames_to_change(hz, target * 0.5, 100000); + CHECK(D.level == 0 && n >= want - 1 && n <= want + 1, "%%.0f fps: back to quality after %%d frames, want %%d", + hz, n, want); + n = frames_to_change(hz, target * 0.9, want * 3); + CHECK(n < 0, "%%.0f fps: a GPU between three quarters of the target and 8 %%%% over it moved the step", hz); + } + set_step = 45; + printf(fails ? "dynres: %%d failures\n" : "dynres: ok\n", fails); + return fails != 0; +} +''' + + +def main(): + ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument('--cc', help='the C compiler (default: MSYS2 UCRT64 clang on Windows, clang elsewhere)') + args = ap.parse_args() + win = os.name == 'nt' + cc = args.cc or (str(MSYS_BIN / 'clang.exe') if win and MSYS_BIN.exists() else 'clang') + env = dict(os.environ) + if win and MSYS_BIN.exists(): + env['PATH'] = str(MSYS_BIN) + os.pathsep + env.get('PATH', '') + src = (ROOT / 'runtime' / 'dynres.c').read_text(encoding='utf-8') + body = src[src.index('#define DYN_LEVELS'):] + with tempfile.TemporaryDirectory(prefix='nx2navi-dynres-') as tmp: + c = Path(tmp) / 'check.c' + c.write_text(HARNESS % body, encoding='utf-8') + exe = Path(tmp) / ('check.exe' if win else 'check') + subprocess.check_call([cc, '-O2', '-Wall', '-Wextra', '-Wno-unused-function', str(c), '-o', str(exe), + *([] if win else ['-lm'])], env=env) + return subprocess.call([str(exe)]) + + +if __name__ == '__main__': + sys.exit(main())