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)