Make the page write generations 64-bit, so a long session no longer wraps them and every texture, mirror and shadow check after the wrap answers wrong.

vk_memory.c handed out page write generations from a 32-bit counter (gen_now) and every consumer compared them as
32-bit values, with 0 meaning "never stamped". At 124 to 336 generations a frame (a 0.10.0 log with trace = perf) the
counter wrapped after about 66 h of play at 60 fps, 27.6 h at 144 and 15 to 17 h at 240. Past the wrap a stamp made
before it compared newer than every later check and one made after it older: textures and VRAM mirrors missed the
CPU's writes (stale textures, and the lazy fetch copying older mirror bytes over a CPU write), pages nothing wrote read
as written at every check (a hash or upload every time, and every such page of a mirrored pool copied up at each
sync), and the generation handed out at the wrap was 0. At 64 bits nothing wraps: 2^64 lasts millions of years at
336 a frame and 240 fps. Below 2^32 every value and every compare is the same as before.

What changed: gen_now, both tables (gen_chunk, cgen_chunk), reg_max and creg_max, PoolRec's synced_gen, mgen and sgen,
dirty_gen and DIRTY_UNSYNCED, VSample.mgen, every local and parameter that holds a generation in vk_memory.c,
vk_texture.c (Tex.wgen and gpu_gen, PEnt.since, storage_rewritten and its callers, sweep_hard's write-back check
included) and tex_probe.c, and vk.h's vk_mem_changed, vk_mem_changed_snap and vk_mem_unchanged(_why)_locked. The verify
mode's mismatch line prints the generations with %llu. The readers that take no gen_lock (lazy_clean, cpu_wrote_since,
verify_cb, vk_mem_sizes) stay lock-free at 64 bits: the slots, mgen, gen_now and a new chunk pointer are now stored
atomically (relaxed, or release where a reader acquires), the same instructions on x86-64, and a static assertion
holds that 64-bit atomics are lock-free. No lock, wait or thread changes.

gen_next, which hands out every generation, no longer warns at 0xC0000000 and up or at the wrap, which cannot come;
it logs one line when the count reaches 2^32, where earlier versions ran out, with the frame, the hours and the rate.

Cost: the tables take 8 bytes a page instead of 4 (512 KiB more per 256 MiB of guest address space stamped, both
tables together, allocated as before when a chunk is first stamped), reg_max and creg_max 2 MiB each instead of 1,
Tex and PoolRec 8 bytes more, PEnt 8 more. The page walks read 8 bytes a slot; a micro-benchmark of the walk measured no
difference.

Not reproduced in the game before the change: the wrong answers were reproduced by compiling vk_memory.c's generation
code with the counter seeded below 2^32. tests/gen_wrap_check now runs that code, with the readers outside its
section, across 2^32, 2^40 and 2^63 and checks that the compares answer as below the boundary; on the code before the
change 19 of its checks fail at 2^32 and the other two seeds do not fit the counter. The new tests/gen_width_check
compiles vk_memory.c, vk_texture.c and tex_probe.c with -Wshorten-64-to-32, -Wformat and -Wincompatible-pointer-types
as errors, so a 32-bit local left behind is found (shown by reverting single locals, fields and formats). Checks that
passed: gen_wrap_check, gen_width_check, pass_cc_check of every runtime file, no new -Wall -Wextra warnings in the
three files, tex_release_check, tex_compact_check, wb_lazy_check, gpa_check, mem_growth_check, mem_budget_check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
chrg6
2026-11-13 18:57:10 -05:00
parent 879c76f91a
commit 6b95c867c8
8 changed files with 426 additions and 181 deletions

71
tests/gen_width_check.py Normal file
View File

