From bb2c84b1534d93420c7981b4a479e4612f24b808 Mon Sep 17 00:00:00 2001 From: cepetr Date: Tue, 16 Apr 2024 15:53:13 +0200 Subject: [PATCH] feat(core): introduce new drawing library [no changelog] --- core/embed/rust/Cargo.lock | 37 + core/embed/rust/Cargo.toml | 11 + core/embed/rust/build.rs | 31 + core/embed/rust/rust_ui_bootloader.h | 4 + core/embed/rust/src/trezorhal/bitblt.rs | 216 +++++ core/embed/rust/src/trezorhal/display.rs | 20 +- core/embed/rust/src/trezorhal/mod.rs | 2 + core/embed/rust/src/ui/display/color.rs | 2 +- core/embed/rust/src/ui/display/font.rs | 45 +- core/embed/rust/src/ui/geometry.rs | 3 + core/embed/rust/src/ui/mod.rs | 1 + core/embed/rust/src/ui/shape/algo/blur.rs | 357 +++++++ core/embed/rust/src/ui/shape/algo/circle.rs | 76 ++ core/embed/rust/src/ui/shape/algo/line.rs | 94 ++ core/embed/rust/src/ui/shape/algo/mod.rs | 9 + core/embed/rust/src/ui/shape/algo/trigo.rs | 29 + core/embed/rust/src/ui/shape/bar.rs | 128 +++ core/embed/rust/src/ui/shape/base.rs | 53 ++ .../rust/src/ui/shape/bitmap/bitmap_base.rs | 313 ++++++ core/embed/rust/src/ui/shape/bitmap/mod.rs | 6 + core/embed/rust/src/ui/shape/bitmap/mono8.rs | 48 + core/embed/rust/src/ui/shape/bitmap/rgb565.rs | 47 + .../rust/src/ui/shape/bitmap/rgba8888.rs | 47 + core/embed/rust/src/ui/shape/blur.rs | 45 + .../rust/src/ui/shape/cache/blur_cache.rs | 59 ++ .../rust/src/ui/shape/cache/drawing_cache.rs | 138 +++ .../rust/src/ui/shape/cache/jpeg_cache.rs | 372 ++++++++ core/embed/rust/src/ui/shape/cache/mod.rs | 7 + .../rust/src/ui/shape/cache/zlib_cache.rs | 173 ++++ core/embed/rust/src/ui/shape/canvas/common.rs | 887 ++++++++++++++++++ core/embed/rust/src/ui/shape/canvas/mod.rs | 11 + core/embed/rust/src/ui/shape/canvas/mono8.rs | 105 +++ core/embed/rust/src/ui/shape/canvas/rgb565.rs | 128 +++ .../rust/src/ui/shape/canvas/rgba8888.rs | 120 +++ .../rust/src/ui/shape/canvas/viewport.rs | 103 ++ core/embed/rust/src/ui/shape/circle.rs | 139 +++ .../rust/src/ui/shape/display/fake_display.rs | 12 + .../rust/src/ui/shape/display/fb_mono8.rs | 55 ++ .../rust/src/ui/shape/display/fb_rgb565.rs | 62 ++ .../rust/src/ui/shape/display/fb_rgba8888.rs | 62 ++ .../embed/rust/src/ui/shape/display/memory.md | 58 ++ core/embed/rust/src/ui/shape/display/mod.rs | 40 + .../rust/src/ui/shape/display/nofb_rgb565.rs | 110 +++ .../ui/shape/drawlib-rust-objects.drawio.svg | 4 + .../ui/shape/drawlib-toplevel-arch.drawio.svg | 4 + core/embed/rust/src/ui/shape/jpeg.rs | 196 ++++ core/embed/rust/src/ui/shape/mod.rs | 33 + core/embed/rust/src/ui/shape/qrcode.rs | 169 ++++ core/embed/rust/src/ui/shape/render.rs | 250 +++++ core/embed/rust/src/ui/shape/text.rs | 134 +++ core/embed/rust/src/ui/shape/toif.rs | 174 ++++ core/embed/rust/src/ui/util.rs | 6 + 52 files changed, 5226 insertions(+), 9 deletions(-) create mode 100644 core/embed/rust/src/trezorhal/bitblt.rs create mode 100644 core/embed/rust/src/ui/shape/algo/blur.rs create mode 100644 core/embed/rust/src/ui/shape/algo/circle.rs create mode 100644 core/embed/rust/src/ui/shape/algo/line.rs create mode 100644 core/embed/rust/src/ui/shape/algo/mod.rs create mode 100644 core/embed/rust/src/ui/shape/algo/trigo.rs create mode 100644 core/embed/rust/src/ui/shape/bar.rs create mode 100644 core/embed/rust/src/ui/shape/base.rs create mode 100644 core/embed/rust/src/ui/shape/bitmap/bitmap_base.rs create mode 100644 core/embed/rust/src/ui/shape/bitmap/mod.rs create mode 100644 core/embed/rust/src/ui/shape/bitmap/mono8.rs create mode 100644 core/embed/rust/src/ui/shape/bitmap/rgb565.rs create mode 100644 core/embed/rust/src/ui/shape/bitmap/rgba8888.rs create mode 100644 core/embed/rust/src/ui/shape/blur.rs create mode 100644 core/embed/rust/src/ui/shape/cache/blur_cache.rs create mode 100644 core/embed/rust/src/ui/shape/cache/drawing_cache.rs create mode 100644 core/embed/rust/src/ui/shape/cache/jpeg_cache.rs create mode 100644 core/embed/rust/src/ui/shape/cache/mod.rs create mode 100644 core/embed/rust/src/ui/shape/cache/zlib_cache.rs create mode 100644 core/embed/rust/src/ui/shape/canvas/common.rs create mode 100644 core/embed/rust/src/ui/shape/canvas/mod.rs create mode 100644 core/embed/rust/src/ui/shape/canvas/mono8.rs create mode 100644 core/embed/rust/src/ui/shape/canvas/rgb565.rs create mode 100644 core/embed/rust/src/ui/shape/canvas/rgba8888.rs create mode 100644 core/embed/rust/src/ui/shape/canvas/viewport.rs create mode 100644 core/embed/rust/src/ui/shape/circle.rs create mode 100644 core/embed/rust/src/ui/shape/display/fake_display.rs create mode 100644 core/embed/rust/src/ui/shape/display/fb_mono8.rs create mode 100644 core/embed/rust/src/ui/shape/display/fb_rgb565.rs create mode 100644 core/embed/rust/src/ui/shape/display/fb_rgba8888.rs create mode 100644 core/embed/rust/src/ui/shape/display/memory.md create mode 100644 core/embed/rust/src/ui/shape/display/mod.rs create mode 100644 core/embed/rust/src/ui/shape/display/nofb_rgb565.rs create mode 100644 core/embed/rust/src/ui/shape/drawlib-rust-objects.drawio.svg create mode 100644 core/embed/rust/src/ui/shape/drawlib-toplevel-arch.drawio.svg create mode 100644 core/embed/rust/src/ui/shape/jpeg.rs create mode 100644 core/embed/rust/src/ui/shape/mod.rs create mode 100644 core/embed/rust/src/ui/shape/qrcode.rs create mode 100644 core/embed/rust/src/ui/shape/render.rs create mode 100644 core/embed/rust/src/ui/shape/text.rs create mode 100644 core/embed/rust/src/ui/shape/toif.rs diff --git a/core/embed/rust/Cargo.lock b/core/embed/rust/Cargo.lock index 9cbd4f828..c2149e641 100644 --- a/core/embed/rust/Cargo.lock +++ b/core/embed/rust/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "alloc-traits" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b2d54853319fd101b8dd81de382bcbf3e03410a64d8928bbee85a3e7dcde483" + [[package]] name = "autocfg" version = "1.1.0" @@ -279,6 +285,15 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "static-alloc" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "570b7e840addf99f80c5b26abba410e21537002316fc82f2747fd87c171e9d7e" +dependencies = [ + "alloc-traits", +] + [[package]] name = "syn" version = "1.0.80" @@ -309,8 +324,11 @@ dependencies = [ "qrcodegen", "serde_json", "spin", + "static-alloc", "trezor-tjpgdec", "ufmt", + "unsize", + "without-alloc", "zeroize", ] @@ -347,6 +365,15 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" +[[package]] +name = "unsize" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fa7a7a734c1a5664a662ddcea0b6c9472a21da8888c957c7f1eaa09dba7a939" +dependencies = [ + "autocfg", +] + [[package]] name = "winapi" version = "0.3.9" @@ -369,6 +396,16 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "without-alloc" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "375db0478b203b950ef10d1cce23cdbe5f30c2454fd9e7673ff56656df23adbb" +dependencies = [ + "alloc-traits", + "unsize", +] + [[package]] name = "zeroize" version = "1.7.0" diff --git a/core/embed/rust/Cargo.toml b/core/embed/rust/Cargo.toml index 6454358e8..d66ab18c1 100644 --- a/core/embed/rust/Cargo.toml +++ b/core/embed/rust/Cargo.toml @@ -52,6 +52,8 @@ test = [ "micropython", "protobuf", "ui", + "ui_jpeg_decoder", + "ui_blurring", "dma2d", "touch", "backlight", @@ -113,6 +115,15 @@ version = "0.2.6" default-features = false features = ["nightly"] +[dependencies.static-alloc] +version = "0.2.4" + +[dependencies.without-alloc] +version = "0.2.2" + +[dependencies.unsize] +version = "1.1.0" + # Build dependencies [build-dependencies.bindgen] diff --git a/core/embed/rust/build.rs b/core/embed/rust/build.rs index 4bf8f574b..a534e1d20 100644 --- a/core/embed/rust/build.rs +++ b/core/embed/rust/build.rs @@ -110,6 +110,16 @@ fn prepare_bindings() -> bindgen::Builder { format!("-DTREZOR_BOARD=\"{}\"", board()).as_str(), ]); + #[cfg(feature = "xframebuffer")] + { + bindings = bindings.clang_args(&["-DXFRAMEBUFFER"]); + } + + #[cfg(feature = "new_rendering")] + { + bindings = bindings.clang_args(["-DNEW_RENDERING"]); + } + // Pass in correct include paths and defines. if is_firmware() { let mut clang_args: Vec<&str> = Vec::new(); @@ -338,6 +348,27 @@ fn generate_trezorhal_bindings() { .allowlist_var("DISPLAY_FRAMEBUFFER_OFFSET_Y") .allowlist_var("DISPLAY_RESX") .allowlist_var("DISPLAY_RESY") + .allowlist_type("display_fb_info_t") + .allowlist_function("display_get_frame_buffer") + .allowlist_function("display_fill") + .allowlist_function("display_copy_rgb565") + // gl_bitblt + .allowlist_type("gl_bitblt_t") + .allowlist_function("gl_rgb565_fill") + .allowlist_function("gl_rgb565_copy_mono4") + .allowlist_function("gl_rgb565_copy_rgb565") + .allowlist_function("gl_rgb565_blend_mono4") + .allowlist_function("gl_rgba8888_fill") + .allowlist_function("gl_rgba8888_copy_mono4") + .allowlist_function("gl_rgba8888_copy_rgb565") + .allowlist_function("gl_rgba8888_copy_rgba8888") + .allowlist_function("gl_rgba8888_blend_mono4") + .allowlist_function("gl_mono8_fill") + .allowlist_function("gl_mono8_copy_mono1p") + .allowlist_function("gl_mono8_copy_mono4") + .allowlist_function("gl_mono8_blend_mono1p") + .allowlist_function("gl_mono8_blend_mono4") + .allowlist_function("dma2d_wait") // fonts .allowlist_function("font_height") .allowlist_function("font_max_height") diff --git a/core/embed/rust/rust_ui_bootloader.h b/core/embed/rust/rust_ui_bootloader.h index 09613cf03..c69f700ac 100644 --- a/core/embed/rust/rust_ui_bootloader.h +++ b/core/embed/rust/rust_ui_bootloader.h @@ -24,3 +24,7 @@ void screen_boot_stage_1(bool fading); uint32_t screen_unlock_bootloader_confirm(void); void screen_unlock_bootloader_success(void); void bld_continue_label(uint16_t bg_color); +void screen_boot(bool warning, const char* vendor_str, size_t vendor_str_len, + uint32_t version, const void* vendor_img, + size_t vendor_img_len, int wait); + diff --git a/core/embed/rust/src/trezorhal/bitblt.rs b/core/embed/rust/src/trezorhal/bitblt.rs new file mode 100644 index 000000000..49eef2751 --- /dev/null +++ b/core/embed/rust/src/trezorhal/bitblt.rs @@ -0,0 +1,216 @@ +use super::ffi; + +use crate::ui::{ + display::Color, + geometry::Rect, + shape::{Bitmap, BitmapFormat, BitmapView}, +}; + +pub type BitBlt = ffi::gl_bitblt_t; + +impl Default for BitBlt { + fn default() -> Self { + Self { + width: 0, + height: 0, + dst_row: core::ptr::null_mut(), + dst_stride: 0, + dst_x: 0, + dst_y: 0, + src_row: core::ptr::null_mut(), + src_bg: 0, + src_fg: 0, + src_stride: 0, + src_x: 0, + src_y: 0, + src_alpha: 255, + } + } +} + +impl BitBlt { + pub fn new_fill(r: Rect, clip: Rect, color: Color, alpha: u8) -> Option { + let r = r.clamp(clip); + if !r.is_empty() { + Some( + Self::default() + .with_rect(r) + .with_fg(color) + .with_alpha(alpha), + ) + } else { + None + } + } + + pub fn new_copy(r: Rect, clip: Rect, src: &BitmapView) -> Option { + let mut offset = src.offset; + let mut r_dst = r; + + // Normalize negative x & y-offset of the bitmap + if offset.x < 0 { + r_dst.x0 -= offset.x; + offset.x = 0; + } + + if offset.y < 0 { + r_dst.y0 -= offset.y; + offset.y = 0; + } + + // Clip with the canvas viewport + let mut r = r_dst.clamp(clip); + + // Clip with the bitmap top-left + if r.x0 > r_dst.x0 { + offset.x += r.x0 - r_dst.x0; + } + + if r.y0 > r_dst.y0 { + offset.y += r.y0 - r_dst.y0; + } + + // Clip with the bitmap size + r.x1 = core::cmp::min(r.x0 + src.size().x - offset.x, r.x1); + r.y1 = core::cmp::min(r.y0 + src.size().y - offset.y, r.y1); + + if !r.is_empty() { + Some( + BitBlt::default() + .with_rect(r) + .with_src(src.bitmap, offset.x, offset.y) + .with_bg(src.bg_color) + .with_fg(src.fg_color), + ) + } else { + None + } + } + + pub fn with_dst(self, dst: &mut Bitmap) -> Self { + Self { + dst_row: unsafe { dst.row_ptr(self.dst_y) }, + dst_stride: dst.stride() as u16, + ..self + } + } + + fn with_rect(self, r: Rect) -> Self { + Self { + width: r.width() as u16, + height: r.height() as u16, + dst_x: r.x0 as u16, + dst_y: r.y0 as u16, + ..self + } + } + + fn with_src(self, bitmap: &Bitmap, x: i16, y: i16) -> Self { + let bitmap_stride = match bitmap.format() { + BitmapFormat::MONO1P => bitmap.width() as u16, // packed bits + _ => bitmap.stride() as u16, + }; + + Self { + src_row: unsafe { bitmap.row_ptr(y as u16) }, + src_stride: bitmap_stride, + src_x: x as u16, + src_y: y as u16, + ..self + } + } + + fn with_fg(self, fg_color: Color) -> Self { + Self { + src_fg: fg_color.into(), + ..self + } + } + + fn with_bg(self, bg_color: Color) -> Self { + Self { + src_bg: bg_color.into(), + ..self + } + } + + fn with_alpha(self, alpha: u8) -> Self { + Self { + src_alpha: alpha, + ..self + } + } + + pub fn wait_for_transfer() { + #[cfg(feature = "dma2d")] + unsafe { + ffi::dma2d_wait() + } + } + + pub unsafe fn rgb565_fill(&self) { + unsafe { ffi::gl_rgb565_fill(self) }; + } + + pub unsafe fn rgb565_copy_mono4(&self) { + unsafe { ffi::gl_rgb565_copy_mono4(self) }; + } + + pub unsafe fn rgb565_copy_rgb565(&self) { + unsafe { ffi::gl_rgb565_copy_rgb565(self) }; + } + + pub unsafe fn rgb565_blend_mono4(&self) { + unsafe { ffi::gl_rgb565_blend_mono4(self) }; + } + + pub unsafe fn rgba8888_fill(&self) { + unsafe { ffi::gl_rgba8888_fill(self) }; + } + + pub unsafe fn rgba8888_copy_mono4(&self) { + unsafe { ffi::gl_rgba8888_copy_mono4(self) }; + } + + pub unsafe fn rgba8888_copy_rgb565(&self) { + unsafe { ffi::gl_rgba8888_copy_rgb565(self) }; + } + + pub unsafe fn rgba8888_copy_rgba8888(&self) { + unsafe { ffi::gl_rgba8888_copy_rgba8888(self) }; + } + + pub unsafe fn rgba8888_blend_mono4(&self) { + unsafe { ffi::gl_rgba8888_blend_mono4(self) }; + } + + pub unsafe fn mono8_fill(&self) { + unsafe { ffi::gl_mono8_fill(self) }; + } + + pub unsafe fn mono8_copy_mono1p(&self) { + unsafe { ffi::gl_mono8_copy_mono1p(self) }; + } + + pub unsafe fn mono8_copy_mono4(&self) { + unsafe { ffi::gl_mono8_copy_mono4(self) }; + } + + pub unsafe fn mono8_blend_mono1p(&self) { + unsafe { ffi::gl_mono8_blend_mono1p(self) }; + } + + pub unsafe fn mono8_blend_mono4(&self) { + unsafe { ffi::gl_mono8_blend_mono4(self) }; + } + + #[cfg(feature = "new_rendering")] + pub unsafe fn display_fill(&self) { + unsafe { ffi::display_fill(self) }; + } + + #[cfg(feature = "new_rendering")] + pub unsafe fn display_copy_rgb565(&self) { + unsafe { ffi::display_copy_rgb565(self) }; + } +} diff --git a/core/embed/rust/src/trezorhal/display.rs b/core/embed/rust/src/trezorhal/display.rs index 95a17350b..b2713f695 100644 --- a/core/embed/rust/src/trezorhal/display.rs +++ b/core/embed/rust/src/trezorhal/display.rs @@ -13,11 +13,11 @@ pub use ffi::{ #[cfg(all(feature = "framebuffer", not(feature = "framebuffer32bit")))] #[derive(Copy, Clone)] -pub struct FrameBuffer(*mut u16); +pub struct FrameBuffer(pub *mut u16); #[cfg(all(feature = "framebuffer", feature = "framebuffer32bit"))] #[derive(Copy, Clone)] -pub struct FrameBuffer(*mut u32); +pub struct FrameBuffer(pub *mut u32); pub fn backlight(val: i32) -> i32 { unsafe { ffi::display_backlight(val) } @@ -98,7 +98,9 @@ pub fn get_fb_addr() -> FrameBuffer { #[inline(always)] #[cfg(all(not(feature = "framebuffer"), feature = "disp_i8080_8bit_dw"))] +#[allow(unused_variables)] pub fn pixeldata(c: u16) { + #[cfg(not(feature = "new_rendering"))] unsafe { ffi::DISPLAY_DATA_ADDRESS.write_volatile((c & 0xff) as u8); ffi::DISPLAY_DATA_ADDRESS.write_volatile((c >> 8) as u8); @@ -178,3 +180,17 @@ pub fn clear() { ffi::display_clear(); } } + +#[cfg(feature = "xframebuffer")] +pub fn get_frame_buffer() -> (&'static mut [u8], usize) { + let fb_info = unsafe { ffi::display_get_frame_buffer() }; + + let fb = unsafe { + core::slice::from_raw_parts_mut( + fb_info.ptr as *mut u8, + DISPLAY_RESY as usize * fb_info.stride, + ) + }; + + (fb, fb_info.stride) +} diff --git a/core/embed/rust/src/trezorhal/mod.rs b/core/embed/rust/src/trezorhal/mod.rs index 39de95151..5175d262d 100644 --- a/core/embed/rust/src/trezorhal/mod.rs +++ b/core/embed/rust/src/trezorhal/mod.rs @@ -2,6 +2,7 @@ pub mod bip39; #[macro_use] #[allow(unused_macros)] pub mod fatal_error; +pub mod bitblt; #[cfg(feature = "ui")] pub mod display; #[cfg(feature = "dma2d")] @@ -9,6 +10,7 @@ pub mod dma2d; mod ffi; #[cfg(feature = "haptic")] pub mod haptic; + pub mod io; pub mod model; pub mod random; diff --git a/core/embed/rust/src/ui/display/color.rs b/core/embed/rust/src/ui/display/color.rs index 945eba3ee..4ad86389c 100644 --- a/core/embed/rust/src/ui/display/color.rs +++ b/core/embed/rust/src/ui/display/color.rs @@ -98,7 +98,7 @@ impl Color { let r = (fg.r() as u16) * fg_mul + (self.r() as u16) * bg_mul; let g = (fg.g() as u16) * fg_mul + (self.g() as u16) * bg_mul; let b = (fg.b() as u16) * fg_mul + (self.b() as u16) * bg_mul; - Color::rgb((r >> 8) as u8, (g >> 8) as u8, (b >> 8) as u8) + Color::rgb((r / 255) as u8, (g / 255) as u8, (b / 255) as u8) } } diff --git a/core/embed/rust/src/ui/display/font.rs b/core/embed/rust/src/ui/display/font.rs index b88953b98..a466fce3c 100644 --- a/core/embed/rust/src/ui/display/font.rs +++ b/core/embed/rust/src/ui/display/font.rs @@ -3,6 +3,7 @@ use crate::{ ui::{ constant, geometry::{Offset, Point, Rect}, + shape::{Bitmap, BitmapFormat}, }, }; use core::slice; @@ -41,12 +42,12 @@ impl Glyph { let width = *data.offset(0) as i16; let height = *data.offset(1) as i16; - let data_bits = constant::FONT_BPP * width * height; - - let data_bytes = if data_bits % 8 == 0 { - data_bits / 8 - } else { - (data_bits / 8) + 1 + let data_bytes = match constant::FONT_BPP { + 1 => (width * height + 7) / 8, // packed bits + 2 => (width * height + 3) / 4, // packed bits + 4 => (width + 1) / 2 * height, // row aligned to bytes + 8 => width * height, + _ => panic!(), }; Glyph { @@ -119,6 +120,28 @@ impl Glyph { _ => 0, } } + + pub fn bitmap(&self) -> Bitmap<'static> { + match constant::FONT_BPP { + 1 => unwrap!(Bitmap::new( + BitmapFormat::MONO1P, + None, + Offset::new(self.width, self.height), + None, + self.data, + )), + 2 => panic!(), + 4 => unwrap!(Bitmap::new( + BitmapFormat::MONO4, + None, + Offset::new(self.width, self.height), + None, + self.data, + )), + 8 => panic!(), + _ => panic!(), + } + } } /// Font constants. Keep in sync with FONT_ definitions in @@ -240,6 +263,16 @@ impl Font { constant::LINE_SPACE + self.text_height() } + // Returns x-coordinate of the text start (including left bearing) + pub fn horz_center(&self, start: i16, end: i16, text: &str) -> i16 { + (start + end - self.visible_text_width(text)) / 2 - self.start_x_bearing(text) + } + + // Returns y-coordinate of the text baseline + pub fn vert_center(&self, start: i16, end: i16, text: &str) -> i16 { + (start + end + self.visible_text_height(text)) / 2 + } + pub fn get_glyph(self, ch: char) -> Glyph { let gl_data = display::get_char_glyph(ch as u16, self.into()); diff --git a/core/embed/rust/src/ui/geometry.rs b/core/embed/rust/src/ui/geometry.rs index d73702340..a5a7d8a2b 100644 --- a/core/embed/rust/src/ui/geometry.rs +++ b/core/embed/rust/src/ui/geometry.rs @@ -545,6 +545,7 @@ pub enum Alignment { End, } +#[derive(Copy, Clone)] pub struct Alignment2D(pub Alignment, pub Alignment); impl Alignment2D { @@ -552,6 +553,8 @@ impl Alignment2D { pub const TOP_RIGHT: Alignment2D = Alignment2D(Alignment::End, Alignment::Start); pub const TOP_CENTER: Alignment2D = Alignment2D(Alignment::Center, Alignment::Start); pub const CENTER: Alignment2D = Alignment2D(Alignment::Center, Alignment::Center); + pub const CENTER_LEFT: Alignment2D = Alignment2D(Alignment::Start, Alignment::Center); + pub const CENTER_RIGHT: Alignment2D = Alignment2D(Alignment::End, Alignment::Center); pub const BOTTOM_LEFT: Alignment2D = Alignment2D(Alignment::Start, Alignment::End); pub const BOTTOM_RIGHT: Alignment2D = Alignment2D(Alignment::End, Alignment::End); pub const BOTTOM_CENTER: Alignment2D = Alignment2D(Alignment::Center, Alignment::End); diff --git a/core/embed/rust/src/ui/mod.rs b/core/embed/rust/src/ui/mod.rs index fd8c51dac..b08f77789 100644 --- a/core/embed/rust/src/ui/mod.rs +++ b/core/embed/rust/src/ui/mod.rs @@ -8,6 +8,7 @@ pub mod display; pub mod event; pub mod geometry; pub mod lerp; +pub mod shape; #[macro_use] pub mod util; diff --git a/core/embed/rust/src/ui/shape/algo/blur.rs b/core/embed/rust/src/ui/shape/algo/blur.rs new file mode 100644 index 000000000..25270bf94 --- /dev/null +++ b/core/embed/rust/src/ui/shape/algo/blur.rs @@ -0,0 +1,357 @@ +use crate::ui::geometry::Offset; +/// This is a simple and fast blurring algorithm that uses a box filter - +/// a square kernel with all coefficients set to 1. +/// +/// The `BlurFilter` structure holds the context of a simple 2D window averaging +/// filter - a sliding window and the sum of all rows in the sliding window. +/// +/// The `BlurFilter` implements only five public functions - `new`, `push`, +/// `push_read`, `pop` and `pop_ready`. +/// +/// The `new()` function creates a blur filter context. +/// - The `size` argument specifies the size of the blurred area. +/// - The `radius` argument specifies the length of the kernel side. +/// +/// ```rust +/// let blur = BlurFilter::new(size, radius); +/// ``` +/// +/// The `push_ready()` function returns the row from the source bitmap +/// needed to be pushed +/// +/// The `push()` function pushes source row data into the sliding window and +/// performs all necessary calculations. +/// +/// ```rust +/// if let Some(y) = blur.push_ready() { +/// blur.push(&src_bitmap.row(y)[x0..x1]); +/// } +/// ``` +/// +/// The `pop_ready()` function returns the row from the destination bitmap +/// that can be popped out +/// +/// The `pop()` function pops the blurred row from the sliding window. +/// +/// ```rust +/// if let Some(y) = blur.pop_ready() { +/// blur.pop(&mut dst_bitmap.row(y)[x0..x1]); +/// } +/// ``` +use core::mem::size_of; + +const MAX_RADIUS: usize = 4; +const MAX_SIDE: usize = 1 + MAX_RADIUS * 2; +const MAX_WIDTH: usize = 240; + +pub type BlurBuff = [u8; MAX_WIDTH * (MAX_SIDE * 3 + size_of::() * 3) + 8]; + +type PixelColor = u16; + +#[derive(Default, Copy, Clone)] +struct Rgb { + pub r: T, + pub g: T, + pub b: T, +} + +impl Rgb { + #[inline(always)] + fn mulshift(&self, multiplier: u32, shift: u8) -> Rgb { + Rgb:: { + r: ((self.r as u32 * multiplier) >> shift) as u8, + g: ((self.g as u32 * multiplier) >> shift) as u8, + b: ((self.b as u32 * multiplier) >> shift) as u8, + } + } +} + +impl From for Rgb { + #[inline(always)] + fn from(value: u16) -> Rgb { + Rgb:: { + r: (value >> 8) & 0xF8, + g: (value >> 3) & 0xFC, + b: (value << 3) & 0xF8, + } + } +} + +impl core::ops::AddAssign for Rgb { + #[inline(always)] + fn add_assign(&mut self, rhs: u16) { + let rgb: Rgb = rhs.into(); + *self += rgb; + } +} + +impl core::ops::SubAssign for Rgb { + #[inline(always)] + fn sub_assign(&mut self, rhs: u16) { + let rgb: Rgb = rhs.into(); + *self -= rgb; + } +} + +impl core::ops::AddAssign for Rgb { + #[inline(always)] + fn add_assign(&mut self, rhs: Self) { + self.r += rhs.r; + self.g += rhs.g; + self.b += rhs.b; + } +} + +impl core::ops::SubAssign for Rgb { + #[inline(always)] + fn sub_assign(&mut self, rhs: Rgb) { + self.r -= rhs.r; + self.g -= rhs.g; + self.b -= rhs.b; + } +} + +impl From> for u16 { + #[inline(always)] + fn from(value: Rgb) -> u16 { + let r = (value.r as u16 & 0xF8) << 8; + let g = (value.g as u16 & 0xFC) << 3; + let b = (value.b as u16 & 0xF8) >> 3; + r | g | b + } +} + +impl From> for Rgb { + #[inline(always)] + fn from(value: Rgb) -> Rgb { + Rgb:: { + r: value.r as u8, + g: value.g as u8, + b: value.b as u8, + } + } +} + +impl core::ops::AddAssign> for Rgb { + #[inline(always)] + fn add_assign(&mut self, rhs: Rgb) { + self.r += rhs.r as u16; + self.g += rhs.g as u16; + self.b += rhs.b as u16; + } +} + +impl core::ops::SubAssign> for Rgb { + #[inline(always)] + fn sub_assign(&mut self, rhs: Rgb) { + self.r -= rhs.r as u16; + self.g -= rhs.g as u16; + self.b -= rhs.b as u16; + } +} + +pub struct BlurAlgorithm<'a> { + size: Offset, + radius: usize, + row: usize, + totals: &'a mut [Rgb], + window: &'a mut [Rgb], + row_count: usize, +} + +impl<'a> BlurAlgorithm<'a> { + /// Constraints: + /// width <= MAX_WIDTH + /// radius <= MAX_RADIUS + /// width >= radius + pub fn new(size: Offset, radius: usize, memory: &'a mut BlurBuff) -> Result { + assert!(size.x as usize <= MAX_WIDTH); + assert!(radius <= MAX_RADIUS); + assert!(size.x as usize > 2 * radius - 1); + + // Split buffer into two parts + let window_size = size.x as usize * (1 + radius * 2); + let (window_buff, total_buff) = + memory.split_at_mut(window_size * core::mem::size_of::>()); + + // Allocate `window` from the beginning of the buffer + let (_, window_buff, _) = unsafe { window_buff.align_to_mut() }; + if window_buff.len() < window_size { + return Err(()); + } + let window = &mut window_buff[..window_size]; + window.iter_mut().for_each(|it| *it = Rgb::::default()); + + // Allocate `totals` from the rest of the buffer + let (_, totals_buff, _) = unsafe { total_buff.align_to_mut() }; + if totals_buff.len() < size.x as usize { + return Err(()); + } + let totals = &mut totals_buff[..size.x as usize]; + totals.iter_mut().for_each(|it| *it = Rgb::::default()); + + Ok(Self { + size, + radius, + row: 0, + window, + totals, + row_count: 0, + }) + } + + /// Returns the length of the box filter side. + fn box_side(&self) -> usize { + 1 + self.radius * 2 + } + + /// Takes an input row and calculates the same-sized vector + /// as the floating average of n subsequent elements where n = 2 * radius + + /// 1. Finally, it stores it into the specifed row in the sliding + /// window. + fn average_to_row(&mut self, inp: &[PixelColor], row: usize) { + let radius = self.radius; + let offset = self.size.x as usize * row; + let row = &mut self.window[offset..offset + self.size.x as usize]; + + let mut sum = Rgb::::default(); + + let divisor = (radius * 2 + 1) as u16; + let shift = 10; + let multiplier = (1 << shift) as u32 / divisor as u32; + + // Prepare before averaging + for i in 0..radius { + sum += inp[0]; // Duplicate pixels on the left + sum += inp[i]; // Add first radius pixels + } + + // Process the first few pixels of the row + for i in 0..radius { + sum += inp[i + radius]; + row[i] = sum.mulshift(multiplier, shift); + sum -= inp[0]; + } + + // Process the inner part of the row + for i in radius..row.len() - radius { + sum += inp[i + radius]; + row[i] = sum.mulshift(multiplier, shift); + sum -= inp[i - radius]; + } + + // Process the last few pixels of the row + for i in (row.len() - radius)..row.len() { + sum += inp[inp.len() - 1]; + row[i] = sum.mulshift(multiplier, shift); + sum -= inp[i - radius]; // Duplicate pixels on the right + } + } + + /// Copy one row from the window to the another row. + fn copy_row(&mut self, from_row: usize, to_row: usize) { + let from_offset = self.size.x as usize * from_row; + let to_offset = self.size.x as usize * to_row; + for i in 0..self.size.x as usize { + self.window[to_offset + i] = self.window[from_offset + i]; + } + } + + /// Subtracts the specified row of sliding window from `totals[]`. + fn subtract_row(&mut self, row: usize) { + let offset = self.size.x as usize * row; + let row = &self.window[offset..offset + self.size.x as usize]; + + for (i, item) in row.iter().enumerate() { + self.totals[i] -= *item; + } + } + + /// Adds the specified row of sliding window to `totals[]`. + fn add_row(&mut self, row: usize) { + let offset = self.size.x as usize * row; + let row = &self.window[offset..offset + self.size.x as usize]; + + for (i, item) in row.iter().enumerate() { + self.totals[i] += *item; + } + } + + /// Pushes the most recently pushed row again. + fn push_last_row(&mut self) { + let to_row = self.row; + let from_row = if to_row > 0 { + to_row - 1 + } else { + self.box_side() - 1 + }; + + self.subtract_row(to_row); + self.copy_row(from_row, to_row); + self.add_row(to_row); + + self.row = (to_row + 1) % self.box_side(); + self.row_count += 1; + } + + /// Returns the index of the row needed to be pushed into. + pub fn push_ready(&self) -> Option { + let y = core::cmp::max(0, self.row_count as i16 - self.radius as i16); + if y < self.size.y { + Some(y) + } else { + None + } + } + + /// Takes the source row and pushes it into the sliding window. + pub fn push(&mut self, input: &[PixelColor]) { + let row = self.row; + + self.subtract_row(row); + self.average_to_row(input, row); + self.add_row(row); + + self.row = (row + 1) % self.box_side(); + self.row_count += 1; + + while self.row_count <= self.radius { + self.push_last_row(); + } + } + + /// Returns the index of row ready to be popped out. + pub fn pop_ready(&self) -> Option { + let y = self.row_count as i16 - self.box_side() as i16; + if y < 0 { + None + } else { + Some(y) + } + } + + /// Copies the current content of `totals[]` to the output buffer. + pub fn pop(&mut self, output: &mut [PixelColor], dim: Option) { + let divisor = match dim { + Some(dim) => { + if dim > 0 { + (self.box_side() as u16 * 255) / dim as u16 + } else { + 65535u16 + } + } + None => self.box_side() as u16, + }; + + let shift = 10; + let multiplier = (1 << shift) as u32 / divisor as u32; + + for (i, item) in output.iter_mut().enumerate() { + *item = self.totals[i].mulshift(multiplier, shift).into(); + } + + if self.push_ready().is_none() { + self.push_last_row(); + } + } +} diff --git a/core/embed/rust/src/ui/shape/algo/circle.rs b/core/embed/rust/src/ui/shape/algo/circle.rs new file mode 100644 index 000000000..2e56a295e --- /dev/null +++ b/core/embed/rust/src/ui/shape/algo/circle.rs @@ -0,0 +1,76 @@ +/// Iterator providing points for 1/8th of a circle (single octant) +/// +/// The iterator supplies coordinates of pixels relative to the +/// circle's center point, along with an alpha value in +/// the range (0..255), indicating the proportion of the pixel +/// that lies inside the circle. +/// +/// for p in circle_points(radius) { +/// println!("{}, {}", p.u, p.v); // coordinates <0,radius>.. +/// println!("{}", p.frac); // distance from the circle <0..255> +/// println!("{}", p.first); // `v` has changed +/// println!("{}", p.last); // next `v` will change +/// } +/// +/// `u` axis is the main and increments at each iteration. +/// +/// endpoint [t, t] or [t - 1, t] where t = radius * (1 / sqrt(2)) + +pub fn circle_points(radius: i16) -> CirclePoints { + CirclePoints { + radius, + u: 0, + v: radius, + t1: radius / 16, + first: true, + } +} + +pub struct CirclePoints { + radius: i16, + u: i16, + v: i16, + t1: i16, + first: bool, +} + +#[derive(Copy, Clone)] +pub struct CirclePointsItem { + pub u: i16, + pub v: i16, + pub frac: u8, + pub first: bool, + pub last: bool, +} + +impl Iterator for CirclePoints { + type Item = CirclePointsItem; + + fn next(&mut self) -> Option { + if self.v >= self.u { + let mut item = CirclePointsItem { + u: self.u, + v: self.v, + frac: 255 - ((self.t1 as i32 * 255) / self.radius as i32) as u8, + first: self.first, + last: false, + }; + + self.first = false; + self.u += 1; + self.t1 += self.u; + let t2 = self.t1 - self.v; + if t2 >= 0 { + self.t1 = t2; + self.v -= 1; + self.first = true; + } + + item.last = item.v != self.v; + + Some(item) + } else { + None + } + } +} diff --git a/core/embed/rust/src/ui/shape/algo/line.rs b/core/embed/rust/src/ui/shape/algo/line.rs new file mode 100644 index 000000000..eeae577cb --- /dev/null +++ b/core/embed/rust/src/ui/shape/algo/line.rs @@ -0,0 +1,94 @@ +/// Iterator providing points on a line (using bresenham's algorithm) +/// +/// The iterator supplies coordinates of pixels relative to the +/// line's start point. +/// +/// constraint: `du` >= `dv`, `start_u` < `du` +/// +/// for p in line_points(du, dv, start_u) { +/// println!("{}, {}", p.u, p.v); // coordinates <0,radius>.. +/// println!("{}", p.frac); // distance from the line <0..255> +/// println!("{}", p.first); // `v` has changed +/// println!("{}", p.last); // next `v` will change +/// } +/// +/// `u` axis is the main and increments at each iteration. + +pub fn line_points(du: i16, dv: i16, start_u: i16) -> LinePoints { + let mut d = 2 * du - 2 * dv; + let mut y = 0; + + for _ in 0..start_u { + if d <= 0 { + d += 2 * du - 2 * dv; + y += 1; + } else { + d -= 2 * dv; + } + } + + LinePoints { + du, + dv, + d, + u: start_u, + v: y, + first: true, + } +} + +pub struct LinePoints { + du: i16, + dv: i16, + d: i16, + u: i16, + v: i16, + first: bool, +} + +#[derive(Copy, Clone)] +pub struct LinePointsItem { + pub u: i16, + pub v: i16, + pub frac: u8, + pub first: bool, + pub last: bool, +} + +impl Iterator for LinePoints { + type Item = LinePointsItem; + + #[inline(always)] + fn next(&mut self) -> Option { + if self.u < self.du { + let frac = if self.dv < self.du { + 255 - ((self.d + 2 * self.dv - 1) as i32 * 255 / (2 * self.du - 1) as i32) as u8 + } else { + 0 + }; + + let next = LinePointsItem { + u: self.u, + v: self.v, + frac, + first: self.first, + last: self.d <= 0, + }; + + if self.d <= 0 { + self.d += 2 * self.du - 2 * self.dv; + self.v += 1; + self.first = true; + } else { + self.d -= 2 * self.dv; + self.first = false; + } + + self.u += 1; + + Some(next) + } else { + None + } + } +} diff --git a/core/embed/rust/src/ui/shape/algo/mod.rs b/core/embed/rust/src/ui/shape/algo/mod.rs new file mode 100644 index 000000000..d9e23288c --- /dev/null +++ b/core/embed/rust/src/ui/shape/algo/mod.rs @@ -0,0 +1,9 @@ +mod blur; +mod circle; +mod line; +mod trigo; + +pub use blur::{BlurAlgorithm, BlurBuff}; +pub use circle::circle_points; +pub use line::line_points; +pub use trigo::{sin_i16, PI4}; diff --git a/core/embed/rust/src/ui/shape/algo/trigo.rs b/core/embed/rust/src/ui/shape/algo/trigo.rs new file mode 100644 index 000000000..a18a7bf79 --- /dev/null +++ b/core/embed/rust/src/ui/shape/algo/trigo.rs @@ -0,0 +1,29 @@ +/// Integer representing an angle of 45 degress (PI/4). +// +// Changing this constant requires revisiting isin() algorithm +// (for higher values consider changing T type to i64 or f32) +pub const PI4: i16 = 45; + +/// Fast sine approximation. +/// +/// Returns mult * sin(angle). +/// +/// Angle must be in range <0..PI4>. +/// This function provides an error within +-1 for multiplier up to 500 +pub fn sin_i16(angle: i16, mult: i16) -> i16 { + assert!((0..=PI4).contains(&angle)); + assert!(mult <= 2500); + + type T = i32; + + // Based on polynomial x - x^3 / 6 + let x = angle as T; + + // Constants for the approximation + const K: f32 = (PI4 as f32) * 4.0 / core::f32::consts::PI; + const M: T = (6.0 * K * K + 0.5) as T; + const N: T = (6.0 * K * K * K + 0.5) as T; + + // Applying the approximation + (((M * x - x * x * x) * mult as T + N / 2) / N) as i16 +} diff --git a/core/embed/rust/src/ui/shape/bar.rs b/core/embed/rust/src/ui/shape/bar.rs new file mode 100644 index 000000000..b23623cde --- /dev/null +++ b/core/embed/rust/src/ui/shape/bar.rs @@ -0,0 +1,128 @@ +use crate::ui::{display::Color, geometry::Rect}; + +use super::{Canvas, DrawingCache, Renderer, Shape, ShapeClone}; + +use without_alloc::alloc::LocalAllocLeakExt; + +/// A shape for the rendering variuous type of rectangles. +pub struct Bar { + /// Rectangle position and dimenstion + area: Rect, + /// Foreground color (default None) + fg_color: Option, + /// Background color (default None) + bg_color: Option, + /// Thickness (default 0) + thickness: i16, + /// Corner radius (default 0) + radius: i16, + /// Alpha (default 255) + alpha: u8, +} + +impl Bar { + pub fn new(area: Rect) -> Self { + Self { + area, + fg_color: None, + bg_color: None, + thickness: 1, + radius: 0, + alpha: 255, + } + } + + pub fn with_fg(self, fg_color: Color) -> Self { + Self { + fg_color: Some(fg_color), + ..self + } + } + + pub fn with_bg(self, bg_color: Color) -> Self { + Self { + bg_color: Some(bg_color), + ..self + } + } + + pub fn with_radius(self, radius: i16) -> Self { + Self { radius, ..self } + } + + pub fn with_thickness(self, thickness: i16) -> Self { + Self { thickness, ..self } + } + + pub fn with_alpha(self, alpha: u8) -> Self { + Self { alpha, ..self } + } + + pub fn render<'s>(self, renderer: &mut impl Renderer<'s>) { + renderer.render_shape(self); + } +} + +impl Shape<'_> for Bar { + fn bounds(&self, _cache: &DrawingCache) -> Rect { + self.area + } + + fn cleanup(&mut self, _cache: &DrawingCache) {} + + fn draw(&mut self, canvas: &mut dyn Canvas, _cache: &DrawingCache) { + // NOTE: drawing of rounded bars without a background + // is not supported. If we needed it, we would have to + // introduce a new function in RgbCanvas. + + // TODO: panic! in unsupported scenarious + + let th = match self.fg_color { + Some(_) => self.thickness, + None => 0, + }; + + if self.radius == 0 { + if let Some(fg_color) = self.fg_color { + // outline + if th > 0 { + let r = self.area; + canvas.fill_rect(Rect { y1: r.y0 + th, ..r }, fg_color, self.alpha); + canvas.fill_rect(Rect { x1: r.x0 + th, ..r }, fg_color, self.alpha); + canvas.fill_rect(Rect { x0: r.x1 - th, ..r }, fg_color, self.alpha); + canvas.fill_rect(Rect { y0: r.y1 - th, ..r }, fg_color, self.alpha); + } + } + if let Some(bg_color) = self.bg_color { + // background + let bg_r = self.area.shrink(th); + canvas.fill_rect(bg_r, bg_color, self.alpha); + } + } else { + if let Some(fg_color) = self.fg_color { + if th > 0 { + if self.bg_color.is_some() { + canvas.fill_round_rect(self.area, self.radius, fg_color, self.alpha); + } else { + #[cfg(not(feature = "ui_antialiasing"))] + canvas.draw_round_rect(self.area, self.radius, fg_color); + } + } + } + if let Some(bg_color) = self.bg_color { + let bg_r = self.area.shrink(th); + canvas.fill_round_rect(bg_r, self.radius, bg_color, self.alpha); + } + } + } +} + +impl<'s> ShapeClone<'s> for Bar { + fn clone_at_bump<'alloc, T>(self, bump: &'alloc T) -> Option<&'alloc mut dyn Shape<'s>> + where + T: LocalAllocLeakExt<'alloc>, + { + let clone = bump.alloc_t::()?; + Some(clone.uninit.init(Bar { ..self })) + } +} diff --git a/core/embed/rust/src/ui/shape/base.rs b/core/embed/rust/src/ui/shape/base.rs new file mode 100644 index 000000000..59e0a9990 --- /dev/null +++ b/core/embed/rust/src/ui/shape/base.rs @@ -0,0 +1,53 @@ +use crate::ui::geometry::Rect; + +use super::{Canvas, DrawingCache}; + +use without_alloc::alloc::LocalAllocLeakExt; + +// ========================================================================== +// trait Shape +// ========================================================================== + +/// This trait is used internally by so-called Renderers - +/// `DirectRenderer` & `ProgressiveRederer`. +/// +/// All shapes (like `Bar`, `Text`, `Circle`, ...) that can be rendered +/// must implement `Shape` trait. +/// +/// `Shape` objects may use `DrawingCache` as a scratch-pad memory or for +/// caching expensive calculations results. +pub trait Shape<'s> { + /// Returns the smallest bounding rectangle containing whole parts of the + /// shape. + /// + /// The function is used by renderer for optimization if the shape + /// must be renderer or not. + fn bounds(&self, cache: &DrawingCache<'s>) -> Rect; + + /// Draws shape on the canvas. + fn draw(&mut self, canvas: &mut dyn Canvas, cache: &DrawingCache<'s>); + + /// The function should release all allocated resources needed + /// for shape drawing. + /// + /// It's called by renderer if the shape's draw() function won't be called + /// anymore. + fn cleanup(&mut self, cache: &DrawingCache<'s>); +} + +// ========================================================================== +// trait ShapeClone +// ========================================================================== + +/// All shapes (like `Bar`, `Text`, `Circle`, ...) that can be rendered +/// by `ProgressiveRender` must implement `ShapeClone`. +pub trait ShapeClone<'s> { + /// Clones a shape object at the specified memory bump. + /// + /// The method is used by `ProgressiveRenderer` to store shape objects for + /// deferred drawing. + fn clone_at_bump<'alloc, T>(self, bump: &'alloc T) -> Option<&'alloc mut dyn Shape<'s>> + where + T: LocalAllocLeakExt<'alloc>, + 'alloc: 's; +} diff --git a/core/embed/rust/src/ui/shape/bitmap/bitmap_base.rs b/core/embed/rust/src/ui/shape/bitmap/bitmap_base.rs new file mode 100644 index 000000000..9cb6c48a7 --- /dev/null +++ b/core/embed/rust/src/ui/shape/bitmap/bitmap_base.rs @@ -0,0 +1,313 @@ +use crate::trezorhal::bitblt::BitBlt; + +use crate::ui::{display::Color, geometry::Offset}; + +use core::{cell::Cell, marker::PhantomData}; + +#[derive(Copy, Clone, PartialEq, Eq)] +pub enum BitmapFormat { + /// 1-bit mono + MONO1, + /// 1-bit mono packed (bitmap stride is in bits) + MONO1P, + /// 4-bit mono + MONO4, + /// 8-bit mono + MONO8, + /// 16-bit color, RGB565 format + RGB565, + /// 32-bit color, RGBA format + RGBA8888, +} + +pub struct Bitmap<'a> { + /// Pointer to top-left pixel + ptr: *mut u8, + /// Stride in bytes + stride: usize, + /// Size in pixels + size: Offset, + /// Format of pixels + format: BitmapFormat, + /// Bitmap data is mutable + mutable: bool, + /// DMA operation is pending + dma_pending: Cell, + /// + _phantom: core::marker::PhantomData<&'a ()>, +} + +impl<'a> Bitmap<'a> { + /// Creates a new bitmap referencing a specified buffer. + /// + /// Optionally minimal height can be specified and then the height + /// of the new bitmap is adjusted to the buffer size. + /// + /// Returns None if the buffer is not big enough. + /// + /// The `buff` needs to be properly aligned and big enough + /// to hold a bitmap with the specified format and size + pub fn new( + format: BitmapFormat, + stride: Option, + mut size: Offset, + min_height: Option, + buff: &'a [u8], + ) -> Option { + if size.x < 0 && size.y < 0 { + return None; + } + + let min_stride = match format { + BitmapFormat::MONO1 => (size.x + 7) / 8, + BitmapFormat::MONO1P => 0, + BitmapFormat::MONO4 => (size.x + 1) / 2, + BitmapFormat::MONO8 => size.x, + BitmapFormat::RGB565 => size.x * 2, + BitmapFormat::RGBA8888 => size.x * 4, + } as usize; + + let stride = stride.unwrap_or(min_stride); + + let alignment = match format { + BitmapFormat::MONO1 => 1, + BitmapFormat::MONO1P => 1, + BitmapFormat::MONO4 => 1, + BitmapFormat::MONO8 => 1, + BitmapFormat::RGB565 => 2, + BitmapFormat::RGBA8888 => 4, + }; + + assert!(stride >= min_stride); + assert!(buff.as_ptr() as usize & (alignment - 1) == 0); + assert!(stride & (alignment - 1) == 0); + + let max_height = if stride == 0 { + size.y as usize + } else { + buff.len() / stride + }; + + if size.y as usize > max_height { + if let Some(min_height) = min_height { + if max_height >= min_height as usize { + size.y = max_height as i16; + } else { + return None; + } + } else { + return None; + } + } + + Some(Self { + ptr: buff.as_ptr() as *mut u8, + stride, + size, + format, + mutable: false, + dma_pending: Cell::new(false), + _phantom: PhantomData, + }) + } + + /// Creates a new mutable bitmap referencing a specified buffer. + /// + /// Optionally minimal height can be specified and then the height + /// of the new bitmap is adjusted to the buffer size. + /// + /// Returns None if the buffer is not big enough. + /// + /// The `buff` needs to be properly aligned and big enough + /// to hold a bitmap with the specified format and size + pub fn new_mut( + format: BitmapFormat, + stride: Option, + size: Offset, + min_height: Option, + buff: &'a mut [u8], + ) -> Option { + let mut bitmap = Self::new(format, stride, size, min_height, buff)?; + bitmap.mutable = true; + Some(bitmap) + } + + /// Returns bitmap width in pixels. + pub fn width(&self) -> i16 { + self.size.x + } + + /// Returns bitmap height in pixels. + pub fn height(&self) -> i16 { + self.size.y + } + + /// Returns bitmap width and height in pixels. + pub fn size(&self) -> Offset { + self.size + } + + /// Returns bitmap stride in bytes. + pub fn stride(&self) -> usize { + self.stride + } + + /// Returns bitmap format. + pub fn format(&self) -> BitmapFormat { + self.format + } + + pub fn view(&self) -> BitmapView { + BitmapView::new(self) + } + + /// Returns the specified row as an immutable slice. + /// + /// Returns None if row is out of range. + pub fn row(&self, row: i16) -> Option<&[T]> { + if row >= 0 && row < self.size.y { + self.wait_for_dma(); + let offset = row as usize * (self.stride / core::mem::size_of::()); + Some(unsafe { + core::slice::from_raw_parts( + (self.ptr as *const T).add(offset), + self.stride / core::mem::size_of::(), + ) + }) + } else { + None + } + } + + /// Returns the specified row as a mutable slice. + /// + /// Returns None if row is out of range. + pub fn row_mut(&mut self, row: i16) -> Option<&mut [T]> { + if row >= 0 && row < self.size.y { + self.wait_for_dma(); + let offset = row as usize * (self.stride / core::mem::size_of::()); + Some(unsafe { + core::slice::from_raw_parts_mut( + (self.ptr as *mut T).add(offset), + self.stride / core::mem::size_of::(), + ) + }) + } else { + None + } + } + + /// Returns specified consecutive rows as a mutable slice + /// + /// Returns None if any of requested row is out of range. + pub fn rows_mut(&mut self, row: i16, height: i16) -> Option<&mut [T]> { + if row >= 0 && height > 0 && row < self.size.y && row + height <= self.size.y { + self.wait_for_dma(); + let offset = self.stride * row as usize; + let len = self.stride * height as usize; + + let array = unsafe { + core::slice::from_raw_parts_mut( + self.ptr as *mut T, + self.size.y as usize * self.stride / core::mem::size_of::(), + ) + }; + + Some(&mut array[offset..offset + len]) + } else { + None + } + } + + /// Return raw mut pointer to the specified bitmap row. + /// + /// # Safety + /// + /// `y` must be in range <0; self.height() - 1>. + pub unsafe fn row_ptr(&self, y: u16) -> *mut cty::c_void { + unsafe { self.ptr.add(self.stride() * y as usize) as *mut cty::c_void } + } + + /// Waits until DMA operation is finished + fn wait_for_dma(&self) { + if self.dma_pending.get() { + BitBlt::wait_for_transfer(); + self.dma_pending.set(false); + } + } + + // Mark bitmap as DMA operation is pending + pub fn mark_dma_pending(&self) { + self.dma_pending.set(true); + } +} + +impl<'a> Drop for Bitmap<'a> { + fn drop(&mut self) { + self.wait_for_dma(); + } +} + +pub struct BitmapView<'a> { + pub bitmap: &'a Bitmap<'a>, + pub offset: Offset, + pub fg_color: Color, + pub bg_color: Color, +} + +impl<'a> BitmapView<'a> { + /// Creates a new reference to the bitmap + pub fn new(bitmap: &'a Bitmap) -> Self { + Self { + bitmap, + offset: Offset::zero(), + fg_color: Color::black(), + bg_color: Color::black(), + } + } + + /// Builds a new structure with offset set to the specified value + pub fn with_offset(self, offset: Offset) -> Self { + Self { + offset: offset + self.offset, + ..self + } + } + + /// Builds a new structure with foreground color set to the specified value + pub fn with_fg(self, fg_color: Color) -> Self { + Self { fg_color, ..self } + } + + /// Builds a new structure with background color set to the specified value + pub fn with_bg(self, bg_color: Color) -> Self { + Self { bg_color, ..self } + } + + /// Returns the bitmap width and height in pixels + pub fn size(&self) -> Offset { + self.bitmap.size + } + + /// Returns the bitmap width in pixels + pub fn width(&self) -> i16 { + self.bitmap.width() + } + + /// Returns the bitmap height in pixels + pub fn height(&self) -> i16 { + self.bitmap.height() + } + + /// Returns the bitmap format + pub fn format(&self) -> BitmapFormat { + self.bitmap.format + } + + /// Returns the specified row as an immutable slice. + /// + /// Returns None if row is out of range. + pub fn row(&self, row: i16) -> Option<&[T]> { + self.bitmap.row(row) + } +} diff --git a/core/embed/rust/src/ui/shape/bitmap/mod.rs b/core/embed/rust/src/ui/shape/bitmap/mod.rs new file mode 100644 index 000000000..cae625e0f --- /dev/null +++ b/core/embed/rust/src/ui/shape/bitmap/mod.rs @@ -0,0 +1,6 @@ +pub mod bitmap_base; +pub mod mono8; +pub mod rgb565; +pub mod rgba8888; + +pub use bitmap_base::{Bitmap, BitmapFormat, BitmapView}; diff --git a/core/embed/rust/src/ui/shape/bitmap/mono8.rs b/core/embed/rust/src/ui/shape/bitmap/mono8.rs new file mode 100644 index 000000000..798c66e6a --- /dev/null +++ b/core/embed/rust/src/ui/shape/bitmap/mono8.rs @@ -0,0 +1,48 @@ +use super::{Bitmap, BitmapFormat, BitmapView}; +use crate::{ + trezorhal::bitblt::BitBlt, + ui::{display::Color, geometry::Rect}, +}; + +impl<'a> Bitmap<'a> { + /// Fills a rectangle with the specified color. + /// + /// The function is aplicable only on bitmaps with RGB565 format. + pub fn mono8_fill(&mut self, r: Rect, clip: Rect, color: Color, alpha: u8) { + assert!(self.format() == BitmapFormat::MONO8); + if let Some(bitblt) = BitBlt::new_fill(r, clip, color, alpha) { + let bitblt = bitblt.with_dst(self); + unsafe { bitblt.mono8_fill() }; + self.mark_dma_pending(); + } + } + + // + pub fn mono8_copy(&mut self, r: Rect, clip: Rect, src: &BitmapView) { + assert!(self.format() == BitmapFormat::MONO8); + if let Some(bitblt) = BitBlt::new_copy(r, clip, src) { + let bitblt = bitblt.with_dst(self); + match src.format() { + BitmapFormat::MONO1P => unsafe { bitblt.mono8_copy_mono1p() }, + BitmapFormat::MONO4 => unsafe { bitblt.mono8_copy_mono4() }, + _ => panic!("Unsupported DMA operation"), + } + self.mark_dma_pending(); + src.bitmap.mark_dma_pending(); + } + } + + pub fn mono8_blend(&mut self, r: Rect, clip: Rect, src: &BitmapView) { + assert!(self.format() == BitmapFormat::MONO8); + if let Some(bitblt) = BitBlt::new_copy(r, clip, src) { + let bitblt = bitblt.with_dst(self); + match src.format() { + BitmapFormat::MONO1P => unsafe { bitblt.mono8_blend_mono1p() }, + BitmapFormat::MONO4 => unsafe { bitblt.mono8_blend_mono4() }, + _ => panic!("Unsupported DMA operation"), + } + self.mark_dma_pending(); + src.bitmap.mark_dma_pending(); + } + } +} diff --git a/core/embed/rust/src/ui/shape/bitmap/rgb565.rs b/core/embed/rust/src/ui/shape/bitmap/rgb565.rs new file mode 100644 index 000000000..79fc0f622 --- /dev/null +++ b/core/embed/rust/src/ui/shape/bitmap/rgb565.rs @@ -0,0 +1,47 @@ +use super::{Bitmap, BitmapFormat, BitmapView}; +use crate::{ + trezorhal::bitblt::BitBlt, + ui::{display::Color, geometry::Rect}, +}; + +impl<'a> Bitmap<'a> { + /// Fills a rectangle with the specified color. + /// + /// The function is aplicable only on bitmaps with RGB565 format. + pub fn rgb565_fill(&mut self, r: Rect, clip: Rect, color: Color, alpha: u8) { + assert!(self.format() == BitmapFormat::RGB565); + if let Some(bitblt) = BitBlt::new_fill(r, clip, color, alpha) { + let bitblt = bitblt.with_dst(self); + unsafe { bitblt.rgb565_fill() }; + self.mark_dma_pending(); + } + } + + // + pub fn rgb565_copy(&mut self, r: Rect, clip: Rect, src: &BitmapView) { + assert!(self.format() == BitmapFormat::RGB565); + if let Some(bitblt) = BitBlt::new_copy(r, clip, src) { + let bitblt = bitblt.with_dst(self); + match src.format() { + BitmapFormat::MONO4 => unsafe { bitblt.rgb565_copy_mono4() }, + BitmapFormat::RGB565 => unsafe { bitblt.rgb565_copy_rgb565() }, + _ => panic!("Unsupported DMA operation"), + } + self.mark_dma_pending(); + src.bitmap.mark_dma_pending(); + } + } + + pub fn rgb565_blend(&mut self, r: Rect, clip: Rect, src: &BitmapView) { + assert!(self.format() == BitmapFormat::RGB565); + if let Some(bitblt) = BitBlt::new_copy(r, clip, src) { + let bitblt = bitblt.with_dst(self); + match src.format() { + BitmapFormat::MONO4 => unsafe { bitblt.rgb565_blend_mono4() }, + _ => panic!("Unsupported DMA operation"), + } + self.mark_dma_pending(); + src.bitmap.mark_dma_pending(); + } + } +} diff --git a/core/embed/rust/src/ui/shape/bitmap/rgba8888.rs b/core/embed/rust/src/ui/shape/bitmap/rgba8888.rs new file mode 100644 index 000000000..38d766f9a --- /dev/null +++ b/core/embed/rust/src/ui/shape/bitmap/rgba8888.rs @@ -0,0 +1,47 @@ +use super::{Bitmap, BitmapFormat, BitmapView}; +use crate::{ + trezorhal::bitblt::BitBlt, + ui::{display::Color, geometry::Rect}, +}; + +impl<'a> Bitmap<'a> { + /// Fills a rectangle with the specified color. + /// + /// The function is aplicable only on bitmaps with RGBA888 format. + pub fn rgba8888_fill(&mut self, r: Rect, clip: Rect, color: Color, alpha: u8) { + assert!(self.format() == BitmapFormat::RGBA8888); + if let Some(bitblt) = BitBlt::new_fill(r, clip, color, alpha) { + let bitblt = bitblt.with_dst(self); + unsafe { bitblt.rgba8888_fill() }; + self.mark_dma_pending(); + } + } + + pub fn rgba8888_copy(&mut self, r: Rect, clip: Rect, src: &BitmapView) { + assert!(self.format() == BitmapFormat::RGBA8888); + if let Some(bitblt) = BitBlt::new_copy(r, clip, src) { + let bitblt = bitblt.with_dst(self); + match src.format() { + BitmapFormat::MONO4 => unsafe { bitblt.rgba8888_copy_mono4() }, + BitmapFormat::RGB565 => unsafe { bitblt.rgba8888_copy_rgb565() }, + BitmapFormat::RGBA8888 => unsafe { bitblt.rgba8888_copy_rgba8888() }, + _ => panic!("Unsupported DMA operation"), + } + self.mark_dma_pending(); + src.bitmap.mark_dma_pending(); + } + } + + pub fn rgba8888_blend(&mut self, r: Rect, clip: Rect, src: &BitmapView) { + assert!(self.format() == BitmapFormat::RGBA8888); + if let Some(bitblt) = BitBlt::new_copy(r, clip, src) { + let bitblt = bitblt.with_dst(self); + match src.format() { + BitmapFormat::MONO4 => unsafe { bitblt.rgba8888_blend_mono4() }, + _ => panic!("Unsupported DMA operation"), + } + self.mark_dma_pending(); + src.bitmap.mark_dma_pending(); + } + } +} diff --git a/core/embed/rust/src/ui/shape/blur.rs b/core/embed/rust/src/ui/shape/blur.rs new file mode 100644 index 000000000..8aca42951 --- /dev/null +++ b/core/embed/rust/src/ui/shape/blur.rs @@ -0,0 +1,45 @@ +use crate::ui::geometry::Rect; + +use super::{Canvas, DrawingCache, Renderer, Shape, ShapeClone}; + +use without_alloc::alloc::LocalAllocLeakExt; + +pub struct Blurring { + // Blurred area + area: Rect, + /// Blurring kernel radius + radius: usize, +} + +/// A shape for the blurring of a specified rectangle area. +impl Blurring { + pub fn new(area: Rect, radius: usize) -> Self { + Self { area, radius } + } + + pub fn render<'s>(self, renderer: &mut impl Renderer<'s>) { + renderer.render_shape(self); + } +} + +impl Shape<'_> for Blurring { + fn bounds(&self, _cache: &DrawingCache) -> Rect { + self.area + } + + fn cleanup(&mut self, _cache: &DrawingCache) {} + + fn draw(&mut self, canvas: &mut dyn Canvas, cache: &DrawingCache) { + canvas.blur_rect(self.area, self.radius, cache); + } +} + +impl<'s> ShapeClone<'s> for Blurring { + fn clone_at_bump<'alloc, T>(self, bump: &'alloc T) -> Option<&'alloc mut dyn Shape<'s>> + where + T: LocalAllocLeakExt<'alloc>, + { + let clone = bump.alloc_t::()?; + Some(clone.uninit.init(Blurring { ..self })) + } +} diff --git a/core/embed/rust/src/ui/shape/cache/blur_cache.rs b/core/embed/rust/src/ui/shape/cache/blur_cache.rs new file mode 100644 index 000000000..499ea07aa --- /dev/null +++ b/core/embed/rust/src/ui/shape/cache/blur_cache.rs @@ -0,0 +1,59 @@ +use super::super::algo::{BlurAlgorithm, BlurBuff}; +use crate::ui::geometry::Offset; +use core::cell::UnsafeCell; +use without_alloc::alloc::LocalAllocLeakExt; + +pub struct BlurCache<'a> { + algo: Option>, + buff: &'a UnsafeCell, + tag: u32, +} + +impl<'a> BlurCache<'a> { + pub fn new<'alloc: 'a, T>(bump: &'alloc T) -> Option + where + T: LocalAllocLeakExt<'alloc>, + { + let buff = bump + .alloc_t::>()? + .uninit + .init(UnsafeCell::new([0; 7928])); // TODO !!! 7928 + + Some(Self { + algo: None, + buff, + tag: 0, + }) + } + + pub fn get( + &mut self, + size: Offset, + radius: usize, + tag: Option, + ) -> Result<(&mut BlurAlgorithm<'a>, u32), ()> { + if let Some(tag) = tag { + if self.tag == tag { + return Ok((unwrap!(self.algo.as_mut()), self.tag)); + } + } + + // Drop the existing blurring inbstance holding + // a mutable reference to its scratchpad buffer + self.algo = None; + self.tag += 1; + + // Now there's nobody else holding any reference to our buffer + // so we can get mutable reference and pass it to a new + // instance of the blurring algorithm + let buff = unsafe { &mut *self.buff.get() }; + + self.algo = Some(BlurAlgorithm::new(size, radius, buff)?); + + Ok((unwrap!(self.algo.as_mut()), self.tag)) + } + + pub const fn get_bump_size() -> usize { + core::mem::size_of::>() + } +} diff --git a/core/embed/rust/src/ui/shape/cache/drawing_cache.rs b/core/embed/rust/src/ui/shape/cache/drawing_cache.rs new file mode 100644 index 000000000..1dfb6f88e --- /dev/null +++ b/core/embed/rust/src/ui/shape/cache/drawing_cache.rs @@ -0,0 +1,138 @@ +use super::zlib_cache::ZlibCache; + +#[cfg(feature = "ui_blurring")] +use super::blur_cache::BlurCache; + +#[cfg(feature = "ui_jpeg_decoder")] +use super::jpeg_cache::JpegCache; + +use core::cell::{RefCell, RefMut}; +use without_alloc::alloc::LocalAllocLeakExt; + +const ALIGN_PAD: usize = 8; + +#[cfg(feature = "xframebuff")] +const ZLIB_CACHE_SLOTS: usize = 1; +#[cfg(not(feature = "xframebuff"))] +const ZLIB_CACHE_SLOTS: usize = 3; + +const JPEG_CACHE_SLOTS: usize = 1; +const RENDER_BUFF_SIZE: usize = (240 * 2 * 16) + ALIGN_PAD; +const IMAGE_BUFF_SIZE: usize = 2048 + ALIGN_PAD; + +pub type ImageBuff = [u8; IMAGE_BUFF_SIZE]; +pub type RenderBuff = [u8; RENDER_BUFF_SIZE]; + +pub type ImageBuffRef<'a> = RefMut<'a, ImageBuff>; +pub type RenderBuffRef<'a> = RefMut<'a, RenderBuff>; + +pub struct DrawingCache<'a> { + zlib_cache: RefCell>, + + #[cfg(feature = "ui_jpeg_decoder")] + jpeg_cache: RefCell>, + + #[cfg(feature = "ui_blurring")] + blur_cache: RefCell>, + + #[cfg(not(feature = "xframebuff"))] + render_buff: &'a RefCell, + image_buff: &'a RefCell, +} + +fn alloc_buf<'a, const S: usize, B>(bump: &'a B) -> Option<&'a RefCell<[u8; S]>> +where + B: LocalAllocLeakExt<'a>, +{ + Some( + bump.alloc_t::>()? + .uninit + .init(RefCell::new([0; S])), + ) +} + +impl<'a> DrawingCache<'a> { + pub fn new(bump_a: &'a TA, bump_b: &'a TB) -> Self + where + TA: LocalAllocLeakExt<'a>, + TB: LocalAllocLeakExt<'a>, + { + Self { + zlib_cache: RefCell::new(unwrap!( + ZlibCache::new(bump_a, ZLIB_CACHE_SLOTS), + "ZLIB cache alloc" + )), + #[cfg(feature = "ui_jpeg_decoder")] + jpeg_cache: RefCell::new(unwrap!( + JpegCache::new(bump_a, JPEG_CACHE_SLOTS), + "JPEG cache alloc" + )), + #[cfg(feature = "ui_blurring")] + blur_cache: RefCell::new(unwrap!(BlurCache::new(bump_a), "Blur cache alloc")), + + #[cfg(not(feature = "xframebuff"))] + render_buff: unwrap!(alloc_buf(bump_b), "Render buff alloc"), + image_buff: unwrap!(alloc_buf(bump_b), "Toif buff alloc"), + } + } + + /// Returns an object for decompression of TOIF images + pub fn zlib(&self) -> RefMut> { + self.zlib_cache.borrow_mut() + } + + /// Returns an object for decompression of JPEG images + #[cfg(feature = "ui_jpeg_decoder")] + pub fn jpeg(&self) -> RefMut> { + self.jpeg_cache.borrow_mut() + } + + /// Returns an object providing blurring algorithm + #[cfg(feature = "ui_blurring")] + pub fn blur(&self) -> RefMut> { + self.blur_cache.borrow_mut() + } + + /// Returns a buffer used for ProgressiveRenderer slice + #[cfg(not(feature = "xframebuff"))] + pub fn render_buff(&self) -> Option> { + self.render_buff.try_borrow_mut().ok() + } + + /// Returns a buffer for intended for drawing of + /// QrCode or ToifImage + pub fn image_buff(&self) -> Option> { + self.image_buff.try_borrow_mut().ok() + } + + pub const fn get_bump_a_size() -> usize { + let mut size = 0; + + size += ZlibCache::get_bump_size(ZLIB_CACHE_SLOTS); + + #[cfg(feature = "ui_jpeg_decoder")] + { + size += JpegCache::get_bump_size(JPEG_CACHE_SLOTS); + } + + #[cfg(feature = "ui_blurring")] + { + size += BlurCache::get_bump_size(); + } + + size + } + + pub const fn get_bump_b_size() -> usize { + let mut size = 0; + + #[cfg(not(feature = "xframebuff"))] + { + size += core::mem::size_of::>(); + } + + size += core::mem::size_of::>(); + + size + } +} diff --git a/core/embed/rust/src/ui/shape/cache/jpeg_cache.rs b/core/embed/rust/src/ui/shape/cache/jpeg_cache.rs new file mode 100644 index 000000000..b3c026a85 --- /dev/null +++ b/core/embed/rust/src/ui/shape/cache/jpeg_cache.rs @@ -0,0 +1,372 @@ +use crate::ui::{ + display::tjpgd, + geometry::{Offset, Point, Rect}, + shape::{BasicCanvas, Bitmap, BitmapFormat, BitmapView, Canvas, Rgb565Canvas}, +}; + +use core::cell::UnsafeCell; +use without_alloc::{alloc::LocalAllocLeakExt, FixedVec}; + +// JDEC work buffer size +// +// number of quantization tables (n_qtbl) = 2..4 (typical 2) +// number of huffman tables (n_htbl) = 2..4 (typical 2) +// mcu size = 1 * 1 .. 2 * 2 = 1..4 (typical 4) +// +// hufflut_ac & hufflut_dc are required only if JD_FASTDECODE == 2 (default) +// +// --------------------------------------------------------------------- +// table | size calculation | MIN..MAX | TYP +// --------------------------------------------------------------------- +// qttbl | n_qtbl * size_of(i32) * 64 | 512..1024 | 512 +// huffbits | n_htbl * size_of(u8) * 16 | 32..64 | 32 +// huffcode | n_htbl * size_of(u16) * 256 | 1024..2048 | 1024 +// huffdata | n_htbl * size_of(u8) * 256 | 512..1024 | 512 +// hufflut_ac | n_htbl * size_of(u16) * 1024 | 4096..8192 | 4096 +// hufflut_dc | n_htbl * size_of(u8) * 1024 | 2048..4096 | 2048 +// workbuf | mcu_size * 192 + 64 | 256..832 | 832 +// mcubuf | (mcu_size + 2) * size_of(u16) * 64 | 384..768 | 768 +// inbuff | JD_SZBUF constant | 512..512 | 512 +// ---------------------------------------------------------------|------ +// SUM | | 9376..18560 | 10336 +// ---------------------------------------------------------------|------ + +const JPEG_SCRATCHPAD_SIZE: usize = 10500; // the same const > 10336 as in original code + +// Buffer for a cached row of JPEG MCUs (up to 240x16 RGB565 pixels) +const ALIGN_PAD: usize = 8; +const JPEG_BUFF_SIZE: usize = (240 * 2 * 16) + ALIGN_PAD; + +pub struct JpegCacheSlot<'a> { + // Reference to compressed data + jpeg: &'a [u8], + // value in range 0..3 leads into scale factor 1 << scale + scale: u8, + // Input buffer referencing compressed data + input: Option>, + // JPEG decoder instance + decoder: Option>, + // Scratchpad memory used by the JPEG decoder + // (it's used just by our decoder and nobody else) + scratchpad: &'a UnsafeCell<[u8; JPEG_SCRATCHPAD_SIZE]>, + // horizontal coordinate of cached row or None + // (valid if row_canvas is Some) + row_y: i16, + // Canvas for recently decoded row of MCU's + row_canvas: Option>, + // Buffer for slice canvas + row_buff: &'a UnsafeCell<[u8; JPEG_BUFF_SIZE]>, +} + +impl<'a> JpegCacheSlot<'a> { + fn new<'alloc: 'a, T>(bump: &'alloc T) -> Option + where + T: LocalAllocLeakExt<'alloc>, + { + let scratchpad = bump + .alloc_t::>()? + .uninit + .init(UnsafeCell::new([0; JPEG_SCRATCHPAD_SIZE])); + + let canvas_buff = bump + .alloc_t::>()? + .uninit + .init(UnsafeCell::new([0; JPEG_BUFF_SIZE])); + + Some(Self { + jpeg: &[], + scale: 0, + input: None, + decoder: None, + scratchpad, + row_y: 0, + row_canvas: None, + row_buff: canvas_buff, + }) + } + + fn reset<'i: 'a>(&mut self, jpeg: &'i [u8], scale: u8) -> Result<(), tjpgd::Error> { + // Drop the existing decoder holding + // a mutable reference to the scratchpad and canvas buffer & c + self.decoder = None; + self.row_canvas = None; + + if !jpeg.is_empty() { + // Now there's nobody else holding any reference to our scratchpad buffer + // so we can get a mutable reference and pass it to a new + // instance of the JPEG decoder + let scratchpad = unsafe { &mut *self.scratchpad.get() }; + // Prepare a input buffer + let mut input = tjpgd::BufferInput(jpeg); + // Initialize the decoder by reading headers from input + let mut decoder = tjpgd::JDEC::new(&mut input, scratchpad)?; + // Set decoder scale factor + decoder.set_scale(scale)?; + self.decoder = Some(decoder); + // Save modified input buffer + self.input = Some(input); + } else { + self.input = None; + } + + self.jpeg = jpeg; + self.scale = scale; + Ok(()) + } + + fn is_for<'i: 'a>(&self, jpeg: &'i [u8], scale: u8) -> bool { + jpeg == self.jpeg && scale == self.scale && self.decoder.is_some() + } + + pub fn get_size<'i: 'a>(&mut self, jpeg: &'i [u8], scale: u8) -> Result { + if !self.is_for(jpeg, scale) { + self.reset(jpeg, scale)?; + } + let decoder = unwrap!(self.decoder.as_mut()); // should never fail + let divisor = 1 << self.scale; + Ok(Offset::new( + decoder.width() / divisor, + decoder.height() / divisor, + )) + } + + // left-top origin of output rectangle must be aligned to JPEG MCU size + pub fn decompress_mcu<'i: 'a>( + &mut self, + jpeg: &'i [u8], + scale: u8, + offset: Point, + output: &mut dyn FnMut(Rect, BitmapView) -> bool, + ) -> Result<(), tjpgd::Error> { + // Reset the slot if the JPEG image is different + if !self.is_for(jpeg, scale) { + self.reset(jpeg, scale)?; + } + + // Get coordinates of the next coming MCU + let decoder = unwrap!(self.decoder.as_ref()); // should never fail + let divisor = 1 << self.scale; + let next_mcu = Offset::new( + decoder.next_mcu().0 as i16 / divisor, + decoder.next_mcu().1 as i16 / divisor, + ); + + // Get height of the MCUs (8 or 16pixels) + let mcu_height = decoder.mcu_height() / (1 << self.scale); + + // Reset the decoder if pixel at the offset was already decoded + if offset.y < next_mcu.y || (offset.x < next_mcu.x && offset.y < next_mcu.y + mcu_height) { + self.reset(self.jpeg, scale)?; + } + + let decoder = unwrap!(self.decoder.as_mut()); // should never fail + let input = unwrap!(self.input.as_mut()); // should never fail + let mut output = JpegFnOutput::new(output); + + match decoder.decomp2(input, &mut output) { + Ok(_) | Err(tjpgd::Error::Interrupted) => Ok(()), + Err(e) => Err(e), + } + } + + pub fn decompress_row<'i: 'a>( + &mut self, + jpeg: &'i [u8], + scale: u8, + mut offset_y: i16, + output: &mut dyn FnMut(Rect, BitmapView) -> bool, + ) -> Result<(), tjpgd::Error> { + // Reset the slot if the JPEG image is different + if !self.is_for(jpeg, scale) { + self.reset(jpeg, scale)?; + } + + let mut row_canvas = self.row_canvas.take(); + let mut row_y = self.row_y; + + // Use cached data if possible + if let Some(row_canvas) = row_canvas.as_mut() { + if offset_y >= self.row_y && offset_y < self.row_y + row_canvas.height() { + if !output( + Rect::from_size(row_canvas.size()).translate(Offset::new(0, row_y)), + row_canvas.view(), + ) { + return Ok(()); + } + // Align to the next MCU row + offset_y += row_canvas.height() - offset_y % row_canvas.height(); + } + } else { + // Create a new row for cahing decoded JPEG data + // Now there's nobody else holding any reference to canvas_buff so + // we can get a mutable reference and pass it to a new instance + // of Rgb565Canvas + let canvas_buff = unsafe { &mut *self.row_buff.get() }; + // Prepare canvas as a cache for a row of decoded JPEG MCUs + let decoder = unwrap!(self.decoder.as_ref()); // shoud never fail + let divisor = 1 << self.scale; + row_canvas = Some(unwrap!( + Rgb565Canvas::new( + Offset::new(decoder.width() / divisor, decoder.mcu_height() / divisor), + None, + None, + canvas_buff + ), + "Buffer too small" + )); + } + + self.decompress_mcu( + jpeg, + scale, + Point::new(0, offset_y), + &mut |mcu_r, mcu_bitmap| { + // Get canvas for MCU caching + let row_canvas = unwrap!(row_canvas.as_mut()); // should never fail + + // Calculate coordinates in the row canvas + let dst_r = Rect { + y0: 0, + y1: mcu_r.height(), + ..mcu_r + }; + // Draw a MCU + row_canvas.draw_bitmap(dst_r, mcu_bitmap); + + if mcu_r.x1 < row_canvas.size().x { + // We are not done with the row yet + true + } else { + // We have a complete row, let's pass it to the callee + row_y = mcu_r.y0; + output( + Rect::from_size(row_canvas.size()).translate(Offset::new(0, row_y)), + row_canvas.view(), + ) + } + }, + )?; + + // Store the recently decoded row for future use + self.row_y = row_y; + self.row_canvas = row_canvas; + + Ok(()) + } +} + +struct JpegFnOutput +where + F: FnMut(Rect, BitmapView) -> bool, +{ + output: F, +} + +impl JpegFnOutput +where + F: FnMut(Rect, BitmapView) -> bool, +{ + pub fn new(output: F) -> Self { + Self { output } + } +} + +impl trezor_tjpgdec::JpegOutput for JpegFnOutput +where + F: FnMut(Rect, BitmapView) -> bool, +{ + fn write( + &mut self, + _jd: &tjpgd::JDEC, + rect_origin: (u32, u32), + rect_size: (u32, u32), + pixels: &[u16], + ) -> bool { + // MCU coordinates in source image + let mcu_r = Rect::from_top_left_and_size( + Point::new(rect_origin.0 as i16, rect_origin.1 as i16), + Offset::new(rect_size.0 as i16, rect_size.1 as i16), + ); + + // SAFETY: aligning from [u16] -> [u8] + let (_, pixels, _) = unsafe { pixels.align_to() }; + + // Create readonly bitmap + let mcu_bitmap = unwrap!(Bitmap::new( + BitmapFormat::RGB565, + None, + mcu_r.size(), + None, + pixels, + )); + + // Return true to continue decompression + (self.output)(mcu_r, BitmapView::new(&mcu_bitmap)) + } +} + +pub struct JpegCache<'a> { + slots: FixedVec<'a, JpegCacheSlot<'a>>, +} + +impl<'a> JpegCache<'a> { + pub fn new<'alloc: 'a, T>(bump: &'alloc T, slot_count: usize) -> Option + where + T: LocalAllocLeakExt<'alloc>, + { + assert!(slot_count <= 1); // we support just 1 decoder + + let mut cache = Self { + slots: bump.fixed_vec(slot_count)?, + }; + + for _ in 0..cache.slots.capacity() { + unwrap!(cache.slots.push(JpegCacheSlot::new(bump)?)); // should never fail + } + + Some(cache) + } + + pub fn get_size<'i: 'a>(&mut self, jpeg: &'i [u8], scale: u8) -> Result { + if self.slots.capacity() > 0 { + self.slots[0].get_size(jpeg, scale) + } else { + Err(tjpgd::Error::MemoryPool) + } + } + + pub fn decompress_mcu<'i: 'a>( + &mut self, + jpeg: &'i [u8], + scale: u8, + offset: Point, + output: &mut dyn FnMut(Rect, BitmapView) -> bool, + ) -> Result<(), tjpgd::Error> { + if self.slots.capacity() > 0 { + self.slots[0].decompress_mcu(jpeg, scale, offset, output) + } else { + Err(tjpgd::Error::MemoryPool) + } + } + + pub fn decompress_row<'i: 'a>( + &mut self, + jpeg: &'i [u8], + scale: u8, + offset_y: i16, + output: &mut dyn FnMut(Rect, BitmapView) -> bool, + ) -> Result<(), tjpgd::Error> { + if self.slots.capacity() > 0 { + self.slots[0].decompress_row(jpeg, scale, offset_y, output) + } else { + Err(tjpgd::Error::MemoryPool) + } + } + + pub const fn get_bump_size(slot_count: usize) -> usize { + (core::mem::size_of::() + + core::mem::size_of::>() + + core::mem::size_of::>()) + * slot_count + } +} diff --git a/core/embed/rust/src/ui/shape/cache/mod.rs b/core/embed/rust/src/ui/shape/cache/mod.rs new file mode 100644 index 000000000..0717577ee --- /dev/null +++ b/core/embed/rust/src/ui/shape/cache/mod.rs @@ -0,0 +1,7 @@ +pub mod blur_cache; +pub mod drawing_cache; + +#[cfg(feature = "ui_jpeg_decoder")] +pub mod jpeg_cache; + +pub mod zlib_cache; diff --git a/core/embed/rust/src/ui/shape/cache/zlib_cache.rs b/core/embed/rust/src/ui/shape/cache/zlib_cache.rs new file mode 100644 index 000000000..f1a8194c2 --- /dev/null +++ b/core/embed/rust/src/ui/shape/cache/zlib_cache.rs @@ -0,0 +1,173 @@ +use crate::{ + trezorhal::uzlib::{UzlibContext, UZLIB_WINDOW_SIZE}, + ui::display::toif::Toif, +}; +use core::cell::UnsafeCell; +use without_alloc::{alloc::LocalAllocLeakExt, FixedVec}; + +struct ZlibCacheSlot<'a> { + /// Reference to compressed data + zdata: &'a [u8], + /// Current offset in docempressed data + offset: usize, + /// Decompression context for the current zdata + dc: Option>, + /// Window used by current decompression context. + /// (It's used just by own dc and nobody else.) + window: &'a UnsafeCell<[u8; UZLIB_WINDOW_SIZE]>, +} + +impl<'a> ZlibCacheSlot<'a> { + fn new<'alloc: 'a, T>(bump: &'alloc T) -> Option + where + T: LocalAllocLeakExt<'alloc>, + { + let window = bump + .alloc_t::>()? + .uninit + .init(UnsafeCell::new([0; UZLIB_WINDOW_SIZE])); + + Some(Self { + zdata: &[], + offset: 0, + dc: None, + window, + }) + } + + /// May be called with zdata == &[] to make the slot free + fn reset(&mut self, zdata: &'a [u8]) { + // Drop the existing decompression context holding + // a mutable reference to window buffer + self.dc = None; + + if !zdata.is_empty() { + // Now there's nobody else holding any reference to our window + // so we can get mutable reference and pass it to a new + // instance of the decompression context + let window = unsafe { &mut *self.window.get() }; + + self.dc = Some(UzlibContext::new(zdata, Some(window))); + } + + self.offset = 0; + self.zdata = zdata; + } + + fn uncompress(&mut self, dest_buf: &mut [u8]) -> Result { + if let Some(dc) = self.dc.as_mut() { + match dc.uncompress(dest_buf) { + Ok(done) => { + if done { + self.reset(&[]); + } else { + self.offset += dest_buf.len(); + } + Ok(done) + } + Err(e) => Err(e), + } + } else { + Err(()) + } + } + + fn skip(&mut self, nbytes: usize) -> Result { + if let Some(dc) = self.dc.as_mut() { + match dc.skip(nbytes) { + Ok(done) => { + if done { + self.reset(&[]); + } else { + self.offset += nbytes; + } + + Ok(done) + } + Err(e) => Err(e), + } + } else { + Err(()) + } + } + + fn is_for(&self, zdata: &[u8], offset: usize) -> bool { + self.zdata == zdata && self.offset == offset + } +} + +pub struct ZlibCache<'a> { + slots: FixedVec<'a, ZlibCacheSlot<'a>>, +} + +impl<'a> ZlibCache<'a> { + pub fn new<'alloc: 'a, T>(bump: &'alloc T, slot_count: usize) -> Option + where + T: LocalAllocLeakExt<'alloc>, + { + let mut cache = Self { + slots: bump.fixed_vec(slot_count)?, + }; + + for _ in 0..cache.slots.capacity() { + unwrap!(cache.slots.push(ZlibCacheSlot::new(bump)?)); // should never fail + } + + Some(cache) + } + + fn select_slot_for_reuse(&self) -> Result { + if self.slots.capacity() > 0 { + let mut selected = 0; + for (i, slot) in self.slots.iter().enumerate() { + if slot.dc.is_none() { + selected = i; + break; + } + } + Ok(selected) + } else { + Err(()) + } + } + + pub fn uncompress( + &mut self, + zdata: &'a [u8], + offset: usize, + dest_buf: &mut [u8], + ) -> Result { + let slot = self + .slots + .iter_mut() + .find(|slot| slot.is_for(zdata, offset)); + + if let Some(slot) = slot { + slot.uncompress(dest_buf) + } else { + let selected = self.select_slot_for_reuse()?; + let slot = &mut self.slots[selected]; + slot.reset(zdata); + slot.skip(offset)?; + slot.uncompress(dest_buf) + } + } + + pub fn uncompress_toif( + &mut self, + toif: Toif<'a>, + from_row: i16, + dest_buf: &mut [u8], + ) -> Result<(), ()> { + let from_offset = toif.stride() * from_row as usize; + self.uncompress(toif.zdata(), from_offset, dest_buf)?; + Ok(()) + } + + pub const fn get_bump_size(slot_count: usize) -> usize { + (core::mem::size_of::() + + core::mem::size_of::>() + + 16) + * slot_count + } +} diff --git a/core/embed/rust/src/ui/shape/canvas/common.rs b/core/embed/rust/src/ui/shape/canvas/common.rs new file mode 100644 index 000000000..db7801843 --- /dev/null +++ b/core/embed/rust/src/ui/shape/canvas/common.rs @@ -0,0 +1,887 @@ +use crate::ui::{ + display::Color, + geometry::{Offset, Point, Rect}, +}; + +use super::super::{ + algo::{circle_points, line_points, sin_i16, PI4}, + BitmapView, Viewport, +}; + +#[cfg(feature = "ui_blurring")] +use crate::ui::shape::DrawingCache; + +pub trait BasicCanvas { + /// Returns dimensions of the canvas in pixels. + fn size(&self) -> Offset; + + /// Returns the dimensions of the canvas as a rectangle with + /// the top-left at (0,0). + fn bounds(&self) -> Rect { + Rect::from_size(self.size()) + } + + /// Returns the width of the canvas in pixels. + fn width(&self) -> i16 { + self.size().x + } + + /// Returns the height of the canvas in pixels. + fn height(&self) -> i16 { + self.size().y + } + + /// Gets the current drawing viewport previously set by `set_viewport()` + /// function. + fn viewport(&self) -> Viewport; + + /// Sets the active viewport valid for all subsequent drawing operations. + fn set_viewport(&mut self, vp: Viewport); + + /// Sets the new viewport that's intersection of the + /// current viewport and the `window` rectangle relative + /// to the current viewport. The viewport's origin is + /// set to the top-left corener of the `window`. + fn set_window(&mut self, window: Rect) -> Viewport { + let viewport = self.viewport(); + self.set_viewport(viewport.relative_window(window)); + viewport + } + + /// Sets the new viewport that's intersection of the + /// current viewport and the `clip` rectangle relative + /// to the current viewport. The viewport's origin is + /// not changed. + fn set_clip(&mut self, clip: Rect) -> Viewport { + let viewport = self.viewport(); + self.set_viewport(viewport.relative_clip(clip)); + viewport + } + + /// Draws a filled rectangle with the specified color. + fn fill_rect(&mut self, r: Rect, color: Color, alpha: u8); + + /// Fills the canvas background with the specified color. + fn fill_background(&mut self, color: Color) { + self.fill_rect(self.viewport().clip, color, 255); + } + + /// Draws a bitmap of bitmap into to the rectangle. + fn draw_bitmap(&mut self, r: Rect, bitmap: BitmapView); +} + +pub trait Canvas: BasicCanvas { + /// Returns a non-mutable view of the underlying bitmap. + fn view(&self) -> BitmapView; + + /// Draw a pixel at specified coordinates. + fn draw_pixel(&mut self, pt: Point, color: Color); + + /// Draws a single pixel and blends its color with the background. + /// + /// - If alpha == 255, the (foreground) pixel color is used. + /// - If 0 < alpha << 255, pixel and backround colors are blended. + /// - If alpha == 0, the background color is used. + fn blend_pixel(&mut self, pt: Point, color: Color, alpha: u8); + + /// Blends a bitmap with the canvas background + fn blend_bitmap(&mut self, r: Rect, src: BitmapView); + + /// Applies a blur effect to the specified rectangle. + /// + /// The blur effect works properly only when the rectangle is not clipped, + /// which is a strong constraint that's hard to be met. The function uses a + /// simple box filter, where the 'radius' argument represents the length + /// of the sides of this filter. + /// + /// It's important to be aware that strong artifacts may appear on images + /// with horizontal/vertical lines. + #[cfg(feature = "ui_blurring")] + fn blur_rect(&mut self, r: Rect, radius: usize, cache: &DrawingCache); + + /// Draws an outline of a rectangle with rounded corners. + fn draw_round_rect(&mut self, r: Rect, radius: i16, color: Color) { + let split = unwrap!(circle_points(radius).last()).v; + + let b = Rect { + y1: r.y0 + radius - split + 1, + ..r + }; + + if self.viewport().contains(b) { + for p in circle_points(radius) { + let pt_l = Point::new(r.x0 + radius - p.u, r.y0 + radius - p.v); + let pt_r = Point::new(r.x1 - radius + p.u - 1, r.y0 + radius - p.v); + if p.v == radius && p.last { + self.fill_rect(Rect::new(pt_l, pt_r.onright().under()), color, 255); + } else { + self.draw_pixel(pt_l, color); + self.draw_pixel(pt_r, color); + } + } + } + + let b = Rect { + y0: r.y0 + radius - split + 1, + y1: r.y0 + radius + 1, + ..r + }; + + if self.viewport().contains(b) { + for p in circle_points(radius).take_while(|p| p.u < p.v) { + let pt_l = Point::new(r.x0 + radius - p.v, r.y0 + radius - p.u); + let pt_r = Point::new(r.x1 - radius + p.v - 1, r.y0 + radius - p.u); + self.draw_pixel(pt_l, color); + self.draw_pixel(pt_r, color); + } + } + + self.fill_rect( + Rect { + x0: r.x0, + y0: r.y0 + radius + 1, + x1: r.x0 + 1, + y1: r.y1 - radius - 1, + }, + color, + 255, + ); + + self.fill_rect( + Rect { + x0: r.x1 - 1, + y0: r.y0 + radius + 1, + x1: r.x1, + y1: r.y1 - radius - 1, + }, + color, + 255, + ); + + let b = Rect { + y0: r.y1 - radius - 1, + y1: r.y1 - radius - 1 + split, + ..r + }; + + if self.viewport().contains(b) { + for p in circle_points(radius).take_while(|p| p.u < p.v) { + let pt_l = Point::new(r.x0 + radius - p.v, r.y1 - radius - 1 + p.u); + let pt_r = Point::new(r.x1 - radius + p.v - 1, r.y1 - radius - 1 + p.u); + self.draw_pixel(pt_l, color); + self.draw_pixel(pt_r, color); + } + } + + let b = Rect { + y0: r.y1 - radius - 1 + split, + ..r + }; + + if self.viewport().contains(b) { + for p in circle_points(radius) { + let pt_l = Point::new(r.x0 + radius - p.u, r.y1 - radius - 1 + p.v); + let pt_r = Point::new(r.x1 - radius + p.u - 1, r.y1 - radius - 1 + p.v); + + if p.v == radius && p.last { + self.fill_rect(Rect::new(pt_l, pt_r.onright().under()), color, 255); + } else { + self.draw_pixel(pt_l, color); + self.draw_pixel(pt_r, color); + } + } + } + } + + /// Draws filled rectangle with rounded corners. + #[cfg(not(feature = "ui_antialiasing"))] + fn fill_round_rect(&mut self, r: Rect, radius: i16, color: Color, alpha: u8) { + let split = unwrap!(circle_points(radius).last()).v; + + let b = Rect { + y1: r.y0 + radius - split + 1, + ..r + }; + + if self.viewport().contains(b) { + for p in circle_points(radius) { + if p.last { + let pt_l = Point::new(r.x0 + radius - p.u, r.y0 + radius - p.v); + let pt_r = Point::new(r.x1 - radius + p.u - 1, r.y0 + radius - p.v); + self.fill_rect(Rect::new(pt_l, pt_r.onright().under()), color, alpha); + } + } + } + + let b = Rect { + y0: r.y0 + radius - split + 1, + y1: r.y0 + radius + 1, + ..r + }; + + if self.viewport().contains(b) { + for p in circle_points(radius).take_while(|p| p.u < p.v) { + let pt_l = Point::new(r.x0 + radius - p.v, r.y0 + radius - p.u); + let pt_r = Point::new(r.x1 - radius + p.v - 1, r.y0 + radius - p.u); + self.fill_rect(Rect::new(pt_l, pt_r.onright().under()), color, alpha); + } + } + + self.fill_rect( + Rect { + x0: r.x0, + y0: r.y0 + radius + 1, + x1: r.x1, + y1: r.y1 - radius - 1, + }, + color, + alpha, + ); + + let b = Rect { + y0: r.y1 - radius - 1, + y1: r.y1 - radius - 1 + split, + ..r + }; + + if self.viewport().contains(b) { + for p in circle_points(radius).take_while(|p| p.u < p.v) { + let pt_l = Point::new(r.x0 + radius - p.v, r.y1 - radius - 1 + p.u); + let pt_r = Point::new(r.x1 - radius + p.v - 1, r.y1 - radius - 1 + p.u); + self.fill_rect(Rect::new(pt_l, pt_r.onright().under()), color, alpha); + } + } + + let b = Rect { + y0: r.y1 - radius - 1 + split, + ..r + }; + + if self.viewport().contains(b) { + for p in circle_points(radius) { + if p.last { + let pt_l = Point::new(r.x0 + radius - p.u, r.y1 - radius - 1 + p.v); + let pt_r = Point::new(r.x1 - radius + p.u - 1, r.y1 - radius - 1 + p.v); + self.fill_rect(Rect::new(pt_l, pt_r.onright().under()), color, alpha); + } + } + } + } + + /// Draws filled rectangle with antialiased rounded corners. + #[cfg(feature = "ui_antialiasing")] + fn fill_round_rect(&mut self, r: Rect, radius: i16, color: Color, alpha: u8) { + let split = unwrap!(circle_points(radius).last()).v; + + let b = Rect { + y1: r.y0 + radius - split + 1, + ..r + }; + + let alpha_mul = |a: u8| -> u8 { ((a as u16 * alpha as u16) / 255) as u8 }; + + if self.viewport().contains(b) { + for p in circle_points(radius) { + let pt_l = Point::new(r.x0 + radius - p.u, r.y0 + radius - p.v); + let pt_r = Point::new(r.x1 - radius + p.u - 1, r.y0 + radius - p.v); + self.blend_pixel(pt_l, color, alpha_mul(p.frac)); + self.blend_pixel(pt_r, color, alpha_mul(p.frac)); + + if p.first { + let inner = Rect::new(pt_l.onright(), pt_r.under()); + self.fill_rect(inner, color, alpha); + } + } + } + + let b = Rect { + y0: r.y0 + radius - split + 1, + y1: r.y0 + radius + 1, + ..r + }; + + if self.viewport().contains(b) { + for p in circle_points(radius).take_while(|p| p.u < p.v) { + let pt_l = Point::new(r.x0 + radius - p.v, r.y0 + radius - p.u); + let pt_r = Point::new(r.x1 - radius + p.v - 1, r.y0 + radius - p.u); + self.blend_pixel(pt_l, color, alpha_mul(p.frac)); + self.blend_pixel(pt_r, color, alpha_mul(p.frac)); + + let inner = Rect::new(pt_l.onright(), pt_r.under()); + self.fill_rect(inner, color, alpha); + } + } + + self.fill_rect( + Rect { + x0: r.x0, + y0: r.y0 + radius + 1, + x1: r.x1, + y1: r.y1 - radius - 1, + }, + color, + alpha, + ); + + let b = Rect { + y0: r.y1 - radius - 1, + y1: r.y1 - radius - 1 + split, + ..r + }; + + if self.viewport().contains(b) { + for p in circle_points(radius).take_while(|p| p.u < p.v) { + let pt_l = Point::new(r.x0 + radius - p.v, r.y1 - radius - 1 + p.u); + let pt_r = Point::new(r.x1 - radius + p.v - 1, r.y1 - radius - 1 + p.u); + self.blend_pixel(pt_l, color, alpha_mul(p.frac)); + self.blend_pixel(pt_r, color, alpha_mul(p.frac)); + + let b = Rect::new(pt_l.onright(), pt_r.under()); + self.fill_rect(b, color, alpha); + } + } + + let b = Rect { + y0: r.y1 - radius - 1 + split, + ..r + }; + + if self.viewport().contains(b) { + for p in circle_points(radius) { + let pt_l = Point::new(r.x0 + radius - p.u, r.y1 - radius - 1 + p.v); + self.blend_pixel(pt_l, color, alpha_mul(p.frac)); + let pt_r = Point::new(r.x1 - radius + p.u - 1, r.y1 - radius - 1 + p.v); + self.blend_pixel(pt_r, color, alpha_mul(p.frac)); + + if p.first { + let b = Rect::new(pt_l.onright(), pt_r.under()); + self.fill_rect(b, color, alpha); + } + } + } + } + + // Draws circle with the specified center and the radius. + #[cfg(not(feature = "ui_antialiasing"))] + fn draw_circle(&mut self, center: Point, radius: i16, color: Color) { + let split = unwrap!(circle_points(radius).last()).v; + + let r = Rect::new( + Point::new(center.x - radius, center.y - radius), + Point::new(center.x + radius + 1, center.y - split + 1), + ); + + if self.viewport().contains(r) { + for p in circle_points(radius) { + let pt_l = Point::new(center.x - p.u, center.y - p.v); + let pt_r = Point::new(center.x + p.u, center.y - p.v); + self.draw_pixel(pt_l, color); + self.draw_pixel(pt_r, color); + } + } + + let r = Rect::new( + Point::new(center.x - radius, center.y - split), + Point::new(center.x + radius + 1, center.y + 1), + ); + + if self.viewport().contains(r) { + for p in circle_points(radius).take_while(|p| p.u < p.v) { + let pt_l = Point::new(center.x - p.v, center.y - p.u); + let pt_r = Point::new(center.x + p.v, center.y - p.u); + self.draw_pixel(pt_l, color); + self.draw_pixel(pt_r, color); + } + } + + let r = Rect::new( + Point::new(center.x - radius, center.y + 1), + Point::new(center.x + radius + 1, center.y + split + 1), + ); + + if self.viewport().contains(r) { + for p in circle_points(radius).skip(1).take_while(|p| p.u < p.v) { + let pt_l = Point::new(center.x - p.v, center.y + p.u); + let pt_r = Point::new(center.x + p.v, center.y + p.u); + self.draw_pixel(pt_l, color); + self.draw_pixel(pt_r, color); + } + } + + let r = Rect::new( + Point::new(center.x - radius, center.y + split), + Point::new(center.x + radius + 1, center.y + radius + 1), + ); + + if self.viewport().contains(r) { + for p in circle_points(radius) { + let pt_l = Point::new(center.x - p.u, center.y + p.v); + let pt_r = Point::new(center.x + p.u, center.y + p.v); + self.draw_pixel(pt_l, color); + self.draw_pixel(pt_r, color); + } + } + } + + /// Draws antialiased circle with the specified center and the radius. + /*#[cfg(feature = "ui_antialiasing")] + fn draw_circle(&mut self, center: Point, radius: i16, color: Color) { + let split = unwrap!(circle_points(radius).last()).v; + + let r = Rect::new( + Point::new(center.x - radius, center.y - radius), + Point::new(center.x + radius + 1, center.y - split + 1), + ); + + if self.viewport().contains(r) { + for p in circle_points(radius) { + let pt_l = Point::new(center.x - p.u, center.y - p.v); + self.blend_pixel(pt_l, color, p.frac); + self.blend_pixel(pt_l.under(), color, 255 - p.frac); + let pt_r = Point::new(center.x + p.u, center.y - p.v); + self.blend_pixel(pt_r, color, p.frac); + self.blend_pixel(pt_r.under(), color, 255 - p.frac); + } + } + + let r = Rect::new( + Point::new(center.x - radius, center.y - split), + Point::new(center.x + radius + 1, center.y + 1), + ); + + if self.viewport().contains(r) { + for p in circle_points(radius).take_while(|p| p.u < p.v) { + let pt_l = Point::new(center.x - p.v, center.y - p.u); + self.blend_pixel(pt_l, color, p.frac); + self.blend_pixel(pt_l.onright(), color, 255 - p.frac); + let pt_r = Point::new(center.x + p.v, center.y - p.u); + self.blend_pixel(pt_r, color, p.frac); + self.blend_pixel(pt_r.onleft(), color, 255 - p.frac); + } + } + + let r = Rect::new( + Point::new(center.x - radius, center.y + 1), + Point::new(center.x + radius + 1, center.y + split + 1), + ); + + if self.viewport().contains(r) { + for p in circle_points(radius).skip(1).take_while(|p| p.u < p.v) { + let pt_l = Point::new(center.x - p.v, center.y + p.u); + self.blend_pixel(pt_l, color, p.frac); + self.blend_pixel(pt_l.onright(), color, 255 - p.frac); + let pt_r = Point::new(center.x + p.v, center.y + p.u); + self.blend_pixel(pt_r, color, p.frac); + self.blend_pixel(pt_r.onleft(), color, 255 - p.frac); + } + } + + let r = Rect::new( + Point::new(center.x - radius, center.y + split), + Point::new(center.x + radius + 1, center.y + radius + 1), + ); + + if self.viewport().contains(r) { + for p in circle_points(radius) { + let pt_l = Point::new(center.x - p.u, center.y + p.v); + self.blend_pixel(pt_l, color, p.frac); + self.blend_pixel(pt_l.above(), color, 255 - p.frac); + let pt_r = Point::new(center.x + p.u, center.y + p.v); + self.blend_pixel(pt_r, color, p.frac); + self.blend_pixel(pt_r.above(), color, 255 - p.frac); + } + } + }*/ + + /// Draws filled circle with the specified center and the radius. + #[cfg(not(feature = "ui_antialiasing"))] + fn fill_circle(&mut self, center: Point, radius: i16, color: Color) { + let split = unwrap!(circle_points(radius).last()).v; + let alpha = 255; + + let r = Rect::new( + Point::new(center.x - radius, center.y - radius), + Point::new(center.x + radius + 1, center.y - split + 1), + ); + + if self.viewport().contains(r) { + for p in circle_points(radius) { + if p.last { + let pt_l = Point::new(center.x - p.u, center.y - p.v); + let pt_r = Point::new(center.x + p.u, center.y - p.v); + self.fill_rect(Rect::new(pt_l, pt_r.onright().under()), color, alpha); + } + } + } + + let r = Rect::new( + Point::new(center.x - radius, center.y - split), + Point::new(center.x + radius + 1, center.y + 1), + ); + + if self.viewport().contains(r) { + for p in circle_points(radius).take_while(|p| p.u < p.v) { + let pt_l = Point::new(center.x - p.v, center.y - p.u); + let pt_r = Point::new(center.x + p.v, center.y - p.u); + self.fill_rect(Rect::new(pt_l, pt_r.onright().under()), color, alpha); + } + } + + let r = Rect::new( + Point::new(center.x - radius, center.y + 1), + Point::new(center.x + radius + 1, center.y + split + 1), + ); + + if self.viewport().contains(r) { + for p in circle_points(radius).skip(1).take_while(|p| p.u < p.v) { + let pt_l = Point::new(center.x - p.v, center.y + p.u); + let pt_r = Point::new(center.x + p.v, center.y + p.u); + self.fill_rect(Rect::new(pt_l, pt_r.onright().under()), color, alpha); + } + } + + let r = Rect::new( + Point::new(center.x - radius, center.y + split), + Point::new(center.x + radius + 1, center.y + radius + 1), + ); + + if self.viewport().contains(r) { + for p in circle_points(radius) { + if p.last { + let pt_l = Point::new(center.x - p.u, center.y + p.v); + let pt_r = Point::new(center.x + p.u, center.y + p.v); + self.fill_rect(Rect::new(pt_l, pt_r.onright().under()), color, alpha); + } + } + } + } + + /// Draws antialiased filled circle with the specified center and the + /// radius. + #[cfg(feature = "ui_antialiasing")] + fn fill_circle(&mut self, center: Point, radius: i16, color: Color) { + let split = unwrap!(circle_points(radius).last()).v; + + let alpha = 255; + let alpha_mul = |a: u8| -> u8 { ((a as u16 * alpha as u16) / 255) as u8 }; + + let r = Rect::new( + Point::new(center.x - radius, center.y - radius), + Point::new(center.x + radius + 1, center.y - split + 1), + ); + + if self.viewport().contains(r) { + for p in circle_points(radius) { + let pt_l = Point::new(center.x - p.u, center.y - p.v); + let pt_r = Point::new(center.x + p.u, center.y - p.v); + self.blend_pixel(pt_l, color, alpha_mul(p.frac)); + if pt_l != pt_r { + self.blend_pixel(pt_r, color, alpha_mul(p.frac)); + } + + if p.first { + let r = Rect::new(pt_l.onright(), pt_r.under()); + self.fill_rect(r, color, alpha); + } + } + } + + let r = Rect::new( + Point::new(center.x - radius, center.y - split), + Point::new(center.x + radius + 1, center.y + 1), + ); + + if self.viewport().contains(r) { + for p in circle_points(radius).take_while(|p| p.u < p.v) { + let pt_l = Point::new(center.x - p.v, center.y - p.u); + let pt_r = Point::new(center.x + p.v, center.y - p.u); + self.blend_pixel(pt_l, color, alpha_mul(p.frac)); + self.blend_pixel(pt_r, color, alpha_mul(p.frac)); + + let r = Rect::new(pt_l.onright(), pt_r.under()); + self.fill_rect(r, color, alpha); + } + } + + let r = Rect::new( + Point::new(center.x - radius, center.y + 1), + Point::new(center.x + radius + 1, center.y + split + 1), + ); + + if self.viewport().contains(r) { + for p in circle_points(radius).skip(1).take_while(|p| p.u < p.v) { + let pt_l = Point::new(center.x - p.v, center.y + p.u); + let pt_r = Point::new(center.x + p.v, center.y + p.u); + self.blend_pixel(pt_l, color, alpha_mul(p.frac)); + self.blend_pixel(pt_r, color, alpha_mul(p.frac)); + + let r = Rect::new(pt_l.onright(), pt_r.under()); + self.fill_rect(r, color, alpha); + } + } + + let r = Rect::new( + Point::new(center.x - radius, center.y + split), + Point::new(center.x + radius + 1, center.y + radius + 1), + ); + + if self.viewport().contains(r) { + for p in circle_points(radius) { + let pt_l = Point::new(center.x - p.u, center.y + p.v); + let pt_r = Point::new(center.x + p.u, center.y + p.v); + if pt_l != pt_r { + self.blend_pixel(pt_l, color, alpha_mul(p.frac)); + } + self.blend_pixel(pt_r, color, alpha_mul(p.frac)); + + if p.first { + let r = Rect::new(pt_l.onright(), pt_r.under()); + self.fill_rect(r, color, alpha); + } + } + } + } + + /// Fills circle sector with a specified color. + fn fill_sector( + &mut self, + center: Point, + radius: i16, + mut start: i16, + mut end: i16, + color: Color, + ) { + start = (PI4 * 8 + start % (PI4 * 8)) % (PI4 * 8); + end = (PI4 * 8 + end % (PI4 * 8)) % (PI4 * 8); + + let alpha = 255; + let alpha_mul = |a: u8| -> u8 { ((a as u16 * alpha as u16) / 255) as u8 }; + + if start != end { + // The algorithm fills everything except the middle point ;-) + self.draw_pixel(center, color); + } + + for octant in 0..8 { + let angle = octant * PI4; + + // Function for calculation of 'u' coordinate inside the circle octant + // radius * sin(angle) + let sin = |angle: i16| -> i16 { sin_i16(angle, radius) }; + + // Calculate the octant's bounding rectangle + let p = Point::new(sin(PI4) + 1, -radius - 1).rot(octant); + let r = Rect::new(center, p + center.into()).normalize(); + + // Skip octant if not visible + if !self.viewport().contains(r) { + continue; + } + + // Function for filling a line between two endpoints with antialiasing. + // The function is special for each octant using 4 different axes of symmetry + let filler = &mut |p1: Option, p1_frac, p2: Point, p2_frac| { + let p2: Point = center + p2.rot(octant).into(); + self.blend_pixel(p2, color, alpha_mul(p2_frac)); + if let Some(p1) = p1 { + let p1: Point = center + p1.rot(octant).into(); + let ofs = Point::new(-1, 0).rot(octant); + self.blend_pixel(p1 + ofs.into(), color, alpha_mul(p1_frac)); + if ofs.x + ofs.y < 0 { + if ofs.x != 0 { + self.fill_rect(Rect::new(p1, p2.under()), color, alpha); + } else { + self.fill_rect(Rect::new(p1, p2.onright()), color, alpha); + } + } else { + let p1 = p1 + ofs.into(); + let p2 = p2 + ofs.into(); + if ofs.x != 0 { + self.fill_rect(Rect::new(p2, p1.under()), color, alpha); + } else { + self.fill_rect(Rect::new(p2, p1.onright()), color, alpha); + } + } + } + }; + + let corr = if octant & 1 == 0 { + // The clockwise octant + |angle| angle + } else { + // The anticlockwise octant + |angle| PI4 - angle + }; + + if start <= end { + // Octant may contain 0 or 1 sector + if start < angle + PI4 && end > angle { + if start <= angle && end >= angle + PI4 { + // Fill all pixels in the octant + fill_octant(radius, 0, sin(PI4), filler); + } else { + // Partial fill + let u1 = if start <= angle { + sin(corr(0)) + } else { + sin(corr(start - angle)) + }; + let u2 = if end <= angle + PI4 { + sin(corr(end - angle)) + } else { + sin(corr(PI4)) + }; + + fill_octant(radius, u1, u2, filler); + } + } + } else { + // Octant may contain 0, 1 or 2 sectors + if end >= angle + PI4 || start <= angle { + // Fill all pixels in the octant + fill_octant(radius, 0, sin(PI4), filler); + } else { + // Partial fill + if (end > angle) && (end < angle + PI4) { + // Fill up to `end` + fill_octant(radius, sin(corr(0)), sin(corr(end - angle)), filler); + } + if start < angle + PI4 { + // Fill all from `start` + fill_octant(radius, sin(corr(start - angle)), sin(corr(PI4)), filler); + } + } + } + } + } +} + +/// Calculates endpoints of a single octant of a circle +/// +/// Used internally by `Canvas::fill_sector()`. +fn fill_octant( + radius: i16, + mut u1: i16, + mut u2: i16, + fill: &mut impl FnMut(Option, u8, Point, u8), +) { + // Starting end ending points on + if u1 > u2 { + (u1, u2) = (u2, u1); + } + + let mut iter = circle_points(radius).skip(u1 as usize); + + // Intersection of the p1 line and the circle + let p1_start = unwrap!(iter.next()); + + // Intersection of the p1 line and the circle + let mut p2_start = p1_start; + + for p in iter.by_ref() { + if p.u > u2 { + break; + } + p2_start = p; + } + + // Flag if we draw section up to 45degs + let join_flag = iter.next().is_none(); + + // Process area between a p1 line and the circle + let mut p1_iter = line_points(p1_start.v, p1_start.u, 0); + let mut first = true; + let mut skip = 0; + + for c in circle_points(radius) + .skip(p1_start.u as usize) + .take((p2_start.u - p1_start.u) as usize) + { + let p2_coord = Point::new(c.u, -c.v); + + if c.first || first { + let p1 = unwrap!(p1_iter.next()); + let p1_coord = Point::new(p1_start.u - p1.v, -p1_start.v + p1.u); + first = false; + + fill(Some(p1_coord), p1.frac, p2_coord, c.frac); + } else { + fill(None, 0, p2_coord, c.frac); + } + + skip = if c.last { 0 } else { 1 }; + } + + // Process area between a p1 and p2 lines + let p2_iter = line_points(p2_start.v, p2_start.u, 0).skip(skip); + for (p1, p2) in p1_iter.zip(p2_iter) { + let p1_coord = Point::new(p1_start.u - p1.v, -p1_start.v + p1.u); + let p2_coord = Point::new(p2_start.u - p2.v, -p2_start.v + p2.u); + let p2_frac = if join_flag { 255 } else { 255 - p2.frac }; + fill(Some(p1_coord), p1.frac, p2_coord, p2_frac); + } +} + +impl Rect { + /// Normalizes the rectangle coordinates. + /// + /// Returns a new `Rect` with potentially swapped left/right, + /// top/bottom coordinates, ensuring that `x0`, `y0` represents + /// the top-left corner and `x1`, `y1` represents the bottom-right corner. + pub fn normalize(&self) -> Self { + Rect { + x0: core::cmp::min(self.x0, self.x1), + y0: core::cmp::min(self.y0, self.y1), + x1: core::cmp::max(self.x0, self.x1), + y1: core::cmp::max(self.y0, self.y1), + } + } +} + +impl Point { + fn onleft(self) -> Self { + Self { + x: self.x - 1, + ..self + } + } + + fn onright(self) -> Self { + Self { + x: self.x + 1, + ..self + } + } + + fn above(self) -> Self { + Self { + y: self.y - 1, + ..self + } + } + + fn under(self) -> Self { + Self { + y: self.y + 1, + ..self + } + } + + fn rot(self, octant: i16) -> Self { + let mut result = self; + + if (octant + 1) & 2 != 0 { + result = Point::new(-result.y, -result.x); + } + + if octant & 4 != 0 { + result = Point::new(-result.x, result.y); + } + + if (octant + 2) & 4 != 0 { + result = Point::new(result.x, -result.y); + } + + result + } +} diff --git a/core/embed/rust/src/ui/shape/canvas/mod.rs b/core/embed/rust/src/ui/shape/canvas/mod.rs new file mode 100644 index 000000000..9e4507d6e --- /dev/null +++ b/core/embed/rust/src/ui/shape/canvas/mod.rs @@ -0,0 +1,11 @@ +mod common; +mod mono8; +mod rgb565; +mod rgba8888; +mod viewport; + +pub use common::{BasicCanvas, Canvas}; +pub use mono8::Mono8Canvas; +pub use rgb565::Rgb565Canvas; +pub use rgba8888::Rgba8888Canvas; +pub use viewport::Viewport; diff --git a/core/embed/rust/src/ui/shape/canvas/mono8.rs b/core/embed/rust/src/ui/shape/canvas/mono8.rs new file mode 100644 index 000000000..405dc8f77 --- /dev/null +++ b/core/embed/rust/src/ui/shape/canvas/mono8.rs @@ -0,0 +1,105 @@ +use crate::ui::{ + display::Color, + geometry::{Offset, Point, Rect}, +}; + +use super::{ + super::{Bitmap, BitmapFormat, BitmapView}, + BasicCanvas, Canvas, Viewport, +}; + +#[cfg(feature = "ui_blurring")] +use super::super::DrawingCache; + +/// A struct representing 8-bit monochromatic canvas +pub struct Mono8Canvas<'a> { + bitmap: Bitmap<'a>, + viewport: Viewport, +} + +impl<'a> Mono8Canvas<'a> { + /// Creates a new canvas with the specified size and buffer. + /// + /// Optionally minimal height can be specified and then the height + /// of the new bitmap is adjusted to the buffer size. + /// + /// Returns None if the buffer is not big enough. + pub fn new( + size: Offset, + stride: Option, + min_height: Option, + buff: &'a mut [u8], + ) -> Option { + let bitmap = Bitmap::new_mut(BitmapFormat::MONO8, stride, size, min_height, buff)?; + let viewport = Viewport::from_size(bitmap.size()); + Some(Self { bitmap, viewport }) + } + + /// Returns the specified row as a mutable slice. + /// + /// Returns None if row is out of range. + pub fn row_mut(&mut self, row: i16) -> Option<&mut [u8]> { + self.bitmap.row_mut(row) + } +} + +impl<'a> BasicCanvas for Mono8Canvas<'a> { + fn viewport(&self) -> Viewport { + self.viewport + } + + fn set_viewport(&mut self, viewport: Viewport) { + self.viewport = viewport.absolute_clip(self.bounds()); + } + + fn size(&self) -> Offset { + self.bitmap.size() + } + + fn fill_rect(&mut self, r: Rect, color: Color, alpha: u8) { + let r = r.translate(self.viewport.origin); + self.bitmap.mono8_fill(r, self.viewport.clip, color, alpha); + } + + fn draw_bitmap(&mut self, r: Rect, bitmap: BitmapView) { + let r = r.translate(self.viewport.origin); + self.bitmap.mono8_copy(r, self.viewport.clip, &bitmap); + } +} + +impl<'a> Canvas for Mono8Canvas<'a> { + fn view(&self) -> BitmapView { + BitmapView::new(&self.bitmap) + } + + fn draw_pixel(&mut self, pt: Point, color: Color) { + let pt = pt + self.viewport.origin; + if self.viewport.clip.contains(pt) { + if let Some(row) = self.row_mut(pt.y) { + row[pt.x as usize] = color.luminance() as u8; + } + } + } + + fn blend_pixel(&mut self, pt: Point, color: Color, alpha: u8) { + let pt = pt + self.viewport.origin; + if self.viewport.clip.contains(pt) { + if let Some(row) = self.row_mut(pt.y) { + let pixel = &mut row[pt.x as usize]; + let fg_color = color.luminance() as u16; + let bg_color = *pixel as u16; + *pixel = ((fg_color * alpha as u16 + bg_color * (255 - alpha) as u16) / 255) as u8; + } + } + } + + fn blend_bitmap(&mut self, r: Rect, src: BitmapView) { + let r = r.translate(self.viewport.origin); + self.bitmap.mono8_blend(r, self.viewport.clip, &src); + } + + #[cfg(feature = "ui_blurring")] + fn blur_rect(&mut self, _r: Rect, _radius: usize, _cache: &DrawingCache) { + // Not implemented + } +} diff --git a/core/embed/rust/src/ui/shape/canvas/rgb565.rs b/core/embed/rust/src/ui/shape/canvas/rgb565.rs new file mode 100644 index 000000000..e197135de --- /dev/null +++ b/core/embed/rust/src/ui/shape/canvas/rgb565.rs @@ -0,0 +1,128 @@ +use crate::ui::{ + display::Color, + geometry::{Offset, Point, Rect}, +}; + +use super::{ + super::{Bitmap, BitmapFormat, BitmapView}, + BasicCanvas, Canvas, Viewport, +}; + +#[cfg(feature = "ui_blurring")] +use super::super::DrawingCache; + +/// A struct representing 16-bit (RGB565) color canvas +pub struct Rgb565Canvas<'a> { + bitmap: Bitmap<'a>, + viewport: Viewport, +} + +impl<'a> Rgb565Canvas<'a> { + /// Creates a new canvas with the specified size and buffer. + /// + /// Optionally minimal height can be specified and then the height + /// of the new bitmap is adjusted to the buffer size. + /// + /// Returns None if the buffer is not big enough. + pub fn new( + size: Offset, + stride: Option, + min_height: Option, + buff: &'a mut [u8], + ) -> Option { + let bitmap = Bitmap::new_mut(BitmapFormat::RGB565, stride, size, min_height, buff)?; + let viewport = Viewport::from_size(bitmap.size()); + Some(Self { bitmap, viewport }) + } + + /// Returns the specified row as a mutable slice. + /// + /// Returns None if row is out of range. + pub fn row_mut(&mut self, row: i16) -> Option<&mut [u16]> { + self.bitmap.row_mut(row) + } +} + +impl<'a> BasicCanvas for Rgb565Canvas<'a> { + fn size(&self) -> Offset { + self.bitmap.size() + } + + fn viewport(&self) -> Viewport { + self.viewport + } + + fn set_viewport(&mut self, viewport: Viewport) { + self.viewport = viewport.absolute_clip(self.bounds()); + } + + fn fill_rect(&mut self, r: Rect, color: Color, alpha: u8) { + let r = r.translate(self.viewport.origin); + self.bitmap.rgb565_fill(r, self.viewport.clip, color, alpha); + } + + fn draw_bitmap(&mut self, r: Rect, bitmap: BitmapView) { + let r = r.translate(self.viewport.origin); + self.bitmap.rgb565_copy(r, self.viewport.clip, &bitmap); + } +} + +impl<'a> Canvas for Rgb565Canvas<'a> { + fn view(&self) -> BitmapView { + BitmapView::new(&self.bitmap) + } + + fn draw_pixel(&mut self, pt: Point, color: Color) { + let pt = pt + self.viewport.origin; + if self.viewport.clip.contains(pt) { + if let Some(row) = self.row_mut(pt.y) { + row[pt.x as usize] = color.into(); + } + } + } + + fn blend_pixel(&mut self, pt: Point, color: Color, alpha: u8) { + let pt = pt + self.viewport.origin; + if self.viewport.clip.contains(pt) { + if let Some(row) = self.row_mut(pt.y) { + let pixel = &mut row[pt.x as usize]; + let bg_color: Color = (*pixel).into(); + *pixel = bg_color.blend(color, alpha).into(); + } + } + } + + fn blend_bitmap(&mut self, r: Rect, src: BitmapView) { + let r = r.translate(self.viewport.origin); + self.bitmap.rgb565_blend(r, self.viewport.clip, &src); + } + + #[cfg(feature = "ui_blurring")] + fn blur_rect(&mut self, r: Rect, radius: usize, cache: &DrawingCache) { + let clip = r.translate(self.viewport.origin).clamp(self.viewport.clip); + + let ofs = radius as i16; + + if clip.width() > 2 * ofs - 1 && clip.height() > 2 * ofs - 1 { + let mut blur_cache = cache.blur(); + let (blur, _) = unwrap!( + blur_cache.get(clip.size(), radius, None), + "Too small blur buffer" + ); + + loop { + if let Some(y) = blur.push_ready() { + let row = unwrap!(self.row_mut(y + clip.y0)); // can't panic + blur.push(&row[clip.x0 as usize..clip.x1 as usize]); + } + if let Some(y) = blur.pop_ready() { + let row = unwrap!(self.row_mut(y + clip.y0)); // can't panic + blur.pop(&mut row[clip.x0 as usize..clip.x1 as usize], None); + if y + 1 >= clip.height() { + break; + } + } + } + } + } +} diff --git a/core/embed/rust/src/ui/shape/canvas/rgba8888.rs b/core/embed/rust/src/ui/shape/canvas/rgba8888.rs new file mode 100644 index 000000000..3f0d7baf2 --- /dev/null +++ b/core/embed/rust/src/ui/shape/canvas/rgba8888.rs @@ -0,0 +1,120 @@ +use crate::ui::{ + display::Color, + geometry::{Offset, Point, Rect}, +}; + +use super::{ + super::{Bitmap, BitmapFormat, BitmapView}, + BasicCanvas, Canvas, Viewport, +}; + +#[cfg(feature = "ui_blurring")] +use super::super::DrawingCache; + +/// A struct representing 32-bit (RGBA8888) color canvas +pub struct Rgba8888Canvas<'a> { + bitmap: Bitmap<'a>, + viewport: Viewport, +} + +impl<'a> Rgba8888Canvas<'a> { + /// Creates a new canvas with the specified size and buffer. + /// + /// Optionally minimal height can be specified and then the height + /// of the new bitmap is adjusted to the buffer size. + /// + /// Returns None if the buffer is not big enough. + pub fn new( + size: Offset, + stride: Option, + min_height: Option, + buff: &'a mut [u8], + ) -> Option { + let bitmap = Bitmap::new_mut(BitmapFormat::RGBA8888, stride, size, min_height, buff)?; + let viewport = Viewport::from_size(bitmap.size()); + Some(Self { bitmap, viewport }) + } + + /// Returns the specified row as a mutable slice. + /// + /// Returns None if row is out of range. + pub fn row_mut(&mut self, row: i16) -> Option<&mut [u32]> { + self.bitmap.row_mut(row) + } +} + +impl<'a> BasicCanvas for Rgba8888Canvas<'a> { + fn size(&self) -> Offset { + self.bitmap.size() + } + + fn viewport(&self) -> Viewport { + self.viewport + } + + fn set_viewport(&mut self, viewport: Viewport) { + self.viewport = viewport.absolute_clip(self.bounds()); + } + + fn fill_rect(&mut self, r: Rect, color: Color, alpha: u8) { + let r = r.translate(self.viewport.origin); + self.bitmap + .rgba8888_fill(r, self.viewport.clip, color, alpha); + } + + fn draw_bitmap(&mut self, r: Rect, bitmap: BitmapView) { + let r = r.translate(self.viewport.origin); + self.bitmap.rgba8888_copy(r, self.viewport.clip, &bitmap); + } +} + +impl<'a> Canvas for Rgba8888Canvas<'a> { + fn view(&self) -> BitmapView { + BitmapView::new(&self.bitmap) + } + + fn draw_pixel(&mut self, pt: Point, color: Color) { + let pt = pt + self.viewport.origin; + if self.viewport.clip.contains(pt) { + if let Some(row) = self.row_mut(pt.y) { + row[pt.x as usize] = color.to_u32(); + } + } + } + + fn blend_pixel(&mut self, pt: Point, color: Color, alpha: u8) { + let pt = pt + self.viewport.origin; + if self.viewport.clip.contains(pt) { + if let Some(row) = self.row_mut(pt.y) { + let bg = row[pt.x as usize]; + + let bg_r = ((bg & 0x00FF0000) >> 16) as u16; + let bg_g = ((bg & 0x0000FF00) >> 8) as u16; + let bg_b = (bg & 0x000000FF) as u16; + + let fg_r = color.r() as u16; + let fg_g = color.g() as u16; + let fg_b = color.b() as u16; + + let fg_mul = alpha as u16; + let bg_mul = (255 - alpha) as u16; + + let r = ((fg_r * fg_mul + bg_r * bg_mul) / 255) as u32; + let g = ((fg_g * fg_mul + bg_g * bg_mul) / 255) as u32; + let b = ((fg_b * fg_mul + bg_b * bg_mul) / 255) as u32; + + row[pt.x as usize] = (0xFF << 24) | (r << 16) | (g << 8) | b; + } + } + } + + fn blend_bitmap(&mut self, r: Rect, src: BitmapView) { + let r = r.translate(self.viewport.origin); + self.bitmap.rgba8888_blend(r, self.viewport.clip, &src); + } + + #[cfg(feature = "ui_blurring")] + fn blur_rect(&mut self, _r: Rect, _radius: usize, _cache: &DrawingCache) { + // TODO + } +} diff --git a/core/embed/rust/src/ui/shape/canvas/viewport.rs b/core/embed/rust/src/ui/shape/canvas/viewport.rs new file mode 100644 index 000000000..aca7f0b65 --- /dev/null +++ b/core/embed/rust/src/ui/shape/canvas/viewport.rs @@ -0,0 +1,103 @@ +use crate::ui::geometry::{Offset, Rect}; + +/// The Viewport concept is foundation for clipping and translating +/// during drawing on the general canvas. +/// +/// The Viewport structure comprises a rectangle representing the +/// clipping area and a drawing origin (or offset), which is applied +/// to all coordinates passed to the drawing functions. +/// +/// Two coordination systems exist - "absolute" and "relative." +/// +/// In the "absolute" coordinate system, (0, 0) is at the left-top of +/// a referenced canvas (device or bitmap). +/// +/// Relative coordinates are with respect to the viewport origin. +/// The relative coordinate (0, 0) is located at (viewport.origin.x, +/// viewport.origin.y). +/// +/// Conversion between "absolute" and "relative" coordinates is straightforward: +/// +/// pt_absolute = pt_relative.translate(viewport.origin) +/// +/// pt_relative = pt_absolute.translate(-viewport.origin) +/// +/// The Viewport's clipping area and origin are always in "absolute" +/// coordinates. Canvas objects utilize the viewport to translate "relative" +/// coordinates passed to drawing functions into "absolute" coordinates that +/// correspond to the target device or bitmap. + +#[derive(Copy, Clone)] +pub struct Viewport { + /// Clipping rectangle relative to the canvas top-left corner + pub clip: Rect, + /// Offset applied to all coordinates before clipping + pub origin: Offset, +} + +impl Viewport { + /// Creates a new viewport with specified clip rectangle and origin at + /// (0,0). + pub fn new(clip: Rect) -> Self { + Self { + clip, + origin: Offset::zero(), + } + } + + /// Creates a new viewport with specified size and origin at (0,0). + pub fn from_size(size: Offset) -> Self { + Self { + clip: Rect::from_size(size), + origin: Offset::zero(), + } + } + + /// Checks if the viewport intersects with the specified rectangle + /// given in relative coordinates. + pub fn contains(&self, r: Rect) -> bool { + !r.translate(self.origin).clamp(self.clip).is_empty() + } + + pub fn translate(self, offset: Offset) -> Self { + Self { + clip: self.clip.translate(offset), + origin: self.origin + offset, + } + } + + /// Creates a new viewport with the new origin given in + /// absolute coordinates. + pub fn with_origin(self, origin: Offset) -> Self { + Self { origin, ..self } + } + + /// Creates a clip of the viewport containing only the specified rectangle + /// given in absolute coordinates. The origin of the new viewport + /// remains unchanged. + pub fn absolute_clip(self, r: Rect) -> Self { + Self { + clip: r.clamp(self.clip), + ..self + } + } + + /// Creates a clip of the viewport containing only the specified rectangle + /// given in relative coordinates. The origin of the new viewport + /// remains unchanged. + pub fn relative_clip(self, r: Rect) -> Self { + Self { + clip: r.translate(self.origin).clamp(self.clip), + ..self + } + } + + /// Creates a clip of the viewport containing only the specified rectangle + /// given in relative coordinates. The origin of the new viewport + /// is set to the top-left corner of the rectangle. + pub fn relative_window(&self, r: Rect) -> Self { + let clip = r.translate(self.origin).clamp(self.clip); + let origin = self.origin + (clip.top_left() - self.clip.top_left()); + Self { clip, origin } + } +} diff --git a/core/embed/rust/src/ui/shape/circle.rs b/core/embed/rust/src/ui/shape/circle.rs new file mode 100644 index 000000000..e390a26ae --- /dev/null +++ b/core/embed/rust/src/ui/shape/circle.rs @@ -0,0 +1,139 @@ +use crate::ui::{ + display::Color, + geometry::{Point, Rect}, +}; + +use super::{Canvas, DrawingCache, Renderer, Shape, ShapeClone}; + +use without_alloc::alloc::LocalAllocLeakExt; + +/// A shape for rendering various types of circles or circle sectors. +pub struct Circle { + center: Point, + radius: i16, + fg_color: Option, + bg_color: Option, + thickness: i16, + start_angle: Option, + end_angle: Option, +} + +impl Circle { + pub fn new(center: Point, radius: i16) -> Self { + Self { + center, + radius, + fg_color: None, + bg_color: None, + thickness: 1, + start_angle: None, + end_angle: None, + } + } + + pub fn with_fg(self, fg_color: Color) -> Self { + Self { + fg_color: Some(fg_color), + ..self + } + } + + pub fn with_bg(self, bg_color: Color) -> Self { + Self { + bg_color: Some(bg_color), + ..self + } + } + + pub fn with_thickness(self, thickness: i16) -> Self { + Self { thickness, ..self } + } + + pub fn with_start_angle(self, from_angle: i16) -> Self { + Self { + start_angle: Some(from_angle), + ..self + } + } + + pub fn with_end_angle(self, to_angle: i16) -> Self { + Self { + end_angle: Some(to_angle), + ..self + } + } + + pub fn render<'s>(self, renderer: &mut impl Renderer<'s>) { + renderer.render_shape(self); + } +} + +impl Shape<'_> for Circle { + fn bounds(&self, _cache: &DrawingCache) -> Rect { + let c = self.center; + let r = self.radius; + Rect::new( + Point::new(c.x - r, c.y - r), + Point::new(c.x + r + 1, c.y + r + 1), + ) + } + + fn cleanup(&mut self, _cache: &DrawingCache) {} + + fn draw(&mut self, canvas: &mut dyn Canvas, _cache: &DrawingCache) { + // NOTE: drawing of circles without a background and with a thickness > 1 + // is not supported. If we needed it, we would have to + // introduce RgbCanvas::draw_ring() function. + + // TODO: panic! in unsupported scenarious + let th = match self.fg_color { + Some(_) => self.thickness, + None => 0, + }; + + if self.start_angle.is_none() && self.end_angle.is_none() { + if th == 1 { + if let Some(color) = self.bg_color { + canvas.fill_circle(self.center, self.radius, color); + } + if let Some(color) = self.fg_color { + #[cfg(not(feature = "ui_antialiasing"))] + canvas.draw_circle(self.center, self.radius, color); + #[cfg(feature = "ui_antialiasing")] + canvas.fill_circle(self.center, self.radius, color); + } + } else { + if let Some(color) = self.fg_color { + if th > 0 { + canvas.fill_circle(self.center, self.radius, color); + } + } + if let Some(color) = self.bg_color { + canvas.fill_circle(self.center, self.radius - th, color); + } + } + } else { + let start = self.start_angle.unwrap_or(0); + let end = self.end_angle.unwrap_or(360); + + if let Some(color) = self.fg_color { + if th > 0 { + canvas.fill_sector(self.center, self.radius, start, end, color); + } + } + if let Some(color) = self.bg_color { + canvas.fill_sector(self.center, self.radius - th, start, end, color); + } + } + } +} + +impl<'s> ShapeClone<'s> for Circle { + fn clone_at_bump<'alloc, T>(self, bump: &'alloc T) -> Option<&'alloc mut dyn Shape<'s>> + where + T: LocalAllocLeakExt<'alloc>, + { + let clone = bump.alloc_t::()?; + Some(clone.uninit.init(Circle { ..self })) + } +} diff --git a/core/embed/rust/src/ui/shape/display/fake_display.rs b/core/embed/rust/src/ui/shape/display/fake_display.rs new file mode 100644 index 000000000..329a838c9 --- /dev/null +++ b/core/embed/rust/src/ui/shape/display/fake_display.rs @@ -0,0 +1,12 @@ +use crate::ui::{ + display::Color, + geometry::Rect, + shape::{DirectRenderer, Mono8Canvas}, +}; + +pub fn render_on_display<'a, F>(_clip: Option, _bg_color: Option, _func: F) +where + F: FnOnce(&mut DirectRenderer<'_, 'a, Mono8Canvas<'a>>), +{ + panic!("Not implemented") +} diff --git a/core/embed/rust/src/ui/shape/display/fb_mono8.rs b/core/embed/rust/src/ui/shape/display/fb_mono8.rs new file mode 100644 index 000000000..d717de43c --- /dev/null +++ b/core/embed/rust/src/ui/shape/display/fb_mono8.rs @@ -0,0 +1,55 @@ +use crate::ui::{ + display::Color, + geometry::{Offset, Rect}, + shape::{BasicCanvas, DirectRenderer, DrawingCache, Mono8Canvas, Viewport}, +}; + +use crate::trezorhal::display; + +use static_alloc::Bump; + +/// Creates the `Renderer` object for drawing on a display and invokes a +/// user-defined function that takes a single argument `target`. The user's +/// function can utilize the `target` for drawing on the display. +/// +/// `clip` specifies a rectangle area that the user will draw to. +/// If no clip is specified, the entire display area is used. +/// +/// `bg_color` specifies a background color with which the clip is filled before +/// the drawing starts. If the background color is None, the background +/// is undefined, and the user has to fill it themselves. +pub fn render_on_display<'a, F>(clip: Option, bg_color: Option, func: F) +where + F: FnOnce(&mut DirectRenderer<'_, 'a, Mono8Canvas<'a>>), +{ + const BUMP_SIZE: usize = DrawingCache::get_bump_a_size() + DrawingCache::get_bump_b_size(); + + static mut BUMP: Bump<[u8; BUMP_SIZE]> = Bump::uninit(); + + let bump = unsafe { &mut *core::ptr::addr_of_mut!(BUMP) }; + { + let width = display::DISPLAY_RESX as i16; + let height = display::DISPLAY_RESY as i16; + + bump.reset(); + + let cache = DrawingCache::new(bump, bump); + + let (fb, fb_stride) = display::get_frame_buffer(); + + let mut canvas = unwrap!(Mono8Canvas::new( + Offset::new(width, height), + Some(fb_stride), + None, + fb + )); + + if let Some(clip) = clip { + canvas.set_viewport(Viewport::new(clip)); + } + + let mut target = DirectRenderer::new(&mut canvas, bg_color, &cache); + + func(&mut target); + } +} diff --git a/core/embed/rust/src/ui/shape/display/fb_rgb565.rs b/core/embed/rust/src/ui/shape/display/fb_rgb565.rs new file mode 100644 index 000000000..e8734958a --- /dev/null +++ b/core/embed/rust/src/ui/shape/display/fb_rgb565.rs @@ -0,0 +1,62 @@ +use crate::ui::{ + display::Color, + geometry::{Offset, Rect}, + shape::{BasicCanvas, DirectRenderer, DrawingCache, Rgb565Canvas, Viewport}, +}; + +use crate::trezorhal::display; + +use static_alloc::Bump; + +/// Creates the `Renderer` object for drawing on a display and invokes a +/// user-defined function that takes a single argument `target`. The user's +/// function can utilize the `target` for drawing on the display. +/// +/// `clip` specifies a rectangle area that the user will draw to. +/// If no clip is specified, the entire display area is used. +/// +/// `bg_color` specifies a background color with which the clip is filled before +/// the drawing starts. If the background color is None, the background +/// is undefined, and the user has to fill it themselves. +pub fn render_on_display<'a, F>(clip: Option, bg_color: Option, func: F) +where + F: FnOnce(&mut DirectRenderer<'_, 'a, Rgb565Canvas<'a>>), +{ + const BUMP_A_SIZE: usize = DrawingCache::get_bump_a_size(); + const BUMP_B_SIZE: usize = DrawingCache::get_bump_b_size(); + + #[cfg_attr(not(target_os = "macos"), link_section = ".no_dma_buffers")] + static mut BUMP_A: Bump<[u8; BUMP_A_SIZE]> = Bump::uninit(); + + #[cfg_attr(not(target_os = "macos"), link_section = ".buf")] + static mut BUMP_B: Bump<[u8; BUMP_B_SIZE]> = Bump::uninit(); + + let bump_a = unsafe { &mut *core::ptr::addr_of_mut!(BUMP_A) }; + let bump_b = unsafe { &mut *core::ptr::addr_of_mut!(BUMP_B) }; + { + let width = display::DISPLAY_RESX as i16; + let height = display::DISPLAY_RESY as i16; + + bump_a.reset(); + bump_b.reset(); + + let cache = DrawingCache::new(bump_a, bump_b); + + let (fb, fb_stride) = display::get_frame_buffer(); + + let mut canvas = unwrap!(Rgb565Canvas::new( + Offset::new(width, height), + Some(fb_stride), + None, + fb + )); + + if let Some(clip) = clip { + canvas.set_viewport(Viewport::new(clip)); + } + + let mut target = DirectRenderer::new(&mut canvas, bg_color, &cache); + + func(&mut target); + } +} diff --git a/core/embed/rust/src/ui/shape/display/fb_rgba8888.rs b/core/embed/rust/src/ui/shape/display/fb_rgba8888.rs new file mode 100644 index 000000000..ec6babfc8 --- /dev/null +++ b/core/embed/rust/src/ui/shape/display/fb_rgba8888.rs @@ -0,0 +1,62 @@ +use crate::ui::{ + display::Color, + geometry::{Offset, Rect}, + shape::{BasicCanvas, DirectRenderer, DrawingCache, Rgba8888Canvas, Viewport}, +}; + +use static_alloc::Bump; + +use crate::trezorhal::display; + +/// Creates the `Renderer` object for drawing on a display and invokes a +/// user-defined function that takes a single argument `target`. The user's +/// function can utilize the `target` for drawing on the display. +/// +/// `clip` specifies a rectangle area that the user will draw to. +/// If no clip is specified, the entire display area is used. +/// +/// `bg_color` specifies a background color with which the clip is filled before +/// the drawing starts. If the background color is None, the background +/// is undefined, and the user has to fill it themselves. +pub fn render_on_display<'a, F>(clip: Option, bg_color: Option, func: F) +where + F: FnOnce(&mut DirectRenderer<'_, 'a, Rgba8888Canvas<'a>>), +{ + const BUMP_A_SIZE: usize = DrawingCache::get_bump_a_size(); + const BUMP_B_SIZE: usize = DrawingCache::get_bump_b_size(); + + #[cfg_attr(not(target_os = "macos"), link_section = ".no_dma_buffers")] + static mut BUMP_A: Bump<[u8; BUMP_A_SIZE]> = Bump::uninit(); + + #[cfg_attr(not(target_os = "macos"), link_section = ".buf")] + static mut BUMP_B: Bump<[u8; BUMP_B_SIZE]> = Bump::uninit(); + + let bump_a = unsafe { &mut *core::ptr::addr_of_mut!(BUMP_A) }; + let bump_b = unsafe { &mut *core::ptr::addr_of_mut!(BUMP_B) }; + { + let width = display::DISPLAY_RESX as i16; + let height = display::DISPLAY_RESY as i16; + + bump_a.reset(); + bump_b.reset(); + + let cache = DrawingCache::new(bump_a, bump_b); + + let (fb, fb_stride) = display::get_frame_buffer(); + + let mut canvas = unwrap!(Rgba8888Canvas::new( + Offset::new(width, height), + Some(fb_stride), + None, + fb + )); + + if let Some(clip) = clip { + canvas.set_viewport(Viewport::new(clip)); + } + + let mut target = DirectRenderer::new(&mut canvas, bg_color, &cache); + + func(&mut target); + } +} diff --git a/core/embed/rust/src/ui/shape/display/memory.md b/core/embed/rust/src/ui/shape/display/memory.md new file mode 100644 index 000000000..ec7093dac --- /dev/null +++ b/core/embed/rust/src/ui/shape/display/memory.md @@ -0,0 +1,58 @@ +## Memory usage comparison + +## Legacy solution + +**Memory with DMA access** + +``` +buffer_line_16bpp @.buf 1440 +buffer_line_4bpp @.buf 360 +buffer_text @.buf 4320 +------------------------------------------------- + 6120 +``` + +**Memory without DMA access** + +``` +buffer_jpeg @.no_dma 7680 +buffer_jpeg_work @.no_dma 10500 +buffer_blurring @.no_dma 14400 +buffer_blurring_totals @.no_dma 1440 +zlib context+window @.stack 2308 +------------------------------------------------- + 36328 +``` + +## New drawing library + +The memory usage is configurable, so the two options are considered.\ + +MIN variant is slower, but consumes less memory. OPT variant should +be sufficient for all purposes. + + +**Memory with DMA access** + +``` + MIN OPT +ProgressiveRenderer.slice @.buf 480 7680 +ProgressiveRenderer.scratch @.buf 480 2048 +--------------------------------------------------------- + 960 9728 +``` + +**Memory without DMA access** + +``` +ProgressiveRenderer.list @.stack 512 2048 +zlib decompression context @.no_dma 2308 6924 +jpeg decompressor @.no_dma 10500 10500 +partial jpeg image @.no_dma 7680 7680 +blurring window/totals @.no_dma 7920 7920 +------------------------------------------------------------------ + 28920 35072 +``` + + + diff --git a/core/embed/rust/src/ui/shape/display/mod.rs b/core/embed/rust/src/ui/shape/display/mod.rs new file mode 100644 index 000000000..e4be78b5b --- /dev/null +++ b/core/embed/rust/src/ui/shape/display/mod.rs @@ -0,0 +1,40 @@ +#[cfg(all(feature = "xframebuffer", feature = "display_mono"))] +pub mod fb_mono8; +#[cfg(all(feature = "xframebuffer", feature = "display_mono"))] +pub use fb_mono8::render_on_display; + +#[cfg(all(not(feature = "xframebuffer"), feature = "display_rgb565"))] +pub mod nofb_rgb565; +#[cfg(all(not(feature = "xframebuffer"), feature = "display_rgb565"))] +pub use nofb_rgb565::render_on_display; + +#[cfg(all( + feature = "xframebuffer", + feature = "display_rgb565", + not(feature = "display_rgba8888") +))] +pub mod fb_rgb565; +#[cfg(all( + feature = "xframebuffer", + feature = "display_rgb565", + not(feature = "display_rgba8888") +))] +pub use fb_rgb565::render_on_display; + +#[cfg(all( + feature = "xframebuffer", + feature = "display_rgba8888", + not(feature = "display_rgb565") +))] +pub mod fb_rgba8888; +#[cfg(all( + feature = "xframebuffer", + feature = "display_rgba8888", + not(feature = "display_rgb565") +))] +pub use fb_rgba8888::render_on_display; + +#[cfg(not(feature = "new_rendering"))] +pub mod fake_display; +#[cfg(not(feature = "new_rendering"))] +pub use fake_display::render_on_display; diff --git a/core/embed/rust/src/ui/shape/display/nofb_rgb565.rs b/core/embed/rust/src/ui/shape/display/nofb_rgb565.rs new file mode 100644 index 000000000..2fda844ed --- /dev/null +++ b/core/embed/rust/src/ui/shape/display/nofb_rgb565.rs @@ -0,0 +1,110 @@ +use crate::trezorhal::{bitblt::BitBlt, display}; + +use crate::ui::{ + display::Color, + geometry::{Offset, Rect}, +}; + +use super::super::{ + BasicCanvas, BitmapFormat, BitmapView, DrawingCache, ProgressiveRenderer, Viewport, +}; + +use static_alloc::Bump; + +// Maximum number of shapes on a single screen +const SHAPE_MAX_COUNT: usize = 45; +// Memory reserved for ProgressiveRenderes shape storage +const SHAPE_MEM_SIZE: usize = 5 * 1024; +// Memory not accessible by DMA +const BUMP_A_SIZE: usize = DrawingCache::get_bump_a_size() + SHAPE_MEM_SIZE; +// Memory accessible by DMA +const BUMP_B_SIZE: usize = DrawingCache::get_bump_b_size(); + +/// Creates the `Renderer` object for drawing on a display and invokes a +/// user-defined function that takes a single argument `target`. The user's +/// function can utilize the `target` for drawing on the display. +/// +/// `clip` specifies a rectangle area that the user will draw to. +/// If no clip is specified, the entire display area is used. +/// +/// `bg_color` specifies a background color with which the clip is filled before +/// the drawing starts. If the background color is None, the background +/// is undefined, and the user has to fill it themselves. +pub fn render_on_display<'a, F>(clip: Option, bg_color: Option, func: F) +where + F: FnOnce(&mut ProgressiveRenderer<'_, 'a, Bump<[u8; BUMP_A_SIZE]>, DisplayCanvas>), +{ + #[cfg_attr(not(target_os = "macos"), link_section = ".no_dma_buffers")] + static mut BUMP_A: Bump<[u8; BUMP_A_SIZE]> = Bump::uninit(); + + #[cfg_attr(not(target_os = "macos"), link_section = ".buf")] + static mut BUMP_B: Bump<[u8; BUMP_B_SIZE]> = Bump::uninit(); + + let bump_a = unsafe { &mut *core::ptr::addr_of_mut!(BUMP_A) }; + let bump_b = unsafe { &mut *core::ptr::addr_of_mut!(BUMP_B) }; + { + bump_a.reset(); + bump_b.reset(); + + let cache = DrawingCache::new(bump_a, bump_b); + let mut canvas = DisplayCanvas::new(); + + if let Some(clip) = clip { + canvas.set_viewport(Viewport::new(clip)); + } + + let mut target = + ProgressiveRenderer::new(&mut canvas, bg_color, &cache, bump_a, SHAPE_MAX_COUNT); + + func(&mut target); + + target.render(16); + } +} + +/// A simple display canvas allowing just two bitblt operations: +/// 'fill_rect' and 'draw_bitmap` needed by `ProgressiveRenderer`. +pub struct DisplayCanvas { + size: Offset, + viewport: Viewport, +} + +impl DisplayCanvas { + pub fn new() -> Self { + let size = Offset::new(display::DISPLAY_RESX as i16, display::DISPLAY_RESY as i16); + let viewport = Viewport::from_size(size); + Self { size, viewport } + } +} + +impl BasicCanvas for DisplayCanvas { + fn viewport(&self) -> Viewport { + self.viewport + } + + fn set_viewport(&mut self, viewport: Viewport) { + self.viewport = viewport.absolute_clip(self.bounds()); + } + + fn size(&self) -> Offset { + self.size + } + + fn fill_rect(&mut self, r: Rect, color: Color, _alpha: u8) { + let r = r.translate(self.viewport.origin); + if let Some(bitblt) = BitBlt::new_fill(r, self.viewport.clip, color, 255) { + unsafe { bitblt.display_fill() }; + } + } + + fn draw_bitmap(&mut self, r: Rect, bitmap: BitmapView) { + let r = r.translate(self.viewport.origin); + if let Some(bitblt) = BitBlt::new_copy(r, self.viewport.clip, &bitmap) { + match bitmap.format() { + BitmapFormat::RGB565 => unsafe { bitblt.display_copy_rgb565() }, + _ => panic!("Unsupported DMA operation"), + } + bitmap.bitmap.mark_dma_pending(); + } + } +} diff --git a/core/embed/rust/src/ui/shape/drawlib-rust-objects.drawio.svg b/core/embed/rust/src/ui/shape/drawlib-rust-objects.drawio.svg new file mode 100644 index 000000000..972961e04 --- /dev/null +++ b/core/embed/rust/src/ui/shape/drawlib-rust-objects.drawio.svg @@ -0,0 +1,4 @@ + + + +

