#![cfg_attr(not(target_os = "linux"), allow(dead_code, unused_variables))]
pub const RT_PRIO_MAX: u8 = 90;
#[derive(Debug)]
pub enum RtError {
NoCapability,
InvalidAffinity,
MemlockExceeded,
Errno(i32),
NotSupported,
}
impl std::fmt::Display for RtError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RtError::NoCapability => write!(
f,
"no CAP_SYS_NICE — run as root or grant via `setcap cap_sys_nice+ep`"
),
RtError::InvalidAffinity => write!(f, "invalid CPU mask for sched_setaffinity"),
RtError::MemlockExceeded => write!(
f,
"mlockall failed — increase RLIMIT_MEMLOCK (`ulimit -l unlimited` or systemd LimitMEMLOCK=infinity)"
),
RtError::Errno(e) => write!(f, "errno {e}"),
RtError::NotSupported => write!(f, "not supported on this platform"),
}
}
}
impl std::error::Error for RtError {}
pub fn set_sched_fifo(prio: u8) -> Result<(), RtError> {
#[cfg(target_os = "linux")]
{
let prio = prio.clamp(1, RT_PRIO_MAX) as i32;
let mut param = libc_sched_param {
sched_priority: prio,
};
let ret = unsafe { sched_setscheduler(0, SCHED_FIFO, &mut param) };
if ret == 0 {
return Ok(());
}
match errno_now() {
1 => Err(RtError::NoCapability),
other => Err(RtError::Errno(other)),
}
}
#[cfg(not(target_os = "linux"))]
Err(RtError::NotSupported)
}
pub fn set_cpu_affinity(cpus: &[u32]) -> Result<(), RtError> {
#[cfg(target_os = "linux")]
{
if cpus.is_empty() || cpus.iter().any(|&c| c >= 64) {
return Err(RtError::InvalidAffinity);
}
let mut set = libc_cpu_set::default();
for &c in cpus {
set.bits[c as usize / 64] |= 1u64 << (c % 64);
}
let ret = unsafe {
sched_setaffinity(
0,
std::mem::size_of::<libc_cpu_set>(),
&set as *const libc_cpu_set,
)
};
if ret == 0 {
Ok(())
} else {
Err(RtError::Errno(errno_now()))
}
}
#[cfg(not(target_os = "linux"))]
Err(RtError::NotSupported)
}
pub fn mlockall_current_and_future() -> Result<(), RtError> {
#[cfg(target_os = "linux")]
{
let ret = unsafe { mlockall(MCL_CURRENT | MCL_FUTURE) };
if ret == 0 {
Ok(())
} else {
match errno_now() {
12 => Err(RtError::MemlockExceeded),
1 => Err(RtError::NoCapability),
other => Err(RtError::Errno(other)),
}
}
}
#[cfg(not(target_os = "linux"))]
Err(RtError::NotSupported)
}
pub fn set_cpu_governor(governor: &str) -> Result<usize, RtError> {
#[cfg(target_os = "linux")]
{
use std::fs;
let sys_root = std::path::Path::new("/sys/devices/system/cpu");
let entries = match fs::read_dir(sys_root) {
Ok(e) => e,
Err(e) => {
return Err(RtError::Errno(e.raw_os_error().unwrap_or(0)));
}
};
let mut applied = 0usize;
for entry in entries.flatten() {
let name = entry.file_name();
let Some(name_str) = name.to_str() else {
continue;
};
if !name_str.starts_with("cpu")
|| !name_str
.trim_start_matches("cpu")
.chars()
.all(|c| c.is_ascii_digit())
{
continue;
}
let gov_path = entry.path().join("cpufreq/scaling_governor");
if !gov_path.exists() {
continue;
}
match fs::write(&gov_path, governor) {
Ok(()) => applied += 1,
Err(e) => eprintln!(
"[yscv-video] governor write to {gov_path:?} failed: {e} \
(need CAP_SYS_ADMIN or root)"
),
}
}
Ok(applied)
}
#[cfg(not(target_os = "linux"))]
{
let _ = governor;
Ok(0)
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct RtAppliedState {
pub sched_fifo: bool,
pub affinity: bool,
pub mlockall: bool,
pub cpu_governor_cores: usize,
}
pub fn apply_rt_config(prio: u8, cpus: &[u32], lock_mem: bool) -> RtAppliedState {
apply_rt_config_with_governor(prio, cpus, lock_mem, None)
}
pub fn apply_rt_config_with_governor(
prio: u8,
cpus: &[u32],
lock_mem: bool,
governor: Option<&str>,
) -> RtAppliedState {
let mut applied = RtAppliedState::default();
match set_sched_fifo(prio) {
Ok(()) => applied.sched_fifo = true,
Err(e) => eprintln!("[yscv-video] sched_fifo({prio}) failed: {e}"),
}
if !cpus.is_empty() {
match set_cpu_affinity(cpus) {
Ok(()) => applied.affinity = true,
Err(e) => eprintln!("[yscv-video] affinity({cpus:?}) failed: {e}"),
}
}
if lock_mem {
match mlockall_current_and_future() {
Ok(()) => applied.mlockall = true,
Err(e) => eprintln!("[yscv-video] mlockall failed: {e}"),
}
}
if let Some(gov) = governor {
match set_cpu_governor(gov) {
Ok(n) => applied.cpu_governor_cores = n,
Err(e) => eprintln!("[yscv-video] set_cpu_governor({gov}) failed: {e}"),
}
}
applied
}
#[cfg(target_os = "linux")]
const SCHED_FIFO: i32 = 1;
#[cfg(target_os = "linux")]
const MCL_CURRENT: i32 = 1;
#[cfg(target_os = "linux")]
const MCL_FUTURE: i32 = 2;
#[cfg(target_os = "linux")]
#[repr(C)]
struct libc_sched_param {
sched_priority: i32,
}
#[cfg(target_os = "linux")]
#[repr(C)]
struct libc_cpu_set {
bits: [u64; 64],
}
#[cfg(target_os = "linux")]
impl Default for libc_cpu_set {
fn default() -> Self {
Self { bits: [0; 64] }
}
}
#[cfg(target_os = "linux")]
unsafe extern "C" {
fn sched_setscheduler(pid: i32, policy: i32, param: *mut libc_sched_param) -> i32;
fn sched_setaffinity(pid: i32, cpusetsize: usize, mask: *const libc_cpu_set) -> i32;
fn mlockall(flags: i32) -> i32;
fn __errno_location() -> *mut i32;
}
#[cfg(target_os = "linux")]
fn errno_now() -> i32 {
unsafe { *__errno_location() }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rt_error_display() {
assert!(RtError::NoCapability.to_string().contains("CAP_SYS_NICE"));
assert!(
RtError::MemlockExceeded
.to_string()
.contains("RLIMIT_MEMLOCK")
);
}
#[cfg(not(target_os = "linux"))]
#[test]
fn ops_return_not_supported_off_linux() {
assert!(matches!(set_sched_fifo(50), Err(RtError::NotSupported)));
assert!(matches!(set_cpu_affinity(&[0]), Err(RtError::NotSupported)));
assert!(matches!(
mlockall_current_and_future(),
Err(RtError::NotSupported)
));
}
#[test]
fn apply_rt_config_logs_and_continues_off_linux() {
let st = apply_rt_config(60, &[0, 1], true);
#[cfg(not(target_os = "linux"))]
{
assert!(!st.sched_fifo);
assert!(!st.affinity);
assert!(!st.mlockall);
}
#[cfg(target_os = "linux")]
{
let _ = st;
}
}
}