libretro-common: resync, and WorkSema on the asymmetric eventcount

Three things, and the middle one is the reason for the other two.

libretro-common is brought level with RetroArch master. Five files
differed and one of them stays as it is -- include/compat/ifaddrs.h keeps
the extern "C" guard ps2 added for DEV9, which is not upstream and without
which the core fails to link; the other four are RetroArch being ahead,
checked line by line. Three files are new: retro_procbarrier.c, the
process-wide barrier, retro_eventcount.c, and retro_asym_eventcount.c,
which is built on both. Makefile.common builds all three.

WorkSema is rewritten on two of those. It was a hand-rolled state
machine whose NotifyOfWork -- what the EE pays per GS packet with the GS
thread awake, and what MTVU's own comment already named as the fetch_add
on its hot path -- measured 6.4 ns. It is now retro_asym_eventcount's
notify, a release store and a relaxed load with no lock prefix, measured
1.2 ns in the same binary and the same run. The symmetric retro_eventcount
was measured too, at 9.6 ns, and would have been a regression: it pays an
RMW and a seq_cst load to serve many producers, and each WorkSema here
has one. The ordering the hot side drops is paid where WaitForWork parks,
by retro_procbarrier; where no barrier exists the object runs the
symmetric protocol for its whole life, decided at init, and MTGS never
needs to know. WaitForEmpty's direction -- worker to producer, once a
frame -- is the plain retro_eventcount, whose prepare/commit contract is
what STATE_FLAG_WAITING_EMPTY was reinventing.

The seven operations keep their contract exactly, and tests/spsc/
worksema.cpp pins each: CheckForWork consumes a burst of notifies as one;
WaitForWork returns at once with work pending and parks without; a timed
wait is false only on a timeout with nothing pending; Kill unblocks every
waiter and makes WaitForEmpty false; Reset revives. One subtlety is
pinned because it bit the test first: "empty" means the worker went idle
after consuming, not merely consumed, and a WaitForEmpty issued between
the two blocks -- the old state machine did the same, checked. The
producer/consumer pair runs 50000 items with WaitForEmpty interleaved
2858 times and never sees it return with work outstanding. Plain, TSan,
ASan and UBSan are all clean; TSan sees the eventcounts' atomics, not the
barrier, so the lost-wake half is only load-bearing on more than one
core.

The last part is what the rewrite made dead, plus what an audit of every
common/ object against every importer in the finished build found
alongside it: KernelSemaphore::WaitFor, whose only caller was the old
timed wait; Mutex::TryLock; both StringUtil::ReplaceAll overloads; and
DecompressBlockBC4 and BC5 with their private helper and the two
Int8ToFloat conversions that served only them -- the replacement loader
dispatches BC1, BC2, BC3 and BC7 and nothing else. Every header in
common/ is still included by something, so nothing is removed at file
level; the audit was at symbol level and discounted vtable dispatch,
which is why SettingsInterface's methods, which nm cannot see a caller
for, are untouched.

The core links, all twelve suites pass, and the patch harness gains the
three objects its Semaphore.cpp link now needs.
This commit is contained in:
libretroadmin committed 2026-09-17 06:06:52 +02:00
1 parent a9da049460
commit 53f62899ca
21 files changed
+2710 -390

No files matched your search

