Keep the freezes of idle regions back after slow ones and around area loads.

After a slow freeze the passes wait 250 ms, doubled for each further slow one, but the first fast freeze after a hold started the doubling over, so in the seconds after a load a 4-5 ms freeze came every 250 ms (Kakariko, 2026-11-13: five in a row, 16 frames apart), and freezes of 4-10 ms pushed frames of 25-52 ms over the stutter line. The doubling now starts over only after freeze_quiet_ms (default 5000; 0: the old rule) without a slow freeze. No pass runs while the texture pool makes an area's textures, when the frames are held anyway and one freeze took 32.4 ms, nor for freeze_load_hold_ms (default 3000; 0: off) after that or after an area's load. The regions that go idle meanwhile stay queued and are frozen later; their write-watch queries go on until then. A region is now queued once instead of again every freeze_idle submissions while the passes are held.

The rules are in runtime/freeze_hold.h, checked by tests/freeze_hold_check.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
chrg6
2026-11-13 12:29:51 -05:00
parent f7487ceee5
commit 0e0cfe0827
8 changed files with 220 additions and 19 deletions

View File

@@ -1098,6 +1098,14 @@
# area's textures are there, instead of showing the ground and the walls black for the first seconds. 0: never.
# pool_load_backlog = 512
# pool_load_ms = 50
# Idle 2 MiB regions of game memory are made read-only so that the per-frame checks for CPU writes skip them. Right
# after an area load such a freeze can take 4-30 ms instead of microseconds, so after a slow one the freezes wait
# (250 ms, doubled for each further slow one up to 2 s) and start the doubling over only after freeze_quiet_ms without
# a slow one (0: after any fast freeze, as in 0.10.1). None is made while an area's textures load nor for
# freeze_load_hold_ms after that or after an area loads (0: no such hold). The regions wait meanwhile; their checks
# cost a few microseconds per MiB each submission until they are frozen.
# freeze_quiet_ms = 5000
# freeze_load_hold_ms = 3000
# Written storage-buffer ranges up to this size in MiB (1 to 256) go back to game memory as a merge, in 4 MiB pieces:
# copied within video memory and only the words that changed written over PCIe. Larger ones are copied back whole at
# the end of every submission. Raising it from 4 to 16 took a 5.5 MiB range off the plain copies (about 46 MiB a frame

View File

