Files
ARMSX3/Utilities/Thread.h
T
Zulux91 e16f0fcd3d Sample thread CPU time by tid on Android
get_cycles passes the pthread handle to pthread_getcpuclockid, which glibc
answers with an error for a thread that has already exited -- the else branch
below returns the last known value for exactly that case. bionic instead looks
the handle up in its list of live threads and aborts the process when it is not
there.

m_thread is never cleared when a thread ends and the performance overlay samples
every PPU, SPU and RSX thread on a timer, so one finished thread is enough to
take the emulator down with it. Latent here rather than absent: it needs the
overlay on and a thread to have gone.

Record the kernel tid at initialize and build the per-thread clock id from it
the way bionic does once its own lookup succeeds, so clock_gettime simply fails
for a dead thread, which is what the surrounding code already expects. Cleared
at finalize so a thread stops being sampled before it goes away. Other platforms
keep the original path.

Found and fixed by Zulux91 in PS3Native.
2026-08-10 11:42:14 -04:00

916 lines
21 KiB
C++

#pragma once
#include "util/types.hpp"
#include "util/atomic.hpp"
#include "util/shared_ptr.hpp"
#include <thread>
#include <string>
// Hardware core layout
enum class native_core_arrangement : u32
{
undefined,
generic,
intel_ht,
amd_ccx,
// ARM heterogeneous (big.LITTLE / DynamIQ). Cores differ in throughput by
// ~3x, so "any core" is not a sane default for a thread that must keep up
// with an emulated SPE.
arm_big_little
};
enum class thread_class : u32
{
general = 0,
ppu = 1,
spu = 2,
rsx = 0x55,
};
enum class thread_state : u32
{
created = 0, // Initial state
aborting = 1, // The thread has been joined in the destructor or explicitly aborted
errored = 2, // Set after the emergency_exit call
finished = 3, // Final state, always set at the end of thread execution
mask = 3,
destroying_context = 7, // Special value assigned to destroy data explicitly before the destructor
};
template <class Context>
class named_thread;
class thread_base;
template <typename Ctx, typename... Args>
struct result_storage
{
static constexpr bool empty = true;
using type = void;
};
template <typename Ctx, typename... Args> requires (!std::is_void_v<std::invoke_result_t<Ctx, Args&&...>>)
struct result_storage<Ctx, Args...>
{
using T = std::invoke_result_t<Ctx, Args&&...>;
static_assert(std::is_default_constructible_v<T>);
alignas(T) std::byte data[sizeof(T)];
static constexpr bool empty = false;
using type = T;
T* _get()
{
return reinterpret_cast<T*>(&data);
}
const T* _get() const
{
return reinterpret_cast<const T*>(&data);
}
void init() noexcept
{
new (data) T();
}
void destroy() noexcept
{
_get()->~T();
}
};
template <typename T>
concept NamedThreadName = requires (const T&)
{
std::string(T::thread_name);
};
// Base class for task queue (linked list)
class thread_future
{
friend class thread_base;
shared_ptr<thread_future> next{};
thread_future* prev{};
protected:
atomic_t<void(*)(const thread_base*, thread_future*)> exec{};
atomic_t<u32> done{0};
public:
// Get reference to the atomic variable for inspection and waiting for
const auto& get_wait() const
{
return done;
}
// Wait (preset)
void wait() const
{
done.wait(0);
}
};
// Thread base class
class thread_base
{
public:
// Native thread entry point function type
#ifdef _WIN32
using native_entry = uint(__stdcall*)(void* arg);
#else
using native_entry = void*(*)(void* arg);
#endif
const native_entry entry_point;
// Set name for debugger
static void set_name(std::string name);
private:
// Thread handle (platform-specific)
atomic_t<u64> m_thread{0};
#ifdef ANDROID
atomic_t<u32> m_native_tid{0};
#endif
// Thread cycles
atomic_t<u64> m_cycles{0};
atomic_t<u32> m_dummy{0};
// Thread state
atomic_t<u32> m_sync{0};
// Thread name
atomic_ptr<std::string> m_tname;
// Thread task queue (reversed linked list)
atomic_ptr<thread_future> m_taskq{};
// Start thread
void start();
// Called at the thread start
void initialize(void (*error_cb)());
// Called at the thread end, returns self handle
u64 finalize(thread_state result) noexcept;
// Cleanup after possibly deleting the thread instance
static native_entry finalize(u64 _self) noexcept;
// Make entry point
static native_entry make_trampoline(u64(*entry)(thread_base* _base));
friend class thread_ctrl;
template <class Context>
friend class named_thread;
protected:
thread_base(native_entry, std::string name) noexcept;
~thread_base() noexcept;
public:
// Get CPU cycles since last time this function was called. First call returns 0.
u64 get_cycles();
// Wait for the thread (it does NOT change thread state, and can be called from multiple threads)
bool join(bool dtor = false) const;
// Notify the thread
void notify();
// Get thread id
u64 get_native_id() const;
// Add work to the queue
void push(shared_ptr<thread_future>);
private:
// Clear task queue (execute unless aborting)
void exec();
};
// Collection of global function for current thread
class thread_ctrl final
{
// Current thread
static thread_local thread_base* g_tls_this_thread;
// Error handling details
static thread_local void(*g_tls_error_callback)();
// Target cpu core layout
static atomic_t<native_core_arrangement> g_native_core_layout;
friend class thread_base;
// Optimized get_name() for logging
static std::string get_name_cached();
public:
// Get current thread name
static std::string get_name()
{
if (!g_tls_this_thread)
{
return "not named_thread";
}
return *g_tls_this_thread->m_tname.load();
}
// Get thread name
template <typename T>
static std::string get_name(const named_thread<T>& thread)
{
return *static_cast<const thread_base&>(thread).m_tname.load();
}
// Set current thread name (not recommended)
static void set_name(std::string name);
// Set thread name (not recommended)
template <typename T>
static void set_name(named_thread<T>& thread, std::string name)
{
static_cast<thread_base&>(thread).m_tname.store(make_single<std::string>(name));
if (g_tls_this_thread == std::addressof(static_cast<thread_base&>(thread)))
{
g_tls_this_thread->set_name(std::move(name));
}
}
template <typename T>
static u64 get_cycles(named_thread<T>& thread)
{
return static_cast<thread_base&>(thread).get_cycles();
}
template <typename T>
static void notify(named_thread<T>& thread)
{
static_cast<thread_base&>(thread).notify();
}
template <typename T>
static u64 get_native_id(named_thread<T>& thread)
{
return static_cast<thread_base&>(thread).get_native_id();
}
// Read current state, possibly executing some tasks
static thread_state state();
// Wait once with timeout. Infinite value is -1.
static void wait_for(u64 usec, bool alert = true);
// Wait once with time point, add_time is added to the time point.
static void wait_until(u64* wait_time, u64 add_time = 0, u64 min_wait = 0, bool update_to_current_time = true);
// Waiting with accurate timeout
static void wait_for_accurate(u64 usec);
// Wait.
static inline void wait()
{
wait_for(-1, true);
}
// Wait for both thread sync var and provided atomic var
template <uint Max, typename Func>
static inline void wait_on_custom(Func&& setter, u64 usec = -1)
{
auto _this = g_tls_this_thread;
if (_this->m_sync.bit_test_reset(2) || _this->m_taskq)
{
return;
}
atomic_wait::list<Max + 2> list{};
list.template set<Max>(_this->m_sync, 0);
list.template set<Max + 1>(_this->m_taskq);
setter(list);
list.wait(atomic_wait_timeout{usec <= 0xffff'ffff'ffff'ffff / 1000 ? usec * 1000 : 0xffff'ffff'ffff'ffff});
}
template <typename T, typename U>
static inline void wait_on(T& wait, U old, u64 usec = -1)
{
wait_on_custom<1>([&](atomic_wait::list<3>& list) { list.template set<0>(wait, old); }, usec);
}
template <typename T>
static inline void wait_on(T& wait)
{
wait_on_custom<1>([&](atomic_wait::list<3>& list) { list.template set<0>(wait); });
}
// Exit.
[[noreturn]] static void emergency_exit(std::string_view reason);
// Exit the current named thread as errored without reporting a fatal error.
[[noreturn]] static void silent_exit() noexcept;
// Get current thread (may be nullptr)
static thread_base* get_current()
{
return g_tls_this_thread;
}
// Detect layout
static void detect_cpu_layout();
// Returns a core affinity mask. Set whether to generate the high priority set or not
static u64 get_affinity_mask(thread_class group);
// Sets the native thread priority
static void set_native_priority(int priority);
// Sets the preferred affinity mask for this thread
static void set_thread_affinity_mask(u64 mask);
// Get process affinity mask
static u64 get_process_affinity_mask();
// Miscellaneous
static u64 get_thread_affinity_mask();
// Get current thread stack addr and size
static std::pair<void*, usz> get_thread_stack();
// Sets the native thread priority and returns it to zero at destructor
struct scoped_priority
{
explicit scoped_priority(int prio) noexcept
{
set_native_priority(prio);
}
scoped_priority(const scoped_priority&) = delete;
scoped_priority& operator=(const scoped_priority&) = delete;
~scoped_priority() noexcept
{
set_native_priority(0);
}
};
// Get thread ID (works for all threads)
static u64 get_tid();
// Check whether current thread is main thread (usually Qt GUI)
static bool is_main();
private:
// Miscellaneous
static const u64 process_affinity_mask;
};
#if defined(__has_cpp_attribute)
#if __has_cpp_attribute(no_unique_address)
#define NO_UNIQUE_ADDRESS [[no_unique_address]]
#else
#define NO_UNIQUE_ADDRESS
#endif
#else
#define NO_UNIQUE_ADDRESS
#endif
// Used internally
template <bool Discard, typename Ctx, typename... Args>
class thread_future_t : public thread_future, result_storage<Ctx, std::conditional_t<Discard, int, void>, Args...>
{
NO_UNIQUE_ADDRESS decltype(std::make_tuple(std::forward<Args>(std::declval<Args>())...)) m_args;
NO_UNIQUE_ADDRESS Ctx m_func;
using future = thread_future_t;
public:
thread_future_t(Ctx&& func, Args&&... args) noexcept
: m_args(std::forward<Args>(args)...)
, m_func(std::forward<Ctx>(func))
{
thread_future::exec.raw() = +[](const thread_base* tb, thread_future* tf)
{
const auto _this = static_cast<future*>(tf);
if (!tb) [[unlikely]]
{
if constexpr (!future::empty && !Discard)
{
_this->init();
}
return;
}
if constexpr (future::empty || Discard)
{
std::apply(_this->m_func, std::move(_this->m_args));
}
else
{
new (_this->_get()) decltype(auto)(std::apply(_this->m_func, std::move(_this->m_args)));
}
};
}
~thread_future_t() noexcept
{
if constexpr (!future::empty && !Discard)
{
if (!this->exec)
{
this->destroy();
}
}
}
decltype(auto) get()
{
while (this->exec)
{
this->wait();
}
if constexpr (!future::empty && !Discard)
{
return *this->_get();
}
}
decltype(auto) get() const
{
while (this->exec)
{
this->wait();
}
if constexpr (!future::empty && !Discard)
{
return *this->_get();
}
}
};
namespace stx
{
struct launch_retainer;
extern atomic_t<u32> g_launch_retainer;
}
// Derived from the callable object Context, possibly a lambda
template <class Context>
class named_thread final : public Context, result_storage<Context>, thread_base
{
using result = result_storage<Context>;
using thread = thread_base;
static u64 entry_point(thread_base* _base)
{
return static_cast<named_thread*>(_base)->entry_point2();
}
u64 entry_point2()
{
while (u32 value = stx::g_launch_retainer)
{
stx::g_launch_retainer.wait(value);
}
thread::initialize([]()
{
if constexpr (!result::empty)
{
// Construct using default constructor in the case of failure
static_cast<result*>(static_cast<named_thread*>(thread_ctrl::get_current()))->init();
}
});
if constexpr (result::empty)
{
// No result
if constexpr (std::is_invocable_v<Context>)
{
Context::operator()();
}
else
{
// Default event loop
while (thread_ctrl::state() != thread_state::aborting)
{
thread_ctrl::wait();
}
}
}
else
{
// Construct the result using placement new (copy elision should happen)
new (result::_get()) decltype(auto)(Context::operator()());
}
return thread::finalize(thread_state::finished);
}
#if defined(ARCH_X64)
static inline thread::native_entry trampoline = thread::make_trampoline(entry_point);
#else
#ifdef _WIN32
static uint trampoline(void* arg)
#else
static void* trampoline(void* arg)
#endif
{
if (const auto next = thread_base::finalize(entry_point(static_cast<thread_base*>(arg))))
{
return next(thread_ctrl::get_current());
}
return {};
}
#endif
friend class thread_ctrl;
public:
// Forwarding constructor with default name (also potentially the default constructor)
template <typename... Args> requires (std::is_constructible_v<Context, Args&&...>) && (!(std::is_same_v<std::remove_cvref_t<Args>, stx::launch_retainer> || ...)) && (NamedThreadName<Context>)
named_thread(Args&&... args) noexcept
: Context(std::forward<Args>(args)...)
, thread(trampoline, std::string(Context::thread_name))
{
thread::start();
}
// Forwarding constructor with default name, does not automatically run the thread
template <typename... Args> requires (std::is_constructible_v<Context, Args&&...>) && (NamedThreadName<Context>)
named_thread(const stx::launch_retainer&, Args&&... args) noexcept
: Context(std::forward<Args>(args)...)
, thread(trampoline, std::string(Context::thread_name))
{
// Create a stand-by thread context
m_sync |= static_cast<u32>(thread_state::finished);
}
// Normal forwarding constructor
template <typename... Args> requires (std::is_constructible_v<Context, Args&&...>) && (!NamedThreadName<Context>)
named_thread(std::string name, Args&&... args) noexcept
: Context(std::forward<Args>(args)...)
, thread(trampoline, std::move(name))
{
thread::start();
}
// Lambda constructor, also the implicit deduction guide candidate
named_thread(std::string_view name, Context&& f) noexcept requires (!NamedThreadName<Context>)
: Context(std::forward<Context>(f))
, thread(trampoline, std::string(name))
{
thread::start();
}
named_thread(const named_thread&) = delete;
named_thread& operator=(const named_thread&) = delete;
// Wait for the completion and access result (if not void)
[[nodiscard]] decltype(auto) operator()() noexcept
{
thread::join();
if constexpr (!result::empty)
{
return *result::_get();
}
}
// Wait for the completion and access result (if not void)
[[nodiscard]] decltype(auto) operator()() const noexcept
{
thread::join();
if constexpr (!result::empty)
{
return *result::_get();
}
}
// Send command to the thread to invoke directly (references should be passed via std::ref())
template <bool Discard = true, typename Arg, typename... Args>
auto operator()(Arg&& arg, Args&&... args) noexcept
{
// Overloaded operator() of the Context.
constexpr bool v1 = std::is_invocable_v<Context, Arg&&, Args&&...>;
// Anything invocable, not necessarily involving the Context.
constexpr bool v2 = std::is_invocable_v<Arg&&, Args&&...>;
// Could be pointer to a non-static member function (or data member) of the Context.
constexpr bool v3 = std::is_member_pointer_v<std::decay_t<Arg>> && std::is_invocable_v<Arg, Context&, Args&&...>;
// Only one invocation type shall be valid, otherwise we don't know.
static_assert((v1 + v2 + v3) == 1, "Ambiguous or invalid named_thread call.");
if constexpr (v1)
{
using future = thread_future_t<Discard, Context&, Arg, Args...>;
single_ptr<future> target = make_single<future>(*static_cast<Context*>(this), std::forward<Arg>(arg), std::forward<Args>(args)...);
if constexpr (!Discard)
{
shared_ptr<future> result = std::move(target);
// Copy result
thread::push(result);
return result;
}
else
{
// Move target
thread::push(std::move(target));
return;
}
}
else if constexpr (v2)
{
using future = thread_future_t<Discard, Arg, Args...>;
single_ptr<future> target = make_single<future>(std::forward<Arg>(arg), std::forward<Args>(args)...);
if constexpr (!Discard)
{
shared_ptr<future> result = std::move(target);
thread::push(result);
return result;
}
else
{
thread::push(std::move(target));
return;
}
}
else if constexpr (v3)
{
using future = thread_future_t<Discard, Arg, Context&, Args...>;
single_ptr<future> target = make_single<future>(std::forward<Arg>(arg), std::ref(*static_cast<Context*>(this)), std::forward<Args>(args)...);
if constexpr (!Discard)
{
shared_ptr<future> result = std::move(target);
thread::push(result);
return result;
}
else
{
thread::push(std::move(target));
return;
}
}
}
// Access thread state
operator thread_state() const noexcept
{
return static_cast<thread_state>(thread::m_sync.load() & 3);
}
named_thread& operator=(thread_state s) noexcept
{
if (s == thread_state::created)
{
// Run thread
ensure(operator thread_state() == thread_state::finished);
thread::start();
return *this;
}
bool notify_sync = false;
// Try to abort by assigning thread_state::aborting/finished
// Join thread by thread_state::finished
if (s >= thread_state::aborting && thread::m_sync.fetch_op([](u32& v) { return !(v & 3) && (v |= 1); }).second)
{
notify_sync = true;
}
if constexpr (std::is_assignable_v<Context&, thread_state>)
{
static_cast<Context&>(*this) = thread_state::aborting;
}
if (notify_sync)
{
// Notify after context abortion has been made so all conditions for wake-up be satisfied by the time of notification
thread::m_sync.notify_all();
}
if (s == thread_state::finished || s == thread_state::destroying_context)
{
// This participates in emulation stopping, use destruction-alike semantics
thread::join(true);
}
if (s == thread_state::destroying_context)
{
if constexpr (std::is_assignable_v<Context&, thread_state>)
{
static_cast<Context&>(*this) = thread_state::destroying_context;
}
}
return *this;
}
// Context type doesn't need virtual destructor
~named_thread() noexcept
{
// Assign aborting state forcefully and join thread
operator=(thread_state::finished);
if constexpr (!result::empty)
{
result::destroy();
}
}
};
// Group of named threads, similar to named_thread
template <class Context>
class named_thread_group final
{
using Thread = named_thread<Context>;
u32 m_count = 0;
Thread* m_threads;
void init_threads()
{
m_threads = static_cast<Thread*>(::operator new(sizeof(Thread) * m_count, std::align_val_t{alignof(Thread)}));
}
public:
// Lambda constructor, also the implicit deduction guide candidate
named_thread_group(std::string_view name, u32 count, Context&& f) noexcept
: m_count(count)
, m_threads(nullptr)
{
if (count == 0)
{
return;
}
init_threads();
// Create all threads
for (u32 i = 0; i < m_count - 1; i++)
{
// Copy the context
new (static_cast<void*>(m_threads + i)) Thread(std::string(name) + std::to_string(i + 1), static_cast<const Context&>(f));
}
// Move the context (if movable)
new (static_cast<void*>(m_threads + m_count - 1)) Thread(std::string(name) + std::to_string(m_count), std::forward<Context>(f));
}
// Constructor with a function performed before adding more threads
template <typename CheckAndPrepare>
named_thread_group(std::string_view name, u32 count, Context&& f, CheckAndPrepare&& check) noexcept
: m_count(count)
, m_threads(nullptr)
{
if (count == 0)
{
return;
}
init_threads();
m_count = 0;
// Create all threads
for (; m_count < count - 1; m_count++)
{
// Copy the context
std::remove_cvref_t<Context> context(static_cast<const Context&>(f));
// Perform the check and additional preparations for each context
if (!std::invoke(std::forward<CheckAndPrepare>(check), m_count, context))
{
return;
}
new (static_cast<void*>(m_threads + m_count)) Thread(std::string(name) + std::to_string(m_count + 1), std::move(context));
}
// Move the context (if movable)
std::remove_cvref_t<Context> context(std::forward<Context>(f));
if (!std::invoke(std::forward<CheckAndPrepare>(check), m_count, context))
{
return;
}
new (static_cast<void*>(m_threads + m_count)) Thread(std::string(name) + std::to_string(m_count + 1), std::move(context));
m_count++;
}
// Default constructor
named_thread_group(std::string_view name, u32 count) noexcept
: m_count(count)
, m_threads(nullptr)
{
if (count == 0)
{
return;
}
init_threads();
// Create all threads
for (u32 i = 0; i < m_count; i++)
{
new (static_cast<void*>(m_threads + i)) Thread(std::string(name) + std::to_string(i + 1));
}
}
named_thread_group(const named_thread_group&) = delete;
named_thread_group& operator=(const named_thread_group&) = delete;
// Wait for completion
bool join() const
{
bool result = true;
for (u32 i = 0; i < m_count; i++)
{
std::as_const(*std::launder(m_threads + i))();
if (std::as_const(*std::launder(m_threads + i)) != thread_state::finished)
result = false;
}
return result;
}
// Join and access specific thread
auto operator[](u32 index) const
{
return std::as_const(*std::launder(m_threads + index))();
}
// Join and access specific thread
auto operator[](u32 index)
{
return (*std::launder(m_threads + index))();
}
// Dumb iterator
auto begin()
{
return std::launder(m_threads);
}
// Dumb iterator
auto end()
{
return m_threads + m_count;
}
u32 size() const
{
return m_count;
}
~named_thread_group() noexcept
{
// Destroy all threads in reverse order (it should join them)
for (u32 i = 0; i < m_count; i++)
{
std::launder(m_threads + (m_count - i - 1))->~Thread();
}
::operator delete(static_cast<void*>(m_threads), std::align_val_t{alignof(Thread)});
}
};