feat(kernel): lazy user FPU: off until first FP insn (illegal-insn trap -> zeroed regs, retry), save on dirty trap, restore on switch, off for threads without + kernel; spawntest per-thread FP check

This commit is contained in:
2026-10-28 03:20:41 +01:00
parent 34a44b5928
commit c256f4ecf3
7 changed files with 302 additions and 1 deletions

View File

@@ -0,0 +1,165 @@
//! Floating point for user threads, lazily. A thread starts with the FPU
//! off (`sstatus.FS`); its first floating-point instruction traps as an
//! illegal instruction, and it is given zeroed registers and the FPU and
//! retries. From then on its registers are saved when a trap finds them
//! changed (FS dirty) and loaded whenever it is switched to; a thread
//! without them runs with the FPU off, so it never sees another's. The
//! kernel itself runs with the FPU off.
use core::arch::asm;
use domain::thread::{FpState, SavedContext};
use super::TrapFrame;
const FS: usize = 3 << 13;
const FS_OFF: usize = 0;
const FS_CLEAN: usize = 2 << 13;
const FS_DIRTY: usize = 3 << 13;
/// Turns the FPU off for the kernel; what the trap frame says the thread
/// returns with is unchanged.
pub(super) fn kernel_off() {
// SAFETY: clears sstatus.FS only.
unsafe { asm!("csrc sstatus, {0}", in(reg) FS) };
}
fn hardware_on() {
// SAFETY: sets sstatus.FS to (at least) clean, so the FP registers can
// be read and written; `kernel_off` turns it off again.
unsafe { asm!("csrs sstatus, {0}", in(reg) FS_CLEAN) };
}
/// On a trap from a thread: its registers into its context if it changed
/// them, and the frame then says they are saved.
pub(super) fn save_if_dirty(frame: &mut TrapFrame, ctx: &mut SavedContext) {
if frame.sstatus & FS != FS_DIRTY {
return;
}
if let Some(state) = ctx.fp_mut() {
hardware_on();
save(state);
frame.sstatus = (frame.sstatus & !FS) | FS_CLEAN;
}
}
/// Returning to a thread: its registers into the FPU, or the FPU off.
pub(super) fn load_for(frame: &mut TrapFrame, ctx: &SavedContext) {
frame.sstatus &= !FS;
match ctx.fp() {
Some(state) => {
hardware_on();
load(state);
frame.sstatus |= FS_CLEAN;
}
None => frame.sstatus |= FS_OFF,
}
}
/// An illegal instruction from a thread with the FPU off and no registers
/// yet: it gets them, zeroed, and retries. Whether that was it (otherwise
/// the instruction is a real fault).
pub(super) fn first_use(frame: &mut TrapFrame, ctx: &mut SavedContext) -> bool {
if frame.sstatus & FS != FS_OFF || ctx.fp().is_some() {
return false;
}
let state = ctx.enable_fp();
hardware_on();
load(state);
// sepc still names the instruction, so it runs again.
frame.sstatus = (frame.sstatus & !FS) | FS_CLEAN;
true
}
/// `$op` (fsd or fld) for every FP register, at `{base}` + 8 each.
macro_rules! fp_registers {
($op:literal) => {
concat!(
$op,
" f0, 0({base})\n",
$op,
" f1, 8({base})\n",
$op,
" f2, 16({base})\n",
$op,
" f3, 24({base})\n",
$op,
" f4, 32({base})\n",
$op,
" f5, 40({base})\n",
$op,
" f6, 48({base})\n",
$op,
" f7, 56({base})\n",
$op,
" f8, 64({base})\n",
$op,
" f9, 72({base})\n",
$op,
" f10, 80({base})\n",
$op,
" f11, 88({base})\n",
$op,
" f12, 96({base})\n",
$op,
" f13, 104({base})\n",
$op,
" f14, 112({base})\n",
$op,
" f15, 120({base})\n",
$op,
" f16, 128({base})\n",
$op,
" f17, 136({base})\n",
$op,
" f18, 144({base})\n",
$op,
" f19, 152({base})\n",
$op,
" f20, 160({base})\n",
$op,
" f21, 168({base})\n",
$op,
" f22, 176({base})\n",
$op,
" f23, 184({base})\n",
$op,
" f24, 192({base})\n",
$op,
" f25, 200({base})\n",
$op,
" f26, 208({base})\n",
$op,
" f27, 216({base})\n",
$op,
" f28, 224({base})\n",
$op,
" f29, 232({base})\n",
$op,
" f30, 240({base})\n",
$op,
" f31, 248({base})\n",
)
};
}
/// The FPU's registers into `state` (the FPU must be on).
fn save(state: &mut FpState) {
let base = state.registers.as_mut_ptr();
let control: u64;
// SAFETY: `base` points at 32 u64s, written in place; frcsr only reads.
unsafe {
asm!(fp_registers!("fsd"), base = in(reg) base, options(nostack));
asm!("frcsr {0}", out(reg) control, options(nostack));
}
state.control = control;
}
/// `state` into the FPU's registers (the FPU must be on).
fn load(state: &FpState) {
let base = state.registers.as_ptr();
// SAFETY: `base` points at 32 u64s, only read; fscsr writes fcsr.
unsafe {
asm!(fp_registers!("fld"), base = in(reg) base, options(nostack, readonly));
asm!("fscsr {0}", in(reg) state.control, options(nostack));
}
}

