[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

@@ -14,6 +14,7 @@
#endif
#include "xenia/base/byte_stream.h"
#include "xenia/base/clock.h"
#include "xenia/base/logging.h"
#include "xenia/base/platform.h"
#include "xenia/base/profiling.h"
@@ -24,8 +25,9 @@
#include "xenia/kernel/user_module.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");
UPDATE_from_bool(ignore_thread_priorities, 2026, 4, 9, 12, true);
DEFINE_bool(ignore_thread_affinities, true,
"Ignores game-specified thread affinities.", "Kernel");
@@ -670,28 +672,87 @@ void XThread::RundownAPCs() {
int32_t XThread::QueryPriority() { return thread_->priority(); }
void XThread::SetPriority(int32_t increment) {
if (is_guest_thread()) {
guest_object<X_KTHREAD>()->priority = static_cast<uint8_t>(increment);
}
priority_ = increment;
int32_t target_priority = 0;
if (increment > 0x22) {
target_priority = xe::threading::ThreadPriority::kHighest;
} else if (increment > 0x11) {
target_priority = xe::threading::ThreadPriority::kAboveNormal;
} else if (increment < -0x22) {
target_priority = xe::threading::ThreadPriority::kLowest;
} else if (increment < -0x11) {
target_priority = xe::threading::ThreadPriority::kBelowNormal;
// Map Xenon's 0-31 priority range across the available host priority levels.
// Priority 18 (0x12) is the Xenon real-time threshold — threads at or above
// it don't get quantum decay on real hardware.
static int32_t GuestPriorityToHost(int32_t guest_priority) {
if (guest_priority >= 24) {
return xe::threading::ThreadPriority::kHighest;
} else if (guest_priority >= 17) {
return xe::threading::ThreadPriority::kAboveNormal;
} else if (guest_priority >= 10) {
return xe::threading::ThreadPriority::kNormal;
} else if (guest_priority >= 5) {
return xe::threading::ThreadPriority::kBelowNormal;
} 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) {
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) {
SetActiveCpu(GetFakeCpuNumber(affinity));
}