mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
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:
71
tests/gen_width_check.py
Normal file
71
tests/gen_width_check.py
Normal 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())
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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__':
|
||||
|
||||
Reference in New Issue
Block a user