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
32 changes: 24 additions & 8 deletions dm/gfx/nvn/nvn_mtl.m
Original file line number Diff line number Diff line change
Expand Up @@ -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<MTLTexture> 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<MTLTexture> 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
Expand Down Expand Up @@ -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; }
Expand All @@ -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));
}
}
Expand Down Expand Up @@ -10973,20 +10987,22 @@ void td_note_layer(id<MTLTexture> 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 <path>", hle_hid.c): the path is kept (strdup), the frame number is the one after the current
// The Mac's DM_CTL "texraw <serial> <path>" (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 <serial> <path>" (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);
if (!o || ![o conformsToProtocol:@protocol(MTLTexture)]) { MLOG("[texraw] ser=%llu: no host texture", (unsigned long long)serial); return; }
id<MTLTexture> 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<MTLBuffer> 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<MTLBuffer> buf = [g_dev newBufferWithLength:sz * ns options:MTLResourceStorageModeShared];
id<MTLCommandBuffer> cb = q_cb(); id<MTLBlitCommandEncoder> 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]);
}
Expand Down
3 changes: 3 additions & 0 deletions dm/gfx/tests/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
76 changes: 76 additions & 0 deletions dm/gfx/tests/t_clear_layers.m
Original file line number Diff line number Diff line change
@@ -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<MTLCommandQueue> 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<MTLTexture> base = [g_dev newTextureWithDescriptor:descriptor];
CHECK(base != nil && base.arrayLength == (NSUInteger)cases[i].layers, "synthetic layered target allocates");
if (!base) continue;
id<MTLCommandBuffer> 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<MTLBuffer> result = [g_dev newBufferWithLength:W * W * 4 * cases[i].layers options:MTLResourceStorageModeShared];
cb = [queue commandBuffer]; id<MTLBlitCommandEncoder> 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;
} }
2 changes: 1 addition & 1 deletion dm/host/hle_hid.c
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ void hid_keys_init(void) {
// (hle_fpslock.c dm_ctl_cvar_f32; "cvari" an int one), "peek <address> <count>" logs that many floats of guest memory, "memdump <address> <bytes> <path>" 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 <NVN target>" logs the
// registered textures of that target (2 = 3D) with storage address and block-linear layout (nvn_mtl.m dm_gfx_texlist), "texraw <serial> <path>"
// saves a host texture's level 0 raw (nvn_mtl.m dm_gfx_texraw), "drag <x0> <y0> <x1> <y1> [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 <x0> <y0> <x1> <y1> [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
Expand Down
Loading