-
-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Fix XMB thumbnail memory exhaustion on low-memory devices #18854
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
|
|
@@ -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 */ | ||||
|
|
||||
|
Comment on lines
+159
to
+160
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
|
||||
| /* 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 | ||||
|
|
||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
|---|---|---|
|
|
@@ -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 | ||
|
Comment on lines
+9233
to
+9239
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Writing
LOW-MEMORY FIXeverywhere is unnecessary.