@@ -0,0 +1,71 @@
"""No 64-to-32-bit narrowing in the files that hold page write generations: runtime/vk_memory.c, runtime/vk_texture.c
and runtime/tex_probe.c, compiled with the build's flags plus -Wshorten-64-to-32, -Wformat and
-Wincompatible-pointer-types as errors (clang -fsyntax-only). The generations are 64 bits (vk_memory.c "page write
generations"); one local, field or format left at 32 bits would bring the wrap of 2^32 back without a word, and the
normal build sees a narrowing only as a silent conversion. The baseline is zero such diagnostics in the whole of the
three files: a narrowing elsewhere in them takes an explicit cast, which also says it is meant.
Needs clang, the Vulkan headers, VMA and the shader headers of tools/build_shaders.py (runtime/*_spv.h). No GPU, no
game files, any Python.
Usage: gen_width_check.py [--cc <clang>]
"""
import argparse
import os
import re
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / 'tools'))
import build_game # noqa: E402 (RT_FLAGS, the compiler and the include folders of the build)
FILES = ['vk_memory.c', 'vk_texture.c', 'tex_probe.c']
STRICT = ['-Werror=shorten-64-to-32', '-Werror=format', '-Werror=incompatible-pointer-types']
# the diagnostics this check is about, by the flag clang names at the end of the line
WATCHED = re.compile(r'^(?P<where>[^\n]*?:\d+:\d+): (?:error|warning): (?P<what>[^\n]*?)'
r'\[(?:-Werror,)?(?P<flag>-Wshorten-64-to-32|-Wformat[\w-]*|-Wincompatible[\w-]*pointer-types)\]',
re.M)
def main():
ap = argparse.ArgumentParser()
ap.add_argument('--cc', help='the C compiler (default: the build\'s clang)')
args = ap.parse_args()
if not (ROOT / 'runtime' / 'wb_merge_spv.h').is_file():
print('gen_width_check: FAIL: runtime/wb_merge_spv.h is missing: run tools/build_shaders.py first')
return 1
cc = args.cc or os.environ.get('CC') or str(build_game.CLANG)
build_game.set_cpu('v3') # adds -DRT_CPU_LEVEL=3, as the build does
flags = [f for f in build_game.RT_FLAGS if f != '-g']
inc = ['-I', str(ROOT / 'runtime'), '-I', str(build_game.vma_include())]
vk = build_game.vulkan_include(cc)
if vk:
inc = ['-I', str(vk)] + inc
env = dict(os.environ)
if os.name == 'nt':
env['PATH'] = str(Path(cc).parent) + os.pathsep + env.get('PATH', '')
bad = 0
for name in FILES:
src = ROOT / 'runtime' / name
p = subprocess.run([cc, '-fsyntax-only', '-std=gnu11'] + flags + STRICT + inc + [str(src)], capture_output=True,
text=True, errors='replace', env=env)
hits = [m for m in WATCHED.finditer(p.stderr)]
for m in hits:
print(f'gen_width_check: FAIL: {m.group("where")}: {m.group("what").strip()} [{m.group("flag")}]')
if p.returncode and not hits:
print(f'gen_width_check: FAIL: runtime/{name} does not compile:\n{p.stderr[-3000:]}')
bad += 1
bad += len(hits)
if not hits and not p.returncode:
print(f'gen_width_check: runtime/{name}: no narrowing, format or pointer-type diagnostic')
if bad:
print(f'gen_width_check: {bad} problems')
return 1
print('gen_width_check: all checks passed')
return 0
if __name__ == '__main__':
sys.exit(main())

View File

