[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.
This commit is contained in:
Herman S.
2026-04-11 17:05:05 +09:00
parent b3d8a21b72
commit 65b74819aa
9 changed files with 127 additions and 47 deletions

View File

@@ -1312,15 +1312,16 @@ void KernelState::EmulateCPInterruptDPC(uint32_t interrupt_callback,
}
void KernelState::InitializeProcess(X_KPROCESS* process, uint32_t type,
char unk_18, char unk_19, char unk_1A) {
char priority_class, char default_priority,
char max_dynamic_priority) {
uint32_t guest_kprocess = memory()->HostToGuestVirtual(process);
uint32_t thread_list_guest_ptr =
guest_kprocess + offsetof(X_KPROCESS, thread_list);
process->unk_18 = unk_18;
process->unk_19 = unk_19;
process->unk_1A = unk_1A;
process->process_priority_class = priority_class;
process->default_thread_priority = default_priority;
process->max_dynamic_priority = max_dynamic_priority;
util::XeInitializeListHead(&process->thread_list, thread_list_guest_ptr);
process->quantum = 60;
// doubt any guest code uses this ptr, which i think probably has something to
@@ -1328,7 +1329,7 @@ void KernelState::InitializeProcess(X_KPROCESS* process, uint32_t type,
process->clrdataa_masked_ptr = 0;
// clrdataa_ & ~(1U << 31);
process->thread_count = 0;
process->unk_1B = 0x06;
process->disable_quantum_decay = 0x06;
process->kernel_stack_size = 16 * 1024;
process->tls_slot_size = 0x80;

View File

@@ -66,10 +66,10 @@ struct X_KPROCESS {
// so it sets this ptr to 0x1C0000
xe::be<uint32_t> clrdataa_masked_ptr;
xe::be<uint32_t> thread_count;
uint8_t unk_18;
uint8_t unk_19;
uint8_t unk_1A;
uint8_t unk_1B;
uint8_t process_priority_class;
uint8_t default_thread_priority;
uint8_t max_dynamic_priority;
uint8_t disable_quantum_decay;
xe::be<uint32_t> kernel_stack_size;
xe::be<uint32_t> tls_static_data_address;
xe::be<uint32_t> tls_data_size;
@@ -332,8 +332,9 @@ class KernelState {
private:
void LoadKernelModule(object_ref<KernelModule> kernel_module);
void InitializeProcess(X_KPROCESS* process, uint32_t type, char unk_18,
char unk_19, char unk_1A);
void InitializeProcess(X_KPROCESS* process, uint32_t type,
char priority_class, char default_priority,
char max_dynamic_priority);
void SetProcessTLSVars(X_KPROCESS* process, int num_slots, int tls_data_size,
int tls_static_data_address);
void InitializeKernelGuestGlobals();

View File

@@ -707,8 +707,7 @@ uint32_t xeKeReleaseSemaphore(X_KSEMAPHORE* semaphore_ptr, uint32_t increment,
return 0;
}
// TODO(benvanik): increment thread priority?
// TODO(benvanik): wait?
sem->set_priority_increment(increment);
int32_t previous_count = 0;
[[maybe_unused]] bool success =

View File

@@ -58,11 +58,13 @@ void XEvent::InitializeNative(void* native_ptr, X_DISPATCH_HEADER* header) {
}
int32_t XEvent::Set(uint32_t priority_increment, bool wait) {
set_priority_increment(priority_increment);
event_->Set();
return 1;
}
int32_t XEvent::Pulse(uint32_t priority_increment, bool wait) {
set_priority_increment(priority_increment);
event_->Pulse();
return 1;
}

View File

@@ -45,6 +45,8 @@ X_STATUS XMutant::ReleaseMutant(uint32_t priority_increment, bool abandon,
owning_thread_ = nullptr;
}
set_priority_increment(priority_increment);
// TODO(benvanik): abandoning.
assert_false(abandon);
if (mutant_->Release()) {

View File

@@ -207,10 +207,9 @@ X_STATUS XObject::Wait(uint32_t wait_reason, uint32_t processor_mode,
switch (result) {
case xe::threading::WaitResult::kSuccess:
case xe::threading::WaitResult::kUserCallback: {
// Thread actually blocked and woke — reset priority to base.
auto current_thread = XThread::GetCurrentThread();
if (current_thread) {
current_thread->ResetQuantum();
current_thread->BoostOnWake(priority_increment());
}
if (result == xe::threading::WaitResult::kSuccess) {
WaitCallback();
@@ -245,7 +244,7 @@ X_STATUS XObject::SignalAndWait(XObject* signal_object, XObject* wait_object,
case xe::threading::WaitResult::kUserCallback: {
auto current_thread = XThread::GetCurrentThread();
if (current_thread) {
current_thread->ResetQuantum();
current_thread->BoostOnWake(wait_object->priority_increment());
}
if (result == xe::threading::WaitResult::kSuccess) {
wait_object->WaitCallback();
@@ -280,12 +279,14 @@ X_STATUS XObject::WaitMultiple(uint32_t count, XObject** objects,
: std::chrono::milliseconds::max();
X_STATUS status;
uint32_t boost_increment = 0;
if (wait_type) {
auto result = xe::threading::WaitAny(wait_handles, count,
alertable ? true : false, timeout_ms);
switch (result.first) {
case xe::threading::WaitResult::kSuccess:
objects[result.second]->WaitCallback();
boost_increment = objects[result.second]->priority_increment();
status = X_STATUS(result.second);
break;
case xe::threading::WaitResult::kUserCallback:
@@ -310,6 +311,10 @@ X_STATUS XObject::WaitMultiple(uint32_t count, XObject** objects,
case xe::threading::WaitResult::kSuccess:
for (uint32_t i = 0; i < count; i++) {
objects[i]->WaitCallback();
// Use the largest increment among the signaled objects.
if (objects[i]->priority_increment() > boost_increment) {
boost_increment = objects[i]->priority_increment();
}
}
status = X_STATUS_SUCCESS;
break;
@@ -328,12 +333,13 @@ X_STATUS XObject::WaitMultiple(uint32_t count, XObject** objects,
}
}
// Only reset quantum if the thread actually blocked (not on timeout/failure).
// Apply priority boost if the thread actually blocked (not on
// timeout/failure).
if (status != X_STATUS_TIMEOUT && status != X_STATUS_UNSUCCESSFUL &&
status != X_STATUS_ABANDONED_WAIT_0) {
auto current_thread = XThread::GetCurrentThread();
if (current_thread) {
current_thread->ResetQuantum();
current_thread->BoostOnWake(boost_increment);
}
}
return status;

View File

@@ -226,6 +226,12 @@ class XObject {
void* native_ptr, int32_t as_type = -1,
bool already_locked = false);
// Priority increment stored by the most recent signal operation
// (KeSetEvent, KeReleaseSemaphore, etc.). Read by the waiter on wake
// to apply a priority boost matching real Xenon scheduler behavior.
uint32_t priority_increment() const { return priority_increment_; }
void set_priority_increment(uint32_t inc) { priority_increment_ = inc; }
protected:
bool SaveObject(ByteStream* stream);
bool RestoreObject(ByteStream* stream);
@@ -253,6 +259,8 @@ class XObject {
KernelState* kernel_state_;
uint32_t priority_increment_ = 0;
// Host objects are persisted through resets/etc.
bool host_object_ = false;

View File

@@ -214,6 +214,19 @@ void XThread::InitializeGuestObject() {
guest_thread->apc_lists[0].Initialize(memory());
guest_thread->apc_lists[1].Initialize(memory());
guest_thread->process_priority_class = process->process_priority_class;
auto base_prio = process->default_thread_priority;
guest_thread->base_priority_copy = base_prio;
guest_thread->base_priority = base_prio;
guest_thread->priority = base_prio;
guest_thread->max_dynamic_priority = process->max_dynamic_priority;
guest_thread->quantum = process->quantum;
// Sync the host-side priority tracking to match the guest defaults.
// Games may later override these via KeSetPriorityThread.
priority_ = base_prio;
base_priority_ = base_prio;
guest_thread->a_prcb_ptr = kpcrb;
guest_thread->another_prcb_ptr = kpcrb;
@@ -706,7 +719,7 @@ void XThread::SetPriority(int32_t increment) {
void XThread::CheckQuantumAndDecay() {
if (cvars::ignore_thread_priorities) return;
// Real-time threads (priority >= 18) don't decay on Xenon.
// Real-time threads (current priority >= 0x12) don't decay on Xenon.
if (priority_ >= 18) return;
uint64_t now = Clock::QueryHostUptimeMillis();
@@ -719,14 +732,15 @@ void XThread::CheckQuantumAndDecay() {
constexpr uint64_t kQuantumPeriodMs = 20;
if (elapsed < kQuantumPeriodMs) return;
// TODO(has207): The real kernel also subtracts the accumulated priority
// boost (boost_accumulator in X_KTHREAD) during decay:
// new_prio = priority - boost_accumulator - 1
// We don't implement priority boosting on wait completion yet, so
// the boost accumulator is always effectively 0. When boost-on-wake
// is added, the accumulator needs to be drained here as well.
int32_t decay_steps = static_cast<int32_t>(elapsed / kQuantumPeriodMs);
int32_t new_priority = priority_ - decay_steps;
// On the first decay step, drain the accumulated priority boost as well.
// The real kernel computes: new_prio = priority - boost_accumulator - 1
// then zeroes the accumulator. Additional decay steps (if the timer
// callback was late) each subtract 1 more.
int32_t total_decay = boost_amount_ + decay_steps;
boost_amount_ = 0;
int32_t new_priority = priority_ - total_decay;
if (new_priority < base_priority_) {
new_priority = base_priority_;
}
@@ -740,16 +754,57 @@ void XThread::CheckQuantumAndDecay() {
quantum_start_ms_ = now;
}
void XThread::ResetQuantum() {
void XThread::BoostOnWake(int32_t increment) {
if (cvars::ignore_thread_priorities) return;
if (priority_ != base_priority_) {
priority_ = base_priority_;
// Real-time threads (priority >= 0x12) just get their quantum reset.
if (priority_ >= 18) {
boost_amount_ = 0;
quantum_start_ms_ = Clock::QueryHostUptimeMillis();
return;
}
// Match the real kernel (xeEnqueueThreadPostWait):
// - Only apply boost if there is no pending decay (priority_decrement == 0)
// AND boost is not disabled on this thread.
// - Boosted priority = base + increment, clamped to max_priority_cap.
// - Only boost UP — never lower priority below its current value.
bool apply_boost = false;
if (increment > 0 && is_guest_thread()) {
auto* kthread = guest_object<X_KTHREAD>();
if (kthread->priority_decrement == 0 && !kthread->boost_disabled) {
apply_boost = true;
}
} else if (increment > 0) {
// Host threads (non-guest): apply boost unconditionally.
apply_boost = true;
}
if (apply_boost) {
int32_t boosted = base_priority_ + increment;
// Clamp to the per-thread max dynamic priority cap.
// For title threads this is 17 (just below real-time threshold).
int32_t max_cap = 17;
if (is_guest_thread()) {
guest_object<X_KTHREAD>()->priority =
static_cast<uint8_t>(base_priority_);
uint8_t guest_cap = guest_object<X_KTHREAD>()->max_dynamic_priority;
if (guest_cap > 0) {
max_cap = guest_cap;
}
thread_->set_priority(GuestPriorityToHost(base_priority_));
}
if (boosted > max_cap) {
boosted = max_cap;
}
// Only boost UP, never lower.
if (boosted > priority_) {
priority_ = boosted;
boost_amount_ = priority_ - base_priority_;
if (is_guest_thread()) {
guest_object<X_KTHREAD>()->priority = static_cast<uint8_t>(priority_);
}
thread_->set_priority(GuestPriorityToHost(priority_));
}
}
quantum_start_ms_ = Clock::QueryHostUptimeMillis();
}

View File

@@ -298,9 +298,9 @@ struct X_KTHREAD {
uint8_t unk_A5[0xB]; // 0xA5
int32_t apc_disable_count; // 0xB0
xe::be<int32_t> quantum; // 0xB4
uint8_t unk_B8; // 0xB8
uint8_t unk_B9; // 0xB9
uint8_t unk_BA; // 0xBA
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
@@ -310,9 +310,9 @@ struct X_KTHREAD {
// all
TypedGuestPointer<X_KPRCB> a_prcb_ptr; // 0xC0
TypedGuestPointer<X_KPRCB> another_prcb_ptr; // 0xC4
uint8_t unk_C8; // 0xC8
uint8_t unk_C9; // 0xC9
uint8_t unk_CA; // 0xCA
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
@@ -424,14 +424,19 @@ class XThread : public XObject, public cpu::Thread {
// Called periodically (~20ms) by KernelState's timestamp timer to simulate
// the Xenon scheduler's quantum-based priority decay for non-real-time
// threads (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.
// 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();
// Resets effective priority back to base and restarts the quantum timer.
// Called when a thread wakes from a kernel wait.
void ResetQuantum();
// 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
@@ -504,6 +509,7 @@ class XThread : public XObject, public cpu::Thread {
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