From 74d7b6df5fd56247bdc4b0cc22e85bb46df2b06d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Hlusi=C4=8Dka?= Date: Sat, 14 Feb 2026 20:03:32 +0100 Subject: [PATCH] WIP --- firmware/acid-firmware/src/main.rs | 158 +++------- firmware/acid-firmware/src/ui/dpi.rs | 427 +++++++++++++++++++++++++++ firmware/acid-firmware/src/ui/mod.rs | 1 + 3 files changed, 477 insertions(+), 109 deletions(-) create mode 100644 firmware/acid-firmware/src/ui/dpi.rs diff --git a/firmware/acid-firmware/src/main.rs b/firmware/acid-firmware/src/main.rs index f64de94..1b0c793 100644 --- a/firmware/acid-firmware/src/main.rs +++ b/firmware/acid-firmware/src/main.rs @@ -40,7 +40,9 @@ use esp_alloc::{HeapRegion, MemoryCapability}; use esp_bootloader_esp_idf::partitions::PartitionTable; use esp_hal::Blocking; use esp_hal::clock::CpuClock; -use esp_hal::dma::{BurstConfig, DmaDescriptor, DmaTxBuf, ExternalBurstConfig}; +use esp_hal::dma::{ + BurstConfig, DmaDescriptor, DmaTxBuf, ExternalBurstConfig, InternalBurstConfig, +}; use esp_hal::efuse::Efuse; use esp_hal::gpio::{Flex, Input, InputConfig, Level, Output, OutputConfig, Pull}; use esp_hal::i2c::master::{I2c, I2cAddress}; @@ -48,10 +50,12 @@ use esp_hal::interrupt::software::SoftwareInterruptControl; use esp_hal::lcd_cam::LcdCam; use esp_hal::lcd_cam::lcd::dpi::Dpi; use esp_hal::mcpwm::{McPwm, PeripheralClockConfig}; +use esp_hal::peripherals::{DMA_CH0, SPI0, SPI2}; use esp_hal::psram::{FlashFreq, PsramConfig, PsramSize, SpiRamFreq, SpiTimingConfigCoreClock}; use esp_hal::ram; use esp_hal::rng::TrngSource; use esp_hal::sha::ShaBackend; +use esp_hal::spi::master::AnySpi; use esp_hal::system::Stack; use esp_hal::timer::timg::TimerGroup; use esp_rtos::embassy::Executor; @@ -60,19 +64,16 @@ use indoc::writedoc; use itertools::Itertools; use log::{error, info, warn}; use rmk::channel::{CONTROLLER_CHANNEL, ControllerSub}; -use rmk::config::{ - DeviceConfig, RmkConfig, StorageConfig, - VialConfig, -}; +use rmk::config::{DeviceConfig, RmkConfig, StorageConfig, VialConfig}; use rmk::controller::{Controller, EventController}; use rmk::debounce::default_debouncer::DefaultDebouncer; use rmk::event::ControllerEvent; use rmk::hid::Report; use rmk::input_device::Runnable; +use rmk::join_all; use rmk::keyboard::Keyboard; use rmk::storage::async_flash_wrapper; use rmk::types::action::{Action, KeyAction}; -use rmk::join_all; use rmk::{initialize_keymap_and_storage, run_devices, run_rmk}; use slint::platform::software_renderer::Rgb565Pixel; use static_cell::StaticCell; @@ -82,6 +83,7 @@ use crate::matrix::IoeMatrix; use crate::peripherals::st7701s::St7701s; use crate::proxy::create_hid_report_interceptor; use crate::ui::backend::{FramebufferPtr, SlintBackend}; +use crate::ui::dpi::{DmaTxBounceBuf, Framebuffer}; use crate::vial::{ CustomKeycodes, VIAL_KEYBOARD_DEF, VIAL_KEYBOARD_ID, VIAL_KEYBOARD_NAME, VIAL_PRODUCT_ID, VIAL_VENDOR_ID, @@ -131,6 +133,26 @@ static SIGNAL_LCD_SUBMIT: Signal = Signal::new(); /// Used to signal that the MCU is ready to render the GUI. static SIGNAL_UI_RENDER: Signal = Signal::new(); +async fn test_bounce_buffers(channel: DMA_CH0<'static>, peripheral: SPI2<'static>) -> ! { + error!("TEST BOUNCE BUFFERS SECTION ENTERED"); + let windows_len = 2; + let window_size = 368 * core::mem::size_of::(); + let buffer_src = Box::leak(vec![0_u8; windows_len * window_size].into_boxed_slice()); + let mut buf = DmaTxBounceBuf::new( + channel, + AnySpi::from(peripheral), + buffer_src, + window_size, + BurstConfig { + internal_memory: InternalBurstConfig::Enabled, + external_memory: ExternalBurstConfig::Size32, + }, + ); + buf.send().await; + error!("TEST BOUNCE BUFFERS SECTION DONE"); + loop {} +} + #[esp_rtos::main] async fn main(_spawner: Spawner) { let config = esp_hal::Config::default() @@ -382,6 +404,9 @@ async fn main(_spawner: Spawner) { info!("ST7701S-based LCD display initialized!"); + test_bounce_buffers(peripherals.DMA_CH0, peripherals.SPI2).await; + return; + // RMK config let vial_config = VialConfig::new(VIAL_KEYBOARD_ID, VIAL_KEYBOARD_DEF, &[(0, 0), (1, 1)]); let storage_config = StorageConfig { @@ -463,7 +488,22 @@ async fn main(_spawner: Spawner) { static FRAMEBUFFER: StaticCell = StaticCell::new(); let framebuffer = FRAMEBUFFER.init(Framebuffer::new( - 360 + /* TODO: Figure out why more bytes are needed: */ 8, + 368, + // It is likely due to DMA transfer requirements. + // The size of each chunk transferred via DMA must be a multiple of 32 bytes. + // (360 * size_of()) % 32 == 16 + // Make it so that the renderer renders into the smallest transmissible region of memory. + // + // | | ( displayed range ) | + // | [ pad ] [ pad ] + // | [ DMA-transmissible range ] + // [ DMA-t. left overscan ] | | | + // 0 112 120 360 368 (index of u16 pixel) + // ^ aligned ^ aligned ^ aligned + // + // TODO: Compute the appropriate ranges to pass to the renderer and DPI peripheral. + // The renderer should pass the size of the `pad`ding to the GUI is parameters, + // to align the content to the displayed range. 960, )); @@ -514,7 +554,7 @@ async fn main(_spawner: Spawner) { run_alloc_stats_reporter(), // We currently send the framebuffer data using the main core, which does not seem to slow // down the rest of the tasks too much. - run_lcd(st7701s, framebuffer), + ui::dpi::run_lcd(st7701s, framebuffer), run_devices! ( (matrix) => rmk::channel::EVENT_CHANNEL, ), @@ -605,109 +645,9 @@ impl Controller for UserController { } } -struct Framebuffer { - width: u32, - height: u32, - dma_buf: Option, -} - -impl Framebuffer { - pub fn new(width: u32, height: u32) -> Self { - let buffer_len = width as usize * height as usize * core::mem::size_of::(); - // Allocate the framebuffer in the external PSRAM memory. - // Note: We just leak this buffer. - let buffer_ptr = unsafe { - // ⚠️ Note: For chips that support DMA to/from PSRAM (ESP32-S3) DMA transfers to/from PSRAM - // have extra alignment requirements. The address and size of the buffer pointed to by each - // descriptor must be a multiple of the cache line (block) size. This is 32 bytes on ESP32-S3. - PSRAM_ALLOCATOR.alloc_caps( - MemoryCapability::External.into(), - Layout::from_size_align(buffer_len, 32).unwrap(), - ) - }; - let buffer = unsafe { core::slice::from_raw_parts_mut(buffer_ptr, buffer_len) }; - let burst_config: BurstConfig = ExternalBurstConfig::Size16.into(); - - info!( - "PSRAM SPI burst config: max_compatible_chunk_size={}", - burst_config.max_compatible_chunk_size() - ); - let dma_buf_descs_len = esp_hal::dma::descriptor_count( - buffer_len, - burst_config.max_compatible_chunk_size(), - false, - ); - // Descriptors are initialized by `DmaTxBuf::new`. - let dma_buf_descs = - Box::leak(vec![DmaDescriptor::EMPTY; dma_buf_descs_len].into_boxed_slice()); - let dma_buf = DmaTxBuf::new(dma_buf_descs, buffer).unwrap(); - - Self { - width, - height, - dma_buf: Some(dma_buf), - } - } - - pub fn as_target_pixels(&mut self) -> &mut [Rgb565Pixel] { - bytemuck::cast_slice_mut::<_, Rgb565Pixel>(self.dma_buf.as_mut().unwrap().as_mut_slice()) - } -} - -#[embassy_executor::task] -async fn run_lcd_task(st7701s: St7701s<'static, Blocking>, framebuffer: &'static mut Framebuffer) { - run_lcd(st7701s, framebuffer).await -} - -async fn run_lcd(mut st7701s: St7701s<'static, Blocking>, framebuffer: &'static mut Framebuffer) { - loop { - // Timer::after(Duration::from_millis(100)).await; - // yield_now().await; - SIGNAL_LCD_SUBMIT.wait().await; - - // TODO: Use bounce buffers: - // https://docs.espressif.com/projects/esp-idf/en/v5.0/esp32s3/api-reference/peripherals/lcd.html#bounce-buffer-with-single-psram-frame-buffer - // This can be implemented as a `DmaTxBuffer`. - let transfer = match st7701s.dpi.send(false, framebuffer.dma_buf.take().unwrap()) { - Err((error, result_dpi, result_dma_buf)) => { - error!( - "An error occurred while initiating transfer of the framebuffer to the LCD display: {error:?}" - ); - st7701s.dpi = result_dpi; - framebuffer.dma_buf = Some(result_dma_buf); - continue; - } - Ok(transfer) => transfer, - }; - - // This could be used to allow other tasks to be executed on the first core, but that causes - // the flash to be accessed, which interferes with the framebuffer transfer. - // For that reason, it is disabled, and this task blocks the first core, until the transfer - // is complete. - #[cfg(not(feature = "limit-fps"))] - while !transfer.is_done() { - // Timer::after_millis(1).await; - rmk::embassy_futures::yield_now().await; - } - - let result; - let dma_buf; - (result, st7701s.dpi, dma_buf) = transfer.wait(); - framebuffer.dma_buf = Some(dma_buf); - - SIGNAL_UI_RENDER.signal(()); - - if let Err(error) = result { - error!( - "An error occurred while transferring framebuffer to the LCD display: {error:?}" - ); - } - } -} - // // TODO: Not needed currently. If it is ever enabled, don't forget to register it in Io. // #[handler] -// #[ram] // TODO: Is this necessary? +// #[ram] // Improves performance. // fn interrupt_handler() { // // esp_println::println!( // // "GPIO Interrupt with priority {}", diff --git a/firmware/acid-firmware/src/ui/dpi.rs b/firmware/acid-firmware/src/ui/dpi.rs new file mode 100644 index 0000000..d30aa36 --- /dev/null +++ b/firmware/acid-firmware/src/ui/dpi.rs @@ -0,0 +1,427 @@ +use core::{alloc::Layout, pin::Pin}; + +use alloc::{ + alloc::{Allocator, Global}, + boxed::Box, + vec, +}; +use embassy_sync::channel::Channel; +use esp_alloc::MemoryCapability; +use esp_hal::{ + Blocking, + dma::{ + self, AnyGdmaChannel, BufView, BurstConfig, DmaChannel, DmaChannelConvert, DmaDescriptor, + DmaDescriptorFlags, DmaEligible, DmaRxStreamBuf, DmaTxBuf, DmaTxBuffer, DmaTxInterrupt, + ExternalBurstConfig, Mem2Mem, + }, + dma_descriptors, handler, interrupt, + peripherals::{DMA, DMA_CH0, Peripherals, SPI2}, + ram, + spi::master::AnySpi, +}; +use esp_sync::RawMutex; +use i_slint_core::software_renderer::Rgb565Pixel; +use indoc::{formatdoc, indoc}; +use log::{error, info}; + +use crate::{PSRAM_ALLOCATOR, SIGNAL_LCD_SUBMIT, SIGNAL_UI_RENDER, peripherals::st7701s::St7701s}; + +pub struct DmaTxBounceBuf { + // TODO: Make these generic. + // They currently cannot be generic, because they lacks a `reborrow` method. + channel: DMA_CH0<'static>, + // This can also be more generic, see `DmaEligible` in `Mem2Mem::new`. + peripheral: AnySpi<'static>, + + burst_config: BurstConfig, + // rx: DmaRxStreamBuf, + /// The size of each window. + window_size: usize, + /// The number of windows. + windows_len: usize, + buffer_src: &'static mut [u8], + // Two buffers of size `window_size`, + // one of which is being written to, while the other is being read from. + bounce_buffer_dst: Box<[u8]>, + bounce_buffer_src: Box<[u8]>, +} + +impl DmaTxBounceBuf { + pub fn new( + channel: DMA_CH0<'static>, + peripheral: AnySpi<'static>, + buffer_src: &'static mut [u8], + window_size: usize, + burst_config: BurstConfig, + ) -> Self { + assert_eq!( + buffer_src.len() % window_size, + 0, + "the size of a source buffer must be a multiple of the window size ({window_size} bytes), but it is {len} bytes large", + len = buffer_src.len() + ); + // let dma_buf_descs_len = esp_hal::dma::descriptor_count( + // window_size, + // burst_config.max_compatible_chunk_size(), + // false, + // ); + // let dma_buf_descs = + // Box::leak(vec![DmaDescriptor::EMPTY; dma_buf_descs_len].into_boxed_slice()); + // let rx = DmaRxStreamBuf::new(dma_buf_descs, buffer_src).unwrap(); + Self { + channel, + peripheral, + burst_config, + // rx, + window_size, + windows_len: buffer_src.len() / window_size, + buffer_src, + bounce_buffer_dst: allocate_dma_buffer_in(window_size, Global), + bounce_buffer_src: allocate_dma_buffer_in(window_size, Global), + } + } + + fn linear_descriptors_for_buffer( + buffer_len: usize, + burst_config: &BurstConfig, + mut setup_desc: impl FnMut(&mut DmaDescriptor), + ) -> &'static mut [DmaDescriptor] { + let max_chunk_size = burst_config.max_compatible_chunk_size(); + let descriptors_len = esp_hal::dma::descriptor_count(buffer_len, max_chunk_size, false); + // TODO: This leaks memory. Ensure it's only called during setup. + let descriptors = Box::leak(vec![DmaDescriptor::EMPTY; descriptors_len].into_boxed_slice()); + + // Link up the descriptors. + let mut next = core::ptr::null_mut(); + for desc in descriptors.iter_mut().rev() { + desc.next = next; + next = desc; + } + + // Prepare each descriptor's buffer size. + let mut descriptors_it = descriptors.iter_mut(); + let mut remaining_len = buffer_len; + while remaining_len > 0 { + let chunk_size = core::cmp::min(max_chunk_size, remaining_len); + let desc = descriptors_it.next().unwrap(); + desc.set_size(chunk_size); + (setup_desc)(desc); + remaining_len -= chunk_size; + } + + descriptors + } + + fn linear_descriptors_set_buffer( + descriptors: &mut [DmaDescriptor], + mut buffer: &mut [u8], + mut setup_desc: impl FnMut(&mut DmaDescriptor), + ) { + for descriptor in descriptors.iter_mut() { + descriptor.buffer = buffer.as_mut_ptr(); + (setup_desc)(descriptor); + buffer = &mut buffer[descriptor.size()..]; + } + assert!( + buffer.is_empty(), + "a buffer of an incompatible length was asssigned to a descriptor set" + ); + } + + pub async fn send(&mut self) { + // TODO: Precompute as much as possible by moving to `Self::new()`. + let src_descs = + Self::linear_descriptors_for_buffer(self.window_size, &self.burst_config, |desc| { + desc.reset_for_tx(desc.next.is_null()); + // Length for TX buffers must be set in software. + // In RX buffers, it is set by hardware. + desc.set_length(desc.size()); + }); + let bounce_dst_descs = + Self::linear_descriptors_for_buffer(self.window_size, &self.burst_config, |_| {}); + + // Enable interrupts for the peripheral + interrupt::enable( + esp_hal::peripherals::Interrupt::DMA_OUT_CH0, + dma_interrupt_handler.priority(), + ) + .unwrap(); + interrupt::enable( + esp_hal::peripherals::Interrupt::SPI2_DMA, + dma_interrupt_handler.priority(), + ) + .unwrap(); + + // Bind the handler + unsafe { + interrupt::bind_interrupt( + esp_hal::peripherals::Interrupt::DMA_OUT_CH0, + dma_interrupt_handler.handler(), + ); + interrupt::bind_interrupt( + esp_hal::peripherals::Interrupt::SPI2_DMA, + dma_interrupt_handler.handler(), + ); + } + + // Enable interrupts in the peripheral. + let channel_number = 0; // TODO: Get from self.channel + DMA::regs() + .ch(channel_number) + .out_int() + .ena() + .modify(|_, w| { + w.out_total_eof().bit(true); + w.out_dscr_err().bit(true); + w.out_eof().bit(true); + w.out_done().bit(true); + w + }); + SPI2::regs().dma_int_ena().modify(|_, w| { + w.slv_rd_dma_done().bit(true); + w.slv_wr_dma_done().bit(true); + w.dma_seg_trans_done().bit(true); + w.trans_done().bit(true); + w + }); + + for window_index in 0..self.windows_len { + // Descriptors are initialized by `DmaTxBuf::new`. + let buffer_src_window = + &mut self.buffer_src[window_index * self.window_size..][..self.window_size]; + + Self::linear_descriptors_set_buffer(src_descs, buffer_src_window, |_| {}); + Self::linear_descriptors_set_buffer(bounce_dst_descs, buffer_src_window, |desc| { + desc.reset_for_rx(); + }); + + // let (channel_rx, channel_tx) = self.channel.split(); + + { + // Extend the lifetime to 'static because it is required by Mem2Mem. + // + // Safety: + // Pointees are done being used by the driver before this scope ends, + // this is because we `SimpleMem2MemTransfer::wait()` on the transfer to finish. + let bounce_dst_descs = unsafe { &mut *(bounce_dst_descs as *mut _) }; + let src_descs = unsafe { &mut *(src_descs as *mut _) }; + let mut mem2mem = Mem2Mem::new(self.channel.reborrow(), self.peripheral.reborrow()) + .with_descriptors(bounce_dst_descs, src_descs, self.burst_config) + .unwrap(); + let transfer = mem2mem + .start_transfer(&mut self.bounce_buffer_dst, buffer_src_window) + .unwrap(); + + let int_raw = DMA::regs() + .ch(channel_number as usize) + .out_int() + .raw() + .read(); + log::error!( + indoc! {" + int_raw: + total_eof: {total_eof} + eof: {eof} + done: {done} + dscr_err: {dscr_err} + "}, + total_eof = int_raw.out_total_eof().bit_is_set(), + eof = int_raw.out_eof().bit_is_set(), + done = int_raw.out_done().bit_is_set(), + dscr_err = int_raw.out_dscr_err().bit_is_set(), + ); + error!("int_raw_msg: 0x{:08x?}", INT_CHANNEL.try_receive()); + + transfer.wait().unwrap(); + } + + let int_raw = DMA::regs() + .ch(channel_number as usize) + .out_int() + .raw() + .read(); + log::error!( + indoc! {" + int_raw: + total_eof: {total_eof} + eof: {eof} + done: {done} + dscr_err: {dscr_err} + "}, + total_eof = int_raw.out_total_eof().bit_is_set(), + eof = int_raw.out_eof().bit_is_set(), + done = int_raw.out_done().bit_is_set(), + dscr_err = int_raw.out_dscr_err().bit_is_set(), + ); + error!("int_raw_msg: 0x{:08x?}", INT_CHANNEL.try_receive()); + + // TODO: Get rid of this! + assert_eq!(&*self.bounce_buffer_dst, buffer_src_window); + } + } +} + +static INT_CHANNEL: Channel = Channel::new(); + +#[handler] +#[ram] // Improves performance. +fn dma_interrupt_handler() { + let int_raw = DMA::regs().ch(0).out_int().raw().read(); + + INT_CHANNEL.try_send(int_raw.bits()).unwrap(); + + // let lcd_cam = unsafe { &*esp_hal::peripherals::LCD_CAM::PTR }; + + // // Check and clear VSYNC interrupt + // if lcd_cam + // .lc_dma_int_raw() + // .read() + // .lcd_vsync_int_raw() + // .bit_is_set() + // { + // lcd_cam + // .lc_dma_int_clr() + // .write(|w| w.lcd_vsync_int_clr().set_bit()); + + // INT_CHANNEL.send(); + // // VSYNC_SIGNAL.signal(()); + + // // Signal the event + // // critical_section::with(|cs| { + // // *VSYNC_FLAG.borrow_ref_mut(cs) = true; + // // }); + // } +} + +// unsafe impl DmaTxBuffer for DmaTxStreamBuf { +// type View = Self; +// type Final = Self; + +// fn prepare(&mut self) -> dma::Preparation { +// dma::Preparation { +// start: (), +// direction: (), +// accesses_psram: false, +// burst_transfer: (), +// check_owner: (), +// auto_write_back: (), +// } +// } + +// fn into_view(self) -> Self::View { +// self +// } + +// fn from_view(view: Self::View) -> Self::Final { +// view +// } +// } + +pub async fn run_lcd( + mut st7701s: St7701s<'static, Blocking>, + framebuffer: &'static mut Framebuffer, +) { + loop { + // Timer::after(Duration::from_millis(100)).await; + // yield_now().await; + SIGNAL_LCD_SUBMIT.wait().await; + + // TODO: Use bounce buffers: + // https://docs.espressif.com/projects/esp-idf/en/v5.0/esp32s3/api-reference/peripherals/lcd.html#bounce-buffer-with-single-psram-frame-buffer + // This can be implemented as a `DmaTxBuffer`. + let transfer = match st7701s.dpi.send(false, framebuffer.dma_buf.take().unwrap()) { + Err((error, result_dpi, result_dma_buf)) => { + error!( + "An error occurred while initiating transfer of the framebuffer to the LCD display: {error:?}" + ); + st7701s.dpi = result_dpi; + framebuffer.dma_buf = Some(result_dma_buf); + continue; + } + Ok(transfer) => transfer, + }; + + // This could be used to allow other tasks to be executed on the first core, but that causes + // the flash to be accessed, which interferes with the framebuffer transfer. + // For that reason, it is disabled, and this task blocks the first core, until the transfer + // is complete. + #[cfg(not(feature = "limit-fps"))] + while !transfer.is_done() { + // Timer::after_millis(1).await; + rmk::embassy_futures::yield_now().await; + } + + let result; + let dma_buf; + (result, st7701s.dpi, dma_buf) = transfer.wait(); + framebuffer.dma_buf = Some(dma_buf); + + SIGNAL_UI_RENDER.signal(()); + + if let Err(error) = result { + error!( + "An error occurred while transferring framebuffer to the LCD display: {error:?}" + ); + } + } +} + +pub struct Framebuffer { + pub width: u32, + pub height: u32, + pub dma_buf: Option, +} + +/// Allocates a buffer appropriately aligned for use with DMA. +pub fn allocate_dma_buffer_in(len: usize, alloc: A) -> Box<[u8], A> { + const DMA_ALIGNMENT: usize = 32; + + assert_eq!( + len % DMA_ALIGNMENT, + 0, + "the size of a DMA buffer must be a multiple of {DMA_ALIGNMENT} bytes, but it is {len} bytes large" + ); + + // ⚠️ Note: For chips that support DMA to/from PSRAM (ESP32-S3) DMA transfers to/from PSRAM + // have extra alignment requirements. The address and size of the buffer pointed to by each + // descriptor must be a multiple of the cache line (block) size. This is 32 bytes on ESP32-S3. + // That is ensured by the `assert_eq` preceding this block. + unsafe { + let raw = alloc + .allocate_zeroed(Layout::from_size_align(len, DMA_ALIGNMENT).unwrap()) + .expect("failed to allocate a DMA buffer"); + Box::from_raw_in(raw.as_ptr(), alloc) + } +} + +impl Framebuffer { + pub fn new(width: u32, height: u32) -> Self { + let buffer_len = width as usize * height as usize * core::mem::size_of::(); + let buffer = allocate_dma_buffer_in(buffer_len, &PSRAM_ALLOCATOR); + let burst_config: BurstConfig = ExternalBurstConfig::Size16.into(); + + info!( + "PSRAM SPI burst config: max_compatible_chunk_size={}", + burst_config.max_compatible_chunk_size() + ); + let dma_buf_descs_len = esp_hal::dma::descriptor_count( + buffer_len, + burst_config.max_compatible_chunk_size(), + false, + ); + // Descriptors are initialized by `DmaTxBuf::new`. + let dma_buf_descs = vec![DmaDescriptor::EMPTY; dma_buf_descs_len].into_boxed_slice(); + // We just leak the buffers. + let dma_buf = DmaTxBuf::new(Box::leak(dma_buf_descs), Box::leak(buffer)).unwrap(); + + Self { + width, + height, + dma_buf: Some(dma_buf), + } + } + + pub fn as_target_pixels(&mut self) -> &mut [Rgb565Pixel] { + bytemuck::cast_slice_mut::<_, Rgb565Pixel>(self.dma_buf.as_mut().unwrap().as_mut_slice()) + } +} diff --git a/firmware/acid-firmware/src/ui/mod.rs b/firmware/acid-firmware/src/ui/mod.rs index a6f9392..fe26b73 100644 --- a/firmware/acid-firmware/src/ui/mod.rs +++ b/firmware/acid-firmware/src/ui/mod.rs @@ -48,6 +48,7 @@ use crate::{ }; pub mod backend; +pub mod dpi; pub mod messages; pub mod storage; pub mod window_adapter;