mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
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>
303 lines
17 KiB
C
303 lines
17 KiB
C
// runtime/vk_tex_release.h: how long a released texture is kept, the bytes the released ones may hold, and the clock
|
|
// that turns presents into seconds. Built and run by tests/tex_release_check.py; prints each failed check and a table
|
|
// of the simulated sessions, and exits 1 on any failure.
|
|
//
|
|
// The simulation follows vk_texture.c sweep_pressure: once a second (the cache checks every 8 submissions) take-backs
|
|
// that fall due find their entry or make it again, drop_aged takes the entries past their hold, and drop_stale takes
|
|
// the oldest entries released 2 s ago or earlier while the released bytes are above trl_keep. The streams are shaped
|
|
// by the figures of two sessions' logs on an RTX 5090 (vk_tex_release.h).
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include "vk_tex_release.h"
|
|
|
|
static int fails;
|
|
#define CHECK(c, ...) do { if (!(c)) { fails++; printf("FAIL line %d: ", __LINE__); printf(__VA_ARGS__); printf("\n"); } } while (0)
|
|
|
|
#define MB(n) ((uint64_t)(n) << 20)
|
|
|
|
static uint64_t rng_state = 0x243f6a8885a308d3ull;
|
|
static uint32_t rnd(uint32_t n) {
|
|
rng_state ^= rng_state << 13, rng_state ^= rng_state >> 7, rng_state ^= rng_state << 17;
|
|
return (uint32_t)(rng_state >> 33) % n;
|
|
}
|
|
|
|
// ---- the clock
|
|
static TrlClock clk;
|
|
|
|
static void clock_checks(void) {
|
|
// presents at a rate that changes every few seconds (60 to 240 a second); a tick each second, as rate_tick makes
|
|
memset(&clk, 0, sizeof clk);
|
|
uint64_t frame = 0, at[4000]; // at[s]: the present count at second s
|
|
for (uint32_t s = 0; s < 4000; s++) {
|
|
at[s] = frame;
|
|
trl_clock_tick(&clk, frame, s);
|
|
frame += 60 + (s / 7 % 4) * 60;
|
|
}
|
|
uint32_t now = 3999;
|
|
for (uint32_t s = 0; s < 4000; s += 13) {
|
|
uint32_t age = trl_clock_age(&clk, at[s] + 1, now), real = now - s; // a present just after tick s
|
|
if (now - s < TRL_CLOCK_N - 1) {
|
|
CHECK(age <= real && age + 1 >= real, "present after second %u: %u s old, %u by the clock", s, real, age);
|
|
} else {
|
|
CHECK(age >= TRL_CLOCK_N - 2, "present after second %u, before the ticks kept: %u by the clock", s, age);
|
|
}
|
|
}
|
|
CHECK(trl_clock_age(&clk, frame + 5, now) == 0, "a present after the last tick is 0 s old");
|
|
CHECK(trl_clock_age(&clk, at[3999], now) == 0, "the last tick's present is 0 s old");
|
|
// the hold in seconds does not depend on the rate: 180 s at 60 or at 240 presents a second
|
|
uint32_t s60 = 2500, s240 = 0; // within the ticks kept (the last TRL_CLOCK_N seconds)
|
|
for (uint32_t s = 2500; s < 4000; s++)
|
|
if ((s / 7 % 4) == 3) { s240 = s; break; }
|
|
CHECK(trl_clock_age(&clk, at[s60], s60 + 180) == 180 && trl_clock_age(&clk, at[s240], s240 + 180) == 180,
|
|
"180 s are 180 s at any rate");
|
|
// a run shorter than the ring, and an empty clock
|
|
TrlClock c2 = {0};
|
|
CHECK(trl_clock_age(&c2, 100, 50) == 0, "no tick yet: 0 s");
|
|
trl_clock_tick(&c2, 0, 0), trl_clock_tick(&c2, 100, 1), trl_clock_tick(&c2, 250, 2);
|
|
CHECK(trl_clock_age(&c2, 50, 10) == 9 && trl_clock_age(&c2, 0, 10) == 10 && trl_clock_age(&c2, 251, 10) == 0,
|
|
"short run: %u %u %u", trl_clock_age(&c2, 50, 10), trl_clock_age(&c2, 0, 10), trl_clock_age(&c2, 251, 10));
|
|
}
|
|
|
|
// ---- the rules
|
|
static void rule_checks(void) {
|
|
CHECK(trl_hold(TRL_HOLD_S, TRL_STORAGE) == 180 && trl_hold(TRL_HOLD_S, TRL_GPU) == 360, "holds 180 and 360 s");
|
|
CHECK(!trl_aged(0, TRL_STORAGE, 100000), "no hold: nothing ages out");
|
|
CHECK(!trl_aged(180, TRL_STORAGE, 179) && trl_aged(180, TRL_STORAGE, 180), "the hold's edge");
|
|
CHECK(!trl_aged(180, TRL_GPU, 359) && trl_aged(180, TRL_GPU, 360), "GPU content waits twice the hold");
|
|
CHECK(2 * TRL_HOLD_MAX_S < TRL_CLOCK_N - 1, "the clock covers the longest hold of GPU content");
|
|
uint64_t budget = MB(31 * 1024);
|
|
// the Balanced session's line at frame 7200: 11786 MiB in use of about 31 GiB, 27.3 GB of 71 GB commit free
|
|
CHECK(trl_room(MB(11786), budget, 80, 70981 - 43683, 7098), "RTX 5090 session: room");
|
|
CHECK(!trl_room(MB(23000), budget, 80, 27000, 7098), "card above 70%% of its budget: no room");
|
|
CHECK(!trl_room(MB(5000), budget, 80, 10000, 7098), "commit under twice its low mark: no room");
|
|
CHECK(!trl_room(MB(5000), budget, 80, -1, 7098), "commit not tracked: no room");
|
|
CHECK(!trl_room(MB(1000), 0, 80, 30000, 4096), "no budget known: no room");
|
|
CHECK(!trl_room(MB(1), budget, 5, 30000, 4096), "pressure under the margin: no room");
|
|
// an 8 GiB card in play: 6.3 of 7.5 GiB in use
|
|
CHECK(!trl_room(MB(6450), MB(7680), 80, 20000, 4096), "8 GiB card in play: no room");
|
|
CHECK(trl_keep(180, MB(4008), 1) == MB(8016) && trl_keep(180, MB(4008), 0) == MB(4008), "keep: twice with room");
|
|
CHECK(trl_keep(0, MB(4008), 1) == MB(4008), "keep: the bound without the hold, as before");
|
|
CHECK(trl_keep(180, UINT64_MAX, 1) == UINT64_MAX, "keep: no bound stays none");
|
|
}
|
|
|
|
// ---- the take-backs of the logs against the hold
|
|
// "vk: released textures frame N" lines of the two sessions (2026-11-12): take-backs by the time released, the buckets
|
|
// under 2 s, 2-10 s, 10-60 s, 1-5 min, longer, with each bucket's upper edge in seconds (the last open).
|
|
static const struct { const char *session; uint64_t n[5]; } TAKEN[] = {
|
|
{"low, frame 3600", {46626, 18, 0, 0, 0}}, {"low, frame 7200", {115111, 126, 69, 0, 0}},
|
|
{"low, frame 10800", {88575, 79, 156, 0, 0}}, {"balanced, frame 3600", {22034, 1, 0, 0, 0}},
|
|
{"balanced, frame 7200", {120779, 91, 1, 0, 0}},
|
|
};
|
|
static const uint32_t EDGE[5] = {2, 10, 60, 300, UINT32_MAX};
|
|
|
|
static void taken_back_checks(void) {
|
|
uint32_t longest = 0;
|
|
for (size_t i = 0; i < sizeof TAKEN / sizeof *TAKEN; i++)
|
|
for (int b = 0; b < 5; b++)
|
|
if (TAKEN[i].n[b] && EDGE[b] > longest) longest = EDGE[b];
|
|
CHECK(longest == 60, "the longest take-back of the logs lies under %u s", longest);
|
|
// nothing the logs took back would have gone past the hold: every take-back came before 60 s, the hold is 180 s
|
|
for (int cls = TRL_STORAGE; cls <= TRL_GPU; cls++)
|
|
CHECK(!trl_aged(TRL_HOLD_S, cls, longest) && trl_hold(TRL_HOLD_S, cls) >= 3 * longest,
|
|
"class %d: hold %u s against take-backs up to %u s", cls, trl_hold(TRL_HOLD_S, cls), longest);
|
|
}
|
|
|
|
// ---- simulated sessions
|
|
typedef struct {
|
|
uint32_t rel; // second of the release
|
|
uint32_t back; // second the game takes it back; 0: never
|
|
uint32_t bytes;
|
|
uint8_t cls, gone, taken;
|
|
} Ent;
|
|
|
|
typedef struct {
|
|
const char *name;
|
|
uint64_t cap; // bytes
|
|
uint32_t hold; // s
|
|
} Policy;
|
|
|
|
typedef struct {
|
|
uint64_t remade, remade_bytes, dropped_age, dropped_bound, peak, end, at_burst_end;
|
|
} Result;
|
|
|
|
#define MAXE 400000
|
|
static Ent ents[MAXE];
|
|
static uint32_t nents;
|
|
static uint32_t backs[MAXE], nbacks; // the entries the game takes back, by the second it does
|
|
|
|
static int back_cmp(const void *a, const void *b) {
|
|
uint32_t x = ents[*(const uint32_t *)a].back, y = ents[*(const uint32_t *)b].back;
|
|
return x < y ? -1 : x > y;
|
|
}
|
|
|
|
// A stream: `burst_s` seconds of area changes at burst_mbs MiB/s of releases the game keeps released, then calm play to
|
|
// end_s at calm_mbs. Sizes 0.1 to 1.9 MiB (the Balanced session held 3946 MiB in 7754 entries, half a MiB each); a
|
|
// tenth hold content only the GPU made. A share `back_pct` per thousand comes back, after a time drawn from the
|
|
// logs' buckets past 2 s (2-10 s and 10-60 s about as often); `late` adds take-backs late_s after the release.
|
|
static void make_stream(uint32_t burst_s, double burst_mbs, uint32_t end_s, double calm_mbs, uint32_t back_pml,
|
|
uint32_t late_s) {
|
|
nents = 0;
|
|
double carry = 0;
|
|
for (uint32_t s = 1; s < end_s; s++) {
|
|
carry += (s <= burst_s ? burst_mbs : calm_mbs) * 1048576.0;
|
|
while (carry > 0 && nents < MAXE) {
|
|
Ent *e = &ents[nents++];
|
|
memset(e, 0, sizeof *e);
|
|
e->rel = s, e->bytes = (uint32_t)(MB(1) / 10 + rnd((uint32_t)MB(18) / 10));
|
|
e->cls = rnd(10) == 0 ? TRL_GPU : TRL_STORAGE;
|
|
if (rnd(1000) < back_pml) e->back = s + (rnd(2) ? 2 + rnd(8) : 10 + rnd(50));
|
|
if (late_s && rnd(1000) < back_pml) e->back = s + late_s;
|
|
carry -= e->bytes;
|
|
}
|
|
}
|
|
nbacks = 0;
|
|
for (uint32_t i = 0; i < nents; i++)
|
|
if (ents[i].back) backs[nbacks++] = i;
|
|
qsort(backs, nbacks, sizeof *backs, back_cmp);
|
|
}
|
|
|
|
static Result run(const Policy *p, int room, uint32_t burst_s, uint32_t end_s) {
|
|
Result r = {0};
|
|
for (uint32_t i = 0; i < nents; i++) ents[i].gone = ents[i].taken = 0;
|
|
uint64_t held = 0;
|
|
uint32_t next = 0, head = 0; // next: the first entry not released yet; head: the oldest that may still be held
|
|
uint32_t nb = 0; // the next take-back in backs[]
|
|
for (uint32_t s = 1; s < end_s; s++) {
|
|
while (next < nents && ents[next].rel <= s) held += ents[next++].bytes;
|
|
for (; nb < nbacks && ents[backs[nb]].back <= s; nb++) { // the take-backs due now
|
|
Ent *e = &ents[backs[nb]];
|
|
if (e->gone) r.remade++, r.remade_bytes += e->bytes;
|
|
else held -= e->bytes;
|
|
e->taken = 1;
|
|
}
|
|
for (uint32_t i = head; p->hold && i < next; i++) { // drop_aged
|
|
Ent *e = &ents[i];
|
|
if (e->gone || e->taken) continue;
|
|
if (s - e->rel < p->hold) break; // in release order: the rest is younger
|
|
if (trl_aged(p->hold, e->cls, s - e->rel)) e->gone = 1, held -= e->bytes, r.dropped_age++;
|
|
}
|
|
uint64_t keep = trl_keep(p->hold, p->cap, room);
|
|
for (uint32_t i = head; i < next && held > keep; i++) { // drop_stale, oldest first, released 2 s ago or more
|
|
Ent *e = &ents[i];
|
|
if (e->gone || e->taken || s - e->rel < 2) continue;
|
|
e->gone = 1, held -= e->bytes, r.dropped_bound++;
|
|
}
|
|
while (head < next && (ents[head].gone || ents[head].taken)) head++;
|
|
if (held > r.peak) r.peak = held;
|
|
if (s == burst_s) r.at_burst_end = held;
|
|
}
|
|
r.end = held;
|
|
return r;
|
|
}
|
|
|
|
static void print_result(const char *scenario, const Policy *p, int room, const Result *r) {
|
|
printf(" %-34s %-12s room %d: made again %5llu (%6.0f MiB), dropped by age %6llu, by the bound %6llu; held at "
|
|
"the burst's end %5.0f MiB, peak %5.0f MiB, at the end %5.0f MiB\n",
|
|
scenario, p->name, room, (unsigned long long)r->remade, r->remade_bytes / 1048576.0,
|
|
(unsigned long long)r->dropped_age, (unsigned long long)r->dropped_bound, r->at_burst_end / 1048576.0,
|
|
r->peak / 1048576.0, r->end / 1048576.0);
|
|
}
|
|
|
|
static void session_checks(void) {
|
|
// RTX 5090, 31 GiB budget: the size bound a quarter of it (4008 MiB, the logs' figure)
|
|
const Policy old_low = {"old low", MB(1024), 0}, balanced = {"old balanced", MB(4008), 0},
|
|
low = {"new low", MB(4008), TRL_HOLD_S};
|
|
// an 8 GiB card: bound 1.9 GiB, no room in play
|
|
const Policy b8 = {"old balanced", MB(1920), 0}, l8 = {"new low", MB(1920), TRL_HOLD_S},
|
|
o8 = {"old low", MB(1024), 0};
|
|
struct { const char *name; uint32_t burst_s; double burst_mbs; uint32_t end_s; double calm_mbs; } sc[] = {
|
|
// the Balanced session's second line: 3.2 GiB released and kept within 35 s; then 10 minutes in one area
|
|
{"area changes 70 s, calm 10 min", 70, 93.0, 670, 1.0},
|
|
// a long session: an area change every few minutes, shown as a slower stream throughout
|
|
{"steady 20 MiB/s for 20 min", 1200, 20.0, 1500, 0.5},
|
|
// a teleport tour: 4 minutes at the burst rate, past twice the bound
|
|
{"teleports 4 min, calm 10 min", 240, 93.0, 840, 1.0},
|
|
};
|
|
printf("simulated sessions (made again: taken back after its drop; MiB: own memory of released textures)\n");
|
|
for (size_t i = 0; i < sizeof sc / sizeof *sc; i++) {
|
|
rng_state = 0x9e3779b97f4a7c15ull + i;
|
|
make_stream(sc[i].burst_s, sc[i].burst_mbs, sc[i].end_s, sc[i].calm_mbs, 20, 0);
|
|
Result rl = run(&low, 1, sc[i].burst_s, sc[i].end_s), rb = run(&balanced, 1, sc[i].burst_s, sc[i].end_s);
|
|
Result ro = run(&old_low, 1, sc[i].burst_s, sc[i].end_s);
|
|
Result rl8 = run(&l8, 0, sc[i].burst_s, sc[i].end_s), rb8 = run(&b8, 0, sc[i].burst_s, sc[i].end_s);
|
|
Result ro8 = run(&o8, 0, sc[i].burst_s, sc[i].end_s);
|
|
print_result(sc[i].name, &old_low, 1, &ro), print_result("", &balanced, 1, &rb), print_result("", &low, 1, &rl);
|
|
print_result(" same on an 8 GiB card", &o8, 0, &ro8), print_result("", &b8, 0, &rb8);
|
|
print_result("", &l8, 0, &rl8);
|
|
// with room the new default makes nothing again, unless a burst passes twice the bound within the hold
|
|
uint64_t total = 0;
|
|
for (uint32_t k = 0; k < nents && ents[k].rel <= sc[i].burst_s; k++) total += ents[k].bytes;
|
|
if (total < 2 * low.cap - MB(256))
|
|
CHECK(rl.remade == 0, "%s: new low made %llu again", sc[i].name, (unsigned long long)rl.remade);
|
|
// never more made again than the old Balanced, on either card
|
|
CHECK(rl.remade <= rb.remade && rl8.remade <= rb8.remade, "%s: new low %llu/%llu against balanced %llu/%llu",
|
|
sc[i].name, (unsigned long long)rl.remade, (unsigned long long)rl8.remade, (unsigned long long)rb.remade,
|
|
(unsigned long long)rb8.remade);
|
|
// what it holds at the end: what the last hold released, far under the old Balanced's
|
|
CHECK(rl.end <= rb.end / 2 || rb.end < MB(256), "%s: new low holds %.0f MiB at the end, balanced %.0f",
|
|
sc[i].name, rl.end / 1048576.0, rb.end / 1048576.0);
|
|
// the old low's 1 GiB made textures again in every burst, as the log showed
|
|
CHECK(ro.remade > 0, "%s: old low made nothing again", sc[i].name);
|
|
CHECK(!rl.dropped_bound || total >= 2 * low.cap - MB(256), "%s: new low dropped %llu by the bound with room",
|
|
sc[i].name, (unsigned long long)rl.dropped_bound);
|
|
}
|
|
// a game that takes textures back late: at 150 s nothing goes before (hold 180), at 200 s the hold costs
|
|
rng_state = 7;
|
|
make_stream(70, 93.0, 670, 1.0, 20, 150);
|
|
Result r150 = run(&low, 1, 70, 670);
|
|
CHECK(r150.remade == 0, "take-backs at 150 s: new low made %llu again", (unsigned long long)r150.remade);
|
|
rng_state = 7;
|
|
make_stream(70, 93.0, 670, 1.0, 20, 200);
|
|
Result r200 = run(&low, 1, 70, 670);
|
|
CHECK(r200.remade > 0, "take-backs at 200 s: the hold should have dropped some first");
|
|
printf(" late take-backs: at 150 s made again %llu, at 200 s %llu (the hold is %u s)\n",
|
|
(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;
|
|
}
|