Kakariko at night on a 4 GB GTX 1650 Ti: textures take twice their budget even as BC3, ~6 fps with black frames, then crash at hookshot.exe+0x87c2d0 #595

Closed
opened 2026-11-08 13:45:33 +00:00 by wasav1 · 5 comments
wasav1 commented 2026-11-08 13:45:33 +00:00 (Migrated from github.com)

Note: the "Game problem" template doesn't show in the New issue list (its link opens a blank issue), so I filled in its fields by hand below.

Hookshot version

fast-updates (source 4187feb5). The honest-eta changelog doesn't touch VRAM.

What kind of problem

Low fps or stutter, and a crash

What happened

Kakariko Village at night (outside, and inside the house with the bed) is unplayable on my 4 GB card. The rest of the game runs well (60–120 fps in Kokiri Forest, Hyrule Field and indoor areas).

  • With the defaults, fps drops from ~50 to 11, then 3–4, and the game crashes:
    CRASH: exception 0xc0000005 (write 0x100000000000, guest 0) at hookshot.exe+0x87c2d0
    CRASH: thread BaseProcCreateAndDeleteThread
    
    I got the same crash in the same place on the low-vram release, when VRAM reached 4512 MB of 3277 MB allowed.
  • With RX_VK_KEEP_RETIRED=0, the Minimum (4GB) preset, 50 % resolution and frame generation Off, it no longer crashes. But it runs at ~6.5 fps with frequent black frames (most of the screen goes black for a moment).

Texture compression is active (all large ASTC textures kept as BC3). Even so, this scene's textures take about twice their budget, VRAM goes ~900 MB over what the driver allows, and the GPU spends 150 ms a frame with 0 shader/pipeline waits. This looks like the driver paging you found in #519 (Goron City, 4 GB RTX 3050), but here it happens with compression already on.

Defaults RX_VK_KEEP_RETIRED=0, FG Off
VRAM 4133 MB used of 3262 allowed 4154 MB used of 3261 allowed
Textures 1571 MB (budget 602 MB) 1883 MB (budget 892 MB)
Imports 2672 MB retired still imported, 0 given back 1520 MB retired given back
GPU time 79.9 ms a frame, 11 → 3.3 fps 151 ms a frame, 6.5 fps
Result crash no crash, ~6 fps, black frames

With frame generation 2x it was the same: 157 ms GPU time, 6.3 fps.

Expected: Kakariko staying inside the VRAM budget like the rest of the game.

Where in the game

Kakariko Village at night: walking in from Hyrule Field, and inside the house with the bed

How often

Every time

Graphics card and driver version

GTX 1650 Ti Laptop 4 GB, NVIDIA 610.88

CPU, RAM and operating system

i5-10300H, 24 GB, Windows 11 (build 26300), ASUS TUF F15

Changes from the defaults

  • Quality preset Minimum (4GB): resolution 50 %, Lanczos, sharpness 50 %
  • Frame generation tested Off and 2x (same result)
  • Handheld profile On, Text speed Instant, Controller in background On
  • Second log run with RX_VK_KEEP_RETIRED=0 (environment variable, set in a .bat launcher)
  • One RomFS mod: Xbox button icons

Other software hooked into the game

Yes: MSI Afterburner + RTSS overlay. The first crash (on low-vram) happened before I installed them.

hookshot_log.txt

crash = fast-updates with defaults; 6fps = fast-updates with RX_VK_KEEP_RETIRED=0, frame generation Off

hookshot_log_kakariko_crash.txt
hookshot_log_kakariko_6fps.txt