<<trait>>
Shape


+ bounds(&DrawingCache) -> Offset

+ draw(&mut dyn Canvas, &DrawingCache)
+ cleanup(&DrawingCache)


<<trait>>...

<<trait>>
ShapeClone


+ clone_at_pool(&LocalAllocExt) -> Option<&dyn Shape>

<<trait>>...

<<trait>>
Renderer


+ render_shape(Shape + ShapeClone)

+ viewport() -> Viewport


+ set_viewport(Viewport)
+ set_window(window: Rect) -> Viewport

+ set_clip(clip: Rect) -> Viewport


+ in_window(Rect, &dyn Fn(&mut Self))

+ in_clip(Rect, &dyn Fn(&mut Self))

+ with_origin(Rect, &dyn Fn(&mut Self))



<<trait>>...

<<struct>>
DirectRenderer


- canvas: &mut C: Canvas
- cache: DrawingCache


+ new(&mut C: Canvas, bg_color: Option<Color>, &DrawingCache)

<<struct>>...

<<struct>>
ProgressiveRenderer


- canvas: &mut C: BasicCanvas
- bump: &LocalAllocExt

- shapes: FixedVec<ShapeHolder>

- viewport: Viewport

- bg_color: Option<Color>

- cache: DrawingCache



+ new(&mug C: BasicCanvvas, bg_color: Option<Color>, &DrawingCavhas,  &LocalAllocExt, max_shapes: usize) -> Self
+ render(lines: usize)


