android: integrate asynchronous native GPU recording

Connect the MediaCodec recording frontend to worker-owned encoders and
Vulkan capture without frame-path readbacks or new waits. Keep capture
leases through native retirement and defer cold queue work on contention.

Calibrate AAC presentation edits, omit an empty audio source, and drain
healthy recordings before video resets. Retain the teardown path for
failed or ambiguous GPU work. Include portable lifecycle, audio, file
and capture regressions.
This commit is contained in:
Codex committed 2026-10-27 13:02:24 +01:00
1 parent 1a7cd82f87
commit d44a83afda
81 files changed
+11714 -131

No files matched your search

+29 -1
View File
@@ -250,6 +250,7 @@ OBJ += frontend/frontend_driver.o \
record/drivers/record_wav.o \
record/drivers/record_nvenc.o \
record/drivers/record_metal.o \
record/drivers/record_mediacodec.o \
command.o \
msg_hash.o \
intl/msg_hash_us.o \
@@ -1825,12 +1826,16 @@ ifeq ($(HAVE_VULKAN), 1)
gfx/common/vulkan_common.o \
$(LIBRETRO_COMM_DIR)/vulkan/vulkan_symbol_wrapper.o
ifeq ($(ANDROID)$(HAVE_THREADS), 11)
OBJ += record/record_android_capture_device.o
endif
# H.264/HEVC previews decoded on the GPU
ifneq ($(findstring 1, $(HAVE_RMP4) $(HAVE_RWEBM)),)
OBJ += gfx/common/vulkan_video_decode.o
endif
ifneq ($(filter 1,$(HAVE_RMP4) $(HAVE_RVP9) $(HAVE_RAV1)),)
ifneq ($(filter 1,$(HAVE_RMP4) $(HAVE_RVP9) $(HAVE_RAV1) $(HAVE_RECORD_MEDIACODEC)),)
ifeq ($(ANDROID), 1)
OBJ += gfx/common/vulkan_android_device.o \
gfx/common/android_image_queue.o \
@@ -3307,3 +3312,26 @@ ifneq ($(findstring Win32,$(OS)),)
endif
endif
endif
ifeq ($(HAVE_RECORD_MEDIACODEC), 1)
ifneq ($(ANDROID)$(HAVE_VULKAN)$(HAVE_THREADS)$(HAVE_CONFIGFILE), 1111)
$(error MediaCodec recording requires Android, Vulkan, threads and config files)
endif
DEFINES += -DHAVE_RECORD_MEDIACODEC
MEDIA_RECORD_OBJ := record/record_mediacodec_encoder.o \
record/record_audio_tap.o \
record/record_mux_mediacodec.o record/record_mediacodec_worker.o \
record/record_aac_probe.o \
record/record_mp4_audio_edit.o record/record_mediacodec_file_edit.o \
record/record_mediacodec_surface.o record/record_mediacodec_file.o \
record/record_session_mediacodec.o record/record_dispatch_mediacodec.o \
record/record_gpu_dispatch_mediacodec.o record/record_mediacodec_gpu_worker.o \
record/record_android_capture_config.o record/record_android_capture_request.o \
record/record_android_capture_images.o record/record_android_capture_submit.o \
record/record_android_capture_pipe.o record/record_android_capture_pool.o \
record/record_android_capture_ops.o record/record_android_capture_egl.o \
record/record_android_capture_gl.o record/record_android_capture_draw.o \
record/record_gpu_queue.o record/record_audio_queue.o
MEDIA_RECORD_OBJ := $(filter-out $(OBJ),$(MEDIA_RECORD_OBJ))
OBJ += $(MEDIA_RECORD_OBJ)
endif
+76
View File
@@ -97,6 +97,9 @@ static bool audio_driver_pipeline_transport_publish(uint32_t tempo_q16,
#include "../list_special.h"
#include "../file_path_special.h"
#include "../record/record_driver.h"
#if defined(HAVE_RECORD_MEDIACODEC) || defined(RECORD_AUDIO_TAP_TEST)
#include "../record/record_audio_tap.h"
#endif
#include "../tasks/task_content.h"
#include "../runloop.h"
#include "../midi_driver.h"
@@ -1268,6 +1271,13 @@ static bool audio_driver_deinit_internal(bool audio_enable)
audio_st->pipe_pending = NULL;
audio_st->pipe_pending_bytes = 0;
audio_driver_inline_free(audio_st);
#if defined(HAVE_RECORD_MEDIACODEC) || defined(RECORD_AUDIO_TAP_TEST)
if (audio_st->record_tap)
{
record_audio_tap_retire(audio_st->record_tap, true);
audio_st->record_tap = NULL;
}
#endif
audio_st->transport_lpf_only = false;
/* The pause tail continues the waveform, and across a core swap or a
@@ -3141,6 +3151,11 @@ static void audio_driver_pause_track(audio_driver_state_t *audio_st,
audio_st->pause_mute_frames -= (unsigned)n;
}
#if defined(HAVE_RECORD_MEDIACODEC) || defined(RECORD_AUDIO_TAP_TEST)
if (audio_st->record_tap)
record_audio_tap_push(audio_st->record_tap, buf, num_frames,
is_float, audio_st->out_rate);
#endif
audio_st->last_out[0] = audio_driver_pause_read(buf, is_float,
((num_frames - 1) * 2) + 0);
audio_st->last_out[1] = audio_driver_pause_read(buf, is_float,
@@ -3288,6 +3303,9 @@ static void audio_driver_flush(audio_driver_state_t *audio_st,
* fall through to the float resampler path below (which is exactly
* where the redundant int16<->float round-trip is avoided). */
if (audio->write_raw
#if defined(HAVE_RECORD_MEDIACODEC) || defined(RECORD_AUDIO_TAP_TEST)
&& !audio_st->record_tap
#endif
&& !audio_st->resampler_hq
&& !is_float
/* The resume ramp and the pause mute are applied on the last
@@ -5344,6 +5362,64 @@ void audio_driver_state_unlock(void)
#endif
}
int audio_driver_record_tap_install(struct record_audio_tap *tap)
{
#if defined(HAVE_RECORD_MEDIACODEC) || defined(RECORD_AUDIO_TAP_TEST)
audio_driver_state_t *s = &audio_driver_st;
int result = -1;
if (!tap)
return -1;
#ifdef HAVE_THREADS
if (s->state_lock && !slock_try_lock(s->state_lock))
return 0;
#endif
if (s->record_tap == tap)
result = 1;
else if (!s->record_tap && (AUDIO_FLAGS_GET(s) & AUDIO_FLAG_ACTIVE)
&& s->out_rate == record_audio_tap_rate(tap)
&& record_audio_tap_begin(tap))
{
s->record_tap = tap;
result = 1;
}
#ifdef HAVE_THREADS
if (s->state_lock)
slock_unlock(s->state_lock);
#endif
return result;
#else
(void)tap;
return -1;
#endif
}
bool audio_driver_record_tap_remove(struct record_audio_tap *tap)
{
#if defined(HAVE_RECORD_MEDIACODEC) || defined(RECORD_AUDIO_TAP_TEST)
audio_driver_state_t *s = &audio_driver_st;
bool ready;
if (!tap)
return true;
#ifdef HAVE_THREADS
if (s->state_lock && !slock_try_lock(s->state_lock))
return false;
#endif
if (s->record_tap == tap)
{
s->record_tap = NULL;
record_audio_tap_retire(tap, false);
}
ready = record_audio_tap_end(tap, NULL);
#ifdef HAVE_THREADS
if (s->state_lock)
slock_unlock(s->state_lock);
#endif
return ready;
#else
return tap == NULL;
#endif
}
/* Converts to the device's format if it needs it, and writes. The pause
* tail reaches the device outside audio_driver_flush()'s own conversion. */
static void audio_driver_write_float(audio_driver_state_t *audio_st,
+6
View File
@@ -484,6 +484,9 @@ typedef struct
uint64_t free_samples_count;
struct audio_record_source *record_source;
#if defined(HAVE_RECORD_MEDIACODEC) || defined(RECORD_AUDIO_TAP_TEST)
struct record_audio_tap *record_tap;
#endif
struct string_list *devices_list;
/* The driver whose device_list_new built devices_list, so that its
* device_list_free is the one that releases it - not whichever
@@ -1248,6 +1251,9 @@ void audio_driver_pipeline_wake(void);
**/
void audio_driver_state_lock(void);
void audio_driver_state_unlock(void);
struct record_audio_tap;
int audio_driver_record_tap_install(struct record_audio_tap *tap);
bool audio_driver_record_tap_remove(struct record_audio_tap *tap);
/* Bits of audio_driver_state_t::runloop_snapshot. */
enum audio_runloop_snapshot_bits
+56 -15
View File
@@ -26,8 +26,12 @@
#endif
#include "vulkan_common.h"
#if defined(ANDROID) && defined(HAVE_THREADS)
#include "../../record/record_android_capture_device.h"
#endif
#if defined(ANDROID) \
&& (defined(HAVE_RMP4) || defined(HAVE_RVP9) || defined(HAVE_RAV1))
&& (defined(HAVE_RMP4) || defined(HAVE_RVP9) || defined(HAVE_RAV1) \
|| defined(HAVE_RECORD_MEDIACODEC))
#include "vulkan_android_device.h"
#include "vulkan_android_cache.h"
#endif
@@ -109,7 +113,8 @@ static bool cached_timeline_vk;
/* The video decode queues a cached device was made with */
static uint32_t cached_video_vk[6];
#if defined(ANDROID) \
&& (defined(HAVE_RMP4) || defined(HAVE_RVP9) || defined(HAVE_RAV1))
&& (defined(HAVE_RMP4) || defined(HAVE_RVP9) || defined(HAVE_RAV1) \
|| defined(HAVE_RECORD_MEDIACODEC))
static bool cached_android_images_vk, cached_android_legacy_vk;
static unsigned cached_android_descriptors_vk;
#endif
@@ -926,7 +931,8 @@ static const char *vulkan_optional_device_extensions[] = {
, "VK_EXT_hdr_metadata"
#endif
#if defined(ANDROID) \
&& (defined(HAVE_RMP4) || defined(HAVE_RVP9) || defined(HAVE_RAV1))
&& (defined(HAVE_RMP4) || defined(HAVE_RVP9) || defined(HAVE_RAV1) \
|| defined(HAVE_RECORD_MEDIACODEC))
, "VK_ANDROID_external_memory_android_hardware_buffer"
, "VK_EXT_queue_family_foreign"
, "VK_KHR_external_semaphore_fd"
@@ -1130,7 +1136,8 @@ static unsigned vulkan_optional_device_count(
{
unsigned count = ARRAY_SIZE(vulkan_optional_device_extensions);
#if defined(ANDROID) \
&& (defined(HAVE_RMP4) || defined(HAVE_RVP9) || defined(HAVE_RAV1))
&& (defined(HAVE_RMP4) || defined(HAVE_RVP9) || defined(HAVE_RAV1) \
|| defined(HAVE_RECORD_MEDIACODEC))
if (!own_device)
while (count && vulkan_android_device_extension(
vulkan_optional_device_extensions[count - 1]))
@@ -1517,7 +1524,8 @@ static bool vulkan_context_init_device(gfx_ctx_vulkan_data_t *vk)
video_driver_state_t *video_st = video_state_get_ptr();
#if defined(ANDROID) \
&& (defined(HAVE_RMP4) || defined(HAVE_RVP9) || defined(HAVE_RAV1))
&& (defined(HAVE_RMP4) || defined(HAVE_RVP9) || defined(HAVE_RAV1) \
|| defined(HAVE_RECORD_MEDIACODEC))
vulkan_android_device_plan_t android_plan;
#endif
VkPhysicalDeviceFeatures features = { false };
@@ -1531,7 +1539,8 @@ static bool vulkan_context_init_device(gfx_ctx_vulkan_data_t *vk)
video_st->hw_render_context_negotiation;
#if defined(ANDROID) \
&& (defined(HAVE_RMP4) || defined(HAVE_RVP9) || defined(HAVE_RAV1))
&& (defined(HAVE_RMP4) || defined(HAVE_RVP9) || defined(HAVE_RAV1) \
|| defined(HAVE_RECORD_MEDIACODEC))
memset(&android_plan, 0, sizeof(android_plan));
vk->context.flags &= ~VK_CTX_FLAG_ANDROID_IMAGES;
vk->context.android_sampler_descriptors = 0;
@@ -1888,7 +1897,8 @@ static bool vulkan_context_init_device(gfx_ctx_vulkan_data_t *vk)
vk->context.flags |= VK_CTX_FLAG_TIMELINE_SEMAPHORE;
}
#if defined(ANDROID) \
&& (defined(HAVE_RMP4) || defined(HAVE_RVP9) || defined(HAVE_RAV1))
&& (defined(HAVE_RMP4) || defined(HAVE_RVP9) || defined(HAVE_RAV1) \
|| defined(HAVE_RECORD_MEDIACODEC))
{
PFN_vkGetInstanceProcAddr proc = vulkan_symbol_wrapper_instance_proc_addr();
uint32_t version = VK_API_VERSION_1_0;
@@ -1935,7 +1945,8 @@ static bool vulkan_context_init_device(gfx_ctx_vulkan_data_t *vk)
if (cached_video_vk[5]) vk->context.flags |= VK_CTX_FLAG_VIDEO_GENERAL;
else vk->context.flags &= ~VK_CTX_FLAG_VIDEO_GENERAL;
#if defined(ANDROID) \
&& (defined(HAVE_RMP4) || defined(HAVE_RVP9) || defined(HAVE_RAV1))
&& (defined(HAVE_RMP4) || defined(HAVE_RVP9) || defined(HAVE_RAV1) \
|| defined(HAVE_RECORD_MEDIACODEC))
if (cached_android_images_vk)
vk->context.flags |= VK_CTX_FLAG_ANDROID_IMAGES;
vk->context.android_sampler_descriptors = cached_android_descriptors_vk;
@@ -2024,7 +2035,8 @@ static bool vulkan_context_init_device(gfx_ctx_vulkan_data_t *vk)
{
dev = VK_NULL_HANDLE;
#if defined(ANDROID) \
&& (defined(HAVE_RMP4) || defined(HAVE_RVP9) || defined(HAVE_RAV1))
&& (defined(HAVE_RMP4) || defined(HAVE_RVP9) || defined(HAVE_RAV1) \
|| defined(HAVE_RECORD_MEDIACODEC))
if (vulkan_android_device_plan_disable(&android_plan, &device_info.pNext,
enabled_device_extensions, &enabled_device_extension_count))
{
@@ -2042,7 +2054,8 @@ static bool vulkan_context_init_device(gfx_ctx_vulkan_data_t *vk)
}
vk->context.device = dev;
#if defined(ANDROID) \
&& (defined(HAVE_RMP4) || defined(HAVE_RVP9) || defined(HAVE_RAV1))
&& (defined(HAVE_RMP4) || defined(HAVE_RVP9) || defined(HAVE_RAV1) \
|| defined(HAVE_RECORD_MEDIACODEC))
if (android_plan.supported)
{
vk->context.flags |= VK_CTX_FLAG_ANDROID_IMAGES;
@@ -5491,6 +5504,9 @@ void vulkan_context_destroy(gfx_ctx_vulkan_data_t *vk,
{
video_driver_state_t *video_st = video_state_get_ptr();
uint32_t video_st_flags = 0;
#if defined(ANDROID) && defined(HAVE_THREADS)
VkResult recording_idle = VK_NOT_READY;
#endif
#ifdef HAVE_OPENXR
/* Its thread uses the queue, which is drained below. */
vulkan_openxr_stop(vk->context.xr);
@@ -5512,8 +5528,22 @@ void vulkan_context_destroy(gfx_ctx_vulkan_data_t *vk,
vk->context.present_pending = true;
vulkan_context_wait_frames(vk);
#endif
vkDeviceWaitIdle(vk->context.device);
#if defined(ANDROID) && defined(HAVE_THREADS)
recording_idle =
#endif
vkDeviceWaitIdle(vk->context.device);
}
#if defined(ANDROID) && defined(HAVE_THREADS)
/* Retire capture children before their device, including failed drains. */
if (vk->context.device
&& !record_android_capture_device_prepare(vk->context.device,
recording_idle == VK_SUCCESS
|| recording_idle == VK_ERROR_DEVICE_LOST))
{
RARCH_ERR("[Vulkan] Recording children still owned; retaining device.\n");
return;
}
#endif
/* Drained above; the fence would only be waited on again. */
vk->context.present_pending = false;
@@ -5542,9 +5572,14 @@ void vulkan_context_destroy(gfx_ctx_vulkan_data_t *vk,
video_st_flags = (uint32_t)retro_atomic_load_relaxed_int(&video_st->flags);
#if defined(ANDROID) \
&& (defined(HAVE_RMP4) || defined(HAVE_RVP9) || defined(HAVE_RAV1))
&& (defined(HAVE_RMP4) || defined(HAVE_RVP9) || defined(HAVE_RAV1) \
|| defined(HAVE_RECORD_MEDIACODEC))
if (vulkan_android_cache_device_pending(vk->context.device))
video_st_flags &= ~VIDEO_FLAG_CACHE_CONTEXT;
#endif
#if defined(ANDROID) && defined(HAVE_THREADS)
if (record_android_capture_device_pending(vk->context.device))
video_st_flags &= ~VIDEO_FLAG_CACHE_CONTEXT;
#endif
if (video_st_flags & VIDEO_FLAG_CACHE_CONTEXT)
{
@@ -5564,7 +5599,8 @@ void vulkan_context_destroy(gfx_ctx_vulkan_data_t *vk,
cached_video_vk[4] = vk->context.video_codec_operations;
cached_video_vk[5] = vk->context.flags & VK_CTX_FLAG_VIDEO_GENERAL;
#if defined(ANDROID) \
&& (defined(HAVE_RMP4) || defined(HAVE_RVP9) || defined(HAVE_RAV1))
&& (defined(HAVE_RMP4) || defined(HAVE_RVP9) || defined(HAVE_RAV1) \
|| defined(HAVE_RECORD_MEDIACODEC))
cached_android_images_vk = !!(vk->context.flags & VK_CTX_FLAG_ANDROID_IMAGES);
cached_android_descriptors_vk = vk->context.android_sampler_descriptors;
cached_android_legacy_vk = vk->context.android_legacy_descriptor_budget;
@@ -5594,12 +5630,17 @@ void vulkan_context_destroy(gfx_ctx_vulkan_data_t *vk,
vk->context.destroy_device();
vk->context.destroy_device = NULL;
#if defined(ANDROID) \
&& (defined(HAVE_RMP4) || defined(HAVE_RVP9) || defined(HAVE_RAV1))
&& (defined(HAVE_RMP4) || defined(HAVE_RVP9) || defined(HAVE_RAV1) \
|| defined(HAVE_RECORD_MEDIACODEC))
vulkan_android_cache_device_destroying(vk->context.device);
#endif
vkDestroyDevice(vk->context.device, NULL);
#if defined(ANDROID) && defined(HAVE_THREADS)
record_android_capture_device_destroyed(vk->context.device);
#endif
#if defined(ANDROID) \
&& (defined(HAVE_RMP4) || defined(HAVE_RVP9) || defined(HAVE_RAV1))
&& (defined(HAVE_RMP4) || defined(HAVE_RVP9) || defined(HAVE_RAV1) \
|| defined(HAVE_RECORD_MEDIACODEC))
vulkan_android_cache_device_destroyed(vk->context.device);
#endif
vk->context.device = NULL;
+2 -1
View File
@@ -273,7 +273,8 @@ typedef struct vulkan_context
VkPhysicalDeviceProperties gpu_properties;
VkPhysicalDeviceMemoryProperties memory_properties;
#if defined(ANDROID) \
&& (defined(HAVE_RMP4) || defined(HAVE_RVP9) || defined(HAVE_RAV1))
&& (defined(HAVE_RMP4) || defined(HAVE_RVP9) || defined(HAVE_RAV1) \
|| defined(HAVE_RECORD_MEDIACODEC))
unsigned android_sampler_descriptors;
bool android_legacy_descriptor_budget;
#endif
+155 -31
View File
@@ -68,7 +68,8 @@
#include "../common/vulkan_video_decode.h"
#endif
#if defined(ANDROID) \
&& (defined(HAVE_RMP4) || defined(HAVE_RVP9) || defined(HAVE_RAV1))
&& (defined(HAVE_RMP4) || defined(HAVE_RVP9) || defined(HAVE_RAV1) \
|| defined(HAVE_RECORD_MEDIACODEC))
#include "../common/vulkan_android_cache.h"
#include "../common/vulkan_android_queue.h"
#endif
@@ -83,6 +84,9 @@
#endif
#include "../../record/record_driver.h"
#if defined(ANDROID) && defined(HAVE_RECORD_MEDIACODEC)
#include "../../record/record_android_capture_renderer.h"
#endif
#include "../../retroarch.h"
#include "../../verbosity.h"
@@ -218,7 +222,8 @@ struct vk_texture
VkBuffer buffer; /* ptr alignment */
VkDeviceMemory memory; /* ptr alignment */
#if defined(ANDROID) \
&& (defined(HAVE_RMP4) || defined(HAVE_RVP9) || defined(HAVE_RAV1))
&& (defined(HAVE_RMP4) || defined(HAVE_RVP9) || defined(HAVE_RAV1) \
|| defined(HAVE_RECORD_MEDIACODEC))
vulkan_android_cache_t *android_cache;
struct vk_texture *android_next;
#endif
@@ -772,7 +777,8 @@ typedef struct vk
* only, like the two lists above. */
struct vk_stream_state *stream_states;
#if defined(ANDROID) \
&& (defined(HAVE_RMP4) || defined(HAVE_RVP9) || defined(HAVE_RAV1))
&& (defined(HAVE_RMP4) || defined(HAVE_RVP9) || defined(HAVE_RAV1) \
|| defined(HAVE_RECORD_MEDIACODEC))
vulkan_android_queue_t android_submissions;
struct vk_texture *android_textures;
unsigned android_device_support;
@@ -804,7 +810,8 @@ typedef struct vk
} vk_t;
#if defined(ANDROID) && defined(HAVE_THREADS) \
&& (defined(HAVE_RMP4) || defined(HAVE_RVP9) || defined(HAVE_RAV1))
&& (defined(HAVE_RMP4) || defined(HAVE_RVP9) || defined(HAVE_RAV1) \
|| defined(HAVE_RECORD_MEDIACODEC))
#include "vulkan_android_backend.h"
#endif
@@ -1931,7 +1938,8 @@ static void vulkan_texture_retire(vk_t *vk, struct vk_texture *texture)
{
struct vk_stream_state **cur = &vk->stream_states;
#if defined(ANDROID) \
&& (defined(HAVE_RMP4) || defined(HAVE_RVP9) || defined(HAVE_RAV1))
&& (defined(HAVE_RMP4) || defined(HAVE_RVP9) || defined(HAVE_RAV1) \
|| defined(HAVE_RECORD_MEDIACODEC))
if (texture->android_cache)
{
struct vk_texture **link = &vk->android_textures;
@@ -4309,7 +4317,7 @@ static void vulkan_font_upload_atlas(vk_t *vk, vulkan_raster_t *font)
/* Compute tight flush range from dirty rectangle
* instead of flushing the entire allocation.
* Aligns offset down and size up to nonCoherentAtomSize
* as required by the spec (§12.1). */
* as required by the spec (§12.1). */
unsigned y1 = VIDEO_SCALE_H(font->dirty_xy1);
VkDeviceSize flush_offset = (VkDeviceSize)
VIDEO_SCALE_H(font->dirty_xy0) * staging_tex->stride
@@ -4677,7 +4685,7 @@ static void vulkan_font_render_msg(
#include "../font_layout.h"
}
/* ── Flush: atlas upload + draw ─────────────────────────────────
/* ── Flush: atlas upload + draw ─────────────────────────────────
* Inlined from the former vulkan_font_flush(). By issuing the
* Vulkan commands directly we eliminate:
* - packing/unpacking through struct vk_draw_triangles
@@ -4799,7 +4807,7 @@ static void vulkan_font_draw_range(vk_t *vk, vulkan_raster_t *font)
/* Bind VBO and issue indexed draw.
* Font glyphs are always quads (4 verts each) drawn via the
* shared index buffer — no need for the generic branch. */
* shared index buffer — no need for the generic branch. */
vkCmdBindVertexBuffers(vk->cmd, 0, 1,
&font->range.buffer, &font->range.offset);
@@ -5670,7 +5678,7 @@ static void vulkan_init_pipelines(vk_t *vk)
* VK_FORMAT_B8G8R8A8_UNORM. On KMS and some other
* platforms, the swapchain may use VK_FORMAT_R8G8B8A8_UNORM
* instead, making the pipeline incompatible with the active
* render pass at draw time — a Vulkan validation error that
* render pass at draw time — a Vulkan validation error that
* many drivers (especially RADV on AMD) turn into a
* segfault.
*
@@ -5679,7 +5687,7 @@ static void vulkan_init_pipelines(vk_t *vk)
* pipelines may not be format-compatible if the swapchain
* format differs from B8G8R8A8. However, the HDR path
* is only active when HDR is supported AND the display uses
* a wide-gamut format — in which case a separate set of
* a wide-gamut format — in which case a separate set of
* HDR display pipelines (indices 4-5) is built against the
* correct render pass at lines 3444-3453. The menu shader
* pipelines (ribbon, snow, bokeh) used through
@@ -8160,7 +8168,8 @@ static void *vulkan_init(const video_info_t *video)
vulkan_init_frame_resources(vk, 0);
#if defined(ANDROID) \
&& (defined(HAVE_RMP4) || defined(HAVE_RVP9) || defined(HAVE_RAV1))
&& (defined(HAVE_RMP4) || defined(HAVE_RVP9) || defined(HAVE_RAV1) \
|| defined(HAVE_RECORD_MEDIACODEC))
vk->android_device_support = vk->context->flags
& (VK_CTX_FLAG_ANDROID_IMAGES | VK_CTX_FLAG_TIMELINE_SEMAPHORE);
#if !defined(HAVE_THREADS) || !defined(RETRO_ATOMIC_LOCK_FREE) || !defined(RETRO_ATOMIC_HAS_PTR)
@@ -8336,7 +8345,7 @@ built:
/* When rendering through the SDR offscreen buffer, the filter chain's
* final pass must use the SDR render pass to match the framebuffer format.
* The default filter chain has spirv_hdr_frag built in and renders directly to
* the swapchain — only custom shader chains need the SDR render pass.
* the swapchain — only custom shader chains need the SDR render pass.
* Passthrough (direct to swapchain) when:
* scRGB mode: shader emits RGBA16F (scRGB)
* HDR10 mode: shader emits A2B10G10R10 (HDR10 PQ) or RGBA16F */
@@ -8690,7 +8699,7 @@ static bool vulkan_shader_load_step(void *data,
goto cleanup;
}
/* All passes compiled — finalize */
/* All passes compiled — finalize */
RARCH_LOG("[Vulkan] Deferred: finalizing chain...\n");
if (vulkan_filter_chain_finalize(ds->new_chain))
@@ -9678,7 +9687,7 @@ static void vulkan_draw_quad(vk_t *vk, const struct vk_draw_quad *quad)
}
}
/* Upload VBO — 4 unique vertices per quad, indexed draw. */
/* Upload VBO — 4 unique vertices per quad, indexed draw. */
{
struct vk_buffer_range range;
if (!vulkan_buffer_chain_alloc(vk->context, &vk->chain->vbo,
@@ -9989,7 +9998,7 @@ static void vulkan_run_hdr_pipeline(VkPipeline pipeline, VkRenderPass render_pas
vkCmdSetScissor(vk->cmd, 0, 1, &sci);
}
/* Upload VBO — 4 unique vertices, indexed draw. */
/* Upload VBO — 4 unique vertices, indexed draw. */
{
struct vk_buffer_range range;
@@ -11795,6 +11804,70 @@ static void vulkan_menu_upload(vk_t *vk, unsigned frame_index)
}
#endif
#if defined(ANDROID) && defined(HAVE_RECORD_MEDIACODEC)
static record_android_capture_renderer_t *vulkan_record_gpu(vk_t *vk,
const video_frame_info_t *info, const struct vk_image *backbuffer)
{
record_android_capture_source_t source;
record_android_capture_renderer_t *recording
= (record_android_capture_renderer_t *)info->recording_gpu;
unsigned mode = 0;
bool menu = recording && (recording->flags & RECORD_ANDROID_CAPTURE_MENU);
if (!recording || !recording->context || !recording->record
|| !recording->release || !recording->submitted
|| !info->recording_gpu_capture
|| (info->frame_repeat && !(recording->flags & RECORD_ANDROID_CAPTURE_MENU))
|| !backbuffer->image || !vk->cmd
|| !(vk->context->flags & VK_CTX_FLAG_HAS_ACQUIRED_SWAPCHAIN)
|| !(vk->android_device_support & VK_CTX_FLAG_ANDROID_IMAGES)
|| (vk->context->flags & VK_CTX_FLAG_SWAP_INTERVAL_EMULATION_LOCK)
|| (vk->flags & VK_FLAG_OPEN_XR) || (!menu && (VIDEO_POS_X(vk->vp.pos) < 0
|| VIDEO_POS_Y(vk->vp.pos) < 0))
|| !VIDEO_SCALE_W(menu ? vk->vp.full_dims : vk->vp.dims)
|| !VIDEO_SCALE_H(menu ? vk->vp.full_dims : vk->vp.dims))
return NULL;
#ifdef VULKAN_HDR_SWAPCHAIN
if (vk->context->flags & VK_CTX_FLAG_HDR_ENABLE)
mode = vk->context->flags & VK_CTX_FLAG_HDR_SCRGB ? 2 : 1;
#endif
memset(&source, 0, sizeof(source));
source.image = backbuffer->image;
source.owner = recording->context;
source.release = recording->release;
source.format = vk->context->swapchain_format;
source.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
source.width = VIDEO_SCALE_W(vk->vp.full_dims);
source.height = VIDEO_SCALE_H(vk->vp.full_dims);
source.x = menu ? 0 : (unsigned)VIDEO_POS_X(vk->vp.pos);
source.y = menu ? 0 : (unsigned)VIDEO_POS_Y(vk->vp.pos);
return recording->record(recording->context, &source, vk->cmd,
info->recording_gpu_timestamp, menu ? vk->vp.full_dims : vk->vp.dims, mode)
== 1
? recording
: NULL;
}
static void vulkan_record_service(const video_frame_info_t *info)
{
record_android_capture_renderer_t *recording
= (record_android_capture_renderer_t *)info->recording_gpu;
if (recording && recording->context && recording->service)
recording->service(recording->context);
}
static VkResult vulkan_record_submit(vk_t *vk, const VkSubmitInfo *submit,
unsigned frame_index, record_android_capture_renderer_t *recording)
{
VkResult result = vkQueueSubmit(vk->context->queue, 1, submit,
vk->context->swapchain_fences[frame_index]);
vk->context->swapchain_fences_signalled[frame_index] = result == VK_SUCCESS;
if (recording)
recording->submitted(recording->context, result);
vulkan_check_device_lost(vk, result);
return result;
}
#endif
static bool vulkan_frame(void *data, const void *frame,
unsigned dims,
uint64_t frame_count,
@@ -11808,6 +11881,15 @@ static bool vulkan_frame(void *data, const void *frame,
VkRenderPassBeginInfo rp_info;
VkCommandBufferBeginInfo begin_info;
VkSemaphore signal_semaphores[2 + RETRO_VIDEO_VIEWS_MAX];
#if defined(ANDROID) && defined(HAVE_RECORD_MEDIACODEC)
record_android_capture_renderer_t *recording = NULL;
VkResult recording_result;
#endif
#if defined(ANDROID) \
&& (defined(HAVE_RMP4) || defined(HAVE_RVP9) || defined(HAVE_RAV1) \
|| defined(HAVE_RECORD_MEDIACODEC))
VkResult android_queue_result;
#endif
retro_perf_tick_t c_lock = 0;
vk_t *vk = (vk_t*)data;
vulkan_filter_chain_t *filter_chain = NULL;
@@ -11912,7 +11994,8 @@ static bool vulkan_frame(void *data, const void *frame,
#endif
#if defined(ANDROID) \
&& (defined(HAVE_RMP4) || defined(HAVE_RVP9) || defined(HAVE_RAV1))
&& (defined(HAVE_RMP4) || defined(HAVE_RVP9) || defined(HAVE_RAV1) \
|| defined(HAVE_RECORD_MEDIACODEC))
{
struct vk_texture *texture;
for (texture = vk->android_textures; texture; texture = texture->android_next)
@@ -11994,7 +12077,7 @@ static bool vulkan_frame(void *data, const void *frame,
/* Use the offscreen buffer when the shader's output format doesn't match
* the swapchain format and the HDR pipeline needs to convert.
* The default filter chain already contains the internal HDR shader
* (spirv_hdr_frag) so it renders directly to the swapchain — no offscreen needed.
* (spirv_hdr_frag) so it renders directly to the swapchain — no offscreen needed.
* Custom shader chains need offscreen unless they emit HDR natively. */
if ((vk->context->flags & VK_CTX_FLAG_HDR_ENABLE)
&& filter_chain != vk->filter_chain_default)
@@ -12198,13 +12281,13 @@ static bool vulkan_frame(void *data, const void *frame,
GFX_INSTR_INC(GFX_INSTR_FRAME_COPY_HOST);
dst = (uint8_t*)chain->texture.mapped;
if (chain->texture.stride == pitch)
/* Stride matches pitch — single contiguous copy regardless
/* Stride matches pitch — single contiguous copy regardless
* of whether pitch == frame_width * bpp (there may be
* trailing padding per row, but the layout is identical). */
memcpy(dst, src, (size_t)chain->texture.stride * frame_height);
else
{
/* Stride and pitch differ — copy each row.
/* Stride and pitch differ — copy each row.
* Use the tight pixel width to avoid copying garbage. */
unsigned row_bytes = frame_width * bpp;
for (y = 0; y < frame_height; y++,
@@ -13211,6 +13294,10 @@ static bool vulkan_frame(void *data, const void *frame,
else if (!views_direct_tex)
vulkan_views_end_frame(vk, video_info, view_chains);
#if defined(ANDROID) && defined(HAVE_RECORD_MEDIACODEC)
recording = vulkan_record_gpu(vk, video_info, backbuffer);
#endif
if (
(backbuffer->image != VK_NULL_HANDLE)
&& (vk->context->flags & VK_CTX_FLAG_HAS_ACQUIRED_SWAPCHAIN)
@@ -13481,21 +13568,38 @@ static bool vulkan_frame(void *data, const void *frame,
c_lock = cpu_features_get_perf_counter() - c_lock;
#endif
#if defined(ANDROID) \
&& (defined(HAVE_RMP4) || defined(HAVE_RVP9) || defined(HAVE_RAV1))
if (vulkan_android_queue_flush(&vk->android_submissions, vkQueueSubmit)
!= VK_SUCCESS)
&& (defined(HAVE_RMP4) || defined(HAVE_RVP9) || defined(HAVE_RAV1) \
|| defined(HAVE_RECORD_MEDIACODEC))
android_queue_result = vulkan_android_queue_flush(
&vk->android_submissions, vkQueueSubmit);
if (android_queue_result != VK_SUCCESS)
{
#ifdef HAVE_THREADS
slock_unlock(vk->context->queue_lock);
#endif
#if defined(HAVE_RECORD_MEDIACODEC) && defined(HAVE_THREADS)
if (recording)
recording->submitted(recording->context, android_queue_result);
#endif
vk->context->swapchain_fences_signalled[frame_index] = false;
video_driver_modify_disp_flags(VIDEO_FLAG_GPU_DEVICE_LOST, 0);
return false;
}
#endif
#if defined(ANDROID) && defined(HAVE_RECORD_MEDIACODEC)
vulkan_record_service(video_info);
recording_result
= vulkan_record_submit(vk, &submit_info, frame_index, recording);
if (recording_result != VK_SUCCESS)
{
slock_unlock(vk->context->queue_lock);
return false;
}
#else
vulkan_check_device_lost(vk, vkQueueSubmit(vk->context->queue, 1,
&submit_info, vk->context->swapchain_fences[frame_index]));
vk->context->swapchain_fences_signalled[frame_index] = true;
#endif
#ifdef HAVE_TEMPORAL_UPSCALER
if (vk->ngx)
vulkan_upscaler_frame_done(vk->ngx);
@@ -14830,7 +14934,8 @@ static struct vk_stream_state *vulkan_import_state_get(vk_t *vk,
}
#if defined(ANDROID) \
&& (defined(HAVE_RMP4) || defined(HAVE_RVP9) || defined(HAVE_RAV1))
&& (defined(HAVE_RMP4) || defined(HAVE_RVP9) || defined(HAVE_RAV1) \
|| defined(HAVE_RECORD_MEDIACODEC))
#include "vulkan_android_video.h"
#endif
@@ -14889,7 +14994,8 @@ static enum video_texture_update vulkan_import_convert(vk_t *vk,
}
#endif
#if defined(ANDROID) \
&& (defined(HAVE_RMP4) || defined(HAVE_RVP9) || defined(HAVE_RAV1))
&& (defined(HAVE_RMP4) || defined(HAVE_RVP9) || defined(HAVE_RAV1) \
|| defined(HAVE_RECORD_MEDIACODEC))
if (android_frame_callback(f->abandon))
return vulkan_android_renderer_import(vk, texture, f);
#endif
@@ -15128,7 +15234,8 @@ static enum video_texture_update vulkan_import_texture(void *data,
VkFormat output_format;
bool opaque_input = false;
#if defined(ANDROID) \
&& (defined(HAVE_RMP4) || defined(HAVE_RVP9) || defined(HAVE_RAV1))
&& (defined(HAVE_RMP4) || defined(HAVE_RVP9) || defined(HAVE_RAV1) \
|| defined(HAVE_RECORD_MEDIACODEC))
opaque_input = f && android_frame_callback(f->abandon);
#endif
@@ -15176,7 +15283,8 @@ static enum video_texture_update vulkan_import_texture(void *data,
|| filter == TEXTURE_FILTER_MIPMAP_LINEAR)
texture->flags |= VK_TEX_FLAG_DEFAULT_SMOOTH;
#if defined(ANDROID) \
&& (defined(HAVE_RMP4) || defined(HAVE_RVP9) || defined(HAVE_RAV1))
&& (defined(HAVE_RMP4) || defined(HAVE_RVP9) || defined(HAVE_RAV1) \
|| defined(HAVE_RECORD_MEDIACODEC))
if (opaque_input && vulkan_android_renderer_prepare(vk, texture, f)
!= VIDEO_TEXTURE_UPDATE_DONE)
{
@@ -15196,7 +15304,8 @@ static enum video_texture_update vulkan_import_texture(void *data,
}
#if defined(ANDROID) \
&& (defined(HAVE_RMP4) || defined(HAVE_RVP9) || defined(HAVE_RAV1))
&& (defined(HAVE_RMP4) || defined(HAVE_RVP9) || defined(HAVE_RAV1) \
|| defined(HAVE_RECORD_MEDIACODEC))
static enum video_texture_update vulkan_prepare_import_texture(void *data,
uintptr_t handle, const video_frame_import_t *frame)
{
@@ -15235,7 +15344,8 @@ static enum video_texture_update vulkan_import_movie_frame(void *data,
&& ((struct vk_texture*)vk->movie_texture)->dims != f->dims)
vulkan_import_movie_frame(vk, NULL);
#if defined(ANDROID) \
&& (defined(HAVE_RMP4) || defined(HAVE_RVP9) || defined(HAVE_RAV1))
&& (defined(HAVE_RMP4) || defined(HAVE_RVP9) || defined(HAVE_RAV1) \
|| defined(HAVE_RECORD_MEDIACODEC))
if (vk->movie_texture)
{
result = vulkan_prepare_import_texture(vk, vk->movie_texture, f);
@@ -15262,8 +15372,21 @@ static bool vulkan_get_device(void *data, video_device_info_t *info)
info->get_instance_proc_addr = (retro_proc_address_t)
vulkan_symbol_wrapper_instance_proc_addr();
info->queue_family = vk->context->graphics_queue_index;
info->queue = (void*)vk->context->queue;
info->queue_lock = NULL;
info->recording_caps = 0;
#if defined(ANDROID) && defined(HAVE_THREADS)
info->queue_lock = vk->context->queue_lock;
#if defined(HAVE_RMP4) || defined(HAVE_RVP9) || defined(HAVE_RAV1) \
|| defined(HAVE_RECORD_MEDIACODEC)
info->recording_caps =
(vk->android_device_support & VK_CTX_FLAG_ANDROID_IMAGES)
? VIDEO_DEVICE_CAP_ANDROID_CAPTURE_INTEROP : 0;
#endif
#endif
#if defined(ANDROID) \
&& (defined(HAVE_RMP4) || defined(HAVE_RVP9) || defined(HAVE_RAV1))
&& (defined(HAVE_RMP4) || defined(HAVE_RVP9) || defined(HAVE_RAV1) \
|| defined(HAVE_RECORD_MEDIACODEC))
info->import_formats =
(vk->android_device_support & VK_CTX_FLAG_ANDROID_IMAGES)
|| ((vk->android_device_support & VK_CTX_FLAG_TIMELINE_SEMAPHORE)
@@ -15980,7 +16103,8 @@ static const video_poke_interface_t vulkan_poke_interface = {
vulkan_driver_pacing_wait,
vulkan_driver_pacing_mark,
#if defined(ANDROID) \
&& (defined(HAVE_RMP4) || defined(HAVE_RVP9) || defined(HAVE_RAV1))
&& (defined(HAVE_RMP4) || defined(HAVE_RVP9) || defined(HAVE_RAV1) \
|| defined(HAVE_RECORD_MEDIACODEC))
vulkan_prepare_import_texture
#else
NULL
@@ -16848,7 +16972,7 @@ static void vulkan_render_overlay(vk_t *vk, unsigned dims)
(vk->overlay.images[idx].flags & VK_TEX_FLAG_MIPMAP)
? vk->samplers.mipmap_linear : vk->samplers.linear))
{
/* Batch full — flush what we have and add again. */
/* Batch full — flush what we have and add again. */
vulkan_descriptor_batch_flush(vk->context->device, batch);
vulkan_descriptor_batch_add(batch, sets[i],
ubo.buffer, ubo.offset, sizeof(vk->mvp),
+9 -1
View File
@@ -1046,6 +1046,11 @@ enum video_device_api
VIDEO_DEVICE_METAL
};
enum video_device_recording_cap
{
VIDEO_DEVICE_CAP_ANDROID_CAPTURE_INTEROP = 1
};
typedef struct video_device_info
{
enum video_device_api api;
@@ -1056,14 +1061,17 @@ typedef struct video_device_info
void *instance;
void *physical_device;
void *device;
/* D3D12's immutable render queue, borrowed with the device. */
/* Vulkan or D3D12 render queue, borrowed with the device. */
void *queue;
/* Existing Vulkan queue serialization; cold render-owner jobs only. */
void *queue_lock;
retro_proc_address_t get_instance_proc_addr;
/* The driver's queue family: an image the driver imports is either
* exclusive to it or shared with it (concurrent). */
unsigned queue_family;
/* image_pixfmt bits import_texture takes; 0 when it takes none. */
uint32_t import_formats;
uint32_t recording_caps;
} video_device_info_t;
/* A frame that lives on the driver's device; handles are given as
+7
View File
@@ -2022,6 +2022,13 @@ static void video_thread_async_run(thread_video_t *thr)
GFX_INSTR_INC(n->dropped ? GFX_INSTR_TEX_UPDATE_DROPPED
: GFX_INSTR_TEX_UPDATE);
}
else if (n->kind == VIDEO_THREAD_ASYNC_JOB)
{
const video_thread_async_job_t *job
= (const video_thread_async_job_t *)n->img;
n->handle = job && job->run
? job->run(driver_data, poke, job->user) : 0;
}
else
{
const struct texture_image *ti =
+11 -1
View File
@@ -229,9 +229,19 @@ enum video_thread_async_kind
{
VIDEO_THREAD_ASYNC_LOAD = 0,
VIDEO_THREAD_ASYNC_UPDATE,
VIDEO_THREAD_ASYNC_IMPORT
VIDEO_THREAD_ASYNC_IMPORT,
VIDEO_THREAD_ASYNC_JOB
};
/* Cold render-owner job. Payload and node survive through done/release.
* Results are opaque, never texture handles. Missing drivers are valid input. */
typedef struct video_thread_async_job
{
uintptr_t (*run)(void *driver_data, const video_poke_interface_t *poke,
void *user);
void *user;
} video_thread_async_job_t;
typedef struct video_thread_async_load
{
/* The wrapper's list link, first so a node and its link share an
+33 -1
View File
@@ -396,7 +396,8 @@ VIDEO CONTEXT
#ifdef HAVE_VULKAN
#if defined(ANDROID) \
&& (defined(HAVE_RMP4) || defined(HAVE_RVP9) || defined(HAVE_RAV1))
&& (defined(HAVE_RMP4) || defined(HAVE_RVP9) || defined(HAVE_RAV1) \
|| defined(HAVE_RECORD_MEDIACODEC))
#include "../gfx/common/vulkan_android_device.c"
#include "../gfx/common/android_image_queue.c"
#include "../gfx/common/android_frame.c"
@@ -428,6 +429,9 @@ VIDEO CONTEXT
#endif
#endif
#endif
#if defined(ANDROID) && defined(HAVE_THREADS)
#include "../record/record_android_capture_device.c"
#endif
#include "../gfx/common/vulkan_common.c"
#include "../libretro-common/vulkan/vulkan_symbol_wrapper.c"
#ifdef HAVE_RMP4
@@ -1690,6 +1694,34 @@ RECORDING
#include "../record/drivers/record_wav.c"
#include "../record/drivers/record_nvenc.c"
#include "../record/drivers/record_metal.c"
#include "../record/drivers/record_mediacodec.c"
#ifdef HAVE_RECORD_MEDIACODEC
#include "../record/record_audio_tap.c"
#include "../record/record_aac_probe.c"
#include "../record/record_mp4_audio_edit.c"
#include "../record/record_mediacodec_encoder.c"
#include "../record/record_mux_mediacodec.c"
#include "../record/record_mediacodec_worker.c"
#include "../record/record_mediacodec_surface.c"
#include "../record/record_mediacodec_file.c"
#include "../record/record_mediacodec_file_edit.c"
#include "../record/record_session_mediacodec.c"
#include "../record/record_dispatch_mediacodec.c"
#include "../record/record_gpu_dispatch_mediacodec.c"
#include "../record/record_mediacodec_gpu_worker.c"
#include "../record/record_android_capture_config.c"
#include "../record/record_android_capture_request.c"
#include "../record/record_android_capture_images.c"
#include "../record/record_android_capture_submit.c"
#include "../record/record_android_capture_pipe.c"
#include "../record/record_android_capture_pool.c"
#include "../record/record_android_capture_ops.c"
#include "../record/record_android_capture_egl.c"
#include "../record/record_android_capture_gl.c"
#include "../record/record_android_capture_draw.c"
#include "../record/record_gpu_queue.c"
#include "../record/record_audio_queue.c"
#endif
#ifdef HAVE_RECORD_METAL
#include "../record/record_gpu_queue.c"
#include "../record/record_audio_queue.c"
@@ -278,6 +278,8 @@ LOCAL_CPPFLAGS += $(INCLUDE_DIRS)
LOCAL_CXXFLAGS += $(INCLUDE_DIRS)
ifeq ($(HAVE_VULKAN),1)
LOCAL_CFLAGS += -DHAVE_RECORD_MEDIACODEC
LOCAL_CPPFLAGS += -DHAVE_RECORD_MEDIACODEC
INCFLAGS += $(LOCAL_PATH)/$(RARCH_DIR)/gfx/include
LOCAL_C_INCLUDES += $(INCFLAGS)
+782
View File
@@ -0,0 +1,782 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier:
* GPL-3.0-or-later */
#ifdef HAVE_CONFIG_H
#include "../../config.h"
#endif
#include "record_mediacodec.h"
#include "../../gfx/video_driver.h"
#if defined(HAVE_RECORD_MEDIACODEC) || defined(RECORD_MEDIACODEC_FRONTEND_TEST)
#include <stdlib.h>
#include <string.h>
#include <limits.h>
#include <math.h>
#include <file/config_file.h>
#include <string/stdstring.h>
#include "../record_android_capture_config.h"
#include "../record_android_capture_request.h"
#include "../record_audio_tap.h"
#include "../../runloop.h"
#include <features/features_cpu.h>
#include "../../audio/audio_driver.h"
#include "../../audio/audio_upmix.h"
#include "../../configuration.h"
#include "../../command.h"
#include "../../verbosity.h"
#if defined(HAVE_FFMPEG) || defined(HAVE_MPV) || defined(HAVE_WEBMPLAYER)
#include "../../defaults.h"
#include "../../playlist.h"
#endif
typedef struct record_mc_context
{
uint64_t next_us, step, carry, audio_frames, offered;
int64_t menu_origin;
uint64_t last_menu_pts;
struct record_mc_context *next;
record_dispatch_mediacodec_t *io;
record_audio_tap_t *tap;
record_android_capture_config_t *config;
record_android_capture_request_t *request;
void *device;
char *path;
unsigned divisor, dims, mode, rate, channels;
enum record_android_capture_operation operation;
enum record_dispatch_mediacodec_state final_state;
int error, file_error, system_error;
bool armed, stopped, failed, audio_closed, gpu_closed;
bool resetting, reset_ready, menu_source;
} record_mc_context_t;
static record_mc_context_t *record_mc_active, *record_mc_retired;
static bool record_mc_start_requested;
/* Rational microsecond cadence: rounding each frame separately must not
* accumulate drift, and movie seeking must never reset this recording clock. */
static bool record_mc_rate(double fps, unsigned *num, unsigned *den)
{
uint64_t h0 = 0, h1 = 1, k0 = 1, k1 = 0;
double value = fps;
unsigned i;
if (fps != fps || fps < 1.0 || fps > 1000.0)
return false;
for (i = 0; i < 32; i++)
{
uint64_t a, h, k;
double fraction;
if (value > UINT_MAX)
return false;
a = (uint64_t)value;
if ((h1 && a > (UINT_MAX - h0) / h1) || (k1 && a > (1000000u - k0) / k1))
return false;
h = a * h1 + h0;
k = a * k1 + k0;
if (!k || k > 1000000u)
return false;
if (fabs((double)h / k - fps) <= fps * 0.000000001)
{
*num = (unsigned)h;
*den = (unsigned)k;
return true;
}
h0 = h1;
h1 = h;
k0 = k1;
k1 = k;
fraction = value - (double)a;
if (fraction <= 0.0)
return false;
value = 1.0 / fraction;
}
return false;
}
/* Android CodecProfileLevel values and picture/rate/bitrate limits:
* AOSP MediaCodecInfo.VideoCapabilities (AVC baseline, HEVC main/high tier).
* This selects a minimum request, not proof of hardware/session support.
* https://android.googlesource.com/platform/frameworks/base/+/fecafc513b542baf3931a95d5096cde679730dca/media/java/android/media/MediaCodecInfo.java
*/
static int record_mc_level(const mediacodec_encoder_request_t *video)
{
struct record_mc_level_limit
{
double rate;
unsigned size, bitrate;
int level;
};
static const struct record_mc_level_limit avc[] = {{1485, 99, 64000, 1},
{1485, 99, 128000, 2}, {3000, 396, 192000, 4}, {6000, 396, 384000, 8},
{11880, 396, 768000, 16}, {11880, 396, 2000000, 32},
{19800, 792, 4000000, 64}, {20250, 1620, 4000000, 128},
{40500, 1620, 10000000, 256}, {108000, 3600, 14000000, 512},
{216000, 5120, 20000000, 1024}, {245760, 8192, 20000000, 2048},
{245760, 8192, 50000000, 4096}, {522240, 8704, 50000000, 8192},
{589824, 22080, 135000000, 16384}, {983040, 36864, 240000000, 32768},
{2073600, 36864, 240000000, 65536},
{4177920, 139264, 240000000, 131072},
{8355840, 139264, 480000000, 262144},
{16711680, 139264, 800000000, 524288}};
static const struct record_mc_level_limit hevc[] = {
{552960, 36864, 128000, 1}, {3686400, 122880, 1500000, 4},
{7372800, 245760, 3000000, 16}, {16588800, 552960, 6000000, 64},
{33177600, 983040, 10000000, 256}, {66846720, 2228224, 12000000, 1024},
{66846720, 2228224, 30000000, 2048},
{133693440, 2228224, 20000000, 4096},
{133693440, 2228224, 50000000, 8192},
{267386880, 8912896, 25000000, 16384},
{267386880, 8912896, 100000000, 32768},
{534773760, 8912896, 40000000, 65536},
{534773760, 8912896, 160000000, 131072},
{1069547520, 8912896, 60000000, 262144},
{1069547520, 8912896, 240000000, 524288},
{1069547520, 35651584, 60000000, 1048576},
{1069547520, 35651584, 240000000, 2097152},
{2139095040, 35651584, 120000000, 4194304},
{2139095040, 35651584, 480000000, 8388608},
{4278190080.0, 35651584, 240000000, 16777216},
{4278190080.0, 35651584, 800000000, 33554432}};
const struct record_mc_level_limit *limits;
size_t count, i;
unsigned tier;
double width, height, size;
bool is_hevc = string_is_equal(video->mime, "video/hevc");
if (!video->width || !video->height || !video->bitrate
|| !(video->frame_rate > 0.0 && video->frame_rate <= 1000.0))
return 0;
if (is_hevc)
{
width = video->width;
height = video->height;
limits = hevc;
count = sizeof(hevc) / sizeof(hevc[0]);
}
else if (string_is_equal(video->mime, "video/avc"))
{
width = ceil((double)video->width / 16.0);
height = ceil((double)video->height / 16.0);
limits = avc;
count = sizeof(avc) / sizeof(avc[0]);
}
else
return 0;
size = width * height;
/* Prefer main tier: a lower high-tier number is not a guarantee that a
* main-tier encoder accepts it. Explicit levels retain the user's choice. */
for (tier = 0; tier < (is_hevc ? 2u : 1u); tier++)
for (i = 0; i < count; i++)
if ((!is_hevc || !!(limits[i].level & 0x2AAAAAA) == !!tier)
&& size <= limits[i].size
&& width * width <= 8.0 * limits[i].size
&& height * height <= 8.0 * limits[i].size
&& size * video->frame_rate <= limits[i].rate
&& video->bitrate <= limits[i].bitrate)
return limits[i].level;
return 0;
}
static bool record_mc_options(
const char *path, mediacodec_encoder_request_t *video, bool *audio)
{
config_file_t *config;
struct config_file_entry entry;
char codec[16], format[8];
unsigned level;
bool ok = false;
*audio = true;
if (!path || !*path)
return true;
config = config_file_new_from_path_to_string(path);
if (!config)
return false;
if (config_get_entry(config, "audio_enable")
&& !config_get_bool(config, "audio_enable", audio))
goto done;
if (config_get_entry(config, "vcodec"))
{
if (!config_get_array(config, "vcodec", codec, sizeof(codec)))
goto done;
if (string_is_equal(codec, "h264"))
video->mime = "video/avc";
else if (string_is_equal(codec, "hevc"))
video->mime = "video/hevc";
else
goto done;
}
if (config_get_entry(config, "format")
&& (!config_get_array(config, "format", format, sizeof(format))
|| !string_is_equal(format, "mp4")))
goto done;
if (config_get_entry(config, "video_bit_rate")
&& (!config_get_uint(config, "video_bit_rate", &video->bitrate)
|| !video->bitrate || video->bitrate > INT_MAX))
goto done;
if (config_get_entry(config, "video_level"))
{
if (!config_get_uint(config, "video_level", &level) || !level
|| level > INT_MAX || (level & (level - 1)))
goto done;
video->level = (int)level;
}
if (config_get_entry_list_head(config, &entry))
do
{
if (!string_is_equal(entry.key, "audio_enable")
&& !string_is_equal(entry.key, "vcodec")
&& !string_is_equal(entry.key, "format")
&& !string_is_equal(entry.key, "video_bit_rate")
&& !string_is_equal(entry.key, "video_level"))
goto done;
} while (config_get_entry_list_next(&entry));
ok = true;
done:
config_file_free(config);
return ok;
}
static bool record_mc_color(
mediacodec_encoder_request_t *video, const struct retro_hdr_metadata *hdr)
{
unsigned i;
float light[2];
if (hdr->version != 1 || hdr->size < sizeof(*hdr) || hdr->output_mode > 2)
return false;
video->color = hdr->output_mode != 0;
video->profile = video->color ? 4096 : 1;
video->range = 1;
if (!video->color)
return true;
if (!string_is_equal(video->mime, "video/hevc")
|| !((hdr->output_mode == 1 && hdr->color_primaries == 9
&& hdr->transfer_function == 16)
|| (hdr->output_mode == 2 && hdr->color_primaries == 1
&& hdr->transfer_function == 8)))
return false;
/* The core ABI does not supply mastering-display chromaticities.
* Unknown fields stay zero; never invent them from panel capabilities.
* The GLES worker converts scRGB to PQ once, before the encoder Surface. */
video->hdr_static_size = 25;
light[0] = hdr->max_cll;
light[1] = hdr->max_fall;
for (i = 0; i < 2; i++)
{
unsigned n;
if (!(light[i] >= 0.0f && light[i] <= 65535.0f))
return false;
n = (unsigned)(light[i] + 0.5f);
video->hdr_static[21 + 2 * i] = (unsigned char)n;
video->hdr_static[22 + 2 * i] = (unsigned char)(n >> 8);
}
return true;
}
static void record_mc_fail(record_mc_context_t *ctx)
{
ctx->failed = true;
ctx->armed = false;
record_dispatch_mediacodec_cancel(ctx->io);
}
static void record_mc_close(record_mc_context_t *ctx)
{
if (ctx->menu_source && ctx->armed)
{
int64_t now = cpu_features_get_time_usec();
uint64_t maximum = (((uint64_t)1 << 63) - 1) / 1000;
if (now >= ctx->menu_origin)
{
uint64_t end = (uint64_t)now - (uint64_t)ctx->menu_origin;
if (end <= maximum && end > ctx->next_us)
ctx->next_us = end;
}
}
ctx->armed = false;
ctx->stopped = true;
if (ctx->tap)
{
if (!audio_driver_record_tap_remove(ctx->tap)
|| !record_audio_tap_end(ctx->tap, &ctx->audio_frames))
return;
}
if (!ctx->audio_closed)
ctx->audio_closed = record_dispatch_mediacodec_audio_finish(
ctx->io, ctx->audio_frames);
}
static void record_mc_retire(record_mc_context_t *ctx)
{
record_mc_close(ctx);
ctx->next = record_mc_retired;
record_mc_retired = ctx;
}
static int record_mc_mixed_audio(
void *io, const int16_t *samples, size_t frames, uint64_t pts)
{
return record_dispatch_mediacodec_audio(
(record_dispatch_mediacodec_t *)io, samples, frames, pts);
}
static void *record_mc_init(const struct record_params *params)
{
mediacodec_encoder_request_t encoder;
video_device_info_t device;
struct retro_hdr_metadata hdr;
record_mc_context_t *ctx;
settings_t *settings = config_get_ptr();
unsigned num, den, count, rate = 0, channels = 0;
size_t capacity = 0, path_length;
bool audio;
if (!params || !settings || record_mc_active || record_mc_retired
|| !params->filename || !*params->filename || !params->video_gpu_record
|| recording_state_get_ptr()->streaming_enable
|| settings->bools.video_record_game_only
|| audio_driver_has_callback() || !video_driver_get_device(&device)
|| device.api != VIDEO_DEVICE_VULKAN)
return NULL;
memset(&encoder, 0, sizeof(encoder));
memset(&hdr, 0, sizeof(hdr));
hdr.version = 1;
hdr.size = sizeof(hdr);
if (!video_driver_get_display_metadata(
RETRO_ENVIRONMENT_GET_HDR_METADATA, &hdr))
return NULL;
encoder.width = VIDEO_SCALE_W(params->out_dims);
encoder.height = VIDEO_SCALE_H(params->out_dims);
encoder.frame_rate = params->fps;
encoder.bitrate = 16000000;
encoder.mime = hdr.output_mode ? "video/hevc" : "video/avc";
if (!encoder.width || !encoder.height
|| ((encoder.width | encoder.height) & 1) || encoder.width > 16384
|| encoder.height > 16384 || !record_mc_rate(params->fps, &num, &den)
|| !record_mc_options(params->config, &encoder, &audio)
|| !record_mc_color(&encoder, &hdr)
|| (!hdr.output_mode
&& settings->uints.video_swapchain_bit_depth == 2))
return NULL;
/* Explicit Android levels. Inventory/session admission checks the complete
* requested dimensions, rate, profile, level and color on the I/O worker. */
if (!encoder.level)
{
encoder.level = record_mc_level(&encoder);
if (!encoder.level)
return NULL;
}
if (audio)
{
double sr = params->samplerate;
uint32_t layout = recording_state_get_ptr()->layout;
if (!(sr >= 8000.0 && sr <= 192000.0) || floor(sr) != sr
|| !params->channels || params->channels > 2
|| audio_layout_channels(layout) != params->channels
|| layout
!= (params->channels == 1 ? AUDIO_SPEAKER_FRONT_CENTER
: AUDIO_LAYOUT_STEREO))
return NULL;
rate = (unsigned)sr;
channels = params->channels;
capacity = (size_t)rate * channels * sizeof(int16_t) * 2 + 4096;
}
count = params->video_fifo_frames >= 2
&& params->video_fifo_frames <= RECORD_GPU_QUEUE_SLOTS
? params->video_fifo_frames
: 4;
for (path_length = 0;
path_length < PATH_MAX_LENGTH && params->filename[path_length];
path_length++)
{
}
if (path_length == PATH_MAX_LENGTH)
return NULL;
ctx = (record_mc_context_t *)calloc(1, sizeof(*ctx) + path_length + 1);
if (!ctx)
return NULL;
ctx->path = (char *)(ctx + 1);
memcpy(ctx->path, params->filename, path_length + 1);
/* Vulkan and encoder surfaces have opposite row origins. */
ctx->config = record_android_capture_config_new(
&device, encoder.width, encoder.height, count, hdr.output_mode, true);
if (!ctx->config)
{
free(ctx);
return NULL;
}
ctx->device = device.device;
ctx->dims = params->out_dims;
ctx->mode = hdr.output_mode;
ctx->step = (uint64_t)1000000 * den;
ctx->divisor = num;
ctx->menu_source = params->menu_source;
ctx->rate = rate;
ctx->channels = channels;
ctx->io = record_dispatch_mediacodec_new(&encoder, 2, rate, channels,
audio ? 128000 : 0, audio ? 4096 : 0, capacity, ctx->path);
if (!ctx->io)
{
record_android_capture_config_free(ctx->config);
free(ctx);
return NULL;
}
if (ctx->menu_source && audio)
{
ctx->tap = record_audio_tap_new(rate, record_mc_mixed_audio, ctx->io);
if (!ctx->tap)
{
record_mc_fail(ctx);
record_mc_retire(ctx);
return NULL;
}
}
ctx->request = record_android_capture_request_new(ctx->io,
record_android_capture_config_pool(ctx->config),
(unsigned)(ctx->step / num), params->allow_frame_drop,
ctx->menu_source);
if (!ctx->request)
{
record_mc_fail(ctx);
record_mc_retire(ctx);
return NULL;
}
record_mc_active = ctx;
return ctx;
}
static bool record_mc_finalize(void *data)
{
record_mc_context_t *ctx = (record_mc_context_t *)data;
record_mc_close(ctx);
return !ctx->failed && !record_dispatch_mediacodec_cancelled(ctx->io);
}
static void record_mc_release(void *data)
{
record_mc_context_t *ctx = (record_mc_context_t *)data;
record_mc_active = NULL;
record_mc_retire(ctx);
}
static bool record_mc_audio(void *data, const struct record_audio_data *audio)
{
record_mc_context_t *ctx = (record_mc_context_t *)data;
const int16_t *samples;
size_t frames;
if (ctx->stopped || ctx->failed)
return false;
/* Menu filler is mixed on the audio owner; it is not the recorded PCM. */
if (ctx->menu_source)
return true;
if (!ctx->channels || !ctx->armed || (audio && !audio->frames))
return true;
if (!audio || !audio->data
|| video_state_get_ptr()->av_info.timing.sample_rate != ctx->rate
|| audio->frames > (~(size_t)0) / ctx->channels / sizeof(int16_t)
|| ctx->audio_frames > ~(uint64_t)0 - audio->frames)
goto failed;
samples = (const int16_t *)audio->data;
frames = audio->frames;
while (frames)
{
size_t chunk = frames > 4096 ? 4096 : frames;
if (record_dispatch_mediacodec_audio(
ctx->io, samples, chunk, ctx->audio_frames)
!= RECORD_AUDIO_OK)
goto failed;
ctx->audio_frames += chunk;
samples += chunk * ctx->channels;
frames -= chunk;
}
return true;
failed:
record_mc_fail(ctx);
return false;
}
const record_driver_t record_mediacodec = {record_mc_init, record_mc_release,
NULL, record_mc_audio, record_mc_finalize, "mediacodec"};
void record_mediacodec_frame_info(struct video_frame_info *info, bool capture)
{
record_mc_context_t *ctx = record_mc_active;
uint64_t delta, carry, maximum = (((uint64_t)1 << 63) - 1) / 1000;
info->recording_gpu = NULL;
info->recording_gpu_capture = false;
info->recording_gpu_timestamp = 0;
info->recording_gpu_pending = record_mc_retired != NULL;
if (!ctx || ctx->stopped || ctx->failed || !ctx->armed)
return;
info->recording_gpu
= (void *)record_android_capture_request_binding(ctx->request);
if (!info->recording_gpu || !capture
|| recording_state_get_ptr()->data != ctx
|| (video_state_get_ptr()->frame_repeat && !ctx->menu_source))
return;
if (ctx->menu_source)
{
int64_t now = cpu_features_get_time_usec();
uint64_t pts;
delta = ctx->step / ctx->divisor;
if (now < ctx->menu_origin)
return;
pts = (uint64_t)now - (uint64_t)ctx->menu_origin;
if (ctx->offered && pts <= ctx->last_menu_pts)
return;
if (pts > maximum - delta)
{
record_mc_fail(ctx);
info->recording_gpu = NULL;
return;
}
info->recording_gpu_capture = true;
info->recording_gpu_timestamp = pts;
ctx->last_menu_pts = pts;
ctx->next_us = pts + delta;
ctx->offered++;
return;
}
carry = ctx->carry + ctx->step % ctx->divisor;
delta = ctx->step / ctx->divisor + carry / ctx->divisor;
if (ctx->next_us > maximum - delta)
{
record_mc_fail(ctx);
info->recording_gpu = NULL;
return;
}
info->recording_gpu_capture = true;
info->recording_gpu_timestamp = ctx->next_us;
ctx->next_us += delta;
ctx->carry = carry % ctx->divisor;
ctx->offered++;
}
void record_mediacodec_stop(void)
{
if (record_mc_active || record_mc_start_requested)
command_event(CMD_EVENT_RECORD_DEINIT, NULL);
}
bool record_mediacodec_pending(void)
{
return record_mc_active || record_mc_retired || record_mc_start_requested;
}
bool record_mediacodec_reset_pending(void)
{
record_mc_context_t *ctx;
bool pending = record_mc_active != NULL || record_mc_start_requested;
for (ctx = record_mc_retired; ctx; ctx = ctx->next)
{
ctx->resetting = true;
if (ctx->request && !ctx->reset_ready)
pending = true;
}
return pending;
}
bool record_mediacodec_defer_start(void)
{
record_mc_start_requested = true;
return true;
}
bool record_mediacodec_cancel_start(void)
{
bool requested = record_mc_start_requested;
record_mc_start_requested = false;
return requested;
}
static bool record_mc_collect(record_mc_context_t *ctx)
{
enum record_dispatch_mediacodec_state state;
if (ctx->request)
{
if (ctx->operation == RECORD_ANDROID_CAPTURE_QUIESCE_DEVICE
&& record_android_capture_request_result(ctx->request) == 1)
ctx->reset_ready = true;
if (record_android_capture_request_free(ctx->request))
{
ctx->request = NULL;
ctx->gpu_closed = true;
}
else
{
enum record_android_capture_operation op
= ctx->resetting && !ctx->reset_ready
&& (ctx->failed
|| record_dispatch_mediacodec_cancelled(ctx->io)
|| record_android_capture_request_result(
ctx->request) < 0)
? RECORD_ANDROID_CAPTURE_QUIESCE_DEVICE
: RECORD_ANDROID_CAPTURE_COLLECT;
if (record_android_capture_request_post(
ctx->request, op, (int64_t)ctx->next_us))
ctx->operation = op;
return false;
}
}
if (!ctx->gpu_closed)
{
state = record_dispatch_mediacodec_status(ctx->io, NULL, NULL, NULL);
if (state != RECORD_MC_DISPATCH_DONE
&& state != RECORD_MC_DISPATCH_FAILED)
{
if (!record_dispatch_mediacodec_gpu_claim(ctx->io))
return false;
ctx->gpu_closed = record_dispatch_mediacodec_gpu_retired(ctx->io, 0)
&& record_dispatch_mediacodec_gpu_released(ctx->io);
record_dispatch_mediacodec_gpu_release_claim(ctx->io);
if (!ctx->gpu_closed)
return false;
}
}
ctx->final_state = record_dispatch_mediacodec_status(
ctx->io, &ctx->error, &ctx->file_error, &ctx->system_error);
if (ctx->final_state != RECORD_MC_DISPATCH_DONE
&& ctx->final_state != RECORD_MC_DISPATCH_FAILED)
return false;
return ctx->audio_closed && record_dispatch_mediacodec_free(ctx->io);
}
void record_mediacodec_poll(void)
{
record_mc_context_t **link;
if (record_mc_start_requested)
{
struct video_viewport vp;
memset(&vp, 0, sizeof(vp));
if (!recording_state_get_ptr()->enable)
record_mc_start_requested = false;
else if (video_driver_get_viewport_info(&vp))
{
unsigned dims
= runloop_state_get_ptr()->current_core_type == CORE_TYPE_DUMMY
? vp.full_dims
: vp.dims;
if (VIDEO_SCALE_W(dims) && VIDEO_SCALE_H(dims))
{
record_mc_start_requested = false;
command_event(CMD_EVENT_RECORD_INIT, NULL);
}
}
}
if (record_mc_active)
{
record_mc_context_t *ctx = record_mc_active;
video_device_info_t device;
struct retro_hdr_metadata hdr;
struct video_viewport vp;
enum record_dispatch_mediacodec_state state
= record_dispatch_mediacodec_status(ctx->io, NULL, NULL, NULL);
memset(&hdr, 0, sizeof(hdr));
memset(&vp, 0, sizeof(vp));
hdr.version = 1;
hdr.size = sizeof(hdr);
if (!video_driver_get_device(&device) || device.device != ctx->device
|| !video_driver_get_display_metadata(
RETRO_ENVIRONMENT_GET_HDR_METADATA, &hdr)
|| hdr.output_mode != ctx->mode
|| !video_driver_get_viewport_info(&vp)
|| (ctx->menu_source ? vp.full_dims : vp.dims) != ctx->dims
|| audio_driver_has_callback()
|| (ctx->tap && record_audio_tap_failed(ctx->tap))
|| (ctx->menu_source
&& runloop_state_get_ptr()->current_core_type
!= CORE_TYPE_DUMMY)
|| state >= RECORD_MC_DISPATCH_DRAINING
|| record_dispatch_mediacodec_cancelled(ctx->io))
record_mc_fail(ctx);
if (ctx->failed || ctx->stopped)
record_mediacodec_stop();
else if (!ctx->armed)
{
if (record_android_capture_request_binding(ctx->request))
{
int attached
= ctx->tap ? audio_driver_record_tap_install(ctx->tap) : 1;
if (attached < 0)
{
record_mc_fail(ctx);
record_mediacodec_stop();
}
else if (attached == 1)
{
ctx->menu_origin = cpu_features_get_time_usec();
ctx->armed = true;
}
}
else if (record_android_capture_request_result(ctx->request) < 0)
{
record_mc_fail(ctx);
record_mediacodec_stop();
}
else if (record_android_capture_request_post(
ctx->request, RECORD_ANDROID_CAPTURE_CREATE, 0))
ctx->operation = RECORD_ANDROID_CAPTURE_CREATE;
}
}
link = &record_mc_retired;
while (*link)
{
record_mc_context_t *ctx = *link;
bool collected;
record_mc_close(ctx);
collected = record_mc_collect(ctx);
if (!ctx->audio_closed || !collected)
{
link = &ctx->next;
continue;
}
if (ctx->final_state == RECORD_MC_DISPATCH_DONE && !ctx->failed
&& !ctx->error && !ctx->file_error && !ctx->system_error
&& ctx->offered)
{
RARCH_LOG("[MediaCodec recording] Finalized %llu offered frames, "
"%llu PCM frames.\n",
(unsigned long long)ctx->offered,
(unsigned long long)ctx->audio_frames);
#if defined(HAVE_FFMPEG) || defined(HAVE_MPV) || defined(HAVE_WEBMPLAYER)
/* DONE follows mux/file finalization and independent native-user
* retirement. Offered frames or frontend finalize alone prove none
* of these; publish only after successful I/O collection. */
{
settings_t *settings = config_get_ptr();
if (settings && settings->bools.history_list_enable
&& g_defaults.video_history)
{
struct playlist_entry entry = {0};
entry.path = ctx->path;
entry.core_path = (char *)"builtin";
entry.core_name = (char *)"movieplayer";
command_playlist_push_write(g_defaults.video_history, &entry);
}
}
#endif
}
else
RARCH_ERR("[MediaCodec recording] Failed (session %d, file %d, "
"system %d).\n",
ctx->error, ctx->file_error, ctx->system_error);
record_audio_tap_free(ctx->tap);
record_android_capture_config_free(ctx->config);
*link = ctx->next;
free(ctx);
}
}
#else
const record_driver_t record_mediacodec
= {NULL, NULL, NULL, NULL, NULL, "mediacodec"};
void record_mediacodec_frame_info(struct video_frame_info *info, bool capture)
{
(void)capture;
info->recording_gpu = NULL;
info->recording_gpu_timestamp = 0;
info->recording_gpu_capture = info->recording_gpu_pending = false;
}
void record_mediacodec_poll(void)
{
}
void record_mediacodec_stop(void)
{
}
bool record_mediacodec_pending(void)
{
return false;
}
bool record_mediacodec_reset_pending(void)
{
return false;
}
bool record_mediacodec_defer_start(void)
{
return false;
}
bool record_mediacodec_cancel_start(void)
{
return false;
}
#endif
+25
View File
@@ -0,0 +1,25 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier:
* GPL-3.0-or-later */
#ifndef RECORD_MEDIACODEC_H
#define RECORD_MEDIACODEC_H
#include "../record_driver.h"
#if defined(HAVE_RECORD_MEDIACODEC) \
&& (!defined(ANDROID) || !defined(HAVE_VULKAN) || !defined(HAVE_THREADS) \
|| !defined(HAVE_CONFIGFILE))
#error Native MediaCodec recording requires Android, Vulkan, threads and config files
#endif
RETRO_BEGIN_DECLS
struct video_frame_info;
extern const record_driver_t record_mediacodec;
void record_mediacodec_frame_info(struct video_frame_info *info, bool capture);
void record_mediacodec_poll(void);
void record_mediacodec_stop(void);
bool record_mediacodec_pending(void);
/* Main owner after stop. Acknowledged producer quiescence allows the existing
* Vulkan teardown drain to prove child retirement, while GLES/I/O stay owned.
* This does not claim native completion or device death. */
bool record_mediacodec_reset_pending(void);
bool record_mediacodec_defer_start(void);
bool record_mediacodec_cancel_start(void);
RETRO_END_DECLS
#endif
+191
View File
@@ -0,0 +1,191 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier:
* GPL-3.0-or-later */
#include "record_aac_probe.h"
#include <stdlib.h>
#ifdef HAVE_RAAC
#include <formats/raac.h>
#include <math.h>
#define PROBE_BURST 2048u
#define PROBE_PACKET 1024u
#define PROBE_DELAY 4096u
struct record_aac_probe
{
double energy, best, second;
raac_t *decoder;
float output[RECORD_AAC_PROBE_FRAMES * 2];
unsigned rate, channels, decoded, candidate, delay;
int16_t input[RECORD_AAC_PROBE_FRAMES * 2];
bool configured, failed, ready;
};
record_aac_probe_t *record_aac_probe_new(unsigned rate, unsigned channels)
{
record_aac_probe_t *p;
unsigned i, c;
if (!rate || rate > 192000 || !channels || channels > 2)
return NULL;
p = (record_aac_probe_t *)calloc(1, sizeof(*p));
if (!p)
return NULL;
p->rate = rate;
p->channels = channels;
p->best = p->second = 1e30;
for (i = 0; i < PROBE_BURST; i++)
for (c = 0; c < channels; c++)
{
double t = (double)i / PROBE_BURST;
double phase = (400.0 + 300.0 * c) * i / rate
+ 500.0 * PROBE_BURST / rate * t * t;
double window = sin(3.141592653589793 * t);
int16_t value = (int16_t)(12000.0 * window * window
* sin(6.283185307179586 * phase));
p->input[(PROBE_BURST + i) * channels + c] = value;
p->energy += (double)value * value / (32768.0 * 32768.0);
}
if (p->energy < 1.0)
{
free(p);
return NULL;
}
return p;
}
const int16_t *record_aac_probe_input(record_aac_probe_t *p,
size_t offset, size_t *frames)
{
if (frames)
*frames = 0;
if (!p || !frames || offset >= RECORD_AAC_PROBE_FRAMES)
return NULL;
*frames = RECORD_AAC_PROBE_FRAMES - offset;
return p->input + offset * p->channels;
}
bool record_aac_probe_config(record_aac_probe_t *p, const void *asc, size_t bytes)
{
if (!p || p->failed || p->configured || !asc || !bytes)
return false;
p->decoder = raac_open((const uint8_t *)asc, bytes);
if (!p->decoder || raac_sample_rate(p->decoder) != p->rate
|| raac_channels(p->decoder) != p->channels
|| raac_frame_len(p->decoder) != PROBE_PACKET)
{
p->failed = true;
return false;
}
p->configured = true;
return true;
}
bool record_aac_probe_packet(record_aac_probe_t *p, const void *data, size_t bytes)
{
int frames;
if (!p || p->failed || !p->configured || !data || !bytes)
return false;
if (p->decoded == RECORD_AAC_PROBE_FRAMES)
return true;
frames = raac_decode_f32(p->decoder, (const uint8_t *)data, bytes,
p->output + p->decoded * p->channels);
if (frames != PROBE_PACKET)
{
p->failed = true;
return false;
}
p->decoded += PROBE_PACKET;
return true;
}
int record_aac_probe_step(record_aac_probe_t *p)
{
unsigned n, i, count;
if (!p || p->failed)
return -1;
if (p->ready)
return 2;
if (p->decoded < RECORD_AAC_PROBE_FRAMES)
return 0;
count = PROBE_BURST * p->channels;
for (n = 0; n < 16 && p->candidate <= PROBE_DELAY; n++, p->candidate++)
{
double error = 0;
const int16_t *ref = p->input + PROBE_BURST * p->channels;
const float *got = p->output + (PROBE_BURST + p->candidate) * p->channels;
for (i = 0; i < count; i++)
{
double delta = got[i] - (double)ref[i] / 32768.0;
error += delta * delta;
}
if (error < p->best)
{
p->second = p->best;
p->best = error;
p->delay = p->candidate;
}
else if (error < p->second)
p->second = error;
}
if (p->candidate <= PROBE_DELAY)
return 1;
if (p->best > p->energy * 0.02 || p->second <= p->best * 2)
{
p->failed = true;
return -1;
}
p->ready = true;
return 2;
}
bool record_aac_probe_delay(const record_aac_probe_t *p, unsigned *delay)
{
if (!p || !delay || !p->ready || p->failed)
return false;
*delay = p->delay;
return true;
}
void record_aac_probe_free(record_aac_probe_t *p)
{
if (!p)
return;
if (p->decoder)
raac_close(p->decoder);
free(p);
}
#else
record_aac_probe_t *record_aac_probe_new(unsigned rate, unsigned channels)
{
(void)rate;
(void)channels;
return NULL;
}
const int16_t *record_aac_probe_input(record_aac_probe_t *p, size_t at, size_t *n)
{
(void)p;
(void)at;
if (n)
*n = 0;
return NULL;
}
bool record_aac_probe_config(record_aac_probe_t *p, const void *asc, size_t n)
{
(void)p;
(void)asc;
(void)n;
return false;
}
bool record_aac_probe_packet(record_aac_probe_t *p, const void *data, size_t n)
{
(void)p;
(void)data;
(void)n;
return false;
}
int record_aac_probe_step(record_aac_probe_t *p)
{
(void)p;
return -1;
}
bool record_aac_probe_delay(const record_aac_probe_t *p, unsigned *delay)
{
(void)p;
(void)delay;
return false;
}
void record_aac_probe_free(record_aac_probe_t *p)
{
(void)p;
}
#endif
+28
View File
@@ -0,0 +1,28 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier:
* GPL-3.0-or-later */
#ifndef RECORD_AAC_PROBE_H
#define RECORD_AAC_PROBE_H
#include <stdint.h>
#include <stddef.h>
#include <boolean.h>
#include <retro_common_api.h>
RETRO_BEGIN_DECLS
#define RECORD_AAC_PROBE_FRAMES 8192u
typedef struct record_aac_probe record_aac_probe_t;
/* Recording I/O owner only. The encoded prefix must remain in the file for
* decoder dependencies, then be excluded by its presentation edit. */
record_aac_probe_t *record_aac_probe_new(unsigned rate, unsigned channels);
const int16_t *record_aac_probe_input(record_aac_probe_t *probe,
size_t offset, size_t *frames);
bool record_aac_probe_config(record_aac_probe_t *probe,
const void *asc, size_t bytes);
/* One borrowed AAC packet; decode only the bounded prefix. */
bool record_aac_probe_packet(record_aac_probe_t *probe,
const void *data, size_t bytes);
/* At most16 delay candidates per call. -1 failure,0 waiting for packets,
* 1 more matching work,2 ready. No I/O, waits or allocation. */
int record_aac_probe_step(record_aac_probe_t *probe);
bool record_aac_probe_delay(const record_aac_probe_t *probe, unsigned *delay);
void record_aac_probe_free(record_aac_probe_t *probe);
RETRO_END_DECLS
#endif
+191
View File
@@ -0,0 +1,191 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier:
* GPL-3.0-or-later */
#include "record_android_capture_config.h"
#if defined(__ANDROID__) || defined(RECORD_ANDROID_CAPTURE_CONFIG_TEST)
#include "../gfx/video_driver.h"
#include <stdlib.h>
#include <string.h>
#ifdef _WIN32
#include <io.h>
#define record_acconfig_close _close
#else
#include <unistd.h>
#define record_acconfig_close close
#endif
struct record_android_capture_config
{
record_android_capture_pool_config_t pool;
VkPhysicalDeviceMemoryProperties memory;
PFN_vkQueueSubmit submit;
};
static VkResult record_acconfig_submit(void *context, VkQueue queue,
uint32_t count, const VkSubmitInfo *submits, VkFence fence)
{
record_android_capture_config_t *c
= (record_android_capture_config_t *)context;
if (!c || queue != c->pool.queue || count != 1 || !submits)
return VK_ERROR_INITIALIZATION_FAILED;
return c->submit(queue, count, submits, fence);
}
static void record_acconfig_close_fd(void *context, int fd)
{
(void)context;
if (fd >= 0)
record_acconfig_close(fd);
}
record_android_capture_config_t *record_android_capture_config_new(
const struct video_device_info *device, unsigned width, unsigned height,
unsigned count, unsigned mode, bool flip_y)
{
record_android_capture_config_t *c;
PFN_vkGetInstanceProcAddr instance_proc;
PFN_vkGetDeviceProcAddr device_proc;
PFN_vkGetPhysicalDeviceMemoryProperties memory;
unsigned i;
if (!device || device->api != VIDEO_DEVICE_VULKAN || !device->instance
|| !device->physical_device || !device->device || !device->queue
|| !device->queue_lock || !device->get_instance_proc_addr
|| !(device->recording_caps & VIDEO_DEVICE_CAP_ANDROID_CAPTURE_INTEROP)
|| device->queue_family >= VK_QUEUE_FAMILY_FOREIGN_EXT || !width
|| !height || width > 16384 || height > 16384 || !count
|| count > RECORD_GPU_QUEUE_SLOTS || mode > 2)
return NULL;
instance_proc = (PFN_vkGetInstanceProcAddr)device->get_instance_proc_addr;
device_proc = (PFN_vkGetDeviceProcAddr)instance_proc(
(VkInstance)device->instance, "vkGetDeviceProcAddr");
memory = (PFN_vkGetPhysicalDeviceMemoryProperties)instance_proc(
(VkInstance)device->instance, "vkGetPhysicalDeviceMemoryProperties");
if (!device_proc || !memory)
return NULL;
c = (record_android_capture_config_t *)calloc(1, sizeof(*c));
if (!c)
return NULL;
c->pool.physical = (VkPhysicalDevice)device->physical_device;
c->pool.device = (VkDevice)device->device;
c->pool.queue = (VkQueue)device->queue;
c->pool.family = device->queue_family;
c->pool.memory = &c->memory;
c->pool.gpu.width = width;
c->pool.gpu.height = height;
c->pool.gpu.count = count;
c->pool.gpu.mode = mode;
c->pool.gpu.flip_y = flip_y;
c->pool.images.query
= (PFN_vkGetPhysicalDeviceImageFormatProperties2)instance_proc(
(VkInstance)device->instance,
"vkGetPhysicalDeviceImageFormatProperties2");
if (!c->pool.images.query)
c->pool.images.query
= (PFN_vkGetPhysicalDeviceImageFormatProperties2)instance_proc(
(VkInstance)device->instance,
"vkGetPhysicalDeviceImageFormatProperties2KHR");
if (!c->pool.images.query)
goto failed;
#define RECORD_ACCONFIG_LOAD(target, type, name) \
do \
{ \
target = (type)device_proc(c->pool.device, name); \
if (!target) \
goto failed; \
} while (0)
RECORD_ACCONFIG_LOAD(c->pool.images.properties,
PFN_vkGetAndroidHardwareBufferPropertiesANDROID,
"vkGetAndroidHardwareBufferPropertiesANDROID");
RECORD_ACCONFIG_LOAD(
c->pool.images.create, PFN_vkCreateImage, "vkCreateImage");
RECORD_ACCONFIG_LOAD(
c->pool.images.destroy, PFN_vkDestroyImage, "vkDestroyImage");
RECORD_ACCONFIG_LOAD(
c->pool.images.allocate, PFN_vkAllocateMemory, "vkAllocateMemory");
RECORD_ACCONFIG_LOAD(
c->pool.images.free_memory, PFN_vkFreeMemory, "vkFreeMemory");
RECORD_ACCONFIG_LOAD(
c->pool.images.bind, PFN_vkBindImageMemory, "vkBindImageMemory");
RECORD_ACCONFIG_LOAD(c->pool.submit.create_pool, PFN_vkCreateCommandPool,
"vkCreateCommandPool");
RECORD_ACCONFIG_LOAD(c->pool.submit.destroy_pool, PFN_vkDestroyCommandPool,
"vkDestroyCommandPool");
RECORD_ACCONFIG_LOAD(c->pool.submit.allocate_commands,
PFN_vkAllocateCommandBuffers, "vkAllocateCommandBuffers");
RECORD_ACCONFIG_LOAD(
c->pool.submit.create_fence, PFN_vkCreateFence, "vkCreateFence");
RECORD_ACCONFIG_LOAD(
c->pool.submit.destroy_fence, PFN_vkDestroyFence, "vkDestroyFence");
RECORD_ACCONFIG_LOAD(c->pool.submit.create_semaphore, PFN_vkCreateSemaphore,
"vkCreateSemaphore");
RECORD_ACCONFIG_LOAD(c->pool.submit.destroy_semaphore,
PFN_vkDestroySemaphore, "vkDestroySemaphore");
RECORD_ACCONFIG_LOAD(c->pool.submit.reset_command, PFN_vkResetCommandBuffer,
"vkResetCommandBuffer");
RECORD_ACCONFIG_LOAD(c->pool.submit.begin, PFN_vkBeginCommandBuffer,
"vkBeginCommandBuffer");
RECORD_ACCONFIG_LOAD(
c->pool.submit.end, PFN_vkEndCommandBuffer, "vkEndCommandBuffer");
RECORD_ACCONFIG_LOAD(c->pool.submit.barrier, PFN_vkCmdPipelineBarrier,
"vkCmdPipelineBarrier");
RECORD_ACCONFIG_LOAD(
c->pool.submit.copy, PFN_vkCmdCopyImage, "vkCmdCopyImage");
RECORD_ACCONFIG_LOAD(
c->pool.submit.reset_fences, PFN_vkResetFences, "vkResetFences");
RECORD_ACCONFIG_LOAD(
c->pool.submit.fence_status, PFN_vkGetFenceStatus, "vkGetFenceStatus");
RECORD_ACCONFIG_LOAD(c->pool.submit.export_fd, PFN_vkGetSemaphoreFdKHR,
"vkGetSemaphoreFdKHR");
RECORD_ACCONFIG_LOAD(c->submit, PFN_vkQueueSubmit, "vkQueueSubmit");
#undef RECORD_ACCONFIG_LOAD
memory(c->pool.physical, &c->memory);
if (!c->memory.memoryTypeCount
|| c->memory.memoryTypeCount > VK_MAX_MEMORY_TYPES
|| !c->memory.memoryHeapCount
|| c->memory.memoryHeapCount > VK_MAX_MEMORY_HEAPS)
goto failed;
for (i = 0; i < c->memory.memoryTypeCount; i++)
if (c->memory.memoryTypes[i].heapIndex >= c->memory.memoryHeapCount)
goto failed;
c->pool.submit.submit = record_acconfig_submit;
c->pool.submit.close_fd = record_acconfig_close_fd;
c->pool.submit.context = c;
return c;
failed:
free(c);
return NULL;
}
const record_android_capture_pool_config_t *record_android_capture_config_pool(
const record_android_capture_config_t *c)
{
return c ? &c->pool : NULL;
}
void record_android_capture_config_free(record_android_capture_config_t *c)
{
free(c);
}
#else
record_android_capture_config_t *record_android_capture_config_new(
const struct video_device_info *device, unsigned width, unsigned height,
unsigned count, unsigned mode, bool flip_y)
{
(void)device;
(void)width;
(void)height;
(void)count;
(void)mode;
(void)flip_y;
return NULL;
}
const record_android_capture_pool_config_t *record_android_capture_config_pool(
const record_android_capture_config_t *c)
{
(void)c;
return NULL;
}
void record_android_capture_config_free(record_android_capture_config_t *c)
{
(void)c;
}
#endif
+26
View File
@@ -0,0 +1,26 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier:
* GPL-3.0-or-later */
#ifndef RECORD_ANDROID_CAPTURE_CONFIG_H
#define RECORD_ANDROID_CAPTURE_CONFIG_H
#include "record_android_capture_pool.h"
RETRO_BEGIN_DECLS
struct video_device_info;
typedef struct record_android_capture_config record_android_capture_config_t;
/* Cold controller. Device info must publish enabled AHB/FOREIGN/SYNC_FD
* prerequisites, not merely extension names or procedure addresses. Owns stable
* operation context and memory properties; allocates no Vulkan/GLES resources.
* Keep original driver alive through request creation acknowledgment. */
record_android_capture_config_t *record_android_capture_config_new(
const struct video_device_info *device, unsigned width, unsigned height,
unsigned count, unsigned mode, bool flip_y);
/* Borrowed through request collection. The request copies the pool description
* but its submit/close callbacks still refer to this stable owner. Submission
* must already hold the current driver's existing queue serialization. */
const record_android_capture_pool_config_t *record_android_capture_config_pool(
const record_android_capture_config_t *config);
/* Only after the matching request and every source lease are collected.
* No native calls, even when the device/library has already been destroyed. */
void record_android_capture_config_free(
record_android_capture_config_t *config);
RETRO_END_DECLS
#endif
+81
View File
@@ -0,0 +1,81 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier:
* GPL-3.0-or-later */
#include "record_android_capture_device.h"
#include <stddef.h>
static record_android_capture_device_t *record_acdevice_owners;
bool record_android_capture_device_attach(
record_android_capture_device_t *owner)
{
record_android_capture_device_t *p;
if (!owner || !owner->context || !owner->device || !owner->prepare
|| !owner->destroyed || owner->prepared)
return false;
for (p = record_acdevice_owners; p; p = p->next)
if (p == owner)
return false;
owner->next = record_acdevice_owners;
record_acdevice_owners = owner;
return true;
}
void record_android_capture_device_detach(
record_android_capture_device_t *owner)
{
record_android_capture_device_t **link = &record_acdevice_owners;
if (!owner)
return;
while (*link && *link != owner)
link = &(*link)->next;
if (*link)
{
*link = owner->next;
owner->next = NULL;
}
}
bool record_android_capture_device_pending(VkDevice device)
{
record_android_capture_device_t *p;
if (!device)
return false;
for (p = record_acdevice_owners; p; p = p->next)
if (p->device == device)
return true;
return false;
}
bool record_android_capture_device_prepare(VkDevice device, bool queue_quiesced)
{
record_android_capture_device_t *p;
bool ready = true;
if (!device)
return false;
for (p = record_acdevice_owners; p; p = p->next)
if (p->device == device && !p->prepared)
{
if (p->prepare(p->context, queue_quiesced))
p->prepared = true;
else
ready = false;
}
return ready;
}
bool record_android_capture_device_destroyed(VkDevice device)
{
record_android_capture_device_t *p;
if (!device)
return false;
for (p = record_acdevice_owners; p; p = p->next)
if (p->device == device && !p->prepared)
return false;
for (p = record_acdevice_owners; p; p = p->next)
if (p->device == device)
{
p->device = VK_NULL_HANDLE;
p->destroyed(p->context);
}
return true;
}
+40
View File
@@ -0,0 +1,40 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier:
* GPL-3.0-or-later */
#ifndef RECORD_ANDROID_CAPTURE_DEVICE_H
#define RECORD_ANDROID_CAPTURE_DEVICE_H
#include <vulkan/vulkan_core.h>
#include <boolean.h>
#include <retro_common_api.h>
RETRO_BEGIN_DECLS
/* Intrusive cold-owner registration. All operations use the serialized render
* owner, including construction, collection and context teardown. Workers never
* access this list. Storage and context survive actual device destruction and
* retained GLES reads; registration requires no allocation or frame work.
* Callbacks cannot mutate the list or free their registration storage. */
typedef struct record_android_capture_device
{
struct record_android_capture_device *next;
void *context;
bool (*prepare)(void *context, bool queue_quiesced);
void (*destroyed)(void *context);
VkDevice device;
bool prepared;
} record_android_capture_device_t;
bool record_android_capture_device_attach(
record_android_capture_device_t *owner);
/* Only after all owned Vulkan resources have retired, or actual device death.
* The node must stay registered through partial construction and cancellation.
*/
void record_android_capture_device_detach(
record_android_capture_device_t *owner);
bool record_android_capture_device_pending(VkDevice device);
/* Renderer quiescent. queue_quiesced is proof from the existing device drain,
* never inferred from cancellation. False retains device and every owner. */
bool record_android_capture_device_prepare(
VkDevice device, bool queue_quiesced);
/* Only after vkDestroyDevice. Refuses notification unless every matching owner
* was prepared. Each callback occurs once; retained CPU/GLES owners stay listed
* with a null device until their eventual collection. */
bool record_android_capture_device_destroyed(VkDevice device);
RETRO_END_DECLS
#endif
+172
View File
@@ -0,0 +1,172 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier:
* GPL-3.0-or-later */
#include "record_android_capture_egl.h"
#if defined(__ANDROID__) || defined(RECORD_ANDROID_CAPTURE_EGL_TEST)
#include <stdlib.h>
struct record_android_capture_egl
{
record_gpu_mediacodec_config_t gpu;
dylib_t egl, gles;
PFNEGLGETPROCADDRESSPROC address;
PFNEGLTERMINATEPROC terminate;
bool initialized, failed;
};
static function_t record_ace_resolve(void *context, const char *name)
{
record_android_capture_egl_t *e = (record_android_capture_egl_t *)context;
function_t proc = dylib_proc(name[0] == 'e' ? e->egl : e->gles, name);
if (!proc && e->address)
proc = (function_t)e->address(name);
return proc;
}
bool record_android_capture_egl_free(record_android_capture_egl_t *e)
{
if (!e)
return true;
if (e->failed)
return false;
if (e->initialized)
{
e->initialized = false;
if (!e->terminate(e->gpu.display))
{
e->failed = true;
return false;
}
}
if (e->gles)
dylib_close(e->gles);
if (e->egl)
dylib_close(e->egl);
free(e);
return true;
}
static bool record_ace_choose(
record_android_capture_egl_t *e, PFNEGLCHOOSECONFIGPROC choose)
{
static const EGLint tokens[]
= {EGL_RED_SIZE, EGL_GREEN_SIZE, EGL_BLUE_SIZE, EGL_ALPHA_SIZE};
EGLint attrs[] = {EGL_SURFACE_TYPE, EGL_WINDOW_BIT, EGL_RENDERABLE_TYPE,
EGL_OPENGL_ES3_BIT_KHR, EGL_RECORDABLE_ANDROID, EGL_TRUE, EGL_RED_SIZE,
8, EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8, EGL_DEPTH_SIZE, 0,
EGL_STENCIL_SIZE, 0, EGL_SAMPLE_BUFFERS, 0, EGL_NONE};
EGLint count = 0, written = 0, i, j, value, bits = e->gpu.mode ? 10 : 8;
EGLConfig *configs;
bool found = false;
attrs[7] = attrs[9] = attrs[11] = bits;
if (!choose(e->gpu.display, attrs, NULL, 0, &count) || count <= 0
|| count > 4096)
return false;
configs = (EGLConfig *)calloc((size_t)count, sizeof(*configs));
if (!configs)
return false;
if (choose(e->gpu.display, attrs, configs, count, &written) && written > 0
&& written <= count)
for (i = 0; i < written; i++)
{
if (!configs[i])
continue;
for (j = 0; j < 4; j++)
{
if (!e->gpu.surface.config_attrib(
e->gpu.display, configs[i], tokens[j], &value))
break;
if (j < 3 ? value != bits
: (value != 0 && value != (bits == 10 ? 2 : 8)))
break;
}
if (j == 4)
{
e->gpu.config = configs[i];
found = true;
break;
}
}
free(configs);
return found;
}
bool record_android_capture_egl_new(
unsigned mode, record_android_capture_egl_t **output)
{
record_android_capture_egl_t *e;
PFNEGLGETDISPLAYPROC display;
PFNEGLINITIALIZEPROC initialize;
PFNEGLCHOOSECONFIGPROC choose;
if (!output)
return false;
*output = NULL;
if (mode > 2)
return false;
e = (record_android_capture_egl_t *)calloc(1, sizeof(*e));
if (!e)
return false;
e->gpu.mode = mode;
e->egl = dylib_load("libEGL.so");
e->gles = dylib_load("libGLESv2.so");
if (!e->egl || !e->gles)
goto error;
e->address
= (PFNEGLGETPROCADDRESSPROC)dylib_proc(e->egl, "eglGetProcAddress");
e->terminate = (PFNEGLTERMINATEPROC)dylib_proc(e->egl, "eglTerminate");
display = (PFNEGLGETDISPLAYPROC)dylib_proc(e->egl, "eglGetDisplay");
initialize = (PFNEGLINITIALIZEPROC)dylib_proc(e->egl, "eglInitialize");
choose = (PFNEGLCHOOSECONFIGPROC)dylib_proc(e->egl, "eglChooseConfig");
if (!e->address || !e->terminate || !display || !initialize || !choose
|| !record_android_capture_load_ops(&e->gpu, record_ace_resolve, e))
goto error;
e->gpu.display = display(EGL_DEFAULT_DISPLAY);
if (e->gpu.display == EGL_NO_DISPLAY
|| !initialize(e->gpu.display, NULL, NULL))
goto error;
e->initialized = true;
if (!record_ace_choose(e, choose))
goto error;
*output = e;
return true;
error:
if (!record_android_capture_egl_free(e))
*output = e;
return false;
}
bool record_android_capture_egl_configure(const record_android_capture_egl_t *e,
record_gpu_mediacodec_config_t *config)
{
if (!e || !config || !e->initialized || e->failed || !e->gpu.config)
return false;
config->surface = e->gpu.surface;
config->capture = e->gpu.capture;
config->draw = e->gpu.draw;
config->display = e->gpu.display;
config->config = e->gpu.config;
config->share = EGL_NO_CONTEXT;
config->mode = e->gpu.mode;
return true;
}
#else
bool record_android_capture_egl_new(
unsigned mode, record_android_capture_egl_t **output)
{
(void)mode;
if (output)
*output = NULL;
return false;
}
bool record_android_capture_egl_configure(
const record_android_capture_egl_t *egl,
record_gpu_mediacodec_config_t *config)
{
(void)egl;
(void)config;
return false;
}
bool record_android_capture_egl_free(record_android_capture_egl_t *egl)
{
return !egl;
}
#endif
+22
View File
@@ -0,0 +1,22 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier:
* GPL-3.0-or-later */
#ifndef RECORD_ANDROID_CAPTURE_EGL_H
#define RECORD_ANDROID_CAPTURE_EGL_H
#include "record_android_capture_ops.h"
RETRO_BEGIN_DECLS
typedef struct record_android_capture_egl record_android_capture_egl_t;
/* Cold Android controller. Uses the system EGL display's Android reference
* counting. False may retain an output if native teardown failed. */
bool record_android_capture_egl_new(
unsigned mode, record_android_capture_egl_t **output);
/* Copies borrowed display/config/operations and mode into the job config.
* Dimensions, buffers, count and orientation are preserved; no share group. */
bool record_android_capture_egl_configure(
const record_android_capture_egl_t *egl,
record_gpu_mediacodec_config_t *config);
/* All recording contexts, surfaces, imports and workers must be collected.
* Failed termination retains libraries and is not retried: Android may have
* already consumed this owner's display reference. */
bool record_android_capture_egl_free(record_android_capture_egl_t *egl);
RETRO_END_DECLS
#endif
+41 -1
View File
@@ -16,9 +16,10 @@ struct record_android_capture_gl
{
record_android_capture_gl_ops_t ops;
record_capture_gl_slot_t slots[16];
GLsync recovery;
EGLDisplay display;
unsigned count;
bool failed;
bool failed, recovered;
};
static bool record_capture_gl_extension(const char *list, const char *name)
{
@@ -36,6 +37,8 @@ bool record_android_capture_gl_free(record_android_capture_gl_t *c)
unsigned i;
if (!c)
return true;
if (c->recovery)
return false;
for (i = 0; i < c->count; i++)
if (c->slots[i].reading || c->slots[i].done)
return false;
@@ -228,6 +231,41 @@ int record_android_capture_gl_poll(
s->ended = false;
return 1;
}
int record_android_capture_gl_recover_step(record_android_capture_gl_t *c)
{
unsigned i;
GLenum status;
if (!c)
return -1;
c->failed = true;
if (c->recovered)
return 1;
if (!c->recovery)
{
c->recovery = c->ops.fence(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
if (!c->recovery)
return -1;
c->ops.flush();
return 0;
}
status = c->ops.poll(c->recovery, 0, 0);
if (status == GL_TIMEOUT_EXPIRED)
return 0;
if (status != GL_ALREADY_SIGNALED && status != GL_CONDITION_SATISFIED)
return -1;
c->ops.delete_fence(c->recovery);
c->recovery = NULL;
for (i = 0; i < c->count; i++)
{
record_capture_gl_slot_t *s = &c->slots[i];
if (s->done)
c->ops.delete_fence(s->done);
s->done = NULL;
s->reading = s->ended = false;
}
c->recovered = true;
return 1;
}
void record_android_capture_gl_after_context_retired(
record_android_capture_gl_t *c)
{
@@ -235,6 +273,8 @@ void record_android_capture_gl_after_context_retired(
if (!c)
return;
c->failed = true;
c->recovery = NULL;
c->recovered = true;
for (i = 0; i < c->count; i++)
{
c->slots[i].texture = 0;
+11 -3
View File
@@ -51,14 +51,22 @@ bool record_android_capture_gl_begin(record_android_capture_gl_t *capture,
bool record_android_capture_gl_end(
record_android_capture_gl_t *capture, unsigned slot);
/* One zero-timeout query. 1 permits producer reuse, 0 pending, -1 error.
* A failed end cannot prove retirement: retain the slot until context loss. */
* A failed end retains the slot until terminal recovery or independent
* context retirement proves completion. */
int record_android_capture_gl_poll(
record_android_capture_gl_t *capture, unsigned slot);
/* Terminal worker recovery, same context current, no more source draws.
* A fresh fence covers every preceding command, including failed slot fences.
* One creation/flush or one zero-timeout query per call. 1 proves all reads
* retired, 0 pending, -1 retains unproven reads. No frame is drawn again. */
int record_android_capture_gl_recover_step(
record_android_capture_gl_t *capture);
/* Worker context current, every begun slot retired. Refuses otherwise.
* Native image destruction failures retain remaining ownership for retry. */
bool record_android_capture_gl_free(record_android_capture_gl_t *capture);
/* ONLY after context destruction AND independent proof all GPU reads retired
* (or device loss). Context destruction alone is not completion proof. No GL
/* ONLY after context/owned GL namespace destruction AND independent proof all
* GPU reads retired (or device loss). Shared objects must also be retired.
* Context destruction alone is not completion proof. No GL
* calls follow; explicit EGL images/syncs still require free. */
void record_android_capture_gl_after_context_retired(
record_android_capture_gl_t *capture);
+247
View File
@@ -0,0 +1,247 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier:
* GPL-3.0-or-later */
#include <string.h>
#include "record_android_capture_ops.h"
bool record_android_capture_load_ops(record_gpu_mediacodec_config_t *config,
record_android_capture_resolve_t resolve, void *context)
{
record_gpu_mediacodec_config_t loaded;
function_t proc;
if (!config)
return false;
memset(&config->surface, 0, sizeof(config->surface));
memset(&config->capture, 0, sizeof(config->capture));
memset(&config->draw, 0, sizeof(config->draw));
if (!resolve)
return false;
memset(&loaded, 0, sizeof(loaded));
proc = resolve(context, "eglQueryString");
if (!proc)
return false;
loaded.surface.query_string = (PFNEGLQUERYSTRINGPROC)proc;
proc = resolve(context, "eglGetConfigAttrib");
if (!proc)
return false;
loaded.surface.config_attrib = (PFNEGLGETCONFIGATTRIBPROC)proc;
proc = resolve(context, "eglCreateContext");
if (!proc)
return false;
loaded.surface.create_context = (PFNEGLCREATECONTEXTPROC)proc;
proc = resolve(context, "eglCreateWindowSurface");
if (!proc)
return false;
loaded.surface.create_surface = (PFNEGLCREATEWINDOWSURFACEPROC)proc;
proc = resolve(context, "eglDestroyContext");
if (!proc)
return false;
loaded.surface.destroy_context = (PFNEGLDESTROYCONTEXTPROC)proc;
proc = resolve(context, "eglDestroySurface");
if (!proc)
return false;
loaded.surface.destroy_surface = (PFNEGLDESTROYSURFACEPROC)proc;
proc = resolve(context, "eglQuerySurface");
if (!proc)
return false;
loaded.surface.query_surface = (PFNEGLQUERYSURFACEPROC)proc;
proc = resolve(context, "eglQueryAPI");
if (!proc)
return false;
loaded.surface.query_api = (PFNEGLQUERYAPIPROC)proc;
proc = resolve(context, "eglGetCurrentContext");
if (!proc)
return false;
loaded.surface.current_context = (PFNEGLGETCURRENTCONTEXTPROC)proc;
proc = resolve(context, "eglGetCurrentDisplay");
if (!proc)
return false;
loaded.surface.current_display = (PFNEGLGETCURRENTDISPLAYPROC)proc;
proc = resolve(context, "eglGetCurrentSurface");
if (!proc)
return false;
loaded.surface.current_surface = (PFNEGLGETCURRENTSURFACEPROC)proc;
proc = resolve(context, "eglMakeCurrent");
if (!proc)
return false;
loaded.surface.make_current = (PFNEGLMAKECURRENTPROC)proc;
proc = resolve(context, "eglPresentationTimeANDROID");
if (!proc)
return false;
loaded.surface.presentation_time = (PFNEGLPRESENTATIONTIMEANDROIDPROC)proc;
proc = resolve(context, "eglSwapBuffers");
if (!proc)
return false;
loaded.surface.swap = (PFNEGLSWAPBUFFERSPROC)proc;
proc = resolve(context, "eglQueryString");
if (!proc)
return false;
loaded.capture.query_string = (PFNEGLQUERYSTRINGPROC)proc;
proc = resolve(context, "eglGetNativeClientBufferANDROID");
if (!proc)
return false;
loaded.capture.native_buffer = (PFNEGLGETNATIVECLIENTBUFFERANDROIDPROC)proc;
proc = resolve(context, "eglCreateImageKHR");
if (!proc)
return false;
loaded.capture.create_image = (PFNEGLCREATEIMAGEKHRPROC)proc;
proc = resolve(context, "eglDestroyImageKHR");
if (!proc)
return false;
loaded.capture.destroy_image = (PFNEGLDESTROYIMAGEKHRPROC)proc;
proc = resolve(context, "eglCreateSyncKHR");
if (!proc)
return false;
loaded.capture.create_sync = (PFNEGLCREATESYNCKHRPROC)proc;
proc = resolve(context, "eglDestroySyncKHR");
if (!proc)
return false;
loaded.capture.destroy_sync = (PFNEGLDESTROYSYNCKHRPROC)proc;
proc = resolve(context, "eglWaitSyncKHR");
if (!proc)
return false;
loaded.capture.wait_sync = (PFNEGLWAITSYNCKHRPROC)proc;
proc = resolve(context, "glGenTextures");
if (!proc)
return false;
loaded.capture.gen_textures = (PFNGLGENTEXTURESPROC)proc;
proc = resolve(context, "glDeleteTextures");
if (!proc)
return false;
loaded.capture.delete_textures = (PFNGLDELETETEXTURESPROC)proc;
proc = resolve(context, "glBindTexture");
if (!proc)
return false;
loaded.capture.bind_texture = (PFNGLBINDTEXTUREPROC)proc;
proc = resolve(context, "glTexParameteri");
if (!proc)
return false;
loaded.capture.texture_parameter = (PFNGLTEXPARAMETERIPROC)proc;
proc = resolve(context, "glEGLImageTargetTexture2DOES");
if (!proc)
return false;
loaded.capture.image_target = (record_capture_gl_image_target_t)proc;
proc = resolve(context, "glGetError");
if (!proc)
return false;
loaded.capture.error = (PFNGLGETERRORPROC)proc;
proc = resolve(context, "glGetString");
if (!proc)
return false;
loaded.capture.string = (PFNGLGETSTRINGPROC)proc;
proc = resolve(context, "glFenceSync");
if (!proc)
return false;
loaded.capture.fence = (PFNGLFENCESYNCPROC)proc;
proc = resolve(context, "glClientWaitSync");
if (!proc)
return false;
loaded.capture.poll = (PFNGLCLIENTWAITSYNCPROC)proc;
proc = resolve(context, "glDeleteSync");
if (!proc)
return false;
loaded.capture.delete_fence = (PFNGLDELETESYNCPROC)proc;
proc = resolve(context, "glFlush");
if (!proc)
return false;
loaded.capture.flush = (PFNGLFLUSHPROC)proc;
proc = resolve(context, "glCreateShader");
if (!proc)
return false;
loaded.draw.create_shader = (PFNGLCREATESHADERPROC)proc;
proc = resolve(context, "glShaderSource");
if (!proc)
return false;
loaded.draw.shader_source = (PFNGLSHADERSOURCEPROC)proc;
proc = resolve(context, "glCompileShader");
if (!proc)
return false;
loaded.draw.compile_shader = (PFNGLCOMPILESHADERPROC)proc;
proc = resolve(context, "glGetShaderiv");
if (!proc)
return false;
loaded.draw.shader_status = (PFNGLGETSHADERIVPROC)proc;
proc = resolve(context, "glDeleteShader");
if (!proc)
return false;
loaded.draw.delete_shader = (PFNGLDELETESHADERPROC)proc;
proc = resolve(context, "glCreateProgram");
if (!proc)
return false;
loaded.draw.create_program = (PFNGLCREATEPROGRAMPROC)proc;
proc = resolve(context, "glAttachShader");
if (!proc)
return false;
loaded.draw.attach_shader = (PFNGLATTACHSHADERPROC)proc;
proc = resolve(context, "glLinkProgram");
if (!proc)
return false;
loaded.draw.link_program = (PFNGLLINKPROGRAMPROC)proc;
proc = resolve(context, "glGetProgramiv");
if (!proc)
return false;
loaded.draw.program_status = (PFNGLGETPROGRAMIVPROC)proc;
proc = resolve(context, "glDeleteProgram");
if (!proc)
return false;
loaded.draw.delete_program = (PFNGLDELETEPROGRAMPROC)proc;
proc = resolve(context, "glGetUniformLocation");
if (!proc)
return false;
loaded.draw.uniform_location = (PFNGLGETUNIFORMLOCATIONPROC)proc;
proc = resolve(context, "glGenVertexArrays");
if (!proc)
return false;
loaded.draw.gen_arrays = (PFNGLGENVERTEXARRAYSPROC)proc;
proc = resolve(context, "glDeleteVertexArrays");
if (!proc)
return false;
loaded.draw.delete_arrays = (PFNGLDELETEVERTEXARRAYSPROC)proc;
proc = resolve(context, "glBindVertexArray");
if (!proc)
return false;
loaded.draw.bind_array = (PFNGLBINDVERTEXARRAYPROC)proc;
proc = resolve(context, "glUseProgram");
if (!proc)
return false;
loaded.draw.use_program = (PFNGLUSEPROGRAMPROC)proc;
proc = resolve(context, "glUniform1i");
if (!proc)
return false;
loaded.draw.uniform = (PFNGLUNIFORM1IPROC)proc;
proc = resolve(context, "glActiveTexture");
if (!proc)
return false;
loaded.draw.active_texture = (PFNGLACTIVETEXTUREPROC)proc;
proc = resolve(context, "glBindTexture");
if (!proc)
return false;
loaded.draw.bind_texture = (PFNGLBINDTEXTUREPROC)proc;
proc = resolve(context, "glBindFramebuffer");
if (!proc)
return false;
loaded.draw.bind_framebuffer = (PFNGLBINDFRAMEBUFFERPROC)proc;
proc = resolve(context, "glViewport");
if (!proc)
return false;
loaded.draw.viewport = (PFNGLVIEWPORTPROC)proc;
proc = resolve(context, "glDisable");
if (!proc)
return false;
loaded.draw.disable = (PFNGLDISABLEPROC)proc;
proc = resolve(context, "glColorMask");
if (!proc)
return false;
loaded.draw.color_mask = (PFNGLCOLORMASKPROC)proc;
proc = resolve(context, "glDrawArrays");
if (!proc)
return false;
loaded.draw.draw = (PFNGLDRAWARRAYSPROC)proc;
proc = resolve(context, "glGetError");
if (!proc)
return false;
loaded.draw.error = (PFNGLGETERRORPROC)proc;
config->surface = loaded.surface;
config->capture = loaded.capture;
config->draw = loaded.draw;
return true;
}
+16
View File
@@ -0,0 +1,16 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier:
* GPL-3.0-or-later */
#ifndef RECORD_ANDROID_CAPTURE_OPS_H
#define RECORD_ANDROID_CAPTURE_OPS_H
#include <dynamic/dylib.h>
#include "record_gpu_dispatch_mediacodec.h"
RETRO_BEGIN_DECLS
typedef function_t (*record_android_capture_resolve_t)(
void *context, const char *name);
/* Cold setup. The resolver's libraries must outlive the recording job.
* Failure clears all operation tables, preserving the other configuration.
* Resolved addresses do not replace context/extension capability checks. */
bool record_android_capture_load_ops(record_gpu_mediacodec_config_t *config,
record_android_capture_resolve_t resolve, void *context);
RETRO_END_DECLS
#endif
+287
View File
@@ -0,0 +1,287 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier:
* GPL-3.0-or-later */
#include "record_android_capture_pipe.h"
#include <stdlib.h>
#include <limits.h>
struct record_android_capture_pipe
{
record_gpu_dispatch_mediacodec_t *gpu;
record_android_capture_submit_t *entries[RECORD_GPU_QUEUE_SLOTS];
void (*close_fd)(void *, int);
void *context;
record_gpu_frame_t frame;
uint64_t last_pts, last_end;
int64_t end_us;
unsigned count, slot, cursor;
uint32_t active, copied;
int fd;
bool reserved, pending, have_frame, cancelled, closed, retired, recorded;
};
record_android_capture_pipe_t *record_android_capture_pipe_new(
record_gpu_dispatch_mediacodec_t *gpu,
record_android_capture_submit_t *const *entries, unsigned count,
void (*close_fd)(void *, int), void *context)
{
record_android_capture_pipe_t *p;
unsigned i, j;
if (!gpu || !entries || !count || count > RECORD_GPU_QUEUE_SLOTS
|| !close_fd)
return NULL;
for (i = 0; i < count; i++)
{
if (!entries[i])
return NULL;
for (j = 0; j < i; j++)
if (entries[i] == entries[j])
return NULL;
}
p = (record_android_capture_pipe_t *)calloc(1, sizeof(*p));
if (!p)
return NULL;
p->gpu = gpu;
p->count = count;
p->close_fd = close_fd;
p->context = context;
p->fd = -1;
for (i = 0; i < count; i++)
p->entries[i] = entries[i];
return p;
}
void record_android_capture_pipe_cancel(record_android_capture_pipe_t *p)
{
if (!p)
return;
p->cancelled = true;
record_gpu_dispatch_mediacodec_cancel(p->gpu);
if (p->fd >= 0)
p->close_fd(p->context, p->fd);
p->fd = -1;
p->pending = false;
if (p->reserved && record_gpu_dispatch_mediacodec_cancel_slot(p->gpu))
p->reserved = false;
}
static int record_acpipe_push(record_android_capture_pipe_t *p,
const record_android_capture_source_t *source, uint64_t pts,
uint64_t duration, VkCommandBuffer command)
{
uint64_t maximum = (((uint64_t)1 << 63) - 1) / 1000;
int result;
if (!p || !source || p->cancelled || p->closed || !duration || pts > maximum
|| duration > maximum - pts || (p->have_frame && pts <= p->last_pts))
return -1;
if (p->pending || p->recorded)
return 0;
if (!p->reserved)
{
result = record_gpu_dispatch_mediacodec_acquire(p->gpu, &p->slot);
if (result != RECORD_GPU_QUEUE_OK)
return result == RECORD_GPU_QUEUE_BUSY ? 0 : -1;
p->reserved = true;
if (p->slot >= p->count)
{
record_android_capture_pipe_cancel(p);
return -1;
}
}
if (p->active & ((uint32_t)1 << p->slot))
{
result = record_android_capture_submit_poll(p->entries[p->slot]);
if (result == 0)
return 0;
if (result < 0
|| !record_android_capture_submit_release(p->entries[p->slot]))
{
record_android_capture_pipe_cancel(p);
return -1;
}
p->active &= ~((uint32_t)1 << p->slot);
p->copied &= ~((uint32_t)1 << p->slot);
}
result = command
? record_android_capture_submit_record(
p->entries[p->slot], source, command)
: record_android_capture_submit_push(p->entries[p->slot], source);
if (result == 1 || result == -2)
{
p->active |= (uint32_t)1 << p->slot;
p->last_pts = pts;
p->last_end = pts + duration;
p->have_frame = true;
p->frame.timestamp = pts;
p->frame.duration = duration;
p->frame.slot = p->slot;
p->pending = true;
p->recorded = command != VK_NULL_HANDLE;
}
if (result < 0)
record_android_capture_pipe_cancel(p);
return result;
}
int record_android_capture_pipe_push(record_android_capture_pipe_t *p,
const record_android_capture_source_t *source, uint64_t pts,
uint64_t duration)
{
return record_acpipe_push(p, source, pts, duration, VK_NULL_HANDLE);
}
int record_android_capture_pipe_record(record_android_capture_pipe_t *p,
const record_android_capture_source_t *source, uint64_t pts,
uint64_t duration, VkCommandBuffer command)
{
if (!command)
return -1;
return record_acpipe_push(p, source, pts, duration, command);
}
int record_android_capture_pipe_commit(record_android_capture_pipe_t *p)
{
int result;
if (!p || !p->recorded)
return -1;
result = record_android_capture_submit_commit(p->entries[p->slot]);
p->recorded = false;
if (result < 0)
record_android_capture_pipe_cancel(p);
return result;
}
bool record_android_capture_pipe_abort_recorded(
record_android_capture_pipe_t *p)
{
if (!p || !p->recorded
|| !record_android_capture_submit_abort_recorded(p->entries[p->slot]))
return false;
p->active &= ~((uint32_t)1 << p->slot);
p->copied &= ~((uint32_t)1 << p->slot);
p->pending = p->recorded = false;
return true;
}
int record_android_capture_pipe_step(record_android_capture_pipe_t *p)
{
enum record_dispatch_mediacodec_state state;
int result;
unsigned slot;
if (!p)
return -1;
if (p->retired)
return 1;
state = record_gpu_dispatch_mediacodec_status(p->gpu, NULL, NULL);
if ((!p->closed && state == RECORD_MC_DISPATCH_DRAINING)
|| state == RECORD_MC_DISPATCH_FAILED)
record_android_capture_pipe_cancel(p);
if (p->pending && !p->cancelled)
{
if (p->recorded)
return 0;
if (p->fd < 0 && !p->frame.ready_value)
{
result = record_android_capture_submit_take_ready(
p->entries[p->slot], &p->fd);
if (result == 0)
{
result = record_android_capture_submit_poll(p->entries[p->slot]);
if (result == 0)
return 0;
if (result > 0)
result = record_android_capture_submit_take_ready(
p->entries[p->slot], &p->fd);
}
if (result != 1 || p->fd == INT_MAX)
{
record_android_capture_pipe_cancel(p);
return -1;
}
p->frame.ready_value = (uint64_t)(p->fd + 1);
}
if (!record_gpu_dispatch_mediacodec_submit(p->gpu, &p->frame))
{
record_android_capture_pipe_cancel(p);
return -1;
}
p->fd = -1;
p->frame.ready_value = 0;
p->pending = p->reserved = false;
return 0;
}
if (!p->closed
|| (state != RECORD_MC_DISPATCH_DONE
&& state != RECORD_MC_DISPATCH_FAILED))
{
if (!p->cancelled)
for (slot = 0; slot < p->count; slot++)
{
p->cursor = (p->cursor + 1) % p->count;
if (!(p->active & ~p->copied & ((uint32_t)1 << p->cursor)))
continue;
result = record_android_capture_submit_poll(p->entries[p->cursor]);
if (result < 0)
{
record_android_capture_pipe_cancel(p);
return -1;
}
if (result == 1)
p->copied |= (uint32_t)1 << p->cursor;
break;
}
return 0;
}
for (slot = 0; slot < p->count; slot++)
{
p->cursor = (p->cursor + 1) % p->count;
if (!(p->active & ((uint32_t)1 << p->cursor)))
continue;
result = record_android_capture_submit_discard(p->entries[p->cursor]);
if (result != 1)
return result;
p->active &= ~((uint32_t)1 << p->cursor);
if (p->slot == p->cursor)
p->recorded = false;
break;
}
p->retired = p->active == 0;
return p->retired ? 1 : 0;
}
bool record_android_capture_pipe_finish(
record_android_capture_pipe_t *p, int64_t end_us)
{
if (!p || end_us < 0 || (uint64_t)end_us < p->last_end)
return false;
if (p->closed)
return p->end_us == end_us;
if (p->pending)
return false;
if (p->reserved)
{
if (!record_gpu_dispatch_mediacodec_cancel_slot(p->gpu))
return false;
p->reserved = false;
}
if (!record_gpu_dispatch_mediacodec_finish(p->gpu, end_us))
return false;
p->end_us = end_us;
p->closed = true;
return true;
}
bool record_android_capture_pipe_sources_retired(
const record_android_capture_pipe_t *p)
{
return !p || !(p->active & ~p->copied);
}
bool record_android_capture_pipe_free(record_android_capture_pipe_t *p)
{
if (!p)
return true;
if (!p->retired || !p->closed || p->reserved || p->pending)
return false;
free(p);
return true;
}
+49
View File
@@ -0,0 +1,49 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier:
* GPL-3.0-or-later */
#ifndef RECORD_ANDROID_CAPTURE_PIPE_H
#define RECORD_ANDROID_CAPTURE_PIPE_H
#include "record_android_capture_submit.h"
#include "record_gpu_dispatch_mediacodec.h"
RETRO_BEGIN_DECLS
typedef struct record_android_capture_pipe record_android_capture_pipe_t;
/* Cold renderer setup. Borrows distinct entries and the GPU executor until
* free succeeds. Entry order must match the executor's fixed buffer slots. */
record_android_capture_pipe_t *record_android_capture_pipe_new(
record_gpu_dispatch_mediacodec_t *gpu,
record_android_capture_submit_t *const *entries, unsigned count,
void (*close_fd)(void *, int), void *context);
/* Renderer, outside render passes. 1 accepts source lease, 0 BUSY takes
* nothing, -1 takes nothing, -2 failed submission retains the source lease.
* One pending frame is serviced by step; never resubmit an accepted source. */
int record_android_capture_pipe_push(record_android_capture_pipe_t *pipe,
const record_android_capture_source_t *source, uint64_t pts,
uint64_t duration);
/* Same source/time contract, recording into a borrowed renderer command.
* Before queue acceptance only abort with never-execute proof is allowed.
* After acceptance call commit once, including when cancellation was requested.
* step never publishes an uncommitted frame. Aborted times remain consumed. */
int record_android_capture_pipe_record(record_android_capture_pipe_t *pipe,
const record_android_capture_source_t *source, uint64_t pts,
uint64_t duration, VkCommandBuffer command);
/* 0 retains busy completion; step retries completion without copying again. */
int record_android_capture_pipe_commit(record_android_capture_pipe_t *pipe);
bool record_android_capture_pipe_abort_recorded(
record_android_capture_pipe_t *pipe);
/* Renderer service. At most one copy-fence query; no CPU waits. 1 terminal
* GPU/copy ownership retired, 0 pending, -1 unproven retirement. */
int record_android_capture_pipe_step(record_android_capture_pipe_t *pipe);
/* Producer closure after final push. BUSY while a pending source is handed
* off; retry with the same end after servicing step. Cancellation drops
* unpublished descriptors but still requires explicit producer closure. */
bool record_android_capture_pipe_finish(
record_android_capture_pipe_t *pipe, int64_t end_us);
void record_android_capture_pipe_cancel(record_android_capture_pipe_t *pipe);
/* Renderer, after source production stops. Cached proof for original source
* leases only; capture buffers and the device still require joint retirement.
*/
bool record_android_capture_pipe_sources_retired(
const record_android_capture_pipe_t *pipe);
/* Renderer quiescent. Does not free borrowed executor, entries or pool. */
bool record_android_capture_pipe_free(record_android_capture_pipe_t *pipe);
RETRO_END_DECLS
#endif
+384
View File
@@ -0,0 +1,384 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier:
* GPL-3.0-or-later */
#include "record_android_capture_pool.h"
#include "record_android_capture_device.h"
#include <stdlib.h>
#include "../gfx/video_defines.h"
struct record_android_capture_pool
{
record_android_capture_device_t device_owner;
record_android_capture_images_t *images;
record_android_capture_submit_t *entries[RECORD_GPU_QUEUE_SLOTS];
record_gpu_dispatch_mediacodec_t *gpu;
record_android_capture_pipe_t *pipe;
int64_t end_us;
uint64_t renderer_last_offer, renderer_end;
unsigned count, dims, mode, renderer_duration, renderer_leases;
bool closed, preparing, prepared, destroyed, fail_on_drop;
bool renderer_bound, renderer_offer, renderer_recorded, renderer_stopping;
};
static bool record_acpool_terminal(const record_android_capture_pool_t *p)
{
enum record_dispatch_mediacodec_state state;
if (!p->gpu)
return true;
state = record_gpu_dispatch_mediacodec_status(p->gpu, NULL, NULL);
return state == RECORD_MC_DISPATCH_DONE
|| state == RECORD_MC_DISPATCH_FAILED;
}
bool record_android_capture_pool_free(record_android_capture_pool_t *p)
{
unsigned i;
if (!p)
return true;
if (!p->closed
|| (p->preparing && (!p->destroyed || !record_acpool_terminal(p))))
return false;
if (p->pipe)
{
if (record_android_capture_pipe_step(p->pipe) != 1
|| !record_android_capture_pipe_free(p->pipe))
return false;
p->pipe = NULL;
}
if (!record_acpool_terminal(p))
return false;
if (p->gpu)
{
if (!record_gpu_dispatch_mediacodec_free(p->gpu))
return false;
p->gpu = NULL;
}
for (i = 0; i < p->count; i++)
{
if (!record_android_capture_submit_free(p->entries[i]))
return false;
p->entries[i] = NULL;
}
if (p->renderer_leases)
return false;
record_android_capture_images_free(p->images);
record_android_capture_device_detach(&p->device_owner);
free(p);
return true;
}
bool record_android_capture_pool_closed(
const record_android_capture_pool_t *p)
{
return p && p->closed;
}
bool record_android_capture_pool_finish(
record_android_capture_pool_t *p, int64_t end_us)
{
if (!p || end_us < 0)
return false;
if (p->closed)
return p->end_us == end_us;
if (p->pipe)
{
if (!record_android_capture_pipe_finish(p->pipe, end_us))
return false;
}
else if (p->gpu && !record_gpu_dispatch_mediacodec_finish(p->gpu, end_us))
return false;
p->end_us = end_us;
p->closed = true;
return true;
}
void record_android_capture_pool_cancel(record_android_capture_pool_t *p)
{
if (!p)
return;
p->renderer_stopping = true;
if (p->pipe)
record_android_capture_pipe_cancel(p->pipe);
else if (p->gpu)
record_gpu_dispatch_mediacodec_cancel(p->gpu);
}
static bool record_acpool_device_prepare(void *context, bool queue_quiesced)
{
record_android_capture_pool_t *p = (record_android_capture_pool_t *)context;
p->preparing = true;
record_android_capture_pool_cancel(p);
if (!p->closed && !record_android_capture_pool_finish(p,
(int64_t)p->renderer_end))
return false;
return record_android_capture_pool_prepare_device_destroy(p, queue_quiesced);
}
static void record_acpool_device_destroyed(void *context)
{
record_android_capture_pool_after_device_destroy(
(record_android_capture_pool_t *)context);
}
bool record_android_capture_pool_new(record_dispatch_mediacodec_t *io,
const record_android_capture_pool_config_t *c,
record_android_capture_pool_t **out)
{
record_android_capture_pool_t *p;
record_gpu_mediacodec_config_t gpu;
VkFormat format;
unsigned i;
if (!out)
return false;
*out = NULL;
if (!io || !c || !c->physical || !c->device || !c->queue || !c->memory
|| !c->submit.close_fd || !c->gpu.count
|| c->gpu.count > RECORD_GPU_QUEUE_SLOTS || c->gpu.mode > 2)
return false;
p = (record_android_capture_pool_t *)calloc(1, sizeof(*p));
if (!p)
return false;
p->device_owner.context = p;
p->device_owner.device = c->device;
p->device_owner.prepare = record_acpool_device_prepare;
p->device_owner.destroyed = record_acpool_device_destroyed;
if (!record_android_capture_device_attach(&p->device_owner))
{
free(p);
return false;
}
p->count = c->gpu.count;
p->fail_on_drop = c->fail_on_drop;
p->dims = VIDEO_SCALE_PACK(c->gpu.width, c->gpu.height);
p->mode = c->gpu.mode;
format = c->gpu.mode == 2 ? VK_FORMAT_R16G16B16A16_SFLOAT
: c->gpu.mode == 1 ? VK_FORMAT_A2B10G10R10_UNORM_PACK32
: VK_FORMAT_R8G8B8A8_UNORM;
p->images = record_android_capture_images_new(c->physical, c->device,
&c->images, c->memory, p->count, c->gpu.width, c->gpu.height, format);
if (!p->images)
goto failed;
gpu = c->gpu;
for (i = 0; i < p->count; i++)
{
gpu.buffers[i] = record_android_capture_images_buffer(p->images, i);
p->entries[i] = record_android_capture_submit_new(c->device, c->queue,
c->family, record_android_capture_images_image(p->images, i),
format, gpu.width, gpu.height, &c->submit);
if (!gpu.buffers[i] || !p->entries[i])
goto failed;
}
p->gpu = record_gpu_dispatch_mediacodec_new(io, &gpu);
if (!p->gpu)
goto failed;
p->pipe = record_android_capture_pipe_new(
p->gpu, p->entries, p->count, c->submit.close_fd, c->submit.context);
if (!p->pipe)
goto failed;
*out = p;
return true;
failed:
record_android_capture_pool_cancel(p);
record_android_capture_pool_finish(p, 0);
if (!record_android_capture_pool_free(p))
*out = p;
return false;
}
int record_android_capture_pool_push(record_android_capture_pool_t *p,
const record_android_capture_source_t *source, uint64_t pts,
uint64_t duration)
{
int result;
if (!p || p->closed || p->preparing || !p->pipe)
return -1;
result = record_android_capture_pipe_push(p->pipe, source, pts, duration);
if (result == 1 || result == -2)
p->renderer_end = pts + duration;
return result;
}
int record_android_capture_pool_record(record_android_capture_pool_t *p,
const record_android_capture_source_t *source, uint64_t pts,
uint64_t duration, VkCommandBuffer command)
{
int result;
if (!p || p->closed || p->preparing || !p->pipe)
return -1;
result = record_android_capture_pipe_record(
p->pipe, source, pts, duration, command);
if (result == 1 || result == -2)
p->renderer_end = pts + duration;
return result;
}
int record_android_capture_pool_commit(record_android_capture_pool_t *p)
{
if (!p || p->preparing || !p->pipe)
return -1;
return record_android_capture_pipe_commit(p->pipe);
}
bool record_android_capture_pool_abort_recorded(
record_android_capture_pool_t *p)
{
return p && !p->preparing && p->pipe
&& record_android_capture_pipe_abort_recorded(p->pipe);
}
int record_android_capture_pool_step(record_android_capture_pool_t *p)
{
if (!p)
return -1;
if (p->preparing && (!p->destroyed || !record_acpool_terminal(p)))
return 0;
return p->pipe ? record_android_capture_pipe_step(p->pipe)
: record_acpool_terminal(p) ? 1
: 0;
}
enum record_dispatch_mediacodec_state record_android_capture_pool_status(
const record_android_capture_pool_t *p, int *error, int *system_error)
{
return record_gpu_dispatch_mediacodec_status(
p ? p->gpu : NULL, error, system_error);
}
bool record_android_capture_pool_sources_retired(
const record_android_capture_pool_t *p)
{
return !p || record_android_capture_pipe_sources_retired(p->pipe);
}
bool record_android_capture_pool_prepare_device_destroy(
record_android_capture_pool_t *p, bool queue_quiesced)
{
unsigned i;
if (!p || p->prepared)
return true;
if (!p->closed)
return false;
p->preparing = true;
for (i = 0; i < p->count; i++)
if (!record_android_capture_submit_prepare_device_destroy(
p->entries[i], queue_quiesced))
return false;
record_android_capture_images_prepare_device_destroy(p->images);
p->prepared = true;
return true;
}
bool record_android_capture_pool_after_device_destroy(
record_android_capture_pool_t *p)
{
unsigned i;
if (!p)
return true;
if (!p->prepared)
return false;
for (i = 0; i < p->count; i++)
if (!record_android_capture_submit_after_device_destroy(p->entries[i]))
return false;
p->destroyed = true;
return true;
}
bool record_android_capture_pool_device_state(
const record_android_capture_pool_t *p, bool *destroyed)
{
if (destroyed)
*destroyed = p && p->destroyed;
return !p || p->prepared;
}
static void record_acpool_renderer_release(void *context, bool destroyed)
{
record_android_capture_pool_t *p = (record_android_capture_pool_t *)context;
(void)destroyed;
p->renderer_leases--;
}
static void record_acpool_renderer_close(record_android_capture_pool_t *p)
{
if (p->renderer_stopping && !p->closed)
record_android_capture_pool_finish(p, (int64_t)p->renderer_end);
}
static int record_acpool_renderer_record(void *context,
const record_android_capture_source_t *source, VkCommandBuffer command,
uint64_t pts, unsigned dims, unsigned mode)
{
record_android_capture_pool_t *p = (record_android_capture_pool_t *)context;
int result;
if (p->closed || p->preparing || p->renderer_stopping)
return 0;
if (!source || source->owner != p
|| source->release != record_acpool_renderer_release || dims != p->dims
|| mode != p->mode)
{
record_android_capture_pool_cancel(p);
record_acpool_renderer_close(p);
return -1;
}
if (p->renderer_offer && pts <= p->renderer_last_offer)
return 0;
p->renderer_offer = true;
p->renderer_last_offer = pts;
result = record_android_capture_pool_record(
p, source, pts, p->renderer_duration, command);
if (result == 1)
{
p->renderer_leases++;
p->renderer_end = pts + p->renderer_duration;
p->renderer_recorded = true;
}
else if (result < 0 || (result == 0 && p->fail_on_drop))
{
record_android_capture_pool_cancel(p);
record_acpool_renderer_close(p);
return -1;
}
return result;
}
static void record_acpool_renderer_service(void *context)
{
record_android_capture_pool_t *p = (record_android_capture_pool_t *)context;
record_acpool_renderer_close(p);
if (record_android_capture_pool_step(p) < 0)
{
record_android_capture_pool_cancel(p);
record_acpool_renderer_close(p);
}
}
static void record_acpool_renderer_submitted(void *context, VkResult result)
{
record_android_capture_pool_t *p = (record_android_capture_pool_t *)context;
if (!p->renderer_recorded)
return;
p->renderer_recorded = false;
if (result != VK_SUCCESS || record_android_capture_pool_commit(p) < 0)
{
record_android_capture_pool_cancel(p);
record_acpool_renderer_close(p);
return;
}
record_acpool_renderer_service(p);
}
bool record_android_capture_pool_bind_renderer(record_android_capture_pool_t *p,
unsigned duration, record_android_capture_renderer_t *binding)
{
if (!p || !binding || !duration || duration > 1000000 || !p->pipe
|| p->closed || p->preparing || p->renderer_bound)
return false;
p->renderer_duration = duration;
p->renderer_bound = true;
binding->context = p;
binding->flags = 0;
binding->release = record_acpool_renderer_release;
binding->record = record_acpool_renderer_record;
binding->submitted = record_acpool_renderer_submitted;
binding->service = record_acpool_renderer_service;
return true;
}
+78
View File
@@ -0,0 +1,78 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier:
* GPL-3.0-or-later */
#ifndef RECORD_ANDROID_CAPTURE_POOL_H
#define RECORD_ANDROID_CAPTURE_POOL_H
#include "record_android_capture_images.h"
#include "record_android_capture_pipe.h"
#include "record_android_capture_renderer.h"
RETRO_BEGIN_DECLS
typedef struct record_android_capture_pool record_android_capture_pool_t;
typedef struct record_android_capture_pool_config
{
record_android_capture_image_ops_t images;
record_android_capture_submit_ops_t submit;
record_gpu_mediacodec_config_t gpu;
VkPhysicalDevice physical;
VkDevice device;
VkQueue queue;
const VkPhysicalDeviceMemoryProperties *memory;
unsigned family;
bool fail_on_drop;
} record_android_capture_pool_config_t;
/* Cold renderer setup. Owns fixed images, entries, executor and handoff.
* Borrows I/O, device and native contexts through collection. Buffers in gpu
* are replaced by the newly allocated pool. False may return a partial owner:
* it is cancelled/closed, but still needs collection before borrowed teardown.
*/
bool record_android_capture_pool_new(record_dispatch_mediacodec_t *io,
const record_android_capture_pool_config_t *config,
record_android_capture_pool_t **out);
/* Cold, once per pool. Binding storage must outlive queued video frames.
* Duration is in microseconds. All callbacks run on the render owner; service
* and successful submission callbacks use the renderer's existing queue lock.
* Source bookkeeping is the pool, never a swapchain-image object. */
bool record_android_capture_pool_bind_renderer(
record_android_capture_pool_t *pool, unsigned duration,
record_android_capture_renderer_t *binding);
int record_android_capture_pool_push(record_android_capture_pool_t *pool,
const record_android_capture_source_t *source, uint64_t pts,
uint64_t duration);
/* Renderer-command path; same record/commit/never-execute abort contract as
* the handoff. Commit/abort remain available during cancellation until device
* preparation; an accepted frame must not be silently abandoned. */
int record_android_capture_pool_record(record_android_capture_pool_t *pool,
const record_android_capture_source_t *source, uint64_t pts,
uint64_t duration, VkCommandBuffer command);
int record_android_capture_pool_commit(record_android_capture_pool_t *pool);
bool record_android_capture_pool_abort_recorded(
record_android_capture_pool_t *pool);
int record_android_capture_pool_step(record_android_capture_pool_t *pool);
/* Executor status; DONE does not authorize pool/device destruction. */
enum record_dispatch_mediacodec_state record_android_capture_pool_status(
const record_android_capture_pool_t *pool, int *error, int *system_error);
bool record_android_capture_pool_sources_retired(
const record_android_capture_pool_t *pool);
/* Render-owner cached closure; does not change the immutable source end. */
bool record_android_capture_pool_closed(
const record_android_capture_pool_t *pool);
/* Same producer closure and cancellation contract as the handoff. */
bool record_android_capture_pool_finish(
record_android_capture_pool_t *pool, int64_t end_us);
void record_android_capture_pool_cancel(record_android_capture_pool_t *pool);
/* Closed producer, renderer quiescent. Starts irreversible device retirement;
* commit/abort stop even if a child still needs completion proof. Vulkan
* children retire before the device; buffers/GLES stay owned until collection.
* queue_quiesced proves deferred/native work can no longer execute. */
bool record_android_capture_pool_prepare_device_destroy(
record_android_capture_pool_t *pool, bool queue_quiesced);
bool record_android_capture_pool_after_device_destroy(
record_android_capture_pool_t *pool);
/* Render-owner cached proof only. prepared means Vulkan children retired;
* destroyed means the matching actual device destruction was observed. */
bool record_android_capture_pool_device_state(
const record_android_capture_pool_t *pool, bool *destroyed);
/* Cold renderer, callers quiescent. Refuses live resources; does not free I/O.
* Device stays alive unless the two-phase device retirement completed. */
bool record_android_capture_pool_free(record_android_capture_pool_t *pool);
RETRO_END_DECLS
#endif
+32
View File
@@ -0,0 +1,32 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier:
* GPL-3.0-or-later */
#ifndef RECORD_ANDROID_CAPTURE_RENDERER_H
#define RECORD_ANDROID_CAPTURE_RENDERER_H
#include "record_android_capture_submit.h"
/* Android Vulkan recording_gpu binding, immutable while frames reference it.
* Context and source leases survive resize and remain alive through collection.
* Callbacks never block, allocate, read back pixels or acquire the queue lock.
*/
enum record_android_capture_renderer_flags
{
RECORD_ANDROID_CAPTURE_MENU = 1
};
typedef struct record_android_capture_renderer
{
void *context;
void (*release)(void *context, bool device_destroyed);
/* Frame command outside render passes. Restore COLOR_ATTACHMENT_OPTIMAL.
* Exactly 1 takes the source lease; no queue submission occurs here.
* Reject unsupported format/extent/output mode without recording a copy. */
int (*record)(void *context, const record_android_capture_source_t *source,
VkCommandBuffer command, uint64_t pts, unsigned dims, unsigned mode);
/* SUCCESS follows accepted main work under its existing queue lock.
* Other results can precede submission/lock acquisition: retain the lease,
* cancel output, and never submit completion or infer never-execute proof.
*/
void (*submitted)(void *context, VkResult result);
/* Optional bounded retirement/completion service under the queue lock. */
void (*service)(void *context);
unsigned flags;
} record_android_capture_renderer_t;
#endif
+340
View File
@@ -0,0 +1,340 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier:
* GPL-3.0-or-later */
#ifdef HAVE_CONFIG_H
#include "../config.h"
#endif
#include "record_android_capture_request.h"
#include "record_android_capture_egl.h"
#ifdef HAVE_THREADS
#include "../gfx/video_driver.h"
#include "../gfx/video_thread_wrapper.h"
#include <rthreads/rthreads.h>
#include <stdlib.h>
#include <string.h>
struct record_android_capture_request
{
record_android_capture_pool_config_t config;
VkPhysicalDeviceMemoryProperties memory;
record_android_capture_renderer_t binding;
video_thread_async_load_t node;
video_thread_async_job_t job;
record_dispatch_mediacodec_t *io;
record_android_capture_pool_t *pool;
record_android_capture_egl_t *egl;
int64_t end_us, close_end;
enum record_android_capture_operation operation;
unsigned duration;
int result;
bool busy, ready, closing, failed, had_pool, menu_source;
};
static bool record_acrequest_in_flight(record_android_capture_request_t *r)
{
video_driver_state_t *video = video_state_get_ptr();
return video_driver_thread_wrapper_active()
&& video_thread_record_gpu_in_flight(video->data, &r->binding);
}
static int record_acrequest_create(record_android_capture_request_t *r)
{
enum record_dispatch_mediacodec_state state;
if (r->failed)
return -1;
if (!r->pool)
{
state = record_dispatch_mediacodec_status(r->io, NULL, NULL, NULL);
if (state == RECORD_MC_DISPATCH_STARTING)
return 0;
if (state != RECORD_MC_DISPATCH_READY)
return -1;
if (!record_android_capture_egl_new(r->config.gpu.mode, &r->egl)
|| !record_android_capture_egl_configure(r->egl, &r->config.gpu))
{
r->failed = true;
return -1;
}
if (!record_android_capture_pool_new(r->io, &r->config, &r->pool))
{
r->had_pool = r->pool != NULL;
r->failed = true;
return -1;
}
r->had_pool = true;
if (!record_android_capture_pool_bind_renderer(
r->pool, r->duration, &r->binding))
{
r->failed = true;
return -1;
}
if (r->menu_source)
r->binding.flags |= RECORD_ANDROID_CAPTURE_MENU;
}
state = record_android_capture_pool_status(r->pool, NULL, NULL);
return state == RECORD_MC_DISPATCH_READY ? 1
: state == RECORD_MC_DISPATCH_STARTING ? 0
: -1;
}
/* Cold render owner. Every native queue operation uses the current driver's
* existing serialization. No borrowed lock is retained in the request. */
static uintptr_t record_acrequest_run(
void *driver_data, const video_poke_interface_t *poke, void *user)
{
record_android_capture_request_t *r
= (record_android_capture_request_t *)user;
video_device_info_t device;
slock_t *lock = NULL;
bool current, destroyed = false;
int result = 0;
memset(&device, 0, sizeof(device));
current = driver_data && poke && poke->get_device
&& poke->get_device(driver_data, &device)
&& device.api == VIDEO_DEVICE_VULKAN
&& device.device == (void *)r->config.device
&& device.physical_device == (void *)r->config.physical
&& device.queue == (void *)r->config.queue
&& device.queue_family == r->config.family && device.queue_lock;
if (r->operation == RECORD_ANDROID_CAPTURE_CREATE)
result = current ? record_acrequest_create(r) : -1;
else
{
if (r->pool)
{
record_android_capture_pool_device_state(r->pool, &destroyed);
/* A stale device/lock is never queried. Common teardown must first
* publish actual device death; unproven owners remain retained. */
if (!current && !destroyed)
goto complete;
if (current && device.queue_lock)
{
if (!slock_try_lock((slock_t *)device.queue_lock))
goto complete;
lock = (slock_t *)device.queue_lock;
}
if (r->operation == RECORD_ANDROID_CAPTURE_CANCEL
|| r->operation == RECORD_ANDROID_CAPTURE_PREPARE_DEVICE
|| r->operation == RECORD_ANDROID_CAPTURE_QUIESCE_DEVICE)
record_android_capture_pool_cancel(r->pool);
if (r->operation == RECORD_ANDROID_CAPTURE_QUIESCE_DEVICE)
{
/* Every pool is registered before native allocation. The common
* teardown must reach its existing drain for ambiguous work; a
* failed nonblocking child proof must not deadlock that drain.
* Cancellation stops publication, but proves no GPU completion. */
record_android_capture_pool_finish(r->pool, r->end_us);
result = 1;
goto unlock;
}
if (r->operation >= RECORD_ANDROID_CAPTURE_FINISH
&& !record_android_capture_pool_finish(r->pool, r->end_us)
&& !(record_dispatch_mediacodec_cancelled(r->io)
&& record_android_capture_pool_closed(r->pool)))
{
record_android_capture_pool_step(r->pool);
goto unlock;
}
if (r->operation == RECORD_ANDROID_CAPTURE_PREPARE_DEVICE)
result = record_android_capture_pool_prepare_device_destroy(
r->pool, false)
? 1
: 0;
else
{
result = record_android_capture_pool_step(r->pool);
if (r->operation == RECORD_ANDROID_CAPTURE_COLLECT && result == 1)
{
if (record_android_capture_pool_free(r->pool))
{
r->pool = NULL;
memset(&r->binding, 0, sizeof(r->binding));
}
else
result = 0;
}
}
unlock:
if (lock)
slock_unlock(lock);
}
else
result = 1;
if (r->operation == RECORD_ANDROID_CAPTURE_COLLECT && !r->pool && r->egl)
{
if (record_android_capture_egl_free(r->egl))
r->egl = NULL;
else
result = -1;
}
}
complete:
r->result = result;
/* Nonzero distinguishes executed work from the wrapper's teardown reply;
* operation success/pending/failure is stored separately. */
return 1;
}
static void record_acrequest_done(void *user, uintptr_t executed)
{
record_android_capture_request_t *r
= (record_android_capture_request_t *)user;
r->busy = false;
if (!executed)
{
r->result = -1;
r->ready = false;
r->failed = true;
}
else if (r->operation == RECORD_ANDROID_CAPTURE_CREATE)
{
r->ready = r->result == 1;
if (r->result < 0)
r->failed = true;
}
}
record_android_capture_request_t *record_android_capture_request_new(
record_dispatch_mediacodec_t *io,
const record_android_capture_pool_config_t *config, unsigned duration,
bool allow_drop, bool menu_source)
{
record_android_capture_request_t *r;
if (!io || !config || !config->device || !config->physical || !config->queue
|| !config->memory || !config->gpu.width || !config->gpu.height
|| !config->gpu.count || config->gpu.count > RECORD_GPU_QUEUE_SLOTS
|| config->gpu.mode > 2 || !duration || duration > 1000000)
return NULL;
r = (record_android_capture_request_t *)calloc(1, sizeof(*r));
if (!r)
return NULL;
r->config = *config;
r->config.fail_on_drop = !allow_drop;
r->memory = *config->memory;
r->config.memory = &r->memory;
r->io = io;
r->duration = duration;
r->menu_source = menu_source;
r->job.run = record_acrequest_run;
r->job.user = r;
r->node.kind = VIDEO_THREAD_ASYNC_JOB;
r->node.user = r;
r->node.done = record_acrequest_done;
return r;
}
bool record_android_capture_request_post(record_android_capture_request_t *r,
enum record_android_capture_operation operation, int64_t end_us)
{
video_driver_state_t *video = video_state_get_ptr();
if (!r || r->busy
|| (unsigned)operation > RECORD_ANDROID_CAPTURE_QUIESCE_DEVICE
|| end_us < 0
|| (r->closing && operation == RECORD_ANDROID_CAPTURE_CREATE))
return false;
if (operation >= RECORD_ANDROID_CAPTURE_FINISH)
{
if (r->closing && r->close_end != end_us)
return false;
r->closing = true;
r->close_end = end_us;
r->ready = false;
if (record_acrequest_in_flight(r))
return false;
}
r->operation = operation;
r->end_us = end_us;
r->result = 0;
r->node.img = &r->job;
r->busy = true;
if (video_driver_thread_wrapper_active())
{
if (video_thread_async_post(&r->node))
return true;
r->busy = false;
r->ready = false;
r->failed = true;
r->result = -1;
return false;
}
record_acrequest_done(r, record_acrequest_run(video->data, video->poke, r));
return true;
}
int record_android_capture_request_result(
const record_android_capture_request_t *r)
{
return !r ? -1 : r->busy ? 0 : r->result;
}
const record_android_capture_renderer_t *record_android_capture_request_binding(
const record_android_capture_request_t *r)
{
return r && r->ready && !r->closing ? &r->binding : NULL;
}
bool record_android_capture_request_free(record_android_capture_request_t *r)
{
if (!r)
return true;
if (r->busy || r->pool || r->egl || record_acrequest_in_flight(r))
return false;
if (!r->had_pool)
{
enum record_dispatch_mediacodec_state state
= record_dispatch_mediacodec_status(r->io, NULL, NULL, NULL);
if (state != RECORD_MC_DISPATCH_DONE
&& state != RECORD_MC_DISPATCH_FAILED)
{
bool closed;
/* Do not close I/O's GPU side through an unrelated live claim.
* A prior completed startup can instead finish on its I/O owner. */
if (!record_dispatch_mediacodec_gpu_claim(r->io))
return false;
closed = record_dispatch_mediacodec_gpu_retired(r->io, 0)
&& record_dispatch_mediacodec_gpu_released(r->io);
record_dispatch_mediacodec_gpu_release_claim(r->io);
if (!closed)
return false;
}
}
free(r);
return true;
}
#else
record_android_capture_request_t *record_android_capture_request_new(
record_dispatch_mediacodec_t *io,
const record_android_capture_pool_config_t *config, unsigned duration,
bool allow_drop, bool menu_source)
{
(void)io;
(void)config;
(void)duration;
(void)allow_drop;
(void)menu_source;
return NULL;
}
bool record_android_capture_request_post(record_android_capture_request_t *r,
enum record_android_capture_operation operation, int64_t end_us)
{
(void)r;
(void)operation;
(void)end_us;
return false;
}
int record_android_capture_request_result(
const record_android_capture_request_t *r)
{
(void)r;
return -1;
}
const record_android_capture_renderer_t *record_android_capture_request_binding(
const record_android_capture_request_t *r)
{
(void)r;
return NULL;
}
bool record_android_capture_request_free(record_android_capture_request_t *r)
{
return r == NULL;
}
#endif
+56
View File
@@ -0,0 +1,56 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier:
* GPL-3.0-or-later */
#ifndef RECORD_ANDROID_CAPTURE_REQUEST_H
#define RECORD_ANDROID_CAPTURE_REQUEST_H
#include "record_android_capture_pool.h"
RETRO_BEGIN_DECLS
typedef struct record_android_capture_request record_android_capture_request_t;
enum record_android_capture_operation
{
RECORD_ANDROID_CAPTURE_CREATE,
RECORD_ANDROID_CAPTURE_SERVICE,
RECORD_ANDROID_CAPTURE_FINISH,
RECORD_ANDROID_CAPTURE_CANCEL,
RECORD_ANDROID_CAPTURE_PREPARE_DEVICE,
RECORD_ANDROID_CAPTURE_COLLECT,
RECORD_ANDROID_CAPTURE_QUIESCE_DEVICE
};
/* Main-owner cold allocation. Copies the validated Vulkan pool configuration
* and memory properties. Operation contexts/callbacks must survive collection;
* context pointers must not refer to driver or swapchain allocations. Native
* operations/device prerequisites remain the
* caller's admission responsibility. Keep the original driver alive through
* CREATE acknowledgment or cancellation. This is the only GPU controller for
* its I/O job, which stays borrowed through free. EGL,
* pool and GPU executor construction/collection occur on the render owner.
* No frontend driver is registered by this private bridge. */
record_android_capture_request_t *record_android_capture_request_new(
record_dispatch_mediacodec_t *io,
const record_android_capture_pool_config_t *config, unsigned duration,
bool allow_drop, bool menu_source);
/* Main owner outside frame publication. At most one embedded job is posted.
* FINISH/CANCEL/PREPARE/COLLECT refuse queued frames naming this binding;
* producers must first stop publishing it. Wrapped posting neither waits nor
* allocates; without a wrapper it runs cold work on the current render owner.
* end_us is immutable for an accepted operation, including dropped frames.
* QUIESCE_DEVICE acknowledges only stopped publication and render-producer
* quiescence. Registered native owners remain retained for the existing device
* teardown drain; it is not child completion or actual device-death proof. */
bool record_android_capture_request_post(record_android_capture_request_t *job,
enum record_android_capture_operation operation, int64_t end_us);
/* Main owner after post: 0 awaiting acknowledgment or progress, 1 complete,
* -1 refused/failed. A discarded node is failure, never proof no setup ran. */
int record_android_capture_request_result(
const record_android_capture_request_t *job);
/* Stable binding address, main owner after successful CREATE acknowledgment.
* Retain job/binding through queued frames and native collection. */
const record_android_capture_renderer_t *record_android_capture_request_binding(
const record_android_capture_request_t *job);
/* Main owner after acknowledgment. False may retain native EGL/device owners.
* Refuses outstanding jobs, frames or uncollected resources. Closes the unused
* GPU side of I/O if setup never acquired a pool. Does not close PCM or free
* I/O.
*/
bool record_android_capture_request_free(record_android_capture_request_t *job);
RETRO_END_DECLS
#endif
+128 -26
View File
@@ -20,6 +20,7 @@ struct record_android_capture_submit
unsigned width, height, family;
int ready_fd;
bool initialized, inflight, waiting, exported, taken, broken, gpu_retired;
bool recorded, committed, completion_pending;
};
static void record_acs_destroy_gpu(record_android_capture_submit_t *e)
@@ -158,34 +159,25 @@ static bool record_acs_export(record_android_capture_submit_t *e)
return true;
}
int record_android_capture_submit_push(record_android_capture_submit_t *e,
static bool record_acs_source_valid(record_android_capture_submit_t *e,
const record_android_capture_source_t *s)
{
VkCommandBufferBeginInfo begin;
VkImageMemoryBarrier before[2], after[2];
VkImageCopy region;
VkSubmitInfo submit;
VkPipelineStageFlags stage = VK_PIPELINE_STAGE_ALL_COMMANDS_BIT;
VkResult result;
unsigned i;
if (!e || e->broken || !s || !s->image || s->image == e->image || !s->owner
|| !s->release || s->ready == e->done
return !(!e || e->broken || !s || !s->image || s->image == e->image
|| !s->owner || !s->release || s->ready == e->done
|| (s->format != e->format
&& !(e->format == VK_FORMAT_R8G8B8A8_UNORM
&& s->format == VK_FORMAT_R8G8B8A8_SRGB))
|| !record_acs_access(s->layout) || s->width > 16384
|| s->height > 16384 || s->x > s->width || s->y > s->height
|| e->width > s->width - s->x || e->height > s->height - s->y)
return -1;
if (e->waiting || e->inflight || e->owner)
return 0;
if (e->ops.reset_command(e->command, 0) != VK_SUCCESS)
goto failed;
memset(&begin, 0, sizeof(begin));
begin.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
if (e->ops.begin(e->command, &begin) != VK_SUCCESS)
goto failed;
|| e->width > s->width - s->x || e->height > s->height - s->y);
}
static void record_acs_copy(record_android_capture_submit_t *e,
const record_android_capture_source_t *s, VkCommandBuffer command)
{
VkImageMemoryBarrier before[2], after[2];
VkImageCopy region;
unsigned i;
memset(before, 0, sizeof(before));
for (i = 0; i < 2; i++)
{
@@ -208,7 +200,7 @@ int record_android_capture_submit_push(record_android_capture_submit_t *e,
before[1].srcQueueFamilyIndex = VK_QUEUE_FAMILY_FOREIGN_EXT;
before[1].dstQueueFamilyIndex = e->family;
before[1].dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
e->ops.barrier(e->command, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
e->ops.barrier(command, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, NULL, 0, NULL, 2, before);
memset(&region, 0, sizeof(region));
region.srcSubresource.aspectMask = region.dstSubresource.aspectMask
@@ -219,7 +211,7 @@ int record_android_capture_submit_push(record_android_capture_submit_t *e,
region.extent.width = e->width;
region.extent.height = e->height;
region.extent.depth = 1;
e->ops.copy(e->command, s->image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
e->ops.copy(command, s->image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
e->image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &region);
memcpy(after, before, sizeof(after));
after[0].oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
@@ -232,8 +224,29 @@ int record_android_capture_submit_push(record_android_capture_submit_t *e,
after[1].dstQueueFamilyIndex = VK_QUEUE_FAMILY_FOREIGN_EXT;
after[1].srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
after[1].dstAccessMask = 0;
e->ops.barrier(e->command, VK_PIPELINE_STAGE_TRANSFER_BIT,
e->ops.barrier(command, VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, NULL, 0, NULL, 2, after);
}
int record_android_capture_submit_push(record_android_capture_submit_t *e,
const record_android_capture_source_t *s)
{
VkCommandBufferBeginInfo begin;
VkSubmitInfo submit;
VkPipelineStageFlags stage = VK_PIPELINE_STAGE_ALL_COMMANDS_BIT;
VkResult result;
if (!record_acs_source_valid(e, s))
return -1;
if (e->waiting || e->inflight || e->owner)
return 0;
if (e->ops.reset_command(e->command, 0) != VK_SUCCESS)
goto failed;
memset(&begin, 0, sizeof(begin));
begin.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
if (e->ops.begin(e->command, &begin) != VK_SUCCESS)
goto failed;
record_acs_copy(e, s, e->command);
if (e->ops.end(e->command) != VK_SUCCESS
|| e->ops.reset_fences(e->device, 1, &e->fence) != VK_SUCCESS)
goto failed;
@@ -270,13 +283,96 @@ failed:
return -1;
}
int record_android_capture_submit_record(record_android_capture_submit_t *e,
const record_android_capture_source_t *s, VkCommandBuffer command)
{
VkCommandBufferBeginInfo begin;
if (!record_acs_source_valid(e, s) || !command || command == e->command
|| s->ready)
return -1;
if (e->waiting || e->inflight || e->owner)
return 0;
memset(&begin, 0, sizeof(begin));
begin.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
if (e->ops.reset_command(e->command, 0) != VK_SUCCESS
|| e->ops.begin(e->command, &begin) != VK_SUCCESS
|| e->ops.end(e->command) != VK_SUCCESS
|| e->ops.reset_fences(e->device, 1, &e->fence) != VK_SUCCESS)
{
e->broken = true;
return -1;
}
record_acs_copy(e, s, command);
e->owner = s->owner;
e->release_source = s->release;
e->recorded = e->waiting = true;
return 1;
}
static int record_acs_complete_recorded(record_android_capture_submit_t *e)
{
VkSubmitInfo submit;
VkResult result;
memset(&submit, 0, sizeof(submit));
submit.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
submit.commandBufferCount = 1;
submit.pCommandBuffers = &e->command;
submit.signalSemaphoreCount = 1;
submit.pSignalSemaphores = &e->done;
result = e->ops.submit(e->ops.context, e->queue, 1, &submit, e->fence);
if (result == VK_TIMEOUT)
return 0;
e->completion_pending = false;
e->inflight = true;
if (result != VK_SUCCESS && result != VK_NOT_READY)
{
e->broken = true;
return -1;
}
if (result == VK_SUCCESS && !e->broken)
{
record_acs_export(e);
if (e->broken)
return -1;
}
return 1;
}
int record_android_capture_submit_commit(record_android_capture_submit_t *e)
{
if (!e || e->gpu_retired || !e->recorded || e->committed)
return -1;
e->initialized = e->committed = e->completion_pending = true;
return record_acs_complete_recorded(e);
}
bool record_android_capture_submit_abort_recorded(
record_android_capture_submit_t *e)
{
if (!e || e->gpu_retired || !e->recorded || e->committed)
return false;
e->release_source(e->owner, false);
e->owner = NULL;
e->recorded = e->waiting = false;
return true;
}
static int record_acs_retire(record_android_capture_submit_t *e)
{
VkResult result;
if (e->gpu_retired)
return e->owner || e->inflight || e->completion_pending ? -1 : 1;
if (e->recorded && !e->committed)
return 0;
if (e->completion_pending)
{
int completion = record_acs_complete_recorded(e);
if (completion <= 0)
return completion;
}
if (!e->inflight)
return 1;
if (e->gpu_retired)
return -1;
result = e->ops.fence_status(e->device, e->fence);
if (result == VK_NOT_READY)
return 0;
@@ -329,6 +425,7 @@ bool record_android_capture_submit_release(record_android_capture_submit_t *e)
if (!e || e->broken || !e->waiting || !e->taken || e->inflight || e->owner)
return false;
e->waiting = e->exported = e->taken = false;
e->recorded = e->committed = e->completion_pending = false;
return true;
}
@@ -345,6 +442,7 @@ int record_android_capture_submit_discard(record_android_capture_submit_t *e)
e->ops.close_fd(e->ops.context, e->ready_fd);
e->ready_fd = -1;
e->waiting = e->exported = e->taken = false;
e->recorded = e->committed = e->completion_pending = false;
return 1;
}
@@ -355,6 +453,9 @@ bool record_android_capture_submit_prepare_device_destroy(
if (!e || e->gpu_retired)
return true;
e->broken = true;
if (e->recorded && (!e->committed || e->completion_pending)
&& !queue_quiesced)
return false;
if (e->inflight && !queue_quiesced)
{
result = e->ops.fence_status(e->device, e->fence);
@@ -379,6 +480,7 @@ bool record_android_capture_submit_after_device_destroy(
e->owner = NULL;
e->ready_fd = -1;
e->waiting = e->inflight = e->exported = e->taken = false;
e->recorded = e->committed = e->completion_pending = false;
e->broken = true;
e->device = VK_NULL_HANDLE;
return true;
+15
View File
@@ -59,6 +59,21 @@ record_android_capture_submit_t *record_android_capture_submit_new(
* conversion occurs. */
int record_android_capture_submit_push(record_android_capture_submit_t *entry,
const record_android_capture_source_t *source);
/* Renderer recording command, outside render passes, same queue/family.
* No ready semaphore: the renderer supplies all prior dependencies. Records
* one copy and takes the lease on 1. The command must never be replayed. */
int record_android_capture_submit_record(record_android_capture_submit_t *entry,
const record_android_capture_source_t *source, VkCommandBuffer command);
/* Exactly once, AFTER the renderer accepted that command on the same queue.
* Submits only a completion signal/fence, no second copy. 0 retains pending
* completion; poll retries only this signal submission, never source work.
* Negative retains ambiguous ownership for terminal device recovery. */
int record_android_capture_submit_commit(
record_android_capture_submit_t *entry);
/* ONLY after the renderer reset/discarded its unsubmitted command and proves
* it can never execute. Refuses after commit; releases the original lease. */
bool record_android_capture_submit_abort_recorded(
record_android_capture_submit_t *entry);
/* One zero-timeout fence query. 1 copy retired, 0 pending, -1 sticky error.
* Export failures cannot leave a binary semaphore eligible for re-signaling. */
int record_android_capture_submit_poll(record_android_capture_submit_t *entry);
+103
View File
@@ -0,0 +1,103 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier:
* GPL-3.0-or-later */
#include "record_audio_tap.h"
#include <stdlib.h>
#include <retro_atomic.h>
#include <audio/conversion/float_to_s16.h>
#define RECORD_AUDIO_TAP_FRAMES 4096
struct record_audio_tap
{
uint64_t frames;
int (*submit)(void *, const int16_t *, size_t, uint64_t);
void *context;
unsigned rate;
retro_atomic_int_t state, failed;
bool producer_failed;
int16_t scratch[RECORD_AUDIO_TAP_FRAMES * 2];
};
record_audio_tap_t *record_audio_tap_new(unsigned rate,
int (*submit)(void *, const int16_t *, size_t, uint64_t), void *context)
{
record_audio_tap_t *t;
if (!submit || rate < 8000 || rate > 192000)
return NULL;
t = (record_audio_tap_t *)calloc(1, sizeof(*t));
if (!t)
return NULL;
t->rate = rate;
t->submit = submit;
t->context = context;
retro_atomic_int_init(&t->state, 0);
retro_atomic_int_init(&t->failed, 0);
return t;
}
unsigned record_audio_tap_rate(const record_audio_tap_t *t)
{
return t ? t->rate : 0;
}
bool record_audio_tap_begin(record_audio_tap_t *t)
{
return t && retro_atomic_cas_int(&t->state, 0, 1);
}
void record_audio_tap_retire(record_audio_tap_t *t, bool disconnected)
{
if (!t)
return;
if (disconnected)
retro_atomic_store_release_int(&t->failed, 1);
retro_atomic_store_release_int(&t->state, 2);
}
bool record_audio_tap_failed(const record_audio_tap_t *t)
{
return !t || retro_atomic_load_acquire_int(&t->failed) != 0;
}
bool record_audio_tap_push(record_audio_tap_t *t, const void *data,
size_t frames, bool floating, unsigned rate)
{
const uint8_t *input = (const uint8_t *)data;
if (!t || retro_atomic_load_acquire_int(&t->state) != 1)
return false;
if (t->producer_failed)
return false;
if (rate != t->rate || (!data && frames)
|| frames
> (~(size_t)0) / 2 / (floating ? sizeof(float) : sizeof(int16_t))
|| t->frames > ~(uint64_t)0 - frames)
goto failed;
while (frames)
{
size_t n = frames > RECORD_AUDIO_TAP_FRAMES ? RECORD_AUDIO_TAP_FRAMES
: frames;
const int16_t *samples = (const int16_t *)input;
if (floating)
{
convert_float_to_s16(t->scratch, (const float *)input, n * 2);
samples = t->scratch;
}
if (t->submit(t->context, samples, n, t->frames) != RECORD_AUDIO_OK)
goto failed;
t->frames += n;
input += n * 2 * (floating ? sizeof(float) : sizeof(int16_t));
frames -= n;
}
return true;
failed:
t->producer_failed = true;
retro_atomic_store_release_int(&t->failed, 1);
return false;
}
bool record_audio_tap_end(const record_audio_tap_t *t, uint64_t *frames)
{
if (!t || retro_atomic_load_acquire_int(&t->state) == 1)
return false;
if (frames)
*frames = t->frames;
return true;
}
bool record_audio_tap_free(record_audio_tap_t *t)
{
if (t && !record_audio_tap_end(t, NULL))
return false;
free(t);
return true;
}
+28
View File
@@ -0,0 +1,28 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier:
* GPL-3.0-or-later */
#ifndef RECORD_AUDIO_TAP_H
#define RECORD_AUDIO_TAP_H
#include "record_audio_queue.h"
RETRO_BEGIN_DECLS
typedef struct record_audio_tap record_audio_tap_t;
/* Cold allocation; one serialized post-mixer producer. Submission borrows S16
* stereo for the call only and returns RECORD_AUDIO_OK/BUSY/ERROR. */
record_audio_tap_t *record_audio_tap_new(unsigned rate,
int (*submit)(void *, const int16_t *, size_t, uint64_t), void *context);
unsigned record_audio_tap_rate(const record_audio_tap_t *tap);
/* Audio driver cold owner under its existing state serialization. begin is
* once-only. retire requires producer quiescence; disconnected marks failure.
*/
bool record_audio_tap_begin(record_audio_tap_t *tap);
void record_audio_tap_retire(record_audio_tap_t *tap, bool disconnected);
/* Existing audio pass serialization; no allocation, wait or lock acquisition.
* Output-rate stereo after mixing/gain. Failed submission advances no clock. */
bool record_audio_tap_push(record_audio_tap_t *tap, const void *data,
size_t frames, bool floating, unsigned rate);
bool record_audio_tap_failed(const record_audio_tap_t *tap);
/* Main owner: false while installed. The immutable end is safe to read only
* after removal or actual audio-driver producer retirement. */
bool record_audio_tap_end(const record_audio_tap_t *tap, uint64_t *frames);
bool record_audio_tap_free(record_audio_tap_t *tap);
RETRO_END_DECLS
#endif
+453
View File
@@ -0,0 +1,453 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier:
* GPL-3.0-or-later */
#ifdef HAVE_CONFIG_H
#include "../config.h"
#endif
#include "record_dispatch_mediacodec.h"
#include <retro_atomic.h>
#include <stdlib.h>
#include <string.h>
#if defined(HAVE_THREADS) && defined(RETRO_ATOMIC_LOCK_FREE) \
&& (defined(__ANDROID__) || defined(RECORD_MEDIACODEC_DISPATCH_TEST))
#include <jni/rjni_env.h>
#include <pthread.h>
#include <sys/eventfd.h>
#include <poll.h>
#include <unistd.h>
#include <errno.h>
struct record_dispatch_mediacodec
{
mediacodec_encoder_request_t video;
uint64_t audio_end;
int64_t video_end;
size_t max_frames, capacity;
char *path, *mime;
record_session_mediacodec_t *session;
void *window;
pthread_t thread;
retro_atomic_int_t state, done, epoch, cancelled, audio_closed;
retro_atomic_int_t gpu_done, gpu_released, gpu_owner, error, file_error,
system_error;
unsigned interval, rate, channels, bitrate;
int wake;
bool joinable;
};
static void record_mcd_system(record_dispatch_mediacodec_t *j, int error)
{
retro_atomic_cas_int(&j->system_error, 0, error ? error : EIO);
retro_atomic_store_release_int(&j->cancelled, 1);
}
void record_dispatch_mediacodec_notify(record_dispatch_mediacodec_t *j)
{
uint64_t one = 1;
if (j && (retro_atomic_fetch_add_seq_cst_int(&j->epoch, 2) & 1)
&& write(j->wake, &one, sizeof(one)) != (ssize_t)sizeof(one)
&& errno != EAGAIN)
record_mcd_system(j, errno);
}
static int record_mcd_service(record_dispatch_mediacodec_t *j)
{
int result, detail, error;
bool audio_closed = retro_atomic_load_acquire_int(&j->audio_closed);
bool gpu_done = retro_atomic_load_acquire_int(&j->gpu_done);
bool gpu_released = retro_atomic_load_acquire_int(&j->gpu_released);
if (!j->session)
return audio_closed && gpu_done && gpu_released ? RECORD_MC_WORKER_ERROR
: RECORD_MC_WORKER_BUSY;
if ((audio_closed
&& !record_session_mediacodec_audio_finish(
j->session, j->audio_end))
|| (gpu_done
&& !record_session_mediacodec_gpu_retired(
j->session, j->video_end))
|| (gpu_released
&& !record_session_mediacodec_gpu_released(j->session)))
retro_atomic_store_release_int(&j->cancelled, 1);
if (retro_atomic_load_acquire_int(&j->cancelled))
record_session_mediacodec_cancel(j->session);
result = record_session_mediacodec_step(j->session);
error = (int)record_session_mediacodec_error(j->session, &detail);
if (error)
retro_atomic_cas_int(&j->error, 0, error);
retro_atomic_store_release_int(&j->file_error, detail);
if (error || retro_atomic_load_acquire_int(&j->cancelled))
retro_atomic_store_release_int(&j->state, RECORD_MC_DISPATCH_DRAINING);
if (result == RECORD_MC_WORKER_END || result == RECORD_MC_WORKER_ERROR)
{
if (!audio_closed || !gpu_done || !gpu_released
|| !record_session_mediacodec_free(j->session))
return RECORD_MC_WORKER_BUSY;
j->session = NULL;
}
return result;
}
static void *record_mcd_run(void *opaque)
{
record_dispatch_mediacodec_t *j = (record_dispatch_mediacodec_t *)opaque;
JNIEnv *env = rjni_env();
struct pollfd descriptor;
uint64_t value;
int result, waited;
if (env)
j->session = record_session_mediacodec_new(env, &j->video, j->interval,
j->rate, j->channels, j->bitrate, j->max_frames, j->capacity,
j->path);
if (j->session)
j->window = record_session_mediacodec_surface(j->session);
if (!j->session || !j->window)
{
retro_atomic_store_release_int(&j->error, RECORD_MC_SESSION_OUTPUT);
retro_atomic_store_release_int(&j->cancelled, 1);
}
retro_atomic_store_release_int(&j->state,
record_dispatch_mediacodec_cancelled(j) ? RECORD_MC_DISPATCH_DRAINING
: RECORD_MC_DISPATCH_READY);
descriptor.fd = j->wake;
descriptor.events = POLLIN;
for (;;)
{
result = record_mcd_service(j);
if (result == RECORD_MC_WORKER_END || result == RECORD_MC_WORKER_ERROR)
break;
if (result != RECORD_MC_WORKER_BUSY)
continue;
retro_atomic_fetch_add_seq_cst_int(&j->epoch, 1);
result = record_mcd_service(j);
if (result != RECORD_MC_WORKER_BUSY)
{
retro_atomic_fetch_and_int(&j->epoch, ~1);
if (result == RECORD_MC_WORKER_END || result == RECORD_MC_WORKER_ERROR)
break;
continue;
}
descriptor.revents = 0;
if (retro_atomic_load_acquire_int(&j->system_error))
descriptor.fd = -1;
waited = poll(&descriptor, 1, j->session ? 1 : 100);
retro_atomic_fetch_and_int(&j->epoch, ~1);
if (waited < 0 && errno != EINTR)
record_mcd_system(j, errno);
else if (descriptor.revents & (POLLERR | POLLHUP | POLLNVAL))
record_mcd_system(j, EIO);
else if (descriptor.revents & POLLIN)
{
if (read(j->wake, &value, sizeof(value)) != (ssize_t)sizeof(value)
&& errno != EAGAIN && errno != EINTR)
record_mcd_system(j, errno);
}
}
retro_atomic_store_release_int(&j->state,
result == RECORD_MC_WORKER_END
&& !retro_atomic_load_acquire_int(&j->system_error)
? RECORD_MC_DISPATCH_DONE
: RECORD_MC_DISPATCH_FAILED);
retro_atomic_store_release_int(&j->done, 1);
return NULL;
}
record_dispatch_mediacodec_t *record_dispatch_mediacodec_new(
const mediacodec_encoder_request_t *video, unsigned interval,
unsigned rate, unsigned channels, unsigned bitrate,
size_t max_audio_frames, size_t audio_capacity, const char *path)
{
record_dispatch_mediacodec_t *j;
size_t path_length, mime_length;
if (!video || !video->mime || !path || !*path)
return NULL;
for (path_length = 0; path_length < 32768 && path[path_length];
path_length++)
{
}
for (mime_length = 0; mime_length < 128 && video->mime[mime_length];
mime_length++)
{
}
if (path_length == 32768 || !mime_length || mime_length == 128)
return NULL;
j = (record_dispatch_mediacodec_t *)calloc(1, sizeof(*j));
if (!j)
return NULL;
j->wake = -1;
j->path = (char *)malloc(path_length + 1);
j->mime = (char *)malloc(mime_length + 1);
if (!j->path || !j->mime)
goto failed;
memcpy(j->path, path, path_length + 1);
memcpy(j->mime, video->mime, mime_length + 1);
j->video = *video;
j->video.mime = j->mime;
j->interval = interval;
j->rate = rate;
j->channels = channels;
j->bitrate = bitrate;
j->max_frames = max_audio_frames;
j->capacity = audio_capacity;
retro_atomic_int_init(&j->state, RECORD_MC_DISPATCH_STARTING);
retro_atomic_int_init(&j->done, 0);
retro_atomic_int_init(&j->epoch, 0);
retro_atomic_int_init(&j->cancelled, 0);
retro_atomic_int_init(&j->audio_closed,
!(rate || channels || bitrate || max_audio_frames || audio_capacity));
retro_atomic_int_init(&j->gpu_done, 0);
retro_atomic_int_init(&j->gpu_released, 0);
retro_atomic_int_init(&j->gpu_owner, 0);
retro_atomic_int_init(&j->error, 0);
retro_atomic_int_init(&j->file_error, 0);
retro_atomic_int_init(&j->system_error, 0);
j->wake = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
if (j->wake < 0 || pthread_create(&j->thread, NULL, record_mcd_run, j))
goto failed;
j->joinable = pthread_detach(j->thread) != 0;
return j;
failed:
if (j->wake >= 0)
close(j->wake);
free(j->path);
free(j->mime);
free(j);
return NULL;
}
void *record_dispatch_mediacodec_surface(record_dispatch_mediacodec_t *j)
{
return j
&& retro_atomic_load_acquire_int(&j->state)
== RECORD_MC_DISPATCH_READY
? j->window
: NULL;
}
bool record_dispatch_mediacodec_video_format(
const record_dispatch_mediacodec_t *j, unsigned *width, unsigned *height,
unsigned *color)
{
if (!j || !width || !height || !color)
return false;
*width = j->video.width;
*height = j->video.height;
*color = j->video.color;
return true;
}
bool record_dispatch_mediacodec_gpu_claim(record_dispatch_mediacodec_t *j)
{
if (!j || !retro_atomic_cas_int(&j->gpu_owner, 0, 1))
return false;
if (retro_atomic_load_acquire_int(&j->gpu_done))
{
retro_atomic_store_release_int(&j->gpu_owner, 0);
return false;
}
return true;
}
void record_dispatch_mediacodec_gpu_release_claim(
record_dispatch_mediacodec_t *j)
{
if (j)
retro_atomic_store_release_int(&j->gpu_owner, 0);
}
int record_dispatch_mediacodec_audio(record_dispatch_mediacodec_t *j,
const int16_t *samples, size_t frames, uint64_t timestamp)
{
int state, result;
if (!j || retro_atomic_load_acquire_int(&j->audio_closed)
|| record_dispatch_mediacodec_cancelled(j))
return RECORD_AUDIO_ERROR;
state = retro_atomic_load_acquire_int(&j->state);
if (state != RECORD_MC_DISPATCH_READY)
return state == RECORD_MC_DISPATCH_STARTING ? RECORD_AUDIO_BUSY
: RECORD_AUDIO_ERROR;
result = record_session_mediacodec_audio(
j->session, samples, frames, timestamp);
if (result == RECORD_AUDIO_OK)
record_dispatch_mediacodec_notify(j);
return result;
}
bool record_dispatch_mediacodec_audio_finish(
record_dispatch_mediacodec_t *j, uint64_t end_sample)
{
if (!j)
return false;
if (retro_atomic_load_acquire_int(&j->audio_closed))
return j->audio_end == end_sample;
j->audio_end = end_sample;
retro_atomic_store_release_int(&j->audio_closed, 1);
record_dispatch_mediacodec_notify(j);
return true;
}
bool record_dispatch_mediacodec_gpu_retired(
record_dispatch_mediacodec_t *j, int64_t end_us)
{
if (!j || end_us < 0)
return false;
if (retro_atomic_load_acquire_int(&j->gpu_done))
return j->video_end == end_us;
j->video_end = end_us;
retro_atomic_store_release_int(&j->gpu_done, 1);
record_dispatch_mediacodec_notify(j);
return true;
}
bool record_dispatch_mediacodec_gpu_released(record_dispatch_mediacodec_t *j)
{
if (!j || !retro_atomic_load_acquire_int(&j->gpu_done))
return false;
retro_atomic_store_release_int(&j->gpu_released, 1);
record_dispatch_mediacodec_notify(j);
return true;
}
void record_dispatch_mediacodec_cancel(record_dispatch_mediacodec_t *j)
{
if (j)
{
retro_atomic_store_release_int(&j->cancelled, 1);
record_dispatch_mediacodec_notify(j);
}
}
bool record_dispatch_mediacodec_cancelled(const record_dispatch_mediacodec_t *j)
{
return !j || retro_atomic_load_acquire_int(&j->cancelled)
|| retro_atomic_load_acquire_int(&j->error)
|| retro_atomic_load_acquire_int(&j->system_error);
}
enum record_dispatch_mediacodec_state record_dispatch_mediacodec_status(
const record_dispatch_mediacodec_t *j, int *error, int *file_error,
int *system_error)
{
if (error)
*error = j ? retro_atomic_load_acquire_int(&j->error)
: RECORD_MC_SESSION_OUTPUT;
if (file_error)
*file_error = j ? retro_atomic_load_acquire_int(&j->file_error) : 0;
if (system_error)
*system_error = j ? retro_atomic_load_acquire_int(&j->system_error) : 0;
return j
? (enum record_dispatch_mediacodec_state)retro_atomic_load_acquire_int(
&j->state)
: RECORD_MC_DISPATCH_FAILED;
}
bool record_dispatch_mediacodec_free(record_dispatch_mediacodec_t *j)
{
if (!j)
return true;
if (retro_atomic_load_acquire_int(&j->gpu_owner)
|| !retro_atomic_load_acquire_int(&j->done))
return false;
if (j->joinable && pthread_join(j->thread, NULL))
return false;
close(j->wake);
free(j->path);
free(j->mime);
free(j);
return true;
}
#else
bool record_dispatch_mediacodec_gpu_claim(record_dispatch_mediacodec_t *j)
{
(void)j;
return false;
}
void record_dispatch_mediacodec_gpu_release_claim(
record_dispatch_mediacodec_t *j)
{
(void)j;
}
bool record_dispatch_mediacodec_video_format(
const record_dispatch_mediacodec_t *j, unsigned *width, unsigned *height,
unsigned *color)
{
(void)j;
(void)width;
(void)height;
(void)color;
return false;
}
record_dispatch_mediacodec_t *record_dispatch_mediacodec_new(
const mediacodec_encoder_request_t *video, unsigned interval,
unsigned rate, unsigned channels, unsigned bitrate,
size_t max_audio_frames, size_t audio_capacity, const char *path)
{
(void)video;
(void)interval;
(void)rate;
(void)channels;
(void)bitrate;
(void)max_audio_frames;
(void)audio_capacity;
(void)path;
return NULL;
}
void *record_dispatch_mediacodec_surface(record_dispatch_mediacodec_t *j)
{
(void)j;
return NULL;
}
int record_dispatch_mediacodec_audio(record_dispatch_mediacodec_t *j,
const int16_t *samples, size_t frames, uint64_t timestamp)
{
(void)j;
(void)samples;
(void)frames;
(void)timestamp;
return RECORD_AUDIO_ERROR;
}
bool record_dispatch_mediacodec_audio_finish(
record_dispatch_mediacodec_t *j, uint64_t end)
{
(void)j;
(void)end;
return false;
}
bool record_dispatch_mediacodec_gpu_retired(
record_dispatch_mediacodec_t *j, int64_t end)
{
(void)j;
(void)end;
return false;
}
bool record_dispatch_mediacodec_gpu_released(record_dispatch_mediacodec_t *j)
{
(void)j;
return false;
}
void record_dispatch_mediacodec_notify(record_dispatch_mediacodec_t *j)
{
(void)j;
}
void record_dispatch_mediacodec_cancel(record_dispatch_mediacodec_t *j)
{
(void)j;
}
bool record_dispatch_mediacodec_cancelled(const record_dispatch_mediacodec_t *j)
{
(void)j;
return true;
}
enum record_dispatch_mediacodec_state record_dispatch_mediacodec_status(
const record_dispatch_mediacodec_t *j, int *error, int *file_error,
int *system_error)
{
(void)j;
if (error)
*error = RECORD_MC_SESSION_OUTPUT;
if (file_error)
*file_error = 0;
if (system_error)
*system_error = 0;
return RECORD_MC_DISPATCH_FAILED;
}
bool record_dispatch_mediacodec_free(record_dispatch_mediacodec_t *j)
{
return j == NULL;
}
#endif
+63
View File
@@ -0,0 +1,63 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier:
* GPL-3.0-or-later */
#ifndef RECORD_DISPATCH_MEDIACODEC_H
#define RECORD_DISPATCH_MEDIACODEC_H
#include "record_session_mediacodec.h"
RETRO_BEGIN_DECLS
typedef struct record_dispatch_mediacodec record_dispatch_mediacodec_t;
enum record_dispatch_mediacodec_state
{
RECORD_MC_DISPATCH_STARTING,
RECORD_MC_DISPATCH_READY,
RECORD_MC_DISPATCH_DRAINING,
RECORD_MC_DISPATCH_DONE,
RECORD_MC_DISPATCH_FAILED
};
/* Cold controller. Copies request/path and starts an independent I/O thread.
* JNI admission, codec/file startup and native finalization run there.
* Unsupported/threadless builds return NULL. GPU executor remains separate. */
record_dispatch_mediacodec_t *record_dispatch_mediacodec_new(
const mediacodec_encoder_request_t *video, unsigned interval,
unsigned rate, unsigned channels, unsigned bitrate,
size_t max_audio_frames, size_t audio_capacity, const char *path);
/* GPU executor, after READY. Window remains borrowed until gpu_released.
* No producer may use the session directly. */
void *record_dispatch_mediacodec_surface(record_dispatch_mediacodec_t *job);
/* Immutable cold configuration, valid until collection. */
bool record_dispatch_mediacodec_video_format(
const record_dispatch_mediacodec_t *job, unsigned *width,
unsigned *height, unsigned *color);
/* Cold GPU-executor construction/collection only. One owner per input window;
* claim survives native retirement until executor collection. */
bool record_dispatch_mediacodec_gpu_claim(record_dispatch_mediacodec_t *job);
void record_dispatch_mediacodec_gpu_release_claim(
record_dispatch_mediacodec_t *job);
/* PCM producer; STARTING is BUSY and copies nothing. Explicit sample clock. */
int record_dispatch_mediacodec_audio(record_dispatch_mediacodec_t *job,
const int16_t *samples, size_t frames, uint64_t timestamp);
/* PCM producer after last push returns, even on startup failure/cancellation.
*/
bool record_dispatch_mediacodec_audio_finish(
record_dispatch_mediacodec_t *job, uint64_t end_sample);
/* GPU executor, after producer closure and independent read retirement, then
* after Surface/context users retire. Required even if startup failed and no
* GPU resources were created. Source end is immutable. No native calls. */
bool record_dispatch_mediacodec_gpu_retired(
record_dispatch_mediacodec_t *job, int64_t end_us);
bool record_dispatch_mediacodec_gpu_released(record_dispatch_mediacodec_t *job);
/* Wake output service after GPU posting/retirement. Nonblocking notification.
*/
void record_dispatch_mediacodec_notify(record_dispatch_mediacodec_t *job);
void record_dispatch_mediacodec_cancel(record_dispatch_mediacodec_t *job);
bool record_dispatch_mediacodec_cancelled(
const record_dispatch_mediacodec_t *job);
/* DONE/FAILED mean worker-owned resources retired. Startup failure still
* awaits producer/GPU closure. error/file_error/system_error are optional. */
enum record_dispatch_mediacodec_state record_dispatch_mediacodec_status(
const record_dispatch_mediacodec_t *job, int *error, int *file_error,
int *system_error);
/* Cold controller, all callers quiescent. Refuses active resources. Detached
* worker normally needs no join; detach failure joins only after retirement. */
bool record_dispatch_mediacodec_free(record_dispatch_mediacodec_t *job);
RETRO_END_DECLS
#endif
+60 -11
View File
@@ -42,6 +42,7 @@
#include "drivers/record_wav.h"
#include "drivers/record_nvenc.h"
#include "drivers/record_metal.h"
#include "drivers/record_mediacodec.h"
#include "drivers/record_avfoundation.h"
static recording_state_t recording_state = {0};
@@ -62,6 +63,9 @@ const record_driver_t *record_drivers[] = {
#ifdef HAVE_RECORD_METAL
&record_metal,
#endif
#ifdef HAVE_RECORD_MEDIACODEC
&record_mediacodec,
#endif
#ifdef HAVE_FFMPEG
&record_ffmpeg,
#endif
@@ -75,7 +79,9 @@ const record_driver_t *record_drivers[] = {
void recording_gpu_frame_info(struct video_frame_info *info,bool capture)
{
#ifdef HAVE_RECORD_METAL
#if defined(HAVE_RECORD_MEDIACODEC)
record_mediacodec_frame_info(info, capture);
#elif defined(HAVE_RECORD_METAL)
record_metal_frame_info(info,capture);
#else
record_nvenc_frame_info(info,capture);
@@ -91,6 +97,7 @@ void recording_gpu_poll(void)
}
record_nvenc_poll();
record_metal_poll();
record_mediacodec_poll();
recording_gpu_reinit_poll();
}
void recording_start_frame(void)
@@ -103,19 +110,35 @@ void recording_start_frame(void)
if (!recording_init())
command_event(CMD_EVENT_RECORD_DEINIT, NULL);
}
void recording_gpu_stop(void) { record_nvenc_stop(); record_metal_stop(); }
bool recording_gpu_pending(void) { return record_nvenc_pending() || record_metal_pending(); }
void recording_gpu_stop(void)
{
record_nvenc_stop();
record_metal_stop();
record_mediacodec_stop();
}
bool recording_gpu_pending(void)
{
return record_nvenc_pending() || record_metal_pending()
|| record_mediacodec_pending();
}
bool recording_gpu_reset_pending(void)
{
return record_nvenc_pending() || record_metal_pending()
|| record_mediacodec_reset_pending();
}
bool recording_gpu_cancel_start(void)
{
bool nvenc=record_nvenc_cancel_start();
bool metal=record_metal_cancel_start();
return nvenc || metal;
bool mediacodec = record_mediacodec_cancel_start();
return nvenc || metal || mediacodec;
}
bool recording_gpu_defer_start(const record_driver_t *driver)
{
if (driver==&record_nvenc) return record_nvenc_defer_start();
if (driver==&record_metal) return record_metal_defer_start();
if (driver == &record_mediacodec) return record_mediacodec_defer_start();
return false;
}
@@ -282,7 +305,12 @@ bool recording_init(void)
bool recording_enable = recording_st->enable;
bool nvenc_native = string_is_equal(settings->arrays.record_driver,"nvenc");
bool metal_native = string_is_equal(settings->arrays.record_driver,"metal");
bool gpu_native = nvenc_native || metal_native;
bool mc_native = string_is_equal(settings->arrays.record_driver,"mediacodec");
bool gpu_native = nvenc_native || metal_native || mc_native;
bool menu_source = false;
#ifdef HAVE_RECORD_MEDIACODEC
menu_source = mc_native && current_core_type == CORE_TYPE_DUMMY;
#endif
if (!recording_enable)
return false;
@@ -308,6 +336,13 @@ bool recording_init(void)
RARCH_ERR("[Recording] Native Metal recording is unavailable in this build.\n");
return false;
}
#endif
#ifndef HAVE_RECORD_MEDIACODEC
if (mc_native)
{
RARCH_ERR("[Recording] Native MediaCodec recording is unavailable in this build.\n");
return false;
}
#endif
if (gpu_native) video_gpu_record = true;
if (video_driver_pix_fmt == RETRO_PIXEL_FORMAT_SCRGB_RGBA16F)
@@ -317,7 +352,7 @@ bool recording_init(void)
}
output[0] = '\0';
if (current_core_type == CORE_TYPE_DUMMY)
if (current_core_type == CORE_TYPE_DUMMY && !menu_source)
{
RARCH_WARN("[Recording] %s\n",
msg_hash_to_str(MSG_USING_LIBRETRO_DUMMY_CORE_RECORDING_SKIPPED));
@@ -432,6 +467,17 @@ bool recording_init(void)
params.filename = output;
params.fps = av_info->timing.fps;
params.samplerate = av_info->timing.sample_rate;
params.menu_source = menu_source;
if (menu_source)
{
/* Menu capture follows output timing, not dummy-core AV info. */
params.fps = settings->floats.video_refresh_rate;
if (!(params.fps > 0.0))
params.fps = video_driver_get_window_refresh_rate();
params.samplerate = audio_state_get_ptr()->out_rate;
params.channels = recording_st->channels = 2;
recording_st->layout = AUDIO_LAYOUT_STEREO;
}
/* XRGB2101010 source frames are down-converted to XRGB8888 before the
* recording path sees them (see video_driver_frame), so they record as
* ARGB8888 just like a native XRGB8888 core. */
@@ -465,15 +511,18 @@ bool recording_init(void)
struct video_viewport vp;
memset(&vp, 0, sizeof(vp));
if (!video_driver_get_viewport_info(&vp)) return false;
if (!VIDEO_SCALE_W(vp.dims) || !VIDEO_SCALE_H(vp.dims))
if (!VIDEO_SCALE_W(menu_source ? vp.full_dims : vp.dims)
|| !VIDEO_SCALE_H(menu_source ? vp.full_dims : vp.dims))
{
recording_st->driver=metal_native?&record_metal:&record_nvenc;
recording_st->driver = mc_native ? &record_mediacodec
: metal_native ? &record_metal : &record_nvenc;
return recording_gpu_defer_start(recording_st->driver);
}
params.out_dims = params.fb_dims = vp.dims;
params.out_dims = params.fb_dims = menu_source ? vp.full_dims : vp.dims;
params.pix_fmt = FFEMU_PIX_ARGB8888;
params.aspect_ratio = (float)VIDEO_SCALE_W(vp.dims) / VIDEO_SCALE_H(vp.dims);
recording_st->gpu_dims = vp.dims;
params.aspect_ratio = (float)VIDEO_SCALE_W(params.out_dims)
/ VIDEO_SCALE_H(params.out_dims);
recording_st->gpu_dims = params.out_dims;
recording_st->gpu_bgrx = false;
}
else if (video_gpu_record && video_st->current_video->read_viewport)
+12 -1
View File
@@ -94,6 +94,7 @@ struct record_params
bool allow_frame_drop;
/* Prefer a hardware H.264 encoder for the built-in presets. */
bool hw_encoder;
bool menu_source;
};
struct record_video_data
@@ -208,12 +209,16 @@ extern const record_driver_t *record_drivers[];
RETRO_BEGIN_DECLS
extern const record_driver_t record_nvenc;
extern const record_driver_t record_metal;
#if defined(HAVE_RECORD_MEDIACODEC) || defined(RECORD_MEDIACODEC_TEST)
extern const record_driver_t record_mediacodec;
#endif
struct video_frame_info;
void recording_gpu_frame_info(struct video_frame_info *info,bool capture);
void recording_gpu_poll(void);
void recording_start_frame(void);
void recording_gpu_stop(void);
bool recording_gpu_pending(void);
bool recording_gpu_reset_pending(void);
/* Main owner: coalesce reinit flags while native users retire. The existing
* GPU poll applies the reinit once, or drops it during content/quit teardown. */
bool recording_gpu_defer_reinit(int flags);
@@ -222,6 +227,12 @@ bool recording_gpu_defer_start(const record_driver_t *driver);
bool recording_gpu_cancel_start(void);
RETRO_END_DECLS
static INLINE bool recording_is_native_gpu(const record_driver_t *driver)
{ return driver==&record_nvenc || driver==&record_metal; }
{
#if defined(HAVE_RECORD_MEDIACODEC) || defined(RECORD_MEDIACODEC_TEST)
if (driver == &record_mediacodec)
return true;
#endif
return driver == &record_nvenc || driver == &record_metal;
}
#endif
+410
View File
@@ -0,0 +1,410 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier:
* GPL-3.0-or-later */
#ifdef HAVE_CONFIG_H
#include "../config.h"
#endif
#include "record_gpu_dispatch_mediacodec.h"
#include <retro_atomic.h>
#include <stdlib.h>
#include <limits.h>
#if defined(HAVE_THREADS) && defined(RETRO_ATOMIC_LOCK_FREE) \
&& (defined(__ANDROID__) \
|| defined(RECORD_MEDIACODEC_GPU_DISPATCH_TEST))
#include <pthread.h>
#include <sys/eventfd.h>
#include <poll.h>
#include <unistd.h>
#include <errno.h>
struct record_gpu_dispatch_mediacodec
{
record_gpu_mediacodec_config_t config;
uint64_t last_pts, last_end;
int64_t end_us;
record_dispatch_mediacodec_t *io;
record_gpu_queue_t *queue;
record_mediacodec_surface_t *surface;
record_android_capture_gl_t *capture;
record_android_capture_draw_t *draw;
record_mediacodec_gpu_worker_t *worker;
pthread_t thread;
retro_atomic_int_t state, done, epoch, closed, cancelled, error,
system_error;
int wake;
bool joinable, initialized, have_frame, reads_retired;
};
static void record_mgd_fail(record_gpu_dispatch_mediacodec_t *j, int error)
{
retro_atomic_cas_int(&j->error, 0, error);
retro_atomic_store_release_int(&j->cancelled, 1);
retro_atomic_store_release_int(&j->state, RECORD_MC_DISPATCH_DRAINING);
record_dispatch_mediacodec_cancel(j->io);
}
static void record_mgd_system(record_gpu_dispatch_mediacodec_t *j, int error)
{
retro_atomic_cas_int(&j->system_error, 0, error ? error : EIO);
retro_atomic_store_release_int(&j->cancelled, 1);
}
static void record_mgd_notify(record_gpu_dispatch_mediacodec_t *j)
{
uint64_t one = 1;
if ((retro_atomic_fetch_add_seq_cst_int(&j->epoch, 2) & 1)
&& write(j->wake, &one, sizeof(one)) != (ssize_t)sizeof(one)
&& errno != EAGAIN)
record_mgd_system(j, errno);
}
static void record_mgd_close_fd(void *context, int fd)
{
record_gpu_dispatch_mediacodec_t *j
= (record_gpu_dispatch_mediacodec_t *)context;
if (close(fd) && errno != EINTR)
record_mgd_system(j, errno);
}
static bool record_mgd_start(record_gpu_dispatch_mediacodec_t *j)
{
void *window = record_dispatch_mediacodec_surface(j->io);
record_gpu_mediacodec_config_t *c = &j->config;
bool ok = false;
if (!window
|| !record_mediacodec_surface_new(&c->surface, c->display, c->config,
c->share, (EGLNativeWindowType)window, c->width, c->height,
c->mode != 0, &j->surface))
return false;
if (!record_mediacodec_surface_begin(j->surface))
return false;
if (record_android_capture_gl_new(
c->display, &c->capture, c->buffers, c->count, &j->capture))
j->draw = record_android_capture_draw_new(
&c->draw, c->width, c->height, c->mode, c->flip_y);
if (j->draw)
j->worker = record_mediacodec_gpu_worker_new(j->queue, j->surface,
j->capture, j->draw, c->count, record_mgd_close_fd, j);
ok = j->worker != NULL;
return record_mediacodec_surface_leave(j->surface) && ok;
}
static int record_mgd_cleanup(record_gpu_dispatch_mediacodec_t *j)
{
if (j->worker)
{
if (!record_mediacodec_gpu_worker_free(j->worker))
return 0;
j->worker = NULL;
}
if (j->capture || j->draw)
{
if (!record_mediacodec_surface_begin_retirement(j->surface))
return 0;
record_android_capture_draw_free(j->draw, false);
j->draw = NULL;
if (!record_android_capture_gl_free(j->capture))
{
record_mediacodec_surface_leave(j->surface);
return 0;
}
j->capture = NULL;
}
if (j->surface)
{
if (!record_mediacodec_surface_leave(j->surface)
|| !record_mediacodec_surface_free(j->surface))
return 0;
j->surface = NULL;
}
if (!record_dispatch_mediacodec_gpu_released(j->io))
return 0;
return 2;
}
static int record_mgd_service(record_gpu_dispatch_mediacodec_t *j)
{
enum record_dispatch_mediacodec_state io_state;
int result;
if (retro_atomic_load_acquire_int(&j->system_error)
|| record_dispatch_mediacodec_cancelled(j->io)
|| retro_atomic_load_acquire_int(&j->cancelled))
record_mgd_fail(j, RECORD_MC_SESSION_CANCELLED);
if (j->reads_retired)
return record_mgd_cleanup(j);
if (!j->initialized && !retro_atomic_load_acquire_int(&j->cancelled))
{
io_state = record_dispatch_mediacodec_status(j->io, NULL, NULL, NULL);
if (io_state == RECORD_MC_DISPATCH_STARTING)
return 0;
j->initialized = true;
if (io_state != RECORD_MC_DISPATCH_READY || !record_mgd_start(j))
record_mgd_fail(j, RECORD_MC_SESSION_OUTPUT);
else
retro_atomic_store_release_int(&j->state, RECORD_MC_DISPATCH_READY);
}
if (j->worker)
{
result = retro_atomic_load_acquire_int(&j->cancelled)
? record_mediacodec_gpu_worker_retire_step(j->worker)
: record_mediacodec_gpu_worker_step(j->worker);
record_dispatch_mediacodec_notify(j->io);
if (result < 0)
{
record_mgd_fail(j, RECORD_MC_SESSION_OUTPUT);
return 0;
}
if (result != 2)
return result;
}
else if (!retro_atomic_load_acquire_int(&j->cancelled))
return 0;
if (!retro_atomic_load_acquire_int(&j->closed))
return 0;
retro_atomic_store_release_int(&j->state, RECORD_MC_DISPATCH_DRAINING);
if (!record_dispatch_mediacodec_gpu_retired(j->io, j->end_us))
{
record_mgd_fail(j, RECORD_MC_SESSION_OUTPUT);
return 0;
}
j->reads_retired = true;
return record_mgd_cleanup(j);
}
static void *record_mgd_run(void *context)
{
record_gpu_dispatch_mediacodec_t *j
= (record_gpu_dispatch_mediacodec_t *)context;
struct pollfd descriptor;
uint64_t value;
int result, waited;
descriptor.fd = j->wake;
descriptor.events = POLLIN;
for (;;)
{
result = record_mgd_service(j);
if (result == 2)
break;
if (result == 1)
continue;
retro_atomic_fetch_add_seq_cst_int(&j->epoch, 1);
result = record_mgd_service(j);
if (result)
{
retro_atomic_fetch_and_int(&j->epoch, ~1);
if (result == 2)
break;
continue;
}
descriptor.revents = 0;
if (retro_atomic_load_acquire_int(&j->system_error))
descriptor.fd = -1;
waited = poll(&descriptor, 1, 1);
retro_atomic_fetch_and_int(&j->epoch, ~1);
if (waited < 0 && errno != EINTR)
record_mgd_system(j, errno);
else if (descriptor.revents & (POLLERR | POLLHUP | POLLNVAL))
record_mgd_system(j, EIO);
else if (descriptor.revents & POLLIN)
{
if (read(j->wake, &value, sizeof(value)) != (ssize_t)sizeof(value)
&& errno != EAGAIN && errno != EINTR)
record_mgd_system(j, errno);
}
}
retro_atomic_store_release_int(&j->state,
retro_atomic_load_acquire_int(&j->error)
|| retro_atomic_load_acquire_int(&j->system_error)
? RECORD_MC_DISPATCH_FAILED
: RECORD_MC_DISPATCH_DONE);
retro_atomic_store_release_int(&j->done, 1);
return NULL;
}
record_gpu_dispatch_mediacodec_t *record_gpu_dispatch_mediacodec_new(
record_dispatch_mediacodec_t *io, const record_gpu_mediacodec_config_t *c)
{
record_gpu_dispatch_mediacodec_t *j;
unsigned width, height, color, i;
if (!io || !c || !c->count || c->count > RECORD_GPU_QUEUE_SLOTS
|| c->mode > 2 || c->display == EGL_NO_DISPLAY || !c->config
|| !record_dispatch_mediacodec_video_format(
io, &width, &height, &color)
|| width != c->width || height != c->height
|| color != (c->mode ? 1u : 0u))
return NULL;
for (i = 0; i < c->count; i++)
if (!c->buffers[i])
return NULL;
if (!record_dispatch_mediacodec_gpu_claim(io))
return NULL;
j = (record_gpu_dispatch_mediacodec_t *)calloc(1, sizeof(*j));
if (!j)
{
record_dispatch_mediacodec_gpu_release_claim(io);
return NULL;
}
j->wake = -1;
j->config = *c;
j->io = io;
j->queue = record_gpu_queue_new(c->count);
if (!j->queue)
goto failed;
retro_atomic_int_init(&j->state, RECORD_MC_DISPATCH_STARTING);
retro_atomic_int_init(&j->done, 0);
retro_atomic_int_init(&j->epoch, 0);
retro_atomic_int_init(&j->closed, 0);
retro_atomic_int_init(&j->cancelled, 0);
retro_atomic_int_init(&j->error, 0);
retro_atomic_int_init(&j->system_error, 0);
j->wake = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
if (j->wake < 0 || pthread_create(&j->thread, NULL, record_mgd_run, j))
goto failed;
j->joinable = pthread_detach(j->thread) != 0;
return j;
failed:
if (j->wake >= 0)
close(j->wake);
record_gpu_queue_free(j->queue);
record_dispatch_mediacodec_gpu_release_claim(io);
free(j);
return NULL;
}
int record_gpu_dispatch_mediacodec_acquire(
record_gpu_dispatch_mediacodec_t *j, unsigned *slot)
{
int state;
if (!j || retro_atomic_load_acquire_int(&j->closed)
|| retro_atomic_load_acquire_int(&j->cancelled)
|| record_dispatch_mediacodec_cancelled(j->io))
return RECORD_GPU_QUEUE_ERROR;
state = retro_atomic_load_acquire_int(&j->state);
if (state != RECORD_MC_DISPATCH_READY)
return state == RECORD_MC_DISPATCH_STARTING ? RECORD_GPU_QUEUE_BUSY
: RECORD_GPU_QUEUE_ERROR;
return record_gpu_queue_acquire(j->queue, slot);
}
bool record_gpu_dispatch_mediacodec_submit(
record_gpu_dispatch_mediacodec_t *j, const record_gpu_frame_t *frame)
{
uint64_t maximum = (((uint64_t)1 << 63) - 1) / 1000;
if (!j || !frame || frame->keyframe || !frame->duration
|| frame->timestamp > maximum
|| frame->duration > maximum - frame->timestamp
|| frame->ready_value > (uint64_t)INT_MAX + 1
|| (j->have_frame && frame->timestamp <= j->last_pts)
|| retro_atomic_load_acquire_int(&j->closed)
|| retro_atomic_load_acquire_int(&j->cancelled)
|| record_dispatch_mediacodec_cancelled(j->io)
|| retro_atomic_load_acquire_int(&j->state) != RECORD_MC_DISPATCH_READY
|| !record_gpu_queue_submit(j->queue, frame))
return false;
j->last_pts = frame->timestamp;
j->last_end = frame->timestamp + frame->duration;
j->have_frame = true;
record_mgd_notify(j);
return true;
}
bool record_gpu_dispatch_mediacodec_cancel_slot(
record_gpu_dispatch_mediacodec_t *j)
{
return j && record_gpu_queue_cancel(j->queue);
}
bool record_gpu_dispatch_mediacodec_finish(
record_gpu_dispatch_mediacodec_t *j, int64_t end_us)
{
if (!j || end_us < 0 || (uint64_t)end_us < j->last_end)
return false;
if (retro_atomic_load_acquire_int(&j->closed))
return j->end_us == end_us;
if (!record_gpu_queue_finish(j->queue))
return false;
j->end_us = end_us;
retro_atomic_store_release_int(&j->closed, 1);
record_mgd_notify(j);
return true;
}
void record_gpu_dispatch_mediacodec_cancel(record_gpu_dispatch_mediacodec_t *j)
{
if (j)
{
retro_atomic_store_release_int(&j->cancelled, 1);
record_dispatch_mediacodec_cancel(j->io);
record_mgd_notify(j);
}
}
enum record_dispatch_mediacodec_state record_gpu_dispatch_mediacodec_status(
const record_gpu_dispatch_mediacodec_t *j, int *error, int *system_error)
{
if (error)
*error = j ? retro_atomic_load_acquire_int(&j->error)
: RECORD_MC_SESSION_OUTPUT;
if (system_error)
*system_error = j ? retro_atomic_load_acquire_int(&j->system_error) : 0;
return j
? (enum record_dispatch_mediacodec_state)retro_atomic_load_acquire_int(
&j->state)
: RECORD_MC_DISPATCH_FAILED;
}
bool record_gpu_dispatch_mediacodec_free(record_gpu_dispatch_mediacodec_t *j)
{
if (!j)
return true;
if (!retro_atomic_load_acquire_int(&j->done))
return false;
if (j->joinable)
{
if (pthread_join(j->thread, NULL))
return false;
j->joinable = false;
}
if (!record_gpu_queue_free(j->queue))
return false;
record_dispatch_mediacodec_gpu_release_claim(j->io);
close(j->wake);
free(j);
return true;
}
#else
record_gpu_dispatch_mediacodec_t *record_gpu_dispatch_mediacodec_new(
record_dispatch_mediacodec_t *io, const record_gpu_mediacodec_config_t *c)
{
(void)io;
(void)c;
return NULL;
}
int record_gpu_dispatch_mediacodec_acquire(
record_gpu_dispatch_mediacodec_t *j, unsigned *slot)
{
(void)j;
(void)slot;
return RECORD_GPU_QUEUE_ERROR;
}
bool record_gpu_dispatch_mediacodec_submit(
record_gpu_dispatch_mediacodec_t *j, const record_gpu_frame_t *frame)
{
(void)j;
(void)frame;
return false;
}
bool record_gpu_dispatch_mediacodec_cancel_slot(
record_gpu_dispatch_mediacodec_t *j)
{
(void)j;
return false;
}
bool record_gpu_dispatch_mediacodec_finish(
record_gpu_dispatch_mediacodec_t *j, int64_t end)
{
(void)j;
(void)end;
return false;
}
void record_gpu_dispatch_mediacodec_cancel(record_gpu_dispatch_mediacodec_t *j)
{
(void)j;
}
enum record_dispatch_mediacodec_state record_gpu_dispatch_mediacodec_status(
const record_gpu_dispatch_mediacodec_t *j, int *error, int *system_error)
{
(void)j;
if (error)
*error = RECORD_MC_SESSION_OUTPUT;
if (system_error)
*system_error = 0;
return RECORD_MC_DISPATCH_FAILED;
}
bool record_gpu_dispatch_mediacodec_free(record_gpu_dispatch_mediacodec_t *j)
{
return j == NULL;
}
#endif
+55
View File
@@ -0,0 +1,55 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier:
* GPL-3.0-or-later */
#ifndef RECORD_GPU_DISPATCH_MEDIACODEC_H
#define RECORD_GPU_DISPATCH_MEDIACODEC_H
#include "record_dispatch_mediacodec.h"
#include "record_mediacodec_gpu_worker.h"
RETRO_BEGIN_DECLS
typedef struct record_gpu_dispatch_mediacodec record_gpu_dispatch_mediacodec_t;
typedef struct record_gpu_mediacodec_config
{
record_mediacodec_surface_ops_t surface;
record_android_capture_gl_ops_t capture;
record_android_capture_draw_ops_t draw;
struct AHardwareBuffer *buffers[RECORD_GPU_QUEUE_SLOTS];
EGLDisplay display;
EGLConfig config;
EGLContext share;
unsigned width, height, count, mode;
bool flip_y;
} record_gpu_mediacodec_config_t;
/* Cold controller: creates a separate GLES thread and fixed queue. Borrows
* I/O job, display, share group and buffers until collection. Request
* dimensions and SDR/PQ metadata must match I/O configuration. Buffers come
* from a validated fixed pool: exact dimensions and RGB8/PQ RGB10/scRGB FP16
* for modes 0/1/2. No ownership on NULL. */
record_gpu_dispatch_mediacodec_t *record_gpu_dispatch_mediacodec_new(
record_dispatch_mediacodec_t *io,
const record_gpu_mediacodec_config_t *config);
/* One render producer. READY only; BUSY takes nothing. Submit transfers fd+1
* encoded in ready_value only on success. Positive duration, increasing source
* microseconds and no keyframe request. Failed submit retains reservation/fd.
*/
int record_gpu_dispatch_mediacodec_acquire(
record_gpu_dispatch_mediacodec_t *job, unsigned *slot);
bool record_gpu_dispatch_mediacodec_submit(
record_gpu_dispatch_mediacodec_t *job, const record_gpu_frame_t *frame);
bool record_gpu_dispatch_mediacodec_cancel_slot(
record_gpu_dispatch_mediacodec_t *job);
/* Same producer, after last call returns, even on startup failure/cancel.
* Refuses a reservation or an end before the last submitted picture end. */
bool record_gpu_dispatch_mediacodec_finish(
record_gpu_dispatch_mediacodec_t *job, int64_t end_us);
void record_gpu_dispatch_mediacodec_cancel(
record_gpu_dispatch_mediacodec_t *job);
/* Same state values as I/O dispatch. DONE/FAILED retire only GLES users; the
* renderer must independently retire Vulkan copy/source leases. Errors with
* unproven reads/native destruction failures retain DRAINING ownership. */
enum record_dispatch_mediacodec_state record_gpu_dispatch_mediacodec_status(
const record_gpu_dispatch_mediacodec_t *job, int *error,
int *system_error);
/* Cold controller, producer quiescent; no active-thread join. I/O job and
* shared native resources must remain alive through successful collection. */
bool record_gpu_dispatch_mediacodec_free(record_gpu_dispatch_mediacodec_t *job);
RETRO_END_DECLS
#endif
+2 -2
View File
@@ -11,7 +11,7 @@ static int recording_reinit_flags;
bool recording_gpu_defer_reinit(int flags)
{
if (!recording_reinit_flags
&& (!(flags & DRIVER_VIDEO_MASK) || !recording_gpu_pending()))
&& (!(flags & DRIVER_VIDEO_MASK) || !recording_gpu_reset_pending()))
return false;
recording_reinit_flags |= flags;
recording_gpu_stop();
@@ -31,7 +31,7 @@ void recording_gpu_reinit_poll(void)
return;
}
recording_gpu_stop();
if (recording_gpu_pending())
if (recording_gpu_reset_pending())
return;
flags = recording_reinit_flags;
recording_reinit_flags = 0;
+52 -2
View File
@@ -110,15 +110,31 @@ struct record_mediacodec_encoder
int profile, base_profile;
unsigned char hdr_static[25];
bool started, failed, held, input_eos, output_eos;
bool audio, seen_input, closing;
bool audio, seen_input, closing, stop_attempted, stopped;
};
bool record_mediacodec_encoder_stop(record_mediacodec_encoder_t *s)
{
if (!s)
return false;
if (s->stop_attempted)
return s->stopped;
s->stop_attempted = true;
s->failed = true;
s->held = false;
memset(&s->packet, 0, sizeof(s->packet));
if (!s->started || s->stop(s->codec) != AMEDIA_OK)
return false;
s->started = false;
s->stopped = true;
return true;
}
void record_mediacodec_encoder_free(record_mediacodec_encoder_t *s)
{
if (!s)
return;
if (s->codec)
{
if (s->started)
if (s->started && !s->stop_attempted)
s->stop(s->codec);
s->delete_codec(s->codec);
}
@@ -303,6 +319,8 @@ record_mediacodec_encoder_t *record_mediacodec_encoder_audio_new(
RECORD_AAC_LOAD(set_integer, record_mc_integer_fn, "AMediaFormat_setInt32");
RECORD_AAC_LOAD(
s->get_integer, record_mc_get_integer_fn, "AMediaFormat_getInt32");
RECORD_AAC_LOAD(
s->get_buffer, record_mc_get_buffer_fn, "AMediaFormat_getBuffer");
RECORD_AAC_LOAD(
s->input, record_mc_input_fn, "AMediaCodec_dequeueInputBuffer");
RECORD_AAC_LOAD(
@@ -448,6 +466,23 @@ void *record_mediacodec_encoder_format(record_mediacodec_encoder_t *s)
{
return s && !s->failed ? s->format : NULL;
}
bool record_mediacodec_encoder_audio_config(record_mediacodec_encoder_t *s,
const void **data, size_t *bytes)
{
void *buffer = NULL;
size_t size = 0;
if (data)
*data = NULL;
if (bytes)
*bytes = 0;
if (!s || !data || !bytes || s->failed || !s->audio || !s->format
|| !s->get_buffer(s->format, "csd-0", &buffer, &size)
|| !buffer || !size)
return false;
*data = buffer;
*bytes = size;
return true;
}
bool record_mediacodec_encoder_finish(record_mediacodec_encoder_t *s)
{
if (!s || s->failed || s->audio)
@@ -631,6 +666,16 @@ void *record_mediacodec_encoder_format(record_mediacodec_encoder_t *s)
(void)s;
return NULL;
}
bool record_mediacodec_encoder_audio_config(record_mediacodec_encoder_t *s,
const void **data, size_t *bytes)
{
(void)s;
if (data)
*data = NULL;
if (bytes)
*bytes = 0;
return false;
}
int record_mediacodec_encoder_pump(
record_mediacodec_encoder_t *s, record_mediacodec_packet_t *packet)
{
@@ -649,6 +694,11 @@ bool record_mediacodec_encoder_finish(record_mediacodec_encoder_t *s)
(void)s;
return false;
}
bool record_mediacodec_encoder_stop(record_mediacodec_encoder_t *s)
{
(void)s;
return false;
}
void record_mediacodec_encoder_free(record_mediacodec_encoder_t *s)
{
(void)s;
+10
View File
@@ -51,9 +51,19 @@ void *record_mediacodec_encoder_surface(record_mediacodec_encoder_t *encoder);
int record_mediacodec_encoder_pump(
record_mediacodec_encoder_t *encoder, record_mediacodec_packet_t *packet);
void *record_mediacodec_encoder_format(record_mediacodec_encoder_t *encoder);
/* AAC configuration borrowed for the current output format. */
bool record_mediacodec_encoder_audio_config(
record_mediacodec_encoder_t *encoder, const void **data, size_t *bytes);
bool record_mediacodec_encoder_release(record_mediacodec_encoder_t *encoder);
/* After the last Surface submission; successful EOS is idempotent. */
bool record_mediacodec_encoder_finish(record_mediacodec_encoder_t *encoder);
/* Terminal cancellation on the owning I/O worker, after its pump/mux borrows
* are quiescent. Native stop may block this worker. Retains codec, libraries
* and input window until free; previously borrowed window remains owned.
* Invalidates packet/format access. Same-result retries do not call stop again.
* Neither success nor failure proves GPU reads retired. Free only after GPU
* producer and Surface/context retirement. Never call from frame/audio/task. */
bool record_mediacodec_encoder_stop(record_mediacodec_encoder_t *encoder);
void record_mediacodec_encoder_free(record_mediacodec_encoder_t *encoder);
RETRO_END_DECLS
#endif
+4
View File
@@ -4,6 +4,7 @@
#define RECORD_MEDIACODEC_FILE_H
#include <boolean.h>
#include <retro_common_api.h>
#include <stdint.h>
RETRO_BEGIN_DECLS
typedef struct record_mediacodec_file record_mediacodec_file_t;
/* Cold I/O worker only: creation and final sync/close can block this worker.
@@ -20,6 +21,9 @@ record_mediacodec_file_t *record_mediacodec_file_adopt_fresh(
int *owned_fd, int *error);
/* Borrowed until close; destroy the muxer and stop all users first. */
int record_mediacodec_file_descriptor(const record_mediacodec_file_t *file);
/* Cold I/O owner, after destroying the mux. Rewrites only trailing moov. */
bool record_mediacodec_file_audio_edit(record_mediacodec_file_t *file,
uint64_t skip, uint64_t frames, unsigned roll, int *error);
/* Cold I/O owner, consumes file even on error. complete requests final fsync
* after successful mux finalization. Closes once, including on EINTR: never
* retry a descriptor number that may already have been reused. */
+140
View File
@@ -0,0 +1,140 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier:
* GPL-3.0-or-later */
#include "record_mediacodec_file.h"
#include "record_mp4_audio_edit.h"
#include <errno.h>
#include <stdlib.h>
#if defined(__ANDROID__) || defined(RECORD_MEDIACODEC_FILE_TEST)
#include <stdio.h>
#include <string.h>
#ifdef _WIN32
#include <io.h>
#define record_mcfe_seek _lseeki64
#define record_mcfe_read _read
#define record_mcfe_write _write
#else
#include <unistd.h>
#ifdef __ANDROID__
#define record_mcfe_seek lseek64
#else
#define record_mcfe_seek lseek
#endif
#define record_mcfe_read read
#define record_mcfe_write write
#endif
static uint64_t record_mcfe_uint(const unsigned char *p, unsigned bytes)
{
uint64_t value = 0;
unsigned i;
for (i = 0; i < bytes; i++)
value = (value << 8) | p[i];
return value;
}
static bool record_mcfe_transfer(int fd, unsigned char *buffer,
size_t bytes, bool writing)
{
while (bytes)
{
int64_t count = writing ? record_mcfe_write(fd, buffer, (unsigned)bytes)
: record_mcfe_read(fd, buffer, (unsigned)bytes);
if (count <= 0 || (uint64_t)count > bytes)
return false;
buffer += (size_t)count;
bytes -= (size_t)count;
}
return true;
}
bool record_mediacodec_file_audio_edit(record_mediacodec_file_t *file,
uint64_t skip, uint64_t frames, unsigned roll, int *error)
{
unsigned char header[16];
unsigned char *source = NULL, *output = NULL;
uint64_t at = 0, size;
int64_t end;
size_t written = 0;
unsigned boxes, head;
int fd = record_mediacodec_file_descriptor(file);
int saved = EINVAL;
bool complete = false;
if (error)
*error = 0;
if (fd < 0 || !skip || !roll)
goto done;
end = record_mcfe_seek(fd, 0, SEEK_END);
if (end < 8)
goto done;
for (boxes = 0; boxes < 4096 && at < (uint64_t)end; boxes++)
{
head = 8;
if ((uint64_t)end - at < 8
|| record_mcfe_seek(fd, (int64_t)at, SEEK_SET) != (int64_t)at
|| !record_mcfe_transfer(fd, header, 8, false))
goto done;
size = record_mcfe_uint(header, 4);
if (size == 1)
{
if ((uint64_t)end - at < 16
|| !record_mcfe_transfer(fd, header + 8, 8, false))
goto done;
head = 16;
size = record_mcfe_uint(header + 8, 8);
}
if (size < head || size > (uint64_t)end - at)
goto done;
if (!memcmp(header + 4, "moov", 4))
break;
at += size;
}
if (boxes == 4096 || at == (uint64_t)end
|| size != (uint64_t)end - at || size > 64 * 1024 * 1024)
goto done;
source = (unsigned char *)malloc((size_t)size);
if (!source)
{
saved = ENOMEM;
goto done;
}
if (record_mcfe_seek(fd, (int64_t)at, SEEK_SET) != (int64_t)at
|| !record_mcfe_transfer(fd, source, (size_t)size, false)
|| !record_mp4_audio_edit(source, (size_t)size, skip, frames,
roll, NULL, 0, &written)
|| written < size || written > 64 * 1024 * 1024
|| (uint64_t)written > (((uint64_t)1 << 63) - 1) - at)
goto done;
output = (unsigned char *)malloc(written);
if (!output)
{
saved = ENOMEM;
goto done;
}
if (!record_mp4_audio_edit(source, (size_t)size, skip, frames,
roll, output, written, &written))
goto done;
saved = EIO;
if (record_mcfe_seek(fd, (int64_t)at, SEEK_SET) != (int64_t)at
|| !record_mcfe_transfer(fd, output, written, true))
goto done;
complete = true;
done:
free(output);
free(source);
if (error)
*error = complete ? 0 : saved;
return complete;
}
#else
bool record_mediacodec_file_audio_edit(record_mediacodec_file_t *file,
uint64_t skip, uint64_t frames, unsigned roll, int *error)
{
(void)file;
(void)skip;
(void)frames;
(void)roll;
if (error)
*error = ENOSYS;
return false;
}
#endif
+2 -1
View File
@@ -59,7 +59,8 @@ static int record_mcg_poll(record_mediacodec_gpu_worker_t *w)
if (!(w->failed ? record_mediacodec_surface_begin_retirement(w->surface)
: record_mediacodec_surface_begin(w->surface)))
return -1;
result = record_android_capture_gl_poll(w->capture, slot);
result = w->failed ? record_android_capture_gl_recover_step(w->capture)
: record_android_capture_gl_poll(w->capture, slot);
if (!record_mediacodec_surface_leave(w->surface))
return -1;
if (result == 1)
+4 -3
View File
@@ -29,9 +29,10 @@ int record_mediacodec_gpu_worker_step(record_mediacodec_gpu_worker_t *worker);
* Can be called after any error; no further draws/posts occur. */
int record_mediacodec_gpu_worker_retire_step(
record_mediacodec_gpu_worker_t *worker);
/* ONLY after matching context destruction and independent GPU-read completion
* proof or device loss. Clears GL bookkeeping and returns pending slots without
* GL calls. Queued descriptors must still drain through retire_step. */
/* ONLY after matching context/owned GL namespace destruction and independent
* GPU-read completion proof or device loss. Clears GL bookkeeping and returns
* pending slots without GL calls. Queued descriptors must still drain through
* retire_step. */
void record_mediacodec_gpu_worker_after_context_retired(
record_mediacodec_gpu_worker_t *worker);
/* Borrowed components remain caller-owned. Refuses active/undrained service. */
+108 -11
View File
@@ -5,18 +5,56 @@
struct record_mediacodec_worker
{
uint64_t audio_end_sample;
uint64_t audio_end_sample, coded_frames;
int64_t end_us[2];
record_mediacodec_encoder_t *encoder[2];
record_audio_queue_t *pcm;
record_mux_mediacodec_t *mux;
size_t consumed;
record_aac_probe_t *probe;
size_t consumed, prefix_sent;
unsigned channels, rate;
bool format[2], drained[2], ended[2];
bool active, closing, video_eos, audio_eos, pcm_end;
bool started, finished, failed;
};
static bool record_mcw_samples_us(uint64_t frames, unsigned rate,
int64_t *timestamp)
{
uint64_t maximum = ((uint64_t)1 << 63) - 1;
uint64_t seconds = frames / rate;
uint64_t remainder = (frames % rate) * 1000000 / rate;
if (seconds > maximum / 1000000
|| seconds * 1000000 > maximum - remainder)
return false;
*timestamp = (int64_t)(seconds * 1000000 + remainder);
return true;
}
bool record_mediacodec_worker_calibrate(record_mediacodec_worker_t *w,
record_aac_probe_t *probe)
{
if (!w || !probe || !w->encoder[1] || w->probe || w->active
|| w->closing || w->failed)
return false;
w->probe = probe;
return true;
}
bool record_mediacodec_worker_audio_edit(
const record_mediacodec_worker_t *w, uint64_t *skip,
uint64_t *frames, unsigned *roll)
{
unsigned delay;
if (!w || !skip || !frames || !roll || !w->finished || w->failed
|| !w->probe || !record_aac_probe_delay(w->probe, &delay))
return false;
*skip = RECORD_AAC_PROBE_FRAMES + (uint64_t)delay;
*frames = w->audio_end_sample;
*roll = delay ? (delay - 1) / 1024 + 1 : 1;
return true;
}
record_mediacodec_worker_t *record_mediacodec_worker_new(
record_mediacodec_encoder_t *video, record_mediacodec_encoder_t *audio,
record_audio_queue_t *pcm, record_mux_mediacodec_t *mux,
@@ -42,7 +80,6 @@ record_mediacodec_worker_t *record_mediacodec_worker_new(
bool record_mediacodec_worker_finish(record_mediacodec_worker_t *w,
int64_t video_end_us, uint64_t audio_end_sample)
{
uint64_t seconds, remainder, maximum = ((uint64_t)1 << 63) - 1;
if (!w || w->failed || video_end_us <= 0)
return false;
if (w->closing)
@@ -50,12 +87,12 @@ bool record_mediacodec_worker_finish(record_mediacodec_worker_t *w,
&& w->audio_end_sample == audio_end_sample;
if (w->encoder[1])
{
seconds = audio_end_sample / w->rate;
remainder = (audio_end_sample % w->rate) * 1000000 / w->rate;
if (!audio_end_sample || seconds > maximum / 1000000
|| seconds * 1000000 > maximum - remainder)
if ((!audio_end_sample && !w->probe)
|| (w->probe && audio_end_sample
> (uint64_t)-1 - RECORD_AAC_PROBE_FRAMES)
|| !record_mcw_samples_us(audio_end_sample, w->rate,
&w->end_us[1]))
return false;
w->end_us[1] = (int64_t)(seconds * 1000000 + remainder);
}
w->end_us[0] = video_end_us;
w->audio_end_sample = audio_end_sample;
@@ -77,6 +114,15 @@ static int record_mcw_output(record_mediacodec_worker_t *w, unsigned at)
case RECORD_MC_PROGRESS:
return 1;
case RECORD_MC_FORMAT:
if (at && w->probe)
{
const void *config = NULL;
size_t bytes = 0;
if (!record_mediacodec_encoder_audio_config(w->encoder[at],
&config, &bytes)
|| !record_aac_probe_config(w->probe, config, bytes))
return -1;
}
if (w->format[at] || !record_mux_mediacodec_track(w->mux, at != 0,
record_mediacodec_encoder_format(w->encoder[at])))
return -1;
@@ -87,6 +133,16 @@ static int record_mcw_output(record_mediacodec_worker_t *w, unsigned at)
return -1;
if (!w->started)
return 0;
if (at && w->probe && !(packet.flags & 2))
{
if (w->coded_frames > (uint64_t)-1 - 1024
|| !record_mcw_samples_us(w->coded_frames, w->rate,
&packet.pts_us)
|| !record_aac_probe_packet(w->probe, packet.data,
packet.size))
return -1;
w->coded_frames += 1024;
}
if (!record_mux_mediacodec_write(w->mux, at != 0, &packet)
|| !record_mediacodec_encoder_release(w->encoder[at]))
return -1;
@@ -110,6 +166,20 @@ static int record_mcw_input(record_mediacodec_worker_t *w)
int result;
if (!w->encoder[1] || w->audio_eos)
return 0;
if (w->probe && w->prefix_sent < RECORD_AAC_PROBE_FRAMES)
{
samples = record_aac_probe_input(w->probe, w->prefix_sent, &frames);
if (!samples || !frames
|| frames > RECORD_AAC_PROBE_FRAMES - w->prefix_sent)
return -1;
result = record_mediacodec_encoder_audio_submit(w->encoder[1],
samples, frames, w->prefix_sent, &accepted);
if (result < 0 || result > 1 || accepted > frames
|| (!result && accepted))
return -1;
w->prefix_sent += accepted;
return result ? 1 : 0;
}
if (!w->pcm_end)
{
result = record_audio_queue_peek(w->pcm, &samples, &frames, &timestamp);
@@ -121,11 +191,14 @@ static int record_mcw_input(record_mediacodec_worker_t *w)
{
if (w->consumed >= frames || frames > (size_t)-1 / w->channels
|| timestamp > (uint64_t)-1 - frames
|| (w->probe && timestamp + frames
> (uint64_t)-1 - RECORD_AAC_PROBE_FRAMES)
|| (w->closing && timestamp + frames > w->audio_end_sample))
return -1;
result = record_mediacodec_encoder_audio_submit(w->encoder[1],
samples + w->consumed * w->channels, frames - w->consumed,
timestamp + w->consumed, &accepted);
timestamp + w->consumed
+ (w->probe ? RECORD_AAC_PROBE_FRAMES : 0), &accepted);
if (result < 0 || result > 1 || accepted > frames - w->consumed
|| (!result && accepted))
return -1;
@@ -144,7 +217,8 @@ static int record_mcw_input(record_mediacodec_worker_t *w)
if (!w->closing)
return 0;
result = record_mediacodec_encoder_audio_finish(
w->encoder[1], w->audio_end_sample);
w->encoder[1], w->audio_end_sample
+ (w->probe ? RECORD_AAC_PROBE_FRAMES : 0));
if (result < 0 || result > 2)
return -1;
if (result == 1)
@@ -157,6 +231,7 @@ int record_mediacodec_worker_step(record_mediacodec_worker_t *w)
unsigned at;
int result;
bool progress = false;
bool calibrated = true;
if (!w || w->failed)
return RECORD_MC_WORKER_ERROR;
if (w->finished)
@@ -180,6 +255,27 @@ int record_mediacodec_worker_step(record_mediacodec_worker_t *w)
goto failed;
progress |= result != 0;
}
if (w->probe)
{
result = record_aac_probe_step(w->probe);
if (result < 0 || (!result && w->drained[1]))
goto failed;
calibrated = result == 2;
progress |= result == 1;
if (w->drained[1] && calibrated)
{
unsigned delay;
uint64_t skip;
if (!record_aac_probe_delay(w->probe, &delay))
goto failed;
skip = RECORD_AAC_PROBE_FRAMES + (uint64_t)delay;
if ((w->audio_end_sample && (w->coded_frames < skip
|| w->coded_frames - skip < w->audio_end_sample))
|| !record_mcw_samples_us(w->coded_frames, w->rate,
&w->end_us[1]))
goto failed;
}
}
if (!w->started)
{
result = record_mux_mediacodec_start(w->mux);
@@ -192,7 +288,8 @@ int record_mediacodec_worker_step(record_mediacodec_worker_t *w)
}
}
for (at = 0; at < 2; at++)
if (w->started && w->drained[at] && !w->ended[at])
if (w->started && w->drained[at] && !w->ended[at]
&& (!at || calibrated))
{
if (!record_mux_mediacodec_end(w->mux, at != 0, w->end_us[at]))
goto failed;
+9 -1
View File
@@ -4,6 +4,7 @@
#define RECORD_MEDIACODEC_WORKER_H
#include "record_audio_queue.h"
#include "record_mux_mediacodec.h"
#include "record_aac_probe.h"
RETRO_BEGIN_DECLS
typedef struct record_mediacodec_worker record_mediacodec_worker_t;
enum record_mediacodec_worker_result
@@ -19,12 +20,19 @@ record_mediacodec_worker_t *record_mediacodec_worker_new(
record_mediacodec_encoder_t *video, record_mediacodec_encoder_t *audio,
record_audio_queue_t *pcm, record_mux_mediacodec_t *mux,
unsigned channels, unsigned rate);
/* Before service starts; caller retains the probe until worker retirement. */
bool record_mediacodec_worker_calibrate(record_mediacodec_worker_t *worker,
record_aac_probe_t *probe);
/* Completed calibrated recording only; source frames exclude the prefix. */
bool record_mediacodec_worker_audio_edit(
const record_mediacodec_worker_t *worker, uint64_t *skip,
uint64_t *frames, unsigned *roll);
/* Worker-only, after GPU producer retirement and PCM producer closure.
* Source ends share the recording epoch. Same-end retries are idempotent. */
bool record_mediacodec_worker_finish(record_mediacodec_worker_t *worker,
int64_t video_end_us, uint64_t audio_end_sample);
/* One output action per encoder and one bounded PCM input action. Borrows
* survive backpressure. No allocation, wait or GPU submission here. */
* survive backpressure. No per-packet allocation, wait or GPU submission. */
int record_mediacodec_worker_step(record_mediacodec_worker_t *worker);
/* Controller still owns encoder/GPU retirement, mux cancellation and PCM
* drain on failure. Free refuses active service unless aborted. */
+401
View File
@@ -0,0 +1,401 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier:
* GPL-3.0-or-later */
#include "record_mp4_audio_edit.h"
#include <string.h>
#define EDIT_LIMIT (64u * 1024u * 1024u)
#define EDIT_EXTRA 98u
#define EDIT_TAG(a, b, c, d) (((uint32_t)(a) << 24) \
| ((uint32_t)(b) << 16) | ((uint32_t)(c) << 8) | (d))
typedef struct edit_box
{
const uint8_t *data;
size_t bytes, header;
uint32_t tag;
} edit_box_t;
typedef struct edit_context
{
uint64_t skip, duration, movie_duration;
edit_box_t root, audio, mdia, minf, stbl, tkhd, mvhd;
uint8_t *output;
size_t pos, capacity;
uint32_t samples, scale, factor;
unsigned roll;
bool drop_audio;
} edit_context_t;
static uint64_t edit_get(const uint8_t *p, unsigned n)
{
uint64_t value = 0;
unsigned i;
for (i = 0; i < n; i++)
value = (value << 8) | p[i];
return value;
}
static void edit_put(uint8_t *p, uint64_t value, unsigned n)
{
unsigned i;
for (i = 0; i < n; i++)
p[n - i - 1] = (uint8_t)(value >> (i * 8));
}
static bool edit_box_read(const uint8_t *p, size_t available, edit_box_t *b)
{
uint64_t size;
if (available < 8)
return false;
size = edit_get(p, 4);
b->header = size == 1 ? 16 : 8;
if (available < b->header)
return false;
if (size == 1)
size = edit_get(p + 8, 8);
if (size < b->header || size > available)
return false;
b->data = p;
b->bytes = (size_t)size;
b->tag = (uint32_t)edit_get(p + 4, 4);
return true;
}
static int edit_find(const edit_box_t *parent, uint32_t tag, edit_box_t *found)
{
edit_box_t child;
size_t pos = parent->header;
unsigned count = 0;
int seen = 0;
while (pos < parent->bytes)
{
if (++count > 4096 || !edit_box_read(parent->data + pos,
parent->bytes - pos, &child))
return -1;
if (child.tag == tag)
{
if (seen)
return -1;
*found = child;
seen = 1;
}
pos += child.bytes;
}
return seen;
}
static bool edit_duration(const edit_box_t *b, bool track,
size_t *offset, unsigned *width, uint64_t *value)
{
const uint8_t *body = b->data + b->header;
unsigned version;
if (b->bytes == b->header)
return false;
version = body[0];
if (version > 1)
return false;
*offset = b->header + (version ? (track ? 28 : 24) : (track ? 20 : 16));
*width = version ? 8 : 4;
if (*offset > b->bytes || *width > b->bytes - *offset)
return false;
*value = edit_get(b->data + *offset, *width);
return true;
}
static bool edit_scale_track(const edit_box_t *track, unsigned factor,
uint8_t *output)
{
edit_box_t tkhd, edits, list;
size_t offset, pos, step;
unsigned width, i, count;
uint64_t value, maximum;
int found;
if (edit_find(track, EDIT_TAG('t', 'k', 'h', 'd'), &tkhd) != 1
|| !edit_duration(&tkhd, true, &offset, &width, &value))
return false;
maximum = width == 4 ? (uint32_t)-1 : (uint64_t)-1;
if (value > maximum / factor)
return false;
if (output)
edit_put(output + (tkhd.data - track->data) + offset,
value * factor, width);
found = edit_find(track, EDIT_TAG('e', 'd', 't', 's'), &edits);
if (found < 0)
return false;
if (!found)
return true;
if (edit_find(&edits, EDIT_TAG('e', 'l', 's', 't'), &list) != 1
|| list.bytes - list.header < 8 || list.data[list.header] > 1)
return false;
width = list.data[list.header] ? 8 : 4;
step = 2 * width + 4;
pos = list.header + 8;
count = (unsigned)edit_get(list.data + list.header + 4, 4);
if (count > 4096 || count != (list.bytes - pos) / step
|| (list.bytes - pos) % step)
return false;
maximum = width == 4 ? (uint32_t)-1 : (uint64_t)-1;
for (i = 0; i < count; i++, pos += step)
{
value = edit_get(list.data + pos, width);
if (value > maximum / factor)
return false;
if (output)
edit_put(output + (list.data - track->data) + pos,
value * factor, width);
}
return true;
}
static bool edit_prepare(edit_context_t *c, uint64_t frames)
{
edit_box_t track, mdia, handler, mdhd, stsd, stsz, group;
const uint8_t *body;
size_t pos = c->root.header, offset;
uint64_t value, whole, fraction;
uint32_t movie_scale, rate, a, b, remainder;
unsigned count = 0, width;
bool found = false;
if (edit_find(&c->root, EDIT_TAG('m', 'v', 'h', 'd'), &c->mvhd) != 1
|| !edit_duration(&c->mvhd, false, &offset, &width, &value))
return false;
movie_scale = (uint32_t)edit_get(c->mvhd.data + offset - 4, 4);
if (!movie_scale)
return false;
c->movie_duration = 0;
c->factor = 1;
c->scale = movie_scale;
while (pos < c->root.bytes)
{
if (++count > 4096 || !edit_box_read(c->root.data + pos,
c->root.bytes - pos, &track))
return false;
pos += track.bytes;
if (track.tag != EDIT_TAG('t', 'r', 'a', 'k'))
continue;
if (edit_find(&track, EDIT_TAG('m', 'd', 'i', 'a'), &mdia) != 1
|| edit_find(&mdia, EDIT_TAG('h', 'd', 'l', 'r'), &handler) != 1
|| handler.bytes - handler.header < 12)
return false;
if (edit_get(handler.data + handler.header + 8, 4)
!= EDIT_TAG('s', 'o', 'u', 'n'))
{
if (edit_find(&track, EDIT_TAG('t', 'k', 'h', 'd'), &group) != 1
|| !edit_duration(&group, true, &offset, &width, &value))
return false;
if (value > c->movie_duration)
c->movie_duration = value;
continue;
}
if (found)
return false;
found = true;
c->audio = track;
c->mdia = mdia;
if (edit_find(&track, EDIT_TAG('e', 'd', 't', 's'), &group) != 0
|| edit_find(&track, EDIT_TAG('t', 'k', 'h', 'd'), &c->tkhd) != 1
|| edit_find(&mdia, EDIT_TAG('m', 'd', 'h', 'd'), &mdhd) != 1
|| edit_find(&mdia, EDIT_TAG('m', 'i', 'n', 'f'), &c->minf) != 1
|| edit_find(&c->minf, EDIT_TAG('s', 't', 'b', 'l'), &c->stbl) != 1
|| edit_find(&c->stbl, EDIT_TAG('s', 't', 's', 'd'), &stsd) != 1
|| edit_find(&c->stbl, EDIT_TAG('s', 't', 's', 'z'), &stsz) != 1
|| edit_find(&c->stbl, EDIT_TAG('s', 'g', 'p', 'd'), &group) != 0
|| edit_find(&c->stbl, EDIT_TAG('s', 'b', 'g', 'p'), &group) != 0)
return false;
if (stsd.bytes - stsd.header < 16 || stsz.bytes - stsz.header < 12)
return false;
body = stsd.data + stsd.header;
if (edit_get(body + 4, 4) != 1
|| edit_get(body + 12, 4) != EDIT_TAG('m', 'p', '4', 'a'))
return false;
c->samples = (uint32_t)edit_get(stsz.data + stsz.header + 8, 4);
if (!c->samples || c->roll > c->samples
|| !edit_duration(&mdhd, false, &offset, &width, &value)
|| (!c->drop_audio
&& (c->skip > value || frames > value - c->skip)))
return false;
rate = (uint32_t)edit_get(mdhd.data + offset - 4, 4);
if (!rate)
return false;
if ((frames % rate) * movie_scale % rate)
{
a = movie_scale;
b = rate;
while (b)
{
remainder = a % b;
a = b;
b = remainder;
}
c->factor = rate / a;
if (movie_scale > (uint32_t)-1 / c->factor)
return false;
c->scale = movie_scale * c->factor;
}
whole = frames / rate;
fraction = (frames % rate) * c->scale / rate;
if (whole > ((uint64_t)-1 - fraction) / c->scale)
return false;
c->duration = whole * c->scale + fraction;
if (!edit_duration(&c->tkhd, true, &offset, &width, &value)
|| (width == 4 && c->duration > (uint32_t)-1))
return false;
}
if (c->movie_duration > (uint64_t)-1 / c->factor)
return false;
c->movie_duration *= c->factor;
pos = c->root.header;
while (pos < c->root.bytes)
{
if (!edit_box_read(c->root.data + pos, c->root.bytes - pos, &track))
return false;
if (track.tag == EDIT_TAG('t', 'r', 'a', 'k')
&& track.data != c->audio.data
&& !edit_scale_track(&track, c->factor, NULL))
return false;
if (c->factor != 1 && track.tag == EDIT_TAG('m', 'v', 'e', 'x'))
return false;
pos += track.bytes;
}
if (c->duration > c->movie_duration)
c->movie_duration = c->duration;
if (!found || (c->drop_audio && !c->movie_duration)
|| !edit_duration(&c->mvhd, false, &offset, &width, &value)
|| (width == 4 && c->movie_duration > (uint32_t)-1))
return false;
return true;
}
static bool edit_emit(edit_context_t *c, const void *data, size_t size)
{
if (size > c->capacity - c->pos)
return false;
memcpy(c->output + c->pos, data, size);
c->pos += size;
return true;
}
static bool edit_add(edit_context_t *c, bool groups)
{
uint8_t data[54];
memset(data, 0, sizeof(data));
if (groups)
{
edit_put(data, 26, 4);
memcpy(data + 4, "sgpd", 4);
data[8] = 1;
memcpy(data + 12, "roll", 4);
edit_put(data + 16, 2, 4);
edit_put(data + 20, 1, 4);
edit_put(data + 24, (uint16_t)(0u - c->roll), 2);
edit_put(data + 26, 28, 4);
memcpy(data + 30, "sbgp", 4);
memcpy(data + 38, "roll", 4);
edit_put(data + 42, 1, 4);
edit_put(data + 46, c->samples, 4);
edit_put(data + 50, 1, 4);
return edit_emit(c, data, sizeof(data));
}
edit_put(data, 44, 4);
memcpy(data + 4, "edts", 4);
edit_put(data + 8, 36, 4);
memcpy(data + 12, "elst", 4);
data[16] = 1;
edit_put(data + 20, 1, 4);
edit_put(data + 24, c->duration, 8);
edit_put(data + 32, c->skip, 8);
edit_put(data + 40, 0x00010000u, 4);
return edit_emit(c, data, 44);
}
static bool edit_write(edit_context_t *c, const edit_box_t *b, unsigned depth)
{
size_t start = c->pos, pos = b->header, offset;
unsigned width, count = 0;
uint64_t value;
edit_box_t child;
bool parent = b->data == c->root.data || b->data == c->audio.data
|| b->data == c->mdia.data || b->data == c->minf.data
|| b->data == c->stbl.data;
if (c->drop_audio && b->data == c->audio.data)
{
if (!edit_emit(c, b->data, b->bytes))
return false;
memcpy(c->output + start + 4, "free", 4);
return true;
}
if (!parent)
{
if (!edit_emit(c, b->data, b->bytes))
return false;
if (b->tag == EDIT_TAG('t', 'r', 'a', 'k')
&& !edit_scale_track(b, c->factor, c->output + start))
return false;
if (b->data == c->tkhd.data || b->data == c->mvhd.data)
{
if (!edit_duration(b, b->data == c->tkhd.data, &offset, &width, &value))
return false;
edit_put(c->output + start + offset,
b->data == c->tkhd.data ? c->duration : c->movie_duration, width);
if (b->data == c->mvhd.data)
edit_put(c->output + start + offset - 4, c->scale, 4);
}
return true;
}
if (depth > 4 || !edit_emit(c, b->data, b->header))
return false;
while (pos < b->bytes)
{
if (++count > 4096 || !edit_box_read(b->data + pos, b->bytes - pos, &child)
|| !edit_write(c, &child, depth + 1))
return false;
if (child.data == c->tkhd.data && !edit_add(c, false))
return false;
pos += child.bytes;
}
if (b->data == c->stbl.data && !edit_add(c, true))
return false;
if (b->header == 16)
edit_put(c->output + start + 8, c->pos - start, 8);
else
edit_put(c->output + start, c->pos - start, 4);
return true;
}
bool record_mp4_audio_edit(const void *moov, size_t bytes,
uint64_t skip, uint64_t frames, unsigned roll,
void *output, size_t capacity, size_t *written)
{
edit_context_t c;
size_t needed;
uintptr_t in = (uintptr_t)moov, out = (uintptr_t)output;
if (!written)
return false;
*written = 0;
if (!moov || bytes > EDIT_LIMIT || skip >> 63
|| !roll || roll > 32768)
return false;
memset(&c, 0, sizeof(c));
if (!edit_box_read((const uint8_t *)moov, bytes, &c.root)
|| c.root.bytes != bytes || c.root.tag != EDIT_TAG('m', 'o', 'o', 'v'))
return false;
c.skip = skip;
c.roll = roll;
c.drop_audio = frames == 0;
needed = bytes + (c.drop_audio ? 0 : EDIT_EXTRA);
if (!edit_prepare(&c, frames))
return false;
if (!output)
{
if (capacity)
return false;
*written = needed;
return true;
}
if (capacity < needed || (out >= in ? out - in < bytes
: in - out < needed))
return false;
c.output = (uint8_t *)output;
c.capacity = capacity;
if (!edit_write(&c, &c.root, 0) || c.pos != needed)
return false;
*written = c.pos;
return true;
}
+21
View File
@@ -0,0 +1,21 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier:
* GPL-3.0-or-later */
#ifndef RECORD_MP4_AUDIO_EDIT_H
#define RECORD_MP4_AUDIO_EDIT_H
#include <stdint.h>
#include <stddef.h>
#include <boolean.h>
#include <retro_common_api.h>
RETRO_BEGIN_DECLS
/* Cold file-finalization worker only. One complete native mux moov, one AAC
* track, no prior audio edit/roll group. Preserve media bytes and chunk offsets.
* skip/frames use mdhd audio ticks; roll counts prerequisite AAC packets.
* Zero source frames replace the audio track with padding, preserving offsets.
* Delay must be established by the caller, never inferred here.
* NULL output queries the validated size. Input/output must not overlap.
* No allocation or I/O. Failure sets written to zero; discard partial output. */
bool record_mp4_audio_edit(const void *moov, size_t bytes,
uint64_t skip, uint64_t frames, unsigned roll,
void *output, size_t capacity, size_t *written);
RETRO_END_DECLS
#endif
+282
View File
@@ -0,0 +1,282 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier:
* GPL-3.0-or-later */
#include "record_session_mediacodec.h"
#include "record_mediacodec_file.h"
#include <retro_atomic.h>
#include <stdlib.h>
struct record_session_mediacodec
{
uint64_t audio_end;
int64_t video_end;
record_mediacodec_encoder_t *video, *audio;
record_audio_queue_t *pcm;
record_mux_mediacodec_t *mux;
record_mediacodec_worker_t *worker;
record_aac_probe_t *probe;
record_mediacodec_file_t *file;
retro_atomic_int_t audio_closed, gpu_done, gpu_released, cancelled;
retro_atomic_int_t error, file_error;
bool finishing, draining, retired;
};
static void record_mcs_fail(record_session_mediacodec_t *s, int error)
{
if (!retro_atomic_load_acquire_int(&s->error))
retro_atomic_store_release_int(&s->error, error);
}
record_session_mediacodec_t *record_session_mediacodec_new(JNIEnv *env,
const mediacodec_encoder_request_t *video, unsigned interval,
unsigned rate, unsigned channels, unsigned bitrate,
size_t max_audio_frames, size_t audio_capacity, const char *path)
{
record_session_mediacodec_t *s;
int error = 0;
bool audio
= rate || channels || bitrate || max_audio_frames || audio_capacity;
if (!video || !path || !*path
|| (audio
&& (!rate || !channels || channels > 2 || !bitrate
|| !max_audio_frames || !audio_capacity)))
return NULL;
s = (record_session_mediacodec_t *)calloc(1, sizeof(*s));
if (!s)
return NULL;
retro_atomic_int_init(&s->audio_closed, !audio);
retro_atomic_int_init(&s->gpu_done, 0);
retro_atomic_int_init(&s->gpu_released, 0);
retro_atomic_int_init(&s->cancelled, 0);
retro_atomic_int_init(&s->error, 0);
retro_atomic_int_init(&s->file_error, 0);
s->video = record_mediacodec_encoder_new(env, video, interval);
if (!s->video)
goto failed;
if (audio)
{
s->audio = record_mediacodec_encoder_audio_new(rate, channels, bitrate);
s->pcm = record_audio_queue_new(
channels, max_audio_frames, audio_capacity);
s->probe = record_aac_probe_new(rate, channels);
if (!s->audio || !s->pcm || !s->probe)
goto failed;
}
s->file = record_mediacodec_file_new(path, &error);
if (!s->file)
goto failed;
s->mux = record_mux_mediacodec_new(
record_mediacodec_file_descriptor(s->file), audio);
if (!s->mux)
goto failed;
s->worker = record_mediacodec_worker_new(
s->video, s->audio, s->pcm, s->mux, channels, rate);
if (s->worker && (!audio
|| record_mediacodec_worker_calibrate(s->worker, s->probe)))
return s;
failed:
if (s->worker)
record_mediacodec_worker_free(s->worker);
record_aac_probe_free(s->probe);
record_mux_mediacodec_free(s->mux);
record_mediacodec_file_close(s->file, false, &error);
record_audio_queue_free(s->pcm);
record_mediacodec_encoder_free(s->audio);
record_mediacodec_encoder_free(s->video);
free(s);
return NULL;
}
void *record_session_mediacodec_surface(record_session_mediacodec_t *s)
{
return s ? record_mediacodec_encoder_surface(s->video) : NULL;
}
bool record_session_mediacodec_cancelled(const record_session_mediacodec_t *s)
{
return !s || retro_atomic_load_acquire_int(&s->cancelled)
|| retro_atomic_load_acquire_int(&s->error);
}
void record_session_mediacodec_cancel(record_session_mediacodec_t *s)
{
if (s)
retro_atomic_store_release_int(&s->cancelled, 1);
}
int record_session_mediacodec_audio(record_session_mediacodec_t *s,
const int16_t *samples, size_t frames, uint64_t timestamp)
{
if (!s || retro_atomic_load_acquire_int(&s->audio_closed)
|| record_session_mediacodec_cancelled(s))
return RECORD_AUDIO_ERROR;
return record_audio_queue_push(s->pcm, samples, frames, timestamp);
}
bool record_session_mediacodec_audio_finish(
record_session_mediacodec_t *s, uint64_t end_sample)
{
if (!s)
return false;
if (retro_atomic_load_acquire_int(&s->audio_closed))
return s->audio_end == end_sample;
if (!record_audio_queue_finish(s->pcm))
return false;
s->audio_end = end_sample;
retro_atomic_store_release_int(&s->audio_closed, 1);
return true;
}
bool record_session_mediacodec_gpu_retired(
record_session_mediacodec_t *s, int64_t end_us)
{
if (!s || end_us < 0)
return false;
if (retro_atomic_load_acquire_int(&s->gpu_done))
return s->video_end == end_us;
s->video_end = end_us;
retro_atomic_store_release_int(&s->gpu_done, 1);
return true;
}
bool record_session_mediacodec_gpu_released(record_session_mediacodec_t *s)
{
if (!s || !retro_atomic_load_acquire_int(&s->gpu_done))
return false;
retro_atomic_store_release_int(&s->gpu_released, 1);
return true;
}
static bool record_mcs_close_output(
record_session_mediacodec_t *s, bool complete)
{
int error = 0;
uint64_t skip = 0, frames = 0;
unsigned roll = 0;
bool edit = complete && s->probe;
if (s->worker)
{
if (edit && !record_mediacodec_worker_audio_edit(s->worker,
&skip, &frames, &roll))
{
record_mcs_fail(s, RECORD_MC_SESSION_OUTPUT);
complete = false;
}
if (!complete)
record_mediacodec_worker_abort(s->worker);
if (!record_mediacodec_worker_free(s->worker))
return false;
s->worker = NULL;
}
record_aac_probe_free(s->probe);
s->probe = NULL;
if (!complete)
{
if (!record_mediacodec_encoder_stop(s->video))
record_mcs_fail(s, RECORD_MC_SESSION_OUTPUT);
if (s->audio && !record_mediacodec_encoder_stop(s->audio))
record_mcs_fail(s, RECORD_MC_SESSION_OUTPUT);
}
record_mux_mediacodec_free(s->mux);
s->mux = NULL;
if (complete && edit && !record_mediacodec_file_audio_edit(s->file,
skip, frames, roll, &error))
{
retro_atomic_store_release_int(&s->file_error, error);
record_mcs_fail(s, RECORD_MC_SESSION_FILE);
complete = false;
}
if (!record_mediacodec_file_close(s->file, complete, &error))
{
retro_atomic_store_release_int(&s->file_error, error);
record_mcs_fail(s, RECORD_MC_SESSION_FILE);
}
s->file = NULL;
s->draining = true;
return true;
}
int record_session_mediacodec_step(record_session_mediacodec_t *s)
{
const int16_t *samples;
size_t frames;
uint64_t timestamp;
int result;
bool closed;
if (!s)
return RECORD_MC_WORKER_ERROR;
if (s->retired)
return retro_atomic_load_acquire_int(&s->error) ? RECORD_MC_WORKER_ERROR
: RECORD_MC_WORKER_END;
if (retro_atomic_load_acquire_int(&s->cancelled))
record_mcs_fail(s, RECORD_MC_SESSION_CANCELLED);
closed = retro_atomic_load_acquire_int(&s->audio_closed)
&& retro_atomic_load_acquire_int(&s->gpu_done);
if (!s->draining && !retro_atomic_load_acquire_int(&s->error))
{
if (closed && !s->finishing)
{
if (!record_mediacodec_worker_finish(
s->worker, s->video_end, s->audio_end))
record_mcs_fail(s, RECORD_MC_SESSION_OUTPUT);
else
s->finishing = true;
}
if (!retro_atomic_load_acquire_int(&s->error))
{
result = record_mediacodec_worker_step(s->worker);
if (result == RECORD_MC_WORKER_END)
{
if (!record_mcs_close_output(s, true))
record_mcs_fail(s, RECORD_MC_SESSION_OUTPUT);
}
else if (result == RECORD_MC_WORKER_ERROR)
record_mcs_fail(s, RECORD_MC_SESSION_OUTPUT);
else
return result;
}
}
if (!s->draining && !record_mcs_close_output(s, false))
return RECORD_MC_WORKER_BUSY;
if (s->pcm)
{
result = record_audio_queue_peek(s->pcm, &samples, &frames, &timestamp);
if (result == RECORD_AUDIO_OK)
{
if (!record_audio_queue_ack(s->pcm))
record_mcs_fail(s, RECORD_MC_SESSION_OUTPUT);
return RECORD_MC_WORKER_PROGRESS;
}
if (result != RECORD_AUDIO_END)
return RECORD_MC_WORKER_BUSY;
}
if (!closed || !retro_atomic_load_acquire_int(&s->gpu_released))
return RECORD_MC_WORKER_BUSY;
if (!record_audio_queue_free(s->pcm))
return RECORD_MC_WORKER_BUSY;
s->pcm = NULL;
record_mediacodec_encoder_free(s->audio);
record_mediacodec_encoder_free(s->video);
s->audio = s->video = NULL;
s->retired = true;
return retro_atomic_load_acquire_int(&s->error) ? RECORD_MC_WORKER_ERROR
: RECORD_MC_WORKER_END;
}
enum record_session_mediacodec_error record_session_mediacodec_error(
const record_session_mediacodec_t *s, int *file_error)
{
if (file_error)
*file_error = s ? retro_atomic_load_acquire_int(&s->file_error) : 0;
return s
? (enum record_session_mediacodec_error)retro_atomic_load_acquire_int(
&s->error)
: RECORD_MC_SESSION_OUTPUT;
}
bool record_session_mediacodec_free(record_session_mediacodec_t *s)
{
if (s && !s->retired)
return false;
free(s);
return true;
}
+57
View File
@@ -0,0 +1,57 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier:
* GPL-3.0-or-later */
#ifndef RECORD_SESSION_MEDIACODEC_H
#define RECORD_SESSION_MEDIACODEC_H
#include "record_mediacodec_worker.h"
RETRO_BEGIN_DECLS
typedef struct record_session_mediacodec record_session_mediacodec_t;
enum record_session_mediacodec_error
{
RECORD_MC_SESSION_NONE,
RECORD_MC_SESSION_CANCELLED,
RECORD_MC_SESSION_OUTPUT,
RECORD_MC_SESSION_FILE
};
/* Cold I/O worker. Owns encoder(s), PCM queue, mux and exclusive file.
* Audio is disabled only when all audio arguments are zero. NULL publishes
* nothing and rolls back cold resources. Startup/final I/O may block here. */
record_session_mediacodec_t *record_session_mediacodec_new(JNIEnv *env,
const mediacodec_encoder_request_t *video, unsigned interval,
unsigned rate, unsigned channels, unsigned bitrate,
size_t max_audio_frames, size_t audio_capacity, const char *path);
/* Cold I/O owner, before publishing the session to the GPU executor. Borrowed
* through gpu_released. Executor owns Surface/capture/draw construction. */
void *record_session_mediacodec_surface(record_session_mediacodec_t *session);
/* One PCM producer. Explicit source sample clock; BUSY copies nothing. */
int record_session_mediacodec_audio(record_session_mediacodec_t *session,
const int16_t *samples, size_t frames, uint64_t timestamp);
/* Producer or its I/O notification owner after the last push returns,
* including cancellation. No pushes
* after close. The end is immutable and includes any dropped source interval.
*/
bool record_session_mediacodec_audio_finish(
record_session_mediacodec_t *session, uint64_t end_sample);
/* GPU executor or its single I/O notification owner, after producer closure
* and all source reads retire.
* Then signal released separately after Surface/context users retire. These
* notifications publish immutable source ends; they perform no native calls. */
bool record_session_mediacodec_gpu_retired(
record_session_mediacodec_t *session, int64_t end_us);
bool record_session_mediacodec_gpu_released(
record_session_mediacodec_t *session);
/* Controller request; does not close either producer. GPU executor observes
* cancelled and uses its retirement path instead of drawing further frames. */
void record_session_mediacodec_cancel(record_session_mediacodec_t *session);
bool record_session_mediacodec_cancelled(
const record_session_mediacodec_t *session);
/* Independent I/O executor; never the GPU/frame/audio/task thread. Uses worker
* results. ERROR/END are terminal only after both producers and GPU users
* retire. On failure drains at most one queued PCM packet per step. No wait
* loop or dispatcher here; executor must wake for input/close/cancellation. */
int record_session_mediacodec_step(record_session_mediacodec_t *session);
enum record_session_mediacodec_error record_session_mediacodec_error(
const record_session_mediacodec_t *session, int *file_error);
/* I/O owner, all callers quiescent; refuses unretired sessions. */
bool record_session_mediacodec_free(record_session_mediacodec_t *session);
RETRO_END_DECLS
#endif
+9 -1
View File
@@ -7,7 +7,7 @@
#include "../../../record/record_gpu_reinit.c"
static video_driver_state_t video;
static runloop_state_t loop;
static bool native_pending, switching;
static bool native_pending, io_pending, switching;
static unsigned stops, reinits;
static int applied;
video_driver_state_t *video_state_get_ptr(void)
@@ -27,6 +27,10 @@ void recording_gpu_stop(void)
stops++;
}
bool recording_gpu_pending(void)
{
return native_pending || io_pending;
}
bool recording_gpu_reset_pending(void)
{
return native_pending;
}
@@ -43,6 +47,10 @@ int main(void)
{
unsigned i, before;
video.data = &video;
io_pending = true;
assert(!recording_gpu_defer_reinit(DRIVER_VIDEO_MASK));
assert(recording_gpu_pending());
io_pending = false;
assert(!recording_gpu_defer_reinit(DRIVER_VIDEO_MASK));
assert(!stops);
native_pending = true;
@@ -0,0 +1,94 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier:
* GPL-3.0-or-later */
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include <math.h>
#include <retro_math.h>
#include <compat/mingw_math.h>
#include "../../../audio/audio_driver.h"
#include "../../../record/record_audio_tap.h"
#define AUDIO_PAUSE_TAIL_FRAMES 512
static audio_driver_state_t audio_driver_st;
#include "audio_tap_fragment.c"
static retro_atomic_int_t entered, release_pass;
static unsigned pushes;
static int submit(
void *context, const int16_t *samples, size_t frames, uint64_t pts)
{
unsigned i;
assert(context == &pushes && frames == 32 && pts == 0);
for (i = 0; i < 64; i++)
assert(samples[i] == (i & 1 ? 4096 : -4096));
pushes++;
return RECORD_AUDIO_OK;
}
#ifdef HAVE_THREADS
static void pass(void *unused)
{
int16_t mixed[64];
unsigned i;
(void)unused;
audio_driver_state_lock();
retro_atomic_store_release_int(&entered, 1);
while (!retro_atomic_load_acquire_int(&release_pass))
{
}
for (i = 0; i < 64; i++)
mixed[i] = (int16_t)(i & 1 ? 4096 : -4096);
audio_driver_pause_track(&audio_driver_st, mixed, 32, false);
audio_driver_state_unlock();
}
#endif
int main(void)
{
record_audio_tap_t *t = record_audio_tap_new(48000, submit, &pushes);
record_audio_tap_t *other = record_audio_tap_new(44100, submit, &pushes);
uint64_t end;
#ifdef HAVE_THREADS
sthread_t *thread;
audio_driver_st.state_lock = slock_new();
assert(audio_driver_st.state_lock);
#endif
assert(t && other);
audio_driver_st.out_rate = 48000;
AUDIO_FLAGS_SET(&audio_driver_st, AUDIO_FLAG_ACTIVE | AUDIO_FLAG_STARTED);
assert(audio_driver_record_tap_install(other) == -1);
assert(audio_driver_record_tap_install(t) == 1);
assert(!record_audio_tap_free(t));
#ifdef HAVE_THREADS
retro_atomic_int_init(&entered, 0);
retro_atomic_int_init(&release_pass, 0);
thread = sthread_create(pass, NULL);
assert(thread);
while (!retro_atomic_load_acquire_int(&entered))
{
}
assert(audio_driver_record_tap_install(t) == 0);
assert(!audio_driver_record_tap_remove(t) && !pushes);
assert(!record_audio_tap_end(t, &end));
retro_atomic_store_release_int(&release_pass, 1);
sthread_join(thread);
assert(pushes == 1);
#else
{
int16_t mixed[64];
unsigned i;
for (i = 0; i < 64; i++)
mixed[i] = (int16_t)(i & 1 ? 4096 : -4096);
audio_driver_pause_track(&audio_driver_st, mixed, 32, false);
assert(pushes == 1);
}
#endif
assert(audio_driver_record_tap_remove(t));
assert(!audio_driver_st.record_tap);
assert(record_audio_tap_end(t, &end) && end == 32);
assert(record_audio_tap_free(t) && record_audio_tap_free(other));
#ifdef HAVE_THREADS
slock_free(audio_driver_st.state_lock);
audio_driver_st.state_lock = NULL;
#endif
puts("PASS shipping audio owner try-install/remove, post-mix capture and "
"callback quiescence without new pass locks");
return 0;
}
@@ -0,0 +1,83 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier:
* GPL-3.0-or-later */
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include "../../../record/record_audio_tap.c"
static unsigned calls, fail_at;
static uint64_t accepted;
static int16_t expected[10000];
static int submit(
void *context, const int16_t *samples, size_t frames, uint64_t pts)
{
assert(context == &accepted && samples && frames <= 4096 && pts == accepted);
calls++;
if (calls == fail_at)
return RECORD_AUDIO_BUSY;
assert(!memcmp(
samples, expected + accepted * 2, frames * 2 * sizeof(int16_t)));
accepted += frames;
return RECORD_AUDIO_OK;
}
static record_audio_tap_t *create(void)
{
record_audio_tap_t *t;
calls = fail_at = 0;
accepted = 0;
t = record_audio_tap_new(48000, submit, &accepted);
assert(t && record_audio_tap_rate(t) == 48000);
assert(record_audio_tap_begin(t) && !record_audio_tap_begin(t));
assert(!record_audio_tap_end(t, NULL) && !record_audio_tap_free(t));
return t;
}
int main(void)
{
static int16_t input[10000];
static float floating[10000];
record_audio_tap_t *t;
uint64_t end;
unsigned i;
for (i = 0; i < 10000; i++)
input[i] = expected[i] = (int16_t)(i % 30000);
t = create();
assert(record_audio_tap_push(t, input, 5000, false, 48000));
assert(calls == 2 && !record_audio_tap_failed(t));
record_audio_tap_retire(t, false);
assert(record_audio_tap_end(t, &end) && end == 5000);
assert(!record_audio_tap_begin(t)
&& !record_audio_tap_push(t, input, 1, false, 48000));
assert(record_audio_tap_free(t));
for (i = 0; i < 10000; i++)
floating[i] = (i & 1) ? 0.5f : -0.5f;
for (i = 0; i < 10000; i++)
expected[i] = (i & 1) ? 16384 : -16384;
t = create();
assert(record_audio_tap_push(t, floating, 5000, true, 48000));
record_audio_tap_retire(t, false);
assert(record_audio_tap_end(t, &end) && end == 5000
&& record_audio_tap_free(t));
t = create();
fail_at = 2;
assert(!record_audio_tap_push(t, floating, 5000, true, 48000));
assert(record_audio_tap_failed(t) && accepted == 4096);
assert(!record_audio_tap_push(t, floating, 1, true, 48000) && calls == 2);
record_audio_tap_retire(t, false);
assert(record_audio_tap_end(t, &end) && end == 4096
&& record_audio_tap_free(t));
t = create();
assert(!record_audio_tap_push(t, input, 1, false, 44100) && !calls);
record_audio_tap_retire(t, true);
assert(record_audio_tap_failed(t) && record_audio_tap_end(t, &end) && !end);
assert(record_audio_tap_free(t));
t = create();
t->frames = ~(uint64_t)0;
assert(!record_audio_tap_push(t, input, 1, false, 48000) && !calls);
record_audio_tap_retire(t, false);
assert(record_audio_tap_free(t));
assert(!record_audio_tap_new(0, submit, NULL));
assert(!record_audio_tap_new(48000, NULL, NULL));
assert(record_audio_tap_free(NULL));
puts("PASS post-mixer S16/float capture, bounded packets, source clock, "
"backpressure and independent producer retirement");
return 0;
}
+51 -3
View File
@@ -15,6 +15,7 @@ static AMediaFormat config, output_format;
static int stage, missing, lookup, libraries, codecs, formats, queues;
static int input_busy, input_owned, output_owned, output_mode, input_calls;
static int allocation_fail;
static int stops, stop_fault;
static unsigned expected_rate, expected_channels;
static size_t capacity = 8, queued_bytes;
static uint64_t queued_pts;
@@ -58,7 +59,8 @@ static media_status_t start(AMediaCodec *c)
static media_status_t stop(AMediaCodec *c)
{
assert(c == &codec);
return 0;
stops++;
return stop_fault ? -99 : 0;
}
static media_status_t destroy(AMediaCodec *c)
{
@@ -188,6 +190,16 @@ static AMediaFormat *get_format(AMediaCodec *c)
formats++;
return &output_format;
}
static unsigned char asc[2] = {0x12, 0x10};
static int config_fault;
static bool get_buffer(AMediaFormat *f, const char *key,
void **data, size_t *bytes)
{
assert(f == &output_format && !strcmp(key, "csd-0"));
*data = config_fault == 2 ? NULL : asc;
*bytes = config_fault == 3 ? 0 : sizeof(asc);
return config_fault != 1;
}
dylib_t dylib_load(const char *name)
{
assert(!strcmp(name, "libmediandk.so"));
@@ -224,6 +236,7 @@ function_t dylib_proc(dylib_t lib, const char *name)
FN("AMediaFormat_setString", set_string);
FN("AMediaFormat_setInt32", set_integer);
FN("AMediaFormat_getInt32", get_integer);
FN("AMediaFormat_getBuffer", get_buffer);
FN("AMediaCodec_dequeueInputBuffer", input);
FN("AMediaCodec_getInputBuffer", input_buffer);
FN("AMediaCodec_queueInputBuffer", queue);
@@ -248,9 +261,10 @@ int main(void)
{
record_mediacodec_encoder_t *s;
record_mediacodec_packet_t packet;
size_t accepted;
size_t accepted, config_bytes;
const void *config_data;
int i, before;
for (missing = 1; missing <= 17; missing++)
for (missing = 1; missing <= 18; missing++)
{
assert(!open_audio(44100, 2));
balanced();
@@ -295,8 +309,20 @@ int main(void)
}
assert(record_mediacodec_encoder_audio_submit(s, pcm, 2, 108, &accepted) == 1
&& accepted == 2 && queued_pts == 2448);
config_data = asc;
config_bytes = 99;
assert(!record_mediacodec_encoder_audio_config(s, &config_data,
&config_bytes) && !config_data && !config_bytes);
output_mode = 1;
assert(record_mediacodec_encoder_pump(s, &packet) == RECORD_MC_FORMAT);
assert(record_mediacodec_encoder_audio_config(s, &config_data,
&config_bytes) && config_data == asc && config_bytes == 2);
for (config_fault = 1; config_fault <= 3; config_fault++)
assert(!record_mediacodec_encoder_audio_config(s, &config_data,
&config_bytes) && !config_data && !config_bytes);
config_fault = 0;
assert(!record_mediacodec_encoder_audio_config(s, NULL, &config_bytes));
assert(!record_mediacodec_encoder_audio_config(s, &config_data, NULL));
output_mode = 2;
assert(record_mediacodec_encoder_pump(s, &packet) == RECORD_MC_PACKET
&& packet.size == 3);
@@ -412,6 +438,28 @@ int main(void)
record_mediacodec_encoder_free(s);
balanced();
input_busy = 0;
for (i = 0; i < 2; i++)
{
s = open_audio(44100, 2);
assert(s);
assert(record_mediacodec_encoder_audio_submit(s, pcm, 2, 0, &accepted)
== 1);
stop_fault = i;
before = stops;
assert(record_mediacodec_encoder_stop(s) == !stop_fault);
assert(!record_mediacodec_encoder_audio_config(s, &config_data,
&config_bytes) && !config_data && !config_bytes);
assert(record_mediacodec_encoder_stop(s) == !stop_fault);
assert(codecs == 1 && formats == 1 && libraries == 1);
accepted = 99;
assert(record_mediacodec_encoder_audio_submit(s, pcm, 2, 2, &accepted)
== -1 && !accepted);
assert(record_mediacodec_encoder_audio_finish(s, 2) == -1);
record_mediacodec_encoder_free(s);
assert(stops == before + 1);
balanced();
stop_fault = 0;
}
puts("PASS: worker AAC PCM ownership, partial/BUSY input, bounded silence, "
"sample-clock timestamps, EOS, native faults and format validation; "
"APIs mocked");
@@ -0,0 +1,256 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier:
* GPL-3.0-or-later */
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include "../../../record/record_mediacodec_worker.h"
struct record_mediacodec_encoder
{
uint64_t accepted;
unsigned packets, calls;
bool audio, format, eos, held;
};
struct record_mux_mediacodec
{
unsigned packets[2], ends[2];
bool format[2], finished;
};
struct record_aac_probe
{
unsigned packets, steps;
bool configured, ready;
};
static int16_t prefix[RECORD_AAC_PROBE_FRAMES];
static const unsigned char config[] = {0x11, 0x88};
static const unsigned char encoded = 1;
static unsigned fault;
static bool empty;
const int16_t *record_aac_probe_input(record_aac_probe_t *p,
size_t at, size_t *frames)
{
assert(p && at < RECORD_AAC_PROBE_FRAMES);
*frames = RECORD_AAC_PROBE_FRAMES - at;
return prefix + at;
}
bool record_aac_probe_config(record_aac_probe_t *p,
const void *data, size_t bytes)
{
assert(!p->configured && data == config && bytes == sizeof(config));
p->configured = true;
return fault != 3;
}
bool record_aac_probe_packet(record_aac_probe_t *p,
const void *data, size_t bytes)
{
assert(p->configured && data == &encoded && bytes == 1);
p->packets++;
return true;
}
int record_aac_probe_step(record_aac_probe_t *p)
{
if (fault == 4)
return -1;
if (p->packets < 8)
return 0;
if (++p->steps < 3)
return 1;
p->ready = true;
return 2;
}
bool record_aac_probe_delay(const record_aac_probe_t *p, unsigned *delay)
{
if (!p->ready)
return false;
*delay = fault == 7 ? 4096 : 2048;
return true;
}
bool record_mediacodec_encoder_audio_config(record_mediacodec_encoder_t *e,
const void **data, size_t *bytes)
{
assert(e->audio && e->format);
*data = config;
*bytes = sizeof(config);
return true;
}
int record_mediacodec_encoder_audio_submit(record_mediacodec_encoder_t *e,
const int16_t *samples, size_t frames, uint64_t timestamp,
size_t *accepted)
{
size_t count = frames > 317 ? 317 : frames;
size_t i;
assert(e->audio && !e->eos && timestamp == e->accepted);
*accepted = 0;
if (++e->calls % 3 == 0)
return 0;
if (timestamp < RECORD_AAC_PROBE_FRAMES)
assert(samples == prefix + timestamp);
else
for (i = 0; i < count; i++)
assert(samples[i] == 7);
*accepted = fault == 2 ? frames + 1 : count;
e->accepted += count;
return 1;
}
int record_mediacodec_encoder_audio_finish(record_mediacodec_encoder_t *e,
uint64_t end)
{
assert(e->audio && end == RECORD_AAC_PROBE_FRAMES + (empty ? 0 : 17)
&& e->accepted == end);
e->eos = true;
return 1;
}
bool record_mediacodec_encoder_finish(record_mediacodec_encoder_t *e)
{
assert(!e->audio);
e->eos = true;
return true;
}
int record_mediacodec_encoder_pump(record_mediacodec_encoder_t *e,
record_mediacodec_packet_t *packet)
{
unsigned limit;
if (!e->format)
{
e->format = true;
return RECORD_MC_FORMAT;
}
limit = e->audio ? (unsigned)(e->accepted / 1024) : 1;
if (e->audio && e->eos)
limit = fault == 5 ? 7 : empty ? 8 : 11;
if (e->packets < limit)
{
e->held = true;
packet->data = &encoded;
packet->size = 1;
packet->pts_us = e->audio ? -99 : 0;
packet->flags = 1;
return RECORD_MC_PACKET;
}
return e->eos ? RECORD_MC_END : RECORD_MC_BUSY;
}
void *record_mediacodec_encoder_format(record_mediacodec_encoder_t *e)
{
assert(e->format);
return e;
}
bool record_mediacodec_encoder_release(record_mediacodec_encoder_t *e)
{
assert(e->held);
e->held = false;
e->packets++;
return true;
}
bool record_mux_mediacodec_track(record_mux_mediacodec_t *m,
bool audio, void *format)
{
assert(format && !m->format[audio]);
m->format[audio] = true;
return true;
}
int record_mux_mediacodec_start(record_mux_mediacodec_t *m)
{
return m->format[0] && m->format[1];
}
bool record_mux_mediacodec_write(record_mux_mediacodec_t *m, bool audio,
const record_mediacodec_packet_t *p)
{
assert(p->data == &encoded && p->size == 1);
assert(p->pts_us == (audio
? (int64_t)((uint64_t)m->packets[1] * 1024 * 1000000 / 48000)
: 0));
m->packets[audio]++;
return true;
}
bool record_mux_mediacodec_end(record_mux_mediacodec_t *m,
bool audio, int64_t end)
{
assert(end == (audio ? (empty ? 170666 : 234666) : 1000000));
m->ends[audio]++;
return true;
}
int record_mux_mediacodec_finish(record_mux_mediacodec_t *m)
{
assert(m->ends[0] && m->ends[1]);
m->finished = true;
return 1;
}
static void exercise(unsigned failure, bool cancel, bool no_pcm)
{
record_mediacodec_encoder_t video, audio;
record_mux_mediacodec_t mux;
record_aac_probe_t probe;
record_mediacodec_worker_t *w;
record_audio_queue_t *q;
int16_t pcm[17];
const int16_t *samples;
size_t frames;
uint64_t timestamp, skip = 0, source = 0;
unsigned i, roll = 0;
int result = 0;
memset(&video, 0, sizeof(video));
memset(&audio, 0, sizeof(audio));
memset(&mux, 0, sizeof(mux));
memset(&probe, 0, sizeof(probe));
audio.audio = true;
fault = failure;
empty = no_pcm;
for (i = 0; i < 17; i++)
pcm[i] = 7;
q = record_audio_queue_new(1, 17, 256);
assert(q);
if (!empty)
assert(record_audio_queue_push(q, pcm, 17, 0) == RECORD_AUDIO_OK);
assert(record_audio_queue_finish(q));
w = record_mediacodec_worker_new(&video, &audio, q, &mux, 1, 48000);
assert(w && record_mediacodec_worker_calibrate(w, &probe));
assert(!record_mediacodec_worker_calibrate(w, &probe));
assert(!record_mediacodec_worker_audio_edit(w, &skip, &source, &roll));
assert(!record_mediacodec_worker_finish(w, 1000000, (uint64_t)-1));
assert(record_mediacodec_worker_finish(w, 1000000, empty ? 0 : 17));
assert(record_mediacodec_worker_finish(w, 1000000, empty ? 0 : 17));
for (i = 0; i < 256; i++)
{
if (cancel && i == 2)
record_mediacodec_worker_abort(w);
result = record_mediacodec_worker_step(w);
assert(!record_mediacodec_worker_calibrate(w, &probe));
if (result == RECORD_MC_WORKER_ERROR || result == RECORD_MC_WORKER_END)
break;
}
assert(i < 256);
if (failure || cancel)
{
assert(result == RECORD_MC_WORKER_ERROR && !mux.finished);
assert(!record_mediacodec_worker_audio_edit(w, &skip, &source, &roll));
}
else
{
assert(result == RECORD_MC_WORKER_END && mux.finished);
assert(record_mediacodec_worker_audio_edit(w, &skip, &source, &roll));
assert(skip == 10240 && source == (empty ? 0 : 17) && roll == 2);
assert(audio.accepted == (empty ? 8192 : 8209)
&& mux.packets[1] == (empty ? 8 : 11));
}
assert(record_mediacodec_worker_step(w) == result);
assert(record_mediacodec_worker_free(w));
while (record_audio_queue_peek(q, &samples, &frames, &timestamp)
== RECORD_AUDIO_OK)
assert(record_audio_queue_ack(q));
assert(record_audio_queue_free(q));
}
int main(void)
{
exercise(0, false, false);
exercise(0, false, true);
exercise(2, false, false);
exercise(3, false, false);
exercise(4, false, false);
exercise(5, false, false);
exercise(7, false, false);
exercise(0, true, false);
puts("PASS: calibrated worker prefix backpressure, coded/source clocks, "
"trim ownership, early EOS and cancellation; codecs/probe mocked");
return 0;
}
@@ -0,0 +1,187 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier:
* GPL-3.0-or-later */
#include <assert.h>
#include <string.h>
#define RECORD_ANDROID_CAPTURE_EGL_TEST
#include "../../../record/record_android_capture_egl.c"
static unsigned libraries, refs, terminated, fault, mode;
static const char *missing;
static bool fallback, bad_configs;
static int egl_library, gles_library, display_token, config_tokens[2];
#define DISPLAY ((EGLDisplay) & display_token)
static void unused(void)
{
}
static EGLDisplay EGLAPIENTRY get_display(EGLNativeDisplayType native)
{
assert(native == EGL_DEFAULT_DISPLAY);
return fault == 3 ? EGL_NO_DISPLAY : DISPLAY;
}
static EGLBoolean EGLAPIENTRY initialize(
EGLDisplay display, EGLint *major, EGLint *minor)
{
assert(display == DISPLAY && !major && !minor);
if (fault == 4)
return EGL_FALSE;
refs++;
return EGL_TRUE;
}
static EGLBoolean EGLAPIENTRY terminate(EGLDisplay display)
{
assert(display == DISPLAY && refs > 7);
refs--;
terminated++;
return fault == 10 ? EGL_FALSE : EGL_TRUE;
}
static EGLBoolean EGLAPIENTRY choose(EGLDisplay display, const EGLint *attrs,
EGLConfig *configs, EGLint size, EGLint *count)
{
assert(display == DISPLAY);
assert(attrs[7] == (mode ? 10 : 8));
assert(attrs[9] == attrs[7] && attrs[11] == attrs[7]);
if (!configs)
{
assert(!size);
*count = fault == 5 || bad_configs ? 0 : fault == 6 ? 4097 : 2;
return fault != 7;
}
assert(size == 2);
configs[0] = (EGLConfig)&config_tokens[0];
configs[1] = (EGLConfig)&config_tokens[1];
*count = fault == 8 ? 3 : 2;
return fault != 9;
}
static EGLBoolean EGLAPIENTRY attrib(
EGLDisplay display, EGLConfig config, EGLint token, EGLint *value)
{
assert(display == DISPLAY);
if (fault == 11)
return EGL_FALSE;
*value = token == EGL_ALPHA_SIZE ? (mode ? 2 : 8) : (mode ? 10 : 8);
if (config == (EGLConfig)&config_tokens[0] || fault == 12)
*value = 16;
return EGL_TRUE;
}
static __eglMustCastToProperFunctionPointerType EGLAPIENTRY address(
const char *name)
{
if (missing && !strcmp(missing, name))
return NULL;
return unused;
}
dylib_t dylib_load(const char *name)
{
bool egl = !strcmp(name, "libEGL.so");
assert(egl || !strcmp(name, "libGLESv2.so"));
if (fault == (egl ? 1u : 2u))
return NULL;
libraries++;
return egl ? &egl_library : &gles_library;
}
function_t dylib_proc(dylib_t library, const char *name)
{
assert(library == &egl_library || library == &gles_library);
if (missing && !strcmp(missing, name))
return NULL;
if (!strcmp(name, "eglGetProcAddress"))
return (function_t)address;
if (!strcmp(name, "eglGetDisplay"))
return (function_t)get_display;
if (!strcmp(name, "eglInitialize"))
return (function_t)initialize;
if (!strcmp(name, "eglTerminate"))
return (function_t)terminate;
if (!strcmp(name, "eglChooseConfig"))
return (function_t)choose;
if (!strcmp(name, "eglGetConfigAttrib"))
return (function_t)attrib;
if (fallback && !strcmp(name, "glDrawArrays"))
return NULL;
return unused;
}
void dylib_close(dylib_t library)
{
assert(library == &egl_library || library == &gles_library);
assert(libraries);
libraries--;
}
int main(void)
{
static record_gpu_mediacodec_config_t config;
static const char *names[] = {"eglGetProcAddress", "eglGetDisplay",
"eglInitialize", "eglTerminate", "eglChooseConfig", "glDrawArrays"};
record_android_capture_egl_t *owner, *second;
unsigned i, before;
refs = 7;
assert(!record_android_capture_egl_new(3, &owner) && !owner);
assert(!record_android_capture_egl_new(0, NULL));
assert(!record_android_capture_egl_configure(NULL, &config));
assert(record_android_capture_egl_free(NULL));
for (mode = 0; mode < 3; mode++)
{
config.width = 1280;
config.height = 720;
config.count = 4;
config.flip_y = true;
config.buffers[0] = (struct AHardwareBuffer *)&config;
fallback = true;
assert(record_android_capture_egl_new(mode, &owner));
assert(libraries == 2 && refs == 8);
assert(!record_android_capture_egl_configure(owner, NULL));
assert(record_android_capture_egl_configure(owner, &config));
assert(config.display == DISPLAY && config.mode == mode);
assert(config.config == (EGLConfig)&config_tokens[1]);
assert(config.share == EGL_NO_CONTEXT && config.flip_y);
assert(config.width == 1280 && config.height == 720 && config.count == 4);
assert(config.buffers[0] == (struct AHardwareBuffer *)&config);
assert(config.draw.draw == (PFNGLDRAWARRAYSPROC)unused);
assert(record_android_capture_egl_free(owner));
assert(!libraries && refs == 7);
}
mode = 0;
for (i = 0; i < sizeof(names) / sizeof(names[0]); i++)
{
missing = names[i];
assert(!record_android_capture_egl_new(mode, &owner) && !owner);
assert(!libraries && refs == 7);
}
missing = NULL;
for (fault = 1; fault <= 12; fault++)
{
if (fault == 10)
continue;
assert(!record_android_capture_egl_new(mode, &owner) && !owner);
assert(!libraries && refs == 7);
}
fault = 0;
assert(record_android_capture_egl_new(mode, &owner));
assert(record_android_capture_egl_new(mode, &second));
assert(refs == 9 && libraries == 4);
assert(record_android_capture_egl_free(owner));
assert(refs == 8 && libraries == 2);
assert(record_android_capture_egl_configure(second, &config));
assert(record_android_capture_egl_free(second));
assert(refs == 7 && !libraries);
assert(record_android_capture_egl_new(mode, &owner));
fault = 10;
before = terminated;
assert(!record_android_capture_egl_free(owner));
assert(refs == 7 && libraries == 2 && terminated == before + 1);
assert(!record_android_capture_egl_configure(owner, &config));
assert(!record_android_capture_egl_free(owner));
assert(terminated == before + 1);
/* Mock display owns no native objects; release retained test storage. */
dylib_close(owner->egl);
dylib_close(owner->gles);
free(owner);
bad_configs = true;
before = terminated;
assert(!record_android_capture_egl_new(mode, &owner) && owner);
assert(refs == 7 && libraries == 2 && terminated == before + 1);
assert(!record_android_capture_egl_free(owner));
assert(terminated == before + 1);
dylib_close(owner->egl);
dylib_close(owner->gles);
free(owner);
return 0;
}
@@ -1,5 +1,8 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier:
* GPL-3.0-or-later */
#ifdef NDEBUG
#error Capture retirement acceptance requires assertions
#endif
#include <assert.h>
#include <stdint.h>
#include <stdio.h>
@@ -9,6 +12,8 @@
static unsigned images, textures, syncs, fences, waits, flushes, imported;
static unsigned image_calls, bad_image;
static bool bad_destroy, bad_wait, bad_fence, bad_sync;
static GLsync live_fences[32], newest, expected_poll;
static unsigned fence_serial;
static GLenum completion;
static const char *EGLAPIENTRY query(EGLDisplay d, EGLint name)
{
@@ -108,20 +113,36 @@ static const GLubyte *GL_APIENTRY string(GLenum name)
}
static GLsync GL_APIENTRY fence(GLenum type, GLbitfield flags)
{
unsigned i;
assert(type == GL_SYNC_GPU_COMMANDS_COMPLETE && !flags);
if (bad_fence)
return NULL;
fences++;
return (GLsync)(uintptr_t)400;
newest = (GLsync)(uintptr_t)(400 + ++fence_serial);
for (i = 0; i < 32; i++)
if (!live_fences[i])
{
live_fences[i] = newest;
return newest;
}
assert(0);
return NULL;
}
static GLenum GL_APIENTRY poll(GLsync s, GLbitfield flags, GLuint64 timeout)
{
assert(s && !flags && !timeout);
assert(!expected_poll || s == expected_poll);
return completion;
}
static void GL_APIENTRY delete_fence(GLsync s)
{
unsigned i;
assert(s && fences);
for (i = 0; i < 32; i++)
if (live_fences[i] == s)
break;
assert(i < 32);
live_fences[i] = NULL;
fences--;
}
static void GL_APIENTRY flush(void)
@@ -236,7 +257,38 @@ int main(void)
assert(record_android_capture_gl_free(c));
assert(!images && !textures && !syncs && !fences);
}
puts("PASS: fixed GLES imports, 16000 GPU-read retirements and "
assert(record_android_capture_gl_new(D, &ops, buffers, 2, &c));
for (i = 0; i < 2; i++)
{
fd = -1;
assert(record_android_capture_gl_begin(c, i, &fd, &texture));
bad_fence = i == 1;
assert(record_android_capture_gl_end(c, i) == !bad_fence);
}
assert(fences == 1);
bad_fence = true;
assert(record_android_capture_gl_recover_step(c) == -1 && fences == 1);
assert(!record_android_capture_gl_free(c));
bad_fence = false;
assert(record_android_capture_gl_recover_step(c) == 0 && fences == 2);
expected_poll = newest;
completion = GL_WAIT_FAILED;
assert(record_android_capture_gl_recover_step(c) == -1 && fences == 2);
assert(!record_android_capture_gl_free(c));
completion = GL_TIMEOUT_EXPIRED;
assert(record_android_capture_gl_recover_step(c) == 0 && fences == 2);
completion = GL_CONDITION_SATISFIED;
assert(record_android_capture_gl_recover_step(c) == 1 && !fences);
expected_poll = NULL;
assert(record_android_capture_gl_recover_step(c) == 1);
assert(record_android_capture_gl_poll(c, 0) == 1);
assert(record_android_capture_gl_poll(c, 1) == 1);
fd = -1;
assert(!record_android_capture_gl_begin(c, 0, &fd, &texture));
assert(record_android_capture_gl_free(c));
assert(!images && !textures && !syncs && !fences);
puts("PASS: fixed GLES imports, 16000 GPU-read retirements, terminal "
"recovery and "
"partial-fault ownership");
return 0;
}
@@ -0,0 +1,55 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier:
* GPL-3.0-or-later */
#include <assert.h>
#include <string.h>
#include "../../../record/record_android_capture_ops.h"
static unsigned calls, missing;
static void resolved(void)
{
}
static function_t resolve(void *context, const char *name)
{
assert(context == &calls);
assert(name && (name[0] == 'e' || name[0] == 'g'));
calls++;
return calls == missing ? NULL : resolved;
}
int main(void)
{
static record_gpu_mediacodec_config_t config, expected, zero;
unsigned count, i;
config.width = 1920;
config.height = 1080;
config.count = 3;
config.mode = 2;
config.flip_y = true;
config.display = (EGLDisplay)&calls;
config.config = (EGLConfig)&config;
config.share = (EGLContext)&expected;
config.buffers[0] = (struct AHardwareBuffer *)&zero;
expected = config;
assert(!record_android_capture_load_ops(NULL, resolve, &calls));
assert(!calls);
assert(record_android_capture_load_ops(&config, resolve, &calls));
count = calls;
assert(count > 0);
assert(config.surface.swap == (PFNEGLSWAPBUFFERSPROC)resolved);
assert(config.capture.image_target
== (record_capture_gl_image_target_t)resolved);
assert(config.draw.draw == (PFNGLDRAWARRAYSPROC)resolved);
for (i = 1; i <= count; i++)
{
calls = 0;
missing = i;
assert(!record_android_capture_load_ops(&config, resolve, &calls));
assert(calls == i);
assert(!memcmp(&config, &expected, sizeof(config)));
missing = 0;
calls = 0;
assert(record_android_capture_load_ops(&config, resolve, &calls));
assert(calls == count);
}
assert(!record_android_capture_load_ops(&config, NULL, NULL));
assert(!memcmp(&config, &expected, sizeof(config)));
return 0;
}
@@ -0,0 +1,287 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier:
* GPL-3.0-or-later */
#ifdef NDEBUG
#error Assertions are required
#endif
#include <assert.h>
#include <stdio.h>
#include "../../../record/record_android_capture_pipe.h"
struct record_android_capture_submit
{
int push, poll, fd, taken, active, released, discarded, queries;
};
struct record_gpu_dispatch_mediacodec
{
enum record_dispatch_mediacodec_state state;
int reserved, owned, fail_submit, cancels, posts;
unsigned slot;
};
static int closed_fd, closed_count;
static void close_fd(void *context, int fd)
{
assert(context == &closed_count);
closed_fd = fd;
closed_count++;
}
int record_android_capture_submit_push(record_android_capture_submit_t *e,
const record_android_capture_source_t *s)
{
(void)s;
assert(!e->active);
if (e->push == 1 || e->push == -2)
e->active = 1;
return e->push;
}
int record_android_capture_submit_record(record_android_capture_submit_t *e,
const record_android_capture_source_t *source, VkCommandBuffer command)
{
assert(command);
return record_android_capture_submit_push(e, source);
}
int record_android_capture_submit_commit(record_android_capture_submit_t *e)
{
assert(e->active);
return 1;
}
bool record_android_capture_submit_abort_recorded(
record_android_capture_submit_t *e)
{
assert(e->active);
e->active = 0;
return true;
}
int record_android_capture_submit_poll(record_android_capture_submit_t *e)
{
e->queries++;
return e->poll;
}
int record_android_capture_submit_take_ready(
record_android_capture_submit_t *e, int *fd)
{
if (!e->active || e->taken || e->poll == 0)
return 0;
if (e->poll < 0)
return -1;
*fd = e->fd;
e->taken = 1;
return 1;
}
bool record_android_capture_submit_release(record_android_capture_submit_t *e)
{
assert(e->active && e->taken && e->poll == 1);
e->active = e->taken = 0;
e->released++;
return true;
}
int record_android_capture_submit_discard(record_android_capture_submit_t *e)
{
e->queries++;
if (e->poll != 1)
return e->poll;
e->active = 0;
e->discarded++;
return 1;
}
int record_gpu_dispatch_mediacodec_acquire(
record_gpu_dispatch_mediacodec_t *g, unsigned *slot)
{
assert(!g->reserved);
if (g->owned)
return RECORD_GPU_QUEUE_BUSY;
*slot = g->slot;
g->reserved = 1;
return RECORD_GPU_QUEUE_OK;
}
bool record_gpu_dispatch_mediacodec_submit(
record_gpu_dispatch_mediacodec_t *g, const record_gpu_frame_t *f)
{
assert(g->reserved && f->slot == g->slot && !f->keyframe);
if (g->fail_submit)
return false;
g->reserved = 0;
g->owned = 1;
g->posts++;
return true;
}
bool record_gpu_dispatch_mediacodec_cancel_slot(
record_gpu_dispatch_mediacodec_t *g)
{
assert(g->reserved);
g->reserved = 0;
return true;
}
void record_gpu_dispatch_mediacodec_cancel(record_gpu_dispatch_mediacodec_t *g)
{
g->cancels++;
}
bool record_gpu_dispatch_mediacodec_finish(
record_gpu_dispatch_mediacodec_t *g, int64_t end)
{
(void)end;
assert(!g->reserved);
return true;
}
enum record_dispatch_mediacodec_state record_gpu_dispatch_mediacodec_status(
const record_gpu_dispatch_mediacodec_t *g, int *error, int *system_error)
{
(void)error;
(void)system_error;
return g->state;
}
#include "../../../record/record_android_capture_pipe.c"
static void lane(int fd, int deferred, int failure)
{
record_android_capture_submit_t entry = {0};
record_android_capture_submit_t *entries[2];
record_gpu_dispatch_mediacodec_t gpu = {0};
record_android_capture_source_t source = {0};
record_android_capture_pipe_t *p;
int queries, closes = closed_count;
entries[0] = entries[1] = &entry;
gpu.state = RECORD_MC_DISPATCH_READY;
entry.push = failure == 2 ? -2 : 1;
entry.poll = deferred ? 0 : 1;
entry.fd = fd;
assert(!record_android_capture_pipe_new(
&gpu, entries, 2, close_fd, &closed_count));
p = record_android_capture_pipe_new(
&gpu, entries, 1, close_fd, &closed_count);
assert(p && !record_android_capture_pipe_free(p));
assert(record_android_capture_pipe_push(p, &source, 0, 10)
== (failure == 2 ? -2 : 1));
assert(record_android_capture_pipe_push(p, &source, 0, 10) == -1);
if (failure != 2)
{
assert(!record_android_capture_pipe_finish(p, 10));
if (deferred)
{
queries = entry.queries;
assert(record_android_capture_pipe_step(p) == 0);
assert(entry.queries == queries + 1 && gpu.posts == 0);
entry.poll = 1;
}
gpu.fail_submit = failure == 1;
assert(record_android_capture_pipe_step(p) == (failure == 1 ? -1 : 0));
if (!failure)
{
assert(gpu.posts == 1);
assert(record_android_capture_pipe_push(p, &source, 20, 10) == 0);
gpu.owned = 0;
entry.poll = 0;
assert(record_android_capture_pipe_push(p, &source, 20, 10) == 0);
assert(entry.released == 0 && gpu.reserved);
assert(record_android_capture_pipe_finish(p, 10));
assert(!gpu.reserved);
}
}
if (failure)
{
assert(record_android_capture_pipe_finish(p, 10));
assert(gpu.cancels && !gpu.reserved);
assert(closed_count == closes + (failure == 1 && fd >= 0));
if (failure == 1 && fd >= 0)
assert(closed_fd == fd);
}
assert(!record_android_capture_pipe_finish(p, 9));
assert(!record_android_capture_pipe_finish(p, 11));
assert(record_android_capture_pipe_step(p) == 0);
gpu.state = failure ? RECORD_MC_DISPATCH_FAILED : RECORD_MC_DISPATCH_DONE;
entry.poll = -1;
assert(record_android_capture_pipe_step(p) == -1);
assert(!record_android_capture_pipe_free(p));
entry.poll = 0;
assert(record_android_capture_pipe_step(p) == 0);
entry.poll = 1;
assert(record_android_capture_pipe_step(p) == 1);
assert(entry.discarded == 1);
assert(record_android_capture_pipe_step(p) == 1);
assert(record_android_capture_pipe_free(p));
}
static void multiple(void)
{
record_android_capture_submit_t first = {0}, second = {0};
record_android_capture_submit_t *entries[2];
record_gpu_dispatch_mediacodec_t gpu = {0};
record_android_capture_source_t source = {0};
record_android_capture_pipe_t *p;
int queries;
entries[0] = &first;
entries[1] = &second;
first.push = first.poll = second.push = second.poll = 1;
first.fd = second.fd = -1;
gpu.state = RECORD_MC_DISPATCH_READY;
p = record_android_capture_pipe_new(
&gpu, entries, 2, close_fd, &closed_count);
assert(p);
assert(record_android_capture_pipe_push(p, &source, 0, 10) == 1);
assert(record_android_capture_pipe_step(p) == 0);
gpu.owned = 0;
gpu.slot = 1;
assert(record_android_capture_pipe_push(p, &source, 20, 10) == 1);
assert(record_android_capture_pipe_step(p) == 0);
assert(!record_android_capture_pipe_sources_retired(p));
assert(record_android_capture_pipe_step(p) == 0);
assert(!record_android_capture_pipe_sources_retired(p));
assert(record_android_capture_pipe_step(p) == 0);
assert(record_android_capture_pipe_sources_retired(p));
queries = first.queries + second.queries;
assert(record_android_capture_pipe_step(p) == 0);
assert(first.queries + second.queries == queries);
assert(record_android_capture_pipe_finish(p, 30));
gpu.owned = 0;
gpu.state = RECORD_MC_DISPATCH_DONE;
queries = first.queries + second.queries;
assert(record_android_capture_pipe_step(p) == 0);
assert(first.queries + second.queries == queries + 1);
assert(!record_android_capture_pipe_free(p));
assert(record_android_capture_pipe_step(p) == 1);
assert(first.discarded == 1 && second.discarded == 1);
assert(record_android_capture_pipe_free(p));
}
static void reuse(void)
{
record_android_capture_submit_t entry = {0};
record_android_capture_submit_t *entries[1];
record_gpu_dispatch_mediacodec_t gpu = {0};
record_android_capture_source_t source = {0};
record_android_capture_pipe_t *p;
entries[0] = &entry;
gpu.state = RECORD_MC_DISPATCH_READY;
entry.push = entry.poll = 1;
entry.fd = -1;
p = record_android_capture_pipe_new(
&gpu, entries, 1, close_fd, &closed_count);
assert(p);
assert(record_android_capture_pipe_push(p, &source, 0, 0) == -1);
assert(record_android_capture_pipe_push(p, &source, (uint64_t)-1, 1) == -1);
assert(!gpu.reserved && !entry.active);
assert(record_android_capture_pipe_push(p, &source, 0, 10) == 1);
assert(record_android_capture_pipe_step(p) == 0);
gpu.owned = 0;
assert(record_android_capture_pipe_push(p, &source, 20, 10) == 1);
assert(entry.released == 1);
assert(!record_android_capture_pipe_sources_retired(p));
assert(record_android_capture_pipe_step(p) == 0);
assert(gpu.posts == 2);
assert(record_android_capture_pipe_finish(p, 30));
gpu.state = RECORD_MC_DISPATCH_DRAINING;
assert(record_android_capture_pipe_step(p) == 0);
assert(!gpu.cancels);
gpu.owned = 0;
gpu.state = RECORD_MC_DISPATCH_DONE;
assert(record_android_capture_pipe_step(p) == 1);
assert(record_android_capture_pipe_free(p));
}
int main(void)
{
reuse();
multiple();
lane(-1, 0, 0);
lane(4, 1, 0);
lane(5, 1, 1);
lane(-1, 0, 1);
lane(6, 1, 2);
puts("PASS: capture relay deferred submission, dual retirement and "
"cancellation");
return 0;
}
@@ -0,0 +1,607 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier:
* GPL-3.0-or-later */
#ifdef NDEBUG
#error Assertions are required
#endif
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include "../../../record/record_android_capture_pool.h"
#include "../../../gfx/video_defines.h"
struct record_android_capture_images
{
unsigned live;
};
struct record_android_capture_submit
{
bool live, prepared;
};
struct record_gpu_dispatch_mediacodec
{
enum record_dispatch_mediacodec_state state;
};
struct record_android_capture_pipe
{
unsigned live;
};
static struct record_android_capture_images images;
static struct record_android_capture_submit entries[RECORD_GPU_QUEUE_SLOTS];
static struct record_gpu_dispatch_mediacodec gpu;
static struct record_android_capture_pipe pipe;
static struct model
{
unsigned count, created, fail_entry, prepared, after, pipe_frees, pipe_steps;
bool fail_images, fail_gpu, fail_pipe, gpu_live, pending, pipe_done;
bool sources_done, cancelled, refuse_gpu_free, images_prepared, destroyed;
bool copies_done;
record_android_capture_source_t source;
uint64_t expected_pts;
unsigned records, commits;
int record_result, commit_result;
VkFormat format;
} m;
static void reset(unsigned mode)
{
assert(!images.live && !m.gpu_live && !pipe.live);
memset(&m, 0, sizeof(m));
memset(entries, 0, sizeof(entries));
m.count = 2;
m.record_result = m.commit_result = 1;
m.format = mode == 2 ? VK_FORMAT_R16G16B16A16_SFLOAT
: mode == 1 ? VK_FORMAT_A2B10G10R10_UNORM_PACK32
: VK_FORMAT_R8G8B8A8_UNORM;
}
record_android_capture_images_t *record_android_capture_images_new(
VkPhysicalDevice physical, VkDevice device,
const record_android_capture_image_ops_t *ops,
const VkPhysicalDeviceMemoryProperties *memory, unsigned count,
unsigned width, unsigned height, VkFormat format)
{
assert(physical && device && ops && memory && count == m.count);
assert(width == 128 && height == 96 && format == m.format);
if (m.fail_images)
return NULL;
images.live = 1;
return &images;
}
VkImage record_android_capture_images_image(
const record_android_capture_images_t *p, unsigned slot)
{
assert(p == &images && p->live && slot < m.count);
return (VkImage)(uintptr_t)(slot + 1);
}
struct AHardwareBuffer *record_android_capture_images_buffer(
const record_android_capture_images_t *p, unsigned slot)
{
assert(p == &images && p->live && slot < m.count);
return (struct AHardwareBuffer *)(uintptr_t)(slot + 1);
}
void record_android_capture_images_prepare_device_destroy(
record_android_capture_images_t *p)
{
assert(p == &images && p->live && m.prepared == m.count);
m.images_prepared = true;
}
void record_android_capture_images_free(record_android_capture_images_t *p)
{
unsigned i;
if (!p)
return;
assert(p == &images && p->live && !m.gpu_live && !pipe.live);
for (i = 0; i < m.count; i++)
assert(!entries[i].live);
assert(!m.images_prepared || m.destroyed);
p->live = 0;
}
record_android_capture_submit_t *record_android_capture_submit_new(
VkDevice device, VkQueue queue, unsigned family, VkImage image,
VkFormat format, unsigned width, unsigned height,
const record_android_capture_submit_ops_t *ops)
{
unsigned i = m.created++;
assert(device && queue && family == 3
&& image == (VkImage)(uintptr_t)(i + 1));
assert(format == m.format && width == 128 && height == 96 && ops);
if (m.fail_entry == i + 1)
return NULL;
entries[i].live = true;
return &entries[i];
}
bool record_android_capture_submit_free(record_android_capture_submit_t *e)
{
if (!e)
return true;
assert(e->live && !pipe.live && !m.gpu_live);
e->live = false;
return true;
}
bool record_android_capture_submit_prepare_device_destroy(
record_android_capture_submit_t *e, bool quiesced)
{
if (!e)
return true;
assert(e->live);
if (!quiesced && !m.copies_done)
return false;
if (m.fail_entry && e == &entries[m.fail_entry - 1])
return false;
if (!e->prepared)
{
e->prepared = true;
m.prepared++;
}
return true;
}
bool record_android_capture_submit_after_device_destroy(
record_android_capture_submit_t *e)
{
if (!e)
return true;
assert(m.images_prepared && m.destroyed && e->live && e->prepared);
m.after++;
return true;
}
record_gpu_dispatch_mediacodec_t *record_gpu_dispatch_mediacodec_new(
record_dispatch_mediacodec_t *io,
const record_gpu_mediacodec_config_t *config)
{
unsigned i;
assert(io && images.live && m.created == m.count);
for (i = 0; i < m.count; i++)
assert(
config->buffers[i] == (struct AHardwareBuffer *)(uintptr_t)(i + 1));
if (m.fail_gpu)
return NULL;
m.gpu_live = true;
gpu.state = RECORD_MC_DISPATCH_STARTING;
return &gpu;
}
enum record_dispatch_mediacodec_state record_gpu_dispatch_mediacodec_status(
const record_gpu_dispatch_mediacodec_t *g, int *error, int *system_error)
{
if (error)
*error = 17;
if (system_error)
*system_error = 19;
if (!g)
return RECORD_MC_DISPATCH_FAILED;
assert(g == &gpu && m.gpu_live);
return g->state;
}
bool record_gpu_dispatch_mediacodec_free(record_gpu_dispatch_mediacodec_t *g)
{
assert(g == &gpu && m.gpu_live && !pipe.live);
if (m.refuse_gpu_free)
return false;
m.gpu_live = false;
return true;
}
void record_gpu_dispatch_mediacodec_cancel(record_gpu_dispatch_mediacodec_t *g)
{
assert(g == &gpu);
m.cancelled = true;
}
bool record_gpu_dispatch_mediacodec_finish(
record_gpu_dispatch_mediacodec_t *g, int64_t end)
{
assert(g == &gpu && end >= 0);
return !m.pending;
}
record_android_capture_pipe_t *record_android_capture_pipe_new(
record_gpu_dispatch_mediacodec_t *g,
record_android_capture_submit_t *const *e, unsigned count,
void (*close_fd)(void *, int), void *context)
{
(void)context;
assert(g == &gpu && e[0] == &entries[0] && count == m.count && close_fd);
if (m.fail_pipe)
return NULL;
pipe.live = 1;
return &pipe;
}
int record_android_capture_pipe_push(record_android_capture_pipe_t *p,
const record_android_capture_source_t *s, uint64_t pts, uint64_t duration)
{
assert(
p == &pipe && p->live && s && pts == m.expected_pts && duration == 10);
m.records++;
if (m.record_result == 1)
m.source = *s;
return m.record_result;
}
int record_android_capture_pipe_record(record_android_capture_pipe_t *p,
const record_android_capture_source_t *source, uint64_t pts,
uint64_t duration, VkCommandBuffer command)
{
assert(command == (VkCommandBuffer)(uintptr_t)77);
return record_android_capture_pipe_push(p, source, pts, duration);
}
int record_android_capture_pipe_commit(record_android_capture_pipe_t *p)
{
assert(p == &pipe && p->live);
m.commits++;
return m.commit_result;
}
bool record_android_capture_pipe_abort_recorded(
record_android_capture_pipe_t *p)
{
assert(p == &pipe && p->live);
return true;
}
int record_android_capture_pipe_step(record_android_capture_pipe_t *p)
{
assert(p == &pipe && p->live);
m.pipe_steps++;
return m.pipe_done ? 1 : 0;
}
bool record_android_capture_pipe_sources_retired(
const record_android_capture_pipe_t *p)
{
return !p || m.sources_done;
}
bool record_android_capture_pipe_finish(
record_android_capture_pipe_t *p, int64_t end)
{
assert(p == &pipe && p->live && end >= 0);
return !m.pending;
}
void record_android_capture_pipe_cancel(record_android_capture_pipe_t *p)
{
assert(p == &pipe && p->live);
m.cancelled = true;
}
bool record_android_capture_pipe_free(record_android_capture_pipe_t *p)
{
assert(p == &pipe && p->live && m.pipe_done);
p->live = 0;
m.pipe_frees++;
return true;
}
#include "../../../record/record_android_capture_device.c"
#include "../../../record/record_android_capture_pool.c"
static void close_fd(void *context, int fd)
{
(void)context;
(void)fd;
}
static record_android_capture_pool_config_t config(unsigned mode)
{
record_android_capture_pool_config_t c;
static VkPhysicalDeviceMemoryProperties memory;
memset(&c, 0, sizeof(c));
c.physical = (VkPhysicalDevice)(uintptr_t)1;
c.device = (VkDevice)(uintptr_t)2;
c.queue = (VkQueue)(uintptr_t)3;
c.family = 3;
c.memory = &memory;
c.submit.close_fd = close_fd;
c.gpu.count = m.count;
c.gpu.width = 128;
c.gpu.height = 96;
c.gpu.mode = mode;
return c;
}
static void normal(unsigned mode, unsigned count)
{
static record_android_capture_pool_config_t c;
record_android_capture_pool_t *p;
record_android_capture_source_t source;
int error, system_error;
reset(mode);
m.count = count;
c = config(mode);
memset(&source, 0, sizeof(source));
assert(record_android_capture_pool_new(
(record_dispatch_mediacodec_t *)&m, &c, &p));
assert(p && !record_android_capture_pool_free(p));
assert(record_android_capture_pool_status(p, &error, &system_error)
== RECORD_MC_DISPATCH_STARTING
&& error == 17 && system_error == 19);
assert(record_android_capture_pool_push(p, &source, 0, 10) == 1);
assert(record_android_capture_pool_record(
p, &source, 0, 10, (VkCommandBuffer)(uintptr_t)77)
== 1);
assert(record_android_capture_pool_abort_recorded(p));
assert(record_android_capture_pool_commit(p) == 1);
assert(!record_android_capture_pool_sources_retired(p));
m.sources_done = true;
assert(record_android_capture_pool_sources_retired(p));
m.pending = true;
assert(!record_android_capture_pool_finish(p, 10));
m.pending = false;
assert(!record_android_capture_pool_prepare_device_destroy(p, true));
assert(record_android_capture_pool_finish(p, 10));
assert(!record_android_capture_pool_finish(p, 11));
assert(record_android_capture_pool_push(p, &source, 0, 10) == -1);
assert(record_android_capture_pool_record(
p, &source, 0, 10, (VkCommandBuffer)(uintptr_t)77)
== -1);
assert(record_android_capture_pool_commit(p) == 1);
assert(record_android_capture_pool_abort_recorded(p));
gpu.state = RECORD_MC_DISPATCH_DONE;
assert(!record_android_capture_pool_free(p));
m.pipe_done = true;
m.refuse_gpu_free = true;
assert(!record_android_capture_pool_free(p) && m.pipe_frees == 1);
m.refuse_gpu_free = false;
assert(record_android_capture_pool_free(p));
}
static void faults(void)
{
static record_android_capture_pool_config_t c;
record_android_capture_pool_t *p;
unsigned i;
for (i = 0; i < 4; i++)
{
reset(0);
c = config(0);
m.fail_images = i == 0;
m.fail_entry = i == 1 ? 2 : 0;
m.fail_gpu = i == 2;
m.fail_pipe = i == 3;
assert(!record_android_capture_pool_new(
(record_dispatch_mediacodec_t *)&m, &c, &p));
if (i != 3)
assert(!p && !images.live && !m.cancelled);
else
{
assert(p && m.cancelled && images.live);
assert(record_android_capture_pool_step(p) == 0);
assert(!record_android_capture_pool_free(p));
gpu.state = RECORD_MC_DISPATCH_FAILED;
assert(record_android_capture_pool_free(p));
}
}
}
static void device_loss(unsigned mode, bool quiesced)
{
static record_android_capture_pool_config_t c;
record_android_capture_pool_t *p;
unsigned i;
reset(mode);
c = config(mode);
assert(record_android_capture_pool_new(
(record_dispatch_mediacodec_t *)&m, &c, &p));
assert(!record_android_capture_pool_after_device_destroy(p));
assert(!record_android_capture_pool_prepare_device_destroy(p, true));
record_android_capture_pool_cancel(p);
assert(record_android_capture_pool_finish(p, 0));
assert(!record_android_capture_pool_prepare_device_destroy(p, false));
assert(!m.prepared && !m.images_prepared);
assert(record_android_capture_pool_commit(p) == -1);
assert(!record_android_capture_pool_abort_recorded(p));
m.copies_done = !quiesced;
m.fail_entry = 2;
assert(!record_android_capture_pool_prepare_device_destroy(p, quiesced));
assert(m.prepared == 1 && !m.images_prepared);
for (i = 0; i < 100; i++)
{
assert(record_android_capture_pool_step(p) == 0);
assert(!record_android_capture_pool_free(p));
assert(!m.pipe_steps && !m.pipe_frees && images.live && m.gpu_live);
}
m.fail_entry = 0;
assert(record_android_capture_pool_prepare_device_destroy(p, quiesced));
assert(m.prepared == 2 && m.images_prepared);
assert(record_android_capture_pool_prepare_device_destroy(p, false));
gpu.state = RECORD_MC_DISPATCH_READY;
assert(record_android_capture_pool_step(p) == 0);
assert(!record_android_capture_pool_free(p));
assert(!m.pipe_steps && images.live && m.gpu_live);
gpu.state = RECORD_MC_DISPATCH_DRAINING;
m.destroyed = true;
assert(record_android_capture_pool_after_device_destroy(p) && m.after == 2);
for (i = 0; i < 100; i++)
{
assert(record_android_capture_pool_step(p) == 0);
assert(!record_android_capture_pool_free(p));
assert(!m.pipe_steps && !m.pipe_frees && images.live && m.gpu_live);
}
gpu.state = RECORD_MC_DISPATCH_FAILED;
assert(record_android_capture_pool_step(p) == 0);
assert(!record_android_capture_pool_free(p));
m.pipe_done = true;
assert(record_android_capture_pool_step(p) == 1);
assert(record_android_capture_pool_free(p));
}
static void renderer_binding(unsigned mode, unsigned fault)
{
static record_android_capture_pool_config_t c;
record_android_capture_pool_t *p;
record_android_capture_renderer_t binding;
record_android_capture_source_t source;
unsigned dims = VIDEO_SCALE_PACK(128, 96);
reset(mode);
c = config(mode);
c.fail_on_drop = fault == 6;
if (fault == 6)
m.record_result = 0;
memset(&binding, 0, sizeof(binding));
memset(&source, 0, sizeof(source));
assert(record_android_capture_pool_new(
(record_dispatch_mediacodec_t *)&m, &c, &p));
assert(!record_android_capture_pool_bind_renderer(NULL, 10, &binding));
assert(!record_android_capture_pool_bind_renderer(p, 0, &binding));
assert(!record_android_capture_pool_bind_renderer(p, 1000001, &binding));
assert(!record_android_capture_pool_bind_renderer(p, 10, NULL));
assert(record_android_capture_pool_bind_renderer(p, 10, &binding));
assert(binding.context == p && binding.record && binding.submitted
&& binding.service && binding.release);
assert(!record_android_capture_pool_bind_renderer(p, 10, &binding));
assert(binding.context == p);
source.owner = binding.context;
source.release = binding.release;
source.image = (VkImage)(uintptr_t)99;
source.format = m.format;
source.width = 128;
source.height = 96;
source.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
if (fault == 3 || fault == 4 || fault == 6)
{
assert(binding.record(binding.context, &source,
(VkCommandBuffer)(uintptr_t)77, 0,
fault == 3 ? VIDEO_SCALE_PACK(64, 48) : dims,
fault == 4 ? (mode + 1) % 3 : mode)
== -1);
assert(m.cancelled && m.records == (fault == 6 ? 1u : 0u)
&& !m.commits && p->closed);
}
else
{
if (fault == 5)
{
m.record_result = 0;
assert(binding.record(binding.context, &source,
(VkCommandBuffer)(uintptr_t)77, 0, dims, mode)
== 0);
assert(binding.record(binding.context, &source,
(VkCommandBuffer)(uintptr_t)77, 0, dims, mode)
== 0);
assert(m.records == 1 && !p->renderer_leases);
m.record_result = 1;
m.expected_pts = 1;
}
assert(binding.record(binding.context, &source,
(VkCommandBuffer)(uintptr_t)77, m.expected_pts, dims, mode)
== 1);
assert(p->renderer_leases == 1 && p->renderer_recorded);
assert(binding.record(binding.context, &source,
(VkCommandBuffer)(uintptr_t)77, m.expected_pts, dims, mode)
== 0);
if (fault == 1)
m.commit_result = -1;
binding.submitted(
binding.context, fault == 2 ? VK_ERROR_DEVICE_LOST : VK_SUCCESS);
assert(!p->renderer_recorded && p->renderer_leases == 1);
assert(m.commits == (fault == 2 ? 0u : 1u));
binding.submitted(binding.context, VK_SUCCESS);
assert(m.commits == (fault == 2 ? 0u : 1u));
if (fault == 1 || fault == 2)
assert(m.cancelled && p->closed);
else
{
assert(!m.cancelled);
binding.service(binding.context);
assert(record_android_capture_pool_finish(p, 10 + m.expected_pts));
}
}
if (m.cancelled)
{
assert(record_android_capture_pool_prepare_device_destroy(p, true));
m.destroyed = true;
assert(record_android_capture_pool_after_device_destroy(p));
}
if (p->renderer_leases)
{
assert(m.source.owner == p && m.source.release == binding.release);
m.source.release(m.source.owner, m.destroyed);
assert(!p->renderer_leases);
}
gpu.state
= m.cancelled ? RECORD_MC_DISPATCH_FAILED : RECORD_MC_DISPATCH_DONE;
m.pipe_done = true;
assert(record_android_capture_pool_free(p));
}
static void device_registration(unsigned mode)
{
static record_android_capture_pool_config_t c;
record_android_capture_pool_t *p;
unsigned i;
record_android_capture_source_t source;
reset(mode);
c = config(mode);
assert(record_android_capture_pool_new(
(record_dispatch_mediacodec_t *)&m, &c, &p));
/* No renderer binding or frame exists yet; startup is still protected. */
assert(record_android_capture_device_pending(c.device));
assert(!record_android_capture_device_pending((VkDevice)(uintptr_t)999));
assert(!record_android_capture_device_destroyed(c.device));
memset(&source, 0, sizeof(source));
if (mode == 1)
assert(record_android_capture_pool_push(p, &source, 0, 10) == 1);
if (mode == 2)
assert(record_android_capture_pool_record(p, &source, 0, 10,
(VkCommandBuffer)(uintptr_t)77) == 1);
assert(p->renderer_end == (mode ? 10u : 0u));
m.pending = true;
assert(!record_android_capture_device_prepare(c.device, true));
assert(!p->closed && p->preparing && !p->device_owner.prepared);
assert(record_android_capture_pool_step(p) == 0);
assert(record_android_capture_pool_push(p, &source, 0, 10) == -1);
assert(record_android_capture_pool_record(p, &source, 0, 10,
(VkCommandBuffer)(uintptr_t)77) == -1);
m.pending = false;
assert(!record_android_capture_device_prepare(c.device, false));
assert(p->closed && p->preparing && !p->device_owner.prepared);
assert(record_android_capture_device_prepare(c.device, true));
assert(p->device_owner.prepared && !m.after);
assert(record_android_capture_device_pending(c.device));
m.destroyed = true;
assert(record_android_capture_device_destroyed(c.device));
assert(m.after == c.gpu.count);
assert(!record_android_capture_device_pending(c.device));
assert(record_android_capture_device_destroyed(c.device));
assert(m.after == c.gpu.count);
for (i = 0; i < 100; i++)
{
assert(!record_android_capture_pool_free(p));
assert(!m.pipe_steps && images.live && m.gpu_live);
}
gpu.state = RECORD_MC_DISPATCH_FAILED;
m.pipe_done = true;
assert(record_android_capture_pool_free(p));
assert(!record_acdevice_owners);
}
static void bad_inputs(void)
{
static record_android_capture_pool_config_t c;
record_android_capture_pool_t *p = NULL;
reset(0);
c = config(0);
assert(!record_android_capture_pool_new(NULL, &c, &p) && !p);
assert(!record_android_capture_pool_new(
(record_dispatch_mediacodec_t *)&m, NULL, &p)
&& !p);
assert(!record_android_capture_pool_new(
(record_dispatch_mediacodec_t *)&m, &c, NULL));
c.gpu.mode = 3;
assert(!record_android_capture_pool_new(
(record_dispatch_mediacodec_t *)&m, &c, &p)
&& !p);
c.gpu.mode = 0;
c.gpu.count = 0;
assert(!record_android_capture_pool_new(
(record_dispatch_mediacodec_t *)&m, &c, &p)
&& !p);
assert(!images.live);
}
int main(void)
{
unsigned mode, fault;
assert(record_android_capture_pool_status(NULL, NULL, NULL)
== RECORD_MC_DISPATCH_FAILED);
bad_inputs();
device_registration(0);
device_registration(1);
device_registration(2);
normal(0, 1);
normal(0, 16);
normal(1, 2);
normal(2, 2);
faults();
device_loss(0, false);
device_loss(0, true);
device_loss(1, false);
device_loss(1, true);
device_loss(2, false);
device_loss(2, true);
for (mode = 0; mode < 3; mode++)
for (fault = 0; fault < 7; fault++)
renderer_binding(mode, fault);
assert(!record_acdevice_owners);
assert(record_android_capture_pool_free(NULL));
puts("PASS: joint capture pool construction, partial startup and ordered "
"collection; native owners mocked");
return 0;
}
@@ -4,6 +4,7 @@
#include <stdio.h>
#include <string.h>
#include "../../../record/record_android_capture_submit.h"
#include "../../../record/record_android_capture_pipe.h"
#include "../../../gfx/common/vulkan_android_queue.h"
#define DEVICE ((VkDevice)(uintptr_t)1)
#define QUEUE ((VkQueue)(uintptr_t)2)
@@ -14,6 +15,7 @@
#define FENCE ((VkFence)(uintptr_t)7)
#define DONE ((VkSemaphore)(uintptr_t)8)
#define READY ((VkSemaphore)(uintptr_t)9)
#define RENDER_COMMAND ((VkCommandBuffer)(uintptr_t)10)
typedef struct model
{
vulkan_android_queue_t queued;
@@ -23,7 +25,7 @@ typedef struct model
unsigned refs, releases, fd_refs, close_calls, export_failures;
bool pool, command, fence, semaphore, pending, complete, signal;
bool deferred, auto_complete, device_alive, gone_release, ready;
bool partial_fd, invalid_fd, signaled_fd;
bool partial_fd, invalid_fd, signaled_fd, external, abort_proof;
} model_t;
static model_t m;
@@ -112,7 +114,9 @@ static VkResult VKAPI_CALL begin(
}
static VkResult VKAPI_CALL end(VkCommandBuffer c)
{
assert(c == COMMAND && m.barriers == 2 && m.copies == 1);
assert(c == COMMAND);
assert(m.external ? (!m.barriers && !m.copies)
: (m.barriers == 2 && m.copies == 1));
return m.record_failure == 3 ? VK_ERROR_DEVICE_LOST : VK_SUCCESS;
}
static void VKAPI_CALL barrier(VkCommandBuffer c, VkPipelineStageFlags src,
@@ -120,7 +124,8 @@ static void VKAPI_CALL barrier(VkCommandBuffer c, VkPipelineStageFlags src,
const VkMemoryBarrier *mb, uint32_t nb, const VkBufferMemoryBarrier *bb,
uint32_t ni, const VkImageMemoryBarrier *b)
{
assert(c == COMMAND && !flags && !nm && !mb && !nb && !bb && ni == 2);
assert(c == (m.external ? RENDER_COMMAND : COMMAND) && !flags && !nm && !mb
&& !nb && !bb && ni == 2);
assert(b[0].sType == VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER
&& b[0].image == SOURCE);
assert(b[1].sType == VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER
@@ -165,8 +170,8 @@ static void VKAPI_CALL barrier(VkCommandBuffer c, VkPipelineStageFlags src,
static void VKAPI_CALL copy(VkCommandBuffer c, VkImage src, VkImageLayout sl,
VkImage dst, VkImageLayout dl, uint32_t count, const VkImageCopy *r)
{
assert(c == COMMAND && src == SOURCE && dst == DEST && count == 1
&& m.barriers == 1);
assert(c == (m.external ? RENDER_COMMAND : COMMAND) && src == SOURCE
&& dst == DEST && count == 1 && m.barriers == 1);
assert(sl == VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL
&& dl == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
assert(r->srcOffset.x == 8 && r->srcOffset.y == 6 && !r->srcOffset.z
@@ -261,7 +266,9 @@ static void close_fd(void *context, int fd)
static void release_source(void *owner, bool destroyed)
{
assert(owner == &m && m.refs == 1);
assert(destroyed ? !m.device_alive : m.pending && m.complete);
assert(destroyed
? !m.device_alive
: (m.pending && m.complete) || (m.abort_proof && !m.pending));
m.refs--;
m.releases++;
m.gone_release = destroyed;
@@ -562,8 +569,435 @@ static void formats_and_layouts(void)
assert(record_android_capture_submit_free(e));
}
}
struct record_gpu_dispatch_mediacodec
{
record_gpu_queue_t *queue;
enum record_dispatch_mediacodec_state state;
bool cancelled;
};
int record_gpu_dispatch_mediacodec_acquire(
record_gpu_dispatch_mediacodec_t *g, unsigned *slot)
{
return record_gpu_queue_acquire(g->queue, slot);
}
bool record_gpu_dispatch_mediacodec_submit(
record_gpu_dispatch_mediacodec_t *g, const record_gpu_frame_t *frame)
{
return !g->cancelled && record_gpu_queue_submit(g->queue, frame);
}
bool record_gpu_dispatch_mediacodec_cancel_slot(
record_gpu_dispatch_mediacodec_t *g)
{
return record_gpu_queue_cancel(g->queue);
}
bool record_gpu_dispatch_mediacodec_finish(
record_gpu_dispatch_mediacodec_t *g, int64_t end_us)
{
(void)end_us;
return record_gpu_queue_finish(g->queue);
}
void record_gpu_dispatch_mediacodec_cancel(record_gpu_dispatch_mediacodec_t *g)
{
g->cancelled = true;
}
enum record_dispatch_mediacodec_state record_gpu_dispatch_mediacodec_status(
const record_gpu_dispatch_mediacodec_t *g, int *error, int *system_error)
{
(void)error;
(void)system_error;
return g->state;
}
static void pipe_lifetime(bool deferred, bool cancel)
{
record_android_capture_submit_t *entry;
record_android_capture_pipe_t *pipe;
record_android_capture_source_t s;
record_gpu_dispatch_mediacodec_t gpu;
record_gpu_frame_t frame;
reset();
m.deferred = deferred;
entry = create();
assert(entry);
memset(&gpu, 0, sizeof(gpu));
gpu.queue = record_gpu_queue_new(1);
gpu.state = RECORD_MC_DISPATCH_READY;
assert(gpu.queue);
pipe = record_android_capture_pipe_new(&gpu, &entry, 1, close_fd, &m);
assert(pipe);
s = source();
assert(record_android_capture_pipe_push(pipe, &s, 0, 10) == 1);
assert(m.refs == 1 && m.submits == (deferred ? 0u : 1u));
if (cancel)
{
record_android_capture_pipe_cancel(pipe);
assert(record_android_capture_pipe_finish(pipe, 10));
assert(gpu.cancelled);
gpu.state = RECORD_MC_DISPATCH_FAILED;
assert(record_android_capture_pipe_step(pipe) == 0);
assert(m.refs == 1 && !record_android_capture_pipe_free(pipe));
}
else
{
assert(!record_android_capture_pipe_finish(pipe, 10));
assert(record_android_capture_pipe_step(pipe) == 0);
if (deferred)
assert(record_gpu_queue_take(gpu.queue, &frame)
== RECORD_GPU_QUEUE_BUSY);
}
if (deferred)
assert(
vulkan_android_queue_flush(&m.queued, native_submit) == VK_SUCCESS);
if (!cancel && deferred)
{
assert(record_android_capture_pipe_step(pipe) == 0);
assert(m.refs == 1 && m.exports == 0 && m.submits == 1);
m.complete = true;
assert(record_android_capture_pipe_step(pipe) == 0);
assert(m.refs == 0 && m.exports == 1);
}
if (!cancel)
{
assert(record_gpu_queue_take(gpu.queue, &frame) == RECORD_GPU_QUEUE_OK);
assert(frame.ready_value == 101 && frame.timestamp == 0
&& frame.duration == 10);
close_fd(&m, (int)frame.ready_value - 1);
assert(record_android_capture_pipe_finish(pipe, 10));
gpu.state = RECORD_MC_DISPATCH_DRAINING;
assert(record_android_capture_pipe_step(pipe) == 0 && !gpu.cancelled);
assert(!record_android_capture_pipe_free(pipe));
assert(record_gpu_queue_release(gpu.queue, frame.slot));
}
gpu.state = cancel ? RECORD_MC_DISPATCH_FAILED : RECORD_MC_DISPATCH_DONE;
if (!m.complete)
{
assert(record_android_capture_pipe_step(pipe) == 0);
assert(m.refs == 1 && !record_android_capture_submit_free(entry));
assert(!record_android_capture_pipe_free(pipe));
}
m.complete = true;
assert(record_android_capture_pipe_step(pipe) == 1);
assert(!m.refs && !m.fd_refs && m.submits == 1 && m.releases == 1);
assert(record_android_capture_pipe_free(pipe));
assert(record_android_capture_submit_free(entry));
assert(record_gpu_queue_free(gpu.queue));
}
static void pipe_device_loss(void)
{
record_android_capture_submit_t *entry;
record_android_capture_pipe_t *pipe;
record_android_capture_source_t s;
record_gpu_dispatch_mediacodec_t gpu;
reset();
entry = create();
assert(entry);
memset(&gpu, 0, sizeof(gpu));
gpu.queue = record_gpu_queue_new(1);
gpu.state = RECORD_MC_DISPATCH_READY;
assert(gpu.queue);
pipe = record_android_capture_pipe_new(&gpu, &entry, 1, close_fd, &m);
assert(pipe);
s = source();
assert(record_android_capture_pipe_push(pipe, &s, 0, 10) == 1);
record_android_capture_pipe_cancel(pipe);
assert(record_android_capture_pipe_finish(pipe, 10));
gpu.state = RECORD_MC_DISPATCH_FAILED;
m.fence_error = VK_ERROR_DEVICE_LOST;
assert(record_android_capture_pipe_step(pipe) == -1);
assert(m.refs == 1 && !record_android_capture_pipe_free(pipe));
assert(record_android_capture_submit_prepare_device_destroy(entry, false));
assert(m.refs == 1 && !m.pool && !m.fence && !m.semaphore);
m.device_alive = false;
assert(record_android_capture_submit_after_device_destroy(entry));
assert(!m.refs && !m.fd_refs && m.gone_release);
assert(record_android_capture_pipe_step(pipe) == 1);
assert(record_android_capture_pipe_free(pipe));
assert(record_android_capture_submit_free(entry));
assert(record_gpu_queue_free(gpu.queue));
}
static void pipe_source_retirement(void)
{
record_android_capture_submit_t *entry;
record_android_capture_pipe_t *pipe;
record_android_capture_source_t s;
record_gpu_dispatch_mediacodec_t gpu;
record_gpu_frame_t frame;
reset();
entry = create();
assert(entry);
memset(&gpu, 0, sizeof(gpu));
gpu.queue = record_gpu_queue_new(1);
gpu.state = RECORD_MC_DISPATCH_READY;
assert(gpu.queue);
pipe = record_android_capture_pipe_new(&gpu, &entry, 1, close_fd, &m);
assert(pipe && record_android_capture_pipe_sources_retired(pipe));
s = source();
assert(record_android_capture_pipe_push(pipe, &s, 0, 10) == 1);
assert(!record_android_capture_pipe_sources_retired(pipe));
assert(record_android_capture_pipe_step(pipe) == 0);
assert(record_gpu_queue_take(gpu.queue, &frame) == RECORD_GPU_QUEUE_OK);
assert(record_android_capture_pipe_step(pipe) == 0 && m.refs == 1);
assert(!record_android_capture_pipe_sources_retired(pipe));
m.complete = true;
assert(record_android_capture_pipe_step(pipe) == 0);
assert(!m.refs && m.releases == 1 && m.fd_refs == 1);
assert(record_android_capture_pipe_sources_retired(pipe));
assert(!record_android_capture_submit_free(entry));
assert(!record_gpu_queue_free(gpu.queue));
assert(record_android_capture_pipe_step(pipe) == 0 && m.releases == 1);
assert(record_android_capture_pipe_finish(pipe, 10));
close_fd(&m, (int)frame.ready_value - 1);
assert(record_gpu_queue_release(gpu.queue, frame.slot));
gpu.state = RECORD_MC_DISPATCH_DONE;
assert(record_android_capture_pipe_step(pipe) == 1);
assert(record_android_capture_pipe_free(pipe));
assert(record_android_capture_submit_free(entry));
assert(record_gpu_queue_free(gpu.queue));
}
static void renderer_recording(bool deferred, bool busy)
{
record_android_capture_submit_t *e;
record_android_capture_source_t s;
int fd;
unsigned submits;
reset();
m.external = true;
m.deferred = deferred;
e = create();
assert(e);
s = source();
s.ready = READY;
assert(record_android_capture_submit_record(e, &s, RENDER_COMMAND) == -1);
s.ready = VK_NULL_HANDLE;
assert(record_android_capture_submit_record(e, &s, RENDER_COMMAND) == 1);
assert(m.copies == 1 && !m.submits && m.refs == 1);
assert(record_android_capture_submit_poll(e) == 0);
assert(!record_android_capture_submit_free(e));
assert(record_android_capture_submit_record(e, &s, RENDER_COMMAND) == 0);
assert(!record_android_capture_submit_prepare_device_destroy(e, false));
/* The retirement probe above is terminal; abort the never-submitted frame.
*/
m.abort_proof = true;
assert(record_android_capture_submit_abort_recorded(e));
assert(!m.refs && record_android_capture_submit_free(e));
reset();
m.external = true;
m.deferred = deferred;
e = create();
s = source();
assert(record_android_capture_submit_record(e, &s, RENDER_COMMAND) == 1);
if (busy)
m.result = VK_TIMEOUT;
assert(record_android_capture_submit_commit(e) == (busy ? 0 : 1));
assert(!record_android_capture_submit_abort_recorded(e));
assert(record_android_capture_submit_commit(e) == -1);
m.result = VK_SUCCESS;
assert(record_android_capture_submit_poll(e) == 0);
if (deferred)
assert(
vulkan_android_queue_flush(&m.queued, native_submit) == VK_SUCCESS);
assert(m.copies == 1 && m.submits == 1 && m.refs == 1);
submits = m.submits;
m.complete = true;
assert(record_android_capture_submit_poll(e) == 1);
assert(!m.refs && m.submits == submits && m.copies == 1);
assert(record_android_capture_submit_take_ready(e, &fd) == 1);
close_fd(&m, fd);
assert(record_android_capture_submit_release(e));
assert(record_android_capture_submit_free(e));
}
static void renderer_handoff(bool deferred, unsigned disposition)
{
record_android_capture_submit_t *entry;
record_android_capture_pipe_t *pipe;
record_android_capture_source_t s;
record_gpu_dispatch_mediacodec_t gpu;
record_gpu_frame_t frame;
int result;
reset();
m.external = true;
m.deferred = deferred;
entry = create();
memset(&gpu, 0, sizeof(gpu));
gpu.queue = record_gpu_queue_new(1);
gpu.state = RECORD_MC_DISPATCH_READY;
assert(entry && gpu.queue);
pipe = record_android_capture_pipe_new(&gpu, &entry, 1, close_fd, &m);
assert(pipe);
s = source();
assert(record_android_capture_pipe_record(pipe, &s, 0, 10, VK_NULL_HANDLE)
== -1);
assert(record_android_capture_pipe_record(pipe, &s, 0, 10, RENDER_COMMAND)
== 1);
assert(record_android_capture_pipe_step(pipe) == 0);
assert(m.copies == 1 && !m.submits && m.refs == 1);
assert(record_gpu_queue_take(gpu.queue, &frame) == RECORD_GPU_QUEUE_BUSY);
assert(!record_android_capture_pipe_finish(pipe, 10));
if (disposition == 1 || disposition == 2)
{
if (disposition == 2)
{
record_android_capture_pipe_cancel(pipe);
assert(record_android_capture_pipe_finish(pipe, 10));
gpu.state = RECORD_MC_DISPATCH_FAILED;
assert(record_android_capture_pipe_step(pipe) == 0);
}
m.abort_proof = true;
assert(record_android_capture_pipe_abort_recorded(pipe));
assert(record_android_capture_pipe_sources_retired(pipe) && !m.refs);
assert(record_android_capture_pipe_commit(pipe) == -1);
}
else
{
if (disposition == 3)
{
record_android_capture_pipe_cancel(pipe);
assert(record_android_capture_pipe_finish(pipe, 10));
gpu.state = RECORD_MC_DISPATCH_FAILED;
assert(record_android_capture_pipe_step(pipe) == 0);
}
if (disposition == 4)
m.result = VK_TIMEOUT;
if (disposition == 5)
m.result = VK_ERROR_DEVICE_LOST;
result = record_android_capture_pipe_commit(pipe);
assert(result
== (disposition == 4 ? 0
: disposition == 5 && !deferred ? -1
: 1));
assert(!record_android_capture_pipe_abort_recorded(pipe));
assert(record_android_capture_pipe_commit(pipe) == -1);
if (disposition >= 3)
{
record_android_capture_pipe_cancel(pipe);
assert(record_android_capture_pipe_finish(pipe, 10));
gpu.state = RECORD_MC_DISPATCH_FAILED;
}
m.result = VK_SUCCESS;
assert(record_android_capture_pipe_step(pipe) == 0);
if (deferred)
{
if (disposition == 5)
m.result = VK_ERROR_DEVICE_LOST;
assert(vulkan_android_queue_flush(&m.queued, native_submit)
== (disposition == 5 ? VK_ERROR_DEVICE_LOST : VK_SUCCESS));
}
assert(m.copies == 1 && m.submits == 1 && m.refs == 1);
m.complete = true;
if (!disposition)
{
assert(record_android_capture_pipe_step(pipe) == 0);
assert(
record_gpu_queue_take(gpu.queue, &frame) == RECORD_GPU_QUEUE_OK);
assert(frame.timestamp == 0 && frame.duration == 10
&& frame.ready_value == 101);
assert(record_android_capture_pipe_step(pipe) == 0);
assert(record_android_capture_pipe_sources_retired(pipe));
close_fd(&m, (int)frame.ready_value - 1);
assert(record_gpu_queue_release(gpu.queue, frame.slot));
}
}
assert(record_android_capture_pipe_finish(pipe, 10));
gpu.state = disposition >= 2 ? RECORD_MC_DISPATCH_FAILED
: RECORD_MC_DISPATCH_DONE;
assert(record_android_capture_pipe_step(pipe) == 1);
assert(!m.refs && !m.fd_refs);
assert(m.copies == 1
&& m.submits == (disposition == 1 || disposition == 2 ? 0u : 1u));
assert(record_android_capture_pipe_free(pipe));
assert(record_android_capture_submit_free(entry));
assert(record_gpu_queue_free(gpu.queue));
}
static void renderer_faults(void)
{
record_android_capture_submit_t *e;
record_android_capture_source_t source_frame;
unsigned i;
for (i = 1; i <= 4; i++)
{
reset();
m.external = true;
e = create();
source_frame = source();
m.record_failure = i;
assert(record_android_capture_submit_record(
e, &source_frame, RENDER_COMMAND)
== -1);
assert(!m.copies && !m.submits && m.refs == 1);
m.refs = 0;
assert(record_android_capture_submit_free(e));
}
reset();
m.external = true;
e = create();
source_frame = source();
assert(record_android_capture_submit_record(e, &source_frame, RENDER_COMMAND)
== 1);
m.abort_proof = true;
assert(record_android_capture_submit_abort_recorded(e));
source_frame = source();
assert(record_android_capture_submit_record(e, &source_frame, RENDER_COMMAND)
== 1);
m.result = VK_TIMEOUT;
assert(record_android_capture_submit_commit(e) == 0);
assert(record_android_capture_submit_discard(e) == 0);
assert(m.refs == 1 && !m.submits && m.copies == 1);
m.result = VK_SUCCESS;
assert(record_android_capture_submit_discard(e) == 0);
assert(m.submits == 1 && m.refs == 1 && m.copies == 1);
m.complete = true;
assert(record_android_capture_submit_discard(e) == 1);
assert(!m.refs && record_android_capture_submit_free(e));
reset();
m.external = true;
e = create();
source_frame = source();
assert(record_android_capture_submit_record(e, &source_frame, RENDER_COMMAND)
== 1);
m.result = VK_ERROR_DEVICE_LOST;
assert(record_android_capture_submit_commit(e) == -1);
assert(m.refs == 1 && m.submits == 1);
assert(record_android_capture_submit_poll(e) == -1);
m.complete = true;
assert(record_android_capture_submit_discard(e) == 1);
assert(!m.refs && record_android_capture_submit_free(e));
reset();
m.external = true;
e = create();
source_frame = source();
assert(record_android_capture_submit_record(e, &source_frame, RENDER_COMMAND)
== 1);
assert(record_android_capture_submit_prepare_device_destroy(e, true));
assert(!record_android_capture_submit_abort_recorded(e));
assert(record_android_capture_submit_poll(e) == -1);
assert(!m.submits && m.refs == 1);
m.device_alive = false;
assert(record_android_capture_submit_after_device_destroy(e));
assert(!m.refs && m.gone_release);
assert(record_android_capture_submit_free(e));
}
int main(void)
{
unsigned disposition;
for (disposition = 0; disposition < 6; disposition++)
{
renderer_handoff(false, disposition);
renderer_handoff(true, disposition);
}
assert(record_android_capture_pipe_sources_retired(NULL));
renderer_faults();
renderer_recording(false, false);
renderer_recording(false, true);
renderer_recording(true, false);
renderer_recording(true, true);
pipe_source_retirement();
pipe_device_loss();
pipe_lifetime(false, false);
pipe_lifetime(true, false);
pipe_lifetime(false, true);
pipe_lifetime(true, true);
cycles(false, false);
cycles(false, true);
cycles(true, false);
@@ -0,0 +1,308 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier:
* GPL-3.0-or-later */
#include <assert.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#ifdef _WIN32
#include <io.h>
#include <fcntl.h>
#else
#include <unistd.h>
#endif
#include "../../../gfx/video_driver.h"
#include "../../../record/record_android_capture_config.h"
static unsigned live, procedures, memories, submissions, native_operations;
static unsigned bad_memory;
static bool fail_allocation, core_query_missing, locked, native_allowed = true;
static const char *missing;
static const VkSubmitInfo *expected_submit;
static VkResult native_result;
static const char *const names[]
= {"vkGetAndroidHardwareBufferPropertiesANDROID", "vkCreateImage",
"vkDestroyImage", "vkAllocateMemory", "vkFreeMemory",
"vkBindImageMemory", "vkCreateCommandPool", "vkDestroyCommandPool",
"vkAllocateCommandBuffers", "vkCreateFence", "vkDestroyFence",
"vkCreateSemaphore", "vkDestroySemaphore", "vkResetCommandBuffer",
"vkBeginCommandBuffer", "vkEndCommandBuffer",
"vkCmdPipelineBarrier", "vkCmdCopyImage", "vkResetFences",
"vkGetFenceStatus", "vkGetSemaphoreFdKHR", "vkQueueSubmit"};
static void *config_calloc(size_t count, size_t size)
{
void *p;
if (fail_allocation)
return NULL;
p = calloc(count, size);
if (p)
live++;
return p;
}
static void config_free(void *p)
{
if (p)
{
assert(live);
live--;
}
free(p);
}
#define calloc config_calloc
#define free config_free
#include "../../../record/record_android_capture_config.c"
#undef calloc
#undef free
static void VKAPI_CALL resource(void)
{
native_operations++;
assert(!"configuration must not allocate native resources");
}
static VkResult VKAPI_CALL queue_submit(
VkQueue queue, uint32_t count, const VkSubmitInfo *submits, VkFence fence)
{
assert(native_allowed && locked && queue == (VkQueue)(uintptr_t)3);
assert(count == 1 && submits == expected_submit
&& fence == (VkFence)(uintptr_t)77);
submissions++;
return native_result;
}
static void VKAPI_CALL memory(
VkPhysicalDevice physical, VkPhysicalDeviceMemoryProperties *out)
{
assert(native_allowed && physical == (VkPhysicalDevice)(uintptr_t)1);
memories++;
memset(out, 0, sizeof(*out));
out->memoryTypeCount = 1;
out->memoryHeapCount = 1;
out->memoryTypes[0].propertyFlags = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT;
out->memoryHeaps[0].size = 1048576;
if (bad_memory == 1)
out->memoryTypeCount = 0;
if (bad_memory == 2)
out->memoryTypeCount = VK_MAX_MEMORY_TYPES + 1;
if (bad_memory == 3)
out->memoryHeapCount = 0;
if (bad_memory == 4)
out->memoryHeapCount = VK_MAX_MEMORY_HEAPS + 1;
if (bad_memory == 5)
out->memoryTypes[0].heapIndex = 1;
}
static PFN_vkVoidFunction VKAPI_CALL device_proc(
VkDevice device, const char *name)
{
unsigned i;
assert(native_allowed && device == (VkDevice)(uintptr_t)2);
procedures++;
for (i = 0; i < sizeof(names) / sizeof(names[0]); i++)
if (!strcmp(names[i], name))
{
if (missing && !strcmp(missing, name))
return NULL;
return !strcmp(name, "vkQueueSubmit")
? (PFN_vkVoidFunction)queue_submit
: resource;
}
assert(!"unexpected device procedure");
return NULL;
}
static PFN_vkVoidFunction VKAPI_CALL instance_proc(
VkInstance instance, const char *name)
{
assert(native_allowed && instance == (VkInstance)(uintptr_t)10);
procedures++;
if (missing && !strcmp(missing, name))
return NULL;
if (!strcmp(name, "vkGetDeviceProcAddr"))
return (PFN_vkVoidFunction)device_proc;
if (!strcmp(name, "vkGetPhysicalDeviceMemoryProperties"))
return (PFN_vkVoidFunction)memory;
if (!strcmp(name, "vkGetPhysicalDeviceImageFormatProperties2"))
return core_query_missing ? NULL : resource;
assert(!strcmp(name, "vkGetPhysicalDeviceImageFormatProperties2KHR"));
return missing
&& !strcmp(missing, "vkGetPhysicalDeviceImageFormatProperties2")
? NULL
: resource;
}
static video_device_info_t info(void)
{
video_device_info_t d;
memset(&d, 0, sizeof(d));
d.api = VIDEO_DEVICE_VULKAN;
d.instance = (void *)(uintptr_t)10;
d.physical_device = (void *)(uintptr_t)1;
d.device = (void *)(uintptr_t)2;
d.queue = (void *)(uintptr_t)3;
d.queue_lock = (void *)(uintptr_t)4;
d.queue_family = 5;
d.recording_caps = VIDEO_DEVICE_CAP_ANDROID_CAPTURE_INTEROP;
d.get_instance_proc_addr = (retro_proc_address_t)instance_proc;
return d;
}
static void reset(void)
{
assert(!live);
procedures = memories = submissions = native_operations = bad_memory = 0;
missing = NULL;
fail_allocation = core_query_missing = locked = false;
native_allowed = true;
native_result = VK_SUCCESS;
}
static void normal(unsigned mode, bool fallback)
{
video_device_info_t d = info();
record_android_capture_config_t *c;
const record_android_capture_pool_config_t *p;
VkSubmitInfo submit;
int descriptors[2];
reset();
core_query_missing = fallback;
c = record_android_capture_config_new(&d, 128, 96, 4, mode, fallback);
assert(c && live == 1 && memories == 1 && !native_operations);
p = record_android_capture_config_pool(c);
assert(p && p->device == (VkDevice)d.device
&& p->physical == (VkPhysicalDevice)d.physical_device);
assert(p->queue == (VkQueue)d.queue && p->family == d.queue_family);
assert(p->memory == &c->memory && p->memory->memoryTypeCount == 1);
assert(p->gpu.width == 128 && p->gpu.height == 96 && p->gpu.count == 4);
assert(p->gpu.mode == mode && p->gpu.flip_y == fallback);
assert(p->submit.context == c && p->submit.submit && p->submit.close_fd);
memset(&submit, 0, sizeof(submit));
submit.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
expected_submit = &submit;
locked = true;
assert(p->submit.submit(c, p->queue, 1, &submit, (VkFence)(uintptr_t)77)
== VK_SUCCESS);
native_result = VK_ERROR_DEVICE_LOST;
assert(p->submit.submit(c, p->queue, 1, &submit, (VkFence)(uintptr_t)77)
== VK_ERROR_DEVICE_LOST);
assert(p->submit.submit(
c, (VkQueue)(uintptr_t)999, 1, &submit, VK_NULL_HANDLE)
== VK_ERROR_INITIALIZATION_FAILED);
assert(p->submit.submit(c, p->queue, 0, &submit, VK_NULL_HANDLE)
== VK_ERROR_INITIALIZATION_FAILED);
assert(p->submit.submit(c, p->queue, 1, NULL, VK_NULL_HANDLE)
== VK_ERROR_INITIALIZATION_FAILED);
assert(p->submit.submit(NULL, p->queue, 1, &submit, VK_NULL_HANDLE)
== VK_ERROR_INITIALIZATION_FAILED);
assert(submissions == 2);
locked = false;
errno = EDOM;
p->submit.close_fd(c, -1);
assert(errno == EDOM);
#ifdef _WIN32
assert(_pipe(descriptors, 256, _O_BINARY) == 0);
p->submit.close_fd(c, descriptors[0]);
assert(_close(descriptors[0]) == -1 && errno == EBADF);
assert(_close(descriptors[1]) == 0);
#else
assert(pipe(descriptors) == 0);
p->submit.close_fd(c, descriptors[0]);
assert(close(descriptors[0]) == -1 && errno == EBADF);
assert(close(descriptors[1]) == 0);
#endif
native_allowed = false;
record_android_capture_config_free(c);
assert(!live && memories == 1 && !native_operations);
}
static void failures(void)
{
video_device_info_t d;
unsigned i;
const char *const instance_names[]
= {"vkGetDeviceProcAddr", "vkGetPhysicalDeviceMemoryProperties",
"vkGetPhysicalDeviceImageFormatProperties2"};
for (i = 0; i < sizeof(names) / sizeof(names[0]); i++)
{
reset();
d = info();
missing = names[i];
assert(!record_android_capture_config_new(&d, 128, 96, 4, 0, false));
assert(!live && !memories && !native_operations);
}
for (i = 0; i < sizeof(instance_names) / sizeof(instance_names[0]); i++)
{
reset();
d = info();
missing = instance_names[i];
assert(!record_android_capture_config_new(&d, 128, 96, 4, 0, false));
assert(!live && !memories && !native_operations);
}
for (i = 1; i <= 5; i++)
{
reset();
d = info();
bad_memory = i;
assert(!record_android_capture_config_new(&d, 128, 96, 4, 0, false));
assert(!live && memories == 1 && !native_operations);
}
reset();
d = info();
fail_allocation = true;
assert(!record_android_capture_config_new(&d, 128, 96, 4, 0, false));
assert(!live && !memories);
}
static void bad_inputs(void)
{
unsigned i;
for (i = 0; i < 15; i++)
{
video_device_info_t d = info();
unsigned width = 128, height = 96, count = 4, mode = 0;
reset();
if (i == 0)
d.api = VIDEO_DEVICE_METAL;
if (i == 1)
d.instance = NULL;
if (i == 2)
d.physical_device = NULL;
if (i == 3)
d.device = NULL;
if (i == 4)
d.queue = NULL;
if (i == 5)
d.queue_lock = NULL;
if (i == 6)
d.recording_caps = 0;
if (i == 7)
d.get_instance_proc_addr = NULL;
if (i == 8)
d.queue_family = VK_QUEUE_FAMILY_IGNORED;
if (i == 9)
width = 0;
if (i == 10)
height = 0;
if (i == 11)
width = 16385;
if (i == 12)
count = 0;
if (i == 13)
count = RECORD_GPU_QUEUE_SLOTS + 1;
if (i == 14)
mode = 3;
assert(!record_android_capture_config_new(
&d, width, height, count, mode, false));
assert(!live && !procedures && !memories);
}
}
int main(void)
{
unsigned mode;
for (mode = 0; mode < 3; mode++)
{
normal(mode, false);
normal(mode, true);
}
failures();
bad_inputs();
assert(!record_android_capture_config_new(NULL, 1, 1, 1, 0, false));
assert(!record_android_capture_config_pool(NULL));
record_android_capture_config_free(NULL);
puts("PASS enabled Android capture admission, complete Vulkan resolver, "
"memory snapshot, "
"serialized submit adapter and fd ownership; native device mocked");
return 0;
}
@@ -0,0 +1,109 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier:
* GPL-3.0-or-later */
#include <assert.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include "../../../record/record_android_capture_device.c"
#define VIDEO_FLAG_CACHE_CONTEXT 1
#define RARCH_ERR(message) ((void)0)
struct test_context
{
struct
{
VkDevice device;
} context;
};
struct owner_state
{
record_android_capture_device_t node;
unsigned prepares, destroyed;
bool ready, proof;
};
static unsigned device_destroys, cached;
static VkDevice expected_device;
static void VKAPI_CALL destroy_device(
VkDevice device, const VkAllocationCallbacks *callbacks)
{
assert(device == expected_device && !callbacks);
device_destroys++;
}
#define vkDestroyDevice destroy_device
#include "device_teardown_fixture.h"
static bool prepare_owner(void *context, bool proof)
{
struct owner_state *s = (struct owner_state *)context;
assert(!device_destroys && !s->destroyed);
s->prepares++;
s->proof = proof;
return s->ready && proof;
}
static void owner_destroyed(void *context)
{
struct owner_state *s = (struct owner_state *)context;
assert(device_destroys == 1 && s->node.prepared && !s->node.device);
s->destroyed++;
}
static void init(struct owner_state *s, VkDevice device)
{
memset(s, 0, sizeof(*s));
s->node.context = s;
s->node.device = device;
s->node.prepare = prepare_owner;
s->node.destroyed = owner_destroyed;
assert(record_android_capture_device_attach(&s->node));
assert(!record_android_capture_device_attach(&s->node));
}
static void run(VkResult idle)
{
struct owner_state a, b, other;
bool proof = idle == VK_SUCCESS || idle == VK_ERROR_DEVICE_LOST;
VkDevice device = (VkDevice)(uintptr_t)11;
VkDevice second = (VkDevice)(uintptr_t)12;
device_destroys = cached = 0;
expected_device = device;
init(&a, device);
init(&b, device);
init(&other, second);
a.ready = other.ready = true;
/* One owner refuses closure; no actual destruction or cache transfer. */
teardown(device, idle, VIDEO_FLAG_CACHE_CONTEXT);
assert(!device_destroys && !cached && !a.destroyed && !b.destroyed);
assert(a.proof == proof && b.proof == proof);
assert(a.node.prepared == proof && !b.node.prepared);
assert(!other.prepares && !other.destroyed);
assert(!record_android_capture_device_destroyed(device));
assert(record_android_capture_device_pending(device));
b.ready = true;
teardown(device, VK_SUCCESS, VIDEO_FLAG_CACHE_CONTEXT);
assert(device_destroys == 1 && !cached);
assert(a.destroyed == 1 && b.destroyed == 1 && !other.destroyed);
assert(a.prepares == (proof ? 1u : 2u) && b.prepares == 2);
assert(!record_android_capture_device_pending(device));
assert(record_android_capture_device_pending(second));
assert(record_android_capture_device_destroyed(device));
assert(a.destroyed == 1 && b.destroyed == 1);
record_android_capture_device_detach(&a.node);
record_android_capture_device_detach(&b.node);
record_android_capture_device_detach(&other.node);
assert(!record_acdevice_owners);
}
int main(void)
{
run(VK_SUCCESS);
run(VK_ERROR_DEVICE_LOST);
run(VK_ERROR_OUT_OF_HOST_MEMORY);
run(VK_ERROR_OUT_OF_DEVICE_MEMORY);
run(VK_ERROR_UNKNOWN);
device_destroys = cached = 0;
teardown((VkDevice)(uintptr_t)11, VK_SUCCESS, VIDEO_FLAG_CACHE_CONTEXT);
assert(cached == 1 && !device_destroys);
assert(!record_android_capture_device_attach(NULL));
assert(!record_android_capture_device_pending(VK_NULL_HANDLE));
assert(!record_android_capture_device_prepare(VK_NULL_HANDLE, true));
assert(!record_android_capture_device_destroyed(VK_NULL_HANDLE));
record_android_capture_device_detach(NULL);
puts("PASS actual Vulkan teardown fragments: matching owners, drain proof, "
"cache refusal and post-destruction notification");
return 0;
}
@@ -0,0 +1,359 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier:
* GPL-3.0-or-later */
#define _POSIX_C_SOURCE 200809L
#ifdef NDEBUG
#error Dispatcher ownership acceptance requires assertions
#endif
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "../../../record/record_dispatch_mediacodec.h"
#ifndef _WIN32
#include <pthread.h>
#include <sys/eventfd.h>
#include <poll.h>
#include <unistd.h>
#include <errno.h>
#include <time.h>
#include <retro_atomic.h>
static retro_atomic_int_t fault, env_fault, startup_fault, output_fault;
static retro_atomic_int_t inputs, live, descriptors;
static retro_atomic_int_t hold_start;
static void pause_test(void);
struct record_session_mediacodec
{
pthread_t owner;
bool audio_closed, gpu_done, gpu_released, cancelled;
};
static int test_eventfd(unsigned int initial, int flags)
{
int fd;
if (retro_atomic_load_acquire_int(&fault) == 1)
return -1;
fd = eventfd(initial, flags);
if (fd >= 0)
retro_atomic_fetch_add_int(&descriptors, 1);
return fd;
}
static int test_close(int fd)
{
retro_atomic_fetch_add_int(&descriptors, -1);
return close(fd);
}
static int test_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*run)(void *), void *context)
{
if (retro_atomic_load_acquire_int(&fault) == 2)
return EAGAIN;
return pthread_create(thread, attr, run, context);
}
static int test_detach(pthread_t thread)
{
return retro_atomic_load_acquire_int(&fault) == 3 ? EINVAL
: pthread_detach(thread);
}
static ssize_t test_write(int fd, const void *data, size_t bytes)
{
if (retro_atomic_load_acquire_int(&fault) == 4)
{
retro_atomic_store_release_int(&fault, 0);
errno = EIO;
return -1;
}
return write(fd, data, bytes);
}
static int test_poll(struct pollfd *fds, nfds_t count, int timeout)
{
if (retro_atomic_load_acquire_int(&fault) == 5)
{
retro_atomic_store_release_int(&fault, 0);
errno = EIO;
return -1;
}
return poll(fds, count, timeout);
}
#define eventfd test_eventfd
#define close test_close
#define pthread_create test_create
#define pthread_detach test_detach
#define write test_write
#define poll test_poll
#include "../../../record/record_dispatch_mediacodec.c"
#undef eventfd
#undef close
#undef pthread_create
#undef pthread_detach
#undef write
#undef poll
JNIEnv *rjni_env(void)
{
static JNIEnv dummy;
return retro_atomic_load_acquire_int(&env_fault) ? NULL : &dummy;
}
static void owner(record_session_mediacodec_t *s)
{
assert(s && pthread_equal(pthread_self(), s->owner));
}
record_session_mediacodec_t *record_session_mediacodec_new(JNIEnv *env,
const mediacodec_encoder_request_t *v, unsigned interval, unsigned rate,
unsigned channels, unsigned bitrate, size_t max_frames, size_t capacity,
const char *path)
{
record_session_mediacodec_t *s;
assert(env && !strcmp(v->mime, "video/hevc") && !strcmp(path, "record.mp4"));
assert(v->width == 320 && v->height == 192 && interval == 2 && rate == 48000
&& channels == 2 && bitrate == 128000 && max_frames == 1024
&& capacity == 65536);
if (retro_atomic_load_acquire_int(&startup_fault))
return NULL;
while (retro_atomic_load_acquire_int(&hold_start))
pause_test();
s = (record_session_mediacodec_t *)calloc(1, sizeof(*s));
assert(s);
s->owner = pthread_self();
retro_atomic_fetch_add_int(&live, 1);
return s;
}
void *record_session_mediacodec_surface(record_session_mediacodec_t *s)
{
owner(s);
return s;
}
int record_session_mediacodec_audio(record_session_mediacodec_t *s,
const int16_t *samples, size_t frames, uint64_t timestamp)
{
assert(s && !s->audio_closed && samples && frames == 2 && timestamp < 2000);
retro_atomic_fetch_add_int(&inputs, 1);
return RECORD_AUDIO_OK;
}
bool record_session_mediacodec_audio_finish(
record_session_mediacodec_t *s, uint64_t end)
{
owner(s);
assert(end == 2000 || !end);
s->audio_closed = true;
return true;
}
bool record_session_mediacodec_gpu_retired(
record_session_mediacodec_t *s, int64_t end)
{
owner(s);
assert(end == 100000 || !end);
s->gpu_done = true;
return true;
}
bool record_session_mediacodec_gpu_released(record_session_mediacodec_t *s)
{
owner(s);
assert(s->gpu_done);
s->gpu_released = true;
return true;
}
void record_session_mediacodec_cancel(record_session_mediacodec_t *s)
{
owner(s);
s->cancelled = true;
}
int record_session_mediacodec_step(record_session_mediacodec_t *s)
{
owner(s);
if (!s->audio_closed || !s->gpu_done || !s->gpu_released)
return RECORD_MC_WORKER_BUSY;
return s->cancelled || retro_atomic_load_acquire_int(&output_fault)
? RECORD_MC_WORKER_ERROR
: RECORD_MC_WORKER_END;
}
enum record_session_mediacodec_error record_session_mediacodec_error(
const record_session_mediacodec_t *s, int *error)
{
*error = retro_atomic_load_acquire_int(&output_fault) ? 5 : 0;
return *error ? RECORD_MC_SESSION_FILE
: s->cancelled ? RECORD_MC_SESSION_CANCELLED
: RECORD_MC_SESSION_NONE;
}
bool record_session_mediacodec_free(record_session_mediacodec_t *s)
{
owner(s);
assert(s->audio_closed && s->gpu_done && s->gpu_released);
retro_atomic_fetch_add_int(&live, -1);
free(s);
return true;
}
static void pause_test(void)
{
struct timespec duration;
duration.tv_sec = 0;
duration.tv_nsec = 1000000;
nanosleep(&duration, NULL);
}
static void wait_state(record_dispatch_mediacodec_t *j,
enum record_dispatch_mediacodec_state expected)
{
unsigned at;
for (at = 0; at < 10000; at++)
{
if (record_dispatch_mediacodec_status(j, NULL, NULL, NULL) == expected)
return;
pause_test();
}
assert(0);
}
static void collect(record_dispatch_mediacodec_t *j)
{
unsigned at;
for (at = 0; at < 10000; at++)
{
if (record_dispatch_mediacodec_free(j))
break;
pause_test();
}
assert(at < 10000 && !retro_atomic_load_acquire_int(&live)
&& !retro_atomic_load_acquire_int(&descriptors));
}
static record_dispatch_mediacodec_t *start(void)
{
mediacodec_encoder_request_t v = {0};
record_dispatch_mediacodec_t *j;
char mime[] = "video/hevc", path[] = "record.mp4";
v.mime = mime;
v.width = 320;
v.height = 192;
j = record_dispatch_mediacodec_new(
&v, 2, 48000, 2, 128000, 1024, 65536, path);
memset(mime, 0, sizeof(mime));
memset(path, 0, sizeof(path));
return j;
}
static void *pcm_producer(void *data)
{
record_dispatch_mediacodec_t *j = (record_dispatch_mediacodec_t *)data;
static const int16_t pcm[4] = {1, 2, 3, 4};
unsigned i;
for (i = 0; i < 1000; i++)
assert(record_dispatch_mediacodec_audio(j, pcm, 2, i * 2)
== RECORD_AUDIO_OK);
assert(record_dispatch_mediacodec_audio_finish(j, 2000));
return NULL;
}
static void *gpu_executor(void *data)
{
record_dispatch_mediacodec_t *j = (record_dispatch_mediacodec_t *)data;
assert(record_dispatch_mediacodec_surface(j));
assert(record_dispatch_mediacodec_gpu_retired(j, 100000));
assert(!record_dispatch_mediacodec_gpu_retired(j, 100001));
assert(record_dispatch_mediacodec_gpu_released(j));
return NULL;
}
int main(void)
{
record_dispatch_mediacodec_t *j;
pthread_t producer, gpu;
int i, error, detail, system;
unsigned width, height, color;
retro_atomic_int_init(&fault, 0);
retro_atomic_int_init(&env_fault, 0);
retro_atomic_int_init(&startup_fault, 0);
retro_atomic_int_init(&output_fault, 0);
retro_atomic_int_init(&inputs, 0);
retro_atomic_int_init(&live, 0);
retro_atomic_int_init(&descriptors, 0);
retro_atomic_int_init(&hold_start, 0);
for (i = 1; i <= 2; i++)
{
retro_atomic_store_release_int(&fault, i);
assert(!start() && !retro_atomic_load_acquire_int(&descriptors));
}
retro_atomic_store_release_int(&fault, 0);
retro_atomic_store_release_int(&hold_start, 1);
j = start();
assert(j
&& record_dispatch_mediacodec_audio(j, NULL, 2, 0)
== RECORD_AUDIO_BUSY);
assert(!record_dispatch_mediacodec_surface(j));
assert(record_dispatch_mediacodec_audio_finish(j, 0));
assert(record_dispatch_mediacodec_gpu_retired(j, 0));
assert(record_dispatch_mediacodec_gpu_released(j));
record_dispatch_mediacodec_cancel(j);
retro_atomic_store_release_int(&hold_start, 0);
wait_state(j, RECORD_MC_DISPATCH_FAILED);
collect(j);
for (i = 0; i < 64; i++)
{
retro_atomic_store_release_int(&fault, i == 0 ? 3 : 0);
j = start();
assert(j);
wait_state(j, RECORD_MC_DISPATCH_READY);
assert(record_dispatch_mediacodec_video_format(j, &width, &height, &color)
&& width == 320 && height == 192 && !color);
assert(
!record_dispatch_mediacodec_video_format(j, NULL, &height, &color));
assert(!record_dispatch_mediacodec_free(j));
assert(record_dispatch_mediacodec_gpu_claim(j));
assert(!record_dispatch_mediacodec_gpu_claim(j));
assert(!record_dispatch_mediacodec_gpu_released(j));
assert(!pthread_create(&producer, NULL, pcm_producer, j));
assert(!pthread_create(&gpu, NULL, gpu_executor, j));
assert(!pthread_join(producer, NULL) && !pthread_join(gpu, NULL));
wait_state(j, RECORD_MC_DISPATCH_DONE);
assert(!record_dispatch_mediacodec_free(j));
record_dispatch_mediacodec_gpu_release_claim(j);
assert(!record_dispatch_mediacodec_gpu_claim(j));
record_dispatch_mediacodec_status(j, &error, &detail, &system);
assert(!error && !detail && !system);
collect(j);
}
assert(retro_atomic_load_acquire_int(&inputs) == 64000);
for (i = 0; i < 6; i++)
{
unsigned at;
retro_atomic_store_release_int(&fault, i >= 4 ? i : 0);
retro_atomic_store_release_int(&env_fault, i == 1);
retro_atomic_store_release_int(&startup_fault, i == 2);
retro_atomic_store_release_int(&output_fault, i == 3);
j = start();
assert(j);
if (!i)
record_dispatch_mediacodec_cancel(j);
if (i == 4)
{
for (at = 0; at < 10000; at++)
{
if (retro_atomic_load_acquire_int(&j->epoch) & 1)
break;
pause_test();
}
assert(at < 10000);
record_dispatch_mediacodec_notify(j);
}
wait_state(j, RECORD_MC_DISPATCH_DRAINING);
assert(record_dispatch_mediacodec_cancelled(j));
assert(!record_dispatch_mediacodec_free(j));
assert(record_dispatch_mediacodec_audio_finish(j, 0));
assert(record_dispatch_mediacodec_gpu_retired(j, 0));
pause_test();
assert(!record_dispatch_mediacodec_free(j));
assert(record_dispatch_mediacodec_gpu_released(j));
wait_state(j, RECORD_MC_DISPATCH_FAILED);
record_dispatch_mediacodec_status(j, &error, &detail, &system);
assert(error && system == (i >= 4 ? EIO : 0)
&& detail == (i == 3 ? 5 : 0));
collect(j);
}
puts("PASS: independent I/O thread, 64000 notifications, JNI thread "
"ownership, "
"deep copies, startup/detach faults and deferred GPU/PCM closure; "
"session mocked");
return 0;
}
#else
#include "../../../record/record_dispatch_mediacodec.c"
int main(void)
{
assert(!record_dispatch_mediacodec_new(NULL, 0, 0, 0, 0, 0, 0, NULL));
assert(record_dispatch_mediacodec_free(NULL));
puts("PASS: unsupported dispatcher stubs");
return 0;
}
#endif
@@ -0,0 +1,136 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier:
* GPL-3.0-or-later */
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include "../../../record/record_mediacodec_file.h"
#ifdef _WIN32
#include <io.h>
#include <windows.h>
#include <winioctl.h>
#define file_write _write
#define file_read _read
#define file_seek _lseeki64
#else
#include <unistd.h>
#define file_write write
#define file_read read
#define file_seek lseek
#endif
static unsigned char *load(const char *path, size_t *bytes)
{
FILE *f = fopen(path, "rb");
unsigned char *data;
long size;
assert(f && !fseek(f, 0, SEEK_END));
size = ftell(f);
assert(size > 0 && size < 65536 && !fseek(f, 0, SEEK_SET));
data = (unsigned char *)malloc((size_t)size);
assert(data && fread(data, 1, (size_t)size, f) == (size_t)size);
assert(!fclose(f));
*bytes = (size_t)size;
return data;
}
int main(int argc, char **argv)
{
static const unsigned char prefix[] = {
0, 0, 0, 8, 'f', 't', 'y', 'p',
0, 0, 0, 1, 'm', 'd', 'a', 't',
0, 0, 0, 0, 0, 0, 0, 24,
1, 2, 3, 4, 5, 6, 7, 8};
record_mediacodec_file_t *f;
unsigned char *input, *expected, *output;
unsigned char large_prefix[sizeof(prefix)];
int64_t large_offset = (int64_t)((uint64_t)1 << 32) + sizeof(prefix);
size_t bytes, expected_size;
char path[1024];
unsigned i;
int fd, error;
unsigned frames;
assert(argc == 4 && strlen(argv[1]) < sizeof(path) - 16);
frames = (unsigned)strtoul(argv[3], NULL, 10);
input = load(argv[1], &bytes);
expected = load(argv[2], &expected_size);
output = (unsigned char *)malloc(expected_size + sizeof(prefix));
assert(output);
sprintf(path, "%s.mp4", argv[1]);
f = record_mediacodec_file_new(path, &error);
assert(f && !error);
fd = record_mediacodec_file_descriptor(f);
assert(file_write(fd, prefix, sizeof(prefix)) == sizeof(prefix));
assert(file_write(fd, input, (unsigned)bytes) == (int)bytes);
assert(record_mediacodec_file_audio_edit(f, 2048, frames, 2, &error));
assert(!error);
assert(file_seek(fd, 0, SEEK_END)
== (int64_t)(sizeof(prefix) + expected_size));
assert(file_seek(fd, 0, SEEK_SET) == 0);
assert(file_read(fd, output, (unsigned)(expected_size + sizeof(prefix)))
== (int)(expected_size + sizeof(prefix)));
assert(!memcmp(output, prefix, sizeof(prefix)));
assert(!memcmp(output + sizeof(prefix), expected, expected_size));
assert(!record_mediacodec_file_audio_edit(f, 2048, frames, 2, &error));
assert(error && record_mediacodec_file_close(f, true, &error));
assert(!remove(path));
sprintf(path, "%s-large.mp4", argv[1]);
f = record_mediacodec_file_new(path, &error);
assert(f);
fd = record_mediacodec_file_descriptor(f);
#ifdef _WIN32
{
DWORD returned = 0;
assert(DeviceIoControl((HANDLE)_get_osfhandle(fd), FSCTL_SET_SPARSE,
NULL, 0, NULL, 0, &returned, NULL));
}
#endif
memcpy(large_prefix, prefix, sizeof(prefix));
large_prefix[19] = 1;
assert(file_write(fd, large_prefix, sizeof(large_prefix))
== sizeof(large_prefix));
assert(file_seek(fd, large_offset, SEEK_SET) == large_offset);
assert(file_write(fd, input, (unsigned)bytes) == (int)bytes);
assert(record_mediacodec_file_audio_edit(f, 2048, frames, 2, &error));
assert(!error);
assert(file_seek(fd, 0, SEEK_END) == large_offset + (int64_t)expected_size);
assert(file_seek(fd, 0, SEEK_SET) == 0);
assert(file_read(fd, output, sizeof(large_prefix)) == sizeof(large_prefix));
assert(!memcmp(output, large_prefix, sizeof(large_prefix)));
assert(file_seek(fd, large_offset, SEEK_SET) == large_offset);
assert(file_read(fd, output, (unsigned)expected_size) == (int)expected_size);
assert(!memcmp(output, expected, expected_size));
assert(record_mediacodec_file_close(f, true, &error) && !error);
assert(!remove(path));
for (i = 0; i < 5; i++)
{
unsigned char header[16] = {0, 0, 0, 8, 'f', 'r', 'e', 'e'};
sprintf(path, "%s-%u.mp4", argv[1], i);
f = record_mediacodec_file_new(path, &error);
assert(f);
fd = record_mediacodec_file_descriptor(f);
if (i == 1)
header[3] = 0;
else if (i == 2)
header[3] = 255;
else if (i == 3)
header[3] = 1;
else if (i == 4)
{
memcpy(header + 4, "moov", 4);
header[3] = 8;
}
assert(file_write(fd, header, 8) == 8);
assert(!record_mediacodec_file_audio_edit(f, 2048, frames, 2, &error));
assert(error && record_mediacodec_file_close(f, false, &error));
assert(!remove(path));
}
assert(!record_mediacodec_file_audio_edit(NULL, 2048, frames, 2, &error));
free(output);
free(expected);
free(input);
puts("PASS: cold MP4 finalization preserves media, edits only moov and "
"rejects malformed boxes and repeated edits");
return 0;
}
@@ -0,0 +1,977 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier:
* GPL-3.0-or-later */
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include "../../../record/drivers/record_mediacodec.c"
struct record_dispatch_mediacodec
{
bool live, cancelled, audio_closed, gpu_closed, claimed;
enum record_dispatch_mediacodec_state state;
};
struct record_android_capture_config
{
bool live;
};
struct record_android_capture_request
{
record_android_capture_renderer_t binding;
enum record_android_capture_operation op;
bool live, created, ready, busy, closed, collected;
int result;
int64_t end;
};
static struct record_dispatch_mediacodec io;
static struct record_android_capture_config configuration;
static struct record_android_capture_request request;
static record_android_capture_pool_config_t pool_config;
static settings_t settings;
static recording_state_t recording;
static video_driver_state_t video;
static runloop_state_t loop;
static int64_t wall_us;
static record_audio_tap_t *active_tap;
static bool tap_busy;
static video_device_info_t device;
static struct retro_hdr_metadata metadata;
static unsigned dims, io_creates, config_creates, config_frees, io_frees;
static unsigned audio_calls, stops, starts, logs, failures;
static unsigned config_fault, io_fault, request_fault;
static bool async_audio, held_frames, producer_ready, native_retired;
static uint64_t expected_audio, closed_audio;
static int audio_result;
static mediacodec_encoder_request_t encoded;
static const char *option_key, *option_value;
static int config_token;
static unsigned history_writes;
static int native_error, native_file_error, native_system_error;
static bool settings_available = true;
#ifdef HAVE_WEBMPLAYER
struct defaults g_defaults;
static int playlist_token;
void command_playlist_push_write(
playlist_t *list, const struct playlist_entry *entry)
{
assert(list == (playlist_t *)&playlist_token
&& list == g_defaults.video_history);
assert(!io.live && !request.live && configuration.live);
assert(entry && entry->path && !strcmp(entry->path, "test.mp4"));
assert(!strcmp(entry->core_path, "builtin")
&& !strcmp(entry->core_name, "movieplayer"));
history_writes++;
}
#endif
runloop_state_t *runloop_state_get_ptr(void) { return &loop; }
retro_time_t cpu_features_get_time_usec(void) { return wall_us; }
int audio_driver_record_tap_install(struct record_audio_tap *tap)
{
if (tap_busy) return 0;
if (active_tap == tap) return 1;
if (active_tap || !record_audio_tap_begin(tap)) return -1;
active_tap = tap;
return 1;
}
bool audio_driver_record_tap_remove(struct record_audio_tap *tap)
{
if (tap_busy) return false;
if (active_tap == tap)
{
active_tap = NULL;
record_audio_tap_retire(tap, false);
}
return record_audio_tap_end(tap, NULL);
}
settings_t *config_get_ptr(void)
{
return settings_available ? &settings : NULL;
}
recording_state_t *recording_state_get_ptr(void)
{
return &recording;
}
video_driver_state_t *video_state_get_ptr(void)
{
return &video;
}
bool audio_driver_has_callback(void)
{
return async_audio;
}
unsigned audio_layout_channels(uint32_t layout)
{
unsigned count = 0;
while (layout)
{
count += layout & 1;
layout >>= 1;
}
return count;
}
bool video_driver_get_device(video_device_info_t *out)
{
*out = device;
return device.device != NULL;
}
bool video_driver_get_display_metadata(unsigned command, void *out)
{
assert(command == RETRO_ENVIRONMENT_GET_HDR_METADATA);
*(struct retro_hdr_metadata *)out = metadata;
return true;
}
static bool menu_view_only;
bool video_driver_get_viewport_info(struct video_viewport *out)
{
out->dims = menu_view_only ? 0 : dims;
out->full_dims = dims;
return true;
}
void RARCH_LOG(const char *format, ...)
{
assert(strstr(format, "Finalized"));
logs++;
}
void RARCH_ERR(const char *format, ...)
{
assert(strstr(format, "Failed"));
failures++;
}
bool command_event(enum event_command command, void *unused)
{
assert(!unused);
if (command == CMD_EVENT_RECORD_DEINIT)
{
stops++;
if (record_mediacodec_cancel_start())
return true;
assert(recording.data && record_mc_active);
record_mediacodec.finalize(recording.data);
record_mediacodec.free(recording.data);
recording.data = NULL;
recording.driver = NULL;
}
else
{
assert(command == CMD_EVENT_RECORD_INIT);
starts++;
}
return true;
}
config_file_t *config_file_new_from_path_to_string(const char *path)
{
assert(!strcmp(path, "options"));
return (config_file_t *)&config_token;
}
void config_file_free(config_file_t *config)
{
assert(config == (config_file_t *)&config_token);
}
struct config_entry_list *config_get_entry(
const config_file_t *config, const char *key)
{
assert(config == (const config_file_t *)&config_token);
return option_key && !strcmp(key, option_key)
? (struct config_entry_list *)&config_token
: NULL;
}
bool config_get_entry_list_head(
config_file_t *config, struct config_file_entry *entry)
{
assert(config == (config_file_t *)&config_token);
entry->key = option_key;
return option_key != NULL;
}
bool config_get_entry_list_next(struct config_file_entry *entry)
{
(void)entry;
return false;
}
bool config_get_array(
config_file_t *config, const char *key, char *out, size_t size)
{
assert(config_get_entry(config, key));
if (strlen(option_value) >= size)
return false;
memcpy(out, option_value, strlen(option_value) + 1);
return true;
}
bool config_get_bool(config_file_t *config, const char *key, bool *out)
{
assert(config_get_entry(config, key));
if (strcmp(option_value, "true") && strcmp(option_value, "false"))
return false;
*out = !strcmp(option_value, "true");
return true;
}
bool config_get_uint(config_file_t *config, const char *key, unsigned *out)
{
char *end;
unsigned long value;
assert(config_get_entry(config, key));
value = strtoul(option_value, &end, 10);
if (*end || value > UINT_MAX)
return false;
*out = (unsigned)value;
return true;
}
record_android_capture_config_t *record_android_capture_config_new(
const struct video_device_info *d, unsigned w, unsigned h, unsigned count,
unsigned mode, bool flip)
{
assert(d->device == device.device && w == VIDEO_SCALE_W(dims)
&& h == VIDEO_SCALE_H(dims) && count == 4
&& mode == metadata.output_mode && flip && !configuration.live);
config_creates++;
if (config_fault)
return NULL;
configuration.live = true;
return &configuration;
}
const record_android_capture_pool_config_t *record_android_capture_config_pool(
const record_android_capture_config_t *c)
{
assert(c == &configuration && c->live);
return &pool_config;
}
void record_android_capture_config_free(record_android_capture_config_t *c)
{
assert(c == &configuration && c->live && !request.live && !io.live);
config_frees++;
c->live = false;
}
record_dispatch_mediacodec_t *record_dispatch_mediacodec_new(
const mediacodec_encoder_request_t *v, unsigned interval, unsigned rate,
unsigned channels, unsigned bitrate, size_t max_frames, size_t capacity,
const char *path)
{
assert(!io.live && configuration.live && interval == 2
&& !strcmp(path, "test.mp4"));
assert((!rate && !channels && !bitrate && !max_frames && !capacity)
|| (rate == 48000 && (channels == 1 || channels == 2)
&& bitrate == 128000 && max_frames == 4096
&& capacity >= (size_t)48000 * channels * 4));
io_creates++;
encoded = *v;
if (io_fault)
return NULL;
io.live = true;
io.state = RECORD_MC_DISPATCH_STARTING;
return &io;
}
int record_dispatch_mediacodec_audio(record_dispatch_mediacodec_t *j,
const int16_t *samples, size_t frames, uint64_t pts)
{
assert(j == &io && j->live && !j->audio_closed && samples && frames <= 4096);
assert(pts == expected_audio);
audio_calls++;
if (audio_result == RECORD_AUDIO_OK)
expected_audio += frames;
return audio_result;
}
bool record_dispatch_mediacodec_audio_finish(
record_dispatch_mediacodec_t *j, uint64_t end)
{
assert(j == &io && j->live && end == expected_audio);
if (j->audio_closed)
assert(closed_audio == end);
closed_audio = end;
j->audio_closed = true;
return true;
}
void record_dispatch_mediacodec_cancel(record_dispatch_mediacodec_t *j)
{
assert(j == &io && j->live);
j->cancelled = true;
}
bool record_dispatch_mediacodec_cancelled(const record_dispatch_mediacodec_t *j)
{
assert(j == &io && j->live);
return j->cancelled;
}
enum record_dispatch_mediacodec_state record_dispatch_mediacodec_status(
const record_dispatch_mediacodec_t *j, int *error, int *file, int *system)
{
assert(j == &io && j->live);
if (error)
*error = native_error;
if (file)
*file = native_file_error;
if (system)
*system = native_system_error;
return j->state;
}
bool record_dispatch_mediacodec_gpu_claim(record_dispatch_mediacodec_t *j)
{
assert(j == &io && j->live);
if (j->claimed || j->gpu_closed)
return false;
j->claimed = true;
return true;
}
bool record_dispatch_mediacodec_gpu_retired(
record_dispatch_mediacodec_t *j, int64_t end)
{
assert(j == &io && j->claimed && end == 0);
return true;
}
bool record_dispatch_mediacodec_gpu_released(record_dispatch_mediacodec_t *j)
{
assert(j == &io && j->claimed);
j->gpu_closed = true;
return true;
}
void record_dispatch_mediacodec_gpu_release_claim(
record_dispatch_mediacodec_t *j)
{
assert(j == &io && j->claimed);
j->claimed = false;
}
bool record_dispatch_mediacodec_free(record_dispatch_mediacodec_t *j)
{
assert(j == &io && j->live && !request.live && j->gpu_closed
&& j->audio_closed && !j->claimed);
assert(j->state == RECORD_MC_DISPATCH_DONE
|| j->state == RECORD_MC_DISPATCH_FAILED);
io_frees++;
j->live = false;
return true;
}
record_android_capture_request_t *record_android_capture_request_new(
record_dispatch_mediacodec_t *j,
const record_android_capture_pool_config_t *config, unsigned duration,
bool allow_drop, bool menu_source)
{
assert(j == &io && config == &pool_config && duration == 16683 && !allow_drop
&& !request.live);
if (request_fault)
return NULL;
request.live = true;
request.binding.flags = menu_source ? RECORD_ANDROID_CAPTURE_MENU : 0;
return &request;
}
int record_android_capture_request_result(
const record_android_capture_request_t *r)
{
assert(r == &request && r->live);
return r->busy ? 0 : r->result;
}
const record_android_capture_renderer_t *record_android_capture_request_binding(
const record_android_capture_request_t *r)
{
assert(r == &request && r->live);
return r->ready && !r->closed ? &r->binding : NULL;
}
bool record_android_capture_request_post(record_android_capture_request_t *r,
enum record_android_capture_operation op, int64_t end)
{
assert(r == &request && r->live);
if (r->busy || (op >= RECORD_ANDROID_CAPTURE_FINISH && held_frames))
return false;
if (op >= RECORD_ANDROID_CAPTURE_FINISH)
{
if (r->closed)
assert(end == r->end);
r->closed = true;
r->ready = false;
r->end = end;
}
r->op = op;
r->busy = true;
r->result = 0;
return true;
}
bool record_android_capture_request_free(record_android_capture_request_t *r)
{
assert(r == &request && r->live);
if (r->busy || held_frames || (r->created && !r->collected))
return false;
r->live = false;
io.gpu_closed = true;
return true;
}
static void ack(int result)
{
assert(request.busy);
if (request.op == RECORD_ANDROID_CAPTURE_CREATE)
{
request.created = true;
request.ready = result == 1;
}
if (request.op == RECORD_ANDROID_CAPTURE_QUIESCE_DEVICE && result == 1)
assert(producer_ready);
if (request.op == RECORD_ANDROID_CAPTURE_COLLECT && result == 1)
{
assert(native_retired);
request.collected = true;
}
request.result = result;
request.busy = false;
}
static struct record_params parameters(void)
{
struct record_params p;
memset(&p, 0, sizeof(p));
p.fps = 60000.0 / 1001;
p.samplerate = 48000;
p.filename = "test.mp4";
p.out_dims = dims;
p.video_gpu_record = true;
p.channels = 2;
return p;
}
static void reset(unsigned mode)
{
assert(!record_mediacodec_pending() && !configuration.live && !io.live
&& !active_tap);
memset(&settings, 0, sizeof(settings));
memset(&recording, 0, sizeof(recording));
memset(&video, 0, sizeof(video));
memset(&device, 0, sizeof(device));
memset(&loop, 0, sizeof(loop));
loop.current_core_type = CORE_TYPE_DUMMY;
wall_us = 100000;
tap_busy = false;
memset(&metadata, 0, sizeof(metadata));
memset(&request, 0, sizeof(request));
memset(&io, 0, sizeof(io));
device.device = &video;
device.api = VIDEO_DEVICE_VULKAN;
dims = VIDEO_SCALE_PACK(1920, 1080);
metadata.version = 1;
metadata.size = sizeof(metadata);
metadata.output_mode = mode;
metadata.color_primaries = mode == 1 ? 9 : 1;
metadata.transfer_function = mode == 1 ? 16 : 8;
metadata.max_cll = 1000;
metadata.max_fall = 300;
video.av_info.timing.sample_rate = 48000;
recording.layout = AUDIO_LAYOUT_STEREO;
recording.enable = true;
async_audio = held_frames = producer_ready = native_retired = false;
config_fault = io_fault = request_fault = 0;
expected_audio = closed_audio = 0;
audio_calls = 0;
native_error = native_file_error = native_system_error = 0;
settings_available = true;
#ifdef HAVE_WEBMPLAYER
settings.bools.history_list_enable = true;
g_defaults.video_history = (playlist_t *)&playlist_token;
#endif
audio_result = RECORD_AUDIO_OK;
option_key = option_value = NULL;
}
static record_mc_context_t *start(void)
{
struct record_params p = parameters();
record_mc_context_t *ctx = (record_mc_context_t *)record_mediacodec.init(&p);
assert(ctx && !ctx->armed && record_mediacodec_pending());
recording.data = ctx;
recording.driver = &record_mediacodec;
return ctx;
}
static void arm(record_mc_context_t *ctx)
{
record_mediacodec_poll();
assert(request.busy && request.op == RECORD_ANDROID_CAPTURE_CREATE);
ack(0);
record_mediacodec_poll();
assert(!ctx->armed && request.busy);
io.state = RECORD_MC_DISPATCH_READY;
ack(1);
record_mediacodec_poll();
assert(ctx->armed && !ctx->next_us && !ctx->audio_frames);
}
static void drain(bool failed)
{
unsigned i, frees = config_frees, writes = history_writes;
if (record_mc_active)
record_mediacodec_stop();
assert(record_mc_retired && !record_mc_active);
held_frames = true;
for (i = 0; i < 100; i++)
record_mediacodec_poll();
assert(record_mc_retired && config_frees == frees && !request.busy);
assert(history_writes == writes);
held_frames = false;
record_mediacodec_poll();
assert(request.busy && request.op == RECORD_ANDROID_CAPTURE_COLLECT);
ack(0);
record_mediacodec_poll();
assert(request.busy && config_frees == frees);
assert(history_writes == writes);
native_retired = true;
ack(1);
io.state = failed ? RECORD_MC_DISPATCH_FAILED : RECORD_MC_DISPATCH_DONE;
record_mediacodec_poll();
assert(!record_mediacodec_pending() && config_frees == frees + 1);
}
static void clocks(unsigned mode)
{
static int16_t pcm[10000];
record_mc_context_t *ctx;
struct record_audio_data audio;
static video_frame_info_t info;
unsigned i, writes = history_writes;
reset(mode);
ctx = start();
assert(encoded.color == (mode != 0) && encoded.profile == (mode ? 4096 : 1));
assert(encoded.level == (mode ? 4096 : 8192));
if (mode)
{
assert(encoded.hdr_static_size == 25);
for (i = 0; i < 21; i++)
assert(!encoded.hdr_static[i]);
assert(encoded.hdr_static[21] == 232 && encoded.hdr_static[22] == 3);
assert(encoded.hdr_static[23] == 44 && encoded.hdr_static[24] == 1);
}
audio.data = pcm;
audio.frames = 5000;
assert(record_mediacodec.push_audio(ctx, &audio) && !audio_calls);
arm(ctx);
for (i = 0; i < 60000; i++)
{
record_mediacodec_frame_info(&info, true);
assert(info.recording_gpu == &request.binding
&& info.recording_gpu_capture);
assert(info.recording_gpu_timestamp
== (uint64_t)i * 1000000 * 1001 / 60000);
}
assert(ctx->next_us == (uint64_t)1001000000);
record_mediacodec_frame_info(&info, false);
assert(!info.recording_gpu_capture && ctx->next_us == (uint64_t)1001000000);
video.frame_repeat = true;
record_mediacodec_frame_info(&info, true);
assert(!info.recording_gpu_capture && ctx->next_us == (uint64_t)1001000000);
video.frame_repeat = false;
assert(record_mediacodec.push_audio(ctx, &audio));
assert(audio_calls == 2 && expected_audio == 5000
&& ctx->audio_frames == 5000);
drain(false);
assert(closed_audio == 5000);
#ifdef HAVE_WEBMPLAYER
assert(history_writes == writes + 1);
#else
assert(history_writes == writes);
#endif
}
static void reset_drains(void)
{
record_mc_context_t *ctx;
static video_frame_info_t info;
unsigned before = failures;
reset(0);
ctx = start();
arm(ctx);
record_mediacodec_frame_info(&info, true);
record_mediacodec_stop();
assert(record_mediacodec_reset_pending());
record_mediacodec_poll();
assert(request.busy && request.op == RECORD_ANDROID_CAPTURE_COLLECT);
assert(!io.cancelled);
ack(0);
record_mediacodec_poll();
assert(record_mediacodec_reset_pending() && !io.cancelled);
native_retired = true;
ack(1);
record_mediacodec_poll();
assert(!record_mediacodec_reset_pending() && record_mediacodec_pending());
assert(!request.live && configuration.live && !io.cancelled);
io.state = RECORD_MC_DISPATCH_DONE;
record_mediacodec_poll();
assert(!record_mediacodec_pending() && failures == before);
}
static void reset_lifecycle(void)
{
record_mc_context_t *ctx;
unsigned i, frees = config_frees;
reset(0);
ctx = start();
arm(ctx);
record_mc_fail(ctx);
record_mediacodec_stop();
assert(record_mediacodec_reset_pending());
record_mediacodec_poll();
assert(request.busy && request.op == RECORD_ANDROID_CAPTURE_QUIESCE_DEVICE);
ack(0);
record_mediacodec_poll();
assert(record_mediacodec_reset_pending());
producer_ready = true;
ack(1);
record_mediacodec_poll();
assert(!record_mediacodec_reset_pending() && record_mediacodec_pending());
assert(request.busy && config_frees == frees);
for (i = 0; i < 100; i++)
{
ack(0);
record_mediacodec_poll();
assert(record_mediacodec_pending() && !record_mediacodec_reset_pending());
}
device.device = &settings;
native_retired = true;
ack(1);
record_mediacodec_poll();
assert(!request.live && record_mediacodec_pending() && configuration.live);
io.state = RECORD_MC_DISPATCH_FAILED;
record_mediacodec_poll();
assert(!record_mediacodec_pending() && config_frees == frees + 1);
}
static void audio_faults(void)
{
static int16_t pcm[2];
record_mc_context_t *ctx;
struct record_audio_data audio;
unsigned fault;
audio.data = pcm;
audio.frames = 1;
for (fault = 0; fault < 3; fault++)
{
reset(0);
ctx = start();
arm(ctx);
if (fault == 0)
audio_result = RECORD_AUDIO_BUSY;
if (fault == 1)
video.av_info.timing.sample_rate = 44100;
if (fault == 2)
ctx->audio_frames = ~(uint64_t)0;
assert(!record_mediacodec.push_audio(ctx, &audio) && ctx->failed
&& io.cancelled);
if (fault == 2)
ctx->audio_frames = 0;
drain(true);
}
}
static void startup_faults(void)
{
unsigned fault;
for (fault = 0; fault < 3; fault++)
{
struct record_params p;
reset(0);
p = parameters();
config_fault = fault == 0;
io_fault = fault == 1;
request_fault = fault == 2;
assert(!record_mediacodec.init(&p));
if (fault == 2)
{
assert(record_mediacodec_pending() && io.cancelled);
record_mediacodec_poll();
assert(io.gpu_closed && io.audio_closed && configuration.live);
io.state = RECORD_MC_DISPATCH_FAILED;
record_mediacodec_poll();
}
assert(!record_mediacodec_pending() && !configuration.live && !io.live);
}
}
static void output_faults(void)
{
unsigned fault;
for (fault = 0; fault < 4; fault++)
{
record_mc_context_t *ctx;
reset(0);
ctx = start();
arm(ctx);
if (fault == 0)
dims = VIDEO_SCALE_PACK(1280, 720);
if (fault == 1)
metadata.output_mode = 1;
if (fault == 2)
device.device = &settings;
if (fault == 3)
io.cancelled = true;
record_mediacodec_poll();
assert(!record_mc_active && io.cancelled && record_mc_retired);
if (request.busy)
ack(0);
drain(true);
}
}
static void video_only(void)
{
unsigned writes = history_writes;
struct record_params p;
record_mc_context_t *ctx;
struct record_audio_data audio;
reset(0);
p = parameters();
p.config = "options";
option_key = "audio_enable";
option_value = "false";
ctx = (record_mc_context_t *)record_mediacodec.init(&p);
assert(ctx && !ctx->channels && !ctx->rate);
recording.data = ctx;
recording.driver = &record_mediacodec;
arm(ctx);
audio.data = NULL;
audio.frames = 300;
assert(record_mediacodec.push_audio(ctx, &audio) && !audio_calls);
drain(false);
assert(history_writes == writes);
}
static void menu_capture(void)
{
static int16_t mixed[10000];
static video_frame_info_t info;
struct record_params p;
struct record_audio_data silence;
record_mc_context_t *ctx;
uint64_t end;
unsigned writes = history_writes;
reset(0);
p = parameters();
p.menu_source = true;
assert(recording_is_native_gpu(&record_mediacodec));
video.av_info.timing.sample_rate = 44100;
ctx = (record_mc_context_t *)record_mediacodec.init(&p);
assert(ctx && ctx->tap);
recording.data = ctx;
recording.driver = &record_mediacodec;
record_mediacodec_poll();
io.state = RECORD_MC_DISPATCH_READY;
ack(1);
tap_busy = true;
record_mediacodec_poll();
assert(!ctx->armed && !active_tap && !expected_audio);
tap_busy = false;
record_mediacodec_poll();
assert(ctx->armed && active_tap == ctx->tap);
assert(!record_audio_tap_end(ctx->tap, &end));
silence.data = mixed; silence.frames = 5000;
assert(record_mediacodec.push_audio(ctx, &silence) && !audio_calls);
assert(record_audio_tap_push(active_tap, mixed, 5000, false, 48000));
assert(audio_calls == 2 && expected_audio == 5000);
video.frame_repeat = true;
record_mediacodec_frame_info(&info, true);
assert(info.recording_gpu_capture && info.recording_gpu_timestamp == 0);
assert(((record_android_capture_renderer_t *)info.recording_gpu)->flags
& RECORD_ANDROID_CAPTURE_MENU);
record_mediacodec_frame_info(&info, true);
assert(!info.recording_gpu_capture);
wall_us += 50000;
record_mediacodec_frame_info(&info, true);
assert(info.recording_gpu_capture && info.recording_gpu_timestamp == 50000);
wall_us += 100000;
tap_busy = true;
record_mediacodec_stop();
assert(active_tap == ctx->tap && !ctx->audio_closed && ctx->next_us == 150000);
record_mediacodec_poll();
assert(request.busy && !io.audio_closed);
native_retired = true;
ack(1);
record_mediacodec_poll();
assert(!request.live && io.live && configuration.live);
assert(history_writes == writes);
tap_busy = false;
record_mediacodec_poll();
assert(!active_tap && closed_audio == 5000 && io.live);
io.state = RECORD_MC_DISPATCH_DONE;
record_mediacodec_poll();
assert(!record_mediacodec_pending());
assert(video.av_info.timing.sample_rate == 44100);
#ifdef HAVE_WEBMPLAYER
assert(history_writes == writes + 1);
#else
assert(history_writes == writes);
#endif
}
static void history(void)
{
unsigned fault;
for (fault = 0; fault < 9; fault++)
{
char filename[] = "test.mp4";
struct record_params p;
record_mc_context_t *ctx;
static video_frame_info_t info;
unsigned writes = history_writes;
reset(0);
p = parameters();
p.filename = filename;
ctx = (record_mc_context_t *)record_mediacodec.init(&p);
assert(ctx && ctx->path != filename && !strcmp(ctx->path, filename));
filename[0] = 'X';
recording.data = ctx;
recording.driver = &record_mediacodec;
arm(ctx);
record_mediacodec_frame_info(&info, true);
assert(info.recording_gpu_capture && ctx->offered == 1);
#ifdef HAVE_WEBMPLAYER
if (fault == 1)
settings.bools.history_list_enable = false;
if (fault == 2)
g_defaults.video_history = NULL;
#endif
if (fault == 3)
native_error = 1;
if (fault == 4)
native_file_error = 1;
if (fault == 5)
native_system_error = 1;
if (fault == 7)
record_mc_fail(ctx);
if (fault == 8)
settings_available = false;
drain(fault == 6);
#ifdef HAVE_WEBMPLAYER
assert(history_writes == writes + (fault == 0 ? 1u : 0u));
#else
assert(history_writes == writes);
#endif
record_mediacodec_poll();
#ifdef HAVE_WEBMPLAYER
assert(history_writes == writes + (fault == 0 ? 1u : 0u));
#endif
}
}
static void level_selection(void)
{
mediacodec_encoder_request_t v;
bool audio;
memset(&v, 0, sizeof(v));
v.mime = "video/avc";
v.width = 1920;
v.height = 1080;
v.bitrate = 16000000;
v.frame_rate = 30.0;
assert(record_mc_level(&v) == 2048);
v.frame_rate = 60.0;
assert(record_mc_level(&v) == 8192);
v.width = 3840;
v.height = 2160;
assert(record_mc_level(&v) == 65536);
v.frame_rate = 30.0;
assert(record_mc_level(&v) == 32768);
v.width = 7680;
v.height = 4320;
assert(record_mc_level(&v) == 131072);
v.mime = "video/hevc";
assert(record_mc_level(&v) == 1048576);
v.width = 1920;
v.height = 1080;
assert(record_mc_level(&v) == 4096);
v.bitrate = 12000000;
assert(record_mc_level(&v) == 1024);
v.bitrate++;
assert(record_mc_level(&v) == 4096);
v.frame_rate = 60.0;
v.bitrate = 20000000;
assert(record_mc_level(&v) == 4096);
v.frame_rate = 60.0001;
assert(record_mc_level(&v) == 4096);
v.frame_rate = 65.0;
assert(record_mc_level(&v) == 16384);
v.width = 3840;
v.height = 2160;
v.frame_rate = 60.0;
assert(record_mc_level(&v) == 65536);
v.bitrate = 800000000;
assert(record_mc_level(&v) == 33554432);
v.bitrate++;
assert(!record_mc_level(&v));
v.bitrate = 1;
v.width = 16384;
v.height = 2;
assert(record_mc_level(&v) == 1048576);
v.width = v.height = 16384;
assert(!record_mc_level(&v));
v.width = 0;
assert(!record_mc_level(&v));
reset(0);
option_key = "video_level";
option_value = "65536";
assert(record_mc_options("options", &v, &audio));
assert(v.level == 65536);
}
static void admission(void)
{
static char overlong_path[PATH_MAX_LENGTH + 1];
struct record_params p;
unsigned fault, before;
const char *keys[] = {"format", "vcodec", "audio_enable", "video_level",
"video_bit_rate", "unknown"};
const char *values[] = {"avi", "vp9", "bad", "3", "0", "yes"};
for (fault = 0; fault < 6; fault++)
{
reset(0);
p = parameters();
p.config = "options";
option_key = keys[fault];
option_value = values[fault];
before = config_creates;
assert(!record_mediacodec.init(&p) && config_creates == before);
}
reset(1);
p = parameters();
p.config = "options";
option_key = "vcodec";
option_value = "h264";
assert(!record_mediacodec.init(&p));
reset(0);
p = parameters();
recording.layout = AUDIO_SPEAKER_FRONT_LEFT | AUDIO_SPEAKER_BACK_LEFT;
assert(!record_mediacodec.init(&p));
reset(0);
p = parameters();
async_audio = true;
assert(!record_mediacodec.init(&p));
reset(0);
p = parameters();
p.channels = 6;
assert(!record_mediacodec.init(&p));
reset(0);
p = parameters();
p.fps = 0;
assert(!record_mediacodec.init(&p));
reset(0);
p = parameters();
p.out_dims = VIDEO_SCALE_PACK(1919, 1080);
assert(!record_mediacodec.init(&p));
reset(0);
p = parameters();
memset(overlong_path, 'x', sizeof(overlong_path) - 1);
overlong_path[sizeof(overlong_path) - 1] = 0;
p.filename = overlong_path;
before = config_creates;
assert(!record_mediacodec.init(&p) && config_creates == before);
reset(0);
assert(record_mediacodec_defer_start() && record_mediacodec_pending());
dims = 0;
record_mediacodec_poll();
assert(record_mc_start_requested);
dims = VIDEO_SCALE_PACK(1920, 1080);
record_mediacodec_poll();
assert(!record_mc_start_requested && starts == 1
&& !record_mediacodec_pending());
menu_view_only = true;
loop.current_core_type = CORE_TYPE_DUMMY;
assert(record_mediacodec_defer_start());
record_mediacodec_poll();
assert(!record_mc_start_requested && starts == 2);
menu_view_only = false;
}
int main(void)
{
clocks(0);
clocks(1);
clocks(2);
reset_drains();
reset_lifecycle();
output_faults();
video_only();
audio_faults();
startup_faults();
history();
menu_capture();
level_selection();
admission();
assert(io_frees && config_frees && logs >= 3 && failures >= 4 && stops);
puts("PASS full MediaCodec frontend startup, rational clocks, PCM, closure, "
"reset readiness and retained native lifetimes; platform boundaries "
"mocked");
return 0;
}
@@ -0,0 +1,492 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier:
* GPL-3.0-or-later */
#define _POSIX_C_SOURCE 200809L
#ifdef NDEBUG
#error GPU dispatcher acceptance requires assertions
#endif
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "../../../record/record_gpu_dispatch_mediacodec.h"
#ifndef _WIN32
#include <pthread.h>
#include <unistd.h>
#include <sys/eventfd.h>
#include <fcntl.h>
#include <errno.h>
#include <time.h>
#include <retro_atomic.h>
struct record_dispatch_mediacodec
{
retro_atomic_int_t cancelled, retired, released, gpu_owner;
unsigned color;
int64_t end;
};
struct record_mediacodec_surface
{
bool active;
};
struct record_android_capture_gl
{
int live;
};
struct record_android_capture_draw
{
int live;
};
struct record_mediacodec_gpu_worker
{
record_gpu_queue_t *queue;
void (*close_fd)(void *, int);
void *context;
unsigned slot;
bool held, ended;
};
static struct record_dispatch_mediacodec io;
static struct record_mediacodec_surface surface;
static struct record_android_capture_gl capture;
static struct record_android_capture_draw draw;
static struct record_mediacodec_gpu_worker worker;
static pthread_t gpu_thread;
static retro_atomic_int_t resources, posted, hold_read, read_error;
static int fault, mode, surface_live, worker_live;
static bool temporary_failure;
static int test_eventfd(unsigned int initial, int flags)
{
return fault == 10 ? -1 : eventfd(initial, flags);
}
static int test_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*run)(void *), void *context)
{
return fault == 11 ? EAGAIN : pthread_create(thread, attr, run, context);
}
static int test_detach(pthread_t thread)
{
return fault == 12 ? EINVAL : pthread_detach(thread);
}
#define eventfd test_eventfd
#define pthread_create test_create
#define pthread_detach test_detach
#include "../../../record/record_gpu_dispatch_mediacodec.c"
#undef eventfd
#undef pthread_create
#undef pthread_detach
static void owner(void)
{
assert(pthread_equal(pthread_self(), gpu_thread));
}
bool record_dispatch_mediacodec_video_format(
const record_dispatch_mediacodec_t *j, unsigned *width, unsigned *height,
unsigned *color)
{
assert(j == &io);
*width = fault == 13 ? 65 : 64;
*height = fault == 14 ? 49 : 48;
*color = fault == 15 ? !io.color : io.color;
return true;
}
bool record_dispatch_mediacodec_gpu_claim(record_dispatch_mediacodec_t *j)
{
assert(j == &io);
return retro_atomic_cas_int(&io.gpu_owner, 0, 1) != 0;
}
void record_dispatch_mediacodec_gpu_release_claim(
record_dispatch_mediacodec_t *j)
{
assert(j == &io && retro_atomic_load_acquire_int(&io.gpu_owner));
retro_atomic_store_release_int(&io.gpu_owner, 0);
}
enum record_dispatch_mediacodec_state record_dispatch_mediacodec_status(
const record_dispatch_mediacodec_t *j, int *error, int *file, int *system)
{
assert(j == &io && !error && !file && !system);
return RECORD_MC_DISPATCH_READY;
}
bool record_dispatch_mediacodec_cancelled(const record_dispatch_mediacodec_t *j)
{
assert(j == &io);
return retro_atomic_load_acquire_int(&io.cancelled) != 0;
}
void record_dispatch_mediacodec_cancel(record_dispatch_mediacodec_t *j)
{
assert(j == &io);
retro_atomic_store_release_int(&io.cancelled, 1);
}
void record_dispatch_mediacodec_notify(record_dispatch_mediacodec_t *j)
{
assert(j == &io);
}
void *record_dispatch_mediacodec_surface(record_dispatch_mediacodec_t *j)
{
assert(j == &io);
return &io;
}
bool record_dispatch_mediacodec_gpu_retired(
record_dispatch_mediacodec_t *j, int64_t end)
{
assert(j == &io && !worker.held);
io.end = end;
retro_atomic_store_release_int(&io.retired, 1);
return true;
}
bool record_dispatch_mediacodec_gpu_released(record_dispatch_mediacodec_t *j)
{
assert(j == &io && retro_atomic_load_acquire_int(&io.retired));
assert(!surface_live && !capture.live && !draw.live && !worker_live);
retro_atomic_store_release_int(&io.released, 1);
return true;
}
bool record_mediacodec_surface_new(const record_mediacodec_surface_ops_t *ops,
EGLDisplay display, EGLConfig config, EGLContext share,
EGLNativeWindowType window, unsigned width, unsigned height, bool pq,
record_mediacodec_surface_t **output)
{
(void)ops;
(void)share;
assert(display && config && window && width == 64 && height == 48);
assert(pq == (mode != 0) && !surface_live);
gpu_thread = pthread_self();
surface.active = false;
surface_live = 1;
retro_atomic_fetch_add_int(&resources, 1);
*output = &surface;
return fault != 1;
}
bool record_mediacodec_surface_begin(record_mediacodec_surface_t *s)
{
owner();
assert(s == &surface && !s->active);
if (fault == 2)
return false;
s->active = true;
return true;
}
bool record_mediacodec_surface_begin_retirement(record_mediacodec_surface_t *s)
{
owner();
assert(s == &surface);
s->active = true;
return true;
}
bool record_mediacodec_surface_leave(record_mediacodec_surface_t *s)
{
owner();
assert(s == &surface);
if (fault == 6 && !temporary_failure)
{
temporary_failure = true;
return false;
}
s->active = false;
return true;
}
bool record_mediacodec_surface_free(record_mediacodec_surface_t *s)
{
owner();
assert(s == &surface && !s->active && !capture.live && !draw.live);
if (fault == 7 && !temporary_failure)
{
temporary_failure = true;
return false;
}
surface_live = 0;
retro_atomic_fetch_add_int(&resources, -1);
return true;
}
bool record_android_capture_gl_new(EGLDisplay display,
const record_android_capture_gl_ops_t *ops,
struct AHardwareBuffer *const *buffers, unsigned count,
record_android_capture_gl_t **output)
{
(void)ops;
owner();
assert(display && surface.active && buffers && count == 1);
capture.live = 1;
retro_atomic_fetch_add_int(&resources, 1);
*output = &capture;
return fault != 3;
}
bool record_android_capture_gl_free(record_android_capture_gl_t *c)
{
owner();
assert(surface.active && !worker.held);
if (!c)
return true;
assert(c == &capture && c->live);
if (fault == 8 && !temporary_failure)
{
temporary_failure = true;
return false;
}
c->live = 0;
retro_atomic_fetch_add_int(&resources, -1);
return true;
}
record_android_capture_draw_t *record_android_capture_draw_new(
const record_android_capture_draw_ops_t *ops, unsigned width,
unsigned height, unsigned input_mode, bool flip)
{
(void)ops;
owner();
assert(surface.active && capture.live && width == 64 && height == 48);
assert(input_mode == (unsigned)mode && flip);
if (fault == 4)
return NULL;
draw.live = 1;
retro_atomic_fetch_add_int(&resources, 1);
return &draw;
}
void record_android_capture_draw_free(
record_android_capture_draw_t *d, bool abandon)
{
owner();
assert(surface.active && !abandon && !worker.held);
if (d)
{
assert(d == &draw && d->live);
d->live = 0;
retro_atomic_fetch_add_int(&resources, -1);
}
}
record_mediacodec_gpu_worker_t *record_mediacodec_gpu_worker_new(
record_gpu_queue_t *q, record_mediacodec_surface_t *s,
record_android_capture_gl_t *c, record_android_capture_draw_t *d,
unsigned count, void (*close_fd)(void *, int), void *context)
{
owner();
assert(q && s == &surface && c == &capture && d == &draw && count == 1);
if (fault == 5)
return NULL;
memset(&worker, 0, sizeof(worker));
worker.queue = q;
worker.close_fd = close_fd;
worker.context = context;
worker_live = 1;
return &worker;
}
static int worker_step(record_mediacodec_gpu_worker_t *w, bool retire)
{
record_gpu_frame_t frame;
int result;
owner();
assert(w == &worker && worker_live);
if (w->held)
{
if (retro_atomic_load_acquire_int(&read_error))
return -1;
if (retro_atomic_load_acquire_int(&hold_read))
return 0;
assert(record_gpu_queue_release(w->queue, w->slot));
w->held = false;
}
result = record_gpu_queue_take(w->queue, &frame);
if (result == RECORD_GPU_QUEUE_BUSY)
return 0;
if (result == RECORD_GPU_QUEUE_END)
{
w->ended = true;
return 2;
}
assert(result == RECORD_GPU_QUEUE_OK);
if (frame.ready_value)
w->close_fd(w->context, (int)(frame.ready_value - 1));
if (retire)
{
assert(record_gpu_queue_release(w->queue, frame.slot));
return 1;
}
w->slot = frame.slot;
w->held = true;
retro_atomic_fetch_add_int(&posted, 1);
return 1;
}
int record_mediacodec_gpu_worker_step(record_mediacodec_gpu_worker_t *w)
{
return worker_step(w, false);
}
int record_mediacodec_gpu_worker_retire_step(record_mediacodec_gpu_worker_t *w)
{
return worker_step(w, true);
}
bool record_mediacodec_gpu_worker_free(record_mediacodec_gpu_worker_t *w)
{
owner();
assert(w == &worker && w->ended && !w->held);
worker_live = 0;
return true;
}
static void pause_test(void)
{
struct timespec t;
t.tv_sec = 0;
t.tv_nsec = 1000000;
nanosleep(&t, NULL);
}
static void wait_state(record_gpu_dispatch_mediacodec_t *j,
enum record_dispatch_mediacodec_state state)
{
unsigned i;
for (i = 0; i < 10000; i++)
{
if (record_gpu_dispatch_mediacodec_status(j, NULL, NULL) == state)
return;
pause_test();
}
assert(0);
}
static void collect(record_gpu_dispatch_mediacodec_t *j)
{
unsigned i;
for (i = 0; i < 10000; i++)
{
if (record_gpu_dispatch_mediacodec_free(j))
break;
pause_test();
}
assert(i < 10000 && !retro_atomic_load_acquire_int(&resources));
assert(retro_atomic_load_acquire_int(&io.released));
assert(!retro_atomic_load_acquire_int(&io.gpu_owner));
}
static record_gpu_dispatch_mediacodec_t *start(void)
{
record_gpu_mediacodec_config_t c;
static char buffer;
memset(&c, 0, sizeof(c));
c.width = 64;
c.height = 48;
c.count = 1;
c.mode = (unsigned)mode;
c.flip_y = true;
c.buffers[0] = (struct AHardwareBuffer *)&buffer;
c.display = (EGLDisplay)&io;
c.config = (EGLConfig)&io;
io.color = mode ? 1 : 0;
io.end = 0;
retro_atomic_int_init(&io.cancelled, 0);
retro_atomic_int_init(&io.retired, 0);
retro_atomic_int_init(&io.released, 0);
retro_atomic_int_init(&io.gpu_owner, 0);
temporary_failure = false;
return record_gpu_dispatch_mediacodec_new(&io, &c);
}
int main(void)
{
record_gpu_dispatch_mediacodec_t *j;
record_gpu_frame_t frame;
unsigned slot, i;
int before, fd;
retro_atomic_int_init(&resources, 0);
retro_atomic_int_init(&posted, 0);
retro_atomic_int_init(&hold_read, 0);
retro_atomic_int_init(&read_error, 0);
for (mode = 0; mode < 3; mode++)
{
fault = mode == 0 ? 12 : 0;
j = start();
assert(j);
wait_state(j, RECORD_MC_DISPATCH_READY);
assert(!record_gpu_dispatch_mediacodec_new(&io, &j->config));
memset(&frame, 0, sizeof(frame));
frame.duration = 1000;
for (i = 0; i < 1000; i++)
{
unsigned retry;
for (retry = 0; retry < 10000; retry++)
{
if (record_gpu_dispatch_mediacodec_acquire(j, &slot)
== RECORD_GPU_QUEUE_OK)
break;
pause_test();
}
assert(retry < 10000);
frame.slot = slot;
if (i)
{
frame.timestamp = (i - 1) * 1000;
assert(!record_gpu_dispatch_mediacodec_submit(j, &frame));
}
frame.timestamp = i * 1000;
assert(record_gpu_dispatch_mediacodec_submit(j, &frame));
}
assert(!record_gpu_dispatch_mediacodec_finish(j, 999999));
assert(record_gpu_dispatch_mediacodec_finish(j, 1000000));
assert(record_gpu_dispatch_mediacodec_finish(j, 1000000));
assert(!record_gpu_dispatch_mediacodec_finish(j, 1000001));
wait_state(j, RECORD_MC_DISPATCH_DONE);
collect(j);
assert(io.end == 1000000);
}
assert(retro_atomic_load_acquire_int(&posted) == 3000);
mode = 0;
for (fault = 1; fault <= 8; fault++)
{
j = start();
assert(j);
wait_state(j,
fault <= 6 ? RECORD_MC_DISPATCH_DRAINING
: RECORD_MC_DISPATCH_READY);
if (fault <= 6)
assert(retro_atomic_load_acquire_int(&io.cancelled));
assert(record_gpu_dispatch_mediacodec_finish(j, 0));
wait_state(j,
fault <= 6 ? RECORD_MC_DISPATCH_FAILED : RECORD_MC_DISPATCH_DONE);
collect(j);
}
for (fault = 10; fault <= 11; fault++)
{
assert(!start());
assert(!retro_atomic_load_acquire_int(&resources));
}
for (fault = 13; fault <= 15; fault++)
assert(!start());
fault = 0;
j = start();
assert(j);
wait_state(j, RECORD_MC_DISPATCH_READY);
assert(record_gpu_dispatch_mediacodec_acquire(j, &slot)
== RECORD_GPU_QUEUE_OK);
assert(!record_gpu_dispatch_mediacodec_finish(j, 0));
assert(record_gpu_dispatch_mediacodec_cancel_slot(j));
assert(record_gpu_dispatch_mediacodec_acquire(j, &slot)
== RECORD_GPU_QUEUE_OK);
memset(&frame, 0, sizeof(frame));
frame.slot = slot;
frame.duration = 1000;
fd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK);
assert(fd >= 0);
frame.ready_value = (uint64_t)fd + 1;
retro_atomic_store_release_int(&hold_read, 1);
before = retro_atomic_load_acquire_int(&posted);
assert(record_gpu_dispatch_mediacodec_submit(j, &frame));
for (i = 0; i < 10000 && retro_atomic_load_acquire_int(&posted) == before;
i++)
pause_test();
assert(i < 10000);
retro_atomic_store_release_int(&read_error, 1);
wait_state(j, RECORD_MC_DISPATCH_DRAINING);
assert(record_gpu_dispatch_mediacodec_finish(j, 1000));
assert(!record_gpu_dispatch_mediacodec_free(j));
assert(!retro_atomic_load_acquire_int(&io.retired)
&& !retro_atomic_load_acquire_int(&io.released));
assert(fcntl(fd, F_GETFD) == -1 && errno == EBADF);
retro_atomic_store_release_int(&hold_read, 0);
retro_atomic_store_release_int(&read_error, 0);
wait_state(j, RECORD_MC_DISPATCH_FAILED);
collect(j);
puts("PASS: real GLES executor thread and slot queue, 3000 posts, source "
"ends, "
"partial-startup cleanup and retained reads on cancellation; native "
"GPU/I/O owners mocked");
return 0;
}
#else
#include "../../../record/record_gpu_dispatch_mediacodec.c"
int main(void)
{
assert(!record_gpu_dispatch_mediacodec_new(NULL, NULL));
assert(record_gpu_dispatch_mediacodec_free(NULL));
puts("PASS: unsupported GPU dispatcher stubs");
return 0;
}
#endif
@@ -1,5 +1,8 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier:
* GPL-3.0-or-later */
#ifdef NDEBUG
#error GPU retirement acceptance requires assertions
#endif
#include <assert.h>
#include <stdio.h>
#include <string.h>
@@ -10,6 +13,7 @@
static bool active, terminal, complete, reading[16], fenced[16];
static unsigned posts, draws, imports, closes, retire_scopes, begins;
static int failure, restore_fail;
static bool recovery_fails;
static int64_t pts;
bool record_mediacodec_surface_begin(record_mediacodec_surface_t *s)
{
@@ -90,6 +94,17 @@ int record_android_capture_gl_poll(
reading[slot] = fenced[slot] = false;
return 1;
}
int record_android_capture_gl_recover_step(record_android_capture_gl_t *c)
{
assert(c == CAPTURE && active);
if (recovery_fails)
return -1;
if (!complete)
return 0;
memset(reading, 0, sizeof(reading));
memset(fenced, 0, sizeof(fenced));
return 1;
}
void record_android_capture_gl_after_context_retired(
record_android_capture_gl_t *c)
{
@@ -109,6 +124,7 @@ static void reset(void)
for (i = 0; i < 16; i++)
assert(!reading[i] && !fenced[i]);
terminal = complete = false;
recovery_fails = false;
posts = draws = imports = closes = retire_scopes = begins = 0;
failure = restore_fail = 0;
pts = -1;
@@ -186,6 +202,7 @@ int main(void)
assert(record_mediacodec_gpu_worker_step(w) == -1);
assert(record_gpu_queue_finish(q));
failure = 0;
recovery_fails = i == 5;
if (i >= 4)
{
assert(record_mediacodec_gpu_worker_retire_step(w)
@@ -201,6 +218,21 @@ int main(void)
assert(record_mediacodec_gpu_worker_free(w) && record_gpu_queue_free(q));
}
reset();
q = record_gpu_queue_new(1);
w = create(q, 1);
assert(w);
submit(q, 0, 0, false);
failure = 5;
assert(record_mediacodec_gpu_worker_step(w) == -1);
failure = 0;
assert(record_gpu_queue_finish(q));
assert(record_mediacodec_gpu_worker_retire_step(w) == 0);
assert(!record_mediacodec_gpu_worker_free(w));
complete = true;
assert(record_mediacodec_gpu_worker_retire_step(w) == 2);
assert(draws == 1 && !posts);
assert(record_mediacodec_gpu_worker_free(w) && record_gpu_queue_free(q));
reset();
q = record_gpu_queue_new(3);
w = create(q, 3);
assert(w);
@@ -0,0 +1,587 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier:
* GPL-3.0-or-later */
#include <assert.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include "../../../record/record_android_capture_request.c"
struct record_android_capture_pool
{
bool live, closed, prepared, dead;
};
static struct record_android_capture_pool pool;
static video_driver_state_t video;
static video_device_info_t device;
static video_thread_async_load_t *pending;
static const void *expected_binding;
static VkPhysicalDeviceMemoryProperties source_memory;
static bool threaded, in_flight, post_allowed, egl_allowed, egl_free_allowed;
static bool io_cancelled;
static bool pool_allowed, partial_pool, finish_allowed, reads_retired, ready;
static bool locked, queue_busy, gpu_done, gpu_released, gpu_claimed;
static unsigned egl_creates, pool_creates, pool_steps, pool_frees, lock_calls;
static unsigned configured_memory;
static int64_t gpu_end;
static enum record_dispatch_mediacodec_state io_state;
static int egl_storage, io_storage;
video_driver_state_t *video_state_get_ptr(void)
{
return &video;
}
bool video_driver_thread_wrapper_active(void)
{
return threaded;
}
bool video_thread_record_gpu_in_flight(void *data, const void *binding)
{
assert(data == video.data && binding == expected_binding);
return in_flight;
}
bool video_thread_async_post(video_thread_async_load_t *node)
{
assert(!pending && node->kind == VIDEO_THREAD_ASYNC_JOB);
if (!post_allowed)
return false;
pending = node;
return true;
}
bool slock_try_lock(slock_t *lock)
{
assert(lock == device.queue_lock && !locked);
if (queue_busy)
return false;
locked = true;
lock_calls++;
return true;
}
void slock_unlock(slock_t *lock)
{
assert(lock == device.queue_lock && locked);
locked = false;
}
bool record_android_capture_pool_closed(const record_android_capture_pool_t *p)
{
return p && p->closed;
}
static bool get_device(void *data, video_device_info_t *out)
{
assert(data == video.data);
*out = device;
return true;
}
static const video_poke_interface_t *test_poke(void)
{
static video_poke_interface_t poke;
memset(&poke, 0, sizeof(poke));
poke.get_device = get_device;
return &poke;
}
bool record_android_capture_egl_new(
unsigned mode, record_android_capture_egl_t **out)
{
assert(mode <= 2 && !locked);
egl_creates++;
*out = (record_android_capture_egl_t *)&egl_storage;
return egl_allowed;
}
bool record_android_capture_egl_configure(
const record_android_capture_egl_t *egl,
record_gpu_mediacodec_config_t *config)
{
assert(egl == (record_android_capture_egl_t *)&egl_storage);
config->display = (EGLDisplay)(uintptr_t)66;
return true;
}
bool record_android_capture_egl_free(record_android_capture_egl_t *egl)
{
assert(egl == (record_android_capture_egl_t *)&egl_storage && !pool.live);
return egl_free_allowed;
}
bool record_android_capture_pool_new(record_dispatch_mediacodec_t *io,
const record_android_capture_pool_config_t *config,
record_android_capture_pool_t **out)
{
assert(io == (record_dispatch_mediacodec_t *)&io_storage && !locked);
assert(config->memory != &source_memory);
assert(config->memory->memoryTypeCount == configured_memory);
assert(config->gpu.display == (EGLDisplay)(uintptr_t)66);
pool_creates++;
*out = pool_allowed || partial_pool ? &pool : NULL;
pool.live = *out != NULL;
if (pool.live)
gpu_claimed = true;
return pool_allowed;
}
bool record_android_capture_pool_bind_renderer(record_android_capture_pool_t *p,
unsigned duration, record_android_capture_renderer_t *binding)
{
assert(p == &pool && duration == 10);
binding->context = p;
return true;
}
enum record_dispatch_mediacodec_state record_android_capture_pool_status(
const record_android_capture_pool_t *p, int *error, int *system)
{
(void)error;
(void)system;
assert(p == &pool);
return ready ? RECORD_MC_DISPATCH_READY : RECORD_MC_DISPATCH_STARTING;
}
bool record_android_capture_pool_device_state(
const record_android_capture_pool_t *p, bool *destroyed)
{
assert(p == &pool);
*destroyed = p->dead;
return p->prepared;
}
void record_android_capture_pool_cancel(record_android_capture_pool_t *p)
{
assert(p == &pool && (locked || p->dead));
}
bool record_android_capture_pool_finish(
record_android_capture_pool_t *p, int64_t end)
{
assert(p == &pool && end == 40 && (locked || p->dead));
if (!finish_allowed)
return false;
p->closed = true;
return true;
}
int record_android_capture_pool_step(record_android_capture_pool_t *p)
{
assert(p == &pool && (locked || p->dead));
pool_steps++;
if (p->prepared && !p->dead)
return 0;
return p->closed && reads_retired ? 1 : 0;
}
bool record_android_capture_pool_prepare_device_destroy(
record_android_capture_pool_t *p, bool queue_quiesced)
{
assert(p == &pool && locked && !queue_quiesced && p->closed);
p->prepared = true;
return true;
}
bool record_android_capture_pool_free(record_android_capture_pool_t *p)
{
assert(p == &pool && (locked || p->dead));
if (!p->closed || !reads_retired || (p->prepared && !p->dead))
return false;
p->live = false;
pool_frees++;
gpu_done = gpu_released = true;
gpu_claimed = false;
gpu_end = 40;
return true;
}
bool record_dispatch_mediacodec_cancelled(
const record_dispatch_mediacodec_t *io)
{
assert(io == (record_dispatch_mediacodec_t *)&io_storage);
return io_cancelled;
}
enum record_dispatch_mediacodec_state record_dispatch_mediacodec_status(
const record_dispatch_mediacodec_t *io, int *error, int *file,
int *system)
{
(void)error;
(void)file;
(void)system;
assert(io == (record_dispatch_mediacodec_t *)&io_storage);
return io_state;
}
bool record_dispatch_mediacodec_gpu_claim(record_dispatch_mediacodec_t *io)
{
assert(io == (record_dispatch_mediacodec_t *)&io_storage);
if (gpu_claimed || gpu_done)
return false;
gpu_claimed = true;
return true;
}
void record_dispatch_mediacodec_gpu_release_claim(
record_dispatch_mediacodec_t *io)
{
assert(io == (record_dispatch_mediacodec_t *)&io_storage && gpu_claimed);
gpu_claimed = false;
}
bool record_dispatch_mediacodec_gpu_retired(
record_dispatch_mediacodec_t *io, int64_t end)
{
assert(io == (record_dispatch_mediacodec_t *)&io_storage);
if (gpu_done)
return gpu_end == end;
gpu_done = true;
gpu_end = end;
return true;
}
bool record_dispatch_mediacodec_gpu_released(record_dispatch_mediacodec_t *io)
{
assert(io == (record_dispatch_mediacodec_t *)&io_storage && gpu_done);
gpu_released = true;
return true;
}
static void flush(bool discard, bool execute)
{
video_thread_async_load_t *node = pending;
const video_thread_async_job_t *job;
uintptr_t result = 0;
assert(node);
pending = NULL;
job = (const video_thread_async_job_t *)node->img;
if (execute)
result = job->run(video.data, video.poke, job->user);
node->img = NULL;
node->done(node->user, discard ? 0 : result);
}
static record_android_capture_request_t *new_request_source(unsigned mode, bool menu_source)
{
static record_android_capture_pool_config_t config;
record_android_capture_request_t *r;
memset(&pool, 0, sizeof(pool));
memset(&video, 0, sizeof(video));
memset(&device, 0, sizeof(device));
memset(&config, 0, sizeof(config));
memset(&source_memory, 0, sizeof(source_memory));
configured_memory = source_memory.memoryTypeCount = 3;
config.physical = (VkPhysicalDevice)(uintptr_t)1;
config.device = (VkDevice)(uintptr_t)2;
config.queue = (VkQueue)(uintptr_t)3;
config.memory = &source_memory;
config.family = 4;
config.gpu.width = 128;
config.gpu.height = 96;
config.gpu.count = 2;
config.gpu.mode = mode;
device.api = VIDEO_DEVICE_VULKAN;
device.device = (void *)config.device;
device.physical_device = (void *)config.physical;
device.queue = (void *)config.queue;
device.queue_family = config.family;
device.queue_lock = (void *)(uintptr_t)5;
video.data = &video;
video.poke = test_poke();
pending = NULL;
threaded = post_allowed = egl_allowed = egl_free_allowed = true;
io_cancelled = false;
pool_allowed = finish_allowed = true;
in_flight = partial_pool = reads_retired = ready = locked = false;
queue_busy = gpu_done = gpu_released = gpu_claimed = false;
egl_creates = pool_creates = pool_steps = pool_frees = lock_calls = 0;
io_state = RECORD_MC_DISPATCH_STARTING;
r = record_android_capture_request_new(
(record_dispatch_mediacodec_t *)&io_storage, &config, 10, true, menu_source);
assert(r);
expected_binding = &r->binding;
source_memory.memoryTypeCount = 9;
return r;
}
static record_android_capture_request_t *new_request(unsigned mode)
{
return new_request_source(mode, false);
}
static void start(record_android_capture_request_t *r)
{
assert(record_android_capture_request_post(
r, RECORD_ANDROID_CAPTURE_CREATE, 0));
assert(!record_android_capture_request_free(r));
assert(!record_android_capture_request_post(
r, RECORD_ANDROID_CAPTURE_SERVICE, 0));
flush(false, true);
assert(record_android_capture_request_result(r) == 0 && !egl_creates);
io_state = RECORD_MC_DISPATCH_READY;
assert(record_android_capture_request_post(
r, RECORD_ANDROID_CAPTURE_CREATE, 0));
flush(false, true);
assert(!record_android_capture_request_binding(r));
ready = true;
assert(record_android_capture_request_post(
r, RECORD_ANDROID_CAPTURE_CREATE, 0));
flush(false, true);
assert(record_android_capture_request_result(r) == 1);
assert(record_android_capture_request_binding(r) == &r->binding);
assert(r->binding.context == &pool && egl_creates == 1 && pool_creates == 1);
}
static void normal(unsigned mode)
{
record_android_capture_request_t *r = new_request(mode);
start(r);
assert(record_android_capture_request_post(
r, RECORD_ANDROID_CAPTURE_SERVICE, 0));
flush(false, true);
assert(pool_steps == 1 && lock_calls == 1);
in_flight = true;
assert(!record_android_capture_request_post(
r, RECORD_ANDROID_CAPTURE_FINISH, 40));
assert(!record_android_capture_request_binding(r));
assert(!record_android_capture_request_post(
r, RECORD_ANDROID_CAPTURE_FINISH, 41));
in_flight = false;
assert(record_android_capture_request_post(
r, RECORD_ANDROID_CAPTURE_FINISH, 40));
flush(false, true);
assert(pool.closed && !record_android_capture_request_free(r));
assert(!record_android_capture_request_post(
r, RECORD_ANDROID_CAPTURE_CREATE, 0));
assert(record_android_capture_request_post(
r, RECORD_ANDROID_CAPTURE_COLLECT, 40));
flush(false, true);
assert(!pool_frees);
reads_retired = true;
egl_free_allowed = false;
assert(record_android_capture_request_post(
r, RECORD_ANDROID_CAPTURE_COLLECT, 40));
flush(false, true);
assert(pool_frees == 1 && !record_android_capture_request_free(r));
egl_free_allowed = true;
assert(record_android_capture_request_post(
r, RECORD_ANDROID_CAPTURE_COLLECT, 40));
flush(false, true);
assert(record_android_capture_request_free(r) && gpu_end == 40);
}
static void busy_queue(void)
{
record_android_capture_request_t *r = new_request(0);
start(r);
queue_busy = true;
assert(record_android_capture_request_post(
r, RECORD_ANDROID_CAPTURE_COLLECT, 40));
flush(false, true);
assert(record_android_capture_request_result(r) == 0);
assert(pool.live && !pool.closed && !pool_steps && !pool_frees);
assert(gpu_claimed && !gpu_done && !gpu_released && !lock_calls);
assert(!record_android_capture_request_free(r));
queue_busy = false;
reads_retired = true;
assert(record_android_capture_request_post(
r, RECORD_ANDROID_CAPTURE_COLLECT, 40));
flush(false, true);
assert(record_android_capture_request_result(r) == 1);
assert(pool_frees == 1 && lock_calls == 1 && !locked);
assert(record_android_capture_request_free(r));
}
static void teardown(bool execute)
{
record_android_capture_request_t *r = new_request(0);
io_state = RECORD_MC_DISPATCH_READY;
ready = true;
assert(record_android_capture_request_post(
r, RECORD_ANDROID_CAPTURE_CREATE, 0));
if (execute)
{
const video_thread_async_job_t *job
= (const video_thread_async_job_t *)pending->img;
job->run(video.data, video.poke, job->user);
pool.prepared = pool.dead = true;
}
flush(true, false);
assert(record_android_capture_request_result(r) == -1);
if (execute)
{
device.device = (void *)(uintptr_t)222;
reads_retired = true;
assert(!record_android_capture_request_free(r));
assert(record_android_capture_request_post(
r, RECORD_ANDROID_CAPTURE_COLLECT, 40));
flush(false, true);
assert(!lock_calls && pool_frees == 1);
}
assert(record_android_capture_request_free(r));
assert(gpu_done && gpu_released && gpu_end == (execute ? 40 : 0));
}
static void prepare(void)
{
record_android_capture_request_t *r = new_request(2);
start(r);
finish_allowed = false;
assert(record_android_capture_request_post(
r, RECORD_ANDROID_CAPTURE_PREPARE_DEVICE, 40));
flush(false, true);
assert(!pool.prepared);
finish_allowed = true;
assert(record_android_capture_request_post(
r, RECORD_ANDROID_CAPTURE_PREPARE_DEVICE, 40));
flush(false, true);
assert(pool.prepared && !pool.dead);
assert(record_android_capture_request_result(r) == 1);
device.device = (void *)(uintptr_t)222;
assert(record_android_capture_request_post(
r, RECORD_ANDROID_CAPTURE_COLLECT, 40));
flush(false, true);
assert(!pool_frees);
pool.dead = true;
reads_retired = true;
assert(record_android_capture_request_post(
r, RECORD_ANDROID_CAPTURE_COLLECT, 40));
flush(false, true);
assert(pool_frees == 1);
assert(record_android_capture_request_free(r));
}
static void startup_fault(bool partial)
{
record_android_capture_request_t *r = new_request(0);
io_state = RECORD_MC_DISPATCH_READY;
pool_allowed = false;
partial_pool = partial;
assert(record_android_capture_request_post(
r, RECORD_ANDROID_CAPTURE_CREATE, 0));
flush(false, true);
assert(record_android_capture_request_result(r) == -1);
assert(!record_android_capture_request_free(r));
reads_retired = true;
assert(record_android_capture_request_post(
r, RECORD_ANDROID_CAPTURE_CANCEL, 40));
flush(false, true);
assert(record_android_capture_request_post(
r, RECORD_ANDROID_CAPTURE_COLLECT, 40));
flush(false, true);
assert(record_android_capture_request_free(r));
assert(gpu_end == (partial ? 40 : 0));
}
static void post_refusal(void)
{
record_android_capture_request_t *r = new_request(0);
start(r);
post_allowed = false;
assert(!record_android_capture_request_post(
r, RECORD_ANDROID_CAPTURE_SERVICE, 0));
assert(!record_android_capture_request_binding(r));
assert(record_android_capture_request_result(r) == -1);
post_allowed = true;
reads_retired = true;
assert(record_android_capture_request_post(
r, RECORD_ANDROID_CAPTURE_COLLECT, 40));
flush(false, true);
assert(record_android_capture_request_free(r));
}
static void inline_owner(void)
{
record_android_capture_request_t *r = new_request_source(1, true);
threaded = false;
io_state = RECORD_MC_DISPATCH_READY;
ready = true;
assert(record_android_capture_request_post(
r, RECORD_ANDROID_CAPTURE_CREATE, 0));
assert(record_android_capture_request_binding(r));
assert(record_android_capture_request_binding(r)->flags
& RECORD_ANDROID_CAPTURE_MENU);
reads_retired = true;
assert(record_android_capture_request_post(
r, RECORD_ANDROID_CAPTURE_COLLECT, 40));
assert(record_android_capture_request_result(r) == 1);
assert(record_android_capture_request_free(r));
}
static void unavailable_queue(void)
{
record_android_capture_request_t *r = new_request(0);
io_state = RECORD_MC_DISPATCH_READY;
device.queue_lock = NULL;
assert(record_android_capture_request_post(
r, RECORD_ANDROID_CAPTURE_CREATE, 0));
flush(false, true);
assert(record_android_capture_request_result(r) == -1 && !egl_creates);
assert(record_android_capture_request_free(r));
}
static void unrelated_gpu_claim(void)
{
record_android_capture_request_t *r = new_request(0);
io_state = RECORD_MC_DISPATCH_READY;
gpu_claimed = true;
pool_allowed = false;
assert(record_android_capture_request_post(
r, RECORD_ANDROID_CAPTURE_CREATE, 0));
flush(false, true);
assert(record_android_capture_request_post(
r, RECORD_ANDROID_CAPTURE_COLLECT, 40));
flush(false, true);
assert(!record_android_capture_request_free(r));
assert(gpu_claimed && !gpu_done && !gpu_released);
/* Only the unrelated I/O owner completes its epoch; this request cannot. */
io_state = RECORD_MC_DISPATCH_DONE;
gpu_done = gpu_released = true;
gpu_end = 77;
assert(record_android_capture_request_free(r));
assert(gpu_claimed && gpu_end == 77);
}
static void quiesce_for_drain(void)
{
record_android_capture_request_t *r = new_request(0);
io_state = RECORD_MC_DISPATCH_READY;
ready = true;
assert(record_android_capture_request_post(
r, RECORD_ANDROID_CAPTURE_CREATE, 0));
flush(false, true);
finish_allowed = false;
in_flight = true;
assert(!record_android_capture_request_post(
r, RECORD_ANDROID_CAPTURE_QUIESCE_DEVICE, 40));
in_flight = false;
assert(record_android_capture_request_post(
r, RECORD_ANDROID_CAPTURE_QUIESCE_DEVICE, 40));
flush(false, true);
assert(record_android_capture_request_result(r) == 1);
assert(pool.live && !pool.prepared && !pool.dead && !pool.closed);
assert(
!record_android_capture_request_free(r) && !pool_steps && !pool_frees);
/* Only modeled common teardown's actual drain can now prepare children. */
pool.prepared = pool.dead = true;
finish_allowed = true;
reads_retired = true;
device.device = (void *)(uintptr_t)222;
assert(record_android_capture_request_post(
r, RECORD_ANDROID_CAPTURE_COLLECT, 40));
flush(false, true);
assert(record_android_capture_request_free(r));
}
static void already_closed(void)
{
record_android_capture_request_t *r = new_request(0);
io_state = RECORD_MC_DISPATCH_READY;
ready = true;
assert(record_android_capture_request_post(
r, RECORD_ANDROID_CAPTURE_CREATE, 0));
flush(false, true);
pool.closed = true;
finish_allowed = false;
reads_retired = true;
assert(record_android_capture_request_post(
r, RECORD_ANDROID_CAPTURE_COLLECT, 40));
flush(false, true);
assert(record_android_capture_request_result(r) == 0 && pool.live);
io_cancelled = true;
assert(record_android_capture_request_post(
r, RECORD_ANDROID_CAPTURE_COLLECT, 40));
flush(false, true);
assert(record_android_capture_request_result(r) == 1);
assert(!pool.live && record_android_capture_request_free(r));
}
int main(void)
{
busy_queue();
quiesce_for_drain();
already_closed();
unrelated_gpu_claim();
post_refusal();
inline_owner();
unavailable_queue();
normal(0);
normal(1);
normal(2);
teardown(false);
teardown(true);
prepare();
startup_fault(false);
startup_fault(true);
assert(record_android_capture_request_free(NULL));
assert(record_android_capture_request_result(NULL) == -1);
puts("PASS asynchronous capture requests: main/render handoff, startup, "
"queued "
"bindings, closure, existing queue lock and independent retirement; "
"native owners mocked");
return 0;
}
+108 -5
View File
@@ -27,24 +27,71 @@ flags += shlex.split(os.environ.get("CFLAGS", ""))
if os.environ.get("SANITIZER"):
flags += ["-fsanitize=" + os.environ["SANITIZER"], "-fno-omit-frame-pointer"]
with tempfile.TemporaryDirectory(prefix="mediacodec-encode-") as directory:
# Compile the actual shipping hook and ownership functions without unrelated
# legacy audio functions, which have different historical stack budgets.
shipping_audio = (root / "audio/audio_driver.c").read_text(encoding="utf-8")
audio_fragment = []
for signature in ("static INLINE int32_t audio_float_to_s16_sat(",
"static float audio_driver_pause_read(",
"static void audio_driver_pause_write(",
"static void audio_driver_pause_track(",
"void audio_driver_state_lock(",
"void audio_driver_state_unlock(",
"int audio_driver_record_tap_install(",
"bool audio_driver_record_tap_remove("):
start = shipping_audio.index(signature)
end = shipping_audio.index("\n}\n", start) + 3
audio_fragment.append(shipping_audio[start:end])
(Path(directory) / "audio_tap_fragment.c").write_text("\n".join(audio_fragment))
binary = Path(directory) / ("encoder_test.exe" if os.name == "nt" else "encoder_test")
for source, extra in (("test.c", []), ("audio_test.c", []),
("mux_test.c", ["-DRECORD_MEDIACODEC_MUX_TEST"]),
("worker_test.c", []), ("surface_test.c", []),
("worker_test.c", []), ("calibrated_worker_test.c", []),
("surface_test.c", []),
("capture_images_test.c", ["-DRECORD_ANDROID_CAPTURE_TEST"]),
("capture_submit_test.c", []), ("capture_gl_test.c", []),
("capture_submit_test.c", []), ("capture_pipe_test.c", []),
("capture_pool_test.c", []), ("device_test.c", []), ("request_test.c", []), ("config_test.c", []), ("audio_tap_test.c", []), ("audio_tap_driver_test.c", []), ("audio_tap_driver_test.c", ["-DHAVE_THREADS"]),
("frontend_test.c", []), ("frontend_test.c", ["-DHAVE_WEBMPLAYER"]), ("capture_ops_test.c", []),
("capture_egl_test.c", []),
("capture_gl_test.c", []),
("draw_test.c", []), ("gpu_worker_test.c", []),
("file_test.c", ["-DRECORD_MEDIACODEC_FILE_TEST"])):
("file_test.c", ["-DRECORD_MEDIACODEC_FILE_TEST"]),
("file_edit_test.c", ["-DRECORD_MEDIACODEC_FILE_TEST"]),
("session_test.c", []), ("dispatch_test.c", []),
("gpu_dispatch_test.c", [])):
command = compiler + flags + extra + [str(here / source), "-o", str(binary)]
if source == "worker_test.c":
if source in ("worker_test.c", "calibrated_worker_test.c"):
command += ["-DHAVE_THREADS",
str(root / "record/record_mediacodec_worker.c"),
str(root / "record/record_audio_queue.c"),
str(root / "libretro-common/queues/retro_spsc.c")]
if source == "worker_test.c":
command += [str(root / "record/record_aac_probe.c")]
if source == "session_test.c":
command += ["-DHAVE_THREADS",
str(root / "record/record_audio_queue.c"),
str(root / "libretro-common/queues/retro_spsc.c")]
if os.name != "nt":
command += ["-pthread"]
if source == "dispatch_test.c" and os.name != "nt":
command += ["-DHAVE_THREADS", "-DRECORD_MEDIACODEC_DISPATCH_TEST", "-pthread"]
if source == "gpu_dispatch_test.c":
command += ["-DUSE_SYSTEM_EGL", "-I" + str(root / "gfx/include/ANGLE")]
if os.name != "nt":
command += ["-DHAVE_THREADS", "-DRECORD_MEDIACODEC_GPU_DISPATCH_TEST", "-pthread",
str(root / "record/record_gpu_queue.c"),
str(root / "libretro-common/queues/retro_spsc.c")]
if source in ("capture_ops_test.c", "capture_egl_test.c"):
command += ["-DUSE_SYSTEM_EGL", "-I" + str(root / "gfx/include/ANGLE"),
str(root / "record/record_android_capture_ops.c")]
if source == "surface_test.c":
command += ["-DUSE_SYSTEM_EGL", "-I" + str(root / "gfx/include/ANGLE"),
str(root / "record/record_mediacodec_surface.c")]
if source == "file_test.c" and os.name != "nt":
if source == "file_edit_test.c":
command += [str(root / "record/record_mediacodec_file.c"),
str(root / "record/record_mediacodec_file_edit.c"),
str(root / "record/record_mp4_audio_edit.c")]
if source in ("file_test.c", "file_edit_test.c") and os.name != "nt":
command += ["-D_POSIX_C_SOURCE=200809L"]
if source == "gpu_worker_test.c":
command += ["-DHAVE_THREADS", "-DUSE_SYSTEM_EGL",
@@ -61,12 +108,68 @@ with tempfile.TemporaryDirectory(prefix="mediacodec-encode-") as directory:
if source == "capture_images_test.c":
command += ["-I" + str(root / "gfx/include"),
str(root / "record/record_android_capture_images.c")]
if source in ("capture_pipe_test.c", "capture_pool_test.c"):
command += ["-DUSE_SYSTEM_EGL", "-I" + str(root / "gfx/include"),
"-I" + str(root / "gfx/include/ANGLE")]
if source == "capture_submit_test.c":
command += ["-I" + str(root / "gfx/include"),
"-DHAVE_THREADS", "-DUSE_SYSTEM_EGL",
"-I" + str(root / "gfx/include/ANGLE"),
str(root / "record/record_android_capture_submit.c"),
str(root / "record/record_android_capture_pipe.c"),
str(root / "record/record_gpu_queue.c"),
str(root / "libretro-common/queues/retro_spsc.c"),
str(root / "gfx/common/vulkan_android_queue.c")]
if source == "audio_tap_test.c":
command += [str(root / "libretro-common/audio/conversion/float_to_s16.c")]
if source == "audio_tap_driver_test.c":
command += ["-I" + directory, "-DRECORD_AUDIO_TAP_TEST", "-D_GNU_SOURCE",
"-ffunction-sections", "-fdata-sections", "-Wl,--gc-sections",
"-Wno-unused-function", "-Wno-unused-parameter", "-Wno-unused-variable",
str(root / "record/record_audio_tap.c"),
str(root / "libretro-common/audio/conversion/float_to_s16.c"),
str(root / "libretro-common/rthreads/rthreads.c"),
str(root / "libretro-common/rthreads/retro_eventcount.c"), "-lm"]
if os.name != "nt":
command += ["-pthread"]
if source == "frontend_test.c":
command += ["-DHAVE_THREADS", "-DHAVE_CONFIGFILE", "-DRECORD_MEDIACODEC_FRONTEND_TEST",
"-DUSE_SYSTEM_EGL", "-I" + str(root / "gfx/include"),
"-I" + str(root / "gfx/include/ANGLE"), "-lm",
str(root / "record/record_audio_tap.c"),
str(root / "libretro-common/audio/conversion/float_to_s16.c")]
if source == "config_test.c":
command += ["-DRECORD_ANDROID_CAPTURE_CONFIG_TEST", "-DUSE_SYSTEM_EGL",
"-I" + str(root / "gfx/include"), "-I" + str(root / "gfx/include/ANGLE")]
if source == "request_test.c":
command += ["-DHAVE_THREADS", "-DUSE_SYSTEM_EGL", "-I" + str(root / "gfx/include"),
"-I" + str(root / "gfx/include/ANGLE")]
if source == "device_test.c":
shipping = (root / "gfx/common/vulkan_common.c").read_text(encoding="utf-8")
start = shipping.index(" /* Retire capture children")
prepare = shipping[start:shipping.index("\n#endif", start)]
start = shipping.index(" if (record_android_capture_device_pending")
pending = shipping[start:shipping.index("\n#endif", start)]
start = shipping.index(" vkDestroyDevice(vk->context.device, NULL);")
end = shipping.index("\n#endif", start)
destroyed = shipping[start:end + len("\n#endif")]
assert prepare.index("record_android_capture_device_prepare") < prepare.index("return;")
assert shipping.index(prepare) < shipping.index(" vulkan_destroy_swapchain(vk);", shipping.index(prepare))
unit = "static void teardown(VkDevice device, VkResult recording_idle, unsigned video_st_flags)\n{\n"
unit += "struct test_context storage; struct test_context *vk = &storage;\n"
unit += "vk->context.device = device;\n" + prepare + "\n" + pending
unit += "\nif (video_st_flags & VIDEO_FLAG_CACHE_CONTEXT) cached++; else {\n"
unit += destroyed + "\n}\n}\n"
(Path(directory) / "device_teardown_fixture.h").write_text(unit)
command += ["-DANDROID", "-DHAVE_THREADS", "-I" + str(root / "gfx/include"),
"-I" + directory]
print("+", shlex.join(command), flush=True)
subprocess.run(command, check=True)
if source == "file_edit_test.c":
subprocess.run([sys.executable,
str(here.parent / "mp4_audio_edit/run.py"),
str(binary)], check=True, timeout=30)
continue
arguments = [str(binary)] + ([directory] if source == "file_test.c" else [])
subprocess.run(arguments, check=True, timeout=30)
if source == "file_test.c":
@@ -0,0 +1,396 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier:
* GPL-3.0-or-later */
#ifdef NDEBUG
#error Session ownership acceptance requires assertions
#endif
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#ifndef _WIN32
#include <pthread.h>
#endif
#include "../../../record/record_session_mediacodec.h"
#include "../../../record/record_mediacodec_file.h"
struct record_mediacodec_encoder
{
int live;
};
struct record_mux_mediacodec
{
int live;
};
struct record_mediacodec_file
{
int live;
};
struct record_mediacodec_worker
{
record_audio_queue_t *pcm;
int live, closing;
};
struct record_aac_probe
{
int live;
};
static struct record_mediacodec_encoder video, audio;
static struct record_mux_mediacodec mux;
static struct record_mediacodec_file file;
static struct record_mediacodec_worker worker;
static struct record_aac_probe probe;
static int fault, stops, completes, closes, aborts, finishes, reads_retired;
static int output_error, stop_error, file_error;
static int trim_error, edit_error;
static const int16_t pcm[4] = {1, 2, 3, 4};
static void *session_allocate(size_t count, size_t bytes)
{
return fault == 1 ? NULL : calloc(count, bytes);
}
#define calloc session_allocate
#include "../../../record/record_session_mediacodec.c"
#undef calloc
record_aac_probe_t *record_aac_probe_new(unsigned rate, unsigned channels)
{
assert(rate == 48000 && channels == 2 && !probe.live);
if (fault == 7)
return NULL;
probe.live = 1;
return &probe;
}
void record_aac_probe_free(record_aac_probe_t *p)
{
if (!p)
return;
assert(p == &probe && p->live && !worker.live);
p->live = 0;
}
bool record_mediacodec_worker_calibrate(record_mediacodec_worker_t *w,
record_aac_probe_t *p)
{
assert(w == &worker && w->live && p == &probe && p->live);
return fault != 8;
}
bool record_mediacodec_worker_audio_edit(const record_mediacodec_worker_t *w,
uint64_t *skip, uint64_t *frames, unsigned *roll)
{
assert(w == &worker && w->live && probe.live);
*skip = 10240;
*frames = 7;
*roll = 2;
return !trim_error;
}
bool record_mediacodec_file_audio_edit(record_mediacodec_file_t *f,
uint64_t skip, uint64_t frames, unsigned roll, int *error)
{
assert(f == &file && f->live && !worker.live && !mux.live && !probe.live);
assert(skip == 10240 && frames == 7 && roll == 2);
*error = edit_error;
return !edit_error;
}
record_mediacodec_encoder_t *record_mediacodec_encoder_new(JNIEnv *env,
const mediacodec_encoder_request_t *request, unsigned interval)
{
(void)env;
assert(request && interval == 2 && !video.live);
if (fault == 2)
return NULL;
video.live = 1;
return &video;
}
record_mediacodec_encoder_t *record_mediacodec_encoder_audio_new(
unsigned rate, unsigned channels, unsigned bitrate)
{
assert(rate == 48000 && channels == 2 && bitrate == 128000 && !audio.live);
if (fault == 3)
return NULL;
audio.live = 1;
return &audio;
}
void *record_mediacodec_encoder_surface(record_mediacodec_encoder_t *e)
{
assert(e == &video && e->live);
return e;
}
bool record_mediacodec_encoder_stop(record_mediacodec_encoder_t *e)
{
assert(e && e->live && !worker.live);
stops++;
return !stop_error;
}
void record_mediacodec_encoder_free(record_mediacodec_encoder_t *e)
{
if (e)
{
assert(e->live && !worker.live && !mux.live && !file.live);
e->live = 0;
}
}
record_mediacodec_file_t *record_mediacodec_file_new(
const char *path, int *error)
{
assert(path && !file.live && error && video.live);
if (fault == 4)
return NULL;
file.live = 1;
return &file;
}
int record_mediacodec_file_descriptor(const record_mediacodec_file_t *f)
{
assert(f == &file && f->live);
return 42;
}
bool record_mediacodec_file_close(
record_mediacodec_file_t *f, bool complete, int *error)
{
assert(!worker.live && !mux.live);
*error = 0;
if (!f)
return true;
assert(f == &file && f->live);
closes++;
completes += complete;
file.live = 0;
*error = file_error;
return !file_error;
}
record_mux_mediacodec_t *record_mux_mediacodec_new(int fd, bool has_audio)
{
assert(fd == 42 && file.live && !mux.live && has_audio == !!audio.live);
if (fault == 5)
return NULL;
mux.live = 1;
return &mux;
}
void record_mux_mediacodec_free(record_mux_mediacodec_t *m)
{
assert(!worker.live);
if (m)
{
assert(m == &mux && m->live && file.live);
m->live = 0;
}
}
record_mediacodec_worker_t *record_mediacodec_worker_new(
record_mediacodec_encoder_t *v, record_mediacodec_encoder_t *a,
record_audio_queue_t *q, record_mux_mediacodec_t *m, unsigned channels,
unsigned rate)
{
assert(v == &video && m == &mux && m->live && !worker.live);
assert((a && q && channels == 2 && rate == 48000)
|| (!a && !q && !channels && !rate));
if (fault == 6)
return NULL;
worker.live = 1;
worker.closing = 0;
worker.pcm = q;
return &worker;
}
bool record_mediacodec_worker_finish(
record_mediacodec_worker_t *w, int64_t video_end, uint64_t audio_end)
{
assert(w == &worker && w->live && reads_retired);
finishes++;
if (!video_end || (w->pcm && audio_end != 7))
return false;
w->closing = 1;
return true;
}
int record_mediacodec_worker_step(record_mediacodec_worker_t *w)
{
const int16_t *samples;
size_t frames;
uint64_t timestamp;
int result = RECORD_AUDIO_END;
assert(w == &worker && w->live);
if (w->pcm)
result = record_audio_queue_peek(w->pcm, &samples, &frames, &timestamp);
if (output_error)
return RECORD_MC_WORKER_ERROR;
if (result == RECORD_AUDIO_OK)
{
assert(frames == 2 && samples[0] == 1 && samples[3] == 4);
assert(timestamp == 0 || timestamp == 5);
assert(record_audio_queue_ack(w->pcm));
return RECORD_MC_WORKER_PROGRESS;
}
return w->closing && result == RECORD_AUDIO_END ? RECORD_MC_WORKER_END
: RECORD_MC_WORKER_BUSY;
}
void record_mediacodec_worker_abort(record_mediacodec_worker_t *w)
{
assert(w == &worker && w->live);
aborts++;
}
bool record_mediacodec_worker_free(record_mediacodec_worker_t *w)
{
assert(w == &worker && w->live);
w->live = 0;
return true;
}
static record_session_mediacodec_t *open_session(bool has_audio)
{
mediacodec_encoder_request_t request = {0};
return record_session_mediacodec_new(NULL, &request, 2,
has_audio ? 48000 : 0, has_audio ? 2 : 0, has_audio ? 128000 : 0,
has_audio ? 2 : 0, has_audio ? 256 : 0, "record.mp4");
}
static void balanced(void)
{
assert(
!video.live && !audio.live && !worker.live && !mux.live && !file.live
&& !probe.live);
}
#ifndef _WIN32
static void *audio_producer(void *data)
{
record_session_mediacodec_t *s = (record_session_mediacodec_t *)data;
assert(record_session_mediacodec_audio(s, pcm, 2, 0) == RECORD_AUDIO_OK);
assert(record_session_mediacodec_audio(s, pcm, 2, 5) == RECORD_AUDIO_OK);
assert(record_session_mediacodec_audio_finish(s, 7));
return NULL;
}
static void *gpu_executor(void *data)
{
record_session_mediacodec_t *s = (record_session_mediacodec_t *)data;
assert(record_session_mediacodec_gpu_retired(s, 100000));
assert(record_session_mediacodec_gpu_released(s));
return NULL;
}
static void concurrent_sessions(void)
{
pthread_t producer, gpu;
record_session_mediacodec_t *s;
int cycle, at, result;
for (cycle = 0; cycle < 64; cycle++)
{
s = open_session(true);
assert(s);
assert(!pthread_create(&producer, NULL, audio_producer, s));
assert(!pthread_create(&gpu, NULL, gpu_executor, s));
for (at = 0; at < 10; at++)
assert(record_session_mediacodec_step(s) != RECORD_MC_WORKER_ERROR);
assert(!pthread_join(producer, NULL));
assert(!pthread_join(gpu, NULL));
result = RECORD_MC_WORKER_BUSY;
for (at = 0; at < 10 && result != RECORD_MC_WORKER_END; at++)
result = record_session_mediacodec_step(s);
assert(result == RECORD_MC_WORKER_END);
assert(record_session_mediacodec_free(s));
balanced();
}
}
#endif
int main(void)
{
mediacodec_encoder_request_t request = {0};
record_session_mediacodec_t *s;
int i, before, saved;
for (fault = 1; fault <= 8; fault++)
{
assert(!open_session(true));
balanced();
}
fault = 0;
assert(!record_session_mediacodec_new(
NULL, &request, 2, 48000, 2, 128000, 2, 1, "record.mp4"));
balanced();
assert(!record_session_mediacodec_new(
NULL, &request, 2, 0, 2, 0, 0, 0, "record.mp4"));
balanced();
s = open_session(true);
assert(s && record_session_mediacodec_surface(s) == &video);
assert(!record_session_mediacodec_free(s));
assert(!record_session_mediacodec_gpu_released(s));
assert(record_session_mediacodec_audio(s, pcm, 2, 0) == RECORD_AUDIO_OK);
assert(record_session_mediacodec_audio(s, pcm, 2, 5) == RECORD_AUDIO_OK);
assert(record_session_mediacodec_step(s) == RECORD_MC_WORKER_PROGRESS);
assert(record_session_mediacodec_audio_finish(s, 7));
assert(record_session_mediacodec_audio_finish(s, 7));
assert(!record_session_mediacodec_audio_finish(s, 8));
assert(record_session_mediacodec_step(s) == RECORD_MC_WORKER_PROGRESS);
assert(!finishes);
reads_retired = 1;
assert(record_session_mediacodec_gpu_retired(s, 100000));
assert(record_session_mediacodec_gpu_retired(s, 100000));
assert(!record_session_mediacodec_gpu_retired(s, 100001));
assert(record_session_mediacodec_step(s) == RECORD_MC_WORKER_BUSY);
assert(finishes == 1 && completes == 1 && video.live && audio.live);
assert(!record_session_mediacodec_free(s));
assert(record_session_mediacodec_gpu_released(s));
assert(record_session_mediacodec_step(s) == RECORD_MC_WORKER_END);
assert(record_session_mediacodec_step(s) == RECORD_MC_WORKER_END);
assert(record_session_mediacodec_free(s));
balanced();
for (i = 0; i < 4; i++)
{
s = open_session(true);
assert(s
&& record_session_mediacodec_audio(s, pcm, 2, 0)
== RECORD_AUDIO_OK);
output_error = i == 0;
stop_error = i == 2;
file_error = i == 3 ? 5 : 0;
if (!output_error)
record_session_mediacodec_cancel(s);
before = stops;
assert(record_session_mediacodec_step(s) == RECORD_MC_WORKER_PROGRESS);
assert(stops == before + 2 && record_session_mediacodec_cancelled(s));
assert(record_session_mediacodec_audio(s, pcm, 2, 5)
== RECORD_AUDIO_ERROR);
assert(record_session_mediacodec_step(s) == RECORD_MC_WORKER_BUSY);
assert(video.live && audio.live && !record_session_mediacodec_free(s));
assert(record_session_mediacodec_audio_finish(s, 7));
assert(record_session_mediacodec_step(s) == RECORD_MC_WORKER_BUSY);
assert(record_session_mediacodec_gpu_retired(s, 0));
assert(record_session_mediacodec_step(s) == RECORD_MC_WORKER_BUSY);
assert(record_session_mediacodec_gpu_released(s));
assert(record_session_mediacodec_step(s) == RECORD_MC_WORKER_ERROR);
assert(record_session_mediacodec_step(s) == RECORD_MC_WORKER_ERROR);
assert(stops == before + 2);
assert(record_session_mediacodec_error(s, &saved)
== (output_error ? RECORD_MC_SESSION_OUTPUT
: RECORD_MC_SESSION_CANCELLED));
assert(saved == file_error);
assert(record_session_mediacodec_free(s));
balanced();
output_error = stop_error = file_error = 0;
}
s = open_session(false);
assert(s && record_session_mediacodec_audio_finish(s, 0));
assert(!record_session_mediacodec_audio_finish(s, 1));
assert(record_session_mediacodec_gpu_retired(s, 100000));
assert(record_session_mediacodec_gpu_released(s));
file_error = 5;
assert(record_session_mediacodec_step(s) == RECORD_MC_WORKER_ERROR);
assert(record_session_mediacodec_error(s, &saved) == RECORD_MC_SESSION_FILE
&& saved == 5);
assert(record_session_mediacodec_free(s));
balanced();
file_error = 0;
for (i = 0; i < 2; i++)
{
s = open_session(true);
assert(s && record_session_mediacodec_audio_finish(s, 7));
assert(record_session_mediacodec_gpu_retired(s, 100000));
assert(record_session_mediacodec_gpu_released(s));
trim_error = !i;
edit_error = i ? 5 : 0;
assert(record_session_mediacodec_step(s) == RECORD_MC_WORKER_ERROR);
assert(record_session_mediacodec_error(s, &saved)
== (i ? RECORD_MC_SESSION_FILE : RECORD_MC_SESSION_OUTPUT));
assert(saved == edit_error);
assert(record_session_mediacodec_free(s));
balanced();
trim_error = edit_error = 0;
}
#ifndef _WIN32
concurrent_sessions();
#endif
assert(record_session_mediacodec_free(NULL));
puts("PASS: recording session startup rollback, real PCM queue, source "
"ends, "
"independent GPU retirement, cancellation and ordered file/codec "
"release; native owners mocked");
return 0;
}
+36 -1
View File
@@ -15,6 +15,7 @@ static AMediaFormat config, output_formats[2];
static ANativeWindow window;
static int stage, lookup, missing, libraries, codecs, windows, formats, names;
static int output_mode, metadata, owned, output_calls, releases, finishes;
static int stops, stop_fault;
static unsigned serial;
static const char *test_mime = "video/hevc";
static unsigned hdr_mode, format_fault, normalized_profile;
@@ -72,7 +73,8 @@ static media_status_t start(AMediaCodec *c)
static media_status_t stop(AMediaCodec *c)
{
assert(c == &codec && windows == 1);
return 0;
stops++;
return stop_fault ? -99 : 0;
}
static media_status_t delete_codec(AMediaCodec *c)
{
@@ -495,6 +497,39 @@ int main(void)
}
test_mime = "video/hevc";
normalized_profile = hdr_mode = 0;
assert(!record_mediacodec_encoder_stop(NULL));
for (i = 0; i < 4; i++)
{
s = open_encoder();
assert(s);
format_ready(s);
output_mode = 1;
assert(record_mediacodec_encoder_pump(s, &packet) == RECORD_MC_PACKET);
if (i == 2)
{
stage = 13;
assert(!record_mediacodec_encoder_release(s));
stage = 0;
}
if (i == 3)
assert(record_mediacodec_encoder_finish(s));
before = stops;
stop_fault = i == 1;
assert(record_mediacodec_encoder_stop(s) == !stop_fault);
assert(stops == before + 1 && codecs == 1 && windows == 1
&& libraries == 2 && formats == 2);
assert(record_mediacodec_encoder_stop(s) == !stop_fault);
assert(!record_mediacodec_encoder_surface(s));
assert(!record_mediacodec_encoder_format(s));
assert(record_mediacodec_encoder_pump(s, &packet) == RECORD_MC_ERROR
&& !packet.data && !packet.size);
assert(!record_mediacodec_encoder_release(s));
assert(!record_mediacodec_encoder_finish(s));
record_mediacodec_encoder_free(s);
assert(stops == before + 1);
balanced();
stop_fault = 0;
}
puts("PASS: Surface encoder startup faults, borrowed packets, format "
"changes, "
"zero-timeout output, bounds, EOS and cleanup; native APIs mocked");
@@ -55,6 +55,15 @@ void *record_mediacodec_encoder_format(record_mediacodec_encoder_t *e)
assert(e->format);
return e;
}
bool record_mediacodec_encoder_audio_config(record_mediacodec_encoder_t *e,
const void **data, size_t *bytes)
{
(void)e;
(void)data;
(void)bytes;
assert(0);
return false;
}
bool record_mediacodec_encoder_release(record_mediacodec_encoder_t *e)
{
assert(e->borrowed);
+18
View File
@@ -0,0 +1,18 @@
CC ?= cc
CFLAGS += -O1 -g1 -std=c89 -pedantic -Wall -Wextra -Werror -Wno-long-long -Wframe-larger-than=2048 -I../../../libretro-common/include
ifneq ($(findstring clang,$(shell $(CC) --version 2>/dev/null)),)
CFLAGS += -Wno-c99-extensions
endif
ifneq ($(SANITIZER),)
CFLAGS += -fsanitize=$(SANITIZER) -fno-omit-frame-pointer
LDFLAGS += -fsanitize=$(SANITIZER)
endif
export CC CFLAGS LDFLAGS
all: audio_edit_test$(EXE)
audio_edit_test$(EXE): audio_edit_test.c ../../../record/record_mp4_audio_edit.c ../../../record/record_mp4_audio_edit.h
$(CC) $(CFLAGS) audio_edit_test.c ../../../record/record_mp4_audio_edit.c -o $@ $(LDFLAGS)
check: all
python3 run.py ./audio_edit_test$(EXE)
clean:
rm -f audio_edit_test audio_edit_test.exe
.PHONY: all check clean
@@ -0,0 +1,64 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier:
* GPL-3.0-or-later */
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "../../../record/record_mp4_audio_edit.h"
static unsigned char *read_file(const char *path, size_t *size)
{
FILE *file = fopen(path, "rb");
unsigned char *data;
long bytes;
assert(file && !fseek(file, 0, SEEK_END));
bytes = ftell(file);
assert(bytes > 0 && bytes <= 64 * 1024 * 1024 && !fseek(file, 0, SEEK_SET));
*size = (size_t)bytes;
data = (unsigned char *)malloc(*size);
assert(data && fread(data, 1, *size, file) == *size);
assert(!fclose(file));
return data;
}
int main(int argc, char **argv)
{
unsigned char *input, *expected, *output;
size_t bytes, expected_size, size, i;
unsigned frames;
assert(argc == 4);
frames = (unsigned)strtoul(argv[3], NULL, 10);
input = read_file(argv[1], &bytes);
expected = read_file(argv[2], &expected_size);
assert(record_mp4_audio_edit(input, bytes, 2048, frames, 2, NULL, 0, &size));
assert(size == expected_size);
output = (unsigned char *)malloc(size);
assert(output);
assert(record_mp4_audio_edit(input, bytes, 2048, frames, 2, output, size, &size));
assert(size == expected_size && !memcmp(output, expected, size));
for (i = 0; i < bytes; i++)
{
size = 1;
assert(!record_mp4_audio_edit(input, i, 2048, frames, 2, NULL, 0, &size));
assert(!size);
}
for (i = 0; i < expected_size; i++)
{
size = 1;
assert(!record_mp4_audio_edit(input, bytes, 2048, frames, 2, output, i, &size));
assert(!size);
}
assert(!record_mp4_audio_edit(input, bytes, 2048, frames, 2,
input, expected_size, &size));
assert(!record_mp4_audio_edit(input, bytes, 2048, frames, 2,
input + 1, expected_size, &size));
assert(!record_mp4_audio_edit(output, expected_size, 2048, frames, 2,
NULL, 0, &size));
assert(!record_mp4_audio_edit(input, bytes, 50176, 1, 2, NULL, 0, &size));
assert(!record_mp4_audio_edit(input, bytes, 2048, 48000, 0, NULL, 0, &size));
assert(!record_mp4_audio_edit(input, bytes, 2048, 48000, 32769, NULL, 0, &size));
free(output);
free(expected);
free(input);
puts("PASS: audio edit clock, roll groups, untouched bytes, bounds and alias admission");
return 0;
}
+56
View File
@@ -0,0 +1,56 @@
#!/usr/bin/env python3
"""Independent native-shaped MP4 metadata fixtures; no encoded media required."""
from pathlib import Path
import os
import shlex
import struct
import subprocess
import sys
import tempfile
def box(tag, data):
return struct.pack(">I4s", len(data) + 8, tag) + data
def fixture(version, empty=False):
def header(tag, scale, duration, track=False):
offset = (28 if track else 24) if version else (20 if track else 16)
width = 8 if version else 4
data = bytearray(offset + width + 16)
data[0] = version
if not track:
struct.pack_into(">I", data, offset - 4, scale)
data[offset:offset + width] = duration.to_bytes(width, "big")
return box(tag, data)
unknown = box(b"free", b"unchanged opaque metadata")
stsd = box(b"stsd", struct.pack(">II", 0, 1) + box(b"mp4a", bytes(28)))
stsz = box(b"stsz", struct.pack(">III", 0, 1, 49))
tables = stsd + stsz + unknown
handler = box(b"hdlr", bytes(8) + b"soun" + bytes(4))
mdhd = header(b"mdhd", 48000, 50176)
tkhd = header(b"tkhd", 0, 10454, True)
audio = box(b"trak", tkhd + box(b"mdia", mdhd + handler + box(b"minf", box(b"stbl", tables))))
video = box(b"trak", header(b"tkhd", 0, 10000, True) + box(b"mdia", box(b"hdlr", bytes(8) + b"vide" + bytes(4))))
source = box(b"moov", header(b"mvhd", 10000, 10454) + video + audio + unknown)
original_audio = audio
edit = box(b"edts", box(b"elst", struct.pack(">IIQqhh", 0x01000000, 1, 10000, 2048, 1, 0)))
groups = box(b"sgpd", struct.pack(">I4sIIh", 0x01000000, b"roll", 2, 1, -2))
groups += box(b"sbgp", struct.pack(">I4sIII", 0, b"roll", 1, 49, 1))
audio = box(b"trak", header(b"tkhd", 0, 10000, True) + edit + box(b"mdia", mdhd + handler + box(b"minf", box(b"stbl", tables + groups))))
if empty:
audio = original_audio[:4] + b"free" + original_audio[8:]
expected = box(b"moov", header(b"mvhd", 10000, 10000) + video + audio + unknown)
return source, expected
runner = shlex.split(os.environ.get("RUNNER", ""))
binary = str(Path(sys.argv[1]).resolve())
with tempfile.TemporaryDirectory(prefix="mp4-audio-edit-") as directory:
folder = Path(directory)
for version, empty in [(0, False), (1, False), (0, True), (1, True)]:
source, expected = fixture(version, empty)
paths = [folder / "source.bin", folder / "expected.bin"]
paths[0].write_bytes(source)
paths[1].write_bytes(expected)
args = list(map(str, paths))
if runner and "wine" in runner[0]:
args = [subprocess.check_output(["winepath", "-w", x], text=True).strip() for x in args]
subprocess.run(runner + [binary] + args + ["0" if empty else "48000"], check=True, timeout=30)