Files
AC6_recomp/thirdparty/rexglue-sdk/src/system/xthread.cpp
T

1034 lines
34 KiB
C++

/**
* ReXGlue runtime - AC6 Recompilation project
* Copyright (c) 2026 Tom Clay. All rights reserved.
*/
#include <algorithm>
#include <atomic>
#include <cstring>
#include <fmt/format.h>
#include <rex/chrono/clock.h>
#include <rex/cvar.h>
#include <rex/dbg.h>
#include <rex/literals.h>
#include <rex/logging.h>
#include <rex/math.h>
#include <rex/ppc/context.h>
#include <rex/ppc/exceptions.h>
#include <rex/system/kernel_state.h>
#include <rex/system/function_dispatcher.h>
#include <rex/system/thread_state.h>
#include <rex/system/user_module.h>
#include <rex/kernel/xboxkrnl/threading.h>
#include <rex/system/xevent.h>
#include <rex/system/xmutant.h>
#include <rex/system/xthread.h>
#include <rex/thread.h>
REXCVAR_DEFINE_BOOL(ignore_thread_priorities, true, "Kernel",
"Ignores game-specified thread priorities");
REXCVAR_DEFINE_BOOL(ignore_thread_affinities, true, "Kernel",
"Ignores game-specified thread affinities");
namespace rex::system {
const uint32_t XAPC::kSize;
const uint32_t XAPC::kDummyKernelRoutine;
const uint32_t XAPC::kDummyRundownRoutine;
using namespace rex::literals;
uint32_t next_xthread_id_ = 0;
XThread::XThread(KernelState* kernel_state)
: XObject(kernel_state, kObjectType), guest_thread_(true) {}
XThread::XThread(KernelState* kernel_state, uint32_t stack_size, uint32_t xapi_thread_startup,
uint32_t start_address, uint32_t start_context, uint32_t creation_flags,
bool guest_thread, bool main_thread, uint32_t guest_process)
: XObject(kernel_state, kObjectType),
thread_id_(++next_xthread_id_),
guest_thread_(guest_thread),
main_thread_(main_thread),
apc_lock_old_irql_(0) {
creation_params_.stack_size = stack_size;
creation_params_.xapi_thread_startup = xapi_thread_startup;
creation_params_.start_address = start_address;
creation_params_.start_context = start_context;
// top 8 bits = processor ID (or 0 for default)
// bit 0 = 1 to create suspended
creation_params_.creation_flags = creation_flags;
creation_params_.guest_process = guest_process;
// Adjust stack size - min of 16k.
if (creation_params_.stack_size < 16 * 1024) {
creation_params_.stack_size = 16 * 1024;
}
if (!guest_thread_) {
host_object_ = true;
}
// The kernel does not take a reference. We must unregister in the dtor.
kernel_state_->RegisterThread(this);
}
XThread::~XThread() {
if (main_fiber_) {
main_fiber_->Destroy();
main_fiber_ = nullptr;
}
// Unregister first to prevent lookups while deleting.
kernel_state_->UnregisterThread(this);
thread_.reset();
kernel_state_->memory()->SystemHeapFree(scratch_address_);
kernel_state_->memory()->SystemHeapFree(tls_static_address_);
kernel_state_->memory()->SystemHeapFree(pcr_address_);
FreeStack();
if (thread_) {
// NOTE(tomc): thread_ is already nullptr here due to reset() above;
// nothing further to do on Windows.
}
}
thread_local XThread* current_xthread_tls_ = nullptr;
namespace {
XThread* GetBoundCurrentXThread() {
return current_xthread_tls_;
}
} // namespace
bool XThread::IsInThread() {
return GetBoundCurrentXThread() != nullptr;
}
bool XThread::IsInThread(XThread* other) {
return GetBoundCurrentXThread() == other;
}
XThread* XThread::GetCurrentThread() {
XThread* thread = GetBoundCurrentXThread();
if (!thread) {
assert_always("Attempting to use kernel stuff from a non-kernel thread");
}
return thread;
}
uint32_t XThread::GetCurrentThreadHandle() {
XThread* thread = XThread::GetCurrentThread();
return thread->handle();
}
uint32_t XThread::GetCurrentThreadId() {
XThread* thread = XThread::GetCurrentThread();
return thread->guest_object<X_KTHREAD>()->thread_id;
}
uint32_t XThread::GetLastError() {
XThread* thread = XThread::GetCurrentThread();
return thread->last_error();
}
void XThread::SetLastError(uint32_t error_code) {
XThread* thread = XThread::GetCurrentThread();
thread->set_last_error(error_code);
}
uint32_t XThread::last_error() {
return guest_object<X_KTHREAD>()->last_error;
}
void XThread::set_last_error(uint32_t error_code) {
guest_object<X_KTHREAD>()->last_error = error_code;
}
void XThread::set_name(const std::string_view name) {
std::lock_guard<std::mutex> lock(thread_lock_);
thread_name_ = fmt::format("{} ({:08X})", name, handle());
if (thread_) {
// May be getting set before the thread is created.
// One the thread is ready it will handle it.
thread_->set_name(thread_name_);
}
}
static uint8_t next_cpu = 0;
static uint8_t GetFakeCpuNumber(uint8_t proc_mask) {
// NOTE: proc_mask is logical processors, not physical processors or cores.
if (!proc_mask) {
next_cpu = (next_cpu + 1) % 6;
return next_cpu; // is this reasonable?
// TODO(Triang3l): Does the following apply here?
// https://docs.microsoft.com/en-us/windows/win32/dxtecharts/coding-for-multiple-cores
// "On Xbox 360, you must explicitly assign software threads to a particular
// hardware thread by using XSetThreadProcessor. Otherwise, all child
// threads will stay on the same hardware thread as the parent."
}
assert_false(proc_mask & 0xC0);
uint8_t cpu_number = 7 - rex::lzcnt(proc_mask);
assert_true(1 << cpu_number == proc_mask);
assert_true(cpu_number < 6);
return cpu_number;
}
void XThread::InitializeGuestObject() {
auto guest_thread = guest_object<X_KTHREAD>();
uint32_t guest_ptr = guest_object();
guest_thread->header.type = 6; // ThreadObject
guest_thread->suspend_count = (creation_params_.creation_flags & X_CREATE_SUSPENDED) ? 1 : 0;
// Self-referencing pointers for wait timeout timer/block
guest_thread->unk_10 = guest_ptr + 0x010;
guest_thread->unk_14 = guest_ptr + 0x010;
guest_thread->wait_timeout_block.wait_list_entry.flink_ptr = guest_ptr + 0x20;
guest_thread->wait_timeout_block.wait_list_entry.blink_ptr = guest_ptr + 0x20;
guest_thread->wait_timeout_block.thread = guest_ptr;
guest_thread->wait_timeout_block.object = guest_ptr + 0x18;
guest_thread->wait_timeout_block.wait_result_xstatus = 0x0100;
guest_thread->wait_timeout_block.wait_type = 0x0201;
guest_thread->stack_base = stack_base_;
guest_thread->stack_limit = stack_limit_;
guest_thread->stack_kernel = stack_base_ - 240;
guest_thread->tls_address = tls_dynamic_address_;
guest_thread->thread_state = 0;
// Initialize APC lists (kernel + user mode)
guest_thread->apc_lists[0].Initialize(memory());
guest_thread->apc_lists[1].Initialize(memory());
// Set process pointer - use guest_process if provided, else default to title process.
uint32_t process_ptr = creation_params_.guest_process
? creation_params_.guest_process
: kernel_state_->process_info_block_address();
guest_thread->process = process_ptr;
guest_thread->may_queue_apcs = 1;
// Set PRCB pointers (derived from this thread's PCR).
uint32_t kpcrb = pcr_address_ + offsetof(X_KPCR, prcb_data);
guest_thread->a_prcb_ptr = kpcrb;
guest_thread->another_prcb_ptr = kpcrb;
// PPCContext for spinlock helpers (valid before thread runs; r13 set at construction).
auto* ctx = thread_state_->context();
// Set per-thread process type and link into process thread list.
if (process_ptr) {
auto target_process = memory()->TranslateVirtual<X_KPROCESS*>(process_ptr);
guest_thread->process_type = target_process->process_type;
guest_thread->process_type_dup = target_process->process_type;
auto old_irql =
kernel::xboxkrnl::xeKeKfAcquireSpinLock(ctx, &target_process->thread_list_spinlock);
util::XeInsertTailList(&target_process->thread_list, &guest_thread->process_threads, memory());
target_process->thread_count = target_process->thread_count + 1;
kernel::xboxkrnl::xeKeKfReleaseSpinLock(ctx, &target_process->thread_list_spinlock, old_irql);
} else {
guest_thread->process_type = X_PROCTYPE_USER;
guest_thread->process_type_dup = X_PROCTYPE_USER;
}
guest_thread->msr_mask = 0xFDFFD7FF;
// current_cpu is expected to be initialized externally via SetActiveCpu.
guest_thread->stack_alloc_base = stack_base_;
guest_thread->create_time = chrono::Clock::QueryGuestSystemTime();
// Initialize timer_list as self-referencing
guest_thread->timer_list.flink_ptr = guest_ptr + offsetof(X_KTHREAD, timer_list);
guest_thread->timer_list.blink_ptr = guest_ptr + offsetof(X_KTHREAD, timer_list);
guest_thread->thread_id = thread_id_;
guest_thread->start_address = creation_params_.start_address;
// Initialize unk_154 list as self-referencing
guest_thread->unk_154.flink_ptr = guest_ptr + offsetof(X_KTHREAD, unk_154);
guest_thread->unk_154.blink_ptr = guest_ptr + offsetof(X_KTHREAD, unk_154);
guest_thread->last_error = 0;
guest_thread->creation_flags = creation_params_.creation_flags;
guest_thread->unk_17C = 1;
}
bool XThread::AllocateStack(uint32_t size) {
auto heap = memory()->LookupHeap(kStackAddressRangeBegin);
auto alignment = heap->page_size();
auto padding = heap->page_size() * 2; // Guard page size * 2
size = rex::round_up(size, alignment);
auto actual_size = size + padding;
uint32_t address = 0;
if (!heap->AllocRange(kStackAddressRangeBegin, kStackAddressRangeEnd, actual_size, alignment,
memory::kMemoryAllocationReserve | memory::kMemoryAllocationCommit,
memory::kMemoryProtectRead | memory::kMemoryProtectWrite, false,
&address)) {
return false;
}
stack_alloc_base_ = address;
stack_alloc_size_ = actual_size;
stack_limit_ = address + (padding / 2);
stack_base_ = stack_limit_ + size;
// Initialize the stack with junk
memory()->Fill(stack_alloc_base_, actual_size, 0xBE);
// Setup the guard pages
heap->Protect(stack_alloc_base_, padding / 2, memory::kMemoryProtectNoAccess);
heap->Protect(stack_base_, padding / 2, memory::kMemoryProtectNoAccess);
return true;
}
void XThread::FreeStack() {
if (stack_alloc_base_) {
auto heap = memory()->LookupHeap(kStackAddressRangeBegin);
heap->Release(stack_alloc_base_);
stack_alloc_base_ = 0;
stack_alloc_size_ = 0;
stack_base_ = 0;
stack_limit_ = 0;
}
}
X_STATUS XThread::Create() {
X_STATUS status = InitializeThreadContext();
if (XFAILED(status)) {
return status;
}
// Always retain when starting - the thread owns itself until exited.
RetainHandle();
rex::thread::Thread::CreationParameters params;
params.stack_size = 16_MiB; // Allocate a big host stack.
params.create_suspended = true;
thread_ = rex::thread::Thread::Create(params, [this]() {
rex::initialize_seh_thread();
runtime::ThreadState::Bind(thread_state_.get());
// Set thread ID override. This is used by logging.
rex::thread::set_current_thread_id(handle());
// Set name immediately, if we have one.
thread_->set_name(thread_name_);
PROFILE_THREAD_ENTER(thread_name_.c_str());
// Execute user code.
current_xthread_tls_ = this;
running_ = true;
Execute();
running_ = false;
current_xthread_tls_ = nullptr;
PROFILE_THREAD_EXIT();
// Release the self-reference to the thread.
ReleaseHandle();
});
if (!thread_) {
// NOTE(tomc): CreateThread failure is surfaced as X_STATUS_NO_MEMORY.
REXSYS_ERROR("CreateThread failed");
return X_STATUS_NO_MEMORY;
}
// Set the thread name based on host ID (for easier debugging).
if (thread_name_.empty()) {
set_name(fmt::format("XThread{:04X}", thread_->system_id()));
}
if (creation_params_.creation_flags & 0x60) {
thread_->set_priority(creation_params_.creation_flags & 0x20 ? 1 : 0);
}
// TODO(tomc): do we need thread notifications (related to processor thread management)?
if ((creation_params_.creation_flags & X_CREATE_SUSPENDED) == 0) {
// Start the thread now that we're all setup.
thread_->Resume();
}
return X_STATUS_SUCCESS;
}
X_STATUS XThread::PrepareHostContext() {
return InitializeThreadContext();
}
X_STATUS XThread::InitializeThreadContext() {
if (thread_state_) {
return X_STATUS_SUCCESS;
}
// Thread kernel object.
if (!CreateNative<X_KTHREAD>()) {
REXSYS_WARN("Unable to allocate thread object");
return X_STATUS_NO_MEMORY;
}
// Allocate a stack.
if (!AllocateStack(creation_params_.stack_size)) {
return X_STATUS_NO_MEMORY;
}
// Allocate thread scratch.
// This is used by interrupts/APCs/etc so we can round-trip pointers through.
scratch_size_ = 4 * 16;
scratch_address_ = memory()->SystemHeapAlloc(scratch_size_);
// Allocate TLS block.
// Games will specify a certain number of 4b slots that each thread will get.
xex2_opt_tls_info* tls_header = nullptr;
auto module = kernel_state_->GetExecutableModule();
if (module) {
module->GetOptHeader(XEX_HEADER_TLS_INFO, &tls_header);
}
const uint32_t kDefaultTlsSlotCount = 1024;
uint32_t tls_slots = kDefaultTlsSlotCount;
uint32_t tls_extended_size = 0;
if (tls_header && tls_header->slot_count) {
tls_slots = tls_header->slot_count;
tls_extended_size = tls_header->data_size;
}
uint32_t tls_slot_size = tls_slots * 4;
tls_total_size_ = tls_slot_size + tls_extended_size;
tls_static_address_ = memory()->SystemHeapAlloc(tls_total_size_);
tls_dynamic_address_ = tls_static_address_ + tls_extended_size;
if (!tls_static_address_) {
REXSYS_WARN("Unable to allocate thread local storage block");
return X_STATUS_NO_MEMORY;
}
memory()->Fill(tls_static_address_, tls_total_size_, 0);
if (tls_extended_size) {
assert_not_zero(tls_header->raw_data_address);
memory()->Copy(tls_static_address_, tls_header->raw_data_address, tls_header->raw_data_size);
}
pcr_address_ = memory()->SystemHeapAlloc(0x2D8);
if (!pcr_address_) {
REXSYS_WARN("Unable to allocate thread state block");
return X_STATUS_NO_MEMORY;
}
thread_state_ =
std::make_unique<runtime::ThreadState>(thread_id_, stack_base_, pcr_address_, memory());
thread_state_->context()->kernel_state = kernel_state_;
REXSYS_DEBUG("XThread{:08X} ({:X}) Stack: {:08X}-{:08X}", handle(), thread_id_, stack_limit_,
stack_base_);
uint8_t cpu_index = GetFakeCpuNumber(static_cast<uint8_t>(creation_params_.creation_flags >> 24));
InitializeGuestObject();
X_KPCR* pcr = memory()->TranslateVirtual<X_KPCR*>(pcr_address_);
pcr->tls_ptr = tls_static_address_;
pcr->pcr_ptr = pcr_address_;
pcr->prcb_data.current_thread = guest_object();
pcr->prcb = pcr_address_ + offsetof(X_KPCR, prcb_data);
pcr->host_stash = reinterpret_cast<uint64_t>(thread_state_->context());
pcr->current_irql = 0;
pcr->stack_base_ptr = stack_base_;
pcr->stack_end_ptr = stack_limit_;
pcr->prcb_data.dpc_active = 0;
SetActiveCpu(cpu_index);
return X_STATUS_SUCCESS;
}
void XThread::BindToCurrentHostThread() {
assert_not_null(thread_state_.get());
runtime::ThreadState::Bind(thread_state_.get());
rex::thread::set_current_thread_id(handle());
current_xthread_tls_ = this;
running_ = true;
}
void XThread::UnbindFromCurrentHostThread() {
if (XThread::IsInThread(this)) {
current_xthread_tls_ = nullptr;
}
runtime::ThreadState::Bind(nullptr);
running_ = false;
}
X_STATUS XThread::Exit(int exit_code) {
// This may only be called on the thread itself.
assert_true(XThread::GetCurrentThread() == this);
// Keep the object alive until Thread::Exit() transitions the host thread
// into pthread_exit(). Otherwise ReleaseHandle() below may delete `this`
// while this method is still running.
auto self = retain_object(this);
// Mark as terminated before running down APCs.
auto kthread = guest_object<X_KTHREAD>();
kthread->terminated = 1;
// TODO(benvanik): dispatch events? waiters? etc?
RundownAPCs();
// Set exit code.
X_KTHREAD* thread = guest_object<X_KTHREAD>();
thread->header.signal_state = 1;
thread->exit_status = exit_code;
// Unlink thread from process thread list.
uint32_t process_guest = thread->process;
if (process_guest) {
auto* ctx = thread_state_->context();
auto kprocess = memory()->TranslateVirtual<X_KPROCESS*>(process_guest);
auto old_irql = kernel::xboxkrnl::xeKeKfAcquireSpinLock(ctx, &kprocess->thread_list_spinlock);
util::XeRemoveEntryList(&thread->process_threads, memory());
kprocess->thread_count = kprocess->thread_count - 1;
kernel::xboxkrnl::xeKeKfReleaseSpinLock(ctx, &kprocess->thread_list_spinlock, old_irql);
}
kernel_state_->OnThreadExit(this);
// TODO(tomc): do we need thread notifications (related to processor thread management)?
// NOTE: unless PlatformExit fails, expect it to never return!
current_xthread_tls_ = nullptr;
PROFILE_THREAD_EXIT();
running_ = false;
ReleaseHandle();
// NOTE: this does not return!
rex::thread::Thread::Exit(exit_code);
return X_STATUS_SUCCESS;
}
X_STATUS XThread::Terminate(int exit_code) {
// TODO(benvanik): inform the profiler that this thread is exiting.
// Set exit code.
X_KTHREAD* thread = guest_object<X_KTHREAD>();
thread->header.signal_state = 1;
thread->exit_status = exit_code;
// TODO(tomc): do we need thread notifications (related to processor thread management)?
running_ = false;
if (XThread::IsInThread(this)) {
// Same lifetime rule as Exit(): don't allow ReleaseHandle() to destroy
// the thread object before Thread::Exit() reaches pthread_exit().
auto self = retain_object(this);
ReleaseHandle();
rex::thread::Thread::Exit(exit_code);
} else {
thread_->Terminate(exit_code);
ReleaseHandle();
}
return X_STATUS_SUCCESS;
}
void XThread::Execute() {
REXSYS_DEBUG("Execute thid {} (handle={:08X}, '{}', native={:08X})", thread_id_, handle(),
thread_name_, thread_->system_id());
// Let the kernel know we are starting.
kernel_state_->OnThreadExecute(this);
// Dispatch any APCs that were queued before the thread was created first.
DeliverAPCs();
uint32_t address;
std::vector<uint64_t> args;
bool want_exit_code;
int exit_code = 0;
// If a XapiThreadStartup value is present, we use that as a trampoline.
// Otherwise, we are a raw thread.
if (creation_params_.xapi_thread_startup) {
address = creation_params_.xapi_thread_startup;
args.push_back(creation_params_.start_address);
args.push_back(creation_params_.start_context);
want_exit_code = false;
} else {
// Run user code.
address = creation_params_.start_address;
args.push_back(creation_params_.start_context);
want_exit_code = true;
}
// NOTE(tomc): JIT execution replaced with direct function calls
// In rexglue, guest code is compiled ahead of time and called directly.
// The start_address points to a 32bit guest address, for which the function
// dispatcher maintains a lookup table to retrieve the host function pointer.
auto* dispatcher = kernel_state_->function_dispatcher();
auto* mem = memory();
if (!dispatcher || !mem) {
REXSYS_ERROR("XThread::Execute - Kernel services not initialized");
return;
}
PPCFunc* func = dispatcher->GetFunction(address);
if (!func) {
REXSYS_ERROR("XThread::Execute - No function registered at {:08X}", address);
return;
}
auto* ctx = thread_state_->context();
uint8_t* base = mem->virtual_membase();
// Pass arguments in r3, r4, ... per PPC calling convention
if (args.size() > 0)
ctx->r3.u64 = args[0];
if (args.size() > 1)
ctx->r4.u64 = args[1];
if (args.size() > 2)
ctx->r5.u64 = args[2];
if (args.size() > 3)
ctx->r6.u64 = args[3];
if (args.size() > 4)
ctx->r7.u64 = args[4];
if (args.size() > 5)
ctx->r8.u64 = args[5];
if (args.size() > 6)
ctx->r9.u64 = args[6];
if (args.size() > 7)
ctx->r10.u64 = args[7];
ctx->fpscr.InitHost();
// Convert this host thread to a fiber so SwitchTo works bidirectionally.
// Required on Windows before any CreateFiber; provides the fallback handle
// when another fiber switches back to the main execution context.
main_fiber_ = rex::thread::Fiber::ConvertCurrentThread();
// Execute the function
REXSYS_DEBUG("XThread::Execute - Calling function at {:08X}", address);
func(*ctx, base);
exit_code = static_cast<int>(ctx->r3.u32);
// If we got here it means the execute completed without an exit being called.
// Treat the return code as an implicit exit code (if desired).
Exit(!want_exit_code ? 0 : exit_code);
}
void XThread::EnterCriticalRegion() {
guest_object<X_KTHREAD>()->apc_disable_count--;
}
void XThread::LeaveCriticalRegion() {
auto kthread = guest_object<X_KTHREAD>();
auto apc_disable_count = ++kthread->apc_disable_count;
if (apc_disable_count == 0) {
// NOTE: intentionally not calling CheckApcs() here.
// Delivering APCs here can cause them to fire in wrong contexts.
}
}
void XThread::LockApc() {
auto kthread = guest_object<X_KTHREAD>();
apc_lock_old_irql_ =
kernel::xboxkrnl::xeKeKfAcquireSpinLock(thread_state_->context(), &kthread->apc_lock);
}
void XThread::UnlockApc(bool queue_delivery) {
auto kthread = guest_object<X_KTHREAD>();
auto mem = memory();
bool needs_apc = !kthread->apc_lists[0].empty(mem) || !kthread->apc_lists[1].empty(mem);
kernel::xboxkrnl::xeKeKfReleaseSpinLock(thread_state_->context(), &kthread->apc_lock,
apc_lock_old_irql_);
if (needs_apc && queue_delivery) {
// Match Edge/Canary behavior: callback is only a wakeup hint.
// User APC execution happens on alertable wait return paths.
thread_->QueueUserCallback([]() {});
}
}
void XThread::EnqueueApc(uint32_t normal_routine, uint32_t normal_context, uint32_t arg1,
uint32_t arg2) {
uint32_t apc_ptr = memory()->SystemHeapAlloc(XAPC::kSize);
if (!apc_ptr) {
REXSYS_WARN("EnqueueApc: allocation failed (thid={}, normal={:08X})", thread_id_,
normal_routine);
return;
}
auto apc = memory()->TranslateVirtual<XAPC*>(apc_ptr);
kernel::xboxkrnl::xeKeInitializeApc(apc, guest_object(), XAPC::kDummyKernelRoutine,
XAPC::kDummyRundownRoutine, normal_routine,
1 /* user apc mode */, normal_context);
// Important: use the caller PPC context when queuing to another thread.
// Using the target thread context here can corrupt APC lock/IRQL bookkeeping.
PPCContext* queue_ctx =
runtime::ThreadState::Get() ? runtime::current_ppc_context() : thread_state_->context();
if (!kernel::xboxkrnl::xeKeInsertQueueApc(apc, arg1, arg2, 0, queue_ctx)) {
memory()->SystemHeapFree(apc_ptr);
REXSYS_ERROR(
"EnqueueApc: queue rejected (thid={}, normal={:08X}, ctx={:08X}, arg1={:08X}, "
"arg2={:08X})",
thread_id_, normal_routine, normal_context, arg1, arg2);
return;
}
// Match Edge/Canary behavior: callback is only a wakeup hint.
// APCs are delivered via alertable wait handling.
thread_->QueueUserCallback([]() {});
}
void XThread::DeliverAPCs() {
auto mem = memory();
auto* ctx = thread_state_->context();
auto kthread = guest_object<X_KTHREAD>();
auto* dispatcher = kernel_state_->function_dispatcher();
auto old_irql = kernel::xboxkrnl::xeKeKfAcquireSpinLock(ctx, &kthread->apc_lock);
auto& user_apc_queue = kthread->apc_lists[1];
while (!user_apc_queue.empty(mem) && kthread->apc_disable_count == 0) {
XAPC* apc = user_apc_queue.HeadObject(mem);
uint32_t apc_ptr = mem->HostToGuestVirtual(apc);
bool needs_freeing = apc->kernel_routine != XAPC::kDummyKernelRoutine;
util::XeRemoveEntryList(&apc->list_entry, mem);
apc->enqueued = 0;
kernel::xboxkrnl::xeKeKfReleaseSpinLock(ctx, &kthread->apc_lock, old_irql);
uint8_t* scratch_ptr = mem->TranslateVirtual(scratch_address_);
memory::store_and_swap<uint32_t>(scratch_ptr + 0, apc->normal_routine);
memory::store_and_swap<uint32_t>(scratch_ptr + 4, apc->normal_context);
memory::store_and_swap<uint32_t>(scratch_ptr + 8, apc->arg1);
memory::store_and_swap<uint32_t>(scratch_ptr + 12, apc->arg2);
if (apc->kernel_routine != XAPC::kDummyKernelRoutine) {
if (dispatcher->GetFunction(apc->kernel_routine)) {
uint64_t kernel_args[] = {apc_ptr, scratch_address_ + 0, scratch_address_ + 4,
scratch_address_ + 8, scratch_address_ + 12};
dispatcher->Execute(thread_state_.get(), apc->kernel_routine, kernel_args,
rex::countof(kernel_args));
} else {
REXSYS_ERROR("DeliverAPCs: kernel_routine {:08X} not found", uint32_t(apc->kernel_routine));
}
} else {
mem->SystemHeapFree(apc_ptr);
needs_freeing = false;
}
uint32_t normal_routine = memory::load_and_swap<uint32_t>(scratch_ptr + 0);
uint32_t normal_context = memory::load_and_swap<uint32_t>(scratch_ptr + 4);
uint32_t arg1 = memory::load_and_swap<uint32_t>(scratch_ptr + 8);
uint32_t arg2 = memory::load_and_swap<uint32_t>(scratch_ptr + 12);
if (normal_routine) {
if (dispatcher->GetFunction(normal_routine)) {
uint64_t normal_args[] = {normal_context, arg1, arg2};
dispatcher->Execute(thread_state_.get(), normal_routine, normal_args,
rex::countof(normal_args));
} else {
REXSYS_ERROR("DeliverAPCs: normal_routine {:08X} not found", normal_routine);
}
}
REXSYS_DEBUG("Completed delivery of APC to {:08X} ({:08X}, {:08X}, {:08X})", normal_routine,
normal_context, arg1, arg2);
if (needs_freeing) {
mem->SystemHeapFree(apc_ptr);
}
old_irql = kernel::xboxkrnl::xeKeKfAcquireSpinLock(ctx, &kthread->apc_lock);
}
kernel::xboxkrnl::xeKeKfReleaseSpinLock(ctx, &kthread->apc_lock, old_irql);
}
void XThread::RundownAPCs() {
assert_true(XThread::GetCurrentThread() == this);
auto mem = memory();
auto kthread = guest_object<X_KTHREAD>();
auto* ctx = thread_state_->context();
// Rundown both user (1) and kernel (0) APC lists.
for (int mode = 1; mode >= 0; --mode) {
auto old_irql = kernel::xboxkrnl::xeKeKfAcquireSpinLock(ctx, &kthread->apc_lock);
auto& apc_queue = kthread->apc_lists[mode];
while (!apc_queue.empty(mem)) {
XAPC* apc = apc_queue.HeadObject(mem);
uint32_t apc_ptr = mem->HostToGuestVirtual(apc);
bool needs_freeing = apc->kernel_routine == XAPC::kDummyKernelRoutine;
util::XeRemoveEntryList(&apc->list_entry, mem);
apc->enqueued = 0;
kernel::xboxkrnl::xeKeKfReleaseSpinLock(ctx, &kthread->apc_lock, old_irql);
if (apc->rundown_routine == XAPC::kDummyRundownRoutine) {
// No-op.
} else if (apc->rundown_routine) {
auto fn = kernel_state_->function_dispatcher()->GetFunction(apc->rundown_routine);
if (fn) {
auto* ctx = thread_state_->context();
ctx->r3.u64 = apc_ptr;
fn(*ctx, mem->virtual_membase());
} else {
REXSYS_WARN("RundownAPCs: rundown_routine {:08X} not found",
uint32_t(apc->rundown_routine));
}
}
if (needs_freeing) {
mem->SystemHeapFree(apc_ptr);
}
old_irql = kernel::xboxkrnl::xeKeKfAcquireSpinLock(ctx, &kthread->apc_lock);
}
kernel::xboxkrnl::xeKeKfReleaseSpinLock(ctx, &kthread->apc_lock, old_irql);
}
}
int32_t XThread::QueryPriority() {
return thread_->priority();
}
void XThread::SetPriority(int32_t increment) {
priority_ = increment;
// Write priority to guest X_KTHREAD struct.
auto kthread = guest_object<X_KTHREAD>();
kthread->priority = static_cast<uint8_t>(std::clamp(increment, 0, 31));
int32_t target_priority = 0;
if (increment > 0x22) {
target_priority = rex::thread::ThreadPriority::kHighest;
} else if (increment > 0x11) {
target_priority = rex::thread::ThreadPriority::kAboveNormal;
} else if (increment < -0x22) {
target_priority = rex::thread::ThreadPriority::kLowest;
} else if (increment < -0x11) {
target_priority = rex::thread::ThreadPriority::kBelowNormal;
} else {
target_priority = rex::thread::ThreadPriority::kNormal;
}
if (!REXCVAR_GET(ignore_thread_priorities)) {
thread_->set_priority(target_priority);
}
}
void XThread::SetAffinity(uint32_t affinity) {
SetActiveCpu(GetFakeCpuNumber(affinity));
}
uint8_t XThread::active_cpu() const {
// Prefer reading from guest KTHREAD (always available for guest threads,
// kept in sync by SetActiveCpu). Avoids dependency on pcr_address_ which
// may not be set yet if the thread is mid-creation.
if (is_guest_thread()) {
auto* kthread = memory()->TranslateVirtual<const X_KTHREAD*>(guest_object());
return kthread->current_cpu;
}
if (!pcr_address_) {
return 0;
}
const X_KPCR& pcr = *memory()->TranslateVirtual<const X_KPCR*>(pcr_address_);
return pcr.prcb_data.current_cpu;
}
void XThread::SetActiveCpu(uint8_t cpu_index) {
// May be called during thread creation - don't skip if current == new.
assert_true(cpu_index < 6);
// Write to guest KTHREAD (always available for guest threads).
if (is_guest_thread()) {
X_KTHREAD& thread_object = *memory()->TranslateVirtual<X_KTHREAD*>(guest_object());
thread_object.current_cpu = cpu_index;
}
// Write to PCR if allocated (may not be during early creation).
if (pcr_address_) {
X_KPCR& pcr = *memory()->TranslateVirtual<X_KPCR*>(pcr_address_);
pcr.prcb_data.current_cpu = cpu_index;
}
if (thread_ && rex::thread::logical_processor_count() >= 6) {
if (!REXCVAR_GET(ignore_thread_affinities)) {
thread_->set_affinity_mask(uint64_t(1) << cpu_index);
}
} else if (thread_) {
REXSYS_WARN("Too few processor cores - scheduling will be wonky");
}
}
bool XThread::GetTLSValue(uint32_t slot, uint32_t* value_out) {
if (slot * 4 > tls_total_size_) {
return false;
}
auto mem = memory()->TranslateVirtual(tls_dynamic_address_ + slot * 4);
*value_out = memory::load_and_swap<uint32_t>(mem);
return true;
}
bool XThread::SetTLSValue(uint32_t slot, uint32_t value) {
if (slot * 4 >= tls_total_size_) {
return false;
}
auto mem = memory()->TranslateVirtual(tls_dynamic_address_ + slot * 4);
memory::store_and_swap<uint32_t>(mem, value);
return true;
}
uint32_t XThread::suspend_count() {
return guest_object<X_KTHREAD>()->suspend_count;
}
X_STATUS XThread::Resume(uint32_t* out_suspend_count) {
auto guest_thread = guest_object<X_KTHREAD>();
uint32_t unused_host_suspend_count = 0;
#if REX_PLATFORM_WIN32
uint8_t previous_suspend_count =
reinterpret_cast<std::atomic_uint8_t*>(&guest_thread->suspend_count)->fetch_sub(1);
if (out_suspend_count) {
*out_suspend_count = previous_suspend_count;
}
return thread_->Resume(&unused_host_suspend_count) ? X_STATUS_SUCCESS : X_STATUS_UNSUCCESSFUL;
#elif REX_PLATFORM_LINUX
bool should_resume_host = false;
{
std::lock_guard<std::mutex> lock(suspend_mutex_);
uint8_t previous = guest_thread->suspend_count;
if (previous > 0) {
guest_thread->suspend_count--;
}
if (out_suspend_count) {
*out_suspend_count = previous;
}
should_resume_host = (guest_thread->suspend_count == 0);
suspend_cv_.notify_all();
}
// Self-suspended threads are resumed via guest suspend count transitions.
if (should_resume_host) {
thread_->Resume(&unused_host_suspend_count);
}
return X_STATUS_SUCCESS;
#else
uint8_t previous_suspend_count = guest_thread->suspend_count;
if (guest_thread->suspend_count > 0) {
--guest_thread->suspend_count;
}
if (out_suspend_count) {
*out_suspend_count = previous_suspend_count;
}
return thread_->Resume(&unused_host_suspend_count) ? X_STATUS_SUCCESS : X_STATUS_UNSUCCESSFUL;
#endif
}
X_STATUS XThread::Suspend(uint32_t* out_suspend_count) {
auto guest_thread = guest_object<X_KTHREAD>();
uint8_t previous_suspend_count =
reinterpret_cast<std::atomic_uint8_t*>(&guest_thread->suspend_count)->fetch_add(1);
if (out_suspend_count) {
*out_suspend_count = previous_suspend_count;
}
uint32_t unused_host_suspend_count = 0;
// Wrapped to 0 - treat as not suspended.
if (guest_thread->suspend_count == 0) {
return X_STATUS_SUCCESS;
}
return thread_->Suspend(&unused_host_suspend_count) ? X_STATUS_SUCCESS : X_STATUS_UNSUCCESSFUL;
}
#if REX_PLATFORM_LINUX
uint32_t XThread::SelfSuspend() {
auto guest_thread = guest_object<X_KTHREAD>();
std::unique_lock<std::mutex> lock(suspend_mutex_);
uint32_t previous = guest_thread->suspend_count;
guest_thread->suspend_count++;
suspend_cv_.wait(lock, [guest_thread]() { return guest_thread->suspend_count == 0; });
return previous;
}
#endif
X_STATUS XThread::Delay(uint32_t processor_mode, uint32_t alertable, uint64_t interval) {
int64_t timeout_ticks = interval;
uint32_t timeout_ms;
if (timeout_ticks > 0) {
// Absolute time, based on January 1, 1601.
// NOTE(tomc): absolute-time sleep not exercised by AC6; assert retained as a guard.
assert_always();
timeout_ms = 0;
} else if (timeout_ticks < 0) {
// Relative time.
timeout_ms = uint32_t(-timeout_ticks / 10000); // Ticks -> MS
} else {
timeout_ms = 0;
}
timeout_ms = chrono::Clock::ScaleGuestDurationMillis(timeout_ms);
if (alertable) {
auto result = rex::thread::AlertableSleep(std::chrono::milliseconds(timeout_ms));
switch (result) {
default:
case rex::thread::SleepResult::kSuccess:
return X_STATUS_SUCCESS;
case rex::thread::SleepResult::kAlerted:
return X_STATUS_USER_APC;
}
} else {
if (timeout_ms == 0) {
if (priority_ <= rex::thread::ThreadPriority::kBelowNormal) {
rex::thread::Sleep(std::chrono::microseconds(100));
} else {
rex::thread::MaybeYield();
}
} else {
rex::thread::Sleep(std::chrono::milliseconds(timeout_ms));
}
}
return X_STATUS_SUCCESS;
}
XHostThread::XHostThread(KernelState* kernel_state, uint32_t stack_size, uint32_t creation_flags,
std::function<int()> host_fn)
: XThread(kernel_state, stack_size, 0, 0, 0, creation_flags, false), host_fn_(host_fn) {
// NOTE(tomc): there was a start suspended check here before but I don't think we need it.
}
void XHostThread::Execute() {
REXSYS_INFO("XThread::Execute thid {} (handle={:08X}, '{}', native={:08X}, <host>)", thread_id_,
handle(), thread_name_, thread_->system_id());
// Let the kernel know we are starting.
kernel_state_->OnThreadExecute(this);
int ret = host_fn_();
// Exit.
Exit(ret);
}
} // namespace rex::system