diff --git a/embassy-stm32-neochrom/Cargo.toml b/embassy-stm32-neochrom/Cargo.toml new file mode 100644 index 0000000000..77d3b4299a --- /dev/null +++ b/embassy-stm32-neochrom/Cargo.toml @@ -0,0 +1,72 @@ +[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 } + +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 } + +[features] +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"] + +## 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..fe81670770 --- /dev/null +++ b/embassy-stm32-neochrom/README.md @@ -0,0 +1,45 @@ +# 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 safe 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 --target thumbv8m.main-none-eabihf --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 | +| `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 new file mode 100644 index 0000000000..7f7f9458c4 --- /dev/null +++ b/embassy-stm32-neochrom/src/color.rs @@ -0,0 +1,62 @@ +//! 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); + +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 + } +} + +/// 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, +} + +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 new file mode 100644 index 0000000000..8355f7a152 --- /dev/null +++ b/embassy-stm32-neochrom/src/driver.rs @@ -0,0 +1,1139 @@ +//! 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_init; +use crate::framebuffer::GpuSurface; + +/// 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, +} + +struct FrameState { + dst: SurfaceSyncInfo, + surfaces: Vec, +} + +/// NeoChrom GPU context. +/// +/// 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 { + cmd: CommandList, + frame: Option, +} + +impl NeoChrom { + /// Initialize NemaGFX and the platform HAL using the in-tree GPU2D stub. + #[cfg(feature = "stub-gpu2d")] + pub fn new() -> Result { + nema_gfx_hal::gpu2d_init_stub().map_err(|_| InitError::Gpu2d)?; + Self::finish_init() + } + + /// Initialize NemaGFX using the real GPU2D peripheral. + #[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); + } + + 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() + } + + /// 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: &impl GpuSurface, + x: i32, + y: i32, + w: i32, + h: i32, + color: Rgba8888, + ) -> Result<(), Error> { + 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(()) + } + + /// Draw a 1-pixel outline rectangle. + pub fn draw_stroke_rect( + &mut self, + framebuffer: &impl GpuSurface, + x: i32, + y: i32, + w: i32, + h: i32, + color: Rgba8888, + ) -> Result<(), Error> { + 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() + } + } + + /// 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`). + pub fn draw_line( + &mut self, + framebuffer: &impl GpuSurface, + x0: i32, + y0: i32, + x1: i32, + y1: i32, + color: Rgba8888, + ) -> Result<(), Error> { + 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() + } + } + + /// 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()); + }); + 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`. + pub fn fill_circle( + &mut self, + framebuffer: &impl GpuSurface, + cx: i32, + cy: i32, + r: i32, + color: Rgba8888, + ) -> Result<(), Error> { + 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(()) + } + + /// Draw a stroked circle with explicit width and anti-aliasing. + pub fn draw_stroke_circle_aa( + &mut self, + framebuffer: &impl GpuSurface, + cx: f32, + cy: f32, + r: f32, + width: f32, + color: Rgba8888, + ) -> Result<(), Error> { + 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() + } + } + + /// 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`. + pub fn fill_rounded_rect( + &mut self, + framebuffer: &impl GpuSurface, + x: i32, + y: i32, + w: i32, + h: i32, + r: i32, + color: Rgba8888, + ) -> Result<(), Error> { + 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. + pub fn fill_triangle( + &mut self, + framebuffer: &impl GpuSurface, + x0: i32, + y0: i32, + x1: i32, + y1: i32, + x2: i32, + y2: i32, + color: Rgba8888, + ) -> Result<(), Error> { + 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() + } + } + + /// 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()); + }); + 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. + pub fn fill_quad( + &mut self, + framebuffer: &impl GpuSurface, + x0: i32, + y0: i32, + x1: i32, + y1: i32, + x2: i32, + y2: i32, + x3: i32, + y3: i32, + color: Rgba8888, + ) -> Result<(), Error> { + 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() + } + } + + /// 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()); + }); + 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 `src` scaled to `dst_w` x `dst_h` at (`dst_x`, `dst_y`). + pub fn blit_rect_fit( + &mut self, + dst: &impl GpuSurface, + src: &impl GpuSurface, + dst_x: i32, + dst_y: i32, + dst_w: i32, + dst_h: i32, + ) -> Result<(), Error> { + 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 `src` rotated by `angle_degrees` at (`dst_x`, `dst_y`). + pub fn blit_rotate( + &mut self, + dst: &impl GpuSurface, + src: &impl GpuSurface, + dst_x: i32, + dst_y: i32, + angle_degrees: u32, + ) -> Result<(), Error> { + 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 `src` with rotation around center (`cx`, `cy`) and pivot (`px`, `py`). + pub fn blit_rotate_pivot( + &mut self, + dst: &impl GpuSurface, + src: &impl GpuSurface, + cx: f32, + cy: f32, + px: f32, + py: f32, + angle_degrees: f32, + ) -> Result<(), Error> { + 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 a sub-rectangle of `src`, scaled to a destination rectangle. + #[allow(clippy::too_many_arguments)] + pub fn blit_subrect_fit( + &mut self, + dst: &impl GpuSurface, + 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> { + 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 `src` warped into a destination quadrilateral. + #[allow(clippy::too_many_arguments)] + pub fn blit_quad_fit( + &mut self, + dst: &impl GpuSurface, + src: &impl GpuSurface, + dx0: f32, + dy0: f32, + dx1: f32, + dy1: f32, + dx2: f32, + dy2: f32, + dx3: f32, + dy3: f32, + ) -> Result<(), Error> { + 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() + } + } + + /// 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); + }); + 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(()) + } + + /// 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 blits. + 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 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, + mask_phys_addr: usize, + mask_w: u32, + mask_h: u32, + mask_stride: i32, + dst_x: i32, + 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_set_blend, nema_set_tex_color, nema_tex_t_NEMA_NOTEX, + nema_tex_t_NEMA_TEX0, nema_tex_t_NEMA_TEX1, + }; + 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, + ); + } + 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`). + 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: &impl GpuSurface) { + unsafe { + use crate::ffi::nema_gfx::nema_set_clip; + 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 new file mode 100644 index 0000000000..3f6d9ce6fe --- /dev/null +++ b/embassy-stm32-neochrom/src/error.rs @@ -0,0 +1,30 @@ +//! 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, + /// 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. +#[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..8645b20998 --- /dev/null +++ b/embassy-stm32-neochrom/src/framebuffer.rs @@ -0,0 +1,156 @@ +//! Framebuffer storage for NeoChrom rendering targets. + +use aligned::{A32, Aligned}; + +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. +/// +/// 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]), + } + } +} + +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 { + 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 { + GpuSurface::phys_addr(self) + } + + /// 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() + } +} + +/// Framebuffer backed by an existing memory region (e.g. AXISRAM used by LTDC). +/// +/// Use this when the render target already lives at a fixed address and may use +/// a format other than RGBA8888, such as RGB565 scan-out buffers. +#[derive(Debug, Clone, Copy)] +pub struct ExternalFrameBuffer { + addr: usize, + width: u32, + height: u32, + format: ColorFormat, + stride: i32, +} + +impl ExternalFrameBuffer { + /// Bind an existing RGB565 buffer (typical LTDC layer framebuffer). + pub const fn rgb565(addr: usize, width: u32, height: u32) -> 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 new file mode 100644 index 0000000000..6b8496d65d --- /dev/null +++ b/embassy-stm32-neochrom/src/gpu2d_bridge.rs @@ -0,0 +1,143 @@ +//! 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`. + +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; +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)); +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)] +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; + } +} + +/// 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)] +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) { + 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 +/// 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) }; + wake(); + } + + 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 new file mode 100644 index 0000000000..124b5cf557 --- /dev/null +++ b/embassy-stm32-neochrom/src/lib.rs @@ -0,0 +1,34 @@ +//! 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 use nema_gfx_hal; + +pub(crate) mod fmt; + +mod coherency; +mod color; +mod command; +mod driver; +mod error; +mod framebuffer; +#[cfg(not(feature = "stub-gpu2d"))] +mod gpu2d_bridge; + +pub mod ffi; +#[cfg(feature = "embedded-graphics")] +pub mod target; + +pub use color::{ColorFormat, Rgba8888}; +pub use driver::{BlendMode, NeoChrom, TextureFilter, TextureWrap}; +pub use error::{Error, InitError}; +pub use framebuffer::{ExternalFrameBuffer, FrameBuffer, GpuSurface}; +#[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) + } +} diff --git a/embassy-stm32/src/gpu2d.rs b/embassy-stm32/src/gpu2d.rs index 7f0f59ce97..f411d3afff 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::interrupt::typelevel::{Binding, Handler, 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))] @@ -24,12 +30,9 @@ 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() }; @@ -61,6 +64,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,11 +95,15 @@ 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(); } } -trait SealedInstance: crate::rcc::RccPeripheral { +trait SealedInstance { fn regs() -> Regs; } 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/embassy-stm32/src/lib.rs b/embassy-stm32/src/lib.rs index 59bdf7c160..f0fc8a9131 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/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/Cargo.toml b/examples/stm32n6/Cargo.toml index 843a9a82ad..cd442207a6 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", default-features = false, features = ["defmt", "stm32n657x0"] } defmt = "1.0.1" defmt-rtt = "1.0.0" @@ -43,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/README.md b/examples/stm32n6/README.md index 3daabd65f1..5f8d5325f3 100644 --- a/examples/stm32n6/README.md +++ b/examples/stm32n6/README.md @@ -3,3 +3,25 @@ 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` 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 +``` + +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/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 new file mode 100644 index 0000000000..a597947c9c --- /dev/null +++ b/examples/stm32n6/src/bin/neochrom.rs @@ -0,0 +1,157 @@ +#![no_std] +#![no_main] + +//! NeoChrom (NemaGFX) smoke test for the STM32N6570-DK. +//! +//! 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 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 +//! 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}; +#[cfg(not(feature = "stub-gpu2d"))] +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 _}; + +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 => Gpu2dInterruptHandler; +}); + +#[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); + #[cfg(feature = "stub-gpu2d")] + let _ = p; + + enable_all_sram(); + promote_gpu2d_master_attributes(); + + 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 mut frame = 0u32; + + loop { + 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| { + 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); + }); +} + +/// 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..d8e6b83822 --- /dev/null +++ b/examples/stm32n6/src/bin/neochrom_lcd.rs @@ -0,0 +1,307 @@ +#![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 render directly into +//! double-buffered RGB565 AXISRAM framebuffers. +//! +//! Features integrated: +//! - 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 = "../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, 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 _}; + +use crate::rk050hr18c::{HEIGHT, LTDC_CONFIG, Rk050Hr18c, WIDTH}; + +bind_interrupts!(struct Irqs { + LTDC_LO => ltdc::InterruptHandler; + GPU2D_ER => Gpu2dInterruptHandler; +}); + +const FB0_BASE: usize = 0x3410_0000; +const FB1_BASE: usize = 0x3420_0000; + +const SPRITE_SIZE: u32 = 64; +const SPRITE_PIXELS: usize = (SPRITE_SIZE * SPRITE_SIZE) 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; + 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, + layer: LtdcLayer::Layer1, + window_x0: 0, + window_x1: WIDTH, + window_y0: 0, + window_y1: HEIGHT, + }; + + 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_BASE as *const ()); + pac::LTDC.srcr().write(|w| w.set_imr(pac::ltdc::vals::Imr::Reload)); + + 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 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.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, back.phys_addr() as *const ()) + .await + .unwrap(); + let flip_us = t_flip.elapsed().as_micros(); + + back_idx = 1 - back_idx; + frame += 1; + frame_count += 1; + + if fps_start.elapsed().as_millis() >= 1000 { + info!("fps={} flip_us={} hue={}", frame_count, flip_us, hue); + fps_start = Instant::now(); + frame_count = 0; + } + + let elapsed = frame_start.elapsed().as_millis(); + if elapsed < 16 { + Timer::after_millis(16 - elapsed).await; + } + } +} + +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); + 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)); + } +}