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
2 changes: 2 additions & 0 deletions dm/games/0400486024DEC000/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ Measured on an M6 Mac with 16 GB, headless, on one walk with camera turns (`walk

## Memory

- Diagnostic census (M5, 24 GB, 2026-11-11, on a branch that also carries PR #1453 and PR #1731, so not main's numbers): the fixed Castle Town walk peaked at 3,950 MiB of distinct GPU-only shared-target allocations versus 841 MiB of guest interval unions per format/pool/sample count; 469 MiB were already family heaps. RGBA16F alone was 757 MiB versus 37 MiB guest. The overwrite estimate at that snapshot was 666 MiB potentially releasable, so heaps per pixel format at the guest offset (one heap per pool, format and sample count) have the larger measured opportunity. With the diagnostic on/off on the same host (one run each, indicative), neither run had a 300-frame window above 17.5 ms; system wired peaked at 10,988/10,868 MiB and process footprint at 12,917/12,472 MiB. This measures the opportunity, not the saving of an optimization.
- Current: on the walk, run from an external drive with a 3.5 GB pipeline archive: resident 10.9-11.4 GB, wired about 9.4 GB, on a 16 GB Mac. `vmmap` and `footprint`: 6.4 GB of guest memory and 3.8 GB of Metal textures and targets, 1 GB of IOAccelerator memory. [Reading the archive into memory](../../../docs/reference/memory.md#a-pipeline-archive-on-an-external-drive-is-read-into-memory) used to add 3.5 GB (footprint 15.0, peak 16.7). `mem_map_limit on`: a map past the application pool fails as on Horizon, and the SDK's allocator then gives back the pages it keeps. Other apps' memory adds to the swap: other games running beside it can push the Mac over.
- This game took the Mac down before the fixes (kernel panic in `IOGPUGroupMemory` at 14.7 GB wired): [raw pool table](../../../docs/reference/memory.md#memory-pools-nothing-gave-an-address-to-stay-out-of-the-raw-pool-table), [textures made only when read](../../../docs/reference/memory.md#shaders-report-which-textures-they-read-through-a-pool), [out-of-memory exit](../../../docs/reference/memory.md#repeated-gpu-out-of-memory-failures-end-the-game).
- Why so much: the game's own memory is about the console's 9 GB application pool, and on the Mac its textures also exist as Metal textures, as Metal cannot sample the guest's block-linear storage.
Expand Down Expand Up @@ -127,6 +128,7 @@ Its 37 kernels that correct samples, NDSR's network among them, gave about 0.25

- [Reads past a uniform binding's size](../../../docs/reference/shaders.md#reads-past-a-uniform-bindings-size): open ([GitHub issue #2218](https://github.com/seedds/nxnative/issues/2218)), long white streaks from the horses at Lon Lon Ranch and from Link's slingshot, and flickering parts of the slingshot and of the ReDeads' faces, reported with the uniform read bound on. The bound now takes the size the driver gives the GPU, rounded up to 256 bytes; the game binds sizes that are not multiples of 256. Not reproduced here before or after (the slingshot at Link's house, the ReDeads of adult Castle Town), so the reporters' retest decides.
- [A NaN colour under a zero blend weight](../../../docs/reference/shaders.md#a-nan-colour-under-a-zero-blend-weight): resolved, white clouds over the ground mist between Kokiri Forest and Hyrule Field and over the ferns at the camera in Kokiri Forest ([issue #1839](https://github.com/seedds/nxnative/issues/1839)). The mist shader `5ad65c0eefd09c7f` writes NaN at alpha 0 where its layer has density 0; NVIDIA's blender, with the option NVN's driver sets, adds nothing there, Apple's stored the NaN.
- [Render-target allocation and guest storage census](../../../docs/reference/memory.md#render-target-allocation-and-guest-storage-census): opt-in measurement of shared targets, guest interval unions, family heaps and the overwrite estimate.
- [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.
- [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 164 `M` rows (Kokiri Forest, Hyrule Field, Castle Town, Hyrule Castle, and 41 from later play, Death Mountain Trail among it), 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`. With the first 123 alone, a cold first start (empty Metal cache and pipeline archive, M4 Pro) on a Death Mountain Trail save built 9 and 10 at their first draw in two runs, 2.7 and 4.2 s, frames of 1,775 and 1,657 ms; with all 164, none, worst frame 25.6 and 18.5 ms, the prepare step 29.6 and 29.3 s → 32.1 and 31.0 s.
Expand Down
45 changes: 44 additions & 1 deletion dm/gfx/nvn/nvn_mtl.m
Original file line number Diff line number Diff line change
Expand Up @@ -462,7 +462,8 @@
else { id<MTLBuffer> b = o; int sh = b.storageMode == MTLStorageModeShared; MLOG(" buffer %p+%#lx (%s): %llu MB", sh ? b.contents : NULL, (unsigned long)b.length, sh ? "shared" : "private", (unsigned long long)(b.allocatedSize >> 20)); } }
} }
void mem_census(const char *why);
void dm_gfx_restop(void) { res_top("DM_CTL restop"); mem_census("DM_CTL restop"); }
static void rt_breakdown(void); // DM_NVN_TEX_BREAKDOWN
void dm_gfx_restop(void) { res_top("DM_CTL restop"); mem_census("DM_CTL restop"); rt_breakdown(); }
// Going on helps nobody once command buffers keep failing: no frame reaches the window, and each attempt asks the driver to wire more. A host left running like that on a 16 GB Mac took the whole
// system down, hung for hours and then a kernel panic in IOGPUGroupMemory at 14.7 GB wired. So 32 failures within a minute end the game
static void res_top_once(NSError *err) {
Expand Down Expand Up @@ -643,7 +644,7 @@
}
id<MTLBuffer> out = [g_dev newBufferWithLength:w * h * 4 options:MTLResourceStorageModeShared];
id<MTLBlitCommandEncoder> b = [cb blitCommandEncoder];
[b copyFromTexture:t sourceSlice:0 sourceLevel:0 sourceOrigin:MTLOriginMake(0, 0, 0) sourceSize:MTLSizeMake(w, h, 1) toBuffer:out destinationOffset:0

Check failure on line 647 in dm/gfx/nvn/nvn_mtl.m

View workflow job for this annotation

GitHub Actions / clang-tidy (ratchet)

Nullable pointer is passed to a callee that requires a non-null 1st parameter [clang-analyzer-nullability.NullablePassedToNonnull] (20 in this file, the baseline has 15)
destinationBytesPerRow:w * 4 destinationBytesPerImage:w * h * 4 options:depth ? MTLBlitOptionDepthFromDepthStencil : MTLBlitOptionNone];
[b endEncoding]; [cb commit]; [cb waitUntilCompleted];
const uint32_t *px = out.contents; NSUInteger bad = 0; for (NSUInteger i = 0; i < w * h; i++) if (px[i] != px[0]) bad++;
Expand Down Expand Up @@ -1630,11 +1631,11 @@
static inline void enc_b_end(id e) { m4_end(e); }
static inline void enc_b_copy_buffer(id e, id<MTLBuffer> s, NSUInteger so, id<MTLBuffer> d, NSUInteger doff, NSUInteger n) { [e copyFromBuffer:s sourceOffset:so toBuffer:d destinationOffset:doff size:n]; }
static inline void enc_b_copy_buffer_texture(id e, id<MTLBuffer> s, NSUInteger so, NSUInteger row, NSUInteger img, MTLSize z, id<MTLTexture> d, NSUInteger sl, NSUInteger lv, MTLOrigin o) { [e copyFromBuffer:s sourceOffset:so sourceBytesPerRow:row sourceBytesPerImage:img sourceSize:z toTexture:d destinationSlice:sl destinationLevel:lv destinationOrigin:o]; }
static inline void enc_b_copy_texture_buffer(id e, id<MTLTexture> s, NSUInteger sl, NSUInteger lv, MTLOrigin o, MTLSize z, id<MTLBuffer> d, NSUInteger doff, NSUInteger row, NSUInteger img) { [e copyFromTexture:s sourceSlice:sl sourceLevel:lv sourceOrigin:o sourceSize:z toBuffer:d destinationOffset:doff destinationBytesPerRow:row destinationBytesPerImage:img]; }

