Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion dm/gfx/nvn/nvn_mtl.m
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,27 @@ static void ws_report(uint64_t frame, uint64_t frames) {
#include "nvn_rec.h" // deferred encoding (DM_NVN_ENC_THREAD)
static NSHashTable *g_res_objects;
static _Atomic unsigned g_image_heap_n, g_big_n; // g_big_n: large pools in chunks, whose buffers overlap (BIG_MIN)
static inline MTL4VisibilityOptions resource_vis(void) { return MTL4VisibilityOptionDevice | (g_image_heap_n || g_big_n ? MTL4VisibilityOptionResourceAlias : 0); }
// Image heaps (a resolution family's sizes sharing one storage) take the alias flush only at the first barrier of each submission (g_alias_heap_pend, set by mtl_sub_begin)
// and after an encoder that may have written memory (g_alias_pend: a compute or copy encoder, or a draw pass with shader writes), not on every barrier: a family changes the
// size it draws at between frames (dynamic resolution), not inside one. With the flush on every barrier a Metal System Trace of Ocarina of Time on an M1 (its dynamic-resolution
// targets are a family from the first frame) showed the GPU's stages never overlapping: vertex work started under a running fragment phase in 0.9 % of kicks, and the GPU was
// idle a quarter of each frame; flushed this way, 32.8 %, and frames take 1.3 ms less. DM_NVN_VIS_HEAPFRAME=0 flushes on every barrier again (ab for the DM_AB phases)
// This skips flushes Metal asks for: a draw pass's attachment stores are writes too, and the host places other texture objects over the same bytes. On an M3 Max (macOS 27.2)
// that left Ocarina of Time's Kokiri Forest bushes and ground ivy out of half to two thirds of frames (#2525); flagging the flush after every draw pass fixes it there, and on an
// M1 that costs as much as no rule at all (36.4-36.8 ms against 33.3-33.5 ms a frame, the foliage steady in all three, screenshot diffs between frames 5 apart 2-3 %). So the rule
// is on by default only on Macs without mesh-shading hardware (no MTLGPUFamilyApple9: M1, M2), where no missing draws were seen; elsewhere every barrier flushes as before
static int sw_mode(const char *name, int dflt); static inline int sw_on(int m);
static int vis_heapframe(void) { static int m = -1; if (m < 0 && g_dev) { int old_gpu = ![g_dev supportsFamily:MTLGPUFamilyApple9]; m = sw_mode("DM_NVN_VIS_HEAPFRAME", old_gpu);
MLOG("DM_NVN_VIS_HEAPFRAME: the image-heap alias flush %s (%s)", m ? "once a submission and after memory writes" : "on every barrier", getenv("DM_NVN_VIS_HEAPFRAME") ? "DM_NVN_VIS_HEAPFRAME" : old_gpu ? "default on M1 / M2" : "default with MTLGPUFamilyApple9"); }
return m > 0 && sw_on(m); }
static _Atomic int g_alias_pend = 1, g_alias_heap_pend = 1;
static inline MTL4VisibilityOptions resource_vis(void) {
if (g_image_heap_n && vis_heapframe()) {
int need = atomic_exchange_explicit(&g_alias_heap_pend, 0, memory_order_relaxed) | atomic_exchange_explicit(&g_alias_pend, 0, memory_order_relaxed);
return MTL4VisibilityOptionDevice | (need ? MTL4VisibilityOptionResourceAlias : 0);
}
return MTL4VisibilityOptionDevice | (g_image_heap_n || g_big_n ? MTL4VisibilityOptionResourceAlias : 0);
}
// Residency churn by call site (the power line every 300 frames, mtl_res_stats): each commit with changes is a kernel call whose cost grows with what was added or removed
enum { RS_SITES = 16 }; static struct { int line; uint64_t n, bytes; } g_rs_site[RS_SITES]; static uint64_t g_rs_rm, g_rs_commits, g_rs_commit_ns;
#define res_add(r) res_add_at((r), __LINE__)
Expand Down Expand Up @@ -1479,6 +1499,7 @@ void mtl_prepare(void) {
static void gmem_commit_patch(void); // the raw pool table words of the arguments encoded so far follow the pools (gmem_slot_note)
void mtl_sub_begin(void) {
mtl_init(); g_sub_serial++; g_seg = 0;
atomic_store_explicit(&g_alias_heap_pend, 1, memory_order_relaxed); // the submission's first barrier carries the image-heap alias flush (resource_vis)
if (g_pkg_kept) { g_pkg_kept = 0; nvn_dirty_touch(); } seg_clk_put(g_sclk); g_sclk = NULL;
sub_cut_begin();
defer_drain();
Expand Down Expand Up @@ -1563,6 +1584,7 @@ static inline void m4_end(id e) {
MTL4VisibilityOptions vis = e == g_blit && g_blit_alias ? blit_vis() : end_alias && sw_on(end_alias) ? resource_vis() : MTL4VisibilityOptionDevice; if (e == g_blit) g_blit_alias = 0;
if ([e conformsToProtocol:@protocol(MTL4CommandEncoder)]) [(id<MTL4CommandEncoder>)e barrierAfterStages:MTLStageAll beforeQueueStages:MTLStageAll visibilityOptions:vis];
[e endEncoding]; g_last_end_relaxed = 0; cm_enc_mark(-1);
if (![e conformsToProtocol:@protocol(MTL4RenderCommandEncoder)] || g_pass_swrite) atomic_store_explicit(&g_alias_pend, 1, memory_order_relaxed); // a write the alias flush may be needed for (resource_vis)
}
// DM_NVN_BARRIER=game (default the profile's draw_barriers, full unless it says game): draw passes are ordered by the game's own barriers. A draw pass ends with a barrier from its vertex and fragment stages to the fragment, compute and copy stages of
// later work, so the next pass's vertex work can overlap this pass's fragments (with the full barriers every pass waited for the previous one to finish completely: about 350 passes a frame in Breath of
Expand Down Expand Up @@ -1664,6 +1686,7 @@ static inline void enc_r_end_pass(id e) {
if (!barrier_game()) { m4_end(e); return; }
gp_end(e); [(id<MTL4CommandEncoder>)e barrierAfterStages:MTLStageVertex | MTLStageObject | MTLStageMesh | MTLStageFragment beforeQueueStages:MTLStageFragment | MTLStageDispatch | MTLStageBlit visibilityOptions:MTL4VisibilityOptionDevice];
[e endEncoding]; g_last_end_relaxed = 1;
if (g_pass_swrite) atomic_store_explicit(&g_alias_pend, 1, memory_order_relaxed); // the pass's shaders wrote memory (resource_vis)
}
static inline id<MTL4RenderCommandEncoder> enc_r_begin(MTL4RenderPassDescriptor *d) { g_cm_enc++; cm_enc_mark(2); id<MTL4RenderCommandEncoder> e = [g_cb renderCommandEncoderWithDescriptor:d]; cm_enc_mark(0); [e barrierAfterQueueStages:MTLStageAll beforeStages:MTLStageAll visibilityOptions:resource_vis()]; [e setArgumentTable:g_vt atStages:MTLRenderStageVertex]; [e setArgumentTable:g_ft atStages:MTLRenderStageFragment]; gp_begin(e, 'R', d); return e; }
static inline void enc_r_end(id e) { m4_end(e); }
Expand Down
1 change: 1 addition & 0 deletions docs/env_switches.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@ DM_NVN_VSDUMP path Output file for post-vertex captures; used with DM_NVN_VSDUMP
DM_NVN_VSDUMP_FROM diagnostic First presented frame eligible for a post-vertex capture (default 0)
DM_NVN_VSDUMP_KEY diagnostic Vertex key to capture, or `*` for all eligible vertex shaders; storage-writing shaders are excluded
DM_NVN_VSDUMP_MAX diagnostic Maximum post-vertex captures (default 1, or 256 for the wildcard key)
DM_NVN_VIS_HEAPFRAME switch `0` gives image heaps the alias flush on every barrier; `1` only at each submission's start and after memory writes (it skips flushes Metal asks for after attachment stores); the default is 1 on Macs without MTLGPUFamilyApple9 (M1, M2: Ocarina of Time about -3 ms a frame, no missing draws seen) and 0 elsewhere (an M3 Max lost Ocarina of Time's foliage); ab for the DM_AB phases
DM_NVN_WAITS diagnostic `=1` turns on the wait ledger: every place a host or game thread waits on the CPU for the GPU or the encode thread, counted per frame
DM_NVN_XV switch `0` turns off the cross-submission attachment validity ledger, which skips write-backs of colour attachments cleared before their next use (on by default, only with `DM_NVN_TBDR` on)
DM_NVN_ZBLOCK diagnostic `=1` logs (`[zb]`) the all-zero blocks per level of each ASTC texture uploaded on the CPU (magenta when drawn); `=<folder>` also writes the raw storage of the first one per format and size
Expand Down
Loading