<<struct>>...

<<struct>>
ShapeHolder


+ shape: &mut dyn Shape
+ viewport: Viewport


<<struct>>...
Relation
Relation
0..n
0..n
1
1

<<struct>>
DrawingCache



+ new() -> Self

+ zlib() -> RefMut<ZlibCache>

+ jpeg() -> RefMut<JpegCache>

+ blur() -> RefMut<BlurCache>

+ image_buff() -> Option<RefMut<ImageBuffRef>>

+ render_buff() -> Option<RefMut<RenderBuffRef>>


<<struct>>...
1
1
1
1

<<struct>>
Viewport


origin: Offset
clip: Rect


+ from_size(Offset) -> Self

+ contains(Rect) -> bool

+ translate(Offset) -> Viewport

+ with_origin(Offset) -> Viewport

+ absolute_clip(Rect) -> Viewport

+ relative_clip(Rect) -> Viewport

+ relative_window(Rect) -> Viewport


<<struct>>...
Extends
Extends
1
1
1
1

<<struct>>
ZlibCache


- slots: [ZlibCacheSlot]


+ new(&bump, slot_count: u32) -> Option<Self>

+ uncompress(zdata: &[u8], offset: usize, dest_buf: &mut [u8]) -> Result<bool, ())

<<struct>>...

