Files
parallel-gx1/Source/Core/Common/ParallelFor.c
T
LibretroAdmin 767c898df8 Move every thread onto rthreads and add a lock-free parallel_for
No std::thread, std::async, std::future or std::promise is left. Each
thread is an sthread with a plain entry point: Common::ThreadEntry<&C::F>
turns a member function into one, Common::JoinThread joins and clears a
handle, and Common::StartDetachedThread covers the fire-and-forget users.

Common/ParallelFor.{h,c} (C89) runs fn(ctx, i) for i in [0, count) on a
persistent pool of one worker per extra core plus the caller. Jobs are
claimed with an atomic counter, workers park on an eventcount between
batches, and the batch hand-off is a seq_cst users/active pair, so there
is no lock anywhere. A caller that finds the pool busy runs its batch
serially instead of waiting. retro_deinit joins the pool.

- VolumeWii::HashGroup spawned 64 threads per 2 MiB group and
  EncryptGroup another one per core; both are now parallel_for batches
  on the pool. The H1/H2 tail is a few small hashes and runs inline.
- VolumeVerifier spawned up to five std::async threads per chunk; the
  chunk's tasks now go to one persistent worker that fans them out with
  parallel_for, still overlapping the next read. The Redump lookup is an
  sthread joined in Finish or the destructor.
- The dual-core "GPU thread" in Core only set a thread_local on itself
  and exited, since the frontend runs the GPU loop. It is gone, along
  with the non-libretro branches of Core.cpp.
- The emu thread is started through Core::StartEmuThread.
- CPU core counts come from cpu_features_get_core_amount().
2026-11-05 13:00:34 +01:00

222 lines
5.7 KiB
C

