diff --git a/dm/gfx/nvn/nvn_dirty.c b/dm/gfx/nvn/nvn_dirty.c index 9e00974..bba25f9 100644 --- a/dm/gfx/nvn/nvn_dirty.c +++ b/dm/gfx/nvn/nvn_dirty.c @@ -18,7 +18,13 @@ enum { PG_NONE = 0, PG_PROT = 1, PG_BUSY = 2 }; // BUSY:正在解除保护 // 跟踪区间 = 各内存池(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 -typedef struct { uintptr_t base, end; _Atomic uint8_t *state; _Atomic uint64_t *gen; size_t cap; _Atomic int live; } range_t; +// 区间按宿主页(16 KB)取整,客体内存池只按 4 KB 对齐:相邻的池会共用首尾页,同一个地址可能落在多个在册区间里。 +// 所以:判「贴图在不在跟踪」要找能整段装下它的区间;缺页 / 宿主写前要把覆盖该页的每个区间都解保护并记代号;销池按池的确切起点 mem 认区间。 +// (原先一律取第一个包含该地址的区间:SMBW 两张 ASTC 6x5 贴图的池首页与前一个小池共页,被判成未跟踪,每次取用都重传,每帧约 6 MB) +// Ranges are rounded to host pages (16 KB) but guest memory pools are only 4 KB aligned: adjacent pools share their first / last pages, so one address can fall in several registered ranges. +// So: "is this texture tracked" looks for a range that holds its whole span; a fault / host pre-write unprotects and stamps every range covering that page; pool destruction identifies its range by the pool's exact start mem. +// (Previously the first range containing the address was always taken: two SMBW ASTC 6x5 textures whose pool's first page is shared with a small preceding pool were judged untracked and re-uploaded on every use, about 6 MB per frame) +typedef struct { uintptr_t base, end, mem; _Atomic uint8_t *state; _Atomic uint64_t *gen; size_t cap; _Atomic int live; } range_t; static range_t g_r[MAX_RANGE]; static _Atomic int g_nr; static uintptr_t g_pg, g_shift; @@ -35,9 +41,11 @@ 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 range_t *find(uintptr_t a) { +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) { int n = atomic_load(&g_nr); - for (int i = 0; i < n; i++) if (atomic_load_explicit(&g_r[i].live, memory_order_acquire) && a >= g_r[i].base && a < g_r[i].end) return &g_r[i]; + for (int i = 0; i < n; i++) if (live_at(i) && a >= g_r[i].base && a + len <= g_r[i].end) return &g_r[i]; return NULL; } static void unprotect_page(range_t *r, uintptr_t p) { // 调用方已把状态从 PROT 改成 BUSY | The caller has already changed the state from PROT to BUSY @@ -45,28 +53,29 @@ 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 +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 +} static int on_fault(void *addr, int is_write, void *uc) { (void)uc; - range_t *r = is_write ? find((uintptr_t)addr) : NULL; - if (!r) return 0; - uintptr_t p = ((uintptr_t)addr - r->base) >> g_shift; - uint8_t st = PG_PROT; - if (atomic_compare_exchange_strong(&r->state[p], &st, PG_BUSY)) { unprotect_page(r, p); atomic_fetch_add(&g_faults, 1); return 1; } - if (st == PG_BUSY) { while (atomic_load(&r->state[p]) == PG_BUSY) {} return 1; } // 别的线程正在解:等它解完重执行 | Another thread is unprotecting it: wait for it to finish and re-execute - return atomic_load(&r->gen[p]) != 0; // 刚被别的线程解掉(本次缺页发生在解之前):重执行;从未保护过的页不是我们的 | Just unprotected by another thread (this fault happened before the unprotect): re-execute; pages that were never protected are not ours + if (!is_write) return 0; + uintptr_t a = (uintptr_t)addr; int ours = 0, n = atomic_load(&g_nr); + for (int i = 0; i < n; i++) // 共页的每个区间都要解:只解一个的话,另一个区间的状态仍是「受保护」,之后的写不再缺页、被漏掉 | Every range sharing the page must be released: releasing only one leaves the other range's state at "protected", and later writes no longer fault and are missed + if (live_at(i) && a >= g_r[i].base && a < g_r[i].end) ours |= release_page(&g_r[i], (a - g_r[i].base) >> g_shift, &g_faults); + return ours; } static void prep_write(void *addr, size_t len) { - uintptr_t a = (uintptr_t)addr, e = a + len; - while (len && a < e) { - range_t *r = find(a); - if (!r) { a = (a + g_pg) & ~(g_pg - 1); continue; } - uintptr_t stop = e < r->end ? e : r->end; - for (uintptr_t p = (a - r->base) >> g_shift, q = (stop - 1 - r->base) >> g_shift; p <= q; p++) { - uint8_t st = PG_PROT; - if (atomic_compare_exchange_strong(&r->state[p], &st, PG_BUSY)) { unprotect_page(r, p); atomic_fetch_add(&g_preps, 1); } - else while (atomic_load(&r->state[p]) == PG_BUSY) {} - } - a = stop; + uintptr_t a = (uintptr_t)addr, e = a + len; int n = atomic_load(&g_nr); + if (!len) return; + for (int i = 0; i < n; i++) { + range_t *r = &g_r[i]; + if (!live_at(i) || e <= r->base || a >= r->end) continue; + uintptr_t lo = a > r->base ? a : r->base, hi = e < r->end ? e : r->end; + for (uintptr_t p = (lo - r->base) >> g_shift, q = (hi - 1 - r->base) >> g_shift; p <= q; p++) release_page(r, p, &g_preps); } } @@ -101,7 +110,7 @@ void nvn_dirty_add_range(const void *mem, size_t size) { } else { memset((void *)state, 0, np); memset((void *)gen, 0, np * sizeof *gen); } - r->base = base; r->end = end; r->state = state; r->gen = gen; + r->base = base; r->end = end; r->mem = (uintptr_t)mem; r->state = state; r->gen = gen; if (slot == n) atomic_store(&g_nr, n + 1); atomic_store_explicit(&r->live, 1, memory_order_release); pthread_mutex_unlock(&g_slot_mu); @@ -110,18 +119,19 @@ void nvn_dirty_add_range(const void *mem, size_t size) { // Pool destruction: first restore still-protected pages to writable (the pool memory may be reused for something else once returned to the game), then mark invalid void nvn_dirty_remove_range(const void *mem) { pthread_mutex_lock(&g_slot_mu); - range_t *r = find((uintptr_t)mem); + range_t *r = NULL; int n = atomic_load(&g_nr); + for (int i = 0; i < n && !r; i++) if (live_at(i) && g_r[i].mem == (uintptr_t)mem) r = &g_r[i]; // 按池的确切起点认:共页的邻池取整后的 base 可能相同 | Match by the pool's exact start: an adjacent pool sharing a page may have the same rounded base if (r) { prep_write((void *)r->base, r->end - r->base); atomic_store_explicit(&r->live, 0, memory_order_release); atomic_fetch_sub(&g_live, 1); } pthread_mutex_unlock(&g_slot_mu); } int nvn_dirty_tracked(const void *addr, size_t len) { - range_t *r = find((uintptr_t)addr); return r && (uintptr_t)addr + len <= r->end; + 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) void nvn_dirty_protect(const void *addr, size_t len) { - if (!len || !nvn_dirty_tracked(addr, len)) return; - range_t *r = find((uintptr_t)addr); + 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 的页断开连续段:不重复保护,也不碰别的线程正在解的页 @@ -138,8 +148,8 @@ void nvn_dirty_protect(const void *addr, size_t len) { uint64_t nvn_dirty_now(void) { return atomic_load(&g_now); } int nvn_dirty_since(const void *addr, size_t len, uint64_t stamp) { if (!len) return 0; - if (!nvn_dirty_tracked(addr, len)) return 1; // 未跟踪时保守视为脏,宁可多传也不显示旧内容 | When untracked, conservatively treat as dirty: better to upload more than to show stale content - range_t *r = find((uintptr_t)addr); + range_t *r = find_span((uintptr_t)addr, len); + if (!r) return 1; // 未跟踪时保守视为脏,宁可多传也不显示旧内容 | When untracked, conservatively treat as dirty: better to upload more than to show stale content uintptr_t a = (uintptr_t)addr, p = (a - r->base) >> g_shift, q = (a + len - 1 - r->base) >> g_shift; for (uintptr_t i = p; i <= q; i++) if (atomic_load(&r->gen[i]) > stamp) return 1; // 上传前最后一次写的代号 ≤ 戳;之后的写必 > 戳 | The generation of the last write before upload is <= the stamp; any later write must be > the stamp return 0; diff --git a/dm/gfx/tests/t_dirty_slots.c b/dm/gfx/tests/t_dirty_slots.c index 856de78..53b5e02 100644 --- a/dm/gfx/tests/t_dirty_slots.c +++ b/dm/gfx/tests/t_dirty_slots.c @@ -4,6 +4,7 @@ #include #include #include +#include module_t g_mod[MAX_MOD]; int g_nmod; @@ -56,5 +57,34 @@ int main(void) { nvn_dirty_remove_range(live[i]); munmap(live[i], 4096); } + + // 相邻的池共用宿主页(池 4 KB 对齐,宿主页 16 KB):SMBW 的 ASTC 贴图池首页与前一个小池共页,曾被判成未跟踪、每次取用都重传 + // Adjacent pools sharing a host page (pools are 4 KB aligned, host pages 16 KB): the first page of SMBW's ASTC texture pool is shared with a small preceding pool, and was judged untracked and re-uploaded on every use + size_t pg = (size_t)getpagesize(); + uint8_t *blk = mmap(NULL, 4 * pg, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0); + if (blk == MAP_FAILED) { perror("mmap"); exit(2); } + uint8_t *pa = blk, *pb = blk + pg - 0x1000; // 池 A 占首页前部,池 B 从首页最后 4 KB 起 | pool A covers the start of page 0, pool B starts in the last 4 KB of page 0 + size_t na = pg - 0x1000, nb = 2 * pg, tex = pg + 0x800; // B 里的贴图跨共用页与下一页 | the texture in B spans the shared page and the next one + nvn_dirty_add_range(pa, na); + nvn_dirty_add_range(pb, nb); + EXPECT("共页的后一个池里的贴图在跟踪", 1, nvn_dirty_tracked(pb, tex)); + nvn_dirty_protect(pb, tex); + stamp = nvn_dirty_now(); + EXPECT("共页贴图保护后没人写:不脏", 0, nvn_dirty_since(pb, tex, stamp)); + pb[0x10] = 3; + EXPECT("游戏写共页里 B 的部分:B 的贴图脏", 1, nvn_dirty_since(pb, tex, stamp)); + nvn_dirty_protect(pb, tex); + stamp = nvn_dirty_now(); + pa[0x10] = 4; // 同一物理页,A 的部分:页已只读,缺页要由 B 的区间认领,否则当成崩溃 | same physical page, A's part: the page is read-only, the fault must be claimed through B's range or it is treated as a crash + EXPECT("游戏写共页里 A 的部分:B 的贴图保守判脏", 1, nvn_dirty_since(pb, tex, stamp)); + nvn_dirty_protect(pb, tex); + stamp = nvn_dirty_now(); + dm_guest_write_prepare(pa, 0x20); pa[0x20] = 5; // 宿主替 GPU 写 A:共页也要解开 B 的保护 | the host writes A on behalf of the GPU: the shared page must also release B's protection + EXPECT("宿主写前解保护共页:B 的贴图脏", 1, nvn_dirty_since(pb, tex, stamp)); + nvn_dirty_remove_range(pb); + EXPECT("销 B 后 A 仍在跟踪(按池起点认区间)", 1, nvn_dirty_tracked(pa, na)); + EXPECT("销 B 后 B 不再跟踪", 0, nvn_dirty_tracked(pb, tex)); + nvn_dirty_remove_range(pa); + munmap(blk, 4 * pg); return g_fail != 0; }