Compare commits

...
Author SHA1 Message Date
Codex 09b678a61f tests: gate native iOS recording on the production application link
After real SDK unit admission succeeds, build the existing iOS Release application without signing. Assert resolved Metal/AVFoundation/thread/configuration and RTMP/FLV flags, reject FFmpeg, retain Xcode diagnostics and inspect the linked executable's libraries. Use structured subprocess arguments for signing overrides. Keep application-link scope distinct from physical device acceptance.
2026-11-12 19:03:46 +01:00
Codex a31bdf288f record: wire eligible Apple SDKs to native recording and shared RTMP
Derive the native Metal backend from a shared SDK floor rather than a desktop-only target check. Add shared RTMP/FLV flags to the existing Apple network configuration and let griffin include the existing native implementation units. Extend the real SDK audit to compile the actual C frontend using automatic backend admission, with ARC kept out of C compilation.

This is an unlanded integration candidate. SDK compilation, full Apple application linking and physical-device recording/streaming acceptance are pending.
2026-11-12 19:03:46 +01:00
Codex 0e3284a884 record: connect native Metal session and dispatcher on mobile Apple SDKs
Compile the real session and dedicated worker on eligible iOS/tvOS SDKs and reject older runtimes before startup. Mobile uses verified hardware admission without assuming a desktop GPU registry ID. Preserve desktop enumeration and affinity selection. Expand the opt-in SDK audit to compile capture, session, dispatcher and muxer units, with explicit non-stub and native-slot assertions.

The SDK audit remains a pending gate. This candidate does not enable mobile application feature defines or claim native-device recording acceptance.
2026-11-12 19:03:46 +01:00
Codex 7f106a3736 record: admit advertised hardware encoders before iOS 17.4
Guard mobile hardware require/query keys at their actual SDK and runtime floor. On older supported mobile systems, retain an advertised hardware encoder ID matching the requested codec and verify the created session reports the same ID. Reject malformed, missing and software-only advertisements. Preserve desktop hardware requirement and query behavior.

Portable strict-C89 VideoToolbox fixture suite passes on native MinGW, including hardware-ID admission and retained-list ownership checks. Real iOS SDK compilation and physical-device acceptance remain separate gates; this does not yet enable the mobile recording frontend.
2026-11-12 19:03:46 +01:00
Codex c7dc2e57b2 tests: add real iOS SDK admission audit for native VT headers
Compile the production wrapper for arm64 iPhoneOS and simulator at iOS 13 and 17.4 deployment floors. Preserve compiler diagnostics and distinguish SDK acceptance from unavailable physical-device encoding tests. The opt-in workflow dispatch isolates this audit without disabling normal default coverage.
2026-11-12 19:03:45 +01:00
13 changed files with 507 additions and 28 deletions

No files matched your search

