Preserve unaligned NVN vertex streams on Metal (#319)

Copy byte-aligned NVN streams into aligned submission-owned storage.
Use CPU snapshots only when host visibility is proven safe; otherwise
encode ordered GPU copies before regular, feedback and geometry draws.
Invalidate copies after overlapping writes and pool remaps while keeping
them reusable across unrelated uniform updates. Restore the encoder after
deferred replay when a stream copy split the pass.

Extend t_vbuf to cover all nonzero address residues, copy reuse, queued
copies, deferred updates, fills, shader writes, previous submissions and
pool replacement. Record the general mechanism and Arkham Knight finding.
The per-stream copy limit remains 64 MiB.

Validated on Apple M4 Pro: 84 GPU checks in each of normal, cache-disabled
and encoder-thread modes with Metal validation; full graphics and host
suites; 70 graphics Python tests; 298 tool tests; documentation tests;
iOS compile check (178 files). Full cross-game validation is incomplete:
the harness reported unsupported OpenGL chain checking and its first NVN
shader rebuild did not finish alongside other active builds. No local
Arkham Knight gameplay run.

Co-authored-by: v0lumehi <aljic.zijad@gmail.com>
This commit is contained in:
seeddsandv0lumehi committed 2026-10-27 09:46:11 +08:00
1 parent 15805ef525
commit 40e6baede5
8 files changed
+138 -12

No files matched your search

+3 -3
View File
@@ -1442,10 +1442,10 @@ static void tsummary(uint64_t frame) {
if (k) { qsort(v, k, sizeof *v, dcmp); LOG("[nvn] on-screen up to frame %llu: %u frames, dropped %u (discarded %u), p99 %.2f ms; footprint %.0f MiB; brightness %.2f; thermal %d", frame, k + discard, drop, discard, v[(k * 99) / 100 < k ? (k * 99) / 100 : k - 1], fp, br, dm_thermal_now()); } if (k) { qsort(v, k, sizeof *v, dcmp); LOG("[nvn] on-screen up to frame %llu: %u frames, dropped %u (discarded %u), p99 %.2f ms; footprint %.0f MiB; brightness %.2f; thermal %d", frame, k + discard, drop, discard, v[(k * 99) / 100 < k ? (k * 99) / 100 : k - 1], fp, br, dm_thermal_now()); }
g_presn = 0; } g_presn = 0; }
power_line(frame); power_line(frame);
{ static uint64_t prev[4], pf; uint64_t v[4]; mtl_vbuf_stats(&v[0], &v[1], &v[2], &v[3]); { static uint64_t prev[6], pf; uint64_t v[6]; mtl_vbuf_stats(&v[0], &v[1], &v[2], &v[3], &v[4], &v[5]);
if (pf && frame > pf) { double nf = (double)(frame - pf); if (pf && frame > pf) { double nf = (double)(frame - pf);
LOG("[nvn] vertex bindings up to frame %llu: per frame address lookups %.0f / reused %.0f, vertex table writes %.0f / skipped %.0f", frame, LOG("[nvn] vertex bindings up to frame %llu: per frame address lookups %.0f / reused %.0f, vertex table writes %.0f / skipped %.0f, streams not 4-byte aligned copied %.1f (%.2f MB)", frame,
(v[0] - prev[0]) / nf, (v[1] - prev[1]) / nf, (v[2] - prev[2]) / nf, (v[3] - prev[3]) / nf); } (v[0] - prev[0]) / nf, (v[1] - prev[1]) / nf, (v[2] - prev[2]) / nf, (v[3] - prev[3]) / nf, (v[4] - prev[4]) / nf, (v[5] - prev[5]) / nf / 1048576.0); }
memcpy(prev, v, sizeof prev); pf = frame; } memcpy(prev, v, sizeof prev); pf = frame; }
{ static uint64_t pi, pc, pd, pf; uint64_t ci = atomic_load(&g_cum_sub_instr), cc = atomic_load(&g_cum_sub_cpu_ns), ps_, dr_; mtl_stats(&ps_, &dr_); // translation layer { static uint64_t pi, pc, pd, pf; uint64_t ci = atomic_load(&g_cum_sub_instr), cc = atomic_load(&g_cum_sub_cpu_ns), ps_, dr_; mtl_stats(&ps_, &dr_); // translation layer
if (pf && frame > pf && dr_ > pd) LOG("[nvn] translation up to frame %llu: %.0f instructions per draw, submit per frame %.2f M instructions / CPU %.3f ms, %.0f draws per frame", frame, if (pf && frame > pf && dr_ > pd) LOG("[nvn] translation up to frame %llu: %.0f instructions per draw, submit per frame %.2f M instructions / CPU %.3f ms, %.0f draws per frame", frame,
+1
View File
@@ -65,6 +65,7 @@ static void tf_draw(NvProg *pr,const p_draw *dw,const p_draw_idx *di) {
id<MTLRenderPipelineState> ps=vs&&vs->tf_fn?tf_pso(vs):nil;if(!ps)return; id<MTLRenderPipelineState> ps=vs&&vs->tf_fn?tf_pso(vs):nil;if(!ps)return;
uint64_t dst[4]={0};uint64_t job=tf_prepare(vs,0,0,(uint32_t)count,dw->prim==0?1:dw->prim==1?2:3, uint64_t dst[4]={0};uint64_t job=tf_prepare(vs,0,0,(uint32_t)count,dw->prim==0?1:dw->prim==1?2:3,
dw->first,dw->count,dw->base_instance,dst);if(!job)return; dw->first,dw->count,dw->base_instance,dst);if(!job)return;
vbuf_prepare();if(g_blit){enc_b_end(g_blit);g_blit=nil;}
MTL4RenderPassDescriptor *rd=[MTL4RenderPassDescriptor new];rd.renderTargetWidth=rd.renderTargetHeight=rd.defaultRasterSampleCount=1; MTL4RenderPassDescriptor *rd=[MTL4RenderPassDescriptor new];rd.renderTargetWidth=rd.renderTargetHeight=rd.defaultRasterSampleCount=1;
id<MTL4RenderCommandEncoder> en=enc_r_begin(rd);enc_r_pso(en,ps);bind_vbufs(en);vconv_bind(en,vs,1,dw,NULL); id<MTL4RenderCommandEncoder> en=enc_r_begin(rd);enc_r_pso(en,ps);bind_vbufs(en);vconv_bind(en,vs,1,dw,NULL);
args_dirty_all();encode_args(en,vs,0,1);enc_r_vbuf(en,14,job); args_dirty_all();encode_args(en,vs,0,1);enc_r_vbuf(en,14,job);
+2
View File
@@ -112,6 +112,7 @@ static void vertex_dump(NvShader *vs, const p_draw *dw, const p_draw_idx *di) {
uint64_t gpu = output.gpuAddress + offset; uint64_t gpu = output.gpuAddress + offset;
uint64_t info[4] = {gpu + 256, 0, (uint64_t)lo | span << 32, (uint32_t)(dw ? dw->base_instance : di->base_instance) | 1ull << 32}; uint64_t info[4] = {gpu + 256, 0, (uint64_t)lo | span << 32, (uint32_t)(dw ? dw->base_instance : di->base_instance) | 1ull << 32};
memcpy((uint8_t *)output.contents + offset, info, sizeof info); memcpy((uint8_t *)output.contents + offset, info, sizeof info);
vbuf_prepare();
if (g_blit) { enc_b_end(g_blit); g_blit = nil; } if (g_blit) { enc_b_end(g_blit); g_blit = nil; }
MTL4RenderPassDescriptor *rd = [MTL4RenderPassDescriptor new]; rd.renderTargetWidth = rd.renderTargetHeight = rd.defaultRasterSampleCount = 1; MTL4RenderPassDescriptor *rd = [MTL4RenderPassDescriptor new]; rd.renderTargetWidth = rd.renderTargetHeight = rd.defaultRasterSampleCount = 1;
id<MTL4RenderCommandEncoder> en = enc_r_begin(rd); enc_r_pso(en, ps); bind_vbufs(en); id<MTL4RenderCommandEncoder> en = enc_r_begin(rd); enc_r_pso(en, ps); bind_vbufs(en);
@@ -292,6 +293,7 @@ static void geo_draw(NvProg *pr, const p_draw *dw0, const p_draw_idx *di0) {
memset(pargs, 0, 20); memset(pargs, 0, 20);
nvn_geo_pack_job pj = {raw, packed, args, (uint32_t)groups, gs->geo_index_stride, gs->geo_output, (uint32_t)(packed_size / 4), cnt}; nvn_geo_pack_job pj = {raw, packed, args, (uint32_t)groups, gs->geo_index_stride, gs->geo_output, (uint32_t)(packed_size / 4), cnt};
memcpy(pjob, &pj, sizeof pj); memcpy(pjob, &pj, sizeof pj);
vbuf_prepare();
if (g_blit) { enc_b_end(g_blit); g_blit = nil; } if (g_blit) { enc_b_end(g_blit); g_blit = nil; }
MTL4RenderPassDescriptor *rd = [MTL4RenderPassDescriptor new]; rd.renderTargetWidth = rd.renderTargetHeight = rd.defaultRasterSampleCount = 1; MTL4RenderPassDescriptor *rd = [MTL4RenderPassDescriptor new]; rd.renderTargetWidth = rd.renderTargetHeight = rd.defaultRasterSampleCount = 1;
id<MTL4RenderCommandEncoder> re = enc_r_begin(rd); enc_r_pso(re, capture); bind_vbufs(re); id<MTL4RenderCommandEncoder> re = enc_r_begin(rd); enc_r_pso(re, capture); bind_vbufs(re);
+1 -1
View File
@@ -6,7 +6,7 @@
#include <stddef.h> #include <stddef.h>
void mtl_init(void); void mtl_init(void);
void mtl_vbuf_stats(uint64_t *resolve, uint64_t *reuse, uint64_t *set, uint64_t *skip); void mtl_vbuf_stats(uint64_t *resolve, uint64_t *reuse, uint64_t *set, uint64_t *skip, uint64_t *copies, uint64_t *copy_bytes);
void mtl_set_depth_zero(int on); // NVN depth mode: 1 = near is 0 (D3D / Metal), 0 = near is -w (GL, NVN's default); picks the vertex shaders' entry point (see shader_load in nvn_mtl.m) void mtl_set_depth_zero(int on); // NVN depth mode: 1 = near is 0 (D3D / Metal), 0 = near is -w (GL, NVN's default); picks the vertex shaders' entry point (see shader_load in nvn_mtl.m)
void mtl_set_origin_lower_left(int on); // NVN window origin: 1 = lower left (GL, NVN's default), 0 = upper left (D3D / Metal); see nvn_mtl.m void mtl_set_origin_lower_left(int on); // NVN window origin: 1 = lower left (GL, NVN's default), 0 = upper left (D3D / Metal); see nvn_mtl.m
void bf_want_note(const char *key); void bf_want_note(const char *key);
+62 -7
View File
@@ -94,7 +94,7 @@ static id<MTL4CommandBuffer> g_present_cb[3];
static id<MTL4ArgumentTable> g_vt, g_ft, g_ct, g_up_ct; static id<MTL4ArgumentTable> g_vt, g_ft, g_ct, g_up_ct;
static int g_vbuf_cache = 1; // DM_NVN_VBUF_CACHE=0 restores the uncached path for A/B measurements static int g_vbuf_cache = 1; // DM_NVN_VBUF_CACHE=0 restores the uncached path for A/B measurements
static uint64_t g_vt_addr[31]; static uint32_t g_vt_valid; static uint64_t g_vt_addr[31]; static uint32_t g_vt_valid;
static uint64_t g_vbuf_resolve, g_vbuf_reuse, g_vbuf_set, g_vbuf_skip; static uint64_t g_vbuf_resolve, g_vbuf_reuse, g_vbuf_set, g_vbuf_skip, g_vbuf_copies, g_vbuf_copy_bytes; // copies: vertex streams at addresses not 4-byte aligned (vbuf_unaligned)
static id<MTL4ArgumentTable> g_present_args; static id<MTL4ArgumentTable> g_present_args;
static id<MTLBuffer> g_present_uv[3]; static id<MTLBuffer> g_present_uv[3];
static uint64_t g_present_alloc_fence[3]; static uint64_t g_present_alloc_fence[3];
@@ -1524,7 +1524,7 @@ void mtl_drain(void) { present_drop_for_suspend(); mtl_present_flush(); snap_dra
// ================= Render replay ================= // ================= Render replay =================
#include "../../host/front.h" #include "../../host/front.h"
static int g_red_once[120]; static int g_red_once[121];
#define RED_ONCE(k, ...) do { if (!g_red_once[k]++) MLOG("[RED] " __VA_ARGS__); } while (0) #define RED_ONCE(k, ...) do { if (!g_red_once[k]++) MLOG("[RED] " __VA_ARGS__); } while (0)
// NVN vertex streams 0–15 → Metal vertex buffer slots: 0–13 go to 16–29 (30 is the constant fallback buffer), 14 and 15 to 14 and 15. Metal has only the 31 slots 0–30; translated shaders use only buffer(0) (the argument buffer), // NVN vertex streams 0–15 → Metal vertex buffer slots: 0–13 go to 16–29 (30 is the constant fallback buffer), 14 and 15 to 14 and 15. Metal has only the 31 slots 0–30; translated shaders use only buffer(0) (the argument buffer),
// and the host's own vertex functions read no vertex buffers. 13 Sentinels uses stream 15, and the old 16 + stream went out of range (Metal assertion: buffer index (31) must be < 31) // and the host's own vertex functions read no vertex buffers. 13 Sentinels uses stream 15, and the old 16 + stream went out of range (Metal assertion: buffer index (31) must be < 31)
@@ -4230,6 +4230,51 @@ static void ubd_dirty(uint64_t lo, uint64_t hi) {
for (int st = 0; st < 6; st++) { if (g_args_dirty[st]) continue; for (int st = 0; st < 6; st++) { if (g_args_dirty[st]) continue;
for (int r = 0; r < 16; r++) { const p_bind_buf *u = &S.ubo[st][r]; if (u->addr && lo < u->addr + u->size && u->addr < hi) { g_args_dirty[st] = 1; break; } } } for (int r = 0; r < 16; r++) { const p_bind_buf *u = &S.ubo[st][r]; if (u->addr && lo < u->addr + u->size && u->addr < hi) { g_args_dirty[st] = 1; break; } } }
} }
// NVN permits byte-aligned vertex streams; Metal drops the low two address bits. Keep an aligned copy in the submission arena.
// A CPU copy is safe only without GPU writers (including earlier submissions and sparse aliases). Otherwise copy on the GPU before
// opening the draw's encoder. Guest writes and pool remaps invalidate reuse; scratch copies themselves do not change guest memory.
#define VBUF_COPY_MAX (64u << 20)
typedef struct { uint64_t gpu, epoch, writes; uint32_t sbw; int npend; } vbuf_copy;
static nvn_hmap g_vbuf_unaligned = {.klen = 16};
static uint64_t g_vbuf_write_epoch; // shader writes invalidate snapshots without disturbing indirect conversions of the same draw
static uint64_t vbuf_unaligned(uint64_t addr, uint64_t size, int prepare) {
uint64_t key[2] = {addr, size}; vbuf_copy *c = nvn_hm_get(&g_vbuf_unaligned, key);
if (c && !prepare) return c->gpu;
if (c) {
int changed = c->epoch != g_wr_epoch || c->writes != g_vbuf_write_epoch || c->sbw != g_sbw_gen || c->npend != g_npend;
int dirty = g_pend_over || g_sbw_over || (changed && nvn_sparse_gen()) // aliases may write through another guest address
|| (changed && sbw_hits(addr, addr + size))
|| (c->npend != g_npend && pend_last(addr, size) >= c->npend);
if (!dirty) { c->epoch = g_wr_epoch; c->writes = g_vbuf_write_epoch; c->sbw = g_sbw_gen; c->npend = g_npend; return c->gpu; }
}
void *mb; uint64_t mo;
if (!size || size > VBUF_COPY_MAX || !mtl_addr(addr, size, &mb, &mo)) {
RED_ONCE(120, "vertex stream at an address not 4-byte aligned (%#llx, %llu bytes) could not be copied: bound as it is, which Metal reads from the aligned address below", (unsigned long long)addr, (unsigned long long)size);
return 0;
}
id<MTLBuffer> src = (__bridge id<MTLBuffer>)mb;
int cpu = !pend_hits(addr, size) && !g_sbw_over && !sbw_hits(addr, addr + size) && !gw_live(addr, addr + size)
&& !nvn_sparse_gen() && nvn_host_readable(addr, size) && src.contents; // a sparse mapping may alias this physical range
if (!cpu && !prepare) MDIE("unaligned vertex stream was not prepared before its render encoder");
id<MTLBuffer> ab; size_t off = arena_alloc((size_t)size, &ab);
if (cpu) memcpy((uint8_t *)ab.contents + off, (const uint8_t *)src.contents + mo, (size_t)size);
else {
g_blit_alias = 1;
id<MTL4ComputeCommandEncoder> e = blit(); // lands deferred updates and orders all preceding GPU writes, including aliases
g_blit_alias = 1;
enc_b_copy_buffer(e, src, mo, ab, off, size);
}
g_vbuf_copies++; g_vbuf_copy_bytes += size;
if (!c) { c = malloc(sizeof *c); if (!c) MDIE("cannot allocate vertex copy cache entry"); nvn_hm_put(&g_vbuf_unaligned, key, c); }
*c = (vbuf_copy){ab.gpuAddress + off, g_wr_epoch, g_vbuf_write_epoch, g_sbw_gen, g_npend}; return c->gpu;
}
static void vbuf_prepare(void) {
static uint64_t sub, gen, sparse;
uint64_t pool_gen = atomic_load_explicit(&g_pool_gen, memory_order_acquire), sparse_gen = nvn_sparse_gen();
if (sub != g_sub_serial || gen != pool_gen || sparse != sparse_gen) nvn_hm_clear(&g_vbuf_unaligned, free);
for (int s = 0; s < 16; s++) if (S.vbuf[s].address & 3) vbuf_unaligned(S.vbuf[s].address, S.vbuf[s].size, 1);
sub = g_sub_serial; gen = pool_gen; sparse = sparse_gen;
}
// Resolved guest streams are separate from the table cache: vconv_bind may overwrite a stream's table slot. // Resolved guest streams are separate from the table cache: vconv_bind may overwrite a stream's table slot.
// Take the pool generation before resolving, so a concurrent pool change forces a fresh lookup next time. // Take the pool generation before resolving, so a concurrent pool change forces a fresh lookup next time.
static void bind_vbufs(id<MTL4RenderCommandEncoder> en) { static void bind_vbufs(id<MTL4RenderCommandEncoder> en) {
@@ -4238,6 +4283,7 @@ static void bind_vbufs(id<MTL4RenderCommandEncoder> en) {
for (int s = 0; s < 16; s++) { for (int s = 0; s < 16; s++) {
uint64_t addr = S.vbuf[s].address, gpu; uint64_t addr = S.vbuf[s].address, gpu;
if (!addr) continue; if (!addr) continue;
if (addr & 3) { uint64_t c = vbuf_unaligned(addr, S.vbuf[s].size, 0); if (c) { cache[s].addr = 0; enc_r_vbuf(en, VB_SLOT(s), c); continue; } } // not cached: the copy is per submission
if (g_vbuf_cache && cache[s].addr == addr && cache[s].gen == gen) { gpu = cache[s].gpu; g_vbuf_reuse++; } if (g_vbuf_cache && cache[s].addr == addr && cache[s].gen == gen) { gpu = cache[s].gpu; g_vbuf_reuse++; }
else { else {
void *mb; uint64_t mo; g_vbuf_resolve++; void *mb; uint64_t mo; g_vbuf_resolve++;
@@ -4290,9 +4336,10 @@ static void bare_dims(NSUInteger *w, NSUInteger *h) {
// draw in one attachment-less pass with barrier bits 0x12 between them: without the split a later element read the window before the earlier one had landed and wrote the old pixels // draw in one attachment-less pass with barrier bits 0x12 between them: without the split a later element read the window before the earlier one had landed and wrote the old pixels
// back (the dialog boxes had holes the size of their text) // back (the dialog boxes had holes the size of their text)
static void swrite_note(NvProg *pr) { static void swrite_note(NvProg *pr) {
if (!pr || g_pass_swrite) return; if (!pr) return;
if (g_pass_swrite) { g_vbuf_write_epoch++; return; } // further shader writes also invalidate aligned vertex snapshots
for (int st = 0; st < 6; st++) { __unsafe_unretained NvShader *sh = pr->sh[st]; if (!sh) continue; for (int st = 0; st < 6; st++) { __unsafe_unretained NvShader *sh = pr->sh[st]; if (!sh) continue;
for (int i = 0; i < sh->rf.nb; i++) if (sh->rf.b[i].rw && (sh->rf.b[i].kind == NVN_RB_IMG || sh->rf.b[i].kind == NVN_RB_SBUFP)) { g_pass_swrite = 1; return; } } for (int i = 0; i < sh->rf.nb; i++) if (sh->rf.b[i].rw && (sh->rf.b[i].kind == NVN_RB_IMG || sh->rf.b[i].kind == NVN_RB_SBUFP)) { g_pass_swrite = 1; g_vbuf_write_epoch++; return; } }
} }
static void udc_note_draw(NvProg *pr) { static void udc_note_draw(NvProg *pr) {
if (!tbdr_on()) { // without the TBDR set only the duplicate-slot report below is live (the zero-source skip and the unwritten-attachment store are part of the set): skip the per-draw scans if (!tbdr_on()) { // without the TBDR set only the duplicate-slot report below is live (the zero-source skip and the unwritten-attachment store are part of the set): skip the per-draw scans
@@ -8574,11 +8621,19 @@ static void draw(const p_draw *dw, const p_draw_idx *di) {
if (g_enc && g_pass.layered != g_rp_layered) rp_end(); // a layered pass only for layer-writing vertex shaders (see rp_begin_ex) if (g_enc && g_pass.layered != g_rp_layered) rp_end(); // a layered pass only for layer-writing vertex shaders (see rp_begin_ex)
if (g_enc && g_sm_pass && draw_samples_rt(pr)) { PD("M %llu a draw in the slice-merged pass samples its attachment; split here", g_stat_pass); rp_end(); } if (g_enc && g_sm_pass && draw_samples_rt(pr)) { PD("M %llu a draw in the slice-merged pass samples its attachment; split here", g_stat_pass); rp_end(); }
if (!g_enc && mode == 0 && !g_rp_layered && !defer_q_draw && g_exec_next && !tbdr_on() && !g_geo_program && sm_on()) sm_plan(pr); if (!g_enc && mode == 0 && !g_rp_layered && !defer_q_draw && g_exec_next && !tbdr_on() && !g_geo_program && sm_on()) sm_plan(pr);
vbuf_prepare(); // an unaligned GPU-written stream needs its ordered copy before the render encoder
id<MTLRenderCommandEncoder> en = rp_begin_ex(mode); id<MTLRenderCommandEncoder> en = rp_begin_ex(mode);
g_rp_layered = 0; g_sm_plan.on = 0; g_rp_layered = 0; g_sm_plan.on = 0;
g_fd_dw = NULL; g_fd_di = NULL; g_fd_dw = NULL; g_fd_di = NULL;
if (!en) { PD("Z %llu draw skipped: the render pass did not begin vs=%.16s fs=%.16s", g_stat_pass, pr && pr->sh[0] ? pr->sh[0]->key : "-", pr && pr->sh[1] ? pr->sh[1]->key : "-"); return; } // Z lines: why a draw was dropped at these early returns if (!en) { PD("Z %llu draw skipped: the render pass did not begin vs=%.16s fs=%.16s", g_stat_pass, pr && pr->sh[0] ? pr->sh[0]->key : "-", pr && pr->sh[1] ? pr->sh[1]->key : "-"); return; } // Z lines: why a draw was dropped at these early returns
if (defer_q_draw) defer_replay(); if (defer_q_draw) {
defer_replay();
// A replayed draw may have ended Q's encoder to align a GPU-written stream. Restore this draw's streams and encoder.
g_rp_layered = pr->sh[0] && pr->sh[0]->layer_out;
if (g_enc && g_pass.layered != g_rp_layered) rp_end();
vbuf_prepare(); g_fd_dw = dw; g_fd_di = di; en = rp_begin_ex(mode); g_fd_dw = NULL; g_fd_di = NULL; g_rp_layered = 0;
if (!en) return;
}
if (skip_d) { g_stat_ds_omit++; if (mode == 3) g_stat_s_only++; else if (mode == 4) g_stat_d_only++; } if (skip_d) { g_stat_ds_omit++; if (mode == 3) g_stat_s_only++; else if (mode == 4) g_stat_d_only++; }
// The pipeline depends only on (program, render pass targets, blend / color / write mask / multisample state, vertex layout): if these did not change, reuse the previous one // The pipeline depends only on (program, render pass targets, blend / color / write mask / multisample state, vertex layout): if these did not change, reuse the previous one
static id<MTLRenderPipelineState> cur_ps; static NvProg *cur_pr; static uint64_t cur_ps_pass; static int cur_pull; // cur_pull: a hybrid (pulling) pipeline, asked for again at the next draw static id<MTLRenderPipelineState> cur_ps; static NvProg *cur_pr; static uint64_t cur_ps_pass; static int cur_pull; // cur_pull: a hybrid (pulling) pipeline, asked for again at the next draw
@@ -10634,8 +10689,8 @@ void mtl_present_wait(uint64_t tk) {
void mtl_present_flush(void) { pthread_mutex_lock(&g_pq_mu); uint64_t h = g_pq_in; pthread_mutex_unlock(&g_pq_mu); mtl_present_wait(h); } void mtl_present_flush(void) { pthread_mutex_lock(&g_pq_mu); uint64_t h = g_pq_in; pthread_mutex_unlock(&g_pq_mu); mtl_present_wait(h); }
void mtl_stats(uint64_t *passes, uint64_t *draws) { *passes = g_stat_pass; *draws = g_stat_draw; } void mtl_stats(uint64_t *passes, uint64_t *draws) { *passes = g_stat_pass; *draws = g_stat_draw; }
void mtl_args_stats(uint64_t *enc, uint64_t *reuse) { *enc = g_stat_args_enc; *reuse = g_stat_args_reuse; } void mtl_args_stats(uint64_t *enc, uint64_t *reuse) { *enc = g_stat_args_enc; *reuse = g_stat_args_reuse; }
void mtl_vbuf_stats(uint64_t *resolve, uint64_t *reuse, uint64_t *set, uint64_t *skip) { void mtl_vbuf_stats(uint64_t *resolve, uint64_t *reuse, uint64_t *set, uint64_t *skip, uint64_t *copies, uint64_t *copy_bytes) {
*resolve = g_vbuf_resolve; *reuse = g_vbuf_reuse; *set = g_vbuf_set; *skip = g_vbuf_skip; *resolve = g_vbuf_resolve; *reuse = g_vbuf_reuse; *set = g_vbuf_set; *skip = g_vbuf_skip; *copies = g_vbuf_copies; *copy_bytes = g_vbuf_copy_bytes;
} }
void mtl_use_stats(uint64_t *call, uint64_t *skip) { *call = 0; *skip = 0; } void mtl_use_stats(uint64_t *call, uint64_t *skip) { *call = 0; *skip = 0; }
uint64_t mtl_ds_omit(void) { return g_stat_ds_omit; } uint64_t mtl_ds_omit(void) { return g_stat_ds_omit; }
+61 -1
View File
@@ -1,4 +1,5 @@
// Real Metal readback: cached stream bindings survive helper overwrites, encoder/submit changes and pool replacement. // Real Metal readback: cached stream bindings survive helper overwrites, encoder/submit changes and pool replacement.
// Byte-aligned streams preserve their bytes and GPU write ordering (Arkham Knight's Scaleform streams sit at addresses 2 mod 4).
#include "../nvn/nvn_mtl.m" #include "../nvn/nvn_mtl.m"
uint64_t g_t0_ns; uint64_t g_t0_ns;
int dm_guest_as_contains(uintptr_t address) { (void)address; return 0; } // Tests use host-thread stacks. int dm_guest_as_contains(uintptr_t address) { (void)address; return 0; } // Tests use host-thread stacks.
@@ -24,6 +25,9 @@ static id<MTLRenderPipelineState> pipeline(void) {
return ps; return ps;
} }
static id<MTL4RenderCommandEncoder> encoder(id<MTLRenderPipelineState> ps) { static id<MTL4RenderCommandEncoder> encoder(id<MTLRenderPipelineState> ps) {
vbuf_prepare();
ubd_land_compute();
if (g_blit) { enc_b_end(g_blit); g_blit = nil; }
MTL4RenderPassDescriptor *d = [MTL4RenderPassDescriptor new]; MTL4RenderPassDescriptor *d = [MTL4RenderPassDescriptor new];
d.renderTargetWidth = d.renderTargetHeight = d.defaultRasterSampleCount = 1; d.renderTargetWidth = d.renderTargetHeight = d.defaultRasterSampleCount = 1;
id<MTL4RenderCommandEncoder> en = enc_r_begin(d); enc_r_pso(en, ps); return en; id<MTL4RenderCommandEncoder> en = enc_r_begin(d); enc_r_pso(en, ps); return en;
@@ -41,7 +45,9 @@ int main(void) { @autoreleasepool {
if (posix_memalign((void **)&mem, size, size)) abort(); if (posix_memalign((void **)&mem, size, size)) abort();
memset(mem, 0, size); mem[0] = 11; mem[1] = 22; mem[2] = 33; mem[3] = 99; memset(mem, 0, size); mem[0] = 11; mem[1] = 22; mem[2] = 33; mem[3] = 99;
mtl_pool_add(mem, size); mtl_pool_add(mem, size);
id<MTLBuffer> out = [g_dev newBufferWithLength:16 * 8 options:MTLResourceStorageModeShared]; res_add(out); uint8_t *ub = (uint8_t *)mem + 64; uint32_t want = 0x87654321u; memcpy(ub + 2, &want, 4); ub[0] = 0x11; ub[1] = 0x22; ub[6] = 0x33; ub[7] = 0x44;
uint8_t *producer = (uint8_t *)mem + 128; uint32_t changed = 0x12345678u; memcpy(producer, ub, 8); memcpy(producer + 2, &changed, 4);
id<MTLBuffer> out = [g_dev newBufferWithLength:16 * 32 options:MTLResourceStorageModeShared]; res_add(out);
state_reset(); state_reset();
// Exercise the compact record through the production dispatcher before the // Exercise the compact record through the production dispatcher before the
// existing shader reads each stream. Its public state remains a full range. // existing shader reads each stream. Its public state remains a full range.
@@ -80,6 +86,60 @@ int main(void) { @autoreleasepool {
bind_vbufs(en); EXPECT("pool replacement resolves all streams", 3, g_vbuf_resolve - resolves); bind_vbufs(en); EXPECT("pool replacement resolves all streams", 3, g_vbuf_resolve - resolves);
EXPECT("replacement address bound", newgpu, g_vt_addr[VB_SLOT(0)]); EXPECT("replacement address bound", newgpu, g_vt_addr[VB_SLOT(0)]);
capture(en, out, 7); enc_r_end(en); finish(); capture(en, out, 7); enc_r_end(en); finish();
mtl_sub_begin(); S.vbuf[0].address = (uint64_t)(ub + 2); S.vbuf[0].size = 6;
uint64_t copies = g_vbuf_copies, copy_bytes = g_vbuf_copy_bytes; en = encoder(ps);
bind_vbufs(en); bind_vbufs(en); capture(en, out, 8); enc_r_end(en);
EXPECT("unchanged unaligned binding reuses one copy", 1, g_vbuf_copies - copies);
EXPECT("unaligned stream copy size", 6, g_vbuf_copy_bytes - copy_bytes);
mtl_sub_copy((uint64_t)producer, (uint64_t)ub, 8);
en = encoder(ps); bind_vbufs(en); capture(en, out, 9); enc_r_end(en); finish();
EXPECT("unaligned stream readback", want, ((const uint32_t *)out.contents)[8 * 4]);
EXPECT("unaligned stream refreshed after GPU write", changed, ((const uint32_t *)out.contents)[9 * 4]);
memcpy(producer + 2, &want, 4);
mtl_sub_begin(); mtl_sub_copy((uint64_t)producer, (uint64_t)ub, 8);
en = encoder(ps); bind_vbufs(en); capture(en, out, 10); enc_r_end(en); finish();
EXPECT("unaligned stream reads queued GPU write", want, ((const uint32_t *)out.contents)[10 * 4]);
// A pool remap at the same guest address invalidates the snapshot even within one submission.
mtl_sub_begin(); en = encoder(ps); bind_vbufs(en); capture(en, out, 11); enc_r_end(en);
copies = g_vbuf_copies; mtl_pool_del(mem); mtl_pool_add(mem, size);
en = encoder(ps); bind_vbufs(en); capture(en, out, 12); enc_r_end(en); finish();
EXPECT("unaligned pool replacement makes a new copy", 1, g_vbuf_copies - copies);
EXPECT("unaligned pool replacement readback", want, ((const uint32_t *)out.contents)[12 * 4]);
// Every nonzero residue, a short odd-sized range, and a source write between two uses of the same binding.
for (unsigned residue = 1; residue < 4; residue++) {
uint32_t value = 0x76543210u + residue; memcpy(producer + residue, &value, 4);
S.vbuf[0].address = (uint64_t)(ub + residue); S.vbuf[0].size = 5;
mtl_sub_begin(); mtl_sub_copy((uint64_t)producer, (uint64_t)ub, 8);
en = encoder(ps); bind_vbufs(en); capture(en, out, 12 + residue); enc_r_end(en); finish();
EXPECT("byte-aligned GPU copy readback", value, ((const uint32_t *)out.contents)[(12 + residue) * 4]);
}
// Inline updates may still be deferred when the next draw needs their bytes as vertices.
changed = 0xa1b2c3d4u; memcpy(producer + 3, &changed, 4);
mtl_sub_begin(); mtl_sub_update(producer, (uint64_t)ub, 8);
en = encoder(ps); bind_vbufs(en); capture(en, out, 16); enc_r_end(en); finish();
EXPECT("unaligned stream reads deferred update", changed, ((const uint32_t *)out.contents)[16 * 4]);
mtl_sub_begin(); mtl_sub_fill((uint64_t)ub, 8, 0x6d6d6d6du);
en = encoder(ps); bind_vbufs(en); capture(en, out, 17); enc_r_end(en); finish();
EXPECT("unaligned stream reads GPU fill", 0x6d6d6d6du, ((const uint32_t *)out.contents)[17 * 4]);
// Begin the consumer without a CPU wait for the producer submission.
mtl_sub_begin(); mtl_sub_copy((uint64_t)producer, (uint64_t)ub, 8); uint64_t prior = mtl_sub_commit();
mtl_sub_begin(); en = encoder(ps); bind_vbufs(en); capture(en, out, 18); enc_r_end(en); finish();
EXPECT("producer submission completed", 1, mtl_wait(prior, 5000000000ull));
EXPECT("unaligned stream reads preceding submission", changed, ((const uint32_t *)out.contents)[18 * 4]);
// A real vertex shader writes the source; storage writes are tracked separately from copy/update/fill ranges.
S.vbuf[0].address = (uint64_t)(mem + 3); S.vbuf[0].size = 4;
uint64_t ubgpu; mtl_gpu_addr((uint64_t)ub, 16, &mb, &off, &ubgpu);
mtl_sub_begin(); en = encoder(ps); bind_vbufs(en); enc_r_vbuf(en, TF_SLOT, ubgpu); enc_r_draw(en, MTLPrimitiveTypePoint, 0, 1); enc_r_end(en);
sbw_note((uint64_t)ub, 16, SBW_GPU);
S.vbuf[0].address = (uint64_t)(ub + 2); S.vbuf[0].size = 6;
en = encoder(ps); bind_vbufs(en); capture(en, out, 19); enc_r_end(en); finish();
EXPECT("unaligned stream reads shader write", 22u << 16, ((const uint32_t *)out.contents)[19 * 4]);
mtl_sub_begin(); en = encoder(ps); bind_vbufs(en); capture(en, out, 20); enc_r_end(en); copies = g_vbuf_copies;
mtl_sub_update(producer, (uint64_t)mem + 256, 8);
en = encoder(ps); bind_vbufs(en); capture(en, out, 21); enc_r_end(en); finish();
EXPECT("unrelated uniform update reuses vertex copy", copies, g_vbuf_copies);
EXPECT("reused unaligned stream readback", 22u << 16, ((const uint32_t *)out.contents)[21 * 4]);
S.vbuf[0].address = (uint64_t)(mem + 3); S.vbuf[0].size = 4;
const uint32_t *pixels = out.contents; const uint32_t *pixels = out.contents;
for (int row = 0; row < 8; row++) { for (int row = 0; row < 8; row++) {
EXPECT("stream 0 readback", row == 2 || row >= 6 ? 99 : 11, pixels[row * 4]); EXPECT("stream 0 readback", row == 2 || row >= 6 ? 99 : 11, pixels[row * 4]);
+1
View File
@@ -361,6 +361,7 @@ One line per symptom in the [subsystem references](../reference/): what you see
- `Vertex attribute m_36(5) of type uint4 cannot be read using MTLAttributeFormatHalf2` (draws dropped: black silhouettes); UVs collapse onto one texel (flat tan characters); vertices far out of view (negative 16-bit values widened with zeros). → [CPU conversion of vertex attributes the input type cannot take](../reference/rendering.md#cpu-conversion-of-vertex-attributes-the-input-type-cannot-take) - `Vertex attribute m_36(5) of type uint4 cannot be read using MTLAttributeFormatHalf2` (draws dropped: black silhouettes); UVs collapse onto one texel (flat tan characters); vertices far out of view (negative 16-bit values widened with zeros). → [CPU conversion of vertex attributes the input type cannot take](../reference/rendering.md#cpu-conversion-of-vertex-attributes-the-input-type-cannot-take)
- `vertex attribute format … not mapped` (printed once). → [Vertex formats Metal lacks](../reference/rendering.md#vertex-formats-metal-lacks) - `vertex attribute format … not mapped` (printed once). → [Vertex formats Metal lacks](../reference/rendering.md#vertex-formats-metal-lacks)
- the assertion `buffer index (31) must be < 31`. → [Vertex streams 14 and 15 at Metal slots 14 and 15](../reference/rendering.md#vertex-streams-14-and-15-at-metal-slots-14-and-15) - the assertion `buffer index (31) must be < 31`. → [Vertex streams 14 and 15 at Metal slots 14 and 15](../reference/rendering.md#vertex-streams-14-and-15-at-metal-slots-14-and-15)
- Scaleform fills come out as solid white or light grey sheets over the whole screen, hard-edged triangles and bands, and the menu text is missing (a language list or a title screen that is all white behind its button prompt). → [Vertex streams at addresses that are not 4-byte aligned](../reference/rendering.md#vertex-streams-at-addresses-that-are-not-4-byte-aligned)
- scenes and characters black, only foliage, light shafts and the HUD drawn; pass dumps full of `Z … draw skipped: no program bound`. → [Programs bound per stage](../reference/rendering.md#programs-bound-per-stage) - scenes and characters black, only foliage, light shafts and the HUD drawn; pass dumps full of `Z … draw skipped: no program bound`. → [Programs bound per stage](../reference/rendering.md#programs-bound-per-stage)
- `[RED] failed to create render pipeline (vertex 790573a52b9c5053_b fragment f39fb42850f50ecd_b): Fragment input(s) user(locn0),user(locn1) … not written by vertex shader`, once on entering the first investigation; `texture type mismatch: shader declares a cube, texture is 2D` (a pixel shader run with another pair's bindings). → [Composite program taken again after a replay state restore](../reference/rendering.md#composite-program-taken-again-after-a-replay-state-restore) - `[RED] failed to create render pipeline (vertex 790573a52b9c5053_b fragment f39fb42850f50ecd_b): Fragment input(s) user(locn0),user(locn1) … not written by vertex shader`, once on entering the first investigation; `texture type mismatch: shader declares a cube, texture is 2D` (a pixel shader run with another pair's bindings). → [Composite program taken again after a replay state restore](../reference/rendering.md#composite-program-taken-again-after-a-replay-state-restore)
- `no program bound before the draw`, `program has no vertex shader` (a draw after a dispatch is dropped); `the dispatched program has no compute shader` (a dispatch after a graphics bind). → [Compute programs bound apart from graphics programs](../reference/rendering.md#compute-programs-bound-apart-from-graphics-programs) - `no program bound before the draw`, `program has no vertex shader` (a draw after a dispatch is dropped); `the dispatched program has no compute shader` (a dispatch after a graphics bind). → [Compute programs bound apart from graphics programs](../reference/rendering.md#compute-programs-bound-apart-from-graphics-programs)
+7
View File
@@ -445,6 +445,13 @@ The replay on Metal (`dm/gfx/nvn/nvn_mtl.m`) and the Vulkan and OpenGL layers. E
- **Fix:** translated shaders use only `buffer(0)`, so streams 14 and 15 now go to slots 14 and 15 (`VB_SLOT`). - **Fix:** translated shaders use only `buffer(0)`, so streams 14 and 15 now go to slots 14 and 15 (`VB_SLOT`).
- **Found on:** [13 Sentinels: Aegis Rim](../../dm/games/01003FC01670C000/README.md) (a minute and a half into the prologue). - **Found on:** [13 Sentinels: Aegis Rim](../../dm/games/01003FC01670C000/README.md) (a minute and a half into the prologue).
### Vertex streams at addresses that are not 4-byte aligned
- **Symptom:** Scaleform fills come out as solid white or light grey sheets over the whole screen, hard-edged triangles and bands, and the menu text is missing (a language list or a title screen that is all white behind its button prompt).
- **Cause:** Metal reads a vertex buffer only from a 4-byte-aligned address: bound at an address 2 mod 4, every vertex was read 2 bytes early. NVN takes any address, and Scaleform's vertex streams (16-byte vertices: two RGBA8 attributes and an RG32F position) sit at addresses such as `…6d6a`. The positions became garbage spanning the screen, and the shapes, white with alpha 0.2–0.7 by their colour transforms, overlapped many times.
- **Fix:** `bind_vbufs` copies a stream whose address is not 4-byte aligned into the submission's arena (`vbuf_unaligned`; one copy per address and size until guest writes or pool mappings change, up to 64 MB) and binds the copy. CPU copies require a host-readable, non-sparse range without pending GPU writers in this or an earlier submission; otherwise `vbuf_prepare` encodes an ordered GPU copy before the render encoder, including feedback and geometry captures. Submissions and pool mappings invalidate copies; buffer and shader writes invalidate the copies they can overlap, so unrelated uniform updates keep their vertex copies. The log's `vertex bindings` line counts the copies; `t_vbuf` checks byte-aligned readbacks, reuse, queued writes, writes between draws and pool replacement. The index buffers at addresses 2 mod 4 (16-bit indices) needed nothing.
- **Found on:** Batman: Arkham Knight (`0100ACD0163D0000`; its profile and note are in a PR of their own) (Scaleform GFx; 2–13 copies of 3–18 MB a frame in the menus and at the title, which the draws' vertex ranges could narrow). With the copy the language menu and the title screen draw right.
### Programs bound per stage ### Programs bound per stage
- **Symptom:** scenes and characters black, only foliage, light shafts and the HUD drawn; pass dumps full of `Z … draw skipped: no program bound`. - **Symptom:** scenes and characters black, only foliage, light shafts and the HUD drawn; pass dumps full of `Z … draw skipped: no program bound`.