View File

@@ -1,4 +1,5 @@
mod fast;
mod fp;
mod sched;
mod vector;
@@ -17,6 +18,7 @@ const CAUSE_INTERRUPT_BIT: usize = 1 << 63;
const CAUSE_S_SOFTWARE: usize = 1;
const CAUSE_S_TIMER: usize = 5;
const CAUSE_S_EXTERNAL: usize = 9;
const CAUSE_ILLEGAL_INSTRUCTION: usize = 2;
const CAUSE_ECALL_U: usize = 8;
/// Set in `sstatus` when the trap came from supervisor mode.
const SSTATUS_SPP: usize = 1 << 8;
@@ -29,6 +31,8 @@ pub fn init() {
let addr = trap_vector as *const () as usize;
core::arch::asm!("csrw stvec, {0}", in(reg) addr);
core::arch::asm!("csrw sscratch, zero");
// The kernel never uses the FPU; user threads get it lazily.
core::arch::asm!("csrc sstatus, {0}", in(reg) 3usize << 13);
// SIE stays clear; interrupts are enabled only by sret into userspace.
let sie_bits = (1usize << 5) | (1usize << 9) | (1usize << 1);
core::arch::asm!("csrs sie, {0}", in(reg) sie_bits);
@@ -90,6 +94,14 @@ extern "C" fn trap_handler_rust(frame: &mut TrapFrame) {
}
crate::smp::service_ipi();
crate::smp::lock();
// The thread's FP registers are saved before anything can switch away.
if let Some(ctx) = kern()
.current_thread()
.and_then(|t| kern().thread_context_mut(t))
{
fp::save_if_dirty(frame, ctx);
}
fp::kernel_off();
if scause & CAUSE_INTERRUPT_BIT != 0 {
handle_interrupt(frame, scause & !CAUSE_INTERRUPT_BIT);
} else {
@@ -163,6 +175,11 @@ fn handle_exception(frame: &mut TrapFrame, scause: usize) {
}
}
}
CAUSE_ILLEGAL_INSTRUCTION
if kern()
.current_thread()
.and_then(|t| kern().thread_context_mut(t))
.is_some_and(|ctx| fp::first_use(frame, ctx)) => {}
_ => {
let uart = crate::hw::uart::Uart::new();
uart.write_str("FAULT: scause=");

View File

@@ -14,6 +14,7 @@ fn load_context_to_frame(ctx: &domain::thread::SavedContext, frame: &mut TrapFra
frame.sstatus = ctx.status();
frame.sstatus &= !(1 << 8); // SPP: return to U-mode
frame.sstatus |= 1 << 5; // SPIE
super::fp::load_for(frame, ctx);
}
fn apply_pending_result(frame: &mut TrapFrame, tid: types::id::ThreadId) {