Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions gfx/gfx_thumbnail.c
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,30 @@ gfx_thumbnail_state_t *gfx_thumb_get_ptr(void)
return &gfx_thumb_st;
}

/* LOW-MEMORY FIX: Concurrent load management */

void gfx_thumbnail_set_max_concurrent_loads(unsigned max_loads)
{
gfx_thumbnail_state_t *p_gfx_thumb = &gfx_thumb_st;
p_gfx_thumb->max_concurrent_loads = max_loads;
}

unsigned gfx_thumbnail_get_concurrent_loads(void)
{
gfx_thumbnail_state_t *p_gfx_thumb = &gfx_thumb_st;
return p_gfx_thumb->current_loads;
}

bool gfx_thumbnail_can_start_load(void)
{
gfx_thumbnail_state_t *p_gfx_thumb = &gfx_thumb_st;

if (p_gfx_thumb->max_concurrent_loads == 0)
return true;

return p_gfx_thumb->current_loads < p_gfx_thumb->max_concurrent_loads;
}

/* Setters */

/* When streaming thumbnails, sets time in ms that an
Expand Down Expand Up @@ -152,6 +176,9 @@ static void gfx_thumbnail_handle_upload(
if (!thumbnail_tag)
goto end;

/* LOW-MEMORY FIX: Decrement concurrent load counter */
p_gfx_thumb->current_loads--;

