diff --git a/src/xenia/base/cvar.h b/src/xenia/base/cvar.h index e174b3a70..c3a9f60b6 100644 --- a/src/xenia/base/cvar.h +++ b/src/xenia/base/cvar.h @@ -511,7 +511,7 @@ class IConfigVarUpdate { // If you're reviewing a pull request with a change here, check if 1) has been // done by the submitter before merging. static constexpr uint32_t kLastCommittedUpdateDate = - MakeConfigVarUpdateDate(2025, 12, 4, 21); + MakeConfigVarUpdateDate(2026, 4, 9, 12); virtual ~IConfigVarUpdate() = default; diff --git a/src/xenia/kernel/kernel_state.cc b/src/xenia/kernel/kernel_state.cc index 3bbad27be..32b138cc6 100644 --- a/src/xenia/kernel/kernel_state.cc +++ b/src/xenia/kernel/kernel_state.cc @@ -1151,6 +1151,18 @@ void KernelState::UpdateKeTimestampBundle() { xe::store_and_swap(&lpKeTimeStampBundle->system_time, Clock::QueryGuestSystemTime()); xe::store_and_swap(&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() { diff --git a/src/xenia/kernel/kernel_state.h b/src/xenia/kernel/kernel_state.h index 2e1a99a76..fb2bf33b8 100644 --- a/src/xenia/kernel/kernel_state.h +++ b/src/xenia/kernel/kernel_state.h @@ -384,6 +384,7 @@ class KernelState { BitMap tls_bitmap_; uint32_t ke_timestamp_bundle_ptr_ = 0; std::unique_ptr timestamp_timer_; + uint32_t quantum_timer_counter_ = 0; cpu::backend::GuestTrampolineGroup kernel_trampoline_group_; // fixed address referenced by dashboards. Data is currently unknown uint32_t strange_hardcoded_page_ = 0x8E038634 & (~0xFFFF); diff --git a/src/xenia/kernel/xboxkrnl/xboxkrnl_threading.cc b/src/xenia/kernel/xboxkrnl/xboxkrnl_threading.cc index 83bb83e18..f524b49ab 100644 --- a/src/xenia/kernel/xboxkrnl/xboxkrnl_threading.cc +++ b/src/xenia/kernel/xboxkrnl/xboxkrnl_threading.cc @@ -1583,8 +1583,6 @@ DECLARE_XBOXKRNL_EXPORT2(KeInitializeDpc, kThreading, kImplemented, kSketchy); dword_result_t KeInsertQueueDpc_entry(pointer_t dpc, dword_t arg1, dword_t arg2) { - assert_always("DPC does not dispatch yet; going to hang!"); - uint32_t list_entry_ptr = dpc.guest_address() + 4; // Lock dispatcher. @@ -1602,9 +1600,43 @@ dword_result_t KeInsertQueueDpc_entry(pointer_t dpc, dword_t arg1, 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(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; } -DECLARE_XBOXKRNL_EXPORT2(KeInsertQueueDpc, kThreading, kStub, kSketchy); +DECLARE_XBOXKRNL_EXPORT2(KeInsertQueueDpc, kThreading, kImplemented, kSketchy); dword_result_t KeRemoveQueueDpc_entry(pointer_t dpc) { bool result = false; diff --git a/src/xenia/kernel/xobject.cc b/src/xenia/kernel/xobject.cc index 5d0148e29..83d2ec8ae 100644 --- a/src/xenia/kernel/xobject.cc +++ b/src/xenia/kernel/xobject.cc @@ -203,13 +203,21 @@ X_STATUS XObject::Wait(uint32_t wait_reason, uint32_t processor_mode, auto result = xe::threading::Wait(wait_handle, alertable ? true : false, timeout_ms); + switch (result) { case xe::threading::WaitResult::kSuccess: - WaitCallback(); - return X_STATUS_SUCCESS; - case xe::threading::WaitResult::kUserCallback: - // Or X_STATUS_ALERTED? + 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(); + } + if (result == xe::threading::WaitResult::kSuccess) { + WaitCallback(); + return X_STATUS_SUCCESS; + } return X_STATUS_USER_APC; + } case xe::threading::WaitResult::kTimeout: xe::threading::MaybeYield(); return X_STATUS_TIMEOUT; @@ -231,13 +239,20 @@ X_STATUS XObject::SignalAndWait(XObject* signal_object, XObject* wait_object, auto result = xe::threading::SignalAndWait( signal_object->GetWaitHandle(), wait_object->GetWaitHandle(), alertable ? true : false, timeout_ms); + switch (result) { case xe::threading::WaitResult::kSuccess: - wait_object->WaitCallback(); - return X_STATUS_SUCCESS; - case xe::threading::WaitResult::kUserCallback: - // Or X_STATUS_ALERTED? + case xe::threading::WaitResult::kUserCallback: { + auto current_thread = XThread::GetCurrentThread(); + if (current_thread) { + current_thread->ResetQuantum(); + } + if (result == xe::threading::WaitResult::kSuccess) { + wait_object->WaitCallback(); + return X_STATUS_SUCCESS; + } return X_STATUS_USER_APC; + } case xe::threading::WaitResult::kTimeout: xe::threading::MaybeYield(); return X_STATUS_TIMEOUT; @@ -264,25 +279,29 @@ X_STATUS XObject::WaitMultiple(uint32_t count, XObject** objects, TimeoutTicksToMs(*opt_timeout))) : std::chrono::milliseconds::max(); + X_STATUS status; 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(); - - return X_STATUS(result.second); + status = X_STATUS(result.second); + break; case xe::threading::WaitResult::kUserCallback: - // Or X_STATUS_ALERTED? - return X_STATUS_USER_APC; + status = X_STATUS_USER_APC; + break; case xe::threading::WaitResult::kTimeout: xe::threading::MaybeYield(); - return X_STATUS_TIMEOUT; - default: + status = X_STATUS_TIMEOUT; + break; 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: - return X_STATUS_UNSUCCESSFUL; + status = X_STATUS_UNSUCCESSFUL; + break; } } else { 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++) { objects[i]->WaitCallback(); } - - return X_STATUS_SUCCESS; + status = X_STATUS_SUCCESS; + break; case xe::threading::WaitResult::kUserCallback: - // Or X_STATUS_ALERTED? - return X_STATUS_USER_APC; + status = X_STATUS_USER_APC; + break; case xe::threading::WaitResult::kTimeout: xe::threading::MaybeYield(); - return X_STATUS_TIMEOUT; + status = X_STATUS_TIMEOUT; + break; default: case xe::threading::WaitResult::kAbandoned: 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) { diff --git a/src/xenia/kernel/xthread.cc b/src/xenia/kernel/xthread.cc index f7ec64bcb..49fd30d21 100644 --- a/src/xenia/kernel/xthread.cc +++ b/src/xenia/kernel/xthread.cc @@ -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()->priority = static_cast(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()->priority = static_cast(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(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()->priority = static_cast(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()->priority = + static_cast(base_priority_); + } + thread_->set_priority(GuestPriorityToHost(base_priority_)); + } + quantum_start_ms_ = Clock::QueryHostUptimeMillis(); +} + void XThread::SetAffinity(uint32_t affinity) { SetActiveCpu(GetFakeCpuNumber(affinity)); } diff --git a/src/xenia/kernel/xthread.h b/src/xenia/kernel/xthread.h index dd0fa31b8..a2fc02020 100644 --- a/src/xenia/kernel/xthread.h +++ b/src/xenia/kernel/xthread.h @@ -422,6 +422,17 @@ class XThread : public XObject, public cpu::Thread { 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 (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: // 0 - core 0, thread 0 - 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 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 // Condition variable for thread self-suspension.