Files
Xenia-Canary/src/xenia/kernel/xthread.h
Herman S. 65b74819aa [Threading] Implement priority boost on wake
When a thread wakes from a kernel wait, the Xenon scheduler boosts its
effective priority by the increment passed to the signaling call
(KeSetEvent, KeReleaseSemaphore, KeReleaseMutant). The boost is clamped
to the per-thread max_dynamic_priority cap, respects the guest
boost_disabled flag, and is drained on the next quantum expiry.
Guest KTHREAD priority fields are now initialized from parent process
defaults, and the previously unknown fields involved have been renamed
to match their identified purpose.
2026-04-11 17:05:05 +09:00

549 lines
20 KiB
C++

/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2020 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#ifndef XENIA_KERNEL_XTHREAD_H_
#define XENIA_KERNEL_XTHREAD_H_
#include <atomic>
#include <string>
#include "xenia/base/mutex.h"
#if !XE_PLATFORM_WIN32
#include <condition_variable>
#include <csignal>
#include <mutex>
#endif
#if XE_PLATFORM_WIN32
#include <csetjmp>
#endif
#include "xenia/base/threading.h"
#include "xenia/cpu/thread.h"
#include "xenia/cpu/thread_state.h"
#include "xenia/kernel/util/native_list.h"
#include "xenia/kernel/util/xfiletime.h"
#include "xenia/kernel/xmutant.h"
#include "xenia/kernel/xobject.h"
#include "xenia/xbox.h"
namespace xe {
namespace kernel {
constexpr fourcc_t kThreadSaveSignature = make_fourcc("THRD");
class XEvent;
enum IRQL_FLAGS : uint8_t {
IRQL_PASSIVE = 0,
IRQL_APC = 1,
IRQL_DISPATCH = 2,
IRQL_DPC = 3,
IRQL_AUDIO = 68, // used a few times in the audio driver
IRQL_CLOCK = 116, // irql used by the clock interrupt
IRQL_HIGHEST = 124
};
enum X_DISPATCHER_FLAGS {
DISPATCHER_MANUAL_RESET_EVENT = 0,
DISPATCHER_AUTO_RESET_EVENT = 1,
DISPATCHER_MUTANT = 2,
DISPATCHER_QUEUE = 4,
DISPATCHER_SEMAPHORE = 5,
DISPATCHER_THREAD = 6,
DISPATCHER_MANUAL_RESET_TIMER = 8,
DISPATCHER_AUTO_RESET_TIMER = 9,
};
// https://www.geoffchappell.com/studies/windows/km/ntoskrnl/inc/ntos/ke/kthread_state.htm
enum X_KTHREAD_STATE_FLAGS : uint8_t {
KTHREAD_STATE_INITIALIZED = 0,
KTHREAD_STATE_READY = 1,
KTHREAD_STATE_RUNNING = 2,
KTHREAD_STATE_STANDBY = 3,
KTHREAD_STATE_TERMINATED = 4,
KTHREAD_STATE_WAITING = 5,
KTHREAD_STATE_UNKNOWN = 6, //"Transition" except that makes no sense here, so
// 6 likely has a different meaning on xboxkrnl
};
constexpr uint32_t X_CREATE_SUSPENDED = 0x00000001;
constexpr uint32_t X_TLS_OUT_OF_INDEXES = UINT32_MAX;
struct XDPC {
xe::be<uint16_t> type;
uint8_t selected_cpu_number;
uint8_t desired_cpu_number;
X_LIST_ENTRY list_entry;
xe::be<uint32_t> routine;
xe::be<uint32_t> context;
xe::be<uint32_t> arg1;
xe::be<uint32_t> arg2;
void Initialize(uint32_t guest_func, uint32_t guest_context) {
type = 19;
selected_cpu_number = 0;
desired_cpu_number = 0;
routine = guest_func;
context = guest_context;
}
};
struct XAPC {
static constexpr uint32_t kSize = 40;
static constexpr uint32_t kDummyKernelRoutine = 0xF00DFF00;
static constexpr uint32_t kDummyRundownRoutine = 0xF00DFF01;
// KAPC is 0x28(40) bytes? (what's passed to ExAllocatePoolWithTag)
// This is 4b shorter than NT - looks like the reserved dword at +4 is gone.
// NOTE: stored in guest memory.
uint16_t type; // +0
uint8_t apc_mode; // +2
uint8_t enqueued; // +3
xe::be<uint32_t> thread_ptr; // +4
X_LIST_ENTRY list_entry; // +8
xe::be<uint32_t> kernel_routine; // +16
xe::be<uint32_t> rundown_routine; // +20
xe::be<uint32_t> normal_routine; // +24
xe::be<uint32_t> normal_context; // +28
xe::be<uint32_t> arg1; // +32
xe::be<uint32_t> arg2; // +36
};
struct X_KSEMAPHORE {
X_DISPATCH_HEADER header;
xe::be<uint32_t> limit;
};
static_assert_size(X_KSEMAPHORE, 0x14);
struct X_KTHREAD;
struct X_KPROCESS;
struct X_KPRCB {
TypedGuestPointer<X_KTHREAD> current_thread; // 0x0
TypedGuestPointer<X_KTHREAD> next_thread; // 0x4
TypedGuestPointer<X_KTHREAD> idle_thread; // 0x8
uint8_t current_cpu; // 0xC
uint8_t unk_D[3]; // 0xD
// should only have 1 bit set, used for ipis
xe::be<uint32_t> processor_mask; // 0x10
// incremented in clock interrupt
xe::be<uint32_t> dpc_clock; // 0x14
xe::be<uint32_t> interrupt_clock; // 0x18
xe::be<uint32_t> unk_1C; // 0x1C
xe::be<uint32_t> unk_20; // 0x20
// various fields used by KeIpiGenericCall
xe::be<uint32_t> ipi_args[3]; // 0x24
// looks like the target cpus clear their corresponding bit
// in this mask to signal completion to the initiator
xe::be<uint32_t> targeted_ipi_cpus_mask; // 0x30
xe::be<uint32_t> ipi_function; // 0x34
// used to synchronize?
TypedGuestPointer<X_KPRCB> ipi_initiator_prcb; // 0x38
xe::be<uint32_t> unk_3C; // 0x3C
xe::be<uint32_t> dpc_related_40; // 0x40
// must be held to modify any dpc-related fields in the kprcb
xe::be<uint32_t> dpc_lock; // 0x44
X_LIST_ENTRY queued_dpcs_list_head; // 0x48
xe::be<uint32_t> dpc_active; // 0x50
X_KSPINLOCK spin_lock; // 0x54
TypedGuestPointer<X_KTHREAD> running_idle_thread; // 0x58
// definitely scheduler related
X_SINGLE_LIST_ENTRY enqueued_threads_list; // 0x5C
xe::be<uint32_t> has_ready_thread_by_priority; // 0x60
// i think the following mask has something to do with the array that comes
// after
xe::be<uint32_t> unk_mask_64; // 0x64
X_LIST_ENTRY unk_68[32]; // 0x68
// ExTerminateThread tail calls a function that does KeInsertQueueDpc of this
// dpc
XDPC thread_exit_dpc; // 0x168
// thread_exit_dpc's routine drains this list and frees each threads threadid,
// kernel stack and dereferences the thread
X_LIST_ENTRY terminating_threads_list; // 0x184
XDPC switch_thread_processor_dpc; // 0x18C
};
// Processor Control Region
struct X_KPCR {
xe::be<uint32_t> tls_ptr; // 0x0
xe::be<uint32_t> msr_mask; // 0x4
union {
xe::be<uint16_t> software_interrupt_state; // 0x8
struct {
uint8_t generic_software_interrupt; // 0x8
uint8_t apc_software_interrupt_state; // 0x9
};
};
xe::be<uint16_t> unk_0A; // 0xA
uint8_t processtype_value_in_dpc; // 0xC
uint8_t timeslice_ended; // 0xD
uint8_t timer_pending; // 0xE
uint8_t unk_0F; // 0xF
// used in KeSaveFloatingPointState / its vmx counterpart
xe::be<uint32_t> thread_fpu_related; // 0x10
xe::be<uint32_t> thread_vmx_related; // 0x14
uint8_t current_irql; // 0x18
uint8_t background_scheduling_active; // 0x19
uint8_t background_scheduling_1A; // 0x1A
uint8_t background_scheduling_1B; // 0x1B
xe::be<uint32_t> timer_related; // 0x1C
uint8_t unk_20[0x10]; // 0x20
xe::be<uint64_t> pcr_ptr; // 0x30
// this seems to be just garbage data? we can stash a pointer to context here
// as a hack for now
union {
uint8_t unk_38[8]; // 0x38
uint64_t host_stash; // 0x38
};
uint8_t unk_40[28]; // 0x40
xe::be<uint32_t> unk_stack_5c; // 0x5C
uint8_t unk_60[12]; // 0x60
xe::be<uint32_t> use_alternative_stack; // 0x6C
xe::be<uint32_t> stack_base_ptr; // 0x70 Stack base address (high addr)
xe::be<uint32_t> stack_end_ptr; // 0x74 Stack end (low addr)
// maybe these are the stacks used in apcs?
// i know they're stacks, RtlGetStackLimits returns them if another var here
// is set
xe::be<uint32_t> alt_stack_base_ptr; // 0x78
xe::be<uint32_t> alt_stack_end_ptr; // 0x7C
// if bit 1 is set in a handler pointer, it actually points to a KINTERRUPT
// otherwise, it points to a function to execute
xe::be<uint32_t> interrupt_handlers[32]; // 0x80
X_KPRCB prcb_data; // 0x100
// pointer to KPCRB?
TypedGuestPointer<X_KPRCB> prcb; // 0x2A8
uint8_t unk_2AC[0x2C]; // 0x2AC
};
#ifdef WAIT_ANY
#undef WAIT_ANY
#endif
enum : uint16_t {
WAIT_ALL = 0,
WAIT_ANY = 1,
};
// https://www.geoffchappell.com/studies/windows/km/ntoskrnl/inc/ntos/ke_x/kwait_block.htm
// pretty much the vista KWAIT_BLOCK verbatim, except that sparebyte is gone
// and WaitType is 2 bytes instead of 1
struct X_KWAIT_BLOCK {
X_LIST_ENTRY wait_list_entry; // 0x0
TypedGuestPointer<X_KTHREAD> thread;
TypedGuestPointer<X_DISPATCH_HEADER> object;
TypedGuestPointer<X_KWAIT_BLOCK> next_wait_block;
// this isnt the official vista name, but i think its better.
// this value is what will be returned to the waiter if this particular wait
// is satisfied
xe::be<uint16_t> wait_result_xstatus;
// WAIT_ALL or WAIT_ANY
xe::be<uint16_t> wait_type;
};
static_assert_size(X_KWAIT_BLOCK, 0x18);
struct X_KTIMER {
X_DISPATCH_HEADER header; // 0x0
xe::be<uint64_t> due_time; // 0x10
X_LIST_ENTRY table_bucket_entry; // 0x18
TypedGuestPointer<XDPC> dpc; // 0x20
xe::be<uint32_t> period; // 0x24
};
static_assert_size(X_KTIMER, 0x28);
struct X_KTHREAD {
X_DISPATCH_HEADER header; // 0x0
xe::be<uint32_t> unk_10; // 0x10
xe::be<uint32_t> unk_14; // 0x14
X_KTIMER wait_timeout_timer; // 0x18
X_KWAIT_BLOCK wait_timeout_block; // 0x40
uint8_t unk_58[0x4]; // 0x58
xe::be<uint32_t> stack_base; // 0x5C
xe::be<uint32_t> stack_limit; // 0x60
xe::be<uint32_t> stack_kernel; // 0x64
xe::be<uint32_t> tls_address; // 0x68
// state = is thread running, suspended, etc
uint8_t thread_state; // 0x6C
// 0x70 = priority?
uint8_t alerted[2]; // 0x6D
uint8_t alertable; // 0x6F
uint8_t priority; // 0x70
uint8_t fpu_exceptions_on; // 0x71
// these two process types both get set to the same thing, process_type is
// referenced most frequently, however process_type_dup gets referenced a few
// times while the process is being created
uint8_t process_type_dup;
uint8_t process_type;
// apc_mode determines which list an apc goes into
util::X_TYPED_LIST<XAPC, offsetof(XAPC, list_entry)> apc_lists[2];
TypedGuestPointer<X_KPROCESS> process; // 0x84
uint8_t executing_kernel_apc; // 0x88
// when context switch happens, this is copied into
// apc_software_interrupt_state for kpcr
uint8_t deferred_apc_software_interrupt_state; // 0x89
uint8_t user_apc_pending; // 0x8A
uint8_t may_queue_apcs; // 0x8B
X_KSPINLOCK apc_lock; // 0x8C
xe::be<uint32_t> num_context_switches_to; // 0x90
X_LIST_ENTRY ready_prcb_entry; // 0x94
xe::be<uint32_t> msr_mask; // 0x9C
xe::be<X_STATUS> wait_result; // 0xA0
uint8_t wait_irql; // 0xA4
uint8_t unk_A5[0xB]; // 0xA5
int32_t apc_disable_count; // 0xB0
xe::be<int32_t> quantum; // 0xB4
uint8_t saturation_increment; // 0xB8
uint8_t base_priority; // 0xB9
uint8_t priority_decrement; // 0xBA
uint8_t boost_disabled; // 0xBB
uint8_t suspend_count; // 0xBC
uint8_t was_preempted; // 0xBD
uint8_t terminated; // 0xBE
uint8_t current_cpu; // 0xBF
// these two pointers point to KPRCBs, but seem to be rarely referenced, if at
// all
TypedGuestPointer<X_KPRCB> a_prcb_ptr; // 0xC0
TypedGuestPointer<X_KPRCB> another_prcb_ptr; // 0xC4
uint8_t process_priority_class; // 0xC8
uint8_t base_priority_copy; // 0xC9
uint8_t max_dynamic_priority; // 0xCA
uint8_t unk_CB; // 0xCB
X_KSPINLOCK timer_list_lock; // 0xCC
xe::be<uint32_t> stack_alloc_base; // 0xD0
XAPC on_suspend; // 0xD4
X_KSEMAPHORE suspend_sema; // 0xFC
// this is an entry in
X_LIST_ENTRY process_threads; // 0x110
xe::be<uint32_t> unk_118; // 0x118
X_LIST_ENTRY queue_related; // 0x11C
xe::be<uint32_t> unk_124; // 0x124
xe::be<uint32_t> unk_128; // 0x128
xe::be<uint32_t> unk_12C; // 0x12C
xe::be<uint64_t> create_time; // 0x130
xe::be<uint64_t> exit_time; // 0x138
xe::be<uint32_t> exit_status; // 0x140
// tracks all pending timers that have apcs which target this thread
X_LIST_ENTRY timer_list; // 0x144
xe::be<uint32_t> thread_id; // 0x14C
xe::be<uint32_t> start_address; // 0x150
X_LIST_ENTRY unk_154; // 0x154
uint8_t unk_15C[0x4]; // 0x15C
xe::be<uint32_t> last_error; // 0x160
xe::be<uint32_t> fiber_ptr; // 0x164
uint8_t unk_168[0x4]; // 0x168
xe::be<uint32_t> creation_flags; // 0x16C
uint8_t unk_170[0xC]; // 0x170
xe::be<uint32_t> unk_17C; // 0x17C
uint8_t unk_180[0x930]; // 0x180
// This struct is actually quite long... so uh, not filling this out!
};
static_assert_size(X_KTHREAD, 0xAB0);
#if !XE_PLATFORM_WIN32
// Exception thrown by XThread::Reenter() to unwind through JIT frames.
// C++ exception unwinding uses DWARF .eh_frame info registered for JIT code,
// ensuring destructors and RAII guards in host C++ frames are properly called.
struct FiberReentryException {
uint32_t address;
};
#endif
class XThread : public XObject, public cpu::Thread {
public:
static const XObject::Type kObjectType = XObject::Type::Thread;
static constexpr uint32_t kStackAddressRangeBegin = 0x70000000;
static constexpr uint32_t kStackAddressRangeEnd = 0x7F000000;
static constexpr uint32_t kThreadKernelStackSize = 0xF0;
struct CreationParams {
uint32_t stack_size;
uint32_t xapi_thread_startup;
uint32_t start_address;
uint32_t start_context;
uint32_t creation_flags;
uint32_t guest_process;
};
XThread(KernelState* kernel_state);
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 = false, uint32_t guest_process = 0);
~XThread() override;
static bool IsInThread(XThread* other);
static bool IsInThread();
static XThread* GetCurrentThread();
static uint32_t GetCurrentThreadHandle();
static uint32_t GetCurrentThreadId();
static uint32_t GetLastError();
static void SetLastError(uint32_t error_code);
const CreationParams* creation_params() const { return &creation_params_; }
uint32_t tls_ptr() const { return tls_static_address_; }
uint32_t pcr_ptr() const { return pcr_address_; }
uint32_t stack_base() const { return stack_base_; }
uint32_t stack_limit() const { return stack_limit_; }
// True if the thread is created by the guest app.
bool is_guest_thread() const { return guest_thread_; }
bool main_thread() const { return main_thread_; }
bool is_running() const { return running_; }
uint32_t thread_id() const { return thread_id_; }
uint32_t last_error();
void set_last_error(uint32_t error_code);
void set_name(const std::string_view name);
X_STATUS Create();
X_STATUS Exit(int exit_code);
X_STATUS Terminate(int exit_code);
virtual void Execute();
virtual void Reenter(uint32_t address);
void EnterCriticalRegion();
void LeaveCriticalRegion();
void EnqueueApc(uint32_t normal_routine, uint32_t normal_context,
uint32_t arg1, uint32_t arg2);
int32_t priority() const { return priority_; }
int32_t QueryPriority();
void SetPriority(int32_t increment);
// Called periodically (~20ms) by KernelState's timestamp timer to simulate
// the Xenon scheduler's quantum-based priority decay for non-real-time
// threads (base_priority < 18). Threads that run for longer than one
// quantum (~20ms) have their effective priority decayed toward the base,
// which causes them to drop into lower host priority buckets and prevents
// starvation. On the first decay step the accumulated priority boost is
// also drained.
void CheckQuantumAndDecay();
// Called when a thread wakes from a kernel wait. Applies a priority
// boost of |increment| above base_priority (matching the Xenon kernel's
// unwait-boost behavior) and restarts the quantum timer. The boost is
// drained on the next quantum expiry via CheckQuantumAndDecay().
// If increment is 0 or the thread has boost disabled, the priority is
// simply restored to base_priority.
void BoostOnWake(int32_t increment);
// Xbox thread IDs:
// 0 - core 0, thread 0 - user
// 1 - core 0, thread 1 - user
// 2 - core 1, thread 0 - sometimes xcontent
// 3 - core 1, thread 1 - user
// 4 - core 2, thread 0 - xaudio
// 5 - core 2, thread 1 - user
void SetAffinity(uint32_t affinity);
uint8_t active_cpu() const;
void SetActiveCpu(uint8_t cpu_index);
bool GetTLSValue(uint32_t slot, uint32_t* value_out);
bool SetTLSValue(uint32_t slot, uint32_t value);
uint32_t suspend_count();
X_FILETIME creation_time();
uint32_t start_address();
X_STATUS Resume(uint32_t* out_suspend_count = nullptr);
X_STATUS Suspend(uint32_t* out_suspend_count = nullptr);
X_STATUS Delay(uint32_t processor_mode, uint32_t alertable,
uint64_t interval);
#if !XE_PLATFORM_WIN32
// Performs self-suspension: increments suspend_count and blocks until
// another thread calls Resume() and suspend_count reaches 0.
// Returns the previous suspend_count value.
uint32_t SelfSuspend();
#endif
xe::threading::Thread* thread() { return thread_.get(); }
virtual bool Save(ByteStream* stream) override;
static object_ref<XThread> Restore(KernelState* kernel_state,
ByteStream* stream);
// Internal - do not use.
void AcquireMutantOnStartup(object_ref<XMutant> mutant) {
pending_mutant_acquires_.push_back(mutant);
}
void SetCurrentThread();
protected:
bool AllocateStack(uint32_t size);
void FreeStack();
void InitializeGuestObject();
void DeliverAPCs();
void RundownAPCs();
xe::threading::WaitHandle* GetWaitHandle() override { return thread_.get(); }
CreationParams creation_params_ = {0};
std::vector<object_ref<XMutant>> pending_mutant_acquires_;
uint32_t thread_id_ = 0;
uint32_t tls_static_address_ = 0;
uint32_t tls_dynamic_address_ = 0;
uint32_t tls_total_size_ = 0;
uint32_t pcr_address_ = 0;
uint32_t stack_alloc_base_ = 0; // Stack alloc base
uint32_t stack_alloc_size_ = 0; // Stack alloc size
uint32_t stack_base_ = 0; // High address
uint32_t stack_limit_ = 0; // Low address
bool guest_thread_ = false;
bool main_thread_ = false; // Entry-point thread
bool running_ = false;
int32_t priority_ = 0; // current effective priority (may be decayed)
int32_t base_priority_ = 0; // priority floor — decay never goes below this
int32_t boost_amount_ = 0; // accumulated priority boost above base
uint64_t quantum_start_ms_ = 0; // host uptime (ms) when quantum last reset
#if !XE_PLATFORM_WIN32
// Condition variable for thread self-suspension.
std::mutex suspend_mutex_;
std::condition_variable suspend_cv_;
#endif
// Reentry mechanism for fiber-based stack switching.
// On Linux, C++ exceptions are used instead of setjmp/longjmp so that
// destructors and RAII guards in host C++ frames are properly unwound.
// JIT code has DWARF .eh_frame unwind info registered via __register_frame.
#if XE_PLATFORM_WIN32
std::jmp_buf reentry_jmp_buf_;
uint32_t reentry_address_ = 0;
#endif
std::mutex thread_lock_;
};
class XHostThread : public XThread {
public:
XHostThread(KernelState* kernel_state, uint32_t stack_size,
uint32_t creation_flags, std::function<int()> host_fn,
uint32_t guest_process = 0);
virtual void Execute();
private:
std::function<int()> host_fn_;
};
} // namespace kernel
} // namespace xe
#endif // XENIA_KERNEL_XTHREAD_H_