@@ -1,11 +1,14 @@
// runtime/vk_memory.c's page write generations (the section from its "page write generations" banner to its "dirty
// census" banner, extracted by tests/gen_wrap_check.py into gen_section.inc): the lines gen_next logs as the 32-bit
// generations near their wrap, and what the comparisons answer across it. Prints each failed check and exits 1 on any.
// Stubs: no write-watch (mem_written_pages reports no page), nothing frozen, no pools.
//
// The comparisons across the wrap are wrong today (AUDIT F-8-01: the generations have no wrap handling; its fix waits
// for approval). The check expects the four wrong answers and says so: the change that handles the wrap turns them
// into failures here, and flips the expectations with it.
// runtime/vk_memory.c's page write generations across a boundary: the section from its "page write generations" banner
// to its "dirty census" banner and the readers of its tables outside it (cpu_wrote_since, shadow_pool_clean,
// lazy_clean, page_dirty), extracted by tests/gen_wrap_check.py into gen_section.inc, start just below a seed's
// boundary and run across it. Every compare the texture checks, the snapshot path, the VRAM mirrors' lazy fetch, the
// shadow storage and the verify mode make must answer as it does below the boundary. Seeds: 2^32 - 8 (where 32-bit
// generations wrapped), 2^40 - 8 (the low 32 bits pass through 0 again, so a truncation to 32 bits anywhere fails
// there too) and 2^63 - 8. gen_next must log one line when the count reaches 2^32, and no other.
// Stubs: no-op locks; a write watch that reports the pages a check marks as written by the CPU; nothing frozen; no
// pools in the index.
// Usage: gen_wrap_check <seed>. Prints each failed check and "gen_wrap_check: seed ...: N of M checks failed"; exit 1
// on any.
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
@@ -30,11 +33,36 @@ static uint64_t kern_now_ns(void) { return fake_now; }
static void grow(void **arr, int *cap, size_t elem) {
int n = *cap ? *cap * 2 : 64;
*arr = realloc(*arr, (size_t)n * elem);
if (!*arr) exit(70);
*cap = n;
}
#define SB_PAGE 4096ull
// The write watch: pages the CPU wrote since the last query over them (mem_written_pages resets them,
// mem_written_peek does not).
static uint64_t ww_pend[64];
static int nww_pend;
static void cpu_write(uint64_t page) { ww_pend[nww_pend++] = page & ~0xfffull; }
static uint64_t ww_take(uint64_t addr, uint64_t size, uint64_t *pages, uint64_t max, int reset) {
uint64_t n = 0;
for (int i = 0; i < nww_pend && n < max;) {
if (ww_pend[i] >= addr && ww_pend[i] < addr + size) {
if (pages) pages[n] = ww_pend[i];
n++;
if (reset) {
ww_pend[i] = ww_pend[--nww_pend];
continue;
}
}
i++;
}
return n;
}
uint64_t mem_written_pages(uint64_t addr, uint64_t size, uint64_t *pages, uint64_t max) {
(void)addr, (void)size, (void)pages, (void)max;
return 0;
return ww_take(addr, size, pages, max, 1);
}
uint64_t mem_written_peek(uint64_t addr, uint64_t size, uint64_t *pages, uint64_t max) {
return ww_take(addr, size, pages, max, 0);
}
int mem_frozen(uint64_t a) { (void)a; return 0; }
int mem_watched(uint64_t addr, uint64_t size) { (void)addr, (void)size; return 1; }
@@ -46,9 +74,8 @@ static pidx pool_index;
static PlatMutex pool_lock;
static const pidx_ent *pidx_find(const pidx *x, uintptr_t a, uint64_t l) { (void)x, (void)a, (void)l; return NULL; }
enum { POOL_IMPORT = 1 };
typedef struct { int kind; void *buf; uint64_t import_lo, import_hi; } PoolRec;
// The log: the generation lines are kept, the others dropped.
// The log: the page generation lines are kept, the others dropped.
static char gen_lines[16][512];
static int n_gen_lines;
static void rt_log(const char *fmt, ...) {
@@ -73,73 +100,149 @@ static void rt_fatal(const char *fmt, ...) {
#include "gen_section.inc"
static int fails;
#define CHECK(c, ...) do { if (!(c)) { fails++; printf("FAIL line %d: ", __LINE__); printf(__VA_ARGS__); printf("\n"); } } while (0)
typedef __typeof__(gen_now) gen_t; // the width the source gives the generations
static int checks, failed;
#define CHECK(cond, ...) \
do { \
checks++; \
if (!(cond)) failed++, printf("FAIL line %d: ", __LINE__), printf(__VA_ARGS__), printf("\n"); \
} while (0)
#define TEX 0x100000000ull // a texture of 4 pages in one 2 MiB region
#define OTHER 0x100200000ull // a second one in the next region
#define SPIN 0x200000000ull // an unrelated range other checks hand out generations for
#define LONE 0x100400000ull // a page written exactly at the boundary
// Hands out generations with vk_guest_written from just below `to` until gen_now reaches `to` + 3.
static void walk_to(uint32_t to) {
gen_now = to - 4;
for (int i = 0; i < 7; i++) vk_guest_written(0x300000000ull, 0x1000);
static uint64_t last_now; // every *now handed back, in order: must grow
static void seen(gen_t now, const char *what) {
CHECK((uint64_t)now > last_now, "%s: generation %llu after %llu: not newer", what, (unsigned long long)now,
(unsigned long long)last_now);
last_now = now;
}
int main(void) {
// ---- the lines: one at each of 0xC0000000, 0xD0000000, 0xE0000000, 0xF0000000 and at the wrap, no other
int main(int argc, char **argv) {
uint64_t seed = argc > 1 ? strtoull(argv[1], NULL, 0) : (1ull << 32) - 8;
uint64_t edge = seed + 8; // the boundary under test: 2^32 for the first seed
gen_t w_tex, w_oth, now, d;
rt_guest_start_ns = 1000000000ull;
fake_now = rt_guest_start_ns + 3600ull * 1000000000ull; // an hour in
gfx_counters.frames = 216000; // at 60 fps
walk_to(0x10000000u); // below the first mark: nothing
walk_to(0xb0000000u);
CHECK(n_gen_lines == 0, "%d lines before 0xC0000000", n_gen_lines);
static const uint32_t marks[] = {0xc0000000u, 0xd0000000u, 0xe0000000u, 0xf0000000u};
for (int k = 0; k < 4; k++) {
int was = n_gen_lines;
walk_to(marks[k]);
char want[32];
snprintf(want, sizeof want, ": %u of 4294967295", marks[k]);
CHECK(n_gen_lines == was + 1 && strstr(gen_lines[was], want), "the mark at %#x gave %d lines (%s)", marks[k],
n_gen_lines - was, n_gen_lines > was ? gen_lines[was] : "none");
gen_now = (gen_t)seed;
CHECK((uint64_t)gen_now == seed, "the seed %#llx does not fit the generations (%u bits)", (unsigned long long)seed,
(unsigned)(8 * sizeof(gen_t)));
if (failed) { // the generations cannot reach the boundary: nothing below can run
printf("gen_wrap_check: seed %#llx: 1 of 1 checks failed\n", (unsigned long long)seed);
return 1;
}
// 0xC0000000 in an hour of 216,000 frames: about 14,913 a frame, and a quarter of the range left: 20 min
CHECK(n_gen_lines >= 1 && strstr(gen_lines[0], "(14913 a frame)") && strstr(gen_lines[0], "in about 0.3 h"),
"the first line's rate or hours: %s", n_gen_lines ? gen_lines[0] : "none");
int was = n_gen_lines;
walk_to(0); // across the wrap
CHECK(n_gen_lines == was + 1 && strstr(gen_lines[was], "ran out at frame 216000 after 1.0 h"),
"the wrap gave %d lines (%s)", n_gen_lines - was, n_gen_lines > was ? gen_lines[was] : "none");
walk_to(0x20000000u);
CHECK(n_gen_lines == 5, "%d lines in all, not 5", n_gen_lines);
for (int i = 0; i < n_gen_lines && i < 16; i++) printf(" %s\n", gen_lines[i]);
last_now = (uint64_t)gen_now;
// ---- the comparisons across the wrap: the four answers known to be wrong (expected, see the top)
int known = 0;
gen_now = 999;
// Before the boundary: both textures loaded (written) and checked.
vk_guest_written(TEX, 0x4000);
vk_guest_written(OTHER, 0x4000);
uint32_t w1, w2, now;
vk_mem_changed(TEX, 0x4000, 0, &w1);
vk_mem_changed(OTHER, 0x4000, 0, &w2);
gen_now = UINT32_MAX - 6;
CHECK(vk_mem_changed(TEX, 0x4000, w1, &w1) == 0, "an unwritten texture read as written before the wrap");
vk_mem_changed(OTHER, 0x4000, w2, &w2);
for (int i = 0; i < 10; i++) vk_mem_changed(0x200000000ull, 0x1000, 0, &now); // other checks use generations
vk_guest_written(TEX, 0x1000); // the CPU writes the texture after the wrap
known += vk_mem_changed(TEX, 0x4000, w1, &now) != 1; // 1. the write is missed
vk_mem_changed(OTHER, 0x4000, w2, &w2);
known += vk_mem_changed(OTHER, 0x4000, w2, &now) == 1; // 2. an unwritten texture reads as written at every check
w2 = now;
known += vk_mem_changed_snap(OTHER, 0x4000, w2, 7, &now) == 1; // 3. the same through the snapshot path
gen_now = UINT32_MAX;
vk_guest_written(TEX + 0x2000, 0x1000); // 4. a write stamped at the wrap gets 0, which reads as never stamped
known += vk_mem_changed(TEX + 0x2000, 0x1000, 0, &now) != 1;
CHECK(known == 4, "%d of the 4 known wrong answers across the wrap remain: if the wrap is handled now, flip these "
"expectations", known);
printf("gen_wrap_check: across the wrap %d of 4 comparisons still answer wrong (known, expected until the wrap "
"is handled)\n", known);
CHECK(vk_mem_changed(TEX, 0x4000, 0, &w_tex) == 1, "TEX: its load is not seen");
seen(w_tex, "TEX first check");
CHECK(vk_mem_changed(OTHER, 0x4000, 0, &w_oth) == 1, "OTHER: its load is not seen");
seen(w_oth, "OTHER first check");
uint64_t pre = (uint64_t)gen_now; // a generation before the boundary: what a mirror or shadow last synced to
if (fails) return printf("gen_wrap_check: %d checks failed\n", fails), 1;
printf("gen_wrap_check: all checks passed\n");
return 0;
// Other checks hand out generations up to edge - 1; the page written next is stamped exactly at edge.
while ((uint64_t)gen_now + 1 < edge) {
vk_mem_changed(SPIN, 0x1000, 0, &d);
seen(d, "spin");
}
vk_guest_written(LONE, 0x1000);
gen_t *ls = gen_slot(LONE, 0);
CHECK(ls && (uint64_t)*ls == edge, "the page written at the boundary is stamped %llu, not %llu",
ls ? (unsigned long long)*ls : 0ull, (unsigned long long)edge);
CHECK(vk_mem_changed(LONE, 0x1000, 0, &now) == 1, "the page written at the boundary reads as never written");
seen(now, "LONE");
CHECK(vk_mem_changed(LONE, 0x1000, (gen_t)(edge - 1), &now) == 1, "the boundary's write is older than edge - 1");
seen(now, "LONE again");
for (int i = 0; i < 6; i++) {
vk_mem_changed(SPIN, 0x1000, 0, &d);
seen(d, "spin past");
}
// After the boundary: a CPU write found by the write watch and one by a host path are both seen.
cpu_write(TEX);
CHECK(vk_mem_changed(TEX, 0x4000, w_tex, &now) == 1, "TEX: a CPU write after the boundary is missed");
seen(now, "TEX after a CPU write");
w_tex = now;
vk_guest_written(TEX + 0x1000, 4);
CHECK(vk_mem_changed(TEX, 0x4000, w_tex, &now) == 1, "TEX: a host write after the boundary is missed");
seen(now, "TEX after a host write");
w_tex = now;
CHECK(vk_mem_changed(TEX, 0x4000, w_tex, &now) == 0, "TEX: no write since its check, but it reads as written");
seen(now, "TEX unwritten");
w_tex = now;
// A texture nobody wrote since before the boundary stays unchanged, check after check.
CHECK(vk_mem_changed(OTHER, 0x4000, w_oth, &now) == 0,
"OTHER: unwritten since before the boundary, reads as written");
seen(now, "OTHER 1");
w_oth = now;
CHECK(vk_mem_changed(OTHER, 0x4000, w_oth, &now) == 0, "OTHER: unwritten, reads as written at the next check too");
seen(now, "OTHER 2");
w_oth = now;
// The snapshot path (draw bindings and the texture pool's check) and the region check (reg_max).
uint32_t ep = 7;
CHECK(vk_mem_changed_snap(OTHER, 0x4000, w_oth, ep, &now) == 0, "OTHER via the snapshot: reads as written");
seen(now, "OTHER snap");
CHECK(vk_mem_unchanged_why_locked(OTHER, 0x4000, w_oth, ep) == 0, "OTHER's region check answers %d, not 0",
vk_mem_unchanged_why_locked(OTHER, 0x4000, w_oth, ep));
cpu_write(OTHER + 0x2000);
ep = 8;
CHECK(vk_mem_changed_snap(OTHER, 0x4000, w_oth, ep, &now) == 1, "OTHER via the snapshot: a CPU write is missed");
seen(now, "OTHER snap written");
CHECK(vk_mem_unchanged_why_locked(OTHER, 0x4000, w_oth, ep) == 3,
"OTHER's region check answers %d, not 3 (written)",
vk_mem_unchanged_why_locked(OTHER, 0x4000, w_oth, ep));
CHECK(vk_mem_unchanged_why_locked(OTHER, 0x4000, now, ep) == 0, "OTHER's region check after its check answers %d",
vk_mem_unchanged_why_locked(OTHER, 0x4000, now, ep));
// The CPU-side tables (what mirror_sync, lazy_clean and cpu_wrote_since read) hold the same stamps.
gen_t *gs = gen_slot(TEX + 0x1000, 0), *cs = slot_in(cgen_chunk, TEX + 0x1000, 0);
CHECK(gs && cs && *gs == *cs && (uint64_t)*cs > edge, "TEX page 1: texture stamp %llu, CPU-side stamp %llu",
gs ? (unsigned long long)*gs : 0ull, cs ? (unsigned long long)*cs : 0ull);
CHECK((uint64_t)creg_max[TEX >> REG_SHIFT] > edge && reg_max[TEX >> REG_SHIFT] == creg_max[TEX >> REG_SHIFT],
"TEX's region: newest stamps %llu / %llu", (unsigned long long)reg_max[TEX >> REG_SHIFT],
(unsigned long long)creg_max[TEX >> REG_SHIFT]);
// A VRAM mirror and a shadow copy synced before the boundary, TEX written after it.
PoolRec p = {.kind = POOL_IMPORT, .import_lo = TEX, .import_hi = TEX + 0x4000};
p.mgen = (__typeof__(p.mgen))pre, p.sgen = (__typeof__(p.sgen))pre;
CHECK((uint64_t)p.mgen == pre && (uint64_t)p.sgen == pre, "PoolRec's mgen and sgen do not hold %llu",
(unsigned long long)pre);
CHECK(!lazy_clean(TEX, TEX + 0x4000, &p), "lazy_clean: the mirror lacks a CPU write after the boundary, yet clean");
CHECK(cpu_wrote_since(TEX + 0x1000, p.sgen), "cpu_wrote_since: a CPU write after the boundary is missed");
CHECK(page_dirty(TEX + 0x1000, p.mgen), "page_dirty: a CPU write after the boundary is missed");
vk_mem_changed_snap(TEX, 0x4000, w_tex, ep, &now); // the region gets this epoch's snapshot
seen(now, "TEX snap");
CHECK(!shadow_pool_clean(&p, ep, 1), "shadow_pool_clean (CPU-side tables): a write after the boundary is missed");
CHECK(!shadow_pool_clean(&p, ep, 0), "shadow_pool_clean: a write after the boundary is missed");
// Synced again now: nothing is newer.
p.mgen = (__typeof__(p.mgen))gen_now, p.sgen = (__typeof__(p.sgen))gen_now;
CHECK(lazy_clean(TEX, TEX + 0x4000, &p), "lazy_clean: synced after every write, yet not clean");
CHECK(!cpu_wrote_since(TEX + 0x1000, p.sgen), "cpu_wrote_since: nothing written since the sync, yet written");
CHECK(!page_dirty(TEX + 0x1000, p.mgen), "page_dirty: nothing written since the sync, yet dirty");
CHECK(shadow_pool_clean(&p, ep, 1) && shadow_pool_clean(&p, ep, 0), "shadow_pool_clean: synced, yet not clean");
// The log: one line where the count reached 2^32, none at any other boundary.
int reached = seed < (1ull << 32) && (uint64_t)gen_now >= (1ull << 32);
CHECK(n_gen_lines == reached, "%d page generation lines, not %d (first: %s)", n_gen_lines, reached,
n_gen_lines ? gen_lines[0] : "none");
if (reached && n_gen_lines)
CHECK(strstr(gen_lines[0], "reached 4294967296 at frame 216000 after 1.0 h of play (19884 a frame)"),
"the line at 2^32: %s", gen_lines[0]);
for (int i = 0; i < n_gen_lines && i < 16; i++) {
CHECK(!strstr(gen_lines[i], "run out") && !strstr(gen_lines[i], "ran out at"), "a line of running out: %s",
gen_lines[i]);
printf(" %s\n", gen_lines[i]);
}
printf("gen_wrap_check: seed %#llx, %u-bit generations, gen_now %#llx: %d of %d checks failed\n",
(unsigned long long)seed, (unsigned)(8 * sizeof(gen_t)), (unsigned long long)gen_now, failed, checks);
return failed ? 1 : 0;
}

View File

@@ -1,7 +1,13 @@
"""Build and run tests/gen_wrap_check.c: runtime/vk_memory.c's page write generations, extracted verbatim from its
"page write generations" banner to its "dirty census" banner, with the lines gen_next logs as the 32-bit generations
near their wrap (one at each of 0xC0000000, 0xD0000000, 0xE0000000, 0xF0000000 and one at the wrap), and the four
comparisons known to answer wrong across the wrap.
"""Build and run tests/gen_wrap_check.c: runtime/vk_memory.c's page write generations across 2^32, 2^40 and 2^63.
From runtime/vk_memory.c it takes, verbatim, into build/test/gen_wrap/gen_section.inc:
- the declarations of PoolRec's mgen and sgen (the generations a VRAM mirror and a shadow copy hold), so the stand-in
PoolRec has their real width;
- the section from the "page write generations" banner to the "dirty census" banner;
- the readers of the tables outside that section: cpu_wrote_since, shadow_pool_clean, lazy_clean and page_dirty.
The program starts the generations just below each seed's boundary and checks that every compare answers across it as
below it (texture checks, the snapshot path, the region check, the CPU-side tables, mirror and shadow cleanliness),
and that gen_next logs one line when the count reaches 2^32 and no other.
No GPU, no game files, any Python.
@@ -9,6 +15,7 @@ Usage: gen_wrap_check.py [--cc <clang>]
"""
import argparse
import os
import re
import shutil
import subprocess
import sys
@@ -18,6 +25,9 @@ ROOT = Path(__file__).resolve().parents[1]
MSYS_BIN = Path(r'C:\msys64\ucrt64\bin')
BEGIN = '// ---------------------------------------------------------------- page write generations'
END = '// ---------------------------------------------------------------- dirty census'
READERS = ('static int cpu_wrote_since(', 'static int shadow_pool_clean(', 'static int lazy_clean(',
'static int page_dirty(')
SEEDS = ((1 << 32) - 8, (1 << 40) - 8, (1 << 63) - 8) # the old wrap; low 32 bits through 0 again; near 2^63
def compiler(cc):
@@ -30,19 +40,56 @@ def compiler(cc):
return shutil.which('clang') or 'clang'
def definition(src, start):
"""The definition that begins with start (not a prototype), up to its closing brace at column 0."""
i = 0
while True:
i = src.find(start, i)
if i < 0:
return None
eol = src.index('\n', i)
if src[eol - 1] == '{':
return src[i:src.index('\n}\n', i) + 3]
i = eol
def extract(src):
"""gen_section.inc's text, or None and the reason."""
a, b = src.find(BEGIN), src.find(END)
if a < 0 or b < a:
return None, 'the section banners are not in runtime/vk_memory.c'
s0 = src.find('typedef struct PoolRec {')
s1 = src.find('} PoolRec;', s0)
if s0 < 0 or s1 < 0:
return None, 'PoolRec is not in runtime/vk_memory.c'
fields = []
for name in ('mgen', 'sgen'):
m = re.search(r'^\s*(u?int\d+_t)\s+' + name + r';', src[s0:s1], re.M)
if not m:
return None, f'PoolRec.{name} is not declared as an integer of fixed width'
fields.append(f'{m.group(1)} {name};')
out = ['// Extracted from runtime/vk_memory.c by tests/gen_wrap_check.py.\n',
'typedef struct PoolRec { int kind; void *buf; uint64_t import_lo, import_hi; ' + ' '.join(fields) +
' } PoolRec;\n', src[a:b]]
for start in READERS:
d = definition(src, start)
if not d:
return None, f'{start.split()[-1]}...) is not in runtime/vk_memory.c'
out.append(d)
return '\n'.join(out), None
def main():
ap = argparse.ArgumentParser()
ap.add_argument('--cc', help='the C compiler (default: $CC, MSYS2 UCRT64 clang, clang on PATH)')
args = ap.parse_args()
src = (ROOT / 'runtime' / 'vk_memory.c').read_text(encoding='utf-8-sig')
a, b = src.find(BEGIN), src.find(END)
if a < 0 or b < a:
print('gen_wrap_check: FAIL: the section banners are not in runtime/vk_memory.c')
inc, why = extract((ROOT / 'runtime' / 'vk_memory.c').read_text(encoding='utf-8-sig'))
if not inc:
print(f'gen_wrap_check: FAIL: {why}')
return 1
work = ROOT / 'build' / 'test' / 'gen_wrap'
work.mkdir(parents=True, exist_ok=True)
(work / 'gen_section.inc').write_text('// Extracted from runtime/vk_memory.c by tests/gen_wrap_check.py.\n' +
src[a:b], encoding='utf-8')
(work / 'gen_section.inc').write_text(inc, encoding='utf-8')
exe = work / ('gen_wrap_check.exe' if os.name == 'nt' else 'gen_wrap_check')
env = dict(os.environ)
if os.name == 'nt' and MSYS_BIN.exists():
@@ -50,7 +97,14 @@ def main():
subprocess.check_call([compiler(args.cc), '-std=gnu11', '-O1', '-Wall', '-Wno-unused-function',
'-Wno-unused-variable', '-I', str(work), str(ROOT / 'tests' / 'gen_wrap_check.c'), '-o',
str(exe)], env=env)
return subprocess.run([str(exe)], env=env, timeout=120).returncode
bad = 0
for seed in SEEDS:
bad += subprocess.run([str(exe), str(seed)], env=env, timeout=120).returncode != 0
if bad:
print(f'gen_wrap_check: {bad} of {len(SEEDS)} runs failed')
return 1
print('gen_wrap_check: all checks passed')
return 0
if __name__ == '__main__':