From 87bb179c78e3a38d4bf2a877c7bf3967fa26e65a Mon Sep 17 00:00:00 2001 From: chrg6 Date: Sat, 14 Nov 2026 08:33:01 -0500 Subject: [PATCH] Keep released textures for one minute and up to a thirty-second of the video memory budget. The textures the game lets go of were kept for three minutes and could hold a quarter of the card's budget, up to 8 GiB with room. A run through Hyrule Field shows what that bought and what it cost (RTX 5090, 5120x2160 with DLSS Performance, about three minutes of running; the game keeps 4.8 GiB of textures live and loads about 190 a second): the released textures held 5.3 GiB on average and the video memory in use went from 11.1 to 17.9 GiB, while the game took back 1,494 MiB of them within 10 s of the release, 171 MiB within a minute and 10 MiB later, beside 33,000 textures it loaded anew. A shorter hold alone gives nothing back: with 20 s the released textures fell to 0.5 GiB and the free space inside the allocator's blocks rose to 4.9 GiB, since a block returns to the driver only when it is empty. What the released textures hold at their fullest is what the session costs, so the bound is the lever. The bound is now a thirty-second of the budget, 384 MiB to 1 GiB (vk_tex_release.h trl_cap_mb), still doubled while the card and the commit have room, and the hold 60 s, 120 for content only the GPU made. The same run with the new defaults: 13.2 GiB in use on average against 16.7, 13.8 at most against 17.9, 532 MiB of textures made again over the run, 77 fps against 75. An 8 GiB card keeps 384 MiB where it kept up to 1.9 GiB. tests/tex_release_check checks the bound by the budget and the sessions it simulates against the rule before. Compaction (tex_compact) and 16 MiB blocks were tried on the same run and gave back 0.7 and 1.2 GiB; neither is changed here. Co-Authored-By: Claude Fable 5.2 --- AUDIT.md | 5 +- docs/architecture.md | 8 ++- docs/releases/v0.10.2.md | 7 ++ nx2navi.ini.example | 14 ++-- runtime/cfg.c | 2 +- runtime/vk_tex_release.h | 39 ++++++++--- runtime/vk_texture.c | 42 +++++------- tests/tex_release_check.c | 135 +++++++++++++++++++++++++------------- 8 files changed, 158 insertions(+), 94 deletions(-) diff --git a/AUDIT.md b/AUDIT.md index 4c63ec93..ca16a5bf 100644 --- a/AUDIT.md +++ b/AUDIT.md @@ -1163,8 +1163,9 @@ back. **Texture cache eviction.** -- *Release overflow.* `sweep_released`: `RELEASED_MAX` 2048 entries, and `NX2NAVI_TEX_RELEASED_MB` (default RAM/32 - clamped 512-2048 MiB, raised by budget/4 up to 8192). +- *Release overflow.* `sweep_released`: `RELEASED_MAX` 2048 entries, and `NX2NAVI_TEX_RELEASED_MB` (default + budget/32 clamped 384-1024 MiB since 0.10.2, `vk_tex_release.h` `trl_cap_mb`; before it RAM/32 clamped 512-2048 + MiB, raised by budget/4 up to 8192). - *Stale.* `drop_stale`: `STALE_FRAMES` 120 presents, `GPU_STALE_FRAMES` 1800. - *Overwritten.* `drop_overwritten`: `OVERWRITTEN_KEEP` 600. - *VRAM pressure.* `PRESSURE_STEP` 32 per check. diff --git a/docs/architecture.md b/docs/architecture.md index b94aa625..0a26c142 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -391,10 +391,12 @@ keeps per-core data without locks and relies on that, so the host models it (`ru by their growth since play began and marks those that only climbed (`mem_growth.h`). - Released textures by age: the game releases a texture when what uses it leaves the view and takes it back when that returns; the cache keeps the released image so that the take-back finds it. In play the take-backs came within a - minute of the release or never, so a released texture goes once it has gone unused for a hold of three minutes, six + minute of the release or never, so a released texture goes once it has gone unused for a hold of one minute, two for content only the GPU made (`vk_texture.c` `drop_aged`, `vk_tex_release.h`, `tex_released_hold`). The size bound - (a quarter of the video memory budget) stays as the safety net for a burst of area changes, and young textures may - pass it, up to twice it, while the card and the system's commit have room. Times are wall seconds from a clock of + (a thirty-second of the video memory budget, 384 MiB to 1 GiB) is what the cache costs in video memory: the + allocator's blocks go back to the driver only when empty, so what the released textures hold at their fullest + stays in use for the session. Young textures may pass it, up to twice it, while the card and the system's commit + have room. Times are wall seconds from a clock of the presents, not present counts. What a switched-off upscaler or SMAA held is freed after it has gone unused for a while (`vk_dlaa.c` `idle_release`, `vk_fsr.c` `idle_note`, `vk_aa.c` `work_idle`). What DLSS or FSR lets go of at a change of size or mode (a network, an FSR context, a work image) goes once the GPU is past the last submission that diff --git a/docs/releases/v0.10.2.md b/docs/releases/v0.10.2.md index a7112630..437e729f 100644 --- a/docs/releases/v0.10.2.md +++ b/docs/releases/v0.10.2.md @@ -23,6 +23,13 @@ cards). their video memory is the PC's own memory. With a card filled to the room of an 8 GB one, a tester measured 29.6 fps with *Full* and 42.8 with *Reduced* on the same tour, and a handheld that got *Full* closed when a save was loaded. A texture detail you chose yourself stays as you set it. +- **Less video memory held in reserve.** The game lets go of textures as you move and takes a few of them back; the + ones it let go of were kept for three minutes and could hold a quarter of the card's video memory (up to 8 GB), and + what they held at their fullest stayed in use for the rest of the session. They are now kept for one minute and may + hold a thirty-second of the card's video memory, 384 MB to 1 GB (`tex_released_hold`, `tex_released_mb`). Measured + on a run through Hyrule Field at 5120x2160 with DLSS Performance on an RTX 5090: 13.2 GB in use on average where + it was 16.7, 13.8 at most where it was 17.9, at the same frame rate. An 8 GB card keeps 384 MB of such textures + where it kept up to 1.9 GB. ## Removed diff --git a/nx2navi.ini.example b/nx2navi.ini.example index b3ae2859..e0b2f520 100644 --- a/nx2navi.ini.example +++ b/nx2navi.ini.example @@ -416,14 +416,16 @@ # left. In play the game took released textures back within a minute or not at all; one taken back after its drop is # made again and shows its stand-in for a frame. A texture whose content only the GPU made waits twice as long, and one # the GPU drew into since its last copy back to game memory stays. 0: released textures stay up to tex_released_mb for -# the whole session (as before 0.10). Default: 180. Read at start. -# tex_released_hold = 180 +# the whole session (as before 0.10). Default: 60 (180 until 0.10.2). Read at start. +# tex_released_hold = 60 # Megabytes the released textures may hold (0: no bound). Past it the oldest go even when young; while tex_released_hold # is on, young ones may stay past it, up to twice it, as long as video memory and the system's commit have room. -# Default: a quarter of the video memory budget where the PC has an eighth of its memory to match, at least a -# thirty-second of the PC's memory (512 to 2048), at most 8192. 1024 (the low of the 0.10 previews) holds less, but -# textures the game takes back after their drop are made again and show their stand-in for a frame. Read at start. -# tex_released_mb = 4096 +# What they hold at their fullest is video memory the game keeps for the rest of the session, so this is what the cache +# costs. Default: a thirty-second of the video memory budget, at least 384 and at most 1024 (until 0.10.2 a quarter of +# the budget, up to 8192: on a run through Hyrule Field that held 3.3 GB more video memory and saved one texture in +# thirty from being loaded again). A larger value keeps more textures the game may take back, which are otherwise made +# again and show their stand-in for a frame. Read at start. +# tex_released_mb = 1024 # Texture compaction (experimental). Video memory is taken in blocks (vma_block_mb), and a block goes back to the # driver only once everything in it is freed: after a long session the blocks hold textures scattered between holes, # and the game holds a gigabyte or two of video memory it does not use. 1: textures no shader sampled for ten seconds diff --git a/runtime/cfg.c b/runtime/cfg.c index 3302ca10..f75f19fa 100644 --- a/runtime/cfg.c +++ b/runtime/cfg.c @@ -424,7 +424,7 @@ static const CfgMeta metas[] = { {"ASTC", "auto", "auto|gpu|native|cpu", RESTART}, {"MEMORY", "auto", "auto|import|mirror|verify", RESTART}, {"TEX_ALIAS", "1", "0|1", RESTART}, {"DEPTH_INIT_INLINE", "1", "0|1", RESTART}, {"DEPTH_ALIAS_LOG", "0", "0|1", RESTART}, {"TEX_DISUSE", "1", "0|1|2", RESTART}, {"TEX_DISUSE_EVICT", "0", "0|1", RESTART}, {"TEX_GEN_UNDO", "1", "0|1", RESTART}, {"TEX_REUSE", "1", "0|1", RESTART}, {"TEX_SPLIT_FILL", "1", "0|1|2", RESTART}, {"TEX_VRAM_PRESSURE", "80", "0..100", RESTART}, // vk_texture.c drop_aged: seconds a released texture is kept unused (vk_tex_release.h) - {"TEX_RELEASED_HOLD", "180", "0..900", RESTART}, {"ASTC_BC", "", "0|1", RESTART}, {"ZSTD_NATIVE", "1", "0|1", RESTART}, {"ZSTD_CHECK", "0", "0|1", RESTART}, + {"TEX_RELEASED_HOLD", "60", "0..900", RESTART}, {"ASTC_BC", "", "0|1", RESTART}, {"ZSTD_NATIVE", "1", "0|1", RESTART}, {"ZSTD_CHECK", "0", "0|1", RESTART}, {"TEX_CLOBBER", "0", "0|1", RESTART}, // the switch of the low-end GPU mode, which is gone: still a known key, and it does nothing (lowend_set, below) {"LOW_END_GPU", "0", "0|1", RESTART}, diff --git a/runtime/vk_tex_release.h b/runtime/vk_tex_release.h index 81ed18a0..6181a76d 100644 --- a/runtime/vk_tex_release.h +++ b/runtime/vk_tex_release.h @@ -14,29 +14,50 @@ // never taken back. // // So a released texture goes once it has gone unused for the hold (TRL_HOLD_S by default, NX2NAVI_TEX_RELEASED_HOLD), -// three times the longest gap seen, whether or not the bound is reached: the cache then holds what the last few -// minutes released, not what the bound allows, and it empties within minutes of the last area change. Unused counts +// the longest gap seen, whether or not the bound is reached: the cache then holds what the last minute released, not +// what the bound allows, and it empties within a minute or two of the last area change. Unused counts // from the release or from the last time a shader sampled the released image (a finalized slot may show it), whichever // is later. The hold is the same for a texture uploaded from the game's memory and for an ASTC one the GPU decodes: // either made again shows its stand-in for a frame, the decode only costs more of the render thread. A texture whose // content only the GPU made (a render target) cannot be made again at all, it comes back empty: it waits twice the // hold. // -// The size bound (released_cap_bytes, a quarter of the video memory budget by default) stays as the safety net it was: -// past it the oldest released textures go even when young. With the hold on, young ones past it are kept while the card -// and the system have room (trl_room), up to TRL_ROOM_TIMES the bound: the old Balanced dropped 31 young ones in a -// burst of area changes at its bound of 4 GiB on a card with 20 GiB free, and each was made again within a minute. The -// hold empties that excess within minutes, so the room it takes is temporary; without the hold (tex_released_hold = 0) the -// bound acts as before. +// The size bound (trl_cap_mb: a thirty-second of the video memory budget, TRL_CAP_MIN_MB to TRL_CAP_MAX_MB) is what +// decides how much video memory the cache costs: past it the oldest released textures go even when young, and with the +// hold on, young ones past it are kept while the card and the system have room (trl_room), up to TRL_ROOM_TIMES the +// bound. Until 0.10.2 the hold was 180 s and the bound a quarter of the budget (4 GiB on a 32 GiB card, 8 with room). +// A run through Hyrule Field showed what that bought (2026-11-14, RTX 5090, 5120x2160 with DLSS Performance, about +// three minutes of running; the game keeps 4.8 GiB of textures live there and lets go of and loads about 190 a +// second): the released textures held 5.3 GiB on average and 6.4 at most, the video memory in use went from 11.1 to +// 17.9 GiB, and of what they held the game took back 1,494 MiB within 2 to 10 s of the release, 171 MiB within 10 to +// 60 s and 10 MiB later, beside 33,000 textures it loaded anew in that time. The memory does not come back when the +// cache shrinks either: a block of the allocator goes back to the driver only when its last allocation went, so with +// a hold of 20 s in the same run the released textures fell to 0.5 GiB while the free space inside the blocks rose to +// 4.9 GiB, and the driver's figure stayed at 15.8. What the cache may hold at its fullest is therefore what the +// session costs for good. With a bound of 1 GiB (2 with room) the same run held 13.4 GiB on average and 14.8 at most, +// 3.3 GiB less, made 1,170 MiB of textures again over the run and kept its frame rate (78 against 75 fps, within what +// two runs differ by). #pragma once #include enum { TRL_STORAGE, TRL_GPU }; // content made again from the game's memory; content only the GPU made -#define TRL_HOLD_S 180 // the default hold, in seconds +#define TRL_HOLD_S 60 // the default hold, in seconds #define TRL_HOLD_MAX_S 900 // the longest hold the setting takes: twice it fits the clock below #define TRL_ROOM_TIMES 2 // with room, the released textures may hold this many times the bound #define TRL_ROOM_MARGIN 10 // room: the card at least this many points of its budget under the VRAM pressure +#define TRL_CAP_SHARE 32 // the default bound: this share of the video memory budget, +#define TRL_CAP_MIN_MB 384 // at least this much (the render targets a frame lets go of and takes back the next + // frame come to about 70 MiB at 2560x1080; they are not dropped, but they count), +#define TRL_CAP_MAX_MB 1024 // and at most this much +#define TRL_CAP_UNKNOWN_MB 512 // without a budget (no VK_EXT_memory_budget and no heap size) + +// The default bound in MiB for a video memory budget in MiB (0: not known). +static inline uint32_t trl_cap_mb(uint64_t budget_mb) { + if (!budget_mb) return TRL_CAP_UNKNOWN_MB; + uint64_t mb = budget_mb / TRL_CAP_SHARE; + return (uint32_t)(mb < TRL_CAP_MIN_MB ? TRL_CAP_MIN_MB : mb > TRL_CAP_MAX_MB ? TRL_CAP_MAX_MB : mb); +} // The hold of a class, in seconds; 0: no hold. static inline uint32_t trl_hold(uint32_t hold_s, int cls) { return cls == TRL_GPU ? hold_s * 2 : hold_s; } diff --git a/runtime/vk_texture.c b/runtime/vk_texture.c index a26d4ee7..1808f9da 100644 --- a/runtime/vk_texture.c +++ b/runtime/vk_texture.c @@ -2579,11 +2579,10 @@ void vk_texture_pool_ctx(int on) { pool_ctx = on; } static int idle(const Tex *e, uint64_t now); -// NX2NAVI_TEX_RELEASED_MB (default a thirty-second of the installed memory, 512 MiB to 2 GiB, or more by the card's -// budget, released_cap_bytes; 0 = no limit) bounds the own memory of the released entries. With the hold on (drop_aged, -// the default) what they hold is what the last minutes released, and the bound is the safety net for a burst of area -// changes within the hold; sweep_pressure lets young entries past it while the card and the system have room -// (vk_tex_release.h trl_keep). Before the hold the bound was all that limited them: RELEASED_MAX alone kept 5-7 GiB of +// NX2NAVI_TEX_RELEASED_MB (default a thirty-second of the video memory budget, 384 MiB to 1 GiB, released_cap_bytes; +// 0 = no limit) bounds the own memory of the released entries. With the hold on (drop_aged, the default) they hold +// what the last minute released, up to the bound; sweep_pressure lets young entries past it while the card and the +// system have room (vk_tex_release.h trl_keep). Before the hold the bound was all that limited them: RELEASED_MAX alone kept 5-7 GiB of // released render targets and decoded textures after an hour of play at a 2160p game resolution, device memory the // driver also charges to the system's commit (the process's private bytes reached 31 GiB). A sixteenth (1 to 4 GiB) // still let them hold 1.3 GiB on a 32 GB PC whose commit then ran out at an area load (device lost); only entries @@ -2616,7 +2615,6 @@ static int idle(const Tex *e, uint64_t now); #define COMMIT_LOW_KEEP ((uint64_t)256 << 20) #define PROC_COMMIT_MIN_MB 24576 static uint64_t released_cap; // bytes; 0 = not read yet, UINT64_MAX = no limit -#define RELEASED_CAP_MAX_MB 8192 static long long commit_free_mb = -2; // -2: not read yet, -1: the default static long long proc_commit_mb; // this process's bound in MB; 0 = none static int commit_low, commit_logs; // commit_low: either of the two below at the last check @@ -2633,29 +2631,19 @@ static uint64_t released_cap_bytes(void) { const char *by = ""; // which term set the bound, for the line below if (s && *s) mb = atoll(s); else { - float used, total, proc; - plat_mem_info(&used, &total, &proc); - mb = total > 0 ? (long long)(total / 32) : 1024; - mb = mb < 512 ? 512 : mb > 2048 ? 2048 : mb; - by = total > 0 ? ", a thirty-second of the installed memory (512 MiB to 2 GiB)" - : ", the default without the installed memory's size"; - // By the card too: a quarter of its budget where the PC has an eighth of its memory to match, up to - // RELEASED_CAP_MAX_MB. The game lets go of the textures of what leaves the view and takes them back when - // it returns; walking through a village the released ones passed 1 GiB within two or three minutes, and - // from then on each release dropped an older one that was decoded and encoded again on its way back, - // hundreds a second, on a card with 30 GiB free. The two pressure rules still come first: released - // entries go before anything else above NX2NAVI_TEX_VRAM_PRESSURE of the budget, and the bound falls to - // COMMIT_LOW_KEEP while the commit is low. + // By the card: a thirty-second of its budget, 384 MiB to 1 GiB (vk_tex_release.h trl_cap_mb, which has the + // run that set it). What the released textures hold at their fullest stays in use for the rest of the + // session, as free space inside the allocator's blocks once they are dropped, so the bound is the price of + // the cache; until 0.10.2 it was a quarter of the budget, up to 8 GiB. The two pressure rules still come + // first: released entries go before anything else above NX2NAVI_TEX_VRAM_PRESSURE of the budget, and the + // bound falls to COMMIT_LOW_KEEP while the commit is low. uint64_t vused = 0, vbudget = 0; int exact = 0; - if (vk_mem_vram(&vused, &vbudget, &exact) && total > 0) { - long long by_card = (long long)(vbudget >> 22), by_ram = (long long)(total / 8); // a quarter, in MiB - long long more = by_card < by_ram ? by_card : by_ram; - const char *more_by = by_card <= by_ram ? ", a quarter of the video memory budget" - : ", an eighth of the installed memory"; - if (more > RELEASED_CAP_MAX_MB) more = RELEASED_CAP_MAX_MB, more_by = ", the largest the rule by the card gives"; - if (more > mb) mb = more, by = more_by; - } + int known = vk_mem_vram(&vused, &vbudget, &exact) && vbudget; + mb = trl_cap_mb(known ? vbudget >> 20 : 0); + by = !known ? ", the default without the card's budget" + : mb == TRL_CAP_MAX_MB ? ", the most the rule by the card gives" + : mb == TRL_CAP_MIN_MB ? ", the least the rule by the card gives" : ", a thirty-second of the video memory budget"; } released_cap = mb > 0 ? (uint64_t)mb << 20 : UINT64_MAX; if (mb > 0) rt_log("vk: released textures keep at most %lld MiB of their own memory%s (NX2NAVI_TEX_RELEASED_MB)", mb, diff --git a/tests/tex_release_check.c b/tests/tex_release_check.c index c7cba906..59b11fba 100644 --- a/tests/tex_release_check.c +++ b/tests/tex_release_check.c @@ -5,7 +5,7 @@ // 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). +// by the figures of three sessions' logs on an RTX 5090 (vk_tex_release.h). #include #include #include @@ -62,7 +62,7 @@ static void clock_checks(void) { // ---- 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_hold(TRL_HOLD_S, TRL_STORAGE) == 60 && trl_hold(TRL_HOLD_S, TRL_GPU) == 120, "holds 60 and 120 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"); @@ -80,6 +80,23 @@ static void rule_checks(void) { 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 default bound by the card's budget (MiB): a thirty-second of it, 384 MiB to 1 GiB + CHECK(trl_cap_mb(0) == TRL_CAP_UNKNOWN_MB, "no budget known: %u", trl_cap_mb(0)); + CHECK(trl_cap_mb(31419) == 981, "RTX 5090 (budget 31419 MiB): %u", trl_cap_mb(31419)); + CHECK(trl_cap_mb(15450) == 482, "a 16 GiB card (budget 15450 MiB): %u", trl_cap_mb(15450)); + CHECK(trl_cap_mb(7680) == TRL_CAP_MIN_MB && trl_cap_mb(3900) == TRL_CAP_MIN_MB, "8 and 4 GiB cards: the least"); + CHECK(trl_cap_mb(48000) == TRL_CAP_MAX_MB && trl_cap_mb(UINT64_MAX) == TRL_CAP_MAX_MB, "a larger card: the most"); + uint32_t last = 0; + for (uint64_t b = 1; b <= 65536; b += 97) { + uint32_t c = trl_cap_mb(b); + CHECK(c >= last && c >= TRL_CAP_MIN_MB && c <= TRL_CAP_MAX_MB, "budget %llu MiB: bound %u after %u", + (unsigned long long)b, c, last); + last = c; + } + // what the released textures may hold at their fullest: under 2 GiB on a 32 GiB card with room (8 GiB by the + // rule before, a quarter of the budget and twice that), 384 MiB on an 8 GiB card in play (1.9 GiB before) + CHECK(trl_keep(TRL_HOLD_S, MB(trl_cap_mb(31419)), 1) < MB(2048), "the most on a 32 GiB card"); + CHECK(trl_keep(TRL_HOLD_S, MB(trl_cap_mb(7680)), 0) == MB(384), "the most on an 8 GiB card in play"); } // ---- the take-backs of the logs against the hold @@ -91,6 +108,11 @@ static const struct { const char *session; uint64_t n[5]; } TAKEN[] = { {"balanced, frame 7200", {120779, 91, 1, 0, 0}}, }; static const uint32_t EDGE[5] = {2, 10, 60, 300, UINT32_MAX}; +// The run through Hyrule Field that set the bound (2026-11-14, the rule before: 180 s, 4008 MiB and twice that with +// room): MiB and textures taken back by the time released, 2-10 s, 10-60 s, later; and the textures loaded anew +// meanwhile. +static const uint64_t FIELD_BACK_MB[3] = {1494, 171, 10}; +static const uint64_t FIELD_BACK_N[3] = {941, 331, 28}, FIELD_UPLOADS = 33118; static void taken_back_checks(void) { uint32_t longest = 0; @@ -98,10 +120,18 @@ static void taken_back_checks(void) { 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 + // nothing those logs took back would have gone past the hold: every take-back came before 60 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); + CHECK(!trl_aged(TRL_HOLD_S, cls, longest - 1) && trl_hold(TRL_HOLD_S, cls) >= longest, + "class %d: hold %u s against take-backs under %u s", cls, trl_hold(TRL_HOLD_S, cls), longest); + // the field run: what came back later than the hold is under a hundredth of what came back at all, and everything + // the cache gave back in three minutes is a twenty-fifth of what the game loaded anew in that time + uint64_t all_mb = FIELD_BACK_MB[0] + FIELD_BACK_MB[1] + FIELD_BACK_MB[2]; + uint64_t all_n = FIELD_BACK_N[0] + FIELD_BACK_N[1] + FIELD_BACK_N[2]; + CHECK(FIELD_BACK_MB[2] * 100 < all_mb, "field run: %llu of %llu MiB came back past a minute", + (unsigned long long)FIELD_BACK_MB[2], (unsigned long long)all_mb); + CHECK(all_n * 20 < FIELD_UPLOADS, "field run: %llu taken back beside %llu loaded", (unsigned long long)all_n, + (unsigned long long)FIELD_UPLOADS); } // ---- simulated sessions @@ -119,7 +149,7 @@ typedef struct { } Policy; typedef struct { - uint64_t remade, remade_bytes, dropped_age, dropped_bound, peak, end, at_burst_end; + uint64_t remade, remade_bytes, remade_quick, dropped_age, dropped_bound, peak, end, at_burst_end; } Result; #define MAXE 400000 @@ -168,7 +198,7 @@ static Result run(const Policy *p, int room, uint32_t burst_s, uint32_t end_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; + if (e->gone) r.remade++, r.remade_bytes += e->bytes, r.remade_quick += e->back - e->rel < 10; else held -= e->bytes; e->taken = 1; } @@ -201,59 +231,72 @@ static void print_result(const char *scenario, const Policy *p, int room, const } 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}; + // RTX 5090, 31 GiB budget: until 0.10.2 a quarter of it (4008 MiB, the logs' figure) and 180 s; now trl_cap_mb + const Policy was = {"until 0.10.2", MB(4008), 180}, now = {"0.10.2", MB(trl_cap_mb(31419)), TRL_HOLD_S}; + // an 8 GiB card: no room in play + const Policy was8 = {"until 0.10.2", MB(1920), 180}, now8 = {"0.10.2", MB(trl_cap_mb(7680)), TRL_HOLD_S}; 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 + // a teleport tour: 4 minutes at the burst rate {"teleports 4 min, calm 10 min", 240, 93.0, 840, 1.0}, + // the run through Hyrule Field: the game lets go of about 130 MiB a second for three minutes + {"field run 3 min, calm 2 min", 170, 130.0, 290, 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); + Result rn = run(&now, 1, sc[i].burst_s, sc[i].end_s), rw = run(&was, 1, sc[i].burst_s, sc[i].end_s); + Result rn8 = run(&now8, 0, sc[i].burst_s, sc[i].end_s), rw8 = run(&was8, 0, sc[i].burst_s, sc[i].end_s); + print_result(sc[i].name, &was, 1, &rw), print_result("", &now, 1, &rn); + print_result(" same on an 8 GiB card", &was8, 0, &rw8), print_result("", &now8, 0, &rn8); + uint64_t total = 0, came_back = 0; + for (uint32_t k = 0; k < nents; k++) total += ents[k].bytes, came_back += ents[k].back ? ents[k].bytes : 0; + // What the cache holds at its fullest is what the session costs in video memory: never more than what it may + // keep and what two seconds release (drop_stale leaves the entries of the last two seconds), on either card + uint64_t slack = MB((uint64_t)(2 * sc[i].burst_mbs) + 4); + CHECK(rn.peak <= trl_keep(now.hold, now.cap, 1) + slack, "%s: holds %.0f MiB at its fullest", sc[i].name, + rn.peak / 1048576.0); + CHECK(rn8.peak <= trl_keep(now8.hold, now8.cap, 0) + slack, "%s, 8 GiB card: holds %.0f MiB at its fullest", + sc[i].name, rn8.peak / 1048576.0); + // and never more than the rule before held; where that one filled up, less than half of it + CHECK(rn.peak <= rw.peak && rn8.peak <= rw8.peak, "%s: peak %.0f/%.0f MiB against %.0f/%.0f before", sc[i].name, + rn.peak / 1048576.0, rn8.peak / 1048576.0, rw.peak / 1048576.0, rw8.peak / 1048576.0); + if (rw.peak > MB(4096)) + CHECK(rn.peak * 2 < rw.peak, "%s: peak %.0f MiB, %.0f before", sc[i].name, rn.peak / 1048576.0, + rw.peak / 1048576.0); + // what it holds once play is calm is what the last hold released + CHECK(rn.end <= rw.end && rn.end <= MB((uint64_t)(sc[i].calm_mbs * 2 * TRL_HOLD_S) + 64), + "%s: holds %.0f MiB at the end, %.0f before", sc[i].name, rn.end / 1048576.0, rw.end / 1048576.0); + // A take-back within ten seconds of the release (turning round) finds its texture wherever ten seconds of + // releases fit in what the cache may keep; on the 32 GiB card that is every scenario here + if ((uint64_t)(sc[i].burst_mbs * 12) < (trl_keep(now.hold, now.cap, 1) >> 20)) + CHECK(rn.remade_quick == 0, "%s: %llu quick take-backs made again", sc[i].name, + (unsigned long long)rn.remade_quick); + if ((uint64_t)(sc[i].burst_mbs * 12) < (trl_keep(now8.hold, now8.cap, 0) >> 20)) + CHECK(rn8.remade_quick == 0, "%s, 8 GiB card: %llu quick take-backs made again", sc[i].name, + (unsigned long long)rn8.remade_quick); + // and what it makes again is never more than what the game takes back at all (a fiftieth of the stream here) + CHECK(rn.remade_bytes <= came_back && rn8.remade_bytes <= came_back && came_back * 40 < total, + "%s: made again %.0f/%.0f MiB of %.0f MiB that came back", sc[i].name, rn.remade_bytes / 1048576.0, + rn8.remade_bytes / 1048576.0, came_back / 1048576.0); } - // a game that takes textures back late: at 150 s nothing goes before (hold 180), at 200 s the hold costs + // a game that takes textures back late, in calm play where the bound is far: at 50 s nothing goes before (the + // hold is 60 s), at 90 s the hold has dropped them 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); + make_stream(0, 0.0, 400, 5.0, 20, 50); + Result r50 = run(&now, 1, 0, 400); + CHECK(r50.remade == 0 && r50.dropped_bound == 0, "take-backs at 50 s: made %llu again, %llu dropped by the bound", + (unsigned long long)r50.remade, (unsigned long long)r50.dropped_bound); 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); + make_stream(0, 0.0, 400, 5.0, 20, 90); + Result r90 = run(&now, 1, 0, 400); + CHECK(r90.remade > 0, "take-backs at 90 s: the hold should have dropped some first"); + printf(" late take-backs in calm play: at 50 s made again %llu, at 90 s %llu (the hold is %u s)\n", + (unsigned long long)r50.remade, (unsigned long long)r90.remade, TRL_HOLD_S); } // ---- past the hard count (trl_hard_class, trl_hard_pick)