diff --git a/dm/gfx/nvn/hle_nvn.c b/dm/gfx/nvn/hle_nvn.c index ffdb23d..c7643f6 100644 --- a/dm/gfx/nvn/hle_nvn.c +++ b/dm/gfx/nvn/hle_nvn.c @@ -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()); } g_presn = 0; } 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); - LOG("[nvn] vertex bindings up to frame %llu: per frame address lookups %.0f / reused %.0f, vertex table writes %.0f / skipped %.0f", frame, - (v[0] - prev[0]) / nf, (v[1] - prev[1]) / nf, (v[2] - prev[2]) / nf, (v[3] - prev[3]) / nf); } + 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[4] - prev[4]) / nf, (v[5] - prev[5]) / nf / 1048576.0); } 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 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, diff --git a/dm/gfx/nvn/nvn_feedback.inc b/dm/gfx/nvn/nvn_feedback.inc index d9fc722..287f1f6 100644 --- a/dm/gfx/nvn/nvn_feedback.inc +++ b/dm/gfx/nvn/nvn_feedback.inc @@ -65,6 +65,7 @@ static void tf_draw(NvProg *pr,const p_draw *dw,const p_draw_idx *di) { id 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, 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; id 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); diff --git a/dm/gfx/nvn/nvn_geometry.inc b/dm/gfx/nvn/nvn_geometry.inc index 51aca82..35d2e0f 100644 --- a/dm/gfx/nvn/nvn_geometry.inc +++ b/dm/gfx/nvn/nvn_geometry.inc @@ -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 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); + vbuf_prepare(); if (g_blit) { enc_b_end(g_blit); g_blit = nil; } MTL4RenderPassDescriptor *rd = [MTL4RenderPassDescriptor new]; rd.renderTargetWidth = rd.renderTargetHeight = rd.defaultRasterSampleCount = 1; id 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); 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); + vbuf_prepare(); if (g_blit) { enc_b_end(g_blit); g_blit = nil; } MTL4RenderPassDescriptor *rd = [MTL4RenderPassDescriptor new]; rd.renderTargetWidth = rd.renderTargetHeight = rd.defaultRasterSampleCount = 1; id re = enc_r_begin(rd); enc_r_pso(re, capture); bind_vbufs(re); diff --git a/dm/gfx/nvn/nvn_mtl.h b/dm/gfx/nvn/nvn_mtl.h index 0756f4c..4a6076a 100644 --- a/dm/gfx/nvn/nvn_mtl.h +++ b/dm/gfx/nvn/nvn_mtl.h @@ -6,7 +6,7 @@ #include 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_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); diff --git a/dm/gfx/nvn/nvn_mtl.m b/dm/gfx/nvn/nvn_mtl.m index 837a68b..5c9b921 100644 --- a/dm/gfx/nvn/nvn_mtl.m +++ b/dm/gfx/nvn/nvn_mtl.m @@ -94,7 +94,7 @@ static id g_present_cb[3]; static id 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 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 g_present_args; static id g_present_uv[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 ================= #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) // 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) @@ -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 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 src = (__bridge id)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 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 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. // Take the pool generation before resolving, so a concurrent pool change forces a fresh lookup next time. static void bind_vbufs(id en) { @@ -4238,6 +4283,7 @@ static void bind_vbufs(id en) { for (int s = 0; s < 16; s++) { uint64_t addr = S.vbuf[s].address, gpu; 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++; } else { 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 // back (the dialog boxes had holes the size of their text) 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 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) { 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_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); + vbuf_prepare(); // an unaligned GPU-written stream needs its ordered copy before the render encoder id en = rp_begin_ex(mode); g_rp_layered = 0; g_sm_plan.on = 0; 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 (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++; } // 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 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_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_vbuf_stats(uint64_t *resolve, uint64_t *reuse, uint64_t *set, uint64_t *skip) { - *resolve = g_vbuf_resolve; *reuse = g_vbuf_reuse; *set = g_vbuf_set; *skip = g_vbuf_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; *copies = g_vbuf_copies; *copy_bytes = g_vbuf_copy_bytes; } 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; } diff --git a/dm/gfx/tests/t_vbuf.m b/dm/gfx/tests/t_vbuf.m index 4c34295..85d8c7d 100644 --- a/dm/gfx/tests/t_vbuf.m +++ b/dm/gfx/tests/t_vbuf.m @@ -1,4 +1,5 @@ // 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" uint64_t g_t0_ns; int dm_guest_as_contains(uintptr_t address) { (void)address; return 0; } // Tests use host-thread stacks. @@ -24,6 +25,9 @@ static id pipeline(void) { return ps; } static id encoder(id ps) { + vbuf_prepare(); + ubd_land_compute(); + if (g_blit) { enc_b_end(g_blit); g_blit = nil; } MTL4RenderPassDescriptor *d = [MTL4RenderPassDescriptor new]; d.renderTargetWidth = d.renderTargetHeight = d.defaultRasterSampleCount = 1; id 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(); memset(mem, 0, size); mem[0] = 11; mem[1] = 22; mem[2] = 33; mem[3] = 99; mtl_pool_add(mem, size); - id 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 out = [g_dev newBufferWithLength:16 * 32 options:MTLResourceStorageModeShared]; res_add(out); state_reset(); // Exercise the compact record through the production dispatcher before the // 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); EXPECT("replacement address bound", newgpu, g_vt_addr[VB_SLOT(0)]); 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; for (int row = 0; row < 8; row++) { EXPECT("stream 0 readback", row == 2 || row >= 6 ? 99 : 11, pixels[row * 4]); diff --git a/docs/agents/symptoms.md b/docs/agents/symptoms.md index cac41b7..a6e5c7b 100644 --- a/docs/agents/symptoms.md +++ b/docs/agents/symptoms.md @@ -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 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) +- 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) - `[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) diff --git a/docs/reference/rendering.md b/docs/reference/rendering.md index d2506ed..97e0a91 100644 --- a/docs/reference/rendering.md +++ b/docs/reference/rendering.md @@ -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`). - **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 - **Symptom:** scenes and characters black, only foliage, light shafts and the HUD drawn; pass dumps full of `Z … draw skipped: no program bound`.