mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 17:09:57 -04:00
Copy untouched lazy-heap pages as zeros in mem_copy_readable instead of faulting on them under frz_lock.
With NX2NAVI_HEAP_LAZY=1, a heap page nothing has touched yet is still reserved. mem_copy_readable holds frz_lock and, for a range with no armed or probed region, copied it with a plain memcpy. A reserved heap page in that range faulted into fault_veh, then access_fault, then heap_lazy_fault, which takes frz_lock on the same thread. The SRW lock is not recursive, so the copy never returned, and every later fault, thaw and freeze waited behind it. Callers are frame_rate.c's floor probe and vk_memory.c mirror_capture (MIRROR_CAPTURE_KB or MIRROR_STAGE). Now, when the lazy heap is on and a range meets the heap, the range is copied run by run as plat_mem_state reports its pages. The heap's reserved runs read zero, as they would once committed, and are not committed, so a diagnostic read or a mirror's first copy does not undo what the setting saves. Readable runs are copied, and anything else leaves the range out (ok = 0). Ranges outside the heap, and every range while the lazy heap is off, keep the copy that asks the system nothing. tests/heap_lazy_copy_check builds memory.c with the Windows platform layer and its fault handler. It copies an untouched heap page, a range from written pages into untouched ones, a range past the heap's end, and a batch of three, each on a worker the main thread gives 3 s. Before the change the first copy hung (exit 4). Checks that passed: heap_lazy_copy_check, pass_cc_check on memory.c, no new -Wall -Wextra warnings, wb_lazy_check, mem_growth_check, mem_budget_check and gpa_check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -555,22 +555,36 @@ int mem_lazy_armed(uint64_t a, uint64_t n) {
|
||||
// could not be sent). frz_lock is held over all of them, so nothing is armed or probed meanwhile; a range in regions
|
||||
// with neither (lzr, prb: both change under that lock, with the pages' protection) is copied without asking the system
|
||||
// for its pages' state. Returns how many were copied.
|
||||
// With the lazy heap (NX2NAVI_HEAP_LAZY) a heap page nothing touched yet is still reserved, and reading it faults
|
||||
// into heap_lazy_fault, which waits for frz_lock: held here, by the same thread, so the copy never returned and every
|
||||
// later fault, thaw and freeze waited behind it. So a range that meets the heap is copied run by run as the system
|
||||
// reports its pages: the heap's reserved runs read zero, as they would once committed, without committing them.
|
||||
int mem_copy_readable(uint8_t *const *dst, const uint64_t (*src)[2], int n, uint8_t *ok) {
|
||||
int got = 0;
|
||||
plat_mutex_lock(&frz_lock);
|
||||
uint64_t heap = L.heap.base, top = heap + __atomic_load_n(&heap_size, __ATOMIC_ACQUIRE);
|
||||
for (int i = 0; i < n; i++) {
|
||||
uint64_t a = src[i][0], len = src[i][1];
|
||||
ok[i] = 0;
|
||||
if (!len || a >= RT_ADDRESS_LIMIT || a + len > RT_ADDRESS_LIMIT) continue;
|
||||
int quick = 1, can = 1;
|
||||
int lazy = heap_lazy && a < top && a + len > heap; // reserved heap pages may lie in it
|
||||
int quick = !lazy, can = 1;
|
||||
for (uint64_t r = a >> FRZ_SHIFT; quick && r <= (a + len - 1) >> FRZ_SHIFT; r++)
|
||||
quick = !__atomic_load_n(&lzr[r], __ATOMIC_RELAXED) && !__atomic_load_n(&prb[r], __ATOMIC_RELAXED);
|
||||
for (uint64_t x = a, e; !quick && can && x < a + len; x = e) {
|
||||
int st = plat_mem_state(x, a + len, &e);
|
||||
can = (st == PLAT_PROT_RW || st == PLAT_PROT_READ) && e > x;
|
||||
int untouched = lazy && st == PLAT_MEM_RESERVED && x >= heap && e <= top;
|
||||
can = (st == PLAT_PROT_RW || st == PLAT_PROT_READ || untouched) && e > x;
|
||||
}
|
||||
if (!can) continue;
|
||||
memcpy(dst[i], (const void *)(uintptr_t)a, len);
|
||||
if (!lazy) {
|
||||
memcpy(dst[i], (const void *)(uintptr_t)a, len);
|
||||
} else { // pages are committed and decommitted under frz_lock only: the runs are those checked above
|
||||
for (uint64_t x = a, e; x < a + len; x = e) {
|
||||
if (plat_mem_state(x, a + len, &e) == PLAT_MEM_RESERVED) memset(dst[i] + (x - a), 0, e - x);
|
||||
else memcpy(dst[i] + (x - a), (const void *)(uintptr_t)x, e - x);
|
||||
}
|
||||
}
|
||||
ok[i] = 1, got++;
|
||||
}
|
||||
plat_mutex_unlock(&frz_lock);
|
||||
|
||||
115
tests/heap_lazy_copy_check.c
Normal file
115
tests/heap_lazy_copy_check.c
Normal file
@@ -0,0 +1,115 @@
|
||||
// runtime/memory.c mem_copy_readable with the lazy heap (NX2NAVI_HEAP_LAZY=1): a heap page nothing touched yet is
|
||||
// still reserved, and the copy holds frz_lock, which the fault on such a page (heap_lazy_fault) waits for on the same
|
||||
// thread. Built and run by tests/heap_lazy_copy_check.py with the Windows platform layer and the real fault handler;
|
||||
// the rest of the runtime is stubbed. Prints each failed check and exits 1 on any, 4 when a copy never returns.
|
||||
// Each copy runs on a worker the main thread gives 3 s: before the fix the first one hung there.
|
||||
#include "memory.c"
|
||||
|
||||
#include <stdarg.h>
|
||||
|
||||
static int fails;
|
||||
#define CHECK(c, ...) do { if (!(c)) { fails++; printf("FAIL line %d: ", __LINE__); printf(__VA_ARGS__); printf("\n"); } } while (0)
|
||||
|
||||
// ---- the runtime memory.c and the platform layer reach
|
||||
unsigned rt_trace;
|
||||
uint64_t rt_guest_start_ns;
|
||||
const unsigned long long rt_app_pool_bytes = 0xCD500000ull;
|
||||
const RtLayoutEntry rt_layout[] = {{"main", MOD_MAIN_BASE, MOD_MAIN_SIZE}};
|
||||
const int rt_layout_count = 1;
|
||||
const char *cfg_get(const char *name) { (void)name; return NULL; }
|
||||
const char *cfg_value(const char *name) { (void)name; return "0"; }
|
||||
int cfg_bool(const char *name) { return !strcmp(name, "NX2NAVI_HEAP_LAZY"); }
|
||||
void cfg_init(void) {}
|
||||
int cfg_setenv(const char *name, const char *value) { (void)name, (void)value; return 0; }
|
||||
int rt_knob(const char *name, const char *def) { (void)name; return strcmp(def, "0") != 0; }
|
||||
int ldr_count(void) { return 0; }
|
||||
const RtLoaded *ldr_module(int i) { (void)i; return NULL; }
|
||||
uint64_t perf_mark_begin(void) { return 0; }
|
||||
void perf_mark_end(int kind, uint64_t t0, uint64_t n) { (void)kind, (void)t0, (void)n; }
|
||||
void perf_size(PerfSizes *s, const char *name, uint64_t count, uint64_t limit, uint64_t bytes) {
|
||||
(void)s, (void)name, (void)count, (void)limit, (void)bytes;
|
||||
}
|
||||
uint64_t res_heap_extra_bytes(void) { return 0; }
|
||||
void rt_log(const char *fmt, ...) { (void)fmt; }
|
||||
void rt_logf(const char *fmt, ...) { (void)fmt; }
|
||||
void rt_fatal(const char *fmt, ...) {
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
printf("FATAL: ");
|
||||
vprintf(fmt, ap);
|
||||
printf("\n");
|
||||
va_end(ap);
|
||||
fflush(stdout);
|
||||
_Exit(3);
|
||||
}
|
||||
|
||||
// ---- one copy on a worker, as the replay (vk_memory.c mirror_capture) or a present (frame_rate.c) makes it
|
||||
static struct { uint64_t a, len; uint8_t *dst, ok; int got; volatile int done; } J;
|
||||
static void copier(void *arg) {
|
||||
(void)arg;
|
||||
uint64_t src[1][2] = {{J.a, J.len}};
|
||||
J.got = mem_copy_readable(&J.dst, (const uint64_t (*)[2])src, 1, &J.ok);
|
||||
__atomic_store_n(&J.done, 1, __ATOMIC_SEQ_CST);
|
||||
}
|
||||
static void copy(uint64_t a, uint64_t len, uint8_t *dst, const char *what) {
|
||||
J.a = a, J.len = len, J.dst = dst, J.ok = 0xEE, J.got = -1, J.done = 0;
|
||||
void *t = plat_thread_new(copier, NULL, "copier");
|
||||
for (int i = 0; i < 300 && !__atomic_load_n(&J.done, __ATOMIC_SEQ_CST); i++) plat_wait_ns(10000000);
|
||||
if (!J.done) {
|
||||
printf("FAIL: %s: the copy has not returned after 3 s (frz_lock held by the copying thread)\n", what);
|
||||
fflush(stdout);
|
||||
_Exit(4);
|
||||
}
|
||||
plat_thread_join(t);
|
||||
}
|
||||
static int all(const uint8_t *p, uint64_t n, uint8_t v) {
|
||||
for (uint64_t i = 0; i < n; i++)
|
||||
if (p[i] != v) return 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
setvbuf(stdout, NULL, _IONBF, 0);
|
||||
mem_init();
|
||||
uint64_t heap = 0, size = 64ull << 20, y;
|
||||
CHECK(mem_set_heap_size(size, &heap) == R_OK && heap, "SetHeapSize failed");
|
||||
CHECK(heap_lazy, "the lazy heap is off");
|
||||
static uint8_t buf[0x30000];
|
||||
|
||||
// ---- a page nothing touched: copied as zeros, and left reserved
|
||||
uint64_t a = heap + 0x30000;
|
||||
CHECK(plat_mem_state(a, a + PAGE, &y) == PLAT_MEM_RESERVED, "the untouched page is not reserved");
|
||||
memset(buf, 0xAB, 64);
|
||||
copy(a, 64, buf, "an untouched heap page");
|
||||
CHECK(J.got == 1 && J.ok == 1 && all(buf, 64, 0), "an untouched heap page: returned %d, ok %d, bytes %#x",
|
||||
J.got, J.ok, buf[0]);
|
||||
CHECK(plat_mem_state(a, a + PAGE, &y) == PLAT_MEM_RESERVED, "the copy committed the untouched page");
|
||||
|
||||
// ---- a range from touched pages into untouched ones: the written bytes, then zeros
|
||||
uint64_t t = heap + 0x100000; // a 64 KiB run committed by a host access, then written
|
||||
CHECK(mem_host_readable(t, 0x10000), "the heap page could not be made readable");
|
||||
memset((void *)(uintptr_t)t, 0x5A, 0x10000);
|
||||
CHECK(plat_mem_state(t + 0x10000, t + 0x10000 + PAGE, &y) == PLAT_MEM_RESERVED, "the next run is not reserved");
|
||||
memset(buf, 0xAB, sizeof buf);
|
||||
copy(t + 0x8000, 0x20000, buf, "touched then untouched heap pages");
|
||||
CHECK(J.got == 1 && J.ok == 1 && all(buf, 0x8000, 0x5A) && all(buf + 0x8000, 0x18000, 0),
|
||||
"touched then untouched heap pages: returned %d, ok %d, bytes %#x %#x", J.got, J.ok, buf[0], buf[0x8000]);
|
||||
|
||||
// ---- past the heap's end the pages are not the heap's: left out, without a fault
|
||||
memset(buf, 0xAB, 64);
|
||||
copy(heap + size - 32, 64, buf, "a range past the heap's end");
|
||||
CHECK(J.got == 0 && J.ok == 0 && all(buf, 64, 0xAB), "a range past the heap's end: returned %d, ok %d", J.got, J.ok);
|
||||
|
||||
// ---- several ranges in one call, as frame_rate.c batches them
|
||||
uint8_t d0[16], d1[16], d2[16], oks[3];
|
||||
uint8_t *dst[3] = {d0, d1, d2};
|
||||
uint64_t src[3][2] = {{t, 16}, {a + 0x100, 16}, {heap + size - 8, 16}};
|
||||
memset(d0, 0, 16), memset(d1, 0xAB, 16), memset(d2, 0xAB, 16);
|
||||
int got = mem_copy_readable(dst, (const uint64_t (*)[2])src, 3, oks);
|
||||
CHECK(got == 2 && oks[0] && oks[1] && !oks[2] && all(d0, 16, 0x5A) && all(d1, 16, 0),
|
||||
"three ranges: returned %d, ok %d %d %d", got, oks[0], oks[1], oks[2]);
|
||||
|
||||
if (fails) return printf("heap_lazy_copy_check: %d checks failed\n", fails), 1;
|
||||
printf("heap_lazy_copy_check: all checks passed\n");
|
||||
return 0;
|
||||
}
|
||||
64
tests/heap_lazy_copy_check.py
Normal file
64
tests/heap_lazy_copy_check.py
Normal file
@@ -0,0 +1,64 @@
|
||||
"""Build and run tests/heap_lazy_copy_check.c: runtime/memory.c mem_copy_readable with the lazy heap
|
||||
(NX2NAVI_HEAP_LAZY=1), where a heap page nothing touched yet is still reserved. The copy holds frz_lock, which the
|
||||
fault on such a page waits for; the check needs every copy to return, untouched pages to read zero and stay reserved,
|
||||
and pages past the heap's end to be left out.
|
||||
|
||||
Windows only (the lazy heap is): memory.c is built with the Windows platform layer and its fault handler, the rest of
|
||||
the runtime stubbed. No game files: the module layout the runtime includes is a stand-in written to the test's build
|
||||
folder.
|
||||
|
||||
Usage: heap_lazy_copy_check.py
|
||||
"""
|
||||
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')
|
||||
|
||||
|
||||
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/heap_lazy_copy_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('heap_lazy_copy_check: the lazy heap is Windows only; skipped')
|
||||
return 0
|
||||
work = ROOT / 'build' / 'test' / 'heap_lazy_copy'
|
||||
work.mkdir(parents=True, exist_ok=True)
|
||||
(work / 'layout.h').write_text(LAYOUT)
|
||||
exe = work / 'heap_lazy_copy_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' / 'heap_lazy_copy_check.c'), *map(str, plat), '-o', str(exe), '-lm',
|
||||
*libs], env=env)
|
||||
try:
|
||||
return subprocess.run([str(exe)], env=env, timeout=120).returncode
|
||||
except subprocess.TimeoutExpired:
|
||||
print('heap_lazy_copy_check: FAIL: the check did not end within 120 s')
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user