diff --git a/docs/building.md b/docs/building.md index 43733aba..52e4f779 100644 --- a/docs/building.md +++ b/docs/building.md @@ -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 diff --git a/runtime/dlssg_pace.h b/runtime/dlssg_pace.h new file mode 100644 index 00000000..7236b0f0 --- /dev/null +++ b/runtime/dlssg_pace.h @@ -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 + +#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; +} diff --git a/tests/dlssg_pace_check.c b/tests/dlssg_pace_check.c new file mode 100644 index 00000000..ce907927 --- /dev/null +++ b/tests/dlssg_pace_check.c @@ -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 +#include + +#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; +} diff --git a/tests/dlssg_pace_check.py b/tests/dlssg_pace_check.py new file mode 100644 index 00000000..25bd4c1c --- /dev/null +++ b/tests/dlssg_pace_check.py @@ -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())