Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .patch/sirit/0001-include-cstdlib.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
diff --git a/src/stream.h b/src/stream.h
--- a/src/stream.h
+++ b/src/stream.h
@@ -10,6 +10,7 @@
#include <cassert>
#include <concepts>
#include <cstddef>
+#include <cstdlib>
#include <functional>
#include <string_view>
#include <unordered_map>
5 changes: 4 additions & 1 deletion cpmfile.json
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,10 @@
"SIRIT_USE_SYSTEM_SPIRV_HEADERS ON"
],
"repo": "eden-emulator/sirit",
"version": "v1.0.5"
"version": "v1.0.5",
"patches": [
"0001-include-cstdlib.patch"
]
},
"sirit-ci": {
"ci": true,
Expand Down
186 changes: 90 additions & 96 deletions src/common/thread.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
// SPDX-FileCopyrightText: 2014 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later

#include <algorithm>
#include <chrono>
#include <limits>
#include <string>
#include <thread>

Expand All @@ -22,7 +22,6 @@
#elif defined(_WIN32)
#include <windows.h>
#include "common/string_util.h"
#include "common/windows/timer_resolution.h"
#else
#if defined(__FreeBSD__)
#include <sys/cpuset.h>
Expand Down Expand Up @@ -437,16 +436,6 @@ void RememberCurrentThreadNice(pid_t tid, s32 nice_value) {
} // Anonymous namespace
#endif

#include "common/cpu_features.h"
#ifdef ARCHITECTURE_x86_64
#ifdef _MSC_VER
#include <intrin.h>
#else
#include <x86intrin.h>
#endif
#include "common/x64/rdtsc.h"
#endif

namespace Common {

void SetCurrentThreadPriority(ThreadPriority new_priority) {
Expand Down Expand Up @@ -629,102 +618,107 @@ void PollThreadPolicies() {
#endif
}

#ifdef ARCHITECTURE_x86_64
// On Linux and UNIX systems, a futex would nominally be used to cover the costs
// the idea is that it's intuitivelly cheaper to use a direct instruction as opposed to a full futex call
// the underlying libc++ implementation uses pthread_cond_timedwait which MAY invoke a futex
// Let's pretend the OS is too expensive to jump into, and avoid ANY context switches
// this should *IN THEORY* lower CPU usage while just waiting for stuff effectively
// For windows the minimal quanta resolution is about 500us, and normal CRT cond var is 1.5ms(?)
// so may as well avoid that too
// Let's just give ALL platforms the same mechanisms (almost) for when they have umonitor OR waitpkg
#ifdef __clang__
__attribute__((target("waitpkg,mwaitx")))
#elif defined(__GNUC__)
#pragma GCC target("waitpkg")
#pragma GCC target("mwaitx")
#ifdef _WIN32
/// @brief Closes the Windows wake handle; all waiters must have finished before destruction.
Event::~Event() {
if (timed_wait_event) {
CloseHandle(timed_wait_event);
}
}

/// @brief Signals the Windows wake handle if allocated. The caller must hold mutex.
void Event::WakeTimedWaiters() {
if (timed_wait_event) {
SetEvent(timed_wait_event);
}
}
#endif

/**
* @brief Waits for and consumes a signal, returning false on timeout.
* Windows uses a per-thread timer and shared wake event, with blocking fallbacks.
* An existing signal is consumed even when time is zero or negative.
*/
bool Event::WaitFor(const std::chrono::nanoseconds time) {
std::unique_lock lk{mutex};
#ifdef _WIN32
auto const start = Common::X64::FencedRDTSC();
auto const& caps = Common::g_cpu_caps;
[[maybe_unused]] auto const end = start + Common::g_wall_clock.NsToTicks(time);
if (caps.monitorx) {
while (true) {
// Armed monitor, as per manual, MWAITX must be conditional if the condition isn't satisfied
// to prevent a race condition.
_mm_monitorx(reinterpret_cast<u64*>(std::addressof(is_set)), 0, 0);
if (!is_set.load()) {
// RDTSC may be fenced here due to atomic load
#ifdef _MSC_VER
auto const now = __rdtsc();
#else
auto const now = _rdtsc();
#endif
if (end > now) {
u32 const cycles = std::min<u32>((std::numeric_limits<u32>::max)(), s64(end) - s64(now));
// See here: https://github.com/torvalds/linux/blob/948a64995aca6820abefd17f1a4258f5835c5ad9/arch/x86/lib/delay.c#L93
// MWAITX accepts a 32-bit input timer which determines the total number of cycles to wait for
// NOT THE TOTAL ABSOLUTE TSC VALUE, it's just a delta
// BIT[1] = use a timer
// Hint = 0: Use C1 state when sleepy (means slower wakeup but better savings)
_mm_mwaitx(1 << 1, 0u, cycles);
if (!is_set.load())
return false;
} else
return false; //timeout
using Clock = std::chrono::steady_clock;
const auto now = Clock::now();
const auto deadline = time > Clock::time_point::max() - now
? Clock::time_point::max()
: now + time;
while (!is_set.load()) {
if (Clock::now() >= deadline) {
return false;
}

// Each waiting host thread owns its timer, so concurrent waits cannot rearm
// each other's deadlines. Reuse the handle across the host timing loop.
struct Timer {
/// @brief Creates a high-resolution timer, falling back to a regular timer.
Timer() {
handle = CreateWaitableTimerExW(nullptr, nullptr,
CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, TIMER_MODIFY_STATE | SYNCHRONIZE);
if (!handle) {
handle = CreateWaitableTimerExW(nullptr, nullptr, 0,
TIMER_MODIFY_STATE | SYNCHRONIZE);
}
}
/// @brief Closes the allocated timer when this host thread exits.
~Timer() {
if (handle) {
CloseHandle(handle);
}
}
bool expected = true;
if (is_set.compare_exchange_weak(expected, false, std::memory_order_release))
return true;
HANDLE handle{};
};
static thread_local Timer timer;
if (!timed_wait_event) {
timed_wait_event = CreateEventW(nullptr, TRUE, FALSE, nullptr);
}
} else if (caps.waitpkg) {
// #UD If CPUID.7.0:ECX.WAITPKG[bit 5]=0.
while (true) {
_umonitor(std::addressof(is_set));
if (!is_set.load() && !_umwait(0, end)) //umwait is absolute time!!!
return false;
bool expected = true;
if (is_set.compare_exchange_weak(expected, false, std::memory_order_release))
return true;
const auto remaining = deadline - Clock::now();
if (remaining <= Clock::duration::zero()) {
return false;
}

// The event is reset while holding the predicate lock. Set() cannot race
// between this reset and the wait without leaving a persistent wake signal.
if (timed_wait_event) {
ResetEvent(timed_wait_event);
using TimerTicks = std::chrono::duration<s64, std::ratio<1, 10'000'000>>;
LARGE_INTEGER due{};
due.QuadPart = -std::chrono::ceil<TimerTicks>(remaining).count();
const bool armed = timer.handle &&
SetWaitableTimerEx(timer.handle, &due, 0, nullptr, nullptr, nullptr, 0);
const HANDLE handles[]{timed_wait_event, timer.handle};
const auto milliseconds = std::chrono::ceil<std::chrono::milliseconds>(remaining);
const DWORD timeout = static_cast<DWORD>(
(std::min<s64>)(milliseconds.count(), INFINITE - 1));
lk.unlock();
const DWORD result = armed ? WaitForMultipleObjects(2, handles, FALSE, INFINITE)
: WaitForSingleObject(timed_wait_event, timeout);
if (armed) {
CancelWaitableTimer(timer.handle);
}
lk.lock();
if (result != WAIT_FAILED) {
continue;
}
}

// Retain a blocking fallback if Windows cannot allocate or wait on handles.
const auto fallback = std::chrono::ceil<std::chrono::milliseconds>(deadline - Clock::now());
if (fallback > std::chrono::milliseconds::zero()) {
condvar.wait_for(lk, fallback, [this] { return is_set.load(); });
}
} else {
#ifdef _MSC_VER
while (!is_set.load() && end > __rdtsc())
Common::Windows::SleepForOneTick();
#else
while (!is_set.load() && end > _rdtsc())
Common::Windows::SleepForOneTick();
#endif
if (is_set.load())
Reset();
return true;
}
#else
std::unique_lock lk{mutex};
if (!condvar.wait_for(lk, time, [this] { return is_set.load(); }))
if (!condvar.wait_for(lk, time, [this] { return is_set.load(); })) {
return false;
is_set = false;
return true;
}
#endif
}
#else
bool Event::WaitFor(const std::chrono::nanoseconds time) {
#ifdef _WIN32
auto const end = Common::g_wall_clock.GetTimeNS() + time;
while (!is_set.load() && end > Common::g_wall_clock.GetTimeNS())
Common::Windows::SleepForOneTick();
if (is_set.load())
Reset();
return true;
#else
std::unique_lock lk{mutex};
if (!condvar.wait_for(lk, time, [this] { return is_set.load(); }))
return false;
is_set = false;
return true;
#endif
}
#endif

} // namespace Common
18 changes: 18 additions & 0 deletions src/common/thread.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,17 @@ namespace Common {

class Event {
public:
#ifdef _WIN32
~Event();
#endif
/// @brief Sets a pending signal and notifies waiters; repeated sets coalesce until consumed.
void Set() {
std::scoped_lock lk{mutex};
if (!is_set) {
is_set = true;
#ifdef _WIN32
WakeTimedWaiters();
#endif
condvar.notify_one();
}
}
Expand All @@ -34,6 +41,11 @@ class Event {
is_set = false;
}

/**
* @brief Waits for a signal and consumes it on success.
* @param time Relative timeout; non-positive values only consume an existing signal.
* @return True if a signal was consumed, or false if the wait timed out.
*/
bool WaitFor(const std::chrono::nanoseconds time);

template<class Clock, class Duration>
Expand All @@ -57,9 +69,15 @@ class Event {
}

private:
#ifdef _WIN32
void WakeTimedWaiters();
#endif
alignas(64) std::atomic<bool> is_set{false};
std::condition_variable condvar;
std::mutex mutex;
#ifdef _WIN32
void* timed_wait_event{};
#endif
};

class Barrier {
Expand Down
1 change: 1 addition & 0 deletions src/tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ add_executable(tests
common/bit_field.cpp
common/cityhash.cpp
common/container_hash.cpp
common/event.cpp
common/fibers.cpp
common/game_presets.cpp
common/host_memory.cpp
Expand Down
Loading