<<struct>>
ZlibCacheSlot


- zdata_tag: *const u8

- offset: usize

- dc: Option<UzlibContext>


+ empty() -> Self

+ initialize(zdata: &[u8])

+ reset()

+ uncompress(dest_buf: &mut [u8]) 

+ skip(nbytes: usize)

+ is_for(zdata: & [u8]) -> bool


<<struct>>...

<<struct>>
JpegCache


- slots: [JpegCacheSlot]


+ new(&bump, slot_count: u32) -> Option<Self>

<<struct>>...

<<struct>>
JpegCacheSlot


- jpeg: & [u8]
- input: tjpgd::BufferInput

- decoder: tjpgd::JDEC


+ new(jpeg: &[u8]) -> Result<Self, Error>

+ jpeg_size() -> Offset

+ is_for(jpeg: &[u8]) -> bool


<<struct>>...
1
1
1
1
Relation
Relation
0..n
0..n
1
1
Relation
Relation
0..1
0..1
1
1

<<struct>>
BlurCache


- algo: Option<BlurAlgorithm>
buff: &UnsafeCell<BlurBuff>

- tag: u32


+ new(&bump)

+ get(size: Offset, radius: usize, tag: Option<u32>) -> Result<&mut BlurAlgorithm<'a>, u32), ()>

