Count the recording copies whose control memory the game reused, behind the new NX2NAVI_COPY_RETIRE switch.

Recording copies are retired only when the game adds their control memory again (release_copies), and the fresh
0.10.0 session's "mem: sizes" lines show them growing for good: 220 at frame 600, 223,646 (56.3 MiB) at frame 39600,
with no zombies. This is the first stage of retiring them: it only counts, so the second stage can be judged on
numbers. With copy_retire = 1, copy_add queues each copy's control position, and an "nvn copy sweep" thread at the
lowest priority fingerprints the 16 bytes there within 5 seconds, then visits up to 4096 copies every 5 seconds from a
cursor and counts those no submission used for 5 minutes whose bytes changed or whose memory is no longer mapped. The
"mem: sizes" line gains "of those idle, control memory changed" and "control memory gone". Nothing is freed.

Our driver never writes control memory, so the bytes compared are the game's own leftovers; a reuse that writes the
same bytes is not seen and the counts are a lower bound. Every read of guest memory happens on the sweep thread, never
on a recording thread: a lazily armed range is not read at all, an untouched lazy-heap page is taken as zeros, and the
read holds the mapping with mem_read_hold. The sweep takes copy_lock shared only, 64 lookups at a time, and its queue
has a lock of its own that is never held with copy_lock. Copies that share a control position (EndRecording falls
back to the block's start when it is full) each keep their own record.

The age ring behind "of them unused for 5 minutes" grows from 1024 to 65536 minutes: at 1024 a copy last used 17 hours
earlier shared its slot with the minute now and counted as recent.

New row COPY_RETIRE (0|1, default 0, restart) in known[], sections[] (diagnostics) and metas[], and a copy_retire entry
in nx2navi.ini.example. tests/copy_sweep_check.py cuts the fingerprint, visit, queue and pass functions from
nvn_api_cmd.c with stub copies and stub guest memory: changed bytes count as changed, unmapped memory as gone,
unchanged bytes and copies used within 5 minutes count nothing, armed ranges and untouched pages are never read, retired
and replaced copies leave the counts, and two passes over 5000 copies visit each once or twice. The cfg row check
reports no missing rows, launcher_test passes, and the compile check adds no warnings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
chrg6
2026-11-13 17:19:05 -05:00
parent 0b217575d8
commit 811024e69b
4 changed files with 334 additions and 2 deletions

View File

@@ -1101,6 +1101,12 @@
# "ref" is perf_lock_timing_here, which the link map names), the recorder's waits, and the texture lock's holds over
# 1 ms. Off by default.
# lock_time = 0
# 1: with trace = perf, the "mem: sizes" line also counts the recording copies (the game's recorded command lists,
# kept until the game hands their control memory over again) that no submission used for 5 minutes and whose control
# memory the game has written over since, or no longer maps: copies the game will not use again. A thread of its own
# reads 16 bytes of game memory per copy every 5 seconds; nothing is freed. The count is a lower bound: memory written
# over with the same bytes is not seen. Read at start. Default: 0.
# copy_retire = 0
# Texture entries the bindless texture pool may create in one refresh (images and their uploads); the rest wait for
# the next refresh and show their stand-in until then. Lower spreads streaming over more frames (smaller hitches, a
# frame or two of pop-in); higher loads sooner.

View File

@@ -84,6 +84,7 @@ static const char *const known[] = {
"TEXUP_LOG_MS", "DYN_RES", "DYN_RES_TARGET", "DYN_RES_STEP", "TEXTURE_DETAIL", "VRAM_USE", // vram_use: the removed Video memory use row, read by nothing
"RECORDER_CORE", "RECORDER_PRIO", "REPLAY_TIME", "LOCK_TIME", "POOL_CREATE_BUDGET", "POOL_CREATE_MS", "POOL_FINI_IDLE", "POOL_LOAD_BACKLOG", "POOL_LOAD_MS",
"VMA_BLOCK_MB", "SYNC_WAKE_ALL", "LOG_ASYNC", "DEVICE_LOST_AT", "STAGING_FAIL_AT", "VRAM_BALLAST_MB",
"COPY_RETIRE",
"MEM_BUDGET_TEST_VRAM_MB", "MEM_BUDGET_TEST_RAM_MB",
"TEX_COMPACT", "TEX_COMPACT_MIN_MB", "TEX_COMPACT_FRAME_MB", // texture compaction (vk_texture.c); no menu row
"STANDIN_FILTER", // the filter of the scene upscaler's stand-in (vk_tonemap.c); no menu row
@@ -170,6 +171,7 @@ static const struct { const char *key, *section; } sections[] = {
{"WAIT_WATCH_S", "diagnostics"},
{"HANDLE_CENSUS", "diagnostics"},
{"VMA_BLOCK_MB", "diagnostics"}, {"DEVICE_LOST_AT", "diagnostics"}, {"STAGING_FAIL_AT", "diagnostics"}, {"VRAM_BALLAST_MB", "diagnostics"},
{"COPY_RETIRE", "diagnostics"},
{"MEM_BUDGET_TEST_VRAM_MB", "diagnostics"}, {"MEM_BUDGET_TEST_RAM_MB", "diagnostics"},
{"TEX_COMPACT", "performance"}, {"TEX_COMPACT_MIN_MB", "performance"}, {"TEX_COMPACT_FRAME_MB", "performance"},
};
@@ -441,6 +443,8 @@ static const CfgMeta metas[] = {
{"MEM_BUDGET_TEST_VRAM_MB", "0", "0..1048576", RESTART},
{"MEM_BUDGET_TEST_RAM_MB", "0", "0..1048576", RESTART},
{"DEVICE_LOST_AT", "0", "0..1000000000", RESTART}, // vk_sync.c lost_at_due: a test of the lost device's end
// nvn_api_cmd.c retire_pass: counts the recording copies whose control memory the game reused; frees nothing
{"COPY_RETIRE", "0", "0|1", RESTART},
{"INPUT_RECORD", "", NULL, RESTART},
};

View File

@@ -425,8 +425,10 @@ static uint64_t ncopies, copy_bytes, copies_made;
// take at every EndRecording. A copy is retired only when the game adds its control memory again
// (nvnCommandBufferAddControlMemory), and a session of 2026-11-13 grew from 9,138 live copies (0.7 MiB) to 223,753
// (58.3 MiB) in 7 minutes of play, about 38,000 more at each return to an area: these figures tell copies the game
// still submits from ones it left behind. Kept for COPY_AGE_MINS minutes of the run, a ring of counters by minute.
#define COPY_AGE_MINS 1024
// still submits from ones it left behind. A ring of counters by minute, COPY_AGE_MINS long (45 days, so it does not
// wrap within a session: a copy last used that long before would share its slot with the minute now and count as
// recent); only the minutes that have passed touch its pages.
#define COPY_AGE_MINS 65536
#define COPY_IDLE_MINS 5
static volatile int64_t age_n[COPY_AGE_MINS], age_b[COPY_AGE_MINS];
@@ -548,6 +550,142 @@ static uint8_t *flatten_recording(const uint8_t *p, size_t n) {
return out + sizeof(copy_prefix);
}
// ---------- recording copies whose control memory the game reused (NX2NAVI_COPY_RETIRE=1: a count, nothing freed)
// The only event that retires a copy is AddControlMemory over its control memory again (release_copies), so a copy
// whose control memory the game has since used for other data stays for good; a session of 2026-11-13 held 223,646
// (56.3 MiB) by frame 39600. This counts those copies before anything retires them. Our driver never writes control
// memory (cmd_t.ctl is accounting only), so the 16 bytes at a copy's control position are whatever the game left
// there, and a fingerprint of them that no longer matches says the game wrote there since. A reuse that writes the
// same bytes (zeros, often) is not seen: the counts are a lower bound.
// The "nvn copy sweep" thread does every read of guest memory, at its lowest priority: a recording thread taking the
// fingerprint at EndRecording could fault into the lazy heap (an untouched control block) and wait there. Every
// RETIRE_PERIOD_MS it fingerprints the copies made since its last pass (a queue copy_add fills, under retire_lock),
// then visits up to RETIRE_BATCH of the copies it knows, from a cursor, and counts those no submission used for
// COPY_IDLE_MINS whose bytes changed or whose memory is gone. It takes copy_lock shared only, RETIRE_CHUNK lookups at a
// time; a copy that is no longer live (retired, or its handle reused: made_at and bytes differ) leaves its records.
#define RETIRE_PERIOD_MS 5000
#define RETIRE_BATCH 4096
#define RETIRE_CHUNK 64
#define FP_UNKNOWN 0ull // not read: a lazily armed range, which a read would fetch from the GPU
#define FP_GONE 2ull // not mapped; every fingerprint of bytes is odd
enum { RS_NONE, RS_CHANGED, RS_GONE }; // what a record counts in: nothing, changed, gone
typedef struct { uint64_t handle, lo, made_at, bytes; } retire_new;
typedef struct { uint64_t handle, lo, made_at, bytes, fp; int state; } retire_rec;
static PlatMutex retire_lock = PLAT_MUTEX_INIT; // the queue; never held with copy_lock
static retire_new *retire_q;
static uint32_t retire_qn, retire_qcap;
static retire_rec *retire_recs; // the sweep thread's own
static uint32_t retire_nrec, retire_cap, retire_cursor;
static volatile int64_t retire_changed_n, retire_changed_b, retire_gone_n, retire_gone_b;
// 16 bytes of guest memory at lo as a fingerprint: odd, FP_GONE or FP_UNKNOWN. An untouched page of the lazy heap reads
// as zeros, so it is taken as zeros without the read that would commit it.
static uint64_t ctl_fingerprint(uint64_t lo) {
uint8_t b[16];
if (mem_lazy_touches(lo, sizeof b)) return FP_UNKNOWN;
if (mem_untouched_run(lo, lo + sizeof b) >= lo + sizeof b) {
memset(b, 0, sizeof b);
} else {
if (!mem_read_hold(lo, sizeof b)) return FP_GONE;
memcpy(b, (const void *)(uintptr_t)lo, sizeof b);
mem_read_release();
}
uint64_t h = 0xcbf29ce484222325ull;
for (size_t i = 0; i < sizeof b; i++) h = (h ^ b[i]) * 0x100000001b3ull;
return h | 1;
}
static void retire_count(int state, uint64_t bytes, int64_t sign) {
if (state == RS_CHANGED) plat_add64(&retire_changed_n, sign), plat_add64(&retire_changed_b, sign * (int64_t)bytes);
if (state == RS_GONE) plat_add64(&retire_gone_n, sign), plat_add64(&retire_gone_b, sign * (int64_t)bytes);
}
// One visit (sweep thread): live, used_min and bytes as copy_lock showed them; min the minute now.
static void retire_visit(retire_rec *r, int live, uint64_t used_min, uint64_t min) {
int state = RS_NONE;
if (live && r->fp != FP_UNKNOWN && r->fp != FP_GONE && min >= used_min + COPY_IDLE_MINS) {
uint64_t now = ctl_fingerprint(r->lo);
state = now == FP_UNKNOWN ? r->state : now == FP_GONE ? RS_GONE : now != r->fp ? RS_CHANGED : RS_NONE;
}
if (state != r->state) retire_count(r->state, r->bytes, -1), retire_count(state, r->bytes, 1), r->state = state;
}
// NX2NAVI_COPY_RETIRE, read at the first copy.
static int retire_on(void) {
static volatile int32_t mode = -1;
int32_t m = __atomic_load_n(&mode, __ATOMIC_RELAXED);
if (m < 0) __atomic_store_n(&mode, m = cfg_int("NX2NAVI_COPY_RETIRE"), __ATOMIC_RELAXED);
return m > 0;
}
// Called by copy_add once the copy is live, copy_lock no longer held. A failed allocation leaves the copy uncounted.
static void retire_queue(uint64_t handle, uint64_t lo, uint64_t made_at, uint64_t bytes) {
plat_mutex_lock(&retire_lock);
if (retire_qn == retire_qcap) {
uint32_t cap = retire_qcap ? retire_qcap * 2 : 1024;
retire_new *q = realloc(retire_q, cap * sizeof *q);
if (q) retire_q = q, retire_qcap = cap;
}
if (retire_qn < retire_qcap) retire_q[retire_qn++] = (retire_new){handle, lo, made_at, bytes};
plat_mutex_unlock(&retire_lock);
}
// One pass of the sweep thread: the new copies' fingerprints, then up to RETIRE_BATCH visits from the cursor.
static void retire_pass(void) {
plat_mutex_lock(&retire_lock);
retire_new *q = retire_q;
uint32_t qn = retire_qn;
retire_q = NULL, retire_qn = retire_qcap = 0;
plat_mutex_unlock(&retire_lock);
for (uint32_t i = 0; i < qn; i++) {
if (retire_nrec == retire_cap) {
uint32_t cap = retire_cap ? retire_cap * 2 : 1024;
retire_rec *v = realloc(retire_recs, cap * sizeof *v);
if (!v) break; // the rest go uncounted
retire_recs = v, retire_cap = cap;
}
retire_recs[retire_nrec++] =
(retire_rec){q[i].handle, q[i].lo, q[i].made_at, q[i].bytes, ctl_fingerprint(q[i].lo), RS_NONE};
}
free(q);
uint64_t min = copy_minute();
uint32_t todo = retire_nrec < RETIRE_BATCH ? retire_nrec : RETIRE_BATCH;
while (todo && retire_nrec) {
if (retire_cursor >= retire_nrec) retire_cursor = 0;
uint32_t c = retire_cursor, n = retire_nrec - c;
if (n > todo) n = todo;
if (n > RETIRE_CHUNK) n = RETIRE_CHUNK;
int live[RETIRE_CHUNK];
uint64_t used[RETIRE_CHUNK];
plat_mutex_lock_shared(&copy_lock);
for (uint32_t k = 0; k < n; k++) {
const retire_rec *r = &retire_recs[c + k];
const copy_prefix *pf = copy_find(r->handle);
live[k] = pf && pf->made_at == r->made_at && pf->bytes == r->bytes;
used[k] = live[k] ? __atomic_load_n(&pf->used_min, __ATOMIC_RELAXED) : 0;
}
plat_mutex_unlock_shared(&copy_lock);
// From the chunk's last record down: a removal moves the last record into the freed place, which then holds a
// record this chunk has visited or one from past it (visited at the next pass).
for (uint32_t k = n; k-- > 0;) {
retire_rec *r = &retire_recs[c + k];
retire_visit(r, live[k], used[k], min);
if (!live[k]) *r = retire_recs[--retire_nrec];
}
retire_cursor = c + n;
todo -= n;
}
}
static void retire_main(void *arg) {
(void)arg;
plat_thread_priority(PLAT_PRIO_LOWEST);
for (;;) {
plat_sleep_ms(RETIRE_PERIOD_MS);
retire_pass();
}
}
static void copy_add(uintptr_t lo, uintptr_t hi, uint8_t *copy) {
copy_prefix *pf = (copy_prefix *)copy - 1;
uint64_t handle = (uintptr_t)copy;
@@ -574,7 +712,16 @@ static void copy_add(uintptr_t lo, uintptr_t hi, uint8_t *copy) {
if (++copies_made % 20000 == 0)
NVN_LOG("recording copies: %llu made, %llu live, %.1f MiB, submit %llu", (unsigned long long)copies_made,
(unsigned long long)ncopies, copy_bytes / 1048576.0, (unsigned long long)nsubmits);
uint64_t made_at = pf->made_at, bytes = pf->bytes;
plat_mutex_unlock(&copy_lock);
if (hi && retire_on()) {
static volatile int32_t started, failed;
if (!plat_xchg32(&started, 1) && !plat_thread_spawn(retire_main, NULL, "nvn copy sweep", 0)) {
plat_xchg32(&failed, 1);
rt_log("nvn: the copy sweep thread did not start: NX2NAVI_COPY_RETIRE counts nothing");
}
if (!__atomic_load_n(&failed, __ATOMIC_RELAXED)) retire_queue(handle, lo, made_at, bytes);
}
}
// Adding control memory again retires every recording copy made in it (the driver's handle lived there).
@@ -1383,6 +1530,18 @@ void nvn_cmd_sizes(PerfSizes *s) {
perf_size(s, "recording copies", nc, 0, cb);
perf_size(s, "of them unused for 5 minutes", idle_n, 0, idle_b);
perf_size(s, "recording copy zombies", nz, 0, zb);
if (retire_on()) {
// NX2NAVI_COPY_RETIRE: of the copies unused for 5 minutes, those whose control memory the game has written
// since and those whose control memory is no longer mapped (retire_pass). A lower bound: a rewrite with the
// same bytes is not seen.
int64_t v[4] = {__atomic_load_n(&retire_changed_n, __ATOMIC_RELAXED),
__atomic_load_n(&retire_changed_b, __ATOMIC_RELAXED),
__atomic_load_n(&retire_gone_n, __ATOMIC_RELAXED),
__atomic_load_n(&retire_gone_b, __ATOMIC_RELAXED)};
for (int i = 0; i < 4; i++) v[i] = v[i] > 0 ? v[i] : 0;
perf_size(s, "of those idle, control memory changed", (uint64_t)v[0], 0, (uint64_t)v[1]);
perf_size(s, "control memory gone", (uint64_t)v[2], 0, (uint64_t)v[3]);
}
perf_size(s, "texture pins", pins, 0, PERF_SIZE_NONE);
perf_size(s, "pin sweep queue", pq, 0, PERF_SIZE_NONE);
perf_size(s, "recording-buffer pool", np, POOL_SLOTS, pb);

163
tests/copy_sweep_check.py Normal file
View File

@@ -0,0 +1,163 @@
"""The count of recording copies whose control memory the game reused (runtime/nvn_api_cmd.c, NX2NAVI_COPY_RETIRE):
builds ctl_fingerprint, retire_visit, retire_queue and retire_pass, cut from nvn_api_cmd.c by their text, with a stub
table of live copies and stub guest memory, and checks that
- a copy's control bytes are fingerprinted at the pass after it was made, and nothing is counted while it is in use;
- once no submission used it for 5 minutes, changed bytes count it as changed and unmapped memory as gone, unchanged
bytes count nothing, and a copy used again within 5 minutes leaves the counts;
- a lazily armed range is never read (unknown, not counted), and an untouched page of the lazy heap is taken as zeros
without a read;
- a retired copy, or one whose handle a new copy took, leaves its records and the counts;
- the visits go on where the last pass stopped: two passes over 5000 copies look each up at least once and none
more than twice.
No game files. Usage: python tests/copy_sweep_check.py [--cc <clang>]
"""
import argparse
import os
import subprocess
import sys
import tempfile
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
MSYS_BIN = Path(r'C:\msys64\ucrt64\bin')
HARNESS = r'''
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef int PlatMutex;
#define PLAT_MUTEX_INIT 0
static void plat_mutex_lock(PlatMutex *m) { (void)m; }
static void plat_mutex_unlock(PlatMutex *m) { (void)m; }
static void plat_mutex_lock_shared(PlatMutex *m) { (void)m; }
static void plat_mutex_unlock_shared(PlatMutex *m) { (void)m; }
static int64_t plat_add64(volatile int64_t *p, int64_t v) { return __atomic_add_fetch(p, v, __ATOMIC_SEQ_CST); }
static int cfg_int(const char *name) { (void)name; return 1; }
typedef struct { uint64_t magic, bytes, made_at, used_at, deferred, pins, used_min; } copy_prefix;
static PlatMutex copy_lock = PLAT_MUTEX_INIT;
#define COPY_IDLE_MINS 5
#define NCOPY 6000
static copy_prefix copies[NCOPY + 1]; // handle i: copies[i]; magic 0 means not live
static int lookups[NCOPY + 1];
static copy_prefix *copy_find(uint64_t handle) {
if (handle > NCOPY) return NULL;
lookups[handle]++;
return copies[handle].magic ? &copies[handle] : NULL;
}
static uint64_t minute;
static uint64_t copy_minute(void) { return minute; }
// Guest memory: 16 bytes of control memory per copy; per copy, whether it is mapped, lazily armed or an untouched
// lazy-heap page (which must not be read).
static uint8_t ctl[NCOPY + 1][16];
static uint8_t unmapped[NCOPY + 1], armed[NCOPY + 1], untouched[NCOPY + 1];
static int reads_untouched, reads_armed;
static long slot_of(uint64_t a) {
uint64_t lo = (uint64_t)(uintptr_t)ctl;
return a >= lo && a < lo + sizeof ctl ? (long)((a - lo) / 16) : -1;
}
static int mem_lazy_touches(uint64_t a, uint64_t n) { (void)n; long i = slot_of(a); return i >= 0 && armed[i]; }
static uint64_t mem_untouched_run(uint64_t a, uint64_t end) { long i = slot_of(a); return i >= 0 && untouched[i] ? end : 0; }
static int mem_read_hold(uint64_t a, uint64_t n) {
(void)n;
long i = slot_of(a);
if (i < 0 || unmapped[i]) return 0;
reads_untouched += untouched[i], reads_armed += armed[i];
return 1;
}
static void mem_read_release(void) {}
%s
static int fails;
#define CHECK(cond, ...) do { if (!(cond)) { printf("FAIL: " __VA_ARGS__); putchar('\n'); fails++; } } while (0)
static void make(uint64_t h, uint64_t bytes) {
copies[h] = (copy_prefix){.magic = 1, .bytes = bytes, .made_at = h * 3, .used_min = minute};
retire_queue(h, (uint64_t)(uintptr_t)ctl[h], copies[h].made_at, bytes);
}
static int64_t ld(volatile int64_t *p) { return __atomic_load_n(p, __ATOMIC_SEQ_CST); }
int main(void) {
minute = 100;
for (int i = 1; i <= 6; i++) memset(ctl[i], i, 16);
armed[4] = 1; // copy 4: a lazily armed range
untouched[5] = 1; // copy 5: an untouched lazy-heap page
memset(ctl[5], 0, 16);
for (uint64_t h = 1; h <= 6; h++) make(h, 1000 * h);
retire_pass();
CHECK(retire_nrec == 6, "six copies known (%%u)", retire_nrec);
CHECK(retire_recs[3].fp == FP_UNKNOWN, "an armed range is not read");
CHECK(reads_armed == 0 && reads_untouched == 0, "no read of an armed range or an untouched page");
CHECK(ld(&retire_changed_n) == 0 && ld(&retire_gone_n) == 0, "nothing counted while in use");
// Six minutes on: copy 2's bytes changed, copy 3's memory unmapped, copy 5 written with other bytes, copy 6 changed
// but used a minute ago, copy 1 as it was.
minute = 106;
copies[6].used_min = 105;
ctl[2][0] ^= 0xff;
unmapped[3] = 1;
untouched[5] = 0, ctl[5][7] = 9;
ctl[6][3] ^= 1;
retire_pass();
CHECK(ld(&retire_changed_n) == 2 && ld(&retire_changed_b) == 2000 + 5000,
"changed: copies 2 and 5 (%%lld, %%lld bytes)", (long long)ld(&retire_changed_n), (long long)ld(&retire_changed_b));
CHECK(ld(&retire_gone_n) == 1 && ld(&retire_gone_b) == 3000, "gone: copy 3 (%%lld)", (long long)ld(&retire_gone_n));
CHECK(reads_armed == 0, "the armed range stays unread");
// Copy 2 used again: it leaves the changed count. Copy 3 retired (AddControlMemory again): it leaves its records.
// Copy 5's handle taken by a new copy: the old record goes, the new one is fingerprinted.
copies[2].used_min = 106;
copies[3].magic = 0;
copies[5] = (copy_prefix){.magic = 1, .bytes = 777, .made_at = 999, .used_min = minute};
retire_queue(5, (uint64_t)(uintptr_t)ctl[5], 999, 777);
retire_pass();
CHECK(ld(&retire_changed_n) == 0 && ld(&retire_changed_b) == 0, "used or replaced copies leave the changed count (%%lld)",
(long long)ld(&retire_changed_n));
CHECK(ld(&retire_gone_n) == 0 && ld(&retire_gone_b) == 0, "a retired copy leaves the gone count");
CHECK(retire_nrec == 5, "the retired copy and the replaced record are dropped (%%u records)", retire_nrec);
// 5000 idle, unchanged copies: two passes look each up at least once, none more than twice.
for (uint64_t h = 1; h <= NCOPY; h++) copies[h].magic = 0;
retire_pass(); // drops every record
CHECK(retire_nrec == 0, "every record dropped once nothing is live (%%u)", retire_nrec);
memset(armed, 0, sizeof armed), memset(unmapped, 0, sizeof unmapped), memset(untouched, 0, sizeof untouched);
minute = 200;
for (uint64_t h = 1; h <= 5000; h++) make(h, 8);
minute = 210;
memset(lookups, 0, sizeof lookups);
retire_pass();
retire_pass();
int low = 0, high = 0;
for (uint64_t h = 1; h <= 5000; h++) low += lookups[h] < 1, high += lookups[h] > 2;
CHECK(!low && !high, "two passes look every copy up once or twice (%%d never, %%d more than twice)", low, high);
CHECK(ld(&retire_changed_n) == 0 && ld(&retire_gone_n) == 0, "unchanged idle copies count nothing");
printf(fails ? "copy sweep: %%d failures\n" : "copy sweep: ok\n", fails);
return fails != 0;
}
'''
def cut(text, start, end_marker):
a = text.index(start)
b = text.index(end_marker, a)
return text[a:b]
def main():
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument('--cc', help='the C compiler (default: MSYS2 UCRT64 clang on Windows, clang elsewhere)')
args = ap.parse_args()
win = os.name == 'nt'
cc = args.cc or (str(MSYS_BIN / 'clang.exe') if win and MSYS_BIN.exists() else 'clang')
env = dict(os.environ)
if win and MSYS_BIN.exists():
env['PATH'] = str(MSYS_BIN) + os.pathsep + env.get('PATH', '')
src = (ROOT / 'runtime' / 'nvn_api_cmd.c').read_text(encoding='utf-8')
body = cut(src, '// ---------- recording copies whose control memory the game reused', '\nstatic void retire_main(')
with tempfile.TemporaryDirectory(prefix='nx2navi-copy-sweep-') as tmp:
c = Path(tmp) / 'check.c'
c.write_text(HARNESS % body, encoding='utf-8')
exe = Path(tmp) / ('check.exe' if win else 'check')
subprocess.check_call([cc, '-O2', '-Wall', '-Wno-unused-function', str(c), '-o', str(exe)], env=env)
return subprocess.call([str(exe)])
if __name__ == '__main__':
sys.exit(main())