diff --git a/gfx/gfx_thumbnail.c b/gfx/gfx_thumbnail.c index 18f5865e2e06..6c9dfda7ef76 100644 --- a/gfx/gfx_thumbnail.c +++ b/gfx/gfx_thumbnail.c @@ -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 @@ -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) @@ -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); @@ -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; diff --git a/gfx/gfx_thumbnail.h b/gfx/gfx_thumbnail.h index df633a636ff3..aac7687ede9c 100644 --- a/gfx/gfx_thumbnail.h +++ b/gfx/gfx_thumbnail.h @@ -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.) */ + unsigned max_concurrent_loads; + unsigned current_loads; }; typedef struct gfx_thumbnail_state gfx_thumbnail_state_t; @@ -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 */ + +/* 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 diff --git a/menu/drivers/xmb.c b/menu/drivers/xmb.c index 0e686dae974c..70fac638b6d8 100644 --- a/menu/drivers/xmb.c +++ b/menu/drivers/xmb.c @@ -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; @@ -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 @@ -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 + xmb->use_ps3_layout = xmb_use_ps3_layout(settings->uints.menu_xmb_layout, width, height); xmb->last_use_ps3_layout = xmb->use_ps3_layout;