Files
benilla-octo/crates/benilla-app/src/thread_qos.rs
T
SamandClaude Fable 5 ba9a333aeb the Ironforge lava returns, and the sound crackle is cornered
- the Great Forge's lava stays drawn from every angle, and the chasm catwalk reads as a connected grate again
- music decoding and shader-compile bursts stop crowding the audio's lane, and the output buffer follows the device's real deadline
- the audio path grows its instruments: a mix tap to WAV, a starvation meter, a hardware overload listener
- trainer detail icons carry the reference's own tooltip

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 16:03:52 +07:00

153 lines
6.9 KiB
Rust

//! macOS thread QoS: keep frame-critical threads on the P-cores under system load.
//!
//! Apple Silicon schedules by QoS class, not fairness: a default-QoS thread queues behind any
//! higher class for P-core time. The main thread is user-interactive out of the box (verified by
//! probe — even bare `cargo run`, no bundle), but every worker Bevy spawns — compute pool, IO
//! pool, the pipelined-rendering thread — starts at default, the same class as `rustc` or an OBS
//! encoder. Under a background build the frame's own workers wait in the compiler's queue and the
//! client drops to 20 fps while the retail client (whose workers are promoted, plus Game Mode)
//! holds steady. Promoting the workers is the per-thread half of that story; the Game Mode half
//! needs an app bundle and is release-track (decision 0609).
//!
//! `pthread_set_qos_class_self_np` is not bound by the `libc` crate, so the extern lives here.
//! Everything is a no-op off macOS.
use bevy::prelude::*;
use bevy::render::{Render, RenderApp, RenderSystems};
/// QoS classes we actually use. Values are Darwin's `qos_class_t`.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(u32)]
pub enum QosClass {
/// Work on the critical path of the current frame (compute pool, render thread).
UserInteractive = 0x21,
/// Work the frame is waiting on soon but not this frame (asset IO, async compute, net IO).
UserInitiated = 0x19,
/// Darwin's unspecified/default class — where a *thread that will block for seconds* belongs.
/// Apple's duration table calls user-interactive "virtually instantaneous" and user-initiated
/// "a few seconds or less"; a multi-second shader-compile burst is neither. Metal's compiler
/// runs the compile at the **calling thread's** QoS, and Apple's own prescription for
/// pipeline prewarming is exactly this class (WWDC25 "Explore Metal 4 games":
/// "Set QoS class to default for pipeline prewarming and streaming", with sample code using
/// `QOS_CLASS_DEFAULT`). Decision 1117.
Default = 0x15,
}
/// Set while the *covered* pipeline-warm burst runs (decision 1117): the render thread spends
/// that window blocked inside Metal pipeline compilation, and there is no frame to protect —
/// the loading cover is a still image. Published by `pipe_warm`, applied by
/// [`promote_render_thread`] on the render thread itself, which is the only thread that can set
/// its own QoS.
pub static COMPILE_BURST: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
/// Promote the calling thread to `class`. Safe to call repeatedly; logs once on failure.
pub fn promote_current_thread(class: QosClass) {
#[cfg(target_os = "macos")]
{
unsafe extern "C" {
fn pthread_set_qos_class_self_np(qos_class: u32, relative_priority: i32) -> i32;
}
let rc = unsafe { pthread_set_qos_class_self_np(class as u32, 0) };
if rc != 0 {
warn_once!("thread QoS promotion to {class:?} failed (rc={rc})");
}
}
#[cfg(not(target_os = "macos"))]
let _ = class;
}
/// Promotes the pipelined-rendering thread. Task-pool threads are promoted at spawn via
/// `TaskPoolOptions::on_thread_spawn` (see `main.rs`), but Bevy spawns the render thread with a
/// bare `std::thread::spawn` and no hook. An exclusive system runs on whichever thread drives the
/// render schedule — the main thread during startup, the render thread once pipelined rendering
/// takes over — so it re-runs each frame behind a thread-local latch instead of `run_once`.
pub struct ThreadQosPlugin;
impl Plugin for ThreadQosPlugin {
fn build(&self, app: &mut App) {
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app.add_systems(Render, promote_render_thread.in_set(RenderSystems::Prepare));
}
}
/// Read back the calling thread's QoS class (support for the macOS-only tests below).
#[cfg(all(test, target_os = "macos"))]
fn current_thread_qos() -> Option<u32> {
unsafe extern "C" {
fn pthread_self() -> *mut core::ffi::c_void;
fn pthread_get_qos_class_np(
thread: *mut core::ffi::c_void,
qos_class: *mut u32,
relative_priority: *mut i32,
) -> i32;
}
let mut qos: u32 = 0;
let rc = unsafe { pthread_get_qos_class_np(pthread_self(), &mut qos, core::ptr::null_mut()) };
(rc == 0).then_some(qos)
}
fn promote_render_thread(_world: &mut World) {
// The class this thread should be in *right now*: its normal frame-critical band, or the
// default band while it is the shader compiler's caller under a cover (1117).
let want = if COMPILE_BURST.load(std::sync::atomic::Ordering::Relaxed) {
QosClass::Default
} else {
QosClass::UserInteractive
};
std::thread_local! {
static APPLIED: std::cell::Cell<Option<QosClass>> = const { std::cell::Cell::new(None) };
}
APPLIED.with(|p| {
if p.get() != Some(want) {
promote_current_thread(want);
debug!(
"thread QoS: render-schedule thread {:?} → {want:?}",
std::thread::current().name().unwrap_or("<unnamed>")
);
p.set(Some(want));
}
});
}
#[cfg(all(test, target_os = "macos"))]
mod tests {
use super::*;
/// The promotion really lands: a fresh (default-QoS) thread reads back the class it set.
#[test]
fn promotion_applies_to_spawned_thread() {
for class in [QosClass::UserInteractive, QosClass::UserInitiated] {
let observed = std::thread::spawn(move || {
let before = current_thread_qos();
promote_current_thread(class);
(before, current_thread_qos())
})
.join()
.unwrap();
// A bare std thread spawns at default (0x15) — the gap this module closes.
assert_eq!(observed.0, Some(0x15), "spawned thread not default-QoS");
assert_eq!(observed.1, Some(class as u32), "promotion did not apply");
}
}
/// A spawned thread does NOT inherit its spawner's QoS class — it lands at default even when
/// the spawner is user-interactive. This is the load-bearing fact of decision 1109: kira
/// spawns its per-stream decode threads from the (promoted) main thread, and without
/// inheritance those threads sit *below* the promoted worker pools and starve under the
/// world-entry burst — hence `sound::mixer::PromotingSource` promoting from inside.
#[test]
fn qos_is_not_inherited_by_spawned_threads() {
let (parent, child) = std::thread::spawn(|| {
promote_current_thread(QosClass::UserInteractive);
let child = std::thread::spawn(current_thread_qos).join().unwrap();
(current_thread_qos(), child)
})
.join()
.unwrap();
assert_eq!(parent, Some(QosClass::UserInteractive as u32));
assert_eq!(child, Some(0x15), "QoS inherited — 1109's premise changed");
}
}