Past the hard count of released textures, drop the cheapest ones first, in one walk per batch.

Past RELEASED_HARD (16,384) the sweep dropped the first released entry in release order that was overwritten or unsampled for 2 s, render targets included, whose drop vk_texture.c already ties to a black frame, and it walked the list from its head for every drop. At the end of the 2026-11-13 session the count sat at 16,384 for a minute while 12,811 of 15,734 released entries owned no memory. The entries now go by class: overwritten ones, then ones that come back intact and are past their usual take-back gap, then other intact ones, and only then ones whose content only the GPU made; still only unsampled ones, and the one sampled longest ago when all were sampled lately, as before. One walk takes a batch down to 256 under the bound. tex_hard_order = 0 keeps the rule of 0.10.1.

The drops past the hard count get a reason of their own, so the texture origins line and the reuse line count them apart from the sweep's drops past RELEASED_MAX, with the classes they came from. The class rules are in vk_tex_release.h and checked by tests/tex_release_check.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
chrg6
2026-11-13 12:35:40 -05:00
parent 7aa29ff12a
commit 2969005edc
5 changed files with 154 additions and 22 deletions

View File

@@ -440,6 +440,10 @@
# Textures the game released that are kept as they are (1 to 16384); past this count only those that lose nothing
# when dropped go. Default: one per 2 MB of the video memory budget, 2048 to 8192. Read at start.
# tex_released_max = 4096
# Past 16384 kept textures some go whatever they cost. 1: the cheapest first: those drawn over, then those that come
# back unchanged from the game's memory (the oldest first), and only then those whose content only the GPU made, which
# would come back empty. 0: the first one in release order that nothing showed for 2 seconds, of any kind (0.10.1).
# tex_hard_order = 1
# Megabytes of this process's commit above which the log says so. Windows counts video memory against the commit;
# with a discrete GPU the video memory in use is left out of the comparison, since it is not in RAM. Textures no longer
# give way at this bound: dropping one lowers both sides of the comparison alike, so nothing they could do brought the

View File

