3 Commits

Author SHA1 Message Date
chrg6
5ee960f811 Add NX2NAVI_GEN_SEED, a test switch that starts the page write generations at a given value, so a play-test can cross 2^32 in seconds instead of 15 to 66 hours.
Whether the 64-bit generations hold past 2^32 in the game, with every texture check, mirror sync and shadow copy of a
real scene, could not be seen in play: reaching 2^32 takes 15 to 66 hours. gen_wrap_check shows it for the generation
code alone. NX2NAVI_GEN_SEED=<n> (nx2navi.ini gen_seed, diagnostics, default 0) makes the run's first generation
n + 1: with 4294966000 a run passes 2^32 within its first seconds, and with 1099511627000 the low 32 bits pass
through 0 again.

The first gen_next, before anything is stamped, reads the value (gen_seed). It is parsed with strtoull, decimal or
0x hexadecimal: strtoul stops at 2^32 - 1 where unsigned long is 32 bits, as on Windows, and would turn a seed past
32 bits into 4294967295 without a word. It is capped at 2^62, far from the 64-bit end, and only ever raises the
counter, since a lower value would leave stamps newer than it that read as written at every check. A value that is
no number, or negative, is logged and ignored; a seed in use is logged as "mem: page generations start at <n>
(NX2NAVI_GEN_SEED, a test switch)". Without the switch the only cost is one compare a generation that is true once.

cfg.c gets its metas row (0, 0..4611686018427387904, read at start), its known[] name and its sections[] entry, each
on a line of its own next to the other memory test switches (STAGING_FAIL_AT, VRAM_BALLAST_MB); nx2navi.ini.example
gets a commented gen_seed entry under [diagnostics]. The settings menu does not show it.

tests/gen_wrap_check runs the generation code a second way: the counter left at 0 and the seed taken from
NX2NAVI_GEN_SEED, for 4294967288, 0xfffffffff8, 0x7ffffffffffffff8 (capped), 12abc, -8, 0 and unset, each with the
start and the log line expected of it, then the same compares across the boundary. With strtoul in place of strtoull
two of those runs fail. Checks that passed: gen_wrap_check, gen_width_check, cfg_rows_check (0 problems),
pass_cc_check on vk_memory.c and cfg.c, no new -Wall -Wextra warnings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 20:12:50 -05:00
chrg6
6b95c867c8 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>
2026-11-13 20:12:50 -05:00
chrg6
5dea57c5e2 Log where a session stands as the 32-bit page generations near their wrap.
vk_memory.c hands out page write generations (gen_now, 32 bits) with no wrap handling. Past the wrap, textures and
pool mirrors the CPU writes can miss a write, and every check reads as a write. At about 181 generations a frame that
takes about 110 h at 60 fps or 27 h at 240. Only trace=perf's "mem: sizes" counted them, so nothing in a player's log
could tell a "textures went stale after a day" report from other causes.

The nine "++gen_now" sites now call gen_next, which hands out the next generation (gen_lock held exclusively, as at
every site). When the value reaches 0xC0000000, 0xD0000000, 0xE0000000 or 0xF0000000 it logs the count, the frame,
the rate per frame and about how many hours of play are left at that rate. At the wrap it logs that the generations
ran out. That is five lines at most a session and one predicted branch per generation. The log queue takes no lock
of vk_memory.c, so logging under gen_lock is safe. The wrap handling itself waits for approval.

The new tests/gen_wrap_check extracts the section from the "page write generations" banner to the "dirty census"
banner and runs it with stubs. It checks one line at each mark and at the wrap and none elsewhere, and the first
line's rate and hours. It also expects the four comparisons that are known to answer wrong across the wrap, and says
so: the change that handles the wrap turns them into failures there and flips them. On the code before this change
the check fails (no lines). Checks that passed: gen_wrap_check, pass_cc_check on vk_memory.c, no new -Wall -Wextra
warnings, gpa_check, mem_growth_check, mem_budget_check, wb_lazy_check, tex_release_check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 18:41:40 -05:00