Image Image
Note: the "Game problem" template doesn't show in the New issue list (its link opens a blank issue), so I filled in its fields by hand below. ### Hookshot version fast-updates (source 4187feb5). The honest-eta changelog doesn't touch VRAM. ### What kind of problem Low fps or stutter, and a crash ### What happened Kakariko Village at night (outside, and inside the house with the bed) is unplayable on my 4 GB card. The rest of the game runs well (60–120 fps in Kokiri Forest, Hyrule Field and indoor areas). - **With the defaults**, fps drops from ~50 to 11, then 3–4, and the game crashes: ``` CRASH: exception 0xc0000005 (write 0x100000000000, guest 0) at hookshot.exe+0x87c2d0 CRASH: thread BaseProcCreateAndDeleteThread ``` I got the same crash in the same place on the `low-vram` release, when VRAM reached 4512 MB of 3277 MB allowed. - **With `RX_VK_KEEP_RETIRED=0`**, the Minimum (4GB) preset, 50 % resolution and frame generation Off, it no longer crashes. But it runs at **~6.5 fps with frequent black frames** (most of the screen goes black for a moment). Texture compression is active (`all large ASTC textures kept as BC3`). Even so, this scene's textures take about twice their budget, VRAM goes ~900 MB over what the driver allows, and the GPU spends 150 ms a frame with 0 shader/pipeline waits. This looks like the driver paging you found in #519 (Goron City, 4 GB RTX 3050), but here it happens with compression already on. | | Defaults | `RX_VK_KEEP_RETIRED=0`, FG Off | |---|---|---| | VRAM | 4133 MB used of 3262 allowed | 4154 MB used of 3261 allowed | | Textures | 1571 MB (budget 602 MB) | 1883 MB (budget 892 MB) | | Imports | 2672 MB retired still imported, 0 given back | 1520 MB retired given back | | GPU time | 79.9 ms a frame, 11 → 3.3 fps | 151 ms a frame, 6.5 fps | | Result | crash | no crash, ~6 fps, black frames | With frame generation 2x it was the same: 157 ms GPU time, 6.3 fps. **Expected:** Kakariko staying inside the VRAM budget like the rest of the game. ### Where in the game Kakariko Village at night: walking in from Hyrule Field, and inside the house with the bed ### How often Every time ### Graphics card and driver version GTX 1650 Ti Laptop 4 GB, NVIDIA 610.88 ### CPU, RAM and operating system i5-10300H, 24 GB, Windows 11 (build 26300), ASUS TUF F15 ### Changes from the defaults - Quality preset Minimum (4GB): resolution 50 %, Lanczos, sharpness 50 % - Frame generation tested Off and 2x (same result) - Handheld profile On, Text speed Instant, Controller in background On - Second log run with `RX_VK_KEEP_RETIRED=0` (environment variable, set in a .bat launcher) - One RomFS mod: Xbox button icons ### Other software hooked into the game Yes: MSI Afterburner + RTSS overlay. The first crash (on `low-vram`) happened before I installed them. ### hookshot_log.txt crash = fast-updates with defaults; 6fps = fast-updates with `RX_VK_KEEP_RETIRED=0`, frame generation Off [hookshot_log_kakariko_crash.txt](https://github.com/user-attachments/files/34544453/hookshot_log_kakariko_crash.txt) [hookshot_log_kakariko_6fps.txt](https://github.com/user-attachments/files/34544452/hookshot_log_kakariko_6fps.txt) <img width="1247" height="747" alt="Image" src="https://github.com/user-attachments/assets/38d9d391-fd15-4716-9896-2f696eee62e5" /> <img width="1288" height="754" alt="Image" src="https://github.com/user-attachments/assets/18176e41-427e-480b-aa02-cdd7cfaf669c" />
RufflezAU commented 2026-11-08 15:33:10 +00:00 (Migrated from github.com)

Same failure mode on a Steam Deck OLED (RADV VANGOGH, 14 GB unified, 6037 MB reported allowed), and I think the reason it takes twice the budget is that the eviction age is counted in presents.

In a first-party title's overworld at 4–6 fps:

vk: 9 textures retired (1619 left, 3868 MB)        peak 4005 MB against a 2200 MB budget
vk: a frame's replay time (ms): submit 183.49, draw 111.09, descriptors 88.90, texture lookups 100.91
vk: GPU time 69.1 ms a frame                        was 26.4 ms earlier in the same run
vk: the window is not shown (no image back in 100 ms)

Then Steam's UI restarted under the game (the OS itself did not reboot). No Not enough memory, no device lost — the driver was paging, as in #519.

texture_sweep evicts what has been unused for 60 presents over budget (RX_VK_EVICT_MIN_UNUSED) and 120 presents under it. That is 1–2 s at 60 fps, but 10–30 s at 4–6 fps, so the set grows far past the budget between sweeps. Counting the age in wall-clock milliseconds instead changes nothing at 60 fps and bounds it on a slow device:

  • Texture::last_ms, set where last_frame is set (create, texture_touch, texture_touch_quick)
  • the sweep's order and its age test on last_ms
  • RX_VK_EVICT_MIN_MS (default 1000; sweep 2000; the rendered keep 10 s / 60 s instead of 600/3600 presents)
// vk_texture.cpp, after frame_no
static uint64_t tex_now_ms() {
    using namespace std::chrono;
    return (uint64_t)duration_cast<milliseconds>(steady_clock::now().time_since_epoch()).count();
}
// in the sweep
for (auto& kv : textures) order.push_back({kv.second->last_ms, kv.second.get()});
std::sort(order.begin(), order.end(), [](const auto& a, const auto& b) { return a.first < b.first; });
const uint64_t now_ms = tex_now_ms();
...
if ((!sweep && total_bytes <= budget / 8 * 7) || now_ms - last < (sweep ? 2000 : 1000)) break;
const uint64_t keep_rendered_ms = known && used > allowed ? 10000 : 60000;
if (!overwritten && tex->gpu_written && !tex->released && !evict_rendered && now_ms - last < keep_rendered_ms) continue;

