diff --git a/AUDIT.md b/AUDIT.md index a7346cea..16a58342 100644 --- a/AUDIT.md +++ b/AUDIT.md @@ -1075,7 +1075,7 @@ back. | 68 | Lazy write-back command buffer and fence | `lz_init_lock` | One | Never freed explicitly | | 69 | Upload batches (command buffers and fences: inflight and spare) | `cmd_lock` | Grow only | Reused; shutdown | | 70 | Staging cache | `cmd_lock` | `STG_CACHE` 24 buffers, `STG_CACHE_BYTES` 192 MiB | At batch retire | -| 71 | Staging VMA pool | — | `STG_BLOCK` 64 MiB blocks, `STG_MIN_BLOCKS` 2; requests ≤ 32 MiB | — | +| 71 | Staging VMA pool | — | `STG_BLOCK` 64 MiB blocks; `STG_MIN_BLOCKS` 4 kept on a discrete GPU (256 MiB of host memory), `STG_MIN_BLOCKS_UMA` 2 on an integrated one; requests ≤ 32 MiB; an open batch is submitted at `STG_FLUSH` 64 MiB of staging | — | | 72 | Staging arena | `cmd_lock` | `ARENA_CHUNK` 8 MiB, ≤ 2 MiB per request, `ARENA_KEEP` 8; `NX2NAVI_STG_ARENA` | At fence; shutdown | | 73 | Deferred destruction list `dead[]` (images, views, buffers, host frees) | `cmd_lock` | None found | `vk_mem_collect` on the completion thread once the stamped value is reached | | 74 | Headroom spare (half a VMA block per device-local type; block `NX2NAVI_VMA_BLOCK_MB` 64, 16..256) | "vk headroom" | 85 % of VMA's budget; `NX2NAVI_VRAM_HEADROOM=0` off | Replaced, dropped when there is no room or commit is low, at stop | diff --git a/nx2navi.ini.example b/nx2navi.ini.example index a72321a1..d2a0fca4 100644 --- a/nx2navi.ini.example +++ b/nx2navi.ini.example @@ -311,7 +311,8 @@ # video memory: 1 turns on the settings written for them and measured on a Radeon RX 580, for every key below that # this file does not set itself. Mesh shaders run as compute where the card has none (mesh_emulation), video memory # stays within what the driver reports free (vram_heap_limit = auto, vram_reserve_mb = 512), large texture upload -# batches go to the GPU early (upload_batch_mb = 256), game memory is copied for the GPU while a frame is recorded and +# batches go to the GPU early (upload_batch_mb = 256; every batch goes at 64 MiB of staging at the latest with or +# without it, so only a value below 64 acts), game memory is copied for the GPU while a frame is recorded and # the game waits less for the GPU (mirror_stage = 1, submit_finish_gpu = 0), frames are prepared on a thread of their # own (replay_async = 1, which can show a dark or flickering frame now and then), pools the game refills every frame # stay in system memory (mirror_hot = 25), and a texture read after another image was drawn over its memory gets diff --git a/runtime/cfg.c b/runtime/cfg.c index 90c8b685..55646ba6 100644 --- a/runtime/cfg.c +++ b/runtime/cfg.c @@ -318,7 +318,9 @@ static const CfgMeta metas[] = { {"FOLIAGE", "full", "full|reduced|off", RESTART}, // vk_exec_draw.c foliage_skip {"VRAM_HEAP_LIMIT", "0", NULL, RESTART}, // vk_device.c vram_heap_limits: auto, 0 or MiB {"VRAM_RESERVE_MB", "0", "0..4096", RESTART}, // vk_vma_budget.c: video memory kept free of VMA's allocations - {"UPLOAD_BATCH_MB", "0", NULL, RESTART}, // vk_memory.c: MiB of staging before a batch goes early, 0 no limit + // vk_memory.c vk_upload_end: MiB of staging before a batch goes early. 0: at 64 MiB, the staging pool's own limit + // (STG_FLUSH), which also holds for every value above it + {"UPLOAD_BATCH_MB", "0", NULL, RESTART}, {"MESH_EMULATION", "0", "0|1", RESTART}, {"MESH_EMU_MB", "256", "32..2048", RESTART}, {"MESH_EMU_TASKS", "16384", "256..262144", RESTART}, {"MESH_EMU_BATCH", "1", "0|1", RESTART}, {"VEC_INDEX_FIX", "0", "0|auto|1", RESTART}, {"GUEST_OPT", "1", "1|2", RESTART}, // tools/make_game.py: the next Rebuild the Game diff --git a/runtime/ui_text.c b/runtime/ui_text.c index 5349e42b..56a854c3 100644 --- a/runtime/ui_text.c +++ b/runtime/ui_text.c @@ -119,8 +119,9 @@ static const char *const text_en[UI_TEXT_COUNT] = { "driver does not run out. Off: no limit.", [UI_MENU_LOWEND_UPLOAD_BATCH] = "Upload batch limit", [UI_MENU_LOWEND_UPLOAD_BATCH_DESC] = "Texture uploads waiting in staging memory are sent to the GPU early once they " - "hold this much, so an area load cannot run video memory out.", - [UI_MENU_LOWEND_NO_LIMIT] = "No limit", + "hold this much, so an area load cannot run video memory out. Every batch is " + "sent at 64 MB at the latest, so none of these choices changes anything now.", + [UI_MENU_LOWEND_NO_LIMIT] = "64 MB", [UI_MENU_LOWEND_ASTC_KEEP] = "Keep the ASTC decode buffer", [UI_MENU_LOWEND_ASTC_KEEP_DESC] = "The buffer that GPU-decodes compressed textures is kept between upload batches " "instead of being allocated again for each one.", diff --git a/runtime/vk_memory.c b/runtime/vk_memory.c index fa75347d..2465fbea 100644 --- a/runtime/vk_memory.c +++ b/runtime/vk_memory.c @@ -5097,7 +5097,8 @@ VkCommandBuffer vk_upload_begin(void) { // NX2NAVI_UPLOAD_BATCH_MB (256 with low_end_gpu = 1; 0, the default, = no limit of its own): once the open batch's // staging buffers hold that much, the outermost vk_upload_end submits it. It does so too once the batch's staging // reaches STG_FLUSH, so that an area load's staging stays inside the staging pool's blocks (the staging pool's -// comment). A save load made thousands of textures between two replay submissions, and every one of them went into +// comment): 0 therefore means STG_FLUSH (64 MiB), not "never", and a value above STG_FLUSH changes nothing, the +// low-end mode's 256 and the settings menu's 128 to 1024 included. A save load made thousands of textures between two replay submissions, and every one of them went into // one batch: about 1.5 GB of raw ASTC blocks plus the decoder's work in a single submission, // which AMD's 23.10 driver on an RX 580 refused with VK_ERROR_OUT_OF_DEVICE_MEMORY. Submitted early, a batch still // precedes the replay submission that uses its textures (the queue keeps order), and its staging is freed as it retires. @@ -5168,19 +5169,27 @@ void vk_mem_oom_report(const char *what, VkResult r) { // which it waits for in order (a wait covers all the queue's work submitted ahead of that batch, tens of ms on a heavy // frame, with cmd_lock held). Only when none of that makes room (the open batch's own staging, staging buffers their // callers hold, the blocks' fragmentation) does the pool grow by a block, and the log says so. -// A discrete GPU keeps STG_MIN_BLOCKS blocks; an integrated one, whose host memory is the machine's RAM and where low -// memory ended the game before (vk_mem_give_back), keeps STG_MIN_BLOCKS_UMA, and its loads wait for batches sooner. +// A discrete GPU keeps STG_MIN_BLOCKS blocks, 256 MiB of host memory for the whole run (128 MiB up to 0.9.2); an +// integrated one, whose host memory is the machine's RAM and where low memory ended the game before +// (vk_mem_give_back), keeps STG_MIN_BLOCKS_UMA, 128 MiB, and its loads wait for batches sooner. #define STG_BLOCK (64ull << 20) #define STG_MIN_BLOCKS 4 #define STG_MIN_BLOCKS_UMA 2 static VmaPool stg_pool; static int stg_pool_tried; static uint64_t stg_room_cache, stg_room_wait, stg_room_new; // cmd_lock: how stg_create found room (rt_log below) +static uint64_t stg_wait_ticks, stg_wait_max; // cmd_lock: qpc ticks stg_create waited for batches // vmaCreateBuffer of a staging buffer (*ai names stg_pool) inside the pool's blocks: when they are full, the kept // staging buffers and the arena's idle chunks go, then the oldest batch in flight is waited for and retired, until the // buffer fits. chunk: the buffer is an arena chunk, and VK_INCOMPLETE says that a retired batch gave chunks back to // arena_free, to take one of those instead. Without a batch to wait for (or once the device is lost) the pool grows. +// The wait runs with cmd_lock held, and cannot leave it: the caller is inside vk_upload_begin .. vk_upload_end with +// the open batch's command buffer in hand, and the staging buffer it asks for belongs to that batch, so letting go of +// the lock would let another thread submit the batch under it. Meanwhile a replay submission that wants to send its +// uploads (vk_upload_flush) waits for the lock; its own commands would queue behind the same batches, so what it +// loses is recording time, not GPU time. How long that is on a load is not measured anywhere yet: the waits are +// timed here and logged (the first four, then at every doubling of their count), so a played session's log says. static VkResult stg_create(VkCtx *c, const VkBufferCreateInfo *bi, const VmaAllocationCreateInfo *ai, VkBuffer *b, VmaAllocation *a, VmaAllocationInfo *info, int chunk) { VmaAllocationCreateInfo in = *ai; @@ -5198,13 +5207,23 @@ static VkResult stg_create(VkCtx *c, const VkBufferCreateInfo *bi, const VmaAllo } if (ninflight && !vk_device_is_lost()) { int before = ninflight; + uint64_t w0 = qpc(); VkResult w = vkWaitForFences(c->device, 1, &inflight[0].fence, VK_TRUE, UINT64_MAX); if (w != VK_SUCCESS) { vk_device_lost_check(w, "the wait for an upload batch"); rt_fatal("vk: the wait for an upload failed (%d)", (int)w); } + uint64_t took = qpc() - w0; + stg_wait_ticks += took; + if (took > stg_wait_max) stg_wait_max = took; retire(c, 0); stg_room_wait++; + if (stg_room_wait <= 4 || !(stg_room_wait & (stg_room_wait - 1))) + rt_log("vk: a staging buffer of %.1f MiB waited %.1f ms for an upload batch to finish, with the upload " + "lock held (%llu such waits so far, %.0f ms in all, the longest %.1f ms; %d batches still in " + "flight). A replay submission that sends its uploads waits for that lock", + bi->size / 1048576.0, qpc_ms(took), (unsigned long long)stg_room_wait, qpc_ms(stg_wait_ticks), + qpc_ms(stg_wait_max), ninflight); if (chunk && narena_free) { r = VK_INCOMPLETE; break;