diff --git a/dm/gfx/nvn/nvn_mtl.m b/dm/gfx/nvn/nvn_mtl.m index 1140f62fde..14075dd458 100644 --- a/dm/gfx/nvn/nvn_mtl.m +++ b/dm/gfx/nvn/nvn_mtl.m @@ -3338,6 +3338,14 @@ static int rt_dup_slot(const p_set_rt *r, int i) { } return 0; } +// The layers an attached render target view covers from its attached slice (3D: depth plane) on: the view's layer count (0: the rest), else every layer the texture has from there +static NSUInteger att_layers(MTLRenderPassAttachmentDescriptor *a, const view_t *v, int has_v) { + id t = a.texture; if (!t) return 1; + NSUInteger all = t.textureType == MTLTextureType3D ? MAX(t.depth >> a.level, 1u) : t.arrayLength * (t.textureType == MTLTextureTypeCube || t.textureType == MTLTextureTypeCubeArray ? 6 : 1); + NSUInteger from = t.textureType == MTLTextureType3D ? a.depthPlane : a.slice, n = all > from ? all - from : 1; + if (has_v && v->set_layers && v->nlayers) n = MIN(n, (NSUInteger)v->nlayers); + return MAX(n, 1u); +} static void attach(MTLRenderPassAttachmentDescriptor *a, const tex_t *t, const view_t *v, int has_v) { id base = tex_mtl(t); // A texture made without render-target use (packaged or CPU-written, uploaded from its storage) drawn into: give it that use, as a clear of one does (clear_tex). Metal's validation @@ -3468,13 +3476,19 @@ static void attach(MTLRenderPassAttachmentDescriptor *a, const tex_t *t, const v } // A pass covers only the smallest of its attachments, and so does a clear folded into its load action, while NVN clears the whole target. Bayonetta 2 (SDK 3.6) starts every frame with a // 32x32 or 256x256 colour target beside its 1280x720 depth buffer and clears the depth there: only that corner was cleared, the scene behind failed the depth test against the - // previous frames' depth everywhere else, and its story narration showed in the top-left corner of a black screen. A cleared attachment larger than the pass is cleared in a pass of its own first + // previous frames' depth everywhere else, and its story narration showed in the top-left corner of a black screen. A cleared attachment larger than the pass is cleared in a pass of its own first. + // The same holds for layers: NVN clears every layer of the target's view, a load action only the pass's layers (one, unless the pass is layered). Little Nightmares clears its point + // lights' 6-face shadow cube in the pass that captures the one-pass geometry shader's vertices: only face 0 was cleared, faces 1-5 kept zero depth, every fragment failed the depth test + // there and the light passed only through the +X face. A cleared attachment whose view has more layers than a pass that is not layered is cleared, every layer, in a pass of its own too { NSUInteger pw, ph; pass_dims(pd, (NSUInteger)R.rt.n, &pw, &ph); for (NSUInteger i = 0; i < (NSUInteger)R.rt.n + 1; i++) { MTLRenderPassAttachmentDescriptor *a = i < (NSUInteger)R.rt.n ? pd.colorAttachments[i] : pd.depthAttachment.texture ? pd.depthAttachment : pd.stencilAttachment; int ds = i == (NSUInteger)R.rt.n, cd = ds && pd.depthAttachment.texture && pd.depthAttachment.loadAction == MTLLoadActionClear, cs = ds && pd.stencilAttachment.texture && pd.stencilAttachment.loadAction == MTLLoadActionClear; - if (!a.texture || (ds ? !cd && !cs : a.loadAction != MTLLoadActionClear) || (mip_extent(a.texture.width, a.level) <= pw && mip_extent(a.texture.height, a.level) <= ph)) continue; + if (!a.texture || (ds ? !cd && !cs : a.loadAction != MTLLoadActionClear)) continue; + NSUInteger nl = g_rp_layered ? 1 : att_layers(a, ds ? &R.rt.dview : &R.rt.cview[i], ds ? R.rt.has_dview : R.rt.has_cview[i]); + if (nl < 2 && mip_extent(a.texture.width, a.level) <= pw && mip_extent(a.texture.height, a.level) <= ph) continue; MTL4RenderPassDescriptor *cp = [MTL4RenderPassDescriptor new]; + if (nl > 1) cp.renderTargetArrayLength = nl; if (!ds) { MTLRenderPassColorAttachmentDescriptor *c = cp.colorAttachments[0], *o = pd.colorAttachments[i]; c.texture = o.texture; c.level = o.level; c.slice = o.slice; c.depthPlane = o.depthPlane; c.clearColor = o.clearColor; c.loadAction = MTLLoadActionClear; c.storeAction = MTLStoreActionStore; o.loadAction = MTLLoadActionLoad; } @@ -3484,8 +3498,8 @@ static void attach(MTLRenderPassAttachmentDescriptor *a, const tex_t *t, const v if (cs) { MTLRenderPassStencilAttachmentDescriptor *s = cp.stencilAttachment, *o = pd.stencilAttachment; s.texture = o.texture; s.level = o.level; s.slice = o.slice; s.clearStencil = o.clearStencil; s.loadAction = MTLLoadActionClear; s.storeAction = MTLStoreActionStore; o.loadAction = MTLLoadActionLoad; } } - PD("C %llu %s%lu cleared in a pass of its own (%lux%lu, the pass covers %lux%lu)", g_stat_pass + 1, ds ? "d" : "c", (unsigned long)i, (unsigned long)mip_extent(a.texture.width, a.level), - (unsigned long)mip_extent(a.texture.height, a.level), (unsigned long)pw, (unsigned long)ph); + PD("C %llu %s%lu cleared in a pass of its own (%lux%lu, %lu layers; the pass covers %lux%lu)", g_stat_pass + 1, ds ? "d" : "c", (unsigned long)i, (unsigned long)mip_extent(a.texture.width, a.level), + (unsigned long)mip_extent(a.texture.height, a.level), (unsigned long)nl, (unsigned long)pw, (unsigned long)ph); ser_gate(); enc_r_end(enc_r_begin(cp)); } } @@ -10973,7 +10987,7 @@ void td_note_layer(id t, uint64_t ser, NSUInteger lv, NSUInteger sl) [g_td_list addObject:@[t, @(ser), @(lv), @(sl)]]; } // The next frame's targets, on request (the Mac's DM_CTL "texdump ", hle_hid.c): the path is kept (strdup), the frame number is the one after the current -// The Mac's DM_CTL "texraw " (hle_hid.c): level 0 / slice 0 of the host texture behind a serial, raw, after the GPU finishes (header line: width height Metal format bytes per row); +// The Mac's DM_CTL "texraw " (hle_hid.c): level 0 of every slice (a cube's six faces) of the host texture behind a serial, raw, after the GPU finishes (header line: width height Metal format bytes per row, then the slice count when above one); // for a texture that is no render target (a default texture, a lookup table) whose content a shader depends on void dm_gfx_texraw(uint64_t serial, const char *path) { __unsafe_unretained id o = tex_get(serial); @@ -10981,12 +10995,14 @@ void dm_gfx_texraw(uint64_t serial, const char *path) { id t = o; mtl_finish_gpu(); if (t.storageMode == MTLStorageModeMemoryless || t.pixelFormat == MTLPixelFormatInvalid) return; NSUInteger w = t.width, h = t.height, row = w * 16, sz = row * h; // 16 bytes a texel: a multiple of every uncompressed texel size - id buf = [g_dev newBufferWithLength:sz options:MTLResourceStorageModeShared]; + NSUInteger ns = (t.textureType == MTLTextureTypeCube || t.textureType == MTLTextureTypeCubeArray ? 6 : 1) * MAX(t.arrayLength, 1u); // every slice (a cube's six faces), one after another + id buf = [g_dev newBufferWithLength:sz * ns options:MTLResourceStorageModeShared]; id cb = q_cb(); id b = [cb blitCommandEncoder]; MTLBlitOption bo = t.pixelFormat == MTLPixelFormatDepth32Float_Stencil8 ? MTLBlitOptionDepthFromDepthStencil : MTLBlitOptionNone; - [b copyFromTexture:t sourceSlice:0 sourceLevel:0 sourceOrigin:MTLOriginMake(0, 0, 0) sourceSize:MTLSizeMake(w, h, 1) toBuffer:buf destinationOffset:0 destinationBytesPerRow:row destinationBytesPerImage:sz options:bo]; + for (NSUInteger s = 0; s < ns; s++) + [b copyFromTexture:t sourceSlice:s sourceLevel:0 sourceOrigin:MTLOriginMake(0, 0, 0) sourceSize:MTLSizeMake(w, h, 1) toBuffer:buf destinationOffset:sz * s destinationBytesPerRow:row destinationBytesPerImage:sz options:bo]; [b endEncoding]; [cb commit]; [cb waitUntilCompleted]; - FILE *f = fopen(path, "wb"); if (f) { fprintf(f, "%lu %lu %lu %lu\n", (unsigned long)w, (unsigned long)h, (unsigned long)t.pixelFormat, (unsigned long)row); fwrite(buf.contents, 1, sz, f); fclose(f); } + FILE *f = fopen(path, "wb"); if (f) { fprintf(f, "%lu %lu %lu %lu", (unsigned long)w, (unsigned long)h, (unsigned long)t.pixelFormat, (unsigned long)row); if (ns > 1) fprintf(f, " %lu", (unsigned long)ns); fputc('\n', f); fwrite(buf.contents, 1, sz * ns, f); fclose(f); } const uint8_t *p = buf.contents; MLOG("[texraw] ser=%llu %lux%lu format %lu type %lu → %s; first bytes %02x %02x %02x %02x %02x %02x %02x %02x", (unsigned long long)serial, (unsigned long)w, (unsigned long)h, (unsigned long)t.pixelFormat, (unsigned long)t.textureType, path, p[0], p[1], p[2], p[3], p[4], p[5], p[6], p[7]); } diff --git a/dm/gfx/tests/Makefile b/dm/gfx/tests/Makefile index 8b9c607f1c..81908cf413 100644 --- a/dm/gfx/tests/Makefile +++ b/dm/gfx/tests/Makefile @@ -351,6 +351,9 @@ check_t_depth16_clear: RUN_ENV = MTL_DEBUG_LAYER=1 t_clear_ds: t_clear_ds.m $(MTL_T_DEP) clang $(CFLAGS) -fobjc-arc -o $@ t_clear_ds.m $(MTL_T_SRC) $(FW) check_t_clear_ds: RUN_ENV = MTL_DEBUG_LAYER=1 +t_clear_layers: t_clear_layers.m $(MTL_T_DEP) + clang $(CFLAGS) -fobjc-arc -o $@ t_clear_layers.m $(MTL_T_SRC) $(FW) +check_t_clear_layers: RUN_ENV = MTL_DEBUG_LAYER=1 t_clear_tex: t_clear_tex.m $(MTL_T_DEP) clang $(CFLAGS) -fobjc-arc -o $@ t_clear_tex.m $(MTL_T_SRC) $(FW) .PHONY: check_t_clear_tex diff --git a/dm/gfx/tests/t_clear_layers.m b/dm/gfx/tests/t_clear_layers.m new file mode 100644 index 0000000000..d341957fab --- /dev/null +++ b/dm/gfx/tests/t_clear_layers.m @@ -0,0 +1,76 @@ +// A clear covers every layer of the bound render target view, as NVN's does, not only the layer a pass that is not layered attaches. +#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) + +enum { W = 4, OLD_D = 0x3f400000, NEW_D = 0x3e800000, OLD_C = 0x11223344, NEW_C = 0x80808080 }; // depth .75 → .25; RGBA8 → 0.5 in every channel + +int main(void) { @autoreleasepool { + mtl_init(); + id queue = [g_dev newCommandQueue]; + // depth: a cube (6 faces) or a 2D array of colour; view: none, or min_layer / nlayers (0: the rest) + struct { int depth, target, layers, view, min_layer, nlayers; const char *label; } cases[] = { + {1, NVN_TEXTURE_TARGET_CUBEMAP, 6, 0, 0, 0, "depth clear of a cube without a view clears all six faces"}, + {1, NVN_TEXTURE_TARGET_CUBEMAP, 6, 1, 2, 1, "depth clear through a one-face view clears that face only"}, + {0, NVN_TEXTURE_TARGET_2D_ARRAY, 4, 0, 0, 0, "colour clear of a 2D array without a view clears every layer"}, + {0, NVN_TEXTURE_TARGET_2D_ARRAY, 4, 1, 1, 0, "colour clear through a view from layer 1 on clears layers 1-3"}, + {0, NVN_TEXTURE_TARGET_2D_ARRAY, 4, 1, 1, 2, "colour clear through a two-layer view clears layers 1-2"}, + {0, NVN_TEXTURE_TARGET_2D, 1, 0, 0, 0, "colour clear of a 2D target stays one layer"}, + }; + for (unsigned i = 0; i < sizeof cases / sizeof *cases; i++) { + tex_t t = {.magic = MAGIC('T','E','X',0), .serial = 99501 + i}; + t.d = (nvn_texdesc){.target = cases[i].target, .format = cases[i].depth ? NVN_FORMAT_DEPTH32F_STENCIL8 : NVN_FORMAT_RGBA8, .width = W, .height = W, + .depth = cases[i].target == NVN_TEXTURE_TARGET_2D_ARRAY ? cases[i].layers : 1, .levels = 1, .samples = 1}; + MTLTextureDescriptor *descriptor = tex_desc(&t, 0); + id base = [g_dev newTextureWithDescriptor:descriptor]; + CHECK(base != nil && base.arrayLength == (NSUInteger)cases[i].layers, "synthetic layered target allocates"); + if (!base) continue; + id cb = [queue commandBuffer]; + for (int s = 0; s < cases[i].layers; s++) { // every layer starts at the old value + MTLRenderPassDescriptor *init = [MTLRenderPassDescriptor renderPassDescriptor]; + if (cases[i].depth) { init.depthAttachment.texture = base; init.depthAttachment.slice = s; init.depthAttachment.loadAction = MTLLoadActionClear; + init.depthAttachment.storeAction = MTLStoreActionStore; init.depthAttachment.clearDepth = .75; } + else { init.colorAttachments[0].texture = base; init.colorAttachments[0].slice = s; init.colorAttachments[0].loadAction = MTLLoadActionClear; + init.colorAttachments[0].storeAction = MTLStoreActionStore; init.colorAttachments[0].clearColor = MTLClearColorMake(0x44 / 255., 0x33 / 255., 0x22 / 255., 0x11 / 255.); } + [[cb renderCommandEncoderWithDescriptor:init] endEncoding]; + } + [cb commit]; [cb waitUntilCompleted]; + CHECK(cb.status == MTLCommandBufferStatusCompleted, "initial layers reach the GPU"); + res_add(base); mtl_tex_init(&t); serial_put(t.serial, base); + state_reset(); memset(&R, 0, sizeof R); mtl_sub_begin(); + view_t v = {0}; if (cases[i].view) { v.set_layers = 1; v.min_layer = (uint16_t)cases[i].min_layer; v.nlayers = (uint16_t)cases[i].nlayers; } + R.has_rt = 1; + if (cases[i].depth) { R.rt.depth = &t; R.rt.dview = v; R.rt.has_dview = (uint8_t)cases[i].view; } + else { R.rt.n = 1; R.rt.color[0] = &t; R.rt.cview[0] = v; R.rt.has_cview[0] = (uint8_t)cases[i].view; } + struct { op_hdr head; union { p_clear_ds ds; p_clear_color c; } clear; op_hdr end; } ops = {0}; + if (cases[i].depth) { ops.head = (op_hdr){OP_CLEAR_DS, sizeof(op_hdr) + sizeof(p_clear_ds)}; ops.clear.ds = (p_clear_ds){.depth = .25, .depth_mask = 1}; } + else { ops.head = (op_hdr){OP_CLEAR_COLOR, sizeof(op_hdr) + sizeof(p_clear_color)}; ops.clear.c = (p_clear_color){.index = 0, .mask = 0xf, .rgba = {.5f, .5f, .5f, .5f}}; } + ((op_hdr *)((uint8_t *)&ops + ops.head.size))->op = OP_END; ((op_hdr *)((uint8_t *)&ops + ops.head.size))->size = sizeof(op_hdr); + mtl_exec((const uint8_t *)&ops); + mtl_sub_commit(); mtl_drain(); + CHECK(!mtl_last_commit_error(), "production clear completes without GPU error"); + id result = [g_dev newBufferWithLength:W * W * 4 * cases[i].layers options:MTLResourceStorageModeShared]; + cb = [queue commandBuffer]; id b = [cb blitCommandEncoder]; + for (int s = 0; s < cases[i].layers; s++) + [b copyFromTexture:base sourceSlice:s sourceLevel:0 sourceOrigin:MTLOriginMake(0, 0, 0) sourceSize:MTLSizeMake(W, W, 1) toBuffer:result destinationOffset:(NSUInteger)(W * W * 4 * s) + destinationBytesPerRow:W * 4 destinationBytesPerImage:W * W * 4 options:cases[i].depth ? MTLBlitOptionDepthFromDepthStencil : MTLBlitOptionNone]; + [b endEncoding]; [cb commit]; [cb waitUntilCompleted]; + CHECK(cb.status == MTLCommandBufferStatusCompleted, "every layer is read back"); + const uint32_t *px = result.contents; int valid = 1; + int lo = cases[i].view ? cases[i].min_layer : 0, hi = cases[i].view && cases[i].nlayers ? lo + cases[i].nlayers : cases[i].layers; + for (int s = 0; s < cases[i].layers; s++) { + uint32_t want = s >= lo && s < hi ? (cases[i].depth ? NEW_D : NEW_C) : (cases[i].depth ? OLD_D : OLD_C); + for (int k = 0; k < W * W; k++) if (px[s * W * W + k] != want) { valid = 0; fprintf(stderr, "case %u layer %d texel %d: %#x, want %#x\n", i, s, k, px[s * W * W + k], want); break; } + } + CHECK(valid, cases[i].label); + printf("case %u: %s %s\n", i, cases[i].label, valid ? "PASS" : "FAIL"); + mtl_tex_release(&t); + } + printf("layered clears: %d checks, %d failed\n", checks, failed); + return failed != 0; +} } diff --git a/dm/host/hle_hid.c b/dm/host/hle_hid.c index db67a3ac9a..768a35701a 100644 --- a/dm/host/hle_hid.c +++ b/dm/host/hle_hid.c @@ -204,7 +204,7 @@ void hid_keys_init(void) { // (hle_fpslock.c dm_ctl_cvar_f32; "cvari" an int one), "peek
" logs that many floats of guest memory, "memdump
" saves guest memory // to a file (a buffer texture's storage, which the pass dump's tbuf lines print, holds what the GPU wrote: buffer textures are views of the pool), "texlist " logs the // registered textures of that target (2 = 3D) with storage address and block-linear layout (nvn_mtl.m dm_gfx_texlist), "texraw " -// saves a host texture's level 0 raw (nvn_mtl.m dm_gfx_texraw), "drag [seconds=0.3]" moves one finger in a straight line between the two normalized picture +// saves a host texture's level 0 raw, every slice (a cube's six faces) (nvn_mtl.m dm_gfx_texraw), "drag [seconds=0.3]" moves one finger in a straight line between the two normalized picture // points over that time and lifts it (the touch screen's pans and swipes, hle_touch.c gesture_sample), "dump" logs what each guest thread waits on and the graphics wait points // (crash_dump_now, as the iOS app's DM_LOG_PORT dump; scripts/regress.py asks for it before stopping a hung game), "#…" is ignored. // Look at a screen, decide, press, look again: menus whose timing moves from run to run (loading) need no blind timed script diff --git a/docs/agents/05-debugging.md b/docs/agents/05-debugging.md index 20dc66ebbe..c18a46f718 100644 --- a/docs/agents/05-debugging.md +++ b/docs/agents/05-debugging.md @@ -102,7 +102,7 @@ Patterns that worked: print every call of one function with its arguments; stop - `DM_KEYS=":