feat: raw framebuffer fallback for fbbootlogd when DRM unavailable

- Add RawFb struct: direct framebuffer rendering via physmap
- Add RawTextScreen: simple text renderer using orbclient font
- Fallback in FbbootlogScheme::new() when V2GraphicsHandle fails
- Reads FRAMEBUFFER_ADDR/WIDTH/HEIGHT/STRIDE from bootloader env
- Scroll via ptr::copy on pixel rows, clear bottom line
- No DRM, no shadow buffer, no GPU required — like MS-DOS text mode
- Add common dependency to fbbootlogd Cargo.toml
This commit is contained in:
2026-05-17 14:56:50 +03:00
parent 20853c41f5
commit 2bfe4b427b
58 changed files with 1691 additions and 3602 deletions
@@ -4,10 +4,16 @@ use crate::{
percpu::PercpuBlock,
syscall::FloatRegisters,
};
use core::{mem::offset_of, ptr};
use core::{mem::offset_of, ptr, sync::atomic::AtomicBool};
use spin::Once;
use syscall::{EnvRegisters, Result};
/// This must be used by the kernel to ensure that context switches are done atomically
/// Compare and exchange this to true when beginning a context switch on any CPU
/// The `Context::switch_to` function will set it back to false, allowing other CPU's to switch
/// This must be done, as no locks can be held on the stack during switch
pub static CONTEXT_SWITCH_LOCK: AtomicBool = AtomicBool::new(false);
// 512 bytes for registers, extra bytes for fpcr and fpsr
pub const KFX_ALIGN: usize = 16;
@@ -2,11 +2,13 @@ use crate::{
arch::interrupt::InterruptStack, context::context::Kstack, memory::RmmA, percpu::PercpuBlock,
syscall::FloatRegisters,
};
use core::mem::offset_of;
use core::{mem::offset_of, sync::atomic::AtomicBool};
use rmm::{Arch, VirtualAddress};
use spin::Once;
use syscall::{error::*, EnvRegisters};
pub static CONTEXT_SWITCH_LOCK: AtomicBool = AtomicBool::new(false);
pub const KFX_ALIGN: usize = 16;
#[derive(Clone, Debug, Default)]
@@ -1,4 +1,4 @@
use core::mem::offset_of;
use core::{mem::offset_of, sync::atomic::AtomicBool};
use rmm::{Arch, VirtualAddress};
use spin::Once;
use syscall::{error::*, EnvRegisters};
@@ -14,6 +14,12 @@ use crate::{
syscall::FloatRegisters,
};
/// This must be used by the kernel to ensure that context switches are done atomically
/// Compare and exchange this to true when beginning a context switch on any CPU
/// The `Context::switch_to` function will set it back to false, allowing other CPU's to switch
/// This must be done, as no locks can be held on the stack during switch
pub static CONTEXT_SWITCH_LOCK: AtomicBool = AtomicBool::new(false);
const ST_RESERVED: u128 = 0xFFFF_FFFF_FFFF_0000_0000_0000_0000_0000;
pub const KFX_ALIGN: usize = 16;
@@ -1,5 +1,6 @@
use core::{
ptr::{addr_of, addr_of_mut},
sync::atomic::AtomicBool,
};
use crate::syscall::FloatRegisters;
@@ -11,6 +12,12 @@ use spin::Once;
use syscall::{error::*, EnvRegisters};
use x86::msr;
/// This must be used by the kernel to ensure that context switches are done atomically
/// Compare and exchange this to true when beginning a context switch on any CPU
/// The `Context::switch_to` function will set it back to false, allowing other CPU's to switch
/// This must be done, as no locks can be held on the stack during switch
pub static CONTEXT_SWITCH_LOCK: AtomicBool = AtomicBool::new(false);
const ST_RESERVED: u128 = 0xFFFF_FFFF_FFFF_0000_0000_0000_0000_0000;
#[cfg(cpu_feature_never = "xsave")]
@@ -148,8 +148,6 @@ pub struct Context {
pub euid: u32,
pub egid: u32,
pub pid: usize,
/// Supplementary group IDs for access control decisions.
pub groups: Vec<u32>,
// See [`PreemptGuard`]
//
@@ -206,7 +204,6 @@ impl Context {
euid: 0,
egid: 0,
pid: 0,
groups: Vec::new(),
#[cfg(feature = "syscall_debug")]
syscall_debug_info: crate::syscall::debug::SyscallDebugInfo::default(),
@@ -482,7 +479,6 @@ impl Context {
uid: self.euid,
gid: self.egid,
pid: self.pid,
groups: self.groups.clone(),
}
}
}
+5 -52
View File
@@ -4,7 +4,7 @@ use crate::{
event,
scheme::{self, SchemeId},
sync::{CleanLockToken, RwLock, L6},
syscall::error::{Error, Result, ESTALE},
syscall::error::Result,
};
use alloc::sync::Arc;
use syscall::{schemev2::NewFdFlags, RwFlags, O_APPEND, O_NONBLOCK};
@@ -18,7 +18,6 @@ pub struct FileDescription {
pub offset: u64,
/// The scheme that this file refers to
pub scheme: SchemeId,
pub scheme_generation: Option<u64>,
/// The number the scheme uses to refer to this file
pub number: usize,
/// The flags passed to open or fcntl(SETFL)
@@ -33,52 +32,6 @@ bitflags! {
}
}
impl FileDescription {
pub fn with_generation(
scheme: SchemeId,
scheme_generation: Option<u64>,
number: usize,
offset: u64,
flags: u32,
internal_flags: InternalFlags,
) -> Self {
Self {
offset,
scheme,
scheme_generation,
number,
flags,
internal_flags,
}
}
pub fn new(
scheme: SchemeId,
number: usize,
offset: u64,
flags: u32,
internal_flags: InternalFlags,
token: &mut CleanLockToken,
) -> Self {
Self::with_generation(
scheme,
Some(scheme::current_scheme_generation(token.token(), scheme)),
number,
offset,
flags,
internal_flags,
)
}
pub fn get_scheme(&self, token: &mut CleanLockToken) -> Result<scheme::KernelSchemes> {
if let Some(expected_generation) = self.scheme_generation
&& expected_generation != scheme::current_scheme_generation(token.token(), self.scheme)
{
return Err(Error::new(ESTALE));
}
scheme::get_scheme(token.token(), self.scheme)
}
pub fn rw_flags(&self, rw: RwFlags) -> u32 {
let mut ret = self.flags & !(O_NONBLOCK | O_APPEND) as u32;
if rw.contains(RwFlags::APPEND) {
@@ -123,7 +76,7 @@ impl FileDescription {
pub fn try_close(self, token: &mut CleanLockToken) -> Result<()> {
event::unregister_file(self.scheme, self.number, token);
let scheme = self.get_scheme(token)?;
let scheme = scheme::get_scheme(token.token(), self.scheme)?;
scheme.close(self.number, token)
}
@@ -132,12 +85,12 @@ impl FileDescription {
impl FileDescriptor {
pub fn close(self, token: &mut CleanLockToken) -> Result<()> {
{
let (desc, number, internal_flags) = {
let (scheme_id, number, internal_flags) = {
let desc = self.description.read(token.token());
(*desc, desc.number, desc.internal_flags)
(desc.scheme, desc.number, desc.internal_flags)
};
if internal_flags.contains(InternalFlags::NOTIFY_ON_NEXT_DETACH) {
let scheme = desc.get_scheme(token)?;
let scheme = scheme::get_scheme(token.token(), scheme_id)?;
scheme.detach(number, token)?;
}
}
@@ -64,13 +64,14 @@ impl UnmapResult {
return Ok(());
};
let (scheme, number) = {
let desc = *description.read(token.token());
(desc.get_scheme(token)?, desc.number)
let (scheme_id, number) = {
let desc = description.write(token.token());
(desc.scheme, desc.number)
};
let funmap_result = scheme
.kfunmap(number, base_offset, self.size, self.flags, token);
let scheme_opt = scheme::get_scheme(token.token(), scheme_id);
let funmap_result = scheme_opt
.and_then(|scheme| scheme.kfunmap(number, base_offset, self.size, self.flags, token));
if let Ok(fd) = Arc::try_unwrap(description) {
fd.into_inner().try_close(token)?;
@@ -2686,13 +2687,20 @@ fn correct_inner<'l>(
// XXX: This is cheating, but guaranteed we won't deadlock because we've dropped addr_space_guard
let mut token = unsafe { CleanLockToken::new() };
let desc = *file_ref.description.read(token.token());
let scheme = desc.get_scheme(&mut token).map_err(|_| PfError::Segv)?;
let scheme_number = desc.number;
let user_inner = match scheme {
KernelSchemes::User(user) => user.inner,
_ => return Err(PfError::Segv),
let (scheme_id, scheme_number) = {
let desc = &file_ref.description.read(token.token());
(desc.scheme, desc.number)
};
let user_inner = scheme::get_scheme(token.token(), scheme_id)
.ok()
.and_then(|s| {
if let KernelSchemes::User(user) = s {
Some(user.inner)
} else {
None
}
})
.ok_or(PfError::Segv)?;
let offset = file_ref.base_offset as u64 + (pages_from_grant_start * PAGE_SIZE) as u64;
user_inner
@@ -14,8 +14,8 @@ use crate::{
memory::{RmmA, RmmArch, TableKind},
percpu::PercpuBlock,
sync::{
ArcRwLockWriteGuard, CleanLockToken, LockToken, McsMutex, McsMutexGuard, Mutex,
MutexGuard, RwLock, RwLockReadGuard, RwLockWriteGuard, L0, L1, L2, L4,
ArcRwLockWriteGuard, CleanLockToken, LockToken, Mutex, MutexGuard, RwLock, RwLockReadGuard,
RwLockWriteGuard, L0, L1, L2, L4,
},
syscall::error::Result,
};
@@ -74,12 +74,10 @@ pub use self::arch::empty_cr3;
// the context file descriptors.
static CONTEXTS: RwLock<L2, BTreeSet<ContextRef>> = RwLock::new(BTreeSet::new());
// Actual context store for the scheduler — uses MCS fair spinlock to
// eliminate cache-line bouncing under multi-CPU contention.
static RUN_CONTEXTS: McsMutex<L1, RunContextData> = McsMutex::new(RunContextData::new());
// Actual context store for the scheduler
static RUN_CONTEXTS: Mutex<L1, RunContextData> = Mutex::new(RunContextData::new());
// Context that has been pushed out from RUN_CONTEXTS after being idle.
// Uses regular Mutex (lower contention; wakeup_contexts uses try_lock).
// Context that has been pushed out from RUN_CONTEXTS after being idle
static IDLE_CONTEXTS: Mutex<L2, VecDeque<WeakContextRef>> = Mutex::new(VecDeque::new());
pub struct RunContextData {
@@ -115,7 +113,7 @@ pub fn idle_contexts_try(
IDLE_CONTEXTS.try_lock(token)
}
pub fn run_contexts(token: LockToken<'_, L0>) -> McsMutexGuard<'_, L1, RunContextData> {
pub fn run_contexts(token: LockToken<'_, L0>) -> MutexGuard<'_, L1, RunContextData> {
RUN_CONTEXTS.lock(token)
}
@@ -15,7 +15,7 @@ use crate::{
use alloc::{sync::Arc, vec::Vec};
use core::{
cell::{Cell, RefCell},
mem,
hint, mem,
sync::atomic::Ordering,
};
use syscall::PtraceFlags;
@@ -26,11 +26,6 @@ enum UpdateResult {
Blocked,
}
/// Default number of PIT ticks before triggering a context switch.
/// At ~2.25 ms per tick, 3 ticks ≈ 6.75 ms timeslice.
/// Configurable per-CPU via `ContextSwitchPercpu::preempt_interval`.
const DEFAULT_PREEMPT_INTERVAL: usize = 3;
// A simple geometric series where value[i] ~= value[i - 1] * 1.25
const SCHED_PRIO_TO_WEIGHT: [usize; 40] = [
88761, 71755, 56483, 46273, 36291, 29154, 23254, 18705, 14949, 11916, 9548, 7620, 6100, 4904,
@@ -95,15 +90,13 @@ struct SwitchResultInner {
///
/// The function also calls the signal handler after switching contexts.
pub fn tick(token: &mut CleanLockToken) {
let percpu = PercpuBlock::current();
let ticks_cell = &percpu.switch_internals.pit_ticks;
let ticks_cell = &PercpuBlock::current().switch_internals.pit_ticks;
let new_ticks = ticks_cell.get() + 1;
ticks_cell.set(new_ticks);
// Trigger a context switch when the per-CPU preempt interval is reached.
let interval = percpu.switch_internals.preempt_interval.get();
if new_ticks >= interval {
// Trigger a context switch after every 3 ticks (approx. 6.75 ms).
if new_ticks >= 3 {
switch(token);
crate::context::signal::signal_handler(token);
}
@@ -127,10 +120,7 @@ pub unsafe extern "C" fn switch_finish_hook() {
crate::arch::stop::emergency_reset();
}
}
PercpuBlock::current()
.switch_internals
.in_context_switch
.set(false);
arch::CONTEXT_SWITCH_LOCK.store(false, Ordering::SeqCst);
crate::percpu::switch_arch_hook();
}
}
@@ -160,15 +150,16 @@ pub fn switch(token: &mut CleanLockToken) -> SwitchResult {
//set PIT Interrupt counter to 0, giving each process same amount of PIT ticks
percpu.switch_internals.pit_ticks.set(0);
// Acquire the per-CPU context switch flag. Each CPU can only be in one context
// switch at a time. The per-context write locks provide cross-CPU safety; this
// flag catches re-entrant switches on the same CPU (a kernel bug).
debug_assert!(
!percpu.switch_internals.in_context_switch.get(),
"context switch re-entry on CPU {}",
percpu.cpu_id
);
percpu.switch_internals.in_context_switch.set(true);
// Acquire the global lock to ensure exclusive access during context switch and avoid
// issues that would be caused by the unsafe operations below
// TODO: Better memory orderings?
while arch::CONTEXT_SWITCH_LOCK
.compare_exchange_weak(false, true, Ordering::SeqCst, Ordering::Relaxed)
.is_err()
{
hint::spin_loop();
percpu.maybe_handle_tlb_shootdown();
}
// Lock the previous context.
let prev_context_lock = crate::context::current();
@@ -176,8 +167,8 @@ pub fn switch(token: &mut CleanLockToken) -> SwitchResult {
let mut prev_context_guard = unsafe { prev_context_lock.write_arc() };
if !prev_context_guard.is_preemptable() {
// Unset per-CPU context switch flag
percpu.switch_internals.in_context_switch.set(false);
// Unset global lock
arch::CONTEXT_SWITCH_LOCK.store(false, Ordering::SeqCst);
// Pretend to have finished switching, so CPU is not idled
return SwitchResult::Switched;
@@ -301,8 +292,8 @@ pub fn switch(token: &mut CleanLockToken) -> SwitchResult {
SwitchResult::Switched
}
_ => {
// No target was found, unset per-CPU context switch flag and return
percpu.switch_internals.in_context_switch.set(false);
// No target was found, unset global lock and return
arch::CONTEXT_SWITCH_LOCK.store(false, Ordering::SeqCst);
percpu.stats.set_state(cpu_stats::CpuState::Idle);
@@ -361,7 +352,6 @@ fn wakeup_contexts(token: &mut CleanLockToken, switch_time: u128) -> Vec<(usize,
}
/// This is the scheduler function which currently utilises Deficit Weighted Round Robin Scheduler
/// with NUMA-aware context selection preference.
fn select_next_context(
token: &mut CleanLockToken,
percpu: &PercpuBlock,
@@ -387,10 +377,6 @@ fn select_next_context(
let total_contexts: usize = contexts_list.iter().map(|q| q.len()).sum();
let mut skipped_contexts = 0;
// NUMA-aware selection: remember cross-node fallback candidate.
let my_numa_node = percpu.numa_node.get();
let mut cross_node_fallback: Option<(usize, ArcContextLockWriteGuard)> = None;
'priority: loop {
i = (i + 1) % 40;
total_iters += 1;
@@ -455,44 +441,9 @@ fn select_next_context(
// Is this context runnable on this CPU?
let sw = unsafe { update_runnable(&mut next_context_guard, cpu_id, switch_time) };
if let UpdateResult::CanSwitch = sw {
// NUMA-aware selection: check if this context's last CPU was on the same node.
let same_node = if my_numa_node != u8::MAX {
next_context_guard.cpu_id
.map(|cid| {
crate::percpu::get_for_cpu(cid)
.map(|p| p.numa_node.get() == my_numa_node)
.unwrap_or(false)
})
.unwrap_or(true) // New context (no last CPU) — treat as same node
} else {
true // No NUMA info — treat all as same node
};
if same_node {
// Cache-warm: select immediately
percpu.current_prio.set(next_context_guard.prio);
next_context_guard_opt = Some(next_context_guard);
balance[i] -= SCHED_PRIO_TO_WEIGHT[20];
break 'priority;
} else {
// Cross-node candidate: save as fallback, keep scanning for same-node
if cross_node_fallback.is_none() {
// Cache the priority and balance for later
cross_node_fallback =
Some((next_context_guard.prio, next_context_guard));
balance[i] -= SCHED_PRIO_TO_WEIGHT[20];
// Don't break — keep looking for a same-node context
continue;
} else {
// Already have a cross-node fallback; push this one back
contexts.push_back(next_context_ref);
skipped_contexts += 1;
if skipped_contexts >= total_contexts {
break 'priority;
}
continue;
}
}
next_context_guard_opt = Some(next_context_guard);
balance[i] -= SCHED_PRIO_TO_WEIGHT[20];
break 'priority;
} else {
if matches!(sw, UpdateResult::Blocked) {
idle_contexts(token.token()).push_back(next_context_ref);
@@ -507,15 +458,6 @@ fn select_next_context(
}
}
}
// If we found a cross-node fallback but no same-node context, use it
if next_context_guard_opt.is_none() {
if let Some((prio, guard)) = cross_node_fallback {
percpu.current_prio.set(prio);
next_context_guard_opt = Some(guard);
}
}
percpu.balance.set(balance);
percpu.last_queue.set(i);
@@ -523,10 +465,7 @@ fn select_next_context(
// Send the old process to the back of the line (if it is still runnable)
let prev_ctx = WeakContextRef(Arc::downgrade(&prev_context_lock));
if prev_context_guard.status.is_runnable() {
let raw_prio = prev_context_guard.prio;
let prio = percpu.effective_prio(raw_prio);
// Clear PI donation — previous context is being re-queued
percpu.pi_donated_prio.store(u32::MAX, Ordering::Relaxed);
let prio = prev_context_guard.prio;
contexts_list[prio].push_back(prev_ctx);
} else {
idle_contexts(token.token()).push_back(prev_ctx);
@@ -538,8 +477,7 @@ fn select_next_context(
return Ok(Some(next_context_guard));
} else {
if !was_idle && !Arc::ptr_eq(&prev_context_lock, &idle_context) {
// Switching to idle context — cache lowest priority
percpu.current_prio.set(39);
// We switch into the idle context
Ok(Some(unsafe { idle_context.write_arc() }))
} else {
// We found no other process to run.
@@ -556,13 +494,6 @@ pub struct ContextSwitchPercpu {
switch_result: Cell<Option<SwitchResultInner>>,
switch_time: Cell<u128>,
pit_ticks: Cell<usize>,
/// Per-CPU context switch flag. Set to true during a context switch on this CPU.
/// Replaced the global CONTEXT_SWITCH_LOCK to eliminate cross-CPU serialization.
in_context_switch: Cell<bool>,
/// Number of PIT ticks before triggering a context switch.
/// Default: 3 (≈6.75 ms). Lower values improve interactive responsiveness;
/// higher values improve throughput for batch/compute workloads.
preempt_interval: Cell<usize>,
current_ctxt: RefCell<Option<Arc<ContextLock>>>,
@@ -577,8 +508,6 @@ impl ContextSwitchPercpu {
switch_result: Cell::new(None),
switch_time: Cell::new(0),
pit_ticks: Cell::new(0),
in_context_switch: Cell::new(false),
preempt_interval: Cell::new(DEFAULT_PREEMPT_INTERVAL),
current_ctxt: RefCell::new(None),
idle_ctxt: RefCell::new(None),
being_sigkilled: Cell::new(false),