/* Copyright 2026 Dolphin Emulator Project
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "Common/ParallelFor.h"
#include <stdlib.h>
#include <features/features_cpu.h>
#include <retro_atomic.h>
#include <rthreads/retro_eventcount.h>
#include <rthreads/rthreads.h>
/* Never more workers than this, whatever the core count. */
#define PARALLEL_FOR_MAX_WORKERS 63
/* One batch at a time, owned by whichever caller won m_owner.
*
* A worker touches the batch only between raising m_users and lowering it
* again, and only if it saw m_active set in between. The caller clears
* m_active once every index is claimed and then waits for m_users to drop
* to zero; both sides use seq_cst operations, so a worker either is counted
* in m_users or sees m_active clear. Once m_users reads zero no worker can
* still be inside the batch, and the caller is free to return and to rewrite
* the batch fields for the next call. */
static struct
{
sthread_t **workers;
unsigned worker_count;
int started;
/* Batch, written only by the owner while no worker is inside it. */
parallel_for_fn fn;
void *ctx;
size_t count;
retro_atomic_size_t next;
retro_atomic_int_t owner;
retro_atomic_int_t active;
retro_atomic_int_t users;
retro_atomic_int_t generation;
retro_atomic_int_t stop;
retro_eventcount_t wake;
retro_eventcount_t idle;
} s_pool;
static void parallel_for_run_jobs(void)
{
for (;;)
{
const size_t i = retro_atomic_fetch_add_size(&s_pool.next, 1);
if (i >= s_pool.count)
return;
s_pool.fn(s_pool.ctx, i);
}
}
static void parallel_for_worker(void *data)
{
int seen = 0;
(void)data;
sthread_setname("parallel_for");
for (;;)
{
int gen;
const int key = retro_eventcount_prepare_wait(&s_pool.wake);
if (retro_atomic_load_acquire_int(&s_pool.stop))
{
retro_eventcount_cancel_wait(&s_pool.wake);
return;
}
gen = retro_atomic_load_acquire_int(&s_pool.generation);
if (gen == seen)
{
retro_eventcount_commit_wait(&s_pool.wake, key);
continue;
}
retro_eventcount_cancel_wait(&s_pool.wake);
seen = gen;
retro_atomic_fetch_add_seq_cst_int(&s_pool.users, 1);
if (retro_atomic_load_seq_cst_int(&s_pool.active))
parallel_for_run_jobs();
if (retro_atomic_fetch_add_seq_cst_int(&s_pool.users, -1) == 1)
retro_eventcount_notify(&s_pool.idle);
}
}
/* Called by the owner. */
static void parallel_for_start(void)
{
unsigned cores = cpu_features_get_core_amount();
unsigned i;
s_pool.started = 1;
if (cores <= 1)
return;
if (cores - 1 > PARALLEL_FOR_MAX_WORKERS)
cores = PARALLEL_FOR_MAX_WORKERS + 1;
s_pool.workers = (sthread_t**)calloc(cores - 1, sizeof(*s_pool.workers));
if (!s_pool.workers)
return;
retro_atomic_int_init(&s_pool.active, 0);
retro_atomic_int_init(&s_pool.users, 0);
retro_atomic_int_init(&s_pool.generation, 0);
retro_atomic_int_init(&s_pool.stop, 0);
retro_atomic_size_init(&s_pool.next, 0);
if (!retro_eventcount_init(&s_pool.wake))
goto fail_wake;
if (!retro_eventcount_init(&s_pool.idle))
goto fail_idle;
for (i = 0; i < cores - 1; i++)
{
s_pool.workers[i] = sthread_create(parallel_for_worker, NULL);
if (!s_pool.workers[i])
break;
}
s_pool.worker_count = i;
if (i)
return;
retro_eventcount_free(&s_pool.idle);
fail_idle:
retro_eventcount_free(&s_pool.wake);
fail_wake:
free(s_pool.workers);
s_pool.workers = NULL;
}
static void parallel_for_serial(size_t count, parallel_for_fn fn, void *ctx)
{
size_t i;
for (i = 0; i < count; i++)
fn(ctx, i);
}
void parallel_for(size_t count, parallel_for_fn fn, void *ctx)
{
if (count < 2 || !retro_atomic_cas_int(&s_pool.owner, 0, 1))
{
parallel_for_serial(count, fn, ctx);
return;
}
if (!s_pool.started)
parallel_for_start();
if (!s_pool.worker_count)
{
parallel_for_serial(count, fn, ctx);
retro_atomic_store_release_int(&s_pool.owner, 0);
return;
}
s_pool.fn = fn;
s_pool.ctx = ctx;
s_pool.count = count;
retro_atomic_store_relaxed_size(&s_pool.next, 0);
retro_atomic_fetch_add_seq_cst_int(&s_pool.active, 1);
retro_atomic_fetch_add_int(&s_pool.generation, 1);
retro_eventcount_notify(&s_pool.wake);
parallel_for_run_jobs();
/* Every index is claimed; wait for the workers still running one. */
retro_atomic_fetch_add_seq_cst_int(&s_pool.active, -1);
while (retro_atomic_load_seq_cst_int(&s_pool.users))
{
const int key = retro_eventcount_prepare_wait(&s_pool.idle);
if (!retro_atomic_load_seq_cst_int(&s_pool.users))
{
retro_eventcount_cancel_wait(&s_pool.idle);
break;
}
retro_eventcount_commit_wait(&s_pool.idle, key);
}
retro_atomic_store_release_int(&s_pool.owner, 0);
}
unsigned parallel_for_threads(void)
{
unsigned cores = cpu_features_get_core_amount();
if (cores < 1)
return 1;
if (cores > PARALLEL_FOR_MAX_WORKERS + 1)
return PARALLEL_FOR_MAX_WORKERS + 1;
return cores;
}
void parallel_for_deinit(void)
{
unsigned i;
while (!retro_atomic_cas_int(&s_pool.owner, 0, 1))
sthread_yield();
if (s_pool.worker_count)
{
retro_atomic_store_release_int(&s_pool.stop, 1);
retro_eventcount_notify(&s_pool.wake);
for (i = 0; i < s_pool.worker_count; i++)
sthread_join(s_pool.workers[i]);
retro_eventcount_free(&s_pool.idle);
retro_eventcount_free(&s_pool.wake);
}
free(s_pool.workers);
s_pool.workers = NULL;
s_pool.worker_count = 0;
s_pool.started = 0;
retro_atomic_store_release_int(&s_pool.owner, 0);
}