mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
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>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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 <math.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
166
tests/dynres_check.py
Normal file
166
tests/dynres_check.py
Normal file
@@ -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 <clang>]
|
||||
"""
|
||||
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 <math.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
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())
|
||||
Reference in New Issue
Block a user