Overlays: an animated PNG in the pack plays

An overlay image that carries an acTL animation chunk keeps its file
bytes and gets a stream over them; the pack gives it a surface with
slots rather than a static one, and its already-decoded first frame
goes into the first slot, so nothing decodes twice. Each poll composes
whatever frame is due into a free slot and submits it, which updates
the texture in place: the pages' handles stand, so a page switch is
still an upload of nothing and an animation costs no texture of its
own. A slot the video thread has not finished with means the frame is
skipped rather than waited for - an overlay is a control surface, and
a late button image is worse than a dropped one - and the end of a
pass rewinds, since a control surface that animates should keep doing
it. A hidden overlay does not advance: composing frames nobody can
see is work with no observer, and a resumed loop is invisible on
button art.

An animated image keeps its compressed bytes and its slots where a
still releases its pixels once the texture exists, which is what a
stream needs to decode from. samples/gfx/overlay_apng builds its own
APNG and holds the contract the pages depend on: frames that differ,
a pass that ends and replays, one texture across the submits, and a
free with a frame still in flight.
This commit is contained in:
libretroadmin committed 2026-09-18 22:42:40 +02:00
1 parent 247519d69c
commit dac760d996
8 files changed
+519 -8

No files matched your search

+18
View File
@@ -757,6 +757,24 @@ jobs:
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1 \
timeout 60 ./modeline_edid_parse_test
- name: Build and run overlay_apng_test (ASan + UBSan, TSan)
shell: bash
working-directory: samples/gfx/overlay_apng
run: |
set -eu
# An animated overlay image is a surface with slots whose
# texture is updated in place, so the pages' handles stand
# while its frames advance. The sample builds its own APNG
# fixture and holds that contract: distinct frames, a pass
# that ends and replays, one texture across the submits, and
# a free with a frame still in flight.
make clean all SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1:allocator_may_return_null=1 \
UBSAN_OPTIONS=print_stacktrace=1 timeout 120 ./overlay_apng_test fixtures/anim.png
make clean all SANITIZER=thread
TSAN_OPTIONS=halt_on_error=1 timeout 120 ./overlay_apng_test fixtures/anim.png
make clean
- name: Build and run gfx_thumbnail_anim_test (ASan + UBSan, TSan)
shell: bash
working-directory: samples/gfx/gfx_thumbnail_anim
+148 -8
View File
@@ -39,6 +39,9 @@
#include "input_driver.h"
#include "../gfx/gfx_instrument.h"
#include "../gfx/gfx_surface.h"
#ifdef HAVE_RPNG
#include <formats/rpng.h>
#endif
#include "input_keymaps.h"
#include "input_remapping.h"
#include "input_osk.h"
@@ -3260,6 +3263,61 @@ static void input_overlay_update_desc_geom(input_overlay_t *ol,
desc->delta_y = 0.0f;
}
#ifdef HAVE_RPNG
/* Advance every animated image of the pack whose frame is due, once
* per input poll on the main thread. A frame is composed by the
* stream into a free slot and submitted: the texture is updated in
* place, so the page's handles stand and a page switch is still an
* upload of nothing. A slot still with the video thread means the
* frame is skipped rather than waited for - the overlay is a control
* surface, and a late button image is worse than a dropped one. */
void input_overlay_animate(input_overlay_t *ol, retro_time_t now)
{
size_t i;
if ( !ol
|| !ol->anim_stream
|| !(ol->flags & INPUT_OVERLAY_ENABLE)
|| !ol->surfaces)
return;
for (i = 0; i < ol->num_images; i++)
{
rpng_apng_stream_t *st = (rpng_apng_stream_t*)ol->anim_stream[i];
gfx_surface_t *s = (gfx_surface_t*)ol->surfaces[i];
const uint32_t *frame;
unsigned slot;
int duration_ms = 0;
if (!st || !s || !s->num_slots)
continue;
if (ol->anim_next_us[i] && now < ol->anim_next_us[i])
continue;
slot = s->inflight ? (s->inflight_slot ^ 1) : 0;
if (s->inflight && slot == s->inflight_slot)
continue;
if (!(frame = rpng_apng_stream_next(st, &duration_ms)))
{
/* End of a pass: overlays loop, which is what an animated
* control surface is for. */
rpng_apng_stream_rewind(st);
if (!(frame = rpng_apng_stream_next(st, &duration_ms)))
continue;
}
memcpy(s->slots[slot], frame,
(size_t)s->width * s->height * sizeof(uint32_t));
if (gfx_surface_submit(s, slot, ol->images[i]->supports_rgba)
== GFX_SURFACE_SUBMIT_FAILED)
continue;
ol->anim_next_us[i] = now
+ (retro_time_t)(duration_ms > 0 ? duration_ms : 100) * 1000;
}
}
#endif
/**
* input_overlay_post_poll:
*
@@ -3582,20 +3640,42 @@ static bool input_overlay_upload_textures(input_overlay_t *ol)
return false;
}
/* Each unique image becomes a surface holding one texture: the
* pixels are the pack's and stay where they are, so the surface
* carries no slots of its own and the upload is the same
* submit-and-own path an animated preview frame takes. The
* driver's page then draws from the surfaces' handles. */
/* Each unique image becomes a surface holding one texture. A still
* image's pixels are the pack's and stay where they are, so its
* surface carries no slots of its own and the upload is the same
* submit-and-own path an animated preview frame takes. An animated
* one (APNG) gets slots instead: its stream composes each frame
* into a slot and the texture is updated in place, so the page's
* handles never change and a frame costs no upload of its own. */
for (i = 0; i < ol->num_images; i++)
{
gfx_surface_t *s = gfx_surface_new_static(ol->images[i]->width,
bool animated = ol->anim_stream && ol->anim_stream[i];
gfx_surface_t *s = animated
? gfx_surface_new(ol->images[i]->width, ol->images[i]->height,
2, TEXTURE_FILTER_LINEAR, NULL, NULL)
: gfx_surface_new_static(ol->images[i]->width,
ol->images[i]->height, TEXTURE_FILTER_LINEAR);
GFX_INSTR_INC(GFX_INSTR_OVERLAY_UPLOAD);
GFX_INSTR_ADD(GFX_INSTR_OVERLAY_PIXEL_KIB,
(int)(((size_t)ol->images[i]->width * ol->images[i]->height
* sizeof(uint32_t)) >> 10));
ol->surfaces[i] = s;
if (animated && s)
{
/* The first frame is already composed in the decoded image:
* it goes into a slot, and the stream advances from the
* second on the tick below. */
memcpy(s->slots[0], ol->images[i]->pixels,
(size_t)s->width * s->height * sizeof(uint32_t));
if (gfx_surface_submit(s, 0, ol->images[i]->supports_rgba)
== GFX_SURFACE_SUBMIT_FAILED)
{
input_overlay_release_textures(ol);
return false;
}
ol->anim_next_us[i] = 0;
continue;
}
/* An asset is uploaded once and never again, so a submit that
* the video thread has not finished with is waited out by the
* poll below rather than dropped. */
@@ -3856,6 +3936,24 @@ static void input_overlay_free_images(input_overlay_t *ol)
for (i = 0; i < ol->num_images; i++)
image_texture_free(ol->images[i]);
#ifdef HAVE_RPNG
if (ol->anim_stream)
for (i = 0; i < ol->num_images; i++)
if (ol->anim_stream[i])
rpng_apng_stream_close((rpng_apng_stream_t*)ol->anim_stream[i]);
#endif
if (ol->anim_data)
for (i = 0; i < ol->num_images; i++)
free(ol->anim_data[i]);
free(ol->anim_stream);
free(ol->anim_data);
free(ol->anim_len);
free(ol->anim_next_us);
ol->anim_stream = NULL;
ol->anim_data = NULL;
ol->anim_len = NULL;
ol->anim_next_us = NULL;
free(ol->images);
ol->images = NULL;
}
@@ -6783,7 +6881,7 @@ void input_overlay_check_mouse_cursor(void)
}
static void input_overlay_loaded_move_images(input_overlay_t *ol,
struct string_list *image_list)
struct string_list *image_list, struct string_list *anim_list)
{
size_t i;
@@ -6799,6 +6897,43 @@ static void input_overlay_loaded_move_images(input_overlay_t *ol,
for (i = 0; i < ol->num_images; i++)
ol->images[i] = (struct texture_image*)image_list->elems[i].attr.p;
#ifdef HAVE_RPNG
/* The animated images' file bytes, and a stream over each: the
* pack owns both from here, and frees them with the images. A
* pack with no animation allocates nothing. */
if (anim_list && anim_list->size >= ol->num_images)
{
size_t animated = 0;
for (i = 0; i < ol->num_images; i++)
if (anim_list->elems[i].attr.p)
animated++;
if ( animated
&& (ol->anim_data = (void**)calloc(ol->num_images, sizeof(void*)))
&& (ol->anim_len = (size_t*)calloc(ol->num_images, sizeof(size_t)))
&& (ol->anim_stream = (void**)calloc(ol->num_images, sizeof(void*)))
&& (ol->anim_next_us = (int64_t*)calloc(ol->num_images, sizeof(int64_t))))
{
for (i = 0; i < ol->num_images; i++)
{
overlay_anim_src_t *src =
(overlay_anim_src_t*)anim_list->elems[i].attr.p;
if (!src)
continue;
ol->anim_data[i] = src->data;
ol->anim_len[i] = src->len;
ol->anim_stream[i] = rpng_apng_stream_open(
(const uint8_t*)src->data, src->len);
if (ol->anim_stream[i])
rpng_apng_stream_set_argb((rpng_apng_stream_t*)ol->anim_stream[i],
ol->images[i]->supports_rgba ? 0 : 1);
}
}
}
if (anim_list)
for (i = 0; i < anim_list->size; i++)
free(anim_list->elems[i].attr.p);
#endif
}
/* task_data = overlay_task_data_t* */
@@ -6861,8 +6996,10 @@ static void input_overlay_loaded(retro_task_t *task,
#endif
if (ol->num_images > 0)
input_overlay_loaded_move_images(ol, data->image_list);
input_overlay_loaded_move_images(ol, data->image_list, data->anim_list);
string_list_free(data->image_list);
if (data->anim_list)
string_list_free(data->anim_list);
free(data);
@@ -7754,6 +7891,9 @@ void input_driver_poll(void)
break;
}
#ifdef HAVE_RPNG
input_overlay_animate(input_st->overlay_ptr, cpu_features_get_time_usec());
#endif
input_poll_overlay(
!!(input_st->flags & INP_FLAG_KB_MAPPING_BLOCKED),
settings,
+29
View File
@@ -420,6 +420,16 @@ struct input_overlay
* which overlay::textures point into. Built by the enable on a
* driver with load_textures, unloaded and freed by the disable. */
uintptr_t *page_textures;
/* An animated image's file bytes and its APNG stream, one entry per
* unique image, NULL for the still ones. The bytes are kept because
* the stream decodes from them frame by frame; a still image's
* bytes and pixels both go once its texture exists. */
void **anim_data;
size_t *anim_len;
void **anim_stream;
/* When the frame showing now is due to be replaced, in
* microseconds on the same clock as the rest of the frontend. */
int64_t *anim_next_us;
/* A gfx_surface per unique image, holding that texture: the same
* ownership the animated previews use, so an overlay asset and a
* preview frame reach the GPU through one path. num_images of
@@ -478,6 +488,11 @@ typedef struct
struct overlay *overlays;
struct overlay *active;
struct string_list *image_list;
/* Parallel to image_list: for an image that turned out to be an
* APNG, the file bytes its frames are composed from, as an
* overlay_anim_src_t in attr.p; NULL entries for the stills. The
* pack takes them over with the images. */
struct string_list *anim_list;
size_t size;
uint16_t overlay_types;
uint8_t flags;
@@ -496,6 +511,20 @@ void input_overlay_auto_rotate_(
bool input_overlay_enable,
input_overlay_t *ol);
/* The file bytes of one animated overlay image, handed from the
* loader to the pack, which frees them with the image. */
typedef struct
{
void *data;
size_t len;
} overlay_anim_src_t;
/* Advance the pack's animated images to the frame due at @now, one
* per poll on the main thread; a pack with none is untouched. The
* frames update their textures in place, so the pages' handles stand
* and nothing is uploaded. */
void input_overlay_animate(input_overlay_t *ol, retro_time_t now);
/* Unload the pack's textures (see video_overlay_interface::load_textures)
* and forget the page lists. Safe to call with none uploaded. */
void input_overlay_release_textures(input_overlay_t *ol);
+59
View File
@@ -0,0 +1,59 @@
TARGET := overlay_apng_test
REPO_ROOT := ../../..
LIBRETRO_COMM_DIR := $(REPO_ROOT)/libretro-common
# The real APNG decoder and the real gfx_surface; the video driver is
# the stub in stubs_retroarch.c, as in the other overlay samples.
SOURCES := \
overlay_apng_test.c \
stubs_retroarch.c \
$(REPO_ROOT)/gfx/gfx_surface.c \
$(LIBRETRO_COMM_DIR)/compat/compat_strl.c \
$(LIBRETRO_COMM_DIR)/compat/fopen_utf8.c \
$(LIBRETRO_COMM_DIR)/encodings/encoding_crc32.c \
$(LIBRETRO_COMM_DIR)/encodings/encoding_deflate.c \
$(LIBRETRO_COMM_DIR)/encodings/encoding_utf.c \
$(LIBRETRO_COMM_DIR)/features/features_cpu.c \
$(LIBRETRO_COMM_DIR)/file/file_path.c \
$(LIBRETRO_COMM_DIR)/file/file_path_io.c \
$(LIBRETRO_COMM_DIR)/formats/png/rpng.c \
$(LIBRETRO_COMM_DIR)/formats/png/rpng_apng.c \
$(LIBRETRO_COMM_DIR)/lists/string_list.c \
$(LIBRETRO_COMM_DIR)/memmap/memmap.c \
$(LIBRETRO_COMM_DIR)/streams/file_stream.c \
$(LIBRETRO_COMM_DIR)/streams/trans_stream.c \
$(LIBRETRO_COMM_DIR)/streams/trans_stream_deflate.c \
$(LIBRETRO_COMM_DIR)/streams/trans_stream_pipe.c \
$(LIBRETRO_COMM_DIR)/string/rstrtod.c \
$(LIBRETRO_COMM_DIR)/string/stdstring.c \
$(LIBRETRO_COMM_DIR)/time/rtime.c \
$(LIBRETRO_COMM_DIR)/vfs/vfs_implementation.c
OBJS := $(SOURCES:.c=.o)
CFLAGS += -Wall -std=gnu99 -g -O1 -DHAVE_RPNG \
-I$(REPO_ROOT) -I$(LIBRETRO_COMM_DIR)/include
LDFLAGS += -lm
ifneq ($(SANITIZER),)
CFLAGS := -fsanitize=$(SANITIZER) -fno-omit-frame-pointer $(CFLAGS)
LDFLAGS := -fsanitize=$(SANITIZER) $(LDFLAGS)
endif
all: $(TARGET) fixtures/anim.png
fixtures/anim.png: mkfixture.py
@mkdir -p fixtures
python3 mkfixture.py
$(TARGET): $(OBJS)
$(CC) -o $@ $(OBJS) $(LDFLAGS)
check: all
./$(TARGET) fixtures/anim.png
clean:
rm -f $(OBJS) $(TARGET)
.PHONY: all check clean
+23
View File
@@ -0,0 +1,23 @@
import zlib, struct
def chunk(t, d):
c = t + d
return struct.pack('>I', len(d)) + c + struct.pack('>I', zlib.crc32(c) & 0xffffffff)
W = H = 4
def idat(color):
raw = b''.join(b'\x00' + bytes(color) * W for _ in range(H))
return zlib.compress(raw)
def fctl(seq):
# seq, w, h, x, y, delay_num, delay_den, dispose, blend
return struct.pack('>IIIIIHHBB', seq, W, H, 0, 0, 1, 10, 0, 0)
png = b'\x89PNG\r\n\x1a\n'
png += chunk(b'IHDR', struct.pack('>IIBBBBB', W, H, 8, 6, 0, 0, 0))
png += chunk(b'acTL', struct.pack('>II', 3, 0))
s = 0
png += chunk(b'fcTL', fctl(s)); s += 1
png += chunk(b'IDAT', idat((255, 0, 0, 255)))
for col in ((0, 255, 0, 255), (0, 0, 255, 255)):
png += chunk(b'fcTL', fctl(s)); s += 1
png += chunk(b'fdAT', struct.pack('>I', s) + idat(col)); s += 1
png += chunk(b'IEND', b'')
open('fixtures/anim.png', 'wb').write(png)
print('bytes', len(png))
@@ -0,0 +1,130 @@
/* overlay_apng_test.c -- an animated overlay image advances in place.
*
* The pack's animated images are gfx_surfaces with slots, and a frame
* is composed into a free slot and submitted, which updates the
* texture the pages already point at. What this asserts is what that
* buys and what it must not break:
*
* - the stream yields distinct frames and loops at the end of a
* pass, which is what an animated control surface needs;
* - advancing a frame changes no texture handle, so the pages
* built from those handles stay valid and a page switch is still
* an upload of nothing;
* - a frame whose slot is still with the video thread is skipped,
* not waited for;
* - closing the stream with a frame in flight is clean.
*
* The decode and the surface are the real ones; the driver is the
* stub below, as in the other overlay samples.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <formats/rpng.h>
#include <streams/file_stream.h>
#include "../../../gfx/gfx_surface.h"
static unsigned failures;
#define CHECK(cond, ...) do { \
if (!(cond)) { printf("[FAIL] "); printf(__VA_ARGS__); printf("\n"); failures++; } \
} while (0)
int main(int argc, char **argv)
{
const char *path = (argc > 1) ? argv[1] : "fixtures/anim.png";
int64_t len = 0;
void *buf = NULL;
rpng_apng_stream_t *st;
unsigned w = 0, h = 0;
int num_frames = 0, loops = 0, dur = 0, i;
uint32_t seen[8];
int nseen = 0;
if (!filestream_read_file(path, &buf, &len) || !buf || len <= 0)
{
printf("[FAIL] cannot read %s\n", path);
return 2;
}
CHECK(rpng_is_apng((const uint8_t*)buf, (size_t)len),
"fixture is not detected as an APNG");
if (!(st = rpng_apng_stream_open((const uint8_t*)buf, (size_t)len)))
{
printf("[FAIL] stream open failed\n");
free(buf);
return 2;
}
rpng_apng_stream_get_info(st, &w, &h, &num_frames, &loops);
CHECK(w && h, "no dimensions");
CHECK(num_frames > 1, "%d frames: not an animation", num_frames);
/* Distinct frames, then end of pass, then a rewind that plays
* again - the loop an overlay relies on. */
for (i = 0; i < num_frames; i++)
{
const uint32_t *f = rpng_apng_stream_next(st, &dur);
CHECK(f != NULL, "frame %d missing", i);
if (!f)
break;
CHECK(dur > 0, "frame %d has no duration", i);
if (nseen < (int)(sizeof(seen) / sizeof(seen[0])))
seen[nseen++] = f[0];
}
CHECK(rpng_apng_stream_next(st, &dur) == NULL,
"stream did not end its pass after %d frames", num_frames);
rpng_apng_stream_rewind(st);
CHECK(rpng_apng_stream_next(st, &dur) != NULL, "rewind did not replay");
for (i = 1; i < nseen; i++)
CHECK(seen[i] != seen[0] || nseen < 2,
"frame %d is identical to frame 0 (%08x)", i, seen[i]);
/* The surface side: frames go into alternating slots and the
* handle the pages hold never changes. */
{
gfx_surface_t *s = gfx_surface_new(w, h, 2, TEXTURE_FILTER_LINEAR,
NULL, NULL);
uintptr_t first = 0;
CHECK(s != NULL, "surface allocation failed");
if (s)
{
rpng_apng_stream_rewind(st);
for (i = 0; i < 6; i++)
{
const uint32_t *f = rpng_apng_stream_next(st, &dur);
unsigned slot = (unsigned)(i & 1);
enum gfx_surface_submit_result r;
if (!f)
{
rpng_apng_stream_rewind(st);
f = rpng_apng_stream_next(st, &dur);
}
CHECK(f != NULL, "no frame at step %d", i);
if (!f)
break;
memcpy(s->slots[slot], f,
(size_t)w * h * sizeof(uint32_t));
r = gfx_surface_submit(s, slot, false);
CHECK(r != GFX_SURFACE_SUBMIT_FAILED,
"submit %d failed", i);
if (!first)
first = s->handle;
else
CHECK(s->handle == first,
"step %d replaced the texture (%lx -> %lx)",
i, (unsigned long)first, (unsigned long)s->handle);
}
/* Freed with the last frame still owned by the surface. */
gfx_surface_free(s);
}
}
rpng_apng_stream_close(st);
free(buf);
if (!failures)
printf("[pass] overlay_apng: %d frames, looping, one texture\n",
num_frames);
printf(failures ? "FAIL\n" : "PASS\n");
return failures ? 1 : 0;
}
@@ -0,0 +1,55 @@
/* The frontend the surface layer calls into, reduced to what an
* upload needs: textures are handles from a counter, and the driver
* updates in place, which is the case the animated overlay path
* exists for. */
#include <stdint.h>
#include <stdlib.h>
#include <boolean.h>
#include "../../../gfx/video_driver.h"
#include "../../../gfx/gfx_surface.h"
static uintptr_t stub_next_handle = 0x100;
static unsigned stub_updates;
bool video_driver_texture_load(void *data, unsigned filter, uintptr_t *id)
{
(void)data; (void)filter;
*id = stub_next_handle++;
return true;
}
bool video_driver_texture_unload(uintptr_t *id)
{
*id = 0;
return true;
}
bool video_driver_texture_can_update(void) { return true; }
bool video_driver_texture_update(uintptr_t id, void *data)
{
(void)data;
stub_updates++;
return id != 0;
}
uint32_t video_driver_get_disp_flags(void) { return 0; }
bool video_driver_test_all_flags(enum display_flags testflag)
{
(void)testflag;
return false;
}
bool video_driver_supports_texture_format(enum texture_gpu_format fmt)
{
(void)fmt;
return false;
}
enum texture_filter_type gfx_display_texture_filter(void)
{
return TEXTURE_FILTER_LINEAR;
}
+57
View File
@@ -24,6 +24,9 @@
#include <file/archive_file.h>
#endif
#include <formats/image.h>
#ifdef HAVE_RPNG
#include <formats/rpng.h>
#endif
#include <streams/file_stream.h>
#include <string/stdstring.h>
#include <lrc_hash.h>
@@ -53,6 +56,7 @@ struct overlay_loader
struct overlay *overlays;
struct overlay *active;
struct string_list *image_list;
struct string_list *anim_list; /* APNG file bytes, parallel */
size_t resolve_pos;
unsigned size;
@@ -356,6 +360,34 @@ static bool task_overlay_load_image_texture(
attr.p = (void*)image;
string_list_append(loader->image_list, rel_path, attr);
#ifdef HAVE_RPNG
/* An animated PNG keeps its file bytes: the pack composes the
* frames from them one at a time, where a still is done with
* the file the moment it is decoded. The decoded image above is
* the animation's first frame, so nothing decodes twice. */
{
union string_list_elem_attr aattr;
overlay_anim_src_t *src = NULL;
int64_t len = 0;
void *buf = NULL;
aattr.i = 0;
if ( !path_get_archive_delim(full_path)
&& filestream_read_file(full_path, &buf, &len)
&& buf && len > 0
&& rpng_is_apng((const uint8_t*)buf, (size_t)len)
&& (src = (overlay_anim_src_t*)calloc(1, sizeof(*src))))
{
src->data = buf;
src->len = (size_t)len;
buf = NULL;
}
free(buf);
aattr.p = (void*)src;
string_list_append(loader->anim_list, rel_path, aattr);
}
#endif
}
else
*image = *((struct texture_image*)loader->image_list->elems[img_idx].attr.p);
@@ -1303,6 +1335,20 @@ static void task_overlay_free(retro_task_t *task)
string_list_free(loader->image_list);
}
if (loader->anim_list)
{
/* Same shape: the elements are file buffers nobody took. */
for (i = 0; i < loader->anim_list->size; i++)
{
overlay_anim_src_t *src =
(overlay_anim_src_t*)loader->anim_list->elems[i].attr.p;
if (src)
free(src->data);
free(src);
}
string_list_free(loader->anim_list);
}
if (loader->overlays)
{
for (i = 0; i < loader->size; i++)
@@ -1379,6 +1425,7 @@ static void task_overlay_handler(retro_task_t *task)
data->overlay_types = loader->overlay_types;
data->overlay_path = loader->overlay_path;
data->image_list = loader->image_list;
data->anim_list = loader->anim_list;
/* Ownership moves with the pointers, so drop the loader's
* references to them. task_overlay_free() below releases
@@ -1393,6 +1440,7 @@ static void task_overlay_handler(retro_task_t *task)
loader->active = NULL;
loader->overlay_path = NULL;
loader->image_list = NULL;
loader->anim_list = NULL;
task_set_data(task, data);
}
@@ -1449,6 +1497,15 @@ bool task_push_overlay_load_default(
return false;
}
loader->anim_list = string_list_new();
if (!loader->anim_list)
{
string_list_free(image_list);
free(loader);
return false;
}
#ifdef HAVE_COMPRESSION
if (path_get_archive_delim(overlay_path))
{