use core::fmt::Display;
use core::ptr::NonNull;
use core::sync::atomic::{AtomicU32, Ordering};
use objc2::runtime::ProtocolObject;
use objc2_metal::{MTLCommandBuffer, MTLCommandBufferStatus};
const CAUSE_LOG_LIMIT: u32 = 16;
const VICTIM_LOG_LIMIT: u32 = 16;
const VICTIM_MARKERS: [&str; 2] = [
"kIOGPUCommandBufferCallbackErrorInnocentVictim",
"kIOGPUCommandBufferCallbackErrorSubmissionsIgnored",
];
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum FaultRole {
Cause,
Victim,
}
fn fault_role(description: &str) -> FaultRole {
if VICTIM_MARKERS
.iter()
.any(|marker| description.contains(marker))
{
FaultRole::Victim
} else {
FaultRole::Cause
}
}
struct FaultThrottle {
causes: AtomicU32,
victims: AtomicU32,
}
impl FaultThrottle {
const fn new() -> Self {
Self {
causes: AtomicU32::new(0),
victims: AtomicU32::new(0),
}
}
fn admit(&self, role: FaultRole) -> bool {
let (logged, limit) = match role {
FaultRole::Cause => (&self.causes, CAUSE_LOG_LIMIT),
FaultRole::Victim => (&self.victims, VICTIM_LOG_LIMIT),
};
logged
.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |n| {
(n < limit).then_some(n + 1)
})
.is_ok()
}
}
static THROTTLE: FaultThrottle = FaultThrottle::new();
pub(super) fn report_fault(cb: &ProtocolObject<dyn MTLCommandBuffer>, what: impl Display) {
if cb.status() != MTLCommandBufferStatus::Error {
return;
}
let description = cb.error().map_or_else(
|| "no error object".to_string(),
|e| e.localizedDescription().to_string(),
);
let role = fault_role(&description);
if !THROTTLE.admit(role) {
return;
}
match role {
FaultRole::Cause => tracing::error!("{what} command buffer faulted: {description}"),
FaultRole::Victim => {
tracing::error!("{what} command buffer discarded by GPU recovery: {description}")
}
}
}
pub(super) fn attach_fault_logger(cmd: &ProtocolObject<dyn MTLCommandBuffer>, what: &'static str) {
let handler = block2::RcBlock::new(move |cb: NonNull<ProtocolObject<dyn MTLCommandBuffer>>| {
report_fault(unsafe { cb.as_ref() }, what);
});
unsafe {
cmd.addCompletedHandler(block2::RcBlock::as_ptr(&handler));
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn innocent_victim_is_a_victim() {
let description = "Discarded (victim of GPU error/recovery) \
(00000005:kIOGPUCommandBufferCallbackErrorInnocentVictim)";
assert_eq!(fault_role(description), FaultRole::Victim);
}
#[test]
fn ignored_submission_is_a_victim() {
let description = "Ignored (for causing prior/excessive GPU errors) \
(00000004:kIOGPUCommandBufferCallbackErrorSubmissionsIgnored)";
assert_eq!(fault_role(description), FaultRole::Victim);
}
#[test]
fn page_fault_is_a_cause() {
let description = "Caused GPU Address Fault Error \
(0000000b:kIOGPUCommandBufferCallbackErrorPageFault)";
assert_eq!(fault_role(description), FaultRole::Cause);
}
#[test]
fn each_role_stops_at_its_limit() {
let throttle = FaultThrottle::new();
for _ in 0..VICTIM_LOG_LIMIT {
assert!(throttle.admit(FaultRole::Victim));
}
assert!(!throttle.admit(FaultRole::Victim));
for _ in 0..CAUSE_LOG_LIMIT {
assert!(throttle.admit(FaultRole::Cause));
}
assert!(!throttle.admit(FaultRole::Cause));
}
#[test]
fn victims_never_spend_the_cause_budget() {
let throttle = FaultThrottle::new();
for _ in 0..VICTIM_LOG_LIMIT * 4 {
throttle.admit(FaultRole::Victim);
}
assert!(throttle.admit(FaultRole::Cause));
}
}