gfx_thumbnail: the viewer reuses the still the file browser shows

Pressing OK on an image in the file browser read and decoded the file
a second time even though its thumbnail was already on screen.

menu_media_open_selected_file only reuses a thumbnail when
gfx_thumbnail_has_file says it holds the file, and that answered yes
only for a live animation session. Every still (PNG, JPEG, BMP, TGA,
single-frame GIF/WEBP) has no session, and neither does an animation
the preview budget refuses - large ones land as a still - nor one that
finished its last loop. All of those were reset and loaded again.

Each thumbnail now records a 64-bit hash of the path it was requested
from (no per-thumbnail path buffer), cleared on reset.
gfx_thumbnail_has_file matches it while the still is shown, or while
its load is in flight and not superseded; a failed or cancelled load
still gets requested again.

samples/gfx/gfx_thumbnail_anim lane 16 drives the real
gfx_thumbnail.c: a still is recognised in flight and once shown, and
not for another path, after a reset, when superseded or when the load
failed. It fails on the old code.
This commit is contained in:
LibretroAdmin authored and libretroadmin committed 2026-11-07 12:57:10 +01:00
1 parent 5c6b2a3867
commit b93738dd4a
3 files changed
+153 -3

No files matched your search

+44 -3
View File
@@ -992,11 +992,46 @@ static bool gfx_thumb_frame_budget(void *ud, size_t avail, size_t len)
return cpu_features_get_time_usec() - *(int64_t*)ud < 2000;
}
/* 64-bit FNV-1a; never 0 for a non-empty path, so 0 means "none". */
static uint64_t gfx_thumbnail_file_hash(const char *path)
{
uint64_t h = 0xcbf29ce484222325ULL;
if (!path || !*path)
return 0;
while (*path)
h = (h ^ (unsigned char)*path++) * 0x100000001b3ULL;
return h ? h : 1;
}
/* Whether @thumbnail already holds @path, shown or on its way.
*
* Asking only the animation session left out every still - and every
* animation the preview budget refused, which lands as a still - so
* opening the viewer on the image the file browser was showing reset
* the thumbnail and read and decoded the file all over again. */
bool gfx_thumbnail_has_file(const gfx_thumbnail_t *thumbnail, const char *path)
{
const gfx_anim_preview_t *session = thumbnail
? (const gfx_anim_preview_t*)thumbnail->anim_sess : NULL;
return session && string_is_equal(session->path, path);
const gfx_anim_preview_t *session;
if (!thumbnail || !path || !*path)
return false;
if ((session = (const gfx_anim_preview_t*)thumbnail->anim_sess))
return string_is_equal(session->path, path);
if (!thumbnail->file_hash)
return false;
switch (GFX_THUMB_STATUS_LOAD(
(retro_atomic_int_t*)&thumbnail->status))
{
case GFX_THUMBNAIL_STATUS_AVAILABLE:
break;
case GFX_THUMBNAIL_STATUS_PENDING:
/* superseded: nothing will ever deliver it */
if (thumbnail->list_id != gfx_thumb_st.list_id)
return false;
break;
default:
return false;
}
return thumbnail->file_hash == gfx_thumbnail_file_hash(path);
}
/* Main-thread transport command; the audio mixer applies its request
@@ -1717,6 +1752,9 @@ void gfx_thumbnail_request(
{
gfx_thumbnail_tag_t *thumbnail_tag;
thumbnail->file_hash = gfx_thumbnail_file_hash(
thumbnail_path);
/* A WebM or MP4 that serves as its own thumbnail takes
* the sliding window rather than the whole-file still
* decode; see gfx_thumbnail_try_video_open. */
@@ -1839,6 +1877,8 @@ void gfx_thumbnail_request_file(
|| !path_is_valid(file_path))
return;
thumbnail->file_hash = gfx_thumbnail_file_hash(file_path);
/* Video containers take the sliding window before the still task
* is ever queued; see gfx_thumbnail_try_video_open. */
if (gfx_thumbnail_try_video_open(thumbnail, file_path))
@@ -1941,6 +1981,7 @@ void gfx_thumbnail_reset(gfx_thumbnail_t *thumbnail)
* thread learn on delivery that this reset happened. */
GFX_THUMB_STATUS_STORE(&thumbnail->status, GFX_THUMBNAIL_STATUS_UNKNOWN);
thumbnail->texture = 0;
thumbnail->file_hash = 0;
thumbnail->dims = 0;
thumbnail->alpha = 0.0f;
thumbnail->delay_timer = 0.0f;
+6
View File
@@ -270,6 +270,12 @@ typedef struct
* ever deliver it, so the slot must be re-requested rather than
* waited on. */
uint64_t list_id;
/* 64-bit FNV-1a of the file this thumbnail was last requested
* from, 0 when none; cleared by gfx_thumbnail_reset. Lets
* gfx_thumbnail_has_file recognise a still (or an animation that
* fell back to one) without keeping a path buffer per thumbnail,
* so the viewer takes what the browser already decoded. */
uint64_t file_hash;
/* Both axes in one word, VIDEO_SCALE_PACK's layout. */
unsigned dims;
float alpha;
@@ -1088,6 +1088,109 @@ int main(void)
bad = 1;
}
/* 16. already loaded: the file browser shows an image as the
* selected entry's thumbnail, and OK opens the viewer on that
* same file. menu_media_open_selected_file reuses a thumbnail
* only when gfx_thumbnail_has_file says it holds the file, and
* that answered yes only for a live animation session - so
* every still (PNG, JPEG, BMP, TGA, single-frame GIF/WEBP), and
* every animation too large for the preview budget (it falls
* back to a still), was reset and decoded a second time on OK.
* A still requested from a path must be recognised as holding
* it once shown and while its load is in flight; not for
* another path, not after a reset, not when its load was
* superseded, and not when the load failed. */
{
char still[256], other[256];
int ok16 = 1;
snprintf(still, sizeof(still), "/tmp/gfx_thumb_has_%d.bin",
(int)getpid());
snprintf(other, sizeof(other), "/tmp/gfx_thumb_has_%d_b.bin",
(int)getpid());
{
FILE *f = fopen(still, "wb");
if (f)
{
fwrite("still", 1, 5, f);
fclose(f);
}
}
task_queue_init(false, NULL);
gt_still_capture = 1;
gt_async_mode = 0;
memset(&th, 0, sizeof(th));
/* in flight: OK pressed before the decode came back */
gt_still_cb = NULL;
gfx_thumbnail_request_file(still, &th, 0);
if (!gt_still_cb || !gfx_thumbnail_has_file(&th, still))
{
printf("[FAIL] has_file: a still in flight not recognised\n");
ok16 = 0;
}
if (gt_still_cb)
{
struct texture_image *img = gt_still_image(0xA000u, 0);
if (img)
gt_still_cb(NULL, img, gt_still_ud, NULL);
}
/* shown: the viewer must take it as is */
if ( (int)th.status != GFX_THUMBNAIL_STATUS_AVAILABLE
|| !gfx_thumbnail_has_file(&th, still))
{
printf("[FAIL] has_file: a shown still not recognised "
"(status=%d)\n", (int)th.status);
ok16 = 0;
}
if ( gfx_thumbnail_has_file(&th, other)
|| gfx_thumbnail_has_file(&th, NULL)
|| gfx_thumbnail_has_file(&th, ""))
{
printf("[FAIL] has_file: a still claimed another path\n");
ok16 = 0;
}
gfx_thumbnail_reset(&th);
if (gfx_thumbnail_has_file(&th, still))
{
printf("[FAIL] has_file: still recognised after a reset\n");
ok16 = 0;
}
/* superseded: the request's list was cancelled, nothing will
* ever deliver it */
gt_still_cb = NULL;
gfx_thumbnail_request_file(still, &th, 0);
gfx_thumbnail_cancel_pending_requests();
if (gfx_thumbnail_has_file(&th, still))
{
printf("[FAIL] has_file: a superseded load recognised\n");
ok16 = 0;
}
if (gt_still_cb)
gt_still_cb(NULL, NULL, gt_still_ud, NULL);
gfx_thumbnail_reset(&th);
/* failed: no image came back, the viewer must try again */
gt_still_cb = NULL;
gfx_thumbnail_request_file(still, &th, 0);
if (gt_still_cb)
gt_still_cb(NULL, NULL, gt_still_ud, "decode failed");
if (gfx_thumbnail_has_file(&th, still))
{
printf("[FAIL] has_file: a failed load recognised "
"(status=%d)\n", (int)th.status);
ok16 = 0;
}
gfx_thumbnail_reset(&th);
gt_still_capture = 0;
task_queue_deinit();
remove(still);
if (ok16)
printf("[ok] has_file: a still is recognised in flight and "
"shown, not for another path, after a reset, when "
"superseded or when it failed\n");
else
bad = 1;
}
remove(path);
printf("%s\n", bad ? "FAILED" : "PASS");
return bad;