<<struct>>...

<<trait>>
BasicCanvas


size() -> Offset

bounds() -> Rect

width() -> i16

height() -> i16


+ viewport() -> Viewport

+ set_viewport(Viewport)

set_window(Rect) -> Viewport

set_clip(Rect) -> Viewport


fill_rect(Rect, Color)

fill_background(Color)

draw_bitmap(Rect, BitmapRef)







<<trait>>...

<<trait>>
Canvas


+ view() -> BitmapView


draw_pixel(Point, Color)

blend_pixel(Point, Color, u8)


blend_bitmap(Rect, BitmapView)

blur_rect(Rect, radius)


draw_round_rect(Rect, radius, Color)

fill_round_rect(Rect, radius, color)


draw_circle(Point, radius, Color)

fill_circle(Point, radius, Color)


+ fill_sector(Point, i16, i16, i16, Color)

<<trait>>...
Extends
Extends

<<struct>>
Rgb565Canvas


- bitmap: Bitmap

- viewport: Viewport


+ new(size: Offset, Option<i16>, &mut [u8]) -> Option<Self>

+ row_mut(row: i16) -> Option<&mut [u16])


<<struct>>...

<<struct>>
Rgba8888Canvas


bitmap: Bitmap

viewport: Viewport


new(size: Offset, Option<i16>, &mut [u8) -> Option<Self>

row_mut(row: i16) -> Option<&mut [u32])