Check failure on line 1634 in dm/gfx/nvn/nvn_mtl.m

View workflow job for this annotation

GitHub Actions / clang-tidy (ratchet)

Nullable pointer is passed to a callee that requires a non-null 6th parameter [clang-analyzer-nullability.NullablePassedToNonnull] (20 in this file, the baseline has 15)
// The same with a blit option: one plane of a combined depth / stencil texture (MTLBlitOptionDepthFromDepthStencil)
static inline void enc_b_copy_buffer_texture_opt(id e, id<MTLBuffer> s, NSUInteger so, NSUInteger row, NSUInteger img, MTLSize z, id<MTLTexture> d, NSUInteger sl, NSUInteger lv, MTLOrigin o, MTLBlitOption opt) { [e copyFromBuffer:s sourceOffset:so sourceBytesPerRow:row sourceBytesPerImage:img sourceSize:z toTexture:d destinationSlice:sl destinationLevel:lv destinationOrigin:o options:opt]; }
static inline void enc_b_copy_texture_buffer_opt(id e, id<MTLTexture> s, NSUInteger sl, NSUInteger lv, MTLOrigin o, MTLSize z, id<MTLBuffer> d, NSUInteger doff, NSUInteger row, NSUInteger img, MTLBlitOption opt) { [e copyFromTexture:s sourceSlice:sl sourceLevel:lv sourceOrigin:o sourceSize:z toBuffer:d destinationOffset:doff destinationBytesPerRow:row destinationBytesPerImage:img options:opt]; }