+3
View File
@@ -117,6 +117,9 @@ SOURCES_C += \
$(LIBRETRO_COMM_DIR)/string/stdstring.c \
$(LIBRETRO_COMM_DIR)/time/rtime.c \
$(LIBRETRO_COMM_DIR)/rthreads/rthreads.c \
$(LIBRETRO_COMM_DIR)/rthreads/retro_eventcount.c \
$(LIBRETRO_COMM_DIR)/rthreads/retro_procbarrier.c \
$(LIBRETRO_COMM_DIR)/rthreads/retro_asym_eventcount.c \
$(LIBRETRO_COMM_DIR)/queues/retro_spsc.c \
$(LIBRETRO_COMM_DIR)/vfs/vfs_implementation.c
+145 -159
View File
@@ -69,204 +69,190 @@ s32 Threading::SpinBudget()
// Semaphore Implementations
// --------------------------------------------------------------------------------------
bool Threading::KernelSemaphore::WaitFor(u32 timeout_ms)
Threading::WorkSema::WorkSema()
{
#if defined(_WIN32)
return WaitForSingleObject(m_sema, timeout_ms) == WAIT_OBJECT_0;
#elif defined(__APPLE__)
mach_timespec_t ts;
ts.tv_sec = timeout_ms / 1000;
ts.tv_nsec = (timeout_ms % 1000) * 1000000;
return semaphore_timedwait(m_sema, ts) == KERN_SUCCESS;
#else
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
ts.tv_sec += timeout_ms / 1000;
ts.tv_nsec += (long)(timeout_ms % 1000) * 1000000L;
if (ts.tv_nsec >= 1000000000L)
{
ts.tv_sec++;
ts.tv_nsec -= 1000000000L;
}
while (sem_timedwait(&m_sema, &ts) == -1)
{
if (errno != EINTR)
return false;
}
/* Both inits decide their protocol once and keep it. The work
* eventcount picks asymmetric wherever retro_procbarrier has a tier,
* symmetric elsewhere; a WorkSema never needs to know which. */
retro_asym_eventcount_init(&m_work);
retro_eventcount_init(&m_empty);
retro_atomic_store_relaxed_int(&m_seen, retro_atomic_load_relaxed_int(&m_work.epoch));
}
Threading::WorkSema::~WorkSema()
{
retro_eventcount_free(&m_empty);
retro_asym_eventcount_free(&m_work);
}
void Threading::WorkSema::GoIdle()
{
/* Order matters for WaitForEmpty's re-check: it reads m_idle, then
* m_seen against the work epoch. Publish idle last, with release, so
* a reader that sees idle=1 also sees the m_seen that goes with it. */
retro_atomic_store_release_int(&m_idle, 1);
retro_eventcount_notify(&m_empty);
}
bool Threading::WorkSema::Consume(int epoch)
{
retro_atomic_store_release_int(&m_seen, epoch);
retro_atomic_store_release_int(&m_idle, 0);
return true;
#endif
}
bool Threading::WorkSema::CheckForWork()
{
/* Load-then-attempt: cas_int is strong with no expected-out
* parameter, so each failed attempt re-reads and re-checks the
* death sentinel - the same guarantee the old expected-out refresh
* provided. */
s32 value;
for (;;)
{
value = retro_atomic_load_acquire_int(&m_state);
// Dead semas stay dead: don't let the CAS below silently rewrite
// INT_MIN to STATE_RUNNING_0 and resurrect a killed worker.
if (value < STATE_SPINNING)
return false;
// we want to switch to the running state, but preserve the waiting empty bit for RUNNING_N -> RUNNING_0
// otherwise, we clear the waiting flag (since we're notifying the waiter that we're empty below)
if (retro_atomic_cas_int(&m_state, value,
((value & (STATE_FLAG_WAITING_EMPTY - 1)) == STATE_RUNNING_0) ? STATE_RUNNING_0 : (value & STATE_FLAG_WAITING_EMPTY)))
break;
}
// if we're not empty, we have work to do
s32 waiting_empty_cleared = value & (STATE_FLAG_WAITING_EMPTY - 1);
if (waiting_empty_cleared != STATE_RUNNING_0)
return true;
// this means we're empty, so notify any waiters
if (value & STATE_FLAG_WAITING_EMPTY)
m_empty_sema.Post();
// no work to do
int epoch;
if (retro_atomic_load_acquire_int(&m_dead))
return false;
/* The epoch is the asym eventcount's own; reading it is what a
* non-blocking check is, and cheaper than a prepare/cancel pair,
* which would pay the barrier. */
epoch = retro_atomic_load_acquire_int(&m_work.epoch);
if (epoch != retro_atomic_load_relaxed_int(&m_seen))
return Consume(epoch);
GoIdle();
return false;
}
void Threading::WorkSema::WaitForWork()
{
// State change:
// SLEEPING, SPINNING: This is the worker thread and it's clearly not asleep or spinning, so these states should be impossible
// DEAD (any value < STATE_SPINNING): also impossible if invariants hold, but guard so a stray call after Kill()
// doesn't silently resurrect us to STATE_RUNNING_0 and then sleep forever on m_sema.
// RUNNING_0: Change state to SPINNING (multi-core) or SLEEPING, wake up thread if WAITING_EMPTY
// RUNNING_N: Change state to RUNNING_0 (and preserve WAITING_EMPTY flag)
const s32 spin_budget = Threading::SpinBudget();
const s32 idle_state = spin_budget ? STATE_SPINNING : STATE_SLEEPING;
s32 value;
for (;;)
int epoch, key;
if (retro_atomic_load_acquire_int(&m_dead))
return;
/* Anything already notified: take it and go. */
epoch = retro_atomic_load_acquire_int(&m_work.epoch);
if (epoch != retro_atomic_load_relaxed_int(&m_seen))
{
s32 waiting_empty_cleared;
s32 new_state;
value = retro_atomic_load_acquire_int(&m_state);
if (value < STATE_SPINNING)
return;
/* The empty-waiter flag cannot survive on the (negative) idle
* states; the post below is what consumes it. */
waiting_empty_cleared = value & (STATE_FLAG_WAITING_EMPTY - 1);
new_state = (waiting_empty_cleared == STATE_RUNNING_0) ? idle_state : (STATE_RUNNING_0 | (value & STATE_FLAG_WAITING_EMPTY));
if (retro_atomic_cas_int(&m_state, value, new_state))
break;
Consume(epoch);
return;
}
s32 waiting_empty_cleared = value & (STATE_FLAG_WAITING_EMPTY - 1);
if (waiting_empty_cleared == STATE_RUNNING_0)
{
/* Wake any WaitForEmpty sleeper before idling, never after:
* the producer's wakeup must not wait out the spin window. */
if (value & STATE_FLAG_WAITING_EMPTY)
m_empty_sema.Post();
/* Nothing pending: the worker is idle from here until something
* arrives, and WaitForEmpty callers are told so. */
GoIdle();
if (spin_budget)
/* Spin first where it is worth it -- a kernel sleep/wake pair costs
* more than SpinBudget() reads -- and only then park. */
if (spin_budget)
{
s32 spins = spin_budget;
while (spins-- > 0)
{
/* Read-only spin in STATE_SPINNING. NotifyOfWork's
* fetch_add moves the state to RUNNING and skips the
* kernel post, so a notify inside this window costs
* neither side a syscall. On timeout, demote to
* SLEEPING and park; a notify racing the demotion makes
* the CAS fail, and the state it left behind is already
* RUNNING. */
s32 spins = spin_budget;
bool have_work = false;
while (spins-- > 0)
epoch = retro_atomic_load_acquire_int(&m_work.epoch);
if (epoch != retro_atomic_load_relaxed_int(&m_seen))
{
if (retro_atomic_load_acquire_int(&m_state) != STATE_SPINNING)
{
have_work = true;
break;
}
THREADING_CPU_RELAX();
Consume(epoch);
return;
}
if (!have_work && retro_atomic_cas_int(&m_state, STATE_SPINNING, STATE_SLEEPING))
m_sema.Wait();
if (retro_atomic_load_relaxed_int(&m_dead))
return;
THREADING_CPU_RELAX();
}
else
m_sema.Wait();
// Acknowledge any additional work added between wake up request and getting here
retro_atomic_fetch_and_int(&m_state, STATE_FLAG_WAITING_EMPTY);
}
/* Park. prepare_wait is where the process-wide barrier is paid:
* after it, either the producer's notify is visible in the key or
* the producer will see our registration and wake us. */
key = retro_asym_eventcount_prepare_wait(&m_work);
if (key != retro_atomic_load_relaxed_int(&m_seen)
|| retro_atomic_load_acquire_int(&m_dead))
{
retro_asym_eventcount_cancel_wait(&m_work);
if (!retro_atomic_load_acquire_int(&m_dead))
Consume(key);
return;
}
retro_asym_eventcount_commit_wait(&m_work, key);
if (!retro_atomic_load_acquire_int(&m_dead))
Consume(retro_atomic_load_acquire_int(&m_work.epoch));
}
bool Threading::WorkSema::WaitForWorkTimed(u32 timeout_ms)
{
int epoch, key;
if (retro_atomic_load_acquire_int(&m_dead))
return true;
epoch = retro_atomic_load_acquire_int(&m_work.epoch);
if (epoch != retro_atomic_load_relaxed_int(&m_seen))
return Consume(epoch);
GoIdle();
key = retro_asym_eventcount_prepare_wait(&m_work);
if (key != retro_atomic_load_relaxed_int(&m_seen)
|| retro_atomic_load_acquire_int(&m_dead))
{
retro_asym_eventcount_cancel_wait(&m_work);
if (!retro_atomic_load_acquire_int(&m_dead))
Consume(key);
return true;
}
if (!retro_asym_eventcount_commit_wait_timeout(&m_work, key,
(int64_t)timeout_ms * 1000))
{
/* Timed out. If something arrived in the meantime it is work,
* not a timeout; only a still-unchanged epoch is the false case. */
epoch = retro_atomic_load_acquire_int(&m_work.epoch);
if (epoch == key && !retro_atomic_load_acquire_int(&m_dead))
return false;
}
if (!retro_atomic_load_acquire_int(&m_dead))
Consume(retro_atomic_load_acquire_int(&m_work.epoch));
return true;
}
bool Threading::WorkSema::WaitForEmpty()
{
for (;;)
{
const s32 value = retro_atomic_load_acquire_int(&m_state);
if (value < 0)
return !(value < STATE_SPINNING); // STATE_SPINNING, queue is empty!
if (retro_atomic_cas_int(&m_state, value, value | STATE_FLAG_WAITING_EMPTY))
break;
}
m_empty_sema.Wait();
return !(retro_atomic_load_acquire_int(&m_state) < STATE_SPINNING);
}
bool Threading::WorkSema::WaitForWorkTimed(u32 timeout_ms)
{
/* Same state walk as WaitForWork, minus the spin phase (a frontend
* thread must park cheaply, not burn a core), plus a bounded sleep.
* On timeout the state is CASed from SLEEPING back to RUNNING_0
* before returning, so no thread is ever recorded as parked while
* running off - the invariant WaitForEmpty depends on. If a notify
* wins that CAS it has already posted m_sema; the stray count makes
* one future wait return early, which the state machine absorbs the
* same way it absorbs any spurious wake. */
s32 value;
for (;;)
{
s32 waiting_empty_cleared;
s32 new_state;
value = retro_atomic_load_acquire_int(&m_state);
if (value < STATE_SPINNING)
int key;
if (retro_atomic_load_acquire_int(&m_dead))
return false;
/* Empty means: the worker is idle AND it went idle having seen
* everything notified so far. Idle alone is not enough -- a notify
* can land after the worker's last check and before its park, and
* that work is still outstanding. */
if (retro_atomic_load_acquire_int(&m_idle)
&& retro_atomic_load_acquire_int(&m_seen)
== retro_atomic_load_acquire_int(&m_work.epoch))
return true;
waiting_empty_cleared = value & (STATE_FLAG_WAITING_EMPTY - 1);
new_state = (waiting_empty_cleared == STATE_RUNNING_0) ? STATE_SLEEPING : (STATE_RUNNING_0 | (value & STATE_FLAG_WAITING_EMPTY));
if (retro_atomic_cas_int(&m_state, value, new_state))
break;
}
if ((value & (STATE_FLAG_WAITING_EMPTY - 1)) == STATE_RUNNING_0)
{
/* Wake any WaitForEmpty sleeper before parking, exactly as
* WaitForWork does. */
if (value & STATE_FLAG_WAITING_EMPTY)
m_empty_sema.Post();
if (!m_sema.WaitFor(timeout_ms))
key = retro_eventcount_prepare_wait(&m_empty);
if (retro_atomic_load_acquire_int(&m_dead))
{
if (retro_atomic_cas_int(&m_state, STATE_SLEEPING, STATE_RUNNING_0))
return false;
/* A notify raced the timeout: state is RUNNING and a sema
* post is in flight. Treat it as a wake. */
retro_eventcount_cancel_wait(&m_empty);
return false;
}
if (retro_atomic_load_acquire_int(&m_idle)
&& retro_atomic_load_acquire_int(&m_seen)
== retro_atomic_load_acquire_int(&m_work.epoch))
{
retro_eventcount_cancel_wait(&m_empty);
return true;
}
retro_eventcount_commit_wait(&m_empty, key);
}
/* Acknowledge any additional work added between wake up request and getting here */
retro_atomic_fetch_and_int(&m_state, STATE_FLAG_WAITING_EMPTY);
return true;
}
void Threading::WorkSema::Kill()
{
s32 value = retro_atomic_exchange_int(&m_state, INT32_MIN);
if (value & STATE_FLAG_WAITING_EMPTY)
m_empty_sema.Post();
retro_atomic_store_release_int(&m_dead, 1);
/* Wake whoever is parked on either side so they see it. */
retro_asym_eventcount_notify(&m_work);
retro_eventcount_notify(&m_empty);
}
void Threading::WorkSema::Reset()
{
retro_atomic_store_release_int(&m_state, STATE_RUNNING_0);
retro_atomic_store_release_int(&m_dead, 0);
retro_atomic_store_release_int(&m_seen, retro_atomic_load_acquire_int(&m_work.epoch));
retro_atomic_store_release_int(&m_idle, 1);
}
Threading::KernelSemaphore::KernelSemaphore()
-20
View File
@@ -130,26 +130,6 @@ namespace StringUtil
return res;
}
std::string ReplaceAll(const std::string_view& subject, const std::string_view& search, const std::string_view& replacement)
{
std::string ret(subject);
ReplaceAll(&ret, search, replacement);
return ret;
}
void ReplaceAll(std::string* subject, const std::string_view& search, const std::string_view& replacement)
{
if (!subject->empty())
{
std::string::size_type start_pos = 0;
while ((start_pos = subject->find(search, start_pos)) != std::string::npos)
{
subject->replace(start_pos, search.length(), replacement);
start_pos += replacement.length();
}
}
}
bool ParseAssignmentString(const std::string_view& str, std::string_view* key, std::string_view* value)
{
const std::string_view::size_type pos = str.find('=');
-3
View File
@@ -170,9 +170,6 @@ namespace StringUtil
/// Splits a string based on a single character delimiter.
std::vector<std::string_view> SplitString(const std::string_view& str, char delimiter, bool skip_empty = true);
/// Replaces all instances of search in subject with replacement.
std::string ReplaceAll(const std::string_view& subject, const std::string_view& search, const std::string_view& replacement);
void ReplaceAll(std::string* subject, const std::string_view& search, const std::string_view& replacement);
/// Parses an assignment string (Key = Value) into its two components.
bool ParseAssignmentString(const std::string_view& str, std::string_view* key, std::string_view* value);
-129
View File
@@ -62,16 +62,6 @@ static uint32_t PackRGBA (uint8_t r, uint8_t g, uint8_t b, uint8_t a)
return r | (g << 8) | (b << 16) | (a << 24);
}
static float Int8ToFloat_SNORM (const uint8_t input)
{
return (float)((int8_t)input) / 127.0f;
}
static float Int8ToFloat_UNORM (const uint8_t input)
{
return (float)input / 255.0f;
}
/**
Decompress a BC 16x3 index block stored as
h g f e
@@ -350,125 +340,6 @@ void DecompressBlockBC2 (uint32_t x, uint32_t y, uint32_t stride,
image + x * sizeof (uint32_t) + (y * stride), stride, alphaValues);
}
static void DecompressBlockBC4Internal (
const uint8_t* block, unsigned char* output,
uint32_t outputStride, const float* colorTable)
{
uint8_t indices [16];
int x, y;
Decompress16x3bitIndices (block + 2, indices);
for (y = 0; y < 4; ++y) {
for (x = 0; x < 4; ++x) {
*(float*)(output + x * sizeof (float)) = colorTable [indices [y*4 + x]];
}
output += outputStride;
}
}
/*
Decompresses one block of a BC4 texture and stores the resulting pixels at the appropriate offset in 'image'.
uint32_t x: x-coordinate of the first pixel in the block.
uint32_t y: y-coordinate of the first pixel in the block.
uint32_t stride: stride of a scanline in bytes.
const uint8_t* blockStorage: pointer to the block to decompress.
float* image: pointer to image where the decompressed pixel data should be stored.
*/
void DecompressBlockBC4 (uint32_t x, uint32_t y, uint32_t stride, enum BC4Mode mode,
const uint8_t* blockStorage, unsigned char* image)
{
float colorTable [8];
float r0, r1;
if (mode == BC4_UNORM) {
r0 = Int8ToFloat_UNORM (blockStorage [0]);
r1 = Int8ToFloat_UNORM (blockStorage [1]);
colorTable [0] = r0;
colorTable [1] = r1;
if (r0 > r1) {
// 6 interpolated color values
colorTable [2] = (6*r0 + 1*r1)/7.0f; // bit code 010
colorTable [3] = (5*r0 + 2*r1)/7.0f; // bit code 011
colorTable [4] = (4*r0 + 3*r1)/7.0f; // bit code 100
colorTable [5] = (3*r0 + 4*r1)/7.0f; // bit code 101
colorTable [6] = (2*r0 + 5*r1)/7.0f; // bit code 110
colorTable [7] = (1*r0 + 6*r1)/7.0f; // bit code 111
} else {
// 4 interpolated color values
colorTable [2] = (4*r0 + 1*r1)/5.0f; // bit code 010
colorTable [3] = (3*r0 + 2*r1)/5.0f; // bit code 011
colorTable [4] = (2*r0 + 3*r1)/5.0f; // bit code 100
colorTable [5] = (1*r0 + 4*r1)/5.0f; // bit code 101
colorTable [6] = 0.0f; // bit code 110
colorTable [7] = 1.0f; // bit code 111
}
} else if (mode == BC4_SNORM) {
r0 = Int8ToFloat_SNORM (blockStorage [0]);
r1 = Int8ToFloat_SNORM (blockStorage [1]);
colorTable [0] = r0;
colorTable [1] = r1;
if (r0 > r1) {
// 6 interpolated color values
colorTable [2] = (6*r0 + 1*r1)/7.0f; // bit code 010
colorTable [3] = (5*r0 + 2*r1)/7.0f; // bit code 011
colorTable [4] = (4*r0 + 3*r1)/7.0f; // bit code 100
colorTable [5] = (3*r0 + 4*r1)/7.0f; // bit code 101
colorTable [6] = (2*r0 + 5*r1)/7.0f; // bit code 110
colorTable [7] = (1*r0 + 6*r1)/7.0f; // bit code 111
} else {
// 4 interpolated color values
colorTable [2] = (4*r0 + 1*r1)/5.0f; // bit code 010
colorTable [3] = (3*r0 + 2*r1)/5.0f; // bit code 011
colorTable [4] = (2*r0 + 3*r1)/5.0f; // bit code 100
colorTable [5] = (1*r0 + 4*r1)/5.0f; // bit code 101
colorTable [6] = -1.0f; // bit code 110
colorTable [7] = 1.0f; // bit code 111
}
}
DecompressBlockBC4Internal (blockStorage,
image + x * sizeof (float) + (y * stride), stride, colorTable);
}
/*
Decompresses one block of a BC5 texture and stores the resulting pixels at the appropriate offset in 'image'.
uint32_t x: x-coordinate of the first pixel in the block.
uint32_t y: y-coordinate of the first pixel in the block.
uint32_t stride: stride of a scanline in bytes.
const uint8_t* blockStorage: pointer to the block to decompress.
float* image: pointer to image where the decompressed pixel data should be stored.
*/
void DecompressBlockBC5 (uint32_t x, uint32_t y, uint32_t stride, enum BC5Mode mode,
const uint8_t* blockStorage, unsigned char* image)
{
// We decompress the two channels separately and interleave them when
// writing to the output
float c0 [16];
float c1 [16];
int dx, dy;
DecompressBlockBC4 (0, 0, 4 * sizeof (float), (enum BC4Mode)mode,
blockStorage, (unsigned char*)c0);
DecompressBlockBC4 (0, 0, 4 * sizeof (float), (enum BC4Mode)mode,
blockStorage + 8, (unsigned char*)c1);
for (dy = 0; dy < 4; ++dy) {
for (dx = 0; dx < 4; ++dx) {
*(float*)(image + stride * (y + dy) + ((x + dx) * 2 + 0) * sizeof (float)) = c0 [dy * 4 + dx];
*(float*)(image + stride * (y + dy) + ((x + dx) * 2 + 1) * sizeof (float)) = c1 [dy * 4 + dx];
}
}
}
// File: bc7decomp.c - Richard Geldreich, Jr. 3/31/2020 - MIT license or public domain (see end of file)
#include <string.h>
-16
View File
@@ -12,28 +12,12 @@
#include <algorithm>
#include <math.h>
enum BC4Mode
{
BC4_UNORM = 0,
BC4_SNORM = 1
};
enum BC5Mode
{
BC5_UNORM = 0,
BC5_SNORM = 1
};
void DecompressBlockBC1(uint32_t x, uint32_t y, uint32_t stride,
const uint8_t* blockStorage, unsigned char* image);
void DecompressBlockBC2(uint32_t x, uint32_t y, uint32_t stride,
const uint8_t* blockStorage, unsigned char* image);
void DecompressBlockBC3(uint32_t x, uint32_t y, uint32_t stride,
const uint8_t* blockStorage, unsigned char* image);
void DecompressBlockBC4(uint32_t x, uint32_t y, uint32_t stride,
enum BC4Mode mode, const uint8_t* blockStorage, unsigned char* image);
void DecompressBlockBC5(uint32_t x, uint32_t y, uint32_t stride,
enum BC5Mode mode, const uint8_t* blockStorage, unsigned char* image);
namespace bc7decomp
{
+66 -52
View File
@@ -16,6 +16,8 @@
#pragma once
#include <retro_atomic.h>
#include <rthreads/retro_eventcount.h>
#include <rthreads/retro_asym_eventcount.h>
#include "Pcsx2Defs.h"
#include "General.h"
@@ -72,7 +74,6 @@ namespace Threading
Mutex& operator=(const Mutex&) = delete;
void Lock();
void Unlock();
bool TryLock();
slock_t* Native() { return m_lock; }
};
@@ -246,7 +247,6 @@ namespace Threading
void Post();
/// Wait with a timeout. Returns true if the semaphore was
/// acquired, false on timeout.
bool WaitFor(u32 timeout_ms);
void Wait();
};
@@ -278,70 +278,84 @@ namespace Threading
/// - Processing thread loops on `WaitForWork()` followed by processing all work in the queue
/// - Threads adding work first add their work to the queue, then call `NotifyOfWork()`
///
/// This is an eventcount, and libretro-common ships one too --
/// retro_eventcount, in rthreads/. Measured on the producer side with
/// the consumer awake, which is what the EE pays per GS packet, this
/// one is cheaper: NotifyOfWork is 6.2 ns against 8.7-9.6 ns for
/// retro_eventcount_notify, because it is a single fetch_add where the
/// other does a read-modify-write and then a sequentially-consistent
/// load. It also carries what a plain eventcount does not -- WaitForEmpty,
/// WaitForWorkTimed, the DEAD state -- and MTGS depends on all three.
/// So it stays. The wake side was not compared; that needs two cores.
/// Built on two libretro-common eventcounts, one per direction.
///
/// Producer to worker -- "work was added" -- is retro_asym_eventcount.
/// Its notify is a release store and a relaxed load, no lock prefix,
/// and that is the whole reason for the change: NotifyOfWork is what
/// the EE pays per GS packet with the GS thread awake, and it measured
/// 6.4 ns as a fetch_add-based state machine against 1.2 ns here. The
/// symmetric retro_eventcount was measured too and is slower than the
/// old code, 9.6 ns, since it pays an RMW and a seq_cst load to serve
/// many producers; each WorkSema here has exactly one, so the
/// asymmetric one applies. The ordering it drops on the hot side is
/// paid on the cold one: WaitForWork's park calls retro_procbarrier
/// once before it sleeps, and where no barrier exists the object runs
/// the symmetric protocol for its whole life, decided at init.
///
/// Worker to producer -- "I have drained everything you told me about"
/// -- is the plain retro_eventcount. WaitForEmpty is once a frame, so
/// its cost does not matter, and its prepare/commit contract is what
/// the old STATE_FLAG_WAITING_EMPTY handshake was reinventing.
///
/// The rest of the old contract is kept exactly: a worker that Kill()s
/// itself makes every wait return at once and WaitForEmpty report
/// false; Reset() revives it; WaitForWorkTimed returns false only on a
/// timeout with nothing pending.
class WorkSema
{
/// Semaphore for sleeping the worker thread
KernelSemaphore m_sema;
/// Semaphore for sleeping thread waiting on worker queue empty
KernelSemaphore m_empty_sema;
/// Current state (see enum below)
retro_atomic_int_t m_state = RETRO_ATOMIC_INT_INITIALIZER(0);
/// Producer -> worker. Its epoch moves once per NotifyOfWork.
retro_asym_eventcount_t m_work;
/// Worker -> producer. Its epoch moves once per idle transition.
retro_eventcount_t m_empty;
/// The work epoch the worker has consumed up to. Worker-written,
/// producer-read in WaitForEmpty.
retro_atomic_int_t m_seen = RETRO_ATOMIC_INT_INITIALIZER(0);
/// 1 while the worker is between finding no work and being told of
/// some: spinning, parked, or about to be. Worker-written.
retro_atomic_int_t m_idle = RETRO_ATOMIC_INT_INITIALIZER(1);
/// Set by Kill, cleared by Reset.
retro_atomic_int_t m_dead = RETRO_ATOMIC_INT_INITIALIZER(0);
// Expected call frequency is NotifyOfWork > WaitForWork > WaitForEmpty
// So optimize states for fast NotifyOfWork
enum
{
/* Any <-2 state: STATE_DEAD: Thread has crashed and is awaiting revival */
STATE_SPINNING = -2, ///< Worker thread is spinning waiting for work
STATE_SLEEPING = -1, ///< Worker thread is sleeping on m_sema
STATE_RUNNING_0 = 0, ///< Worker thread is processing work, but no work has been added since it last checked for new work
/* Any >0 state: STATE_RUNNING_N: Worker thread is processing work, and work has been added since it last checked for new work */
STATE_FLAG_WAITING_EMPTY = 1 << 30, ///< Flag to indicate that a thread is sleeping on m_empty_sema (can be applied to any STATE_RUNNING)
};
/// The worker found nothing to do: publish that for WaitForEmpty.
void GoIdle();
/// The worker has taken everything up to `epoch`.
bool Consume(int epoch);
public:
/// Notify the worker thread that you've added new work to its queue
WorkSema();
~WorkSema();
WorkSema(const WorkSema&) = delete;
WorkSema& operator=(const WorkSema&) = delete;
/// Notify the worker of new work. One release store and one relaxed
/// load with the worker awake; a broadcast under a lock only when it
/// is parked.
void NotifyOfWork()
{
// State change:
// DEAD: Stay in DEAD (starting DEAD state is INT_MIN so we can assume we won't flip over to anything else)
// SPINNING: Change state to RUNNING. Thread will notice and process the new data
// SLEEPING: Change state to RUNNING and wake worker. Thread will wake up and process the new data.
// RUNNING_0: Change state to RUNNING_N.
// RUNNING_N: Stay in RUNNING_N
s32 old = retro_atomic_fetch_add_int(&m_state, 2);
if (old == STATE_SLEEPING)
m_sema.Post();
retro_asym_eventcount_notify(&m_work);
}
/// Checks if there's any work in the queue
/// Checks if there's any work to do without blocking. Returns true if
/// work was added since the last check or wait; a false return means
/// the worker is idle and WaitForEmpty callers are told so.
bool CheckForWork();
/// Wait for work to be added to the queue
/// Wait for work to be added. Spins for SpinBudget() iterations
/// first, then parks -- the one place the process-wide barrier is
/// paid.
void WaitForWork();
/// Like WaitForWork, but parks for at most timeout_ms. Returns
/// false on timeout with the state restored to RUNNING_0, so the
/// caller can bail out and try again later; the SLEEPING ->
/// parked-on-sema invariant that WaitForEmpty relies on is never
/// violated by the early exit. For consumers that must not block
/// unboundedly (the libretro frontend thread).
/// As WaitForWork, but gives up after `timeout_ms` with nothing
/// pending. Returns false only in that case: true means work is
/// pending or the worker is dead, and the caller must process.
bool WaitForWorkTimed(u32 timeout_ms);
/// Wait for the worker thread to finish processing all entries in the queue or die
/// Returns false if the thread is dead
/// Wait until the worker has consumed every notification so far and
/// gone idle. Returns false if the worker is dead, in which case work
/// may still be queued.
bool WaitForEmpty();
/// Called by the worker thread to notify others of its death
/// Dead threads don't process work, and WaitForEmpty will return instantly even though there may be work in the queue
/// Dead threads don't process work, and WaitForEmpty will return
/// instantly even though there may be work in the queue.
void Kill();
/// Reset the semaphore to the initial state
/// Should be called by the worker thread if it restarts after dying
/// Reset to the initial state: alive, idle, nothing pending.
void Reset();
};
-2
View File
@@ -402,8 +402,6 @@ Threading::Mutex::Mutex() { m_lock = slock_new(); }
Threading::Mutex::~Mutex() { slock_free(m_lock); }
void Threading::Mutex::Lock() { slock_lock(m_lock); }
void Threading::Mutex::Unlock() { slock_unlock(m_lock); }
bool Threading::Mutex::TryLock() { return slock_try_lock(m_lock); }
Threading::RecursiveMutex::RecursiveMutex()
: m_owner(0)
, m_depth(0)
@@ -1580,3 +1580,69 @@ end:
}
#endif
}
/* Sleep until cpu_features_get_time_usec() reads at least @deadline.
* It lives here, beside the clock it is measured on, and nowhere a
* launcher compiles: the salamanders build rtime.c for localtime alone
* and never link this file.
*
* Absolute where the platform offers it, so the time between reading
* the clock and entering the kernel, and any early or interrupted
* wake, are not added to when the caller comes back. Never early
* against that clock on any backend; late as every sleep may be, and
* a caller that needs the instant itself sleeps short and spins the
* rest, as the frame limiter does with its measured margin. */
#if (defined(__linux__) || defined(ANDROID)) && !defined(__MACH__)
/* The exact tool: the clock above is clock_gettime(CLOCK_MONOTONIC)
* here, and clock_nanosleep() takes an absolute deadline on the same
* clock. EINTR re-arms against the unchanged deadline by definition
* of TIMER_ABSTIME. */
void retro_sleep_until_us(retro_time_t deadline)
{
struct timespec ts;
ts.tv_sec = (time_t)(deadline / 1000000);
ts.tv_nsec = (long)((deadline % 1000000) * 1000);
while (clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &ts, NULL) != 0)
;
}
#elif defined(__APPLE__) && defined(__MACH__)
#include <mach/mach_time.h>
/* mach_wait_until() is the absolute wait, on the Mach clock; the
* public microsecond clock above is CLOCK_MONOTONIC, which Darwin
* derives from the same hardware ticks. The two are bridged with one
* paired read - nanoseconds apart - and unlike a relative sleep the
* arming is still absolute in Mach terms, so an early wake or the
* kernel's leeway re-arms nothing and accumulates nothing. */
void retro_sleep_until_us(retro_time_t deadline)
{
static mach_timebase_info_data_t tb;
uint64_t ticks;
retro_time_t now = cpu_features_get_time_usec();
uint64_t mach_now = mach_absolute_time();
if (deadline <= now)
return;
/* A constant; a racing first read fills in the same values. */
if (!tb.denom)
mach_timebase_info(&tb);
ticks = (uint64_t)(deadline - now) * 1000 * tb.denom / tb.numer;
mach_wait_until(mach_now + ticks);
}
#else
/* Windows and everything else: re-arm the platform's best relative
* wait - retro_sleep_us from retro_timers.h, included above on every
* platform - against the deadline until the clock agrees. On desktop
* Windows that wait is rtime.c's per-thread high-resolution timer, so
* each lap is microsecond-grained; on a platform whose sleep rounds
* up the loop ends one lap past the deadline, no worse than the
* relative call was. */
void retro_sleep_until_us(retro_time_t deadline)
{
for (;;)
{
retro_time_t now = cpu_features_get_time_usec();
if (now >= deadline)
return;
retro_sleep_us((unsigned)(deadline - now));
}
}
#endif
@@ -60,6 +60,19 @@ retro_perf_tick_t cpu_features_get_perf_counter(void);
*/
retro_time_t cpu_features_get_time_usec(void);
/**
* retro_sleep_until_us:
* @deadline : an instant on cpu_features_get_time_usec()'s clock.
*
* Sleeps until that clock reads at least @deadline. Absolute where the
* platform offers it (clock_nanosleep TIMER_ABSTIME on Linux and
* Android, mach_wait_until on Darwin), a re-armed high-resolution
* relative wait elsewhere. Never early against that clock; may be
* late, as every sleep may. Not available to the salamander launchers,
* which do not link features_cpu.c.
**/
void retro_sleep_until_us(retro_time_t deadline);
/**
* Returns the available features (mostly SIMD extensions)
* supported by this CPU.
@@ -824,7 +824,8 @@ typedef volatile intptr_t retro_atomic_size_t;
#define RETRO_ATOMIC_INT_INITIALIZER(v) (v)
#define retro_atomic_size_init(p, v) (*(p) = (intptr_t)(v))
#define retro_atomic_load_acquire_int(p) OSAtomicAdd32Barrier(0, (p))
#define retro_atomic_load_acquire_int(p) \
OSAtomicAdd32Barrier(0, (retro_atomic_int_t*)(p))
/* Relaxed int load/store: aligned native-width volatile access, which
* is indivisible on every Apple target; unlike the barrier'd Add(0)
* above it is neither an RMW nor a barrier. */
@@ -887,7 +888,7 @@ typedef volatile size_t retro_atomic_size_t;
* The "load via fetch_and_add 0" / "store via lock+swap" idioms are the
* canonical way to get an atomic load/store out of __sync. */
#define retro_atomic_load_acquire_int(p) \
__sync_fetch_and_add((p), 0)
__sync_fetch_and_add((retro_atomic_int_t*)(p), 0)
/* Relaxed int load/store: aligned volatile int access, no RMW and no
* __sync_synchronize. */
#define retro_atomic_load_relaxed_int(p) (*(p))
@@ -904,7 +905,7 @@ typedef volatile size_t retro_atomic_size_t;
__sync_fetch_and_and((p), (v))
#define retro_atomic_load_acquire_size(p) \
__sync_fetch_and_add((p), (size_t)0)
__sync_fetch_and_add((retro_atomic_size_t*)(p), (size_t)0)
/* Relaxed load: aligned volatile size_t read, no RMW. */
#define retro_atomic_load_relaxed_size(p) (*(p))
#define retro_atomic_store_relaxed_size(p, v) \
@@ -0,0 +1,115 @@
/* Copyright (C) 2010-2026 The RetroArch team
*
* ---------------------------------------------------------------------------------------
* The following license statement only applies to this file (retro_asym_eventcount.h).
* ---------------------------------------------------------------------------------------
*
* Permission is hereby granted, free of charge,
* to any person obtaining a copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
* and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
* OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#ifndef __LIBRETRO_SDK_ASYM_EVENTCOUNT_H
#define __LIBRETRO_SDK_ASYM_EVENTCOUNT_H
#include <stdint.h>
#include <boolean.h>
#include <retro_common_api.h>
#include <retro_atomic.h>
RETRO_BEGIN_DECLS
/*
* retro_asym_eventcount: an eventcount whose notify costs a plain store.
*
* Same shape and same contract as retro_eventcount -- notify on one side,
* prepare / commit / cancel on the other, the epoch as the handshake --
* with one difference in where the ordering is paid for.
*
* retro_eventcount's notify is a sequentially-consistent read-modify-write
* followed by a sequentially-consistent load: on x86 a locked instruction
* on every publish, so that a notify and a registering waiter can never
* each miss the other. That is the right price for a general primitive
* with any number of producers.
*
* This one moves the price to the waiter. notify is a release store and a
* relaxed load -- three ordinary moves on x86, no lock prefix, no fence.
* prepare_wait, after registering, calls retro_procbarrier(), which makes
* every other running thread pass through a full fence. So either the
* producer's store to the epoch was executed before the barrier and the
* waiter's re-check sees it, or it was executed after, in which case the
* waiter's registration is already visible and the producer's load finds
* it and wakes. The lost-wakeup window is closed from the cold side.
*
* Measured on the producer: 6.2 ns for a fetch_add-based notify against
* 2.6 ns for this, per publish, with the consumer awake. That is the
* whole point, and it only pays off when the consumer is awake far more
* often than it parks -- a render thread fed by a core thread, say.
*
* TWO CONDITIONS, BOTH ENFORCED BY THE CALLER
*
* Single producer. A plain store cannot serve concurrent notifiers; two
* of them would lose each other's increments. If more than one thread
* can notify, use retro_eventcount.
*
* Never call retro_procbarrier on the hot path. It is a syscall at best
* and a signal per running thread at worst. This primitive calls it in
* prepare_wait and nowhere else, which is the cold side by construction.
* If the consumer parks at a high rate, the barrier's cost -- which
* grows with core count -- can exceed what the producer saved. Spin
* before parking; retro_eventcount_spin_iters() gives a sane budget.
*
* WHEN THERE IS NO BARRIER
*
* retro_procbarrier() returns 0 on platforms with nothing -- the
* multi-core consoles -- and this primitive then behaves exactly as
* retro_eventcount does: notify becomes the sequentially-consistent pair.
* The decision is made once at init and stays for the object's life, so
* a caller never has to know which mode it is in; it is only slower.
* retro_asym_eventcount_is_asymmetric() reports it, for logs.
*/
typedef struct retro_asym_eventcount
{
struct slock *lock;
struct scond *cond;
retro_atomic_int_t epoch; /* bumped once per notify */
retro_atomic_int_t waiters; /* threads inside a prepare/commit */
int asymmetric; /* 1: barrier-backed; 0: seq_cst pair */
} retro_asym_eventcount_t;
bool retro_asym_eventcount_init(retro_asym_eventcount_t *ec);
void retro_asym_eventcount_free(retro_asym_eventcount_t *ec);
/* Producer side. A release store and a relaxed load when asymmetric.
* Wakes parked waiters if there are any. */
void retro_asym_eventcount_notify(retro_asym_eventcount_t *ec);
/* Consumer side, three phases as in retro_eventcount:
* key = prepare_wait(ec); register, barrier, snapshot the epoch
* if (work available) { cancel_wait(ec); consume; }
* else commit_wait(ec, key); park until the epoch moves past key
* The predicate check between prepare and commit is the caller's; the
* barrier in prepare is what makes it safe. */
int retro_asym_eventcount_prepare_wait(retro_asym_eventcount_t *ec);
void retro_asym_eventcount_cancel_wait(retro_asym_eventcount_t *ec);
void retro_asym_eventcount_commit_wait(retro_asym_eventcount_t *ec, int key);
bool retro_asym_eventcount_commit_wait_timeout(retro_asym_eventcount_t *ec,
int key, int64_t timeout_us);
/* Whether init resolved a process-wide barrier. 0 means the object is
* running the symmetric protocol. */
int retro_asym_eventcount_is_asymmetric(const retro_asym_eventcount_t *ec);
RETRO_END_DECLS
#endif
@@ -0,0 +1,260 @@
/* Copyright (C) 2010-2026 The RetroArch team
*
* ---------------------------------------------------------------------------------------
* The following license statement only applies to this file (retro_eventcount.h).
* ---------------------------------------------------------------------------------------
*
* Permission is hereby granted, free of charge,
* to any person obtaining a copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
* and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#ifndef __LIBRETRO_SDK_EVENTCOUNT_H
#define __LIBRETRO_SDK_EVENTCOUNT_H
/* retro_eventcount.h - park a consumer without putting a lock on the
* producer's fast path.
*
* A lock-free queue answers "is there work?" without a lock, but says
* nothing about what a consumer should do when the answer is no.
* Spinning burns a core; a condition variable puts a lock acquisition on
* every publish whether or not anyone is asleep. An eventcount is the
* third answer: the consumer registers its intent to sleep, re-checks
* its own condition, and only then blocks, and the producer pays one
* sequentially-consistent read-modify-write and one sequentially-
* consistent load when nobody is parked - no lock, no syscall.
*
* The pairing this exists for is retro_spsc plus retro_atomic: the queue
* moves the data, a retired-work counter published by the consumer
* answers "has it caught up", and this answers "wake me when it has not".
*
* Usage
* -----
* The consumer owns the loop and its own predicate, so the predicate is
* never a callback and never an indirect call in a spin:
*
* for (;;)
* {
* int key;
*
* if (have_work())
* break;
*
* key = retro_eventcount_prepare_wait(&ec);
*
* if (have_work())
* {
* retro_eventcount_cancel_wait(&ec);
* break;
* }
*
* retro_eventcount_commit_wait(&ec, key);
* }
*
* and the producer, after publishing:
*
* retro_eventcount_notify(&ec);
*
* commit_wait may return without the condition having changed, exactly
* as a condition variable may, which is why the shape above is a loop.
*
* Rules
* -----
* 1. Every retro_eventcount_prepare_wait() must be answered by exactly
* one retro_eventcount_commit_wait() or retro_eventcount_cancel_wait()
* on the same thread.
*
* 2. No lock is held across that window on any backend, so the window
* is yours: evaluating a predicate that touches other objects is
* fine. Keep it short anyway. A notifier that sees a registered
* waiter does the wake work whether or not the waiter goes on to
* sleep, so a long window buys a notifier pointless syscalls.
*
* 3. Any number of threads may notify. Any number may wait; a notify
* releases all of them. That holds on every backend, including the
* ones whose atomics are not lock-free, where the bookkeeping is
* kept under a mutex because their read-modify-writes are not
* indivisible.
*
* 4. The window between prepare_wait and its answer must not span 2^32
* notifications, or the key could match a different epoch than the
* one it named. Keeping the window short, which rule 2 asks for
* anyway, is several orders of magnitude more than enough.
*
* 5. A consumer that spins before parking must gate the spin on
* RETRO_ATOMIC_LOCK_FREE, not merely on this header existing. On a
* backend where the atomics are real but not lock-free -- the PS2 EE
* masks interrupts around a read-modify-write and reschedules only
* out of an interrupt -- a spinning thread never lets the thread it
* waits for run. Parking through this object is fine there; spinning
* first is not.
*
* Backends
* --------
* Linux / Android futex(FUTEX_WAIT_PRIVATE), no lock at all
* Windows a waiter list of stack blocks, slept on with the
* best primitive ntdll offers, resolved at runtime:
* NtWaitForAlertByThreadId on 8 and newer,
* NtWaitForKeyedEvent back to XP, and a per-thread
* auto-reset event on anything older, including 9x.
* No mutex on any tier. If none of the three can
* be had - no ntdll entry points and no TLS index
* left for the event - the object falls back to
* the scond backend below rather than failing.
* everything else rthreads scond, with the lock taken only across
* the sleep itself
*
* The scond backend is not a degraded mode; it is correct and it is what
* macOS, the BSDs and the console ports use. What it costs is one lock
* acquisition per notify while a consumer is parked, which is the case
* where a syscall was going to happen anyway.
*
* Where the atomics themselves are not lock-free, the handshake that
* lets notify skip the lock cannot be relied on, so that build takes the
* lock on every notify instead. retro_eventcount_backend_name() reports
* which of these is live.
*/
#include <retro_common_api.h>
#include <retro_atomic.h>
#include <boolean.h>
#include <stdint.h>
RETRO_BEGIN_DECLS
/* Spelled as bare struct pointers rather than slock_t / scond_t so this
* header needs no include of rthreads.h and repeats no typedef: a
* translation unit that includes both is then legal C89. */
typedef struct retro_eventcount
{
struct slock *lock; /* NULL unless the backend needs a condvar */
struct scond *cond; /* NULL unless the backend needs a condvar */
#if defined(_WIN32) && !defined(_XBOX) && defined(RETRO_ATOMIC_HAS_PTR)
/* Win32 keeps its own waiter list: the blocks live on the waiters'
* stacks and the low bit of the head is the list's spin lock. No
* caller mutex is involved, which is the whole point of it -- a
* condition variable would re-acquire one on every wake. */
retro_atomic_ptr_t waitlist;
#endif
retro_atomic_int_t epoch; /* bumped once per notify */
retro_atomic_int_t waiters; /* threads inside a prepare/commit window */
} retro_eventcount_t;
/**
* retro_eventcount_init:
* @ec : object to initialise.
*
* Brings @ec up in the unsignalled state with no waiters. Allocates
* only on the backends that need a mutex and a condition variable.
*
* @return true on success. On failure @ec is left safe to pass to
* retro_eventcount_free().
*/
bool retro_eventcount_init(retro_eventcount_t *ec);
/**
* retro_eventcount_free:
* @ec : object to release.
*
* Releases whatever init allocated. No thread may be inside any other
* call on @ec. Safe on a zeroed object and on one init failed for.
*/
void retro_eventcount_free(retro_eventcount_t *ec);
/**
* retro_eventcount_notify:
* @ec : object to signal.
*
* Releases every thread currently waiting on @ec, and causes any
* prepare_wait that has already run to return without blocking. Call
* after the work is published, never before.
*
* With nobody parked this is one sequentially-consistent
* read-modify-write and one sequentially-consistent load: no lock, no
* syscall.
*/
void retro_eventcount_notify(retro_eventcount_t *ec);
/**
* retro_eventcount_prepare_wait:
* @ec : object to wait on.
*
* Opens a wait window and returns the key that names the current state
* of @ec. Re-check your predicate after this returns: a notify that
* lands from here on is guaranteed either to be visible to that check
* or to make the matching commit_wait return immediately.
*
* @return the key to hand to retro_eventcount_commit_wait().
*/
int retro_eventcount_prepare_wait(retro_eventcount_t *ec);
/**
* retro_eventcount_cancel_wait:
* @ec : object whose wait window to close.
*
* Closes the window opened by retro_eventcount_prepare_wait() without
* blocking. Use when the re-check found work.
*/
void retro_eventcount_cancel_wait(retro_eventcount_t *ec);
/**
* retro_eventcount_commit_wait:
* @ec : object to block on.
* @key : value returned by the matching retro_eventcount_prepare_wait().
*
* Blocks until @ec is notified, and closes the wait window. May return
* early and spuriously, so callers loop.
*/
void retro_eventcount_commit_wait(retro_eventcount_t *ec, int key);
/**
* retro_eventcount_commit_wait_timeout:
* @ec : object to block on.
* @key : value returned by the matching prepare_wait().
* @timeout_us : how long to block for, in microseconds. Zero or less
* polls. A bound long enough to overflow the backend's
* own unit is clamped, never wrapped.
*
* As retro_eventcount_commit_wait(), bounded in time.
*
* @return true if @ec was notified, false if the bound expired. A
* spurious early return reports true, so the caller's loop still owns
* the decision.
*/
bool retro_eventcount_commit_wait_timeout(retro_eventcount_t *ec,
int key, int64_t timeout_us);
/**
* retro_eventcount_spin_iters:
*
* @return how many times a waiter spins on its flag word before it
* commits to the kernel, as this build resolved it. Zero on a
* uniprocessor, where the spin is skipped. For logs and benchmarks;
* on Windows it is only meaningful after the first
* retro_eventcount_init().
*/
unsigned retro_eventcount_spin_iters(void);
/**
* retro_eventcount_backend_name:
*
* @return a string literal naming the live parking backend, for logs
* and CI. On Windows this reflects the runtime probe, so it is only
* meaningful after the first retro_eventcount_init().
*/
const char *retro_eventcount_backend_name(void);
RETRO_END_DECLS
#endif
@@ -0,0 +1,134 @@
/* Copyright (C) 2010-2026 The RetroArch team
*
* ---------------------------------------------------------------------------------------
* The following license statement only applies to this file (retro_procbarrier.h).
* ---------------------------------------------------------------------------------------
*
* Permission is hereby granted, free of charge,
* to any person obtaining a copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
* and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
* OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#ifndef __LIBRETRO_SDK_PROCBARRIER_H
#define __LIBRETRO_SDK_PROCBARRIER_H
#include <retro_common_api.h>
RETRO_BEGIN_DECLS
/*
* retro_procbarrier: a process-wide memory barrier, resolved at runtime.
*
* WHAT IT IS FOR
*
* A normal fence orders the calling thread's own memory operations. A
* process-wide barrier does more: when it returns, every other thread of
* this process that was running has passed through a full fence too, so
* any store one of them made before this call is visible to the caller
* afterwards. That lets a handshake be asymmetric. The side that runs
* constantly -- a producer publishing work -- can use a plain store and
* a plain load, with no locked instruction and no fence, while the side
* that runs rarely -- a consumer about to park -- pays for this call
* once before it commits to sleeping. The producer's store cannot slip
* past the consumer's decision, because the barrier drains it first.
*
* On a producer/consumer pair that is exactly the shape of a core thread
* pushing frames to a render thread, and the difference on the producer
* side is a locked RMW against three ordinary moves.
*
* WHAT IT COSTS
*
* Nothing in user mode can make another core execute a fence, so every
* implementation of this is a request to the kernel to interrupt the
* other cores. It is a syscall at best and a signal per thread at worst,
* so it belongs on the cold side of a protocol only. Never call it on a
* hot path.
*
* HOW IT IS IMPLEMENTED
*
* By whichever of these the running system supports, chosen once at
* retro_procbarrier_init():
*
* RETRO_PROCBARRIER_UNIPROCESSOR
* One CPU: a store is globally visible with nothing to reorder
* against, so the barrier is a compiler fence and nothing else.
*
* RETRO_PROCBARRIER_MEMBARRIER
* Linux 4.14+, FreeBSD 14.1+: membarrier(MEMBARRIER_CMD_PRIVATE_
* EXPEDITED). The kernel sends an IPI to exactly the CPUs currently
* running a thread of this process. The best tier where it exists.
*
* RETRO_PROCBARRIER_FLUSHWRITEBUFFERS
* Windows Vista+: FlushProcessWriteBuffers(). One syscall; the
* kernel IPIs the process's affinity. Equivalent to membarrier.
*
* RETRO_PROCBARRIER_PAGEFLIP
* x86 only, any Linux, any macOS on Intel, Windows back to 2000:
* change the protection of a locked dummy page. The kernel must
* invalidate its TLB entry on every CPU the address space is active
* on, and on x86 that invalidation travels as an IPI whose handler
* is a serialising event. The barrier is a side effect and is not
* documented anywhere; it is nonetheless what .NET and the JVM used
* before the kernels grew a real call. It is REFUSED on non-x86: ARM
* and PowerPC broadcast TLB invalidation without an IPI, and the
* trick fences nothing there.
*
* RETRO_PROCBARRIER_SIGNAL
* Any Linux, any macOS including Apple Silicon, any Android: send a
* signal to every thread currently running on another CPU and wait
* for each to acknowledge from its handler. The kernel delivers a
* signal to a running thread by IPI, and entering the kernel is a
* fence on every architecture. Threads that are not on a CPU are
* skipped -- a context switch already drained them. Cost is a
* syscall per running thread; correct everywhere, fastest nowhere.
*
* RETRO_PROCBARRIER_NONE
* Nothing available. retro_procbarrier() returns 0 and the caller
* must use a symmetric primitive instead. This is where the
* multi-core consoles land.
*
* The signal tier needs a signal number. The default is SIGRTMAX-1 where
* real-time signals exist and SIGUSR2 otherwise; pass another to init if
* the application uses those.
*/
enum retro_procbarrier_tier
{
RETRO_PROCBARRIER_NONE = 0,
RETRO_PROCBARRIER_UNIPROCESSOR,
RETRO_PROCBARRIER_MEMBARRIER,
RETRO_PROCBARRIER_FLUSHWRITEBUFFERS,
RETRO_PROCBARRIER_PAGEFLIP,
RETRO_PROCBARRIER_SIGNAL
};
/* Resolve the tier for this process. Safe to call more than once; the
* first call wins. signum is only used by the signal tier; pass 0 for
* the default. Returns the tier selected. */
enum retro_procbarrier_tier retro_procbarrier_init(int signum);
/* The tier in use, without initialising. RETRO_PROCBARRIER_NONE before
* init has run. */
enum retro_procbarrier_tier retro_procbarrier_tier(void);
/* A human-readable name for a tier, for logs. */
const char *retro_procbarrier_tier_name(enum retro_procbarrier_tier tier);
/* Issue a process-wide barrier. Returns 1 if one was issued (or was
* unnecessary), 0 if this system cannot issue one -- in which case the
* caller must not rely on asymmetric ordering. Initialises on first use
* with the default signal if init was not called. */
int retro_procbarrier(void);
RETRO_END_DECLS
#endif
@@ -0,0 +1,183 @@
/* Copyright (C) 2010-2026 The RetroArch team
*
* ---------------------------------------------------------------------------------------
* The following license statement only applies to this file (retro_asym_eventcount.c).
* ---------------------------------------------------------------------------------------
*
* Permission is hereby granted, free of charge,
* to any person obtaining a copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
* and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
* OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
/* See the header for the design. This file is the two protocols and the
* park.
*
* The park is a condition variable everywhere. That is deliberate for a
* first version: it is the cold side, a condvar is portable to every
* backend rthreads has, and the correctness argument is short --
* commit_wait re-checks the epoch under the lock and notify broadcasts
* under the same lock, so a wake cannot fall between the check and the
* sleep. A futex park on the epoch word would save a lock on the wake,
* and can replace this once the primitive has been on real SMP hardware.
*/
#include <stdlib.h>
#include <string.h>
#include <rthreads/rthreads.h>
#include <rthreads/retro_procbarrier.h>
#include <rthreads/retro_asym_eventcount.h>
bool retro_asym_eventcount_init(retro_asym_eventcount_t *ec)
{
if (!ec)
return false;
memset(ec, 0, sizeof(*ec));
ec->lock = slock_new();
if (!ec->lock)
return false;
ec->cond = scond_new();
if (!ec->cond)
{
slock_free(ec->lock);
ec->lock = NULL;
return false;
}
retro_atomic_store_relaxed_int(&ec->epoch, 0);
retro_atomic_store_relaxed_int(&ec->waiters, 0);
/* Decide the protocol once. A platform with no process-wide barrier
* runs the symmetric pair for this object's whole life; alternating
* would leave one side ordered and the other not. */
ec->asymmetric = (retro_procbarrier_init(0) != RETRO_PROCBARRIER_NONE);
return true;
}
void retro_asym_eventcount_free(retro_asym_eventcount_t *ec)
{
if (!ec)
return;
if (ec->cond)
scond_free(ec->cond);
if (ec->lock)
slock_free(ec->lock);
ec->cond = NULL;
ec->lock = NULL;
}
int retro_asym_eventcount_is_asymmetric(const retro_asym_eventcount_t *ec)
{
return ec ? ec->asymmetric : 0;
}
/* ------------------------------------------------------------------ */
/* Producer */
/* ------------------------------------------------------------------ */
void retro_asym_eventcount_notify(retro_asym_eventcount_t *ec)
{
if (ec->asymmetric)
{
/* Release, so the data published before this call is visible to a
* waiter that acquires the new epoch. Plain otherwise: no lock
* prefix, no fence. The single-producer condition in the header is
* what makes the non-atomic increment sound. */
int e = retro_atomic_load_relaxed_int(&ec->epoch);
retro_atomic_store_release_int(&ec->epoch, (int)((unsigned)e + 1u));
/* Relaxed. The barrier in prepare_wait is what orders this against
* a waiter's registration; a fence here would put the cost back
* where this primitive exists to remove it. */
if (retro_atomic_load_relaxed_int(&ec->waiters) == 0)
return;
}
else
{
/* No barrier available: the symmetric pair, exactly as
* retro_eventcount does it. */
retro_atomic_fetch_add_seq_cst_int(&ec->epoch, 1);
if (retro_atomic_load_seq_cst_int(&ec->waiters) == 0)
return;
}
/* Someone is inside a prepare/commit window. Broadcast under the
* lock, which is what pairs with commit_wait's check-under-lock. */
slock_lock(ec->lock);
scond_broadcast(ec->cond);
slock_unlock(ec->lock);
}
/* ------------------------------------------------------------------ */
/* Consumer */
/* ------------------------------------------------------------------ */
int retro_asym_eventcount_prepare_wait(retro_asym_eventcount_t *ec)
{
if (ec->asymmetric)
{
/* Register with a plain increment: the barrier below is what
* publishes it, and the barrier is also a full fence for this
* thread on every tier, so the store cannot linger in this core's
* buffer past it. */
retro_atomic_fetch_add_int(&ec->waiters, 1);
/* The whole design in one call. Every other running thread passes
* through a full fence before this returns, so a producer store to
* the epoch that executed before now is visible to the load below,
* and a producer that stores after now will find our registration
* when it loads waiters. */
retro_procbarrier();
/* Belt and braces on the caller's own core, in case a tier ever
* fails to serialise the calling thread. Cold path; costs nothing
* that matters. */
retro_atomic_thread_fence_seq_cst();
return retro_atomic_load_acquire_int(&ec->epoch);
}
/* Symmetric: mirror of the notify pair. */
retro_atomic_fetch_add_seq_cst_int(&ec->waiters, 1);
return retro_atomic_load_seq_cst_int(&ec->epoch);
}
void retro_asym_eventcount_cancel_wait(retro_asym_eventcount_t *ec)
{
retro_atomic_fetch_sub_int(&ec->waiters, 1);
}
void retro_asym_eventcount_commit_wait(retro_asym_eventcount_t *ec, int key)
{
/* The lock covers the re-check and the sleep together. A notify that
* bumped the epoch before this acquires the lock is seen by the
* re-check; one that bumps after it must take the lock to broadcast,
* and so cannot slip between the check and the wait. */
slock_lock(ec->lock);
while (retro_atomic_load_acquire_int(&ec->epoch) == key)
scond_wait(ec->cond, ec->lock);
retro_atomic_fetch_sub_int(&ec->waiters, 1);
slock_unlock(ec->lock);
}
bool retro_asym_eventcount_commit_wait_timeout(retro_asym_eventcount_t *ec,
int key, int64_t timeout_us)
{
bool woken = true;
slock_lock(ec->lock);
if (retro_atomic_load_acquire_int(&ec->epoch) == key)
woken = scond_wait_timeout(ec->cond, ec->lock, timeout_us);
retro_atomic_fetch_sub_int(&ec->waiters, 1);
slock_unlock(ec->lock);
return woken;
}
@@ -0,0 +1,857 @@
/* Copyright (C) 2010-2026 The RetroArch team
*
* ---------------------------------------------------------------------------------------
* The following license statement only applies to this file (retro_eventcount.c).
* ---------------------------------------------------------------------------------------
*
* Permission is hereby granted, free of charge,
* to any person obtaining a copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
* and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
/* struct timespec and syscall() are POSIX/glibc surface that a strict
* C89 compile does not expose by default; libretro-common builds as
* C89, and rthreads.c asks for the same baseline for the same reason.
*
* Guarded the way rthreads.c guards its own, and not only for symmetry.
* Darwin does not need it - the surface is visible there by default -
* and asking for it does harm: _POSIX_C_SOURCE lowers
* __DARWIN_C_LEVEL, which hides the BSD names. In a normal build that
* would stop at the end of this file, but griffin is one translation
* unit, so a define made here applies to every file included after it.
* IFF_UP in net/if.h and RTLD_DEFAULT in dlfcn.h are two that then
* vanish, in files that never asked for any of this. */
#if defined(__unix__) && !defined(__APPLE__) && !defined(__sun__)
#ifndef _POSIX_C_SOURCE
#define _POSIX_C_SOURCE 199309
#endif
#endif
#if defined(__linux__) && !defined(_GNU_SOURCE)
#define _GNU_SOURCE
#endif
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <rthreads/retro_eventcount.h>
#include <rthreads/rthreads.h>
/* The address-wait backends hand the kernel a bare 32-bit word, so the
* epoch must be exactly that. C89 has no static assertion; a negative
* array bound is the portable spelling. */
typedef char retro_eventcount_epoch_is_a_word_
[(sizeof(retro_atomic_int_t) == sizeof(int)) ? 1 : -1];
/* Backend selection.
*
* RETRO_EC_ADDR_LINUX and RETRO_EC_ADDR_WIN32 name builds that can park
* a thread on a bare word. Both still need the atomics to be lock-free
* for the handshake in notify to hold, so a build that lands on the
* volatile fallback drops to the locked backend regardless of platform.
*
* RETRO_EVENTCOUNT_FORCE_SCOND selects the condition-variable backend on
* any target, so a host with futex can still build and test the path the
* console and Apple ports take. It mirrors retro_atomic.h's
* RETRO_ATOMIC_FORCE_* overrides and exists for the same reason.
*/
#if defined(RETRO_ATOMIC_LOCK_FREE) && !defined(RETRO_EVENTCOUNT_FORCE_SCOND)
#if defined(__linux__) && !defined(ANDROID_NO_FUTEX)
#define RETRO_EC_ADDR_LINUX 1
#elif defined(_WIN32) && !defined(_XBOX) && defined(RETRO_ATOMIC_HAS_PTR)
#define RETRO_EC_ADDR_WIN32 1
#endif
#endif
/* Where the atomics are not lock-free their read-modify-writes are not
* atomic either -- the volatile backend spells fetch-add as a plain
* load, add and store -- so two notifiers can lose an epoch bump and
* two waiters can lose a count. The epoch and the waiter count are
* then kept under the mutex like everything else, which is what makes
* the any-number-of-waiters, any-number-of-notifiers promise in the
* header true on those builds as well. Nothing is held across the
* caller's predicate window either way.
*
* The PS2 backend is atomic but deliberately not lock-free, so it
* takes the lock too; it has one core, where it is uncontended. */
#if !defined(RETRO_ATOMIC_LOCK_FREE)
#define RETRO_EC_LOCKED_BOOKKEEPING 1
#endif
#if defined(RETRO_EC_ADDR_LINUX)
#include <unistd.h>
#include <limits.h>
#include <errno.h>
#include <sys/syscall.h>
#include <linux/futex.h>
/* Older kernel headers predate the _PRIVATE flags; the syscall has
* carried them since 2.6.22 and the numbers are ABI. */
#ifndef FUTEX_WAIT_PRIVATE
#define FUTEX_WAIT_PRIVATE 128
#endif
#ifndef FUTEX_WAKE_PRIVATE
#define FUTEX_WAKE_PRIVATE 129
#endif
#endif
#if defined(RETRO_EC_ADDR_WIN32)
#include <windows.h>
/* Windows has no one primitive that reaches every version, so the
* waiter list is ours and only the sleep is delegated. Three tiers,
* resolved once from ntdll:
*
* ALERT NtWaitForAlertByThreadId / NtAlertThreadByThreadId, Windows 8
* and newer. A per-thread wakeup that sticks if it arrives
* before the wait, so no rendezvous accounting is needed.
* KEYED NtWaitForKeyedEvent / NtReleaseKeyedEvent, Windows XP and
* newer. A rendezvous: a release blocks until a waiter with
* the same key arrives, so a waker may only release for a
* block that has committed to sleeping. The ASLEEP flag below
* is what makes that exact.
* EVENT One auto-reset event per thread, kept in TLS for the
* thread's life. Works everywhere, including 9x.
*
* rthreads' scond resolves the same three and its protocol is the one
* copied here; what is dropped is the condition variable's mutex, which
* every scond_wait re-acquires before returning and which serialises a
* broadcast across its waiters.
*/
#define EC_W_WOKEN 1 /* a waker has taken this block */
#define EC_W_ASLEEP 2 /* the waiter committed to the kernel wait */
#define EC_HEAD_LOCK ((uintptr_t)1)
#define EC_HEAD_MASK (~EC_HEAD_LOCK)
#define EC_STATUS_TIMEOUT 0x102
/* How long a waiter spins on its flag word before committing to the
* kernel, on multiprocessor only. A budget in microseconds rather
* than a count of iterations: a PAUSE is worth an order of magnitude
* more cycles on some processors than others, so a fixed count is a
* different amount of time on every machine. The count is derived
* from a measured relax at resolve time.
*
* Measured on Windows 11 x64, four waiters on one object, where a
* relax costs 11.8ns. Round trip and broadcast per waiter, against
* what a spin that finds nothing costs:
*
* spin burn round trip broadcast/waiter
* 0 0.00us 4.21us 0.94us
* 64 0.76us 0.27us 0.96us
* 128 1.49us 0.11us 0.51us
* 256 2.99us 0.06us 0.11us
* 512 6.04us 0.08us 0.12us
* 4096 48.63us 0.07us 0.12us
*
* The knee is at 256 iterations there, which is 3us, and nothing above
* it buys anything -- a spin that finds nothing then costs about what
* the wake syscall it avoids would have. RETRO_EVENTCOUNT_SPIN_US
* moves the budget; RETRO_EVENTCOUNT_SPIN sets the count outright and
* skips the calibration. */
#define EC_SPIN_US 3
/* Bounds on the derived count, so a mismeasured relax cannot turn the
* spin into either a no-op or an unbounded burn. */
#define EC_SPIN_MIN 32
#define EC_SPIN_MAX 8192
struct ec_waiter
{
struct ec_waiter *next;
HANDLE event; /* EVENT tier only */
retro_atomic_int_t flags;
DWORD tid;
};
typedef LONG (WINAPI *ec_nt_wait_alert_t)(void*, LARGE_INTEGER*);
typedef LONG (WINAPI *ec_nt_alert_tid_t)(HANDLE);
typedef LONG (WINAPI *ec_nt_keyed_t)(HANDLE, void*, BOOLEAN, LARGE_INTEGER*);
typedef LONG (WINAPI *ec_nt_create_keyed_t)(HANDLE*, ULONG, void*, ULONG);
enum
{
EC_SLEEP_ALERT = 1,
EC_SLEEP_KEYED,
EC_SLEEP_EVENT
};
static struct
{
ec_nt_wait_alert_t wait_alert;
ec_nt_alert_tid_t alert_tid;
ec_nt_keyed_t wait_keyed;
ec_nt_keyed_t release_keyed;
HANDLE keyed;
DWORD tls_event;
retro_atomic_int_t state;
unsigned spin; /* 0 on a single processor */
int sleep;
} ec_g;
/* Relax iterations that fit in @budget_us. Timed rather than assumed,
* and taken as the best of a few short passes so a preemption in the
* middle of one does not stretch the answer. QueryPerformanceCounter
* is the clock because it is the one available on every Windows this
* backend runs on. */
static unsigned ec_spin_for_budget(unsigned budget_us)
{
const unsigned samples = 3;
const unsigned iters = 2048;
LARGE_INTEGER freq;
double best = 0.0;
unsigned s, i;
if (!budget_us)
return 0;
if (!QueryPerformanceFrequency(&freq) || freq.QuadPart <= 0)
return EC_SPIN_MIN * 8; /* no clock: a middling fixed count */
for (s = 0; s < samples; s++)
{
LARGE_INTEGER t0, t1;
double us;
QueryPerformanceCounter(&t0);
for (i = 0; i < iters; i++)
retro_cpu_relax();
QueryPerformanceCounter(&t1);
us = (double)(t1.QuadPart - t0.QuadPart) * 1000000.0
/ (double)freq.QuadPart;
if (us > 0.0 && (best == 0.0 || us < best))
best = us;
}
if (best <= 0.0)
return EC_SPIN_MIN * 8;
{
double n = (double)budget_us * (double)iters / best;
if (n < (double)EC_SPIN_MIN)
return EC_SPIN_MIN;
if (n > (double)EC_SPIN_MAX)
return EC_SPIN_MAX;
return (unsigned)n;
}
}
static void ec_win32_resolve(void)
{
HMODULE nt = GetModuleHandleA("ntdll.dll");
/* alert / keyed / event pin a tier; none pins the case where no tier
* is usable, which is otherwise only reachable by exhausting the
* process's TLS indices. That case has its own code path and had
* never been run - see the fallback at the end of this function. */
const char *force = getenv("RETRO_EVENTCOUNT_WIN32");
ec_g.sleep = EC_SLEEP_EVENT;
if (nt && !(force && !strcmp(force, "none")))
{
ec_nt_create_keyed_t create_keyed;
ec_g.wait_alert = (ec_nt_wait_alert_t)(void (*)(void))
GetProcAddress(nt, "NtWaitForAlertByThreadId");
ec_g.alert_tid = (ec_nt_alert_tid_t)(void (*)(void))
GetProcAddress(nt, "NtAlertThreadByThreadId");
ec_g.wait_keyed = (ec_nt_keyed_t)(void (*)(void))
GetProcAddress(nt, "NtWaitForKeyedEvent");
ec_g.release_keyed = (ec_nt_keyed_t)(void (*)(void))
GetProcAddress(nt, "NtReleaseKeyedEvent");
create_keyed = (ec_nt_create_keyed_t)(void (*)(void))
GetProcAddress(nt, "NtCreateKeyedEvent");
if (ec_g.wait_alert && ec_g.alert_tid
&& !(force && strcmp(force, "alert")))
ec_g.sleep = EC_SLEEP_ALERT;
else if (ec_g.wait_keyed && ec_g.release_keyed && create_keyed
&& !(force && strcmp(force, "keyed"))
&& create_keyed(&ec_g.keyed, 0x1f0003 /* EVENT_ALL_ACCESS */,
NULL, 0) == 0)
ec_g.sleep = EC_SLEEP_KEYED;
}
if (ec_g.sleep == EC_SLEEP_EVENT)
{
ec_g.tls_event = (force && !strcmp(force, "none"))
? TLS_OUT_OF_INDEXES : TlsAlloc();
/* Nothing else can park on this tier without it, so fall through
* to the condition variable rather than handing out a bad index.
* sleep == 0 is read by retro_eventcount_init(), which then gives
* the object a mutex and a condition variable instead of the
* waiter list, and by every path that would otherwise have used
* that list. */
if (ec_g.tls_event == TLS_OUT_OF_INDEXES)
ec_g.sleep = 0;
}
/* Spinning before the kernel wait only pays where the thread being
* waited for can run at the same time. On one processor it is pure
* delay, so the spin is skipped entirely there -- the same gate
* rthreads' scond applies. */
{
SYSTEM_INFO si;
const char *env;
unsigned budget = EC_SPIN_US;
GetSystemInfo(&si);
/* An asked-for budget is honoured whatever the processor count,
* so the calibration is reachable for measurement on a machine
* that would otherwise skip the spin entirely. */
if ((env = getenv("RETRO_EVENTCOUNT_SPIN_US")))
budget = (unsigned)strtoul(env, NULL, 0);
else if (si.dwNumberOfProcessors <= 1)
budget = 0;
ec_g.spin = ec_spin_for_budget(budget);
/* An explicit count wins over the budget, for sweeping. */
if ((env = getenv("RETRO_EVENTCOUNT_SPIN")))
ec_g.spin = (unsigned)strtoul(env, NULL, 0);
}
}
static void ec_win32_init(void)
{
if (retro_atomic_load_acquire_int(&ec_g.state) == 2)
return;
if (retro_atomic_cas_int(&ec_g.state, 0, 1))
{
ec_win32_resolve();
retro_atomic_store_release_int(&ec_g.state, 2);
return;
}
while (retro_atomic_load_acquire_int(&ec_g.state) != 2)
Sleep(0);
}
/* block until woken or the timeout (NULL = never) passes; false on timeout */
static bool ec_sleep(struct ec_waiter *w, LARGE_INTEGER *timeout)
{
switch (ec_g.sleep)
{
case EC_SLEEP_ALERT:
return ec_g.wait_alert(&w->flags, timeout) != EC_STATUS_TIMEOUT;
case EC_SLEEP_KEYED:
return ec_g.wait_keyed(ec_g.keyed, w, FALSE, timeout)
!= EC_STATUS_TIMEOUT;
default:
{
DWORD ms = INFINITE;
DWORD rc;
if (timeout)
{
LONGLONG t = (-timeout->QuadPart + 9999) / 10000;
/* Rounding up costs at most a millisecond; wrapping a
* long wait into a short one would be a bug, so it is
* clamped instead. INFINITE is not a duration. */
ms = (t >= (LONGLONG)INFINITE) ? INFINITE - 1 : (DWORD)t;
}
rc = WaitForSingleObject(w->event, ms);
if (rc == WAIT_TIMEOUT)
return false;
if (rc == WAIT_OBJECT_0)
return true;
/* Anything else is the handle being unusable, which since
* the TLS fallback was fixed should not be reachable: a
* CreateEvent that fails is answered in ec_win32_park
* before the block is ever listed. Reported as a wake
* because the caller re-checks its own predicate and a
* false timeout would be a lie, but slept on first: a bad
* handle fails immediately, and without this the caller's
* loop turns into a spin on a core that has no work. A
* millisecond of poll is the right shape for something
* that should not happen at all.
*
* There is nowhere to report it from - this is
* libretro-common and has no logger - so the poll is the
* whole mitigation. This branch is not exercised by any
* test: no way to induce a failing wait on a valid handle
* was found. */
Sleep(1);
return true;
}
}
}
static void ec_wake_one(struct ec_waiter *w)
{
/* copies taken first: the waiter may leave as soon as it sees WOKEN */
DWORD tid = w->tid;
HANDLE event = w->event;
int prev = retro_atomic_fetch_or_int(&w->flags, EC_W_WOKEN);
if (!(prev & EC_W_ASLEEP))
return; /* still spinning: it sees the flag, no syscall */
switch (ec_g.sleep)
{
case EC_SLEEP_ALERT:
ec_g.alert_tid((HANDLE)(uintptr_t)tid);
break;
case EC_SLEEP_KEYED:
/* only ever for a block that has committed, so the rendezvous
* always finds its waiter */
ec_g.release_keyed(ec_g.keyed, w, FALSE, NULL);
break;
default:
SetEvent(event);
break;
}
}
static INLINE uintptr_t ec_head(retro_eventcount_t *ec)
{
return (uintptr_t)retro_atomic_load_acquire_ptr(&ec->waitlist);
}
static void ec_list_lock(retro_eventcount_t *ec)
{
for (;;)
{
uintptr_t old = ec_head(ec);
if (!(old & EC_HEAD_LOCK)
&& retro_atomic_cas_ptr(&ec->waitlist, (void*)old,
(void*)(old | EC_HEAD_LOCK)))
return;
retro_cpu_relax();
}
}
static void ec_list_unlock(retro_eventcount_t *ec)
{
for (;;)
{
uintptr_t old = ec_head(ec);
if (retro_atomic_cas_ptr(&ec->waitlist, (void*)old,
(void*)(old & EC_HEAD_MASK)))
return;
}
}
/* unlink w if it is still listed; the list lock must be held. A block
* that is gone has been taken by a waker, whose wake is on its way. */
static bool ec_list_unlink(retro_eventcount_t *ec, struct ec_waiter *w)
{
for (;;)
{
uintptr_t old = ec_head(ec);
struct ec_waiter *n = (struct ec_waiter*)(old & EC_HEAD_MASK);
if (n == w)
{
if (retro_atomic_cas_ptr(&ec->waitlist, (void*)old,
(void*)((uintptr_t)w->next | EC_HEAD_LOCK)))
return true;
continue; /* a push landed in front of it: look again */
}
while (n && n->next != w)
n = n->next;
if (!n)
return false;
n->next = w->next;
return true;
}
}
static void ec_list_push(retro_eventcount_t *ec, struct ec_waiter *w)
{
uintptr_t old;
do
{
old = ec_head(ec);
w->next = (struct ec_waiter*)(old & EC_HEAD_MASK);
} while (!retro_atomic_cas_ptr(&ec->waitlist, (void*)old,
(void*)((uintptr_t)w | (old & EC_HEAD_LOCK))));
}
static void ec_wake_all(retro_eventcount_t *ec)
{
struct ec_waiter *w;
uintptr_t old;
if (!(ec_head(ec) & EC_HEAD_MASK))
return;
ec_list_lock(ec);
/* Take the whole list, dropping the lock in the same swap. A push
* does not take the list lock -- it only preserves the bit -- so
* reading the head and then storing over it would drop any block
* that landed in between, and that block would never be woken. */
do
{
old = ec_head(ec);
} while (!retro_atomic_cas_ptr(&ec->waitlist, (void*)old, NULL));
w = (struct ec_waiter*)(old & EC_HEAD_MASK);
while (w)
{
struct ec_waiter *next = w->next;
ec_wake_one(w);
w = next;
}
}
/* returns false only when a bounded wait expired */
static bool ec_win32_park(retro_eventcount_t *ec, int key, bool bounded,
int64_t timeout_us)
{
struct ec_waiter w;
LARGE_INTEGER timeout;
bool woken = true;
unsigned i;
w.event = NULL;
w.next = NULL;
w.tid = GetCurrentThreadId();
retro_atomic_int_init(&w.flags, 0);
if (ec_g.sleep == EC_SLEEP_EVENT)
{
if (!(w.event = (HANDLE)TlsGetValue(ec_g.tls_event)))
{
if (!(w.event = CreateEvent(NULL, FALSE, FALSE, NULL)))
return true; /* nothing to wait on: a spurious wake-up */
TlsSetValue(ec_g.tls_event, w.event);
}
}
ec_list_push(ec, &w);
/* Listed first, then re-check: a notify from here on either finds
* this block or has already moved the epoch. */
if (retro_atomic_load_acquire_int(&ec->epoch) != key)
{
bool unlinked;
ec_list_lock(ec);
unlinked = ec_list_unlink(ec, &w);
ec_list_unlock(ec);
/* This block lives on this thread's stack, so leaving here frees
* it. That is only safe while it is still listed: a waker that
* has already taken it off the list is walking it right now, and
* publishes WOKEN once it is done reading -- after it has taken
* the next pointer and the wake-up details. So when the unlink
* finds nothing, wait for that flag before the frame goes away.
* No wake is in flight to consume: ASLEEP is not set yet, so the
* waker issued none. */
if (!unlinked)
{
while (!(retro_atomic_load_acquire_int(&w.flags) & EC_W_WOKEN))
retro_cpu_relax();
}
return true;
}
for (i = 0; i < ec_g.spin; i++)
{
if (retro_atomic_load_acquire_int(&w.flags) & EC_W_WOKEN)
return true;
retro_cpu_relax();
}
/* commit: past this a waker that takes the block must wake us */
if (retro_atomic_fetch_or_int(&w.flags, EC_W_ASLEEP) & EC_W_WOKEN)
return true;
if (bounded)
timeout.QuadPart = -(LONGLONG)timeout_us * 10;
if (!ec_sleep(&w, bounded ? &timeout : NULL))
{
/* timed out, unless a waker already took the block, in which case
* its wake is in flight and has to be consumed */
ec_list_lock(ec);
woken = !ec_list_unlink(ec, &w);
ec_list_unlock(ec);
if (woken)
ec_sleep(&w, NULL);
}
return woken;
}
#endif
bool retro_eventcount_init(retro_eventcount_t *ec)
{
int lockless = 0;
if (!ec)
return false;
memset(ec, 0, sizeof(*ec));
retro_atomic_int_init(&ec->epoch, 0);
retro_atomic_int_init(&ec->waiters, 0);
#if defined(RETRO_EC_ADDR_LINUX)
lockless = 1;
#elif defined(RETRO_EC_ADDR_WIN32)
ec_win32_init();
/* Only if a sleep tier resolved. With none - no ntdll entry points
* and no TLS index for the per-thread event - the waiter list has
* nothing to sleep on, so this object takes a mutex and a condition
* variable and every path below follows it there. ec->cond is what
* says which of the two this object is, rather than the global:
* one load of a field that never changes after this point. */
if (ec_g.sleep)
{
retro_atomic_ptr_init(&ec->waitlist, NULL);
lockless = 1;
}
#endif
if (lockless)
return true;
if (!(ec->lock = slock_new()))
return false;
if (!(ec->cond = scond_new()))
{
slock_free(ec->lock);
ec->lock = NULL;
return false;
}
return true;
}
void retro_eventcount_free(retro_eventcount_t *ec)
{
if (!ec)
return;
if (ec->cond)
scond_free(ec->cond);
if (ec->lock)
slock_free(ec->lock);
ec->cond = NULL;
ec->lock = NULL;
}
void retro_eventcount_notify(retro_eventcount_t *ec)
{
/* Publish the state change, then read the waiter count, both
* sequentially consistent. The two here and the mirrored pair in
* prepare_wait share one total order, which is what stops a notify
* reading zero waiters while a consumer that has not yet seen the
* new epoch is on its way into a park. A separate seq_cst fence
* would do the same job and costs a second locked operation. */
#if defined(RETRO_EC_LOCKED_BOOKKEEPING)
slock_lock(ec->lock);
retro_atomic_fetch_add_int(&ec->epoch, 1);
if (retro_atomic_load_relaxed_int(&ec->waiters) != 0)
scond_broadcast(ec->cond);
slock_unlock(ec->lock);
return;
#else
retro_atomic_fetch_add_seq_cst_int(&ec->epoch, 1);
#endif
#if defined(RETRO_ATOMIC_LOCK_FREE)
/* The ordering is the sequentially-consistent bump above and this
* sequentially-consistent load, as a pair: the store is visible
* before the load is taken, so a notify cannot read zero waiters
* while a registering waiter reads the pre-notify epoch. There is
* no separate fence here any more - there was, and this comment
* used to name it.
*
* Where the atomics degrade to a compiler barrier the pair carries
* no such guarantee, so that build takes the lock on every notify
* instead -- the single-core case, where it is uncontended. */
if (retro_atomic_load_seq_cst_int(&ec->waiters) == 0)
return;
#endif
#if defined(RETRO_EC_ADDR_LINUX)
syscall(SYS_futex, (void*)&ec->epoch, FUTEX_WAKE_PRIVATE,
INT_MAX, NULL, NULL, 0);
#else
#if defined(RETRO_EC_ADDR_WIN32)
if (!ec->cond)
{
ec_wake_all(ec);
return;
}
#endif
/* Taking the lock here cannot overtake a waiter's own re-check,
* because commit_wait does that re-check under this same lock and
* then sleeps on the condition variable, which releases it
* atomically. prepare_wait does NOT hold it - it registers and
* reads the epoch with atomics alone, which is what keeps N
* waiters from serialising here just to announce themselves. */
slock_lock(ec->lock);
scond_broadcast(ec->cond);
slock_unlock(ec->lock);
#endif
}
int retro_eventcount_prepare_wait(retro_eventcount_t *ec)
{
/* Mirror of notify: register with a sequentially-consistent
* read-modify-write, then take a sequentially-consistent load of the
* epoch. There are no standalone fences here any more - this
* comment used to name two. The pair here and the pair in notify
* share one total order, which is what makes it impossible for a
* notify to see no waiters and for this thread to read the
* pre-notify epoch.
*
* Lock-free backends register with atomics alone. Where the
* atomics are not lock-free the bookkeeping is protected by
* ec->lock, just below -- but never held across the caller's
* predicate window either way. The epoch carries the handshake, so
* the condition-variable backend needs its mutex only across the
* re-check-and-sleep in commit_wait, which is what keeps N waiters
* from serialising on this object to register. */
#if defined(RETRO_EC_LOCKED_BOOKKEEPING)
{
int key;
slock_lock(ec->lock);
retro_atomic_fetch_add_int(&ec->waiters, 1);
key = retro_atomic_load_relaxed_int(&ec->epoch);
slock_unlock(ec->lock);
return key;
}
#else
retro_atomic_fetch_add_seq_cst_int(&ec->waiters, 1);
return retro_atomic_load_seq_cst_int(&ec->epoch);
#endif
}
void retro_eventcount_cancel_wait(retro_eventcount_t *ec)
{
#if defined(RETRO_EC_LOCKED_BOOKKEEPING)
slock_lock(ec->lock);
#endif
retro_atomic_fetch_sub_int(&ec->waiters, 1);
#if defined(RETRO_EC_LOCKED_BOOKKEEPING)
slock_unlock(ec->lock);
#endif
}
void retro_eventcount_commit_wait(retro_eventcount_t *ec, int key)
{
#if defined(RETRO_EC_ADDR_LINUX)
int expect = key;
if (retro_atomic_load_acquire_int(&ec->epoch) == key)
syscall(SYS_futex, (void*)&ec->epoch, FUTEX_WAIT_PRIVATE,
expect, NULL, NULL, 0);
retro_atomic_fetch_sub_int(&ec->waiters, 1);
#else
#if defined(RETRO_EC_ADDR_WIN32)
if (!ec->cond)
{
if (retro_atomic_load_acquire_int(&ec->epoch) == key)
ec_win32_park(ec, key, false, 0);
retro_atomic_fetch_sub_int(&ec->waiters, 1);
return;
}
#endif
/* The mutex is taken here, not in prepare_wait: it has to cover the
* epoch re-check and the sleep together, and nothing before that. A
* notify that lands before the lock is acquired has already moved
* the epoch, so the re-check finds it and this never sleeps. */
slock_lock(ec->lock);
if (retro_atomic_load_acquire_int(&ec->epoch) == key)
scond_wait(ec->cond, ec->lock);
retro_atomic_fetch_sub_int(&ec->waiters, 1);
slock_unlock(ec->lock);
#endif
}
bool retro_eventcount_commit_wait_timeout(retro_eventcount_t *ec,
int key, int64_t timeout_us)
{
bool signalled = true;
if (timeout_us < 0)
timeout_us = 0;
#if defined(RETRO_EC_ADDR_LINUX)
if (retro_atomic_load_acquire_int(&ec->epoch) == key)
{
struct timespec ts;
int expect = key;
ts.tv_sec = (time_t)(timeout_us / 1000000);
ts.tv_nsec = (long)((timeout_us % 1000000) * 1000);
if (syscall(SYS_futex, (void*)&ec->epoch, FUTEX_WAIT_PRIVATE,
expect, &ts, NULL, 0) != 0 && errno == ETIMEDOUT)
signalled = false;
}
retro_atomic_fetch_sub_int(&ec->waiters, 1);
#else
#if defined(RETRO_EC_ADDR_WIN32)
if (!ec->cond)
{
if (retro_atomic_load_acquire_int(&ec->epoch) == key)
signalled = ec_win32_park(ec, key, true, timeout_us);
retro_atomic_fetch_sub_int(&ec->waiters, 1);
return signalled;
}
#endif
slock_lock(ec->lock);
if (retro_atomic_load_acquire_int(&ec->epoch) == key)
signalled = scond_wait_timeout(ec->cond, ec->lock, timeout_us);
retro_atomic_fetch_sub_int(&ec->waiters, 1);
slock_unlock(ec->lock);
#endif
return signalled;
}
unsigned retro_eventcount_spin_iters(void)
{
#if defined(RETRO_EC_ADDR_WIN32)
ec_win32_init();
/* The fallback parks on a condition variable, which has no flag word
* to spin on. */
return ec_g.sleep ? ec_g.spin : 0;
#else
return 0;
#endif
}
const char *retro_eventcount_backend_name(void)
{
#if defined(RETRO_EC_ADDR_LINUX)
return "futex";
#elif defined(RETRO_EC_ADDR_WIN32)
ec_win32_init();
switch (ec_g.sleep)
{
case EC_SLEEP_ALERT: return "ntdll alert-by-thread-id";
case EC_SLEEP_KEYED: return "ntdll keyed event";
case EC_SLEEP_EVENT: return "win32 event";
default: return "scond (no win32 sleep primitive)";
}
#elif !defined(RETRO_ATOMIC_LOCK_FREE)
return "scond (atomics not lock-free)";
#elif defined(RETRO_EVENTCOUNT_FORCE_SCOND)
return "scond (forced)";
#else
return "scond";
#endif
}
@@ -0,0 +1,669 @@
/* Copyright (C) 2010-2026 The RetroArch team
*
* ---------------------------------------------------------------------------------------
* The following license statement only applies to this file (retro_procbarrier.c).
* ---------------------------------------------------------------------------------------
*
* Permission is hereby granted, free of charge,
* to any person obtaining a copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
* and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
* OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
/* See the header for what this is and why each tier is shaped the way it
* is. This file is the platform plumbing.
*
* Every tier below was checked against the kernel that provides it, by
* reading its source or disassembling it, before being written down here:
*
* Linux membarrier kernel/sched/membarrier.c: PRIVATE_EXPEDITED IPIs
* each online CPU whose current task shares our mm,
* with ipi_mb(), which is smp_mb(). 4.14 and later.
* 4.3-4.13 only have SHARED, an RCU grace period that
* waits for every CPU on the system; that is not
* used here, since it can take milliseconds.
* Linux page flip arch/x86/mm/tlb.c: flush_tlb_mm_range sends an IPI
* to mm_cpumask(mm), back to at least 2.6.32.
* Windows FPWB ntoskrnl 10.0.26100 KeFlushProcessWriteBuffers:
* raise IRQL, skip if one CPU, else KiIpiSendRequestEx
* to the affinity and spin for acks.
* Windows page flip The same IPI, reached through the TLB shootdown a
* protection change forces. All APIs it needs exist
* on Windows 2000.
* Darwin page flip osfmk/x86_64/pmap.c pmap_flush_tlbs:
* i386_signal_cpu(cpu, MP_TLB_FLUSH) per active CPU,
* then a cpus_to_respond wait.
* Darwin signal cause_ast_check: i386_signal_cpu(MP_AST) on x86,
* cpu_signal(SIGPast) on arm64 and on ppc (xnu-1228),
* the ppc path bracketed by sync; isync.
* Linux signal Signal delivery to a running thread goes through
* kick_process(), which is an IPI.
*/
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <retro_atomic.h>
#include <rthreads/retro_procbarrier.h>
/* ------------------------------------------------------------------ */
/* Platform selection */
/* ------------------------------------------------------------------ */
#if defined(_WIN32) || defined(USE_WIN32_THREADS)
#define PB_WINDOWS 1
#elif defined(__APPLE__)
#define PB_DARWIN 1
#elif defined(__linux__) || defined(__ANDROID__)
#define PB_LINUX 1
#elif defined(__FreeBSD__) && !defined(__ORBIS__) && !defined(ORBIS)
/* The PS4 is FreeBSD underneath and its toolchain defines __FreeBSD__,
* but Sony's kernel is a 9-era fork with none of the 14.1 membarrier
* and no process-barrier API of its own. It is a multi-core console and
* resolves to NONE like the others. */
#define PB_FREEBSD 1
#endif
#if defined(__i386__) || defined(__x86_64__) || defined(_M_IX86) || defined(_M_X64)
#define PB_X86 1
#endif
#if defined(PB_WINDOWS)
#include <windows.h>
#endif
#if defined(PB_LINUX) || defined(PB_FREEBSD_MEMBARRIER) || defined(PB_DARWIN)
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <sys/mman.h>
#include <pthread.h>
#endif
#if defined(PB_LINUX)
#include <sys/syscall.h>
#include <dirent.h>
#include <fcntl.h>
#endif
#if defined(PB_FREEBSD)
#include <sys/param.h>
/* membarrier(2) arrived in FreeBSD 14.1, and so did its header; on
* anything older the include would fail and the tier is simply absent,
* leaving the x86 page flip. */
#if defined(__FreeBSD_version) && __FreeBSD_version >= 1401000
#include <sys/membarrier.h>
#define PB_FREEBSD_MEMBARRIER 1
#endif
#endif
#if defined(PB_DARWIN)
#include <mach/mach.h>
#include <mach/thread_info.h>
#include <mach/thread_act.h>
#include <mach/task.h>
#endif
/* ------------------------------------------------------------------ */
/* State */
/* ------------------------------------------------------------------ */
/* s_tier holds one more state than the public enum: RESOLVED_NONE, for
* "probed, and this platform has nothing". The public NONE is what a
* caller sees in both that case and before init, but the two must not
* be confused internally, or an unsupported platform re-runs the whole
* probe -- sigaction, mmap, a /proc walk -- on every call. */
#define PB_RESOLVED_NONE (-1)
static retro_atomic_int_t s_tier; /* PB_RESOLVED_NONE or a public tier */
static int s_signum;
#if defined(PB_WINDOWS)
typedef VOID (WINAPI *pb_fpwb_t)(VOID);
static pb_fpwb_t s_fpwb;
#endif
/* Page-flip storage: only where the tier exists, so a console build is
* not left with variables nothing references. The gate is defined below
* with the tier; forward the condition here. */
#if defined(PB_X86) && (defined(PB_WINDOWS) || defined(PB_LINUX) \
|| defined(PB_FREEBSD) || defined(PB_DARWIN))
#if defined(PB_WINDOWS)
static void *s_page;
static SIZE_T s_page_size;
#else
static void *s_page;
static size_t s_page_size;
#endif
#endif
#if defined(PB_LINUX)
static retro_atomic_int_t s_acks;
#endif
/* ------------------------------------------------------------------ */
/* Helpers */
/* ------------------------------------------------------------------ */
static unsigned pb_num_cpus(void)
{
#if defined(PB_WINDOWS)
SYSTEM_INFO si;
GetSystemInfo(&si);
return (unsigned)si.dwNumberOfProcessors;
#elif defined(PB_LINUX) || defined(PB_FREEBSD) || defined(PB_DARWIN)
/* Only on the platforms this file otherwise handles. Keying on
* _SC_NPROCESSORS_ONLN being defined is not enough: PSP's newlib
* defines the constant and has no sysconf, and the link fails. */
long n = sysconf(_SC_NPROCESSORS_ONLN);
return (n > 0) ? (unsigned)n : 1u;
#elif defined(PSP) || defined(PS2) || defined(GEKKO) \
|| defined(DJGPP) || defined(__DJGPP__)
/* Single-core consoles: nothing to fence against. Naming them here
* is what lets the barrier be free there rather than NONE. */
return 1u;
#else
return 2u; /* unknown: assume SMP, which is the safe direction */
#endif
}
/* ------------------------------------------------------------------ */
/* Tier: membarrier */
/* ------------------------------------------------------------------ */
#if defined(PB_LINUX)
/* ABI constants, spelled out rather than taken from the header: the
* values are fixed since 4.14 but the header may be older than the
* kernel that is actually running. */
#ifndef __NR_membarrier
#define PB_NO_MEMBARRIER 1
#endif
#define PB_MEMBARRIER_CMD_QUERY 0
#define PB_MEMBARRIER_CMD_PRIVATE_EXPEDITED (1 << 3)
#define PB_MEMBARRIER_CMD_REGISTER_PRIVATE_EXPEDITED (1 << 4)
static int pb_membarrier_try(void)
{
#if defined(PB_NO_MEMBARRIER)
return 0;
#else
long q = syscall(__NR_membarrier, PB_MEMBARRIER_CMD_QUERY, 0, 0);
if (q < 0)
return 0; /* ENOSYS before 4.3, or filtered */
if (!(q & PB_MEMBARRIER_CMD_PRIVATE_EXPEDITED))
return 0; /* 4.3-4.13: only the slow SHARED form; not used */
if (syscall(__NR_membarrier,
PB_MEMBARRIER_CMD_REGISTER_PRIVATE_EXPEDITED, 0, 0) != 0)
return 0;
/* Registration succeeding is not the same as the barrier working: a
* sandbox can permit the query and the registration and still refuse
* the expedited call. Issue one now and believe its return value. A
* tier that would silently fence nothing is worse than no tier. */
if (syscall(__NR_membarrier, PB_MEMBARRIER_CMD_PRIVATE_EXPEDITED, 0, 0) != 0)
return 0;
return 1;
#endif
}
static void pb_membarrier(void)
{
#if !defined(PB_NO_MEMBARRIER)
syscall(__NR_membarrier, PB_MEMBARRIER_CMD_PRIVATE_EXPEDITED, 0, 0);
#endif
}
#endif
#if defined(PB_FREEBSD_MEMBARRIER)
static int pb_membarrier_try(void)
{
int q = membarrier(MEMBARRIER_CMD_QUERY, 0, 0);
if (q < 0 || !(q & MEMBARRIER_CMD_PRIVATE_EXPEDITED))
return 0;
if (membarrier(MEMBARRIER_CMD_REGISTER_PRIVATE_EXPEDITED, 0, 0) != 0)
return 0;
return 1;
}
static void pb_membarrier(void)
{
membarrier(MEMBARRIER_CMD_PRIVATE_EXPEDITED, 0, 0);
}
#endif
/* ------------------------------------------------------------------ */
/* Tier: FlushProcessWriteBuffers */
/* ------------------------------------------------------------------ */
#if defined(PB_WINDOWS)
static int pb_fpwb_try(void)
{
HMODULE k32 = GetModuleHandleA("kernel32.dll");
if (!k32)
return 0;
/* FARPROC to pb_fpwb_t directly: both are function pointers, and
* routing the cast through void* is what ISO C objects to. */
s_fpwb = (pb_fpwb_t)GetProcAddress(k32, "FlushProcessWriteBuffers");
return s_fpwb != NULL;
}
#endif
/* ------------------------------------------------------------------ */
/* Tier: page flip */
/* ------------------------------------------------------------------ */
/* The page flip needs mmap/mprotect (or the Win32 equivalents), so it
* exists only on the platforms this file handles, not merely on x86: an
* x86 console has neither. */
#if defined(PB_X86) && (defined(PB_WINDOWS) || defined(PB_LINUX) \
|| defined(PB_FREEBSD) || defined(PB_DARWIN))
#define PB_HAVE_PAGEFLIP 1
#endif
#if defined(PB_HAVE_PAGEFLIP)
static int pb_pageflip_try(void)
{
#if defined(PB_WINDOWS)
SYSTEM_INFO si;
GetSystemInfo(&si);
s_page_size = si.dwPageSize;
s_page = VirtualAlloc(NULL, s_page_size, MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE);
if (!s_page)
return 0;
/* A page that is paged out has no TLB entry to shoot down. */
VirtualLock(s_page, s_page_size);
return 1;
#else
long ps = sysconf(_SC_PAGESIZE);
s_page_size = (ps > 0) ? (size_t)ps : 4096u;
s_page = mmap(NULL, s_page_size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (s_page == MAP_FAILED)
{
s_page = NULL;
return 0;
}
mlock(s_page, s_page_size);
return 1;
#endif
}
static void pb_pageflip(void)
{
#if defined(PB_WINDOWS)
DWORD old;
VirtualProtect(s_page, s_page_size, PAGE_READWRITE, &old);
*(volatile char*)s_page = 0;
VirtualProtect(s_page, s_page_size, PAGE_NOACCESS, &old);
#else
mprotect(s_page, s_page_size, PROT_READ | PROT_WRITE);
*(volatile char*)s_page = 0;
mprotect(s_page, s_page_size, PROT_NONE);
#endif
}
#endif
/* ------------------------------------------------------------------ */
/* Tier: signal */
/* ------------------------------------------------------------------ */
#if defined(PB_LINUX) || defined(PB_DARWIN)
#if defined(PB_LINUX)
static void pb_ack_handler(int sig)
{
(void)sig;
/* Async-signal-safe: one atomic increment. The release pairs with the
* acquire in the waiter, so the interrupted thread's earlier stores
* are ordered before its acknowledgement. */
retro_atomic_fetch_add_int(&s_acks, 1);
}
#endif
static int pb_signal_try(int signum)
{
#if defined(PB_DARWIN)
(void)signum; /* no signal on Darwin; see pb_signal_barrier */
{
/* Prove task_threads is permitted here; sandboxes can deny it. */
thread_act_array_t list;
mach_msg_type_number_t n;
if (task_threads(mach_task_self(), &list, &n) != KERN_SUCCESS)
return 0;
{
mach_msg_type_number_t i;
for (i = 0; i < n; i++)
mach_port_deallocate(mach_task_self(), list[i]);
}
vm_deallocate(mach_task_self(), (vm_address_t)list,
n * sizeof(thread_act_t));
}
#else
{
struct sigaction sa;
DIR *d;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = pb_ack_handler;
sa.sa_flags = SA_RESTART;
sigemptyset(&sa.sa_mask);
if (sigaction(signum, &sa, NULL) != 0)
return 0;
/* And that /proc/self/task is readable; hidepid exempts self but
* a hardened container may not. */
d = opendir("/proc/self/task");
if (!d)
return 0;
closedir(d);
}
#endif
return 1;
}
#if defined(PB_LINUX)
static int pb_thread_running(pid_t tid)
{
char path[64], buf[256], *p, *q;
int fd;
ssize_t n;
unsigned v;
/* /proc/self/task/<tid>/stat: field 3 is the state, after the comm
* in parentheses; the comm may itself contain spaces or parentheses,
* so scan back from the last ')' rather than forward. The path is
* built by hand: snprintf is not C89, and a tid is a small decimal. */
strcpy(path, "/proc/self/task/");
q = path + strlen(path);
{
char digits[16];
int nd = 0;
v = (unsigned)tid;
do { digits[nd++] = (char)('0' + v % 10u); v /= 10u; } while (v);
while (nd) *q++ = digits[--nd];
}
strcpy(q, "/stat");
fd = open(path, O_RDONLY);
if (fd < 0)
return 0;
n = read(fd, buf, sizeof(buf) - 1);
close(fd);
if (n <= 0)
return 0;
buf[n] = '\0';
p = strrchr(buf, ')');
return p && p[1] == ' ' && p[2] == 'R';
}
static void pb_signal_barrier(void)
{
DIR *d;
struct dirent *e;
pid_t self = (pid_t)syscall(SYS_gettid);
pid_t pid = getpid();
int sent = 0;
retro_atomic_store_relaxed_int(&s_acks, 0);
d = opendir("/proc/self/task");
if (!d)
return;
while ((e = readdir(d)) != NULL)
{
pid_t tid = (pid_t)atoi(e->d_name);
if (tid <= 0 || tid == self)
continue;
/* Only threads on a CPU need interrupting; a thread that was
* switched out was drained by the switch. 'R' is runnable rather
* than strictly running, so under overcommit this can also wait
* for a preempted thread to be rescheduled -- bounded by a
* scheduler quantum, and acceptable on a path about to sleep. */
if (!pb_thread_running(tid))
continue;
if (syscall(SYS_tgkill, pid, tid, s_signum) == 0)
sent++;
}
closedir(d);
while (retro_atomic_load_acquire_int(&s_acks) < sent)
;
}
#endif
#if defined(PB_DARWIN)
/* Darwin does not deliver the signal here at all. thread_suspend on a
* Mach port is already a synchronous barrier: XNU's thread_wait loops
* while the target's state has TH_RUN, and blocks while it is the active
* thread on any processor, so thread_suspend does not return until the
* target has been switched off its CPU -- and a context switch drains
* its store buffer. Checked in the current tree and in xnu-1228, the
* last PowerPC kernel. thread_resume then puts it back.
*
* This is why no pthread_t is needed. The earlier version converted
* each port with pthread_from_mach_thread_np and sent a signal, and that
* function is 10.6 and later; RetroArch's PowerPC floor is 10.4. The
* function itself is a walk of libpthread's private thread list under
* its private lock, so there is nothing to reimplement against an older
* libpthread that would not be a layout guess.
*
* Nothing is called between the suspend and the resume. A suspended
* thread may hold any lock, including malloc's, and the window has to
* be one in which this thread needs none of them. */
static void pb_signal_barrier(void)
{
thread_act_array_t list;
mach_msg_type_number_t n, i;
thread_t self = mach_thread_self();
if (task_threads(mach_task_self(), &list, &n) != KERN_SUCCESS)
{
mach_port_deallocate(mach_task_self(), self);
return;
}
for (i = 0; i < n; i++)
{
thread_basic_info_data_t info;
mach_msg_type_number_t count = THREAD_BASIC_INFO_COUNT;
if (list[i] == self)
continue;
if (thread_info(list[i], THREAD_BASIC_INFO,
(thread_info_t)&info, &count) != KERN_SUCCESS)
continue;
/* Only a thread on a CPU has anything to drain; one that is
* already off was drained by the switch that took it off. */
if (info.run_state != TH_STATE_RUNNING)
continue;
if (thread_suspend(list[i]) == KERN_SUCCESS)
thread_resume(list[i]);
}
for (i = 0; i < n; i++)
mach_port_deallocate(mach_task_self(), list[i]);
vm_deallocate(mach_task_self(), (vm_address_t)list,
n * sizeof(thread_act_t));
mach_port_deallocate(mach_task_self(), self);
}
#endif
#endif /* PB_LINUX || PB_DARWIN */
/* ------------------------------------------------------------------ */
/* Resolution */
/* ------------------------------------------------------------------ */
static int pb_default_signum(void)
{
#if defined(SIGRTMAX)
return SIGRTMAX - 1;
#elif defined(SIGUSR2)
return SIGUSR2;
#else
return 0;
#endif
}
enum retro_procbarrier_tier retro_procbarrier_init(int signum)
{
enum retro_procbarrier_tier t;
{
int cur = retro_atomic_load_acquire_int(&s_tier);
if (cur == PB_RESOLVED_NONE)
return RETRO_PROCBARRIER_NONE;
if (cur != RETRO_PROCBARRIER_NONE)
return (enum retro_procbarrier_tier)cur;
}
s_signum = signum ? signum : pb_default_signum();
/* RETRO_PROCBARRIER=<tier name> forces a tier for testing, the way
* RTHREADS_SCOND does for the condvar. A forced tier still has to be
* available: asking for membarrier on a 4.4 kernel gets the normal
* resolution, not a broken barrier. */
{
const char *force = getenv("RETRO_PROCBARRIER");
if (force && *force)
{
#if defined(PB_LINUX) || defined(PB_FREEBSD_MEMBARRIER)
if (!strcmp(force, "membarrier") && pb_membarrier_try())
{ t = RETRO_PROCBARRIER_MEMBARRIER; goto done; }
#endif
#if defined(PB_WINDOWS)
if (!strcmp(force, "fpwb") && pb_fpwb_try())
{ t = RETRO_PROCBARRIER_FLUSHWRITEBUFFERS; goto done; }
#endif
#if defined(PB_HAVE_PAGEFLIP)
if (!strcmp(force, "pageflip") && pb_pageflip_try())
{ t = RETRO_PROCBARRIER_PAGEFLIP; goto done; }
#endif
#if defined(PB_LINUX) || defined(PB_DARWIN)
if (!strcmp(force, "signal") && s_signum && pb_signal_try(s_signum))
{ t = RETRO_PROCBARRIER_SIGNAL; goto done; }
#endif
}
}
/* One CPU: nothing to fence against. Every tier below already
* short-circuits this in the kernel, but they pay a ring transition
* to find out. */
if (pb_num_cpus() <= 1)
{
t = RETRO_PROCBARRIER_UNIPROCESSOR;
goto done;
}
#if defined(PB_LINUX) || defined(PB_FREEBSD_MEMBARRIER)
if (pb_membarrier_try())
{
t = RETRO_PROCBARRIER_MEMBARRIER;
goto done;
}
#endif
#if defined(PB_WINDOWS)
if (pb_fpwb_try())
{
t = RETRO_PROCBARRIER_FLUSHWRITEBUFFERS;
goto done;
}
#endif
#if defined(PB_HAVE_PAGEFLIP)
/* x86 only, by the #if: the shootdown is not an IPI elsewhere. On
* Windows this is the whole pre-Vista story. */
if (pb_pageflip_try())
{
t = RETRO_PROCBARRIER_PAGEFLIP;
goto done;
}
#endif
#if (defined(PB_LINUX) || defined(PB_DARWIN))
if (s_signum && pb_signal_try(s_signum))
{
t = RETRO_PROCBARRIER_SIGNAL;
goto done;
}
#endif
t = RETRO_PROCBARRIER_NONE;
done:
/* Cache the outcome either way. A platform with nothing is recorded
* as such so the probe runs once, not once per call. */
retro_atomic_store_release_int(&s_tier,
t == RETRO_PROCBARRIER_NONE ? PB_RESOLVED_NONE : (int)t);
return t;
}
enum retro_procbarrier_tier retro_procbarrier_tier(void)
{
int cur = retro_atomic_load_acquire_int(&s_tier);
return cur == PB_RESOLVED_NONE ? RETRO_PROCBARRIER_NONE
: (enum retro_procbarrier_tier)cur;
}
const char *retro_procbarrier_tier_name(enum retro_procbarrier_tier tier)
{
switch (tier)
{
case RETRO_PROCBARRIER_UNIPROCESSOR: return "uniprocessor";
case RETRO_PROCBARRIER_MEMBARRIER: return "membarrier";
case RETRO_PROCBARRIER_FLUSHWRITEBUFFERS: return "FlushProcessWriteBuffers";
case RETRO_PROCBARRIER_PAGEFLIP: return "page-flip";
case RETRO_PROCBARRIER_SIGNAL: return "signal";
default: return "none";
}
}
int retro_procbarrier(void)
{
enum retro_procbarrier_tier t;
{
int cur = retro_atomic_load_acquire_int(&s_tier);
if (cur == PB_RESOLVED_NONE)
return 0; /* probed already; nothing here */
t = (cur == RETRO_PROCBARRIER_NONE)
? retro_procbarrier_init(0) /* first use without init */
: (enum retro_procbarrier_tier)cur;
}
switch (t)
{
case RETRO_PROCBARRIER_UNIPROCESSOR:
retro_atomic_thread_fence_seq_cst();
return 1;
#if defined(PB_LINUX) || defined(PB_FREEBSD_MEMBARRIER)
case RETRO_PROCBARRIER_MEMBARRIER:
pb_membarrier();
return 1;
#endif
#if defined(PB_WINDOWS)
case RETRO_PROCBARRIER_FLUSHWRITEBUFFERS:
s_fpwb();
return 1;
#endif
#if defined(PB_HAVE_PAGEFLIP)
case RETRO_PROCBARRIER_PAGEFLIP:
pb_pageflip();
return 1;
#endif
#if defined(PB_LINUX) || defined(PB_DARWIN)
case RETRO_PROCBARRIER_SIGNAL:
pb_signal_barrier();
return 1;
#endif
default:
return 0;
}
}
+11 -6
View File
@@ -332,12 +332,17 @@ typedef sys_lwcond_attribute_t rthreads_ps3_lwcond_attr_t;
#include <unistd.h>
#endif
/* Android: scond goes straight to the futex. Bionic's condvar issues
* the wake syscall on every signal whether or not anyone is waiting,
* so a waiter count in front of it makes the common empty signal
* free. The constants are spelled out here because they are ABI
* facts, not header facts: the same values from 2.6-era kernels on. */
#if defined(__ANDROID__) && !defined(USE_WIN32_THREADS)
/* Linux: scond goes straight to the futex. The case for it on Android
* is that Bionic's condvar issues the wake syscall on every signal
* whether or not anyone is waiting, so a waiter count in front of it
* makes the common empty signal free. glibc already skips that syscall,
* but its condvar still takes its internal lock on the way to finding
* out: measured on one core, an empty scond_signal costs 3.6 ns through
* pthread_cond and 1.5 ns through the futex path, so the gate is widened
* to every Linux build rather than Bionic alone. The constants are
* spelled out here because they are ABI facts, not header facts: the
* same values from 2.6-era kernels on. */
#if defined(__linux__) && !defined(USE_WIN32_THREADS)
#include <sys/syscall.h>
#include <errno.h>
#define RTHREADS_FUTEX_SCOND 1
+2
View File
@@ -23,6 +23,8 @@ ${CXX:-c++} -std=c++17 -O1 -w $SANFLAGS -I "$ROOT" -I "$ROOT/pcsx2" \
"$ROOT/pcsx2/GameDatabase.cpp" "$ROOT/pcsx2/Pcsx2Config.cpp" \
"$ROOT/common/StringUtil.cpp" "$ROOT/common/Threads.cpp" \
"$ROOT/common/Semaphore.cpp" \
"$LC/rthreads/retro_eventcount.c" "$LC/rthreads/retro_procbarrier.c" \
"$LC/rthreads/retro_asym_eventcount.c" \
"$LC/formats/yaml/ryaml.c" "$LC/rthreads/rthreads.c" \
"$LC/file/file_path.c" "$LC/string/rstrtod.c" "$LC/string/stdstring.c" \
"$LC/compat/fopen_utf8.c" "$LC/compat/compat_strl.c" \
+17
View File
@@ -22,3 +22,20 @@ else
-o "$DIR/spsc_test_tsan" $SRC -lpthread
echo "built: $DIR/spsc_test $DIR/spsc_test_tsan"
fi
# WorkSema contract, on the two-eventcount implementation in common/.
# Plain and TSan, like the queue above: the two threads share nothing
# but the primitive, which is exactly what TSan is for.
if [ "$WINDOWS" != "1" ]; then
LC="$ROOT/libretro/libretro-common"
WS_SRC="$DIR/worksema.cpp $ROOT/common/Semaphore.cpp $ROOT/common/Threads.cpp \
$LC/rthreads/rthreads.c $LC/rthreads/retro_eventcount.c \
$LC/rthreads/retro_procbarrier.c $LC/rthreads/retro_asym_eventcount.c"
WS_INC="-I $ROOT -I $ROOT/common -I $ROOT/common/include -I $LC/include"
${CXX:-c++} -std=c++17 -O1 -g -w -D_GNU_SOURCE -DHAVE_THREADS $WS_INC \
-o "$DIR/spsc_worksema" $WS_SRC -lpthread
"$DIR/spsc_worksema"
${CXX:-c++} -std=c++17 -O1 -g -w -fsanitize=thread -D_GNU_SOURCE -DHAVE_THREADS $WS_INC \
-o "$DIR/spsc_worksema_tsan" $WS_SRC -lpthread
"$DIR/spsc_worksema_tsan"
fi
+165
View File
@@ -0,0 +1,165 @@
/* WorkSema contract, on the eventcount-backed implementation.
*
* The old WorkSema was a hand-rolled state machine; this one is two
* libretro-common eventcounts. Same header, same seven operations, and
* MTGS and MTVU depend on the exact contract, so this pins each part:
*
* 1. NotifyOfWork then CheckForWork returns true once, then false.
* 2. WaitForWork returns at once when work is pending.
* 3. WaitForWork parks with nothing pending and wakes on notify.
* 4. WaitForEmpty returns only after the worker has consumed every
* notify so far and gone idle -- including a notify that lands
* between the worker's last check and its park.
* 5. WaitForWorkTimed returns false only on a timeout with nothing
* pending, true whenever work arrived.
* 6. Kill makes every wait return and WaitForEmpty report false;
* Reset revives.
* 7. A producer/consumer pair run hard, with WaitForEmpty interleaved,
* loses no wake and never sees WaitForEmpty return while work is
* outstanding.
*
* Run under TSan: the two threads share nothing but the primitive.
*
* As with every eventcount test in this series, the lost-wake half of 7
* is only load-bearing on more than one core.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "common/Threading.h"
extern "C" {
#include <rthreads/rthreads.h>
}
static int fails = 0;
#define CHECK(c, msg) do { if (!(c)) { printf(" FAIL: %s\n", msg); fails++; } } while (0)
/* ---- 1, 2, 5, 6: single-threaded contract ------------------------- */
static void contract_single(void)
{
Threading::WorkSema ws;
CHECK(!ws.CheckForWork(), "fresh sema reports no work");
ws.NotifyOfWork();
CHECK(ws.CheckForWork(), "notify makes CheckForWork true");
CHECK(!ws.CheckForWork(), "a second CheckForWork is false");
ws.NotifyOfWork(); ws.NotifyOfWork(); ws.NotifyOfWork();
CHECK(ws.CheckForWork(), "three notifies are one check");
CHECK(!ws.CheckForWork(), "and consumed together");
ws.NotifyOfWork();
ws.WaitForWork(); /* must not block */
CHECK(!ws.CheckForWork(), "WaitForWork consumed the pending notify");
CHECK(!ws.WaitForWorkTimed(5), "timed wait with nothing pending: false");
ws.NotifyOfWork();
CHECK(ws.WaitForWorkTimed(5), "timed wait with work pending: true");
/* "Empty" is the worker having gone idle after consuming, not merely
* having consumed: a worker that took work and has not yet checked
* again is still processing, and WaitForEmpty must block for it. The
* old state machine did exactly this, checked. So the worker goes
* idle first -- one CheckForWork that finds nothing -- and only then
* is WaitForEmpty immediate. */
CHECK(!ws.CheckForWork(), "worker drains and finds nothing: idle");
CHECK(ws.WaitForEmpty(), "idle worker: WaitForEmpty returns at once, true");
ws.Kill();
CHECK(!ws.CheckForWork(), "dead: CheckForWork false");
ws.WaitForWork(); /* must not block */
CHECK(ws.WaitForWorkTimed(1000), "dead: timed wait true at once");
CHECK(!ws.WaitForEmpty(), "dead: WaitForEmpty false");
ws.Reset();
CHECK(!ws.CheckForWork(), "reset: alive and idle");
CHECK(ws.WaitForEmpty(), "reset: WaitForEmpty true");
/* And the first sequence again: a fresh object is idle. */
{
Threading::WorkSema fresh;
CHECK(fresh.WaitForEmpty(), "fresh object: WaitForEmpty true at once");
}
printf(" contract (single thread): %s\n", fails ? "see above" : "ok");
}
/* ---- 3, 4, 7: the pair ---------------------------------------------- */
static Threading::WorkSema g_ws;
static retro_atomic_int_t g_queue; /* items the producer has pushed */
static retro_atomic_int_t g_taken; /* items the worker has drained */
static retro_atomic_int_t g_stop;
static retro_atomic_int_t g_bad_empty; /* WaitForEmpty returned with work outstanding */
static void worker(void*)
{
while (!retro_atomic_load_relaxed_int(&g_stop))
{
g_ws.WaitForWork();
/* Drain: take everything that has been pushed. */
for (;;)
{
int q = retro_atomic_load_acquire_int(&g_queue);
int t = retro_atomic_load_relaxed_int(&g_taken);
if (t >= q) break;
retro_atomic_store_release_int(&g_taken, t + 1);
}
}
}
static void contract_pair(void)
{
sthread_t* th;
int i, empties = 0;
retro_atomic_store_relaxed_int(&g_queue, 0);
retro_atomic_store_relaxed_int(&g_taken, 0);
retro_atomic_store_relaxed_int(&g_stop, 0);
retro_atomic_store_relaxed_int(&g_bad_empty, 0);
th = sthread_create(worker, NULL);
for (i = 0; i < 20000; i++)
{
int q = retro_atomic_load_relaxed_int(&g_queue);
int burst = 1 + (i % 4), b;
for (b = 0; b < burst; b++)
{
retro_atomic_store_release_int(&g_queue, q + b + 1);
g_ws.NotifyOfWork();
}
/* Every so often, the thing MTGS does at vsync. */
if ((i % 7) == 0)
{
CHECK(g_ws.WaitForEmpty(), "WaitForEmpty true while alive");
empties++;
if (retro_atomic_load_acquire_int(&g_taken)
!= retro_atomic_load_acquire_int(&g_queue))
retro_atomic_fetch_add_int(&g_bad_empty, 1);
}
/* A short pause so the worker actually parks sometimes. */
if ((i % 50) == 0) usleep(200);
}
CHECK(g_ws.WaitForEmpty(), "final WaitForEmpty");
CHECK(retro_atomic_load_acquire_int(&g_taken)
== retro_atomic_load_acquire_int(&g_queue), "final drain complete");
CHECK(retro_atomic_load_acquire_int(&g_bad_empty) == 0,
"WaitForEmpty never returned with work outstanding");
retro_atomic_store_relaxed_int(&g_stop, 1);
g_ws.Kill();
sthread_join(th);
printf(" pair: %d items, %d WaitForEmpty calls, %d returned early\n",
retro_atomic_load_relaxed_int(&g_queue), empties,
retro_atomic_load_relaxed_int(&g_bad_empty));
}
int main(void)
{
long n = sysconf(_SC_NPROCESSORS_ONLN);
setvbuf(stdout, NULL, _IONBF, 0);
printf("worksema (eventcount-backed)\n cpus: %ld%s\n", n,
n > 1 ? "" : " (lost-wake half not load-bearing on one core)");
contract_single();
contract_pair();
printf(fails ? "worksema: FAILED (%d)\n" : "worksema: ok\n", fails);
return fails != 0;
}