<<struct>>...
Use
Use

<<struct>>
Bitmap


- ptr: *mut u8

- stride: usize

- size: Offset

- format: BitmapFormat

- mutable: bool

- dma_pending: bool


+ new(BitmapFormat, Option<usize>, Offset,  Option<i16>, & [u8]) -> Self

new_mut(BitmapFormat, Option<usize>, Offset,  Option<i16>, &mut  [u8]) -> Self


size() -> Offset

width() -> i16

height() -> i16

+ stride() -> usize

+ view() -> BitmapView


+ row<T>(i16) -> Option<&[T}>

+ row_mut<T>(i16) -> Option<&mut [T}>

+ rows_mut<T>(i16, i16) -> Option<&mut [T}>


+ rgb565_fill(Rect, Rect, Color)

+ rgb565_copy(Rect, Rect, &BitmapView)

+ rgb565_blend(Rect, Rect, &BitmapView)


rgba8888_fill(Rect, Rect, Color)

rgba8888_copy(Rect, Rect, &BitmapView)

rgba8888_blend(Rect, Rect, &BitmapView)


mono8_fill(Rect, Rect, Color)

mono8_copy(Rect, Rect, &BitmapView)

mono8_blend(Rect, Rect, &BitmapView)




<<struct>>...

<<struct>>
BitmapView


