diff --git a/nx2navi.ini.example b/nx2navi.ini.example index 482c3bed..a088c5ec 100644 --- a/nx2navi.ini.example +++ b/nx2navi.ini.example @@ -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 diff --git a/runtime/cfg.c b/runtime/cfg.c index f0ff25b4..78e45906 100644 --- a/runtime/cfg.c +++ b/runtime/cfg.c @@ -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 diff --git a/runtime/freeze_hold.h b/runtime/freeze_hold.h new file mode 100644 index 00000000..fd1c4d88 --- /dev/null +++ b/runtime/freeze_hold.h @@ -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 + +#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; +} diff --git a/runtime/host.h b/runtime/host.h index 50f810ce..06ee0c20 100644 --- a/runtime/host.h +++ b/runtime/host.h @@ -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: diff --git a/runtime/perf.c b/runtime/perf.c index 67b35ddc..9dced1af 100644 --- a/runtime/perf.c +++ b/runtime/perf.c @@ -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]; diff --git a/runtime/vk_memory.c b/runtime/vk_memory.c index 3e423897..6e762570 100644 --- a/runtime/vk_memory.c +++ b/runtime/vk_memory.c @@ -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; } } diff --git a/tests/freeze_hold_check.c b/tests/freeze_hold_check.c new file mode 100644 index 00000000..9dee6174 --- /dev/null +++ b/tests/freeze_hold_check.c @@ -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 + +#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; +} diff --git a/tests/freeze_hold_check.py b/tests/freeze_hold_check.py new file mode 100644 index 00000000..ec750131 --- /dev/null +++ b/tests/freeze_hold_check.py @@ -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())