Check failure on line 1637 in dm/gfx/nvn/nvn_mtl.m

View workflow job for this annotation

GitHub Actions / clang-tidy (ratchet)

Nullable pointer is passed to a callee that requires a non-null 6th parameter [clang-analyzer-nullability.NullablePassedToNonnull] (20 in this file, the baseline has 15)
static inline void enc_b_copy_texture(id e, id<MTLTexture> s, NSUInteger sl, NSUInteger lv, MTLOrigin so, MTLSize z, id<MTLTexture> d, NSUInteger dl, NSUInteger dv, MTLOrigin doo) { [e copyFromTexture:s sourceSlice:sl sourceLevel:lv sourceOrigin:so sourceSize:z toTexture:d destinationSlice:dl destinationLevel:dv destinationOrigin:doo]; }

Check failure on line 1638 in dm/gfx/nvn/nvn_mtl.m

View workflow job for this annotation

GitHub Actions / clang-tidy (ratchet)

Nullable pointer is passed to a callee that requires a non-null 6th parameter [clang-analyzer-nullability.NullablePassedToNonnull] (20 in this file, the baseline has 15)
static void blit_note(void); // Per-pass dump: a copy / buffer update ended the render pass (defined after PD)
// Ordering inside the copy encoder: Metal 4 does not order copies / fills within one encoder (no automatic hazard tracking); an operation reading or writing a range an earlier one wrote needs an intra-encoder barrier.
// Without it, SMBW's two updates per frame to the same uniform slot ("write 4 bytes −1, then 16 bytes of coefficients") occasionally landed in reverse order; the fountain water wave simulation took −1 as its self coefficient, diverged, and the water vertices were pushed to the top of the screen.
Expand Down Expand Up @@ -2244,7 +2245,7 @@
const char *e = getenv("DM_GPU_CAPTURE_DIR"); NSString *dir;
if (e && *e) dir = @(e); else { char cwd[1024]; if (!getcwd(cwd, sizeof cwd)) strcpy(cwd, "."); dir = [@(cwd) stringByAppendingPathComponent:@"gpu_captures"]; }
NSError *err = nil;
if (![NSFileManager.defaultManager createDirectoryAtPath:dir withIntermediateDirectories:YES attributes:nil error:&err]) { MLOG("[RED] DM_GPU_CAPTURE: cannot make the folder %s: %s", dir.UTF8String, err.localizedDescription.UTF8String); return nil; }

Check failure on line 2248 in dm/gfx/nvn/nvn_mtl.m

View workflow job for this annotation

GitHub Actions / clang-tidy (ratchet)

