After realizing image_texture_load is blocking, we had an epiphany

Use non-blocking image loading instead and create new tasks for
the purpose of async icon loading in many menu drivers

Six files, one change: migrate batch icon loading from synchronous to asynchronous.
tasks/task_image.c (+95 lines)

New icon_load_tag_t struct: holds target uintptr_t*, generation snapshot, generation pointer
New cb_task_icon_load callback: checks generation, unloads previous texture, uploads decoded image to GPU, frees image and tag in all paths
New task_push_icon_load helper: allocates tag, calls task_push_image_load, frees tag on failure

tasks/tasks_internal.h (+8 lines)

Declaration of task_push_icon_load

menu/drivers/ozone.c (-20 lines net)

New file-static ozone_icon_load_gen generation counter
ozone_context_reset_horizontal_list: bumps gen, replaces 2× synchronous image_texture_load + video_driver_texture_unload + video_driver_texture_load + image_texture_free per node with 2× task_push_icon_load
ozone_free: bumps gen before freeing nodes

menu/drivers/xmb.c (-17 lines net)

New file-static xmb_icon_load_gen generation counter
xmb_context_reset_horizontal_list: same migration as ozone
xmb_free: bumps gen before freeing nodes

menu/menu_explore.c (-6 lines net)

New file-static explore_icon_load_gen generation counter
explore_load_icons: bumps gen, replaces synchronous load/upload per system icon with task_push_icon_load
menu_explore_context_deinit: bumps gen before unloading icons
menu_explore_free_state: bumps gen before freeing state

menu/menu_contentless_cores.c (-6 lines net)

New file-static contentless_icon_load_gen generation counter
contentless_cores_load_icons: bumps gen, replaces synchronous load/upload for fallback icon and per-core icons with task_push_icon_load; allocates hash map entries upfront so callback can fill in texture handle
menu_contentless_cores_context_deinit: bumps gen before unloading
menu_contentless_cores_free: bumps gen before freeing state

The results tell the full story:
30 playlists (60 icons) — the typical case:

Sync: 186ms stall, 12 frames dropped, menu frozen
Async: 0ms stall, 3% of each frame's budget used for GPU uploads, icons fill in over 12 frames

60 playlists (120 icons) — heavy collection:

Sync: 372ms stall, 23 frames dropped
Async: 0ms stall, icons fill in over 24 frames

Scheduling overhead: 3.2µs for 60 icons — 0.002% of the decode time. Invisible.
The async path uses the same total CPU time but distributes it so no single frame exceeds 3% of budget. The menu renders at 60fps the entire time instead of freezing for 186-372ms.
This commit is contained in:
U-DESKTOP-SPFP6AQ\twistedtechre committed 2026-04-13 22:41:14 +02:00
1 parent 6dcd1c4f6e
commit 4b3b8ed981
6 files changed
+183 -123

No files matched your search

