mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
Move RivaTuner Statistics Server's decisions into a function of their own, and pin them with a check that needs no RTSS.
The worker read RTSS's shared memory, decided and wrote the profile in one loop over file statics, so its decisions could be checked only against a running RTSS. rtss_decide.h now holds the parts that make no system call: rtss_parse reads the shared memory's header and this process's entry (bounded by the memory's size and 4096 entries, as before), rtss_decide takes one poll's view (the want, whether it is still current, a closed run, the hooks library, the entry) and the state between polls (RtssState: the limit applied and the one refused, whether the run changed the profile, the frame count, the failure) and returns what to do and whether RTSS holds the game, and rtss_poll carries that out through the profile calls rtss_limit.c hands it (apply and put_back, which keep their I/O and log lines). The worker, rtss_limit_restore and the menu state read the same state. No decision changes: the order of the rules, the immediate write before RTSS lists the game, six polls over 1.2 times the limit, a write per movie and per switch of low latency all stay as they are. The rules that wait for approval are not in it (the pacer keeping movies and windows with no host present, Vulkan flags, settling and a fresh period before standing aside, rounding the limit down, the state file). tests/rtss_worker_check.c gains nine scenarios that pin those decisions against the real worker (a read-only profile, a refused limit, the frame count rule at 144, 69, 73 fps and unlimited, RTSS quitting, a hooks library that does not load, low latency switching, a movie); the logs of all 23 scenarios are the same, line for line, before and after this change. tests/rtss_limit_check.c checks the same decisions on rtss_decide.h alone, with a fake RTSSSharedMemoryV2 for the reader (a bad header, short entries, an entry past the memory or past the 4096th) and a fake profile behind RtssOps; mutating the frame count's polls or slack, the refusal, the library failure or the clear of a refused limit makes it fail. docs/building.md describes the check. Every runtime file compiles; -Wall adds no warning; frame_step_check passes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -476,6 +476,17 @@ for all three, rounded up by `osolver` and at least 4 every update by `osolverfu
|
||||
game's words and the set's: the page's cut is 100 at 60 and 30 fps, and from 90 to 240 fps the set cuts a floor's
|
||||
velocity at the time scales where the game cuts it at 60 fps and nowhere else. No game files.
|
||||
|
||||
A change to how the frame limit goes to RivaTuner Statistics Server (`runtime/rtss_limit.c`, `runtime/rtss_decide.h`)
|
||||
passes `python tests\rtss_limit_check.py`. It builds `rtss_decide.h` alone and checks its reader of RTSS's shared
|
||||
memory against a fake `RTSSSharedMemoryV2` (a bad header, short entries, an entry past the memory or past the 4096th
|
||||
find nothing) and the worker's decisions poll by poll against a fake profile: RTSS's injection delay, RTSS quitting and
|
||||
coming back, a read-only profile, a refused limit, the frame count rule, kept-out wants and their reasons, low latency
|
||||
switching at every poll, a movie, a closed run, a hooks library that does not load, and two limiters. On Windows it
|
||||
also builds the real `rtss_limit.c` against a fake RTSS (its registry key, hooks library, profile files and shared
|
||||
memory) and runs each scenario in a process of its own, among them a want after the exit's restore and one the worker
|
||||
read before it, a worker polling without pause while the want switches, and the registry and file calls a present
|
||||
must not make. No GPU, no RTSS, no game files.
|
||||
|
||||
A change to how the mixer applies volumes (`runtime/audio_mixer.c` `accumulate_ramp`, the voice and splitter paths
|
||||
of `audio_mixer_render`) or to what it owes the device after a stall (`ring_pull`, `audio_mixer_owed`) passes
|
||||
`python tests\audio_mixer_check.py`, which builds `audio_mixer.c` with the rest of the runtime stubbed. It mixes
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
// 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.
|
||||
// tests/rtss_limit_check.c builds them on any system. rtss_parse reads RTSS's shared memory, rtss_decide decides each
|
||||
// poll of the worker, and rtss_poll carries that decision out through the calls rtss_limit.c gives it (RtssOps).
|
||||
#pragma once
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.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.
|
||||
@@ -28,6 +30,190 @@ static inline const char *rtss_out_limiter(int64_t want) {
|
||||
: "the game's limiter";
|
||||
}
|
||||
|
||||
// ---- RTSS's shared memory, "RTSSSharedMemoryV2". The header: signature, version, application entry size, offset of
|
||||
// the application array, its length. An entry: the process id, the executable path (260 bytes), flags, then RTSS's
|
||||
// last frame rate period for the process: its start and end in ms and the frames in it.
|
||||
#define RTSS_SIGNATURE 0x52545353u // 'RTSS' as RTSS writes it: the memory holds valid data
|
||||
enum { RTSS_PERIOD_AT = 4 + 260 + 4, RTSS_ENTRY_MIN = RTSS_PERIOD_AT + 12, RTSS_APPS_MAX = 4096 };
|
||||
|
||||
typedef struct {
|
||||
int running; // the signature and a version of 2.0 or later
|
||||
int listed; // the application list holds this process: RTSS has hooked it
|
||||
uint32_t t0, t1, frames; // the period of this process's entry
|
||||
} RtssApp;
|
||||
|
||||
// Reads the memory of size bytes at p for the process pid; never reads past size.
|
||||
static inline void rtss_parse(const uint8_t *p, size_t size, uint32_t pid, RtssApp *a) {
|
||||
memset(a, 0, sizeof *a);
|
||||
if (!p || size < 20) return;
|
||||
uint32_t h[5];
|
||||
memcpy(h, p, sizeof h);
|
||||
a->running = h[0] == RTSS_SIGNATURE && h[1] >= 0x20000;
|
||||
for (uint32_t i = 0; a->running && h[2] >= RTSS_ENTRY_MIN && i < h[4] && i < RTSS_APPS_MAX; i++) {
|
||||
uint64_t at = (uint64_t)h[3] + (uint64_t)i * h[2];
|
||||
if (at + RTSS_ENTRY_MIN > size) break;
|
||||
uint32_t id, t[3];
|
||||
memcpy(&id, p + at, 4);
|
||||
if (id != pid) continue;
|
||||
a->listed = 1;
|
||||
memcpy(t, p + at + RTSS_PERIOD_AT, sizeof t);
|
||||
a->t0 = t[0], a->t1 = t[1], a->frames = t[2];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// The frame rate RTSS measured for the process over its period; 0 unknown.
|
||||
static inline double rtss_fps(const RtssApp *a) {
|
||||
return a->listed && a->t1 > a->t0 ? 1000.0 * a->frames / (a->t1 - a->t0) : 0;
|
||||
}
|
||||
|
||||
// ---- the worker's decisions
|
||||
#define RTSS_CHECK_POLLS 6 // polls in a row RTSS's own frame rate of the game may stay above the limit
|
||||
#define RTSS_CHECK_SLACK 1.2 // above limit * this, RTSS is taken not to hold the game to it
|
||||
|
||||
enum { RTSS_FAIL_NONE, RTSS_FAIL_LIBRARY, RTSS_FAIL_READONLY, RTSS_FAIL_NOT_LIMITING }; // out of it for the run
|
||||
enum { RTSS_DO_NOTHING, RTSS_DO_APPLY, RTSS_DO_PUT_BACK };
|
||||
enum {
|
||||
RTSS_WHY_HOLDS, // RTSS holds the game to the want
|
||||
RTSS_WHY_NOT_LISTED, // RTSS has the limit but has not hooked the game yet
|
||||
RTSS_WHY_OUT, // kept out (the want says why)
|
||||
RTSS_WHY_FAILED, // RtssState.fail
|
||||
RTSS_WHY_NOT_RUNNING,
|
||||
RTSS_WHY_REFUSED, // RTSS refused this limit
|
||||
RTSS_WHY_NEWER, // the want changed while the poll ran: nothing decided, the next poll takes the new one
|
||||
RTSS_WHY_UNSET, // no want yet
|
||||
RTSS_WHY_CLOSED, // the exit put the profile back (rtss_limit_restore)
|
||||
};
|
||||
|
||||
// What the worker keeps between polls.
|
||||
typedef struct {
|
||||
int64_t applied; // the millihertz the profile holds from this run; -1 untouched
|
||||
int64_t refused; // a limit RTSS refused (out of its range): the game's own limiter keeps that one
|
||||
int changed; // the profile holds a value from this run: put the old one back on exit
|
||||
int over; // polls in a row RTSS's frame rate of the game stayed above the limit
|
||||
int fail; // RTSS_FAIL_*
|
||||
} RtssState;
|
||||
#define RTSS_STATE_INIT {-1, -1, 0, 0, RTSS_FAIL_NONE}
|
||||
|
||||
// One poll's view.
|
||||
typedef struct {
|
||||
int64_t want; // the want the poll read
|
||||
int current; // the want is still that one when the poll decides
|
||||
int closed; // rtss_limit_restore ran
|
||||
int hooks; // RTSS's hooks library: 1 loaded, 0 failed to load, -1 not tried (rtss_wants_hooks)
|
||||
RtssApp app;
|
||||
} RtssIn;
|
||||
|
||||
typedef struct {
|
||||
int act; // RTSS_DO_*
|
||||
int64_t mhz; // the want decided for
|
||||
int holds; // RTSS holds the game to it: the game's own limiter stands aside
|
||||
int why; // RTSS_WHY_*
|
||||
double fps; // RTSS's frame rate of the game, when the frame count check gave up on it
|
||||
} RtssOut;
|
||||
|
||||
// The hooks library is loaded only on a poll that may write the profile: a limit is wanted, RTSS is not out of it for
|
||||
// the run, and it runs. A kept-out run never loads it.
|
||||
static inline int rtss_wants_hooks(const RtssState *s, const RtssIn *in) {
|
||||
return in->want >= 0 && !s->fail && in->app.running;
|
||||
}
|
||||
|
||||
// The decision of one poll, in this order: a closed run or no want does nothing; a kept-out want puts back what this
|
||||
// run wrote (once the hooks are loaded); a failure lasts the run; RTSS not running leaves the profile for its return; a
|
||||
// library that did not load fails the run; a refused limit stays with the game; a new limit goes into the profile at
|
||||
// once, and RTSS holds it as soon as it lists the game. On an unchanged limit RTSS's own frame count is checked: more
|
||||
// than RTSS_CHECK_SLACK times the limit for RTSS_CHECK_POLLS polls in a row puts the profile back for the run.
|
||||
static inline RtssOut rtss_decide(RtssState *s, const RtssIn *in) {
|
||||
int64_t w = in->want;
|
||||
RtssOut o = {RTSS_DO_NOTHING, w, 0, RTSS_WHY_NOT_LISTED, 0};
|
||||
if (in->closed) return o.why = RTSS_WHY_CLOSED, o;
|
||||
if (w == RTSS_UNSET) return o.why = RTSS_WHY_UNSET, o;
|
||||
if (w < 0) {
|
||||
if (in->hooks == 1 && s->changed) o.act = RTSS_DO_PUT_BACK;
|
||||
return o.why = RTSS_WHY_OUT, o;
|
||||
}
|
||||
if (s->fail) return o.why = RTSS_WHY_FAILED, o;
|
||||
if (!in->app.running) return o.why = RTSS_WHY_NOT_RUNNING, o;
|
||||
if (in->hooks == 0) return s->fail = RTSS_FAIL_LIBRARY, o.why = RTSS_WHY_FAILED, o;
|
||||
if (w == s->refused) return o.why = RTSS_WHY_REFUSED, o;
|
||||
o.holds = in->app.listed;
|
||||
o.why = o.holds ? RTSS_WHY_HOLDS : RTSS_WHY_NOT_LISTED;
|
||||
if (w != s->applied) { // the result of the write may still undo the hold (rtss_written)
|
||||
o.act = RTSS_DO_APPLY;
|
||||
s->over = 0;
|
||||
return o;
|
||||
}
|
||||
if (!in->current) return o.holds = 0, o.why = RTSS_WHY_NEWER, o; // these frames are not the new want's
|
||||
double fps = rtss_fps(&in->app);
|
||||
if (!in->app.listed || w == 0 || fps <= w / 1000.0 * RTSS_CHECK_SLACK) s->over = 0;
|
||||
else if (++s->over == RTSS_CHECK_POLLS) {
|
||||
s->fail = RTSS_FAIL_NOT_LIMITING;
|
||||
o.act = RTSS_DO_PUT_BACK, o.holds = 0, o.why = RTSS_WHY_FAILED, o.fps = fps;
|
||||
}
|
||||
return o;
|
||||
}
|
||||
|
||||
// What a write of the limit did (RtssOps.apply).
|
||||
enum {
|
||||
RTSS_WROTE_READONLY = -1, // the save did not reach the file
|
||||
RTSS_WROTE_REFUSED = 0, // RTSS refused the value; the profile keeps what it held
|
||||
RTSS_WROTE_HELD = 1, // the profile holds the limit
|
||||
RTSS_WROTE_REFUSED_CLEARED = 2, // refused, and the earlier limit of this run was cleared to none
|
||||
};
|
||||
|
||||
static inline void rtss_written(RtssState *s, RtssOut *o, int r) {
|
||||
if (r == RTSS_WROTE_READONLY) {
|
||||
s->fail = RTSS_FAIL_READONLY, o->holds = 0, o->why = RTSS_WHY_FAILED;
|
||||
return;
|
||||
}
|
||||
s->changed = 1;
|
||||
if (r == RTSS_WROTE_HELD) {
|
||||
s->applied = o->mhz;
|
||||
return;
|
||||
}
|
||||
if (r == RTSS_WROTE_REFUSED_CLEARED) s->applied = 0;
|
||||
s->refused = o->mhz, o->holds = 0, o->why = RTSS_WHY_REFUSED;
|
||||
}
|
||||
|
||||
// The profile was put back: nothing of this run is in it.
|
||||
static inline void rtss_put_back_done(RtssState *s) { s->changed = 0, s->applied = -1; }
|
||||
|
||||
// The profile calls rtss_poll makes. apply writes mhz (first: the first write of the run, which records what to put
|
||||
// back; clear: on a refusal, clear this run's earlier limit to none) and returns RTSS_WROTE_*; put_back puts the profile
|
||||
// back as it was before the run (o says why: kept out, or the frame count check gave up).
|
||||
typedef struct {
|
||||
void *ctx;
|
||||
int (*apply)(void *ctx, int64_t mhz, int first, int clear);
|
||||
void (*put_back)(void *ctx, const RtssOut *o);
|
||||
} RtssOps;
|
||||
|
||||
static inline RtssOut rtss_poll(RtssState *s, const RtssIn *in, const RtssOps *ops) {
|
||||
RtssOut o = rtss_decide(s, in);
|
||||
if (o.act == RTSS_DO_APPLY) {
|
||||
rtss_written(s, &o, ops->apply(ops->ctx, o.mhz, !s->changed, s->applied != 0));
|
||||
} else if (o.act == RTSS_DO_PUT_BACK) {
|
||||
ops->put_back(ops->ctx, &o);
|
||||
rtss_put_back_done(s);
|
||||
}
|
||||
return o;
|
||||
}
|
||||
|
||||
static inline const char *rtss_fail_text(int fail) {
|
||||
return fail == RTSS_FAIL_READONLY ? "RivaTuner Statistics Server's profile cannot be changed"
|
||||
: fail == RTSS_FAIL_NOT_LIMITING ? "RivaTuner Statistics Server does not hold the game to the limit"
|
||||
: "RivaTuner Statistics Server's hooks library did not load";
|
||||
}
|
||||
|
||||
// Why the game's own limiter paces, for the log line "the game's own limiter (<text>)".
|
||||
static inline void rtss_why_text(char *out, size_t n, const RtssOut *o, int fail) {
|
||||
if (o->why == RTSS_WHY_OUT) snprintf(out, n, "RivaTuner Statistics Server kept out: %s", rtss_out_text(o->mhz));
|
||||
else if (o->why == RTSS_WHY_FAILED) snprintf(out, n, "%s", rtss_fail_text(fail));
|
||||
else if (o->why == RTSS_WHY_NOT_RUNNING) snprintf(out, n, "RivaTuner Statistics Server is not running");
|
||||
else if (o->why == RTSS_WHY_REFUSED) snprintf(out, n, "RivaTuner Statistics Server refused this limit");
|
||||
else snprintf(out, n, "RivaTuner Statistics Server has not hooked the game yet");
|
||||
}
|
||||
|
||||
// ---- RTSS's own settings
|
||||
// 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 off for each; and the modes beside them, for the log: SyncDisplay, ReflexSleep
|
||||
@@ -40,6 +226,12 @@ typedef struct {
|
||||
// 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; }
|
||||
|
||||
// Two limiters: RTSS is kept out of the game's limit, and while it lists the game a setting of its own still holds the
|
||||
// frames (rtss_settings_limit), beside the game's limiter, the driver's or frame generation's.
|
||||
static inline int rtss_two(int64_t want, int listed, int own_limit) {
|
||||
return want < 0 && want != RTSS_UNSET && listed && own_limit;
|
||||
}
|
||||
|
||||
// "limit L/D, frame time limit T/TD, scanline sync S0/S1", and with modes ", display sync X, Reflex sleep R, passive
|
||||
// wait P": the values as the file holds them.
|
||||
static inline void rtss_settings_text(char *out, size_t n, const RtssSettings *s, int modes) {
|
||||
|
||||
@@ -47,17 +47,12 @@ typedef void (*ProfileFn)(LPCSTR name);
|
||||
typedef BOOL (*PropertyFn)(LPCSTR property, LPBYTE data, DWORD size);
|
||||
typedef void (*UpdateFn)(void);
|
||||
|
||||
#define RTSS_SIGNATURE 0x52545353u // 'RTSS' as RTSS writes it: the memory holds valid data
|
||||
#define POLL_MS 500
|
||||
#define CHECK_POLLS 6 // polls in a row RTSS's own frame rate of the game may stay above the limit
|
||||
#define CHECK_SLACK 1.2 // above limit * this, RTSS is taken not to hold the game to it
|
||||
|
||||
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 = RTSS_UNSET; // millihertz from the pacer: >0 a limit, 0 none; RTSS_OUT_* kept out
|
||||
static _Atomic int64_t active_for = RTSS_UNSET; // the want RTSS holds the game to now; RTSS_UNSET none
|
||||
static _Atomic int failed; // FAIL_*: RTSS is out of it for the rest of the run
|
||||
static _Atomic int failed; // RTSS_FAIL_*: RTSS is out of it for the rest of the run (S.fail)
|
||||
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 _Atomic int started; // rtss_limit_want spawned the worker
|
||||
@@ -68,45 +63,21 @@ static EnumProfilesFn enum_profiles;
|
||||
static ProfileFn load_profile, save_profile, delete_profile;
|
||||
static PropertyFn get_property, set_property;
|
||||
static UpdateFn update_profiles;
|
||||
static RtssState S = RTSS_STATE_INIT; // the worker's decisions (rtss_decide.h), under lock
|
||||
static int has_denominator; // this RTSS knows FramerateLimitDenominator
|
||||
static int64_t applied = -1; // the millihertz the profile holds from this run; -1 untouched
|
||||
static int64_t refused = -1; // a limit RTSS refused (out of its range): the game's own limiter keeps that one
|
||||
static int changed; // the profile holds a value from this run: put the old one back on exit
|
||||
static int had_profile; // the profile existed before this run changed it
|
||||
static DWORD old_limit, old_denominator = 1;
|
||||
|
||||
// RTSS's shared memory: running (1), whether its application list holds this process, and the frame rate RTSS measured
|
||||
// for it over its last period (0 unknown). The header: signature, version, application entry size, offset of the
|
||||
// application array, its length. An entry: the process id, the executable path (MAX_PATH), flags, then the period's
|
||||
// start and end in ms and the frames in it.
|
||||
static int rtss_running(int *hooked, double *fps) {
|
||||
*hooked = 0, *fps = 0;
|
||||
// RTSS's shared memory as rtss_parse reads it: whether RTSS runs, whether it lists this process, and its frame count.
|
||||
static void rtss_map(RtssApp *a) {
|
||||
memset(a, 0, sizeof *a);
|
||||
HANDLE m = OpenFileMappingA(FILE_MAP_READ, FALSE, "RTSSSharedMemoryV2");
|
||||
if (!m) return 0;
|
||||
if (!m) return;
|
||||
const uint8_t *p = MapViewOfFile(m, FILE_MAP_READ, 0, 0, 0);
|
||||
int run = 0;
|
||||
MEMORY_BASIC_INFORMATION mi;
|
||||
if (p && VirtualQuery(p, &mi, sizeof mi) && mi.RegionSize >= 20) {
|
||||
DWORD h[5];
|
||||
memcpy(h, p, sizeof h);
|
||||
run = h[0] == RTSS_SIGNATURE && h[1] >= 0x20000;
|
||||
DWORD pid = GetCurrentProcessId();
|
||||
enum { PERIOD_AT = 4 + MAX_PATH + 4, ENTRY_MIN = PERIOD_AT + 12 };
|
||||
for (DWORD i = 0; run && h[2] >= ENTRY_MIN && i < h[4] && i < 4096; i++) {
|
||||
uint64_t at = (uint64_t)h[3] + (uint64_t)i * h[2];
|
||||
if (at + ENTRY_MIN > mi.RegionSize) break;
|
||||
DWORD id, t[3];
|
||||
memcpy(&id, p + at, 4);
|
||||
if (id != pid) continue;
|
||||
*hooked = 1;
|
||||
memcpy(t, p + at + PERIOD_AT, sizeof t); // start, end, frames
|
||||
if (t[1] > t[0]) *fps = 1000.0 * t[2] / (t[1] - t[0]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (p && VirtualQuery(p, &mi, sizeof mi)) rtss_parse(p, mi.RegionSize, GetCurrentProcessId(), a);
|
||||
if (p) UnmapViewOfFile(p);
|
||||
CloseHandle(m);
|
||||
return run;
|
||||
}
|
||||
|
||||
static int load_hooks(void) {
|
||||
@@ -176,10 +147,11 @@ static int write_limit(DWORD limit, DWORD denominator) {
|
||||
return l == limit && (!has_denominator || d == denominator) ? 1 : -1;
|
||||
}
|
||||
|
||||
// 1: the profile holds mhz; 0: RTSS refused it (the profile then holds no limit of this run); -1: the profile cannot be
|
||||
// written.
|
||||
static int apply(int64_t mhz) {
|
||||
if (!changed) { // what to put back on exit
|
||||
// Writes mhz into the profile (rtss_poll's RtssOps.apply): RTSS_WROTE_*. first: the run's first write, which records
|
||||
// what to put back on exit; clear: on a refusal, clear the limit of this run the profile holds.
|
||||
static int apply(void *ctx, int64_t mhz, int first, int clear) {
|
||||
(void)ctx;
|
||||
if (first) { // what to put back on exit
|
||||
had_profile = profile_exists();
|
||||
load_profile(profile);
|
||||
old_limit = 0, old_denominator = 1;
|
||||
@@ -195,23 +167,23 @@ static int apply(int64_t mhz) {
|
||||
if (r < 0) {
|
||||
rt_log("frame rate: RivaTuner Statistics Server's profile %s did not keep the limit: its Profiles folder needs the "
|
||||
"game run as administrator", profile);
|
||||
return -1;
|
||||
return RTSS_WROTE_READONLY;
|
||||
}
|
||||
if (!changed) {
|
||||
if (first) {
|
||||
if (!had_profile)
|
||||
rt_log("frame rate: RivaTuner Statistics Server profile %s created; it goes away when the game closes", profile);
|
||||
else
|
||||
rt_log("frame rate: RivaTuner Statistics Server profile %s held a limit of %lu/%lu; it gets that back when the "
|
||||
"game closes", profile, old_limit, old_denominator);
|
||||
changed = 1;
|
||||
}
|
||||
if (r == 0) { // RTSS must not keep an earlier limit of this run under the game's own one
|
||||
rt_log("frame rate: RivaTuner Statistics Server refused a limit of %.3f fps", mhz / 1000.0);
|
||||
if (applied != 0 && write_limit(0, 1) > 0) applied = 0, update_profiles();
|
||||
refused = mhz;
|
||||
return 0;
|
||||
if (clear && write_limit(0, 1) > 0) {
|
||||
update_profiles();
|
||||
return RTSS_WROTE_REFUSED_CLEARED;
|
||||
}
|
||||
return RTSS_WROTE_REFUSED;
|
||||
}
|
||||
applied = mhz;
|
||||
update_profiles();
|
||||
if (!mhz) rt_log("frame rate: RivaTuner Statistics Server's profile %s has no limit (unlimited)", profile);
|
||||
else if ((int64_t)limit * 1000 == mhz * denominator)
|
||||
@@ -219,13 +191,11 @@ static int apply(int64_t mhz) {
|
||||
else // rounded to whole frames by an RTSS without FramerateLimitDenominator: the log says what RTSS holds
|
||||
rt_log("frame rate: RivaTuner Statistics Server's profile %s limits to %lu/%lu (%.3f fps; the game asked %.3f)",
|
||||
profile, limit, denominator, (double)limit / denominator, mhz / 1000.0);
|
||||
return 1;
|
||||
return RTSS_WROTE_HELD;
|
||||
}
|
||||
|
||||
// The profile as it was before this run changed it (the caller checked S.changed, and marks it done).
|
||||
static void put_back(void) {
|
||||
if (!changed) return;
|
||||
changed = 0;
|
||||
applied = -1;
|
||||
if (!had_profile) {
|
||||
delete_profile(profile);
|
||||
rt_log("frame rate: RivaTuner Statistics Server profile %s removed, as before the game started", profile);
|
||||
@@ -238,6 +208,14 @@ static void put_back(void) {
|
||||
update_profiles();
|
||||
}
|
||||
|
||||
static void put_back_op(void *ctx, const RtssOut *o) { // rtss_poll's RtssOps.put_back
|
||||
(void)ctx;
|
||||
if (o->why == RTSS_WHY_FAILED) // the frame count check gave up on RTSS
|
||||
rt_log("frame rate: RivaTuner Statistics Server measures %.1f fps with its limit at %.3f fps", o->fps,
|
||||
o->mhz / 1000.0);
|
||||
put_back();
|
||||
}
|
||||
|
||||
// Whether RTSS holds the game to the want w this poll read, and the log of which limiter is in use: the first decision,
|
||||
// then each change. The want may have changed since the poll read it: rtss_limit_active compares the two, so a stale
|
||||
// store never lets the game's limiter stand aside for a newer want.
|
||||
@@ -250,12 +228,6 @@ static void set_active(int64_t w, int on, const char *why) {
|
||||
else rt_log("frame rate: the game's own limiter (%s)", why);
|
||||
}
|
||||
|
||||
static const char *fail_reason(int f) {
|
||||
return f == FAIL_READONLY ? "RivaTuner Statistics Server's profile cannot be changed"
|
||||
: f == FAIL_NOT_LIMITING ? "RivaTuner Statistics Server does not hold the game to the limit"
|
||||
: "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
|
||||
@@ -314,7 +286,7 @@ static void watch(int64_t w, int listed) {
|
||||
snprintf(said_settings, sizeof said_settings, "%s", all);
|
||||
}
|
||||
if (w < 0 && listed) {
|
||||
on = rtss_settings_limit(&settings);
|
||||
on = rtss_two(w, listed, rtss_settings_limit(&settings));
|
||||
rtss_settings_text(text, sizeof text, &settings, 0);
|
||||
}
|
||||
if (w >= 0) said = 0; // a later kept-out want logs afresh
|
||||
@@ -368,7 +340,8 @@ static void worker(void *arg) {
|
||||
(void)arg;
|
||||
if (!find_rtss()) return;
|
||||
atomic_store_explicit(&ready, 1, memory_order_release); // rtss_limit_restore may read the names now
|
||||
int was_running = -1, was_hooked = 0, over = 0;
|
||||
int was_running = -1, was_listed = 0;
|
||||
const RtssOps ops = {NULL, apply, put_back_op};
|
||||
for (;; plat_sleep_ms(POLL_MS)) {
|
||||
int64_t w = atomic_load(&want);
|
||||
if (w == RTSS_UNSET) continue;
|
||||
@@ -377,49 +350,24 @@ static void worker(void *arg) {
|
||||
plat_mutex_unlock(&lock);
|
||||
continue;
|
||||
}
|
||||
int hooked;
|
||||
double fps;
|
||||
int run = rtss_running(&hooked, &fps), f = atomic_load(&failed);
|
||||
if (run != was_running || hooked != was_hooked) {
|
||||
if (!run) rt_log("frame rate: RivaTuner Statistics Server is installed but not running");
|
||||
else if (!hooked) rt_log("frame rate: RivaTuner Statistics Server runs; it has not hooked the game yet");
|
||||
RtssIn in = {.want = w};
|
||||
rtss_map(&in.app);
|
||||
if (in.app.running != was_running || in.app.listed != was_listed) {
|
||||
if (!in.app.running) rt_log("frame rate: RivaTuner Statistics Server is installed but not running");
|
||||
else if (!in.app.listed) rt_log("frame rate: RivaTuner Statistics Server runs; it has not hooked the game yet");
|
||||
else rt_log("frame rate: RivaTuner Statistics Server has hooked the game");
|
||||
was_running = run, was_hooked = hooked;
|
||||
was_running = in.app.running, was_listed = in.app.listed;
|
||||
}
|
||||
if (w < 0) {
|
||||
if (dll) put_back();
|
||||
char why[96];
|
||||
snprintf(why, sizeof why, "RivaTuner Statistics Server kept out: %s", rtss_out_text(w));
|
||||
set_active(w, 0, why);
|
||||
} else if (f) {
|
||||
set_active(w, 0, fail_reason(f));
|
||||
} else if (!run) {
|
||||
set_active(w, 0, "RivaTuner Statistics Server is not running");
|
||||
} else if (!load_hooks()) {
|
||||
atomic_store(&failed, FAIL_LIBRARY);
|
||||
set_active(w, 0, fail_reason(FAIL_LIBRARY));
|
||||
} else if (w == refused) {
|
||||
set_active(w, 0, "RivaTuner Statistics Server refused this limit");
|
||||
} else if (w != applied) {
|
||||
int r = apply(w);
|
||||
over = 0;
|
||||
if (r < 0) atomic_store(&failed, FAIL_READONLY), set_active(w, 0, fail_reason(FAIL_READONLY));
|
||||
else if (r == 0) set_active(w, 0, "RivaTuner Statistics Server refused this limit");
|
||||
else set_active(w, hooked, "RivaTuner Statistics Server has not hooked the game yet");
|
||||
} else if (atomic_load(&want) == w) { // a newer want waits for the next poll: these frames are not its
|
||||
set_active(w, hooked, "RivaTuner Statistics Server has not hooked the game yet");
|
||||
// RTSS's own count of the game's frames while the game's limiter stands aside: well above the limit for
|
||||
// three seconds means RTSS does not hold the game to it, and the game takes the limiting back for the run
|
||||
if (!hooked || w == 0 || fps <= w / 1000.0 * CHECK_SLACK) over = 0;
|
||||
else if (++over == CHECK_POLLS) {
|
||||
rt_log("frame rate: RivaTuner Statistics Server measures %.1f fps with its limit at %.3f fps", fps,
|
||||
w / 1000.0);
|
||||
atomic_store(&failed, FAIL_NOT_LIMITING);
|
||||
put_back();
|
||||
set_active(w, 0, fail_reason(FAIL_NOT_LIMITING));
|
||||
}
|
||||
in.hooks = dll ? 1 : rtss_wants_hooks(&S, &in) ? load_hooks() : -1;
|
||||
in.current = atomic_load(&want) == w;
|
||||
RtssOut o = rtss_poll(&S, &in, &ops);
|
||||
atomic_store(&failed, S.fail);
|
||||
if (o.why != RTSS_WHY_NEWER) { // a newer want waits for the next poll
|
||||
char why[128];
|
||||
rtss_why_text(why, sizeof why, &o, S.fail);
|
||||
set_active(w, o.holds, why);
|
||||
}
|
||||
watch(w, run && hooked);
|
||||
watch(w, in.app.listed);
|
||||
plat_mutex_unlock(&lock);
|
||||
}
|
||||
}
|
||||
@@ -446,7 +394,7 @@ int rtss_limit_state(void) {
|
||||
if (atomic_load_explicit(&closed, memory_order_relaxed)) return 0;
|
||||
int f = atomic_load_explicit(&failed, memory_order_relaxed);
|
||||
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 && (f == RTSS_FAIL_READONLY || f == RTSS_FAIL_NOT_LIMITING)) return f == RTSS_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;
|
||||
}
|
||||
@@ -461,7 +409,7 @@ void rtss_limit_restore(void) {
|
||||
// the exit may come from a crash on any thread: give up after a second rather than wait on a lock held there
|
||||
for (int i = 0; i < 100; i++) {
|
||||
if (plat_mutex_trylock(&lock)) {
|
||||
if (dll) put_back();
|
||||
if (dll && S.changed) put_back(), rtss_put_back_done(&S);
|
||||
plat_mutex_unlock(&lock);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
// 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.
|
||||
// call: the shared memory's reader against a fake RTSSSharedMemoryV2, and the worker's decisions poll by poll against a
|
||||
// fake profile. The cases pin the decisions as they stand, including those a pending change would alter (writing
|
||||
// before RTSS lists the game, the frame count rule, a write per movie and per switch of low latency). Built and run by
|
||||
// tests/rtss_limit_check.py on any system; prints each failed check and exits 1 on any.
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "rtss_decide.h"
|
||||
@@ -54,9 +59,338 @@ static void reasons(void) {
|
||||
"the reasons differ");
|
||||
}
|
||||
|
||||
// ---- a fake RTSSSharedMemoryV2: the header and application entries written as RTSS lays them out
|
||||
enum { PID = 4242, ESIZE = 1024, OFF = 0x100 };
|
||||
static void shm_set(uint8_t *v, uint32_t sig, uint32_t ver, uint32_t esize, uint32_t off, uint32_t n) {
|
||||
uint32_t h[5] = {sig, ver, esize, off, n};
|
||||
memcpy(v, h, sizeof h);
|
||||
}
|
||||
static void app_put(uint8_t *v, uint32_t esize, uint32_t off, uint32_t slot, uint32_t pid, uint32_t t0, uint32_t t1,
|
||||
uint32_t frames) {
|
||||
uint8_t *e = v + off + (size_t)slot * esize;
|
||||
uint32_t t[3] = {t0, t1, frames};
|
||||
memcpy(e, &pid, 4);
|
||||
memcpy(e + RTSS_PERIOD_AT, t, sizeof t);
|
||||
}
|
||||
|
||||
// rtss_parse: where the entry is, and that a bad header, a short entry or an array past the memory finds nothing.
|
||||
static void parse(void) {
|
||||
static uint8_t v[65536];
|
||||
RtssApp a;
|
||||
shm_set(v, RTSS_SIGNATURE, 0x0002000E, ESIZE, OFF, 3);
|
||||
app_put(v, ESIZE, OFF, 0, 7, 0, 0, 0);
|
||||
app_put(v, ESIZE, OFF, 1, 8, 0, 0, 0);
|
||||
app_put(v, ESIZE, OFF, 2, PID, 1000, 2000, 60);
|
||||
rtss_parse(v, sizeof v, PID, &a);
|
||||
CHECK(a.running && a.listed && a.t0 == 1000 && a.t1 == 2000 && a.frames == 60, "the third entry is ours");
|
||||
CHECK(rtss_fps(&a) == 60.0, "60 frames in a second: %f", rtss_fps(&a));
|
||||
rtss_parse(v, sizeof v, 99, &a);
|
||||
CHECK(a.running && !a.listed && rtss_fps(&a) == 0, "another process: running, not listed");
|
||||
shm_set(v, 0xDEAD, 0x0002000E, ESIZE, OFF, 3);
|
||||
rtss_parse(v, sizeof v, PID, &a);
|
||||
CHECK(!a.running && !a.listed, "no signature: not running");
|
||||
shm_set(v, RTSS_SIGNATURE, 0x00010003, ESIZE, OFF, 3);
|
||||
rtss_parse(v, sizeof v, PID, &a);
|
||||
CHECK(!a.running, "a version before 2.0: not running");
|
||||
shm_set(v, RTSS_SIGNATURE, 0x0002000E, RTSS_ENTRY_MIN - 1, OFF, 3);
|
||||
rtss_parse(v, sizeof v, PID, &a);
|
||||
CHECK(a.running && !a.listed, "entries shorter than the period: not read");
|
||||
rtss_parse(v, 19, PID, &a);
|
||||
CHECK(!a.running, "a memory shorter than the header");
|
||||
rtss_parse(NULL, 0, PID, &a);
|
||||
CHECK(!a.running, "no memory");
|
||||
// entries of the smallest size, ours the last that fits: found; past the memory's end: not read
|
||||
memset(v, 0, sizeof v);
|
||||
shm_set(v, RTSS_SIGNATURE, 0x0002000E, RTSS_ENTRY_MIN, OFF, 1000);
|
||||
uint32_t fit = (uint32_t)((sizeof v - OFF) / RTSS_ENTRY_MIN) - 1;
|
||||
app_put(v, RTSS_ENTRY_MIN, OFF, fit, PID, 1000, 1500, 30);
|
||||
rtss_parse(v, sizeof v, PID, &a);
|
||||
CHECK(a.listed && a.frames == 30, "the last entry inside the memory is read");
|
||||
rtss_parse(v, OFF + (size_t)fit * RTSS_ENTRY_MIN + RTSS_ENTRY_MIN - 1, PID, &a);
|
||||
CHECK(a.running && !a.listed, "an entry one byte past the memory is not read");
|
||||
// more than RTSS_APPS_MAX entries: the walk stops there
|
||||
size_t big = OFF + (size_t)5000 * RTSS_ENTRY_MIN;
|
||||
uint8_t *b = calloc(1, big);
|
||||
shm_set(b, RTSS_SIGNATURE, 0x0002000E, RTSS_ENTRY_MIN, OFF, 5000);
|
||||
app_put(b, RTSS_ENTRY_MIN, OFF, 4500, PID, 1000, 2000, 60);
|
||||
rtss_parse(b, big, PID, &a);
|
||||
CHECK(a.running && !a.listed, "an entry past %d is not looked at", RTSS_APPS_MAX);
|
||||
app_put(b, RTSS_ENTRY_MIN, OFF, 4095, PID, 1000, 2000, 60);
|
||||
rtss_parse(b, big, PID, &a);
|
||||
CHECK(a.listed, "entry %d is", RTSS_APPS_MAX - 1);
|
||||
free(b);
|
||||
}
|
||||
|
||||
// ---- a fake profile behind RtssOps, and one poll as the worker makes it
|
||||
typedef struct {
|
||||
int readonly, no_library, loaded;
|
||||
int64_t refuse_above; // RTSS refuses a limit above this (millihertz); 0 none
|
||||
int64_t holds; // the limit the profile holds from this run; -1 none
|
||||
int applies, firsts, clears, put_backs, last_put_back_why;
|
||||
} Store;
|
||||
static int f_apply(void *ctx, int64_t mhz, int first, int clear) {
|
||||
Store *st = ctx;
|
||||
st->applies++, st->firsts += first;
|
||||
if (st->readonly) return RTSS_WROTE_READONLY;
|
||||
if (st->refuse_above && mhz > st->refuse_above) {
|
||||
if (!clear) return RTSS_WROTE_REFUSED;
|
||||
st->clears++, st->holds = 0;
|
||||
return RTSS_WROTE_REFUSED_CLEARED;
|
||||
}
|
||||
st->holds = mhz;
|
||||
return RTSS_WROTE_HELD;
|
||||
}
|
||||
static void f_put_back(void *ctx, const RtssOut *o) {
|
||||
Store *st = ctx;
|
||||
st->put_backs++, st->holds = -1, st->last_put_back_why = o->why;
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
RtssState s;
|
||||
Store st;
|
||||
RtssOps ops;
|
||||
int running, listed;
|
||||
uint32_t t0, t1, frames;
|
||||
} Run;
|
||||
static void run_init(Run *r) {
|
||||
memset(r, 0, sizeof *r);
|
||||
r->s = (RtssState)RTSS_STATE_INIT;
|
||||
r->st.holds = -1;
|
||||
r->ops = (RtssOps){&r->st, f_apply, f_put_back};
|
||||
r->running = r->listed = 1, r->t0 = 1000, r->t1 = 2000, r->frames = 60;
|
||||
}
|
||||
// One poll for want, as rtss_limit.c worker makes it: the hooks library loads on the first poll that wants it and stays.
|
||||
static RtssOut step_ex(Run *r, int64_t want, int current, int closed) {
|
||||
RtssIn in = {.want = want, .current = current, .closed = closed};
|
||||
in.app = (RtssApp){r->running, r->running && r->listed, r->t0, r->t1, r->frames};
|
||||
if (r->st.loaded) in.hooks = 1;
|
||||
else if (rtss_wants_hooks(&r->s, &in)) in.hooks = r->st.no_library ? 0 : (r->st.loaded = 1);
|
||||
else in.hooks = -1;
|
||||
return rtss_poll(&r->s, &in, &r->ops);
|
||||
}
|
||||
static RtssOut step(Run *r, int64_t want) { return step_ex(r, want, 1, 0); }
|
||||
|
||||
// RTSS's injection delay: it runs but has not hooked the game for 30 polls. The limit goes into the profile at the
|
||||
// first poll, and RTSS holds the game once it lists it, with no second write.
|
||||
static void injection_delay(void) {
|
||||
Run r;
|
||||
run_init(&r);
|
||||
r.listed = 0;
|
||||
RtssOut o = step(&r, 60000);
|
||||
CHECK(o.act == RTSS_DO_APPLY && !o.holds && o.why == RTSS_WHY_NOT_LISTED, "written at once, not held yet");
|
||||
CHECK(r.st.firsts == 1 && r.s.applied == 60000 && r.s.changed, "the first write of the run");
|
||||
for (int i = 0; i < 30; i++) o = step(&r, 60000);
|
||||
CHECK(r.st.applies == 1 && !o.holds && o.why == RTSS_WHY_NOT_LISTED, "no write while not listed");
|
||||
r.listed = 1;
|
||||
o = step(&r, 60000);
|
||||
CHECK(o.holds && o.why == RTSS_WHY_HOLDS && r.st.applies == 1, "held once listed, no second write");
|
||||
}
|
||||
|
||||
// RTSS quits while it limits, and comes back: the profile stays for its return.
|
||||
static void quit(void) {
|
||||
Run r;
|
||||
run_init(&r);
|
||||
step(&r, 60000);
|
||||
r.running = 0;
|
||||
RtssOut o = step(&r, 60000);
|
||||
CHECK(!o.holds && o.why == RTSS_WHY_NOT_RUNNING && o.act == RTSS_DO_NOTHING, "not running: the game limits");
|
||||
CHECK(!r.st.put_backs && r.st.holds == 60000, "the profile stays");
|
||||
r.running = 1;
|
||||
o = step(&r, 60000);
|
||||
CHECK(o.holds && r.st.applies == 1, "back: held with no new write");
|
||||
}
|
||||
|
||||
// Profiles under Program Files without administrator rights: out for the run.
|
||||
static void readonly(void) {
|
||||
Run r;
|
||||
run_init(&r);
|
||||
r.st.readonly = 1;
|
||||
RtssOut o = step(&r, 60000);
|
||||
CHECK(o.act == RTSS_DO_APPLY && !o.holds && o.why == RTSS_WHY_FAILED && r.s.fail == RTSS_FAIL_READONLY, "failed");
|
||||
CHECK(!r.s.changed, "nothing of the run in the profile");
|
||||
for (int i = 0; i < 20; i++) o = step(&r, i < 10 ? 60000 : 144000);
|
||||
CHECK(r.st.applies == 1 && o.why == RTSS_WHY_FAILED, "no write after it");
|
||||
char t[128];
|
||||
rtss_why_text(t, sizeof t, &o, r.s.fail);
|
||||
CHECK(!strcmp(t, "RivaTuner Statistics Server's profile cannot be changed"), "%s", t);
|
||||
}
|
||||
|
||||
// A limit out of RTSS's range: refused and kept by the game; the earlier limit of this run is cleared to none; a limit
|
||||
// in range goes to RTSS at once.
|
||||
static void refused(void) {
|
||||
Run r;
|
||||
run_init(&r);
|
||||
r.st.refuse_above = 200000;
|
||||
step(&r, 60000);
|
||||
RtssOut o = step(&r, 240000);
|
||||
CHECK(o.act == RTSS_DO_APPLY && !o.holds && o.why == RTSS_WHY_REFUSED, "refused");
|
||||
CHECK(r.s.refused == 240000 && r.s.applied == 0 && r.st.clears == 1 && r.st.holds == 0, "the 60 cleared to none");
|
||||
for (int i = 0; i < 5; i++) o = step(&r, 240000);
|
||||
CHECK(r.st.applies == 2 && o.why == RTSS_WHY_REFUSED && !o.holds, "not written again");
|
||||
o = step(&r, 144000);
|
||||
CHECK(o.act == RTSS_DO_APPLY && o.holds && r.st.holds == 144000, "144 at once");
|
||||
CHECK(r.st.firsts == 1, "one first write in the run (%d)", r.st.firsts);
|
||||
o = step(&r, 240000);
|
||||
CHECK(o.act == RTSS_DO_NOTHING && o.why == RTSS_WHY_REFUSED, "the refused limit stays with the game");
|
||||
// refused as the run's first write: the profile is the run's from then on
|
||||
run_init(&r);
|
||||
r.st.refuse_above = 200000;
|
||||
o = step(&r, 240000);
|
||||
CHECK(o.why == RTSS_WHY_REFUSED && r.s.changed && r.st.clears == 1, "a first refusal clears too, as it stands");
|
||||
}
|
||||
|
||||
// RTSS's own frame count: more than 1.2 times the limit for six polls in a row on an unchanged limit, and the
|
||||
// profile goes back for the run. The poll that writes the limit does not count; 1.15 times is not caught; a period
|
||||
// RTSS no longer moves still counts; no limit and not listed never do.
|
||||
static void frame_count(void) {
|
||||
Run r;
|
||||
run_init(&r);
|
||||
r.frames = 144;
|
||||
RtssOut o = step(&r, 60000);
|
||||
for (int i = 0; i < 5; i++) o = step(&r, 60000);
|
||||
CHECK(o.holds && !r.s.fail && r.s.over == 5, "five polls over: still held");
|
||||
o = step(&r, 60000);
|
||||
CHECK(o.act == RTSS_DO_PUT_BACK && !o.holds && o.why == RTSS_WHY_FAILED && r.s.fail == RTSS_FAIL_NOT_LIMITING,
|
||||
"the sixth gives up");
|
||||
CHECK(o.fps == 144.0 && r.st.put_backs == 1 && r.st.last_put_back_why == RTSS_WHY_FAILED && !r.s.changed,
|
||||
"put back, with the measured rate");
|
||||
o = step(&r, 30000);
|
||||
CHECK(o.why == RTSS_WHY_FAILED && r.st.applies == 1, "out for the run");
|
||||
run_init(&r);
|
||||
r.frames = 69;
|
||||
for (int i = 0; i < 40; i++) o = step(&r, 60000);
|
||||
CHECK(o.holds && !r.s.fail, "69 fps at 60 is not caught");
|
||||
r.frames = 73;
|
||||
for (int i = 0; i < 6; i++) o = step(&r, 60000);
|
||||
CHECK(r.s.fail == RTSS_FAIL_NOT_LIMITING, "73 fps at 60 is");
|
||||
run_init(&r);
|
||||
r.frames = 72; // 1.2 times the limit exactly
|
||||
for (int i = 0; i < 40; i++) o = step(&r, 60000);
|
||||
CHECK(o.holds && !r.s.fail, "72 fps at 60 is not");
|
||||
run_init(&r);
|
||||
r.frames = 700;
|
||||
for (int i = 0; i < 20; i++) o = step(&r, 0);
|
||||
CHECK(o.holds && !r.s.fail, "no limit: nothing to check");
|
||||
run_init(&r);
|
||||
r.frames = 144, r.listed = 0;
|
||||
for (int i = 0; i < 20; i++) o = step(&r, 60000);
|
||||
CHECK(!r.s.fail && !r.s.over, "not listed: no count");
|
||||
r.listed = 1;
|
||||
for (int i = 0; i < 3; i++) step(&r, 60000);
|
||||
r.listed = 0;
|
||||
step(&r, 60000);
|
||||
CHECK(r.s.over == 0, "a poll not listed starts the count again");
|
||||
run_init(&r);
|
||||
r.frames = 144; // t0 and t1 never move: the same period every poll
|
||||
for (int i = 0; i < 7; i++) step(&r, 60000);
|
||||
CHECK(r.s.fail == RTSS_FAIL_NOT_LIMITING, "a period RTSS no longer moves still counts, as it stands");
|
||||
// a want that changed while the poll ran: nothing decided, the count waits
|
||||
run_init(&r);
|
||||
r.frames = 144;
|
||||
step(&r, 60000);
|
||||
step(&r, 60000);
|
||||
o = step_ex(&r, 60000, 0, 0);
|
||||
CHECK(o.why == RTSS_WHY_NEWER && !o.holds && o.act == RTSS_DO_NOTHING && r.s.over == 1, "a newer want: no count");
|
||||
}
|
||||
|
||||
// Kept out and let in again: the first kept-out poll puts back what the run wrote, once; a limit afterwards is
|
||||
// written at once and is the run's first write again.
|
||||
static void kept_out(void) {
|
||||
Run r;
|
||||
run_init(&r);
|
||||
step(&r, 60000);
|
||||
RtssOut o = step(&r, RTSS_OUT_FRAMEGEN);
|
||||
CHECK(o.act == RTSS_DO_PUT_BACK && o.why == RTSS_WHY_OUT && !o.holds && r.st.holds == -1, "put back");
|
||||
CHECK(r.st.last_put_back_why == RTSS_WHY_OUT, "the put back says why");
|
||||
o = step(&r, RTSS_OUT_FRAMEGEN);
|
||||
CHECK(o.act == RTSS_DO_NOTHING && r.st.put_backs == 1, "once");
|
||||
o = step(&r, RTSS_OUT_LOWLAT);
|
||||
CHECK(o.act == RTSS_DO_NOTHING && o.mhz == RTSS_OUT_LOWLAT, "a second reason: nothing more");
|
||||
char t[128];
|
||||
rtss_why_text(t, sizeof t, &o, r.s.fail);
|
||||
CHECK(!strcmp(t, "RivaTuner Statistics Server kept out: the driver's low latency"), "%s", t);
|
||||
o = step(&r, 60000);
|
||||
CHECK(o.act == RTSS_DO_APPLY && o.holds && r.st.firsts == 2, "written at once, the run's first write again");
|
||||
// kept out from the start: the hooks library is never wanted, and nothing is written or put back
|
||||
run_init(&r);
|
||||
for (int i = 0; i < 10; i++) o = step(&r, RTSS_OUT_SETTING);
|
||||
CHECK(!r.st.loaded && !r.st.applies && !r.st.put_backs && o.why == RTSS_WHY_OUT, "read only");
|
||||
}
|
||||
|
||||
// Low latency switching on and off at every poll: each switch writes or puts back, as it stands.
|
||||
static void churn(void) {
|
||||
Run r;
|
||||
run_init(&r);
|
||||
step(&r, 60000);
|
||||
for (int i = 0; i < 20; i++) step(&r, i & 1 ? 60000 : RTSS_OUT_LOWLAT);
|
||||
CHECK(r.st.applies == 11 && r.st.put_backs == 10, "%d writes, %d put backs", r.st.applies, r.st.put_backs);
|
||||
}
|
||||
|
||||
// A movie under RTSS: the want follows the movie's rate, and each start and end is a write.
|
||||
static void movie(void) {
|
||||
Run r;
|
||||
run_init(&r);
|
||||
step(&r, 144000);
|
||||
RtssOut o = step(&r, 60000);
|
||||
CHECK(o.act == RTSS_DO_APPLY && o.holds, "the movie's 60 written");
|
||||
o = step(&r, 144000);
|
||||
CHECK(o.act == RTSS_DO_APPLY && o.holds && r.st.applies == 3, "144 written again at its end");
|
||||
}
|
||||
|
||||
// No want yet, and a closed run: nothing at all. The hooks library that does not load fails the run.
|
||||
static void closed_and_library(void) {
|
||||
Run r;
|
||||
run_init(&r);
|
||||
RtssOut o = step(&r, RTSS_UNSET);
|
||||
CHECK(o.why == RTSS_WHY_UNSET && o.act == RTSS_DO_NOTHING && !r.st.loaded, "no want");
|
||||
step(&r, 60000);
|
||||
o = step_ex(&r, 144000, 1, 1);
|
||||
CHECK(o.why == RTSS_WHY_CLOSED && o.act == RTSS_DO_NOTHING && r.st.holds == 60000, "closed: nothing written");
|
||||
o = step_ex(&r, RTSS_OUT_FRAMEGEN, 1, 1);
|
||||
CHECK(o.act == RTSS_DO_NOTHING && !r.st.put_backs, "closed: nothing put back");
|
||||
run_init(&r);
|
||||
r.st.no_library = 1;
|
||||
o = step(&r, 60000);
|
||||
CHECK(o.why == RTSS_WHY_FAILED && r.s.fail == RTSS_FAIL_LIBRARY && !r.st.applies, "the library did not load");
|
||||
char t[128];
|
||||
rtss_why_text(t, sizeof t, &o, r.s.fail);
|
||||
CHECK(!strcmp(t, "RivaTuner Statistics Server's hooks library did not load"), "%s", t);
|
||||
r.st.no_library = 0;
|
||||
o = step(&r, 60000);
|
||||
CHECK(o.why == RTSS_WHY_FAILED && !r.st.loaded, "not tried again");
|
||||
// not running: the library is not wanted until RTSS runs
|
||||
run_init(&r);
|
||||
r.running = 0;
|
||||
o = step(&r, 60000);
|
||||
CHECK(!r.st.loaded && o.why == RTSS_WHY_NOT_RUNNING, "not loaded while RTSS does not run");
|
||||
rtss_why_text(t, sizeof t, &o, r.s.fail);
|
||||
CHECK(!strcmp(t, "RivaTuner Statistics Server is not running"), "%s", t);
|
||||
}
|
||||
|
||||
// Two limiters: kept out, listed and a limit of RTSS's own; never for a limit the game hands RTSS.
|
||||
static void two(void) {
|
||||
CHECK(rtss_two(RTSS_OUT_SETTING, 1, 1) && rtss_two(RTSS_OUT_FRAMEGEN, 1, 1) && rtss_two(RTSS_OUT_LOWLAT, 1, 1),
|
||||
"every reason");
|
||||
CHECK(!rtss_two(RTSS_OUT_SETTING, 0, 1), "not listed: RTSS holds nothing");
|
||||
CHECK(!rtss_two(RTSS_OUT_SETTING, 1, 0), "no limit of its own");
|
||||
CHECK(!rtss_two(60000, 1, 1) && !rtss_two(0, 1, 1), "the game's limit in RTSS: one limiter");
|
||||
CHECK(!rtss_two(RTSS_UNSET, 1, 1), "no want yet");
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
settings();
|
||||
reasons();
|
||||
parse();
|
||||
injection_delay();
|
||||
quit();
|
||||
readonly();
|
||||
refused();
|
||||
frame_count();
|
||||
kept_out();
|
||||
churn();
|
||||
movie();
|
||||
closed_and_library();
|
||||
two();
|
||||
printf("rtss_limit_check: %s\n", fails ? "FAIL" : "ok");
|
||||
return fails != 0;
|
||||
}
|
||||
|
||||
@@ -58,6 +58,8 @@ typedef struct {
|
||||
static Prof global = {1, 0, 1, 0, 0, 0, 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 DWORD refuse_over; // RTSS refuses a limit above this many frames a second (0: none refused)
|
||||
static int no_library; // LoadLibraryA fails
|
||||
static volatile LONG saves, deletes, updates, loads, created, removed;
|
||||
|
||||
static void touch(Prof *p) { p->stamp++; }
|
||||
@@ -86,6 +88,9 @@ static BOOL WINAPI f_get(LPCSTR prop, LPBYTE data, DWORD size) {
|
||||
}
|
||||
static BOOL WINAPI f_set(LPCSTR prop, LPBYTE data, DWORD size) {
|
||||
if (size != 4) return FALSE;
|
||||
DWORD v;
|
||||
memcpy(&v, data, 4);
|
||||
if (!strcmp(prop, "FramerateLimit") && refuse_over && v / (loaded.denom ? loaded.denom : 1) > refuse_over) 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;
|
||||
return FALSE;
|
||||
@@ -133,6 +138,7 @@ LSTATUS stub_RegGetValueA(HKEY k, LPCSTR sub, LPCSTR val, DWORD fl, LPDWORD ty,
|
||||
HMODULE stub_LoadLibraryA(LPCSTR p) {
|
||||
(void)p;
|
||||
loads++;
|
||||
if (no_library) return NULL;
|
||||
return (HMODULE)(uintptr_t)0x1000;
|
||||
}
|
||||
FARPROC stub_GetProcAddress(HMODULE m, LPCSTR n) {
|
||||
@@ -524,6 +530,142 @@ static void settings_logged(void) {
|
||||
CHECK(count("settings for the game") == 2, "once per change (%d)", count("settings for the game"));
|
||||
}
|
||||
|
||||
// The decisions as they stand, pinned against the real worker (rtss_limit_check.c pins the same in rtss_decide.h).
|
||||
|
||||
// Profiles under Program Files without administrator rights: the save does not stick, and RTSS is out for the run.
|
||||
static void readonly_run(void) {
|
||||
readonly = 1;
|
||||
app_set(1, 0xA, 1000, 2000, 60);
|
||||
ask(60000);
|
||||
poll();
|
||||
show("read-only profiles");
|
||||
CHECK(rtss_limit_state() == 2 && !rtss_limit_active(), "the menu says RTSS needs administrator rights");
|
||||
ask(144000);
|
||||
poll();
|
||||
poll();
|
||||
CHECK(saves == 1, "no write after the failure (%ld saves)", saves);
|
||||
CHECK(logged("RivaTuner Statistics Server's profile cannot be changed"), "logged");
|
||||
}
|
||||
|
||||
// RTSS refuses a limit out of its range: the game keeps that one, the earlier limit of this run is cleared to none,
|
||||
// and a limit in range goes to RTSS again at once.
|
||||
static void refused_run(void) {
|
||||
refuse_over = 200;
|
||||
app_set(1, 0xA, 1000, 2000, 60);
|
||||
ask(60000);
|
||||
poll();
|
||||
CHECK(own.limit == 60 && rtss_limit_active(), "60 taken");
|
||||
ask(240000);
|
||||
poll();
|
||||
show("240 refused");
|
||||
CHECK(own.exists && own.limit == 0, "the earlier limit of this run is cleared to none");
|
||||
CHECK(!rtss_limit_active() && rtss_limit_state() == 0, "the game's limiter keeps 240");
|
||||
long s0 = saves;
|
||||
poll();
|
||||
poll();
|
||||
CHECK(saves == s0, "the refused limit is not written again");
|
||||
ask(144000);
|
||||
poll();
|
||||
show("144 after the refusal");
|
||||
CHECK(own.limit == 144 && rtss_limit_active(), "144 taken at once");
|
||||
ask(240000);
|
||||
poll();
|
||||
CHECK(!rtss_limit_active(), "240 stays with the game");
|
||||
}
|
||||
|
||||
// RTSS lists the game but lets it run well over the limit: after six polls in a row over 1.2 times the limit the
|
||||
// profile is put back and the game limits for the run. 1.15 times the limit is not caught (the rule as it stands),
|
||||
// and a period RTSS no longer moves still counts.
|
||||
static void over_run(void) {
|
||||
app_set(1, 0xA, 1000, 2000, 144);
|
||||
ask(60000);
|
||||
for (int i = 0; i < 5; i++) poll();
|
||||
CHECK(rtss_limit_active() && own.exists, "five polls over: RTSS still limits");
|
||||
poll();
|
||||
show("six polls at 144 fps with the limit at 60");
|
||||
CHECK(rtss_limit_state() == 3 && !rtss_limit_active(), "the menu says RTSS does not limit the game");
|
||||
CHECK(!own.exists, "the profile is put back");
|
||||
CHECK(logged("RivaTuner Statistics Server measures 144.0 fps with its limit at 60.000 fps"), "logged");
|
||||
ask(30000);
|
||||
poll();
|
||||
CHECK(saves == 1 && rtss_limit_state() == 3, "out for the run");
|
||||
}
|
||||
static void over_slack_run(void) {
|
||||
app_set(1, 0xA, 1000, 2000, 69);
|
||||
ask(60000);
|
||||
for (int i = 0; i < 20; i++) poll();
|
||||
show("20 polls at 69 fps with the limit at 60");
|
||||
CHECK(rtss_limit_active() && rtss_limit_state() == 1, "1.15 times the limit is not caught");
|
||||
app_set(1, 0xA, 1000, 2000, 73);
|
||||
for (int i = 0; i < 6; i++) poll();
|
||||
CHECK(rtss_limit_state() == 3, "1.22 times the limit is caught after six polls");
|
||||
}
|
||||
static void over_unlimited_run(void) {
|
||||
app_set(1, 0xA, 1000, 2000, 700);
|
||||
ask(0);
|
||||
for (int i = 0; i < 10; i++) poll();
|
||||
show("unlimited at 700 fps");
|
||||
CHECK(rtss_limit_active() && rtss_limit_state() == 0, "no limit, nothing to check");
|
||||
}
|
||||
|
||||
// RTSS quits while it limits and comes back: the profile stays for its return, no write either way.
|
||||
static void quit_run(void) {
|
||||
app_set(1, 0xA, 1000, 2000, 60);
|
||||
ask(60000);
|
||||
poll();
|
||||
running = 0;
|
||||
poll();
|
||||
show("RTSS quit");
|
||||
CHECK(!rtss_limit_active() && own.exists && own.limit == 60, "the game limits; the profile stays");
|
||||
running = 1;
|
||||
poll();
|
||||
show("RTSS back");
|
||||
CHECK(rtss_limit_active() && saves == 1, "RTSS limits again with no new write");
|
||||
}
|
||||
|
||||
// RTSS's hooks library does not load: the game limits for the run.
|
||||
static void library_run(void) {
|
||||
no_library = 1;
|
||||
app_set(1, 0xA, 1000, 2000, 60);
|
||||
ask(60000);
|
||||
poll();
|
||||
ask(144000);
|
||||
poll();
|
||||
show("no hooks library");
|
||||
CHECK(loads == 1 && !rtss_limit_active() && !saves, "one try, then the game's limiter for the run");
|
||||
CHECK(logged("RivaTuner Statistics Server's hooks library did not load"), "logged");
|
||||
}
|
||||
|
||||
// Low latency switching on and off at every poll (the want flips between a limit and kept out): each switch writes or
|
||||
// puts back the profile, as it stands.
|
||||
static void churn_run(void) {
|
||||
app_set(1, 0xA, 1000, 2000, 60);
|
||||
ask(60000);
|
||||
poll();
|
||||
for (int i = 0; i < 10; i++) {
|
||||
ask(i & 1 ? 60000 : RTSS_OUT_LOWLAT);
|
||||
poll();
|
||||
}
|
||||
show("ten polls with the want flipping");
|
||||
CHECK(created == 6 && removed == 5, "%ld creations, %ld removals", created, removed);
|
||||
CHECK(rtss_limit_active(), "RTSS limits after the last want of a limit");
|
||||
}
|
||||
|
||||
// A movie at 60 under a limit of 144: the want follows the movie, and each start and end is a write.
|
||||
static void movie_run(void) {
|
||||
app_set(1, 0xA, 1000, 2000, 144);
|
||||
ask(144000);
|
||||
poll();
|
||||
long s0 = saves;
|
||||
ask(60000);
|
||||
poll();
|
||||
CHECK(own.limit == 60 && rtss_limit_active(), "RTSS holds the movie's 60");
|
||||
ask(144000);
|
||||
poll();
|
||||
show("after one movie");
|
||||
CHECK(own.limit == 144 && saves - s0 == 2, "two writes for one movie");
|
||||
}
|
||||
|
||||
static const struct { const char *name; void (*run)(void); } scenarios[] = {
|
||||
{"restore_race", restore_race},
|
||||
{"restore_player", restore_player},
|
||||
@@ -539,6 +681,15 @@ static const struct { const char *name; void (*run)(void); } scenarios[] = {
|
||||
{"start_off_present", start_off_present},
|
||||
{"not_installed", not_installed},
|
||||
{"settings_logged", settings_logged},
|
||||
{"readonly_run", readonly_run},
|
||||
{"refused_run", refused_run},
|
||||
{"over_run", over_run},
|
||||
{"over_slack_run", over_slack_run},
|
||||
{"over_unlimited_run", over_unlimited_run},
|
||||
{"quit_run", quit_run},
|
||||
{"library_run", library_run},
|
||||
{"churn_run", churn_run},
|
||||
{"movie_run", movie_run},
|
||||
};
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
|
||||
Reference in New Issue
Block a user