NVN: dirty-page tracker race; graphics unit tests build and run again

- Ported from d2r-mobile ef9b1ff13, adapted to our shared-page ranges:
  setting a page read-only marked it PROT before mprotect landed, so a
  concurrent unprotect-before-write could set it back to NONE and then
  the mprotect made it read-only anyway. A game thread writing it then
  spun forever in the fault handler, and a host system call into it
  failed with EFAULT. Now NONE -> BUSY -> mprotect -> PROT (and back),
  waiters re-check after BUSY clears, and the wait is capped at 2 s
  with a red line. Engage's movie decoder writes frames from a host
  thread while replay protects pages; this is a likely cause of the
  old early stalls.
- tests/t_dirty_race.c (ported) reproduces it deterministically: on
  the old tracker read() into the page fails with EFAULT; it passes
  now, and t_dirty_slots still passes.
- The graphics unit tests had stopped linking (prog_key moved to
  hle_nvn.c) and some crashed (hand-built textures lacked the texture
  magic replay now checks; t_nvn used a function that has since been
  implemented as its unimplemented example and hard-coded 523 names).
  Fixed: t_nvn, t_fuse_check, t_zskip, t_xv, t_ub, t_pfv, t_present_crop,
  t_rt_depth_ro, t_rt_range_shape, the dirty tests and others pass.
  Still failing, the same before this change: t_reorder_chain (30 of
  155), t_defer_chain (1 of 59), t_mip_extent (1 of 303), t_rt_range,
  t_rt_range_ro; t_refl needs D2R shader samples.
- Mario regression t77, an Engage movie / title / demo run (0 missed
  dirty pages) and the iOS compile are clean.
This commit is contained in:
seedds committed 2026-10-15 07:59:35 +08:00
1 parent 7aef571f0b
commit 6788f74019
19 files changed
+146 -30

No files matched your search

