mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
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:
@@ -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
|
||||
|
||||
@@ -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
57
runtime/freeze_hold.h
Normal 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;
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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
68
tests/freeze_hold_check.c
Normal 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;
|
||||
}
|
||||
40
tests/freeze_hold_check.py
Normal file
40
tests/freeze_hold_check.py
Normal 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())
|
||||
Reference in New Issue
Block a user