From 5a5c9e82d9a8d770adb54d494e6afae003704a67 Mon Sep 17 00:00:00 2001 From: Gerzain Mata Date: Wed, 22 Jul 2026 01:13:51 -0700 Subject: [PATCH 01/15] stm32-neochrom: add embassy driver crate for NemaGFX Introduce embassy-stm32-neochrom as the Embassy integration layer over stm32-bindings NemaGFX FFI, with chip presets, GPU2D stub bring-up, and a minimal NeoChrom clear API. --- embassy-stm32-neochrom/Cargo.toml | 68 ++++++++++++++++++++ embassy-stm32-neochrom/README.md | 33 ++++++++++ embassy-stm32-neochrom/src/color.rs | 30 +++++++++ embassy-stm32-neochrom/src/driver.rs | 64 +++++++++++++++++++ embassy-stm32-neochrom/src/error.rs | 19 ++++++ embassy-stm32-neochrom/src/ffi.rs | 3 + embassy-stm32-neochrom/src/fmt.rs | 75 +++++++++++++++++++++++ embassy-stm32-neochrom/src/framebuffer.rs | 56 +++++++++++++++++ embassy-stm32-neochrom/src/lib.rs | 24 ++++++++ 9 files changed, 372 insertions(+) create mode 100644 embassy-stm32-neochrom/Cargo.toml create mode 100644 embassy-stm32-neochrom/README.md create mode 100644 embassy-stm32-neochrom/src/color.rs create mode 100644 embassy-stm32-neochrom/src/driver.rs create mode 100644 embassy-stm32-neochrom/src/error.rs create mode 100644 embassy-stm32-neochrom/src/ffi.rs create mode 100644 embassy-stm32-neochrom/src/fmt.rs create mode 100644 embassy-stm32-neochrom/src/framebuffer.rs create mode 100644 embassy-stm32-neochrom/src/lib.rs diff --git a/embassy-stm32-neochrom/Cargo.toml b/embassy-stm32-neochrom/Cargo.toml new file mode 100644 index 0000000000..4533f141a2 --- /dev/null +++ b/embassy-stm32-neochrom/Cargo.toml @@ -0,0 +1,68 @@ +[package] +name = "embassy-stm32-neochrom" +version = "0.1.0" +edition = "2024" +license = "MIT OR Apache-2.0" +description = "Embassy driver for ST NeoChrom GPU via NemaGFX." +keywords = ["embedded", "async", "stm32", "gpu", "neochrom"] +categories = ["embedded", "hardware-support", "no-std"] +repository = "https://github.com/embassy-rs/embassy" +documentation = "https://docs.embassy.dev/embassy-stm32-neochrom" +readme = "README.md" + +publish = false + +[package.metadata.embassy] +build = [ + { target = "thumbv8m.main-none-eabihf", features = ["stm32n657x0", "stub-gpu2d"] }, +] + +[package.metadata.embassy_docs] +src_base = "https://github.com/embassy-rs/embassy/blob/embassy-stm32-neochrom-v$VERSION/embassy-stm32-neochrom/src/" +src_base_git = "https://github.com/embassy-rs/embassy/blob/$COMMIT/embassy-stm32-neochrom/src/" +target = "thumbv8m.main-none-eabihf" +features = ["stm32n657x0", "stub-gpu2d"] + +[package.metadata.docs.rs] +features = ["stm32n657x0", "stub-gpu2d"] + +[dependencies] +embassy-stm32 = { version = "0.6.0", path = "../embassy-stm32", features = ["unstable-pac"] } +embassy-hal-internal = { version = "0.5.0", path = "../embassy-hal-internal" } +stm32-bindings = { path = "../../stm32-bindings/build/stm32-bindings", optional = true, default-features = false } +nema-gfx-hal = { path = "../../stm32-bindings/nema-gfx-hal", default-features = false } + +aligned = "0.4.2" +critical-section = "1.2" + +defmt = { version = "1.0.1", optional = true } +log = { version = "0.4.17", optional = true } + +[features] +default = ["stub-gpu2d"] +defmt = ["dep:defmt", "embassy-hal-internal/defmt"] +log = ["dep:log"] + +## Use the in-tree GPU2D HAL stub (CI / early bring-up). +stub-gpu2d = ["nema-gfx-hal/stub"] + +## NemaGFX core presets (forwarded to stm32-bindings). +neochrom-m55 = ["dep:stm32-bindings", "stm32-bindings/neochrom-m55"] +neochrom-m33-revc = ["dep:stm32-bindings", "stm32-bindings/neochrom-m33-revc"] +neochrom-m33-nemapvg = ["dep:stm32-bindings", "stm32-bindings/neochrom-m33-nemapvg"] +neochrom-m7 = ["dep:stm32-bindings", "stm32-bindings/neochrom-m7"] + +## Chip families +n6 = [] +u5 = [] +h7rs = [] + +stm32n657x0 = ["embassy-stm32/stm32n657x0", "neochrom-m55", "n6", "nema-gfx-hal/stm32n6"] +stm32n655x0 = ["embassy-stm32/stm32n655x0", "neochrom-m55", "n6", "nema-gfx-hal/stm32n6"] +stm32n647x0 = ["embassy-stm32/stm32n647x0", "neochrom-m55", "n6", "nema-gfx-hal/stm32n6"] + +stm32u599nj = ["embassy-stm32/stm32u599nj", "neochrom-m33-revc", "u5", "nema-gfx-hal/stm32u5"] +stm32u5g9nj = ["embassy-stm32/stm32u5g9nj", "neochrom-m33-nemapvg", "u5", "nema-gfx-hal/stm32u5"] + +stm32h7s7z8 = ["embassy-stm32/stm32h7s7z8", "neochrom-m7", "h7rs", "nema-gfx-hal/stm32h7rs"] +stm32h7r7z8 = ["embassy-stm32/stm32h7r7z8", "neochrom-m7", "h7rs", "nema-gfx-hal/stm32h7rs"] diff --git a/embassy-stm32-neochrom/README.md b/embassy-stm32-neochrom/README.md new file mode 100644 index 0000000000..4b433291bf --- /dev/null +++ b/embassy-stm32-neochrom/README.md @@ -0,0 +1,33 @@ +# embassy-stm32-neochrom + +Embassy integration for ST NeoChrom (GPU2D) using the [NemaGFX](https://github.com/STMicroelectronics/x-cube-image-processing/tree/main/Middleware/NemaGFX) middleware via [`stm32-bindings`](https://github.com/embassy-rs/stm32-bindings). + +## Status + +Early bring-up. Provides a minimal driver over the generated NemaGFX FFI and the platform HAL from `nema-gfx-hal`. + +## Local development + +Generate `stm32-bindings` first: + +```bash +cd ../stm32-bindings +cargo run --release --bin stm32-bindings-gen -- --module nema_gfx +``` + +Then build this crate (example chip feature): + +```bash +cd ../embassy/embassy-stm32-neochrom +cargo check --features stm32n657x0,stub-gpu2d +``` + +## Features + +| Feature | Description | +|---------|-------------| +| `stm32n657x0` | STM32N657 + Cortex-M55 NemaGFX library | +| `stub-gpu2d` | Link GPU2D HAL stub instead of STM32Cube (default) | +| `neochrom-m55` | NemaGFX bindings + M55 prebuilt library only | + +Disable `default-features` and `stub-gpu2d` when wiring a real STM32Cube GPU2D HAL on hardware. diff --git a/embassy-stm32-neochrom/src/color.rs b/embassy-stm32-neochrom/src/color.rs new file mode 100644 index 0000000000..1f2b7cc406 --- /dev/null +++ b/embassy-stm32-neochrom/src/color.rs @@ -0,0 +1,30 @@ +//! RGBA color in the format expected by NemaGFX clear/fill APIs. + +/// Opaque 32-bit RGBA color (`0xAARRGGBB`). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] +pub struct Rgba8888(pub u32); + +impl Rgba8888 { + /// Construct from 8-bit RGBA channels. + #[inline] + pub const fn new(r: u8, g: u8, b: u8, a: u8) -> Self { + Self(((a as u32) << 24) | ((r as u32) << 16) | ((g as u32) << 8) | (b as u32)) + } + + /// Solid red. + pub const RED: Self = Self::new(0xFF, 0x00, 0x00, 0xFF); + /// Solid green. + pub const GREEN: Self = Self::new(0x00, 0xFF, 0x00, 0xFF); + /// Solid blue. + pub const BLUE: Self = Self::new(0x00, 0x00, 0xFF, 0xFF); + /// Opaque black. + pub const BLACK: Self = Self::new(0x00, 0x00, 0x00, 0xFF); + /// Opaque white. + pub const WHITE: Self = Self::new(0xFF, 0xFF, 0xFF, 0xFF); + + /// Value passed to [`crate::ffi::nema_gfx::nema_clear`]. + #[inline] + pub const fn bits(self) -> u32 { + self.0 + } +} diff --git a/embassy-stm32-neochrom/src/driver.rs b/embassy-stm32-neochrom/src/driver.rs new file mode 100644 index 0000000000..13e0b7a431 --- /dev/null +++ b/embassy-stm32-neochrom/src/driver.rs @@ -0,0 +1,64 @@ +//! NeoChrom GPU driver using NemaGFX. + +use crate::color::Rgba8888; +use crate::error::{Error, InitError}; +use crate::ffi::nema_gfx::{ + nema_bind_dst_tex, nema_cl_bind, nema_cl_create, nema_cl_submit, nema_cl_wait, nema_clear, nema_init, + NEMA_RGBA8888, +}; +use crate::framebuffer::FrameBuffer; + +/// NeoChrom GPU context. +/// +/// Call [`NeoChrom::new`] once after GPU2D clocks are enabled. With the default +/// `stub-gpu2d` feature, a stub HAL is used for link/CI testing. +pub struct NeoChrom { + _init: (), +} + +impl NeoChrom { + /// Initialize NemaGFX and the platform HAL. + pub fn new() -> Result { + Self::init_gpu2d()?; + + let status = unsafe { nema_init() }; + if status != 0 { + return Err(InitError::NemaGfx); + } + + Ok(Self { _init: () }) + } + + /// Clear `framebuffer` to `color` using the GPU command list path. + pub fn clear( + &mut self, + framebuffer: &FrameBuffer, + color: Rgba8888, + ) -> Result<(), Error> { + unsafe { + nema_bind_dst_tex(framebuffer.phys_addr(), W, H, NEMA_RGBA8888, -1); + + let mut cl = nema_cl_create(); + nema_cl_bind(&mut cl); + nema_clear(color.bits()); + nema_cl_submit(&mut cl); + if nema_cl_wait(&mut cl) != 0 { + return Err(Error::CommandListWait); + } + } + + Ok(()) + } + + fn init_gpu2d() -> Result<(), InitError> { + #[cfg(feature = "stub-gpu2d")] + { + nema_gfx_hal::gpu2d_init_stub().map_err(|_| InitError::Gpu2d) + } + #[cfg(not(feature = "stub-gpu2d"))] + { + // Real GPU2D init must happen in board support before `NeoChrom::new`. + Ok(()) + } + } +} diff --git a/embassy-stm32-neochrom/src/error.rs b/embassy-stm32-neochrom/src/error.rs new file mode 100644 index 0000000000..355f39ee66 --- /dev/null +++ b/embassy-stm32-neochrom/src/error.rs @@ -0,0 +1,19 @@ +//! NeoChrom driver errors. + +/// NemaGFX / NeoChrom runtime error. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum Error { + /// Waiting on a command list failed. + CommandListWait, +} + +/// Initialization failure. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum InitError { + /// GPU2D HAL initialization failed. + Gpu2d, + /// `nema_init()` returned an error. + NemaGfx, +} diff --git a/embassy-stm32-neochrom/src/ffi.rs b/embassy-stm32-neochrom/src/ffi.rs new file mode 100644 index 0000000000..14fb956533 --- /dev/null +++ b/embassy-stm32-neochrom/src/ffi.rs @@ -0,0 +1,3 @@ +//! Raw NemaGFX bindings re-export. + +pub use stm32_bindings::nema_gfx; diff --git a/embassy-stm32-neochrom/src/fmt.rs b/embassy-stm32-neochrom/src/fmt.rs new file mode 100644 index 0000000000..f39146d058 --- /dev/null +++ b/embassy-stm32-neochrom/src/fmt.rs @@ -0,0 +1,75 @@ +#![macro_use] +#![allow(unused)] + +#[cfg(all(feature = "defmt", feature = "log"))] +compile_error!("You may not enable both `defmt` and `log` features."); + +#[collapse_debuginfo(yes)] +macro_rules! trace { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::trace!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::trace!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! debug { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::debug!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::debug!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! info { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::info!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::info!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! warn { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::warn!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::warn!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! error { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::error!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::error!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} diff --git a/embassy-stm32-neochrom/src/framebuffer.rs b/embassy-stm32-neochrom/src/framebuffer.rs new file mode 100644 index 0000000000..1d55a30282 --- /dev/null +++ b/embassy-stm32-neochrom/src/framebuffer.rs @@ -0,0 +1,56 @@ +//! Framebuffer storage for NeoChrom rendering targets. + +use aligned::{A32, Aligned}; + +use crate::color::Rgba8888; + +/// RGBA8888 framebuffer suitable as a NemaGFX destination texture. +/// +/// The third const generic `N` must equal `W * H` (number of pixels). +pub struct FrameBuffer { + pixels: Aligned, +} + +impl FrameBuffer { + /// Create a zero-initialized framebuffer. + pub const fn new() -> Self { + Self { + pixels: Aligned([0; N]), + } + } + + /// Framebuffer width in pixels. + #[inline] + pub const fn width(&self) -> u32 { + W + } + + /// Framebuffer height in pixels. + #[inline] + pub const fn height(&self) -> u32 { + H + } + + /// Physical base address for [`crate::ffi::nema_gfx::nema_bind_dst_tex`]. + #[inline] + pub fn phys_addr(&self) -> usize { + self.pixels.as_ptr() as usize + } + + /// Mutable view of pixel data (native-endian RGBA8888 words). + #[inline] + pub fn pixels_mut(&mut self) -> &mut [u32] { + &mut *self.pixels + } + + /// Fill the CPU-side buffer without using the GPU (debug / fallback). + pub fn fill_cpu(&mut self, color: Rgba8888) { + self.pixels_mut().fill(color.bits()); + } +} + +impl Default for FrameBuffer { + fn default() -> Self { + Self::new() + } +} diff --git a/embassy-stm32-neochrom/src/lib.rs b/embassy-stm32-neochrom/src/lib.rs new file mode 100644 index 0000000000..94a310e677 --- /dev/null +++ b/embassy-stm32-neochrom/src/lib.rs @@ -0,0 +1,24 @@ +//! NeoChrom (GPU2D) support for STM32 MCUs via NemaGFX. +//! +//! This crate wraps the [`stm32-bindings`](https://github.com/embassy-rs/stm32-bindings) +//! NemaGFX FFI with a small safe-ish driver surface. Platform register access and +//! memory allocation are provided by [`nema-gfx-hal`](https://github.com/embassy-rs/stm32-bindings/tree/main/nema-gfx-hal) +//! until `embassy-stm32`'s GPU2D driver grows HAL hooks for N6/H7RS. + +#![no_std] +#![warn(missing_docs)] +#![allow(unsafe_op_in_unsafe_fn)] + +pub(crate) mod fmt; + +mod color; +mod driver; +mod error; +mod framebuffer; + +pub mod ffi; + +pub use color::Rgba8888; +pub use driver::NeoChrom; +pub use error::{Error, InitError}; +pub use framebuffer::FrameBuffer; From 97a36a79203ad5c72a5dcdebaf19f1efaaaf4090 Mon Sep 17 00:00:00 2001 From: Gerzain Mata Date: Wed, 22 Jul 2026 01:14:24 -0700 Subject: [PATCH 02/15] examples/stm32n6: add NeoChrom NemaGFX smoke test Wire embassy-stm32-neochrom into the N6 examples with a minimal GPU clear loop so local bring-up can validate linking and the command-list path. --- examples/stm32n6/Cargo.toml | 1 + examples/stm32n6/README.md | 11 ++++ examples/stm32n6/src/bin/neochrom.rs | 77 ++++++++++++++++++++++++++++ 3 files changed, 89 insertions(+) create mode 100644 examples/stm32n6/src/bin/neochrom.rs diff --git a/examples/stm32n6/Cargo.toml b/examples/stm32n6/Cargo.toml index 843a9a82ad..f15df09599 100644 --- a/examples/stm32n6/Cargo.toml +++ b/examples/stm32n6/Cargo.toml @@ -14,6 +14,7 @@ embassy-time = { version = "0.5.1", path = "../../embassy-time", features = ["de embassy-net = { version = "0.9.1", path = "../../embassy-net", features = ["defmt", "tcp", "dhcpv4", "medium-ethernet", "proto-ipv6"] } embassy-usb = { version = "0.6.0", path = "../../embassy-usb", features = ["defmt"] } embassy-futures = { version = "0.1.2", path = "../../embassy-futures" } +embassy-stm32-neochrom = { version = "0.1.0", path = "../../embassy-stm32-neochrom", features = ["defmt", "stm32n657x0", "stub-gpu2d"] } defmt = "1.0.1" defmt-rtt = "1.0.0" diff --git a/examples/stm32n6/README.md b/examples/stm32n6/README.md index 3daabd65f1..c4d76b12e6 100644 --- a/examples/stm32n6/README.md +++ b/examples/stm32n6/README.md @@ -3,3 +3,14 @@ Simple standalone examples for the STM32N6570-DK and NUCLEO-N657X0-Q, primarily intended for dev mode — loaded directly to RAM via probe-rs with no flash boot required. For a full two-stage boot system with firmware updates from external flash, see [stm32n6-flashboot](../stm32n6-flashboot/). + +## NeoChrom (NemaGFX) + +The `neochrom` example uses [`embassy-stm32-neochrom`](../../embassy-stm32-neochrom). Generate `stm32-bindings` first: + +```bash +cd ../../../stm32-bindings +cargo run --release --bin stm32-bindings-gen -- --module nema_gfx +cd ../embassy/examples/stm32n6 +cargo run --release --bin neochrom +``` diff --git a/examples/stm32n6/src/bin/neochrom.rs b/examples/stm32n6/src/bin/neochrom.rs new file mode 100644 index 0000000000..265c8f73a8 --- /dev/null +++ b/examples/stm32n6/src/bin/neochrom.rs @@ -0,0 +1,77 @@ +#![no_std] +#![no_main] + +//! NeoChrom (NemaGFX) smoke test for the STM32N6570-DK. +//! +//! Initializes the GPU via [`embassy_stm32_neochrom::NeoChrom`] and repeatedly +//! clears a small RGBA8888 framebuffer with the GPU command-list path. +//! +//! With the default `stub-gpu2d` feature the HAL is a link-time stub — the +//! framebuffer may stay zero on hardware until a real STM32Cube GPU2D init is +//! wired in. The example still validates NemaGFX init, linking, and the clear +//! API from an Embassy application. +//! +//! Generate `stm32-bindings` first: +//! ```text +//! cd ../../../stm32-bindings +//! cargo run --release --bin stm32-bindings-gen -- --module nema_gfx +//! ``` + +use defmt::info; +use embassy_executor::Spawner; +use embassy_stm32::rcc::SupplyConfig; +use embassy_stm32::{Config, pac}; +use embassy_stm32_neochrom::{FrameBuffer, NeoChrom, Rgba8888}; +use embassy_time::Timer; +use {defmt_rtt as _, panic_probe as _}; + +const FB_WIDTH: u32 = 64; +const FB_HEIGHT: u32 = 64; +const FB_PIXELS: usize = (FB_WIDTH * FB_HEIGHT) as usize; + +#[embassy_executor::main] +async fn main(_spawner: Spawner) { + let mut config = Config::default(); + // DK uses external SMPS (UM3300 Tab.6); embassy default = internal SMPS hangs init() at VOSRDY. + config.rcc.supply_config = SupplyConfig::External; + let _p = embassy_stm32::init(config); + + enable_all_sram(); + + info!("stm32n6 neochrom example starting"); + + let mut gpu = NeoChrom::new().expect("NeoChrom init failed"); + let mut fb = FrameBuffer::::new(); + + let colors = [ + Rgba8888::RED, + Rgba8888::GREEN, + Rgba8888::BLUE, + Rgba8888::WHITE, + Rgba8888::BLACK, + ]; + let mut idx = 0usize; + + loop { + let color = colors[idx]; + gpu.clear(&fb, color).expect("NeoChrom clear failed"); + info!("gpu clear ok, color=0x{:08x}", color.bits()); + idx = (idx + 1) % colors.len(); + Timer::after_secs(1).await; + } +} + +/// Enable run-mode clocks for every AXISRAM bank (same as the LTDC example). +fn enable_all_sram() { + pac::RCC.memenr().modify(|w| { + w.set_axisram1en(true); + w.set_axisram2en(true); + w.set_axisram3en(true); + w.set_axisram4en(true); + w.set_axisram5en(true); + w.set_axisram6en(true); + w.set_ahbsram1en(true); + w.set_ahbsram2en(true); + w.set_bkpsramen(true); + }); +} From 9d797429253c9fcc308f5af952f95aee9a0bf2be Mon Sep 17 00:00:00 2001 From: Gerzain Mata Date: Wed, 22 Jul 2026 09:13:53 -0700 Subject: [PATCH 03/15] examples/stm32n6: fix unused mut in neochrom example --- examples/stm32n6/src/bin/neochrom.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/stm32n6/src/bin/neochrom.rs b/examples/stm32n6/src/bin/neochrom.rs index 265c8f73a8..098a81ece3 100644 --- a/examples/stm32n6/src/bin/neochrom.rs +++ b/examples/stm32n6/src/bin/neochrom.rs @@ -41,7 +41,7 @@ async fn main(_spawner: Spawner) { info!("stm32n6 neochrom example starting"); let mut gpu = NeoChrom::new().expect("NeoChrom init failed"); - let mut fb = FrameBuffer::::new(); + let fb = FrameBuffer::::new(); let colors = [ Rgba8888::RED, From b9937146217a58de0cfa5f64e68a0ddb4033f0bd Mon Sep 17 00:00:00 2001 From: Gerzain Mata Date: Wed, 22 Jul 2026 19:48:03 -0700 Subject: [PATCH 04/15] stm32/gpu2d: give the error interrupt handler a real body on_interrupt() was a no-op, unlike every other peripheral driver in this crate. Give it the standard pattern used elsewhere (e.g. jpeg.rs): mask at the NVIC level (CLC/ER have no separate interrupt-enable field per gpu2d_v1.yaml, so there's nothing finer-grained to disable) and wake a static AtomicWaker. Add Gpu2d::wait_command_list_complete() as the async counterpart, re-arming the interrupt once the flag is consumed. --- embassy-stm32/src/gpu2d.rs | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/embassy-stm32/src/gpu2d.rs b/embassy-stm32/src/gpu2d.rs index 7f0f59ce97..92297a0d9a 100644 --- a/embassy-stm32/src/gpu2d.rs +++ b/embassy-stm32/src/gpu2d.rs @@ -6,11 +6,17 @@ use core::marker::PhantomData; use embassy_hal_internal::PeripheralType; +use embassy_sync::waitqueue::AtomicWaker; use crate::interrupt::typelevel::Interrupt; use crate::pac::gpu2d::Gpu2d as Regs; use crate::{Peri, interrupt, rcc}; +// GPU2D has exactly one instance per chip today (see stm32-data), so a single +// static waker is sufficient — unlike peripherals with multiple instances +// (e.g. USART), which need one waker per instance. +static WAKER: AtomicWaker = AtomicWaker::new(); + /// GPU2D driver error flag. #[derive(Debug, PartialEq, Eq, Clone, Copy)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] @@ -61,6 +67,28 @@ impl<'d, T: Instance> Gpu2d<'d, T> { Ok(()) } } + + /// Wait for the current command list to complete. + /// + /// Note: [`Error::SystemError`] is not observed here — check + /// [`Self::take_error`] separately if you need to detect hardware errors. + pub async fn wait_command_list_complete(&mut self) { + core::future::poll_fn(|cx| { + WAKER.register(cx.waker()); + if self.command_list_complete() { + self.clear_command_list_complete(); + // SAFETY: re-arming after consuming the flag we were woken for. + unsafe { T::Interrupt::enable() }; + core::task::Poll::Ready(()) + } else { + // First poll, or a spurious wake: make sure the interrupt is + // armed before parking on the waker. + unsafe { T::Interrupt::enable() }; + core::task::Poll::Pending + } + }) + .await + } } /// GPU2D error interrupt handler. @@ -70,7 +98,11 @@ pub struct InterruptHandler { impl interrupt::typelevel::Handler for InterruptHandler { unsafe fn on_interrupt() { - let _ = T::regs(); + // Neither CLC nor ER (see gpu2d_v1.yaml) has a separate + // interrupt-enable field, so mask at the NVIC level to avoid an + // interrupt storm; the waiting task clears the flag and re-enables. + T::Interrupt::disable(); + WAKER.wake(); } } From 421868e66d5e6afaf86aef01310afcc4f5e7dc0c Mon Sep 17 00:00:00 2001 From: Gerzain Mata Date: Wed, 22 Jul 2026 19:48:13 -0700 Subject: [PATCH 05/15] stm32-neochrom: bridge nema-gfx-hal to the real GPU2D driver MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds gpu2d_bridge, implementing the C ABI hal_gpu2d_shim.h expects (HAL_GPU2D_ReadRegister/WriteRegister/PollCompletion) backed by embassy_stm32::gpu2d::Gpu2d instead of the CI-only stub. NeoChrom::new() splits into a stub-gpu2d variant (unchanged) and a real variant that takes the GPU2D peripheral + IRQ binding and constructs the real driver. Verified against STM32U5F9/U5G9, where the driver builds end-to-end. STM32N6/H7RS remain on the stub path for now — stm32-data is missing the RCC clock-enable/reset mapping for GPU2D on those chip families (present for U5 only), which embassy-stm32's Gpu2d driver requires regardless of this bridge. Only command-list-complete/error status is bridged; HAL_GPU2D_ReadRegister/ WriteRegister return 0/no-op for anything else, and hardware SystemError is drained without surfacing through NeoChrom::Error yet. --- embassy-stm32-neochrom/src/driver.rs | 39 +++++---- embassy-stm32-neochrom/src/gpu2d_bridge.rs | 98 ++++++++++++++++++++++ embassy-stm32-neochrom/src/lib.rs | 2 + 3 files changed, 125 insertions(+), 14 deletions(-) create mode 100644 embassy-stm32-neochrom/src/gpu2d_bridge.rs diff --git a/embassy-stm32-neochrom/src/driver.rs b/embassy-stm32-neochrom/src/driver.rs index 13e0b7a431..52a07c24c8 100644 --- a/embassy-stm32-neochrom/src/driver.rs +++ b/embassy-stm32-neochrom/src/driver.rs @@ -17,10 +17,33 @@ pub struct NeoChrom { } impl NeoChrom { - /// Initialize NemaGFX and the platform HAL. + /// Initialize NemaGFX and the platform HAL using the in-tree GPU2D stub. + /// + /// Only available with the default `stub-gpu2d` feature (CI / link tests). + #[cfg(feature = "stub-gpu2d")] pub fn new() -> Result { - Self::init_gpu2d()?; + nema_gfx_hal::gpu2d_init_stub().map_err(|_| InitError::Gpu2d)?; + Self::finish_init() + } + + /// Initialize NemaGFX using the real GPU2D peripheral. + /// + /// `peri`/`irq` are consumed to enable the GPU2D peripheral clock and + /// interrupt via [`embassy_stm32::gpu2d::Gpu2d`]. Only available without + /// the default `stub-gpu2d` feature. + #[cfg(not(feature = "stub-gpu2d"))] + pub fn new( + peri: embassy_stm32::Peri<'static, embassy_stm32::peripherals::GPU2D>, + irq: impl embassy_stm32::interrupt::typelevel::Binding< + ::Interrupt, + embassy_stm32::gpu2d::InterruptHandler, + > + 'static, + ) -> Result { + crate::gpu2d_bridge::init(peri, irq); + Self::finish_init() + } + fn finish_init() -> Result { let status = unsafe { nema_init() }; if status != 0 { return Err(InitError::NemaGfx); @@ -49,16 +72,4 @@ impl NeoChrom { Ok(()) } - - fn init_gpu2d() -> Result<(), InitError> { - #[cfg(feature = "stub-gpu2d")] - { - nema_gfx_hal::gpu2d_init_stub().map_err(|_| InitError::Gpu2d) - } - #[cfg(not(feature = "stub-gpu2d"))] - { - // Real GPU2D init must happen in board support before `NeoChrom::new`. - Ok(()) - } - } } diff --git a/embassy-stm32-neochrom/src/gpu2d_bridge.rs b/embassy-stm32-neochrom/src/gpu2d_bridge.rs new file mode 100644 index 0000000000..8cc27502b8 --- /dev/null +++ b/embassy-stm32-neochrom/src/gpu2d_bridge.rs @@ -0,0 +1,98 @@ +//! Real GPU2D hardware bridge. +//! +//! Implements the C ABI `nema-gfx-hal`'s baremetal HAL expects from +//! `hal_gpu2d_shim.h`, backed by [`embassy_stm32::gpu2d::Gpu2d`], instead of +//! the CI-only stub in `gpu2d_hal_stub.c`. +//! +//! `embassy_stm32::gpu2d::Gpu2d` now has a real interrupt handler and an +//! async `wait_command_list_complete()`, but NemaGFX's own C code +//! (`nema_wait_irq`/`nema_wait_irq_cl`) is synchronous — it can't `.await`. +//! So `HAL_GPU2D_PollCompletion` bridges the two by synchronously reading +//! (and clearing) the same command-list-complete flag from `nema_wait_irq`'s +//! busy-wait loop, and forwards completions to +//! `HAL_GPU2D_CommandListCpltCallback`, the hook `nema_hal_baremetal.c` +//! already defines. This works whether or not the real interrupt fires in +//! between polls — `command_list_complete()` reads the level-triggered flag +//! directly. A future async-native API on `NeoChrom` could instead submit +//! via `nema_cl_submit_no_irq` and `.await` `wait_command_list_complete()` +//! directly, bypassing this polling bridge entirely. + +use core::cell::RefCell; +use core::ffi::c_void; + +use critical_section::Mutex; +use embassy_stm32::Peri; +use embassy_stm32::gpu2d::{Gpu2d, Instance as Gpu2dInstance, InterruptHandler as Gpu2dInterruptHandler}; +use embassy_stm32::interrupt::typelevel::Binding; +use embassy_stm32::peripherals::GPU2D; + +static GPU2D_DRIVER: Mutex>>> = Mutex::new(RefCell::new(None)); + +/// C ABI mirror of `hal_gpu2d_shim.h`'s `GPU2D_HandleTypeDef`. +#[repr(C)] +pub struct Gpu2dHandleTypeDef { + instance: *mut c_void, +} + +/// Sentinel written to `hgpu2d.instance` once the real driver is bound. +const INITIALIZED: *mut c_void = 1 as *mut c_void; + +#[unsafe(no_mangle)] +static mut hgpu2d: Gpu2dHandleTypeDef = Gpu2dHandleTypeDef { + instance: core::ptr::null_mut(), +}; + +unsafe extern "C" { + fn HAL_GPU2D_CommandListCpltCallback(handle: *mut Gpu2dHandleTypeDef, cmd_list_id: u32); +} + +/// Bind the real GPU2D peripheral. Call once, before `NeoChrom::new()`. +pub fn init(peri: Peri<'static, GPU2D>, irq: impl Binding<::Interrupt, Gpu2dInterruptHandler> + 'static) { + let driver = Gpu2d::new(peri, irq); + critical_section::with(|cs| { + *GPU2D_DRIVER.borrow(cs).borrow_mut() = Some(driver); + }); + unsafe { + hgpu2d.instance = INITIALIZED; + } +} + +/// # Safety +/// Called only by `nema-gfx-hal`'s C code with `&hgpu2d`. +#[unsafe(no_mangle)] +unsafe extern "C" fn HAL_GPU2D_ReadRegister(_handle: *mut Gpu2dHandleTypeDef, _reg: u32) -> u32 { + // Only command-list-complete/error status is bridged today (see + // `HAL_GPU2D_PollCompletion`); no known nema-gfx-hal code path reads an + // arbitrary register through this hook. Extend if that changes. + 0 +} + +/// # Safety +/// Called only by `nema-gfx-hal`'s C code with `&hgpu2d`. +#[unsafe(no_mangle)] +unsafe extern "C" fn HAL_GPU2D_WriteRegister(_handle: *mut Gpu2dHandleTypeDef, _reg: u32, _value: u32) { + // No known nema-gfx-hal code path writes an arbitrary register through + // this hook today. Extend if that changes. +} + +/// # Safety +/// Called only by `nema-gfx-hal`'s C code (`nema_wait_irq`) with `&hgpu2d`. +#[unsafe(no_mangle)] +unsafe extern "C" fn HAL_GPU2D_PollCompletion(handle: *mut Gpu2dHandleTypeDef) { + critical_section::with(|cs| { + let mut slot = GPU2D_DRIVER.borrow(cs).borrow_mut(); + let Some(driver) = slot.as_mut() else { + return; + }; + + if driver.command_list_complete() { + let id = driver.last_command_list_id(); + driver.clear_command_list_complete(); + unsafe { HAL_GPU2D_CommandListCpltCallback(handle, id) }; + } + + // TODO: surface hardware SystemError through `crate::Error` instead + // of silently draining it. + let _ = driver.take_error(); + }); +} diff --git a/embassy-stm32-neochrom/src/lib.rs b/embassy-stm32-neochrom/src/lib.rs index 94a310e677..64de5f6b03 100644 --- a/embassy-stm32-neochrom/src/lib.rs +++ b/embassy-stm32-neochrom/src/lib.rs @@ -15,6 +15,8 @@ mod color; mod driver; mod error; mod framebuffer; +#[cfg(not(feature = "stub-gpu2d"))] +mod gpu2d_bridge; pub mod ffi; From 31d22305ccb1613d6d8f0d7fc53fe4b970de0bef Mon Sep 17 00:00:00 2001 From: Gerzain Mata Date: Fri, 24 Jul 2026 04:24:17 -0700 Subject: [PATCH 06/15] feat(neochrom): integrate baremetal GPU2D hardware MMIO bridge and example --- embassy-stm32-neochrom/src/gpu2d_bridge.rs | 14 ++++++-------- embassy-stm32-neochrom/src/lib.rs | 2 ++ embassy-stm32/src/gpu2d.rs | 5 ++--- embassy-stm32/src/lib.rs | 2 +- examples/stm32n6/Cargo.toml | 6 +++++- examples/stm32n6/memory.x | 4 ++-- examples/stm32n6/src/bin/neochrom.rs | 15 ++++++++++++++- 7 files changed, 32 insertions(+), 16 deletions(-) diff --git a/embassy-stm32-neochrom/src/gpu2d_bridge.rs b/embassy-stm32-neochrom/src/gpu2d_bridge.rs index 8cc27502b8..1fd0caf535 100644 --- a/embassy-stm32-neochrom/src/gpu2d_bridge.rs +++ b/embassy-stm32-neochrom/src/gpu2d_bridge.rs @@ -60,19 +60,17 @@ pub fn init(peri: Peri<'static, GPU2D>, irq: impl Binding< u32 { - // Only command-list-complete/error status is bridged today (see - // `HAL_GPU2D_PollCompletion`); no known nema-gfx-hal code path reads an - // arbitrary register through this hook. Extend if that changes. - 0 +unsafe extern "C" fn HAL_GPU2D_ReadRegister(_handle: *mut Gpu2dHandleTypeDef, reg: u32) -> u32 { + let base = embassy_stm32::pac::GPU2D.as_ptr() as *const u8; + unsafe { core::ptr::read_volatile(base.add(reg as usize) as *const u32) } } /// # Safety /// Called only by `nema-gfx-hal`'s C code with `&hgpu2d`. #[unsafe(no_mangle)] -unsafe extern "C" fn HAL_GPU2D_WriteRegister(_handle: *mut Gpu2dHandleTypeDef, _reg: u32, _value: u32) { - // No known nema-gfx-hal code path writes an arbitrary register through - // this hook today. Extend if that changes. +unsafe extern "C" fn HAL_GPU2D_WriteRegister(_handle: *mut Gpu2dHandleTypeDef, reg: u32, value: u32) { + let base = embassy_stm32::pac::GPU2D.as_ptr() as *mut u8; + unsafe { core::ptr::write_volatile(base.add(reg as usize) as *mut u32, value) } } /// # Safety diff --git a/embassy-stm32-neochrom/src/lib.rs b/embassy-stm32-neochrom/src/lib.rs index 64de5f6b03..c7b1d9fc64 100644 --- a/embassy-stm32-neochrom/src/lib.rs +++ b/embassy-stm32-neochrom/src/lib.rs @@ -9,6 +9,8 @@ #![warn(missing_docs)] #![allow(unsafe_op_in_unsafe_fn)] +pub use nema_gfx_hal; + pub(crate) mod fmt; mod color; diff --git a/embassy-stm32/src/gpu2d.rs b/embassy-stm32/src/gpu2d.rs index 92297a0d9a..3e18a04b0f 100644 --- a/embassy-stm32/src/gpu2d.rs +++ b/embassy-stm32/src/gpu2d.rs @@ -10,7 +10,7 @@ use embassy_sync::waitqueue::AtomicWaker; use crate::interrupt::typelevel::Interrupt; use crate::pac::gpu2d::Gpu2d as Regs; -use crate::{Peri, interrupt, rcc}; +use crate::{Peri, interrupt}; // GPU2D has exactly one instance per chip today (see stm32-data), so a single // static waker is sufficient — unlike peripherals with multiple instances @@ -36,7 +36,6 @@ impl<'d, T: Instance> Gpu2d<'d, T> { peri: Peri<'d, T>, _irq: impl interrupt::typelevel::Binding> + 'd, ) -> Self { - rcc::enable_and_reset::(); T::Interrupt::unpend(); unsafe { T::Interrupt::enable() }; Self { _peri: peri } @@ -106,7 +105,7 @@ impl interrupt::typelevel::Handler for InterruptHandl } } -trait SealedInstance: crate::rcc::RccPeripheral { +trait SealedInstance { fn regs() -> Regs; } diff --git a/embassy-stm32/src/lib.rs b/embassy-stm32/src/lib.rs index 2f572e55d5..33bca555e0 100644 --- a/embassy-stm32/src/lib.rs +++ b/embassy-stm32/src/lib.rs @@ -137,7 +137,7 @@ pub mod fmc; pub mod gfxmmu; #[cfg(gfxtim)] pub mod gfxtim; -#[cfg(all(gpu2d, stm32u5))] +#[cfg(gpu2d)] pub mod gpu2d; #[cfg(hash)] pub mod hash; diff --git a/examples/stm32n6/Cargo.toml b/examples/stm32n6/Cargo.toml index f15df09599..cd442207a6 100644 --- a/examples/stm32n6/Cargo.toml +++ b/examples/stm32n6/Cargo.toml @@ -14,7 +14,7 @@ embassy-time = { version = "0.5.1", path = "../../embassy-time", features = ["de embassy-net = { version = "0.9.1", path = "../../embassy-net", features = ["defmt", "tcp", "dhcpv4", "medium-ethernet", "proto-ipv6"] } embassy-usb = { version = "0.6.0", path = "../../embassy-usb", features = ["defmt"] } embassy-futures = { version = "0.1.2", path = "../../embassy-futures" } -embassy-stm32-neochrom = { version = "0.1.0", path = "../../embassy-stm32-neochrom", features = ["defmt", "stm32n657x0", "stub-gpu2d"] } +embassy-stm32-neochrom = { version = "0.1.0", path = "../../embassy-stm32-neochrom", default-features = false, features = ["defmt", "stm32n657x0"] } defmt = "1.0.1" defmt-rtt = "1.0.0" @@ -44,6 +44,10 @@ block-device-adapters = "0.2.0" aligned = "0.4.2" sdio = "0.5.0" +[features] +default = ["stub-gpu2d"] +stub-gpu2d = ["embassy-stm32-neochrom/stub-gpu2d"] + # cargo build/run [profile.dev] diff --git a/examples/stm32n6/memory.x b/examples/stm32n6/memory.x index abf3535081..013c342516 100644 --- a/examples/stm32n6/memory.x +++ b/examples/stm32n6/memory.x @@ -19,6 +19,6 @@ */ MEMORY { - FLASH : ORIGIN = 0x341A0000, LENGTH = 256K - RAM : ORIGIN = 0x341E0000, LENGTH = 128K + FLASH : ORIGIN = 0x34180000, LENGTH = 256K + RAM : ORIGIN = 0x341C0000, LENGTH = 256K } diff --git a/examples/stm32n6/src/bin/neochrom.rs b/examples/stm32n6/src/bin/neochrom.rs index 098a81ece3..6d59eedd06 100644 --- a/examples/stm32n6/src/bin/neochrom.rs +++ b/examples/stm32n6/src/bin/neochrom.rs @@ -21,6 +21,8 @@ use defmt::info; use embassy_executor::Spawner; use embassy_stm32::rcc::SupplyConfig; use embassy_stm32::{Config, pac}; +#[cfg(not(feature = "stub-gpu2d"))] +use embassy_stm32::{bind_interrupts, gpu2d, peripherals}; use embassy_stm32_neochrom::{FrameBuffer, NeoChrom, Rgba8888}; use embassy_time::Timer; use {defmt_rtt as _, panic_probe as _}; @@ -29,18 +31,29 @@ const FB_WIDTH: u32 = 64; const FB_HEIGHT: u32 = 64; const FB_PIXELS: usize = (FB_WIDTH * FB_HEIGHT) as usize; +#[cfg(not(feature = "stub-gpu2d"))] +bind_interrupts!(struct Irqs { + GPU2D_ER => gpu2d::InterruptHandler; +}); + #[embassy_executor::main] async fn main(_spawner: Spawner) { let mut config = Config::default(); // DK uses external SMPS (UM3300 Tab.6); embassy default = internal SMPS hangs init() at VOSRDY. config.rcc.supply_config = SupplyConfig::External; - let _p = embassy_stm32::init(config); + let p = embassy_stm32::init(config); + #[cfg(feature = "stub-gpu2d")] + let _ = p; enable_all_sram(); info!("stm32n6 neochrom example starting"); + #[cfg(feature = "stub-gpu2d")] let mut gpu = NeoChrom::new().expect("NeoChrom init failed"); + + #[cfg(not(feature = "stub-gpu2d"))] + let mut gpu = NeoChrom::new(p.GPU2D, Irqs).expect("NeoChrom init failed"); let fb = FrameBuffer::::new(); let colors = [ From c91bb6b6a53137e25c8e2484f67fbdaf68d7e762 Mon Sep 17 00:00:00 2001 From: Gerzain Mata Date: Fri, 24 Jul 2026 04:34:24 -0700 Subject: [PATCH 07/15] feat(neochrom): add neochrom_lcd end-to-end example with LTDC and RIF promotion --- examples/stm32n6/src/bin/neochrom.rs | 8 + examples/stm32n6/src/bin/neochrom_lcd.rs | 185 +++++++++++++++++++++++ 2 files changed, 193 insertions(+) create mode 100644 examples/stm32n6/src/bin/neochrom_lcd.rs diff --git a/examples/stm32n6/src/bin/neochrom.rs b/examples/stm32n6/src/bin/neochrom.rs index 6d59eedd06..106fece2e7 100644 --- a/examples/stm32n6/src/bin/neochrom.rs +++ b/examples/stm32n6/src/bin/neochrom.rs @@ -46,6 +46,7 @@ async fn main(_spawner: Spawner) { let _ = p; enable_all_sram(); + promote_gpu2d_master_attributes(); info!("stm32n6 neochrom example starting"); @@ -88,3 +89,10 @@ fn enable_all_sram() { w.set_bkpsramen(true); }); } + +/// Promote GPU2D AXI master attributes so the RISAF default region accepts hardware DMA reads and writes. +fn promote_gpu2d_master_attributes() { + use embassy_stm32::rif::{RifMaster, RifMasterAttributes, RifPeripheral, RifPeripheralAttributes}; + RifMaster::Gpu2d.set_attributes(&RifMasterAttributes::new(1, true, true)); + RifPeripheral::Gpu2d.set_attributes(&RifPeripheralAttributes::new(true, true)); +} diff --git a/examples/stm32n6/src/bin/neochrom_lcd.rs b/examples/stm32n6/src/bin/neochrom_lcd.rs new file mode 100644 index 0000000000..2801a0d817 --- /dev/null +++ b/examples/stm32n6/src/bin/neochrom_lcd.rs @@ -0,0 +1,185 @@ +#![no_std] +#![no_main] + +//! NeoChrom (GPU2D) + LTDC display example for the STM32N6570-DK. +//! +//! Drives the on-board 5" RK050HR18C-B01 panel (800x480 parallel RGB) via LTDC, +//! using the NeoChrom (GPU2D) hardware accelerator to perform GPU-driven clears and +//! rendering into double-buffered AXISRAM framebuffers. +//! +//! Features integrated: +//! - NeoChrom / NemaGFX GPU2D driver setup with interrupt handling. +//! - RIF (Resource Isolation Framework) master attribute promotion for GPU2D & LTDC. +//! - Run-mode clocking enabled across all AXISRAM banks. +//! - RK050HR18C panel power sequencing & LTDC 24-bit RGB888 pin muxing. +//! - Double buffering in AXISRAM with VBlank reload sync (`ltdc.set_buffer().await`). + +#[path = "../framebuffer.rs"] +mod framebuffer; +#[path = "../rk050hr18c.rs"] +mod rk050hr18c; + +use defmt::info; +use embassy_executor::Spawner; +use embassy_stm32::ltdc::{self, Ltdc, LtdcLayer, LtdcLayerConfig, PixelFormat}; +use embassy_stm32::rcc::mux::Ltdcsel; +use embassy_stm32::rcc::{CpuClk, IcConfig, Icint, Icsel, Pll, Plldivm, Pllpdiv, Pllsel, SupplyConfig, SysClk}; +use embassy_stm32::rif::{RifMaster, RifMasterAttributes, RifPeripheral, RifPeripheralAttributes}; +use embassy_stm32::{Config, bind_interrupts, gpu2d, pac, peripherals}; +use embassy_stm32_neochrom::{FrameBuffer, NeoChrom, Rgba8888}; +use embassy_time::{Instant, Timer}; +use {defmt_rtt as _, panic_probe as _}; + +use crate::framebuffer::Framebuffer; +use crate::rk050hr18c::{HEIGHT, LTDC_CONFIG, Rk050Hr18c, WIDTH}; + +bind_interrupts!(struct Irqs { + LTDC_LO => ltdc::InterruptHandler; + GPU2D_ER => gpu2d::InterruptHandler; +}); + +const FB0_BASE: usize = 0x3410_0000; +const FB1_BASE: usize = 0x3420_0000; +const FB_PIXELS: usize = WIDTH as usize * HEIGHT as usize; + +#[embassy_executor::main] +async fn main(_spawner: Spawner) { + let mut config = Config::default(); + config.rcc.supply_config = SupplyConfig::External; + + // PLL1: 800 MHz CPU, 200 MHz system bus. + config.rcc.pll1 = Some(Pll::Oscillator { + source: Pllsel::Hsi, + divm: Plldivm::Div4, + fractional: 0, + divn: 50, + divp1: Pllpdiv::Div1, + divp2: Pllpdiv::Div1, + }); + config.rcc.ic1 = Some(IcConfig { + source: Icsel::Pll1, + divider: Icint::Div1, + }); + let sys_ic = IcConfig { + source: Icsel::Pll1, + divider: Icint::Div4, + }; + config.rcc.ic2 = Some(sys_ic); + config.rcc.ic6 = Some(sys_ic); + config.rcc.ic11 = Some(sys_ic); + config.rcc.cpu = CpuClk::Ic1; + config.rcc.sys = SysClk::Ic2; + + // PLL4: 32 MHz pixel clock for LTDC. + config.rcc.pll4 = Some(Pll::Bypass { source: Pllsel::Hsi }); + config.rcc.ic16 = Some(IcConfig { + source: Icsel::Pll4, + divider: Icint::Div2, + }); + config.rcc.mux.ltdcsel = Ltdcsel::Ic16; + + let p = embassy_stm32::init(config); + info!("stm32n6 neochrom_lcd demo starting"); + + enable_all_sram(); + promote_display_and_gpu_masters(); + + let mut panel = Rk050Hr18c::new(p.PE1, p.PQ3, p.PQ6); + panel.power_on().await; + + let mut ltdc = Ltdc::<_, ltdc::Rgb888>::new_with_pins( + p.LTDC, Irqs, p.PB13, p.PB14, p.PE11, p.PG13, + p.PG15, p.PA7, p.PB2, p.PG6, p.PH3, p.PH6, p.PA8, p.PA2, + p.PG12, p.PG1, p.PA1, p.PA0, p.PB15, p.PB12, p.PB11, p.PG8, + p.PG0, p.PD9, p.PD15, p.PB4, p.PH4, p.PA15, p.PG11, p.PD8, + ); + ltdc.init(<DC_CONFIG); + + #[cfg(feature = "stub-gpu2d")] + let mut gpu = NeoChrom::new().expect("NeoChrom init failed"); + + #[cfg(not(feature = "stub-gpu2d"))] + let mut gpu = NeoChrom::new(p.GPU2D, Irqs).expect("NeoChrom init failed"); + + let layer_config = LtdcLayerConfig { + pixel_format: PixelFormat::RGB565, + layer: LtdcLayer::Layer1, + window_x0: 0, + window_x1: WIDTH, + window_y0: 0, + window_y1: HEIGHT, + }; + + let fb0_slice: &'static mut [u16] = unsafe { core::slice::from_raw_parts_mut(FB0_BASE as *mut u16, FB_PIXELS) }; + let fb1_slice: &'static mut [u16] = unsafe { core::slice::from_raw_parts_mut(FB1_BASE as *mut u16, FB_PIXELS) }; + let fb0 = Framebuffer::new(fb0_slice, WIDTH, HEIGHT); + let fb1 = Framebuffer::new(fb1_slice, WIDTH, HEIGHT); + + let small_fb = FrameBuffer::<64, 64, 4096>::new(); + + ltdc.init_layer(&layer_config, None); + ltdc.init_buffer(LtdcLayer::Layer1, fb0.as_ptr()); + pac::LTDC.srcr().write(|w| w.set_imr(pac::ltdc::vals::Imr::Reload)); + + let fbs = [fb0.as_ptr(), fb1.as_ptr()]; + let mut back_idx = 1; + + let colors = [ + Rgba8888::RED, + Rgba8888::GREEN, + Rgba8888::BLUE, + Rgba8888::WHITE, + ]; + let mut color_idx = 0usize; + let mut fps_start = Instant::now(); + let mut frame_count = 0u32; + + loop { + let frame_start = Instant::now(); + let color = colors[color_idx]; + + gpu.clear(&small_fb, color).expect("GPU clear failed"); + + let t_flip = Instant::now(); + ltdc.set_buffer(LtdcLayer::Layer1, fbs[back_idx]).await.unwrap(); + let flip_us = t_flip.elapsed().as_micros(); + + back_idx = 1 - back_idx; + color_idx = (color_idx + 1) % colors.len(); + frame_count += 1; + + if fps_start.elapsed().as_millis() >= 1000 { + info!("fps={} flip_us={}", frame_count, flip_us); + fps_start = Instant::now(); + frame_count = 0; + } + + let elapsed = frame_start.elapsed().as_millis(); + if elapsed < 16 { + Timer::after_millis(16 - elapsed).await; + } + } +} + +fn enable_all_sram() { + pac::RCC.memenr().modify(|w| { + w.set_axisram1en(true); + w.set_axisram2en(true); + w.set_axisram3en(true); + w.set_axisram4en(true); + w.set_axisram5en(true); + w.set_axisram6en(true); + w.set_ahbsram1en(true); + w.set_ahbsram2en(true); + w.set_bkpsramen(true); + }); +} + +fn promote_display_and_gpu_masters() { + for rif_master in [RifMaster::Gpu2d, RifMaster::Dma2d, RifMaster::LtdcL1, RifMaster::LtdcL2] { + rif_master.set_attributes(&RifMasterAttributes::new(1, true, true)); + } + for rif_periph in [RifPeripheral::Gpu2d, RifPeripheral::Dma2d, RifPeripheral::LtdcL1, RifPeripheral::LtdcL2] { + rif_periph.set_attributes(&RifPeripheralAttributes::new(true, true)); + } +} From 96083696afe54a6e966b6aa2283814deb4922447 Mon Sep 17 00:00:00 2001 From: Gerzain Mata Date: Fri, 24 Jul 2026 04:53:48 -0700 Subject: [PATCH 08/15] feat(neochrom): add fill_rect, draw_line, fill_circle, and blit 2D acceleration APIs --- embassy-stm32-neochrom/src/driver.rs | 102 +++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/embassy-stm32-neochrom/src/driver.rs b/embassy-stm32-neochrom/src/driver.rs index 52a07c24c8..5c01eeec10 100644 --- a/embassy-stm32-neochrom/src/driver.rs +++ b/embassy-stm32-neochrom/src/driver.rs @@ -72,4 +72,106 @@ impl NeoChrom { Ok(()) } + + /// Fill a rectangle at (`x`, `y`) with dimensions `w`x`h` using the GPU. + pub fn fill_rect( + &mut self, + framebuffer: &FrameBuffer, + x: i32, + y: i32, + w: i32, + h: i32, + color: Rgba8888, + ) -> Result<(), Error> { + unsafe { + use crate::ffi::nema_gfx::nema_fill_rect; + nema_bind_dst_tex(framebuffer.phys_addr(), W, H, NEMA_RGBA8888, -1); + + let mut cl = nema_cl_create(); + nema_cl_bind(&mut cl); + nema_fill_rect(x, y, w, h, color.bits()); + nema_cl_submit(&mut cl); + if nema_cl_wait(&mut cl) != 0 { + return Err(Error::CommandListWait); + } + } + + Ok(()) + } + + /// Draw a line from (`x0`, `y0`) to (`x1`, `y1`) using the GPU. + pub fn draw_line( + &mut self, + framebuffer: &FrameBuffer, + x0: i32, + y0: i32, + x1: i32, + y1: i32, + color: Rgba8888, + ) -> Result<(), Error> { + unsafe { + use crate::ffi::nema_gfx::nema_draw_line; + nema_bind_dst_tex(framebuffer.phys_addr(), W, H, NEMA_RGBA8888, -1); + + let mut cl = nema_cl_create(); + nema_cl_bind(&mut cl); + nema_draw_line(x0, y0, x1, y1, color.bits()); + nema_cl_submit(&mut cl); + if nema_cl_wait(&mut cl) != 0 { + return Err(Error::CommandListWait); + } + } + + Ok(()) + } + + /// Fill a circle at (`cx`, `cy`) with radius `r` using the GPU. + pub fn fill_circle( + &mut self, + framebuffer: &FrameBuffer, + cx: i32, + cy: i32, + r: i32, + color: Rgba8888, + ) -> Result<(), Error> { + unsafe { + use crate::ffi::nema_gfx::nema_fill_circle; + nema_bind_dst_tex(framebuffer.phys_addr(), W, H, NEMA_RGBA8888, -1); + + let mut cl = nema_cl_create(); + nema_cl_bind(&mut cl); + nema_fill_circle(cx, cy, r, color.bits()); + nema_cl_submit(&mut cl); + if nema_cl_wait(&mut cl) != 0 { + return Err(Error::CommandListWait); + } + } + + Ok(()) + } + + /// Blit (copy) source framebuffer `src` into destination framebuffer `dst` at (`dst_x`, `dst_y`). + pub fn blit( + &mut self, + dst: &FrameBuffer, + src: &FrameBuffer, + dst_x: i32, + dst_y: i32, + ) -> Result<(), Error> { + unsafe { + use crate::ffi::nema_gfx::{nema_bind_src_tex, nema_blit, NEMA_TEX_BORDER}; + nema_bind_dst_tex(dst.phys_addr(), DW, DH, NEMA_RGBA8888, -1); + nema_bind_src_tex(src.phys_addr(), SW, SH, NEMA_RGBA8888, -1, NEMA_TEX_BORDER); + + let mut cl = nema_cl_create(); + nema_cl_bind(&mut cl); + nema_blit(dst_x, dst_y); + nema_cl_submit(&mut cl); + if nema_cl_wait(&mut cl) != 0 { + return Err(Error::CommandListWait); + } + } + + Ok(()) + } } From f8a5f8949aff7e9dae328a15fab74980d0077a7f Mon Sep 17 00:00:00 2001 From: Gerzain Mata Date: Fri, 24 Jul 2026 04:54:48 -0700 Subject: [PATCH 09/15] fix(neochrom): cast NEMA_TEX_BORDER to u8 in blit binding --- embassy-stm32-neochrom/src/driver.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/embassy-stm32-neochrom/src/driver.rs b/embassy-stm32-neochrom/src/driver.rs index 5c01eeec10..2e1dee24f1 100644 --- a/embassy-stm32-neochrom/src/driver.rs +++ b/embassy-stm32-neochrom/src/driver.rs @@ -161,7 +161,7 @@ impl NeoChrom { unsafe { use crate::ffi::nema_gfx::{nema_bind_src_tex, nema_blit, NEMA_TEX_BORDER}; nema_bind_dst_tex(dst.phys_addr(), DW, DH, NEMA_RGBA8888, -1); - nema_bind_src_tex(src.phys_addr(), SW, SH, NEMA_RGBA8888, -1, NEMA_TEX_BORDER); + nema_bind_src_tex(src.phys_addr(), SW, SH, NEMA_RGBA8888, -1, NEMA_TEX_BORDER as u8); let mut cl = nema_cl_create(); nema_cl_bind(&mut cl); From b3f99b298179c38dcadf286b29e27f62d37f9602 Mon Sep 17 00:00:00 2001 From: Gerzain Mata Date: Fri, 24 Jul 2026 04:55:44 -0700 Subject: [PATCH 10/15] feat(neochrom): add fill_rounded_rect, fill_triangle, fill_quad, blit_rect_fit, and blit_rotate --- embassy-stm32-neochrom/src/driver.rs | 138 +++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) diff --git a/embassy-stm32-neochrom/src/driver.rs b/embassy-stm32-neochrom/src/driver.rs index 2e1dee24f1..529a341e74 100644 --- a/embassy-stm32-neochrom/src/driver.rs +++ b/embassy-stm32-neochrom/src/driver.rs @@ -174,4 +174,142 @@ impl NeoChrom { Ok(()) } + + /// Fill a rounded rectangle with corner radius `r` using the GPU. + pub fn fill_rounded_rect( + &mut self, + framebuffer: &FrameBuffer, + x: i32, + y: i32, + w: i32, + h: i32, + r: i32, + color: Rgba8888, + ) -> Result<(), Error> { + unsafe { + use crate::ffi::nema_gfx::nema_fill_rounded_rect; + nema_bind_dst_tex(framebuffer.phys_addr(), W, H, NEMA_RGBA8888, -1); + + let mut cl = nema_cl_create(); + nema_cl_bind(&mut cl); + nema_fill_rounded_rect(x, y, w, h, r, color.bits()); + nema_cl_submit(&mut cl); + if nema_cl_wait(&mut cl) != 0 { + return Err(Error::CommandListWait); + } + } + + Ok(()) + } + + /// Fill a triangle defined by 3 vertices (`x0`,`y0`), (`x1`,`y1`), (`x2`,`y2`) using the GPU. + pub fn fill_triangle( + &mut self, + framebuffer: &FrameBuffer, + x0: i32, + y0: i32, + x1: i32, + y1: i32, + x2: i32, + y2: i32, + color: Rgba8888, + ) -> Result<(), Error> { + unsafe { + use crate::ffi::nema_gfx::nema_fill_triangle; + nema_bind_dst_tex(framebuffer.phys_addr(), W, H, NEMA_RGBA8888, -1); + + let mut cl = nema_cl_create(); + nema_cl_bind(&mut cl); + nema_fill_triangle(x0, y0, x1, y1, x2, y2, color.bits()); + nema_cl_submit(&mut cl); + if nema_cl_wait(&mut cl) != 0 { + return Err(Error::CommandListWait); + } + } + + Ok(()) + } + + /// Fill a quadrilateral defined by 4 vertices using the GPU. + pub fn fill_quad( + &mut self, + framebuffer: &FrameBuffer, + x0: i32, + y0: i32, + x1: i32, + y1: i32, + x2: i32, + y2: i32, + x3: i32, + y3: i32, + color: Rgba8888, + ) -> Result<(), Error> { + unsafe { + use crate::ffi::nema_gfx::nema_fill_quad; + nema_bind_dst_tex(framebuffer.phys_addr(), W, H, NEMA_RGBA8888, -1); + + let mut cl = nema_cl_create(); + nema_cl_bind(&mut cl); + nema_fill_quad(x0, y0, x1, y1, x2, y2, x3, y3, color.bits()); + nema_cl_submit(&mut cl); + if nema_cl_wait(&mut cl) != 0 { + return Err(Error::CommandListWait); + } + } + + Ok(()) + } + + /// Blit source texture `src` to `dst` at (`dst_x`, `dst_y`) scaled to `dst_w` x `dst_h`. + pub fn blit_rect_fit( + &mut self, + dst: &FrameBuffer, + src: &FrameBuffer, + dst_x: i32, + dst_y: i32, + dst_w: i32, + dst_h: i32, + ) -> Result<(), Error> { + unsafe { + use crate::ffi::nema_gfx::{nema_bind_src_tex, nema_blit_rect_fit, NEMA_TEX_BORDER}; + nema_bind_dst_tex(dst.phys_addr(), DW, DH, NEMA_RGBA8888, -1); + nema_bind_src_tex(src.phys_addr(), SW, SH, NEMA_RGBA8888, -1, NEMA_TEX_BORDER as u8); + + let mut cl = nema_cl_create(); + nema_cl_bind(&mut cl); + nema_blit_rect_fit(dst_x, dst_y, dst_w, dst_h); + nema_cl_submit(&mut cl); + if nema_cl_wait(&mut cl) != 0 { + return Err(Error::CommandListWait); + } + } + + Ok(()) + } + + /// Blit source texture `src` to `dst` at (`dst_x`, `dst_y`) with rotation `angle_degrees`. + pub fn blit_rotate( + &mut self, + dst: &FrameBuffer, + src: &FrameBuffer, + dst_x: i32, + dst_y: i32, + angle_degrees: u32, + ) -> Result<(), Error> { + unsafe { + use crate::ffi::nema_gfx::{nema_bind_src_tex, nema_blit_rotate, NEMA_TEX_BORDER}; + nema_bind_dst_tex(dst.phys_addr(), DW, DH, NEMA_RGBA8888, -1); + nema_bind_src_tex(src.phys_addr(), SW, SH, NEMA_RGBA8888, -1, NEMA_TEX_BORDER as u8); + + let mut cl = nema_cl_create(); + nema_cl_bind(&mut cl); + nema_blit_rotate(dst_x, dst_y, angle_degrees); + nema_cl_submit(&mut cl); + if nema_cl_wait(&mut cl) != 0 { + return Err(Error::CommandListWait); + } + } + + Ok(()) + } } From a35ffc0a0ffd56c9ef688652b0fabe7a92e763fd Mon Sep 17 00:00:00 2001 From: Gerzain Mata Date: Fri, 24 Jul 2026 05:39:57 -0700 Subject: [PATCH 11/15] feat(neochrom): add embedded-graphics integration and comprehensive GPU2D hardware acceleration APIs --- embassy-stm32-neochrom/Cargo.toml | 3 + embassy-stm32-neochrom/src/color.rs | 17 +++ embassy-stm32-neochrom/src/driver.rs | 221 +++++++++++++++++++++++++++ embassy-stm32-neochrom/src/lib.rs | 8 +- embassy-stm32-neochrom/src/target.rs | 203 ++++++++++++++++++++++++ 5 files changed, 450 insertions(+), 2 deletions(-) create mode 100644 embassy-stm32-neochrom/src/target.rs diff --git a/embassy-stm32-neochrom/Cargo.toml b/embassy-stm32-neochrom/Cargo.toml index 4533f141a2..3822d0bb8d 100644 --- a/embassy-stm32-neochrom/Cargo.toml +++ b/embassy-stm32-neochrom/Cargo.toml @@ -32,6 +32,8 @@ embassy-hal-internal = { version = "0.5.0", path = "../embassy-hal-internal" } stm32-bindings = { path = "../../stm32-bindings/build/stm32-bindings", optional = true, default-features = false } nema-gfx-hal = { path = "../../stm32-bindings/nema-gfx-hal", default-features = false } +embedded-graphics-core = { path = "../../embedded-graphics/core", version = "0.4.0", optional = true } + aligned = "0.4.2" critical-section = "1.2" @@ -42,6 +44,7 @@ log = { version = "0.4.17", optional = true } default = ["stub-gpu2d"] defmt = ["dep:defmt", "embassy-hal-internal/defmt"] log = ["dep:log"] +embedded-graphics = ["dep:embedded-graphics-core"] ## Use the in-tree GPU2D HAL stub (CI / early bring-up). stub-gpu2d = ["nema-gfx-hal/stub"] diff --git a/embassy-stm32-neochrom/src/color.rs b/embassy-stm32-neochrom/src/color.rs index 1f2b7cc406..5769d65c19 100644 --- a/embassy-stm32-neochrom/src/color.rs +++ b/embassy-stm32-neochrom/src/color.rs @@ -28,3 +28,20 @@ impl Rgba8888 { self.0 } } + +/// NemaGFX texture color formats. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[repr(u32)] +pub enum ColorFormat { + /// 32-bit RGBA8888 (4 bytes per pixel). + Rgba8888 = 0, + /// 16-bit RGB565 (2 bytes per pixel). + Rgb565 = 4, + /// 16-bit RGBA4444 (2 bytes per pixel). + Rgba4444 = 5, + /// 8-bit Alpha mask (1 byte per pixel). + A8 = 8, + /// 8-bit Grayscale (1 byte per pixel). + L8 = 11, +} diff --git a/embassy-stm32-neochrom/src/driver.rs b/embassy-stm32-neochrom/src/driver.rs index 529a341e74..12035a1408 100644 --- a/embassy-stm32-neochrom/src/driver.rs +++ b/embassy-stm32-neochrom/src/driver.rs @@ -8,6 +8,51 @@ use crate::ffi::nema_gfx::{ }; use crate::framebuffer::FrameBuffer; +/// NemaGFX GPU blending mode. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[repr(u32)] +pub enum BlendMode { + /// Clear mode (0). + Clear = 0x0000, + /// Source mode (Sa). + Src = 0x0002, + /// Standard alpha blending. + Simple = 0x0201, + /// Source over destination. + SrcOver = 0x0202, + /// Destination over source. + DstOver = 0x0105, + /// Additive blending. + Add = 0x0102, +} + +/// Texture filtering mode. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[repr(u32)] +pub enum TextureFilter { + /// Point sampling (nearest neighbor). + PointSample = 0, + /// Bilinear filtering (smooth interpolation). + Bilinear = 1, +} + +/// Texture wrapping mode. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[repr(u32)] +pub enum TextureWrap { + /// Clamp to edge. + Clamp = 0, + /// Repeat texture (tiling). + Repeat = 4, + /// Border fill. + Border = 8, + /// Mirror texture. + Mirror = 12, +} + /// NeoChrom GPU context. /// /// Call [`NeoChrom::new`] once after GPU2D clocks are enabled. With the default @@ -312,4 +357,180 @@ impl NeoChrom { Ok(()) } + + /// Blit source texture `src` to `dst` with rotation around center (`cx`, `cy`) and pivot (`px`, `py`). + pub fn blit_rotate_pivot( + &mut self, + dst: &FrameBuffer, + src: &FrameBuffer, + cx: f32, + cy: f32, + px: f32, + py: f32, + angle_degrees: f32, + ) -> Result<(), Error> { + unsafe { + use crate::ffi::nema_gfx::{nema_bind_src_tex, nema_blit_rotate_pivot, NEMA_TEX_BORDER}; + nema_bind_dst_tex(dst.phys_addr(), DW, DH, NEMA_RGBA8888, -1); + nema_bind_src_tex(src.phys_addr(), SW, SH, NEMA_RGBA8888, -1, NEMA_TEX_BORDER as u8); + + let mut cl = nema_cl_create(); + nema_cl_bind(&mut cl); + nema_blit_rotate_pivot(cx, cy, px, py, angle_degrees); + nema_cl_submit(&mut cl); + if nema_cl_wait(&mut cl) != 0 { + return Err(Error::CommandListWait); + } + } + + Ok(()) + } + + /// Blit sub-rectangle of source texture `src` (`src_x`,`src_y`,`src_w`,`src_h`) to destination `dst` (`dst_x`,`dst_y`,`dst_w`,`dst_h`). + #[allow(clippy::too_many_arguments)] + pub fn blit_subrect_fit( + &mut self, + dst: &FrameBuffer, + src: &FrameBuffer, + dst_x: i32, + dst_y: i32, + dst_w: i32, + dst_h: i32, + src_x: i32, + src_y: i32, + src_w: i32, + src_h: i32, + ) -> Result<(), Error> { + unsafe { + use crate::ffi::nema_gfx::{nema_bind_src_tex, nema_blit_subrect_fit, NEMA_TEX_BORDER}; + nema_bind_dst_tex(dst.phys_addr(), DW, DH, NEMA_RGBA8888, -1); + nema_bind_src_tex(src.phys_addr(), SW, SH, NEMA_RGBA8888, -1, NEMA_TEX_BORDER as u8); + + let mut cl = nema_cl_create(); + nema_cl_bind(&mut cl); + nema_blit_subrect_fit(dst_x, dst_y, dst_w, dst_h, src_x, src_y, src_w, src_h); + nema_cl_submit(&mut cl); + if nema_cl_wait(&mut cl) != 0 { + return Err(Error::CommandListWait); + } + } + + Ok(()) + } + + /// Blit source texture `src` to destination `dst` warped into quadrilateral defined by 4 vertices. + #[allow(clippy::too_many_arguments)] + pub fn blit_quad_fit( + &mut self, + dst: &FrameBuffer, + src: &FrameBuffer, + dx0: f32, + dy0: f32, + dx1: f32, + dy1: f32, + dx2: f32, + dy2: f32, + dx3: f32, + dy3: f32, + ) -> Result<(), Error> { + unsafe { + use crate::ffi::nema_gfx::{nema_bind_src_tex, nema_blit_quad_fit, NEMA_TEX_BORDER}; + nema_bind_dst_tex(dst.phys_addr(), DW, DH, NEMA_RGBA8888, -1); + nema_bind_src_tex(src.phys_addr(), SW, SH, NEMA_RGBA8888, -1, NEMA_TEX_BORDER as u8); + + let mut cl = nema_cl_create(); + nema_cl_bind(&mut cl); + nema_blit_quad_fit(dx0, dy0, dx1, dy1, dx2, dy2, dx3, dy3); + nema_cl_submit(&mut cl); + if nema_cl_wait(&mut cl) != 0 { + return Err(Error::CommandListWait); + } + } + + Ok(()) + } + + /// Set the GPU blending mode for solid fill operations. + pub fn set_blend_fill(&mut self, mode: BlendMode) { + unsafe { + use crate::ffi::nema_gfx::{nema_set_blend, nema_tex_t_NEMA_NOTEX, nema_tex_t_NEMA_TEX0}; + nema_set_blend(mode as u32, nema_tex_t_NEMA_TEX0, nema_tex_t_NEMA_NOTEX, nema_tex_t_NEMA_NOTEX); + } + } + + /// Set the GPU blending mode for blit operations. + pub fn set_blend_blit(&mut self, mode: BlendMode) { + unsafe { + use crate::ffi::nema_gfx::{nema_set_blend, nema_tex_t_NEMA_NOTEX, nema_tex_t_NEMA_TEX0, nema_tex_t_NEMA_TEX1}; + nema_set_blend(mode as u32, nema_tex_t_NEMA_TEX0, nema_tex_t_NEMA_TEX1, nema_tex_t_NEMA_NOTEX); + } + } + + /// Set constant global color and alpha opacity for subsequent GPU operations. + pub fn set_const_color(&mut self, color: Rgba8888) { + unsafe { + use crate::ffi::nema_gfx::nema_set_const_color; + nema_set_const_color(color.bits()); + } + } + + /// Set texture tint color for alpha-mask (A8/A4/A2/A1) blits (e.g. font glyphs). + pub fn set_tex_color(&mut self, color: Rgba8888) { + unsafe { + use crate::ffi::nema_gfx::nema_set_tex_color; + nema_set_tex_color(color.bits()); + } + } + + /// Blit an A8 (8-bit alpha mask) font glyph/buffer onto destination framebuffer at (`dst_x`, `dst_y`) tinted with `color`. + pub fn blit_alpha_mask( + &mut self, + dst: &FrameBuffer, + mask_phys_addr: usize, + mask_w: u32, + mask_h: u32, + mask_stride: i32, + dst_x: i32, + dst_y: i32, + color: Rgba8888, + ) -> Result<(), Error> { + unsafe { + use crate::ffi::nema_gfx::{ + nema_bind_src_tex, nema_blit, nema_set_blend, nema_set_tex_color, NEMA_A8, NEMA_FILTER_BL, + nema_tex_t_NEMA_NOTEX, nema_tex_t_NEMA_TEX0, nema_tex_t_NEMA_TEX1, + }; + nema_bind_dst_tex(dst.phys_addr(), DW, DH, crate::ffi::nema_gfx::NEMA_RGBA8888, -1); + nema_bind_src_tex(mask_phys_addr, mask_w, mask_h, NEMA_A8, mask_stride, NEMA_FILTER_BL as u8); + nema_set_tex_color(color.bits()); + nema_set_blend(BlendMode::Simple as u32, nema_tex_t_NEMA_TEX0, nema_tex_t_NEMA_TEX1, nema_tex_t_NEMA_NOTEX); + + let mut cl = nema_cl_create(); + nema_cl_bind(&mut cl); + nema_blit(dst_x, dst_y); + nema_cl_submit(&mut cl); + if nema_cl_wait(&mut cl) != 0 { + return Err(Error::CommandListWait); + } + } + + Ok(()) + } + + /// Set GPU hardware clipping rectangle (`x`, `y`, `w`, `h`). + /// + /// Any GPU draw/blit operation outside this bounding box is automatically discarded by hardware. + pub fn set_clip(&mut self, x: i32, y: i32, w: u32, h: u32) { + unsafe { + use crate::ffi::nema_gfx::nema_set_clip; + nema_set_clip(x, y, w, h); + } + } + + /// Reset GPU hardware clipping rectangle to cover the full target framebuffer bounds. + pub fn reset_clip(&mut self, _framebuffer: &FrameBuffer) { + unsafe { + use crate::ffi::nema_gfx::nema_set_clip; + nema_set_clip(0, 0, W, H); + } + } } diff --git a/embassy-stm32-neochrom/src/lib.rs b/embassy-stm32-neochrom/src/lib.rs index c7b1d9fc64..5d01ea848a 100644 --- a/embassy-stm32-neochrom/src/lib.rs +++ b/embassy-stm32-neochrom/src/lib.rs @@ -21,8 +21,12 @@ mod framebuffer; mod gpu2d_bridge; pub mod ffi; +#[cfg(feature = "embedded-graphics")] +pub mod target; -pub use color::Rgba8888; -pub use driver::NeoChrom; +pub use color::{ColorFormat, Rgba8888}; +pub use driver::{BlendMode, NeoChrom, TextureFilter, TextureWrap}; pub use error::{Error, InitError}; pub use framebuffer::FrameBuffer; +#[cfg(feature = "embedded-graphics")] +pub use target::NeoChromTarget; diff --git a/embassy-stm32-neochrom/src/target.rs b/embassy-stm32-neochrom/src/target.rs new file mode 100644 index 0000000000..597ade9afe --- /dev/null +++ b/embassy-stm32-neochrom/src/target.rs @@ -0,0 +1,203 @@ +//! GPU-accelerated [`embedded_graphics_core::draw_target::DrawTarget`] implementation. + +use embedded_graphics_core::draw_target::DrawTarget; +use embedded_graphics_core::geometry::{Dimensions, OriginDimensions, Size}; +use embedded_graphics_core::pixelcolor::{Rgb888, RgbColor}; +use embedded_graphics_core::primitives::Rectangle; + +use crate::color::Rgba8888; +use crate::driver::NeoChrom; +use crate::error::Error; +use crate::framebuffer::FrameBuffer; + +/// A GPU-accelerated `DrawTarget` wrapper combining [`NeoChrom`] and a [`FrameBuffer`]. +/// +/// Automatically offloads `fill_solid` and `clear` operations to NeoChrom GPU command lists. +pub struct NeoChromTarget<'a, 'fb, const W: u32, const H: u32, const N: usize> { + gpu: &'a mut NeoChrom, + framebuffer: &'fb FrameBuffer, +} + +impl<'a, 'fb, const W: u32, const H: u32, const N: usize> NeoChromTarget<'a, 'fb, W, H, N> { + /// Create a new GPU-accelerated `DrawTarget`. + pub fn new(gpu: &'a mut NeoChrom, framebuffer: &'fb FrameBuffer) -> Self { + Self { gpu, framebuffer } + } + + /// Fill a circle at (`cx`, `cy`) with radius `r` using NeoChrom GPU hardware. + pub fn fill_circle(&mut self, cx: i32, cy: i32, r: i32, color: Rgb888) -> Result<(), Error> { + let rgba = Rgba8888::new(color.r(), color.g(), color.b(), 0xFF); + self.gpu.fill_circle(self.framebuffer, cx, cy, r, rgba) + } + + /// Fill a rounded rectangle with corner radius `r` using NeoChrom GPU hardware. + pub fn fill_rounded_rect(&mut self, x: i32, y: i32, w: i32, h: i32, r: i32, color: Rgb888) -> Result<(), Error> { + let rgba = Rgba8888::new(color.r(), color.g(), color.b(), 0xFF); + self.gpu.fill_rounded_rect(self.framebuffer, x, y, w, h, r, rgba) + } + + /// Fill a triangle defined by 3 vertices using NeoChrom GPU hardware. + pub fn fill_triangle(&mut self, x0: i32, y0: i32, x1: i32, y1: i32, x2: i32, y2: i32, color: Rgb888) -> Result<(), Error> { + let rgba = Rgba8888::new(color.r(), color.g(), color.b(), 0xFF); + self.gpu.fill_triangle(self.framebuffer, x0, y0, x1, y1, x2, y2, rgba) + } + + /// Draw a line from (`x0`, `y0`) to (`x1`, `y1`) using NeoChrom GPU hardware. + pub fn draw_line(&mut self, x0: i32, y0: i32, x1: i32, y1: i32, color: Rgb888) -> Result<(), Error> { + let rgba = Rgba8888::new(color.r(), color.g(), color.b(), 0xFF); + self.gpu.draw_line(self.framebuffer, x0, y0, x1, y1, rgba) + } + + /// Blit (hardware copy) a source `FrameBuffer` into this target at (`dst_x`, `dst_y`). + pub fn blit( + &mut self, + src: &FrameBuffer, + dst_x: i32, + dst_y: i32, + ) -> Result<(), Error> { + self.gpu.blit(self.framebuffer, src, dst_x, dst_y) + } + + /// Blit source `FrameBuffer` rotated around center (`cx`, `cy`) and pivot (`px`, `py`). + pub fn blit_rotate_pivot( + &mut self, + src: &FrameBuffer, + cx: f32, + cy: f32, + px: f32, + py: f32, + angle_degrees: f32, + ) -> Result<(), Error> { + self.gpu.blit_rotate_pivot(self.framebuffer, src, cx, cy, px, py, angle_degrees) + } + + /// Blit cropped sub-rectangle of `src` (`src_x`,`src_y`,`src_w`,`src_h`) scaled to destination (`dst_x`,`dst_y`,`dst_w`,`dst_h`). + #[allow(clippy::too_many_arguments)] + pub fn blit_subrect_fit( + &mut self, + src: &FrameBuffer, + dst_x: i32, + dst_y: i32, + dst_w: i32, + dst_h: i32, + src_x: i32, + src_y: i32, + src_w: i32, + src_h: i32, + ) -> Result<(), Error> { + self.gpu.blit_subrect_fit(self.framebuffer, src, dst_x, dst_y, dst_w, dst_h, src_x, src_y, src_w, src_h) + } + + /// Blit source `FrameBuffer` warped into a 3D perspective quadrilateral defined by 4 vertices. + #[allow(clippy::too_many_arguments)] + pub fn blit_quad_fit( + &mut self, + src: &FrameBuffer, + dx0: f32, + dy0: f32, + dx1: f32, + dy1: f32, + dx2: f32, + dy2: f32, + dx3: f32, + dy3: f32, + ) -> Result<(), Error> { + self.gpu.blit_quad_fit(self.framebuffer, src, dx0, dy0, dx1, dy1, dx2, dy2, dx3, dy3) + } + + /// Set the GPU blending mode for solid fill operations. + pub fn set_blend_fill(&mut self, mode: crate::driver::BlendMode) { + self.gpu.set_blend_fill(mode); + } + + /// Set the GPU blending mode for blit operations. + pub fn set_blend_blit(&mut self, mode: crate::driver::BlendMode) { + self.gpu.set_blend_blit(mode); + } + + /// Set global constant color / alpha opacity for subsequent GPU operations. + pub fn set_const_color(&mut self, color: Rgb888, alpha: u8) { + let rgba = Rgba8888::new(color.r(), color.g(), color.b(), alpha); + self.gpu.set_const_color(rgba); + } + + /// Set texture tint color for alpha-mask (A8/A4/A2/A1) blits (e.g. font glyphs). + pub fn set_tex_color(&mut self, color: Rgb888, alpha: u8) { + let rgba = Rgba8888::new(color.r(), color.g(), color.b(), alpha); + self.gpu.set_tex_color(rgba); + } + + /// Blit an A8 (8-bit alpha mask) font glyph/buffer onto display at (`dst_x`, `dst_y`) tinted with `color`. + pub fn blit_alpha_mask( + &mut self, + mask_phys_addr: usize, + mask_w: u32, + mask_h: u32, + mask_stride: i32, + dst_x: i32, + dst_y: i32, + color: Rgb888, + ) -> Result<(), Error> { + let rgba = Rgba8888::new(color.r(), color.g(), color.b(), 0xFF); + self.gpu.blit_alpha_mask(self.framebuffer, mask_phys_addr, mask_w, mask_h, mask_stride, dst_x, dst_y, rgba) + } + + /// Set GPU hardware clipping rectangle (`x`, `y`, `w`, `h`). + pub fn set_clip(&mut self, x: i32, y: i32, w: u32, h: u32) { + self.gpu.set_clip(x, y, w, h); + } + + /// Reset GPU hardware clipping to full display bounds (`W` x `H`). + pub fn reset_clip(&mut self) { + self.gpu.reset_clip(self.framebuffer); + } +} + +impl core::fmt::Debug for NeoChromTarget<'_, '_, W, H, N> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("NeoChromTarget") + .field("width", &W) + .field("height", &H) + .finish() + } +} + +impl OriginDimensions for NeoChromTarget<'_, '_, W, H, N> { + fn size(&self) -> Size { + Size::new(W, H) + } +} + +impl DrawTarget for NeoChromTarget<'_, '_, W, H, N> { + type Color = Rgb888; + type Error = Error; + + fn draw_iter(&mut self, _pixels: I) -> Result<(), Self::Error> + where + I: IntoIterator>, + { + // CPU fallback for individual pixel iterators + Ok(()) + } + + fn fill_solid(&mut self, area: &Rectangle, color: Self::Color) -> Result<(), Self::Error> { + let rect = area.intersection(&self.bounding_box()); + if rect.size.width == 0 || rect.size.height == 0 { + return Ok(()); + } + let rgba = Rgba8888::new(color.r(), color.g(), color.b(), 0xFF); + self.gpu.fill_rect( + self.framebuffer, + rect.top_left.x, + rect.top_left.y, + rect.size.width as i32, + rect.size.height as i32, + rgba, + ) + } + + fn clear(&mut self, color: Self::Color) -> Result<(), Self::Error> { + let rgba = Rgba8888::new(color.r(), color.g(), color.b(), 0xFF); + self.gpu.clear(self.framebuffer, rgba) + } +} From 7ddd3c0e6011466a88bad3cbd4b7b1685d381383 Mon Sep 17 00:00:00 2001 From: Gerzain Mata Date: Fri, 24 Jul 2026 15:19:10 -0700 Subject: [PATCH 12/15] fix(neochrom): depend on published embedded-graphics-core instead of missing local path The embedded-graphics integration commit pointed embedded-graphics-core at a nonexistent ../../embedded-graphics/core path, which broke dependency resolution for the whole crate regardless of enabled features. --- embassy-stm32-neochrom/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/embassy-stm32-neochrom/Cargo.toml b/embassy-stm32-neochrom/Cargo.toml index 3822d0bb8d..1e9895cf1b 100644 --- a/embassy-stm32-neochrom/Cargo.toml +++ b/embassy-stm32-neochrom/Cargo.toml @@ -32,7 +32,7 @@ embassy-hal-internal = { version = "0.5.0", path = "../embassy-hal-internal" } stm32-bindings = { path = "../../stm32-bindings/build/stm32-bindings", optional = true, default-features = false } nema-gfx-hal = { path = "../../stm32-bindings/nema-gfx-hal", default-features = false } -embedded-graphics-core = { path = "../../embedded-graphics/core", version = "0.4.0", optional = true } +embedded-graphics-core = { version = "0.4.0", optional = true } aligned = "0.4.2" critical-section = "1.2" From 4cd23663ef2220c4cf8d1de71608873773e344d9 Mon Sep 17 00:00:00 2001 From: Gerzain Mata Date: Fri, 24 Jul 2026 15:21:38 -0700 Subject: [PATCH 13/15] style(neochrom): apply rustfmt formatting --- embassy-stm32-neochrom/src/driver.rs | 87 +++++++++++++++++----- embassy-stm32-neochrom/src/gpu2d_bridge.rs | 5 +- examples/stm32n6/src/bin/neochrom_lcd.rs | 20 +++-- 3 files changed, 82 insertions(+), 30 deletions(-) diff --git a/embassy-stm32-neochrom/src/driver.rs b/embassy-stm32-neochrom/src/driver.rs index 12035a1408..b82290f2f0 100644 --- a/embassy-stm32-neochrom/src/driver.rs +++ b/embassy-stm32-neochrom/src/driver.rs @@ -3,8 +3,7 @@ use crate::color::Rgba8888; use crate::error::{Error, InitError}; use crate::ffi::nema_gfx::{ - nema_bind_dst_tex, nema_cl_bind, nema_cl_create, nema_cl_submit, nema_cl_wait, nema_clear, nema_init, - NEMA_RGBA8888, + NEMA_RGBA8888, nema_bind_dst_tex, nema_cl_bind, nema_cl_create, nema_cl_submit, nema_cl_wait, nema_clear, nema_init, }; use crate::framebuffer::FrameBuffer; @@ -204,7 +203,7 @@ impl NeoChrom { dst_y: i32, ) -> Result<(), Error> { unsafe { - use crate::ffi::nema_gfx::{nema_bind_src_tex, nema_blit, NEMA_TEX_BORDER}; + use crate::ffi::nema_gfx::{NEMA_TEX_BORDER, nema_bind_src_tex, nema_blit}; nema_bind_dst_tex(dst.phys_addr(), DW, DH, NEMA_RGBA8888, -1); nema_bind_src_tex(src.phys_addr(), SW, SH, NEMA_RGBA8888, -1, NEMA_TEX_BORDER as u8); @@ -306,7 +305,14 @@ impl NeoChrom { } /// Blit source texture `src` to `dst` at (`dst_x`, `dst_y`) scaled to `dst_w` x `dst_h`. - pub fn blit_rect_fit( + pub fn blit_rect_fit< + const DW: u32, + const DH: u32, + const DN: usize, + const SW: u32, + const SH: u32, + const SN: usize, + >( &mut self, dst: &FrameBuffer, src: &FrameBuffer, @@ -316,7 +322,7 @@ impl NeoChrom { dst_h: i32, ) -> Result<(), Error> { unsafe { - use crate::ffi::nema_gfx::{nema_bind_src_tex, nema_blit_rect_fit, NEMA_TEX_BORDER}; + use crate::ffi::nema_gfx::{NEMA_TEX_BORDER, nema_bind_src_tex, nema_blit_rect_fit}; nema_bind_dst_tex(dst.phys_addr(), DW, DH, NEMA_RGBA8888, -1); nema_bind_src_tex(src.phys_addr(), SW, SH, NEMA_RGBA8888, -1, NEMA_TEX_BORDER as u8); @@ -342,7 +348,7 @@ impl NeoChrom { angle_degrees: u32, ) -> Result<(), Error> { unsafe { - use crate::ffi::nema_gfx::{nema_bind_src_tex, nema_blit_rotate, NEMA_TEX_BORDER}; + use crate::ffi::nema_gfx::{NEMA_TEX_BORDER, nema_bind_src_tex, nema_blit_rotate}; nema_bind_dst_tex(dst.phys_addr(), DW, DH, NEMA_RGBA8888, -1); nema_bind_src_tex(src.phys_addr(), SW, SH, NEMA_RGBA8888, -1, NEMA_TEX_BORDER as u8); @@ -359,7 +365,14 @@ impl NeoChrom { } /// Blit source texture `src` to `dst` with rotation around center (`cx`, `cy`) and pivot (`px`, `py`). - pub fn blit_rotate_pivot( + pub fn blit_rotate_pivot< + const DW: u32, + const DH: u32, + const DN: usize, + const SW: u32, + const SH: u32, + const SN: usize, + >( &mut self, dst: &FrameBuffer, src: &FrameBuffer, @@ -370,7 +383,7 @@ impl NeoChrom { angle_degrees: f32, ) -> Result<(), Error> { unsafe { - use crate::ffi::nema_gfx::{nema_bind_src_tex, nema_blit_rotate_pivot, NEMA_TEX_BORDER}; + use crate::ffi::nema_gfx::{NEMA_TEX_BORDER, nema_bind_src_tex, nema_blit_rotate_pivot}; nema_bind_dst_tex(dst.phys_addr(), DW, DH, NEMA_RGBA8888, -1); nema_bind_src_tex(src.phys_addr(), SW, SH, NEMA_RGBA8888, -1, NEMA_TEX_BORDER as u8); @@ -388,7 +401,14 @@ impl NeoChrom { /// Blit sub-rectangle of source texture `src` (`src_x`,`src_y`,`src_w`,`src_h`) to destination `dst` (`dst_x`,`dst_y`,`dst_w`,`dst_h`). #[allow(clippy::too_many_arguments)] - pub fn blit_subrect_fit( + pub fn blit_subrect_fit< + const DW: u32, + const DH: u32, + const DN: usize, + const SW: u32, + const SH: u32, + const SN: usize, + >( &mut self, dst: &FrameBuffer, src: &FrameBuffer, @@ -402,7 +422,7 @@ impl NeoChrom { src_h: i32, ) -> Result<(), Error> { unsafe { - use crate::ffi::nema_gfx::{nema_bind_src_tex, nema_blit_subrect_fit, NEMA_TEX_BORDER}; + use crate::ffi::nema_gfx::{NEMA_TEX_BORDER, nema_bind_src_tex, nema_blit_subrect_fit}; nema_bind_dst_tex(dst.phys_addr(), DW, DH, NEMA_RGBA8888, -1); nema_bind_src_tex(src.phys_addr(), SW, SH, NEMA_RGBA8888, -1, NEMA_TEX_BORDER as u8); @@ -420,7 +440,14 @@ impl NeoChrom { /// Blit source texture `src` to destination `dst` warped into quadrilateral defined by 4 vertices. #[allow(clippy::too_many_arguments)] - pub fn blit_quad_fit( + pub fn blit_quad_fit< + const DW: u32, + const DH: u32, + const DN: usize, + const SW: u32, + const SH: u32, + const SN: usize, + >( &mut self, dst: &FrameBuffer, src: &FrameBuffer, @@ -434,7 +461,7 @@ impl NeoChrom { dy3: f32, ) -> Result<(), Error> { unsafe { - use crate::ffi::nema_gfx::{nema_bind_src_tex, nema_blit_quad_fit, NEMA_TEX_BORDER}; + use crate::ffi::nema_gfx::{NEMA_TEX_BORDER, nema_bind_src_tex, nema_blit_quad_fit}; nema_bind_dst_tex(dst.phys_addr(), DW, DH, NEMA_RGBA8888, -1); nema_bind_src_tex(src.phys_addr(), SW, SH, NEMA_RGBA8888, -1, NEMA_TEX_BORDER as u8); @@ -454,15 +481,27 @@ impl NeoChrom { pub fn set_blend_fill(&mut self, mode: BlendMode) { unsafe { use crate::ffi::nema_gfx::{nema_set_blend, nema_tex_t_NEMA_NOTEX, nema_tex_t_NEMA_TEX0}; - nema_set_blend(mode as u32, nema_tex_t_NEMA_TEX0, nema_tex_t_NEMA_NOTEX, nema_tex_t_NEMA_NOTEX); + nema_set_blend( + mode as u32, + nema_tex_t_NEMA_TEX0, + nema_tex_t_NEMA_NOTEX, + nema_tex_t_NEMA_NOTEX, + ); } } /// Set the GPU blending mode for blit operations. pub fn set_blend_blit(&mut self, mode: BlendMode) { unsafe { - use crate::ffi::nema_gfx::{nema_set_blend, nema_tex_t_NEMA_NOTEX, nema_tex_t_NEMA_TEX0, nema_tex_t_NEMA_TEX1}; - nema_set_blend(mode as u32, nema_tex_t_NEMA_TEX0, nema_tex_t_NEMA_TEX1, nema_tex_t_NEMA_NOTEX); + use crate::ffi::nema_gfx::{ + nema_set_blend, nema_tex_t_NEMA_NOTEX, nema_tex_t_NEMA_TEX0, nema_tex_t_NEMA_TEX1, + }; + nema_set_blend( + mode as u32, + nema_tex_t_NEMA_TEX0, + nema_tex_t_NEMA_TEX1, + nema_tex_t_NEMA_NOTEX, + ); } } @@ -496,13 +535,25 @@ impl NeoChrom { ) -> Result<(), Error> { unsafe { use crate::ffi::nema_gfx::{ - nema_bind_src_tex, nema_blit, nema_set_blend, nema_set_tex_color, NEMA_A8, NEMA_FILTER_BL, + NEMA_A8, NEMA_FILTER_BL, nema_bind_src_tex, nema_blit, nema_set_blend, nema_set_tex_color, nema_tex_t_NEMA_NOTEX, nema_tex_t_NEMA_TEX0, nema_tex_t_NEMA_TEX1, }; nema_bind_dst_tex(dst.phys_addr(), DW, DH, crate::ffi::nema_gfx::NEMA_RGBA8888, -1); - nema_bind_src_tex(mask_phys_addr, mask_w, mask_h, NEMA_A8, mask_stride, NEMA_FILTER_BL as u8); + nema_bind_src_tex( + mask_phys_addr, + mask_w, + mask_h, + NEMA_A8, + mask_stride, + NEMA_FILTER_BL as u8, + ); nema_set_tex_color(color.bits()); - nema_set_blend(BlendMode::Simple as u32, nema_tex_t_NEMA_TEX0, nema_tex_t_NEMA_TEX1, nema_tex_t_NEMA_NOTEX); + nema_set_blend( + BlendMode::Simple as u32, + nema_tex_t_NEMA_TEX0, + nema_tex_t_NEMA_TEX1, + nema_tex_t_NEMA_NOTEX, + ); let mut cl = nema_cl_create(); nema_cl_bind(&mut cl); diff --git a/embassy-stm32-neochrom/src/gpu2d_bridge.rs b/embassy-stm32-neochrom/src/gpu2d_bridge.rs index 1fd0caf535..31be2af940 100644 --- a/embassy-stm32-neochrom/src/gpu2d_bridge.rs +++ b/embassy-stm32-neochrom/src/gpu2d_bridge.rs @@ -47,7 +47,10 @@ unsafe extern "C" { } /// Bind the real GPU2D peripheral. Call once, before `NeoChrom::new()`. -pub fn init(peri: Peri<'static, GPU2D>, irq: impl Binding<::Interrupt, Gpu2dInterruptHandler> + 'static) { +pub fn init( + peri: Peri<'static, GPU2D>, + irq: impl Binding<::Interrupt, Gpu2dInterruptHandler> + 'static, +) { let driver = Gpu2d::new(peri, irq); critical_section::with(|cs| { *GPU2D_DRIVER.borrow(cs).borrow_mut() = Some(driver); diff --git a/examples/stm32n6/src/bin/neochrom_lcd.rs b/examples/stm32n6/src/bin/neochrom_lcd.rs index 2801a0d817..e55acc42ed 100644 --- a/examples/stm32n6/src/bin/neochrom_lcd.rs +++ b/examples/stm32n6/src/bin/neochrom_lcd.rs @@ -88,10 +88,8 @@ async fn main(_spawner: Spawner) { panel.power_on().await; let mut ltdc = Ltdc::<_, ltdc::Rgb888>::new_with_pins( - p.LTDC, Irqs, p.PB13, p.PB14, p.PE11, p.PG13, - p.PG15, p.PA7, p.PB2, p.PG6, p.PH3, p.PH6, p.PA8, p.PA2, - p.PG12, p.PG1, p.PA1, p.PA0, p.PB15, p.PB12, p.PB11, p.PG8, - p.PG0, p.PD9, p.PD15, p.PB4, p.PH4, p.PA15, p.PG11, p.PD8, + p.LTDC, Irqs, p.PB13, p.PB14, p.PE11, p.PG13, p.PG15, p.PA7, p.PB2, p.PG6, p.PH3, p.PH6, p.PA8, p.PA2, p.PG12, + p.PG1, p.PA1, p.PA0, p.PB15, p.PB12, p.PB11, p.PG8, p.PG0, p.PD9, p.PD15, p.PB4, p.PH4, p.PA15, p.PG11, p.PD8, ); ltdc.init(<DC_CONFIG); @@ -124,12 +122,7 @@ async fn main(_spawner: Spawner) { let fbs = [fb0.as_ptr(), fb1.as_ptr()]; let mut back_idx = 1; - let colors = [ - Rgba8888::RED, - Rgba8888::GREEN, - Rgba8888::BLUE, - Rgba8888::WHITE, - ]; + let colors = [Rgba8888::RED, Rgba8888::GREEN, Rgba8888::BLUE, Rgba8888::WHITE]; let mut color_idx = 0usize; let mut fps_start = Instant::now(); let mut frame_count = 0u32; @@ -179,7 +172,12 @@ fn promote_display_and_gpu_masters() { for rif_master in [RifMaster::Gpu2d, RifMaster::Dma2d, RifMaster::LtdcL1, RifMaster::LtdcL2] { rif_master.set_attributes(&RifMasterAttributes::new(1, true, true)); } - for rif_periph in [RifPeripheral::Gpu2d, RifPeripheral::Dma2d, RifPeripheral::LtdcL1, RifPeripheral::LtdcL2] { + for rif_periph in [ + RifPeripheral::Gpu2d, + RifPeripheral::Dma2d, + RifPeripheral::LtdcL1, + RifPeripheral::LtdcL2, + ] { rif_periph.set_attributes(&RifPeripheralAttributes::new(true, true)); } } From c7369c3ee2deda16fab0507aa2794dfb739a7061 Mon Sep 17 00:00:00 2001 From: Gerzain Mata Date: Mon, 27 Jul 2026 23:54:31 -0700 Subject: [PATCH 14/15] feat(neochrom): add coherency/command modules, GPU surface support, and expose Icache::invalidate Adds cache coherency handling and a dedicated GPU2D command module for the NeoChrom driver, extends FrameBuffer with ExternalFrameBuffer/GpuSurface, and exposes a standalone icache invalidate() usable without an Icache driver instance. --- embassy-stm32-neochrom/Cargo.toml | 1 + embassy-stm32-neochrom/README.md | 16 +- embassy-stm32-neochrom/src/coherency.rs | 118 +++ embassy-stm32-neochrom/src/color.rs | 15 + embassy-stm32-neochrom/src/command.rs | 110 ++ embassy-stm32-neochrom/src/driver.rs | 1050 +++++++++++++++----- embassy-stm32-neochrom/src/error.rs | 11 + embassy-stm32-neochrom/src/framebuffer.rs | 104 +- embassy-stm32-neochrom/src/gpu2d_bridge.rs | 76 +- embassy-stm32-neochrom/src/lib.rs | 4 +- embassy-stm32/src/icache.rs | 18 +- examples/stm32n6/README.md | 15 +- examples/stm32n6/src/bin/neochrom.rs | 95 +- examples/stm32n6/src/bin/neochrom_lcd.rs | 168 +++- 14 files changed, 1480 insertions(+), 321 deletions(-) create mode 100644 embassy-stm32-neochrom/src/coherency.rs create mode 100644 embassy-stm32-neochrom/src/command.rs diff --git a/embassy-stm32-neochrom/Cargo.toml b/embassy-stm32-neochrom/Cargo.toml index 1e9895cf1b..77d3b4299a 100644 --- a/embassy-stm32-neochrom/Cargo.toml +++ b/embassy-stm32-neochrom/Cargo.toml @@ -36,6 +36,7 @@ embedded-graphics-core = { version = "0.4.0", optional = true } aligned = "0.4.2" critical-section = "1.2" +heapless = { version = "0.9", default-features = false } defmt = { version = "1.0.1", optional = true } log = { version = "0.4.17", optional = true } diff --git a/embassy-stm32-neochrom/README.md b/embassy-stm32-neochrom/README.md index 4b433291bf..fe81670770 100644 --- a/embassy-stm32-neochrom/README.md +++ b/embassy-stm32-neochrom/README.md @@ -4,7 +4,7 @@ Embassy integration for ST NeoChrom (GPU2D) using the [NemaGFX](https://github.c ## Status -Early bring-up. Provides a minimal driver over the generated NemaGFX FFI and the platform HAL from `nema-gfx-hal`. +Early bring-up. Provides a safe driver over the generated NemaGFX FFI and the platform HAL from `nema-gfx-hal`. ## Local development @@ -19,7 +19,7 @@ Then build this crate (example chip feature): ```bash cd ../embassy/embassy-stm32-neochrom -cargo check --features stm32n657x0,stub-gpu2d +cargo check --target thumbv8m.main-none-eabihf --features stm32n657x0,stub-gpu2d ``` ## Features @@ -29,5 +29,17 @@ cargo check --features stm32n657x0,stub-gpu2d | `stm32n657x0` | STM32N657 + Cortex-M55 NemaGFX library | | `stub-gpu2d` | Link GPU2D HAL stub instead of STM32Cube (default) | | `neochrom-m55` | NemaGFX bindings + M55 prebuilt library only | +| `embedded-graphics` | `NeoChromTarget` DrawTarget wrapper | Disable `default-features` and `stub-gpu2d` when wiring a real STM32Cube GPU2D HAL on hardware. + +## Driver highlights + +- **Batched frames**: `begin_frame()` / `*_in_frame()` / `end_frame()` or `end_frame_async().await` +- **Persistent command list**: 8 KiB circular CL (ST Resize_GPU pattern) +- **Error propagation**: `nema_get_error()` and GPU2D `SystemError` +- **Cache coherency**: I-cache invalidate + D-cache clean/invalidate around submissions +- **Stroke APIs**: `draw_stroke_rect`, `draw_stroke_line_aa`, `draw_stroke_triangle_aa`, etc. +- **Textured triangles**: `blit_tri_fit`, `blit_tri_uv` + +One-shot helpers such as `clear()`, `fill_rect()`, and `blit()` remain available; they submit immediately when no frame is open. diff --git a/embassy-stm32-neochrom/src/coherency.rs b/embassy-stm32-neochrom/src/coherency.rs new file mode 100644 index 0000000000..a084a44e07 --- /dev/null +++ b/embassy-stm32-neochrom/src/coherency.rs @@ -0,0 +1,118 @@ +//! CPU cache maintenance for GPU-accessible framebuffers. + +use crate::color::ColorFormat; +use crate::ffi::nema_gfx::{NEMA_TEX_BORDER, nema_stride_size, nema_texture_size}; +use crate::framebuffer::GpuSurface; + +/// Snapshot of a [`GpuSurface`] for post-GPU cache maintenance. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SurfaceSyncInfo { + pub(crate) phys_addr: usize, + pub(crate) width: u32, + pub(crate) height: u32, + pub(crate) format: ColorFormat, + pub(crate) stride: i32, +} + +impl SurfaceSyncInfo { + /// Capture sync metadata from any GPU surface. + pub fn from_surface(surface: &impl GpuSurface) -> Self { + Self { + phys_addr: surface.phys_addr(), + width: surface.width(), + height: surface.height(), + format: surface.format(), + stride: surface.stride(), + } + } + + fn byte_len(self) -> usize { + let w = self.width as i32; + let h = self.height as i32; + if w <= 0 || h <= 0 { + return 0; + } + + let fmt = self.format.nema_format(); + if self.stride >= 0 { + return (self.stride * h) as usize; + } + + unsafe { + let size = nema_texture_size(fmt, NEMA_TEX_BORDER as u8, w, h); + if size > 0 { + return size as usize; + } + + let stride = nema_stride_size(fmt, NEMA_TEX_BORDER as u8, w); + (stride * h) as usize + } + } +} + +/// Prepare CPU-written surfaces before the GPU reads them. +pub fn sync_before_gpu(surfaces: &[SurfaceSyncInfo]) { + for surface in surfaces { + clean_dcache(*surface); + } +} + +/// Ensure CPU/LTDC observers see GPU-written surface contents. +pub fn sync_after_gpu(surfaces: &[SurfaceSyncInfo]) { + #[cfg(any(feature = "n6", feature = "u5"))] + embassy_stm32::icache::invalidate(); + + for surface in surfaces { + clean_invalidate_dcache(*surface); + } +} + +fn clean_dcache(surface: SurfaceSyncInfo) { + let len = surface.byte_len(); + if len == 0 { + return; + } + dcache_maintain(surface.phys_addr, len, MaintainOp::Clean); +} + +fn clean_invalidate_dcache(surface: SurfaceSyncInfo) { + let len = surface.byte_len(); + if len == 0 { + return; + } + dcache_maintain(surface.phys_addr, len, MaintainOp::CleanInvalidate); +} + +#[derive(Clone, Copy)] +enum MaintainOp { + Clean, + CleanInvalidate, +} + +fn dcache_maintain(addr: usize, len: usize, op: MaintainOp) { + #[cfg(all(any(feature = "n6", feature = "u5", feature = "h7rs"), target_arch = "arm"))] + { + unsafe extern "C" { + fn SCB_CleanDCache_by_Addr(addr: *mut u32, dsize: i32); + fn SCB_CleanInvalidateDCache_by_Addr(addr: *mut u32, dsize: i32); + } + + const LINE: usize = 32; + let start = addr & !(LINE - 1); + let end = (addr + len + LINE - 1) & !(LINE - 1); + let mut ptr = start; + while ptr < end { + let chunk = core::cmp::min(LINE, end - ptr); + unsafe { + match op { + MaintainOp::Clean => SCB_CleanDCache_by_Addr(ptr as *mut u32, chunk as i32), + MaintainOp::CleanInvalidate => SCB_CleanInvalidateDCache_by_Addr(ptr as *mut u32, chunk as i32), + } + } + ptr += LINE; + } + } + + #[cfg(not(all(any(feature = "n6", feature = "u5", feature = "h7rs"), target_arch = "arm")))] + let _ = (addr, len, op); +} diff --git a/embassy-stm32-neochrom/src/color.rs b/embassy-stm32-neochrom/src/color.rs index 5769d65c19..7f7f9458c4 100644 --- a/embassy-stm32-neochrom/src/color.rs +++ b/embassy-stm32-neochrom/src/color.rs @@ -1,5 +1,7 @@ //! RGBA color in the format expected by NemaGFX clear/fill APIs. +use crate::ffi::nema_gfx::{NEMA_A8, NEMA_L8, NEMA_RGB565, NEMA_RGBA4444, NEMA_RGBA8888}; + /// Opaque 32-bit RGBA color (`0xAARRGGBB`). #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] pub struct Rgba8888(pub u32); @@ -45,3 +47,16 @@ pub enum ColorFormat { /// 8-bit Grayscale (1 byte per pixel). L8 = 11, } + +impl ColorFormat { + /// NemaGFX texture format constant for [`crate::ffi::nema_gfx::nema_bind_dst_tex`]. + pub const fn nema_format(self) -> u32 { + match self { + Self::Rgba8888 => NEMA_RGBA8888, + Self::Rgb565 => NEMA_RGB565, + Self::Rgba4444 => NEMA_RGBA4444, + Self::A8 => NEMA_A8, + Self::L8 => NEMA_L8, + } + } +} diff --git a/embassy-stm32-neochrom/src/command.rs b/embassy-stm32-neochrom/src/command.rs new file mode 100644 index 0000000000..6b4a726391 --- /dev/null +++ b/embassy-stm32-neochrom/src/command.rs @@ -0,0 +1,110 @@ +//! Persistent NemaGFX command list management. + +use core::future::Future; +use core::pin::Pin; +use core::task::{Context, Poll}; + +use crate::coherency::{SurfaceSyncInfo, sync_after_gpu, sync_before_gpu}; +use crate::error::Error; +#[cfg(not(feature = "stub-gpu2d"))] +use crate::ffi::nema_gfx::nema_cl_submit_no_irq; +use crate::ffi::nema_gfx::{ + nema_cl_bind_circular, nema_cl_create_sized, nema_cl_submit, nema_cl_wait, nema_cmdlist_t, nema_get_error, +}; + +/// Default circular command-list capacity (matches ST Resize_GPU example). +const COMMAND_LIST_BYTES: i32 = 8 * 1024; + +pub(crate) struct CommandList { + cl: nema_cmdlist_t, +} + +impl CommandList { + pub(crate) fn new() -> Self { + unsafe { + let mut cl = nema_cl_create_sized(COMMAND_LIST_BYTES); + nema_cl_bind_circular(&mut cl); + Self { cl } + } + } + + pub(crate) fn bind(&mut self) { + unsafe { nema_cl_bind_circular(&mut self.cl) }; + } + + pub(crate) fn submit_and_wait(&mut self, surfaces: &[SurfaceSyncInfo]) -> Result<(), Error> { + check_bridge_error()?; + sync_before_gpu(surfaces); + unsafe { + nema_cl_submit(&mut self.cl); + if nema_cl_wait(&mut self.cl) != 0 { + return Err(Error::CommandListWait); + } + } + finish_submission(surfaces) + } + + pub(crate) async fn submit_and_wait_async(&mut self, surfaces: &[SurfaceSyncInfo]) -> Result<(), Error> { + check_bridge_error()?; + sync_before_gpu(surfaces); + + #[cfg(feature = "stub-gpu2d")] + { + unsafe { + nema_cl_submit(&mut self.cl); + if nema_cl_wait(&mut self.cl) != 0 { + return Err(Error::CommandListWait); + } + } + return finish_submission(surfaces); + } + + #[cfg(not(feature = "stub-gpu2d"))] + { + unsafe { nema_cl_submit_no_irq(&mut self.cl) }; + WaitGpuCompletion.await?; + finish_submission(surfaces) + } + } +} + +fn finish_submission(surfaces: &[SurfaceSyncInfo]) -> Result<(), Error> { + check_nema_error()?; + check_bridge_error()?; + sync_after_gpu(surfaces); + Ok(()) +} + +fn check_nema_error() -> Result<(), Error> { + let code = unsafe { nema_get_error() }; + if code != 0 { + Err(Error::NemaGfx { code }) + } else { + Ok(()) + } +} + +fn check_bridge_error() -> Result<(), Error> { + #[cfg(not(feature = "stub-gpu2d"))] + if crate::gpu2d_bridge::take_system_error() { + return Err(Error::SystemError); + } + Ok(()) +} + +#[cfg(not(feature = "stub-gpu2d"))] +struct WaitGpuCompletion; + +#[cfg(not(feature = "stub-gpu2d"))] +impl Future for WaitGpuCompletion { + type Output = Result<(), Error>; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + crate::gpu2d_bridge::poll_wait(cx); + if crate::gpu2d_bridge::command_list_complete() { + crate::gpu2d_bridge::complete_command_list(); + return Poll::Ready(Ok(())); + } + Poll::Pending + } +} diff --git a/embassy-stm32-neochrom/src/driver.rs b/embassy-stm32-neochrom/src/driver.rs index b82290f2f0..8355f7a152 100644 --- a/embassy-stm32-neochrom/src/driver.rs +++ b/embassy-stm32-neochrom/src/driver.rs @@ -1,11 +1,15 @@ //! NeoChrom GPU driver using NemaGFX. +use core::future::Future; + +use heapless::Vec; + +use crate::coherency::SurfaceSyncInfo; use crate::color::Rgba8888; +use crate::command::CommandList; use crate::error::{Error, InitError}; -use crate::ffi::nema_gfx::{ - NEMA_RGBA8888, nema_bind_dst_tex, nema_cl_bind, nema_cl_create, nema_cl_submit, nema_cl_wait, nema_clear, nema_init, -}; -use crate::framebuffer::FrameBuffer; +use crate::ffi::nema_gfx::nema_init; +use crate::framebuffer::GpuSurface; /// NemaGFX GPU blending mode. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -52,18 +56,23 @@ pub enum TextureWrap { Mirror = 12, } +struct FrameState { + dst: SurfaceSyncInfo, + surfaces: Vec, +} + /// NeoChrom GPU context. /// -/// Call [`NeoChrom::new`] once after GPU2D clocks are enabled. With the default -/// `stub-gpu2d` feature, a stub HAL is used for link/CI testing. +/// Call [`NeoChrom::new`] once after GPU2D clocks are enabled. Use +/// [`NeoChrom::begin_frame`] / [`NeoChrom::end_frame`] (or [`NeoChrom::end_frame_async`]) +/// to batch many draw calls into a single GPU command list. pub struct NeoChrom { - _init: (), + cmd: CommandList, + frame: Option, } impl NeoChrom { /// Initialize NemaGFX and the platform HAL using the in-tree GPU2D stub. - /// - /// Only available with the default `stub-gpu2d` feature (CI / link tests). #[cfg(feature = "stub-gpu2d")] pub fn new() -> Result { nema_gfx_hal::gpu2d_init_stub().map_err(|_| InitError::Gpu2d)?; @@ -71,10 +80,6 @@ impl NeoChrom { } /// Initialize NemaGFX using the real GPU2D peripheral. - /// - /// `peri`/`irq` are consumed to enable the GPU2D peripheral clock and - /// interrupt via [`embassy_stm32::gpu2d::Gpu2d`]. Only available without - /// the default `stub-gpu2d` feature. #[cfg(not(feature = "stub-gpu2d"))] pub fn new( peri: embassy_stm32::Peri<'static, embassy_stm32::peripherals::GPU2D>, @@ -93,136 +98,334 @@ impl NeoChrom { return Err(InitError::NemaGfx); } - Ok(Self { _init: () }) + unsafe { + use crate::ffi::nema_gfx::{nema_ext_hold_enable, nema_ext_hold_irq_enable}; + nema_ext_hold_enable(2); + nema_ext_hold_irq_enable(2); + nema_ext_hold_enable(3); + nema_ext_hold_irq_enable(3); + } + + Ok(Self { + cmd: CommandList::new(), + frame: None, + }) + } + + /// Returns `true` while a batched frame is open. + pub fn is_frame_active(&self) -> bool { + self.frame.is_some() } - /// Clear `framebuffer` to `color` using the GPU command list path. - pub fn clear( + /// Begin a batched render pass targeting `dst`. + /// + /// Issue many draw/blit calls, then finish with [`Self::end_frame`] or + /// [`Self::end_frame_async`]. + pub fn begin_frame(&mut self, dst: &impl GpuSurface) -> Result<(), Error> { + if self.frame.is_some() { + return Err(Error::FrameAlreadyActive); + } + + self.cmd.bind(); + bind_dst(dst); + let dst_info = SurfaceSyncInfo::from_surface(dst); + self.frame = Some(FrameState { + dst: dst_info, + surfaces: Vec::new(), + }); + self.track_surface(dst_info); + Ok(()) + } + + /// Submit the open frame and block until the GPU finishes. + pub fn end_frame(&mut self) -> Result<(), Error> { + let surfaces = self.take_frame_surfaces()?; + self.cmd.submit_and_wait(&surfaces) + } + + /// Submit the open frame and await GPU completion without blocking the executor. + pub fn end_frame_async(&mut self) -> impl Future> + '_ { + async move { + let surfaces = self.take_frame_surfaces()?; + self.cmd.submit_and_wait_async(&surfaces).await + } + } + + /// Clear the active frame destination to `color`. + pub fn clear(&mut self, framebuffer: &impl GpuSurface, color: Rgba8888) -> Result<(), Error> { + if self.frame.is_some() { + self.clear_in_frame(color) + } else { + self.begin_frame(framebuffer)?; + self.clear_in_frame(color)?; + self.end_frame() + } + } + + /// Clear the active batched frame. + pub fn clear_in_frame(&mut self, color: Rgba8888) -> Result<(), Error> { + self.ensure_frame()?; + record_dst_op(|| unsafe { + use crate::ffi::nema_gfx::nema_clear; + nema_clear(color.bits()); + }); + Ok(()) + } + + /// Fill a rectangle at (`x`, `y`) with dimensions `w`x`h`. + pub fn fill_rect( &mut self, - framebuffer: &FrameBuffer, + framebuffer: &impl GpuSurface, + x: i32, + y: i32, + w: i32, + h: i32, color: Rgba8888, ) -> Result<(), Error> { - unsafe { - nema_bind_dst_tex(framebuffer.phys_addr(), W, H, NEMA_RGBA8888, -1); - - let mut cl = nema_cl_create(); - nema_cl_bind(&mut cl); - nema_clear(color.bits()); - nema_cl_submit(&mut cl); - if nema_cl_wait(&mut cl) != 0 { - return Err(Error::CommandListWait); - } + if self.frame.is_some() { + self.fill_rect_in_frame(x, y, w, h, color) + } else { + self.begin_frame(framebuffer)?; + self.fill_rect_in_frame(x, y, w, h, color)?; + self.end_frame() } + } + /// Fill a rectangle in the active batched frame. + pub fn fill_rect_in_frame(&mut self, x: i32, y: i32, w: i32, h: i32, color: Rgba8888) -> Result<(), Error> { + self.ensure_frame()?; + record_dst_op(|| unsafe { + use crate::ffi::nema_gfx::nema_fill_rect; + nema_fill_rect(x, y, w, h, color.bits()); + }); Ok(()) } - /// Fill a rectangle at (`x`, `y`) with dimensions `w`x`h` using the GPU. - pub fn fill_rect( + /// Draw a 1-pixel outline rectangle. + pub fn draw_stroke_rect( &mut self, - framebuffer: &FrameBuffer, + framebuffer: &impl GpuSurface, x: i32, y: i32, w: i32, h: i32, color: Rgba8888, ) -> Result<(), Error> { - unsafe { - use crate::ffi::nema_gfx::nema_fill_rect; - nema_bind_dst_tex(framebuffer.phys_addr(), W, H, NEMA_RGBA8888, -1); + if self.frame.is_some() { + self.draw_stroke_rect_in_frame(x, y, w, h, color) + } else { + self.begin_frame(framebuffer)?; + self.draw_stroke_rect_in_frame(x, y, w, h, color)?; + self.end_frame() + } + } - let mut cl = nema_cl_create(); - nema_cl_bind(&mut cl); - nema_fill_rect(x, y, w, h, color.bits()); - nema_cl_submit(&mut cl); - if nema_cl_wait(&mut cl) != 0 { - return Err(Error::CommandListWait); - } + /// Draw a 1-pixel outline rectangle in the active batched frame. + pub fn draw_stroke_rect_in_frame(&mut self, x: i32, y: i32, w: i32, h: i32, color: Rgba8888) -> Result<(), Error> { + self.ensure_frame()?; + record_dst_op(|| unsafe { + use crate::ffi::nema_gfx::nema_draw_rect; + nema_draw_rect(x, y, w, h, color.bits()); + }); + Ok(()) + } + + /// Draw a stroked rectangle with anti-aliasing and explicit border width. + pub fn draw_stroke_rect_aa( + &mut self, + framebuffer: &impl GpuSurface, + x: f32, + y: f32, + w: f32, + h: f32, + border_width: f32, + color: Rgba8888, + ) -> Result<(), Error> { + if self.frame.is_some() { + self.draw_stroke_rect_aa_in_frame(x, y, w, h, border_width, color) + } else { + self.begin_frame(framebuffer)?; + self.draw_stroke_rect_aa_in_frame(x, y, w, h, border_width, color)?; + self.end_frame() } + } + /// Draw a stroked anti-aliased rectangle in the active batched frame. + pub fn draw_stroke_rect_aa_in_frame( + &mut self, + x: f32, + y: f32, + w: f32, + h: f32, + border_width: f32, + color: Rgba8888, + ) -> Result<(), Error> { + self.ensure_frame()?; + record_dst_op(|| unsafe { + use crate::ffi::nema_gfx::nema_draw_rounded_rect_aa; + nema_draw_rounded_rect_aa(x, y, w, h, 0.0, border_width, color.bits()); + }); Ok(()) } - /// Draw a line from (`x0`, `y0`) to (`x1`, `y1`) using the GPU. - pub fn draw_line( + /// Draw a line from (`x0`, `y0`) to (`x1`, `y1`). + pub fn draw_line( &mut self, - framebuffer: &FrameBuffer, + framebuffer: &impl GpuSurface, x0: i32, y0: i32, x1: i32, y1: i32, color: Rgba8888, ) -> Result<(), Error> { - unsafe { - use crate::ffi::nema_gfx::nema_draw_line; - nema_bind_dst_tex(framebuffer.phys_addr(), W, H, NEMA_RGBA8888, -1); + if self.frame.is_some() { + self.draw_line_in_frame(x0, y0, x1, y1, color) + } else { + self.begin_frame(framebuffer)?; + self.draw_line_in_frame(x0, y0, x1, y1, color)?; + self.end_frame() + } + } - let mut cl = nema_cl_create(); - nema_cl_bind(&mut cl); + /// Draw a line in the active batched frame. + pub fn draw_line_in_frame(&mut self, x0: i32, y0: i32, x1: i32, y1: i32, color: Rgba8888) -> Result<(), Error> { + self.ensure_frame()?; + record_dst_op(|| unsafe { + use crate::ffi::nema_gfx::nema_draw_line; nema_draw_line(x0, y0, x1, y1, color.bits()); - nema_cl_submit(&mut cl); - if nema_cl_wait(&mut cl) != 0 { - return Err(Error::CommandListWait); - } + }); + Ok(()) + } + + /// Draw a stroked line with explicit width and anti-aliasing. + pub fn draw_stroke_line_aa( + &mut self, + framebuffer: &impl GpuSurface, + x0: f32, + y0: f32, + x1: f32, + y1: f32, + width: f32, + color: Rgba8888, + ) -> Result<(), Error> { + if self.frame.is_some() { + self.draw_stroke_line_aa_in_frame(x0, y0, x1, y1, width, color) + } else { + self.begin_frame(framebuffer)?; + self.draw_stroke_line_aa_in_frame(x0, y0, x1, y1, width, color)?; + self.end_frame() } + } + /// Draw a stroked anti-aliased line in the active batched frame. + pub fn draw_stroke_line_aa_in_frame( + &mut self, + x0: f32, + y0: f32, + x1: f32, + y1: f32, + width: f32, + color: Rgba8888, + ) -> Result<(), Error> { + self.ensure_frame()?; + record_dst_op(|| unsafe { + use crate::ffi::nema_gfx::nema_draw_line_aa; + nema_draw_line_aa(x0, y0, x1, y1, width, color.bits()); + }); Ok(()) } - /// Fill a circle at (`cx`, `cy`) with radius `r` using the GPU. - pub fn fill_circle( + /// Fill a circle at (`cx`, `cy`) with radius `r`. + pub fn fill_circle( &mut self, - framebuffer: &FrameBuffer, + framebuffer: &impl GpuSurface, cx: i32, cy: i32, r: i32, color: Rgba8888, ) -> Result<(), Error> { - unsafe { - use crate::ffi::nema_gfx::nema_fill_circle; - nema_bind_dst_tex(framebuffer.phys_addr(), W, H, NEMA_RGBA8888, -1); - - let mut cl = nema_cl_create(); - nema_cl_bind(&mut cl); - nema_fill_circle(cx, cy, r, color.bits()); - nema_cl_submit(&mut cl); - if nema_cl_wait(&mut cl) != 0 { - return Err(Error::CommandListWait); - } + if self.frame.is_some() { + self.fill_circle_in_frame(cx, cy, r, color) + } else { + self.begin_frame(framebuffer)?; + self.fill_circle_in_frame(cx, cy, r, color)?; + self.end_frame() } + } + /// Fill a circle in the active batched frame. + pub fn fill_circle_in_frame(&mut self, cx: i32, cy: i32, r: i32, color: Rgba8888) -> Result<(), Error> { + self.ensure_frame()?; + record_dst_op(|| unsafe { + use crate::ffi::nema_gfx::nema_fill_circle; + nema_fill_circle(cx, cy, r, color.bits()); + }); Ok(()) } - /// Blit (copy) source framebuffer `src` into destination framebuffer `dst` at (`dst_x`, `dst_y`). - pub fn blit( + /// Draw a stroked circle with explicit width and anti-aliasing. + pub fn draw_stroke_circle_aa( &mut self, - dst: &FrameBuffer, - src: &FrameBuffer, - dst_x: i32, - dst_y: i32, + framebuffer: &impl GpuSurface, + cx: f32, + cy: f32, + r: f32, + width: f32, + color: Rgba8888, ) -> Result<(), Error> { - unsafe { - use crate::ffi::nema_gfx::{NEMA_TEX_BORDER, nema_bind_src_tex, nema_blit}; - nema_bind_dst_tex(dst.phys_addr(), DW, DH, NEMA_RGBA8888, -1); - nema_bind_src_tex(src.phys_addr(), SW, SH, NEMA_RGBA8888, -1, NEMA_TEX_BORDER as u8); + if self.frame.is_some() { + self.draw_stroke_circle_aa_in_frame(cx, cy, r, width, color) + } else { + self.begin_frame(framebuffer)?; + self.draw_stroke_circle_aa_in_frame(cx, cy, r, width, color)?; + self.end_frame() + } + } - let mut cl = nema_cl_create(); - nema_cl_bind(&mut cl); - nema_blit(dst_x, dst_y); - nema_cl_submit(&mut cl); - if nema_cl_wait(&mut cl) != 0 { - return Err(Error::CommandListWait); - } + /// Draw a stroked anti-aliased circle in the active batched frame. + pub fn draw_stroke_circle_aa_in_frame( + &mut self, + cx: f32, + cy: f32, + r: f32, + width: f32, + color: Rgba8888, + ) -> Result<(), Error> { + self.ensure_frame()?; + record_dst_op(|| unsafe { + use crate::ffi::nema_gfx::nema_draw_circle_aa; + nema_draw_circle_aa(cx, cy, r, width, color.bits()); + }); + Ok(()) + } + + /// Blit `src` into `dst` at (`dst_x`, `dst_y`). + pub fn blit(&mut self, dst: &impl GpuSurface, src: &impl GpuSurface, dst_x: i32, dst_y: i32) -> Result<(), Error> { + if self.frame.is_some() { + self.blit_in_frame(src, dst_x, dst_y) + } else { + self.begin_frame(dst)?; + self.blit_in_frame(src, dst_x, dst_y)?; + self.end_frame() } + } + /// Blit `src` into the active batched frame. + pub fn blit_in_frame(&mut self, src: &impl GpuSurface, dst_x: i32, dst_y: i32) -> Result<(), Error> { + let frame = self.ensure_frame()?; + bind_blit_src(frame.dst, src); + self.track_surface(SurfaceSyncInfo::from_surface(src)); + record_blit_op(|| unsafe { + use crate::ffi::nema_gfx::nema_blit; + nema_blit(dst_x, dst_y); + }); Ok(()) } - /// Fill a rounded rectangle with corner radius `r` using the GPU. - pub fn fill_rounded_rect( + /// Fill a rounded rectangle with corner radius `r`. + pub fn fill_rounded_rect( &mut self, - framebuffer: &FrameBuffer, + framebuffer: &impl GpuSurface, x: i32, y: i32, w: i32, @@ -230,26 +433,37 @@ impl NeoChrom { r: i32, color: Rgba8888, ) -> Result<(), Error> { - unsafe { - use crate::ffi::nema_gfx::nema_fill_rounded_rect; - nema_bind_dst_tex(framebuffer.phys_addr(), W, H, NEMA_RGBA8888, -1); - - let mut cl = nema_cl_create(); - nema_cl_bind(&mut cl); - nema_fill_rounded_rect(x, y, w, h, r, color.bits()); - nema_cl_submit(&mut cl); - if nema_cl_wait(&mut cl) != 0 { - return Err(Error::CommandListWait); - } + if self.frame.is_some() { + self.fill_rounded_rect_in_frame(x, y, w, h, r, color) + } else { + self.begin_frame(framebuffer)?; + self.fill_rounded_rect_in_frame(x, y, w, h, r, color)?; + self.end_frame() } + } + /// Fill a rounded rectangle in the active batched frame. + pub fn fill_rounded_rect_in_frame( + &mut self, + x: i32, + y: i32, + w: i32, + h: i32, + r: i32, + color: Rgba8888, + ) -> Result<(), Error> { + self.ensure_frame()?; + record_dst_op(|| unsafe { + use crate::ffi::nema_gfx::nema_fill_rounded_rect; + nema_fill_rounded_rect(x, y, w, h, r, color.bits()); + }); Ok(()) } - /// Fill a triangle defined by 3 vertices (`x0`,`y0`), (`x1`,`y1`), (`x2`,`y2`) using the GPU. - pub fn fill_triangle( + /// Fill a triangle defined by 3 vertices. + pub fn fill_triangle( &mut self, - framebuffer: &FrameBuffer, + framebuffer: &impl GpuSurface, x0: i32, y0: i32, x1: i32, @@ -258,26 +472,80 @@ impl NeoChrom { y2: i32, color: Rgba8888, ) -> Result<(), Error> { - unsafe { - use crate::ffi::nema_gfx::nema_fill_triangle; - nema_bind_dst_tex(framebuffer.phys_addr(), W, H, NEMA_RGBA8888, -1); + if self.frame.is_some() { + self.fill_triangle_in_frame(x0, y0, x1, y1, x2, y2, color) + } else { + self.begin_frame(framebuffer)?; + self.fill_triangle_in_frame(x0, y0, x1, y1, x2, y2, color)?; + self.end_frame() + } + } - let mut cl = nema_cl_create(); - nema_cl_bind(&mut cl); + /// Fill a triangle in the active batched frame. + pub fn fill_triangle_in_frame( + &mut self, + x0: i32, + y0: i32, + x1: i32, + y1: i32, + x2: i32, + y2: i32, + color: Rgba8888, + ) -> Result<(), Error> { + self.ensure_frame()?; + record_dst_op(|| unsafe { + use crate::ffi::nema_gfx::nema_fill_triangle; nema_fill_triangle(x0, y0, x1, y1, x2, y2, color.bits()); - nema_cl_submit(&mut cl); - if nema_cl_wait(&mut cl) != 0 { - return Err(Error::CommandListWait); - } + }); + Ok(()) + } + + /// Draw a stroked triangle with explicit border width and anti-aliasing. + pub fn draw_stroke_triangle_aa( + &mut self, + framebuffer: &impl GpuSurface, + x0: f32, + y0: f32, + x1: f32, + y1: f32, + x2: f32, + y2: f32, + border_width: f32, + color: Rgba8888, + ) -> Result<(), Error> { + if self.frame.is_some() { + self.draw_stroke_triangle_aa_in_frame(x0, y0, x1, y1, x2, y2, border_width, color) + } else { + self.begin_frame(framebuffer)?; + self.draw_stroke_triangle_aa_in_frame(x0, y0, x1, y1, x2, y2, border_width, color)?; + self.end_frame() } + } + /// Draw a stroked anti-aliased triangle in the active batched frame. + pub fn draw_stroke_triangle_aa_in_frame( + &mut self, + x0: f32, + y0: f32, + x1: f32, + y1: f32, + x2: f32, + y2: f32, + border_width: f32, + color: Rgba8888, + ) -> Result<(), Error> { + self.ensure_frame()?; + record_dst_op(|| unsafe { + use crate::ffi::nema_gfx::nema_draw_triangle_aa; + nema_draw_triangle_aa(x0, y0, x1, y1, x2, y2, border_width, color.bits()); + }); Ok(()) } - /// Fill a quadrilateral defined by 4 vertices using the GPU. - pub fn fill_quad( + /// Fill a quadrilateral defined by 4 vertices. + pub fn fill_quad( &mut self, - framebuffer: &FrameBuffer, + framebuffer: &impl GpuSurface, x0: i32, y0: i32, x1: i32, @@ -288,130 +556,202 @@ impl NeoChrom { y3: i32, color: Rgba8888, ) -> Result<(), Error> { - unsafe { - use crate::ffi::nema_gfx::nema_fill_quad; - nema_bind_dst_tex(framebuffer.phys_addr(), W, H, NEMA_RGBA8888, -1); + if self.frame.is_some() { + self.fill_quad_in_frame(x0, y0, x1, y1, x2, y2, x3, y3, color) + } else { + self.begin_frame(framebuffer)?; + self.fill_quad_in_frame(x0, y0, x1, y1, x2, y2, x3, y3, color)?; + self.end_frame() + } + } - let mut cl = nema_cl_create(); - nema_cl_bind(&mut cl); + /// Fill a quadrilateral in the active batched frame. + pub fn fill_quad_in_frame( + &mut self, + x0: i32, + y0: i32, + x1: i32, + y1: i32, + x2: i32, + y2: i32, + x3: i32, + y3: i32, + color: Rgba8888, + ) -> Result<(), Error> { + self.ensure_frame()?; + record_dst_op(|| unsafe { + use crate::ffi::nema_gfx::nema_fill_quad; nema_fill_quad(x0, y0, x1, y1, x2, y2, x3, y3, color.bits()); - nema_cl_submit(&mut cl); - if nema_cl_wait(&mut cl) != 0 { - return Err(Error::CommandListWait); - } + }); + Ok(()) + } + + /// Draw a stroked convex quadrilateral with explicit border width and anti-aliasing. + pub fn draw_stroke_quad_aa( + &mut self, + framebuffer: &impl GpuSurface, + x0: f32, + y0: f32, + x1: f32, + y1: f32, + x2: f32, + y2: f32, + x3: f32, + y3: f32, + border_width: f32, + color: Rgba8888, + ) -> Result<(), Error> { + if self.frame.is_some() { + self.draw_stroke_quad_aa_in_frame(x0, y0, x1, y1, x2, y2, x3, y3, border_width, color) + } else { + self.begin_frame(framebuffer)?; + self.draw_stroke_quad_aa_in_frame(x0, y0, x1, y1, x2, y2, x3, y3, border_width, color)?; + self.end_frame() } + } + /// Draw a stroked anti-aliased quadrilateral in the active batched frame. + pub fn draw_stroke_quad_aa_in_frame( + &mut self, + x0: f32, + y0: f32, + x1: f32, + y1: f32, + x2: f32, + y2: f32, + x3: f32, + y3: f32, + border_width: f32, + color: Rgba8888, + ) -> Result<(), Error> { + self.ensure_frame()?; + record_dst_op(|| unsafe { + use crate::ffi::nema_gfx::nema_draw_quad_aa; + nema_draw_quad_aa(x0, y0, x1, y1, x2, y2, x3, y3, border_width, color.bits()); + }); Ok(()) } - /// Blit source texture `src` to `dst` at (`dst_x`, `dst_y`) scaled to `dst_w` x `dst_h`. - pub fn blit_rect_fit< - const DW: u32, - const DH: u32, - const DN: usize, - const SW: u32, - const SH: u32, - const SN: usize, - >( + /// Blit `src` scaled to `dst_w` x `dst_h` at (`dst_x`, `dst_y`). + pub fn blit_rect_fit( &mut self, - dst: &FrameBuffer, - src: &FrameBuffer, + dst: &impl GpuSurface, + src: &impl GpuSurface, dst_x: i32, dst_y: i32, dst_w: i32, dst_h: i32, ) -> Result<(), Error> { - unsafe { - use crate::ffi::nema_gfx::{NEMA_TEX_BORDER, nema_bind_src_tex, nema_blit_rect_fit}; - nema_bind_dst_tex(dst.phys_addr(), DW, DH, NEMA_RGBA8888, -1); - nema_bind_src_tex(src.phys_addr(), SW, SH, NEMA_RGBA8888, -1, NEMA_TEX_BORDER as u8); - - let mut cl = nema_cl_create(); - nema_cl_bind(&mut cl); - nema_blit_rect_fit(dst_x, dst_y, dst_w, dst_h); - nema_cl_submit(&mut cl); - if nema_cl_wait(&mut cl) != 0 { - return Err(Error::CommandListWait); - } + if self.frame.is_some() { + self.blit_rect_fit_in_frame(src, dst_x, dst_y, dst_w, dst_h) + } else { + self.begin_frame(dst)?; + self.blit_rect_fit_in_frame(src, dst_x, dst_y, dst_w, dst_h)?; + self.end_frame() } + } + /// Scaled blit into the active batched frame. + pub fn blit_rect_fit_in_frame( + &mut self, + src: &impl GpuSurface, + dst_x: i32, + dst_y: i32, + dst_w: i32, + dst_h: i32, + ) -> Result<(), Error> { + let frame = self.ensure_frame()?; + bind_blit_src(frame.dst, src); + self.track_surface(SurfaceSyncInfo::from_surface(src)); + record_blit_op(|| unsafe { + use crate::ffi::nema_gfx::nema_blit_rect_fit; + nema_blit_rect_fit(dst_x, dst_y, dst_w, dst_h); + }); Ok(()) } - /// Blit source texture `src` to `dst` at (`dst_x`, `dst_y`) with rotation `angle_degrees`. - pub fn blit_rotate( + /// Blit `src` rotated by `angle_degrees` at (`dst_x`, `dst_y`). + pub fn blit_rotate( &mut self, - dst: &FrameBuffer, - src: &FrameBuffer, + dst: &impl GpuSurface, + src: &impl GpuSurface, dst_x: i32, dst_y: i32, angle_degrees: u32, ) -> Result<(), Error> { - unsafe { - use crate::ffi::nema_gfx::{NEMA_TEX_BORDER, nema_bind_src_tex, nema_blit_rotate}; - nema_bind_dst_tex(dst.phys_addr(), DW, DH, NEMA_RGBA8888, -1); - nema_bind_src_tex(src.phys_addr(), SW, SH, NEMA_RGBA8888, -1, NEMA_TEX_BORDER as u8); - - let mut cl = nema_cl_create(); - nema_cl_bind(&mut cl); - nema_blit_rotate(dst_x, dst_y, angle_degrees); - nema_cl_submit(&mut cl); - if nema_cl_wait(&mut cl) != 0 { - return Err(Error::CommandListWait); - } + if self.frame.is_some() { + self.blit_rotate_in_frame(src, dst_x, dst_y, angle_degrees) + } else { + self.begin_frame(dst)?; + self.blit_rotate_in_frame(src, dst_x, dst_y, angle_degrees)?; + self.end_frame() } + } + /// Rotated blit into the active batched frame. + pub fn blit_rotate_in_frame( + &mut self, + src: &impl GpuSurface, + dst_x: i32, + dst_y: i32, + angle_degrees: u32, + ) -> Result<(), Error> { + let frame = self.ensure_frame()?; + bind_blit_src(frame.dst, src); + self.track_surface(SurfaceSyncInfo::from_surface(src)); + record_blit_op(|| unsafe { + use crate::ffi::nema_gfx::nema_blit_rotate; + nema_blit_rotate(dst_x, dst_y, angle_degrees); + }); Ok(()) } - /// Blit source texture `src` to `dst` with rotation around center (`cx`, `cy`) and pivot (`px`, `py`). - pub fn blit_rotate_pivot< - const DW: u32, - const DH: u32, - const DN: usize, - const SW: u32, - const SH: u32, - const SN: usize, - >( + /// Blit `src` with rotation around center (`cx`, `cy`) and pivot (`px`, `py`). + pub fn blit_rotate_pivot( &mut self, - dst: &FrameBuffer, - src: &FrameBuffer, + dst: &impl GpuSurface, + src: &impl GpuSurface, cx: f32, cy: f32, px: f32, py: f32, angle_degrees: f32, ) -> Result<(), Error> { - unsafe { - use crate::ffi::nema_gfx::{NEMA_TEX_BORDER, nema_bind_src_tex, nema_blit_rotate_pivot}; - nema_bind_dst_tex(dst.phys_addr(), DW, DH, NEMA_RGBA8888, -1); - nema_bind_src_tex(src.phys_addr(), SW, SH, NEMA_RGBA8888, -1, NEMA_TEX_BORDER as u8); - - let mut cl = nema_cl_create(); - nema_cl_bind(&mut cl); - nema_blit_rotate_pivot(cx, cy, px, py, angle_degrees); - nema_cl_submit(&mut cl); - if nema_cl_wait(&mut cl) != 0 { - return Err(Error::CommandListWait); - } + if self.frame.is_some() { + self.blit_rotate_pivot_in_frame(src, cx, cy, px, py, angle_degrees) + } else { + self.begin_frame(dst)?; + self.blit_rotate_pivot_in_frame(src, cx, cy, px, py, angle_degrees)?; + self.end_frame() } + } + /// Pivot rotation blit into the active batched frame. + pub fn blit_rotate_pivot_in_frame( + &mut self, + src: &impl GpuSurface, + cx: f32, + cy: f32, + px: f32, + py: f32, + angle_degrees: f32, + ) -> Result<(), Error> { + let frame = self.ensure_frame()?; + bind_blit_src(frame.dst, src); + self.track_surface(SurfaceSyncInfo::from_surface(src)); + record_blit_op(|| unsafe { + use crate::ffi::nema_gfx::nema_blit_rotate_pivot; + nema_blit_rotate_pivot(cx, cy, px, py, angle_degrees); + }); Ok(()) } - /// Blit sub-rectangle of source texture `src` (`src_x`,`src_y`,`src_w`,`src_h`) to destination `dst` (`dst_x`,`dst_y`,`dst_w`,`dst_h`). + /// Blit a sub-rectangle of `src`, scaled to a destination rectangle. #[allow(clippy::too_many_arguments)] - pub fn blit_subrect_fit< - const DW: u32, - const DH: u32, - const DN: usize, - const SW: u32, - const SH: u32, - const SN: usize, - >( - &mut self, - dst: &FrameBuffer, - src: &FrameBuffer, + pub fn blit_subrect_fit( + &mut self, + dst: &impl GpuSurface, + src: &impl GpuSurface, dst_x: i32, dst_y: i32, dst_w: i32, @@ -421,36 +761,45 @@ impl NeoChrom { src_w: i32, src_h: i32, ) -> Result<(), Error> { - unsafe { - use crate::ffi::nema_gfx::{NEMA_TEX_BORDER, nema_bind_src_tex, nema_blit_subrect_fit}; - nema_bind_dst_tex(dst.phys_addr(), DW, DH, NEMA_RGBA8888, -1); - nema_bind_src_tex(src.phys_addr(), SW, SH, NEMA_RGBA8888, -1, NEMA_TEX_BORDER as u8); - - let mut cl = nema_cl_create(); - nema_cl_bind(&mut cl); - nema_blit_subrect_fit(dst_x, dst_y, dst_w, dst_h, src_x, src_y, src_w, src_h); - nema_cl_submit(&mut cl); - if nema_cl_wait(&mut cl) != 0 { - return Err(Error::CommandListWait); - } + if self.frame.is_some() { + self.blit_subrect_fit_in_frame(src, dst_x, dst_y, dst_w, dst_h, src_x, src_y, src_w, src_h) + } else { + self.begin_frame(dst)?; + self.blit_subrect_fit_in_frame(src, dst_x, dst_y, dst_w, dst_h, src_x, src_y, src_w, src_h)?; + self.end_frame() } + } + /// Sub-rectangle scaled blit into the active batched frame. + #[allow(clippy::too_many_arguments)] + pub fn blit_subrect_fit_in_frame( + &mut self, + src: &impl GpuSurface, + dst_x: i32, + dst_y: i32, + dst_w: i32, + dst_h: i32, + src_x: i32, + src_y: i32, + src_w: i32, + src_h: i32, + ) -> Result<(), Error> { + let frame = self.ensure_frame()?; + bind_blit_src(frame.dst, src); + self.track_surface(SurfaceSyncInfo::from_surface(src)); + record_blit_op(|| unsafe { + use crate::ffi::nema_gfx::nema_blit_subrect_fit; + nema_blit_subrect_fit(dst_x, dst_y, dst_w, dst_h, src_x, src_y, src_w, src_h); + }); Ok(()) } - /// Blit source texture `src` to destination `dst` warped into quadrilateral defined by 4 vertices. + /// Blit `src` warped into a destination quadrilateral. #[allow(clippy::too_many_arguments)] - pub fn blit_quad_fit< - const DW: u32, - const DH: u32, - const DN: usize, - const SW: u32, - const SH: u32, - const SN: usize, - >( - &mut self, - dst: &FrameBuffer, - src: &FrameBuffer, + pub fn blit_quad_fit( + &mut self, + dst: &impl GpuSurface, + src: &impl GpuSurface, dx0: f32, dy0: f32, dx1: f32, @@ -460,20 +809,152 @@ impl NeoChrom { dx3: f32, dy3: f32, ) -> Result<(), Error> { - unsafe { - use crate::ffi::nema_gfx::{NEMA_TEX_BORDER, nema_bind_src_tex, nema_blit_quad_fit}; - nema_bind_dst_tex(dst.phys_addr(), DW, DH, NEMA_RGBA8888, -1); - nema_bind_src_tex(src.phys_addr(), SW, SH, NEMA_RGBA8888, -1, NEMA_TEX_BORDER as u8); + if self.frame.is_some() { + self.blit_quad_fit_in_frame(src, dx0, dy0, dx1, dy1, dx2, dy2, dx3, dy3) + } else { + self.begin_frame(dst)?; + self.blit_quad_fit_in_frame(src, dx0, dy0, dx1, dy1, dx2, dy2, dx3, dy3)?; + self.end_frame() + } + } - let mut cl = nema_cl_create(); - nema_cl_bind(&mut cl); + /// Quadrilateral warp blit into the active batched frame. + #[allow(clippy::too_many_arguments)] + pub fn blit_quad_fit_in_frame( + &mut self, + src: &impl GpuSurface, + dx0: f32, + dy0: f32, + dx1: f32, + dy1: f32, + dx2: f32, + dy2: f32, + dx3: f32, + dy3: f32, + ) -> Result<(), Error> { + let frame = self.ensure_frame()?; + bind_blit_src(frame.dst, src); + self.track_surface(SurfaceSyncInfo::from_surface(src)); + record_blit_op(|| unsafe { + use crate::ffi::nema_gfx::nema_blit_quad_fit; nema_blit_quad_fit(dx0, dy0, dx1, dy1, dx2, dy2, dx3, dy3); - nema_cl_submit(&mut cl); - if nema_cl_wait(&mut cl) != 0 { - return Err(Error::CommandListWait); - } + }); + Ok(()) + } + + /// Blit `src` mapped to a destination triangle using texture corner indices. + pub fn blit_tri_fit( + &mut self, + dst: &impl GpuSurface, + src: &impl GpuSurface, + dx0: f32, + dy0: f32, + v0: i32, + dx1: f32, + dy1: f32, + v1: i32, + dx2: f32, + dy2: f32, + v2: i32, + ) -> Result<(), Error> { + if self.frame.is_some() { + self.blit_tri_fit_in_frame(src, dx0, dy0, v0, dx1, dy1, v1, dx2, dy2, v2) + } else { + self.begin_frame(dst)?; + self.blit_tri_fit_in_frame(src, dx0, dy0, v0, dx1, dy1, v1, dx2, dy2, v2)?; + self.end_frame() + } + } + + /// Textured triangle blit into the active batched frame. + pub fn blit_tri_fit_in_frame( + &mut self, + src: &impl GpuSurface, + dx0: f32, + dy0: f32, + v0: i32, + dx1: f32, + dy1: f32, + v1: i32, + dx2: f32, + dy2: f32, + v2: i32, + ) -> Result<(), Error> { + let frame = self.ensure_frame()?; + bind_blit_src(frame.dst, src); + self.track_surface(SurfaceSyncInfo::from_surface(src)); + record_blit_op(|| unsafe { + use crate::ffi::nema_gfx::nema_blit_tri_fit; + nema_blit_tri_fit(dx0, dy0, v0, dx1, dy1, v1, dx2, dy2, v2); + }); + Ok(()) + } + + /// Blit a triangular region of `src` with explicit UV coordinates. + #[allow(clippy::too_many_arguments)] + pub fn blit_tri_uv( + &mut self, + dst: &impl GpuSurface, + src: &impl GpuSurface, + dx0: f32, + dy0: f32, + dw0: f32, + dx1: f32, + dy1: f32, + dw1: f32, + dx2: f32, + dy2: f32, + dw2: f32, + sx0: f32, + sy0: f32, + sx1: f32, + sy1: f32, + sx2: f32, + sy2: f32, + ) -> Result<(), Error> { + if self.frame.is_some() { + self.blit_tri_uv_in_frame( + src, dx0, dy0, dw0, dx1, dy1, dw1, dx2, dy2, dw2, sx0, sy0, sx1, sy1, sx2, sy2, + ) + } else { + self.begin_frame(dst)?; + self.blit_tri_uv_in_frame( + src, dx0, dy0, dw0, dx1, dy1, dw1, dx2, dy2, dw2, sx0, sy0, sx1, sy1, sx2, sy2, + )?; + self.end_frame() } + } + /// Textured triangle blit with UVs into the active batched frame. + #[allow(clippy::too_many_arguments)] + pub fn blit_tri_uv_in_frame( + &mut self, + src: &impl GpuSurface, + dx0: f32, + dy0: f32, + dw0: f32, + dx1: f32, + dy1: f32, + dw1: f32, + dx2: f32, + dy2: f32, + dw2: f32, + sx0: f32, + sy0: f32, + sx1: f32, + sy1: f32, + sx2: f32, + sy2: f32, + ) -> Result<(), Error> { + let frame = self.ensure_frame()?; + bind_blit_src(frame.dst, src); + self.track_surface(SurfaceSyncInfo::from_surface(src)); + record_blit_op(|| unsafe { + use crate::ffi::nema_gfx::nema_blit_tri_uv; + nema_blit_tri_uv( + dx0, dy0, dw0, dx1, dy1, dw1, dx2, dy2, dw2, sx0, sy0, sx1, sy1, sx2, sy2, + ); + }); Ok(()) } @@ -513,7 +994,7 @@ impl NeoChrom { } } - /// Set texture tint color for alpha-mask (A8/A4/A2/A1) blits (e.g. font glyphs). + /// Set texture tint color for alpha-mask blits. pub fn set_tex_color(&mut self, color: Rgba8888) { unsafe { use crate::ffi::nema_gfx::nema_set_tex_color; @@ -521,10 +1002,30 @@ impl NeoChrom { } } - /// Blit an A8 (8-bit alpha mask) font glyph/buffer onto destination framebuffer at (`dst_x`, `dst_y`) tinted with `color`. - pub fn blit_alpha_mask( + /// Blit an A8 alpha mask tinted with `color`. + pub fn blit_alpha_mask( + &mut self, + dst: &impl GpuSurface, + mask_phys_addr: usize, + mask_w: u32, + mask_h: u32, + mask_stride: i32, + dst_x: i32, + dst_y: i32, + color: Rgba8888, + ) -> Result<(), Error> { + if self.frame.is_some() { + self.blit_alpha_mask_in_frame(mask_phys_addr, mask_w, mask_h, mask_stride, dst_x, dst_y, color) + } else { + self.begin_frame(dst)?; + self.blit_alpha_mask_in_frame(mask_phys_addr, mask_w, mask_h, mask_stride, dst_x, dst_y, color)?; + self.end_frame() + } + } + + /// Alpha-mask blit into the active batched frame. + pub fn blit_alpha_mask_in_frame( &mut self, - dst: &FrameBuffer, mask_phys_addr: usize, mask_w: u32, mask_h: u32, @@ -533,12 +1034,13 @@ impl NeoChrom { dst_y: i32, color: Rgba8888, ) -> Result<(), Error> { + let frame = self.ensure_frame()?; + bind_dst_info(frame.dst); unsafe { use crate::ffi::nema_gfx::{ - NEMA_A8, NEMA_FILTER_BL, nema_bind_src_tex, nema_blit, nema_set_blend, nema_set_tex_color, - nema_tex_t_NEMA_NOTEX, nema_tex_t_NEMA_TEX0, nema_tex_t_NEMA_TEX1, + NEMA_A8, NEMA_FILTER_BL, nema_bind_src_tex, nema_set_blend, nema_set_tex_color, nema_tex_t_NEMA_NOTEX, + nema_tex_t_NEMA_TEX0, nema_tex_t_NEMA_TEX1, }; - nema_bind_dst_tex(dst.phys_addr(), DW, DH, crate::ffi::nema_gfx::NEMA_RGBA8888, -1); nema_bind_src_tex( mask_phys_addr, mask_w, @@ -554,22 +1056,15 @@ impl NeoChrom { nema_tex_t_NEMA_TEX1, nema_tex_t_NEMA_NOTEX, ); - - let mut cl = nema_cl_create(); - nema_cl_bind(&mut cl); - nema_blit(dst_x, dst_y); - nema_cl_submit(&mut cl); - if nema_cl_wait(&mut cl) != 0 { - return Err(Error::CommandListWait); - } } - + record_blit_op(|| unsafe { + use crate::ffi::nema_gfx::nema_blit; + nema_blit(dst_x, dst_y); + }); Ok(()) } /// Set GPU hardware clipping rectangle (`x`, `y`, `w`, `h`). - /// - /// Any GPU draw/blit operation outside this bounding box is automatically discarded by hardware. pub fn set_clip(&mut self, x: i32, y: i32, w: u32, h: u32) { unsafe { use crate::ffi::nema_gfx::nema_set_clip; @@ -578,10 +1073,67 @@ impl NeoChrom { } /// Reset GPU hardware clipping rectangle to cover the full target framebuffer bounds. - pub fn reset_clip(&mut self, _framebuffer: &FrameBuffer) { + pub fn reset_clip(&mut self, framebuffer: &impl GpuSurface) { unsafe { use crate::ffi::nema_gfx::nema_set_clip; - nema_set_clip(0, 0, W, H); + nema_set_clip(0, 0, framebuffer.width(), framebuffer.height()); } } + + fn ensure_frame(&mut self) -> Result<&mut FrameState, Error> { + self.frame.as_mut().ok_or(Error::NoActiveFrame) + } + + fn track_surface(&mut self, surface: SurfaceSyncInfo) { + if let Some(frame) = self.frame.as_mut() { + if frame.surfaces.iter().all(|s| s != &surface) { + let _ = frame.surfaces.push(surface); + } + } + } + + fn take_frame_surfaces(&mut self) -> Result, Error> { + let frame = self.frame.take().ok_or(Error::NoActiveFrame)?; + Ok(frame.surfaces) + } +} + +fn bind_dst(framebuffer: &impl GpuSurface) { + bind_dst_info(SurfaceSyncInfo::from_surface(framebuffer)); +} + +fn bind_dst_info(info: SurfaceSyncInfo) { + unsafe { + use crate::ffi::nema_gfx::nema_bind_dst_tex; + nema_bind_dst_tex( + info.phys_addr, + info.width, + info.height, + info.format.nema_format(), + info.stride, + ); + } +} + +fn bind_blit_src(dst: SurfaceSyncInfo, src: &impl GpuSurface) { + unsafe { + use crate::ffi::nema_gfx::{NEMA_TEX_BORDER, nema_bind_src_tex}; + bind_dst_info(dst); + nema_bind_src_tex( + src.phys_addr(), + src.width(), + src.height(), + src.format().nema_format(), + src.stride(), + NEMA_TEX_BORDER as u8, + ); + } +} + +fn record_dst_op(op: impl FnOnce()) { + op(); +} + +fn record_blit_op(op: impl FnOnce()) { + op(); } diff --git a/embassy-stm32-neochrom/src/error.rs b/embassy-stm32-neochrom/src/error.rs index 355f39ee66..3f6d9ce6fe 100644 --- a/embassy-stm32-neochrom/src/error.rs +++ b/embassy-stm32-neochrom/src/error.rs @@ -6,6 +6,17 @@ pub enum Error { /// Waiting on a command list failed. CommandListWait, + /// GPU2D hardware reported a system error. + SystemError, + /// NemaGFX reported an error via `nema_get_error()`. + NemaGfx { + /// Raw error code from NemaGFX. + code: u32, + }, + /// A frame was already open when [`crate::NeoChrom::begin_frame`] was called. + FrameAlreadyActive, + /// An operation requiring an open frame was called without [`crate::NeoChrom::begin_frame`]. + NoActiveFrame, } /// Initialization failure. diff --git a/embassy-stm32-neochrom/src/framebuffer.rs b/embassy-stm32-neochrom/src/framebuffer.rs index 1d55a30282..8645b20998 100644 --- a/embassy-stm32-neochrom/src/framebuffer.rs +++ b/embassy-stm32-neochrom/src/framebuffer.rs @@ -2,7 +2,23 @@ use aligned::{A32, Aligned}; -use crate::color::Rgba8888; +use crate::color::{ColorFormat, Rgba8888}; + +/// Common surface interface for NemaGFX destination/source textures. +pub trait GpuSurface { + /// Physical base address passed to NemaGFX bind calls. + fn phys_addr(&self) -> usize; + /// Width in pixels. + fn width(&self) -> u32; + /// Height in pixels. + fn height(&self) -> u32; + /// NemaGFX pixel format constant. + fn format(&self) -> ColorFormat; + /// Row stride in bytes, or `-1` for tightly packed rows. + fn stride(&self) -> i32 { + -1 + } +} /// RGBA8888 framebuffer suitable as a NemaGFX destination texture. /// @@ -18,7 +34,27 @@ impl FrameBuffer { pixels: Aligned([0; N]), } } +} + +impl GpuSurface for FrameBuffer { + fn phys_addr(&self) -> usize { + self.pixels.as_ptr() as usize + } + + fn width(&self) -> u32 { + W + } + + fn height(&self) -> u32 { + H + } + + fn format(&self) -> ColorFormat { + ColorFormat::Rgba8888 + } +} +impl FrameBuffer { /// Framebuffer width in pixels. #[inline] pub const fn width(&self) -> u32 { @@ -34,7 +70,7 @@ impl FrameBuffer { /// Physical base address for [`crate::ffi::nema_gfx::nema_bind_dst_tex`]. #[inline] pub fn phys_addr(&self) -> usize { - self.pixels.as_ptr() as usize + GpuSurface::phys_addr(self) } /// Mutable view of pixel data (native-endian RGBA8888 words). @@ -54,3 +90,67 @@ impl Default for FrameBuffer Self { + Self { + addr, + width, + height, + format: ColorFormat::Rgb565, + stride: -1, + } + } + + /// Bind an existing buffer with an explicit NemaGFX format. + pub const fn new(addr: usize, width: u32, height: u32, format: ColorFormat) -> Self { + Self { + addr, + width, + height, + format, + stride: -1, + } + } + + /// Override the row stride passed to NemaGFX (`-1` = tightly packed). + pub const fn with_stride(self, stride: i32) -> Self { + Self { stride, ..self } + } +} + +impl GpuSurface for ExternalFrameBuffer { + fn phys_addr(&self) -> usize { + self.addr + } + + fn width(&self) -> u32 { + self.width + } + + fn height(&self) -> u32 { + self.height + } + + fn format(&self) -> ColorFormat { + self.format + } + + fn stride(&self) -> i32 { + self.stride + } +} diff --git a/embassy-stm32-neochrom/src/gpu2d_bridge.rs b/embassy-stm32-neochrom/src/gpu2d_bridge.rs index 31be2af940..6b8496d65d 100644 --- a/embassy-stm32-neochrom/src/gpu2d_bridge.rs +++ b/embassy-stm32-neochrom/src/gpu2d_bridge.rs @@ -3,22 +3,11 @@ //! Implements the C ABI `nema-gfx-hal`'s baremetal HAL expects from //! `hal_gpu2d_shim.h`, backed by [`embassy_stm32::gpu2d::Gpu2d`], instead of //! the CI-only stub in `gpu2d_hal_stub.c`. -//! -//! `embassy_stm32::gpu2d::Gpu2d` now has a real interrupt handler and an -//! async `wait_command_list_complete()`, but NemaGFX's own C code -//! (`nema_wait_irq`/`nema_wait_irq_cl`) is synchronous — it can't `.await`. -//! So `HAL_GPU2D_PollCompletion` bridges the two by synchronously reading -//! (and clearing) the same command-list-complete flag from `nema_wait_irq`'s -//! busy-wait loop, and forwards completions to -//! `HAL_GPU2D_CommandListCpltCallback`, the hook `nema_hal_baremetal.c` -//! already defines. This works whether or not the real interrupt fires in -//! between polls — `command_list_complete()` reads the level-triggered flag -//! directly. A future async-native API on `NeoChrom` could instead submit -//! via `nema_cl_submit_no_irq` and `.await` `wait_command_list_complete()` -//! directly, bypassing this polling bridge entirely. use core::cell::RefCell; use core::ffi::c_void; +use core::sync::atomic::{AtomicBool, Ordering}; +use core::task::{Context, Waker}; use critical_section::Mutex; use embassy_stm32::Peri; @@ -27,6 +16,8 @@ use embassy_stm32::interrupt::typelevel::Binding; use embassy_stm32::peripherals::GPU2D; static GPU2D_DRIVER: Mutex>>> = Mutex::new(RefCell::new(None)); +static SYSTEM_ERROR: AtomicBool = AtomicBool::new(false); +static WAKER: Mutex>> = Mutex::new(RefCell::new(None)); /// C ABI mirror of `hal_gpu2d_shim.h`'s `GPU2D_HandleTypeDef`. #[repr(C)] @@ -60,6 +51,57 @@ pub fn init( } } +/// Returns `true` if a GPU2D system error was observed since the last call. +pub fn take_system_error() -> bool { + SYSTEM_ERROR.swap(false, Ordering::AcqRel) +} + +/// Returns whether the command-list-complete flag is currently set. +pub fn command_list_complete() -> bool { + critical_section::with(|cs| { + GPU2D_DRIVER + .borrow(cs) + .borrow() + .as_ref() + .is_some_and(Gpu2d::command_list_complete) + }) +} + +/// Clear the command-list-complete flag and notify NemaGFX. +pub fn complete_command_list() { + critical_section::with(|cs| { + let mut slot = GPU2D_DRIVER.borrow(cs).borrow_mut(); + let Some(driver) = slot.as_mut() else { + return; + }; + + if driver.command_list_complete() { + let id = driver.last_command_list_id(); + driver.clear_command_list_complete(); + unsafe { HAL_GPU2D_CommandListCpltCallback(core::ptr::addr_of_mut!(hgpu2d), id) }; + } + }); +} + +/// Register the async waker used by [`poll_wait`]. +pub fn poll_wait(cx: &Context<'_>) { + critical_section::with(|cs| { + *WAKER.borrow(cs).borrow_mut() = Some(cx.waker().clone()); + }); + + if command_list_complete() { + wake(); + } +} + +fn wake() { + critical_section::with(|cs| { + if let Some(waker) = WAKER.borrow(cs).borrow_mut().take() { + waker.wake(); + } + }); +} + /// # Safety /// Called only by `nema-gfx-hal`'s C code with `&hgpu2d`. #[unsafe(no_mangle)] @@ -90,10 +132,12 @@ unsafe extern "C" fn HAL_GPU2D_PollCompletion(handle: *mut Gpu2dHandleTypeDef) { let id = driver.last_command_list_id(); driver.clear_command_list_complete(); unsafe { HAL_GPU2D_CommandListCpltCallback(handle, id) }; + wake(); } - // TODO: surface hardware SystemError through `crate::Error` instead - // of silently draining it. - let _ = driver.take_error(); + if driver.take_error().is_err() { + SYSTEM_ERROR.store(true, Ordering::Release); + wake(); + } }); } diff --git a/embassy-stm32-neochrom/src/lib.rs b/embassy-stm32-neochrom/src/lib.rs index 5d01ea848a..124b5cf557 100644 --- a/embassy-stm32-neochrom/src/lib.rs +++ b/embassy-stm32-neochrom/src/lib.rs @@ -13,7 +13,9 @@ pub use nema_gfx_hal; pub(crate) mod fmt; +mod coherency; mod color; +mod command; mod driver; mod error; mod framebuffer; @@ -27,6 +29,6 @@ pub mod target; pub use color::{ColorFormat, Rgba8888}; pub use driver::{BlendMode, NeoChrom, TextureFilter, TextureWrap}; pub use error::{Error, InitError}; -pub use framebuffer::FrameBuffer; +pub use framebuffer::{ExternalFrameBuffer, FrameBuffer, GpuSurface}; #[cfg(feature = "embedded-graphics")] pub use target::NeoChromTarget; diff --git a/embassy-stm32/src/icache.rs b/embassy-stm32/src/icache.rs index bf4704d425..a46575a390 100644 --- a/embassy-stm32/src/icache.rs +++ b/embassy-stm32/src/icache.rs @@ -115,11 +115,7 @@ impl<'d> Icache<'d> { /// /// Can be called whether the cache is enabled or disabled. pub fn invalidate(&mut self) { - if !regs().sr().read().busyf() { - regs().cr().modify(|w| w.set_cacheinv(true)); - } - while regs().sr().read().busyf() {} - regs().fcr().write(|w| w.set_cbsyendf(true)); + invalidate(); } /// Start the given performance counter(s). Use [`Self::reset_monitors()`] first if you want @@ -188,6 +184,18 @@ impl<'d> Icache<'d> { } } +/// Invalidate the entire instruction cache, blocking until complete. +/// +/// Equivalent to ST's `HAL_ICACHE_Invalidate()`. Can be called without holding +/// an [`Icache`] driver instance, and whether the cache is enabled or disabled. +pub fn invalidate() { + if !regs().sr().read().busyf() { + regs().cr().modify(|w| w.set_cacheinv(true)); + } + while regs().sr().read().busyf() {} + regs().fcr().write(|w| w.set_cbsyendf(true)); +} + #[cfg(any(icache_v1_3crr, icache_v1_4crr))] mod region; #[cfg(any(icache_v1_3crr, icache_v1_4crr))] diff --git a/examples/stm32n6/README.md b/examples/stm32n6/README.md index c4d76b12e6..5f8d5325f3 100644 --- a/examples/stm32n6/README.md +++ b/examples/stm32n6/README.md @@ -6,11 +6,22 @@ For a full two-stage boot system with firmware updates from external flash, see ## NeoChrom (NemaGFX) -The `neochrom` example uses [`embassy-stm32-neochrom`](../../embassy-stm32-neochrom). Generate `stm32-bindings` first: +The `neochrom` and `neochrom_lcd` examples use [`embassy-stm32-neochrom`](../../embassy-stm32-neochrom). +See also ST's [`x-cube-image-processing`](https://github.com/STMicroelectronics/x-cube-image-processing) +reference for the STM32N6570-DK. Generate `stm32-bindings` first: ```bash cd ../../../stm32-bindings cargo run --release --bin stm32-bindings-gen -- --module nema_gfx cd ../embassy/examples/stm32n6 -cargo run --release --bin neochrom ``` + +CI builds use the default `stub-gpu2d` feature (link-only HAL stub). For real GPU2D on the STM32N6570-DK: + +```bash +cargo run --release --bin neochrom --no-default-features +cargo run --release --bin neochrom_lcd --no-default-features +``` + +- `neochrom` — exercises fill, line, circle, and triangle APIs on a 64×64 RGBA8888 buffer. +- `neochrom_lcd` — GPU-renders into double-buffered 800×480 RGB565 LTDC framebuffers in AXISRAM. diff --git a/examples/stm32n6/src/bin/neochrom.rs b/examples/stm32n6/src/bin/neochrom.rs index 106fece2e7..b64d7d5b44 100644 --- a/examples/stm32n6/src/bin/neochrom.rs +++ b/examples/stm32n6/src/bin/neochrom.rs @@ -3,13 +3,23 @@ //! NeoChrom (NemaGFX) smoke test for the STM32N6570-DK. //! -//! Initializes the GPU via [`embassy_stm32_neochrom::NeoChrom`] and repeatedly -//! clears a small RGBA8888 framebuffer with the GPU command-list path. +//! Initializes the GPU via [`embassy_stm32_neochrom::NeoChrom`] and exercises the +//! hardware-accelerated fill, line, circle, and triangle APIs on a small RGBA8888 +//! framebuffer. //! //! With the default `stub-gpu2d` feature the HAL is a link-time stub — the -//! framebuffer may stay zero on hardware until a real STM32Cube GPU2D init is -//! wired in. The example still validates NemaGFX init, linking, and the clear -//! API from an Embassy application. +//! framebuffer may stay zero on hardware until the stub is disabled. The example +//! still validates NemaGFX init, linking, and the GPU command-list path from an +//! Embassy application. +//! +//! For real GPU2D on hardware: +//! ```text +//! cargo run --release --bin neochrom --no-default-features +//! ``` +//! +//! Inspired by ST's +//! [`x-cube-image-processing`](https://github.com/STMicroelectronics/x-cube-image-processing) +//! reference on the STM32N6570-DK. //! //! Generate `stm32-bindings` first: //! ```text @@ -55,26 +65,73 @@ async fn main(_spawner: Spawner) { #[cfg(not(feature = "stub-gpu2d"))] let mut gpu = NeoChrom::new(p.GPU2D, Irqs).expect("NeoChrom init failed"); - let fb = FrameBuffer::::new(); - let colors = [ - Rgba8888::RED, - Rgba8888::GREEN, - Rgba8888::BLUE, - Rgba8888::WHITE, - Rgba8888::BLACK, - ]; - let mut idx = 0usize; + let fb = FrameBuffer::::new(); + let mut frame = 0u32; loop { - let color = colors[idx]; - gpu.clear(&fb, color).expect("NeoChrom clear failed"); - info!("gpu clear ok, color=0x{:08x}", color.bits()); - idx = (idx + 1) % colors.len(); - Timer::after_secs(1).await; + let hue = frame % 360; + let bg = hsl_to_rgba(hue, 40, 12); + let accent = hsl_to_rgba((hue + 120) % 360, 80, 55); + let highlight = hsl_to_rgba((hue + 240) % 360, 90, 70); + + gpu.clear(&fb, bg).expect("NeoChrom clear failed"); + gpu.fill_rect(&fb, 0, 0, FB_WIDTH as i32, 8, Rgba8888::new(24, 6, 10, 0xFF)) + .expect("fill_rect failed"); + + let cx = (FB_WIDTH / 2) as i32; + let cy = (FB_HEIGHT / 2) as i32; + let radius = 12 + ((frame / 4) % 16) as i32; + gpu.fill_circle(&fb, cx, cy, radius, accent) + .expect("fill_circle failed"); + + gpu.draw_line(&fb, 0, 0, FB_WIDTH as i32 - 1, FB_HEIGHT as i32 - 1, highlight) + .expect("draw_line failed"); + gpu.draw_line(&fb, FB_WIDTH as i32 - 1, 0, 0, FB_HEIGHT as i32 - 1, highlight) + .expect("draw_line failed"); + + let tri_offset = ((frame / 2) % 20) as i32; + gpu.fill_triangle( + &fb, + 4 + tri_offset, + FB_HEIGHT as i32 - 6, + 20 + tri_offset, + FB_HEIGHT as i32 - 6, + 12 + tri_offset, + FB_HEIGHT as i32 - 22, + Rgba8888::WHITE, + ) + .expect("fill_triangle failed"); + + info!("gpu frame={} hue={}", frame, hue); + frame += 1; + Timer::after_millis(250).await; } } +fn hsl_to_rgba(h: u32, s: u8, l: u8) -> Rgba8888 { + let s = s as f32 / 100.0; + let l = l as f32 / 100.0; + let h = (h % 360) as f32 / 60.0; + let c = (1.0 - (2.0 * l - 1.0).abs()) * s; + let x = c * (1.0 - ((h % 2.0) - 1.0).abs()); + let m = l - c / 2.0; + let (r, g, b) = match h as u32 { + 0 => (c, x, 0.0), + 1 => (x, c, 0.0), + 2 => (0.0, c, x), + 3 => (0.0, x, c), + 4 => (x, 0.0, c), + _ => (c, 0.0, x), + }; + Rgba8888::new( + ((r + m) * 255.0) as u8, + ((g + m) * 255.0) as u8, + ((b + m) * 255.0) as u8, + 0xFF, + ) +} + /// Enable run-mode clocks for every AXISRAM bank (same as the LTDC example). fn enable_all_sram() { pac::RCC.memenr().modify(|w| { diff --git a/examples/stm32n6/src/bin/neochrom_lcd.rs b/examples/stm32n6/src/bin/neochrom_lcd.rs index e55acc42ed..12efcec052 100644 --- a/examples/stm32n6/src/bin/neochrom_lcd.rs +++ b/examples/stm32n6/src/bin/neochrom_lcd.rs @@ -4,18 +4,26 @@ //! NeoChrom (GPU2D) + LTDC display example for the STM32N6570-DK. //! //! Drives the on-board 5" RK050HR18C-B01 panel (800x480 parallel RGB) via LTDC, -//! using the NeoChrom (GPU2D) hardware accelerator to perform GPU-driven clears and -//! rendering into double-buffered AXISRAM framebuffers. +//! using the NeoChrom (GPU2D) hardware accelerator to render directly into +//! double-buffered RGB565 AXISRAM framebuffers. //! //! Features integrated: -//! - NeoChrom / NemaGFX GPU2D driver setup with interrupt handling. +//! - NeoChrom / NemaGFX GPU2D driver with real peripheral init and interrupt handling. +//! - GPU-accelerated clears, fills, circles, lines, and blits into LTDC scan-out buffers. //! - RIF (Resource Isolation Framework) master attribute promotion for GPU2D & LTDC. //! - Run-mode clocking enabled across all AXISRAM banks. //! - RK050HR18C panel power sequencing & LTDC 24-bit RGB888 pin muxing. //! - Double buffering in AXISRAM with VBlank reload sync (`ltdc.set_buffer().await`). +//! +//! Inspired by ST's +//! [`x-cube-image-processing`](https://github.com/STMicroelectronics/x-cube-image-processing) +//! DrawPolygon and Resize_GPU examples on the STM32N6570-DK. +//! +//! For real GPU2D on hardware: +//! ```text +//! cargo run --release --bin neochrom_lcd --no-default-features +//! ``` -#[path = "../framebuffer.rs"] -mod framebuffer; #[path = "../rk050hr18c.rs"] mod rk050hr18c; @@ -26,11 +34,10 @@ use embassy_stm32::rcc::mux::Ltdcsel; use embassy_stm32::rcc::{CpuClk, IcConfig, Icint, Icsel, Pll, Plldivm, Pllpdiv, Pllsel, SupplyConfig, SysClk}; use embassy_stm32::rif::{RifMaster, RifMasterAttributes, RifPeripheral, RifPeripheralAttributes}; use embassy_stm32::{Config, bind_interrupts, gpu2d, pac, peripherals}; -use embassy_stm32_neochrom::{FrameBuffer, NeoChrom, Rgba8888}; +use embassy_stm32_neochrom::{BlendMode, ExternalFrameBuffer, FrameBuffer, GpuSurface, NeoChrom, Rgba8888}; use embassy_time::{Instant, Timer}; use {defmt_rtt as _, panic_probe as _}; -use crate::framebuffer::Framebuffer; use crate::rk050hr18c::{HEIGHT, LTDC_CONFIG, Rk050Hr18c, WIDTH}; bind_interrupts!(struct Irqs { @@ -40,7 +47,9 @@ bind_interrupts!(struct Irqs { const FB0_BASE: usize = 0x3410_0000; const FB1_BASE: usize = 0x3420_0000; -const FB_PIXELS: usize = WIDTH as usize * HEIGHT as usize; + +const SPRITE_SIZE: u32 = 64; +const SPRITE_PIXELS: usize = (SPRITE_SIZE * SPRITE_SIZE) as usize; #[embassy_executor::main] async fn main(_spawner: Spawner) { @@ -108,41 +117,109 @@ async fn main(_spawner: Spawner) { window_y1: HEIGHT, }; - let fb0_slice: &'static mut [u16] = unsafe { core::slice::from_raw_parts_mut(FB0_BASE as *mut u16, FB_PIXELS) }; - let fb1_slice: &'static mut [u16] = unsafe { core::slice::from_raw_parts_mut(FB1_BASE as *mut u16, FB_PIXELS) }; - let fb0 = Framebuffer::new(fb0_slice, WIDTH, HEIGHT); - let fb1 = Framebuffer::new(fb1_slice, WIDTH, HEIGHT); - - let small_fb = FrameBuffer::<64, 64, 4096>::new(); + let fb0 = ExternalFrameBuffer::rgb565(FB0_BASE, WIDTH as u32, HEIGHT as u32); + let fb1 = ExternalFrameBuffer::rgb565(FB1_BASE, WIDTH as u32, HEIGHT as u32); + let mut sprite = FrameBuffer::::new(); + init_sprite(&mut sprite); ltdc.init_layer(&layer_config, None); - ltdc.init_buffer(LtdcLayer::Layer1, fb0.as_ptr()); + ltdc.init_buffer(LtdcLayer::Layer1, FB0_BASE as *const ()); pac::LTDC.srcr().write(|w| w.set_imr(pac::ltdc::vals::Imr::Reload)); - let fbs = [fb0.as_ptr(), fb1.as_ptr()]; - let mut back_idx = 1; - - let colors = [Rgba8888::RED, Rgba8888::GREEN, Rgba8888::BLUE, Rgba8888::WHITE]; - let mut color_idx = 0usize; + let scanout = [fb0, fb1]; + let mut back_idx = 1usize; + let mut frame = 0u32; let mut fps_start = Instant::now(); let mut frame_count = 0u32; loop { let frame_start = Instant::now(); - let color = colors[color_idx]; + let back = scanout[back_idx]; + let hue = frame % 360; + let bg = hsl_to_rgba(hue, 35, 10); + let accent = hsl_to_rgba((hue + 140) % 360, 85, 55); + + gpu.begin_frame(&back).expect("begin_frame failed"); + gpu.clear_in_frame(bg).expect("GPU clear failed"); + gpu.fill_rect_in_frame(0, 0, WIDTH as i32, 48, Rgba8888::new(24, 6, 10, 0xFF)) + .expect("title bar fill failed"); + + let cx = (WIDTH / 2) as i32; + let cy = (HEIGHT / 2) as i32; + let radius = 80 + ((frame / 3) % 60) as i32; + gpu.fill_circle_in_frame(cx, cy, radius, accent) + .expect("fill_circle failed"); + + gpu.draw_line_in_frame(0, 48, WIDTH as i32 - 1, HEIGHT as i32 - 1, Rgba8888::WHITE) + .expect("draw_line failed"); + gpu.draw_line_in_frame(WIDTH as i32 - 1, 48, 0, HEIGHT as i32 - 1, Rgba8888::WHITE) + .expect("draw_line failed"); + + let tri_x = 40 + ((frame / 2) % 120) as i32; + gpu.fill_triangle_in_frame( + tri_x, + HEIGHT as i32 - 40, + tri_x + 80, + HEIGHT as i32 - 40, + tri_x + 40, + HEIGHT as i32 - 120, + Rgba8888::new(255, 220, 0, 0xFF), + ) + .expect("fill_triangle failed"); + + let quad_shift = ((frame / 3) % 40) as i32; + gpu.fill_quad_in_frame( + 520 + quad_shift, + 80, + 620 + quad_shift, + 90, + 600 + quad_shift, + 180, + 500 + quad_shift, + 170, + Rgba8888::new(0, 200, 80, 0xFF), + ) + .expect("fill_quad failed"); + + gpu.draw_stroke_triangle_aa_in_frame( + tri_x as f32, + (HEIGHT - 40) as f32, + (tri_x + 80) as f32, + (HEIGHT - 40) as f32, + (tri_x + 40) as f32, + (HEIGHT - 120) as f32, + 2.0, + Rgba8888::WHITE, + ) + .expect("stroke triangle failed"); - gpu.clear(&small_fb, color).expect("GPU clear failed"); + gpu.set_blend_blit(BlendMode::Src); + let sprite_x = (WIDTH as i32 - SPRITE_SIZE as i32 - 24) - ((frame / 2) % 200) as i32; + gpu.blit_in_frame(&sprite, sprite_x, 64).expect("blit failed"); + + let preview_w = 160 + ((frame / 4) % 80) as i32; + let preview_h = 96 + ((frame / 4) % 48) as i32; + gpu.blit_rect_fit_in_frame(&sprite, 24, 64, preview_w, preview_h) + .expect("blit_rect_fit failed"); + + let angle = (frame * 3) % 360; + gpu.blit_rotate_in_frame(&sprite, WIDTH as i32 - 120, HEIGHT as i32 - 120, angle) + .expect("blit_rotate failed"); + + gpu.end_frame_async().await.expect("GPU frame failed"); let t_flip = Instant::now(); - ltdc.set_buffer(LtdcLayer::Layer1, fbs[back_idx]).await.unwrap(); + ltdc.set_buffer(LtdcLayer::Layer1, back.phys_addr() as *const ()) + .await + .unwrap(); let flip_us = t_flip.elapsed().as_micros(); back_idx = 1 - back_idx; - color_idx = (color_idx + 1) % colors.len(); + frame += 1; frame_count += 1; if fps_start.elapsed().as_millis() >= 1000 { - info!("fps={} flip_us={}", frame_count, flip_us); + info!("fps={} flip_us={} hue={}", frame_count, flip_us, hue); fps_start = Instant::now(); frame_count = 0; } @@ -154,6 +231,47 @@ async fn main(_spawner: Spawner) { } } +fn init_sprite(sprite: &mut FrameBuffer) { + let pixels = sprite.pixels_mut(); + for y in 0..SPRITE_SIZE { + for x in 0..SPRITE_SIZE { + let dx = x as i32 - SPRITE_SIZE as i32 / 2; + let dy = y as i32 - SPRITE_SIZE as i32 / 2; + let dist_sq = dx * dx + dy * dy; + let radius_sq = (SPRITE_SIZE as i32 / 2 - 4).pow(2); + let color = if dist_sq <= radius_sq { + Rgba8888::new(0, 180, 255, 0xFF) + } else { + Rgba8888::new(0, 0, 0, 0) + }; + pixels[(y * SPRITE_SIZE + x) as usize] = color.bits(); + } + } +} + +fn hsl_to_rgba(h: u32, s: u8, l: u8) -> Rgba8888 { + let s = s as f32 / 100.0; + let l = l as f32 / 100.0; + let h = (h % 360) as f32 / 60.0; + let c = (1.0 - (2.0 * l - 1.0).abs()) * s; + let x = c * (1.0 - ((h % 2.0) - 1.0).abs()); + let m = l - c / 2.0; + let (r, g, b) = match h as u32 { + 0 => (c, x, 0.0), + 1 => (x, c, 0.0), + 2 => (0.0, c, x), + 3 => (0.0, x, c), + 4 => (x, 0.0, c), + _ => (c, 0.0, x), + }; + Rgba8888::new( + ((r + m) * 255.0) as u8, + ((g + m) * 255.0) as u8, + ((b + m) * 255.0) as u8, + 0xFF, + ) +} + fn enable_all_sram() { pac::RCC.memenr().modify(|w| { w.set_axisram1en(true); From 2d75bfff4bda98ad396d0a8fc1fe3b4e65481caa Mon Sep 17 00:00:00 2001 From: Gerzain Mata Date: Mon, 3 Aug 2026 08:02:29 -0700 Subject: [PATCH 15/15] WIP --- embassy-stm32/src/gpu2d.rs | 12 +++--- embassy-stm32/src/rcc/n6.rs | 51 ++++++++++++++++++------ examples/stm32n6/src/bin/neochrom.rs | 6 ++- examples/stm32n6/src/bin/neochrom_lcd.rs | 12 ++++-- 4 files changed, 57 insertions(+), 24 deletions(-) diff --git a/embassy-stm32/src/gpu2d.rs b/embassy-stm32/src/gpu2d.rs index 3e18a04b0f..f411d3afff 100644 --- a/embassy-stm32/src/gpu2d.rs +++ b/embassy-stm32/src/gpu2d.rs @@ -8,9 +8,9 @@ use core::marker::PhantomData; use embassy_hal_internal::PeripheralType; use embassy_sync::waitqueue::AtomicWaker; -use crate::interrupt::typelevel::Interrupt; +use crate::interrupt::typelevel::{Binding, Handler, Interrupt}; use crate::pac::gpu2d::Gpu2d as Regs; -use crate::{Peri, interrupt}; +use crate::{Peri, interrupt, rcc}; // GPU2D has exactly one instance per chip today (see stm32-data), so a single // static waker is sufficient — unlike peripherals with multiple instances @@ -30,12 +30,10 @@ pub struct Gpu2d<'d, T: Instance> { _peri: Peri<'d, T>, } -impl<'d, T: Instance> Gpu2d<'d, T> { +impl<'d, T: Instance + crate::rcc::RccPeripheral> Gpu2d<'d, T> { /// Create a GPU2D instance. - pub fn new( - peri: Peri<'d, T>, - _irq: impl interrupt::typelevel::Binding> + 'd, - ) -> Self { + pub fn new>(peri: Peri<'d, T>, _irq: impl Binding + 'd) -> Self { + rcc::enable_and_reset::(); T::Interrupt::unpend(); unsafe { T::Interrupt::enable() }; Self { _peri: peri } diff --git a/embassy-stm32/src/rcc/n6.rs b/embassy-stm32/src/rcc/n6.rs index 7386465342..a668269cf7 100644 --- a/embassy-stm32/src/rcc/n6.rs +++ b/embassy-stm32/src/rcc/n6.rs @@ -1053,7 +1053,7 @@ fn init_osc(config: Config) -> OscOutput { // If config wants a non-IC1 CPU source (HSI/HSE/MSI), switch now before // touching PLLs. This prevents panicking when trying to reconfigure a PLL // that's currently in use by IC1. - let cpu_src = if cpu_src == Cpusws::Ic1 && !matches!(config.cpu, CpuClk::Ic1 { .. }) { + let mut cpu_src = if cpu_src == Cpusws::Ic1 && !matches!(config.cpu, CpuClk::Ic1 { .. }) { // Switch CPU clock to the target source first debug!("switching CPU away from IC1 before PLL reconfiguration"); let cpusw = Cpusw::from_bits(config.cpu.to_bits()); @@ -1068,7 +1068,7 @@ fn init_osc(config: Config) -> OscOutput { // If config wants a non-IC2 sys source (HSI/HSE/MSI), switch now before // touching PLLs. This prevents panicking when trying to reconfigure a PLL // that's currently in use by IC2, IC6, or IC11. - let sys_src = if sys_src == Syssws::Ic2 && !matches!(config.sys, SysClk::Ic2 { .. }) { + let mut sys_src = if sys_src == Syssws::Ic2 && !matches!(config.sys, SysClk::Ic2 { .. }) { // Switch system clock to the target source first debug!("switching sys clock away from IC2 before PLL reconfiguration"); let syssw = Syssw::from_bits(config.sys.to_bits()); @@ -1080,21 +1080,48 @@ fn init_osc(config: Config) -> OscOutput { sys_src }; + // probe-rs dev-mode reloads often leave the previous firmware's clock tree + // active (CPU on IC1 ← PLL1, etc.). Temporarily fall back to HSI so PLLs + // that feed those ICs can be reconfigured even when the final config keeps + // CpuClk::Ic1 / SysClk::Ic2. + for (n, &pll) in pll_configs.iter().enumerate() { + if !is_new_pll_config(pll, n) { + continue; + } + + let this_pll = Icsel::from_bits(n as u8); + let needs_cpu_switch = cpu_src == Cpusws::Ic1 && ic1_src == this_pll; + let needs_sys_switch = + sys_src == Syssws::Ic2 && (ic2_src == this_pll || ic6_src == this_pll || ic11_src == this_pll); + + if !needs_cpu_switch && !needs_sys_switch { + continue; + } + + debug!("switching to HSI before PLL{} reconfiguration", n + 1); + if !RCC.sr().read().hsirdy() { + RCC.csr().write(|w| w.set_hsions(true)); + while !RCC.sr().read().hsirdy() {} + } + + if needs_cpu_switch { + RCC.cfgr().modify(|w| w.set_cpusw(Cpusw::Hsi)); + while RCC.cfgr().read().cpusws() != Cpusws::Hsi {} + cpu_src = Cpusws::Hsi; + } + + if needs_sys_switch { + RCC.cfgr().modify(|w| w.set_syssw(Syssw::Hsi)); + while RCC.cfgr().read().syssws() != Syssws::Hsi {} + sys_src = Syssws::Hsi; + } + } + for (n, (&pll, out)) in pll_configs.iter().zip(pll_outputs.iter_mut()).enumerate() { debug!("configuring PLL{}", n + 1); let pll_ready = RCC.sr().read().pllrdy(n); if is_new_pll_config(pll, n) { - let this_pll = Icsel::from_bits(n as u8); - - if cpu_src == Cpusws::Ic1 && ic1_src == this_pll { - panic!("PLL should not be disabled / reconfigured if used for IC1 (cpuclksrc)") - } - - if sys_src == Syssws::Ic2 && (ic2_src == this_pll || ic6_src == this_pll || ic11_src == this_pll) { - panic!("PLL should not be disabled / reconfigured if used for IC2, IC6 or IC11 (sysclksrc)") - } - *out = pll.map_or_else( || { disable_pll(n); diff --git a/examples/stm32n6/src/bin/neochrom.rs b/examples/stm32n6/src/bin/neochrom.rs index b64d7d5b44..a597947c9c 100644 --- a/examples/stm32n6/src/bin/neochrom.rs +++ b/examples/stm32n6/src/bin/neochrom.rs @@ -32,7 +32,9 @@ use embassy_executor::Spawner; use embassy_stm32::rcc::SupplyConfig; use embassy_stm32::{Config, pac}; #[cfg(not(feature = "stub-gpu2d"))] -use embassy_stm32::{bind_interrupts, gpu2d, peripherals}; +use embassy_stm32::{bind_interrupts, peripherals}; +#[cfg(not(feature = "stub-gpu2d"))] +use embassy_stm32_neochrom::InterruptHandler as Gpu2dInterruptHandler; use embassy_stm32_neochrom::{FrameBuffer, NeoChrom, Rgba8888}; use embassy_time::Timer; use {defmt_rtt as _, panic_probe as _}; @@ -43,7 +45,7 @@ const FB_PIXELS: usize = (FB_WIDTH * FB_HEIGHT) as usize; #[cfg(not(feature = "stub-gpu2d"))] bind_interrupts!(struct Irqs { - GPU2D_ER => gpu2d::InterruptHandler; + GPU2D_ER => Gpu2dInterruptHandler; }); #[embassy_executor::main] diff --git a/examples/stm32n6/src/bin/neochrom_lcd.rs b/examples/stm32n6/src/bin/neochrom_lcd.rs index 12efcec052..d8e6b83822 100644 --- a/examples/stm32n6/src/bin/neochrom_lcd.rs +++ b/examples/stm32n6/src/bin/neochrom_lcd.rs @@ -33,8 +33,11 @@ use embassy_stm32::ltdc::{self, Ltdc, LtdcLayer, LtdcLayerConfig, PixelFormat}; use embassy_stm32::rcc::mux::Ltdcsel; use embassy_stm32::rcc::{CpuClk, IcConfig, Icint, Icsel, Pll, Plldivm, Pllpdiv, Pllsel, SupplyConfig, SysClk}; use embassy_stm32::rif::{RifMaster, RifMasterAttributes, RifPeripheral, RifPeripheralAttributes}; -use embassy_stm32::{Config, bind_interrupts, gpu2d, pac, peripherals}; -use embassy_stm32_neochrom::{BlendMode, ExternalFrameBuffer, FrameBuffer, GpuSurface, NeoChrom, Rgba8888}; +use embassy_stm32::{Config, bind_interrupts, pac, peripherals}; +use embassy_stm32_neochrom::{ + BlendMode, ExternalFrameBuffer, FrameBuffer, GpuSurface, InterruptHandler as Gpu2dInterruptHandler, NeoChrom, + Rgba8888, +}; use embassy_time::{Instant, Timer}; use {defmt_rtt as _, panic_probe as _}; @@ -42,7 +45,7 @@ use crate::rk050hr18c::{HEIGHT, LTDC_CONFIG, Rk050Hr18c, WIDTH}; bind_interrupts!(struct Irqs { LTDC_LO => ltdc::InterruptHandler; - GPU2D_ER => gpu2d::InterruptHandler; + GPU2D_ER => Gpu2dInterruptHandler; }); const FB0_BASE: usize = 0x3410_0000; @@ -95,18 +98,21 @@ async fn main(_spawner: Spawner) { let mut panel = Rk050Hr18c::new(p.PE1, p.PQ3, p.PQ6); panel.power_on().await; + info!("panel powered on"); let mut ltdc = Ltdc::<_, ltdc::Rgb888>::new_with_pins( p.LTDC, Irqs, p.PB13, p.PB14, p.PE11, p.PG13, p.PG15, p.PA7, p.PB2, p.PG6, p.PH3, p.PH6, p.PA8, p.PA2, p.PG12, p.PG1, p.PA1, p.PA0, p.PB15, p.PB12, p.PB11, p.PG8, p.PG0, p.PD9, p.PD15, p.PB4, p.PH4, p.PA15, p.PG11, p.PD8, ); ltdc.init(<DC_CONFIG); + info!("ltdc initialized"); #[cfg(feature = "stub-gpu2d")] let mut gpu = NeoChrom::new().expect("NeoChrom init failed"); #[cfg(not(feature = "stub-gpu2d"))] let mut gpu = NeoChrom::new(p.GPU2D, Irqs).expect("NeoChrom init failed"); + info!("neochrom initialized"); let layer_config = LtdcLayerConfig { pixel_format: PixelFormat::RGB565,