Upload the menu's font atlas through the runtime's upload batch, checked, instead of Dear ImGui's own upload.

ImGui_ImplVulkan_CreateFontsTexture checked no result: with the video memory full, the atlas a window size change
makes again got no memory, its copy still went out, and the GPU wrote to address 0 (issue #165, device lost after
Alt+Tab). It also submitted and waited on the game's queue itself, and when ImGui_ImplVulkan_NewFrame ran it (whenever
the back end held no atlas) that took no queue lock, beside the game's own submissions; the crashes in AMD's driver
inside the menu's drawing (issue #230) fit that. ui/menu_art.cpp menu_font_upload now uploads the atlas as the menu's
art is uploaded: VMA picks the memory (device-local, else what is left), the copy goes in the runtime's upload batch
under the queue lock, every step is checked, and the previous atlas is freed only once the new one is on the GPU and
the queue is idle. The menu no longer calls ImGui_ImplVulkan_NewFrame, whose only work was that upload. The back end's
VkResults are checked and logged (menu_vk_check), a texture whose descriptor set was not allocated is not drawn with,
and the descriptor pool holds 32 sets, room for two atlases while one replaces the other.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
chrg6
2026-11-09 13:10:47 -05:00
parent 7a60f4e2a0
commit 9c10827018
3 changed files with 94 additions and 24 deletions

View File

