[iterate-4A] intro-video: fix decode-timeout clock, feeder starvation, k_8 texture decode
Three layered root causes kept ADV.wmv from playing. This lands the first three fixes: the intro video now decodes end-to-end and uploads its YUV planes. The on-screen composite still needs a multi-texture render path (root #3, tracked separately — the shader interpreter binds one texture slot but the YUV->RGB pass samples three). 1. Clock scale (xenia-kernel/state.rs): INSTRUCTIONS_PER_MS 10_000 -> 1_000_000. KeTimeStampBundle tick_count = global_clock / INSTRUCTIONS_PER_MS. At 10_000 (~10 MIPS; global_clock further inflated ~58x by the serialized scheduler summing busy-spin) the movie handler's 2000 ms software-decode deadline (sub_821B4968 @0x821b68c0) tripped on a legitimate ~20M-instruction 720p-YUV420 decode and aborted playback (canary never enters that wait loop). 1_000_000 sits in the validated [~600k, ~2.77M] window that fits both the movie (decode <=2000 ms) and boot (the worker-hub +66 ms gate still elapses before the movie, ~66M instr). XENIA_INSTR_PER_MS overrides. 2. Scheduler fairness (xenia-cpu/scheduler.rs): pick_runnable's equal-priority tiebreak now prefers the incumbent (running_idx) so decrement_quantum's quantum rotation sticks. Previously each round re-picked the lowest index, so co-located equal-priority threads never alternated and the movie's demux feeder (tid24, co-located on hw=1) starved until the STARVE_LIMIT=4096 backstop -- the decode ring never refilled. Priority preemption unaffected. XENIA_INCUMBENT_PICK=0 rolls back. 3. k_8 texture decode (xenia-gpu/texture_cache.rs + xenia-ui/texture_cache_host.rs): the video uploads its YUV420 planes as linear k_8 textures (Y 1280x720, U/V 640x360); with no k_8 decoder ensure_cached rejected them and the frames never reached the GPU. Adds decode_k8 (1 byte/texel expanded to Rgba8Unorm) + the host Rgba8Unorm mapping. Read-only diagnostic probe knobs used to find these remain uncommitted in the working tree. Boot goldens re-baselined in a follow-up commit (the clock change intentionally moves the digest; see tests/golden/README.md). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -49,6 +49,16 @@ pub const QUANTUM_DEFAULT: u32 = 50_000;
|
||||
/// guarantees *bounded* forward progress, it does not invert priority.
|
||||
pub const STARVE_LIMIT: u32 = 4096;
|
||||
|
||||
/// Toggle for the `pick_runnable` incumbent-preference tiebreak (the fix).
|
||||
/// Default on. `XENIA_INCUMBENT_PICK=0` reverts to the old lowest-index
|
||||
/// tiebreak — a rollback knob to A/B the fix's effect on boot rendering vs
|
||||
/// the movie feeder-starvation cure. Inert unless explicitly set to 0.
|
||||
fn incumbent_pick() -> bool {
|
||||
use std::sync::OnceLock;
|
||||
static IP: OnceLock<bool> = OnceLock::new();
|
||||
*IP.get_or_init(|| std::env::var("XENIA_INCUMBENT_PICK").map(|s| s.trim() != "0").unwrap_or(true))
|
||||
}
|
||||
|
||||
/// Above this depth, `spawn` prunes `Exited` entries from a slot's runqueue
|
||||
/// before pushing the new thread. Keeps peer `ThreadRef`s stable on the
|
||||
/// common (low-depth) path — a game that spawns a handful of long-lived
|
||||
@@ -264,11 +274,32 @@ impl HwSlot {
|
||||
/// `STARVE_LIMIT` visits). The boost is a pure function of the per-thread
|
||||
/// counters/priority/index, so picks stay deterministic.
|
||||
pub fn pick_runnable(&self) -> Option<usize> {
|
||||
// Tiebreak among equal-effective-priority Ready threads PREFERS THE
|
||||
// INCUMBENT (`running_idx`) over the lowest index. `decrement_quantum`
|
||||
// rotates `running_idx` to the next same-priority peer when the 50k
|
||||
// quantum expires; without this preference the next round's re-pick
|
||||
// would discard that rotation and re-select the lowest-index peer,
|
||||
// so co-located equal-priority threads never alternate — the lowest
|
||||
// index monopolizes the slot until the `STARVE_LIMIT` backstop fires
|
||||
// (~1 turn in 4096 rounds). That starved the intro-movie's demux
|
||||
// feeder (tid24, co-located on hw=1 with tid17): the decode ring never
|
||||
// refilled. Honoring the incumbent makes the quantum rotation stick,
|
||||
// so co-located peers share the slot every quantum (fair round-robin).
|
||||
// Priority-based preemption is unaffected — `effective_priority` is the
|
||||
// primary key, so a higher-priority (or STARVE_LIMIT-boosted) Ready
|
||||
// thread still wins immediately regardless of the incumbent.
|
||||
let incumbent = if incumbent_pick() { self.running_idx } else { None };
|
||||
self.runqueue
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, t)| matches!(t.state, HwState::Ready | HwState::ServicingIrq(_)))
|
||||
.max_by_key(|(i, t)| (Self::effective_priority(t), -(*i as i64)))
|
||||
.max_by_key(|(i, t)| {
|
||||
(
|
||||
Self::effective_priority(t),
|
||||
Some(*i) == incumbent,
|
||||
-(*i as i64),
|
||||
)
|
||||
})
|
||||
.map(|(i, _)| i)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user