sim: RGA + NPU models (P3) — completes the RGA/RISC-V/NPU simulator requirement
- npu.rs: /proc/rknpu/load model (present-at-load% vs absent) mirroring sysmon's parse; 100% coverage. - rga.rs: recording improcess() fake — logs dispatched blits + programmable IM_STATUS to drive the CPU-fallback path; the blit pixels aren't modelled, the dispatch logic is; 100% coverage. - lib.rs re-exports NpuSim / RgaSim / Blit / Surface / Rect / ImStatus. 37 sim tests green; RISC-V (hpmcu) + RGA + NPU all simulated per future-features-2 §SDK. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017wB8KB3MMQztRDXCMCkPrf
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
d27bb7151d
commit
85b0db7eee
+9
-3
@@ -3,20 +3,26 @@
|
|||||||
//! Lets driver and supervisor logic run and be tested on the host, with no panel,
|
//! 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
|
//! by modelling the RV1106 hardware the vendor SDK cannot: the register/SRAM bus
|
||||||
//! ([`membus`]), the RISC-V HPMCU watchdog coprocessor ([`hpmcu`]), the CRU reset
|
//! ([`membus`]), the RISC-V HPMCU watchdog coprocessor ([`hpmcu`]), the CRU reset
|
||||||
//! ladder ([`cru`]), and the RS-485 device end ([`modbus`]) — and, as they land,
|
//! ladder ([`cru`]), the RS-485 device end ([`modbus`]), the **RGA** 2D blitter
|
||||||
//! the RGA blitter and the NPU.
|
//! ([`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`]
|
//! 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
|
//! 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.
|
//! 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 cru;
|
||||||
pub mod hpmcu;
|
pub mod hpmcu;
|
||||||
pub mod membus;
|
pub mod membus;
|
||||||
pub mod modbus;
|
pub mod modbus;
|
||||||
|
pub mod npu;
|
||||||
|
pub mod rga;
|
||||||
|
|
||||||
pub use cru::{BootMode, CruSim, ResetCause};
|
pub use cru::{BootMode, CruSim, ResetCause};
|
||||||
pub use hpmcu::HpmcuSim;
|
pub use hpmcu::HpmcuSim;
|
||||||
pub use membus::{MemBus, SimBus};
|
pub use membus::{MemBus, SimBus};
|
||||||
pub use modbus::ModbusSlave;
|
pub use modbus::ModbusSlave;
|
||||||
|
pub use npu::NpuSim;
|
||||||
|
pub use rga::{Blit, ImStatus, Rect, RgaSim, Surface};
|
||||||
|
|||||||
+110
@@ -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<String> {
|
||||||
|
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<u8> {
|
||||||
|
let after = text.split_once(':')?.1;
|
||||||
|
let digits: String = after
|
||||||
|
.trim_start()
|
||||||
|
.chars()
|
||||||
|
.take_while(|c| c.is_ascii_digit())
|
||||||
|
.collect();
|
||||||
|
digits.parse::<u8>().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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
+154
@@ -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<Blit>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user