I have the full patch (vk_texture.cpp/.h, ~10 lines of real change) and the before/after logs. I can open a PR against main with it if that is easier to review.

Same failure mode on a Steam Deck OLED (RADV VANGOGH, 14 GB unified, 6037 MB reported allowed), and I think the reason it takes twice the budget is that the eviction age is counted in presents. In a first-party title's overworld at 4–6 fps: ``` vk: 9 textures retired (1619 left, 3868 MB) peak 4005 MB against a 2200 MB budget vk: a frame's replay time (ms): submit 183.49, draw 111.09, descriptors 88.90, texture lookups 100.91 vk: GPU time 69.1 ms a frame was 26.4 ms earlier in the same run vk: the window is not shown (no image back in 100 ms) ``` Then Steam's UI restarted under the game (the OS itself did not reboot). No `Not enough memory`, no device lost — the driver was paging, as in #519. `texture_sweep` evicts what has been unused for 60 presents over budget (`RX_VK_EVICT_MIN_UNUSED`) and 120 presents under it. That is 1–2 s at 60 fps, but **10–30 s at 4–6 fps**, so the set grows far past the budget between sweeps. Counting the age in wall-clock milliseconds instead changes nothing at 60 fps and bounds it on a slow device: - `Texture::last_ms`, set where `last_frame` is set (`create`, `texture_touch`, `texture_touch_quick`) - the sweep's order and its age test on `last_ms` - `RX_VK_EVICT_MIN_MS` (default 1000; sweep 2000; the rendered keep 10 s / 60 s instead of 600/3600 presents) ```cpp // vk_texture.cpp, after frame_no static uint64_t tex_now_ms() { using namespace std::chrono; return (uint64_t)duration_cast<milliseconds>(steady_clock::now().time_since_epoch()).count(); } // in the sweep for (auto& kv : textures) order.push_back({kv.second->last_ms, kv.second.get()}); std::sort(order.begin(), order.end(), [](const auto& a, const auto& b) { return a.first < b.first; }); const uint64_t now_ms = tex_now_ms(); ... if ((!sweep && total_bytes <= budget / 8 * 7) || now_ms - last < (sweep ? 2000 : 1000)) break; const uint64_t keep_rendered_ms = known && used > allowed ? 10000 : 60000; if (!overwritten && tex->gpu_written && !tex->released && !evict_rendered && now_ms - last < keep_rendered_ms) continue; ``` I have the full patch (vk_texture.cpp/.h, ~10 lines of real change) and the before/after logs. I can open a PR against main with it if that is easier to review.
wasav1 commented 2026-11-08 16:49:08 +00:00 (Migrated from github.com)

Update on v2.0.0 (same laptop, GTX 1650 Ti 4 GB, preset Minimum, 50%, FG 2x):

  • No crash and no black frames anywhere in Kakariko, including the house with the bed: the flicker and scene-change fixes worked here.
  • Kakariko outside: 26 fps average over the session (3 to 60). Interiors 31-62 fps. Inside the bed house: 16 fps, was 6.5 (GPU time 49 ms, was 151 ms).
  • VRAM is still over the budget in 54 of 56 log samples, peak 5046 MB of 3347 MB allowed. The drops to 8 fps outdoors line up with that, so @RufflezAU's wall-clock eviction would likely help here too. Happy to test it on this card.
Update on v2.0.0 (same laptop, GTX 1650 Ti 4 GB, preset Minimum, 50%, FG 2x): - No crash and no black frames anywhere in Kakariko, including the house with the bed: the flicker and scene-change fixes worked here. - Kakariko outside: 26 fps average over the session (3 to 60). Interiors 31-62 fps. Inside the bed house: 16 fps, was 6.5 (GPU time 49 ms, was 151 ms). - VRAM is still over the budget in 54 of 56 log samples, peak 5046 MB of 3347 MB allowed. The drops to 8 fps outdoors line up with that, so @RufflezAU's wall-clock eviction would likely help here too. Happy to test it on this card.
Locksmitch commented 2026-11-08 17:35:58 +00:00 (Migrated from github.com)

The same pattern on an 8 GB card, in case it helps: VRAM goes over what the driver allows while VMA's blocks hold more free space than the overage.