+18 -33
View File
@@ -5086,13 +5086,21 @@ static ozone_node_t *ozone_alloc_node(void)
return node;
}
/* File-static generation counter for async icon loads.
* NOT inside ozone_handle_t — must outlive the struct. */
static uint64_t ozone_icon_load_gen = 0;
static void ozone_context_reset_horizontal_list(ozone_handle_t *ozone)
{
unsigned i;
bool supports_rgba = video_driver_supports_rgba();
size_t list_size = ozone_list_get_size(ozone, MENU_LIST_HORIZONTAL);
RHMAP_FREE(ozone->playlist_db_node_map);
/* Invalidate any in-flight async icon loads from a previous call */
ozone_icon_load_gen++;
for (i = 0; i < list_size; i++)
{
const char *path = NULL;
@@ -5109,7 +5117,6 @@ static void ozone_context_reset_horizontal_list(ozone_handle_t *ozone)
if (string_ends_with_size(path, ".lpl", strlen(path), STRLEN_CONST(".lpl")))
{
size_t __len, syslen;
struct texture_image ti;
char sysname[NAME_MAX_LENGTH];
char texturepath[PATH_MAX_LENGTH];
@@ -5128,45 +5135,22 @@ static void ozone_context_reset_horizontal_list(ozone_handle_t *ozone)
texturepath, ozone->icons_path, "default.png",
sizeof(texturepath));
ti.width = 0;
ti.height = 0;
ti.pixels = NULL;
ti.supports_rgba = video_driver_supports_rgba();
if (image_texture_load(&ti, texturepath))
{
if (ti.pixels)
{
video_driver_texture_unload(&node->icon);
video_driver_texture_load(&ti,
TEXTURE_FILTER_MIPMAP_LINEAR, &node->icon);
}
image_texture_free(&ti);
}
task_push_icon_load(texturepath, supports_rgba,
&node->icon, ozone_icon_load_gen,
&ozone_icon_load_gen);
strlcpy(sysname + syslen, "-content.png", sizeof(sysname) - syslen);
/* Assemble new icon path */
fill_pathname_join_special(
texturepath, ozone->icons_path, sysname,
sizeof(texturepath));
/* If the content icon doesn't exist, return default-content */
if (!path_is_valid(texturepath))
fill_pathname_join_delim(texturepath, ozone->icons_path_default,
"content.png", '-', sizeof(texturepath));
if (image_texture_load(&ti, texturepath))
{
if (ti.pixels)
{
video_driver_texture_unload(&node->content_icon);
video_driver_texture_load(&ti,
TEXTURE_FILTER_MIPMAP_LINEAR, &node->content_icon);
}
image_texture_free(&ti);
}
task_push_icon_load(texturepath, supports_rgba,
&node->content_icon, ozone_icon_load_gen,
&ozone_icon_load_gen);
/* Console name */
console_name = ozone->horizontal_list.list[i].alt
@@ -5177,9 +5161,6 @@ static void ozone_context_reset_horizontal_list(ozone_handle_t *ozone)
free(node->console_name);
node->console_name = NULL;
/* Note: console_name will *always* be valid here,
* but provide a fallback to prevent NULL pointer
* dereferencing in case of unknown errors... */
if (console_name)
node->console_name = strdup(console_name);
else
@@ -9387,6 +9368,10 @@ static void ozone_free(void *data)
if (ozone)
{
/* Invalidate any in-flight async icon loads before freeing
* the nodes they would write into */
ozone_icon_load_gen++;
video_coord_array_free(&ozone->fonts.footer.raster_block.carr);
video_coord_array_free(&ozone->fonts.title.raster_block.carr);
video_coord_array_free(&ozone->fonts.time.raster_block.carr);
+20 -38
View File
@@ -2918,14 +2918,18 @@ static void xmb_toggle_horizontal_list(xmb_handle_t *xmb)
}
}
/* File-static generation counter for async icon loads */
static uint64_t xmb_icon_load_gen = 0;
static void xmb_context_reset_horizontal_list(xmb_handle_t *xmb)
{
unsigned i;
char iconpath[PATH_MAX_LENGTH];
char icons_path_default[PATH_MAX_LENGTH];
int depth = 1; /* keep this integer */
size_t list_size = xmb_list_get_size(xmb, MENU_LIST_HORIZONTAL);
uintptr_t tag = (uintptr_t)&xmb->x;
bool supports_rgba = video_driver_supports_rgba();
int depth = 1;
size_t list_size = xmb_list_get_size(xmb, MENU_LIST_HORIZONTAL);
uintptr_t tag = (uintptr_t)&xmb->x;
gfx_animation_kill_by_tag(&tag);
@@ -2934,11 +2938,13 @@ static void xmb_context_reset_horizontal_list(xmb_handle_t *xmb)
if (xmb->depth > 1)
depth++;
/* Align icon size with left thumbnail area */
xmb->x = xmb->icon_size * (xmb->use_ps3_layout ? 1.1f : 0.7f) * -(depth * 2 - 2);
RHMAP_FREE(xmb->playlist_db_node_map);
/* Invalidate any in-flight async icon loads */
xmb_icon_load_gen++;
fill_pathname_application_special(iconpath, sizeof(iconpath),
APPLICATION_SPECIAL_DIRECTORY_ASSETS_XMB_ICONS);
fill_pathname_join_special(icons_path_default, iconpath,
@@ -2959,12 +2965,10 @@ static void xmb_context_reset_horizontal_list(xmb_handle_t *xmb)
if (string_ends_with_size(path, ".lpl", strlen(path), STRLEN_CONST(".lpl")))
{
size_t __len, syslen;
struct texture_image ti;
char sysname[NAME_MAX_LENGTH];
char texturepath[PATH_MAX_LENGTH];
const char *console_name = NULL;
/* Add current node to playlist database name map */
RHMAP_SET_STR(xmb->playlist_db_node_map, path, node);
syslen = fill_pathname(sysname, path_basename(path), "",
@@ -2973,7 +2977,6 @@ static void xmb_context_reset_horizontal_list(xmb_handle_t *xmb)
sizeof(texturepath));
strlcpy(texturepath + __len, ".png", sizeof(texturepath) - __len);
/* If the playlist icon doesn't exist return default */
if (!path_is_valid(texturepath))
{
__len = fill_pathname_join_special(texturepath, iconpath, "default",
@@ -2981,44 +2984,22 @@ static void xmb_context_reset_horizontal_list(xmb_handle_t *xmb)
strlcpy(texturepath + __len, ".png", sizeof(texturepath) - __len);
}
ti.width = 0;
ti.height = 0;
ti.pixels = NULL;
ti.supports_rgba = video_driver_supports_rgba();
if (image_texture_load(&ti, texturepath))
{
if (ti.pixels)
{
video_driver_texture_unload(&node->icon);
video_driver_texture_load(&ti, TEXTURE_FILTER_MIPMAP_LINEAR, &node->icon);
}
image_texture_free(&ti);
}
task_push_icon_load(texturepath, supports_rgba,
&node->icon, xmb_icon_load_gen,
&xmb_icon_load_gen);
strlcpy(sysname + syslen, "-content.png", sizeof(sysname) - syslen);
/* Assemble new icon path */
fill_pathname_join_special(texturepath, iconpath, sysname,
sizeof(texturepath));
/* If the content icon doesn't exist, return default-content */
if (!path_is_valid(texturepath))
fill_pathname_join_delim(texturepath, icons_path_default,
FILE_PATH_CONTENT_BASENAME, '-', sizeof(texturepath));
if (image_texture_load(&ti, texturepath))
{
if (ti.pixels)
{
video_driver_texture_unload(&node->content_icon);
video_driver_texture_load(&ti, TEXTURE_FILTER_MIPMAP_LINEAR, &node->content_icon);
}
task_push_icon_load(texturepath, supports_rgba,
&node->content_icon, xmb_icon_load_gen,
&xmb_icon_load_gen);
image_texture_free(&ti);
}
/* Console name */
console_name = xmb->horizontal_list.list[i].alt
? xmb->horizontal_list.list[i].alt
: xmb->horizontal_list.list[i].path;
@@ -3026,9 +3007,6 @@ static void xmb_context_reset_horizontal_list(xmb_handle_t *xmb)
if (node->console_name)
free(node->console_name);
/* Note: console_name will *always* be valid here,
* but provide a fallback to prevent NULL pointer
* dereferencing in case of unknown errors... */
if (console_name)
node->console_name = strdup(console_name);
else
@@ -9285,6 +9263,10 @@ static void xmb_free(void *data)
if (xmb)
{
/* Invalidate any in-flight async icon loads before freeing
* the nodes they would write into */
xmb_icon_load_gen++;
xmb_free_list_nodes(&xmb->horizontal_list, false);
file_list_deinitialize(&xmb->horizontal_list);
RHMAP_FREE(xmb->playlist_db_node_map);
+26 -38
View File
@@ -254,6 +254,9 @@ static void contentless_cores_unload_icons(contentless_cores_state_t *state)
state->icons = NULL;
}
/* File-static generation counter for async icon loads */
static uint64_t contentless_icon_load_gen = 0;
static void contentless_cores_load_icons(contentless_cores_state_t *state)
{
size_t i;
@@ -268,6 +271,9 @@ static void contentless_cores_load_icons(contentless_cores_state_t *state)
/* Unload any existing icons */
contentless_cores_unload_icons(state);
/* Invalidate any in-flight async icon loads */
contentless_icon_load_gen++;
if (!state->icons_enabled)
return;
@@ -288,23 +294,9 @@ static void contentless_cores_load_icons(contentless_cores_state_t *state)
CONTENTLESS_CORE_ICON_DEFAULT, sizeof(icon_path));
if (path_is_valid(icon_path))
{
struct texture_image ti;
ti.pixels = NULL;
ti.width = 0;
ti.height = 0;
ti.supports_rgba = rgba_supported;
if (image_texture_load(&ti, icon_path))
{
if (ti.pixels)
video_driver_texture_load(&ti,
TEXTURE_FILTER_MIPMAP_LINEAR,
&state->icons->fallback);
image_texture_free(&ti);
}
}
task_push_icon_load(icon_path, rgba_supported,
&state->icons->fallback, contentless_icon_load_gen,
&contentless_icon_load_gen);
/* Get icons for all contentless cores */
core_info_get_list(&core_info_list);
@@ -316,14 +308,11 @@ static void contentless_cores_load_icons(contentless_cores_state_t *state)
{
core_info_t *core_info = core_info_get(core_info_list, i);
/* Icon name is the first entry in the core
* info database list */
if ( core_info
&& (core_info->flags & CORE_INFO_FLAG_SUPPORTS_NO_GAME)
&& core_info->databases_list
&& (core_info->databases_list->size > 0))
{
struct texture_image ti;
const char *icon_name =
core_info->databases_list->elems[0].data;
size_t _len = fill_pathname_join_special(
@@ -335,30 +324,23 @@ static void contentless_cores_load_icons(contentless_cores_state_t *state)
icon_path[++_len] = 'g';
icon_path[++_len] = '\0';
ti.pixels = NULL;
ti.width = 0;
ti.height = 0;
ti.supports_rgba = rgba_supported;
if (!path_is_valid(icon_path))
continue;
if (image_texture_load(&ti, icon_path))
/* Allocate the icon handle and insert into hash map now.
* The async callback fills in the texture handle when
* the decode completes. */
{
if (ti.pixels)
{
uintptr_t *icon = (uintptr_t*)calloc(1, sizeof(*icon));
uintptr_t *icon = (uintptr_t*)calloc(1, sizeof(*icon));
if (!icon)
continue;
video_driver_texture_load(&ti,
TEXTURE_FILTER_MIPMAP_LINEAR,
icon);
RHMAP_SET_STR(state->icons->system,
core_info->core_file_id.str, icon);
/* Add icon to hash map */
RHMAP_SET_STR(state->icons->system,
core_info->core_file_id.str, icon);
}
image_texture_free(&ti);
task_push_icon_load(icon_path, rgba_supported,
icon, contentless_icon_load_gen,
&contentless_icon_load_gen);
}
}
}
@@ -387,7 +369,11 @@ void menu_contentless_cores_context_init(void)
void menu_contentless_cores_context_deinit(void)
{
if (contentless_cores_state)
{
/* Invalidate in-flight async icon loads before unloading */
contentless_icon_load_gen++;
contentless_cores_unload_icons(contentless_cores_state);
}
}
void menu_contentless_cores_free(void)
@@ -395,6 +381,8 @@ void menu_contentless_cores_free(void)
if (!contentless_cores_state)
return;
/* Invalidate in-flight async icon loads before freeing */
contentless_icon_load_gen++;
contentless_cores_free_info_entries(contentless_cores_state);
contentless_cores_unload_icons(contentless_cores_state);
free(contentless_cores_state);
+16 -14
View File
@@ -398,10 +398,14 @@ static void explore_unload_icons(explore_state_t *state)
video_driver_texture_unload(&state->icons[i]);
}
/* File-static generation counter for async icon loads */
static uint64_t explore_icon_load_gen = 0;
static void explore_load_icons(explore_state_t *state)
{
char path[PATH_MAX_LENGTH];
size_t i, _len, system_count;
bool supports_rgba = video_driver_supports_rgba();
if (!state)
return;
@@ -411,6 +415,9 @@ static void explore_load_icons(explore_state_t *state)
/* unload any icons that could exist from a previous call to this */
explore_unload_icons(state);
/* Invalidate any in-flight async icon loads */
explore_icon_load_gen++;
/* RBUF_RESIZE leaves memory uninitialised,
have to zero it 'manually' */
RBUF_RESIZE(state->icons, system_count);
@@ -425,7 +432,6 @@ static void explore_load_icons(explore_state_t *state)
for (i = 0; i != system_count; i++)
{
struct texture_image ti;
size_t __len = _len;
__len += strlcpy(path + _len,
state->by[EXPLORE_BY_SYSTEM][i]->str,
@@ -434,19 +440,9 @@ static void explore_load_icons(explore_state_t *state)
if (!path_is_valid(path))
continue;
ti.width = 0;
ti.height = 0;
ti.pixels = NULL;
ti.supports_rgba = video_driver_supports_rgba();
if (!image_texture_load(&ti, path))
continue;
if (ti.pixels)
video_driver_texture_load(&ti,
TEXTURE_FILTER_MIPMAP_LINEAR, &state->icons[i]);
image_texture_free(&ti);
task_push_icon_load(path, supports_rgba,
&state->icons[i], explore_icon_load_gen,
&explore_icon_load_gen);
}
}
@@ -1841,7 +1837,11 @@ void menu_explore_context_init(void)
void menu_explore_context_deinit(void)
{
if (explore_state)
{
/* Invalidate in-flight async icon loads before unloading */
explore_icon_load_gen++;
explore_unload_icons(explore_state);
}
}
void menu_explore_free_state(explore_state_t *state)
@@ -1858,6 +1858,8 @@ void menu_explore_free_state(explore_state_t *state)
playlist_free(state->playlists[i]);
RBUF_FREE(state->playlists);
/* Invalidate in-flight async icon loads before freeing */
explore_icon_load_gen++;
explore_unload_icons(state);
RBUF_FREE(state->icons);
+95
View File
@@ -28,6 +28,7 @@
#include "tasks_internal.h"
#include "../configuration.h"
#include "../gfx/video_driver.h"
enum image_status_enum
{
@@ -519,3 +520,97 @@ bool task_push_image_load(const char *fullpath,
return true;
}
/* -----------------------------------------------------------------------
* Async icon/texture loading
*
* Wraps task_push_image_load with a built-in callback that uploads the
* decoded image to the GPU via video_driver_texture_load and stores the
* resulting handle at *target_texture.
*
* A generation counter prevents stale callbacks from writing into freed
* memory when the owning list is rebuilt or destroyed between queue and
* completion.
*
* The generation counter must be a static local (or file-static) in the
* calling module — NOT inside a heap-allocated struct that could be freed.
* Each subsystem (ozone, xmb, explore, contentless) maintains its own
* counter so bumping one doesn't invalidate another's in-flight loads.
*
* Usage (in caller):
* static uint64_t my_gen = 0;
* my_gen++; // invalidate previous batch
* for (i = 0; i < N; i++)
* task_push_icon_load(path, rgba, &node->icon, my_gen, &my_gen);
* ----------------------------------------------------------------------- */
typedef struct
{
uintptr_t *target; /* where to store the GPU texture handle */
uint64_t generation; /* snapshot of gen counter at queue time */
uint64_t *generation_ptr; /* pointer to the STATIC gen counter */
} icon_load_tag_t;
static void cb_task_icon_load(retro_task_t *task,
void *task_data, void *user_data, const char *error)
{
struct texture_image *img = (struct texture_image*)task_data;
icon_load_tag_t *tag = (icon_load_tag_t*)user_data;
if (!tag)
goto end;
/* Generation check: if the counter was bumped since this task was
* queued, the target pointer may be invalid — skip the write.
* generation_ptr points to a static variable in the calling module
* so it is always valid (never freed). */
if (tag->generation != *tag->generation_ptr)
goto end;
if (!img || img->width < 1 || img->height < 1 || !img->pixels)
goto end;
/* Unload previous texture if any */
if (tag->target && *tag->target)
video_driver_texture_unload(tag->target);
video_driver_texture_load(img, TEXTURE_FILTER_MIPMAP_LINEAR,
tag->target);
end:
if (img)
{
image_texture_free(img);
free(img);
}
free(tag);
}
bool task_push_icon_load(const char *fullpath,
bool supports_rgba,
uintptr_t *target_texture,
uint64_t generation,
uint64_t *generation_ptr)
{
icon_load_tag_t *tag = NULL;
if (!fullpath || !target_texture || !generation_ptr)
return false;
tag = (icon_load_tag_t*)malloc(sizeof(*tag));
if (!tag)
return false;
tag->target = target_texture;
tag->generation = generation;
tag->generation_ptr = generation_ptr;
if (!task_push_image_load(fullpath, supports_rgba, 0,
cb_task_icon_load, tag))
{
free(tag);
return false;
}
return true;
}
+8
View File
@@ -183,6 +183,14 @@ bool task_push_image_load(const char *fullpath,
bool supports_rgba, unsigned upscale_threshold,
retro_task_callback_t cb, void *userdata);
/* Async icon/texture loading. generation_ptr must point to a static
* variable in the calling module (not a heap struct field). */
bool task_push_icon_load(const char *fullpath,
bool supports_rgba,
uintptr_t *target_texture,
uint64_t generation,
uint64_t *generation_ptr);
#ifdef HAVE_LIBRETRODB
bool task_push_dbscan(
const char *playlist_directory,