Nullable pointer is passed to a callee that requires a non-null 1st parameter [clang-analyzer-nullability.NullablePassedToNonnull] (20 in this file, the baseline has 15)
return dir;
}
static void gpu_capture_prune(NSString *dir) { // only dm_*.gputrace entries directly in the capture folder; the newest DM_GPU_CAPTURE_KEEP - 1 stay
Expand Down Expand Up @@ -2584,6 +2585,15 @@
static void held_add(const void *o, int d) { count_add(&g_by_serial_held, o, d); }
static void ghost_held_add(const void *o, int d) { count_add(&g_ghost_held, o, d); }
static nvn_hmap g_alias_seen = {.klen = sizeof(alias_key)}; // key → g_fence + 1 at its last lookup or creation (alias_take_over spares entries looked up lately)
// Diagnostic only: frame + 1 of the last recorded GPU write to an existing sharing entry.
// Keeping this separate from g_alias_seen avoids mistaking a descriptor lookup for a write.
static nvn_hmap g_alias_written = {.klen = sizeof(alias_key)};
static int rt_breakdown_on(void) {
static int on; static dispatch_once_t once;
dispatch_once(&once, ^{ const char *e = getenv("DM_NVN_TEX_BREAKDOWN"); on = e && atoi(e) > 0; });
return on;
}

// Live GPU-only image descriptions, including those not used yet. A dynamic-resolution family declares its largest image before any particular resolution is used.
// Keeping only descriptions here lets its first allocation cover that family exactly, without allocating every resolution or rounding to a power of two.
static nvn_hmap g_image_desc = {.klen = 8}; // serial → malloc'ed tex_t, under g_smu
Expand Down Expand Up @@ -2646,6 +2656,7 @@
void *old = nvn_hm_del(&g_alias, &ks[j]);
if (!old) continue;
nvn_hm_del(&g_alias_seen, &ks[j]);
nvn_hm_del(&g_alias_written, &ks[j]);
id o = CFBridgingRelease(old);
if (g_alias_set) CFSetRemoveValue(g_alias_set, (__bridge const void *)o);
defer_obj_release(o);
Expand Down Expand Up @@ -2691,6 +2702,7 @@
void *old = nvn_hm_del(&g_alias, &ks[j]);
if (!old) continue;
nvn_hm_del(&g_alias_seen, &ks[j]);
nvn_hm_del(&g_alias_written, &ks[j]);
id o = CFBridgingRelease(old);
if (g_alias_set) CFSetRemoveValue(g_alias_set, (__bridge const void *)o);
defer_obj_release(o);
Expand Down Expand Up @@ -3399,7 +3411,7 @@
}
up_begin();
id<MTLBuffer> stg = [g_dev newBufferWithLength:total ? total : 16 options:MTLResourceStorageModeShared];
res_add(stg); [g_up_keep addObject:stg];

Check failure on line 3414 in dm/gfx/nvn/nvn_mtl.m

View workflow job for this annotation

GitHub Actions / clang-tidy (ratchet)

