mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
Tell in the log and the menu when RivaTuner Statistics Server's own limit holds the game beside another limiter.
With NX2NAVI_RTSS=0, frame generation or the driver's low latency, the game keeps RTSS out of its frame limit, but a limit the player set in RTSS itself (its Global profile or the game's own: a frame rate limit, a frame time limit or scanline sync) still holds the frames, on top of the game's limiter, the driver's or frame generation's. Nothing read RTSS's settings in such a run: a first want that kept RTSS out never started its thread, and frame_step_cap_ns folded the reasons into -1, so the log said "NX2NAVI_RTSS=0 or frame generation" for low latency too. frame_step_cap_ns now passes the reason (rtss_decide.h RTSS_OUT_SETTING, RTSS_OUT_FRAMEGEN, RTSS_OUT_LOWLAT; the unlimited branch too), and the kept-out line names it. A kept-out first want starts the worker as well. While RTSS is kept out the worker never loads RTSS's hooks library and only reads: its shared memory, and once RTSS lists the game the [Framerate] values of Profiles\<exe>.cfg, else Profiles\Global, with the system's ini reader, again only when either file's write time changes. When one of them limits the game, the log names the file, the values and the other limiter, once per change, and rtss_limit_state returns 4, which the frame rate row shows as a new note (UI_MENU_FPS_RTSS_TWO, in English, Russian, Brazilian Portuguese and Simplified Chinese). A later want of a limit (frame generation or low latency switched off) loads the hooks and hands RTSS the limit as before; NX2NAVI_RTSS=0 stays read-only for the run. Pacing does not change. Not reproduced before the change: a stacked limit needs RTSS running in a play-test. tests/rtss_worker_check.c gains three scenarios against the fake RTSS (a Global limit with NX2NAVI_RTSS=0, scanline sync in the game's profile under low latency and then a limit, frame generation with no limit of RTSS's own), and tests/rtss_limit_check.c builds rtss_decide.h alone and checks which settings hold the frames and their text. Both pass, with frame_step_check and a compile of every runtime file; -Wall adds no warning. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -55,6 +55,7 @@
|
||||
#include "input.h"
|
||||
#include "layout.h"
|
||||
#include "platform/platform.h"
|
||||
#include "rtss_decide.h"
|
||||
#include "vk_dlssg.h"
|
||||
#include "vk_fg.h"
|
||||
#include "vk_lowlat.h"
|
||||
@@ -276,7 +277,8 @@ double frame_step_game_hz(void) {
|
||||
// RivaTuner Statistics Server (rtss_limit.c) gets the same limit; while it limits this process the pacer holds no
|
||||
// frames, so the two never stack. NX2NAVI_RTSS=0 and frame generation (RTSS would count the generated frames) keep it
|
||||
// out, and so does a driver's low latency while it is in effect (vk_lowlat.c): the driver holds the limit where the
|
||||
// frame starts, and RTSS would hold the finished picture, the wait that mode is there to remove.
|
||||
// frame starts, and RTSS would hold the finished picture, the wait that mode is there to remove. RTSS gets the reason
|
||||
// (RTSS_OUT_*), for the log and for its check of a limit set in RTSS itself.
|
||||
static double paced_hz; // the game frames a second the newest frame_step_cap_ns paces at (ocarina_step); 0: none
|
||||
static int movie_paced; // the movie rate the newest frame_step_cap_ns paces at (ocarina_step, frame_step_movie_hz); 0: none
|
||||
int frame_step_movie_hz(void) { return movie_paced; }
|
||||
@@ -287,7 +289,10 @@ uint64_t frame_step_cap_ns(void) {
|
||||
const char *e = cfg_get("NX2NAVI_RTSS");
|
||||
rtss_allowed = !(e && !strcmp(e, "0"));
|
||||
}
|
||||
int rtss = rtss_allowed && !vk_fg_wanted() && !vk_dlssg_active() && !vk_lowlat_on();
|
||||
int64_t rtss_out = !rtss_allowed ? RTSS_OUT_SETTING
|
||||
: vk_fg_wanted() || vk_dlssg_active() ? RTSS_OUT_FRAMEGEN
|
||||
: vk_lowlat_on() ? RTSS_OUT_LOWLAT
|
||||
: 0;
|
||||
int gen = generated_factor();
|
||||
int movie_hz = movie_cap_hz();
|
||||
static int movie_clock = -1; // NX2NAVI_MOVIE_CLOCK=0: the steady step and the usual pacing through a movie too
|
||||
@@ -305,7 +310,7 @@ uint64_t frame_step_cap_ns(void) {
|
||||
}
|
||||
}
|
||||
if (limit_hz == -2 && !movie_hz && !frame_free_running()) { // unlimited: the pacer does not hold frames, and
|
||||
rtss_limit_want(rtss ? 0 : -1); // RTSS's limit is cleared
|
||||
rtss_limit_want(rtss_out ? rtss_out : 0); // RTSS's limit is cleared
|
||||
paced_hz = 0;
|
||||
return 0;
|
||||
}
|
||||
@@ -322,7 +327,7 @@ uint64_t frame_step_cap_ns(void) {
|
||||
}
|
||||
if (movie_hz) hz = movie_hz;
|
||||
paced_hz = hz;
|
||||
rtss_limit_want(rtss ? (int64_t)(hz * 1000 + 0.5) : -1);
|
||||
rtss_limit_want(rtss_out ? rtss_out : (int64_t)(hz * 1000 + 0.5));
|
||||
if (rtss_limit_active()) return 0;
|
||||
return (uint64_t)(1e9 / hz + 0.5);
|
||||
}
|
||||
|
||||
@@ -428,10 +428,12 @@ void frame_rate_sound_step_host(Cpu *c); // the step the sound
|
||||
float frame_step_frames(void); // the 60 Hz frames the game steps per update (set ostep), else 1
|
||||
void frame_step_flight(float *page, float *game, uint64_t *presents); // flight.c: the step at a host present
|
||||
// rtss_limit.c: RivaTuner Statistics Server takes the frame limit where it runs
|
||||
void rtss_limit_want(int64_t mhz); // the pacer's limit in millihertz: >0, 0 none, -1 keep RTSS out
|
||||
void rtss_limit_want(int64_t mhz); // the pacer's limit in millihertz: >0, 0 none; or why RTSS is
|
||||
// kept out (rtss_decide.h RTSS_OUT_*)
|
||||
int rtss_limit_active(void); // RTSS limits to the wanted rate: the pacer holds no frames
|
||||
int rtss_limit_state(void); // menu: 1 RTSS limits, 2 its profile is read-only, 3 it let the
|
||||
// game run past its limit
|
||||
// game run past its limit, 4 kept out while a limit set in
|
||||
// RTSS itself holds the game too
|
||||
void rtss_limit_restore(void); // before the process ends: the profile's old limit back
|
||||
int64_t frame_step_world_us(void); // game time since the loaded save's first frame, -1 before
|
||||
// (NX2NAVI_FPS_LIMIT, live)
|
||||
|
||||
45
runtime/rtss_decide.h
Normal file
45
runtime/rtss_decide.h
Normal file
@@ -0,0 +1,45 @@
|
||||
// RivaTuner Statistics Server as the frame limiter (rtss_limit.c): the parts that make no system call, so that
|
||||
// tests/rtss_limit_check.c builds them on any system.
|
||||
#pragma once
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
|
||||
// rtss_limit_want's argument: a limit in millihertz (>0), 0 for none, or why RTSS is kept out of it (frame_rate.c
|
||||
// frame_step_cap_ns). RTSS_UNSET is the want before the first one.
|
||||
enum {
|
||||
RTSS_UNSET = -2,
|
||||
RTSS_OUT_SETTING = -1, // NX2NAVI_RTSS=0, for the whole run
|
||||
RTSS_OUT_FRAMEGEN = -3, // frame generation: RTSS would count the generated frames too
|
||||
RTSS_OUT_LOWLAT = -4, // a driver's low latency (vk_lowlat.c) holds the frames where they start
|
||||
};
|
||||
|
||||
// What keeps RTSS out, for the log.
|
||||
static inline const char *rtss_out_text(int64_t want) {
|
||||
return want == RTSS_OUT_FRAMEGEN ? "frame generation"
|
||||
: want == RTSS_OUT_LOWLAT ? "the driver's low latency"
|
||||
: "NX2NAVI_RTSS=0";
|
||||
}
|
||||
|
||||
// The limiter that holds the frames while RTSS is kept out, for the log.
|
||||
static inline const char *rtss_out_limiter(int64_t want) {
|
||||
return want == RTSS_OUT_FRAMEGEN ? "frame generation and the game's limiter"
|
||||
: want == RTSS_OUT_LOWLAT ? "the driver's low latency"
|
||||
: "the game's limiter";
|
||||
}
|
||||
|
||||
// RTSS's own [Framerate] settings for the game, as its profile file holds them (rtss_limit.c read_settings): a frame
|
||||
// rate limit (Limit over LimitDenominator), a frame time limit (LimitTime over LimitTimeDenominator) and scanline sync
|
||||
// (SyncScanline0 and SyncScanline1); 0 is off for each.
|
||||
typedef struct {
|
||||
int limit, limit_den, time, time_den, scan0, scan1;
|
||||
} RtssSettings;
|
||||
|
||||
// RTSS holds the game's frames by a setting of its own.
|
||||
static inline int rtss_settings_limit(const RtssSettings *s) { return s->limit || s->time || s->scan0 || s->scan1; }
|
||||
|
||||
// "limit L/D, frame time limit T/TD, scanline sync S0/S1", the values as the file holds them.
|
||||
static inline void rtss_settings_text(char *out, size_t n, const RtssSettings *s) {
|
||||
snprintf(out, n, "limit %d/%d, frame time limit %d/%d, scanline sync %d/%d", s->limit, s->limit_den, s->time,
|
||||
s->time_den, s->scan0, s->scan1);
|
||||
}
|
||||
@@ -33,6 +33,7 @@
|
||||
|
||||
#include "host.h"
|
||||
#include "platform/platform.h"
|
||||
#include "rtss_decide.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
@@ -50,10 +51,11 @@ typedef void (*UpdateFn)(void);
|
||||
enum { FAIL_NONE, FAIL_LIBRARY, FAIL_READONLY, FAIL_NOT_LIMITING };
|
||||
|
||||
static PlatMutex lock = PLAT_MUTEX_INIT; // the hooks calls and the state below
|
||||
static _Atomic int64_t want = -2; // millihertz from the pacer: >0 a limit, 0 none, -1 RTSS kept out; -2 unset
|
||||
static _Atomic int64_t want = RTSS_UNSET; // millihertz from the pacer: >0 a limit, 0 none; RTSS_OUT_* kept out
|
||||
static _Atomic int active; // RTSS limits this process now
|
||||
static _Atomic int failed; // FAIL_*: RTSS is out of it for the rest of the run
|
||||
static _Atomic int closed; // rtss_limit_restore ran: nothing reaches the profile any more
|
||||
static _Atomic int two; // kept out, and RTSS limits the game by a setting of its own (watch)
|
||||
static int started;
|
||||
static char dll_path[MAX_PATH], profile[MAX_PATH], rtss_dir[MAX_PATH];
|
||||
static HMODULE dll;
|
||||
@@ -243,12 +245,76 @@ static const char *fail_reason(int f) {
|
||||
: "RivaTuner Statistics Server's hooks library did not load";
|
||||
}
|
||||
|
||||
// RTSS's own settings for the game, read from its profile file as RTSS keeps it: Profiles\<exe>.cfg, else
|
||||
// Profiles\Global, and a key the game's profile lacks from Global. The system's ini reader does it, so no call goes into
|
||||
// RTSS's hooks library, which a run that keeps RTSS out never loads. The files are read again only when the time
|
||||
// either was written changes. Worker thread only.
|
||||
static RtssSettings settings;
|
||||
static char settings_file[MAX_PATH + 16]; // which file they came from, as Profiles\<name>
|
||||
|
||||
static uint64_t file_stamp(const char *path) { // 0: no such file
|
||||
WIN32_FILE_ATTRIBUTE_DATA a;
|
||||
if (!GetFileAttributesExA(path, GetFileExInfoStandard, &a)) return 0;
|
||||
return ((uint64_t)a.ftLastWriteTime.dwHighDateTime << 32 | a.ftLastWriteTime.dwLowDateTime) | 1;
|
||||
}
|
||||
|
||||
static int cfg_int(const char *own, const char *global, const char *key) {
|
||||
char v[32] = "";
|
||||
if (own) GetPrivateProfileStringA("Framerate", key, "", v, sizeof v, own);
|
||||
if (!*v) GetPrivateProfileStringA("Framerate", key, "", v, sizeof v, global);
|
||||
return (int)strtol(v, NULL, 10); // GetPrivateProfileIntA would read a negative scanline as 0
|
||||
}
|
||||
|
||||
static void read_settings(void) {
|
||||
static uint64_t stamps[2] = {~0ull, ~0ull}; // never read
|
||||
char own[MAX_PATH * 2], global[MAX_PATH * 2];
|
||||
snprintf(own, sizeof own, "%s\\Profiles\\%s.cfg", rtss_dir, profile);
|
||||
snprintf(global, sizeof global, "%s\\Profiles\\Global", rtss_dir);
|
||||
uint64_t a = file_stamp(own), b = file_stamp(global);
|
||||
if (a == stamps[0] && b == stamps[1]) return;
|
||||
stamps[0] = a, stamps[1] = b;
|
||||
const char *p = a ? own : NULL;
|
||||
settings = (RtssSettings){cfg_int(p, global, "Limit"), cfg_int(p, global, "LimitDenominator"),
|
||||
cfg_int(p, global, "LimitTime"), cfg_int(p, global, "LimitTimeDenominator"),
|
||||
cfg_int(p, global, "SyncScanline0"), cfg_int(p, global, "SyncScanline1")};
|
||||
if (a) snprintf(settings_file, sizeof settings_file, "Profiles\\%s.cfg", profile);
|
||||
else snprintf(settings_file, sizeof settings_file, "Profiles\\Global");
|
||||
}
|
||||
|
||||
// While RTSS is kept out (w < 0) the game's own limiter, the driver's low latency or frame generation holds the frames,
|
||||
// and a limit the player set in RTSS itself for the game (its global profile or the game's own, a frame time limit or
|
||||
// scanline sync) still holds them a second time. Logged once per change and shown in the menu (rtss_limit_state 4).
|
||||
static void watch(int64_t w, int listed) {
|
||||
static int said, lines;
|
||||
static char said_text[128];
|
||||
int on = 0;
|
||||
char text[128] = "";
|
||||
if (w < 0 && listed) {
|
||||
read_settings();
|
||||
on = rtss_settings_limit(&settings);
|
||||
rtss_settings_text(text, sizeof text, &settings);
|
||||
}
|
||||
if (w >= 0) said = 0; // a later kept-out want logs afresh
|
||||
else if (on != said || (on && strcmp(text, said_text))) {
|
||||
if (lines++ < 40) {
|
||||
if (on)
|
||||
rt_log("frame rate: two limiters: RivaTuner Statistics Server's own setting for the game (%s: %s) limits "
|
||||
"it beside %s", settings_file, text, rtss_out_limiter(w));
|
||||
else
|
||||
rt_log("frame rate: RivaTuner Statistics Server no longer limits the game beside %s", rtss_out_limiter(w));
|
||||
}
|
||||
said = on;
|
||||
snprintf(said_text, sizeof said_text, "%s", text);
|
||||
}
|
||||
atomic_store(&two, on);
|
||||
}
|
||||
|
||||
static void worker(void *arg) {
|
||||
(void)arg;
|
||||
int was_running = -1, was_hooked = 0, over = 0;
|
||||
for (;; plat_sleep_ms(POLL_MS)) {
|
||||
int64_t w = atomic_load(&want);
|
||||
if (w == -2) continue;
|
||||
if (w == RTSS_UNSET) continue;
|
||||
plat_mutex_lock(&lock);
|
||||
if (atomic_load(&closed)) { // a want read before rtss_limit_restore must not reach the profile after it
|
||||
plat_mutex_unlock(&lock);
|
||||
@@ -265,7 +331,9 @@ static void worker(void *arg) {
|
||||
}
|
||||
if (w < 0) {
|
||||
if (dll) put_back();
|
||||
set_active(0, "RivaTuner Statistics Server kept out: NX2NAVI_RTSS=0 or frame generation");
|
||||
char why[96];
|
||||
snprintf(why, sizeof why, "RivaTuner Statistics Server kept out: %s", rtss_out_text(w));
|
||||
set_active(0, why);
|
||||
} else if (f) {
|
||||
set_active(0, fail_reason(f));
|
||||
} else if (!run) {
|
||||
@@ -294,11 +362,12 @@ static void worker(void *arg) {
|
||||
set_active(0, fail_reason(FAIL_NOT_LIMITING));
|
||||
}
|
||||
}
|
||||
watch(w, run && hooked);
|
||||
plat_mutex_unlock(&lock);
|
||||
}
|
||||
}
|
||||
|
||||
static void start(void) {
|
||||
static void start(int64_t first) {
|
||||
started = 1;
|
||||
char dir[MAX_PATH];
|
||||
DWORD size = sizeof dir;
|
||||
@@ -320,14 +389,17 @@ static void start(void) {
|
||||
for (const char *s = exe; *s; s++)
|
||||
if (*s == '\\' || *s == '/') base = s + 1;
|
||||
snprintf(profile, sizeof profile, "%s", base); // RTSS names a profile after the executable, as ocarina.exe
|
||||
rt_log("frame rate: RivaTuner Statistics Server found in %s; it takes the frame limit while it runs", dir);
|
||||
if (first >= 0)
|
||||
rt_log("frame rate: RivaTuner Statistics Server found in %s; it takes the frame limit while it runs", dir);
|
||||
else
|
||||
rt_log("frame rate: RivaTuner Statistics Server found in %s; kept out (%s), so its thread only reads RTSS's own "
|
||||
"settings for the game, to tell when RTSS limits it a second time", dir, rtss_out_text(first));
|
||||
if (!plat_thread_spawn(worker, NULL, "rtss", 0)) rt_log("frame rate: the RivaTuner Statistics Server thread did not start");
|
||||
}
|
||||
|
||||
void rtss_limit_want(int64_t mhz) {
|
||||
if (atomic_load_explicit(&closed, memory_order_relaxed)) return; // the exit put the profile back
|
||||
if (!started && mhz < 0) return; // kept out from the start, for the whole run (both reasons need a restart)
|
||||
if (!started) start();
|
||||
if (!started) start(mhz); // a first want that keeps RTSS out starts the worker too, which then only reads (watch)
|
||||
// a new limit: the game's own limiter holds it until the worker had RTSS apply it, up to POLL_MS later (a movie's
|
||||
// cap that waited for that ran the movie's 60 Hz count fast for half a second)
|
||||
if (atomic_exchange(&want, mhz) != mhz) atomic_store(&active, 0);
|
||||
@@ -340,8 +412,10 @@ int rtss_limit_active(void) {
|
||||
int rtss_limit_state(void) {
|
||||
if (atomic_load_explicit(&closed, memory_order_relaxed)) return 0;
|
||||
int f = atomic_load_explicit(&failed, memory_order_relaxed);
|
||||
if (atomic_load(&want) >= 0 && (f == FAIL_READONLY || f == FAIL_NOT_LIMITING)) return f == FAIL_READONLY ? 2 : 3;
|
||||
return rtss_limit_active() && atomic_load(&want) > 0;
|
||||
int64_t w = atomic_load(&want);
|
||||
if (w >= 0 && (f == FAIL_READONLY || f == FAIL_NOT_LIMITING)) return f == FAIL_READONLY ? 2 : 3;
|
||||
if (w < 0 && w != RTSS_UNSET && atomic_load_explicit(&two, memory_order_relaxed)) return 4;
|
||||
return rtss_limit_active() && w > 0;
|
||||
}
|
||||
|
||||
void rtss_limit_restore(void) {
|
||||
|
||||
@@ -861,6 +861,7 @@ static void row_fps_limit() {
|
||||
const char *note = rtss == 1 ? menu_text(UI_MENU_FPS_RTSS)
|
||||
: rtss == 2 ? menu_text(UI_MENU_FPS_RTSS_READONLY)
|
||||
: rtss == 3 ? menu_text(UI_MENU_FPS_RTSS_IGNORED)
|
||||
: rtss == 4 ? menu_text(UI_MENU_FPS_RTSS_TWO)
|
||||
: nullptr;
|
||||
opt_choice_list("NX2NAVI_FPS_LIMIT", UI_MENU_FPS_LIMIT, vals, names, n, UI_MENU_FPS_LIMIT_DESC, cur, note);
|
||||
}
|
||||
|
||||
@@ -278,6 +278,7 @@ static const char *const text_en[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_FPS_RTSS] = "limited by RivaTuner Statistics Server",
|
||||
[UI_MENU_FPS_RTSS_READONLY] = "RivaTuner needs the game run as administrator",
|
||||
[UI_MENU_FPS_RTSS_IGNORED] = "RivaTuner does not limit this game; the game does",
|
||||
[UI_MENU_FPS_RTSS_TWO] = "RivaTuner's own limit also applies: two limiters",
|
||||
[UI_MENU_REALTIME] = "Game speed follows the clock",
|
||||
[UI_MENU_REALTIME_DESC] = "Steps the game by real time, so a slow frame does not slow the game down.",
|
||||
[UI_MENU_CLOTH_REALTIME] = "Cloth on the clock",
|
||||
@@ -914,6 +915,7 @@ static const char *const text_ru[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_FPS_RTSS] = "ограничивает RivaTuner Statistics Server",
|
||||
[UI_MENU_FPS_RTSS_READONLY] = "RivaTuner нужен запуск игры от администратора",
|
||||
[UI_MENU_FPS_RTSS_IGNORED] = "RivaTuner не ограничивает эту игру; ограничивает игра",
|
||||
[UI_MENU_FPS_RTSS_TWO] = "действует и ограничение самого RivaTuner: два ограничителя",
|
||||
[UI_MENU_REALTIME] = "Скорость игры по часам",
|
||||
[UI_MENU_REALTIME_DESC] = "Игра идёт по реальному времени, медленный кадр её не замедляет.",
|
||||
[UI_MENU_CLOTH_REALTIME] = "Ткань по часам",
|
||||
@@ -1424,6 +1426,7 @@ static const char *const text_pt_br[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_FPS_RTSS] = "limitado pelo RivaTuner Statistics Server",
|
||||
[UI_MENU_FPS_RTSS_READONLY] = "o RivaTuner pede o jogo como administrador",
|
||||
[UI_MENU_FPS_RTSS_IGNORED] = "o RivaTuner não limita este jogo; o jogo limita",
|
||||
[UI_MENU_FPS_RTSS_TWO] = "o limite do próprio RivaTuner também vale: dois limitadores",
|
||||
[UI_MENU_REALTIME] = "Velocidade do jogo pelo relógio",
|
||||
[UI_MENU_REALTIME_DESC] = "Avança o jogo pelo tempo real, para que um quadro lento não deixe o jogo mais lento.",
|
||||
[UI_MENU_CLOTH_REALTIME] = "Tecidos pelo relógio",
|
||||
@@ -2027,6 +2030,7 @@ static const char *const text_zh_hans[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_FPS_RTSS] = "由 RivaTuner Statistics Server 限制",
|
||||
[UI_MENU_FPS_RTSS_READONLY] = "RivaTuner 需要以管理员身份运行游戏",
|
||||
[UI_MENU_FPS_RTSS_IGNORED] = "RivaTuner 未限制本游戏;由游戏自行限制",
|
||||
[UI_MENU_FPS_RTSS_TWO] = "RivaTuner 自身的限制也在生效:两个限制器",
|
||||
[UI_MENU_REALTIME] = "游戏速度跟随时钟",
|
||||
[UI_MENU_REALTIME_DESC] = "按真实时间推进游戏,帧率低时游戏速度也不会变慢。",
|
||||
[UI_MENU_CLOTH_REALTIME] = "布料跟随时钟",
|
||||
|
||||
@@ -73,6 +73,7 @@ typedef enum {
|
||||
UI_MENU_FPS_LIMIT, UI_MENU_FPS_LIMIT_DESC, UI_MENU_FPS_30, UI_MENU_FPS_40, UI_MENU_FPS_45, UI_MENU_FPS_60, UI_MENU_FPS_90,
|
||||
UI_MENU_FPS_120, UI_MENU_FPS_144,
|
||||
UI_MENU_FPS_DISPLAY, UI_MENU_FPS_DISPLAY_HZ, UI_MENU_FPS_UNLIMITED, UI_MENU_FPS_RTSS, UI_MENU_FPS_RTSS_READONLY, UI_MENU_FPS_RTSS_IGNORED,
|
||||
UI_MENU_FPS_RTSS_TWO,
|
||||
UI_MENU_REALTIME, UI_MENU_REALTIME_DESC,
|
||||
UI_MENU_CLOTH_REALTIME, UI_MENU_CLOTH_REALTIME_DESC,
|
||||
UI_MENU_HIGHFPS_FIX, UI_MENU_HIGHFPS_FIX_DESC,
|
||||
|
||||
55
tests/rtss_limit_check.c
Normal file
55
tests/rtss_limit_check.c
Normal file
@@ -0,0 +1,55 @@
|
||||
// runtime/rtss_decide.h: the parts of RivaTuner Statistics Server's frame limiting (rtss_limit.c) that make no system
|
||||
// call. Built and run by tests/rtss_limit_check.py on any system; prints each failed check and exits 1 on any.
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "rtss_decide.h"
|
||||
|
||||
static int fails;
|
||||
#define CHECK(c, ...) do { if (!(c)) { fails++; printf("FAIL line %d: ", __LINE__); printf(__VA_ARGS__); printf("\n"); } } while (0)
|
||||
|
||||
// RTSS's own settings: which of them hold the game's frames, and the log's text of them.
|
||||
static void settings(void) {
|
||||
RtssSettings none = {0, 1, 0, 0, 0, 0};
|
||||
CHECK(!rtss_settings_limit(&none), "no limit, no frame time limit, no scanline sync: RTSS holds nothing");
|
||||
RtssSettings s = none;
|
||||
s.limit = 50;
|
||||
CHECK(rtss_settings_limit(&s), "a frame rate limit holds the frames");
|
||||
s = none, s.time = 16667, s.time_den = 1;
|
||||
CHECK(rtss_settings_limit(&s), "a frame time limit holds the frames");
|
||||
s = none, s.scan0 = -40;
|
||||
CHECK(rtss_settings_limit(&s), "scanline sync from the bottom holds the frames");
|
||||
s = none, s.scan1 = 1;
|
||||
CHECK(rtss_settings_limit(&s), "the second scanline sync holds the frames");
|
||||
s = none, s.limit_den = 1000, s.time_den = 7;
|
||||
CHECK(!rtss_settings_limit(&s), "denominators alone hold nothing");
|
||||
char text[128];
|
||||
s = (RtssSettings){59940, 1000, 16667, 1, -40, 0};
|
||||
rtss_settings_text(text, sizeof text, &s);
|
||||
CHECK(!strcmp(text, "limit 59940/1000, frame time limit 16667/1, scanline sync -40/0"), "text: %s", text);
|
||||
char small[16];
|
||||
rtss_settings_text(small, sizeof small, &s);
|
||||
CHECK(strlen(small) == sizeof small - 1, "a short buffer is cut, not overrun");
|
||||
}
|
||||
|
||||
// The kept-out reasons, for the log.
|
||||
static void reasons(void) {
|
||||
CHECK(!strcmp(rtss_out_text(RTSS_OUT_SETTING), "NX2NAVI_RTSS=0"), "the setting");
|
||||
CHECK(!strcmp(rtss_out_text(RTSS_OUT_FRAMEGEN), "frame generation"), "frame generation");
|
||||
CHECK(!strcmp(rtss_out_text(RTSS_OUT_LOWLAT), "the driver's low latency"), "low latency");
|
||||
CHECK(!strcmp(rtss_out_limiter(RTSS_OUT_SETTING), "the game's limiter"), "the setting leaves the game's limiter");
|
||||
CHECK(!strcmp(rtss_out_limiter(RTSS_OUT_LOWLAT), "the driver's low latency"), "low latency holds the frames");
|
||||
CHECK(RTSS_UNSET < 0 && RTSS_OUT_SETTING < 0 && RTSS_OUT_FRAMEGEN < 0 && RTSS_OUT_LOWLAT < 0,
|
||||
"every reason is below 0, where a limit never is");
|
||||
CHECK(RTSS_UNSET != RTSS_OUT_SETTING && RTSS_UNSET != RTSS_OUT_FRAMEGEN && RTSS_UNSET != RTSS_OUT_LOWLAT &&
|
||||
RTSS_OUT_SETTING != RTSS_OUT_FRAMEGEN && RTSS_OUT_FRAMEGEN != RTSS_OUT_LOWLAT &&
|
||||
RTSS_OUT_SETTING != RTSS_OUT_LOWLAT,
|
||||
"the reasons differ");
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
settings();
|
||||
reasons();
|
||||
printf("rtss_limit_check: %s\n", fails ? "FAIL" : "ok");
|
||||
return fails != 0;
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
"""Build and run the checks of runtime/rtss_limit.c, RivaTuner Statistics Server as the frame limiter.
|
||||
|
||||
tests/rtss_worker_check.c (Windows only, skipped elsewhere) compiles the real rtss_limit.c against a fake RTSS and runs
|
||||
each of its scenarios in a process of its own.
|
||||
tests/rtss_limit_check.c builds runtime/rtss_decide.h alone, the parts that make no system call. tests/rtss_worker_check.c
|
||||
(Windows only, skipped elsewhere) compiles the real rtss_limit.c against a fake RTSS and runs each of its scenarios in a
|
||||
process of its own.
|
||||
|
||||
No GPU, no game files, no RTSS, any Python.
|
||||
|
||||
@@ -38,9 +39,13 @@ def main():
|
||||
if os.name == 'nt' and MSYS_BIN.exists():
|
||||
env['PATH'] = str(MSYS_BIN) + os.pathsep + env.get('PATH', '')
|
||||
failed = []
|
||||
pure = work / ('rtss_limit_check.exe' if os.name == 'nt' else 'rtss_limit_check')
|
||||
build('rtss_limit_check.c', pure, env)
|
||||
if subprocess.run([str(pure)], env=env).returncode != 0:
|
||||
failed.append('rtss_limit_check')
|
||||
if os.name != 'nt':
|
||||
print('rtss_worker_check: skipped (rtss_limit.c limits only on Windows)')
|
||||
return 0
|
||||
return 1 if failed else 0
|
||||
exe = work / 'rtss_worker_check.exe'
|
||||
build('rtss_worker_check.c', exe, env)
|
||||
names = subprocess.run([str(exe), '--list'], env=env, capture_output=True, text=True, check=True).stdout.split()
|
||||
@@ -54,7 +59,7 @@ def main():
|
||||
if not ok:
|
||||
print(out)
|
||||
failed.append(name)
|
||||
print(f'{len(names) - len(failed)} of {len(names)} scenarios ok' + (f'; failed: {", ".join(failed)}' if failed else ''))
|
||||
print(f'{len(names) + 1 - len(failed)} of {len(names) + 1} checks ok' + (f'; failed: {", ".join(failed)}' if failed else ''))
|
||||
return 1 if failed else 0
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// The real runtime/rtss_limit.c against a fake RivaTuner Statistics Server: the Win32 calls it makes go to the fakes
|
||||
// below (its folder in the registry, its hooks library with the profile calls, the profiles, the shared memory
|
||||
// below (its folder in the registry, its hooks library with the profile calls, the profile files, the shared memory
|
||||
// RTSSSharedMemoryV2), and its worker thread polls when a scenario says so (plat_sleep_ms on that thread waits for the
|
||||
// next poll). Each scenario runs in a process of its own, since rtss_limit.c's state lasts the process. Windows only;
|
||||
// built and run by tests/rtss_limit_check.py. Prints the log and each failed check, and exits 1 on any.
|
||||
@@ -22,6 +22,8 @@ BOOL stub_UnmapViewOfFile(LPCVOID);
|
||||
SIZE_T stub_VirtualQuery(LPCVOID, PMEMORY_BASIC_INFORMATION, SIZE_T);
|
||||
BOOL stub_CloseHandle(HANDLE);
|
||||
DWORD stub_GetFileAttributesA(LPCSTR);
|
||||
BOOL stub_GetFileAttributesExA(LPCSTR, GET_FILEEX_INFO_LEVELS, LPVOID);
|
||||
DWORD stub_GetPrivateProfileStringA(LPCSTR, LPCSTR, LPCSTR, LPSTR, DWORD, LPCSTR);
|
||||
DWORD stub_GetModuleFileNameA(HMODULE, LPSTR, DWORD);
|
||||
DWORD stub_GetCurrentProcessId(void);
|
||||
#define RegGetValueA stub_RegGetValueA
|
||||
@@ -34,6 +36,8 @@ DWORD stub_GetCurrentProcessId(void);
|
||||
#define VirtualQuery stub_VirtualQuery
|
||||
#define CloseHandle stub_CloseHandle
|
||||
#define GetFileAttributesA stub_GetFileAttributesA
|
||||
#define GetFileAttributesExA stub_GetFileAttributesExA
|
||||
#define GetPrivateProfileStringA stub_GetPrivateProfileStringA
|
||||
#define GetModuleFileNameA stub_GetModuleFileNameA
|
||||
#define GetCurrentProcessId stub_GetCurrentProcessId
|
||||
|
||||
@@ -42,44 +46,59 @@ DWORD stub_GetCurrentProcessId(void);
|
||||
static int fails;
|
||||
#define CHECK(c, ...) do { if (!(c)) { fails++; printf("FAIL line %d: ", __LINE__); printf(__VA_ARGS__); printf("\n"); } } while (0)
|
||||
|
||||
// ---- the fake profiles: Global, and the game's own Profiles\ocarina.exe.cfg; LoadProfile copies one into the loaded copy
|
||||
typedef struct { int exists; DWORD limit, denom; } Prof;
|
||||
static Prof global = {1, 0, 1}, own;
|
||||
static DWORD loaded_limit, loaded_denom;
|
||||
// ---- the fake profiles: Global, and the game's own Profiles\ocarina.exe.cfg. LoadProfile copies one into the loaded
|
||||
// copy (Global for a name without its own), SaveProfile writes the loaded copy as the game's own. Each [Framerate] key
|
||||
// rtss_limit.c reads is a field; stamp is the file's write time, which every change moves on.
|
||||
typedef struct {
|
||||
int exists;
|
||||
DWORD limit, denom; // FramerateLimit and FramerateLimitDenominator: Limit and LimitDenominator in the file
|
||||
int time, time_den, scan0, scan1;
|
||||
DWORD stamp;
|
||||
} Prof;
|
||||
static Prof global = {1, 0, 1, 0, 0, 0, 0, 1}, own, loaded;
|
||||
static int no_denominator; // an RTSS that does not know FramerateLimitDenominator
|
||||
static int readonly; // SaveProfile does not reach the file (no administrator rights)
|
||||
static volatile LONG saves, deletes, updates, loads, created, removed;
|
||||
|
||||
static void touch(Prof *p) { p->stamp++; }
|
||||
static void WINAPI f_load(LPCSTR name) {
|
||||
(void)name;
|
||||
const Prof *p = own.exists ? &own : &global;
|
||||
loaded_limit = p->limit, loaded_denom = p->denom;
|
||||
loaded = own.exists ? own : global;
|
||||
}
|
||||
static void WINAPI f_save(LPCSTR name) {
|
||||
(void)name;
|
||||
saves++;
|
||||
if (!readonly) own.exists = 1, own.limit = loaded_limit, own.denom = loaded_denom;
|
||||
if (readonly) return;
|
||||
DWORD stamp = own.stamp;
|
||||
own = loaded, own.exists = 1, own.stamp = stamp;
|
||||
touch(&own);
|
||||
}
|
||||
static void WINAPI f_delete(LPCSTR name) {
|
||||
(void)name;
|
||||
deletes++;
|
||||
if (!readonly) own.exists = 0;
|
||||
if (!readonly) own.exists = 0, touch(&own);
|
||||
}
|
||||
static BOOL WINAPI f_get(LPCSTR prop, LPBYTE data, DWORD size) {
|
||||
if (size != 4) return FALSE;
|
||||
if (!strcmp(prop, "FramerateLimit")) return memcpy(data, &loaded_limit, 4), TRUE;
|
||||
if (!strcmp(prop, "FramerateLimitDenominator") && !no_denominator) return memcpy(data, &loaded_denom, 4), TRUE;
|
||||
if (!strcmp(prop, "FramerateLimit")) return memcpy(data, &loaded.limit, 4), TRUE;
|
||||
if (!strcmp(prop, "FramerateLimitDenominator") && !no_denominator) return memcpy(data, &loaded.denom, 4), TRUE;
|
||||
return FALSE;
|
||||
}
|
||||
static BOOL WINAPI f_set(LPCSTR prop, LPBYTE data, DWORD size) {
|
||||
if (size != 4) return FALSE;
|
||||
if (!strcmp(prop, "FramerateLimit")) return memcpy(&loaded_limit, data, 4), TRUE;
|
||||
if (!strcmp(prop, "FramerateLimitDenominator") && !no_denominator) return memcpy(&loaded_denom, data, 4), TRUE;
|
||||
if (!strcmp(prop, "FramerateLimit")) return memcpy(&loaded.limit, data, 4), TRUE;
|
||||
if (!strcmp(prop, "FramerateLimitDenominator") && !no_denominator) return memcpy(&loaded.denom, data, 4), TRUE;
|
||||
return FALSE;
|
||||
}
|
||||
static void WINAPI f_update(void) { updates++; }
|
||||
static DWORD WINAPI f_enum(LPSTR list, DWORD size) { (void)size; list[0] = 0; return 1; }
|
||||
|
||||
static Prof *file_of(const char *path) {
|
||||
if (strstr(path, "\\Profiles\\ocarina.exe.cfg")) return own.exists ? &own : NULL;
|
||||
if (strstr(path, "\\Profiles\\Global")) return &global;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// ---- the fake shared memory: the header and one application entry, this process's (listed) or another's
|
||||
static uint8_t shm[65536];
|
||||
static volatile LONG running = 1;
|
||||
@@ -135,8 +154,32 @@ SIZE_T stub_VirtualQuery(LPCVOID p, PMEMORY_BASIC_INFORMATION mi, SIZE_T n) {
|
||||
BOOL stub_CloseHandle(HANDLE h) { (void)h; return TRUE; }
|
||||
DWORD stub_GetFileAttributesA(LPCSTR p) {
|
||||
if (strstr(p, "RTSSHooks64.dll")) return FILE_ATTRIBUTE_NORMAL;
|
||||
if (strstr(p, "\\Profiles\\ocarina.exe.cfg")) return own.exists ? FILE_ATTRIBUTE_NORMAL : INVALID_FILE_ATTRIBUTES;
|
||||
return INVALID_FILE_ATTRIBUTES;
|
||||
return file_of(p) ? FILE_ATTRIBUTE_NORMAL : INVALID_FILE_ATTRIBUTES;
|
||||
}
|
||||
BOOL stub_GetFileAttributesExA(LPCSTR p, GET_FILEEX_INFO_LEVELS level, LPVOID out) {
|
||||
(void)level;
|
||||
const Prof *f = file_of(p);
|
||||
if (!f) return FALSE;
|
||||
WIN32_FILE_ATTRIBUTE_DATA *a = out;
|
||||
memset(a, 0, sizeof *a);
|
||||
a->ftLastWriteTime.dwLowDateTime = f->stamp;
|
||||
return TRUE;
|
||||
}
|
||||
DWORD stub_GetPrivateProfileStringA(LPCSTR sec, LPCSTR key, LPCSTR def, LPSTR out, DWORD n, LPCSTR path) {
|
||||
const Prof *f = file_of(path);
|
||||
long v = 0;
|
||||
int have = f && !strcmp(sec, "Framerate");
|
||||
if (have) {
|
||||
if (!strcmp(key, "Limit")) v = (long)f->limit;
|
||||
else if (!strcmp(key, "LimitDenominator")) v = (long)f->denom;
|
||||
else if (!strcmp(key, "LimitTime")) v = f->time;
|
||||
else if (!strcmp(key, "LimitTimeDenominator")) v = f->time_den;
|
||||
else if (!strcmp(key, "SyncScanline0")) v = f->scan0;
|
||||
else if (!strcmp(key, "SyncScanline1")) v = f->scan1;
|
||||
else have = 0;
|
||||
}
|
||||
if (have) return (DWORD)snprintf(out, n, "%ld", v);
|
||||
return (DWORD)snprintf(out, n, "%s", def);
|
||||
}
|
||||
DWORD stub_GetModuleFileNameA(HMODULE m, LPSTR out, DWORD n) {
|
||||
(void)m;
|
||||
@@ -145,7 +188,8 @@ DWORD stub_GetModuleFileNameA(HMODULE m, LPSTR out, DWORD n) {
|
||||
DWORD stub_GetCurrentProcessId(void) { return PID; }
|
||||
|
||||
// ---- host.h and platform.h, as far as rtss_limit.c uses them
|
||||
static char last_line[512];
|
||||
static char logged_lines[1 << 16];
|
||||
static size_t logged_at;
|
||||
void rt_log(const char *fmt, ...) {
|
||||
char line[512];
|
||||
va_list ap;
|
||||
@@ -154,9 +198,11 @@ void rt_log(const char *fmt, ...) {
|
||||
va_end(ap);
|
||||
if (strstr(line, " created;")) created++;
|
||||
if (strstr(line, " removed,")) removed++;
|
||||
snprintf(last_line, sizeof last_line, "%s", line);
|
||||
int k = snprintf(logged_lines + logged_at, sizeof logged_lines - logged_at, "%s\n", line);
|
||||
if (k > 0 && logged_at + (size_t)k < sizeof logged_lines) logged_at += (size_t)k;
|
||||
printf(" log: %s\n", line);
|
||||
}
|
||||
static int logged(const char *text) { return strstr(logged_lines, text) != NULL; }
|
||||
int rt_knob(const char *name, const char *def) { (void)name, (void)def; return 0; }
|
||||
|
||||
static DWORD worker_tid;
|
||||
@@ -207,9 +253,9 @@ static void ask(int64_t mhz) { // a want at a present; the first one that star
|
||||
if (spawned && !was) CHECK(wait_done(), "the worker did not finish its first poll");
|
||||
}
|
||||
static void show(const char *what) {
|
||||
printf(" %s: profile %s %lu/%lu; active %d; state %d; saves %ld deletes %ld updates %ld\n", what,
|
||||
printf(" %s: profile %s %lu/%lu; active %d; state %d; saves %ld deletes %ld updates %ld loads %ld\n", what,
|
||||
own.exists ? "exists," : "absent,", own.limit, own.denom, rtss_limit_active(), rtss_limit_state(), saves,
|
||||
deletes, updates);
|
||||
deletes, updates, loads);
|
||||
}
|
||||
|
||||
// The exit puts the profile back (rtss_limit_restore) and the game presents on until the process ends: a want after it
|
||||
@@ -235,7 +281,7 @@ static void restore_race(void) {
|
||||
|
||||
// The same with the player's own profile: it keeps the player's limit.
|
||||
static void restore_player(void) {
|
||||
own = (Prof){1, 30, 1};
|
||||
own = (Prof){1, 30, 1, 0, 0, 0, 0, 1};
|
||||
app_set(1, 0xA, 1000, 2000, 60);
|
||||
ask(60000);
|
||||
poll();
|
||||
@@ -265,7 +311,7 @@ static void restore_inflight(void) {
|
||||
CHECK(created == 1, "the profile was created %ld times", created);
|
||||
}
|
||||
|
||||
// An exit before the first want, and one while RTSS was kept out: a want after it does not start the worker.
|
||||
// An exit before the first want, and one while RTSS was kept out: a want after it does not reach the profile.
|
||||
static void restore_first(void) {
|
||||
app_set(1, 0xA, 1000, 2000, 60);
|
||||
rtss_limit_restore();
|
||||
@@ -277,7 +323,7 @@ static void restore_first(void) {
|
||||
}
|
||||
static void restore_kept_out(void) {
|
||||
app_set(1, 0xA, 1000, 2000, 60);
|
||||
ask(-1);
|
||||
ask(RTSS_OUT_SETTING);
|
||||
rtss_limit_restore();
|
||||
ask(60000);
|
||||
if (spawned) poll();
|
||||
@@ -286,9 +332,72 @@ static void restore_kept_out(void) {
|
||||
CHECK(!rtss_limit_active(), "the game's limiter paces");
|
||||
}
|
||||
|
||||
// NX2NAVI_RTSS=0 with a limit set in RTSS's Global profile: the worker only reads, never loads the hooks library, and
|
||||
// tells of the two limiters until the player clears that limit.
|
||||
static void kept_two(void) {
|
||||
global.limit = 50;
|
||||
app_set(1, 0xA, 1000, 2000, 50);
|
||||
ask(RTSS_OUT_SETTING);
|
||||
poll();
|
||||
show("kept out by the setting, Global limit 50");
|
||||
CHECK(spawned, "a kept-out first want starts the worker");
|
||||
CHECK(!loads && !saves && !deletes, "kept out, the hooks library is never loaded and no profile written");
|
||||
CHECK(rtss_limit_state() == 4 && !rtss_limit_active(), "the menu says two limiters; the game's limiter paces");
|
||||
CHECK(logged("kept out: NX2NAVI_RTSS=0"), "the kept-out line names the setting");
|
||||
CHECK(logged("two limiters: RivaTuner Statistics Server's own setting for the game (Profiles\\Global: limit 50/1, "
|
||||
"frame time limit 0/0, scanline sync 0/0) limits it beside the game's limiter"),
|
||||
"the two-limiter line names the file and the values");
|
||||
global.limit = 0, touch(&global);
|
||||
poll();
|
||||
show("the player cleared RTSS's limit");
|
||||
CHECK(rtss_limit_state() == 0, "one limiter again");
|
||||
CHECK(logged("RivaTuner Statistics Server no longer limits the game beside the game's limiter"), "logged once it ended");
|
||||
}
|
||||
|
||||
// Low latency keeps RTSS out from the first present, with scanline sync set in the game's own profile (a negative line
|
||||
// counts from the bottom); once low latency is off, a limit starts RTSS's part as usual.
|
||||
static void kept_lowlat_then_limit(void) {
|
||||
own = (Prof){1, 0, 1, 0, 0, -40, 0, 1};
|
||||
app_set(0, 0xA, 1000, 2000, 60);
|
||||
ask(RTSS_OUT_LOWLAT);
|
||||
poll();
|
||||
CHECK(rtss_limit_state() == 0, "not listed yet: RTSS holds nothing");
|
||||
app_set(1, 0xA, 1000, 2000, 60);
|
||||
poll();
|
||||
show("listed, kept out by low latency");
|
||||
CHECK(rtss_limit_state() == 4, "scanline sync -40 holds the game: two limiters");
|
||||
CHECK(logged("kept out: the driver's low latency"), "the kept-out line names low latency");
|
||||
CHECK(logged("(Profiles\\ocarina.exe.cfg: limit 0/1, frame time limit 0/0, scanline sync -40/0) limits it beside "
|
||||
"the driver's low latency"),
|
||||
"the two-limiter line names the game's profile and low latency");
|
||||
CHECK(!loads, "kept out, the hooks library is never loaded");
|
||||
ask(60000);
|
||||
poll();
|
||||
show("low latency off, limit 60");
|
||||
CHECK(loads == 1 && own.exists && own.limit == 60, "the limit goes into the profile once RTSS is let in");
|
||||
CHECK(rtss_limit_state() == 1 && rtss_limit_active(), "RTSS limits the game");
|
||||
}
|
||||
|
||||
// Frame generation keeps RTSS out, with no limit of RTSS's own: one limiter, and the line names frame generation.
|
||||
static void kept_framegen(void) {
|
||||
app_set(1, 0xA, 1000, 2000, 60);
|
||||
ask(RTSS_OUT_FRAMEGEN);
|
||||
poll();
|
||||
show("kept out by frame generation");
|
||||
CHECK(logged("kept out: frame generation"), "the kept-out line names frame generation");
|
||||
CHECK(!logged("two limiters") && rtss_limit_state() == 0, "no second limiter");
|
||||
CHECK(!loads && !saves, "kept out, the hooks library is never loaded");
|
||||
}
|
||||
|
||||
static const struct { const char *name; void (*run)(void); } scenarios[] = {
|
||||
{"restore_race", restore_race}, {"restore_player", restore_player}, {"restore_inflight", restore_inflight},
|
||||
{"restore_first", restore_first}, {"restore_kept_out", restore_kept_out},
|
||||
{"restore_race", restore_race},
|
||||
{"restore_player", restore_player},
|
||||
{"restore_inflight", restore_inflight},
|
||||
{"restore_first", restore_first},
|
||||
{"restore_kept_out", restore_kept_out},
|
||||
{"kept_two", kept_two},
|
||||
{"kept_lowlat_then_limit", kept_lowlat_then_limit},
|
||||
{"kept_framegen", kept_framegen},
|
||||
};
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
|
||||
Reference in New Issue
Block a user