@@ -47,7 +47,7 @@ static const char *const known[] = {
"COLOR_PRESET", "COLOR_SATURATION", "CONTACT_DEBUG", "CONTACT_SHADOWS", "CORE_DIAG", "CRIT", "CRIT_OUT",
"DMABUF", "DMABUF_MB", "DRAW_CRUMBS", "GPA_LATE", "DESC_TEMPLATE", "DEVICE_PROFILE", "DISCORD_APP_ID", "DISCORD_IMAGE", "DISCORD_RPC", "DLAA", "DLAA_CAM_KEEP", "DLAA_CUT", "DLSS_IDLE_FREE", "DLAA_EDGE_FILL", "DLAA_AT_UPSCALER", "FFX_DEBUG", "FFX_KEYS", "FSR3_SHARPEN", "DLAA_JITTER", "DLAA_MV", "DLAA_MASKDEBUG", "DLAA_MVDEBUG", "DLAA_OBJMV", "DLAA_SURVEY", "DLAA_TARGET_KEEP", "DLAA_TIMES", "DLSS", "DLSS_CHAR_MASK", "DLSS_PRESET", "DLSS_PREBLOOM", "DLSS_RANGE", "DLSS_REACTIVE", "DUMP", "DUMP_BUF", "DUMP_DIR",
"DUMP_LOG", "DUMP_RAW", "DUMP_SHADERS", "DUMP_FILE", "DS3", "LOG_CS_BINDS", "LOG_TEX_FLAGS", "LOG_MESH", "MESH", "POOL", "NATIVE_TENSOR", "LOG_RCOND", "LOG_RT_SIZES", "DUMP_WHITE", "DUMP_AFTER", "DUMP_MOVIE", "SUBMIT_FINISH", "ACQUIRE_WAIT", "CRASH_FILTER_GUARD", "CS_FALLBACK", "EXEFS", "FEEDBACK", "FIX_C0", "FIX_ZERO", "FLIGHT", "FLIGHT_AT", "FLIGHT_DIR", "FLIGHT_FRAMES", "GPU_TIMELINE",
"FFX_PIPE_CACHE", "FLIGHT_SPIKE_MS", "STUTTER_MAP", "PAR_JOBS", "PIPE_RELEASE", "FLIGHT_THUMBS", "FLIGHT_EVERY", "FLIGHT_FLASH", "FLIGHT_FLASH_DIFF", "FLIGHT_FLASH_CELLS", "FLUSH_SPAN", "FLUSH_UPLOAD", "FOG_CULL", "FOLIAGE_SHADOW_CULL", "FOLIAGE_SHADOW_VRS", "FOV", "FOV_PATCH", "FG_HUDLESS_SHOW", "FG_MULT", "FPS60", "FPS_LIMIT", "FRAMEGEN", "FRAMEGEN_MULTIPLIER", "FREEZE_IDLE", "FREEZE_ASYNC", "FREEZE_BUDGET_US", "FREE_CORES",
"FFX_PIPE_CACHE", "FLIGHT_SPIKE_MS", "FREEZE_QUIET_MS", "FREEZE_LOAD_HOLD_MS", "STUTTER_MAP", "PAR_JOBS", "PIPE_RELEASE", "FLIGHT_THUMBS", "FLIGHT_EVERY", "FLIGHT_FLASH", "FLIGHT_FLASH_DIFF", "FLIGHT_FLASH_CELLS", "FLUSH_SPAN", "FLUSH_UPLOAD", "FOG_CULL", "FOLIAGE_SHADOW_CULL", "FOLIAGE_SHADOW_VRS", "FOV", "FOV_PATCH", "FG_HUDLESS_SHOW", "FG_MULT", "FPS60", "FPS_LIMIT", "FRAMEGEN", "FRAMEGEN_MULTIPLIER", "FREEZE_IDLE", "FREEZE_ASYNC", "FREEZE_BUDGET_US", "FREE_CORES",
"FULLSCREEN", "FXAA", "GAME_ASPECT", "GAME_RES", "GPA", "GPA_EMPTY", "GPA_TWIN", "GPA_TWIN_MAX_KB", "MIRROR_CAPTURE_KB", "WB_SKIP_MB", "GPL", "GPL_DRAW_BUILD", "GPL_FEEDBACK", "GPL_MESH", "PIPE_AHEAD", "PIPE_FAST_WORKERS", "PIPE_FRAME_WAIT_MS", "PIPE_LINK_DRAW_MS", "PIPE_SKIP_STEADY", "DRAW_WRITERS_WAIT", "PROG_LEAD", "GPU", "GPU_INDEX", "GPU_PASSES", "GPU_NAME", "HDR",
"HDR_GAME_NITS", "HDR_LIVE", "HEAP_LAZY", "HDR_PEAK_NITS", "HDR_UI_NITS",
// retired: the HDR look's settings of earlier versions, now fixed (vk_hdr.c, shaders/hdr_tonemap.frag); read by
@@ -147,7 +147,7 @@ static const struct { const char *key, *section; } sections[] = {
{"DYN_DELTA", "performance"}, {"STG_ARENA", "performance"}, {"SUBMIT_BATCH", "performance"}, {"SUBMIT_THREAD", "performance"},
{"BUILD_FREE_MB", "performance"}, {"BUILD_RESERVE_MB", "performance"}, {"MEM_NEED_MB", "diagnostics"},
{"ASTC_GPU_DESWIZZLE", "performance"}, {"ASTC_SCRATCH_KEEP", "performance"}, {"ASTC_ARENA", "performance"}, {"ASTC_BC1", "performance"}, {"POOL_CREATE_BUDGET", "performance"},
{"POOL_CREATE_MS", "performance"}, {"POOL_FINI_IDLE", "performance"}, {"FREEZE_BUDGET_US", "performance"}, {"POOL_LOAD_BACKLOG", "performance"}, {"POOL_LOAD_MS", "performance"},
{"POOL_CREATE_MS", "performance"}, {"POOL_FINI_IDLE", "performance"}, {"FREEZE_BUDGET_US", "performance"}, {"FREEZE_QUIET_MS", "performance"}, {"FREEZE_LOAD_HOLD_MS", "performance"}, {"POOL_LOAD_BACKLOG", "performance"}, {"POOL_LOAD_MS", "performance"},
{"TIMER_1MS", "performance"}, {"PIPE_WAIT", "performance"}, {"PIPE_SKIP", "performance"}, {"ONESHOT_BUDGET", "performance"},
{"PIPE_FRAME_WAIT_MS", "performance"}, {"PIPE_SKIP_STEADY", "performance"}, {"DRAW_WRITERS_WAIT", "performance"},
{"CACHE_DIE", "performance"}, {"ONE_PER_CORE", "performance"},
@@ -305,7 +305,9 @@ static const CfgMeta metas[] = {
{"DYN_RES", "0", "0|1", LIVE}, {"DYN_RES_TARGET", "16.7", "1..100", LIVE}, {"DYN_RES_STEP", "45", "8..600", LIVE},
// the recording thread: a core of its own (auto: with 12 or more logical processors) and the highest priority
// (vk_exec.c rec_claim)
{"RECORDER_CORE", "0", "auto|0|1", RESTART}, {"RECORDER_PRIO", "1", "0|1", RESTART}, {"REPLAY_TIME", "0", "0|1", RESTART}, {"LOCK_TIME", "0", "0|1", RESTART}, {"POOL_CREATE_BUDGET", "16", "1..4096", RESTART}, {"POOL_CREATE_MS", "6", "0..100", RESTART}, {"POOL_FINI_IDLE", "0", "0|1", RESTART}, {"FREEZE_BUDGET_US", "1000", "0..100000", RESTART}, {"POOL_LOAD_BACKLOG", "512", "0..65536", RESTART}, {"POOL_LOAD_MS", "50", "1..1000", RESTART},
{"RECORDER_CORE", "0", "auto|0|1", RESTART}, {"RECORDER_PRIO", "1", "0|1", RESTART}, {"REPLAY_TIME", "0", "0|1", RESTART}, {"LOCK_TIME", "0", "0|1", RESTART}, {"POOL_CREATE_BUDGET", "16", "1..4096", RESTART}, {"POOL_CREATE_MS", "6", "0..100", RESTART}, {"POOL_FINI_IDLE", "0", "0|1", RESTART}, {"FREEZE_BUDGET_US", "1000", "0..100000", RESTART}, {"POOL_LOAD_BACKLOG", "512", "0..65536", RESTART},
// vk_memory.c freeze_pass: the backoff's quiet time and the hold around area loads
{"FREEZE_QUIET_MS", "5000", "0..60000", RESTART}, {"FREEZE_LOAD_HOLD_MS", "3000", "0..30000", RESTART}, {"POOL_LOAD_MS", "50", "1..1000", RESTART},
// held submissions handed to the queue together (vk_sync.c, submit batching)
{"SUBMIT_BATCH", "4", "1..16", RESTART},
#ifdef _WIN32

57
runtime/freeze_hold.h Normal file
View File

@@ -0,0 +1,57 @@
// When the freezing of idle guest regions waits, the part without the OS: vk_memory.c freeze_pass makes the freezes
// (memory.c mem_freeze) and asks these functions whether a pass may run; tests/freeze_hold_check.py runs this file alone.
//
// A freeze is a VirtualQuery and a VirtualProtect of 2 MiB, some microseconds as a rule, and it only saves later
// write-watch queries (about a microsecond per MiB per submission), so it can wait. Around an area load it is slow: in
// the second after it, while the loaders write and the driver imports and locks new memory, freezes took 4 to 32 ms.
// Two rules hold the passes back:
// - A freeze of FRZ_SLOW_NS or more holds them for FRZ_BACKOFF_NS, doubled for each further slow one up to
// FRZ_BACKOFF_MAX_NS. The doubling starts over once quiet_ns pass without a slow freeze (quiet_ns 0: at the first
// fast freeze, the rule of 0.10.1, under which a slow freeze came every 250 ms for seconds after a load, fast ones
// between them).
// - While the texture pool makes an area's textures, and for load_hold_ns after that or after an area's load, no pass
// runs (load_hold_ns 0: no such hold).
#pragma once
#include <stdint.h>
#define FRZ_SLOW_NS 2000000ull
#define FRZ_BACKOFF_NS 250000000ull
#define FRZ_BACKOFF_MAX_NS 2000000000ull
typedef struct {
uint64_t hold_until; // the backoff holds the passes until then
uint64_t backoff; // the last hold's length; 0: none since the doubling started over
uint64_t last_slow; // when the last slow freeze ended; 0: none yet
uint64_t load_until; // an area load holds the passes until then
} FrzHold;
// A freeze ended at t1 after taking took ns.
static inline void frz_freeze_done(FrzHold *h, uint64_t t1, uint64_t took, uint64_t quiet_ns) {
if (took < FRZ_SLOW_NS) {
if (!quiet_ns) h->backoff = 0;
return;
}
if (quiet_ns && h->last_slow && t1 - h->last_slow >= quiet_ns) h->backoff = 0;
h->backoff = h->backoff ? h->backoff * 2 : FRZ_BACKOFF_NS;
if (h->backoff > FRZ_BACKOFF_MAX_NS) h->backoff = FRZ_BACKOFF_MAX_NS;
h->hold_until = t1 + h->backoff;
h->last_slow = t1;
}
// Whether an area load holds the passes at now: loading, the texture pool is making an area's textures; area_at, when
// the last area load began (0: none).
static inline int frz_load_hold(FrzHold *h, uint64_t now, uint64_t load_hold_ns, int loading, uint64_t area_at) {
if (!load_hold_ns) return 0;
if (loading) {
h->load_until = now + load_hold_ns;
return 1;
}
if (area_at && area_at <= now && area_at + load_hold_ns > h->load_until) h->load_until = area_at + load_hold_ns;
return now < h->load_until;
}
// Whether a pass may run at now (the backoff, then the load).
static inline int frz_may_run(FrzHold *h, uint64_t now, uint64_t load_hold_ns, int loading, uint64_t area_at) {
int load = frz_load_hold(h, now, load_hold_ns, loading, area_at);
return now >= h->hold_until && !load;
}

View File

@@ -63,6 +63,7 @@ void perf_mark_add(int kind, uint64_t ns, uint64_t bytes); // nothing while th
// (romfs.c, the area's static placement file) and takes the memory snapshot once no area has loaded for 10 s.
void perf_segment(const char *name);
void perf_scene(const char *name);
uint64_t perf_area_load_ns(void); // plat_now_ns of the last perf_scene (any trace setting), 0 before the first
// NX2NAVI_TRACE=perf "mem: sizes" lines (perf.c sizes_log): the entries and bytes of the caches, pools and tables no
// other line gives, every 600 presents and with each memory snapshot. Each subsystem's *_sizes function (weak no-ops
// in perf.c) adds its own with perf_size, taking its locks briefly on the present thread. count or bytes UINT64_MAX:

View File

@@ -261,7 +261,12 @@ void perf_segment(const char *name) {
plat_mutex_unlock(&seg_lock);
}
static volatile uint64_t area_load_ns; // plat_now_ns of the last area load (perf_scene), 0 none
uint64_t perf_area_load_ns(void) { return __atomic_load_n(&area_load_ns, __ATOMIC_RELAXED); }
void perf_scene(const char *name) {
__atomic_store_n(&area_load_ns, plat_now_ns(), __ATOMIC_RELAXED);
stutter_area(name);
if (!(rt_trace & RT_TRACE_PERF)) return;
char wb[24];

View File

@@ -15,6 +15,7 @@
#include "cfg.h"
#include "flight.h"
#include "freeze_hold.h"
#include "gfx.h"
#include "host.h"
#include "logq.h"
@@ -1201,6 +1202,9 @@ static int freeze_idle; // 0: off
static uint32_t ww_ep = 1; // gen_lock: the submission being recorded (vk_mem_mirror_begin)
static uint32_t reg_last[RT_ADDRESS_LIMIT >> REG_SHIFT]; // gen_lock: ww_ep of the region's last stamp
static uint32_t reg_tried[RT_ADDRESS_LIMIT >> REG_SHIFT]; // gen_lock: ww_ep of its last freeze candidacy
// gen_lock: the region is in fcand. While the passes are held a region still idle came up again every freeze_idle
// submissions and went in a second time; mem_frozen skipped the copies, but the queue grew with them.
static uint8_t reg_queued[RT_ADDRESS_LIMIT >> REG_SHIFT];
static uint32_t *fcand; // gen_lock: regions to try at the next freeze pass
static int nfcand, cfcand;
static uint64_t n_frz_refused; // gen_lock: candidates mem_freeze refused
@@ -1306,10 +1310,11 @@ static uint64_t snap_regions(uint64_t r0, uint64_t r1, uint32_t epoch) {
for (uint64_t j = r; j <= k; j++) {
snap_at[j] = epoch, snap_untracked[j] = (uint8_t)!tracked;
if (tracked && freeze_idle && ww_ep - reg_last[j] >= (uint32_t)freeze_idle &&
ww_ep - reg_tried[j] >= (uint32_t)freeze_idle) {
ww_ep - reg_tried[j] >= (uint32_t)freeze_idle && !reg_queued[j]) {
reg_tried[j] = ww_ep;
if (nfcand == cfcand) grow((void **)&fcand, &cfcand, sizeof *fcand);
fcand[nfcand++] = (uint32_t)j;
reg_queued[j] = 1;
}
}
r = k + 1;
@@ -3896,26 +3901,43 @@ void nvn_pool_flushed(const pool_t *p, uint64_t off, uint64_t size) {
// regions all go idle together, so 32 of them came up in one submission: its mirror begin took 70.5 ms and the frame
// 78 ms, and one freeze of 9 ms followed in each of the next 20 frames. A freeze only saves later write-watch queries
// (about a microsecond per MiB per submission), so it can wait: a pass stops once it has spent freeze_budget_ns, and a
// freeze over FREEZE_SLOW_NS holds the passes for FREEZE_BACKOFF_NS, doubled for each slow one in a row up to 2 s.
// NX2NAVI_FREEZE_BUDGET_US (default 1000; 0: no budget and no backoff, as before).
#define FREEZE_SLOW_NS 2000000ull
#define FREEZE_BACKOFF_NS 250000000ull
#define FREEZE_BACKOFF_MAX_NS 2000000000ull
// freeze over FREEZE_SLOW_NS holds the passes for FREEZE_BACKOFF_NS, doubled for each slow one up to 2 s.
// NX2NAVI_FREEZE_BUDGET_US (default 1000; 0: no budget, no backoff and no hold at loads, as before).
//
// The doubling starts over only once NX2NAVI_FREEZE_QUIET_MS (default 5000) have passed without a slow freeze. It
// started over at the first fast freeze after a hold, and the slow ones are slow for seconds after a load while fast
// ones come between them: after a load in Kakariko (2026-11-13) a 4-5 ms freeze came every 250 ms, 16 frames apart,
// five in a row, and 4-10 ms freezes made frames of 25-52 ms cross the stutter line. 0: as before, a fast freeze
// starts the doubling over.
// And no pass runs while the texture pool makes an area's textures (vk_exec_pool_loading: the frames are held, and a
// freeze in the load's frame took 32.4 ms) nor for NX2NAVI_FREEZE_LOAD_HOLD_MS (default 3000; 0: off) after that or
// after an area's load (perf_area_load_ns), when the loaders write and the driver imports the new memory. The regions
// that go idle meanwhile stay queued and are frozen after it; until then their write-watch queries go on, a few
// microseconds per MiB per submission for those seconds. freeze_hold.h has the rules.
static uint64_t freeze_budget_ns = ~0ull; // ~0: not read yet
static uint64_t frz_hold_until, frz_backoff, frz_held_passes, frz_slow; // gen_lock
static uint64_t frz_quiet_ns, frz_load_hold_ns;
static FrzHold frz; // gen_lock
static uint64_t frz_held_passes, frz_slow, frz_load_held; // gen_lock
static void freeze_pass(void) {
if (freeze_budget_ns == ~0ull) {
long long us = cfg_int("NX2NAVI_FREEZE_BUDGET_US");
freeze_budget_ns = us > 0 ? (uint64_t)us * 1000 : 0;
long long q = cfg_int("NX2NAVI_FREEZE_QUIET_MS"), h = cfg_int("NX2NAVI_FREEZE_LOAD_HOLD_MS");
frz_quiet_ns = q > 0 ? (uint64_t)q * 1000000 : 0;
frz_load_hold_ns = h > 0 && freeze_budget_ns ? (uint64_t)h * 1000000 : 0;
}
uint64_t start = qpc();
if (freeze_budget_ns && start < frz_hold_until) {
if (nfcand) frz_held_passes++;
if (freeze_budget_ns && !frz_may_run(&frz, start, frz_load_hold_ns, vk_exec_pool_loading(), perf_area_load_ns())) {
if (nfcand) {
frz_held_passes++;
if (start >= frz.hold_until) frz_load_held++;
}
return;
}
for (int done = 0; nfcand && done < FREEZE_PER_SUB;) {
uint64_t j = fcand[--nfcand], a = j << REG_SHIFT;
reg_queued[j] = 0;
if (ww_ep - reg_last[j] < (uint32_t)freeze_idle || mem_frozen(a)) continue;
done++;
uint64_t t0 = qpc();
@@ -3925,19 +3947,17 @@ static void freeze_pass(void) {
if (ok) query_stamp(a, a + (1ull << REG_SHIFT), ++gen_now);
else n_frz_refused++;
if (!freeze_budget_ns) continue;
if (t1 - t0 >= FREEZE_SLOW_NS) {
frz_freeze_done(&frz, t1, t1 - t0, frz_quiet_ns);
if (t1 - t0 >= FRZ_SLOW_NS) {
frz_slow++;
frz_backoff = frz_backoff ? frz_backoff * 2 : FREEZE_BACKOFF_NS;
if (frz_backoff > FREEZE_BACKOFF_MAX_NS) frz_backoff = FREEZE_BACKOFF_MAX_NS;
frz_hold_until = t1 + frz_backoff;
static uint32_t logged;
if (++logged <= 40)
rt_log("mem: freezing an idle region took %.1f ms: no freezes for %.0f ms (%llu slow freezes so far, %llu "
"passes held, %d regions waiting)", (t1 - t0) / 1e6, frz_backoff / 1e6, (unsigned long long)frz_slow,
(unsigned long long)frz_held_passes, nfcand);
"passes held, %llu of them by area loads, %d regions waiting)", (t1 - t0) / 1e6, frz.backoff / 1e6,
(unsigned long long)frz_slow, (unsigned long long)frz_held_passes,
(unsigned long long)frz_load_held, nfcand);
break;
}
frz_backoff = 0;
if (qpc() - start >= freeze_budget_ns) break;
}
}

68
tests/freeze_hold_check.c Normal file
View File

@@ -0,0 +1,68 @@
// runtime/freeze_hold.h: when the freezing of idle guest regions waits. Built and run by tests/freeze_hold_check.py;
// prints each failed check and exits 1 on any failure.
//
// The backoff after slow freezes keeps doubling while slow ones come within the quiet time, fast freezes between them
// included, and starts over after the quiet time; with quiet 0 a fast freeze starts it over (the rule of 0.10.1). An
// area load holds the passes while the texture pool loads and for the hold after it or after the area's load.
#include <stdio.h>
#include "freeze_hold.h"
static int fails;
#define CHECK(c, ...) do { if (!(c)) { fails++; printf("FAIL line %d: ", __LINE__); printf(__VA_ARGS__); printf("\n"); } } while (0)
#define MS 1000000ull
#define S 1000000000ull
int main(void) {
// slow freezes 300 ms apart with fast ones between, as after a load in Kakariko: the holds double up to 2 s
{
FrzHold h = {0};
uint64_t t = 10 * S;
frz_freeze_done(&h, t, 5 * MS, 5 * S);
CHECK(h.backoff == 250 * MS && h.hold_until == t + 250 * MS, "first slow: %llu", (unsigned long long)h.backoff);
frz_freeze_done(&h, t + 260 * MS, 100000, 5 * S); // a fast one after the hold
CHECK(h.backoff == 250 * MS, "a fast freeze keeps the backoff: %llu", (unsigned long long)h.backoff);
frz_freeze_done(&h, t + 300 * MS, 4 * MS, 5 * S);
CHECK(h.backoff == 500 * MS, "second slow doubles: %llu", (unsigned long long)h.backoff);
frz_freeze_done(&h, t + 900 * MS, 4 * MS, 5 * S);
frz_freeze_done(&h, t + 2 * S, 4 * MS, 5 * S);
frz_freeze_done(&h, t + 4 * S, 4 * MS, 5 * S);
CHECK(h.backoff == FRZ_BACKOFF_MAX_NS, "capped at 2 s: %llu", (unsigned long long)h.backoff);
frz_freeze_done(&h, t + 10 * S, 3 * MS, 5 * S); // 6 s after the last slow one: starts over
CHECK(h.backoff == 250 * MS, "after the quiet time it starts over: %llu", (unsigned long long)h.backoff);
}
// quiet 0: the rule of 0.10.1, a fast freeze starts the doubling over
{
FrzHold h = {0};
frz_freeze_done(&h, 1 * S, 5 * MS, 0);
frz_freeze_done(&h, 1 * S + 260 * MS, 100000, 0);
frz_freeze_done(&h, 1 * S + 300 * MS, 5 * MS, 0);
CHECK(h.backoff == 250 * MS, "quiet 0: a fast freeze between restarts it: %llu", (unsigned long long)h.backoff);
}
// the backoff holds the passes until hold_until
{
FrzHold h = {0};
frz_freeze_done(&h, 1 * S, 5 * MS, 5 * S);
CHECK(!frz_may_run(&h, 1 * S + 100 * MS, 3 * S, 0, 0), "held by the backoff");
CHECK(frz_may_run(&h, 1 * S + 250 * MS, 3 * S, 0, 0), "runs after the backoff");
}
// area loads: the pool's load holds the passes, and the hold lasts after it ends
{
FrzHold h = {0};
CHECK(frz_may_run(&h, 100 * S, 3 * S, 0, 0), "no load: runs");
CHECK(!frz_may_run(&h, 100 * S, 3 * S, 1, 0), "the pool loads: held");
CHECK(!frz_may_run(&h, 101 * S, 3 * S, 1, 0), "still loading: held");
CHECK(!frz_may_run(&h, 103 * S, 3 * S, 0, 0), "2 s after the load ended: held");
CHECK(frz_may_run(&h, 104 * S + 1, 3 * S, 0, 0), "3 s after: runs");
// an area's load (perf_scene) 1 s ago holds; one 4 s ago does not
CHECK(!frz_may_run(&h, 200 * S, 3 * S, 0, 199 * S), "area loaded 1 s ago: held");
CHECK(frz_may_run(&h, 204 * S, 3 * S, 0, 200 * S), "area loaded 4 s ago: runs");
// a hold of 0 never holds for loads
FrzHold z = {0};
CHECK(frz_may_run(&z, 300 * S, 0, 1, 299 * S), "load hold 0: runs while loading");
}
if (fails) return 1;
printf("freeze_hold_check: all checks passed\n");
return 0;
}

View File

@@ -0,0 +1,40 @@
"""Build and run tests/freeze_hold_check.c: runtime/freeze_hold.h, which says when the freezing of idle guest regions
waits (the backoff after slow freezes and its quiet time, and the hold around area loads).
No GPU, no game files, any Python.
Usage: freeze_hold_check.py
"""
import os
import shutil
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
MSYS_BIN = Path(r'C:\msys64\ucrt64\bin')
def compiler():
"""$CC, else MSYS2 UCRT64's clang, else clang on PATH (as tests/diff/common.py picks it, without its Unicorn)."""
if os.environ.get('CC'):
return os.environ['CC']
if os.name == 'nt' and (MSYS_BIN / 'clang.exe').exists():
return str(MSYS_BIN / 'clang.exe')
return shutil.which('clang') or 'clang'
def main():
work = ROOT / 'build' / 'test' / 'freeze_hold'
work.mkdir(parents=True, exist_ok=True)
exe = work / ('freeze_hold_check.exe' if os.name == 'nt' else 'freeze_hold_check')
env = dict(os.environ)
if os.name == 'nt' and MSYS_BIN.exists():
env['PATH'] = str(MSYS_BIN) + os.pathsep + env.get('PATH', '')
subprocess.check_call([compiler(), '-std=gnu11', '-O1', '-Wall', '-Werror', '-I', str(ROOT / 'runtime'),
str(ROOT / 'tests' / 'freeze_hold_check.c'), '-o', str(exe)], env=env)
return subprocess.run([str(exe)], env=env).returncode
if __name__ == '__main__':
sys.exit(main())