/** * ReXGlue runtime - AC6 Recompilation project * Copyright (c) 2026 Tom Clay. All rights reserved. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include 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()->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()->last_error; } void XThread::set_last_error(uint32_t error_code) { guest_object()->last_error = error_code; } void XThread::set_name(const std::string_view name) { std::lock_guard 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(); 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(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()) { 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(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(creation_params_.creation_flags >> 24)); InitializeGuestObject(); X_KPCR* pcr = memory()->TranslateVirtual(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(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(); kthread->terminated = 1; // TODO(benvanik): dispatch events? waiters? etc? RundownAPCs(); // Set exit code. X_KTHREAD* thread = guest_object(); 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(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(); 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 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(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()->apc_disable_count--; } void XThread::LeaveCriticalRegion() { auto kthread = guest_object(); 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(); apc_lock_old_irql_ = kernel::xboxkrnl::xeKeKfAcquireSpinLock(thread_state_->context(), &kthread->apc_lock); } void XThread::UnlockApc(bool queue_delivery) { auto kthread = guest_object(); 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(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(); 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(scratch_ptr + 0, apc->normal_routine); memory::store_and_swap(scratch_ptr + 4, apc->normal_context); memory::store_and_swap(scratch_ptr + 8, apc->arg1); memory::store_and_swap(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(scratch_ptr + 0); uint32_t normal_context = memory::load_and_swap(scratch_ptr + 4); uint32_t arg1 = memory::load_and_swap(scratch_ptr + 8); uint32_t arg2 = memory::load_and_swap(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(); 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(); kthread->priority = static_cast(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(guest_object()); return kthread->current_cpu; } if (!pcr_address_) { return 0; } const X_KPCR& pcr = *memory()->TranslateVirtual(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(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(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(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(mem, value); return true; } uint32_t XThread::suspend_count() { return guest_object()->suspend_count; } X_STATUS XThread::Resume(uint32_t* out_suspend_count) { auto guest_thread = guest_object(); uint32_t unused_host_suspend_count = 0; #if REX_PLATFORM_WIN32 uint8_t previous_suspend_count = reinterpret_cast(&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 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(); uint8_t previous_suspend_count = reinterpret_cast(&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(); std::unique_lock 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 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}, )", 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