From 8fc22289960b8276ca7f99e1a605ff53243173a2 Mon Sep 17 00:00:00 2001 From: chrg6 Date: Fri, 13 Nov 2026 17:27:29 -0500 Subject: [PATCH] Time the hard-count batches of released textures, to measure what one costs. Past RELEASED_HARD, sweep_hard walks every released entry (up to 16k), classes each, and drops up to HARD_BATCH of them in one call, under the texture lock, on the thread that releases (a guest thread's finalize or the replay's overwrite_others). The rule before dropped one entry per release, so the work now comes in bursts, and nothing measured a burst. Each call is now timed, and counts the entries it walked, the entries it dropped and the entries written back first. A call of 2 ms or more logs "released textures past the hard count: a batch walked N entries and dropped M (K written back) in X ms", 16 lines at most. The trace=perf "texture origins" line adds the calls of its window, their time and the longest. The behaviour changes the finding proposes after a measurement (a cap on the GPU class in trl_hard_pick) are not part of this commit. Diagnostics only. Checks that passed: pass_cc_check on vk_texture.c, no new -Wall -Wextra warnings, tex_release_check, tex_compact_check. Co-Authored-By: Claude Opus 5.5 --- runtime/vk_texture.c | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/runtime/vk_texture.c b/runtime/vk_texture.c index 18e180ea..714086d9 100644 --- a/runtime/vk_texture.c +++ b/runtime/vk_texture.c @@ -2385,6 +2385,11 @@ static void sweep_released(void) { #define HARD_SLACK 256 #define HARD_BATCH 1024 static uint64_t n_hard_by[TRL_HC_N]; // entries dropped past RELEASED_HARD since the last perf line, by class +// Each call of sweep_hard is timed: it walks every released entry and drops a batch under the texture lock, on the +// thread that releases (a guest thread's finalize, the replay's overwrite_others), and nothing measured what a batch +// costs. A call of HARD_LOG_NS or more gets a line (16 at most); the texture origins perf line sums them. +#define HARD_LOG_NS 2000000ull +static uint64_t n_hard_calls, ns_hard, ns_hard_max; // since the last perf line static int hard_order(void) { static int on = -1; @@ -2412,6 +2417,8 @@ static void sweep_hard(void) { // tex_lock held static uint8_t *cls; static int *out, cap; const uint64_t stale = PRESENTS(120); // STALE_FRAMES + const uint64_t t0 = plat_now_ns(), drops0 = n_drop_by[DROP_HARD], wb0 = n_drop_wb; + uint64_t walked = 0; while (nreleased > RELEASED_HARD && rel_head) { uint64_t now = frames_now(); Tex *lru = rel_head; @@ -2427,6 +2434,7 @@ static void sweep_hard(void) { // tex_lock held if (!hard_order() || nreleased > cap) { Tex *gone = NULL; for (Tex *e = rel_head; e && !gone; e = e->rel_next) { // in release order + walked++; if (e->overwritten || now - e->samp_frame >= SWEEP_UNSAMPLED) gone = e; else if (e->samp_frame < lru->samp_frame) lru = e; } @@ -2439,6 +2447,7 @@ static void sweep_hard(void) { // tex_lock held walk[n] = e, cls[n] = (uint8_t)e_hard_class(e, now, stale), n++; if (e->samp_frame < lru->samp_frame) lru = e; } + walked += (uint64_t)n; int need = nreleased - (RELEASED_HARD - HARD_SLACK); int k = trl_hard_pick(cls, n, need < HARD_BATCH ? need : HARD_BATCH, out); // The picks the GPU drew into go back to their storage first, in one upload batch with one wait @@ -2459,6 +2468,16 @@ static void sweep_hard(void) { // tex_lock held for (int i = 0; i < k; i++) n_hard_by[cls[out[i]]]++, drop_tex(walk[out[i]], DROP_HARD); if (!k) hard_lru_drop(lru); } + uint64_t d = plat_now_ns() - t0; + n_hard_calls++, ns_hard += d; + if (d > ns_hard_max) ns_hard_max = d; + if (d >= HARD_LOG_NS) { + static volatile int32_t n_slow; + log_limited(&n_slow, "released textures past the hard count: a batch walked %llu entries and dropped %llu " + "(%llu written back) in %.2f ms", (unsigned long long)walked, + (unsigned long long)(n_drop_by[DROP_HARD] - drops0), (unsigned long long)(n_drop_wb - wb0), + d / 1e6); + } } // NX2NAVI_TEX_VRAM_PRESSURE (percent of the device-local budget, default VRAM_PRESSURE_DEFAULT; 0 = off): above it, each @@ -5266,15 +5285,18 @@ void vk_texture_new_submit(void) { "replaced by another " "descriptor at their address %llu; entries the CPU does not write made without content at an address it " "can see: on their own %llu, over a released entry %llu, over a live one %llu; marked slots that met an " - "entry without content %llu, that show no entry %llu; GPU-drawn entries written back before their drop %llu", + "entry without content %llu, that show no entry %llu; GPU-drawn entries written back before their drop %llu" + "; hard-count batches %llu, %.1f ms in all, the longest %.2f ms", (unsigned long long)f * 60, (unsigned long long)n_drop_by[DROP_MAX], (unsigned long long)n_drop_by[DROP_HARD], (unsigned long long)n_hard_by[TRL_HC_OVER], (unsigned long long)n_hard_by[TRL_HC_INTACT_OLD], (unsigned long long)n_hard_by[TRL_HC_INTACT], (unsigned long long)n_hard_by[TRL_HC_GPU], (unsigned long long)n_hard_by[TRL_HC_LATELY], (unsigned long long)n_drop_by[DROP_REPLACED], (unsigned long long)n_cpu_empty[0], (unsigned long long)n_cpu_empty[1], (unsigned long long)n_cpu_empty[2], - (unsigned long long)n_mark_empty, (unsigned long long)n_mark_none, (unsigned long long)n_drop_wb); + (unsigned long long)n_mark_empty, (unsigned long long)n_mark_none, (unsigned long long)n_drop_wb, + (unsigned long long)n_hard_calls, ns_hard / 1e6, ns_hard_max / 1e6); memset(n_drop_by, 0, sizeof n_drop_by), memset(n_cpu_empty, 0, sizeof n_cpu_empty), memset(n_hard_by, 0, sizeof n_hard_by); n_mark_empty = n_mark_none = n_drop_wb = 0; + n_hard_calls = ns_hard = ns_hard_max = 0; rt_log("perf: frame %llu: texture finalizes: slots marked %llu; slots of another live texture with the same " "parameters at the storage, left current %llu (terrain heightmaps %llu); lookups the stale rule gave the " "stand-in %llu (terrain heightmaps %llu), their own kept image %llu (NX2NAVI_TEX_STALE_KEPT)",