[Threading] Add thread priority mapping with timer-driven quantum decay

And default ignore_thread_priorities to false.

Map Xenon's 0-31 priority range across all 5 host priority levels
instead of collapsing 0-17 into kNormal.

Use timer-driven quantum decay (~20ms period) matching Xenon's
60-quantum / 3-per-tick cycle to prevent starvation by gradually lowering
effective priority for non-real-time threads (< 18), piggybacking on the
existing 1ms timestamp timer.
This commit is contained in:
Herman S.
2026-04-09 22:33:08 +09:00
parent 61c8eb0707
commit b3d8a21b72
7 changed files with 194 additions and 44 deletions

View File

@@ -511,7 +511,7 @@ class IConfigVarUpdate {
// If you're reviewing a pull request with a change here, check if 1) has been // If you're reviewing a pull request with a change here, check if 1) has been
// done by the submitter before merging. // done by the submitter before merging.
static constexpr uint32_t kLastCommittedUpdateDate = static constexpr uint32_t kLastCommittedUpdateDate =
MakeConfigVarUpdateDate(2025, 12, 4, 21); MakeConfigVarUpdateDate(2026, 4, 9, 12);
virtual ~IConfigVarUpdate() = default; virtual ~IConfigVarUpdate() = default;

View File

@@ -1151,6 +1151,18 @@ void KernelState::UpdateKeTimestampBundle() {
xe::store_and_swap<uint64_t>(&lpKeTimeStampBundle->system_time, xe::store_and_swap<uint64_t>(&lpKeTimeStampBundle->system_time,
Clock::QueryGuestSystemTime()); Clock::QueryGuestSystemTime());
xe::store_and_swap<uint32_t>(&lpKeTimeStampBundle->tick_count, uptime_ms); xe::store_and_swap<uint32_t>(&lpKeTimeStampBundle->tick_count, uptime_ms);
// Every 20 ticks (~20ms), decay priority on running guest threads.
// This simulates the Xenon decrementer-driven quantum expiration.
if (++quantum_timer_counter_ >= 20) {
quantum_timer_counter_ = 0;
auto global_lock = global_critical_region_.Acquire();
for (auto& [id, thread] : threads_by_id_) {
if (thread->is_running()) {
thread->CheckQuantumAndDecay();
}
}
}
} }
uint32_t KernelState::GetKeTimestampBundle() { uint32_t KernelState::GetKeTimestampBundle() {

View File

@@ -384,6 +384,7 @@ class KernelState {
BitMap tls_bitmap_; BitMap tls_bitmap_;
uint32_t ke_timestamp_bundle_ptr_ = 0; uint32_t ke_timestamp_bundle_ptr_ = 0;
std::unique_ptr<xe::threading::HighResolutionTimer> timestamp_timer_; std::unique_ptr<xe::threading::HighResolutionTimer> timestamp_timer_;
uint32_t quantum_timer_counter_ = 0;
cpu::backend::GuestTrampolineGroup kernel_trampoline_group_; cpu::backend::GuestTrampolineGroup kernel_trampoline_group_;
// fixed address referenced by dashboards. Data is currently unknown // fixed address referenced by dashboards. Data is currently unknown
uint32_t strange_hardcoded_page_ = 0x8E038634 & (~0xFFFF); uint32_t strange_hardcoded_page_ = 0x8E038634 & (~0xFFFF);

View File

@@ -1583,8 +1583,6 @@ DECLARE_XBOXKRNL_EXPORT2(KeInitializeDpc, kThreading, kImplemented, kSketchy);
dword_result_t KeInsertQueueDpc_entry(pointer_t<XDPC> dpc, dword_t arg1, dword_result_t KeInsertQueueDpc_entry(pointer_t<XDPC> dpc, dword_t arg1,
dword_t arg2) { dword_t arg2) {
assert_always("DPC does not dispatch yet; going to hang!");
uint32_t list_entry_ptr = dpc.guest_address() + 4; uint32_t list_entry_ptr = dpc.guest_address() + 4;
// Lock dispatcher. // Lock dispatcher.
@@ -1602,9 +1600,43 @@ dword_result_t KeInsertQueueDpc_entry(pointer_t<XDPC> dpc, dword_t arg1,
dpc_list->Insert(list_entry_ptr); dpc_list->Insert(list_entry_ptr);
// Dispatch the DPC inline on the calling thread. On real hardware DPCs
// are deferred to DISPATCH_IRQL on the target processor, but DPC routines
// access per-CPU state via r13 (KPCR) so they must run on a thread whose
// KPCR is valid for the target CPU. The calling thread's KPCR satisfies
// this for the common case (desired_cpu_number == 0, meaning current CPU).
// Inline dispatch also avoids latency issues with shared work queues.
uint32_t routine = dpc->routine;
if (routine) {
auto thread = XThread::GetCurrentThread();
if (thread) {
auto thread_state = thread->thread_state();
auto ppc_context = thread_state->context();
auto kpcr = ppc_context->TranslateVirtualGPR<X_KPCR*>(ppc_context->r[13]);
// If we're already inside a DPC (reentrant KeInsertQueueDpc from a DPC
// routine), skip the impersonation — we're already at DISPATCH_IRQL.
bool already_in_dpc = kpcr->prcb_data.dpc_active != 0;
DPCImpersonationScope dpc_scope{};
if (!already_in_dpc) {
kernel_state()->BeginDPCImpersonation(ppc_context, dpc_scope);
}
uint64_t args[] = {dpc.guest_address(), (uint64_t)dpc->context,
(uint64_t)arg1, (uint64_t)arg2};
kernel_state()->processor()->Execute(thread_state, routine, args,
xe::countof(args));
if (!already_in_dpc) {
kernel_state()->EndDPCImpersonation(ppc_context, dpc_scope);
}
}
}
return 1; return 1;
} }
DECLARE_XBOXKRNL_EXPORT2(KeInsertQueueDpc, kThreading, kStub, kSketchy); DECLARE_XBOXKRNL_EXPORT2(KeInsertQueueDpc, kThreading, kImplemented, kSketchy);
dword_result_t KeRemoveQueueDpc_entry(pointer_t<XDPC> dpc) { dword_result_t KeRemoveQueueDpc_entry(pointer_t<XDPC> dpc) {
bool result = false; bool result = false;

View File

@@ -203,13 +203,21 @@ X_STATUS XObject::Wait(uint32_t wait_reason, uint32_t processor_mode,
auto result = auto result =
xe::threading::Wait(wait_handle, alertable ? true : false, timeout_ms); xe::threading::Wait(wait_handle, alertable ? true : false, timeout_ms);
switch (result) { switch (result) {
case xe::threading::WaitResult::kSuccess: case xe::threading::WaitResult::kSuccess:
WaitCallback(); case xe::threading::WaitResult::kUserCallback: {
return X_STATUS_SUCCESS; // Thread actually blocked and woke — reset priority to base.
case xe::threading::WaitResult::kUserCallback: auto current_thread = XThread::GetCurrentThread();
// Or X_STATUS_ALERTED? if (current_thread) {
current_thread->ResetQuantum();
}
if (result == xe::threading::WaitResult::kSuccess) {
WaitCallback();
return X_STATUS_SUCCESS;
}
return X_STATUS_USER_APC; return X_STATUS_USER_APC;
}
case xe::threading::WaitResult::kTimeout: case xe::threading::WaitResult::kTimeout:
xe::threading::MaybeYield(); xe::threading::MaybeYield();
return X_STATUS_TIMEOUT; return X_STATUS_TIMEOUT;
@@ -231,13 +239,20 @@ X_STATUS XObject::SignalAndWait(XObject* signal_object, XObject* wait_object,
auto result = xe::threading::SignalAndWait( auto result = xe::threading::SignalAndWait(
signal_object->GetWaitHandle(), wait_object->GetWaitHandle(), signal_object->GetWaitHandle(), wait_object->GetWaitHandle(),
alertable ? true : false, timeout_ms); alertable ? true : false, timeout_ms);
switch (result) { switch (result) {
case xe::threading::WaitResult::kSuccess: case xe::threading::WaitResult::kSuccess:
wait_object->WaitCallback(); case xe::threading::WaitResult::kUserCallback: {
return X_STATUS_SUCCESS; auto current_thread = XThread::GetCurrentThread();
case xe::threading::WaitResult::kUserCallback: if (current_thread) {
// Or X_STATUS_ALERTED? current_thread->ResetQuantum();
}
if (result == xe::threading::WaitResult::kSuccess) {
wait_object->WaitCallback();
return X_STATUS_SUCCESS;
}
return X_STATUS_USER_APC; return X_STATUS_USER_APC;
}
case xe::threading::WaitResult::kTimeout: case xe::threading::WaitResult::kTimeout:
xe::threading::MaybeYield(); xe::threading::MaybeYield();
return X_STATUS_TIMEOUT; return X_STATUS_TIMEOUT;
@@ -264,25 +279,29 @@ X_STATUS XObject::WaitMultiple(uint32_t count, XObject** objects,
TimeoutTicksToMs(*opt_timeout))) TimeoutTicksToMs(*opt_timeout)))
: std::chrono::milliseconds::max(); : std::chrono::milliseconds::max();
X_STATUS status;
if (wait_type) { if (wait_type) {
auto result = xe::threading::WaitAny(wait_handles, count, auto result = xe::threading::WaitAny(wait_handles, count,
alertable ? true : false, timeout_ms); alertable ? true : false, timeout_ms);
switch (result.first) { switch (result.first) {
case xe::threading::WaitResult::kSuccess: case xe::threading::WaitResult::kSuccess:
objects[result.second]->WaitCallback(); objects[result.second]->WaitCallback();
status = X_STATUS(result.second);
return X_STATUS(result.second); break;
case xe::threading::WaitResult::kUserCallback: case xe::threading::WaitResult::kUserCallback:
// Or X_STATUS_ALERTED? status = X_STATUS_USER_APC;
return X_STATUS_USER_APC; break;
case xe::threading::WaitResult::kTimeout: case xe::threading::WaitResult::kTimeout:
xe::threading::MaybeYield(); xe::threading::MaybeYield();
return X_STATUS_TIMEOUT; status = X_STATUS_TIMEOUT;
default: break;
case xe::threading::WaitResult::kAbandoned: case xe::threading::WaitResult::kAbandoned:
return X_STATUS(X_STATUS_ABANDONED_WAIT_0 + result.second); status = X_STATUS(X_STATUS_ABANDONED_WAIT_0 + result.second);
break;
default:
case xe::threading::WaitResult::kFailed: case xe::threading::WaitResult::kFailed:
return X_STATUS_UNSUCCESSFUL; status = X_STATUS_UNSUCCESSFUL;
break;
} }
} else { } else {
auto result = xe::threading::WaitAll(wait_handles, count, auto result = xe::threading::WaitAll(wait_handles, count,
@@ -292,20 +311,32 @@ X_STATUS XObject::WaitMultiple(uint32_t count, XObject** objects,
for (uint32_t i = 0; i < count; i++) { for (uint32_t i = 0; i < count; i++) {
objects[i]->WaitCallback(); objects[i]->WaitCallback();
} }
status = X_STATUS_SUCCESS;
return X_STATUS_SUCCESS; break;
case xe::threading::WaitResult::kUserCallback: case xe::threading::WaitResult::kUserCallback:
// Or X_STATUS_ALERTED? status = X_STATUS_USER_APC;
return X_STATUS_USER_APC; break;
case xe::threading::WaitResult::kTimeout: case xe::threading::WaitResult::kTimeout:
xe::threading::MaybeYield(); xe::threading::MaybeYield();
return X_STATUS_TIMEOUT; status = X_STATUS_TIMEOUT;
break;
default: default:
case xe::threading::WaitResult::kAbandoned: case xe::threading::WaitResult::kAbandoned:
case xe::threading::WaitResult::kFailed: case xe::threading::WaitResult::kFailed:
return X_STATUS_ABANDONED_WAIT_0; status = X_STATUS_ABANDONED_WAIT_0;
break;
} }
} }
// Only reset quantum 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();
}
}
return status;
} }
uint8_t* XObject::CreateNative(uint32_t size) { uint8_t* XObject::CreateNative(uint32_t size) {

View File

@@ -14,6 +14,7 @@
#endif #endif
#include "xenia/base/byte_stream.h" #include "xenia/base/byte_stream.h"
#include "xenia/base/clock.h"
#include "xenia/base/logging.h" #include "xenia/base/logging.h"
#include "xenia/base/platform.h" #include "xenia/base/platform.h"
#include "xenia/base/profiling.h" #include "xenia/base/profiling.h"
@@ -24,8 +25,9 @@
#include "xenia/kernel/user_module.h" #include "xenia/kernel/user_module.h"
#include "xenia/kernel/xboxkrnl/xboxkrnl_threading.h" #include "xenia/kernel/xboxkrnl/xboxkrnl_threading.h"
DEFINE_bool(ignore_thread_priorities, true, DEFINE_bool(ignore_thread_priorities, false,
"Ignores game-specified thread priorities.", "Kernel"); "Ignores game-specified thread priorities.", "Kernel");
UPDATE_from_bool(ignore_thread_priorities, 2026, 4, 9, 12, true);
DEFINE_bool(ignore_thread_affinities, true, DEFINE_bool(ignore_thread_affinities, true,
"Ignores game-specified thread affinities.", "Kernel"); "Ignores game-specified thread affinities.", "Kernel");
@@ -670,28 +672,87 @@ void XThread::RundownAPCs() {
int32_t XThread::QueryPriority() { return thread_->priority(); } int32_t XThread::QueryPriority() { return thread_->priority(); }
void XThread::SetPriority(int32_t increment) { // Map Xenon's 0-31 priority range across the available host priority levels.
if (is_guest_thread()) { // Priority 18 (0x12) is the Xenon real-time threshold — threads at or above
guest_object<X_KTHREAD>()->priority = static_cast<uint8_t>(increment); // it don't get quantum decay on real hardware.
} static int32_t GuestPriorityToHost(int32_t guest_priority) {
priority_ = increment; if (guest_priority >= 24) {
int32_t target_priority = 0; return xe::threading::ThreadPriority::kHighest;
if (increment > 0x22) { } else if (guest_priority >= 17) {
target_priority = xe::threading::ThreadPriority::kHighest; return xe::threading::ThreadPriority::kAboveNormal;
} else if (increment > 0x11) { } else if (guest_priority >= 10) {
target_priority = xe::threading::ThreadPriority::kAboveNormal; return xe::threading::ThreadPriority::kNormal;
} else if (increment < -0x22) { } else if (guest_priority >= 5) {
target_priority = xe::threading::ThreadPriority::kLowest; return xe::threading::ThreadPriority::kBelowNormal;
} else if (increment < -0x11) {
target_priority = xe::threading::ThreadPriority::kBelowNormal;
} else { } else {
target_priority = xe::threading::ThreadPriority::kNormal; return xe::threading::ThreadPriority::kLowest;
} }
}
void XThread::SetPriority(int32_t increment) {
// Clamp to valid Xenon priority range. Negative values can arrive via
// KeSetBasePriorityThread (signed offset from process base).
int32_t clamped = std::max(increment, 0);
if (is_guest_thread()) {
guest_object<X_KTHREAD>()->priority = static_cast<uint8_t>(clamped);
}
priority_ = clamped;
base_priority_ = clamped;
quantum_start_ms_ = Clock::QueryHostUptimeMillis();
if (!cvars::ignore_thread_priorities) { if (!cvars::ignore_thread_priorities) {
thread_->set_priority(target_priority); thread_->set_priority(GuestPriorityToHost(clamped));
} }
} }
void XThread::CheckQuantumAndDecay() {
if (cvars::ignore_thread_priorities) return;
// Real-time threads (priority >= 18) don't decay on Xenon.
if (priority_ >= 18) return;
uint64_t now = Clock::QueryHostUptimeMillis();
uint64_t elapsed = now - quantum_start_ms_;
// On Xenon, the clock interrupt fires every ~1ms and decrements the
// thread's quantum by 3. The process quantum is 60, so it takes ~20ms
// for quantum to expire. When it does, the scheduler decays the
// effective priority by exactly 1 and resets quantum. We approximate
// this by decaying 1 priority level per 20ms of elapsed wall-clock time.
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;
if (new_priority < base_priority_) {
new_priority = base_priority_;
}
if (new_priority != priority_) {
priority_ = new_priority;
if (is_guest_thread()) {
guest_object<X_KTHREAD>()->priority = static_cast<uint8_t>(new_priority);
}
thread_->set_priority(GuestPriorityToHost(new_priority));
}
quantum_start_ms_ = now;
}
void XThread::ResetQuantum() {
if (cvars::ignore_thread_priorities) return;
if (priority_ != base_priority_) {
priority_ = base_priority_;
if (is_guest_thread()) {
guest_object<X_KTHREAD>()->priority =
static_cast<uint8_t>(base_priority_);
}
thread_->set_priority(GuestPriorityToHost(base_priority_));
}
quantum_start_ms_ = Clock::QueryHostUptimeMillis();
}
void XThread::SetAffinity(uint32_t affinity) { void XThread::SetAffinity(uint32_t affinity) {
SetActiveCpu(GetFakeCpuNumber(affinity)); SetActiveCpu(GetFakeCpuNumber(affinity));
} }

View File

@@ -422,6 +422,17 @@ class XThread : public XObject, public cpu::Thread {
int32_t QueryPriority(); int32_t QueryPriority();
void SetPriority(int32_t increment); 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 (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.
void CheckQuantumAndDecay();
// Resets effective priority back to base and restarts the quantum timer.
// Called when a thread wakes from a kernel wait.
void ResetQuantum();
// Xbox thread IDs: // Xbox thread IDs:
// 0 - core 0, thread 0 - user // 0 - core 0, thread 0 - user
// 1 - core 0, thread 1 - user // 1 - core 0, thread 1 - user
@@ -491,7 +502,9 @@ class XThread : public XObject, public cpu::Thread {
bool main_thread_ = false; // Entry-point thread bool main_thread_ = false; // Entry-point thread
bool running_ = false; bool running_ = false;
int32_t priority_ = 0; int32_t priority_ = 0; // current effective priority (may be decayed)
int32_t base_priority_ = 0; // priority floor — decay never goes below this
uint64_t quantum_start_ms_ = 0; // host uptime (ms) when quantum last reset
#if !XE_PLATFORM_WIN32 #if !XE_PLATFORM_WIN32
// Condition variable for thread self-suspension. // Condition variable for thread self-suspension.