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:
BFE Engineering
2026-08-25 14:12:10 -06:00
co-authored by Claude Opus 4.8
parent d27bb7151d
commit 85b0db7eee
3 changed files with 273 additions and 3 deletions
+154
View File
@@ -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());
}
}