+49 -14
View File
@@ -13,8 +13,22 @@
#include <sys/mman.h>
#include <unistd.h>
#include <mach/mach_time.h>
#include <sched.h>
enum { PG_NONE = 0, PG_PROT = 1, PG_BUSY = 2 }; // BUSY:正在解除保护(另一线程同时缺页时自旋等它) | BUSY: being unprotected (another thread faulting at the same time spins waiting for it)
// 页状态与 mprotect 的次序(取自 d2r-mobile ef9b1ff13):改保护的一方先把状态改成 BUSY,mprotect 落地后才改成终态,别的线程见 BUSY 就等。
// 设只读:NONE → BUSY → mprotect(只读) → PROT;解保护:PROT → BUSY → mprotect(可写) → NONE。
// 不变式:状态是 NONE 的页一定可写、是 PROT 的页一定只读。原先设只读是「先记 PROT、再 mprotect」,这中间另一线程的写前解保护
// (影片解码线程往贴图存储写一帧、读盘读进客体缓冲)会把状态改回 NONE,随后 mprotect 落地 ⇒ 页只读而状态 NONE:游戏线程写它时缺页处理只回
// 「重执行」、永远空转;宿主用系统调用往里读时 prep_write 看 NONE 跳过,系统调用直接 EFAULT(t_dirty_race 复现)。
// BUSY 最长持续一次 mprotect(设只读时再加几次原子写)。等待有上限 BUSY_WAIT_MS,超限报红:缺页处理里超限按「不是我们的页」返回,交给崩溃处理如实报崩
// Order of page state and mprotect (from d2r-mobile ef9b1ff13): the side changing protection first sets the state to BUSY and sets the final state only after mprotect has landed; other threads wait when they see BUSY.
// set read-only: NONE -> BUSY -> mprotect(read-only) -> PROT; unprotect: PROT -> BUSY -> mprotect(writable) -> NONE.
// Invariant: a page whose state is NONE is always writable, one whose state is PROT is always read-only. Setting read-only used to be "record PROT, then mprotect"; in between, another thread's unprotect-before-write
// (the movie decoder thread writing a frame into texture storage, a disk read into a guest buffer) could set the state back to NONE, then the mprotect landed => the page read-only with state NONE: a game thread writing it got only
// "re-execute" from the fault handler and spun forever; a host system call reading into it saw NONE in prep_write, skipped, and failed with EFAULT (reproduced by t_dirty_race).
// BUSY lasts at most one mprotect (plus a few atomic writes when setting read-only). The wait is capped at BUSY_WAIT_MS and reports red beyond it: in the fault handler it then returns "not our page" so the crash handler reports truthfully
enum { PG_NONE = 0, PG_PROT = 1, PG_BUSY = 2 };
enum { BUSY_WAIT_MS = 2000 };
// 跟踪区间 = 各内存池(MemoryPoolInitialize 登记)。池销毁后复用 live=0 的槽,信号处理里无锁扫描。
// Tracked ranges = the memory pools (registered by MemoryPoolInitialize). After a pool is destroyed its live=0 slot is reused; the signal handler scans without locks.
#define MAX_RANGE 4096 // 原为 256:内存池多于它时超出的池不跟踪,里面的贴图每次取用都当成脏的重传(暗黑实测峰值见内存读数行的「内存池」;塞尔达约 300 个,踩过)。扫描只到已用的槽数,容量大不增加开销 | was 256: with more memory pools than that the excess pools are untracked and their textures are treated as dirty and re-uploaded on every use (for Diablo's measured peak see "memory pools" in the memory readout line; Zelda has about 300 and hit this). The scan only covers slots in use, so a larger capacity costs nothing
@@ -41,6 +55,22 @@ static void mp(uintptr_t a, size_t len, int prot) {
atomic_fetch_add(&g_mp_ticks, mach_absolute_time() - t); atomic_fetch_add(&g_mp_calls, 1); atomic_fetch_add(&g_mp_pages, len >> g_shift);
}
static mach_timebase_info_data_t g_tb; // 首次登记池时取好(缺页路径里只做乘除) | obtained when the first pool is registered (the fault path only multiplies and divides)
static _Atomic int g_busy_red;
// 等这一页离开 BUSY。返回 1 = 已离开;0 = 等过了上限(报红一次)。信号处理里可用:只有原子读、mach_absolute_time、sched_yield
// Wait for this page to leave BUSY. Returns 1 = it has left; 0 = waited past the limit (reported red once). Usable in a signal handler: only atomic reads, mach_absolute_time and sched_yield
static int busy_wait(range_t *r, uintptr_t p) {
uint64_t t0 = 0;
for (unsigned n = 1; atomic_load(&r->state[p]) == PG_BUSY; n++) {
if (n & 0x3ff) continue;
uint64_t now = mach_absolute_time(); if (!t0) t0 = now;
if ((now - t0) * g_tb.numer / g_tb.denom > (uint64_t)BUSY_WAIT_MS * 1000000ull) {
if (!atomic_exchange(&g_busy_red, 1)) LOG("【红】脏页跟踪:页 %p 停在「忙」超过 %d 毫秒(改保护的线程没走完),不再等", (void *)(r->base + (p << g_shift)), BUSY_WAIT_MS);
return 0; }
sched_yield();
}
return 1;
}
static int live_at(int i) { return atomic_load_explicit(&g_r[i].live, memory_order_acquire); }
// 能整段装下 [a, a+len) 的在册区间(贴图的跟踪 / 保护 / 查脏都用它) | The registered range that holds all of [a, a+len) (used for a texture's tracking / protection / dirty check)
static range_t *find_span(uintptr_t a, size_t len) {
@@ -53,12 +83,17 @@ static void unprotect_page(range_t *r, uintptr_t p) { // 调用方已把状态
atomic_store(&r->gen[p], atomic_fetch_add(&g_now, 1) + 1);
atomic_store(&r->state[p], PG_NONE);
}
// 解开一个区间里的一页:受保护就解并记代号;别的线程正在解就等它。返回 1 = 这一页是本模块保护过的 | Unprotect one page of a range: if protected, unprotect and stamp it; if another thread is unprotecting it, wait. Returns 1 = this page was protected by this module
// 解开一个区间里的一页:受保护就解并记代号;别的线程正在改它的保护就等它改完再看——对方若是在设只读,等完这页是 PROT,照样得解
// (写前解保护走不到缺页处理,必须在这里解掉)。返回 1 = 这一页是本模块管的;0 = 不是,或等「忙」超时
// Unprotect one page of a range: if protected, unprotect and stamp it; if another thread is changing its protection, wait for it to finish and look again -- if the other side was setting read-only, the page is PROT after the wait and still has to be unprotected
// (an unprotect-before-write never reaches the fault handler, so it must be done here). Returns 1 = this page is managed by this module; 0 = it is not, or the wait on BUSY timed out
static int release_page(range_t *r, uintptr_t p, _Atomic uint64_t *count) {
uint8_t st = PG_PROT;
if (atomic_compare_exchange_strong(&r->state[p], &st, PG_BUSY)) { unprotect_page(r, p); if (count) atomic_fetch_add(count, 1); return 1; }
if (st == PG_BUSY) { while (atomic_load(&r->state[p]) == PG_BUSY) {} return 1; }
return atomic_load(&r->gen[p]) != 0; // 刚被别的线程解掉(缺页发生在解之前):重执行;从未保护过的页不是我们的 | Just unprotected by another thread (the fault happened before the unprotect): re-execute; pages that were never protected are not ours
for (;;) {
uint8_t st = PG_PROT;
if (atomic_compare_exchange_strong(&r->state[p], &st, PG_BUSY)) { unprotect_page(r, p); if (count) atomic_fetch_add(count, 1); return 1; }
if (st != PG_BUSY) return atomic_load(&r->gen[p]) != 0; // 已是 NONE:刚被别的线程解掉(缺页发生在解之前)就重执行;从未保护过的页不是我们的 | already NONE: just unprotected by another thread (the fault happened before the unprotect) -> re-execute; pages never protected are not ours
if (!busy_wait(r, p)) return 0;
}
}
static int on_fault(void *addr, int is_write, void *uc) {
(void)uc;
@@ -81,7 +116,7 @@ static void prep_write(void *addr, size_t len) {
void nvn_dirty_add_range(const void *mem, size_t size) {
static int inited;
if (!inited) { inited = 1; g_pg = (uintptr_t)getpagesize(); g_shift = __builtin_ctzl(g_pg); dm_fault_hook_register(on_fault); dm_guest_write_prep_register(prep_write); }
if (!inited) { inited = 1; g_pg = (uintptr_t)getpagesize(); g_shift = __builtin_ctzl(g_pg); mach_timebase_info(&g_tb); dm_fault_hook_register(on_fault); dm_guest_write_prep_register(prep_write); }
atomic_fetch_add(&g_adds_total, 1);
pthread_mutex_lock(&g_slot_mu);
int n = atomic_load(&g_nr);
@@ -127,22 +162,22 @@ void nvn_dirty_remove_range(const void *mem) {
int nvn_dirty_tracked(const void *addr, size_t len) {
return find_span((uintptr_t)addr, len) != NULL;
}
// 设只读:先置状态再 mprotect(反过来会有「页已只读、状态仍为 0」的窗口,那时的合法写入会被当成崩溃)
// Set read-only: set the state first, then mprotect (the reverse leaves a window of "page already read-only, state still 0", during which a legitimate write would be treated as a crash)
// 设只读:先把页标成 BUSY,mprotect 落地后才标 PROT(次序与不变式见文件头的状态说明)
// Set read-only: mark the pages BUSY first and mark them PROT only after mprotect has landed (for the order and the invariant see the state notes at the top of the file)
void nvn_dirty_protect(const void *addr, size_t len) {
range_t *r = len ? find_span((uintptr_t)addr, len) : NULL;
if (!r) return;
uintptr_t a = (uintptr_t)addr, p = (a - r->base) >> g_shift, q = (a + len - 1 - r->base) >> g_shift;
// 连续置成 PROT 的页合成一次 mprotect(逐页调用在首传大贴图时一帧上千次;选角页启动帧 5:4888 次)。
// 已是 PROT / BUSY 的页断开连续段:不重复保护,也不碰别的线程正在解的页
// Consecutive pages set to PROT are merged into one mprotect (per-page calls reach thousands per frame on the first upload of large textures; character select page startup frame 5: 4888 calls).
// Pages already PROT / BUSY break the consecutive run: do not protect again, and do not touch pages another thread is unprotecting
// 已是 PROT / BUSY 的页断开连续段:不重复保护,也不碰别的线程正在改的页
// Consecutive pages are merged into one mprotect (per-page calls reach thousands per frame on the first upload of large textures; character select page startup frame 5: 4888 calls).
// Pages already PROT / BUSY break the consecutive run: do not protect again, and do not touch pages another thread is changing
uintptr_t run = 0; int open = 0;
for (uintptr_t i = p; i <= q + 1; i++) {
uint8_t st = PG_NONE;
int took = i <= q && atomic_compare_exchange_strong(&r->state[i], &st, PG_PROT);
int took = i <= q && atomic_compare_exchange_strong(&r->state[i], &st, PG_BUSY);
if (took && !open) { run = i; open = 1; }
if (!took && open) { mp(r->base + (run << g_shift), (i - run) << g_shift, PROT_READ); open = 0; }
if (!took && open) { mp(r->base + (run << g_shift), (i - run) << g_shift, PROT_READ); for (uintptr_t k = run; k < i; k++) atomic_store(&r->state[k], PG_PROT); open = 0; }
}
}
uint64_t nvn_dirty_now(void) { return atomic_load(&g_now); }
+2
View File
@@ -41,3 +41,5 @@ t_xv
t_xv.dSYM/
t_ub
t_ub.dSYM/
t_dirty_race
t_dirty_race.dSYM/
+4 -1
View File
@@ -20,6 +20,8 @@ t_nvn: t_nvn.c ../nvn/hle_nvn.c ../../host/crash.c $(GFX) $(GFX_H)
clang $(CFLAGS) -fobjc-arc -o $@ t_nvn.c ../../host/crash.c $(GFX) $(FW)
t_dirty_slots: t_dirty_slots.c ../nvn/nvn_dirty.c ../../host/crash.c ../nvn/nvn_dirty.h
clang $(CFLAGS) -o $@ t_dirty_slots.c ../../host/crash.c ../nvn/nvn_dirty.c
t_dirty_race: t_dirty_race.c ../nvn/nvn_dirty.c ../../host/crash.c ../nvn/nvn_dirty.h
clang $(CFLAGS) -o $@ t_dirty_race.c ../../host/crash.c
t_builtin: t_builtin.m ../nvn/nvn_builtin.h
clang -O1 -g -Wall -fobjc-arc -o $@ t_builtin.m -framework Metal -framework Foundation
t_deswz_gpu: t_deswz_gpu.m ../tex/nvn_deswizzle.c ../tex/nvn_deswizzle_gpu.m ../tex/nvn_deswizzle.h ../tex/nvn_deswizzle_gpu.h ../nvn/nvn_tex.c ../nvn/nvn_tex.h
@@ -60,9 +62,10 @@ t_pso_skey: t_pso_skey.m ../nvn/nvn_pso_skey.h ../nvn/nvn_obj.h
clang $(CFLAGS) -fobjc-arc -o $@ t_pso_skey.m -framework Foundation
t_pfv: t_pfv.m ../../host/crash.c $(GFX) $(GFX_H)
clang $(CFLAGS) -fobjc-arc -o $@ t_pfv.m ../../host/crash.c $(filter-out ../nvn/nvn_mtl.m ../nvn/nvn_entry.S ../nvn/nvn_thunks.S,$(GFX)) $(FW)
run: t_nvn t_dirty_slots t_builtin t_deswz_gpu t_pace t_refl t_hmap t_cover t_fuse_check t_present_crop t_reorder_chain t_defer_chain t_zskip t_xv t_ub t_mip_extent t_rt_range_shape t_rt_range_ro t_rt_range t_rt_depth_ro t_pso_skey t_pfv
run: t_nvn t_dirty_slots t_dirty_race t_builtin t_deswz_gpu t_pace t_refl t_hmap t_cover t_fuse_check t_present_crop t_reorder_chain t_defer_chain t_zskip t_xv t_ub t_mip_extent t_rt_range_shape t_rt_range_ro t_rt_range t_rt_depth_ro t_pso_skey t_pfv
./t_nvn
./t_dirty_slots
./t_dirty_race
./t_builtin
./t_deswz_gpu
./t_pace
+2 -1
View File
@@ -1,4 +1,5 @@
#include "../nvn/nvn_mtl.m"
int prog_key(const prog_t *p, unsigned stage, uint8_t key[8]) { (void)p; (void)stage; memset(key, 0, 8); return 0; } // hle_nvn.c 的着色器键:本测不链它,给零键(原先缺这个桩,链接失败) | shader keys from hle_nvn.c: this test does not link it, so a zero key (the missing stub broke the link)
void *front_metal_layer(void) { return NULL; }
module_t *module_of(const void *p) { (void)p; return NULL; }
module_t g_mod[MAX_MOD]; int g_nmod;
@@ -9,7 +10,7 @@ static int fail, count;
typedef struct { uint8_t *p; size_t n; } chain;
static chain chain_new(void) { chain c={0}; if (posix_memalign((void **)&c.p,getpagesize(),65536)) abort(); memset(c.p,0,65536); mtl_pool_add(c.p,65536); return c; }
static void emit(chain *c, unsigned op, const void *a, size_t n) { size_t z=(8+n+7)&~7ull; op_hdr h={op,(uint32_t)z}; memcpy(c->p+c->n,&h,8); if(n) memcpy(c->p+c->n+8,a,n); c->n+=z; }
static tex_t tex(unsigned w) { static uint64_t s=9000; tex_t t={.serial=s++}; t.d.width=t.d.height=w; t.d.depth=t.d.samples=1; t.d.levels=2; t.d.target=NVN_TEXTURE_TARGET_2D; t.d.format=NVN_FORMAT_RGBA8; return t; }
static tex_t tex(unsigned w) { static uint64_t s=9000; tex_t t={.magic=MAGIC('T', 'E', 'X', 0), .serial =s++}; t.d.width=t.d.height=w; t.d.depth=t.d.samples=1; t.d.levels=2; t.d.target=NVN_TEXTURE_TARGET_2D; t.d.format=NVN_FORMAT_RGBA8; return t; }
static int view(const tex_t *t, int lv) { static int id=1200; view_t v={.set_levels=1,.base_level=lv,.nlevels=1}; mtl_reg_tex(id,t,&v); return id++; }
static void set_rt(chain *c, const tex_t *a, const tex_t *b, int alv) { p_set_rt r={.n=b?2:1}; r.color[0]=a; r.color[1]=b; for(int i=0;i<r.n;i++){r.has_cview[i]=1;r.cview[i].set_levels=1;r.cview[i].base_level=i?0:alv;r.cview[i].nlevels=1;} emit(c,OP_SET_RT,&r,sizeof r); p_viewports v={.n=1};v.v[0][2]=v.v[0][3]=8;emit(c,OP_VIEWPORTS,&v,sizeof v);p_scissor sc={0,0,8,8};emit(c,OP_SCISSOR,&sc,sizeof sc); }
static void clear_color(chain *c, int i, float r, float g, float b) { p_clear_color x={.index=i,.mask=15,.rgba={r,g,b,1}}; emit(c,OP_CLEAR_COLOR,&x,sizeof x); }
+64
View File
@@ -0,0 +1,64 @@
// 脏页跟踪「设只读」与「写前解保护」的次序竞态(取自 d2r-mobile ef9b1ff13 的 t_dirty_race.c,按本仓库的跟踪表改了桩)。
// 现场:线程甲 nvn_dirty_protect 已把页的状态记成「受保护」、mprotect 还没落地;线程乙这时来 prep_write(宿主往客体内存写之前的解保护,
// 例如影片解码线程把一帧写进贴图存储、读盘把文件读进客体缓冲)。旧实现:乙把状态改回「无」,随后甲的 mprotect 落地 ⇒ 页只读而状态「无」——
// 宿主再往里读文件:prep_write 看状态「无」跳过,系统调用不走缺页处理,直接 EFAULT;游戏线程直接写:缺页处理只回「重执行」,永远空转。
// 造法:把 nvn_dirty.c 里的 mprotect 换成这里的 t_mprotect——甲设只读的那一次调用落地之前,先放乙进来并等 100 毫秒,次序是确定的。
// 期望值不靠被测实现:乙的 prep_write 返回之后,这一页必须真的可写(往里 read 16 字节得 16;直接写一个字节不卡、读回是写进去的值),
// 且这次写要被记成脏(nvn_dirty_since 对设只读之前取的戳返回 1)。
// Ordering race between "set read-only" and "unprotect before write" in dirty-page tracking (from d2r-mobile ef9b1ff13's t_dirty_race.c, stubs adapted to this repository's tracking table).
// Scene: thread A, in nvn_dirty_protect, has already recorded the page state as "protected" while its mprotect has not landed yet; thread B now arrives with prep_write (the unprotect done before the host writes into guest memory,
// for example the movie decoder thread writing a frame into texture storage, or a disk read into a guest buffer). Old implementation: B sets the state back to "none", then A's mprotect lands => the page is read-only while its state is "none" --
// when the host reads a file into it again: prep_write sees state "none" and skips, the system call does not go through the page-fault handler and fails with EFAULT; when a game thread writes directly: the page-fault handler only returns "re-execute" and spins forever.
// How it is staged: the mprotect in nvn_dirty.c is replaced by t_mprotect here -- before the call with which A sets read-only lands, B is let in and given 100 ms, so the order is deterministic.
// Expected values do not depend on the implementation under test: after B's prep_write returns, this page must really be writable (reading 16 bytes into it gives 16; writing one byte directly does not hang and reads back as written),
// and this write must be recorded as dirty (nvn_dirty_since returns 1 for a stamp taken before the page was set read-only).
#define mprotect t_mprotect
#include "../nvn/nvn_dirty.c"
#undef mprotect
int mprotect(void *, size_t, int);
#include <fcntl.h>
#include <errno.h>
#include <stdio.h>
module_t g_mod[MAX_MOD];
int g_nmod;
module_t *module_of(const void *p) { (void)p; return NULL; }
void crash_install(void);
static int g_fail;
#define EXPECT(name, want, got) do { \
long long want_ = (long long)(want), got_ = (long long)(got); \
printf("%-58s 期望 %-4lld → 真结果 %-4lld %s\n", name, want_, got_, want_ == got_ ? "✓" : "✗"); \
if (want_ != got_) g_fail++; \
} while (0)
static uint8_t *g_page; static _Atomic int g_armed, g_go, g_b_done;
static void *thread_b(void *arg) { (void)arg; while (!atomic_load(&g_go)) usleep(200); prep_write(g_page, 16); atomic_store(&g_b_done, 1); return NULL; }
int t_mprotect(void *a, size_t n, int prot) {
if (prot == PROT_READ && atomic_exchange(&g_armed, 0)) { atomic_store(&g_go, 1); usleep(100000); } // 甲的 mprotect 落地前:放乙进来,等它跑到头或卡在等待上 | Before A's mprotect lands: let B in and wait until it runs to the end or gets stuck waiting
return mprotect(a, n, prot);
}
int main(void) {
crash_install();
size_t pg = (size_t)getpagesize();
g_page = mmap(NULL, 2 * pg, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0);
if (g_page == MAP_FAILED) { perror("mmap"); return 2; }
nvn_dirty_add_range(g_page, 2 * pg);
uint64_t stamp = nvn_dirty_now();
pthread_t tb; pthread_create(&tb, NULL, thread_b, NULL);
atomic_store(&g_armed, 1);
nvn_dirty_protect(g_page, 2 * pg); // 甲:置状态 → (乙插进来)→ mprotect 落地 | A: set the state -> (B cuts in) -> mprotect lands
for (int i = 0; i < 2000 && !atomic_load(&g_b_done); i++) usleep(1000);
EXPECT("乙的写前解保护在 2 秒内返回", 1, atomic_load(&g_b_done));
if (!atomic_load(&g_b_done)) { printf("t_dirty_race:乙没返回,后面的项不测\n"); return 1; }
pthread_join(tb, NULL);
int fd = open("/dev/zero", O_RDONLY); g_page[pg] = 0; // 第二页没人解过保护,这一写走缺页处理(对照:正常路径是通的) | Nobody has unprotected the second page, so this write goes through the page-fault handler (control: the normal path works)
ssize_t r = read(fd, g_page, 16); // 乙接下来要做的那次宿主写:系统调用直接写进页里 | The host write that B is about to make: a system call writing straight into the page
EXPECT("乙解完保护后往这页读文件:读到的字节数 = 请求的 16", 16, r);
if (r == 16) { ((volatile uint8_t *)g_page)[1] = 0x5a; EXPECT("直接写这页不卡、读回是写进去的值", 0x5a, g_page[1]); }
else { printf(" (read 失败 errno=%d;页只读而状态为「无」,直接写会在缺页处理里空转,这一项不执行)\n", errno); g_fail++; }
EXPECT("这次写被记成脏(对设只读前的戳)", 1, nvn_dirty_since(g_page, 16, stamp));
close(fd);
printf("t_dirty_race:%s\n", g_fail ? "不通" : "全通"); return g_fail != 0;
}
+2 -1
View File
@@ -7,6 +7,7 @@ void *front_metal_layer(void) { return NULL; }
module_t *module_of(const void *p) { (void)p; return NULL; }
module_t g_mod[MAX_MOD]; int g_nmod;
void svc_thread_dump(void) {}
int prog_key(const prog_t *p, unsigned stage, uint8_t key[8]) { (void)p; (void)stage; memset(key, 0, 8); return 0; } // hle_nvn.c 的着色器键:本测不链它,给零键(原先缺这个桩,链接失败) | shader keys from hle_nvn.c: this test does not link it, so a zero key (the missing stub broke the link)
uint64_t now_ns(void) { return bp_now(); }
static int fail, count;
@@ -49,7 +50,7 @@ int main(void) { @autoreleasepool {
pr->sh[1] = [NvShader new]; pr->sh[1]->fn = [lib newFunctionWithName:@"f"]; pr->sh[1]->stage = 1;
prog_t pg = {.serial = 100}; serial_put(pg.serial, pr);
tex_t t[4] = {0};
for (int i = 0; i < 4; i++) { t[i].serial = 101 + i; t[i].d.width = t[i].d.height = 16;
for (int i = 0; i < 4; i++) { t[i].magic = MAGIC('T', 'E', 'X', 0); t[i].serial = 101 + i; t[i].d.width = t[i].d.height = 16;
t[i].d.depth = t[i].d.levels = t[i].d.samples = 1; t[i].d.target = NVN_TEXTURE_TARGET_2D; t[i].d.format = NVN_FORMAT_RGBA8; }
check_color(0, &pg, t); check_color(1, &pg, t);
printf("== %d 项,失败 %d ==\n", count, fail); return fail != 0;
+1 -1
View File
@@ -11,7 +11,7 @@ uint64_t now_ns(void) { return bp_now(); }
static int cases,failed; static prog_t colour_pg,depth_pg;
#define EXPECT(n,w,v) do { uint64_t a=(w),b=(v);cases++;printf("%s 期望 %llu → 真结果 %llu %s\n",n,(unsigned long long)a,(unsigned long long)b,a==b?"✓":"✗");failed+=a!=b;}while(0)
static tex_t make_tex(int w,int h,int levels,int format) {
static uint64_t ser=90000; tex_t t={.serial=ser++};t.d.width=w;t.d.height=h;
static uint64_t ser=90000; tex_t t={.magic=MAGIC('T', 'E', 'X', 0), .serial =ser++};t.d.width=w;t.d.height=h;
t.d.depth=t.d.samples=1;t.d.levels=levels;t.d.target=NVN_TEXTURE_TARGET_2D;t.d.format=format;return t;
}
typedef struct { NVNcommandBuffer cb; pool_t pool; void *mem; } recording;
+4 -3
View File
@@ -59,7 +59,7 @@ int main(void) {
gpa = (PFNNVNDEVICEGETPROCADDRESSPROC)boot("nvnDeviceGetProcAddress");
EXPECT("bootstrap 取 GetProcAddress 非空", 1, gpa != NULL);
int nonnull = 0; for (unsigned i = 0; i < NVN_NFUNC; i++) nonnull += gpa(NULL, nvn_names[i]) != NULL;
EXPECT("523 个名字全部取到地址", 523, nonnull);
EXPECT("名字表里每个名字都取到地址(NVN_NFUNC 个)", NVN_NFUNC, nonnull); // 原先写死 523,名字表加到 536 后一直报不通 | used to be hard-coded 523 and kept failing once the name table grew to 536
EXPECT("未知名字取址返回空", 0, gpa(NULL, "nvnNoSuchFunction") != NULL);
// 2. 设备
@@ -361,8 +361,9 @@ int main(void) {
// 7. 未实现函数:返回 0、报红、只报一次
// 7. Unimplemented functions: return 0, report a failure, report only once
unsigned ui = 0; while (strcmp(nvn_names[ui], "nvnBufferBuilderSetDefaults")) ui++;
((void (*)(void *))proc("nvnBufferBuilderSetDefaults"))(NULL); ((void (*)(void *))proc("nvnBufferBuilderSetDefaults"))(NULL);
// 取一个仍未实现的函数(原先用的 nvnBufferBuilderSetDefaults 后来实现了,传 NULL 就崩) | Use a function that is still unimplemented (nvnBufferBuilderSetDefaults, used before, was implemented later and crashes on NULL)
unsigned ui = 0; while (strcmp(nvn_names[ui], "nvnBufferBuilderGetDevice")) ui++;
((void (*)(void *))proc("nvnBufferBuilderGetDevice"))(NULL); ((void (*)(void *))proc("nvnBufferBuilderGetDevice"))(NULL);
EXPECT("未实现函数被调用后记为已报红", 1, g_unimpl_seen[ui] > 0);
EXPECT("未实现函数计数 2", 2, nvn_slot[ui].cnt);
+2 -1
View File
@@ -1,4 +1,5 @@
#include "../nvn/nvn_mtl.m"
int prog_key(const prog_t *p, unsigned stage, uint8_t key[8]) { (void)p; (void)stage; memset(key, 0, 8); return 0; } // hle_nvn.c 的着色器键:本测不链它,给零键(原先缺这个桩,链接失败) | shader keys from hle_nvn.c: this test does not link it, so a zero key (the missing stub broke the link)
void *front_metal_layer(void) { return NULL; }
module_t *module_of(const void *p) { (void)p; return NULL; }
@@ -13,7 +14,7 @@ int main(void) {
failed += tex_need_pfv(MTLPixelFormatBGRA8Unorm, MTLPixelFormatBGRA8Unorm_sRGB) != 0;
failed += tex_need_pfv(MTLPixelFormatBC7_RGBAUnorm, MTLPixelFormatBC7_RGBAUnorm_sRGB) != 0;
failed += tex_need_pfv(MTLPixelFormatRGBA8Unorm, MTLPixelFormatR32Uint) != 1;
tex_t t = {.serial = 70001};
tex_t t = {.magic=MAGIC('T', 'E', 'X', 0), .serial = 70001};
t.d.width = 4; t.d.height = 1; t.d.depth = 1; t.d.levels = 1; t.d.target = NVN_TEXTURE_TARGET_2D; t.d.format = NVN_FORMAT_RGBA8;
mtl_init();
MTLTextureDescriptor *d = tex_desc(&t, 0);
+1 -1
View File
@@ -84,7 +84,7 @@ int main(void) { @autoreleasepool {
PFNNVNDEVICEGETPROCADDRESSPROC get=(PFNNVNDEVICEGETPROCADDRESSPROC)h_nvnBootstrapLoader("nvnDeviceGetProcAddress");
NVNT_nvnWindowSetCrop set=(NVNT_nvnWindowSetCrop)get(NULL,"nvnWindowSetCrop");
NVNT_nvnQueuePresentTexture present=(NVNT_nvnQueuePresentTexture)get(NULL,"nvnQueuePresentTexture");
tex_t tex={.serial=10001};tex.d.width=tex.d.height=8;tex.d.depth=tex.d.levels=1;tex.d.format=NVN_FORMAT_RGBA8;tex.d.target=NVN_TEXTURE_TARGET_2D;serial_put(tex.serial,src);
tex_t tex={.magic=MAGIC('T', 'E', 'X', 0), .serial =10001};tex.d.width=tex.d.height=8;tex.d.depth=tex.d.levels=1;tex.d.format=NVN_FORMAT_RGBA8;tex.d.target=NVN_TEXTURE_TARGET_2D;serial_put(tex.serial,src);
win_t win={.ntex=1};win.tex[0]=(NVNtexture *)&tex;set((NVNwindow *)&win,4,4,4,4);
mtl_present_test_stall(100000000);present(NULL,(NVNwindow *)&win,0);uint64_t tk=win.ticket[0];set((NVNwindow *)&win,0,0,8,8);
#ifdef CROP_LEGACY_HEADER
+2 -1
View File
@@ -3,6 +3,7 @@
// Fixed command chain experiment: production mtl_exec, hand-written constant shaders and closed-form expectations; does not include the production reorderer.
// G full stencil block, M small view + copy, U blend, R stencil resolve; only G/M are swapped.
#include "../nvn/nvn_mtl.m"
int prog_key(const prog_t *p, unsigned stage, uint8_t key[8]) { (void)p; (void)stage; memset(key, 0, 8); return 0; } // hle_nvn.c 的着色器键:本测不链它,给零键(原先缺这个桩,链接失败) | shader keys from hle_nvn.c: this test does not link it, so a zero key (the missing stub broke the link)
void *front_metal_layer(void) { return NULL; }
module_t *module_of(const void *p) { (void)p; return NULL; }
module_t g_mod[MAX_MOD]; int g_nmod;
@@ -25,7 +26,7 @@ static void state(chain *c, int kind, const void *s, size_t n) {
p_bind_state b={.kind=kind}; if(n>sizeof b.bytes) abort(); memcpy(b.bytes,s,n); emit(c,OP_BIND_STATE,&b,sizeof b);
}
static tex_t texture(unsigned w, unsigned levels, int depth) {
static uint64_t ser=5000; tex_t t={.serial=ser++}; t.d.width=t.d.height=w;
static uint64_t ser=5000; tex_t t={.magic=MAGIC('T', 'E', 'X', 0), .serial =ser++}; t.d.width=t.d.height=w;
t.d.depth=t.d.samples=1; t.d.levels=levels; t.d.target=NVN_TEXTURE_TARGET_2D;
t.d.format=depth?NVN_FORMAT_DEPTH32F_STENCIL8:NVN_FORMAT_RGBA8; return t;
}
+2 -1
View File
@@ -1,6 +1,7 @@
// CPU资格反例:真实production rt_range_same,Metal对象由明确mock提供,不启GPU/不证明像素。
// CPU eligibility counterexample: real production rt_range_same, Metal objects supplied by explicit mocks; no GPU is started and no pixels are proven.
#include "../nvn/nvn_mtl.m"
int prog_key(const prog_t *p, unsigned stage, uint8_t key[8]) { (void)p; (void)stage; memset(key, 0, 8); return 0; } // hle_nvn.c 的着色器键:本测不链它,给零键(原先缺这个桩,链接失败) | shader keys from hle_nvn.c: this test does not link it, so a zero key (the missing stub broke the link)
void *front_metal_layer(void){return NULL;}
module_t *module_of(const void *p){(void)p;return NULL;}
module_t g_mod[MAX_MOD];int g_nmod;void svc_thread_dump(void){}uint64_t now_ns(void){return bp_now();}
@@ -15,7 +16,7 @@ static void history(void){
rt_range_note_depth();
#endif
}
int main(void){@autoreleasepool{setenv("DM_NVN_RT_RANGE","1",1);tex_t c={.serial=26},d={.serial=29};c.d.width=c.d.height=d.d.width=d.d.height=8;c.d.depth=d.d.depth=c.d.samples=d.d.samples=1;c.d.levels=4;d.d.levels=1;c.d.target=d.d.target=NVN_TEXTURE_TARGET_2D;c.d.format=NVN_FORMAT_RGBA8;d.d.format=NVN_FORMAT_DEPTH32F_STENCIL8;
int main(void){@autoreleasepool{setenv("DM_NVN_RT_RANGE","1",1);tex_t c={.magic=MAGIC('T', 'E', 'X', 0), .serial =26},d={.magic=MAGIC('T', 'E', 'X', 0), .serial =29};c.d.width=c.d.height=d.d.width=d.d.height=8;c.d.depth=d.d.depth=c.d.samples=d.d.samples=1;c.d.levels=4;d.d.levels=1;c.d.target=d.d.target=NVN_TEXTURE_TARGET_2D;c.d.format=NVN_FORMAT_RGBA8;d.d.format=NVN_FORMAT_DEPTH32F_STENCIL8;
view_t cv={.set_levels=1,.nlevels=4};p_set_rt a={.n=1,.depth=&d};a.color[0]=&c;a.has_cview[0]=1;a.cview[0]=cv;p_set_rt b=a;b.cview[0].nlevels=1;
id<MTLTexture> tc=(id)[MockTex new],td=(id)[MockTex new];serial_put(26,tc);serial_put(29,td);g_enc=(id)[NSObject new];g_enc_mode=4;g_pass.hd=1;g_pass.tw=g_pass.th=8;g_pass.range_color=tc;g_pass.range_depth=td;g_pass.reg_gen=g_reg_gen;g_pass.ser_gen=g_ser_gen;g_pass.range_dirty=nvn_dirty_now();S.ds.dt=1;
E("原已证深度RO资格保持",1,rt_range_same(&a,&b));S.ds.dw=1;E("当前深度写拒绝",0,rt_range_same(&a,&b));history();S.ds.dw=0;E("曾写深度不能只看末态",0,rt_range_same(&a,&b));
+2 -1
View File
@@ -1,6 +1,7 @@
// 实际RT单级选择不变,仅view范围4→1;同纹理同CB原guard参考与候选,手写常量shader不冒游戏shader。
// The actual RT single-level selection is unchanged, only the view range goes 4→1; same texture and same CB for the original-guard reference and the candidate; hand-written constant shaders do not pass themselves off as game shaders.
#include "../nvn/nvn_mtl.m"
int prog_key(const prog_t *p, unsigned stage, uint8_t key[8]) { (void)p; (void)stage; memset(key, 0, 8); return 0; } // hle_nvn.c 的着色器键:本测不链它,给零键(原先缺这个桩,链接失败) | shader keys from hle_nvn.c: this test does not link it, so a zero key (the missing stub broke the link)
void *front_metal_layer(void){return NULL;}
module_t *module_of(const void *p){(void)p;return NULL;}
module_t g_mod[MAX_MOD];int g_nmod;
@@ -11,7 +12,7 @@ static int cases,bad,enabled,small_depth,force_invalid,sample_attachment,both_fl
typedef struct{uint8_t b[8192];size_t n;} code;
static void emit(code *c,int op,const void *p,size_t n){size_t z=(8+n+7)&~7ull;op_hdr h={op,(uint32_t)z};memcpy(c->b+c->n,&h,8);if(n)memcpy(c->b+c->n+8,p,n);c->n+=z;}
static void st(code *c,int k,const void *v,size_t n){p_bind_state b={.kind=k};memcpy(b.bytes,v,n);emit(c,OP_BIND_STATE,&b,sizeof b);}
static tex_t color={.serial=52000},depth={.serial=52001};static prog_t pg[2]={{.serial=52002},{.serial=52003}};
static tex_t color={.magic=MAGIC('T', 'E', 'X', 0), .serial =52000},depth={.magic=MAGIC('T', 'E', 'X', 0), .serial =52001};static prog_t pg[2]={{.serial=52002},{.serial=52003}};
static p_set_rt rt(int levels){p_set_rt r={.n=1,.depth=&depth,.has_dview=1};r.color[0]=&color;r.has_cview[0]=1;
r.cview[0]=(view_t){.set_levels=1,.set_layers=1,.set_format=1,.set_target=1,.nlevels=levels,.nlayers=1,.format=NVN_FORMAT_RGBA8,.target=NVN_TEXTURE_TARGET_2D};
r.dview=r.cview[0];r.dview.nlevels=1;r.dview.base_level=small_depth?1:0;r.dview.format=NVN_FORMAT_DEPTH32F_STENCIL8;return r;}
+1
View File
@@ -1,4 +1,5 @@
#include "../nvn/nvn_mtl.m"
int prog_key(const prog_t *p, unsigned stage, uint8_t key[8]) { (void)p; (void)stage; memset(key, 0, 8); return 0; } // hle_nvn.c 的着色器键:本测不链它,给零键(原先缺这个桩,链接失败) | shader keys from hle_nvn.c: this test does not link it, so a zero key (the missing stub broke the link)
void *front_metal_layer(void){return NULL;}
module_t *module_of(const void *p){(void)p;return NULL;}
module_t g_mod[MAX_MOD];int g_nmod;
+2 -1
View File
@@ -1,4 +1,5 @@
#include "../nvn/nvn_mtl.m"
int prog_key(const prog_t *p, unsigned stage, uint8_t key[8]) { (void)p; (void)stage; memset(key, 0, 8); return 0; } // hle_nvn.c 的着色器键:本测不链它,给零键(原先缺这个桩,链接失败) | shader keys from hle_nvn.c: this test does not link it, so a zero key (the missing stub broke the link)
void *front_metal_layer(void){return NULL;}
module_t *module_of(const void *p){(void)p;return NULL;}
module_t g_mod[MAX_MOD];int g_nmod;
@@ -14,7 +15,7 @@ static int shape(const p_set_rt *a,const p_set_rt *b){
#endif
}
int main(void){@autoreleasepool{
tex_t c={.serial=26},d={.serial=29};c.d.width=c.d.height=8;c.d.depth=c.d.samples=1;c.d.levels=4;c.d.target=NVN_TEXTURE_TARGET_2D;c.d.format=NVN_FORMAT_RGBA8;d.d=c.d;d.d.levels=1;d.d.format=NVN_FORMAT_DEPTH32F_STENCIL8;
tex_t c={.magic=MAGIC('T', 'E', 'X', 0), .serial =26},d={.magic=MAGIC('T', 'E', 'X', 0), .serial =29};c.d.width=c.d.height=8;c.d.depth=c.d.samples=1;c.d.levels=4;c.d.target=NVN_TEXTURE_TARGET_2D;c.d.format=NVN_FORMAT_RGBA8;d.d=c.d;d.d.levels=1;d.d.format=NVN_FORMAT_DEPTH32F_STENCIL8;
view_t cv={.set_levels=1,.set_layers=1,.set_format=1,.set_target=1,.base_level=0,.nlevels=4,.min_layer=0,.nlayers=1,.format=NVN_FORMAT_RGBA8,.target=NVN_TEXTURE_TARGET_2D};view_t dv=cv;dv.nlevels=1;dv.format=d.d.format;
p_set_rt a={.n=1,.depth=&d,.has_dview=1,.dview=dv};a.color[0]=&c;a.has_cview[0]=1;a.cview[0]=cv;p_set_rt b=a;b.cview[0].nlevels=1;
E("实际level0同层仅级数变化",1,shape(&a,&b));
+1
View File
@@ -1,4 +1,5 @@
#include "../nvn/nvn_mtl.m"
int prog_key(const prog_t *p, unsigned stage, uint8_t key[8]) { (void)p; (void)stage; memset(key, 0, 8); return 0; } // hle_nvn.c 的着色器键:本测不链它,给零键(原先缺这个桩,链接失败) | shader keys from hle_nvn.c: this test does not link it, so a zero key (the missing stub broke the link)
void *front_metal_layer(void) { return NULL; }
module_t *module_of(const void *p) { (void)p; return NULL; }
module_t g_mod[MAX_MOD]; int g_nmod;
+2 -1
View File
@@ -1,4 +1,5 @@
#include "../nvn/nvn_mtl.m"
int prog_key(const prog_t *p, unsigned stage, uint8_t key[8]) { (void)p; (void)stage; memset(key, 0, 8); return 0; } // hle_nvn.c 的着色器键:本测不链它,给零键(原先缺这个桩,链接失败) | shader keys from hle_nvn.c: this test does not link it, so a zero key (the missing stub broke the link)
void *front_metal_layer(void) { return NULL; }
module_t *module_of(const void *p) { (void)p; return NULL; }
module_t g_mod[MAX_MOD]; int g_nmod;
@@ -9,7 +10,7 @@ static int fail, count;
typedef struct { uint8_t *p; size_t n; } chain;
static chain chain_new(void) { chain c={0}; if (posix_memalign((void **)&c.p,getpagesize(),65536)) abort(); memset(c.p,0,65536); mtl_pool_add(c.p,65536); return c; }
static void emit(chain *c, unsigned op, const void *a, size_t n) { size_t z=(8+n+7)&~7ull; op_hdr h={op,(uint32_t)z}; memcpy(c->p+c->n,&h,8); if(n) memcpy(c->p+c->n+8,a,n); c->n+=z; }
static tex_t tex(unsigned w) { static uint64_t s=9000; tex_t t={.serial=s++}; t.d.width=t.d.height=w; t.d.depth=t.d.samples=1; t.d.levels=2; t.d.target=NVN_TEXTURE_TARGET_2D; t.d.format=NVN_FORMAT_RGBA8; return t; }
static tex_t tex(unsigned w) { static uint64_t s=9000; tex_t t={.magic=MAGIC('T', 'E', 'X', 0), .serial =s++}; t.d.width=t.d.height=w; t.d.depth=t.d.samples=1; t.d.levels=2; t.d.target=NVN_TEXTURE_TARGET_2D; t.d.format=NVN_FORMAT_RGBA8; return t; }
static int view(const tex_t *t, int lv) { static int id=1200; view_t v={.set_levels=1,.base_level=lv,.nlevels=1}; mtl_reg_tex(id,t,&v); return id++; }
static void set_rt(chain *c, const tex_t *a, const tex_t *b, int alv) { p_set_rt r={.n=b?2:1}; r.color[0]=a; r.color[1]=b; for(int i=0;i<r.n;i++){r.has_cview[i]=1;r.cview[i].set_levels=1;r.cview[i].base_level=i?0:alv;r.cview[i].nlevels=1;} emit(c,OP_SET_RT,&r,sizeof r); p_viewports v={.n=1};v.v[0][2]=v.v[0][3]=8;emit(c,OP_VIEWPORTS,&v,sizeof v);p_scissor sc={0,0,8,8};emit(c,OP_SCISSOR,&sc,sizeof sc); }
static void clear_color(chain *c, int i, float r, float g, float b) { p_clear_color x={.index=i,.mask=15,.rgba={r,g,b,1}}; emit(c,OP_CLEAR_COLOR,&x,sizeof x); }
+2 -1
View File
@@ -1,4 +1,5 @@
#include "../nvn/nvn_mtl.m"
int prog_key(const prog_t *p, unsigned stage, uint8_t key[8]) { (void)p; (void)stage; memset(key, 0, 8); return 0; } // hle_nvn.c 的着色器键:本测不链它,给零键(原先缺这个桩,链接失败) | shader keys from hle_nvn.c: this test does not link it, so a zero key (the missing stub broke the link)
void *front_metal_layer(void) { return NULL; }
module_t *module_of(const void *p) { (void)p; return NULL; }
module_t g_mod[MAX_MOD]; int g_nmod;
@@ -9,7 +10,7 @@ static int fail, count;
typedef struct { uint8_t *p; size_t n; } chain;
static chain chain_new(void) { chain c={0}; if (posix_memalign((void **)&c.p,getpagesize(),65536)) abort(); memset(c.p,0,65536); mtl_pool_add(c.p,65536); return c; }
static void emit(chain *c, unsigned op, const void *a, size_t n) { size_t z=(8+n+7)&~7ull; op_hdr h={op,(uint32_t)z}; memcpy(c->p+c->n,&h,8); if(n) memcpy(c->p+c->n+8,a,n); c->n+=z; }
static tex_t tex(unsigned w) { static uint64_t s=9000; tex_t t={.serial=s++}; t.d.width=t.d.height=w; t.d.depth=t.d.samples=1; t.d.levels=2; t.d.target=NVN_TEXTURE_TARGET_2D; t.d.format=NVN_FORMAT_RGBA8; return t; }
static tex_t tex(unsigned w) { static uint64_t s=9000; tex_t t={.magic=MAGIC('T', 'E', 'X', 0), .serial =s++}; t.d.width=t.d.height=w; t.d.depth=t.d.samples=1; t.d.levels=2; t.d.target=NVN_TEXTURE_TARGET_2D; t.d.format=NVN_FORMAT_RGBA8; return t; }
static int view(const tex_t *t, int lv) { static int id=1200; view_t v={.set_levels=1,.base_level=lv,.nlevels=1}; mtl_reg_tex(id,t,&v); return id++; }
static void set_rt(chain *c, const tex_t *a, const tex_t *b, int alv) { p_set_rt r={.n=b?2:1}; r.color[0]=a; r.color[1]=b; for(int i=0;i<r.n;i++){r.has_cview[i]=1;r.cview[i].set_levels=1;r.cview[i].base_level=i?0:alv;r.cview[i].nlevels=1;} emit(c,OP_SET_RT,&r,sizeof r); p_viewports v={.n=1};v.v[0][2]=v.v[0][3]=8;emit(c,OP_VIEWPORTS,&v,sizeof v);p_scissor sc={0,0,8,8};emit(c,OP_SCISSOR,&sc,sizeof sc); }
static void clear_color(chain *c, int i, float r, float g, float b) { p_clear_color x={.index=i,.mask=15,.rgba={r,g,b,1}}; emit(c,OP_CLEAR_COLOR,&x,sizeof x); }
+1 -1
View File
@@ -73,7 +73,7 @@ Consequence: any optimization that infers "which textures a draw reads" from bou
- **One geometry shader** (in `unity_builtin_extra`): Metal has no geometry stage, so it fails to compile (`unknown main0_out`); nothing in the opening scenes uses it.
- **Fixed: a crash a few minutes into the prologue battle (scripted run, about 357 s).** It wrote to `NULL + 0x1fffe0` at `main+0x9a4664`: Unity's TLSF heap allocator (`main+0x9a45d0`) got no address region for a new 2 MB pool. The alias region was out of address space, not memory; see [03](03-bringup.md), "A null-based write minutes into play". A 12-minute scripted run now plays through the battle.
- **Fixed: GPU memory grew with every scene change** (the idle title demo reached 5.4 GB of GPU memory and 3.6 GB resident in 850 s; it now stays at 1.2 to 1.4 GB). Unity frees memory pools on scene changes and builds new ones, which Mario never did in its first course. Two host structures kept freed pools alive: the residency set still held each pool's no-copy `MTLBuffer` after `nvnMemoryPoolFinalize` (now retired through deferred release), and the storage-sharing texture table (`g_alias`, keyed by pool object, offset and description) never evicted, so the old pools' render targets stayed, and linear textures built on a pool buffer kept the whole buffer alive. Finalizing a pool now evicts its entries (`mtl_alias_drop_pool`). The `内存 至帧` line now also reports `序号对象` (host objects by serial), `已释放仍活` (finalized pool buffers still alive) and `共用贴图` (sharing table size and evictions); growth in GPU memory with those counts flat points to an allocation outside them.
- **Probably fixed: about one run in four stalled early** (around the opening movie or the title; no log output, not even the time-limit report). The stalls happened before the real thread pause for IL2CPP's GC ([03](03-bringup.md)); since then 12 runs (eight of them 200 s runs through the movie and title, four of 330 to 850 s) have not stalled once, where one in four used to. Without the pause the collector ran while other threads kept changing the heap. If a stall comes back, sample the process when its log stops growing (`sample <pid> 3`) to see where every thread waits; the crash handler now writes one line with `write()` and exits if the stderr lock stays taken for 1 s, so a stall that holds that lock shows up as such.
- **Probably fixed: about one run in four stalled early** (around the opening movie or the title; no log output, not even the time-limit report). The stalls happened before the real thread pause for IL2CPP's GC ([03](03-bringup.md)); since then 12 runs (eight of them 200 s runs through the movie and title, four of 330 to 850 s) have not stalled once, where one in four used to. Without the pause the collector ran while other threads kept changing the heap. A second candidate, fixed later: a race in the dirty-page tracker (`nvn_dirty.c`, ported from d2r-mobile) could leave a page read-only while marked writable, so a game thread writing it spun forever in the fault handler; Engage's movie decoder writes frames into texture storage from a host thread while the replay thread protects pages (`tests/t_dirty_race.c` reproduces it). If a stall comes back, sample the process when its log stops growing (`sample <pid> 3`) to see where every thread waits; the crash handler now writes one line with `write()` and exits if the stderr lock stays taken for 1 s, so a stall that holds that lock shows up as such.
- **Fixed: the idle attract demo crashed after about 220 s.** Unity's particle geometry jobs wrote through a NULL index pointer on several job threads at once (`main+0xdb0ad4`, `0xdaea58`, `0xdadb5c`). The scheduler (`main+0xd91e0c`) takes its vertex and index chunks from a fallback that creates and maps a fresh buffer each time (`main+0x7b9c64`; Unity's Switch renderer always takes it, as on hardware), and from about 180 s 32-bit mesh indices also turned up inside Unity's texture objects. The cause was the alias region running out of address space, as in the battle crash ([03](03-bringup.md), "A null-based write minutes into play"): past 2 MB the index buffers got no mapping. With the node-area cut an 850 s idle run is clean, with no corrupted texture objects; with the old SDK the same run dies at 159 s in Unity's heap allocator. Reproduce the old state: `boot.sh <log> 245` with no input on an SDK rewritten without the cut.
## 7. Diagnostic switches (host)