This commit is contained in:
libretroadmin committed 2026-09-19 10:45:34 +02:00
1 parent 38907d073f
commit e4529182fa
9 files changed
+502 -21

No files matched your search

+13 -2
View File
@@ -40,11 +40,16 @@
#endif
#if defined(__APPLE__)
#include <sys/ucontext.h>
#include <TargetConditionals.h>
/* tvOS marks the mach task API unavailable, and the SDK does not ship
* the headers for it. Nothing below needs them there. */
#if !defined(TARGET_OS_TV) || !TARGET_OS_TV
#include <mach/mach_init.h>
#include <mach/mach_port.h>
#include <mach/task.h>
#endif
#endif
#endif
/* The state, and why each piece is shaped as it is:
*
@@ -323,9 +328,15 @@ bool retro_faulthandler_install(retro_fault_handler_t handler)
if (ok && sigaction(SIGSEGV, &sa, &s_old_sigsegv) != 0)
ok = false;
#endif
#if defined(__APPLE__) && defined(__aarch64__)
/* Not && !TARGET_OS_TV: on a platform that never includes
* TargetConditionals.h the name is undefined, and an undefined name in
* a preprocessor expression is a warning under -Wundef and a silent
* zero otherwise. Ask whether it is defined first. */
#if defined(__APPLE__) && defined(__aarch64__) \
&& (!defined(TARGET_OS_TV) || !TARGET_OS_TV)
/* Keeps a debugger out of an EXC_BAD_ACCESS loop when the fault
* is one we are going to handle. */
* is one we are going to handle; tvOS has neither the API nor a
* debugger to keep out. */
if (ok)
task_set_exception_ports(mach_task_self(), EXC_MASK_BAD_ACCESS,
MACH_PORT_NULL, EXCEPTION_DEFAULT, 0);
+11
View File
@@ -143,6 +143,14 @@ static bool image_texture_load_internal(
image_transfer_set_buffer_ptr(img, type, (uint8_t*)ptr, len);
/* A caller that wants the extra precision says so through
* ->pix10; the decoders that have a 10-bit path (PNG 16-bit, and
* the video stills) then emit XRGB2101010 and report what they
* actually produced. A decoder or a file without one leaves the
* flag clear and the caller gets the ordinary 8-bit image. */
if (out_img->pix10)
image_transfer_set_want_10bit(img, type, 1);
if (!image_transfer_start(img, type))
{
image_transfer_free(img, type);
@@ -163,6 +171,8 @@ static bool image_texture_load_internal(
return false;
}
out_img->pix10 = image_transfer_is_10bit(img, type);
/* GPU-native fast path: if the loader can hand back BCn blocks for
* direct upload, copy the source (so the mip pointers survive the
* caller freeing its buffer) and defer any RGBA8 decode. */
@@ -392,6 +402,7 @@ bool image_texture_load_ex(struct texture_image *out_img,
out_img->width = 0;
out_img->height = 0;
out_img->compressed = NULL;
out_img->pix10 = false;
return false;
}
+4
View File
@@ -145,6 +145,10 @@ enum image_type_enum image_texture_get_type(const char *path);
bool image_texture_load_buffer(struct texture_image *img,
enum image_type_enum type, void *s, size_t len);
/* ->pix10 is an ask on the way in and an answer on the way out: set
* it before the call to have a decoder that can emit XRGB2101010 do
* so, and read it after to find out whether it did. Clear on entry
* means the ordinary 8-bit image, as before. */
bool image_texture_load(struct texture_image *img, const char *path);
/* image_texture_load with an abort hook: both decode stages are
+19 -1
View File
@@ -138,7 +138,25 @@ enum retro_task_flags
* If set, the task queue will not call \c progress_cb
* and will not display any messages from this task.
*/
RETRO_TASK_FLG_MUTE = (1 << 3)
RETRO_TASK_FLG_MUTE = (1 << 3),
/**
* Set by the pusher to promise that \c handler touches nothing
* but the task itself - its own state, and the task_set_* and
* task_get_* accessors - and nothing that outlives the queue or
* is torn down with it: no logging, no pushing other tasks, no
* subsystem the frontend frees on the way out.
*
* In exchange, if \c task_queue_deinit() finds the worker still
* inside this task's handler after its bound, it stops waiting:
* the task is taken off the queue and the worker is left to
* finish the handler on its own. \c callback and \c cleanup never
* run, so whatever they would have released stays allocated; the
* task itself is freed when the handler returns. Meant for
* handlers that can
* block in an OS call for arbitrarily long, such as a device
* enumeration.
*/
RETRO_TASK_FLG_DETACHABLE = (1 << 4)
};
/**
+16
View File
@@ -1065,6 +1065,13 @@ typedef volatile size_t retro_atomic_size_t;
#define retro_atomic_fetch_sub_int(p, v) retro_atomic_fetch_sub_int_fb((p), (v))
#define retro_atomic_fetch_or_int(p, v) retro_atomic_fetch_or_int_fb((p), (v))
#define retro_atomic_fetch_and_int(p, v) retro_atomic_fetch_and_int_fb((p), (v))
/* Compare-and-swap, which this backend can only offer as a plain
* read-compare-write: correct where the fallback itself is - one core,
* or a platform whose only preemption is cooperative - and no worse
* than the loads and stores beside it. Callers that need a real CAS
* are on a backend that has one. */
#define retro_atomic_cas_int(p, expected, desired) \
retro_atomic_cas_int_fb((p), (expected), (desired))
static INLINE int retro_atomic_fetch_add_int_fb(retro_atomic_int_t *p, int v)
{
@@ -1094,6 +1101,15 @@ static INLINE int retro_atomic_fetch_and_int_fb(retro_atomic_int_t *p, int v)
return old;
}
static INLINE bool retro_atomic_cas_int_fb(retro_atomic_int_t *p,
int expected, int desired)
{
if (*p != expected)
return false;
*p = desired;
return true;
}
#define retro_atomic_load_acquire_size(p) (*(p))
#define retro_atomic_load_relaxed_size(p) (*(p))
#define retro_atomic_store_release_size(p, v) do { *(p) = (v); } while (0)
+31
View File
@@ -73,6 +73,22 @@
#define SSL_MBED_LEGACY_RNG 1
#endif
/* Mbed TLS 3.x keeps the legacy RNG but runs TLS 1.3 (and everything
* else once MBEDTLS_USE_PSA_CRYPTO is set) on top of PSA, and leaves
* psa_crypto_init() to the application. Without it the client cannot
* even build its ClientHello key share and every handshake fails with
* MBEDTLS_ERR_SSL_INTERNAL_ERROR. 3.6 compiles TLS 1.3 in by default,
* so a system 3.6 build hits this on every connection. */
#if SSL_MBED_LEGACY_RNG && !defined(HAVE_BUILTINMBEDTLS) \
&& defined(MBEDTLS_VERSION_MAJOR) && MBEDTLS_VERSION_MAJOR == 3 \
&& defined(MBEDTLS_PSA_CRYPTO_C) \
&& (defined(MBEDTLS_SSL_PROTO_TLS1_3) || defined(MBEDTLS_USE_PSA_CRYPTO))
#include <psa/crypto.h>
#define SSL_MBED_LEGACY_PSA_INIT 1
#else
#define SSL_MBED_LEGACY_PSA_INIT 0
#endif
#if defined(VITA)
#include <psp2/kernel/rng.h>
#endif
@@ -181,6 +197,11 @@ void* ssl_socket_init(int fd, const char *domain)
#endif
&state->entropy, (const unsigned char*)pers, strlen(pers)) != 0)
goto error;
#if SSL_MBED_LEGACY_PSA_INIT
/* Idempotent - safe to call once per socket. */
if (psa_crypto_init() != PSA_SUCCESS)
goto error;
#endif
#else
/* Idempotent - safe to call once per socket. */
if (psa_crypto_init() != PSA_SUCCESS)
@@ -360,6 +381,11 @@ ssize_t ssl_socket_receive_all_nonblocking(void *state_data,
*err = true;
return -1;
}
#ifdef MBEDTLS_ERR_SSL_RECEIVED_NEW_SESSION_TICKET
/* TLS 1.3 servers send tickets after the handshake; not an error */
else if (ret == MBEDTLS_ERR_SSL_RECEIVED_NEW_SESSION_TICKET)
continue;
#endif
else if (isagain((int)ret) || ret == MBEDTLS_ERR_SSL_WANT_READ)
{
/* Would block - return what we have so far */
@@ -398,6 +424,11 @@ int ssl_socket_receive_all_blocking(void *state_data,
|| ret == MBEDTLS_ERR_SSL_WANT_WRITE)
continue;
#ifdef MBEDTLS_ERR_SSL_RECEIVED_NEW_SESSION_TICKET
if (ret == MBEDTLS_ERR_SSL_RECEIVED_NEW_SESSION_TICKET)
continue;
#endif
if (ret == MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY)
break;
+175 -18
View File
@@ -100,6 +100,44 @@ static scond_t *finished_cond = NULL;
static sthread_t *worker_thread = NULL;
static bool worker_continue = true;
/* use running_lock when touching it */
/* Where a worker is, for deinit. Per worker rather than global: a
* worker deinit gave up on still owns its record after the next init
* has started a new one, and must not read the new worker's. */
enum task_worker_state
{
TASK_WORKER_IDLE = 0, /* in the queue's own code */
TASK_WORKER_IN_HANDLER, /* inside task->handler() */
TASK_WORKER_ORPHANED, /* deinit let go of it; it owns the rest */
TASK_WORKER_EXITED /* left the loop; joinable */
};
struct task_worker
{
retro_atomic_int_t state;
/* The task whose handler it is in; written under running_lock. */
retro_task_t *task;
/* The primitives of the queue this worker was started for, so an
* orphan can find and free its own generation's after deinit has
* cleared the globals. Set at init, never changed. */
slock_t *running_lock;
slock_t *finished_lock;
slock_t *queue_lock;
scond_t *worker_cond;
scond_t *finished_cond;
};
static struct task_worker *worker_self = NULL;
/* How long deinit waits for the worker before it may leave one stuck
* in a detachable task's handler behind. */
#define TASK_QUEUE_DETACH_AFTER_US 1000000
/* Once a worker has been left behind, its handler may still be
* setting task properties, which takes property_lock. The lock is
* then kept for the life of the process and reused by every later
* init instead of being freed. */
static bool property_lock_pinned = false;
#endif
#ifdef HAVE_GCD
@@ -742,6 +780,8 @@ static void retro_task_threaded_retrieve(task_retriever_data_t *data)
static void threaded_worker(void *userdata)
{
struct task_worker *self = (struct task_worker*)userdata;
sthread_setname("ra-task");
for (;;)
@@ -753,6 +793,10 @@ static void threaded_worker(void *userdata)
if (!worker_continue)
{
/* Tell deinit, which may be waiting on worker_cond with a
* bound, that there is nothing left to wait for. */
retro_atomic_store_release_int(&self->state, TASK_WORKER_EXITED);
scond_broadcast(worker_cond);
slock_unlock(running_lock);
/* No: draining is the caller's job, done while the
* subsystems that finish callbacks reach are still alive
@@ -783,8 +827,34 @@ static void threaded_worker(void *userdata)
}
}
self->task = task;
retro_atomic_store_release_int(&self->state, TASK_WORKER_IN_HANDLER);
slock_unlock(running_lock);
task->handler(task);
/* Deinit gave up on this handler and is taking the task off the
* queue; the queue's globals may belong to a new init by now.
* Deinit holds this generation's running_lock until it is done
* letting go, so taking it once waits exactly that long. Then
* free this generation's primitives and the task, and leave
* without touching any global. The task's callback and cleanup
* never run. */
if (!retro_atomic_cas_int(&self->state,
TASK_WORKER_IN_HANDLER, TASK_WORKER_IDLE))
{
slock_lock(self->running_lock);
slock_unlock(self->running_lock);
scond_free(self->worker_cond);
scond_free(self->finished_cond);
slock_free(self->running_lock);
slock_free(self->finished_lock);
slock_free(self->queue_lock);
free(task->title);
free(task->error);
free(task);
free(self);
return;
}
#if defined(__EMSCRIPTEN__) || defined(_3DS)
/* Workaround emscripten pthread bug where not parking the
thread will prevent other important stuff from
@@ -853,14 +923,17 @@ static void retro_task_sync_primitives_free(void)
scond_free(finished_cond);
slock_free(running_lock);
slock_free(finished_lock);
slock_free(property_lock);
if (!property_lock_pinned)
{
slock_free(property_lock);
property_lock = NULL;
}
slock_free(queue_lock);
worker_cond = NULL;
finished_cond = NULL;
running_lock = NULL;
finished_lock = NULL;
property_lock = NULL;
queue_lock = NULL;
}
@@ -872,7 +945,8 @@ static bool retro_task_sync_primitives_new(void)
{
running_lock = slock_new();
finished_lock = slock_new();
property_lock = slock_new();
if (!property_lock_pinned)
property_lock = slock_new();
queue_lock = slock_new();
worker_cond = scond_new();
finished_cond = scond_new();
@@ -894,6 +968,18 @@ static bool retro_task_threaded_init(void)
if (!retro_task_sync_primitives_new())
return false;
if (!(worker_self = (struct task_worker*)calloc(1, sizeof(*worker_self))))
{
retro_task_sync_primitives_free();
return false;
}
retro_atomic_store_release_int(&worker_self->state, TASK_WORKER_IDLE);
worker_self->running_lock = running_lock;
worker_self->finished_lock = finished_lock;
worker_self->queue_lock = queue_lock;
worker_self->worker_cond = worker_cond;
worker_self->finished_cond = finished_cond;
slock_lock(running_lock);
worker_continue = true;
slock_unlock(running_lock);
@@ -902,37 +988,108 @@ static bool retro_task_threaded_init(void)
* starts. Without a worker there is nobody to run what gets queued,
* which is why this reports failure rather than leaving the queue
* pointed at an implementation that cannot service it. */
if ((worker_thread = sthread_create(threaded_worker, NULL)))
if ((worker_thread = sthread_create(threaded_worker, worker_self)))
return true;
worker_continue = false;
free(worker_self);
worker_self = NULL;
retro_task_sync_primitives_free();
return false;
}
/* Called with running_lock held, once the bound has passed and the
* worker inside a detachable task's handler has been marked orphaned.
* Takes the task off the queue and clears the globals; the worker,
* blocked on this generation's running_lock as soon as its handler
* returns, frees the primitives and the task after the unlock below,
* which is therefore the last thing done here. property_lock stays:
* the handler may still be setting task properties through it. */
static void retro_task_threaded_orphan_worker(retro_task_t *task)
{
slock_t *lock = running_lock;
slock_lock(queue_lock);
task_queue_remove(&tasks_running, task);
slock_unlock(queue_lock);
retro_atomic_fetch_sub_int(&tasks_running_count, 1);
sthread_detach(worker_thread);
worker_thread = NULL;
worker_self = NULL;
property_lock_pinned = true;
worker_cond = NULL;
finished_cond = NULL;
running_lock = NULL;
finished_lock = NULL;
queue_lock = NULL;
slock_unlock(lock);
}
/* Stops the worker, waiting for it with a bound. A worker that is
* between tasks, or doing the queue's own bookkeeping, leaves as soon
* as it sees worker_continue, so the bound only ever runs out on one
* stuck inside a handler. If that task was pushed detachable, its
* handler promises to touch nothing but the task itself, so the
* worker is left to finish it alone and deinit returns. Otherwise the
* handler may be holding state that teardown is about to free, and
* the only safe thing is to keep waiting for it. */
static void retro_task_threaded_deinit(void)
{
retro_time_t deadline = cpu_features_get_time_usec()
+ TASK_QUEUE_DETACH_AFTER_US;
slock_lock(running_lock);
worker_continue = false;
scond_signal(worker_cond);
slock_unlock(running_lock);
scond_broadcast(worker_cond);
for (;;)
{
int state = retro_atomic_load_acquire_int(&worker_self->state);
retro_time_t left;
if (state == TASK_WORKER_EXITED)
break;
left = deadline - cpu_features_get_time_usec();
if (left > 0)
{
scond_wait_timeout(worker_cond, running_lock, left);
continue;
}
if (state == TASK_WORKER_IN_HANDLER)
{
retro_task_t *task = worker_self->task;
bool detachable;
slock_lock(property_lock);
detachable = (task->flags & RETRO_TASK_FLG_DETACHABLE) != 0;
slock_unlock(property_lock);
if (detachable && retro_atomic_cas_int(&worker_self->state,
TASK_WORKER_IN_HANDLER, TASK_WORKER_ORPHANED))
{
retro_task_threaded_orphan_worker(task);
return;
}
}
/* Not in a detachable handler: it is either leaving or can only
* leave by returning. Wait for its signal, without a bound. */
scond_wait(worker_cond, running_lock);
}
slock_unlock(running_lock);
sthread_join(worker_thread);
scond_free(worker_cond);
scond_free(finished_cond);
slock_free(running_lock);
slock_free(finished_lock);
slock_free(property_lock);
slock_free(queue_lock);
free(worker_self);
retro_task_sync_primitives_free();
worker_thread = NULL;
worker_cond = NULL;
finished_cond = NULL;
running_lock = NULL;
finished_lock = NULL;
property_lock = NULL;
queue_lock = NULL;
worker_self = NULL;
}
static struct retro_task_impl impl_threaded = {
@@ -0,0 +1,39 @@
TARGET := task_queue_detach_test
LIBRETRO_COMM_DIR := ../../..
# Built with HAVE_THREADS: what is under test is how deinit stops the
# threaded worker when a handler has not returned.
SOURCES := \
task_queue_detach_test.c \
$(LIBRETRO_COMM_DIR)/queues/task_queue.c \
$(LIBRETRO_COMM_DIR)/rthreads/rthreads.c \
$(LIBRETRO_COMM_DIR)/features/features_cpu.c
OBJS := $(SOURCES:.c=.o)
CFLAGS += -Wall -pedantic -std=gnu99 -g -O1 -fno-omit-frame-pointer \
-DHAVE_THREADS -I$(LIBRETRO_COMM_DIR)/include
LIBS += -lpthread
ifneq ($(SANITIZER),)
CFLAGS := -fsanitize=$(SANITIZER) $(CFLAGS)
LDFLAGS := -fsanitize=$(SANITIZER) $(LDFLAGS)
endif
all: $(TARGET)
%.o: %.c
$(CC) -c -o $@ $< $(CFLAGS)
$(TARGET): $(OBJS)
$(CC) -o $@ $^ $(LDFLAGS) $(LIBS)
check: $(TARGET)
./$(TARGET)
clean:
rm -f $(TARGET) $(OBJS)
.PHONY: all check clean
@@ -0,0 +1,194 @@
/* Regression test for how task_queue_deinit() stops the threaded
* worker in libretro-common/queues/task_queue.c.
*
* Deinit used to join the worker with no limit, so a handler blocked
* in an OS call - a DirectInput device walk held by a Bluetooth stack
* still coming up, say - held quit and every reinit of the queue for
* as long as it stayed blocked.
*
* The contract this pins:
*
* idle worker -> deinit returns at once
* detachable handler stuck -> deinit returns after its bound; the
* task is dropped, its callback never
* runs, a new queue works meanwhile,
* and the handler finishing later
* touches nothing of it
* other handler past bound -> deinit still waits for it, since
* its handler may use what teardown
* frees next
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <boolean.h>
#include <retro_common_api.h>
#include <retro_timers.h>
#include <retro_atomic.h>
#include <features/features_cpu.h>
#include <queues/task_queue.h>
static unsigned failures = 0;
static void check(bool cond, const char *what)
{
if (cond)
printf(" [pass] %s\n", what);
else
{
printf(" [FAIL] %s\n", what);
failures++;
}
}
/* --- a handler that blocks until released ---------------------------- */
static retro_atomic_int_t blocked_entered = RETRO_ATOMIC_INT_INITIALIZER(0);
static retro_atomic_int_t blocked_release = RETRO_ATOMIC_INT_INITIALIZER(0);
static retro_atomic_int_t blocked_returned = RETRO_ATOMIC_INT_INITIALIZER(0);
static retro_atomic_int_t blocked_callback = RETRO_ATOMIC_INT_INITIALIZER(0);
static void blocked_handler(retro_task_t *task)
{
retro_atomic_store_release_int(&blocked_entered, 1);
while (!retro_atomic_load_acquire_int(&blocked_release))
retro_sleep(1);
/* What an orphaned handler is still allowed to do. */
task_set_progress(task, 100);
task_set_flags(task, RETRO_TASK_FLG_FINISHED, true);
retro_atomic_store_release_int(&blocked_returned, 1);
}
static void blocked_cb(retro_task_t *task, void *td, void *ud, const char *e)
{
retro_atomic_store_release_int(&blocked_callback, 1);
}
/* --- a handler that takes a fixed time, then finishes ---------------- */
#define SLOW_HANDLER_MS 1500
static retro_atomic_int_t slow_entered = RETRO_ATOMIC_INT_INITIALIZER(0);
static retro_atomic_int_t slow_returned = RETRO_ATOMIC_INT_INITIALIZER(0);
static void slow_handler(retro_task_t *task)
{
retro_atomic_store_release_int(&slow_entered, 1);
retro_sleep(SLOW_HANDLER_MS);
task_set_flags(task, RETRO_TASK_FLG_FINISHED, true);
retro_atomic_store_release_int(&slow_returned, 1);
}
/* --- a quick task, to prove a queue works ---------------------------- */
static retro_atomic_int_t quick_callback = RETRO_ATOMIC_INT_INITIALIZER(0);
static void quick_handler(retro_task_t *task)
{
task_set_flags(task, RETRO_TASK_FLG_FINISHED, true);
}
static void quick_cb(retro_task_t *task, void *td, void *ud, const char *e)
{
retro_atomic_store_release_int(&quick_callback, 1);
}
static void push(retro_task_handler_t h, retro_task_callback_t cb,
bool detachable)
{
retro_task_t *task = task_init();
task->handler = h;
task->callback = cb;
task->flags |= RETRO_TASK_FLG_MUTE;
if (detachable)
task->flags |= RETRO_TASK_FLG_DETACHABLE;
task_queue_push(task);
}
static void wait_for(retro_atomic_int_t *flag)
{
while (!retro_atomic_load_acquire_int(flag))
retro_sleep(1);
}
static int64_t ms_since(retro_time_t start)
{
return (int64_t)((cpu_features_get_time_usec() - start) / 1000);
}
int main(void)
{
retro_time_t start;
int64_t took;
int i;
printf("idle worker\n");
task_queue_init(true, NULL);
check(task_queue_is_threaded(), "queue is threaded");
start = cpu_features_get_time_usec();
task_queue_deinit();
took = ms_since(start);
check(took < 200, "deinit of an idle worker returns at once");
printf("detachable handler stuck past the bound\n");
task_queue_init(true, NULL);
push(blocked_handler, blocked_cb, true);
wait_for(&blocked_entered);
start = cpu_features_get_time_usec();
task_queue_deinit();
took = ms_since(start);
check(took >= 900 && took < 3000, "deinit gives up after its bound");
check(!retro_atomic_load_acquire_int(&blocked_returned),
"the handler was still blocked when deinit returned");
/* A new queue while the orphan is still running. */
task_queue_init(true, NULL);
push(quick_handler, quick_cb, false);
for (i = 0; i < 2000 && !retro_atomic_load_acquire_int(&quick_callback); i++)
{
task_queue_check();
retro_sleep(1);
}
check(retro_atomic_load_acquire_int(&quick_callback),
"a new queue runs tasks while the orphan is blocked");
/* Let the orphan finish while the new queue is live; it may set its
* own task's properties and must touch nothing else. */
retro_atomic_store_release_int(&blocked_release, 1);
wait_for(&blocked_returned);
for (i = 0; i < 200; i++)
{
task_queue_check();
retro_sleep(1);
}
check(!retro_atomic_load_acquire_int(&blocked_callback),
"the dropped task's callback never runs");
start = cpu_features_get_time_usec();
task_queue_deinit();
check(ms_since(start) < 200, "the new queue still deinits at once");
printf("non-detachable handler past the bound\n");
task_queue_init(true, NULL);
push(slow_handler, NULL, false);
wait_for(&slow_entered);
start = cpu_features_get_time_usec();
task_queue_deinit();
took = ms_since(start);
check(retro_atomic_load_acquire_int(&slow_returned),
"deinit waited for a handler that was not detachable");
check(took >= SLOW_HANDLER_MS - 200, "and waited past the bound to do it");
/* Give a leftover orphan, if any, time to be seen by the sanitizers. */
retro_sleep(50);
if (failures)
{
printf("\n%u failure(s)\n", failures);
return 1;
}
printf("\nall passed\n");
return 0;
}