@@ -260,6 +260,15 @@ static void make_context(void) {
g_ctx_ok = 1;
}
// Dear ImGui's Vulkan back end calls this with each VkResult it gets (it went unchecked before): failures are logged and
// counted, so a texture whose descriptor set was not allocated is not drawn with (ui/menu_art.cpp upload_rgba).
static std::atomic<int> g_vk_errors;
static void menu_vk_check(VkResult r) {
if (r == VK_SUCCESS) return;
if (g_vk_errors.fetch_add(1) < 8) rt_log("menu: Dear ImGui's Vulkan back end got VkResult %d", (int)r);
}
extern "C" int menu_vk_errors(void) { return g_vk_errors.load(); }
static void vk_down(void) {
if (!g_vk_ok) return;
menu_art_release();
@@ -283,7 +292,8 @@ static int vk_up(const MenuTarget *t) {
ii.Device = t->device;
ii.QueueFamily = t->queue_family;
ii.Queue = t->queue;
ii.DescriptorPoolSize = 16;
ii.DescriptorPoolSize = 32; // the art's sets, and two font atlases while one replaces the other
ii.CheckVkResultFn = menu_vk_check;
ii.MinImageCount = 2;
ii.ImageCount = 4; // vertex buffers rotate per recorded frame: more than gfx_present.c's 2 frames in flight
ii.MSAASamples = VK_SAMPLE_COUNT_1_BIT;
@@ -301,7 +311,7 @@ static int vk_up(const MenuTarget *t) {
// and Chinese language names merged in from a system font when there is one (each language is named in its own script).
// A catalog row also holds menu text (Simplified Chinese), so every string of the row goes into the builder: the merged
// font must cover the whole menu of that language, not only its name. Made again when the window height changes; the old
// texture goes after the queue is idle (CreateFontsTexture).
// texture goes after the queue is idle (ui/menu_art.cpp menu_font_upload).
static const ImWchar kRanges[] = {0x0020, 0x00FF, 0x0400, 0x052F, 0x2010, 0x2027, 0};
static const struct { const char *lang; UiTextId name; } kCjk[] = {
{"ja", UI_MENU_LANGNAME_JA}, {"ko", UI_MENU_LANGNAME_KO}, {"zh-Hans", UI_MENU_LANGNAME_ZH_HANS},
@@ -442,12 +452,15 @@ static int fonts_up(float scale) {
}
skin_atlas_add(fa); // the boxes' grain tile (ui/menu_skin.cpp), drawn into the atlas once it is built
skin_atlas_fill(fa); // builds the atlas, which the upload below then takes as it is
vk_queue_lock(); // the upload submits on the queue and waits for it
int ok = ImGui_ImplVulkan_CreateFontsTexture();
unsigned char *px = nullptr;
int aw = 0, ah = 0;
fa->GetTexDataAsRGBA32(&px, &aw, &ah);
ImTextureID tex = 0;
int ok = menu_font_upload(px, aw, ah, &tex);
if (ok) fa->SetTexID(tex);
// ClarimoUDPE's line (ascent to descent, which is all Dear ImGui spaces lines by) leaves wrapped lines touching: the
// line height grows by a sixth over the glyphs' size, the glyphs stay as rasterized.
if (ok && g_game_font && fa->Fonts.Size) fa->Fonts[0]->FontSize *= 1.16f;
vk_queue_unlock();
fa->ClearInputData(); // the glyphs are in the texture: the font files and the pixels are not needed again
fa->ClearTexData();
for (uint8_t *d : data) free(d);
@@ -1534,7 +1547,8 @@ extern "C" int menu_overlay_frame(uint64_t frame, const MenuTarget *t, const cha
ev_n = 0;
plat_mutex_unlock(&ev_lock);
}
ImGui_ImplVulkan_NewFrame();
// no ImGui_ImplVulkan_NewFrame: all it does is upload Dear ImGui's own font atlas when the back end holds none,
// which it never does here (fonts_up uploads it, ui/menu_art.cpp menu_font_upload)
ImGui::NewFrame();
if (open) layout_menu(t->width, t->height, scale);
perf_draw(t, scale);
@@ -1592,7 +1606,8 @@ extern "C" int menu_boot_frame(const MenuTarget *t) {
double dt = g_last_ns ? (now - g_last_ns) / 1e9 : 1 / 60.0;
io.DeltaTime = (float)(dt < 1e-4 ? 1e-4 : dt > 0.1 ? 0.1 : dt);
g_last_ns = now;
ImGui_ImplVulkan_NewFrame();
// no ImGui_ImplVulkan_NewFrame: all it does is upload Dear ImGui's own font atlas when the back end holds none,
// which it never does here (fonts_up uploads it, ui/menu_art.cpp menu_font_upload)
ImGui::NewFrame();
int phase = g_boot_phase.load(std::memory_order_acquire);

View File

@@ -66,9 +66,20 @@ extern "C" void menu_art_start(void) {
if (!plat_thread_spawn(decode_all, nullptr, "menu-art", 0)) decode_all(nullptr);
}
static int upload(Art &a) {
// Frees what upload_rgba made for a (none of it in use: the caller waited for the queue or never drew with it).
static void art_free(Art &a) {
VkCtx *c = vk_ctx();
if (!c || !a.rgba) return 0;
if (a.ds) ImGui_ImplVulkan_RemoveTexture(a.ds);
if (c && a.view) vkDestroyImageView(c->device, a.view, nullptr);
if (c && a.img) vmaDestroyImage(c->vma, a.img, a.alloc);
a.ds = VK_NULL_HANDLE, a.view = VK_NULL_HANDLE, a.img = VK_NULL_HANDLE, a.alloc = nullptr;
}
// rgba (w x h) as a sampled image of up to max_levels levels, each halved from the one before, with its view and Dear
// ImGui descriptor set in a; 1 when all of it is there, else 0 with nothing left allocated.
static int upload_rgba(Art &a, const uint8_t *rgba, uint32_t w, uint32_t h, uint32_t max_levels) {
VkCtx *c = vk_ctx();
if (!c || !rgba) return 0;
if (!g_sampler) {
VkSamplerCreateInfo si = {VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO};
si.magFilter = si.minFilter = VK_FILTER_LINEAR;
@@ -77,9 +88,9 @@ static int upload(Art &a) {
si.maxLod = MAX_LEVELS;
if (vkCreateSampler(c->device, &si, nullptr, &g_sampler) != VK_SUCCESS) return 0;
}
uint32_t lw[MAX_LEVELS] = {a.w}, lh[MAX_LEVELS] = {a.h}, levels = 1;
VkDeviceSize bytes = (VkDeviceSize)a.w * a.h * 4;
while (levels < MAX_LEVELS && (lw[levels - 1] > 1 || lh[levels - 1] > 1)) {
uint32_t lw[MAX_LEVELS] = {w}, lh[MAX_LEVELS] = {h}, levels = 1;
VkDeviceSize bytes = (VkDeviceSize)w * h * 4;
while (levels < max_levels && levels < MAX_LEVELS && (lw[levels - 1] > 1 || lh[levels - 1] > 1)) {
lw[levels] = lw[levels - 1] > 1 ? lw[levels - 1] / 2 : 1;
lh[levels] = lh[levels - 1] > 1 ? lh[levels - 1] / 2 : 1;
bytes += (VkDeviceSize)lw[levels] * lh[levels] * 4;
@@ -88,7 +99,7 @@ static int upload(Art &a) {
VkImageCreateInfo ii = {VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
ii.imageType = VK_IMAGE_TYPE_2D;
ii.format = VK_FORMAT_R8G8B8A8_UNORM;
ii.extent = {a.w, a.h, 1};
ii.extent = {w, h, 1};
ii.mipLevels = levels;
ii.arrayLayers = 1;
ii.samples = VK_SAMPLE_COUNT_1_BIT;
@@ -96,19 +107,22 @@ static int upload(Art &a) {
ii.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT;
VmaAllocationCreateInfo ai = {};
ai.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE;
if (vmaCreateImage(c->vma, &ii, &ai, &a.img, &a.alloc, nullptr) != VK_SUCCESS) return 0;
if (vmaCreateImage(c->vma, &ii, &ai, &a.img, &a.alloc, nullptr) != VK_SUCCESS) {
a.img = VK_NULL_HANDLE, a.alloc = nullptr;
return 0;
}
VkBuffer sb;
VmaAllocation sa;
uint8_t *map = (uint8_t *)vk_staging(bytes, &sb, &sa);
if (!map) return 0;
if (!map) return art_free(a), 0;
// the levels one after the other in a plain allocation, each halved from the one before (the staging memory may be
// slow to read back)
uint8_t *chain = (uint8_t *)malloc((size_t)bytes);
if (!chain) {
vmaDestroyBuffer(c->vma, sb, sa);
return 0;
return art_free(a), 0;
}
memcpy(chain, a.rgba, (size_t)a.w * a.h * 4);
memcpy(chain, rgba, (size_t)w * h * 4);
VkBufferImageCopy regions[MAX_LEVELS] = {};
VkDeviceSize at = 0;
for (uint32_t l = 0; l < levels; l++) {
@@ -150,9 +164,46 @@ static int upload(Art &a) {
vi.viewType = VK_IMAGE_VIEW_TYPE_2D;
vi.format = VK_FORMAT_R8G8B8A8_UNORM;
vi.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, levels, 0, 1};
if (vkCreateImageView(c->device, &vi, nullptr, &a.view) != VK_SUCCESS) return 0;
if (vkCreateImageView(c->device, &vi, nullptr, &a.view) != VK_SUCCESS) {
a.view = VK_NULL_HANDLE;
return art_free(a), 0;
}
int errs = menu_vk_errors();
a.ds = ImGui_ImplVulkan_AddTexture(g_sampler, a.view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
return a.ds != VK_NULL_HANDLE; // the pixels stay (a few MB): the back end may start again on another format
if (menu_vk_errors() != errs) a.ds = VK_NULL_HANDLE; // the descriptor set was not allocated (menu_vk_check logged it)
if (!a.ds) return art_free(a), 0;
return 1;
}
static int upload(Art &a) {
return upload_rgba(a, a.rgba, a.w, a.h, MAX_LEVELS); // the pixels stay (a few MB): the back end may start again
}
// The menu's font atlas (ui/menu.cpp fonts_up), uploaded as the art is: through the runtime's upload batch, which
// submits under the queue lock, into memory VMA picks (device-local, else what is left), every step checked. Dear
// ImGui's own upload (ImGui_ImplVulkan_CreateFontsTexture) checked no result, so with the video memory full its copy
// went to an image without memory and the GPU wrote to address 0 (issue #165); and run from ImGui_ImplVulkan_NewFrame,
// its submit and queue wait took no lock and could meet the game's submissions on the same queue. The previous atlas
// goes once the new one is on the GPU and the queue has finished the frames that drew with it.
static Art g_font;
extern "C" int menu_font_upload(const unsigned char *rgba, int w, int h, ImTextureID *tex) {
VkCtx *c = vk_ctx();
if (!c || !rgba || w <= 0 || h <= 0) return 0;
Art n = {};
if (!upload_rgba(n, rgba, (uint32_t)w, (uint32_t)h, 1)) {
rt_log("menu: the %dx%d font atlas could not be put on the GPU", w, h);
return 0;
}
if (g_font.ds) {
vk_queue_lock();
vkQueueWaitIdle(c->gfx_queue);
vk_queue_unlock();
art_free(g_font);
}
g_font = n;
*tex = (ImTextureID)n.ds;
return 1;
}
extern "C" int menu_art(MenuArt id, ImTextureID *tex, float *w, float *h) {
@@ -170,11 +221,10 @@ extern "C" void menu_art_release(void) {
VkCtx *c = vk_ctx();
if (!c) return;
for (Art &a : g_art) {
if (a.ds) ImGui_ImplVulkan_RemoveTexture(a.ds);
if (a.view) vkDestroyImageView(c->device, a.view, nullptr);
if (a.img) vmaDestroyImage(c->vma, a.img, a.alloc);
a.ds = VK_NULL_HANDLE, a.view = VK_NULL_HANDLE, a.img = VK_NULL_HANDLE, a.alloc = nullptr;
art_free(a);
a.failed = 0; // uploaded again at the next use
}
art_free(g_font); // the menu makes its atlas again after the back end starts again (fonts_up)
ImGui::GetIO().Fonts->SetTexID(0);
if (g_sampler) vkDestroySampler(c->device, g_sampler, nullptr), g_sampler = VK_NULL_HANDLE;
}

View File

@@ -25,7 +25,12 @@ extern "C" void menu_art_start(void); // decodes the textures on a worker, on
// The texture for the menu's draw lists and its size in pixels; 0 while it is not there (still decoding, or absent).
// Window thread: the first call for an art uploads it.
extern "C" int menu_art(MenuArt id, ImTextureID *tex, float *w, float *h);
extern "C" void menu_art_release(void); // before ImGui's Vulkan back end shuts down
extern "C" void menu_art_release(void); // before ImGui's Vulkan back end shuts down; the font atlas goes too
// The font atlas's pixels (w x h, RGBA) as a texture for the draw lists in *tex; 0 when it could not be made, the previous
// atlas then kept. The previous one is freed once the queue has finished with it.
extern "C" int menu_font_upload(const unsigned char *rgba, int w, int h, ImTextureID *tex);
// Results other than VK_SUCCESS that Dear ImGui's Vulkan back end reported so far (ui/menu.cpp menu_vk_check).
extern "C" int menu_vk_errors(void);
// Draws an art stretched over [a, b] with a tint; 0 (nothing drawn) while it is not there.
extern "C" int menu_art_draw(ImDrawList *dl, MenuArt id, ImVec2 a, ImVec2 b, ImU32 tint);
// Draws an art as nine pieces: corners `corner` pixels on screen from the outer `frac` of the texture, edges stretched,