bitmap: &Bitmap

- fg_color: Color
- bg_color: Color

- offset: Offset



+ new(&Bitmap) -> Self

+ with_fg(Color) -> Self

with_bg(Color) -> Self

with_offset(Offset) -> Self


+ size() -> Offset

+ width() -> i16

+ height() -> i16

+ format() -> BitmapFormat

+ row<T>(i16) -> Option<&[T]>

<<struct>>...

<<struct>>
BitBlt = ffi::gl_bitblt_t


+ new_fill(Rect, Rect, color) -> Option<Self>

+ new_copy(Rect, Rect, &BitmapView)

+ with_dst(&Bitmap)


+ rgb565_fill()

rgb565_copy_mono4()

rgb565_copy_rgb565()

rgb565_blend_mono4()


rgba8888_fill()

rgba8888_copy_mono4()

rgba8888_copy_rgb565()

rgba8888_copy_rgba8888()

rgba8888_blend_mono4()


mono8_fill()

mono8_copy_mono1p()

mono8_copy_mono4()

mono8_blend_mono1p()

mono8_blend_mono4()




<<struct>>...
Shape objects
Shape objects

<<struct>>
shape::Bar


area: Rect

- fg_color: Option<Color>
- bg_color: Option<Color>

- thickness: i16

radius: i16


+ new(Rect) -> Self

+ with_fg(Color) -> Self

with_bg(Color) -> Self

with_radius(i16) -> Self

with_thickness(i16) -> Self


+ render(&mut impl Renderer)

<<struct>>...

<<struct>>
shape::Circle


center:  Point

- fg_color: Option<Color>
- bg_color: Option<Color>

- thickness: i16

radius: i16

- start_angle: Option<i16>

- end_angle: Option<i16>


+ new(center: Point,radius: i16) -> Self

+ with_fg(Color) -> Self

with_bg(Color) -> Self

with_thickness(i16) -> Self

+ with_start_angle(i16) -> Self

+ with_end_angle(i16) -> Self



+ render(&mut impl Renderer)

<<struct>>...

<<struct>>
shape::Text


area: Rect

text: &str

- color: Color

- font: Font


+ new(area: Rect, &str) -> Self

+ with_fg(Color) -> Self

with_font(Font) -> Self





+ render(&mut impl Renderer)

<<struct>>...

<<struct>>
shape::ToifImage


- pos: Point

- toif: Toif

- fg_color: Color

- bg_color: Option<Color>


+ new(area: Rect, Toif) -> Self

+ with_fg(Color) -> Self

with_bg(Color) -> Self





+ render(&mut impl Renderer)

<<struct>>...

<<struct>>
shape::JpegImage


- pos: Point

- jpeg: &[u8]

- scale: u8

- blur_radius: usize


+ new(area: Rect, &[u8]) -> Self

with_scale(u8) -> Self

with_blur(usize) -> Self





+ render(&mut impl Renderer)

<<struct>>...

<<struct>>
shape::QrCode


- area: Rect

- qr_modules: [u8; ..]

- qr_size: i16

- fg_color: Color

- bg_color: Option<Color>


+ new(pos: Point, &str) -> Self






+ render(&mut impl Renderer)

<<struct>>...

<<struct>>
shape::Blurring


- area: Rect

- radius: usize


+ new(area: Rect, usize) -> Self









+ render(&mut impl Renderer)

<<struct>>...
1
1

<<struct>>
Mono8Canvas


bitmap: Bitmap

viewport: Viewport


new(size: Offset, Option<i16>, &mut [u8) -> Option<Self>

row_mut(row: i16) -> Option<&mut [u32])


<<struct>>...
\ No newline at end of file diff --git a/core/embed/rust/src/ui/shape/drawlib-toplevel-arch.drawio.svg b/core/embed/rust/src/ui/shape/drawlib-toplevel-arch.drawio.svg new file mode 100644 index 000000000..05e7faaee --- /dev/null +++ b/core/embed/rust/src/ui/shape/drawlib-toplevel-arch.drawio.svg @@ -0,0 +1,4 @@ + + + +
SHAPE
SHAPE
RENDERER
RENDERER
CANVAS
CANVAS
BitBlt (bitblt.rs)

2D bit-block transfer routines 
(Rust API)
BitBlt (bitblt.rs)...
gl_bitblt

2D bit-block transfer routines
(C API)
gl_bitblt...
dma2d_bitblt

Hardware accelerated bitblt operations
dma2d_bitblt...
display_driver

Display initialization, framebuffer implementation, backlight, orientation, synchronization

display_driver...
Bit-blt API
for simple access or non-fb drivers
Bit-blt API...
gl_draw

