diff --git a/src/common-tests/CMakeLists.txt b/src/common-tests/CMakeLists.txt
index fbd05bda7..8b2026883 100644
--- a/src/common-tests/CMakeLists.txt
+++ b/src/common-tests/CMakeLists.txt
@@ -19,6 +19,7 @@ add_executable(common-tests
small_string_tests.cpp
string_pool_tests.cpp
string_tests.cpp
+ threading_tests.cpp
)
target_include_directories(common-tests PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/..")
diff --git a/src/common-tests/common-tests.vcxproj b/src/common-tests/common-tests.vcxproj
index 4fd9de3a0..bafde57ac 100644
--- a/src/common-tests/common-tests.vcxproj
+++ b/src/common-tests/common-tests.vcxproj
@@ -16,6 +16,7 @@
+
diff --git a/src/common-tests/common-tests.vcxproj.filters b/src/common-tests/common-tests.vcxproj.filters
index 074a6d511..bbd1b87ae 100644
--- a/src/common-tests/common-tests.vcxproj.filters
+++ b/src/common-tests/common-tests.vcxproj.filters
@@ -16,5 +16,6 @@
+
diff --git a/src/common-tests/threading_tests.cpp b/src/common-tests/threading_tests.cpp
new file mode 100644
index 000000000..85adba12d
--- /dev/null
+++ b/src/common-tests/threading_tests.cpp
@@ -0,0 +1,157 @@
+// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin
+// SPDX-License-Identifier: CC-BY-NC-ND-4.0
+
+#include "common/threading.h"
+
+#include
+
+#include
+#include
+#include
+#include
+#include
+
+static_assert(!std::is_copy_constructible_v);
+static_assert(!std::is_copy_assignable_v);
+static_assert(!std::is_copy_constructible_v);
+static_assert(!std::is_copy_assignable_v);
+
+TEST(ThreadingMutex, TryLock)
+{
+ Threading::Mutex mutex;
+ EXPECT_TRUE(mutex.try_lock());
+ EXPECT_FALSE(mutex.try_lock());
+ mutex.unlock();
+ EXPECT_TRUE(mutex.try_lock());
+ mutex.unlock();
+}
+
+TEST(ThreadingMutex, StandardLockWrappers)
+{
+ Threading::Mutex mutex;
+
+ {
+ const std::lock_guard lock(mutex);
+ EXPECT_FALSE(mutex.try_lock());
+ }
+
+ {
+ std::unique_lock lock(mutex);
+ EXPECT_TRUE(lock.owns_lock());
+ EXPECT_FALSE(mutex.try_lock());
+ }
+
+ EXPECT_TRUE(mutex.try_lock());
+ mutex.unlock();
+}
+
+TEST(ThreadingMutex, MutualExclusion)
+{
+ static constexpr u32 NUM_THREADS = 4;
+ static constexpr u32 NUM_INCREMENTS = 10000;
+
+ Threading::Mutex mutex;
+ u32 value = 0;
+ std::vector threads;
+ threads.reserve(NUM_THREADS);
+ for (u32 i = 0; i < NUM_THREADS; i++)
+ {
+ threads.emplace_back([&mutex, &value]() {
+ for (u32 j = 0; j < NUM_INCREMENTS; j++)
+ {
+ const std::lock_guard lock(mutex);
+ value++;
+ }
+ });
+ }
+
+ for (std::thread& thread : threads)
+ thread.join();
+
+ EXPECT_EQ(value, NUM_THREADS * NUM_INCREMENTS);
+}
+
+TEST(ThreadingConditionVariable, NotifyOneAndPredicateWait)
+{
+ Threading::Mutex mutex;
+ Threading::ConditionVariable condition;
+ Threading::KernelSemaphore waiting;
+ bool wake = false;
+ bool woke = false;
+
+ std::thread thread([&]() {
+ std::unique_lock lock(mutex);
+ waiting.Post();
+ condition.wait(lock, [&wake]() { return wake; });
+ woke = true;
+ });
+
+ waiting.Wait();
+ {
+ const std::lock_guard lock(mutex);
+ EXPECT_FALSE(woke);
+ wake = true;
+ }
+ condition.notify_one();
+ thread.join();
+ EXPECT_TRUE(woke);
+}
+
+TEST(ThreadingConditionVariable, NotifyAll)
+{
+ static constexpr u32 NUM_THREADS = 4;
+
+ Threading::Mutex mutex;
+ Threading::ConditionVariable condition;
+ Threading::KernelSemaphore waiting;
+ bool wake = false;
+ u32 num_woken = 0;
+ std::vector threads;
+ threads.reserve(NUM_THREADS);
+ for (u32 i = 0; i < NUM_THREADS; i++)
+ {
+ threads.emplace_back([&]() {
+ std::unique_lock lock(mutex);
+ waiting.Post();
+ condition.wait(lock, [&wake]() { return wake; });
+ num_woken++;
+ });
+ }
+
+ for (u32 i = 0; i < NUM_THREADS; i++)
+ waiting.Wait();
+
+ {
+ const std::lock_guard lock(mutex);
+ wake = true;
+ }
+ condition.notify_all();
+
+ for (std::thread& thread : threads)
+ thread.join();
+
+ EXPECT_EQ(num_woken, NUM_THREADS);
+}
+
+TEST(ThreadingKernelSemaphore, PostWaitAndTryWait)
+{
+ Threading::KernelSemaphore semaphore;
+ EXPECT_FALSE(semaphore.TryWait());
+ semaphore.Post();
+ EXPECT_TRUE(semaphore.TryWait());
+ EXPECT_FALSE(semaphore.TryWait());
+
+ Threading::KernelSemaphore started;
+ std::atomic_bool finished = false;
+ std::thread thread([&]() {
+ started.Post();
+ semaphore.Wait();
+ finished.store(true, std::memory_order_release);
+ });
+
+ started.Wait();
+ EXPECT_FALSE(finished.load(std::memory_order_acquire));
+ semaphore.Post();
+ thread.join();
+ EXPECT_TRUE(finished.load(std::memory_order_acquire));
+}
diff --git a/src/common/threading.cpp b/src/common/threading.cpp
index 278a56441..2f5dbdcff 100644
--- a/src/common/threading.cpp
+++ b/src/common/threading.cpp
@@ -22,7 +22,11 @@
#include "windows_headers.h"
#include
#else
+#include
#include
+#if !defined(__APPLE__)
+#include
+#endif
#include
#if defined(__linux__)
#include
@@ -49,6 +53,20 @@
LOG_CHANNEL(Threading);
+#ifdef _WIN32
+#define NATIVE_MUTEX_PTR(storage) static_cast(static_cast(&(storage)))
+#define NATIVE_CONDITION_VARIABLE_PTR(storage) static_cast(static_cast(&(storage)))
+#else
+#define NATIVE_MUTEX_PTR(storage) static_cast(static_cast(&(storage)))
+#define NATIVE_CONDITION_VARIABLE_PTR(storage) static_cast(static_cast(&(storage)))
+#endif
+
+#if defined(__APPLE__)
+#define NATIVE_SEMAPHORE(storage) (*static_cast(static_cast(&(storage))))
+#elif !defined(_WIN32)
+#define NATIVE_SEMAPHORE(storage) static_cast(static_cast(&(storage)))
+#endif
+
#ifdef _WIN32
union FileTimeU64Union
{
@@ -549,6 +567,129 @@ Threading::ThreadHandle& Threading::Thread::operator=(Thread&& thread)
return *this;
}
+#ifdef _WIN32
+
+// Trick we do to avoid the constructor/destructor.
+static_assert(sizeof(SRWLOCK) == sizeof(void*));
+static_assert(alignof(SRWLOCK) == alignof(void*));
+static_assert(
+ []() {
+ const SRWLOCK ensure_srwlock_init_is_zero = SRWLOCK_INIT;
+ return (ensure_srwlock_init_is_zero.Ptr == nullptr);
+ }(),
+ "SRWLOCK_INIT is equivalent to null");
+
+#else // _WIN32
+
+Threading::Mutex::Mutex()
+{
+ static_assert(sizeof(m_data) == sizeof(pthread_mutex_t));
+ static_assert(alignof(Mutex) >= alignof(pthread_mutex_t));
+ [[maybe_unused]] const int result = pthread_mutex_init(NATIVE_MUTEX_PTR(m_data), nullptr);
+ DebugAssert(result == 0);
+}
+
+Threading::Mutex::~Mutex()
+{
+ [[maybe_unused]] const int result = pthread_mutex_destroy(NATIVE_MUTEX_PTR(m_data));
+ DebugAssert(result == 0);
+}
+
+#endif // _WIN32
+
+void Threading::Mutex::lock()
+{
+#ifdef _WIN32
+ AcquireSRWLockExclusive(NATIVE_MUTEX_PTR(m_data));
+#else
+ [[maybe_unused]] const int result = pthread_mutex_lock(NATIVE_MUTEX_PTR(m_data));
+ DebugAssert(result == 0);
+#endif
+}
+
+bool Threading::Mutex::try_lock()
+{
+#ifdef _WIN32
+ return TryAcquireSRWLockExclusive(NATIVE_MUTEX_PTR(m_data)) != FALSE;
+#else
+ const int result = pthread_mutex_trylock(NATIVE_MUTEX_PTR(m_data));
+ DebugAssert(result == 0 || result == EBUSY);
+ return result == 0;
+#endif
+}
+
+void Threading::Mutex::unlock()
+{
+#ifdef _WIN32
+ ReleaseSRWLockExclusive(NATIVE_MUTEX_PTR(m_data));
+#else
+ [[maybe_unused]] const int result = pthread_mutex_unlock(NATIVE_MUTEX_PTR(m_data));
+ DebugAssert(result == 0);
+#endif
+}
+
+#ifndef _WIN32
+
+Threading::ConditionVariable::ConditionVariable()
+{
+ static_assert(sizeof(m_data) == sizeof(pthread_cond_t));
+ static_assert(alignof(ConditionVariable) >= alignof(pthread_cond_t));
+ [[maybe_unused]] const int result = pthread_cond_init(NATIVE_CONDITION_VARIABLE_PTR(m_data), nullptr);
+ DebugAssert(result == 0);
+}
+
+Threading::ConditionVariable::~ConditionVariable()
+{
+ [[maybe_unused]] const int result = pthread_cond_destroy(NATIVE_CONDITION_VARIABLE_PTR(m_data));
+ DebugAssert(result == 0);
+}
+
+#else // _WIN32
+
+static_assert(sizeof(CONDITION_VARIABLE) == sizeof(void*));
+static_assert(alignof(CONDITION_VARIABLE) == alignof(void*));
+static_assert(
+ []() {
+ const CONDITION_VARIABLE ensure_condition_variable_init_is_zero = CONDITION_VARIABLE_INIT;
+ return (ensure_condition_variable_init_is_zero.Ptr == nullptr);
+ }(),
+ "CONDITION_VARIABLE_INIT is equivalent to null");
+
+#endif // _WIN32
+
+void Threading::ConditionVariable::notify_one()
+{
+#ifdef _WIN32
+ WakeConditionVariable(NATIVE_CONDITION_VARIABLE_PTR(m_data));
+#else
+ [[maybe_unused]] const int result = pthread_cond_signal(NATIVE_CONDITION_VARIABLE_PTR(m_data));
+ DebugAssert(result == 0);
+#endif
+}
+
+void Threading::ConditionVariable::notify_all()
+{
+#ifdef _WIN32
+ WakeAllConditionVariable(NATIVE_CONDITION_VARIABLE_PTR(m_data));
+#else
+ [[maybe_unused]] const int result = pthread_cond_broadcast(NATIVE_CONDITION_VARIABLE_PTR(m_data));
+ DebugAssert(result == 0);
+#endif
+}
+
+void Threading::ConditionVariable::Wait(Mutex& mutex)
+{
+#ifdef _WIN32
+ [[maybe_unused]] const BOOL result =
+ SleepConditionVariableSRW(NATIVE_CONDITION_VARIABLE_PTR(m_data), NATIVE_MUTEX_PTR(mutex.m_data), INFINITE, 0);
+ DebugAssert(result);
+#else
+ [[maybe_unused]] const int result =
+ pthread_cond_wait(NATIVE_CONDITION_VARIABLE_PTR(m_data), NATIVE_MUTEX_PTR(mutex.m_data));
+ DebugAssert(result == 0);
+#endif
+}
+
u64 Threading::GetThreadCpuTime()
{
#if defined(_WIN32) && !defined(_M_ARM64)
@@ -673,15 +814,21 @@ void Threading::SetNameOfCurrentThread(const char* name)
Threading::KernelSemaphore::KernelSemaphore()
{
#ifdef _WIN32
- m_sema = CreateSemaphore(nullptr, 0, LONG_MAX, nullptr);
- if (m_sema == NULL) [[unlikely]]
+ static_assert(sizeof(m_data) == sizeof(HANDLE));
+ static_assert(alignof(KernelSemaphore) >= alignof(HANDLE));
+ m_data = CreateSemaphore(nullptr, 0, LONG_MAX, nullptr);
+ if (m_data == nullptr) [[unlikely]]
Panic("CreateSemaphore() failed");
#elif defined(__APPLE__)
- const kern_return_t kr = semaphore_create(mach_task_self(), &m_sema, SYNC_POLICY_FIFO, 0);
+ static_assert(sizeof(m_data) >= sizeof(semaphore_t));
+ static_assert(alignof(KernelSemaphore) >= alignof(semaphore_t));
+ const kern_return_t kr = semaphore_create(mach_task_self(), &NATIVE_SEMAPHORE(m_data), SYNC_POLICY_FIFO, 0);
if (kr != KERN_SUCCESS) [[unlikely]]
Panic("CreateSemaphore() failed");
#else
- if (sem_init(&m_sema, false, 0) != 0) [[unlikely]]
+ static_assert(sizeof(m_data) == sizeof(sem_t));
+ static_assert(alignof(KernelSemaphore) >= alignof(sem_t));
+ if (sem_init(NATIVE_SEMAPHORE(m_data), false, 0) != 0) [[unlikely]]
Panic("sem_init() failed");
#endif
}
@@ -689,35 +836,35 @@ Threading::KernelSemaphore::KernelSemaphore()
Threading::KernelSemaphore::~KernelSemaphore()
{
#ifdef _WIN32
- CloseHandle(m_sema);
+ CloseHandle(m_data);
#elif defined(__APPLE__)
- semaphore_destroy(mach_task_self(), m_sema);
+ semaphore_destroy(mach_task_self(), NATIVE_SEMAPHORE(m_data));
#else
- sem_destroy(&m_sema);
+ sem_destroy(NATIVE_SEMAPHORE(m_data));
#endif
}
void Threading::KernelSemaphore::Post()
{
#ifdef _WIN32
- ReleaseSemaphore(m_sema, 1, nullptr);
+ ReleaseSemaphore(m_data, 1, nullptr);
#elif defined(__APPLE__)
- semaphore_signal(m_sema);
+ semaphore_signal(NATIVE_SEMAPHORE(m_data));
#else
- sem_post(&m_sema);
+ sem_post(NATIVE_SEMAPHORE(m_data));
#endif
}
void Threading::KernelSemaphore::Wait()
{
#ifdef _WIN32
- WaitForSingleObject(m_sema, INFINITE);
+ WaitForSingleObject(m_data, INFINITE);
#elif defined(__APPLE__)
- semaphore_wait(m_sema);
+ semaphore_wait(NATIVE_SEMAPHORE(m_data));
#else
do
{
- if (sem_wait(&m_sema) == 0) [[likely]]
+ if (sem_wait(NATIVE_SEMAPHORE(m_data)) == 0) [[likely]]
return;
} while (errno == EINTR);
#endif
@@ -726,12 +873,18 @@ void Threading::KernelSemaphore::Wait()
bool Threading::KernelSemaphore::TryWait()
{
#ifdef _WIN32
- return WaitForSingleObject(m_sema, 0) == WAIT_OBJECT_0;
+ return WaitForSingleObject(m_data, 0) == WAIT_OBJECT_0;
#elif defined(__APPLE__)
mach_timespec_t time = {};
- kern_return_t res = semaphore_timedwait(m_sema, time);
+ kern_return_t res = semaphore_timedwait(NATIVE_SEMAPHORE(m_data), time);
return (res != KERN_OPERATION_TIMED_OUT);
#else
- return sem_trywait(&m_sema) == 0;
+ return sem_trywait(NATIVE_SEMAPHORE(m_data)) == 0;
#endif
}
+
+#undef NATIVE_MUTEX_PTR
+#undef NATIVE_CONDITION_VARIABLE_PTR
+#if !defined(_WIN32)
+#undef NATIVE_SEMAPHORE
+#endif
diff --git a/src/common/threading.h b/src/common/threading.h
index b6f71ca81..2309fbb4e 100644
--- a/src/common/threading.h
+++ b/src/common/threading.h
@@ -5,12 +5,6 @@
#include "types.h"
-#if defined(__APPLE__)
-#include
-#elif !defined(_WIN32)
-#include
-#endif
-
#include
#include
@@ -119,6 +113,107 @@ protected:
#endif
};
+// --------------------------------------------------------------------------------------
+// Mutex
+// --------------------------------------------------------------------------------------
+// A lightweight replacement for std::mutex. The native object is stored inline to avoid
+// the oversized standard library representation on Windows, without exposing platform
+// headers to users of this header.
+//
+class Mutex
+{
+public:
+#ifdef _WIN32
+ Mutex() = default;
+#else
+ Mutex();
+ ~Mutex();
+#endif
+
+ Mutex(const Mutex&) = delete;
+ Mutex& operator=(const Mutex&) = delete;
+
+ void lock();
+ bool try_lock();
+ void unlock();
+
+private:
+ friend class ConditionVariable;
+
+#if defined(_WIN32)
+ void* m_data = nullptr;
+#elif defined(__APPLE__)
+ static constexpr u32 NATIVE_STORAGE_SIZE = 64;
+#elif defined(__ANDROID__)
+ static constexpr u32 NATIVE_STORAGE_SIZE = (sizeof(void*) == 8) ? 40 : 4;
+#elif defined(__linux__) && defined(CPU_ARCH_ARM64)
+ static constexpr u32 NATIVE_STORAGE_SIZE = 48;
+#elif defined(__linux__)
+ static constexpr u32 NATIVE_STORAGE_SIZE = (sizeof(void*) == 8) ? 40 : 24;
+#else
+#error Unsupported platform.
+#endif
+
+#if !defined(_WIN32)
+ alignas(void*) u8 m_data[NATIVE_STORAGE_SIZE];
+#endif
+};
+
+// --------------------------------------------------------------------------------------
+// ConditionVariable
+// --------------------------------------------------------------------------------------
+// A lightweight replacement for the subset of std::condition_variable used by the
+// project. Spurious wakeups are permitted, matching std::condition_variable.
+//
+class ConditionVariable
+{
+public:
+#ifdef _WIN32
+ ConditionVariable() = default;
+#else
+ ConditionVariable();
+ ~ConditionVariable();
+#endif
+
+ ConditionVariable(const ConditionVariable&) = delete;
+ ConditionVariable& operator=(const ConditionVariable&) = delete;
+
+ void notify_one();
+ void notify_all();
+
+ template
+ void wait(LockType& lock)
+ {
+ Wait(*lock.mutex());
+ }
+
+ template
+ void wait(LockType& lock, Predicate predicate)
+ {
+ while (!predicate())
+ wait(lock);
+ }
+
+private:
+ void Wait(Mutex& mutex);
+
+#if defined(_WIN32)
+ void* m_data = nullptr;
+#elif defined(__APPLE__) || (defined(__linux__) && !defined(__ANDROID__))
+ static constexpr u32 NATIVE_STORAGE_SIZE = 48;
+ static constexpr u32 NATIVE_STORAGE_ALIGNMENT = 8;
+#elif defined(__ANDROID__)
+ static constexpr u32 NATIVE_STORAGE_SIZE = (sizeof(void*) == 8) ? 48 : 4;
+ static constexpr u32 NATIVE_STORAGE_ALIGNMENT = alignof(void*);
+#else
+#error Unsupported platform.
+#endif
+
+#if !defined(_WIN32)
+ alignas(NATIVE_STORAGE_ALIGNMENT) u8 m_data[NATIVE_STORAGE_SIZE];
+#endif
+};
+
/// A semaphore that requires a system call to wake/sleep.
class KernelSemaphore
{
@@ -134,13 +229,19 @@ public:
bool TryWait();
private:
-#if defined(_WIN32)
- void* m_sema;
-#elif defined(__APPLE__)
- semaphore_t m_sema;
+#if defined(_WIN32) || defined(__APPLE__)
+ void* m_data = nullptr;
+#elif defined(__ANDROID__)
+ static constexpr u32 NATIVE_STORAGE_SIZE = (sizeof(void*) == 8) ? 16 : 4;
+#elif defined(__linux__)
+ static constexpr u32 NATIVE_STORAGE_SIZE = (sizeof(void*) == 8) ? 32 : 16;
#else
- sem_t m_sema;
+#error Unsupported platform.
+#endif
+
+#if !defined(_WIN32) && !defined(__APPLE__)
+ alignas(void*) u8 m_data[NATIVE_STORAGE_SIZE] = {};
#endif
};
-} // namespace Threading
\ No newline at end of file
+} // namespace Threading