diff --git a/dm/games/0100F2C0115B6000/README.md b/dm/games/0100F2C0115B6000/README.md index 8a8c68e..e259af8 100644 --- a/dm/games/0100F2C0115B6000/README.md +++ b/dm/games/0100F2C0115B6000/README.md @@ -40,6 +40,7 @@ Game-specific findings. The general host changes they led to are listed under [F - **Resolved: draws into a 32×32×32 BC4 3D texture were lost.** agl's BC4 block encoder (`0544282c162305db`, the same shader Mario Wonder ships) encodes it one depth plane per pass through an `RGBA16UI` view. It draws through the compressed-target shadow (Breath of the Wild's), with its output declared `uint` in `shader_facts_manual.tsv`. A raw dump at frame 1500 of the new-game route: all 32 planes hold encoded blocks. - **Resolved: a draw with no render target.** An agl fragment shader that only stores to a storage buffer draws one triangle into a 1×1 viewport with no target (it samples a 64×64 cube map; the game reads the buffer back). With the attachment-less pass, the cave's rock and stone blocks get ambient light they lacked, and Zelda's bloom halo in the descent cutscene is gone. - **How the tile lighting was traced:** the G-buffer class byte selects per-class half-resolution passes through a depth buffer of class values (`gl_FragDepth = mix(0.5, 1, nibble-swapped class / 255)`, then full-screen or per-tile quads with depth `EQUAL`), so `DM_NVN_DEPTHOVR` (depth test forced to `ALWAYS`) with `HILITE` tells "rejected by depth" from "never rasterized". +- **Current: Ultrahand and Recall highlight validation pending.** The [Depth16 load-clear correction](../../../docs/reference/rendering.md#depth16-load-clears-match-fragment-writes), ported from MacNativeGames, addresses its reported tile grid. The depth conversion mismatch is reproduced in synthetic Mac GPU tests; this game's highlights and the iPhone have not been rechecked here. - The game issues a depth clear scissored to 8×8 of a 64×64 target outside a pass, which the host still clears whole (`depth-stencil clear with a scissor or partial stencil mask, cleared whole`). - About one draw per 8-bit index widening, up to a dozen widenings a frame on the title and load screens, no pass split (log line `8-bit index GPU expansion`). - Its 14,766 vertex shaders have no transform feedback. @@ -72,6 +73,7 @@ Game-specific findings. The general host changes they led to are listed under [F General host changes first needed here; each applies to every game: - **Resolved in the host: dirty-page writes missed during upload**, with both unprotect paths covered by regression tests; the glyph-cache symptom remains unconfirmed here: [Dirty-page writes missed during upload](../../../docs/reference/nvn-objects.md#dirty-page-writes-missed-during-upload). +- **Depth16 load-clears match fragment writes:** [Depth16 load-clears match fragment writes](../../../docs/reference/rendering.md#depth16-load-clears-match-fragment-writes). - **Pipeline keys omit absent-target blend and mask state**, and old prewarm rows are normalized and deduplicated: [Pipeline keys omit unused colour state](../../../docs/reference/shaders.md#pipeline-keys-omit-unused-colour-state). - **Bounded upload staging and retirement on upload completion:** [Upload staging grows within one rendering submission](../../../docs/reference/memory.md#upload-staging-grows-within-one-rendering-submission). - **Resolved: registry lock priority inversion** (`g_rmu`): [Registry lock priority inheritance](../../../docs/reference/frame-rate.md#registry-lock-priority-inheritance). Ported from MacNativeGames; checked here through the Metal registry view tests, without a new gameplay contention measurement. diff --git a/dm/gfx/nvn/nvn_depth_clear.h b/dm/gfx/nvn/nvn_depth_clear.h new file mode 100644 index 0000000..a940307 --- /dev/null +++ b/dm/gfx/nvn/nvn_depth_clear.h @@ -0,0 +1,12 @@ +// A Depth16Unorm load-clear value that lands where a fragment writing the same depth lands. Apple GPUs truncate a load-clear but round a fragment's depth: 0.5 clears +// to 32767 and a fragment writes 32768, so an EQUAL test after a clear failed in whole tiles (MacNativeGames 3d90e41: a tile grid in Tears of the Kingdom's Ultrahand +// and Recall highlights). Take the nearest D16 value, then a point inside both its truncation and its rounding bin: dividing by 65535 alone can fall below the integer +// when Metal converts the descriptor's double to float, hence the quarter-unit margin. Other depth formats and the in-pass clears (fragments, clear_ds_tri) need no change +#pragma once +#include + +static inline double nvn_depth16_clear_value(double depth) { + if (!isfinite(depth) || depth <= 0.0 || depth >= 1.0) return depth; + double q = round(depth * 65535.0); + return q >= 65535.0 ? 1.0 : (q + 0.25) / 65535.0; +} diff --git a/dm/gfx/nvn/nvn_mtl.m b/dm/gfx/nvn/nvn_mtl.m index e972109..6674908 100644 --- a/dm/gfx/nvn/nvn_mtl.m +++ b/dm/gfx/nvn/nvn_mtl.m @@ -21,6 +21,7 @@ #include "../tex/nvn_deswizzle_gpu.h" #include "nvn_builtin.h" #include "nvn_dirty.h" +#include "nvn_depth_clear.h" #include "nvn_refl.h" #include "nvn_cover.h" #include "nvn_primitive.h" @@ -2745,8 +2746,8 @@ static id rp_begin_ex_(int clear_only) { if (pd.depthAttachment.texture) { any = 1; pd.depthAttachment.loadAction = R.clear_d ? MTLLoadActionClear : MTLLoadActionLoad; - pd.depthAttachment.clearDepth = R.cd; pd.depthAttachment.storeAction = MTLStoreActionStore; MTLPixelFormat pf = pd.depthAttachment.texture.pixelFormat; + pd.depthAttachment.clearDepth = pf == MTLPixelFormatDepth16Unorm ? nvn_depth16_clear_value(R.cd) : R.cd; pd.depthAttachment.storeAction = MTLStoreActionStore; if (clear_only != 4 && (pf == MTLPixelFormatDepth32Float_Stencil8 || pf == MTLPixelFormatStencil8)) { // 4: depth only pd.stencilAttachment.texture = pd.depthAttachment.texture; pd.stencilAttachment.level = pd.depthAttachment.level; pd.stencilAttachment.slice = pd.depthAttachment.slice; @@ -7922,7 +7923,7 @@ static void clear_tex(const p_clear_tex *c) { MTLRenderPassAttachmentDescriptor *a = depth ? rp.depthAttachment : stencil ? rp.stencilAttachment : rp.colorAttachments[0]; a.texture = t; a.level = lv; a.loadAction = MTLLoadActionClear; a.storeAction = MTLStoreActionStore; if (is3d) a.depthPlane = c->r.zoffset + z; else a.slice = l0 + c->r.zoffset + z; - if (depth) { rp.depthAttachment.clearDepth = c->c.f[0]; + if (depth) { rp.depthAttachment.clearDepth = pf == MTLPixelFormatDepth16Unorm ? nvn_depth16_clear_value(c->c.f[0]) : c->c.f[0]; if (stencil) { rp.stencilAttachment.texture = t; rp.stencilAttachment.level = lv; rp.stencilAttachment.slice = a.slice; rp.stencilAttachment.loadAction = MTLLoadActionClear; rp.stencilAttachment.storeAction = MTLStoreActionStore; rp.stencilAttachment.clearStencil = c->c.u[1]; } } else if (stencil) rp.stencilAttachment.clearStencil = c->c.u[0]; diff --git a/dm/gfx/tests/.gitignore b/dm/gfx/tests/.gitignore index 2589627..eb5721c 100644 --- a/dm/gfx/tests/.gitignore +++ b/dm/gfx/tests/.gitignore @@ -88,3 +88,5 @@ t_pso_archive t_pso_archive.dSYM/ t_pso_warm t_pso_warm.dSYM/ +t_depth16_clear +t_depth16_clear.dSYM/ diff --git a/dm/gfx/tests/Makefile b/dm/gfx/tests/Makefile index 10b36c5..df5ce52 100644 --- a/dm/gfx/tests/Makefile +++ b/dm/gfx/tests/Makefile @@ -140,6 +140,8 @@ t_integer_blend: t_integer_blend.m $(MTL_T_DEP) clang $(CFLAGS) -fobjc-arc -o $@ t_integer_blend.m $(MTL_T_SRC) $(FW) t_primitive_restart: t_primitive_restart.m $(MTL_T_DEP) clang $(CFLAGS) -fobjc-arc -o $@ t_primitive_restart.m $(MTL_T_SRC) $(FW) +t_depth16_clear: t_depth16_clear.m $(MTL_T_DEP) + clang $(CFLAGS) -fobjc-arc -o $@ t_depth16_clear.m $(MTL_T_SRC) $(FW) t_clear_ds: t_clear_ds.m $(MTL_T_DEP) clang $(CFLAGS) -fobjc-arc -o $@ t_clear_ds.m $(MTL_T_SRC) $(FW) t_clear_tex: t_clear_tex.m $(MTL_T_DEP) @@ -155,7 +157,7 @@ t_stencil_view: t_stencil_view.m $(MTL_T_DEP) # The Vulkan layer (#included) over the whole NVN layer, whose device and present path it uses t_vk_desc: t_vk_desc.m ../vk/hle_vk.m $(MTL_T_DEP) clang $(CFLAGS) -I../../third_party/vulkan-headers/include -fobjc-arc -o $@ t_vk_desc.m $(MTL_T_SRC) ../nvn/nvn_mtl.m $(FW) -run: t_nvn t_dirty_slots t_dirty_race t_builtin t_vin t_tbuf2d t_deswz_gpu t_pace t_fstep t_refl t_hmap t_cover t_fuse_check t_present_crop t_reorder_chain t_defer_chain t_zskip t_xv t_ub t_vbuf t_geometry t_mip_extent t_rt_range_shape t_rt_range_ro t_rt_range t_rt_depth_ro t_pso_skey t_pso_archive t_pso_pending t_pfv t_view_levels t_view_swizzle t_vk_desc t_integer_blend t_primitive_restart t_clear_ds t_upgrade_alias t_stencil_view +run: t_nvn t_dirty_slots t_dirty_race t_builtin t_vin t_tbuf2d t_deswz_gpu t_pace t_fstep t_refl t_hmap t_cover t_fuse_check t_present_crop t_reorder_chain t_defer_chain t_zskip t_xv t_ub t_vbuf t_geometry t_mip_extent t_rt_range_shape t_rt_range_ro t_rt_range t_rt_depth_ro t_pso_skey t_pso_archive t_pso_pending t_pfv t_view_levels t_view_swizzle t_vk_desc t_integer_blend t_primitive_restart t_clear_ds t_depth16_clear t_upgrade_alias t_stencil_view ./t_nvn ./t_dirty_slots ./t_dirty_race @@ -198,6 +200,7 @@ run: t_nvn t_dirty_slots t_dirty_race t_builtin t_vin t_tbuf2d t_deswz_gpu t_pac MTL_DEBUG_LAYER=1 ./t_integer_blend MTL_DEBUG_LAYER=1 ./t_primitive_restart MTL_DEBUG_LAYER=1 ./t_clear_ds + MTL_DEBUG_LAYER=1 ./t_depth16_clear MTL_DEBUG_LAYER=1 ./t_upgrade_alias MTL_DEBUG_LAYER=1 ./t_stencil_view DM_GAME=$(GAME) python3 test_tbdr_budget.py diff --git a/dm/gfx/tests/t_depth16_clear.m b/dm/gfx/tests/t_depth16_clear.m new file mode 100644 index 0000000..955a96d --- /dev/null +++ b/dm/gfx/tests/t_depth16_clear.m @@ -0,0 +1,114 @@ +// Both production Depth16Unorm clear paths must store the same depth as a fragment. Use NVN's float inputs, including half-bin boundaries and their neighboring floats. +// Expectations come from the GPU's fragment writes; count raw load-clear differences for comparison without requiring a particular GPU's unadjusted conversion. +#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[MAX_MOD]; int g_nmod; +void svc_thread_dump(void) {} +uint64_t now_ns(void) { return bp_now(); } + +static int production_clears(id attachment, id texture, const float *depth, int n) { + tex_t targets[2] = {0}; + id textures[2] = {attachment, texture}; + for (int k = 0; k < 2; k++) { + targets[k].magic = MAGIC('T','E','X',0); targets[k].serial = 99501 + k; + targets[k].d = (nvn_texdesc){.target=NVN_TEXTURE_TARGET_2D_ARRAY, .format=NVN_FORMAT_DEPTH16, + .width=(int)attachment.width, .height=(int)attachment.height, .depth=n, .levels=1, .samples=1}; + res_add(textures[k]); mtl_tex_init(&targets[k]); serial_put(targets[k].serial, textures[k]); + } + typedef struct { + op_hdr rt_header; p_set_rt rt; + op_hdr ds_header; p_clear_ds ds; + op_hdr tex_header; p_clear_tex tex; + } clear_ops; + clear_ops *ops = calloc(1, (size_t)n * sizeof(*ops) + sizeof(op_hdr)); + if (!ops) return 1; + for (int i = 0; i < n; i++) { + ops[i].rt_header = (op_hdr){OP_SET_RT, sizeof(op_hdr) + sizeof(p_set_rt)}; + ops[i].rt = (p_set_rt){.depth=&targets[0], .has_dview=1, .dview={.set_layers=1, .min_layer=i}}; + ops[i].ds_header = (op_hdr){OP_CLEAR_DS, sizeof(op_hdr) + sizeof(p_clear_ds)}; + ops[i].ds = (p_clear_ds){.depth=depth[i], .depth_mask=1}; + ops[i].tex_header = (op_hdr){OP_CLEAR_TEX, sizeof(op_hdr) + sizeof(p_clear_tex)}; + ops[i].tex = (p_clear_tex){.tex=&targets[1], .mask=15, + .r={0, 0, i, (int)texture.width, (int)texture.height, 1}}; + ops[i].tex.c.f[0] = depth[i]; + } + *(op_hdr *)(ops + n) = (op_hdr){OP_END, sizeof(op_hdr)}; + state_reset(); memset(&R, 0, sizeof R); mtl_sub_begin(); + mtl_exec((const uint8_t *)ops); mtl_sub_commit(); mtl_drain(); + int failed = mtl_last_commit_error() != 0; + free(ops); + for (int k = 0; k < 2; k++) mtl_tex_release(&targets[k]); + return failed; +} + +int main(void) { @autoreleasepool { + id dev = MTLCreateSystemDefaultDevice(); + if (!dev) { puts("t_depth16_clear: no Metal device, skipped"); return 0; } + mtl_init(); dev = g_dev; + NSError *err = nil; + id lib = [dev newLibraryWithSource:@"#include \nusing namespace metal;\n" + "struct V { float4 p [[position]]; };\n" + "vertex V vs(uint i [[vertex_id]]) { float2 q = float2((i << 1) & 2, i & 2); return { float4(q * 2 - 1, 0, 1) }; }\n" + "struct F { float d [[depth(any)]]; };\n" + "fragment F fs(constant float &d [[buffer(0)]]) { return { d }; }\n" options:nil error:&err]; + if (!lib) { printf("t_depth16_clear: shader compile failed: %s\n", err.localizedDescription.UTF8String); return 1; } + MTLRenderPipelineDescriptor *pd = [MTLRenderPipelineDescriptor new]; + pd.vertexFunction = [lib newFunctionWithName:@"vs"]; pd.fragmentFunction = [lib newFunctionWithName:@"fs"]; pd.depthAttachmentPixelFormat = MTLPixelFormatDepth16Unorm; + id ps = [dev newRenderPipelineStateWithDescriptor:pd error:&err]; + MTLDepthStencilDescriptor *dd = [MTLDepthStencilDescriptor new]; dd.depthCompareFunction = MTLCompareFunctionAlways; dd.depthWriteEnabled = YES; + id ds = [dev newDepthStencilStateWithDescriptor:dd]; + if (!ps || !ds) { printf("t_depth16_clear: pipeline failed: %s\n", err.localizedDescription.UTF8String); return 1; } + enum { N = 1000 + 6 + 12 * 3, W = 8, H = 8 }; + float depth[N]; + for (int i = 0; i < 1000; i++) depth[i] = i / 999.f; + const float extra[6] = {0.5f, 0.25f, 0.75f, 1.f / 3, 2.f / 3, 32767.5f / 65535}; + for (int i = 0; i < 6; i++) depth[1000 + i] = extra[i]; + const int bins[12] = {0, 1, 2, 3, 127, 128, 255, 256, 32766, 32767, 32768, 65534}; + for (int i = 0; i < 12; i++) { + float d = (float)((bins[i] + 0.5) / 65535.0); + depth[1006 + 3 * i] = nextafterf(d, 0); + depth[1007 + 3 * i] = d; + depth[1008 + 3 * i] = nextafterf(d, 1); + } + MTLTextureDescriptor *td = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatDepth16Unorm width:W height:H mipmapped:NO]; + td.textureType = MTLTextureType2DArray; td.arrayLength = N; td.usage = MTLTextureUsageRenderTarget; td.storageMode = MTLStorageModePrivate; + id tex[4] = {[dev newTextureWithDescriptor:td], [dev newTextureWithDescriptor:td], [dev newTextureWithDescriptor:td], [dev newTextureWithDescriptor:td]}; // fragment, ClearDepthStencil, raw clear, ClearTexture + id out = [dev newBufferWithLength:(size_t)4 * N * W * H * 2 options:MTLResourceStorageModeShared]; + if (!tex[0] || !tex[1] || !tex[2] || !tex[3] || !out) { puts("t_depth16_clear: resource allocation failed"); return 1; } + id q = [dev newCommandQueue]; + id cb = [q commandBuffer]; + for (int i = 0; i < N; i++) for (int k = 0; k < 3; k += 2) { + MTLRenderPassDescriptor *rp = [MTLRenderPassDescriptor renderPassDescriptor]; + rp.depthAttachment.texture = tex[k]; rp.depthAttachment.slice = (NSUInteger)i; rp.depthAttachment.loadAction = MTLLoadActionClear; rp.depthAttachment.storeAction = MTLStoreActionStore; + rp.depthAttachment.clearDepth = k == 0 ? 1.0 : depth[i]; + id e = [cb renderCommandEncoderWithDescriptor:rp]; + if (k == 0) { float d = (float)depth[i]; [e setRenderPipelineState:ps]; [e setDepthStencilState:ds]; [e setFragmentBytes:&d length:4 atIndex:0]; [e drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:3]; } + [e endEncoding]; + } + [cb commit]; [cb waitUntilCompleted]; + if (cb.error) { printf("t_depth16_clear: reference command buffer failed: %s\n", cb.error.localizedDescription.UTF8String); return 1; } + if (production_clears(tex[1], tex[3], depth, N)) { puts("t_depth16_clear: production clear failed"); return 1; } + cb = [q commandBuffer]; + id b = [cb blitCommandEncoder]; + for (int k = 0; k < 4; k++) for (int i = 0; i < N; i++) + [b copyFromTexture:tex[k] sourceSlice:(NSUInteger)i sourceLevel:0 sourceOrigin:MTLOriginMake(0, 0, 0) sourceSize:MTLSizeMake(W, H, 1) + toBuffer:out destinationOffset:((size_t)k * N + i) * W * H * 2 destinationBytesPerRow:W * 2 destinationBytesPerImage:W * H * 2]; + [b endEncoding]; [cb commit]; [cb waitUntilCompleted]; + if (cb.error) { printf("t_depth16_clear: command buffer failed: %s\n", cb.error.localizedDescription.UTF8String); return 1; } + const uint16_t *v = out.contents; + int fail[2] = {0}, raw_off = 0; + for (int i = 0; i < N; i++) { + const uint16_t *f = v + (size_t)i * W * H, *c = v + ((size_t)N + i) * W * H, *r = v + ((size_t)2 * N + i) * W * H; + const uint16_t *t = v + ((size_t)3 * N + i) * W * H; + int bad[2] = {0}, roff = 0; + for (int p = 0; p < W * H; p++) { bad[0] |= c[p] != f[p]; bad[1] |= t[p] != f[p]; roff |= r[p] != f[p]; } + raw_off += roff; + if ((bad[0] || bad[1]) && fail[0] + fail[1] < 10) + printf(" depth %.9f: fragment %u, ClearDepthStencil %u, ClearTexture %u (raw clear %u)\n", depth[i], f[0], c[0], t[0], r[0]); + for (int k = 0; k < 2; k++) fail[k] += bad[k]; + } + printf("t_depth16_clear: %d depths, ClearDepthStencil differs from the fragment in %d, ClearTexture in %d, raw clear in %d (0.5: fragment %u, raw clear %u)\n", + N, fail[0], fail[1], raw_off, v[(size_t)1000 * W * H], v[((size_t)2 * N + 1000) * W * H]); + return fail[0] != 0 || fail[1] != 0; +} } diff --git a/docs/agents/symptoms.md b/docs/agents/symptoms.md index b52dbcf..eebfa3e 100644 --- a/docs/agents/symptoms.md +++ b/docs/agents/symptoms.md @@ -296,6 +296,7 @@ One line per symptom in the [subsystem references](../reference/): what you see - `texture clear case not implemented … mask 0x1`. → [A channel mask that covers every channel is a whole clear](../reference/rendering.md#a-channel-mask-that-covers-every-channel-is-a-whole-clear) - `texture clear case not implemented … mask 0x8` while entering a rendered scene. → [Texture clears with partial channel masks or regions](../reference/rendering.md#texture-clears-with-partial-channel-masks-or-regions) - red lines for unimplemented mid-pass, partial-mask and partial-scissor clears (the clears were dropped); `depth-stencil clear with a scissor or partial stencil mask, cleared whole` (logged once). → [Clears inside a pass, with a partial mask or a partial scissor](../reference/rendering.md#clears-inside-a-pass-with-a-partial-mask-or-a-partial-scissor) +- a tile grid in Ultrahand and Recall highlights; an `EQUAL` depth test rejects fragments writing the same depth as a cleared Depth16 target. → [Depth16 load-clears match fragment writes](../reference/rendering.md#depth16-load-clears-match-fragment-writes) - solid magenta blocks, exactly (255, 0, 255), hard-edged and aligned to the 16-pixel grid (16×16, sometimes two stacked): Super Mario 3D World showed them only on the iPhone; Captain Toad on a Mac, Apple M6, in about half the frames of the opening course, moving with the falling leaves. → [Same-pass feedback read through a snapshot](../reference/rendering.md#same-pass-feedback-read-through-a-snapshot) - text a game renders into textures for the title and the menus comes out upside down, while the scene looks right. → [Lower-left window origin](../reference/rendering.md#lower-left-window-origin) - a pink window with every command buffer ignored while headless runs look right: the first presents fail on the display command buffer with `kIOGPUCommandBufferCallbackErrorPageFault`, and after a few of those macOS ignores every submission of the process ("Ignored (for causing prior/excessive GPU errors)"). → [The residency set committed before a present](../reference/rendering.md#the-residency-set-committed-before-a-present) diff --git a/docs/reference/rendering.md b/docs/reference/rendering.md index a784f11..f3d66b4 100644 --- a/docs/reference/rendering.md +++ b/docs/reference/rendering.md @@ -244,6 +244,13 @@ The replay on Metal (`dm/gfx/nvn/nvn_mtl.m`) and the Vulkan and OpenGL layers. E - **Fix:** such clears are drawn as one triangle over the pass in the open render encoder (`clear_color_tri` / `clear_ds_tri` in `nvn_mtl.m`, adapted from MacNativeGames, MIT). The pipeline has the pass's formats, the clear's mask as the write mask of its slot and no writes to the other slots. Depth is `ALWAYS` (and written for a depth clear), stencil `REPLACE` through the clear's write mask, the scissor is clamped as for draws, and visibility counting is off for the triangle. The value comes from the frame arena through the triangle's own argument tables, and the draws' tables are put back after it. A clear outside a pass opens the pass as a draw would. Clears a load action can express keep it. Two cases are still cleared whole, logged once: a depth / stencil clear with a scissor outside a pass, and a partial stencil write mask. - **Found on:** [The Legend of Zelda: Tears of the Kingdom](../../dm/games/0100F2C0115B6000/README.md): it issues the first whole-cleared case (a depth clear scissored to 8×8 of a 64×64 target), and so does Wonder ([Super Mario Bros. Wonder](../../dm/games/010015100B514000/README.md)). With `render_cond` ([Conditional rendering evaluated at replay](#conditional-rendering-evaluated-at-replay)) TOTK's scissored colour clears are all skipped; without it they are 50×50 tiles of a 500×400 target cleared to opaque black, about five a frame. [Metroid Dread](../../dm/games/010093801237C000/README.md) logged the red line once while loading on its smoke route. +### Depth16 load-clears match fragment writes + +- **Symptom:** a tile grid in Ultrahand and Recall highlights; an `EQUAL` depth test rejects fragments writing the same depth as a cleared Depth16 target. +- **Cause:** Apple GPUs truncate a `Depth16Unorm` load-clear but round fragment depth. A clear of 0.5 stores 32767 while a fragment writing 0.5 stores 32768. +- **Fix:** `nvn_depth16_clear_value` selects the nearest D16 value in double precision and adds a quarter-unit margin inside both its truncation and rounding bins, surviving Metal's conversion of the descriptor to float. Both attachment load-clears (`rp_begin_ex_`) and whole-texture clears (`clear_tex`) use it only for `Depth16Unorm`. Other formats and clears drawn as fragments retain their existing conversion. `t_depth16_clear` compares both production command paths with fragment-written reference depths, including half-bin boundaries and adjacent floats. +- **Found on:** [The Legend of Zelda: Tears of the Kingdom](../../dm/games/0100F2C0115B6000/README.md), reported in MacNativeGames commit `3d90e41` and ported in PR #199. The conversion difference is reproduced here with a synthetic Mac GPU test; the game's highlight effect and the iPhone have not been rechecked. + ### Integer colour clears - **Cause:** `nvnCommandBufferClearColori` / `ClearColorui` clear an integer target.