@@ -61,7 +61,7 @@ static const char *const known[] = {
"SKIN_MOTION", // vk_skin_motion.inc
"MVOBJ_ALL", "MVOBJ_BANKS", "MVOBJ_EQUAL", "MVOBJ_EXACT", "MVOBJ_EXEC", "MVOBJ_NOC4", "MVOBJ_ONLY", "MVOBJ_TIMES", "MVPREV_CHECK", "MVPREV_GROW", "MVPREV_SKIP", "MVPREV_TRUST", "MV_CHECK",
"MV_SURVEY",
"PIPE_SEED_BOOT", "ONE_PER_CORE", "OPERATION_MODE", "OUTPUT_RES", "PAD_LAYOUT", "PACE_HZ", "PASS_CRUMBS", "PATCH_OUTPUT", "PATCH_RES_FIXED", "PATCH_SCENE", "PATCH_WIDE_ASPECT", "PATCH_WIDE_HUD", "PATCH_WIDE_MOVIE", "PATCH_WIDE_OUT", "PATCH_FREECAM", "PATCH_OCCLUSION_OFF", "PATCH_WIDE_SAVE", "PATCH_WIDE_SCENE", "PCSS", "PCSS_K", "PERF_OVERLAY", "PERF_SLOW_MS", "PIPELINE_CACHE", "PIPELINE_CACHE_MAX_MB", "PIPELINE_CACHE_PLAY", "PIPELINE_CACHE_PLAY_MB", "PIPE_BINARIES", "PIPE_BINARIES_MAX_MB", "PIPE_FEEDBACK", "PIPE_MEMO", "PIPE_PREBUILD", "PIPE_SEED", "PIPE_SKIP", "PIPE_SYNC", "PIPE_WAIT", "POWER_FULL", "TEX_SAME", "TEX_SAME_BIG", "TEX_FLAT3D", "TEX_KEEP_TAKEN", "TEX_STALE_KEPT", "PRESENT_HELD", "HELD_SYNC",
"PIPE_SEED_BOOT", "ONE_PER_CORE", "OPERATION_MODE", "OUTPUT_RES", "PAD_LAYOUT", "PACE_HZ", "PASS_CRUMBS", "PATCH_OUTPUT", "PATCH_RES_FIXED", "PATCH_SCENE", "PATCH_WIDE_ASPECT", "PATCH_WIDE_HUD", "PATCH_WIDE_MOVIE", "PATCH_WIDE_OUT", "PATCH_FREECAM", "PATCH_OCCLUSION_OFF", "PATCH_WIDE_SAVE", "PATCH_WIDE_SCENE", "PCSS", "PCSS_K", "PERF_OVERLAY", "PERF_SLOW_MS", "PIPELINE_CACHE", "PIPELINE_CACHE_MAX_MB", "PIPELINE_CACHE_PLAY", "PIPELINE_CACHE_PLAY_MB", "PIPE_BINARIES", "PIPE_BINARIES_MAX_MB", "PIPE_FEEDBACK", "PIPE_MEMO", "PIPE_PREBUILD", "PIPE_SEED", "PIPE_SKIP", "PIPE_SYNC", "PIPE_WAIT", "POWER_FULL", "TEX_SAME", "TEX_SAME_BIG", "TEX_FLAT3D", "TEX_KEEP_TAKEN", "TEX_HARD_ORDER", "TEX_STALE_KEPT", "PRESENT_HELD", "HELD_SYNC",
"PREPARE_PIPELINES", "PREWARM_FULL", "PREWARM_FULL_S", "PREWARM_WAIT_MS", "PREWARM_XLATE", "PROBE_MOVIE", "PROBE_MOVIE_SHOT", "PROFILE", "PROFILE_CHAINS", "PROFILE_DIR", "PROFILE_FRAMES", "PROFILE_REPLAY", "PROFILE_SLOW",
"POOL_FAST", "POOL_GROUP", "POOL_GROUP_CHECK", "POOL_PRESNAP", "POOL_PRESNAP_RUNS",
"SUBMIT_FINISH_GPU", "SVC_WAITS", "SNAP_WIDEN", "SUBMIT_FINISH_RESULTS", "SUBMIT_FINISH_KINDS", "FOLIAGE", "MIRROR_STAGE", "FLUSH_SKIP_CLEAN", "MIRROR_HOT", "MESH_EMULATION", "MESH_EMU_MB", "MESH_EMU_TASKS", "MESH_EMU_BATCH", "GUEST_OPT", "LOG_TEX_NEW", "DUMP_KEY", "VEC_INDEX_FIX", "PASS_TIMES",

View File

@@ -63,6 +63,31 @@ static inline uint64_t trl_keep(uint32_t hold_s, uint64_t cap, int room) {
return cap > UINT64_MAX / TRL_ROOM_TIMES ? UINT64_MAX : cap * TRL_ROOM_TIMES;
}
// Past the hard count of released textures (vk_texture.c sweep_hard, RELEASED_HARD) entries go whatever they cost,
// cheapest first by class: one drawn over (its content is gone anyway); one that comes back intact from the game's
// memory, released longer ago than its usual gap before a take-back and not looked up lately (past_gap); another one
// that comes back intact; one whose content only the GPU made, which would come back empty. The last three only when
// nothing sampled them lately (TRL_HC_LATELY, kept).
enum { TRL_HC_OVER, TRL_HC_INTACT_OLD, TRL_HC_INTACT, TRL_HC_GPU, TRL_HC_LATELY, TRL_HC_N };
static inline int trl_hard_class(int overwritten, int sampled_lately, int intact, int past_gap) {
if (overwritten) return TRL_HC_OVER;
if (sampled_lately) return TRL_HC_LATELY;
if (!intact) return TRL_HC_GPU;
return past_gap ? TRL_HC_INTACT_OLD : TRL_HC_INTACT;
}
// Up to need entries to drop: cls[i] is the class of the i-th released entry in release order, n of them; out gets
// their indices, the cheapest class first and in release order within a class. Returns how many; 0 when every entry
// was sampled lately (the caller then takes the one sampled longest ago).
static inline int trl_hard_pick(const uint8_t *cls, int n, int need, int *out) {
int k = 0;
for (int c = 0; c < TRL_HC_LATELY && k < need; c++)
for (int i = 0; i < n && k < need; i++)
if (cls[i] == c) out[k++] = i;
return k;
}
// The wall clock of the presents. The cache stamps its textures with present counts (gfx_counters.frames), and the
// presents per second change with the scene and the menus: a hold counted in presents would be shorter in wall time
// after a fast stretch. The clock notes the present count once a second or so (tick) and answers how many seconds ago

View File

@@ -750,7 +750,7 @@ static int slot_names(const Tex *e) {
// Why an entry was destroyed (drop_tex), in the order of drop_names.
enum { DROP_NONE, DROP_MAX, DROP_CAP, DROP_COMMIT, DROP_VRAM, DROP_IDLE_VRAM, DROP_IDLE_COMMIT, DROP_DISUSE,
DROP_OVERWRITTEN, DROP_REPLACED, DROP_REUSED, DROP_RANGE, DROP_EXACT, DROP_SHUTDOWN, DROP_COMPACT,
DROP_AGE, DROP_N };
DROP_AGE, DROP_HARD, DROP_N };
static void drop_tex(Tex *e, int why);
static void reuse_dropped(const Tex *e, int why); // "reuse of released textures", after vk_texture_census
static void reuse_made(const Tex *e);
@@ -1951,7 +1951,7 @@ static const char *const drop_names[DROP_N] = {
"under VRAM pressure", "idle, under VRAM pressure", "idle, under commit pressure", "unsampled (disuse)",
"overwritten at another address", "replaced by another descriptor at its address", "its image went to another texture",
"its storage went", "made whole again (texture detail)", "shutdown", "released, in a block a compaction empties",
"released and unused for longer than the hold"};
"released and unused for longer than the hold", "past RELEASED_HARD released entries"};
static const char *const start_names[START_N] = {
"started on its own", "filled from storage over a live entry", "content copied from a live entry",
"content copied from a released entry", "started empty over a live entry", "started empty over a released entry",
@@ -2322,6 +2322,7 @@ __attribute__((noinline)) static void destroy_tex(Tex *e) {
static int comes_back_intact(const Tex *e);
static uint64_t stale_after(const Tex *e);
static void sweep_hard(void);
// The limit of released entries past which the sweep drops those that lose nothing: one per 2 MiB of the video memory
// budget, from RELEASED_MAX to RELEASED_MAX_TOP. In Castle Town market on a 16 GB card (RTX 5060 Ti) with 2048 the
@@ -2365,20 +2366,81 @@ static void sweep_released(void) {
}
sweep_at = e;
}
if (nreleased > RELEASED_HARD) sweep_hard();
}
// Past RELEASED_HARD the count is a hard bound: entries go whatever they cost. The rule was the first entry in release
// order that was overwritten or that nothing sampled for SWEEP_UNSAMPLED presents, of whatever kind, render targets
// included, whose drop the comment above ties to a black frame; and each drop walked the list from its head again.
// Most entries there cost little to make again: at the end of a session of 2026-11-13, when the count sat at 16,384
// for a minute, 12,811 of 15,734 released entries owned no memory. So the entries now go cheapest first, by the
// classes of vk_tex_release.h trl_hard_class: overwritten ones; ones that come back intact, released longer ago than
// their stale_after and not looked up lately; other ones that come back intact; ones whose content only the GPU made;
// the last three only when nothing sampled them for SWEEP_UNSAMPLED presents; while every entry was sampled lately, the
// one sampled longest ago, as before. Within a class in release order. One walk of the list takes a batch down to
// RELEASED_HARD - HARD_SLACK entries, so the next HARD_SLACK releases need no walk. NX2NAVI_TEX_HARD_ORDER=0: the
// rule before.
#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
static int hard_order(void) {
static int on = -1;
if (on < 0) on = rt_knob("TEX_HARD_ORDER", "1") == 1;
return on;
}
static int e_hard_class(const Tex *e, uint64_t now, uint64_t stale) {
int intact = comes_back_intact(e);
return trl_hard_class(e->overwritten, now - e->samp_frame < SWEEP_UNSAMPLED, intact,
intact && now - e->rel_frame >= stale_after(e) && !used_lately(e, now, stale));
}
static void hard_lru_drop(Tex *lru) {
static volatile int32_t n_all_used;
log_limited(&n_all_used, "all %d released textures were sampled or drawn into lately: the one longest ago goes "
"(%#llx fmt %#x %ux%u, last in frame %llu)", nreleased, (unsigned long long)lru->key.addr,
lru->t.d.format, lru->t.d.width, lru->t.d.height, (unsigned long long)lru->samp_frame);
n_hard_by[TRL_HC_LATELY]++;
drop_tex(lru, DROP_HARD);
}
static void sweep_hard(void) { // tex_lock held
static Tex **walk;
static uint8_t *cls;
static int *out, cap;
const uint64_t stale = PRESENTS(120); // STALE_FRAMES
while (nreleased > RELEASED_HARD && rel_head) {
uint64_t now = frames_now();
Tex *gone = NULL, *lru = rel_head;
for (Tex *e = rel_head; e && !gone; e = e->rel_next) { // in release order
if (e->overwritten || now - e->samp_frame >= SWEEP_UNSAMPLED) gone = e;
else if (e->samp_frame < lru->samp_frame) lru = e;
Tex *lru = rel_head;
if (nreleased > cap && hard_order()) { // room for the whole list; without it the rule before
int want = nreleased + HARD_BATCH;
Tex **w = realloc(walk, (size_t)want * sizeof *w);
if (w) walk = w;
uint8_t *c = w ? realloc(cls, (size_t)want) : NULL;
if (c) cls = c;
int *o = c ? realloc(out, (size_t)want * sizeof *o) : NULL;
if (o) out = o, cap = want;
}
if (!gone) {
static volatile int32_t n_all_used;
log_limited(&n_all_used, "all %d released textures were sampled or drawn into lately: the one longest ago goes "
"(%#llx fmt %#x %ux%u, last in frame %llu)", nreleased, (unsigned long long)lru->key.addr,
lru->t.d.format, lru->t.d.width, lru->t.d.height, (unsigned long long)lru->samp_frame);
if (!hard_order() || nreleased > cap) {
Tex *gone = NULL;
for (Tex *e = rel_head; e && !gone; e = e->rel_next) { // in release order
if (e->overwritten || now - e->samp_frame >= SWEEP_UNSAMPLED) gone = e;
else if (e->samp_frame < lru->samp_frame) lru = e;
}
if (!gone) hard_lru_drop(lru);
else n_hard_by[e_hard_class(gone, now, stale)]++, drop_tex(gone, DROP_HARD);
continue;
}
drop_tex(gone ? gone : lru, DROP_MAX);
int n = 0;
for (Tex *e = rel_head; e && n < cap; e = e->rel_next) { // in release order
walk[n] = e, cls[n] = (uint8_t)e_hard_class(e, now, stale), n++;
if (e->samp_frame < lru->samp_frame) lru = e;
}
int need = nreleased - (RELEASED_HARD - HARD_SLACK);
int k = trl_hard_pick(cls, n, need < HARD_BATCH ? need : HARD_BATCH, out);
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);
}
}
@@ -5134,14 +5196,19 @@ void vk_texture_new_submit(void) {
n_commit_back = n_commit_held = 0;
n_disuse = n_disuse_bytes = n_disuse_back = n_retire_freed = n_retire_cancel = 0;
n_reused = n_reuse_new = n_bind_skips = n_restarts = n_overwritten = n_over_drops = 0;
rt_log("perf: frame %llu: texture origins: released entries dropped past RELEASED_MAX %llu, replaced by another "
rt_log("perf: frame %llu: texture origins: released entries dropped past RELEASED_MAX %llu, past RELEASED_HARD %llu "
"(overwritten %llu, intact past their gap %llu, intact %llu, made by the GPU %llu, all sampled lately %llu), "
"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",
(unsigned long long)f * 60, (unsigned long long)n_drop_by[DROP_MAX], (unsigned long long)n_drop_by[DROP_REPLACED],
(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);
memset(n_drop_by, 0, sizeof n_drop_by), memset(n_cpu_empty, 0, sizeof n_cpu_empty);
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;
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 "
@@ -6554,8 +6621,8 @@ static void reuse_taken_back(const Tex *e) { // tex_lock held; e is being take
// Drops of an entry the game may still ask for: past a bound, or under a pressure, or unsampled.
static int drop_costs(int why) {
return why == DROP_MAX || why == DROP_CAP || why == DROP_COMMIT || why == DROP_VRAM || why == DROP_IDLE_VRAM ||
why == DROP_IDLE_COMMIT || why == DROP_DISUSE || why == DROP_AGE;
return why == DROP_MAX || why == DROP_HARD || why == DROP_CAP || why == DROP_COMMIT || why == DROP_VRAM ||
why == DROP_IDLE_VRAM || why == DROP_IDLE_COMMIT || why == DROP_DISUSE || why == DROP_AGE;
}
static void reuse_dropped(const Tex *e, int why) { // tex_lock held
@@ -6588,11 +6655,12 @@ void vk_texture_reuse_log(uint64_t frame) {
for (int a = 0; a < AGE_N; a++)
o += snprintf(line + o, sizeof line - (size_t)o, "%s %s %llu", a ? "," : "", age_names[a], (unsigned long long)ra_n[a]);
snprintf(line + o, sizeof line - (size_t)o, "; by the drop: released past the hold %llu, past the size bound "
"%llu, past the count %llu, VRAM pressure %llu released and %llu idle, commit %llu released and %llu "
"idle, unsampled %llu); dropped past the hold %llu (%.0f MiB); now %d released, %.0f MiB of a bound of "
"%.0f MiB, hold %u s",
"%llu, past the count %llu (of them past the hard count %llu), VRAM pressure %llu released and %llu idle, "
"commit %llu released and %llu idle, unsampled %llu); dropped past the hold %llu (%.0f MiB); now %d "
"released, %.0f MiB of a bound of %.0f MiB, hold %u s",
(unsigned long long)ra_why[DROP_AGE], (unsigned long long)ra_why[DROP_CAP],
(unsigned long long)ra_why[DROP_MAX], (unsigned long long)ra_why[DROP_VRAM],
(unsigned long long)(ra_why[DROP_MAX] + ra_why[DROP_HARD]), (unsigned long long)ra_why[DROP_HARD],
(unsigned long long)ra_why[DROP_VRAM],
(unsigned long long)ra_why[DROP_IDLE_VRAM], (unsigned long long)ra_why[DROP_COMMIT],
(unsigned long long)ra_why[DROP_IDLE_COMMIT], (unsigned long long)ra_why[DROP_DISUSE],
(unsigned long long)n_age_drops, n_age_bytes / M, nreleased, rel_bytes / M,

View File

@@ -256,11 +256,46 @@ static void session_checks(void) {
(unsigned long long)r150.remade, (unsigned long long)r200.remade, TRL_HOLD_S);
}
// ---- past the hard count (trl_hard_class, trl_hard_pick)
static void hard_checks(void) {
CHECK(trl_hard_class(1, 1, 0, 0) == TRL_HC_OVER, "an overwritten entry goes first, sampled or not");
CHECK(trl_hard_class(0, 1, 1, 1) == TRL_HC_LATELY, "a sampled entry is kept");
CHECK(trl_hard_class(0, 0, 0, 0) == TRL_HC_GPU, "GPU-made content");
CHECK(trl_hard_class(0, 0, 1, 1) == TRL_HC_INTACT_OLD && trl_hard_class(0, 0, 1, 0) == TRL_HC_INTACT, "intact");
// the session's end: release order mixes render targets (GPU), intact entries and a few overwritten ones; the
// render targets are the oldest. The rule before took them first.
enum { N = 64 };
uint8_t cls[N];
int out[N];
for (int i = 0; i < N; i++)
cls[i] = i < 16 ? TRL_HC_GPU : i % 9 == 0 ? TRL_HC_OVER : i % 3 == 0 ? TRL_HC_LATELY : i < 40 ? TRL_HC_INTACT_OLD
: TRL_HC_INTACT;
int k = trl_hard_pick(cls, N, 20, out);
CHECK(k == 20, "picked %d of 20", k);
int last = -1, gpu = 0, prev_i = -1;
for (int j = 0; j < k; j++) {
int c = cls[out[j]];
CHECK(c != TRL_HC_LATELY, "a lately sampled entry was picked (%d)", out[j]);
CHECK(c >= last, "class order: %d after %d", c, last);
if (c == last) CHECK(out[j] > prev_i, "release order within a class");
gpu += c == TRL_HC_GPU;
last = c, prev_i = out[j];
}
CHECK(gpu == 0, "%d render targets went while intact entries were left", gpu);
// only render targets and sampled ones: the render targets go, in release order; none unsampled: nothing
for (int i = 0; i < N; i++) cls[i] = i % 2 ? TRL_HC_GPU : TRL_HC_LATELY;
k = trl_hard_pick(cls, N, 5, out);
CHECK(k == 5 && out[0] == 1 && out[4] == 9, "render targets in release order: %d, %d..%d", k, out[0], out[4]);
for (int i = 0; i < N; i++) cls[i] = TRL_HC_LATELY;
CHECK(trl_hard_pick(cls, N, 5, out) == 0, "all sampled lately: none picked");
}
int main(void) {
clock_checks();
rule_checks();
taken_back_checks();
session_checks();
hard_checks();
if (fails) printf("%d checks failed\n", fails);
else printf("tex_release_check: all checks passed\n");
return fails ? 1 : 0;