Simple drawing routines
(C API)
gl_draw...
Canvas
Canvas
BasicCanvas
BasicCanvas
LIB
(C)
LIB...
HAL
(C)
HAL...
TrezorHal (RUST)
TrezorH...
High-level abstraction for drawing implements simple objects (such as BarCircleImageToifImageJpeg) that can be renderered on the display through the lower layer. 
High-level abstraction for drawing implements simple objects (such as Bar, C...
Provides two distinguished mechanisms for rendering shape on the display: DirectRenderer for drawing using display drivers exposing framebuffer, and ProgressiveRenderer for drawing on displays without framebuffer in MCU RAM providing just simple bit-blt operations.
Provides two distinguished mechanisms for rendering shape on the display: Dire...
Several implementations of the Canvas trait (Rgb565Canvas, RGBA8888Canvas, Mono8Canvas) for different types of framebuffers/displays.
Several implementations of the Canvas trait (Rgb565Canvas, RGBA8888Canvas...
direct drawing without rust lib
direct drawing without rust...
UI (RUST)
UI (RUS...
\ No newline at end of file diff --git a/core/embed/rust/src/ui/shape/jpeg.rs b/core/embed/rust/src/ui/shape/jpeg.rs new file mode 100644 index 000000000..58aa1ee4c --- /dev/null +++ b/core/embed/rust/src/ui/shape/jpeg.rs @@ -0,0 +1,196 @@ +use crate::ui::geometry::{Alignment2D, Offset, Point, Rect}; + +use super::{Bitmap, BitmapFormat, BitmapView, Canvas, DrawingCache, Renderer, Shape, ShapeClone}; + +use without_alloc::alloc::LocalAllocLeakExt; + +/// A shape for rendering compressed JPEG images. +pub struct JpegImage<'a> { + /// Image position + pos: Point, + // Image position alignment + align: Alignment2D, + /// JPEG data + jpeg: &'a [u8], + /// Scale factor (default 0) + scale: u8, + /// Blurring radius or 0 if no blurring required (default 0) + blur_radius: usize, + /// Dimming of blurred image in range of 0..255 (default 255) + dim: u8, + /// Set if blurring is pending + /// (used only during image drawing). + blur_tag: Option, +} + +impl<'a> JpegImage<'a> { + pub fn new(pos: Point, jpeg: &'a [u8]) -> Self { + JpegImage { + pos, + align: Alignment2D::TOP_LEFT, + scale: 0, + dim: 255, + blur_radius: 0, + jpeg, + blur_tag: None, + } + } + + pub fn with_align(self, align: Alignment2D) -> Self { + Self { align, ..self } + } + + pub fn with_scale(self, scale: u8) -> Self { + assert!(scale <= 3); + Self { scale, ..self } + } + + pub fn with_blur(self, blur_radius: usize) -> Self { + Self { + blur_radius, + ..self + } + } + + pub fn with_dim(self, dim: u8) -> Self { + Self { dim, ..self } + } + + pub fn render(self, renderer: &mut impl Renderer<'a>) { + renderer.render_shape(self); + } +} + +impl<'a> Shape<'a> for JpegImage<'a> { + fn bounds(&self, cache: &DrawingCache<'a>) -> Rect { + let size = unwrap!(cache.jpeg().get_size(self.jpeg, self.scale), "Invalid JPEG"); + Rect::from_top_left_and_size(size.snap(self.pos, self.align), size) + } + + fn cleanup(&mut self, _cache: &DrawingCache<'a>) { + self.blur_tag = None; + } + + /* + // Faster implementation suitable for DirectRenderer without blurring support + // (but is terribly slow on ProgressiveRenderer if slices are not aligned + // to JPEG MCUs ) + fn draw(&mut self, canvas: &mut dyn RgbCanvasEx, cache: &DrawingCache<'a>) { + let bounds = self.bounds(cache); + let clip = canvas.viewport().relative_clip(bounds).clip; + + // translate clip to JPEG relative coordinates + let clip = clip.translate(-canvas.viewport().origin); + let clip = clip.translate((-bounds.top_left()).into()); + + unwrap!( + cache.jpeg().decompress_mcu( + self.jpeg, + self.scale, + clip.top_left(), + &mut |mcu_r, mcu_bitmap| { + // Draw single MCU + canvas.draw_bitmap(mcu_r.translate(bounds.top_left().into()), mcu_bitmap); + // Return true if we are not done yet + mcu_r.x1 < clip.x1 || mcu_r.y1 < clip.y1 + } + ), + "Invalid JPEG" + ); + }*/ + + // This is a little bit slower implementation suitable for ProgressiveRenderer + fn draw(&mut self, canvas: &mut dyn Canvas, cache: &DrawingCache<'a>) { + let bounds = self.bounds(cache); + let clip = canvas.viewport().relative_clip(bounds).clip; + + // Translate clip to JPEG relative coordinates + let clip = clip.translate(-canvas.viewport().origin); + let clip = clip.translate((-bounds.top_left()).into()); + + if self.blur_radius == 0 { + // Draw JPEG without blurring + + // Routine for drawing single JPEG MCU + let draw_mcu = &mut |row_r: Rect, row_bitmap: BitmapView| { + // Draw a row of decoded MCUs + canvas.draw_bitmap(row_r.translate(bounds.top_left().into()), row_bitmap); + // Return true if we are not done yet + row_r.y1 < clip.y1 + }; + + unwrap!( + cache + .jpeg() + .decompress_row(self.jpeg, self.scale, clip.y0, draw_mcu), + "Invalid JPEG" + ); + } else { + // Draw JPEG with blurring effect + let jpeg_size = self.bounds(cache).size(); + + // Get a single line working bitmap + let buff = &mut unwrap!(cache.image_buff(), "No image buffer"); + let mut slice = unwrap!( + Bitmap::new( + BitmapFormat::RGB565, + None, + Offset::new(jpeg_size.x, 1), + None, + &buff[..] + ), + "Too small buffer" + ); + + // Get the blurring algorithm instance + let mut blur_cache = cache.blur(); + let (blur, blur_tag) = + unwrap!(blur_cache.get(jpeg_size, self.blur_radius, self.blur_tag)); + self.blur_tag = Some(blur_tag); + + if let Some(y) = blur.push_ready() { + // A function for drawing a row of JPEG MCUs + let draw_row = &mut |row_r: Rect, jpeg_slice: BitmapView| { + loop { + if let Some(y) = blur.push_ready() { + if y < row_r.y1 { + // should never fail + blur.push(unwrap!(jpeg_slice.row(y - row_r.y0))); + } else { + return true; // need more data + } + } + + if let Some(y) = blur.pop_ready() { + blur.pop(unwrap!(slice.row_mut(0)), Some(self.dim)); // should never fail + let dst_r = Rect::from_top_left_and_size(bounds.top_left(), jpeg_size) + .translate(Offset::new(0, y)); + canvas.draw_bitmap(dst_r, slice.view()); + + if y + 1 >= clip.y1 { + return false; // we are done + } + } + } + }; + + unwrap!( + cache + .jpeg() + .decompress_row(self.jpeg, self.scale, y, draw_row), + "Invalid JPEG" + ); + } + } + } +} + +impl<'a> ShapeClone<'a> for JpegImage<'a> { + fn clone_at_bump<'alloc, T>(self, bump: &'alloc T) -> Option<&'alloc mut dyn Shape<'a>> + where + T: LocalAllocLeakExt<'alloc>, + { + let clone = bump.alloc_t::()?; + Some(clone.uninit.init(JpegImage { ..self })) + } +} diff --git a/core/embed/rust/src/ui/shape/mod.rs b/core/embed/rust/src/ui/shape/mod.rs new file mode 100644 index 000000000..9fc9d7fe2 --- /dev/null +++ b/core/embed/rust/src/ui/shape/mod.rs @@ -0,0 +1,33 @@ +mod algo; +mod bar; +mod base; +mod bitmap; +#[cfg(feature = "ui_blurring")] +mod blur; +mod cache; +mod canvas; +mod circle; +mod display; +#[cfg(feature = "ui_jpeg_decoder")] +mod jpeg; +mod qrcode; +mod render; +mod text; +mod toif; + +pub use algo::PI4; +pub use bar::Bar; +pub use base::{Shape, ShapeClone}; +pub use bitmap::{Bitmap, BitmapFormat, BitmapView}; +#[cfg(feature = "ui_blurring")] +pub use blur::Blurring; +pub use cache::drawing_cache::DrawingCache; +pub use canvas::{BasicCanvas, Canvas, Mono8Canvas, Rgb565Canvas, Rgba8888Canvas, Viewport}; +pub use circle::Circle; +pub use display::render_on_display; +#[cfg(feature = "ui_jpeg_decoder")] +pub use jpeg::JpegImage; +pub use qrcode::QrImage; +pub use render::{DirectRenderer, ProgressiveRenderer, Renderer}; +pub use text::Text; +pub use toif::ToifImage; diff --git a/core/embed/rust/src/ui/shape/qrcode.rs b/core/embed/rust/src/ui/shape/qrcode.rs new file mode 100644 index 000000000..c9f956eb2 --- /dev/null +++ b/core/embed/rust/src/ui/shape/qrcode.rs @@ -0,0 +1,169 @@ +use crate::ui::{ + display::Color, + geometry::{Offset, Rect}, +}; + +use qrcodegen::QrCode; + +use super::{ + algo::line_points, Bitmap, BitmapFormat, Canvas, DrawingCache, Renderer, Shape, ShapeClone, +}; + +use without_alloc::alloc::LocalAllocLeakExt; + +const MAX_QRCODE_BYTES: usize = 400; + +/// A shape for `QrCode` rendering. +pub struct QrImage { + /// Destination rectangle + area: Rect, + /// QR code bitmap + qr_modules: [u8; MAX_QRCODE_BYTES], + /// Size of QR code bitmap in bytes + qr_size: i16, + /// Foreground color + fg_color: Color, + /// Optional background color + bg_color: Option, +} + +impl QrImage { + pub fn new(area: Rect, qrcode: &QrCode) -> Self { + if area.width() < qrcode.size() as i16 || area.height() < qrcode.size() as i16 { + panic!("Too small area"); + } + + let mut result = QrImage { + area, + qr_size: qrcode.size() as i16, + qr_modules: [0u8; MAX_QRCODE_BYTES], + fg_color: Color::white(), + bg_color: None, + }; + + // Copy content of QR code to the qrmodules buffer + for y in 0..result.qr_size { + for x in 0..result.qr_size { + result.set_module(x, y, qrcode.get_module(x as i32, y as i32)); + } + } + + result + } + + fn set_module(&mut self, x: i16, y: i16, value: bool) { + // Every row starts at byte aligned address + let row_offset = (y * (self.qr_size + 7) / 8) as usize; + let row = &mut self.qr_modules[row_offset..]; + let col_offset = (x / 8) as usize; + let col_bit = 1 << (x & 0x7); + if value { + row[col_offset] |= col_bit; + } else { + row[col_offset] &= col_bit ^ 0xFF; + } + } + + fn get_module(&self, x: i16, y: i16) -> bool { + // Every row starts at byte aligned address + let row_offset = (y * (self.qr_size + 7) / 8) as usize; + let row = &self.qr_modules[row_offset..]; + let col_offset = (x / 8) as usize; + let col_bit = 1 << (x & 0x7); + (row[col_offset] & col_bit) != 0 + } + + pub fn with_fg(self, fg_color: Color) -> Self { + Self { fg_color, ..self } + } + + pub fn with_bg(self, bg_color: Color) -> Self { + Self { + bg_color: Some(bg_color), + ..self + } + } + + pub fn render<'s>(self, renderer: &mut impl Renderer<'s>) { + renderer.render_shape(self); + } + + fn draw_row(&self, slice_row: &mut [u8], qr_y: i16) { + slice_row.iter_mut().for_each(|b| *b = 0); + + let mut qr_module = false; + + for p in line_points(self.area.x1 - self.area.x0, self.qr_size, 0) { + if p.first { + qr_module = self.get_module(p.v, qr_y); + } + if !qr_module { + if p.u & 0x01 == 0 { + slice_row[(p.u / 2) as usize] |= 0x0F; + } else { + slice_row[(p.u / 2) as usize] |= 0xF0; + } + } + } + } +} + +impl Shape<'_> for QrImage { + fn bounds(&self, _cache: &DrawingCache) -> Rect { + self.area + } + + fn cleanup(&mut self, _cache: &DrawingCache) {} + + fn draw(&mut self, canvas: &mut dyn Canvas, cache: &DrawingCache) { + let buff = &mut unwrap!(cache.image_buff(), "No TOIF buffer"); + + let mut slice = unwrap!( + Bitmap::new_mut( + BitmapFormat::MONO4, + None, + Offset::new(self.area.width(), 1), + Some(1), + &mut buff[..] + ), + "Too small buffer" + ); + + let clip = canvas.viewport().relative_clip(self.bounds(cache)).clip; + + // translate clip to the relative coordinates + let clip = clip.translate(-canvas.viewport().origin); + let clip = clip.translate((-self.area.top_left()).into()); + + for p in line_points(self.area.y1 - self.area.y0, self.qr_size, clip.y0) + .take(clip.height() as usize) + { + if p.first { + self.draw_row(slice.row_mut(0).unwrap(), p.v); + } + + let r = Rect { + y0: self.area.y0 + p.u, + y1: self.area.y0 + p.u + 1, + ..self.area + }; + + let slice_view = slice.view().with_fg(self.fg_color); + + match self.bg_color { + Some(bg_color) => canvas.draw_bitmap(r, slice_view.with_bg(bg_color)), + None => canvas.blend_bitmap(r, slice_view), + } + } + } +} + +impl<'s> ShapeClone<'s> for QrImage { + fn clone_at_bump<'alloc, T>(self, bump: &'alloc T) -> Option<&'alloc mut dyn Shape<'s>> + where + T: LocalAllocLeakExt<'alloc>, + { + let clone = bump.alloc_t::()?; + Some(clone.uninit.init(QrImage { ..self })) + } +} diff --git a/core/embed/rust/src/ui/shape/render.rs b/core/embed/rust/src/ui/shape/render.rs new file mode 100644 index 000000000..dd5191dd5 --- /dev/null +++ b/core/embed/rust/src/ui/shape/render.rs @@ -0,0 +1,250 @@ +use crate::ui::{ + display::Color, + geometry::{Offset, Point, Rect}, +}; + +use super::{BasicCanvas, Canvas, DrawingCache, Rgb565Canvas, Shape, ShapeClone, Viewport}; + +use without_alloc::{alloc::LocalAllocLeakExt, FixedVec}; + +// ========================================================================== +// trait Renderer +// ========================================================================== + +/// All renders must implement Renderer trait +/// Renderers can immediately use the draw() method of the passed shape or +/// may store it (using the boxed() method) and draw it later +pub trait Renderer<'a> { + fn viewport(&self) -> Viewport; + + fn set_viewport(&mut self, viewport: Viewport); + + fn set_window(&mut self, window: Rect) -> Viewport { + let viewport = self.viewport(); + self.set_viewport(viewport.relative_window(window)); + viewport + } + + fn set_clip(&mut self, clip: Rect) -> Viewport { + let viewport = self.viewport(); + self.set_viewport(viewport.relative_clip(clip)); + viewport + } + + fn render_shape(&mut self, shape: S) + where + S: Shape<'a> + ShapeClone<'a>; + + fn in_window(&mut self, r: Rect, inner: &dyn Fn(&mut Self)) { + let original = self.set_window(r); + inner(self); + self.set_viewport(original); + } + + fn in_clip(&mut self, r: Rect, inner: &dyn Fn(&mut Self)) { + let original = self.set_clip(r); + inner(self); + self.set_viewport(original); + } + + fn with_origin(&mut self, origin: Offset, inner: &dyn Fn(&mut Self)) { + let original = self.viewport(); + self.set_viewport(self.viewport().with_origin(origin)); + inner(self); + self.set_viewport(original); + } +} + +// ========================================================================== +// struct DirectRenderer +// ========================================================================== + +/// A simple implementation of a Renderer that draws directly onto the CanvasEx +pub struct DirectRenderer<'a, 'alloc, C> +where + C: Canvas, +{ + /// Target canvas + canvas: &'a mut C, + /// Drawing cache (decompression context, scratch-pad memory) + cache: &'a DrawingCache<'alloc>, +} + +impl<'a, 'alloc, C> DirectRenderer<'a, 'alloc, C> +where + C: Canvas, +{ + /// Creates a new DirectRenderer instance with the given canvas + pub fn new( + canvas: &'a mut C, + bg_color: Option, + cache: &'a DrawingCache<'alloc>, + ) -> Self { + if let Some(color) = bg_color { + canvas.fill_background(color); + } + + // TODO: consider storing original canvas.viewport + // and restoring it by drop() function + + Self { canvas, cache } + } +} + +impl<'a, 'alloc, C> Renderer<'alloc> for DirectRenderer<'a, 'alloc, C> +where + C: Canvas, +{ + fn viewport(&self) -> Viewport { + self.canvas.viewport() + } + + fn set_viewport(&mut self, viewport: Viewport) { + self.canvas.set_viewport(viewport); + } + + fn render_shape(&mut self, mut shape: S) + where + S: Shape<'alloc> + ShapeClone<'alloc>, + { + if self.canvas.viewport().contains(shape.bounds(self.cache)) { + shape.draw(self.canvas, self.cache); + shape.cleanup(self.cache); + } + } +} + +// ========================================================================== +// struct ProgressiveRenderer +// ========================================================================== + +struct ShapeHolder<'a> { + shape: &'a mut dyn Shape<'a>, + viewport: Viewport, +} + +/// A more advanced Renderer implementation that supports deferred rendering. +pub struct ProgressiveRenderer<'a, 'alloc, T, C> +where + T: LocalAllocLeakExt<'alloc>, + C: BasicCanvas, +{ + /// Target canvas + canvas: &'a mut C, + /// Bump for cloning shapes + bump: &'alloc T, + /// List of rendered shapes + shapes: FixedVec<'alloc, ShapeHolder<'alloc>>, + /// Current viewport + viewport: Viewport, + // Default background color + bg_color: Option, + /// Drawing cache (decompression context, scratch-pad memory) + cache: &'a DrawingCache<'alloc>, +} + +impl<'a, 'alloc, T, C> ProgressiveRenderer<'a, 'alloc, T, C> +where + T: LocalAllocLeakExt<'alloc>, + C: BasicCanvas, +{ + /// Creates a new ProgressiveRenderer instance + pub fn new( + canvas: &'a mut C, + bg_color: Option, + cache: &'a DrawingCache<'alloc>, + bump: &'alloc T, + max_shapes: usize, + ) -> Self { + let viewport = canvas.viewport(); + Self { + canvas, + bump, + shapes: unwrap!(bump.fixed_vec(max_shapes), "No shape memory"), + viewport, + bg_color, + cache, + } + } + + /// Renders stored shapes onto the specified canvas + pub fn render(&mut self, lines: usize) { + let canvas_clip = self.canvas.viewport().clip; + let canvas_origin = self.canvas.viewport().origin; + + let buff = &mut unwrap!(self.cache.render_buff(), "No render buffer"); + + let mut slice = unwrap!( + Rgb565Canvas::new( + Offset::new(canvas_clip.width(), lines as i16), + None, + Some(1), + &mut buff[..], + ), + "No render memory" + ); + + for y in (canvas_clip.y0..canvas_clip.y1).step_by(lines) { + // Calculate the coordinates of the slice we will draw into + let slice_r = Rect::new( + // slice_r is in absolute coordinates + Point::new(canvas_clip.x0, y), + Point::new(canvas_clip.x1, y + lines as i16), + ) + .translate(-canvas_origin); + + // Clear the slice background + if let Some(color) = self.bg_color { + slice.set_viewport(Viewport::from_size(slice_r.size())); + slice.fill_background(color); + } + + // Draw all shapes that overlaps the slice + for holder in self.shapes.iter_mut() { + let shape_viewport = holder.viewport.absolute_clip(slice_r); + let shape_bounds = holder.shape.bounds(self.cache); + + // Is the shape overlapping the current slice? + if shape_viewport.contains(shape_bounds) { + slice.set_viewport(shape_viewport.translate((-slice_r.top_left()).into())); + holder.shape.draw(&mut slice, self.cache); + + if shape_bounds.y1 + shape_viewport.origin.y <= shape_viewport.clip.y1 { + // The shape will never be drawn again + holder.shape.cleanup(self.cache); + } + } + } + self.canvas.draw_bitmap(slice_r, slice.view()); + } + } +} + +impl<'a, 'alloc, T, C> Renderer<'alloc> for ProgressiveRenderer<'a, 'alloc, T, C> +where + T: LocalAllocLeakExt<'alloc>, + C: BasicCanvas, +{ + fn viewport(&self) -> Viewport { + self.viewport + } + + fn set_viewport(&mut self, viewport: Viewport) { + self.viewport = viewport.absolute_clip(self.canvas.bounds()); + } + + fn render_shape(&mut self, shape: S) + where + S: Shape<'alloc> + ShapeClone<'alloc>, + { + // Is the shape visible? + if self.viewport.contains(shape.bounds(self.cache)) { + // Clone the shape & push it to the list + let holder = ShapeHolder { + shape: unwrap!(shape.clone_at_bump(self.bump), "No shape memory"), + viewport: self.viewport, + }; + unwrap!(self.shapes.push(holder), "Shape list full"); + } + } +} diff --git a/core/embed/rust/src/ui/shape/text.rs b/core/embed/rust/src/ui/shape/text.rs new file mode 100644 index 000000000..cc83da8b2 --- /dev/null +++ b/core/embed/rust/src/ui/shape/text.rs @@ -0,0 +1,134 @@ +use crate::ui::{ + display::{Color, Font}, + geometry::{Alignment, Offset, Point, Rect}, +}; + +use super::{BitmapView, Canvas, DrawingCache, Renderer, Shape, ShapeClone}; + +use without_alloc::alloc::LocalAllocLeakExt; + +/// A shape for text strings rendering. +pub struct Text<'a> { + // Text position + pos: Point, + // Text string + text: &'a str, + // Text color + color: Color, + // Text font + font: Font, + // Horizontal alignment + align: Alignment, + // Final bounds calculated when rendered + bounds: Rect, +} + +impl<'a> Text<'a> { + /// Creates a `shape::Text` structure with a specified + /// text (`str`) and the top-left corner (`pos`). + pub fn new(pos: Point, text: &'a str) -> Self { + Self { + pos, + text, + color: Color::white(), + font: Font::NORMAL, + align: Alignment::Start, + bounds: Rect::zero(), + } + } + + pub fn with_fg(self, color: Color) -> Self { + Self { color, ..self } + } + + pub fn with_font(self, font: Font) -> Self { + Self { font, ..self } + } + + pub fn with_align(self, align: Alignment) -> Self { + Self { align, ..self } + } + + pub fn render<'r>(mut self, renderer: &mut impl Renderer<'r>) { + self.bounds = self.calc_bounds(); + renderer.render_shape(self); + } + + fn aligned_pos(&self) -> Point { + match self.align { + Alignment::Start => self.pos, + Alignment::Center => Point::new( + self.font.horz_center(self.pos.x, self.pos.x, self.text), + self.pos.y, + ), + Alignment::End => Point::new(self.pos.x - self.font.text_width(self.text), self.pos.y), + } + } + + fn calc_bounds(&self) -> Rect { + let pos = self.aligned_pos(); + let (ascent, descent) = self.font.visible_text_height_ex(self.text); + Rect { + x0: pos.x, + y0: pos.y - ascent, + x1: pos.x + self.font.text_width(self.text), + y1: pos.y + descent, + } + } +} + +impl<'a> Shape<'_> for Text<'a> { + fn bounds(&self, _cache: &DrawingCache) -> Rect { + self.bounds + } + + fn cleanup(&mut self, _cache: &DrawingCache) {} + + fn draw(&mut self, canvas: &mut dyn Canvas, cache: &DrawingCache) { + let mut r = self.bounds(cache); + let max_ascent = self.pos.y - r.y0; + + // TODO: optimize text clipping, use canvas.viewport() + + for ch in self.text.chars() { + if r.x0 >= r.x1 { + break; + } + + let glyph = self.font.get_glyph(ch); + let glyph_bitmap = glyph.bitmap(); + let glyph_view = BitmapView::new(&glyph_bitmap) + .with_fg(self.color) + .with_offset(Offset::new( + -glyph.bearing_x, + -(max_ascent - glyph.bearing_y), + )); + + canvas.blend_bitmap(r, glyph_view); + r.x0 += glyph.adv; + } + } +} + +impl<'a, 's> ShapeClone<'s> for Text<'a> { + fn clone_at_bump<'alloc, T>(self, bump: &'alloc T) -> Option<&'alloc mut dyn Shape<'s>> + where + T: LocalAllocLeakExt<'alloc>, + { + let clone = bump.alloc_t::()?; + let text = bump.copy_str(self.text)?; + Some(clone.uninit.init(Text { text, ..self })) + } +} + +impl Font { + fn visible_text_height_ex(&self, text: &str) -> (i16, i16) { + let (mut ascent, mut descent) = (0, 0); + for c in text.chars() { + let glyph = self.get_glyph(c); + ascent = ascent.max(glyph.bearing_y); + descent = descent.max(glyph.height - glyph.bearing_y); + } + (ascent, descent) + } +} diff --git a/core/embed/rust/src/ui/shape/toif.rs b/core/embed/rust/src/ui/shape/toif.rs new file mode 100644 index 000000000..f01dde5cf --- /dev/null +++ b/core/embed/rust/src/ui/shape/toif.rs @@ -0,0 +1,174 @@ +use crate::ui::{ + display::{toif::Toif, Color}, + geometry::{Alignment2D, Offset, Point, Rect}, +}; + +use super::{Bitmap, BitmapFormat, Canvas, DrawingCache, Renderer, Shape, ShapeClone}; + +use without_alloc::alloc::LocalAllocLeakExt; + +/// A shape for rendering compressed TOIF images. +pub struct ToifImage<'a> { + /// Image position + pos: Point, + // Image position alignment + align: Alignment2D, + // Image data + toif: Toif<'a>, + // Foreground color + fg_color: Color, + // Optional background color + bg_color: Option, +} + +impl<'a> ToifImage<'a> { + pub fn new(pos: Point, toif: Toif<'a>) -> Self { + Self { + pos, + align: Alignment2D::TOP_LEFT, + toif, + fg_color: Color::white(), + bg_color: None, + } + } + + pub fn with_align(self, align: Alignment2D) -> Self { + Self { align, ..self } + } + + pub fn with_fg(self, fg_color: Color) -> Self { + Self { fg_color, ..self } + } + + pub fn with_bg(self, bg_color: Color) -> Self { + Self { + bg_color: Some(bg_color), + ..self + } + } + + pub fn render(self, renderer: &mut impl Renderer<'a>) { + renderer.render_shape(self); + } + + fn draw_grayscale(&self, canvas: &mut dyn Canvas, cache: &DrawingCache<'a>) { + // TODO: introduce new viewport/shape function for this calculation + let bounds = self.bounds(cache); + let viewport = canvas.viewport(); + let mut clip = self + .bounds(cache) + .clamp(viewport.clip.translate(-viewport.origin)) + .translate((-bounds.top_left()).into()); + + let buff = &mut unwrap!(cache.image_buff(), "No image buffer"); + let mut slice = unwrap!( + Bitmap::new_mut( + BitmapFormat::MONO4, + None, + self.toif.size(), + Some(1), + &mut buff[..] + ), + "Too small buffer" + ); + + while !clip.is_empty() { + let height = core::cmp::min(slice.height(), clip.height()); + unwrap!( + cache.zlib().uncompress_toif( + self.toif, + clip.y0, + unwrap!(slice.rows_mut(0, height)), // should never fail + ), + "Invalid TOIF" + ); + + let r = clip.translate(bounds.top_left().into()); + + let slice_view = slice + .view() + .with_fg(self.fg_color) + .with_offset(Offset::new(r.x0 - bounds.top_left().x, 0)); + + match self.bg_color { + Some(bg_color) => canvas.draw_bitmap(r, slice_view.with_bg(bg_color)), + None => canvas.blend_bitmap(r, slice_view), + } + + clip.y0 += height; + } + } + + fn draw_rgb(&self, canvas: &mut dyn Canvas, cache: &DrawingCache<'a>) { + // TODO: introduce new viewport/shape function for this calculation + let bounds = self.bounds(cache); + let viewport = canvas.viewport(); + let mut clip = self + .bounds(cache) + .clamp(viewport.clip.translate(-viewport.origin)) + .translate((-bounds.top_left()).into()); + + let buff = &mut unwrap!(cache.image_buff(), "No image buffer"); + let mut slice = unwrap!( + Bitmap::new_mut( + BitmapFormat::RGB565, + None, + self.toif.size(), + Some(1), + &mut buff[..] + ), + "Too small buffer" + ); + + while !clip.is_empty() { + let height = core::cmp::min(slice.height(), clip.height()); + + if let Some(row_bytes) = slice.rows_mut(0, height) { + // always true + unwrap!( + cache.zlib().uncompress_toif(self.toif, clip.y0, row_bytes,), + "Invalid TOIF" + ); + } + + let r = clip.translate(bounds.top_left().into()); + + let slice_view = slice + .view() + .with_offset(Offset::new(r.x0 - bounds.top_left().x, 0)); + + canvas.draw_bitmap(r, slice_view); + + clip.y0 += height; + } + } +} + +impl<'a> Shape<'a> for ToifImage<'a> { + fn bounds(&self, _cache: &DrawingCache<'a>) -> Rect { + let size = Offset::new(self.toif.width(), self.toif.height()); + Rect::from_top_left_and_size(size.snap(self.pos, self.align), size) + } + + fn cleanup(&mut self, _cache: &DrawingCache<'a>) { + // TODO: inform the cache that we won't use the zlib slot anymore + } + + fn draw(&mut self, canvas: &mut dyn Canvas, cache: &DrawingCache<'a>) { + if self.toif.is_grayscale() { + self.draw_grayscale(canvas, cache); + } else { + self.draw_rgb(canvas, cache); + } + } +} + +impl<'a> ShapeClone<'a> for ToifImage<'a> { + fn clone_at_bump<'alloc, T>(self, bump: &'alloc T) -> Option<&'alloc mut dyn Shape<'a>> + where + T: LocalAllocLeakExt<'alloc>, + { + let clone = bump.alloc_t::()?; + Some(clone.uninit.init(ToifImage { ..self })) + } +} diff --git a/core/embed/rust/src/ui/util.rs b/core/embed/rust/src/ui/util.rs index f3e4f33fb..6fac941de 100644 --- a/core/embed/rust/src/ui/util.rs +++ b/core/embed/rust/src/ui/util.rs @@ -179,6 +179,12 @@ macro_rules! include_icon { }; } +/// Splits a version stored as a u32 into four numbers +/// starting with the major version. +pub fn version_split(version: u32) -> [u8; 4] { + version.to_le_bytes() +} + #[cfg(test)] mod tests { use crate::strutil;