Skip to content
Closed
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
1 change: 1 addition & 0 deletions dm/games/0400486024DEC000/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ From an empty save: at about 45 s `A` (title), `A` (New Game), then the movie. F
## Found on this game

- [The 60 Hz link at 40 fps in fullscreen on a ProMotion screen](../../../docs/reference/frame-rate.md#the-60-hz-link-at-40-fps-in-fullscreen-on-a-promotion-screen): 40 fps in fullscreen on an M5 Pro MacBook Pro with the GPU at 10 ms of the 16.7, every present one 120 Hz refresh late and every other display-link tick skipped; the screen's fixed 60 Hz mode held 60.0 fps there and fullscreen now switches to it, with the 120 Hz link (59 fps) for screens with no such mode.
- [A draw that samples the depth it writes](../../../docs/reference/rendering.md#a-draw-that-samples-the-depth-it-writes): resolved, black 16×16 blocks flickering where the snow meets rock and walls (the snow draws `55845639d605e028` and `ee567dea65288f58` sample the depth they write).
- [The shader package and the shader map come from different splits](../../../docs/reference/packing.md#the-shader-package-and-the-shader-map-come-from-different-splits): resolved, a black screen with sound in a build through NX2Apple ([issue #1841](https://github.com/seedds/nxnative/issues/1841)); the present pass was translated from a terrain shader's record. Records now carry their keys, and the shader build finds them by key.
- [Native mesh pipelines compiled at their first draw](../../../docs/reference/shaders.md#native-mesh-pipelines-compiled-at-their-first-draw): resolved. The foliage, shadows and enemies drawn with task and mesh shaders stalled at their first draw on a cold Metal cache (59.5 s of builds at the Kokiri Forest entry on an M5 Max, one frame of 42.9 s), as nothing recorded or kept their pipelines; `pso_manifest.tsv` now carries 123 `M` rows (Kokiri Forest, Hyrule Field, Castle Town, Hyrule Castle), and with the prepare step they are built before the first frame. Pipelines outside the list still stall once, then go into `pso_user.tsv`.
- [One Pro Controller read as two players](../../../docs/reference/services.md#one-pro-controller-read-as-two-players): resolved, no music in Kokiri Forest gameplay and its cutscenes; the controller applet opened over and over and the game kept its music suspended while it had focus.
Expand Down
44 changes: 44 additions & 0 deletions dm/gfx/nvn/nvn_mtl.m
Original file line number Diff line number Diff line change
Expand Up @@ -4972,6 +4972,13 @@ static void rt_mirror_flush(void) {
}
if (g_blit) { enc_b_end(g_blit); g_blit = nil; }
}
// A draw that writes the depth attachment while a texture slot or bindless handle of its fragment shader names that depth reads what Metal leaves undefined:
// on Apple's GPUs whole 16x16 tiles of another state come back, where NVN reads the depth from before the draw. Ocarina of Time's snow (55845639d605e028, ee567dea65288f58 and
// others: depth LEQUAL with writes, a depth fade sampled through a texture slot) drew flickering black blocks where it met rock and walls. Such a draw gets a pass of its own whose depth
// attachment is a copy of the depth (g_dswap): it writes the copy and reads the original, which nothing changes until the pass ends and the copy goes back. DM_NVN_DEPTH_SWAP=0: off
static int g_dswap_want, g_dswap_on; static __strong id<MTLTexture> g_dswap_src, g_dswap_tex; static uint64_t g_dswap_n;
static int dswap_enabled(void) { static int v = -1; if (v < 0) { const char *e = getenv("DM_NVN_DEPTH_SWAP"); v = !(e && !strcmp(e, "0")); if (!v) MLOG("depth read while written: no copy of the attachment (DM_NVN_DEPTH_SWAP=0)"); } return v; }
static void dswap_back(void);
static void rp_end_at(const char *fn, int line) { if (g_enc) { AP_B; geo_mesh_flush_pass(); PD("E %llu pass ends in %s:%d", g_stat_pass, fn, line);
if (enc_label_on()) { static uint64_t lf = ~0ull, base; uint64_t f = atomic_load(&g_cur_frame); if (f != lf) { lf = f; base = g_stat_pass - 1; } // Intra-frame serial: the same pass has the same name every frame, so the table can aggregate by name
[(id)g_enc setLabel:[NSString stringWithFormat:@"p%02llu %dx%d %s d%d", (unsigned long long)(g_stat_pass - base), g_el_w, g_el_h, g_el_key[0] ? g_el_key : "-", g_el_draws]]; }
Expand All @@ -4980,6 +4987,7 @@ static void rt_mirror_flush(void) {
#endif
sd_finish(); enc_r_end_pass(g_enc); g_gp_bw_l0 = g_bw_load; g_gp_bw_s0 = g_bw_store; // the profile's per-pass ledger counts from the previous pass end, profiled or not
zskip_commit_clears(); g_enc = nil; g_enc_geo_nocolor = 0; g_rt_merged = 0; g_sm_pass = 0; g_rt_range_active = g_rt_range_pending = 0; pg_end_render(); ts_after(); ms_after(); if (g_lp.count) lp_finish(); fz_end();
if (g_dswap_on) dswap_back(); // the depth this pass wrote into the copy goes back to the attachment
if (g_nubd && g_nubd >= ubd_lazy()) ubd_flush(); // the inline updates deferred in this pass, in order (see mtl_sub_update); across passes they wait for the next copy or dispatch (blit_raw)
if (g_nbcrt) bcrt_flush(); // blocks drawn into shadows go back into their compressed textures
if (g_nrtm) rt_mirror_flush();
Expand Down Expand Up @@ -5121,6 +5129,12 @@ static void attach(MTLRenderPassAttachmentDescriptor *a, const tex_t *t, const v
// (Mac character select screen: the color-clear pass drags along a 400x225 depth load and store)
static id<MTL4RenderCommandEncoder> rp_begin_ex(int clear_only);
static NSUInteger g_rp_aw; static int g_rp_whole;
static void dswap_back(void) {
id<MTLTexture> src = g_dswap_src, cp = g_dswap_tex; g_dswap_on = 0; g_dswap_src = nil;
if (!src || !cp) return;
id<MTL4ComputeCommandEncoder> be = blit(); enc_b_copy_texture(be, cp, 0, 0, MTLOriginMake(0, 0, 0), MTLSizeMake(src.width, src.height, 1), src, 0, 0, MTLOriginMake(0, 0, 0));
g_game_barrier = 1; // the next pass waits for it too
}
static id<MTL4RenderCommandEncoder> rp_begin(void) { if (g_enc && g_rp_aw) rp_end(); g_rp_whole = 1; id<MTL4RenderCommandEncoder> e = rp_begin_ex(0); g_rp_whole = 0; return e; } // the callers draw anywhere in the target (see g_rp_aw)
static id<MTL4RenderCommandEncoder> rp_begin_ex_(int clear_only);
static id<MTL4RenderCommandEncoder> rp_begin_ex(int clear_only) { AP_B; id<MTL4RenderCommandEncoder> e = rp_begin_ex_(clear_only); AP_E(1); return e; }
Expand All @@ -5136,6 +5150,7 @@ static void attach(MTLRenderPassAttachmentDescriptor *a, const tex_t *t, const v
if (rt_range_on()) { reg0 = g_reg_gen; ser0 = g_ser_gen; }
if (rt_range_on()) dirty0=nvn_dirty_now();
if (g_blit) { enc_b_end(g_blit); g_blit = nil; }
int dswap = g_dswap_want; g_dswap_want = 0; // asked by the draw opening this pass (fb_prepare), for this pass only
MTL4RenderPassDescriptor *pd = [MTL4RenderPassDescriptor new];
int any = 0;
for (int i = 0; i < R.rt.n; i++) {
Expand Down Expand Up @@ -5179,6 +5194,22 @@ static void attach(MTLRenderPassAttachmentDescriptor *a, const tex_t *t, const v
if (R.rt.depth && clear_only != 2 && !(clear_only == 1 && !R.clear_d && !R.clear_s)) {
attach(pd.depthAttachment, R.rt.depth, &R.rt.dview, R.rt.has_dview);
id<MTLTexture> t = pd.depthAttachment.texture;
if (dswap) {
if (t && mtl_fmt_depth(t.pixelFormat) && !R.clear_d && !t.parentTexture && t.textureType == MTLTextureType2D && t.sampleCount == 1 && !pd.depthAttachment.level && !pd.depthAttachment.slice) {
if (!g_dswap_tex || g_dswap_tex.pixelFormat != t.pixelFormat || g_dswap_tex.width != t.width || g_dswap_tex.height != t.height) {
if (g_dswap_tex) defer_obj_release(g_dswap_tex);
MTLTextureDescriptor *d = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:t.pixelFormat width:t.width height:t.height mipmapped:NO];
d.usage = MTLTextureUsageRenderTarget | MTLTextureUsageShaderRead; d.storageMode = MTLStorageModePrivate;
g_dswap_tex = [g_dev newTextureWithDescriptor:d]; if (g_dswap_tex) res_add(g_dswap_tex); }
if (g_dswap_tex) {
id<MTL4ComputeCommandEncoder> be = blit(); enc_b_copy_texture(be, t, 0, 0, MTLOriginMake(0, 0, 0), MTLSizeMake(t.width, t.height, 1), g_dswap_tex, 0, 0, MTLOriginMake(0, 0, 0));
enc_b_end(g_blit); g_blit = nil; g_game_barrier = 1; // the pass waits for the copy (enc_r_begin_pass)
g_dswap_src = t; g_dswap_on = 1; t = g_dswap_tex; pd.depthAttachment.texture = t;
if (!g_dswap_n++) MLOG("depth read while written: a draw whose texture slot or handle names the depth it writes draws into a copy of it (first at pass %llu; DM_NVN_DEPTH_SWAP)", g_stat_pass + 1);
PD("S %llu depth attachment ser=%llu swapped for a copy: a draw reads the depth it writes", g_stat_pass + 1, (unsigned long long)R.rt.depth->serial);
}
} else { static int once; if (!once++) MLOG("depth read while written: the attachment is not a whole single-level 2D texture, or the pass clears it; left as is"); }
}
MTLPixelFormat pf = t.pixelFormat;
// Combined storage stays on both attachments: single-plane descriptors work
// on macOS but iOS rejects Invalid / Depth32Float_Stencil8 at pipeline creation.
Expand Down Expand Up @@ -12261,6 +12292,17 @@ static int fb_texu_reads(NvShader *fs, uint64_t ser) {
if (s == ser) return 1; } }
return 0;
}
// Whether a sampled texture or read-only image slot of the fragment stage names this texture's storage (the depth attachment): read as fb_prepare reads the slots
static int fb_slot_reads(NvShader *fs, const tex_t *att) {
for (int i = 0; i < fs->rf.nb; i++) { const nvn_rbind *b = &fs->rf.b[i];
int image = b->kind == NVN_RB_IMG && b->reg < 32;
if (!image && (b->kind != NVN_RB_TEX || b->reg >= NTEX)) continue;
uint64_t handle = image ? S.img[1][b->reg] : tex_slot_h(S.tex[1], S.img[1], b->reg);
int tid = (int)(handle & 0xfffff); if (!tid) continue;
os_unfair_lock_lock(&g_rmu); tex_t t = tid < g_ntreg && g_treg[tid].ok ? g_treg[tid].t : (tex_t){0}; os_unfair_lock_unlock(&g_rmu);
if (t.serial && tex_storage_overlap(&t, att)) return 1; }
return 0;
}
// Called before the draw opens its render pass: if a snapshot is needed, end the current render pass, copy the snapshot (or reuse a valid one) and register the substitution (used by this draw's argument encoding)
static void fb_prepare(NvProg *pr) {
g_draw_seq++; g_fb_n = 0;
Expand All @@ -12274,6 +12316,8 @@ static void fb_prepare(NvProg *pr) {
int dsw = R.rt.depth && ((S.ds.dt && S.ds.dw) || (S.ds.st && (S.st_wmask[0] || S.st_wmask[1]) && (S.ds.fail_f != NVN_STENCIL_OP_KEEP || S.ds.zfail_f != NVN_STENCIL_OP_KEEP || S.ds.zpass_f != NVN_STENCIL_OP_KEEP
|| S.ds.fail_b != NVN_STENCIL_OP_KEEP || S.ds.zfail_b != NVN_STENCIL_OP_KEEP || S.ds.zpass_b != NVN_STENCIL_OP_KEEP)));
if (g_enc && dsw && g_fb_dread) { g_fb_split++; g_fb_split_pool += g_fb_dread_pool; g_fb_dread_pool = 0; rp_end(); fb_pass_sync(); } // the same for its depth and stencil (see rtc_on)
if (dsw && R.rt.depth && S.ds.dt && S.ds.dw && !g_dswap_on && dswap_enabled() && (fb_slot_reads(fs, R.rt.depth) || (fs->rf.bindless && fb_texu_reads(fs, R.rt.depth->serial)))) { // a read the slots or handles prove: a pool or dynamic read may name any texture (pool_ds_read), and a copy for each would cost a pass and two copies a draw
if (g_enc) { rp_end(); fb_pass_sync(); } g_dswap_want = 1; } // it reads the depth it writes: its pass draws into a copy (g_dswap)
if (R.rt.depth && !g_fb_dread && ((fs->rf.pool && pool_ds_read(R.rt.depth) && (g_fb_dread_pool = 1)) || fs->rf.dyn || (fs->rf.bindless && fb_texu_reads(fs, R.rt.depth->serial)))) g_fb_dread = 1;
uint64_t tile[4]; int ntile = fb_tile_plan(pr, wm, tile);
for (int i = 0; i < fs->rf.nb && g_fb_n < 4; i++) {
Expand Down
6 changes: 6 additions & 0 deletions dm/gfx/tests/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,8 @@ t_geometry: t_geometry.m $(MTL_T_DEP)
check_t_geometry: RUN_ENV = MTL_DEBUG_LAYER=1
t_feedback: t_feedback.m $(MTL_T_DEP)
clang $(CFLAGS) -fobjc-arc -o $@ t_feedback.m $(MTL_T_SRC) $(FW)
t_depth_swap: t_depth_swap.m $(MTL_T_DEP)
clang $(CFLAGS) -fobjc-arc -o $@ t_depth_swap.m $(MTL_T_SRC) $(FW)
t_rast_discard: t_rast_discard.m $(MTL_T_DEP)
clang $(CFLAGS) -fobjc-arc -o $@ t_rast_discard.m $(MTL_T_SRC) $(FW)
t_queue_origin: t_queue_origin.m $(MTL_T_DEP)
Expand Down Expand Up @@ -313,6 +315,10 @@ t_geometry_mesh: t_geometry_mesh.m t_geometry.m ../tools/dmsh_mesh.py $(MTL_T_DE
.PHONY: check_t_geometry_mesh
check_t_geometry_mesh: t_geometry_mesh
@set -e; work=$$(mktemp -d); trap 'rm -rf "$$work"' EXIT; python3 test_dmsh_geometry.py --fixture "$$work"; MTL_DEBUG_LAYER=1 ./t_geometry_mesh "$$work"; DM_NVN_GEO_MESH_RUN=0 MTL_DEBUG_LAYER=1 ./t_geometry_mesh "$$work"; DM_NVN_ENC_THREAD=1 MTL_DEBUG_LAYER=1 ./t_geometry_mesh "$$work"
.PHONY: check_t_depth_swap
check_t_depth_swap: t_depth_swap
MTL_DEBUG_LAYER=1 ./t_depth_swap
DM_NVN_DEPTH_SWAP=0 MTL_DEBUG_LAYER=1 ./t_depth_swap
.PHONY: check_t_feedback
check_t_feedback: t_feedback
@set -e; work=$$(mktemp -d); trap 'rm -rf "$$work"' EXIT; python3 test_dmsh_fetch.py --fixture "$$work"; MTL_DEBUG_LAYER=1 ./t_feedback "$$work"
Expand Down
94 changes: 94 additions & 0 deletions dm/gfx/tests/t_depth_swap.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// A draw that writes the depth attachment while a fragment texture slot names that depth draws into a copy (g_dswap in nvn_mtl.m): fb_prepare asks for it, the pass the draw
// opens attaches the copy, and the pass end copies it back, so the depth the draw wrote reaches the attachment. A draw that writes depth with no read of it, and one that reads it
// with no depth write, keep the attachment. With DM_NVN_DEPTH_SWAP=0 no draw takes the copy, and the depth is written all the same.
#include "../nvn/nvn_mtl.m"
void *front_metal_layer(void) { return NULL; }
module_t *module_of(const void *p) { (void)p; return NULL; }
module_t *g_mod; int g_nmod;
void svc_thread_dump(void) {}
uint64_t now_ns(void) { return bp_now(); }
static int checks, failed;
#define CHECK(c, label) do { checks++; if (!(c)) { failed++; fprintf(stderr, "FAIL: %s\n", label); } } while (0)

static id<MTLTexture> make(tex_t *t, uint64_t serial, int format) {
*t = (tex_t){.magic=MAGIC('T','E','X',0), .serial=serial};
t->d = (nvn_texdesc){.target=NVN_TEXTURE_TARGET_2D, .format=format, .width=4, .height=4, .depth=1, .levels=1, .samples=1};
MTLTextureDescriptor *d = tex_desc(t, 0);
id<MTLTexture> texture = [g_dev newTextureWithDescriptor:d];
CHECK(texture != nil, "texture created");
res_add(texture); mtl_tex_init(t); serial_put(serial, texture);
return texture;
}
// The depth of every texel, read back after the submission
static int depth_is(id<MTLTexture> depth, float want, const char *label) {
id<MTLBuffer> b = [g_dev newBufferWithLength:1024 options:MTLResourceStorageModeShared]; res_add(b);
enc_b_copy_texture_buffer(blit(), depth, 0, 0, MTLOriginMake(0, 0, 0), MTLSizeMake(4, 4, 1), b, 0, 256, 1024);
CHECK(mtl_wait(mtl_sub_commit(), 5000000000ull) && !mtl_last_commit_error(), "GPU submission completed without error");
int ok = 1; for (int y = 0; y < 4; y++) for (int x = 0; x < 4; x++) ok &= ((const float *)((const uint8_t *)b.contents + y * 256))[x] == want;
CHECK(ok, label);
mtl_sub_begin();
return ok;
}

int main(void) { @autoreleasepool {
setenv("DM_NVN_TBDR", "0", 1); setenv("DM_NVN_GPU_TS", "segment", 1);
const char *e = getenv("DM_NVN_DEPTH_SWAP"); int swap = !(e && !strcmp(e, "0"));
mtl_init(); NSError *error = nil;
id<MTLLibrary> lib = [g_dev newLibraryWithSource:@"#include <metal_stdlib>\nusing namespace metal;\n"
"struct V { float4 p[[position]]; };\n"
"vertex V v(uint i[[vertex_id]]) { V o; o.p = float4(i == 1 ? 3.0 : -1.0, i == 2 ? 3.0 : -1.0, 0.25, 1); return o; }\n"
"fragment float4 f() { return float4(0, 1, 0, 1); }\n" options:nil error:&error];
CHECK(lib != nil, "synthetic shader compiles");
if (!lib) { fprintf(stderr, "%s\n", error.localizedDescription.UTF8String); return 1; }
MTLRenderPipelineDescriptor *pdesc = [MTLRenderPipelineDescriptor new];
pdesc.vertexFunction = [lib newFunctionWithName:@"v"]; pdesc.fragmentFunction = [lib newFunctionWithName:@"f"];
pdesc.colorAttachments[0].pixelFormat = MTLPixelFormatRGBA8Unorm; pdesc.depthAttachmentPixelFormat = MTLPixelFormatDepth32Float;
id<MTLRenderPipelineState> pipeline = [g_dev newRenderPipelineStateWithDescriptor:pdesc error:&error];
CHECK(pipeline != nil, "depth-writing pipeline compiles");
if (!pipeline) { fprintf(stderr, "%s\n", error.localizedDescription.UTF8String); return 1; }
MTLDepthStencilDescriptor *dd = [MTLDepthStencilDescriptor new]; dd.depthCompareFunction = MTLCompareFunctionLessEqual; dd.depthWriteEnabled = YES;
id<MTLDepthStencilState> lequal = [g_dev newDepthStencilStateWithDescriptor:dd];

NvProg *pr = [NvProg new]; NvShader *fs = [NvShader new]; pr->sh[1] = fs;
fs->stage = 1; strcpy(fs->key, "0123456789abcdef");
fs->rf.nb = 1; fs->rf.b[0] = (nvn_rbind){.kind=NVN_RB_TEX, .reg=0, .aid=0, .dtt=MTLTextureType2D};
tex_t color, depth; make(&color, 99901, NVN_FORMAT_RGBA8); id<MTLTexture> dtex = make(&depth, 99902, NVN_FORMAT_DEPTH32F);
const int tid = 700; mtl_reg_tex(tid, &depth, NULL);

state_reset(); memset(&R, 0, sizeof R); mtl_sub_begin();
R.has_rt = 1; R.rt.n = 1; R.rt.color[0] = &color; R.rt.depth = &depth; S.chmask.mask = 0xffffffffu;
R.clear_c[0] = 1; R.cc[0] = MTLClearColorMake(0, 0, 0, 1); R.clear_d = 1; R.cd = 1.0;
CHECK(rp_begin() != nil, "the clearing pass opens"); rp_end();
depth_is(dtex, 1.0f, "the depth starts cleared to 1");

// Written and read through a texture slot: a pass of its own on a copy, and the depth comes back at its end
S.ds.dt = 1; S.ds.dw = 1; S.ds.df = NVN_DEPTH_FUNC_LEQUAL; S.tex[1][0] = tid;
fb_prepare(pr);
CHECK(g_dswap_want == swap, swap ? "a draw reading the depth it writes asks for the copy" : "DM_NVN_DEPTH_SWAP=0: no copy asked for");
CHECK(rp_begin() != nil, "the draw's pass opens");
CHECK(g_dswap_on == swap && (!swap || (g_dswap_src == dtex && g_dswap_tex && g_dswap_tex != dtex)), swap ? "its pass attaches a copy of the depth" : "its pass attaches the depth itself");
enc_r_pso(g_enc, pipeline); enc_r_dss(g_enc, lequal); enc_r_viewport(g_enc, (MTLViewport){0, 0, 4, 4, 0, 1});
enc_r_draw(g_enc, MTLPrimitiveTypeTriangle, 0, 3);
rp_end();
CHECK(!g_dswap_on && !g_dswap_src, "the pass end hands the copy back");
depth_is(dtex, 0.25f, "the depth the draw wrote reaches the attachment");

// Written with no read of it, and read with no write: the attachment as it is
S.tex[1][0] = 0; fb_prepare(pr);
CHECK(!g_dswap_want, "a draw writing depth it does not read asks for no copy");
S.tex[1][0] = tid; S.ds.dw = 0; fb_prepare(pr);
CHECK(!g_dswap_want, "a draw reading depth it does not write asks for no copy");
CHECK(rp_begin() != nil && !g_dswap_on, "its pass attaches the depth itself");
rp_end();

// A request no pass with a depth attachment takes is not left for a later pass
S.ds.dw = 1; fb_prepare(pr); R.rt.depth = NULL;
CHECK(rp_begin() != nil, "a pass without depth opens"); rp_end();
R.rt.depth = &depth; S.tex[1][0] = 0; S.ds.dw = 0;
CHECK(rp_begin() != nil && !g_dswap_on, "a later pass with the depth does not take the copy asked for before");
rp_end();
CHECK(mtl_wait(mtl_sub_commit(), 5000000000ull) && !mtl_last_commit_error(), "last submission completed without error");
mtl_tex_release(&color); mtl_tex_release(&depth);
printf("depth swap (%s): %d checks, %d failed\n", swap ? "on" : "DM_NVN_DEPTH_SWAP=0", checks, failed);
return failed != 0;
} }
Loading
Loading