Hookshot version: v2.0.2 (hookshot v2.0.2 (source 998ef8f9)); the same in v2.0.0 and v2.0.1
What kind of problem: Low fps or stutter
Where in the game: Hyrule Castle Town, loaded from the autosave at the town gate
How often: Every run
Graphics card and driver: NVIDIA GeForce RTX 3070 8 GB (the driver allows 7.1-7.2 GB), driver 616.56
CPU, RAM and operating system: Intel Core i5-12600K, 32 GB DDR5-5600, Windows 11 (build 26100)
Changes from the defaults: Insert menu: Resolution 100%, Upscaling Catmull-Rom, Frame generation Off; fullscreen at 2560x1440. hookshot_env.txt: RX_VK_ASTC_BC_ALL=1, plus the test-run switches listed in my comment on #574.
Other software hooked into the game: None

In 8 runs of the same 4-minute route (v2.0.0 to v2.0.2), VRAM was over the allowed in about half of the vk: VRAM: reports, by 0.6-1.0 GB on average per run. In those same reports, 1.3-1.8 GB on average per run (up to 2.6 GB) was free inside VMA's blocks. From the v2.0.2 run with default settings:

vk: VRAM: 7716 MB used of 7157 MB allowed (2319 MB of it free in the blocks); textures 1979 MB (budget 1205 MB), 1437 textures

Textures peaked at 3.3-4.3 GB while the budget shrank to 0.5-2.8 GB in those reports; up to 2.0-2.2 GB of the textures are images the GPU drew, which are kept on purpose. Lowering the texture budget (RX_VK_VRAM_MB=2500, v2.0.1) cut the textures' peak to ~3.6 GB, but VRAM stayed over the allowed and fps did not change.

Since the free space inside the blocks is about twice the overage, giving it back (defragmentation, or dedicated allocations for the large images) might keep 8 GB cards within the budget without evicting anything more.

The logs are available if you want them.

The runs were scripted and the numbers collected with Claude Code.

The same pattern on an **8 GB** card, in case it helps: VRAM goes over what the driver allows while VMA's blocks hold more free space than the overage. **Hookshot version:** v2.0.2 (`hookshot v2.0.2 (source 998ef8f9)`); the same in v2.0.0 and v2.0.1 **What kind of problem:** Low fps or stutter **Where in the game:** Hyrule Castle Town, loaded from the autosave at the town gate **How often:** Every run **Graphics card and driver:** NVIDIA GeForce RTX 3070 8 GB (the driver allows 7.1-7.2 GB), driver 616.56 **CPU, RAM and operating system:** Intel Core i5-12600K, 32 GB DDR5-5600, Windows 11 (build 26100) **Changes from the defaults:** Insert menu: Resolution 100%, Upscaling Catmull-Rom, Frame generation Off; fullscreen at 2560x1440. `hookshot_env.txt`: `RX_VK_ASTC_BC_ALL=1`, plus the test-run switches listed in my comment on #574. **Other software hooked into the game:** None In 8 runs of the same 4-minute route (v2.0.0 to v2.0.2), VRAM was over the allowed in about half of the `vk: VRAM:` reports, by 0.6-1.0 GB on average per run. In those same reports, 1.3-1.8 GB on average per run (up to 2.6 GB) was free inside VMA's blocks. From the v2.0.2 run with default settings: ``` vk: VRAM: 7716 MB used of 7157 MB allowed (2319 MB of it free in the blocks); textures 1979 MB (budget 1205 MB), 1437 textures ``` Textures peaked at 3.3-4.3 GB while the budget shrank to 0.5-2.8 GB in those reports; up to 2.0-2.2 GB of the textures are images the GPU drew, which are kept on purpose. Lowering the texture budget (`RX_VK_VRAM_MB=2500`, v2.0.1) cut the textures' peak to ~3.6 GB, but VRAM stayed over the allowed and fps did not change. Since the free space inside the blocks is about twice the overage, giving it back (defragmentation, or dedicated allocations for the large images) might keep 8 GB cards within the budget without evicting anything more. The logs are available if you want them. _The runs were scripted and the numbers collected with Claude Code._
RufflezAU commented 2026-11-09 03:50:23 +00:00 (Migrated from github.com)

Yes — that is exactly the case the wall-clock eviction is for, and it applies cleanly to current main (b234b43). Happy to have you test it on the 1650 Ti.

What it changes (gpu/vk/vk_texture.cpp, .h, 3 places):

  • Texture::last_ms, set where last_frame is set (create, texture_touch, texture_touch_quick);
  • the sweep ordered by last_ms and its age test in milliseconds, RX_VK_EVICT_MIN_MS (default 1000; sweep 2000);
  • the rendered-texture keep in time too (10 s over the allowance, 60 s under) instead of 600/3600 presents.

At 60 fps the numbers are identical to today (60 presents = 1 s). At 4–6 fps the old rule waits 10–30 s before anything can be evicted, which is how the texture set gets to twice the budget between two sweeps.