Nullable pointer is passed to a callee that requires a non-null 1st parameter [clang-analyzer-nullability.NullablePassedToNonnull] (20 in this file, the baseline has 15)
size_t off = 0;
int zb = zblock_on() && t->d.format >= 0x79 && t->d.format <= 0x94; if (zb) { g_zb_ser = t->serial; g_zb_n = (int)MIN(d.mipmapLevelCount, 16u); memset(g_zb, 0, sizeof g_zb); memset(g_zb_tot, 0, sizeof g_zb_tot); }
for (unsigned l = 0; l < d.mipmapLevelCount; l++) {
Expand Down Expand Up @@ -4046,6 +4058,12 @@
atomic_store(&g_gws_new, 1);
}
void mtl_tex_gpu_written(const tex_t *t) {
if (rt_breakdown_on() && t && t->pool && (t->pool->flags & (NVN_MEMORY_POOL_FLAGS_CPU_NO_ACCESS | NVN_MEMORY_POOL_FLAGS_VIRTUAL))) {
alias_key k = tex_alias_key(t);
pthread_mutex_lock(&g_smu);
if (nvn_hm_get(&g_alias, &k)) nvn_hm_put(&g_alias_written, &k, (void *)(uintptr_t)(atomic_load(&g_cur_frame) + 1));
pthread_mutex_unlock(&g_smu);
}
if (!t || !t->mem || !t->L.size || !t->pool || (t->pool->flags & (NVN_MEMORY_POOL_FLAGS_CPU_NO_ACCESS | NVN_MEMORY_POOL_FLAGS_VIRTUAL))) {
return;
}
Expand Down Expand Up @@ -15539,6 +15557,31 @@
t->d.width, t->d.height, t->d.depth, t->d.format, t->d.levels, t->d.flags, (void *)t->mem, (unsigned long long)t->L.size, t->L.tileH, t->L.tileD); }
os_unfair_lock_unlock(&g_rmu);
}
// Raw snapshots let the offline report union guest intervals per pool/format/sample count,
// and charge shared heaps once. The existing shape report sums texture sizes instead.
static void rt_breakdown(void) {
if (!rt_breakdown_on()) return;
pthread_mutex_lock(&g_smu);
uint64_t frame = atomic_load(&g_cur_frame);
MLOG("[rtbreakdown] begin=1 frame=%llu", (unsigned long long)frame);
for (uint32_t i = 0; i < g_alias.cap; i++) if (g_alias.vals[i]) {
alias_key k; memcpy(&k, g_alias.keys + (size_t)i * g_alias.klen, sizeof k);
const pool_t *p = (const pool_t *)(uintptr_t)k.pool;
if (!p || !(p->flags & (NVN_MEMORY_POOL_FLAGS_CPU_NO_ACCESS | NVN_MEMORY_POOL_FLAGS_VIRTUAL))) continue;
id<MTLTexture> t = (__bridge id)g_alias.vals[i];
if (!(t.usage & MTLTextureUsageRenderTarget)) continue;
if (t.parentTexture) t = t.parentTexture;
nvn_texlayout L; nvn_tex_layout(&k.d, &L);
NvImageHeap *owner = objc_getAssociatedObject(t, &g_image_heap_owner_key);
MLOG("[rtbreakdown] pool=%llu flags=%u off=%llu guest=%llu format=%lu samples=%lu texture=%llu bytes=%llu heap=%llu heap_bytes=%llu family=%d written=%llu",
(unsigned long long)k.pool, p->flags, (unsigned long long)k.off, (unsigned long long)L.size,
(unsigned long)t.pixelFormat, (unsigned long)t.sampleCount, (unsigned long long)(uintptr_t)(__bridge void *)t,
(unsigned long long)t.allocatedSize, (unsigned long long)(uintptr_t)(__bridge void *)t.heap, (unsigned long long)t.heap.size,
owner != nil, (unsigned long long)(uintptr_t)nvn_hm_get(&g_alias_written, &k));
}
MLOG("[rtbreakdown] end=1 frame=%llu", (unsigned long long)frame);
pthread_mutex_unlock(&g_smu);
}
void dm_gfx_texdump_next(const char *path) {
if (g_td_frame == -2) td_note(nil, 0); // read DM_NVN_TEXDUMP first, so it does not override the request
g_td_path = strdup(path); g_td_list = [NSMutableArray array]; g_td_done = 0; g_td_frame = (long)atomic_load(&g_cur_frame) + 1;
Expand Down
8 changes: 8 additions & 0 deletions dm/gfx/tests/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -623,3 +623,11 @@ check_t_pool_standin: t_pool_standin
MTL_DEBUG_LAYER=1 DM_NVN_ENC_THREAD=0 ./t_pool_standin
MTL_DEBUG_LAYER=1 DM_NVN_ENC_THREAD=1 ./t_pool_standin
MTL_DEBUG_LAYER=1 DM_NVN_ENC_THREAD=0 DM_NVN_POOL_STANDIN=0 ./t_pool_standin

# Snapshot diagnostics must not materialize guest pool buffers.
t_rt_breakdown: t_rt_breakdown.m $(MTL_T_DEP)
clang $(CFLAGS) -fobjc-arc -o $@ t_rt_breakdown.m $(MTL_T_SRC) $(FW)
.PHONY: check_t_rt_breakdown
check_t_rt_breakdown: t_rt_breakdown
./t_rt_breakdown
./t_rt_breakdown off
29 changes: 29 additions & 0 deletions dm/gfx/tests/t_rt_breakdown.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// A snapshot records GPU writes without changing pool residency or retaining dead entries.
#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)

int main(int argc, char **argv) { @autoreleasepool {
int on = argc == 1; setenv("DM_NVN_TEX_BREAKDOWN", on ? "1" : "0", 1);
mtl_init();
pool_t p = {.magic = MAGIC('P','O','O','L'), .flags = NVN_MEMORY_POOL_FLAGS_CPU_NO_ACCESS, .size = 1 << 20};
tex_t t = {.pool = &p, .serial = 998000, .off = 512};
t.d = (nvn_texdesc){.target = NVN_TEXTURE_TARGET_2D, .format = NVN_FORMAT_RGBA8, .flags = NVN_TEXTURE_FLAGS_COMPRESSIBLE, .width = 64, .height = 64, .depth = 1, .levels = 1, .samples = 1};
nvn_tex_layout(&t.d, &t.L);
alias_key k = tex_alias_key(&t);
id<MTLTexture> mt = [g_dev newTextureWithDescriptor:tex_desc(&t, 0)];
hm_put_id(&g_alias, &k, mt);
atomic_store(&g_cur_frame, 10);
mtl_tex_gpu_written(&t);
CHECK((uintptr_t)nvn_hm_get(&g_alias_written, &k) == (on ? 11 : 0), "only the enabled diagnostic stamps GPU writes");
rt_breakdown();
CHECK(g_npool == 0, "a diagnostic does not create pool buffers");
mtl_alias_drop_pool(&p);
CHECK(g_alias_written.n == 0, "pool finalization removes recorded writes");
printf("%d checks, %d failures\n", checks, failed); return failed != 0;
} }
107 changes: 107 additions & 0 deletions dm/tools/nvn_rt_breakdown.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
"""Summarize DM_NVN_TEX_BREAKDOWN snapshots without charging aliases twice.

The overwrite count is an estimate over entries still in the sharing table:
recorded GPU writes describe whole textures, not the pixels of a render area.
Heap bytes are releasable only if every observed member is a candidate; other
holders outside the sharing table can still keep that heap alive.
"""
import argparse
from collections import defaultdict
import json
import re


def overwritten(old, writer, frame):
lo, hi = old['off'], old['off'] + old['guest']
wl, wh = writer['off'], writer['off'] + writer['guest']
return (old['written'] > 0 and frame + 1 >= old['written'] + 2
and writer['written'] > old['written']
and old['pool'] == writer['pool'] and lo != wl
and wl < hi and wh > lo and not (wl >= lo and wh <= hi))


def union_bytes(ranges):
total, end = 0, -1
for lo, hi in sorted(ranges):
total += max(0, hi - max(lo, end))
end = max(end, hi)
return total


def summarize(rows, frame):
stats = defaultdict(lambda: defaultdict(int))
ranges = defaultdict(list)
allocations = defaultdict(list)
textures = set()
last_write = defaultdict(int)
for row in rows:
last_write[row['texture']] = max(last_write[row['texture']], row['written'])
for row in rows:
fmt = row['format']
ranges[(fmt, row['pool'], row['samples'])].append(
(row['off'], row['off'] + row['guest']))
if row['texture'] in textures:
continue
textures.add(row['texture'])
s = stats[fmt]
s['textures'] += 1
s['nominal'] += row['bytes']
old = dict(row, written=last_write[row['texture']])
dead = any(overwritten(old, other, frame) for other in rows)
s['overwritten_nominal'] += row['bytes'] if dead else 0
key = ('heap', row['heap']) if row['heap'] else ('texture', row['texture'])
allocations[key].append((row, dead))
for (fmt, _, _), spans in ranges.items():
stats[fmt]['guest'] += union_bytes(spans)
for members in allocations.values():
row = members[0][0]
mixed = len({r['format'] for r, _ in members}) > 1
s = stats[-1 if mixed else row['format']]
size = row['heap_bytes'] if row['heap'] else row['bytes']
s['allocated'] += size
s['family'] += size if row['family'] else 0
s['releasable'] += size if all(dead for _, dead in members) else 0
# The old pool heaps can span formats: report their charge separately.
s['mixed_heap'] += size if mixed else 0
return {fmt: dict(values) for fmt, values in stats.items()}


def snapshots(lines):
frame, rows = None, []
for line in lines:
if '[rtbreakdown]' not in line:
continue
values = {k: int(v, 0) for k, v in re.findall(r'(\w+)=(0x[0-9a-f]+|\d+)', line)}
if 'begin' in values:
frame, rows = values['frame'], []
elif 'end' in values and frame is not None:
yield frame, rows
frame = None
elif 'texture' in values and frame is not None:
rows.append(values)


def main():
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument('log')
ap.add_argument('--json', action='store_true')
args = ap.parse_args()
with open(args.log, errors='replace') as f:
reports = [(frame, summarize(rows, frame)) for frame, rows in snapshots(f)]
if not reports:
ap.error('no complete render-target snapshots found')
if args.json:
print(json.dumps(reports))
return
peak = max(reports, key=lambda r: sum(s.get('allocated', 0) for s in r[1].values()))
for label, (frame, report) in [('peak', peak), ('last', reports[-1])]:
print(f'{label} frame {frame}: MiB, GPU-only shared targets; overwrite is an estimate')
print('format -1 charges a heap shared by multiple formats once, outside the per-format totals')
print('format count nominal allocated guest_union family_heap overwritten releasable mixed_heap')
for fmt, s in sorted(report.items()):
keys = ('nominal', 'allocated', 'guest', 'family', 'overwritten_nominal', 'releasable', 'mixed_heap')
print(fmt, s.get('textures', 0), *(f'{s.get(k, 0) / 2**20:.2f}' for k in keys))


if __name__ == '__main__':
main()
Loading
Loading