mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
The review found two windows in memory.c lazy_fault, where a fault holding frz_lock and lz_lock finds its address neither armed nor in flight. It returned 0 there, and access_fault then asked for the page's state without a lock. A write whose range a flight gave back while the write waited for frz_lock went on to thaw_fault, whose thaw released every armed range of the 2 MiB region (lazy_drop_region_locked), each fetched under the lock. And after a release that sets no hold (mem_lazy_skip's give-back of another buffer's range), the completion thread could arm the page again between that 0 and access_fault's query, so a read saw an inaccessible page and went to the crash handler; that one predates the ranges in flight. Now a fault that finds a flight there waits for it as before. Otherwise it releases lz_lock, asks for the page's state while it still holds frz_lock, under which no arm runs, and runs the access again when the page allows it: a read on a readable page, a write on a read-write one. A write to a frozen page still goes on to thaw_fault, and an address that is not ours to the crash handler. The new late_write scenario of tests/lazy_fetch_check.py has a thread write a range whose release starts and ends while the write waits for frz_lock: the write runs again and the region's other range stays armed. With the old return of 0 it fails, the other range released. The read's window has no check: the arm would have to land between two instructions. Not reproduced in the game before the change; lazy_fetch_check passed in all its scenarios. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
118 lines
4.6 KiB
Python
118 lines
4.6 KiB
Python
"""Build and run tests/lazy_fetch_check.c: runtime/memory.c's lazy write-back (memory.c "Lazy write-back", "Ranges in
|
|
flight") with the Windows platform layer and its real fault handler, the rest of the runtime stubbed and the back end's
|
|
fetch a stand-in that only waits. It checks that a fetch holds up nothing but the touches of its range, that those
|
|
wait for it, and that arms, thaws, mapping changes, freezes and releases see it; also with NX2NAVI_WB_LAZY_CHUNK=1 and
|
|
with NX2NAVI_WB_LAZY_UNLOCKED=0. Each scenario runs as its own process, so an access violation passed to the crash
|
|
handler shows as its exit code; any exit code other than 0, or a scenario that does not end within 120 s, fails.
|
|
|
|
Windows only (the fault handler is): no game files, no GPU. The module layout the runtime includes is a stand-in
|
|
written to the test's build folder.
|
|
|
|
Usage: lazy_fetch_check.py [scenario ...]
|
|
"""
|
|
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')
|
|
# name: (scenario argument, extra environment)
|
|
CHUNK = {'CHECK_CHUNK': '1'} # NX2NAVI_WB_LAZY_CHUNK=1
|
|
LOCKED = {'CHECK_LOCKED': '1'} # NX2NAVI_WB_LAZY_UNLOCKED=0
|
|
SCENARIOS = {
|
|
'arm_race': ('arm_race', {}),
|
|
'arm_race_slow': ('arm_race_slow', {}),
|
|
'no_stall': ('no_stall', {}),
|
|
'join': ('join', {}),
|
|
'loser': ('loser', {}),
|
|
'late_write': ('late_write', {}),
|
|
'concurrent': ('concurrent', {}),
|
|
'thaw': ('thaw', {}),
|
|
'thaw_unlocked': ('thaw_unlocked', {}),
|
|
'locked_thaw': ('thaw_unlocked', LOCKED),
|
|
'mapping': ('mapping', {}),
|
|
'release': ('release', {}),
|
|
'release_join': ('release_join', {}),
|
|
'arm_busy': ('arm_busy', {}),
|
|
'arm_flight': ('arm_flight', {}),
|
|
'foreign': ('foreign', {}),
|
|
'chunk': ('chunk', CHUNK),
|
|
'locked': ('locked', LOCKED),
|
|
'locked_join': ('join', LOCKED),
|
|
'locked_loser': ('loser', LOCKED),
|
|
'locked_arm_race': ('arm_race_slow', LOCKED),
|
|
'locked_release': ('release', LOCKED),
|
|
'report': ('report', {}),
|
|
'flood': ('flood', {}),
|
|
'freeze': ('freeze', {}),
|
|
}
|
|
|
|
|
|
def compiler():
|
|
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'
|
|
|
|
|
|
LAYOUT = '''// A stand-in for the generated layout.h (tests/lazy_fetch_check.py).
|
|
#pragma once
|
|
#define MOD_MAIN_BASE 0x7000000000ull
|
|
#define MOD_MAIN_SIZE 0x4ea0000ull
|
|
#define MOD_COUNT 3
|
|
'''
|
|
|
|
|
|
def main():
|
|
if os.name != 'nt':
|
|
print('lazy_fetch_check: the fault handler it runs is Windows only; skipped')
|
|
return 0
|
|
names = sys.argv[1:] or list(SCENARIOS)
|
|
unknown = [n for n in names if n not in SCENARIOS]
|
|
if unknown:
|
|
print('lazy_fetch_check: unknown scenario %s; known: %s' % (', '.join(unknown), ', '.join(SCENARIOS)))
|
|
return 2
|
|
work = ROOT / 'build' / 'test' / 'lazy_fetch'
|
|
work.mkdir(parents=True, exist_ok=True)
|
|
(work / 'layout.h').write_text(LAYOUT)
|
|
exe = work / 'lazy_fetch_check.exe'
|
|
env = dict(os.environ)
|
|
if MSYS_BIN.exists():
|
|
env['PATH'] = str(MSYS_BIN) + os.pathsep + env.get('PATH', '')
|
|
rt = ROOT / 'runtime'
|
|
plat = [rt / 'platform' / 'win32.c', rt / 'platform' / 'common.c']
|
|
libs = ['-lbcrypt', '-lws2_32', '-lpsapi', '-lwinmm', '-ldbghelp', '-lshell32', '-lole32', '-luser32', '-ladvapi32']
|
|
subprocess.check_call([compiler(), '-O1', '-g', '-Wall', '-Wno-unused-function', '-fno-strict-aliasing',
|
|
'-march=x86-64-v3', '-I', str(rt), '-I', str(work),
|
|
str(ROOT / 'tests' / 'lazy_fetch_check.c'), *map(str, plat), '-o', str(exe), '-lm',
|
|
*libs], env=env)
|
|
bad = 0
|
|
for name in names:
|
|
arg, extra = SCENARIOS[name]
|
|
try:
|
|
p = subprocess.run([str(exe), arg], env={**env, **extra}, capture_output=True, text=True, timeout=120)
|
|
except subprocess.TimeoutExpired:
|
|
bad += 1
|
|
print('lazy_fetch_check: %s: FAIL: did not end within 120 s' % name)
|
|
continue
|
|
code = p.returncode & 0xFFFFFFFF
|
|
if code:
|
|
bad += 1
|
|
print('lazy_fetch_check: %s: FAIL: exit code %#x%s' % (
|
|
name, code, ' (an access violation)' if code == 0xC0000005 else ''))
|
|
sys.stdout.write(p.stdout)
|
|
else:
|
|
print('lazy_fetch_check: %s: passed' % name)
|
|
if bad:
|
|
print('lazy_fetch_check: %d of %d scenarios failed' % (bad, len(names)))
|
|
return 1
|
|
print('lazy_fetch_check: all %d scenarios passed' % len(names))
|
|
return 0
|
|
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(main())
|