On the Deck (RADV VANGOGH, 14 GB unified, 2200 MB budget): before, textures peaked at 4005 MB, GPU time went 26 → 69 ms and submissions to 100–183 ms as the driver paged; after, the set stays at the budget and the paging stops. No device lost in ~30 four-minute runs.

Patch (git apply, against b234b43)
diff --git a/gpu/vk/vk_texture.cpp b/gpu/vk/vk_texture.cpp
index addfc25..e166c5d 100644
--- a/gpu/vk/vk_texture.cpp
+++ b/gpu/vk/vk_texture.cpp
@@ -166,6 +166,13 @@ std::unordered_map<Key, std::unique_ptr<Texture>, KeyHash> textures;
 std::unordered_multimap<uint64_t, Texture*> by_gpu;
 uint64_t frame_no = 1;
 uint64_t total_bytes = 0;
+// The eviction's age in wall-clock milliseconds, not presents: on a device slow enough to run the world at 4 fps,
+// "unused for 60 presents" is 15 seconds, and VRAM ran over the driver's limit between two sweeps. At 60 fps the two
+// measures agree (60 presents = 1 s), so this changes nothing there.
+static uint64_t tex_now_ms() {
+    using namespace std::chrono;
+    return (uint64_t)duration_cast<milliseconds>(steady_clock::now().time_since_epoch()).count();
+}
 
 // (RX_VK_LOG_REMADE=1) Textures evicted and made again within 600 frames, and those of them that were then sampled
 // empty (storage the GPU rendered, not uploaded): what the eviction's minimum age below is measured by
