use alloc::boxed::Box;
use alloc::string::String;
use alloc::sync::Arc;
use alloc::vec;
use alloc::vec::Vec;
use core::fmt;
use crate::core::device::{Device, DeviceClass, PropertySpec, RealizeCtx, ResetKind};
use crate::core::error::{BusError, Error, Result};
use crate::core::props::{Props, ValueKind};
use crate::core::sched::{AccessKind, LazyHandle};
use crate::core::space::{
AccessConstraints, AddressSpace, MemAttrs, MemOps, MemResult, Region, RegionRef, RequesterId,
};
use crate::core::state::{ChunkReader, ChunkWriter, Sink, Source};
use crate::core::sync::{AtomicU64, LockRank, Mutex, Ordering};
use crate::core::value::{Endian, Width};
use crate::machine::realize::{BindCtx, Instance};
const CLASS_NAME: &str = "lcd.scanout";
const STATE_VERSION: u32 = 1;
pub const REGISTER_BYTES: u64 = 0x20;
const CTRL_EN: u32 = 1 << 0;
const CTRL_MASK: u32 = CTRL_EN;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
#[repr(transparent)]
pub struct FbFormat(pub u16);
impl FbFormat {
pub const RGB888: FbFormat = FbFormat(0);
pub const BGR888: FbFormat = FbFormat(1);
pub const RGB565: FbFormat = FbFormat(2);
pub const XRGB8888: FbFormat = FbFormat(3);
#[must_use]
pub const fn bytes_per_pixel(self) -> u64 {
match self {
FbFormat::RGB565 => 2,
FbFormat::XRGB8888 => 4,
_ => 3,
}
}
#[must_use]
pub const fn name(self) -> &'static str {
match self {
FbFormat::RGB888 => "rgb888",
FbFormat::BGR888 => "bgr888",
FbFormat::RGB565 => "rgb565",
FbFormat::XRGB8888 => "xrgb8888",
_ => "unknown",
}
}
#[must_use]
pub fn from_name(name: &str) -> Option<FbFormat> {
match name {
"rgb888" => Some(FbFormat::RGB888),
"bgr888" => Some(FbFormat::BGR888),
"rgb565" => Some(FbFormat::RGB565),
"xrgb8888" => Some(FbFormat::XRGB8888),
_ => None,
}
}
pub const NAMES: &'static [&'static str] = &["rgb888", "bgr888", "rgb565", "xrgb8888"];
#[must_use]
pub fn decode(self, bytes: &[u8]) -> [u8; 3] {
match self {
FbFormat::BGR888 => [bytes[2], bytes[1], bytes[0]],
FbFormat::RGB565 => {
let v = u16::from_le_bytes([bytes[0], bytes[1]]);
let r = ((v >> 11) & 0x1f) as u8;
let g = ((v >> 5) & 0x3f) as u8;
let b = (v & 0x1f) as u8;
[
(r << 3) | (r >> 2),
(g << 2) | (g >> 4),
(b << 3) | (b >> 2),
]
}
FbFormat::XRGB8888 => [bytes[2], bytes[1], bytes[0]],
_ => [bytes[0], bytes[1], bytes[2]],
}
}
}
impl fmt::Display for FbFormat {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.name())
}
}
#[derive(Debug)]
pub struct Scanout {
shared: Arc<Shared>,
region: RegionRef,
}
struct Shared {
state: Mutex<State>,
frame_ticks: u64,
frame_nanos: AtomicU64,
ticks: AtomicU64,
next_event: AtomicU64,
frames: AtomicU64,
bus: Mutex<Option<Arc<AddressSpace>>>,
requester: Mutex<RequesterId>,
lazy: Mutex<Option<LazyHandle>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct State {
ticks: u64,
frames: u64,
ctrl: u32,
base: u64,
stride: u64,
width: u32,
height: u32,
format: FbFormat,
reset_base: u64,
reset_stride: u64,
reset_width: u32,
reset_height: u32,
reset_format: FbFormat,
}
impl State {
fn stride(&self) -> u64 {
if self.stride != 0 {
self.stride
} else {
u64::from(self.width) * self.format.bytes_per_pixel()
}
}
}
impl fmt::Debug for Shared {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut s = f.debug_struct("Shared");
s.field("frame_ticks", &self.frame_ticks);
match self.state.try_lock() {
Some(state) => s.field("state", &*state).finish(),
None => s.field("state", &"<in use>").finish(),
}
}
}
impl Scanout {
pub fn new(props: &Props) -> Result<Scanout> {
let mut r = props.reader();
let width: u64 = r.require("width")?;
let height: u64 = r.require("height")?;
let base: u64 = r.or("base", 0)?;
let stride: u64 = r.or("stride", 0)?;
let format_name = r.optional_str("format")?.unwrap_or("rgb888");
let htotal: u64 = r.or("htotal", width)?;
let vtotal: u64 = r.or("vtotal", height)?;
r.finish()?;
let bad = |message: String| Error::Config {
at: String::from(CLASS_NAME),
message,
};
if width == 0 || height == 0 {
return Err(bad(alloc::format!(
"the scanout is {width}x{height}; both dimensions must be at least 1"
)));
}
if width > u64::from(u32::MAX) || height > u64::from(u32::MAX) {
return Err(bad(String::from(
"a scanout dimension is a pixel count, not an address",
)));
}
let format = FbFormat::from_name(format_name).ok_or_else(|| {
bad(alloc::format!(
"`format` is `{format_name}`; it must be one of {:?}",
FbFormat::NAMES
))
})?;
if htotal < width || vtotal < height {
return Err(bad(alloc::format!(
"the total period {htotal}x{vtotal} is smaller than the visible {width}x{height}; \
the totals include the blanking, so they are never the smaller pair"
)));
}
let frame_ticks = htotal.saturating_mul(vtotal);
let state = State {
ticks: 0,
frames: 0,
ctrl: 0,
base,
stride,
width: width as u32,
height: height as u32,
format,
reset_base: base,
reset_stride: stride,
reset_width: width as u32,
reset_height: height as u32,
reset_format: format,
};
let shared = Arc::new(Shared {
state: Mutex::with_rank(LockRank::DEVICE, state),
frame_ticks,
frame_nanos: AtomicU64::new(0),
ticks: AtomicU64::new(0),
next_event: AtomicU64::new(frame_ticks),
frames: AtomicU64::new(0),
bus: Mutex::with_rank(LockRank::WIRE, None),
requester: Mutex::with_rank(LockRank::WIRE, RequesterId::ANONYMOUS),
lazy: Mutex::with_rank(LockRank::WIRE, None),
});
let port = Arc::new(ScanoutPort {
shared: Arc::clone(&shared),
});
let region = Arc::new(Region::io("lcdc", REGISTER_BYTES, port as Arc<dyn MemOps>));
Ok(Scanout { shared, region })
}
#[must_use]
pub fn geometry(&self) -> (u32, u32) {
let state = self.shared.state.lock();
(state.width, state.height)
}
#[must_use]
pub fn format(&self) -> FbFormat {
self.shared.state.lock().format
}
#[must_use]
pub fn base(&self) -> u64 {
self.shared.state.lock().base
}
#[must_use]
pub fn stride(&self) -> u64 {
self.shared.state.lock().stride()
}
#[must_use]
pub fn enabled(&self) -> bool {
self.shared.state.lock().ctrl & CTRL_EN != 0
}
#[must_use]
pub fn frame(&self) -> u64 {
self.shared.frames.load(Ordering::Relaxed)
}
#[must_use]
pub fn frame_ticks(&self) -> u64 {
self.shared.frame_ticks
}
#[must_use]
pub fn frame_period_nanos(&self) -> u64 {
self.shared.frame_nanos.load(Ordering::Relaxed)
}
pub fn read_row(&self, y: u32, dst: &mut [[u8; 3]]) -> bool {
for pixel in dst.iter_mut() {
*pixel = [0, 0, 0];
}
let (base, stride, width, height, format, enabled) = {
let state = self.shared.state.lock();
(
state.base,
state.stride(),
state.width,
state.height,
state.format,
state.ctrl & CTRL_EN != 0,
)
};
if !enabled || y >= height {
return false;
}
let bus = self.shared.bus.lock().clone();
let Some(bus) = bus else {
return false;
};
let requester = *self.shared.requester.lock();
let bpp = format.bytes_per_pixel();
let count = (dst.len() as u64).min(u64::from(width));
let row_addr = base.wrapping_add(u64::from(y).wrapping_mul(stride));
let mut row = vec![0u8; (count * bpp) as usize];
if bus
.read_bytes(row_addr, &mut row, self.attrs(requester))
.is_err()
{
return false;
}
for (i, pixel) in dst.iter_mut().take(count as usize).enumerate() {
let at = i * bpp as usize;
*pixel = format.decode(&row[at..]);
}
true
}
fn attrs(&self, requester: RequesterId) -> MemAttrs {
MemAttrs {
requester,
debug: true,
..MemAttrs::DEFAULT
}
}
pub fn advance_to(&self, target: u64) {
self.shared.advance_to(target);
}
}
impl Shared {
fn publish(&self, state: &State) {
self.ticks.store(state.ticks, Ordering::Relaxed);
self.frames.store(state.frames, Ordering::Relaxed);
let next = state
.ticks
.saturating_sub(state.ticks % self.frame_ticks)
.saturating_add(self.frame_ticks);
self.next_event
.store(next.max(state.ticks.saturating_add(1)), Ordering::Relaxed);
}
fn advance_to(&self, target: u64) {
let mut state = self.state.lock();
if target <= state.ticks {
return;
}
let before = state.ticks / self.frame_ticks;
let after = target / self.frame_ticks;
state.ticks = target;
if after > before && state.ctrl & CTRL_EN != 0 {
state.frames += after - before;
}
self.publish(&state);
}
fn sync(&self, attrs: MemAttrs) {
let handle = self.lazy.lock().clone();
let Some(handle) = handle else {
return;
};
let kind = if attrs.debug {
AccessKind::Debug
} else {
AccessKind::Guest
};
let _ = handle.sync(kind);
}
}
struct ScanoutPort {
shared: Arc<Shared>,
}
impl fmt::Debug for ScanoutPort {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ScanoutPort").finish_non_exhaustive()
}
}
impl MemOps for ScanoutPort {
fn read(&self, offset: u64, dst: &mut [u8], attrs: MemAttrs) -> MemResult {
if dst.len() != 4 || !offset.is_multiple_of(4) {
return Err(BusError::BadAccess);
}
self.shared.sync(attrs);
let state = self.shared.state.lock();
let value = match offset {
0x00 => state.ctrl,
0x04 => state.base as u32,
0x08 => (state.base >> 32) as u32,
0x0c => state.stride as u32,
0x10 => state.width,
0x14 => state.height,
0x18 => u32::from(state.format.0),
0x1c => state.frames as u32,
_ => 0,
};
dst.copy_from_slice(&value.to_le_bytes());
Ok(())
}
fn write(&self, offset: u64, src: &[u8], attrs: MemAttrs) -> MemResult {
if src.len() != 4 || !offset.is_multiple_of(4) {
return Err(BusError::BadAccess);
}
if attrs.debug {
return Err(BusError::BadAccess);
}
self.shared.sync(attrs);
let value = u32::from_le_bytes([src[0], src[1], src[2], src[3]]);
let mut state = self.shared.state.lock();
match offset {
0x00 => state.ctrl = value & CTRL_MASK,
0x04 => state.base = (state.base & 0xffff_ffff_0000_0000) | u64::from(value),
0x08 => state.base = (state.base & 0x0000_0000_ffff_ffff) | (u64::from(value) << 32),
0x0c => state.stride = u64::from(value),
0x10 => state.width = value.max(1),
0x14 => state.height = value.max(1),
0x18 => state.format = FbFormat(value as u16),
_ => {}
}
self.shared.publish(&state);
Ok(())
}
fn constraints(&self) -> AccessConstraints {
AccessConstraints::word(Width::U32, Endian::Little)
}
}
impl Device for Scanout {
fn class(&self) -> &'static DeviceClass {
&SCANOUT_CLASS
}
fn realize(&self, _ctx: &mut RealizeCtx<'_>) -> Result<()> {
Ok(())
}
fn reset(&self, _kind: ResetKind) {
let mut state = self.shared.state.lock();
state.ctrl = 0;
state.frames = 0;
state.base = state.reset_base;
state.stride = state.reset_stride;
state.width = state.reset_width;
state.height = state.reset_height;
state.format = state.reset_format;
self.shared.publish(&state);
}
fn save(&self, w: &mut ChunkWriter<'_>) -> Result<()> {
let state = *self.shared.state.lock();
w.write_u64(state.ticks)?;
w.write_u64(state.frames)?;
w.write_u32(state.ctrl)?;
w.write_u64(state.base)?;
w.write_u64(state.stride)?;
w.write_u32(state.width)?;
w.write_u32(state.height)?;
w.write_u16(state.format.0)
}
fn load(&self, r: &mut ChunkReader<'_>) -> Result<()> {
let mut state = self.shared.state.lock();
state.ticks = r.read_u64()?;
state.frames = r.read_u64()?;
state.ctrl = r.read_u32()?;
state.base = r.read_u64()?;
state.stride = r.read_u64()?;
state.width = r.read_u32()?;
state.height = r.read_u32()?;
state.format = FbFormat(r.read_u16()?);
self.shared.publish(&state);
Ok(())
}
fn region(&self, name: &str) -> Option<RegionRef> {
matches!(name, "" | "regs").then(|| Arc::clone(&self.region))
}
fn is_lazy(&self) -> bool {
true
}
fn current_tick(&self) -> u64 {
self.shared.ticks.load(Ordering::Relaxed)
}
fn advance_to(&self, tick: u64) {
Scanout::advance_to(self, tick);
}
fn next_event_tick(&self) -> Option<u64> {
Some(self.shared.next_event.load(Ordering::Relaxed))
}
fn attach_lazy(&self, handle: LazyHandle) {
*self.shared.lazy.lock() = Some(handle);
}
}
impl Instance for Scanout {
fn bind(&self, ctx: &BindCtx<'_>) -> Result<()> {
let space = ctx.space().ok_or_else(|| Error::Config {
at: String::from(ctx.path()),
message: String::from(
"a scanout engine is a bus master and needs the address space its framebuffer \
lives in (`space = mem`)",
),
})?;
*self.shared.bus.lock() = Some(Arc::clone(space));
*self.shared.requester.lock() = ctx.requester();
Ok(())
}
}
pub fn set_frame_rate(engine: &Scanout, hz_num: u64, hz_den: u64) {
if hz_num == 0 {
engine.shared.frame_nanos.store(0, Ordering::Relaxed);
return;
}
let nanos = engine
.shared
.frame_ticks
.saturating_mul(hz_den)
.saturating_mul(1_000_000_000)
/ hz_num;
engine.shared.frame_nanos.store(nanos, Ordering::Relaxed);
}
pub static SCANOUT_CLASS: DeviceClass = DeviceClass {
name: CLASS_NAME,
version: STATE_VERSION,
summary: "a generic RGB scanout engine: reads a framebuffer out of an address space",
properties: &[
PropertySpec {
name: "width",
kind: ValueKind::Uint,
required: true,
summary: "visible pixels across",
},
PropertySpec {
name: "height",
kind: ValueKind::Uint,
required: true,
summary: "visible pixels down",
},
PropertySpec {
name: "base",
kind: ValueKind::Uint,
required: false,
summary: "where the framebuffer starts, if the guest does not program it",
},
PropertySpec {
name: "stride",
kind: ValueKind::Uint,
required: false,
summary: "bytes per row (default 0, meaning width x bytes-per-pixel)",
},
PropertySpec {
name: "format",
kind: ValueKind::Str,
required: false,
summary: "how the guest packs a pixel: rgb888, bgr888, rgb565, xrgb8888",
},
PropertySpec {
name: "htotal",
kind: ValueKind::Uint,
required: false,
summary: "one horizontal period in pixel clocks, blanking included",
},
PropertySpec {
name: "vtotal",
kind: ValueKind::Uint,
required: false,
summary: "one vertical period in lines, blanking included",
},
],
construct: |props| Ok(Box::new(Scanout::new(props)?)),
};
pub fn register(registry: &mut crate::core::Registry) -> Result<()> {
registry.add(&SCANOUT_CLASS)
}
pub fn bind(bindings: &mut crate::machine::Bindings) -> Result<()> {
bindings.bind(CLASS_NAME, |props| Ok(Arc::new(Scanout::new(props)?)))
}
#[must_use]
pub fn schema() -> crate::machine::validate::ClassSchema {
use crate::machine::validate::{ClassSchema, PropSchema};
ClassSchema::new(CLASS_NAME)
.prop(
PropSchema::new("width", ValueKind::Uint)
.required()
.range(1, u64::from(u32::MAX)),
)
.prop(
PropSchema::new("height", ValueKind::Uint)
.required()
.range(1, u64::from(u32::MAX)),
)
.prop(PropSchema::new("base", ValueKind::Uint))
.prop(PropSchema::new("stride", ValueKind::Uint))
.prop(PropSchema::new("format", ValueKind::Str).values(FbFormat::NAMES))
.prop(PropSchema::new("htotal", ValueKind::Uint).range(1, u64::from(u32::MAX)))
.prop(PropSchema::new("vtotal", ValueKind::Uint).range(1, u64::from(u32::MAX)))
.region("")
.region("regs")
}
#[must_use]
pub fn read_frame(engine: &Scanout) -> Vec<Vec<[u8; 3]>> {
let (width, height) = engine.geometry();
let mut rows = Vec::with_capacity(height as usize);
for y in 0..height {
let mut row = vec![[0u8; 3]; width as usize];
engine.read_row(y, &mut row);
rows.push(row);
}
rows
}
#[cfg(test)]
mod tests;