@@ -81,6 +81,10 @@ on:
- 'libretro-common/include/retro_atomic.h'
workflow_dispatch:
inputs:
ios_sdk_only:
description: 'Audit production VideoToolbox declarations against real iOS SDKs'
type: boolean
default: false
native_aac_only:
description: 'Run native AudioToolbox AAC acceptance without GPU/frontend jobs'
type: boolean
@@ -93,7 +97,7 @@ permissions:
contents: read
jobs:
stream-native:
if: ${{ !inputs.native_aac_only && !inputs.frontend_stream_only }}
if: ${{ !inputs.ios_sdk_only && !inputs.native_aac_only && !inputs.frontend_stream_only }}
runs-on: macos-latest
timeout-minutes: 10
steps:
@@ -152,7 +156,7 @@ jobs:
${{ runner.temp }}/vt-metal-dispatch-mrc
if-no-files-found: ignore
aac-native:
if: ${{ inputs.native_aac_only || !inputs.frontend_stream_only }}
if: ${{ !inputs.ios_sdk_only && (inputs.native_aac_only || !inputs.frontend_stream_only) }}
runs-on: macos-latest
timeout-minutes: 10
steps:
@@ -184,7 +188,7 @@ jobs:
path: ${{ runner.temp }}/audiotoolbox-src-evidence
if-no-files-found: ignore
encode:
if: ${{ !inputs.native_aac_only && !inputs.frontend_stream_only }}
if: ${{ !inputs.ios_sdk_only && !inputs.native_aac_only && !inputs.frontend_stream_only }}
runs-on: macos-latest
timeout-minutes: 20
steps:
@@ -285,7 +289,7 @@ jobs:
if-no-files-found: warn
frontend-build:
if: ${{ !inputs.native_aac_only && !inputs.frontend_stream_only }}
if: ${{ !inputs.ios_sdk_only && !inputs.native_aac_only && !inputs.frontend_stream_only }}
runs-on: macos-latest
timeout-minutes: 40
steps:
@@ -322,7 +326,7 @@ jobs:
if-no-files-found: warn
frontend-stream-build:
if: ${{ !inputs.native_aac_only }}
if: ${{ !inputs.ios_sdk_only && !inputs.native_aac_only }}
runs-on: macos-latest
timeout-minutes: 20
strategy:
@@ -372,9 +376,46 @@ jobs:
if-no-files-found: ignore
frontend-contracts:
if: ${{ !inputs.native_aac_only }}
if: ${{ !inputs.ios_sdk_only && !inputs.native_aac_only }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Source-clock capture and backpressure under sanitizers
run: make -C samples/record/metal_frontend check SANITIZER=address,undefined
ios-sdk:
if: ${{ inputs.ios_sdk_only }}
runs-on: macos-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v6
- name: Compile production VideoToolbox headers for iOS and simulator
run: python3 samples/record/videotoolbox_encode/ios_sdk_check.py --output-dir "${{ runner.temp }}/vt-ios-sdk"
- name: Save actual SDK diagnostics
if: ${{ always() }}
uses: actions/upload-artifact@v4
with:
name: vt-ios-sdk
path: ${{ runner.temp }}/vt-ios-sdk
if-no-files-found: error
ios-app:
if: ${{ inputs.ios_sdk_only }}
needs: ios-sdk
runs-on: macos-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v6
- name: Build and link actual iOS application with native recording
run: python3 samples/record/videotoolbox_encode/ios_app_check.py --output-dir "${{ runner.temp }}/vt-ios-app"
- name: Save full application build evidence
if: ${{ always() }}
uses: actions/upload-artifact@v4
with:
name: vt-ios-app
path: |
${{ runner.temp }}/vt-ios-app/*.json
${{ runner.temp }}/vt-ios-app/*.log
${{ runner.temp }}/vt-ios-app/*.txt
${{ runner.temp }}/vt-ios-app/build.xcresult
if-no-files-found: error
+38 -5
View File
@@ -9,6 +9,8 @@
#else
#define VT_ENCODE_AVAILABLE
#endif
#include "videotoolbox_encode_hardware.h"
typedef struct videotoolbox_encode_config
{
unsigned width, height, slots, bitrate, key_interval, timescale, bit_depth;
@@ -57,7 +59,15 @@ static INLINE VT_ENCODE_AVAILABLE videotoolbox_encode_t *videotoolbox_encode_new
videotoolbox_encode_t *encoder = NULL;
CFMutableDictionaryRef spec = NULL, attrs = NULL, props = NULL;
CFTypeRef hardware = NULL, selected_id = NULL;
CFStringRef admitted_id = NULL;
#ifdef VT_ENCODE_MOBILE
bool query_hardware = false;
#endif
OSStatus status = kVTParameterErr;
#ifdef VT_ENCODE_MOBILE
if (VT_ENCODE_HARDWARE_KEYS_AVAILABLE)
query_hardware = true;
#endif
if (error) *error = status;
if ((encoder_id && !CFStringGetLength(encoder_id)) || !config || !config->width || !config->height || (config->width & 1)
|| (config->height & 1) || config->width > 16384 || config->height > 16384
@@ -68,6 +78,20 @@ static INLINE VT_ENCODE_AVAILABLE videotoolbox_encode_t *videotoolbox_encode_new
|| (config->bit_depth != 8 && config->bit_depth != 10)
|| (config->bit_depth == 10 && !config->hevc)
|| (config->hdr && config->bit_depth != 10)) return NULL;
#ifdef VT_ENCODE_MOBILE
if (!query_hardware)
{
admitted_id = videotoolbox_encode_hardware_id(encoder_id,
config->hevc ? kCMVideoCodecType_HEVC : kCMVideoCodecType_H264);
if (!admitted_id)
{
if (error)
*error = kVTCouldNotFindVideoEncoderErr;
return NULL;
}
encoder_id = admitted_id;
}
#endif
encoder = (videotoolbox_encode_t*)calloc(1, sizeof(*encoder));
spec = CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
attrs = CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
@@ -76,7 +100,10 @@ static INLINE VT_ENCODE_AVAILABLE videotoolbox_encode_t *videotoolbox_encode_new
encoder->config = *config;
encoder->pool = videotoolbox_encode_slots_new(config->slots);
if (!encoder->pool) { status = kVTAllocationFailedErr; goto done; }
CFDictionarySetValue(spec, kVTVideoEncoderSpecification_RequireHardwareAcceleratedVideoEncoder, kCFBooleanTrue);
#ifdef VT_ENCODE_HAS_HARDWARE_KEYS
if (VT_ENCODE_HARDWARE_KEYS_AVAILABLE)
CFDictionarySetValue(spec, kVTVideoEncoderSpecification_RequireHardwareAcceleratedVideoEncoder, kCFBooleanTrue);
#endif
if (encoder_id) CFDictionarySetValue(spec,kVTVideoEncoderSpecification_EncoderID,encoder_id);
if (gpu_registry_id)
{
@@ -132,10 +159,15 @@ static INLINE VT_ENCODE_AVAILABLE videotoolbox_encode_t *videotoolbox_encode_new
if (status) goto done;
status = VTCompressionSessionPrepareToEncodeFrames(encoder->session);
if (status) goto done;
status = VTSessionCopyProperty(encoder->session,
kVTCompressionPropertyKey_UsingHardwareAcceleratedVideoEncoder, NULL, &hardware);
if (!status && (!hardware || CFGetTypeID(hardware) != CFBooleanGetTypeID()
|| !CFBooleanGetValue((CFBooleanRef)hardware))) status = kVTCouldNotFindVideoEncoderErr;
#ifdef VT_ENCODE_HAS_HARDWARE_KEYS
if (VT_ENCODE_HARDWARE_KEYS_AVAILABLE)
{
status = VTSessionCopyProperty(encoder->session,
kVTCompressionPropertyKey_UsingHardwareAcceleratedVideoEncoder, NULL, &hardware);
if (!status && (!hardware || CFGetTypeID(hardware) != CFBooleanGetTypeID()
|| !CFBooleanGetValue((CFBooleanRef)hardware))) status = kVTCouldNotFindVideoEncoderErr;
}
#endif
if (!status && encoder_id)
{
status=VTSessionCopyProperty(encoder->session,kVTCompressionPropertyKey_EncoderID,NULL,&selected_id);
@@ -143,6 +175,7 @@ static INLINE VT_ENCODE_AVAILABLE videotoolbox_encode_t *videotoolbox_encode_new
|| !CFEqual(selected_id,encoder_id))) status=kVTCouldNotFindVideoEncoderErr;
}
done:
if (admitted_id) CFRelease((CFTypeRef)admitted_id);
if (selected_id) CFRelease(selected_id);
if (hardware) CFRelease(hardware);
if (props) CFRelease(props);
+85
View File
@@ -0,0 +1,85 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier: GPL-3.0-or-later */
#ifndef RETROARCH_VIDEOTOOLBOX_ENCODE_HARDWARE_H
#define RETROARCH_VIDEOTOOLBOX_ENCODE_HARDWARE_H
#if defined(__APPLE__) && !defined(VIDEOTOOLBOX_ENCODE_TEST_TYPES)
#include <TargetConditionals.h>
#if TARGET_OS_IPHONE
#define VT_ENCODE_MOBILE
#endif
#endif
#if !defined(VT_ENCODE_MOBILE) \
|| (defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 170400) \
|| (defined(__TV_OS_VERSION_MAX_ALLOWED) && __TV_OS_VERSION_MAX_ALLOWED >= 170400) \
|| (defined(__VISION_OS_VERSION_MAX_ALLOWED) && __VISION_OS_VERSION_MAX_ALLOWED >= 10100)
#define VT_ENCODE_HAS_HARDWARE_KEYS
#endif
#if defined(VT_ENCODE_MOBILE) && defined(VT_ENCODE_HAS_HARDWARE_KEYS)
#define VT_ENCODE_HARDWARE_KEYS_AVAILABLE \
__builtin_available(iOS 17.4, tvOS 17.4, macCatalyst 17.4, visionOS 1.1, *)
#elif defined(VT_ENCODE_HAS_HARDWARE_KEYS)
#define VT_ENCODE_HARDWARE_KEYS_AVAILABLE true
#else
#define VT_ENCODE_HARDWARE_KEYS_AVAILABLE false
#endif
#if defined(VT_ENCODE_MOBILE) || defined(VIDEOTOOLBOX_ENCODE_HARDWARE_LIST_TEST)
/* Cold admission only. Return a retained, advertised hardware encoder ID;
* the caller must independently confirm the session selected that same ID. */
static INLINE CFStringRef videotoolbox_encode_hardware_id(
CFStringRef requested, uint32_t codec_type)
{
#if defined(VIDEOTOOLBOX_ENCODE_HARDWARE_LIST_TEST) \
|| (defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000) \
|| (defined(__TV_OS_VERSION_MAX_ALLOWED) && __TV_OS_VERSION_MAX_ALLOWED >= 130000) \
|| defined(__VISION_OS_VERSION_MAX_ALLOWED)
#ifndef VIDEOTOOLBOX_ENCODE_HARDWARE_LIST_TEST
if (__builtin_available(iOS 13.0, tvOS 13.0, *))
#endif
{
CFArrayRef list = NULL;
CFStringRef selected = NULL;
CFIndex i, count;
OSStatus status = VTCopyVideoEncoderList(NULL, &list);
if (status || !list)
{
if (list)
CFRelease(list);
return NULL;
}
count = CFArrayGetCount(list);
if (count >= 0 && count <= 4096)
for (i = 0; i < count; i++)
{
CFDictionaryRef entry = (CFDictionaryRef)CFArrayGetValueAtIndex(list, i);
CFTypeRef hardware, codec, identifier;
int64_t value = 0;
if (!entry || CFGetTypeID(entry) != CFDictionaryGetTypeID())
continue;
hardware = CFDictionaryGetValue(entry, kVTVideoEncoderList_IsHardwareAccelerated);
codec = CFDictionaryGetValue(entry, kVTVideoEncoderList_CodecType);
identifier = CFDictionaryGetValue(entry, kVTVideoEncoderList_EncoderID);
if (!hardware || CFGetTypeID(hardware) != CFBooleanGetTypeID()
|| !CFBooleanGetValue((CFBooleanRef)hardware)
|| !codec || CFGetTypeID(codec) != CFNumberGetTypeID()
|| !CFNumberGetValue((CFNumberRef)codec, kCFNumberSInt64Type, &value)
|| value != (int64_t)codec_type
|| !identifier || CFGetTypeID(identifier) != CFStringGetTypeID()
|| !CFStringGetLength((CFStringRef)identifier)
|| (requested && !CFEqual(identifier, requested)))
continue;
selected = (CFStringRef)CFRetain(identifier);
break;
}
CFRelease(list);
return selected;
}
#endif
(void)requested;
(void)codec_type;
return NULL;
}
#endif
#endif
+1 -1
View File
@@ -7,7 +7,7 @@
// The cleanroom network stack, as on the other platforms: the crypto,
// the encrypted keychain, the TLS 1.2/1.3 client and the SMB2/3 client
// with Kerberos, plus the NFSv3/v4 client.
NETWORK_STACK_FLAGS = -DHAVE_CRYPTO -DHAVE_KEYCHAIN -DHAVE_RETROSSL -DHAVE_SMBCLIENT -DHAVE_RETROSMB -DHAVE_NFSCLIENT -DHAVE_RETRONFS
NETWORK_STACK_FLAGS = -DHAVE_RTMP -DHAVE_RFLV -DHAVE_CRYPTO -DHAVE_KEYCHAIN -DHAVE_RETROSSL -DHAVE_SMBCLIENT -DHAVE_RETROSMB -DHAVE_NFSCLIENT -DHAVE_RETRONFS
OTHER_CFLAGS = $(inherited) $(NETWORK_STACK_FLAGS)
+2 -2
View File
@@ -3,8 +3,8 @@
#define RECORD_METAL_H
#include "../record_driver.h"
#ifdef __APPLE__
#include <TargetConditionals.h>
#if TARGET_OS_OSX && defined(HAVE_METAL) && defined(HAVE_AVF) \
#include "../record_metal_platform.h"
#if defined(RECORD_METAL_NATIVE_SDK) && defined(HAVE_METAL) && defined(HAVE_AVF) \
&& defined(HAVE_THREADS) && defined(HAVE_CONFIGFILE)
#define HAVE_RECORD_METAL
#endif
+7 -1
View File
@@ -15,11 +15,12 @@
#undef RECORD_METAL_DISPATCH_UNDEF_INLINE
#endif
#include "record_gpu_dispatch_metal.h"
#include "record_metal_platform.h"
#include <retro_atomic.h>
#include <defines/cocoa_defines.h>
#include <stdlib.h>
#include <string.h>
#if TARGET_OS_OSX && defined(HAVE_THREADS) && defined(RETRO_ATOMIC_LOCK_FREE)
#if defined(RECORD_METAL_NATIVE_SDK) && defined(HAVE_THREADS) && defined(RETRO_ATOMIC_LOCK_FREE)
#include <pthread.h>
#if !defined(__cplusplus) && (!defined(__STDC_VERSION__) || __STDC_VERSION__<199901L) && !defined(inline)
#define inline __inline__
@@ -447,6 +448,11 @@ static record_gpu_dispatch_metal_t *record_metal_dispatch_new(void *device,
id<MTLDevice> metal=(__bridge id<MTLDevice>)device;
size_t length;
int error;
if (RECORD_METAL_NATIVE_RUNTIME)
{
}
else
return NULL;
if (!metal || !config || (!output && !stream)
|| (output && output->video_format) || !path || !*path) return NULL;
#ifndef RECORD_METAL_STREAM
+22 -10
View File
@@ -17,16 +17,22 @@
#endif
#include <string.h>
#include "record_gpu_session_metal.h"
#include "record_metal_platform.h"
#include "record_avfoundation_session_native.h"
#if defined(VIDEOTOOLBOX_ENCODE_SLOTS) && TARGET_OS_OSX \
&& defined(__MAC_OS_X_VERSION_MAX_ALLOWED) && __MAC_OS_X_VERSION_MAX_ALLOWED>=101400
#if defined(VIDEOTOOLBOX_ENCODE_SLOTS) && defined(RECORD_METAL_NATIVE_SDK)
/* Enumerated IDs prevent silent default/software/other-GPU selection. A
* hardware encoder with no advertised GPU is a separate explicit category,
* as used by the hosted paravirtualized media engines. It is not evidence of
* physical same-GPU encoding; CVPixelBuffer/Metal interop remains gated. */
static __attribute__((availability(macos,introduced=10.14))) videotoolbox_encode_t *record_metal_select_encoder(
static RECORD_METAL_NATIVE_AVAILABLE videotoolbox_encode_t *record_metal_select_encoder(
const videotoolbox_encode_config_t *config,uint64_t renderer_gpu,OSStatus *error)
{
#if TARGET_OS_IPHONE
/* Mobile admission verifies hardware and the selected ID in the VT adapter.
* CVPixelBuffer/Metal compatibility is still checked by capture creation. */
(void)renderer_gpu;
return videotoolbox_encode_new(config, error);
#else
CFArrayRef list=NULL;
CFIndex i,count;
unsigned priority;
@@ -62,6 +68,7 @@ static __attribute__((availability(macos,introduced=10.14))) videotoolbox_encode
}
done:
CFRelease(list); *error=status; return encoder;
#endif
}
#endif
record_avfoundation_session_t *record_gpu_session_metal_new_output(void *device,
@@ -73,8 +80,8 @@ record_avfoundation_session_t *record_gpu_session_metal_new_output(void *device,
if (queue_out) *queue_out=NULL;
if (stage) *stage=RECORD_METAL_STARTUP_UNSUPPORTED;
if (native_status) *native_status=0;
#if defined(VIDEOTOOLBOX_ENCODE_SLOTS) && TARGET_OS_OSX \
&& defined(__MAC_OS_X_VERSION_MAX_ALLOWED) && __MAC_OS_X_VERSION_MAX_ALLOWED>=101400
#if defined(VIDEOTOOLBOX_ENCODE_SLOTS) && defined(RECORD_METAL_NATIVE_SDK)
if (RECORD_METAL_NATIVE_RUNTIME)
@autoreleasepool
{
videotoolbox_encode_config_t config;
@@ -94,8 +101,12 @@ record_avfoundation_session_t *record_gpu_session_metal_new_output(void *device,
|| !c->width || !c->height || c->width>16384 || c->height>16384
|| ((c->width|c->height)&1) || c->slots<2 || c->slots>VIDEOTOOLBOX_ENCODE_SLOTS
|| c->mode>2 || (c->mode && c->bit_depth!=10)) goto done;
#if TARGET_OS_IPHONE
gpu = 0;
#else
gpu=[(__bridge id<MTLDevice>)device registryID];
if (!gpu) goto done;
#endif
config.width=c->width; config.height=c->height; config.slots=c->slots;
config.bitrate=c->bitrate; config.key_interval=c->key_interval; config.timescale=c->timescale;
config.bit_depth=c->bit_depth; config.hevc=c->hevc; config.hdr=c->mode!=0;
@@ -128,20 +139,20 @@ done:
if (native_status) *native_status=(int32_t)status;
return session;
}
return NULL;
#else
(void)device; (void)c; (void)bind; (void)context;
return NULL;
#endif
}
#if defined(VIDEOTOOLBOX_ENCODE_SLOTS) && TARGET_OS_OSX \
&& defined(__MAC_OS_X_VERSION_MAX_ALLOWED) && __MAC_OS_X_VERSION_MAX_ALLOWED>=101400
#if defined(VIDEOTOOLBOX_ENCODE_SLOTS) && defined(RECORD_METAL_NATIVE_SDK)
typedef struct record_metal_file_binding
{
const record_mux_avfoundation_config_t *output;
const char *path;
size_t audio_capacity;
} record_metal_file_binding_t;
static __attribute__((availability(macos,introduced=10.14))) record_avfoundation_session_t *record_metal_file_bind(
static RECORD_METAL_NATIVE_AVAILABLE record_avfoundation_session_t *record_metal_file_bind(
void *data,record_gpu_queue_t *queue,record_gpu_capture_metal_t *capture,
struct videotoolbox_encode *encoder,unsigned width,unsigned height)
{
@@ -159,8 +170,8 @@ record_avfoundation_session_t *record_gpu_session_metal_new(void *device,
if (queue_out) *queue_out=NULL;
if (stage) *stage=RECORD_METAL_STARTUP_UNSUPPORTED;
if (native_status) *native_status=0;
#if defined(VIDEOTOOLBOX_ENCODE_SLOTS) && TARGET_OS_OSX \
&& defined(__MAC_OS_X_VERSION_MAX_ALLOWED) && __MAC_OS_X_VERSION_MAX_ALLOWED>=101400
#if defined(VIDEOTOOLBOX_ENCODE_SLOTS) && defined(RECORD_METAL_NATIVE_SDK)
if (RECORD_METAL_NATIVE_RUNTIME)
{
record_metal_file_binding_t binding;
if (!c || !output || output->video_format || c->timescale!=output->timescale || !path || !*path)
@@ -172,6 +183,7 @@ record_avfoundation_session_t *record_gpu_session_metal_new(void *device,
return record_gpu_session_metal_new_output(device,c,record_metal_file_bind,&binding,
capture_out,queue_out,stage,native_status);
}
return NULL;
#else
(void)device; (void)c; (void)output; (void)audio_capacity_bytes; (void)path;
return NULL;
+21
View File
@@ -0,0 +1,21 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier: GPL-3.0-or-later */
#ifndef RECORD_METAL_PLATFORM_H
#define RECORD_METAL_PLATFORM_H
#include <TargetConditionals.h>
#include <Availability.h>
#if (TARGET_OS_OSX && defined(__MAC_OS_X_VERSION_MAX_ALLOWED) \
&& __MAC_OS_X_VERSION_MAX_ALLOWED >= 101400) \
|| (TARGET_OS_IPHONE && defined(__IPHONE_OS_VERSION_MAX_ALLOWED) \
&& __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000) \
|| (TARGET_OS_TV && defined(__TV_OS_VERSION_MAX_ALLOWED) \
&& __TV_OS_VERSION_MAX_ALLOWED >= 130000)
#define RECORD_METAL_NATIVE_SDK
#endif
#define RECORD_METAL_NATIVE_AVAILABLE \
__attribute__((availability(macos,introduced=10.14), \
availability(ios,introduced=13.0), availability(tvos,introduced=13.0)))
#define RECORD_METAL_NATIVE_RUNTIME \
__builtin_available(macOS 10.14, iOS 13.0, tvOS 13.0, *)
#endif
@@ -26,3 +26,6 @@
/session_gpu_refusal_test
/session_gpu_refusal_test.exe
/hardware_id_test
/hardware_id_test.exe
+6 -3
View File
@@ -9,7 +9,7 @@ ifneq ($(SANITIZER),)
CFLAGS += -fsanitize=$(SANITIZER) -fno-omit-frame-pointer
LDFLAGS += -fsanitize=$(SANITIZER)
endif
all: av_output_test$(EXE) audio_lend_test$(EXE) packet_test$(EXE) session_gpu_refusal_test$(EXE) av_session_test$(EXE) crop_test$(EXE) av_worker_test$(EXE) audio_queue_test$(EXE) slots_test$(EXE) session_test$(EXE) worker_test$(EXE) audio_rate_test$(EXE) audio_rate_native_test$(EXE)
all: hardware_id_test$(EXE) av_output_test$(EXE) audio_lend_test$(EXE) packet_test$(EXE) session_gpu_refusal_test$(EXE) av_session_test$(EXE) crop_test$(EXE) av_worker_test$(EXE) audio_queue_test$(EXE) slots_test$(EXE) session_test$(EXE) worker_test$(EXE) audio_rate_test$(EXE) audio_rate_native_test$(EXE)
av_output_test$(EXE): av_output_test.c av_worker_test.c $(RA)/record/record_avfoundation_session.c $(RA)/record/record_avfoundation_session.h
$(CC) $(CFLAGS) -Wno-unused-function -o $@ av_output_test.c $(RA)/record/record_avfoundation_session.c $(RA)/record/record_avfoundation_worker.c $(RA)/record/record_gpu_worker_vt.c $(RA)/record/record_gpu_queue.c $(RA)/record/record_audio_queue.c $(RA)/libretro-common/queues/retro_spsc.c $(LDFLAGS)
audio_lend_test$(EXE): audio_lend_test.c $(RA)/record/record_audio_queue.c $(RA)/record/record_audio_queue.h
@@ -34,9 +34,11 @@ crop_test$(EXE): crop_test.c $(RA)/libretro-common/formats/h265/rh265_sps_crop.c
$(CC) $(CFLAGS) -o $@ $< $(LDFLAGS)
worker_test$(EXE): worker_test.c $(RA)/record/record_gpu_worker_vt.c $(RA)/record/record_gpu_worker_vt.h $(RA)/record/record_gpu_queue.c
$(CC) $(CFLAGS) -o $@ worker_test.c $(RA)/record/record_gpu_worker_vt.c $(RA)/record/record_gpu_queue.c $(RA)/libretro-common/queues/retro_spsc.c $(LDFLAGS)
session_gpu_refusal_test$(EXE): session_test.c $(RA)/gfx/common/videotoolbox_encode.h
session_gpu_refusal_test$(EXE): session_test.c $(RA)/gfx/common/videotoolbox_encode.h $(RA)/gfx/common/videotoolbox_encode_hardware.h
$(CC) $(CFLAGS) -DVIDEOTOOLBOX_ENCODE_TEST_NO_GPU_SELECTION -o $@ $< $(LDFLAGS)
session_test$(EXE): session_test.c $(RA)/gfx/common/videotoolbox_encode.h $(RA)/gfx/common/videotoolbox_encode_slots.h
session_test$(EXE): session_test.c $(RA)/gfx/common/videotoolbox_encode.h $(RA)/gfx/common/videotoolbox_encode_slots.h $(RA)/gfx/common/videotoolbox_encode_hardware.h
$(CC) $(CFLAGS) -o $@ $< $(LDFLAGS)
hardware_id_test$(EXE): hardware_id_test.c $(RA)/gfx/common/videotoolbox_encode_hardware.h
$(CC) $(CFLAGS) -o $@ $< $(LDFLAGS)
slots_test$(EXE): slots_test.c $(RA)/gfx/common/videotoolbox_encode_slots.h
$(CC) $(CFLAGS) -o $@ $< $(LDFLAGS)
@@ -58,6 +60,7 @@ check: all legacy-check
$(RUNNER) ./worker_test$(EXE)
$(RUNNER) ./audio_rate_test$(EXE)
clean:
rm -f hardware_id_test hardware_id_test.exe
rm -f av_output_test av_output_test.exe
rm -f audio_lend_test audio_lend_test.exe
rm -f packet_test packet_test.exe
@@ -0,0 +1,149 @@
/* 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 <boolean.h>
#include <retro_inline.h>
typedef struct object object_t;
typedef object_t *CFTypeRef;
typedef object_t *CFArrayRef;
typedef object_t *CFDictionaryRef;
typedef object_t *CFStringRef;
typedef object_t *CFNumberRef;
typedef object_t *CFBooleanRef;
typedef long CFIndex;
typedef int OSStatus;
struct object
{
unsigned type, refs;
int64_t number;
const char *text;
object_t *values[3];
};
#define kVTVideoEncoderList_IsHardwareAccelerated 0
#define kVTVideoEncoderList_CodecType 1
#define kVTVideoEncoderList_EncoderID 2
#define kCFNumberSInt64Type 1
static object_t list, entry, hardware, codec, identifier, requested;
static CFIndex count = 1;
static OSStatus list_status;
static unsigned visits;
static CFTypeRef CFRetain(CFTypeRef object)
{
assert(object && object->refs);
object->refs++;
return object;
}
static void CFRelease(CFTypeRef object)
{
assert(object && object->refs > 1);
object->refs--;
}
static OSStatus VTCopyVideoEncoderList(void *options, CFArrayRef *output)
{
(void)options;
*output = CFRetain(&list);
return list_status;
}
static CFIndex CFArrayGetCount(CFArrayRef array)
{
assert(array == &list);
return count;
}
static CFTypeRef CFArrayGetValueAtIndex(CFArrayRef array, CFIndex index)
{
assert(array == &list && index == 0);
visits++;
return &entry;
}
static unsigned CFGetTypeID(CFTypeRef object)
{
return object->type;
}
static unsigned CFDictionaryGetTypeID(void)
{
return 1;
}
static unsigned CFBooleanGetTypeID(void)
{
return 2;
}
static unsigned CFNumberGetTypeID(void)
{
return 3;
}
static unsigned CFStringGetTypeID(void)
{
return 4;
}
static CFTypeRef CFDictionaryGetValue(CFDictionaryRef object, unsigned key)
{
assert(object == &entry && key < 3);
return object->values[key];
}
static bool CFBooleanGetValue(CFBooleanRef value)
{
return value->number != 0;
}
static bool CFNumberGetValue(CFNumberRef value, unsigned type, void *output)
{
assert(type == kCFNumberSInt64Type);
*(int64_t*)output = value->number;
return true;
}
static unsigned CFStringGetLength(CFStringRef value)
{
return (unsigned)strlen(value->text);
}
static bool CFEqual(CFTypeRef first, CFStringRef second)
{
return !strcmp(first->text, second->text);
}
#define VIDEOTOOLBOX_ENCODE_HARDWARE_LIST_TEST
#include "../../../gfx/common/videotoolbox_encode_hardware.h"
int main(void)
{
CFStringRef selected;
list.refs = entry.refs = hardware.refs = codec.refs = identifier.refs = requested.refs = 1;
entry.type = 1; hardware.type = 2; codec.type = 3; identifier.type = requested.type = 4;
hardware.number = 1; codec.number = 7;
identifier.text = requested.text = "hardware-encoder";
entry.values[0] = &hardware;
entry.values[1] = &codec;
entry.values[2] = &identifier;
selected = videotoolbox_encode_hardware_id(NULL, 7);
assert(selected == &identifier && identifier.refs == 2 && list.refs == 1);
CFRelease(selected);
selected = videotoolbox_encode_hardware_id(&requested, 7);
assert(selected == &identifier);
CFRelease(selected);
requested.text = "other-encoder";
assert(!videotoolbox_encode_hardware_id(&requested, 7));
assert(!videotoolbox_encode_hardware_id(NULL, 8));
hardware.number = 0;
assert(!videotoolbox_encode_hardware_id(NULL, 7));
hardware.number = 1;
hardware.type = 3;
assert(!videotoolbox_encode_hardware_id(NULL, 7));
hardware.type = 2;
identifier.text = "";
assert(!videotoolbox_encode_hardware_id(NULL, 7));
identifier.text = "hardware-encoder";
entry.values[2] = NULL;
assert(!videotoolbox_encode_hardware_id(NULL, 7));
entry.values[2] = &identifier;
list_status = -1;
assert(!videotoolbox_encode_hardware_id(NULL, 7) && list.refs == 1);
list_status = 0;
visits = 0;
count = 4097;
assert(!videotoolbox_encode_hardware_id(NULL, 7) && !visits);
count = -1;
assert(!videotoolbox_encode_hardware_id(NULL, 7) && !visits);
assert(identifier.refs == 1 && list.refs == 1);
puts("PASS advertised hardware encoder ID, codec admission, malformed lists and retained ownership");
return 0;
}
@@ -0,0 +1,59 @@
#!/usr/bin/env python3
"""Build and link the production iOS application; never report device acceptance."""
import argparse
import json
from pathlib import Path
import shlex
import subprocess
import sys
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--output-dir', type=Path, required=True)
args = parser.parse_args()
if sys.platform != 'darwin':
parser.error('full iOS application linking requires macOS/Xcode')
root = Path(__file__).resolve().parents[3]
output = args.output_dir.resolve()
output.mkdir(parents=True, exist_ok=True)
common = ['xcodebuild', '-project', 'pkg/apple/RetroArch_iOS13.xcodeproj',
'-scheme', 'RetroArch iOS Release', '-configuration', 'Release',
'-destination', 'generic/platform=iOS', '-xcconfig', 'pkg/apple/GitHubCI.xcconfig',
'-derivedDataPath', str(output / 'derived'),
'CODE_SIGNING_ALLOWED=NO', 'CODE_SIGNING_REQUIRED=NO',
'CODE_SIGN_IDENTITY=', 'PROVISIONING_PROFILE_SPECIFIER=']
settings = subprocess.run(common + ['-showBuildSettings', '-json'], cwd=root,
check=True, capture_output=True, text=True, timeout=180)
(output / 'build-settings.json').write_text(settings.stdout)
rows = json.loads(settings.stdout)
main_target = next(row['buildSettings'] for row in rows if row['target'] == 'RetroArchiOS')
flags = shlex.split(main_target['OTHER_CFLAGS'])
defines = {flag[2:].split('=', 1)[0] for flag in flags if flag.startswith('-D')}
required = {'HAVE_METAL', 'HAVE_AVF', 'HAVE_THREADS', 'HAVE_CONFIGFILE',
'HAVE_NETWORKING', 'HAVE_CRYPTO', 'HAVE_RTMP', 'HAVE_RFLV'}
assert required <= defines, sorted(required - defines)
assert 'HAVE_FFMPEG' not in defines
command = common + ['-resultBundlePath', str(output / 'build.xcresult'), 'build']
report = dict(command=command, required_defines=sorted(required),
scope='Full unsigned device application build/link only; no physical playback or recording acceptance',
result='FAIL')
(output / 'results.json').write_text(json.dumps(report, indent=2) + '\n')
with (output / 'xcodebuild.log').open('w') as log:
result = subprocess.run(command, cwd=root, stdout=log, stderr=subprocess.STDOUT, timeout=3000)
report['exit_code'] = result.returncode
if not result.returncode:
executable = output / 'derived/Build/Products/Release-iphoneos/RetroArch.app/RetroArch'
assert executable.is_file() and executable.stat().st_size > 0
linked = subprocess.run(['otool', '-L', str(executable)], check=True,
capture_output=True, text=True, timeout=60).stdout
(output / 'linked-libraries.txt').write_text(linked)
assert not any(name in linked.lower() for name in ('libavcodec', 'libavformat', 'libavutil', 'libswscale'))
report.update(result='PASS', executable_bytes=executable.stat().st_size)
(output / 'results.json').write_text(json.dumps(report, indent=2) + '\n')
print(report['result'], report['scope'], flush=True)
return int(bool(result.returncode))
if __name__ == '__main__':
sys.exit(main())
@@ -0,0 +1,67 @@
#!/usr/bin/env python3
"""Compile production VT headers against real iOS SDKs; no device acceptance claim."""
import argparse
import json
from pathlib import Path
import subprocess
import sys
import tempfile
def check(sdk, minimum, output):
root = Path(__file__).resolve().parents[3]
sdk_path = subprocess.check_output(['xcrun', '--sdk', sdk, '--show-sdk-path'], text=True).strip()
clang = subprocess.check_output(['xcrun', '--sdk', sdk, '--find', 'clang'], text=True).strip()
target = 'arm64-apple-ios' + minimum + ('-simulator' if sdk == 'iphonesimulator' else '')
with tempfile.TemporaryDirectory(prefix='vt-ios-sdk-') as tmp:
probe = Path(tmp) / 'probe.m'
probe.write_text('#include "gfx/common/videotoolbox_encode.h"\n'
'#include "record/drivers/record_metal.h"\n'
'#ifndef HAVE_RECORD_METAL\n#error native frontend unavailable\n#endif\n'
'#ifndef RECORD_METAL_NATIVE_SDK\n#error mobile recording compiled as a stub\n#endif\n'
'#ifndef VIDEOTOOLBOX_ENCODE_SLOTS\n#error native encoding slots unavailable\n#endif\n'
'VT_ENCODE_AVAILABLE void *record_vt_ios_sdk_probe(\n'
' const videotoolbox_encode_config_t *config, OSStatus *status)\n'
'{\n return videotoolbox_encode_new(config, status);\n}\n')
command = [clang, '-target', target, '-isysroot', sdk_path, '-std=gnu89',
'-DHAVE_THREADS', '-DHAVE_METAL', '-DHAVE_AVF', '-DHAVE_CONFIGFILE', '-DHAVE_NETWORKING',
'-DHAVE_RTMP', '-DHAVE_RFLV', '-DHAVE_CRYPTO', '-fblocks', '-fobjc-arc', '-I' + str(root), '-I' + str(root / 'libretro-common/include'),
'-Wall', '-Wextra', '-Werror', '-Wunguarded-availability-new',
'-Wno-unused-function', '-fsyntax-only', str(probe)]
units = [probe] + [root / ('record/' + name) for name in (
'record_gpu_capture_metal.m', 'record_gpu_session_metal.m',
'record_gpu_dispatch_metal.m', 'record_mux_avfoundation.m', 'drivers/record_metal.c')]
results = []
for unit in units:
invocation = command[:-1] + [str(unit)]
if unit.suffix == '.c':
invocation.remove('-fobjc-arc')
result = subprocess.run(invocation, text=True, capture_output=True, timeout=120)
name = sdk + '-' + minimum + '-' + unit.stem
(output / (name + '.log')).write_text(result.stdout + result.stderr)
results.append(dict(sdk=sdk, minimum=minimum, target=target, unit=unit.name,
command=invocation, result='PASS' if not result.returncode else 'FAIL',
exit_code=result.returncode))
return results
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--output-dir', type=Path, required=True)
args = parser.parse_args()
if sys.platform != 'darwin':
parser.error('real Apple SDK compilation requires macOS/Xcode; no local pass is reported')
args.output_dir.mkdir(parents=True, exist_ok=True)
results = [case for sdk in ('iphoneos', 'iphonesimulator')
for minimum in ('13.0', '17.4')
for case in check(sdk, minimum, args.output_dir)]
(args.output_dir / 'results.json').write_text(json.dumps(dict(
scope='SDK production unit compilation only; no full application link or device acceptance',
cases=results), indent=2) + '\n')
for result in results:
print(result['result'], result['target'], result['unit'], flush=True)
return int(any(result['exit_code'] for result in results))
if __name__ == '__main__':
sys.exit(main())