@@ -1409,6 +1416,7 @@ void forget_gpu(Texture* tex) {
 Texture* create(const Key& key, const nvn_tex* t, VkFormat vf, bool astc, int bw, int bh, int bpb) {
     auto tex = std::make_unique<Texture>();
     Texture& x = *tex;
+    x.last_ms = tex_now_ms();
     x.gpu = t->gpu, x.d = t->d, x.L = t->L, x.nvn_format = t->d.format;
     x.guest = host->gpu_guest(t->gpu);
     x.guest_size = t->L.size;
@@ -1846,6 +1854,7 @@ uint32_t texture_epoch() {
 
 Texture* texture_touch(Texture* tex, bool for_sampling) {
     tex->last_frame = frame_no;
+    tex->last_ms = tex_now_ms();
     tex->last_value = rec_value;
     if (!for_sampling) return tex;
     // contents: from guest memory unless the GPU wrote the image (then it is the content, unless the texture was
@@ -2511,6 +2520,7 @@ void texture_written(Texture* tex) {
 
 bool texture_touch_quick(Texture* tex, bool for_sampling) {
     tex->last_frame = frame_no;   // (the same values whichever thread stores them)
+    tex->last_ms = tex_now_ms();
     tex->last_value = rec_value;
     if (!for_sampling) return true;
     if (tex->gpu_written && !tex->released && !tex->only_cleared) return true;
@@ -2995,7 +3005,7 @@ void texture_frame(uint64_t completed) {
     if (no_evict || (sweep && frame_no % 600)) return;
     std::vector<std::pair<uint64_t, Texture*>> order;
     order.reserve(textures.size());
-    for (auto& kv : textures) order.push_back({kv.second->last_frame, kv.second.get()});
+    for (auto& kv : textures) order.push_back({kv.second->last_ms, kv.second.get()});
     std::sort(order.begin(), order.end(), [](const auto& a, const auto& b) { return a.first < b.first; });
     std::vector<Texture*> gone;
     // (over the budget, what goes has gone unused for RX_VK_EVICT_MIN_UNUSED frames, 60 by default; it was 8. On an
@@ -3004,8 +3014,9 @@ void texture_frame(uint64_t completed) {
     // within 10), uploaded and decoded again each time; and a 260 x 260 one the GPU renders and the game samples every
     // 40 frames came back empty each time (22 times; the lit ground black for a frame, a tiled pattern over the ground).
     // With 60: 2. The textures that are really done with still go, 52 frames later)
-    static const uint64_t min_unused =
-        getenv("RX_VK_EVICT_MIN_UNUSED") ? (uint64_t)std::max(1, atoi(getenv("RX_VK_EVICT_MIN_UNUSED"))) : 60;
+    static const uint64_t min_unused_ms =
+        getenv("RX_VK_EVICT_MIN_MS") ? (uint64_t)std::max(1, atoi(getenv("RX_VK_EVICT_MIN_MS"))) : 1000;
+    const uint64_t now_ms = tex_now_ms();
     // (hookshot) a render target another image has since been drawn over (same storage, another size or format:
     // OoT's dynamic resolution makes a new set at each size in the same pool memory) holds nothing any more: it
     // goes like any other. Kept, they were 3.6 GB of 4.5 GB rendered, VRAM near the driver's limit, and the
@@ -3057,7 +3068,7 @@ void texture_frame(uint64_t completed) {
              (unsigned long long)(finalized_kept >> 10), n ? line : "");
     }
     for (auto& [last, tex] : order) {
-        if ((!sweep && total_bytes <= budget / 8 * 7) || frame_no - last < (sweep ? std::max<uint64_t>(120, min_unused) : min_unused)) break;
+        if ((!sweep && total_bytes <= budget / 8 * 7) || now_ms - last < (sweep ? std::max<uint64_t>(2000, min_unused_ms) : min_unused_ms)) break;
         if (tex->last_value > completed) continue;
         // (nor what the GPU wrote and the game still has: the image is its only copy, and one made again over that
         // storage started empty or from stale memory: a sky or course texture drawn once, garbage from then on.
@@ -3066,7 +3077,7 @@ void texture_frame(uint64_t completed) {
         // (one unused for 3600 presents (a minute) goes: a course left behind; the game draws it again when it comes
         // back to that course. Without it a course change kept 1.4 GB arrays and VRAM ran over)
         // (600 presents when the process is over what the driver allows: then the driver pages VRAM to system memory)
-        const uint64_t keep_rendered = known && used > allowed ? 600 : 3600;
+        const uint64_t keep_rendered_ms = known && used > allowed ? 10000 : 60000;
         const bool overwritten = drawn_over(tex);   // (above)
         if (finalized_kept && keep_finalized(tex)) {   // (above. Oldest first: beyond the cap the least recently used go)
             if (finalized_kept <= keep_finalized_max) continue;
@@ -3077,7 +3088,7 @@ void texture_frame(uint64_t completed) {
                      (unsigned long long)(keep_finalized_max >> 20), tex->id, (unsigned long long)tex->gpu, tex->width,
                      tex->height, tex->nvn_format, (unsigned long long)(frame_no - last));
         }
-        if (!overwritten && tex->gpu_written && !tex->released && !evict_rendered && frame_no - last < keep_rendered) continue;
+        if (!overwritten && tex->gpu_written && !tex->released && !evict_rendered && now_ms - last < keep_rendered_ms) continue;
         if (sweep && !overwritten) continue;
         // (and a small one never: its texels exist nowhere else, and it is made once and kept by the game. The sky's
         // 256 x 256 lookup table, unused while a menu was up, went and came back from memory as garbage: the rainbow
diff --git a/gpu/vk/vk_texture.h b/gpu/vk/vk_texture.h
index 8fcadfa..7b7ac32 100644
--- a/gpu/vk/vk_texture.h
+++ b/gpu/vk/vk_texture.h
@@ -71,6 +71,7 @@ struct Texture {
     uint64_t upload_value = 0;   // (the submission its last upload was recorded in, and the parallel replay's segment
     uint64_t upload_seg = 0;     // that recorded it: one before it in the stream uploads it again)
     uint64_t last_frame = 0;  // the present it was last used in (the cache's age)
+    uint64_t last_ms = 0;     // (and when: the eviction's age is time, not presents: a slow device's presents are long)
     uint64_t first_render_frame = 0;   // the present it was first rendered into in (0: never)
     uint64_t last_value = 0;  // the timeline value of the last submission that used it
     uint64_t bytes = 0;       // device memory it holds (the texels', or a sparse image's chunks)

The same change is on branch deck/steamdeck of https://github.com/RufflezAU/Hookshot together with the Deck settings, if a branch is easier than a patch. It is independent of the other Deck changes there (build toolchain, configs) — the texture diff is all of it.

If you can build with it, the numbers to watch are textures N MB (budget N MB) (should stop running away) and GPU time / slow submit (should stop climbing as VRAM goes over). Your Kakariko sample at 5046 of 3347 MB is the exact shape this is meant to remove.

Yes — that is exactly the case the wall-clock eviction is for, and it applies cleanly to current `main` (`b234b43`). Happy to have you test it on the 1650 Ti. **What it changes** (`gpu/vk/vk_texture.cpp`, `.h`, 3 places): - `Texture::last_ms`, set where `last_frame` is set (create, `texture_touch`, `texture_touch_quick`); - the sweep ordered by `last_ms` and its age test in milliseconds, `RX_VK_EVICT_MIN_MS` (default 1000; sweep 2000); - the rendered-texture keep in time too (10 s over the allowance, 60 s under) instead of 600/3600 presents. At 60 fps the numbers are identical to today (60 presents = 1 s). At 4–6 fps the old rule waits 10–30 s before anything can be evicted, which is how the texture set gets to twice the budget between two sweeps. **On the Deck (RADV VANGOGH, 14 GB unified, 2200 MB budget):** before, textures peaked at 4005 MB, `GPU time` went 26 → 69 ms and submissions to 100–183 ms as the driver paged; after, the set stays at the budget and the paging stops. No device lost in ~30 four-minute runs. <details><summary>Patch (git apply, against b234b43)</summary> ```diff diff --git a/gpu/vk/vk_texture.cpp b/gpu/vk/vk_texture.cpp index addfc25..e166c5d 100644 --- a/gpu/vk/vk_texture.cpp +++ b/gpu/vk/vk_texture.cpp @@ -166,6 +166,13 @@ std::unordered_map<Key, std::unique_ptr<Texture>, KeyHash> textures; std::unordered_multimap<uint64_t, Texture*> by_gpu; uint64_t frame_no = 1; uint64_t total_bytes = 0; +// The eviction's age in wall-clock milliseconds, not presents: on a device slow enough to run the world at 4 fps, +// "unused for 60 presents" is 15 seconds, and VRAM ran over the driver's limit between two sweeps. At 60 fps the two +// measures agree (60 presents = 1 s), so this changes nothing there. +static uint64_t tex_now_ms() { + using namespace std::chrono; + return (uint64_t)duration_cast<milliseconds>(steady_clock::now().time_since_epoch()).count(); +} // (RX_VK_LOG_REMADE=1) Textures evicted and made again within 600 frames, and those of them that were then sampled // empty (storage the GPU rendered, not uploaded): what the eviction's minimum age below is measured by @@ -1409,6 +1416,7 @@ void forget_gpu(Texture* tex) { Texture* create(const Key& key, const nvn_tex* t, VkFormat vf, bool astc, int bw, int bh, int bpb) { auto tex = std::make_unique<Texture>(); Texture& x = *tex; + x.last_ms = tex_now_ms(); x.gpu = t->gpu, x.d = t->d, x.L = t->L, x.nvn_format = t->d.format; x.guest = host->gpu_guest(t->gpu); x.guest_size = t->L.size; @@ -1846,6 +1854,7 @@ uint32_t texture_epoch() { Texture* texture_touch(Texture* tex, bool for_sampling) { tex->last_frame = frame_no; + tex->last_ms = tex_now_ms(); tex->last_value = rec_value; if (!for_sampling) return tex; // contents: from guest memory unless the GPU wrote the image (then it is the content, unless the texture was @@ -2511,6 +2520,7 @@ void texture_written(Texture* tex) { bool texture_touch_quick(Texture* tex, bool for_sampling) { tex->last_frame = frame_no; // (the same values whichever thread stores them) + tex->last_ms = tex_now_ms(); tex->last_value = rec_value; if (!for_sampling) return true; if (tex->gpu_written && !tex->released && !tex->only_cleared) return true; @@ -2995,7 +3005,7 @@ void texture_frame(uint64_t completed) { if (no_evict || (sweep && frame_no % 600)) return; std::vector<std::pair<uint64_t, Texture*>> order; order.reserve(textures.size()); - for (auto& kv : textures) order.push_back({kv.second->last_frame, kv.second.get()}); + for (auto& kv : textures) order.push_back({kv.second->last_ms, kv.second.get()}); std::sort(order.begin(), order.end(), [](const auto& a, const auto& b) { return a.first < b.first; }); std::vector<Texture*> gone; // (over the budget, what goes has gone unused for RX_VK_EVICT_MIN_UNUSED frames, 60 by default; it was 8. On an @@ -3004,8 +3014,9 @@ void texture_frame(uint64_t completed) { // within 10), uploaded and decoded again each time; and a 260 x 260 one the GPU renders and the game samples every // 40 frames came back empty each time (22 times; the lit ground black for a frame, a tiled pattern over the ground). // With 60: 2. The textures that are really done with still go, 52 frames later) - static const uint64_t min_unused = - getenv("RX_VK_EVICT_MIN_UNUSED") ? (uint64_t)std::max(1, atoi(getenv("RX_VK_EVICT_MIN_UNUSED"))) : 60; + static const uint64_t min_unused_ms = + getenv("RX_VK_EVICT_MIN_MS") ? (uint64_t)std::max(1, atoi(getenv("RX_VK_EVICT_MIN_MS"))) : 1000; + const uint64_t now_ms = tex_now_ms(); // (hookshot) a render target another image has since been drawn over (same storage, another size or format: // OoT's dynamic resolution makes a new set at each size in the same pool memory) holds nothing any more: it // goes like any other. Kept, they were 3.6 GB of 4.5 GB rendered, VRAM near the driver's limit, and the @@ -3057,7 +3068,7 @@ void texture_frame(uint64_t completed) { (unsigned long long)(finalized_kept >> 10), n ? line : ""); } for (auto& [last, tex] : order) { - if ((!sweep && total_bytes <= budget / 8 * 7) || frame_no - last < (sweep ? std::max<uint64_t>(120, min_unused) : min_unused)) break; + if ((!sweep && total_bytes <= budget / 8 * 7) || now_ms - last < (sweep ? std::max<uint64_t>(2000, min_unused_ms) : min_unused_ms)) break; if (tex->last_value > completed) continue; // (nor what the GPU wrote and the game still has: the image is its only copy, and one made again over that // storage started empty or from stale memory: a sky or course texture drawn once, garbage from then on. @@ -3066,7 +3077,7 @@ void texture_frame(uint64_t completed) { // (one unused for 3600 presents (a minute) goes: a course left behind; the game draws it again when it comes // back to that course. Without it a course change kept 1.4 GB arrays and VRAM ran over) // (600 presents when the process is over what the driver allows: then the driver pages VRAM to system memory) - const uint64_t keep_rendered = known && used > allowed ? 600 : 3600; + const uint64_t keep_rendered_ms = known && used > allowed ? 10000 : 60000; const bool overwritten = drawn_over(tex); // (above) if (finalized_kept && keep_finalized(tex)) { // (above. Oldest first: beyond the cap the least recently used go) if (finalized_kept <= keep_finalized_max) continue; @@ -3077,7 +3088,7 @@ void texture_frame(uint64_t completed) { (unsigned long long)(keep_finalized_max >> 20), tex->id, (unsigned long long)tex->gpu, tex->width, tex->height, tex->nvn_format, (unsigned long long)(frame_no - last)); } - if (!overwritten && tex->gpu_written && !tex->released && !evict_rendered && frame_no - last < keep_rendered) continue; + if (!overwritten && tex->gpu_written && !tex->released && !evict_rendered && now_ms - last < keep_rendered_ms) continue; if (sweep && !overwritten) continue; // (and a small one never: its texels exist nowhere else, and it is made once and kept by the game. The sky's // 256 x 256 lookup table, unused while a menu was up, went and came back from memory as garbage: the rainbow diff --git a/gpu/vk/vk_texture.h b/gpu/vk/vk_texture.h index 8fcadfa..7b7ac32 100644 --- a/gpu/vk/vk_texture.h +++ b/gpu/vk/vk_texture.h @@ -71,6 +71,7 @@ struct Texture { uint64_t upload_value = 0; // (the submission its last upload was recorded in, and the parallel replay's segment uint64_t upload_seg = 0; // that recorded it: one before it in the stream uploads it again) uint64_t last_frame = 0; // the present it was last used in (the cache's age) + uint64_t last_ms = 0; // (and when: the eviction's age is time, not presents: a slow device's presents are long) uint64_t first_render_frame = 0; // the present it was first rendered into in (0: never) uint64_t last_value = 0; // the timeline value of the last submission that used it uint64_t bytes = 0; // device memory it holds (the texels', or a sparse image's chunks) ``` </details> The same change is on branch `deck/steamdeck` of <https://github.com/RufflezAU/Hookshot> together with the Deck settings, if a branch is easier than a patch. It is independent of the other Deck changes there (build toolchain, configs) — the texture diff is all of it. If you can build with it, the numbers to watch are `textures N MB (budget N MB)` (should stop running away) and `GPU time` / `slow submit` (should stop climbing as VRAM goes over). Your Kakariko sample at 5046 of 3347 MB is the exact shape this is meant to remove.
NotRexed commented 2026-11-09 05:50:27 +00:00 (Migrated from github.com)

Tracked in #993, follow there. That issue collects every report of garbled or rainbow textures that get worse over play time, and this one is linked from it with its GPU, version and log details. It is a known issue in v2.0.6 and not fixed yet. The eviction patch posted here is linked from the tracker. If it still happens on v2.0.6 (https://github.com/NotRexed/Hookshot/releases/tag/v2.0.6; Update next to Play in the launcher), please add a comment there with your hookshot_log.txt.

Tracked in #993, follow there. That issue collects every report of garbled or rainbow textures that get worse over play time, and this one is linked from it with its GPU, version and log details. It is a known issue in v2.0.6 and not fixed yet. The eviction patch posted here is linked from the tracker. If it still happens on v2.0.6 (https://github.com/NotRexed/Hookshot/releases/tag/v2.0.6; Update next to Play in the launcher), please add a comment there with your `hookshot_log.txt`.
Commenting is not possible because the repository is archived.
No milestone
No assignees
1 participant
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
MagicBeanDev/Hookshot#595
No description provided.