diff --git a/sim/src/lib.rs b/sim/src/lib.rs index 43b60a4..7ed505e 100644 --- a/sim/src/lib.rs +++ b/sim/src/lib.rs @@ -3,20 +3,26 @@ //! Lets driver and supervisor logic run and be tested on the host, with no panel, //! by modelling the RV1106 hardware the vendor SDK cannot: the register/SRAM bus //! ([`membus`]), the RISC-V HPMCU watchdog coprocessor ([`hpmcu`]), the CRU reset -//! ladder ([`cru`]), and the RS-485 device end ([`modbus`]) — and, as they land, -//! the RGA blitter and the NPU. +//! ladder ([`cru`]), the RS-485 device end ([`modbus`]), the **RGA** 2D blitter +//! ([`rga`], a recording `improcess` fake), and the **NPU** load surface ([`npu`], +//! the `/proc/rknpu/load` model). //! //! Design: one [`membus::MemBus`] seam, two backends. On the host, [`membus::SimBus`] //! is an in-memory word map; on the device, flared's `devmem.rs` implements the same //! trait over `/dev/mem`, so the same code runs against either. See the repo README. -//! (The [`modbus`] slave rides a byte-stream seam, not the register bus.) +//! (The [`modbus`] slave rides a byte-stream seam and [`rga`] its own call seam, not +//! the register bus.) pub mod cru; pub mod hpmcu; pub mod membus; pub mod modbus; +pub mod npu; +pub mod rga; pub use cru::{BootMode, CruSim, ResetCause}; pub use hpmcu::HpmcuSim; pub use membus::{MemBus, SimBus}; pub use modbus::ModbusSlave; +pub use npu::NpuSim; +pub use rga::{Blit, ImStatus, Rect, RgaSim, Surface}; diff --git a/sim/src/npu.rs b/sim/src/npu.rs new file mode 100644 index 0000000..be235f5 --- /dev/null +++ b/sim/src/npu.rs @@ -0,0 +1,110 @@ +//! NPU load model — the `/proc/rknpu/load` surface. +//! +//! The rknpu driver exposes utilisation at `/proc/rknpu/load` as `"NPU load: N%"`, +//! and the file exists only once `rknpu.ko` is loaded — so a *missing* file means +//! the driver is absent, not idle (sysmon reports absent as 0 and labels the +//! screen). This models both a present NPU at a chosen load and an absent one, and +//! mirrors sysmon's parse (`strchr(buf, ':')` then the leading integer) so the +//! driver's reader can be exercised against realistic text. +//! +//! (Only `/proc/rknpu/load` is modelled. `/proc/rknpu/volt` is deliberately NOT — +//! reading it SIGSEGVs the reader on this board, so no code should ever open it.) + +/// A modelled NPU. `present == false` models rknpu.ko not loaded (no proc file). +#[derive(Clone, Copy, Debug)] +pub struct NpuSim { + present: bool, + load: u8, +} + +impl NpuSim { + /// A present NPU reporting 0% load. + pub fn new() -> Self { + Self { present: true, load: 0 } + } + + /// An absent NPU (rknpu.ko not loaded): `/proc/rknpu/load` does not exist. + pub fn absent() -> Self { + Self { present: false, load: 0 } + } + + /// Set the reported load, clamped to 0..=100. + pub fn set_load(&mut self, pct: u8) { + self.load = if pct > 100 { 100 } else { pct }; + } + + /// Contents of `/proc/rknpu/load`, or `None` when the NPU is absent (the file + /// would not exist, so the reader fails soft to "NPU absent"). + pub fn proc_load(&self) -> Option { + if self.present { + Some(format!("NPU load: {}%\n", self.load)) + } else { + None + } + } + + /// Parse a load percent out of a `/proc/rknpu/load` line the way sysmon does: + /// the text after the first ':' then the leading integer. `None` if there is + /// no colon or no number (which the driver treats as "report 0 / absent"). + pub fn parse_load(text: &str) -> Option { + let after = text.split_once(':')?.1; + let digits: String = after + .trim_start() + .chars() + .take_while(|c| c.is_ascii_digit()) + .collect(); + digits.parse::().ok() + } +} + +impl Default for NpuSim { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn present_reports_load_and_round_trips() { + let mut n = NpuSim::new(); + n.set_load(42); + let s = n.proc_load().unwrap(); + assert_eq!(s, "NPU load: 42%\n"); + assert_eq!(NpuSim::parse_load(&s), Some(42)); + } + + #[test] + fn load_clamps_to_100() { + let mut n = NpuSim::new(); + n.set_load(250); + assert_eq!(NpuSim::parse_load(&n.proc_load().unwrap()), Some(100)); + } + + #[test] + fn absent_has_no_proc_file() { + assert!(NpuSim::absent().proc_load().is_none()); + } + + #[test] + fn parse_rejects_no_colon() { + assert_eq!(NpuSim::parse_load("no colon here"), None); + } + + #[test] + fn parse_rejects_non_numeric() { + assert_eq!(NpuSim::parse_load("NPU load: x%"), None); + } + + #[test] + fn parse_handles_zero() { + assert_eq!(NpuSim::parse_load("NPU load: 0%"), Some(0)); + } + + #[test] + fn default_is_present_idle() { + assert_eq!(NpuSim::default().proc_load(), Some("NPU load: 0%\n".to_string())); + } +} diff --git a/sim/src/rga.rs b/sim/src/rga.rs new file mode 100644 index 0000000..76f211d --- /dev/null +++ b/sim/src/rga.rs @@ -0,0 +1,154 @@ +//! RGA 2D blitter model — a recording `improcess` fake. +//! +//! `warden_rga.c` offloads copies/scales/format-conversions to the RGA via +//! librga's `improcess(src, dst, ..., IM_SYNC)`, and falls back to the CPU draw +//! path when it returns anything but `IM_STATUS_SUCCESS`. The blit *pixels* are +//! not modelled — what matters for testing is the **dispatch** logic: which ops +//! get sent, with what geometry/format, and that a non-success status drives the +//! CPU fallback. So the sim records each requested op and returns a programmable +//! status. It rides its own call seam (behind the driver's `#if WARDEN_USE_RGA`), +//! not the register bus. + +/// A rectangle in a surface (im2d `im_rect`). +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct Rect { + pub x: i32, + pub y: i32, + pub w: i32, + pub h: i32, +} + +/// A surface descriptor — the subset of im2d `rga_buffer_t` the dispatch cares +/// about (dimensions + pixel format). +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct Surface { + pub width: i32, + pub height: i32, + pub format: u32, +} + +/// One recorded RGA operation. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct Blit { + pub src: Surface, + pub dst: Surface, + pub srect: Rect, + pub drect: Rect, +} + +/// im2d status subset: success, or a failure that must drive the CPU fallback. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum ImStatus { + Success, + Failed, +} + +/// A recording RGA. `status` is what `improcess` returns; `blits` is the log. +pub struct RgaSim { + status: ImStatus, + blits: Vec, +} + +impl RgaSim { + /// A working RGA whose `improcess` succeeds. + pub fn new() -> Self { + Self { status: ImStatus::Success, blits: Vec::new() } + } + + /// Program the status `improcess` returns (set `Failed` to drive the driver's + /// CPU fallback path). + pub fn set_status(&mut self, s: ImStatus) { + self.status = s; + } + + /// Model one `improcess()` call: record it and return the programmed status. + pub fn improcess(&mut self, src: Surface, dst: Surface, srect: Rect, drect: Rect) -> ImStatus { + self.blits.push(Blit { src, dst, srect, drect }); + self.status + } + + /// The recorded blit log, in dispatch order. + pub fn blits(&self) -> &[Blit] { + &self.blits + } + + /// Number of blits dispatched. + pub fn count(&self) -> usize { + self.blits.len() + } + + /// The most recent blit, if any. + pub fn last(&self) -> Option<&Blit> { + self.blits.last() + } + + /// Forget the recorded blits (e.g. between frames). + pub fn clear(&mut self) { + self.blits.clear(); + } +} + +impl Default for RgaSim { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn surf(w: i32, h: i32) -> Surface { + Surface { width: w, height: h, format: 0 } + } + fn rect(w: i32, h: i32) -> Rect { + Rect { x: 0, y: 0, w, h } + } + + #[test] + fn records_a_successful_blit() { + let mut r = RgaSim::new(); + let st = r.improcess(surf(720, 720), surf(360, 360), rect(720, 720), rect(360, 360)); + assert_eq!(st, ImStatus::Success); + assert_eq!(r.count(), 1); + let b = r.last().unwrap(); + assert_eq!(b.src, surf(720, 720)); + assert_eq!(b.drect, rect(360, 360)); + } + + #[test] + fn failed_status_drives_fallback() { + let mut r = RgaSim::new(); + r.set_status(ImStatus::Failed); + assert_eq!(r.improcess(surf(10, 10), surf(10, 10), rect(10, 10), rect(10, 10)), + ImStatus::Failed); + // the op is still recorded — the driver dispatched it, then fell back. + assert_eq!(r.count(), 1); + } + + #[test] + fn logs_multiple_in_order() { + let mut r = RgaSim::new(); + r.improcess(surf(1, 1), surf(1, 1), rect(1, 1), rect(1, 1)); + r.improcess(surf(2, 2), surf(2, 2), rect(2, 2), rect(2, 2)); + assert_eq!(r.count(), 2); + assert_eq!(r.blits()[0].src, surf(1, 1)); + assert_eq!(r.blits()[1].src, surf(2, 2)); + } + + #[test] + fn clear_empties_the_log() { + let mut r = RgaSim::new(); + r.improcess(surf(1, 1), surf(1, 1), rect(1, 1), rect(1, 1)); + r.clear(); + assert_eq!(r.count(), 0); + assert!(r.last().is_none()); + } + + #[test] + fn default_is_empty_and_succeeds() { + let r = RgaSim::default(); + assert_eq!(r.count(), 0); + assert!(r.last().is_none()); + } +}