/* Ensure that we are operating on the correct
* thumbnail... */
if (thumbnail_tag->list_id != p_gfx_thumb->list_id)
Expand Down Expand Up @@ -295,6 +322,10 @@ void gfx_thumbnail_request(
if (!path_data || !thumbnail)
return;

/* LOW-MEMORY FIX: Check if we can start a new load */
if (!gfx_thumbnail_can_start_load())
return;

/* Reset thumbnail, then set 'missing' status by default
* (saves a number of checks later) */
gfx_thumbnail_reset(thumbnail);
Expand All @@ -317,6 +348,9 @@ void gfx_thumbnail_request(
if (!thumbnail_tag)
goto end;

/* LOW-MEMORY FIX: Increment concurrent load counter */
p_gfx_thumb->current_loads++;

/* Configure user data */
thumbnail_tag->thumbnail = thumbnail;
thumbnail_tag->list_id = p_gfx_thumb->list_id;
Expand Down
19 changes: 19 additions & 0 deletions gfx/gfx_thumbnail.h
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,11 @@ struct gfx_thumbnail_state
/* When true, 'fade in' animation will also be
* triggered for missing thumbnails */
bool fade_missing;

/* LOW-MEMORY FIX: Maximum number of concurrent thumbnail load tasks
* Helps prevent memory exhaustion on low-memory devices (RPi, Switch, etc.) */
Comment on lines +131 to +132
Copy link
Copy Markdown
Member

@RobLoach RobLoach Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Writing LOW-MEMORY FIX everywhere is unnecessary.

Suggested change
/* LOW-MEMORY FIX: Maximum number of concurrent thumbnail load tasks
* Helps prevent memory exhaustion on low-memory devices (RPi, Switch, etc.) */
/* Limits the number of thumbnails that can be loaded concurrently.
* Helps prevent memory exhaustion on low-memory devices (RPi, Switch, etc.) */

unsigned max_concurrent_loads;
unsigned current_loads;
};

typedef struct gfx_thumbnail_state gfx_thumbnail_state_t;
Expand All @@ -151,6 +156,20 @@ void gfx_thumbnail_set_fade_duration(float duration);
* any 'thumbnail unavailable' notifications */
void gfx_thumbnail_set_fade_missing(bool fade_missing);

/* LOW-MEMORY FIX: Concurrent load management API */

Comment on lines +159 to +160
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not needed. It's covered in the doxygen docs of the function definitions.

Suggested change
/* LOW-MEMORY FIX: Concurrent load management API */

/* Sets maximum number of concurrent thumbnail load tasks
* allowed at any time. Helps prevent memory exhaustion on
* low-memory devices.
* > Use 0 for unlimited (default behavior) */
void gfx_thumbnail_set_max_concurrent_loads(unsigned max_loads);

/* Returns current number of active thumbnail load tasks */
unsigned gfx_thumbnail_get_concurrent_loads(void);

/* Returns whether a new thumbnail load can be started */
bool gfx_thumbnail_can_start_load(void);

/* Core interface */

/* When called, prevents the handling of any pending
Expand Down
134 changes: 96 additions & 38 deletions menu/drivers/xmb.c
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to apply this beyond XMB? The other themes load thumbnails too, maybe they'd apply?

Original file line number Diff line number Diff line change
Expand Up @@ -6847,6 +6847,12 @@ static void xmb_render(void *data,
unsigned last = (unsigned)end;
unsigned gfx_thumbnail_upscale_threshold = settings->uints.gfx_thumbnail_upscale_threshold;
bool network_on_demand_thumbnails = settings->bools.network_on_demand_thumbnails;
/* LOW-MEMORY FIX: Variables for rapid scroll detection
* Must be declared at block start for C89 compliance */
static retro_time_t last_scroll_time = 0;
static unsigned scroll_count = 0;
retro_time_t current_time;
bool rapid_scroll;

if (!xmb)
return;
Expand Down Expand Up @@ -7150,57 +7156,99 @@ static void xmb_render(void *data,
/* Handle any pending icon thumbnail load requests */
if (xmb->thumbnails.pending_icons != XMB_PENDING_THUMBNAIL_NONE)
{
/* Limit image loading per frame to prevent slowdowns,
* and hide the usual icon while pending */
uint8_t max_per_frame = 2;
uint8_t cur_per_frame = 0;
/* LOW-MEMORY FIX: Detect rapid scrolling and defer thumbnail loading
* This prevents texture exhaustion on devices like Raspberry Pi and Switch */
current_time = cpu_features_get_time_usec();

/* Based on height of screen calculate the available entries that are visible */
if (height)
xmb_calculate_visible_range(xmb, height, end, (unsigned)selection, &first, &last);
/* Count scrolls within 100ms window */
if (current_time - last_scroll_time < 100000)
scroll_count++;
else
scroll_count = 1;

xmb->thumbnails.pending_icons = XMB_PENDING_THUMBNAIL_NONE;
last_scroll_time = current_time;

for (i = first; i <= last; i++)
/* If scrolling rapidly (>5 scrolls in 100ms), skip thumbnail loading */
rapid_scroll = (scroll_count > 5);

if (!rapid_scroll)
{
xmb_node_t *node = (xmb_node_t*)selection_buf->list[i].userdata;
xmb_icons_t *thumbnail_icon;
/* Limit image loading per frame to prevent slowdowns,
* and hide the usual icon while pending */
uint8_t max_per_frame = 2;
uint8_t cur_per_frame = 0;

if (!node)
continue;
/* Based on height of screen calculate the available entries that are visible */
if (height)
xmb_calculate_visible_range(xmb, height, end, (unsigned)selection, &first, &last);

thumbnail_icon = &node->thumbnail_icon;
xmb->thumbnails.pending_icons = XMB_PENDING_THUMBNAIL_NONE;

if (cur_per_frame >= max_per_frame)
for (i = first; i <= last; i++)
{
node->icon_hide = true;
xmb->thumbnails.pending_icons = XMB_PENDING_THUMBNAIL_ICONS;
continue;
}
xmb_node_t *node = (xmb_node_t*)selection_buf->list[i].userdata;
xmb_icons_t *thumbnail_icon;

if (!node)
continue;

thumbnail_icon = &node->thumbnail_icon;

/* LOW-MEMORY FIX: Skip loading if already at max concurrent loads */
if (!gfx_thumbnail_can_start_load())
{
xmb->thumbnails.pending_icons = XMB_PENDING_THUMBNAIL_ICONS;
continue;
}

if (cur_per_frame >= max_per_frame)
{
node->icon_hide = true;
xmb->thumbnails.pending_icons = XMB_PENDING_THUMBNAIL_ICONS;
continue;
}

if ( thumbnail_icon->icon.status == GFX_THUMBNAIL_STATUS_UNKNOWN
&& !string_is_empty(thumbnail_icon->thumbnail_path_data.icon_path))
{
node->icon_hide = false;
if (!xmb_load_dynamic_icon(
thumbnail_icon->thumbnail_path_data.icon_path,
&thumbnail_icon->icon))
{
gfx_thumbnail_request_stream(
&thumbnail_icon->thumbnail_path_data,
p_anim,
GFX_THUMBNAIL_ICON,
playlist, i,
&thumbnail_icon->icon,
gfx_thumbnail_upscale_threshold,
network_on_demand_thumbnails);
}
else
cur_per_frame++;
}

if ( thumbnail_icon->icon.status == GFX_THUMBNAIL_STATUS_UNKNOWN
&& !string_is_empty(thumbnail_icon->thumbnail_path_data.icon_path))
if (thumbnail_icon->icon.status == GFX_THUMBNAIL_STATUS_UNKNOWN)
xmb->thumbnails.pending_icons = XMB_PENDING_THUMBNAIL_ICONS;
}
}
else
{
/* During rapid scroll, reset off-screen thumbnails to free memory */
size_t sel = menu_st->selection_ptr;
for (i = first; i <= last; i++)
{
node->icon_hide = false;
if (!xmb_load_dynamic_icon(
thumbnail_icon->thumbnail_path_data.icon_path,
&thumbnail_icon->icon))
if (i != sel)
{
gfx_thumbnail_request_stream(
&thumbnail_icon->thumbnail_path_data,
p_anim,
GFX_THUMBNAIL_ICON,
playlist, i,
&thumbnail_icon->icon,
gfx_thumbnail_upscale_threshold,
network_on_demand_thumbnails);
xmb_node_t *other_node = (xmb_node_t*)selection_buf->list[i].userdata;
if (other_node)
gfx_thumbnail_reset(&other_node->thumbnail_icon.icon);
}
else
cur_per_frame++;
}

if (thumbnail_icon->icon.status == GFX_THUMBNAIL_STATUS_UNKNOWN)
xmb->thumbnails.pending_icons = XMB_PENDING_THUMBNAIL_ICONS;
/* Still need to retry later */
xmb->thumbnails.pending_icons = XMB_PENDING_THUMBNAIL_ICONS;
}
}
else if (xmb->thumbnails.icon.status == GFX_THUMBNAIL_STATUS_UNKNOWN
Expand Down Expand Up @@ -9180,6 +9228,16 @@ static void *xmb_init(void **userdata, bool video_is_threaded)
gfx_thumbnail_set_fade_duration(-1.0f);
gfx_thumbnail_set_fade_missing(false);

/* LOW-MEMORY FIX: Set platform-appropriate concurrent load limits
* Prevents texture memory exhaustion on RPi, Switch, and other low-RAM devices */
#if defined(HAVE_OPENGLES) || defined(__SWITCH__) || defined(ANDROID)
/* Low-memory platforms: limit to 2 concurrent loads */
gfx_thumbnail_set_max_concurrent_loads(2);
#else
/* Desktop platforms: allow 4 concurrent loads */
gfx_thumbnail_set_max_concurrent_loads(4);
#endif
Comment on lines +9233 to +9239
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardcoding 2 vs 4 indicates it could likely live better as a defined macro that could be changed during compilation flags.

#ifndef GFX_THUMBNAIL_MAX_CONCURRENT_LOADS
#define GFX_THUMBNAIL_MAX_CONCURRENT_LOADS 4
#endif


xmb->use_ps3_layout =
xmb_use_ps3_layout(settings->uints.menu_xmb_layout, width, height);
xmb->last_use_ps3_layout = xmb->use_ps3_layout;
Expand Down
Loading