mirror of
https://github.com/libretro/parallel-ps2.git
synced 2026-11-12 18:44:32 -10:00
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:
1 parent
a9da049460
commit
53f62899ca
21 files changed
+2710
-390
No files matched your search
@@ -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
@@ -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()
|
||||
|
||||
@@ -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('=');
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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();
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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" \
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user