mirror of
https://github.com/libretro/RetroArch.git
synced 2026-11-08 17:28:49 -10:00
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:
1 parent
247519d69c
commit
dac760d996
8 files changed
+519
-8
No files matched your search
@@ -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
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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))
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user