mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
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>
This commit is contained in:
@@ -764,6 +764,13 @@ A change to which frames FSR 3.1 frame generation is told are camera cuts (`runt
|
||||
frame id gets its own frame's cut, that an id never noted or no longer held counts as a cut, and that a slot rewritten
|
||||
on another thread never answers an old id with a newer frame's value. No GPU, no game files.
|
||||
|
||||
A change to who holds the frame limit with DLSS Frame Generation (`runtime/dlssg_pace.h`, `runtime/vk_dlssg.c`
|
||||
`vk_dlssg_pace`) passes `python tests\dlssg_pace_check.py`, which builds the header alone and checks that Reflex holds
|
||||
the limit only once Streamline took exactly that limit, that the game's own pacer keeps it during a movie, before the
|
||||
first host present, after 200 ms without one and with the setting off, that frames coming too soon over a window of 60
|
||||
give it to the pacer for the rest of the run, and that frames the multiplier periods apart switch Reflex to the frames
|
||||
on screen once and to the pacer the second time. No GPU, no game files.
|
||||
|
||||
A change to which frames the flight recorder takes for a one-frame flash (`runtime/flight_flash.h`, `runtime/flight.c`
|
||||
`flight_cells`) passes `python tests\flight_flash_check.py`, which builds the header alone, draws pictures of the
|
||||
thumbnails' size and checks that a patch or a whole frame shown once is a flash, that a camera cut, a fade, a pan and
|
||||
|
||||
72
runtime/dlssg_pace.h
Normal file
72
runtime/dlssg_pace.h
Normal file
@@ -0,0 +1,72 @@
|
||||
// The frame limit Reflex holds with DLSS Frame Generation (vk_dlssg.c vk_dlssg_pace), as a function of what the
|
||||
// presenting guest thread sees and nothing else, so tests/dlssg_pace_check.c drives it without a GPU.
|
||||
//
|
||||
// The pacer's period counts the frames the game draws (frame_rate.c frame_step_cap_ns). Reflex holds it where the frame
|
||||
// starts, before the pad is read, and the pacer then keeps only its ceiling (frame_rate.c frame_step_ceiling_ns); the
|
||||
// pacer keeps the whole period while a movie plays (the caller passes 0), before the first host present and while none
|
||||
// went out for DLSSG_HOLD_IDLE_NS (Reflex's sleep is left out then, vk_dlssg.c vk_dlssg_frame_start), while the limit
|
||||
// Reflex should hold is not the one Streamline last took, and for the rest of the run once the frames show that Reflex
|
||||
// does not hold it.
|
||||
//
|
||||
// The frames are checked over windows of DLSSG_PACE_WINDOW presents Reflex held to one period and one multiplier.
|
||||
// Over half of them sooner than three quarters of the period: nothing holds the game, and a game that runs free can
|
||||
// outrun what its own code survives, so the pacer takes the limit for the run. Over half of them about the multiplier
|
||||
// times the period apart (within half a period): the driver counted the frames on screen, the generated ones too, so
|
||||
// Reflex is given the period over the multiplier from then on; the same again (a GPU that only managed that rate)
|
||||
// gives the limit to the pacer for the run. A frame a load or a hitch held counts as neither.
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
|
||||
#define DLSSG_HOLD_IDLE_NS 200000000ull // no host present for this long: Reflex's sleep is left out
|
||||
#define DLSSG_PACE_WINDOW 60 // presents over which Reflex's hold of the limit is checked
|
||||
|
||||
enum { DLSSG_PACE_SAME, DLSSG_PACE_FAST, DLSSG_PACE_SHOWN, DLSSG_PACE_SLOW }; // what the last window found
|
||||
|
||||
typedef struct {
|
||||
uint64_t last_ns; // the newest present whose frame Reflex held, 0 when the pacer held it
|
||||
uint64_t last_period; // its period
|
||||
int last_mult; // and its multiplier
|
||||
int window, fast, slow; // presents counted in the window, those too soon, those about the multiplier periods apart
|
||||
int shown; // Reflex counts the frames on screen: it is given the period over the multiplier
|
||||
int bad; // Reflex did not hold the limit: the pacer holds it for the rest of the run
|
||||
} DlssgPace;
|
||||
|
||||
// now: kern_now_ns at the game's present. period_ns: the pacer's period (0: a movie, or no limit). mult: frames on
|
||||
// screen per drawn frame now (vk_dlssg_multiplier). hosted_ns: kern_now_ns after the newest host present, 0 before
|
||||
// the first. allowed: Reflex may hold the limit (the setting on, a DLL with nx2sl_reflex2 that Streamline did not
|
||||
// refuse). applied_us: the limit Streamline last took. *want_us: the limit Reflex should hold, 0 none. *found: what a
|
||||
// window that ended here found (DLSSG_PACE_*). Returns 1 when Reflex holds this frame's limit: the pacer keeps only
|
||||
// its ceiling.
|
||||
static inline int dlssg_pace_step(DlssgPace *p, uint64_t now, uint64_t period_ns, int mult, uint64_t hosted_ns,
|
||||
int allowed, uint32_t applied_us, uint32_t *want_us, int *found) {
|
||||
*found = DLSSG_PACE_SAME;
|
||||
if (mult < 1) mult = 1;
|
||||
if (!p->bad && p->last_ns && now >= p->last_ns && p->last_period == period_ns && p->last_mult == mult) {
|
||||
uint64_t d = now - p->last_ns, m = (uint64_t)mult;
|
||||
if (d < period_ns / 4 * 3) p->fast++;
|
||||
else if (m > 1 && d * 2 > period_ns * (2 * m - 1) && d * 2 < period_ns * (2 * m + 1)) p->slow++;
|
||||
if (++p->window >= DLSSG_PACE_WINDOW) {
|
||||
if (p->fast > DLSSG_PACE_WINDOW / 2) p->bad = 1, *found = DLSSG_PACE_FAST;
|
||||
else if (p->slow > DLSSG_PACE_WINDOW / 2 && p->shown) p->bad = 1, *found = DLSSG_PACE_SLOW;
|
||||
else if (p->slow > DLSSG_PACE_WINDOW / 2) p->shown = 1, *found = DLSSG_PACE_SHOWN;
|
||||
p->window = p->fast = p->slow = 0;
|
||||
}
|
||||
} else {
|
||||
p->window = p->fast = p->slow = 0; // a window counts only presents Reflex held to one period and multiplier
|
||||
}
|
||||
uint64_t us = 0;
|
||||
// a host present stamped after now went out while this thread read the clock: as recent as one can be
|
||||
int live = hosted_ns && (hosted_ns >= now || now - hosted_ns < DLSSG_HOLD_IDLE_NS);
|
||||
if (allowed && period_ns && !p->bad && live) {
|
||||
uint64_t per = p->shown ? period_ns / (uint64_t)mult : period_ns;
|
||||
us = (per + 500) / 1000;
|
||||
if (!us) us = 1;
|
||||
if (us > 0x7fffffffu) us = 0; // no limit Reflex could be given: the pacer keeps it
|
||||
}
|
||||
*want_us = (uint32_t)us;
|
||||
int held = us && applied_us == (uint32_t)us;
|
||||
p->last_ns = held ? now : 0;
|
||||
p->last_period = held ? period_ns : 0;
|
||||
p->last_mult = mult;
|
||||
return held;
|
||||
}
|
||||
145
tests/dlssg_pace_check.c
Normal file
145
tests/dlssg_pace_check.c
Normal file
@@ -0,0 +1,145 @@
|
||||
// runtime/dlssg_pace.h, who holds the frame limit with DLSS Frame Generation (vk_dlssg.c vk_dlssg_pace), driven with
|
||||
// made-up present times: when Reflex holds it and the pacer keeps only its ceiling, the cases the pacer keeps it
|
||||
// (no host present yet or for 200 ms, a movie, the setting off, a limit Streamline has yet to take), and the checks
|
||||
// over windows of presents: frames that come too soon give the limit to the pacer for the run, frames the multiplier
|
||||
// periods apart switch Reflex to the frames on screen, and the same again gives it to the pacer. No GPU, no game files.
|
||||
// Built and run by tests/dlssg_pace_check.py.
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "dlssg_pace.h"
|
||||
|
||||
static int fails;
|
||||
#define CHECK(c, ...) do { if (!(c)) { fails++; printf("FAIL line %d: ", __LINE__); printf(__VA_ARGS__); printf("\n"); } } while (0)
|
||||
|
||||
#define MS 1000000ull
|
||||
static const uint64_t P60 = 16666667ull; // frame_step_cap_ns at 60
|
||||
|
||||
// A run of n presents d ns apart starting after *t, each followed by a host present 1 ms later; the present thread takes
|
||||
// every limit asked for at once (applied follows want). Returns how many were held by Reflex; *last_found the last
|
||||
// non-SAME window result.
|
||||
static int run(DlssgPace *p, uint64_t *t, int n, uint64_t d, uint64_t period, int mult, int allowed, uint32_t *applied,
|
||||
uint32_t *want, int *last_found) {
|
||||
int held = 0;
|
||||
for (int i = 0; i < n; i++) {
|
||||
*t += d;
|
||||
int found;
|
||||
held += dlssg_pace_step(p, *t, period, mult, *t - d + 1 * MS, allowed, *applied, want, &found);
|
||||
if (found != DLSSG_PACE_SAME && last_found) *last_found = found;
|
||||
*applied = *want; // the present thread applies it before the next game present
|
||||
}
|
||||
return held;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
uint32_t want = 0, applied = 0;
|
||||
int found = 0;
|
||||
uint64_t t = 1000 * MS;
|
||||
|
||||
// before the first host present: the pacer
|
||||
DlssgPace p;
|
||||
memset(&p, 0, sizeof p);
|
||||
CHECK(!dlssg_pace_step(&p, t, P60, 2, 0, 1, 0, &want, &found) && want == 0, "no host present yet: pacer, no limit");
|
||||
|
||||
// a host present 5 ms ago: Reflex is asked for the drawn period, the pacer holds until Streamline took it
|
||||
CHECK(!dlssg_pace_step(&p, t, P60, 2, t - 5 * MS, 1, 0, &want, &found) && want == 16667,
|
||||
"first frame: limit asked (%u), not yet applied", want);
|
||||
CHECK(dlssg_pace_step(&p, t + P60, P60, 2, t, 1, 16667, &want, &found) && want == 16667, "applied: Reflex holds");
|
||||
CHECK(!dlssg_pace_step(&p, t + 2 * P60, P60, 2, t, 1, 20000, &want, &found), "another limit applied: the pacer holds");
|
||||
|
||||
// a movie (period 0): no limit for Reflex, the pacer holds
|
||||
CHECK(!dlssg_pace_step(&p, t, 0, 2, t - MS, 1, 16667, &want, &found) && want == 0, "movie: pacer");
|
||||
// the setting off, or an older DLL
|
||||
CHECK(!dlssg_pace_step(&p, t, P60, 2, t - MS, 0, 16667, &want, &found) && want == 0, "not allowed: pacer");
|
||||
// no host present for 200 ms (minimised): the pacer; 199 ms still Reflex
|
||||
CHECK(!dlssg_pace_step(&p, t, P60, 2, t - 200 * MS, 1, 16667, &want, &found) && want == 0, "idle 200 ms: pacer");
|
||||
CHECK(dlssg_pace_step(&p, t, P60, 2, t - 199 * MS, 1, 16667, &want, &found), "idle 199 ms: Reflex");
|
||||
|
||||
// Reflex holds the limit: presents a period apart, no verdict over many windows
|
||||
memset(&p, 0, sizeof p), applied = 0, found = 0;
|
||||
int held = run(&p, &t, 600, P60, P60, 2, 1, &applied, &want, &found);
|
||||
CHECK(held == 599 && !p.bad && !p.shown && found == 0 && want == 16667, "held: %d of 600, bad %d shown %d", held, p.bad, p.shown);
|
||||
|
||||
// a GPU slower than the limit at 1x: frames 2 periods apart, never taken for the shown count
|
||||
memset(&p, 0, sizeof p), applied = 0, found = 0;
|
||||
run(&p, &t, 300, 2 * P60, P60, 1, 1, &applied, &want, &found);
|
||||
CHECK(!p.bad && !p.shown && found == 0, "1x, slow GPU: no verdict");
|
||||
|
||||
// nothing holds the game: 31 of 60 too soon -> the pacer for the run; 30 of 60 is not enough
|
||||
memset(&p, 0, sizeof p), applied = 0, found = 0;
|
||||
run(&p, &t, 2, P60, P60, 2, 1, &applied, &want, NULL); // asked, then held: the next present counts
|
||||
run(&p, &t, 30, 5 * MS, P60, 2, 1, &applied, &want, &found);
|
||||
run(&p, &t, 30, P60, P60, 2, 1, &applied, &want, &found);
|
||||
CHECK(!p.bad && found == 0, "30 of 60 too soon: Reflex keeps it");
|
||||
run(&p, &t, 31, 5 * MS, P60, 2, 1, &applied, &want, &found);
|
||||
run(&p, &t, 29, P60, P60, 2, 1, &applied, &want, &found);
|
||||
CHECK(p.bad && found == DLSSG_PACE_FAST, "31 of 60 too soon: the pacer for the run");
|
||||
held = run(&p, &t, 120, P60, P60, 2, 1, &applied, &want, NULL);
|
||||
CHECK(held == 0 && want == 0, "after that, the pacer, whatever the frames do (%d held)", held);
|
||||
|
||||
// the driver counts the frames on screen: at 2x the drawn frames come two periods apart -> shown units
|
||||
memset(&p, 0, sizeof p), applied = 0, found = 0;
|
||||
run(&p, &t, 2, P60, P60, 2, 1, &applied, &want, NULL);
|
||||
run(&p, &t, 60, 2 * P60, P60, 2, 1, &applied, &want, &found);
|
||||
CHECK(p.shown && !p.bad && found == DLSSG_PACE_SHOWN && want == 8333, "2x, two periods apart: shown units (%u)", want);
|
||||
// it does now: a period apart, Reflex keeps it
|
||||
found = 0;
|
||||
held = run(&p, &t, 300, P60, P60, 2, 1, &applied, &want, &found);
|
||||
CHECK(!p.bad && found == 0 && held == 300, "shown units hold the drawn period");
|
||||
// 4x: the period over 4
|
||||
run(&p, &t, 2, P60, P60, 4, 1, &applied, &want, NULL);
|
||||
CHECK(want == 4167, "4x shown units: %u", want);
|
||||
// the generation paused (the menu, 1x): the period itself
|
||||
run(&p, &t, 2, P60, P60, 1, 1, &applied, &want, NULL);
|
||||
CHECK(want == 16667, "1x shown units: %u", want);
|
||||
|
||||
// shown units guessed wrong (the driver counts drawn frames): the frames come too soon -> the pacer
|
||||
memset(&p, 0, sizeof p), applied = 0, found = 0;
|
||||
p.shown = 1;
|
||||
run(&p, &t, 2, P60, P60, 2, 1, &applied, &want, NULL);
|
||||
run(&p, &t, 60, P60 / 2, P60, 2, 1, &applied, &want, &found);
|
||||
CHECK(p.bad && found == DLSSG_PACE_FAST, "shown units, frames at twice the limit: the pacer");
|
||||
|
||||
// the same slow band twice (a GPU that only manages the limit over the multiplier) -> the pacer
|
||||
memset(&p, 0, sizeof p), applied = 0, found = 0;
|
||||
run(&p, &t, 2, P60, P60, 3, 1, &applied, &want, NULL);
|
||||
run(&p, &t, 60, 3 * P60, P60, 3, 1, &applied, &want, &found);
|
||||
CHECK(p.shown && found == DLSSG_PACE_SHOWN, "3x, three periods apart: shown units");
|
||||
run(&p, &t, 61, 3 * P60, P60, 3, 1, &applied, &want, &found);
|
||||
CHECK(p.bad && found == DLSSG_PACE_SLOW, "again: the pacer for the run");
|
||||
|
||||
// hitches (a load: 10 periods) are neither
|
||||
memset(&p, 0, sizeof p), applied = 0, found = 0;
|
||||
run(&p, &t, 2, P60, P60, 2, 1, &applied, &want, NULL);
|
||||
run(&p, &t, 120, 10 * P60, P60, 2, 1, &applied, &want, &found);
|
||||
CHECK(!p.bad && !p.shown && found == 0, "hitches: no verdict");
|
||||
|
||||
// a change of the period or the multiplier starts the window again: 40 too soon at one limit, 40 at the next
|
||||
memset(&p, 0, sizeof p), applied = 0, found = 0;
|
||||
run(&p, &t, 2, P60, P60, 2, 1, &applied, &want, NULL);
|
||||
run(&p, &t, 40, 5 * MS, P60, 2, 1, &applied, &want, &found);
|
||||
run(&p, &t, 40, 5 * MS, P60 / 2, 2, 1, &applied, &want, &found);
|
||||
CHECK(!p.bad, "window restarted at the period's change");
|
||||
run(&p, &t, 40, 2 * MS, P60 / 2, 3, 1, &applied, &want, &found);
|
||||
CHECK(!p.bad, "window restarted at the multiplier's change");
|
||||
|
||||
// presents the pacer held are not counted: the limit never applied
|
||||
memset(&p, 0, sizeof p), found = 0;
|
||||
for (int i = 0; i < 200; i++) {
|
||||
t += 5 * MS;
|
||||
dlssg_pace_step(&p, t, P60, 2, t - MS, 1, 0, &want, &found);
|
||||
}
|
||||
CHECK(!p.bad && found == 0, "unapplied: no window");
|
||||
|
||||
// the longest limit Reflex takes: 30 fps is 33333 us; a period too long for it is the pacer's
|
||||
memset(&p, 0, sizeof p);
|
||||
dlssg_pace_step(&p, t, 1000000000ull / 30, 1, t - MS, 1, 0, &want, &found);
|
||||
CHECK(want == 33333, "30 fps: %u", want);
|
||||
dlssg_pace_step(&p, t, 3000000000000000ull, 1, t - MS, 1, 0, &want, &found);
|
||||
CHECK(want == 0, "a period no limit can hold: %u", want);
|
||||
// the clock going back (a host present stamped after the game's present read the clock) holds nothing up
|
||||
CHECK(dlssg_pace_step(&p, t, P60, 2, t + MS, 1, 16667, &want, &found) && want == 16667, "a host present stamped after the game present read the clock: Reflex");
|
||||
|
||||
printf(fails ? "%d checks failed\n" : "all checks passed\n", fails);
|
||||
return fails != 0;
|
||||
}
|
||||
42
tests/dlssg_pace_check.py
Normal file
42
tests/dlssg_pace_check.py
Normal file
@@ -0,0 +1,42 @@
|
||||
"""Build and run tests/dlssg_pace_check.c: runtime/dlssg_pace.h, whether Reflex or the game's own pacer holds the frame
|
||||
limit with DLSS Frame Generation (vk_dlssg.c vk_dlssg_pace), and its checks of the frames Reflex held.
|
||||
|
||||
Needs a C compiler only: no Vulkan headers, no GPU and no game files.
|
||||
|
||||
Usage: dlssg_pace_check.py
|
||||
"""
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
MSYS_BIN = Path(r'C:\msys64\ucrt64\bin')
|
||||
|
||||
|
||||
def compiler():
|
||||
"""$CC, else MSYS2 UCRT64's clang, else clang on PATH (as tests/wb_lazy_check.py picks it)."""
|
||||
if os.environ.get('CC'):
|
||||
return os.environ['CC']
|
||||
if os.name == 'nt' and (MSYS_BIN / 'clang.exe').exists():
|
||||
return str(MSYS_BIN / 'clang.exe')
|
||||
return shutil.which('clang') or 'clang'
|
||||
|
||||
|
||||
def main():
|
||||
work = ROOT / 'build' / 'test' / 'dlssg_pace'
|
||||
work.mkdir(parents=True, exist_ok=True)
|
||||
exe = work / ('dlssg_pace_check.exe' if os.name == 'nt' else 'dlssg_pace_check')
|
||||
env = dict(os.environ)
|
||||
if os.name == 'nt' and MSYS_BIN.exists():
|
||||
env['PATH'] = str(MSYS_BIN) + os.pathsep + env.get('PATH', '')
|
||||
# undefined behaviour traps: an overflow in the period arithmetic fails the check instead of passing by chance
|
||||
subprocess.check_call([compiler(), '-std=gnu11', '-O1', '-Wall', '-Werror', '-fsanitize=undefined',
|
||||
'-fsanitize-trap=undefined', '-I', str(ROOT / 'runtime'),
|
||||
str(ROOT / 'tests' / 'dlssg_pace_check.c'), '-o', str(exe)], env=env)
|
||||
return subprocess.run([str(exe)], env=env).returncode
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user