diff --git a/core/SConscript.bootloader b/core/SConscript.bootloader
index a29701e2b..a40309ce3 100644
--- a/core/SConscript.bootloader
+++ b/core/SConscript.bootloader
@@ -81,6 +81,15 @@ CPPPATH_MOD += [
SOURCE_MOD += [
'embed/extmod/modtrezorcrypto/rand.c',
+ 'embed/gdc/gdc_color.c',
+ 'embed/gdc/gdc_core.c',
+ 'embed/gdc/gdc_mono8_ops.c',
+ 'embed/gdc/gdc_rgb565.c',
+ 'embed/gdc/gdc_rgb565_ops.c',
+ 'embed/gdc/gdc_rgba8888.c',
+ 'embed/gdc/gdc_text.c',
+ 'embed/gdc/gdc_wnd565.c',
+ 'embed/gdc/gdc_wnd565_ops.c',
'embed/lib/buffers.c',
'embed/lib/colors.c',
'embed/lib/display_draw.c',
@@ -220,6 +229,9 @@ def cargo_build():
features.append("bootloader")
features.extend(FEATURES_AVAILABLE)
+ if TREZOR_MODEL in ('T',):
+ features.append('ui_antialiasing')
+
cargo_opts = [
f'--target={env.get("ENV")["RUST_TARGET"]}',
f'--target-dir=../../build/bootloader/rust',
diff --git a/core/SConscript.firmware b/core/SConscript.firmware
index f312990cf..13fa447e7 100644
--- a/core/SConscript.firmware
+++ b/core/SConscript.firmware
@@ -188,6 +188,15 @@ CPPPATH_MOD += [
]
SOURCE_MOD += [
'embed/extmod/modtrezorui/modtrezorui.c',
+ 'embed/gdc/gdc_color.c',
+ 'embed/gdc/gdc_core.c',
+ 'embed/gdc/gdc_mono8_ops.c',
+ 'embed/gdc/gdc_rgb565.c',
+ 'embed/gdc/gdc_rgb565_ops.c',
+ 'embed/gdc/gdc_rgba8888.c',
+ 'embed/gdc/gdc_text.c',
+ 'embed/gdc/gdc_wnd565.c',
+ 'embed/gdc/gdc_wnd565_ops.c',
'embed/lib/buffers.c',
'embed/lib/colors.c',
'embed/lib/display_draw.c',
@@ -734,6 +743,11 @@ def cargo_build():
features.append('translations')
if PYOPT == '0':
features.append('debug')
+ features.append('ui_debug')
+ if TREZOR_MODEL in ('T',):
+ features.append('ui_antialiasing')
+ features.append('ui_blurring')
+ features.append('ui_jpeg_decoder')
features.extend(FEATURES_AVAILABLE)
diff --git a/core/SConscript.unix b/core/SConscript.unix
index 1ff872700..619a161be 100644
--- a/core/SConscript.unix
+++ b/core/SConscript.unix
@@ -190,6 +190,15 @@ CPPPATH_MOD += [
]
SOURCE_MOD += [
'embed/extmod/modtrezorui/modtrezorui.c',
+ 'embed/gdc/gdc_color.c',
+ 'embed/gdc/gdc_core.c',
+ 'embed/gdc/gdc_mono8_ops.c',
+ 'embed/gdc/gdc_rgb565.c',
+ 'embed/gdc/gdc_rgb565_ops.c',
+ 'embed/gdc/gdc_rgba8888.c',
+ 'embed/gdc/gdc_text.c',
+ 'embed/gdc/gdc_wnd565.c',
+ 'embed/gdc/gdc_wnd565_ops.c',
'embed/lib/buffers.c',
'embed/lib/colors.c',
'embed/lib/display_draw.c',
@@ -808,8 +817,10 @@ if ARGUMENTS.get('TREZOR_EMULATOR_DEBUGGABLE', '0') == '1':
RUST_PROFILE = 'dev'
RUST_LIBDIR = f'build/unix/rust/{TARGET}/debug'
else:
- RUST_PROFILE = 'release'
- RUST_LIBDIR = f'build/unix/rust/{TARGET}/release'
+ RUST_PROFILE = 'dev'
+ RUST_LIBDIR = f'build/unix/rust/{TARGET}/debug'
+# RUST_PROFILE = 'release'
+# RUST_LIBDIR = f'build/unix/rust/{TARGET}/release'
RUST_LIB = 'trezor_lib'
RUST_LIBPATH = f'{RUST_LIBDIR}/lib{RUST_LIB}.a'
@@ -827,6 +838,9 @@ def cargo_build():
if TREZOR_MODEL in ('T', 'T3T1'):
features.append('touch')
features.append('sd_card')
+ features.append('ui_antialiasing')
+ features.append('ui_blurring')
+ features.append('ui_jpeg_decoder')
if TREZOR_MODEL in ('R', '1'):
features.append('button')
diff --git a/core/embed/bootloader_ci/memory_stm32f4.ld b/core/embed/bootloader_ci/memory_stm32f4.ld
index d66d8bfc3..42eb1cd20 100644
--- a/core/embed/bootloader_ci/memory_stm32f4.ld
+++ b/core/embed/bootloader_ci/memory_stm32f4.ld
@@ -73,4 +73,10 @@ SECTIONS {
*(.boot_args*);
. = ALIGN(8);
} >BOOT_ARGS
+
+ .data_ccm : ALIGN(4) {
+ *(.no_dma_buffers*);
+ . = ALIGN(4);
+ } >CCMRAM
+
}
diff --git a/core/embed/gdc/gdc.h b/core/embed/gdc/gdc.h
new file mode 100644
index 000000000..20f67638c
--- /dev/null
+++ b/core/embed/gdc/gdc.h
@@ -0,0 +1,30 @@
+/*
+ * This file is part of the Trezor project, https://trezor.io/
+ *
+ * Copyright (c) SatoshiLabs
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#ifndef GDC_H
+#define GDC_H
+
+#include "gdc_bitmap.h"
+#include "gdc_color.h"
+#include "gdc_core.h"
+#include "gdc_geom.h"
+#include "gdc_ops.h"
+#include "gdc_text.h"
+
+#endif // GDC_H
diff --git a/core/embed/gdc/gdc_bitmap.h b/core/embed/gdc/gdc_bitmap.h
new file mode 100644
index 000000000..842f32011
--- /dev/null
+++ b/core/embed/gdc/gdc_bitmap.h
@@ -0,0 +1,106 @@
+/*
+ * This file is part of the Trezor project, https://trezor.io/
+ *
+ * Copyright (c) SatoshiLabs
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#ifndef GDC_BITMAP_H
+#define GDC_BITMAP_H
+
+#include "gdc_color.h"
+#include "gdc_geom.h"
+
+#include
+#include
+
+// forward declaration
+typedef struct gdc_vmt gdc_vmt_t;
+
+// ------------------------------------------------------------------------
+// GDC Bitmap pixel format
+//
+
+typedef enum {
+ GDC_FORMAT_UNKNOWN, //
+ GDC_FORMAT_MONO1, // 1-bpp per pixel
+ GDC_FORMAT_MONO4, // 4-bpp per pixel
+ GDC_FORMAT_RGB565, // 16-bpp per pixel
+ GDC_FORMAT_RGBA8888, // 32-bpp
+
+} gdc_format_t;
+
+// ------------------------------------------------------------------------
+// GDC Bitmap Attributes
+//
+
+#define GDC_BITMAP_READ_ONLY 0x01 // Read-only data
+// #define GDC_BITMAP_DMA_READ 0x02 // DMA read pending
+// #define GDC_BITMAP_DMA_WRITE 0x04 // DMA write pending
+
+// ------------------------------------------------------------------------
+// GDC Bitmap
+//
+// Structure holding pointer to the bitmap data, its format and sizes
+//
+// Note: gdc_bitmap_t itself can be used as GDC as long as it contains
+// valid gdc virtual table pointer.
+
+typedef struct gdc_bitmap {
+ // GDC virtual method table
+ // (must be the first field of the structure)
+ const gdc_vmt_t* vmt;
+ // pointer to top-left pixel
+ void* ptr;
+ // stride in bytes
+ size_t stride;
+ // size in pixels
+ gdc_size_t size;
+ // format of pixels, GDC_FORMAT_xxx
+ uint8_t format;
+ // attributes, GDC_BITMAP_xxx
+ uint8_t attrs;
+
+} gdc_bitmap_t;
+
+// Initializes RGB565 bitmap structure
+// GDC and format fields and filled automatically.
+gdc_bitmap_t gdc_bitmap_rgb565(void* data_ptr, size_t stride, gdc_size_t size,
+ uint8_t attrs);
+
+// Initializes RGBA8888 bitmap structure
+// GDC and format fields and filled automatically.
+gdc_bitmap_t gdc_bitmap_rgba8888(void* data_ptr, size_t stride, gdc_size_t size,
+ uint8_t attrs);
+
+// ------------------------------------------------------------------------
+// GDC Bitmap reference
+//
+// Structure is used when bitmap is beeing drawed to supply
+// additional parameters
+
+typedef struct {
+ // soruce bitmap
+ const gdc_bitmap_t* bitmap;
+ // offset used when bitmap is drawed on gdc
+ gdc_offset_t offset;
+ // foreground color (used with MONOx formats)
+ gdc_color_t fg_color;
+ // background color (used with MONOx formats)
+ gdc_color_t bg_color;
+
+} gdc_bitmap_ref_t;
+
+#endif // GDC_BITMAP_H
diff --git a/core/embed/gdc/gdc_clip.h b/core/embed/gdc/gdc_clip.h
new file mode 100644
index 000000000..daa250bed
--- /dev/null
+++ b/core/embed/gdc/gdc_clip.h
@@ -0,0 +1,92 @@
+/*
+ * This file is part of the Trezor project, https://trezor.io/
+ *
+ * Copyright (c) SatoshiLabs
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#ifndef GDC_CLIP_H
+#define GDC_CLIP_H
+
+#include
+#include "gdc.h"
+
+typedef struct {
+ int16_t dst_x;
+ int16_t dst_y;
+ int16_t src_x;
+ int16_t src_y;
+ int16_t width;
+ int16_t height;
+} gdc_clip_t;
+
+static inline gdc_clip_t gdc_clip(gdc_rect_t dst, gdc_size_t size,
+ const gdc_bitmap_ref_t* src) {
+ int16_t dst_x = dst.x0;
+ int16_t dst_y = dst.y0;
+
+ int16_t src_x = 0;
+ int16_t src_y = 0;
+
+ if (src != NULL) {
+ src_x += src->offset.x;
+ src_y += src->offset.y;
+
+ // Normalize negative x-offset of src bitmap
+ if (src_x < 0) {
+ dst_x -= src_x;
+ src_x = 0;
+ }
+
+ // Normalize negative y-offset of src bitmap
+ if (src_y < 0) {
+ dst_y -= src_y;
+ src_y = 0;
+ }
+ }
+
+ // Normalize negative top-left of destination rectangle
+ if (dst_x < 0) {
+ src_x -= dst_x;
+ dst_x = 0;
+ }
+
+ if (dst_y < 0) {
+ src_y -= dst_y;
+ dst_y = 0;
+ }
+
+ // Calculate dimension of effective rectangle
+ int16_t width = MIN(size.x, dst.x1) - dst_x;
+ int16_t height = MIN(size.y, dst.y1) - dst_y;
+
+ if (src != NULL) {
+ width = MIN(width, src->bitmap->size.x - src_x);
+ height = MIN(height, src->bitmap->size.y - src_y);
+ }
+
+ gdc_clip_t clip = {
+ .dst_x = dst_x,
+ .dst_y = dst_y,
+ .src_x = src_x,
+ .src_y = src_y,
+ .width = width,
+ .height = height,
+ };
+
+ return clip;
+}
+
+#endif // GDC_CLIP_H
diff --git a/core/embed/gdc/gdc_color.c b/core/embed/gdc/gdc_color.c
new file mode 100644
index 000000000..6ebbf94c3
--- /dev/null
+++ b/core/embed/gdc/gdc_color.c
@@ -0,0 +1,49 @@
+/*
+ * This file is part of the Trezor project, https://trezor.io/
+ *
+ * Copyright (c) SatoshiLabs
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#include "gdc_color.h"
+#include "colors.h"
+
+const gdc_color16_t* gdc_color16_gradient_a4(gdc_color_t fg_color,
+ gdc_color_t bg_color) {
+ static gdc_color16_t cache[16] = {0};
+
+ if (gdc_color_to_color16(bg_color) != cache[0] ||
+ gdc_color_to_color16(fg_color) != cache[15]) {
+ for (int alpha = 0; alpha < 16; alpha++) {
+ cache[alpha] = gdc_color16_blend_a4(fg_color, bg_color, alpha);
+ }
+ }
+
+ return (const gdc_color16_t*)&cache[0];
+}
+
+const gdc_color32_t* gdc_color32_gradient_a4(gdc_color_t fg_color,
+ gdc_color_t bg_color) {
+ static gdc_color32_t cache[16] = {0};
+
+ if (bg_color != gdc_color32_to_color(cache[0]) ||
+ fg_color != gdc_color32_to_color(cache[15])) {
+ for (int alpha = 0; alpha < 16; alpha++) {
+ cache[alpha] = gdc_color32_blend_a4(fg_color, bg_color, alpha);
+ }
+ }
+
+ return (const gdc_color32_t*)&cache[0];
+}
\ No newline at end of file
diff --git a/core/embed/gdc/gdc_color.h b/core/embed/gdc/gdc_color.h
new file mode 100644
index 000000000..fb670fde5
--- /dev/null
+++ b/core/embed/gdc/gdc_color.h
@@ -0,0 +1,281 @@
+/*
+ * This file is part of the Trezor project, https://trezor.io/
+ *
+ * Copyright (c) SatoshiLabs
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#ifndef GDC_COLOR_H
+#define GDC_COLOR_H
+
+#include
+
+#define GDC_COLOR_16BIT
+// #define GDC_COLOR_32BIT
+
+// Color in RGB565 format
+//
+// |15 8 | 7 0|
+// |---------------------------------|
+// |r r r r r g g g | g g g b b b b b|
+// |---------------------------------|
+
+typedef uint16_t gdc_color16_t;
+
+// Color in RGBA8888 format
+//
+// |31 24 |23 16 |15 8 | 7 0 |
+// |----------------------------------------------------------------------|
+// |a a a a a a a a | r r r r r r r r | g g g g g g g g | b b b b b b b b |
+// |----------------------------------------------------------------------|
+//
+
+typedef uint32_t gdc_color32_t;
+
+#ifdef GDC_COLOR_16BIT
+#define gdc_color_t gdc_color16_t
+#define gdc_color_to_color16(c) (c)
+#define gdc_color16_to_color(c) (c)
+#define gdc_color_to_color32(c) (gdc_color16_to_color32(c))
+#define gdc_color32_to_color(c) (gdc_color32_to_color16(c))
+#define gdc_color_lum(c) (gdc_color16_lum(c))
+#elif GDC_COLOR_32BIT
+#define gdc_color_t gdc_color32_t
+#define gdc_color_to_color16(c) (gdc_color32_to_color16(c))
+#define gdc_color16_to_color(c) (gdc_color16_to_color32(c))
+#define gdc_color_to_color32(c) (c)
+#define gdc_color32_to_color(c) (c)
+#else
+#error "GDC_COLOR_16BIT/32BIT not specified"
+#endif
+
+// Constructs a 16-bit color from the given red (r),
+// green (g), and blue (b) values in the range 0..255
+static inline gdc_color16_t gdc_color16_rgb(uint8_t r, uint8_t g, uint8_t b) {
+ return ((r & 0xF8U) << 8) | ((g & 0xFCU) << 3) | ((b & 0xF8U) >> 3);
+}
+
+// Constructs a 32-bit color from the given red (r),
+// green (g), and blue (b) values in the range 0..255.
+// Alpha is set to 255.
+static inline gdc_color32_t gdc_color32_rgb(uint8_t r, uint8_t g, uint8_t b) {
+ return (0xFFU << 24) | ((uint32_t)r << 16) | ((uint32_t)g << 8) | b;
+}
+
+// Converts a 16-bit color to a 32-bit color; alpha is set to 255
+static inline gdc_color32_t gdc_color16_to_color32(gdc_color16_t color) {
+ uint32_t r = (color & 0xF800) >> 8;
+ uint32_t g = (color & 0x07E0) >> 3;
+ uint32_t b = (color & 0x001F) << 3;
+
+ r |= (r >> 5);
+ g |= (g >> 6);
+ b |= (b >> 5);
+
+ return (0xFFU << 24) | (r << 16) | (g << 8) | b;
+}
+
+// Converts 32-bit color to 16-bit color, alpha is ignored
+static inline gdc_color16_t gdc_color32_to_color16(gdc_color32_t color) {
+ uint16_t r = (color & 0x00F80000) >> 8;
+ uint16_t g = (color & 0x0000FC00) >> 5;
+ uint16_t b = (color & 0x000000F8) >> 3;
+
+ return r | g | b;
+}
+
+// Converts 16-bit color into luminance (ranging from 0 to 255)
+static inline uint8_t gdc_color_lum(gdc_color16_t color) {
+ uint16_t r = (color & 0x00F80000) >> 8;
+ uint16_t g = (color & 0x0000FC00) >> 5;
+ uint16_t b = (color & 0x000000F8) >> 3;
+
+ return (r + g + b) / 3;
+}
+
+#ifdef GDC_COLOR_16BIT
+// Blends foreground and background colors with 4-bit alpha
+//
+// Returns a color in 16-bit format
+//
+// If alpha is 0, the function returns the background color
+// If alpha is 15, the function returns the foreground color
+static inline gdc_color16_t gdc_color16_blend_a4(gdc_color16_t fg,
+ gdc_color16_t bg,
+ uint8_t alpha) {
+ uint16_t fg_r = (fg & 0xF800) >> 11;
+ uint16_t bg_r = (bg & 0xF800) >> 11;
+
+ uint16_t r = (fg_r * alpha + (bg_r * (15 - alpha))) / 15;
+
+ uint16_t fg_g = (fg & 0x07E0) >> 5;
+ uint16_t bg_g = (bg & 0x07E0) >> 5;
+ uint16_t g = (fg_g * alpha + (bg_g * (15 - alpha))) / 15;
+
+ uint16_t fg_b = (fg & 0x001F) >> 0;
+ uint16_t bg_b = (bg & 0x001F) >> 0;
+ uint16_t b = (fg_b * alpha + (bg_b * (15 - alpha))) / 15;
+
+ return (r << 11) | (g << 5) | b;
+}
+
+// Blends foreground and background colors with 4-bit alpha
+//
+// Returns a color in 16-bit format
+//
+// If alpha is 0, the function returns the background color
+// If alpha is 15, the function returns the foreground color
+static inline gdc_color16_t gdc_color16_blend_a8(gdc_color16_t fg,
+ gdc_color16_t bg,
+ uint8_t alpha) {
+ uint16_t fg_r = (fg & 0xF800) >> 11;
+ uint16_t bg_r = (bg & 0xF800) >> 11;
+
+ uint16_t r = (fg_r * alpha + (bg_r * (255 - alpha))) / 255;
+
+ uint16_t fg_g = (fg & 0x07E0) >> 5;
+ uint16_t bg_g = (bg & 0x07E0) >> 5;
+ uint16_t g = (fg_g * alpha + (bg_g * (255 - alpha))) / 255;
+
+ uint16_t fg_b = (fg & 0x001F) >> 0;
+ uint16_t bg_b = (bg & 0x001F) >> 0;
+ uint16_t b = (fg_b * alpha + (bg_b * (255 - alpha))) / 255;
+
+ return (r << 11) | (g << 5) | b;
+}
+
+// Blends foreground and background colors with 4-bit alpha
+//
+// Returns a color in 32-bit format
+//
+// If alpha is 0, the function returns the background color
+// If alpha is 15, the function returns the foreground color
+static inline gdc_color32_t gdc_color32_blend_a4(gdc_color16_t fg,
+ gdc_color16_t bg,
+ uint8_t alpha) {
+ uint16_t fg_r = (fg & 0xF800) >> 8;
+ fg_r |= fg_r >> 5;
+ uint16_t bg_r = (bg & 0xF800) >> 8;
+ bg_r |= bg_r >> 5;
+
+ uint16_t r = (fg_r * alpha + (bg_r * (15 - alpha))) / 15;
+
+ uint16_t fg_g = (fg & 0x07E0) >> 2;
+ fg_g |= fg_g >> 6;
+ uint16_t bg_g = (bg & 0x07E0) >> 2;
+ bg_g |= bg_g >> 6;
+ uint16_t g = (fg_g * alpha + (bg_g * (15 - alpha))) / 15;
+
+ uint16_t fg_b = (fg & 0x001F) << 3;
+ fg_b |= fg_b >> 5;
+ uint16_t bg_b = (bg & 0x001F) << 3;
+ bg_b |= bg_b >> 5;
+ uint16_t b = (fg_b * alpha + (bg_b * (15 - alpha))) / 15;
+
+ return (0xFFU << 24) | ((uint32_t)r << 16) | ((uint32_t)g << 8) | b;
+}
+
+#elif GDC_COLOR_32BIT
+
+// Blends foreground and background colors with 4-bit alpha
+//
+// Returns a color in 16-bit format
+//
+// If alpha is 0, the function returns the background color
+// If alpha is 15, the function returns the foreground color
+static inline gdc_color16_t gdc_color16_blend_a4(gdc_color32_t fg,
+ gdc_color32_t bg,
+ uint8_t alpha) {
+ uint16_t fg_r = (fg & 0x00FF0000) >> 16;
+ uint16_t bg_r = (bg & 0x00FF0000) >> 16;
+ uint16_t r = (fg_r * alpha + (bg_r * (15 - alpha))) / 15;
+
+ uint16_t fg_g = (fg & 0x0000FF00) >> 8;
+ uint16_t bg_g = (bg & 0x0000FF00) >> 8;
+ uint16_t g = (fg_g * alpha + (bg_g * (15 - alpha))) / 15;
+
+ uint16_t fg_b = (fg & 0x000000FF) >> 0;
+ uint16_t bg_b = (bg & 0x000000FF) >> 0;
+ uint16_t b = (fg_b * alpha + (bg_b * (15 - alpha))) / 15;
+
+ return gdc_color16_rgb(r, g, b)
+}
+
+// Blends foreground and background colors with 8-bit alpha
+//
+// Returns a color in 16-bit format
+//
+// If alpha is 0, the function returns the background color
+// If alpha is 255, the function returns the foreground color
+static inline gdc_color16_t gdc_color16_blend_a8(gdc_color32_t fg,
+ gdc_color32_t bg,
+ uint8_t alpha) {
+ uint16_t fg_r = (fg & 0x00FF0000) >> 16;
+ uint16_t bg_r = (bg & 0x00FF0000) >> 16;
+ uint16_t r = (fg_r * alpha + (bg_r * (255 - alpha))) / 255;
+
+ uint16_t fg_g = (fg & 0x0000FF00) >> 8;
+ uint16_t bg_g = (bg & 0x0000FF00) >> 8;
+ uint16_t g = (fg_g * alpha + (bg_g * (255 - alpha))) / 255;
+
+ uint16_t fg_b = (fg & 0x000000FF) >> 0;
+ uint16_t bg_b = (bg & 0x000000FF) >> 0;
+ uint16_t b = (fg_b * alpha + (bg_b * (255 - alpha))) / 255;
+
+ return gdc_color16_rgb(r, g, b)
+}
+
+
+// Blends foreground and background colors with 4-bit alpha
+//
+// Returns a color in 32-bit format
+//
+// If alpha is 0, the function returns the background color
+// If alpha is 15, the function returns the foreground color
+static inline gdc_color32_t gdc_color32_blend_a4(gdc_color32_t fg,
+ gdc_color32_t bg,
+ uint8_t alpha) {
+ uint16_t fg_r = (fg & 0x00FF0000) >> 16;
+ uint16_t bg_r = (bg & 0x00FF0000) >> 16;
+ uint16_t r = (fg_r * alpha + (bg_r * (15 - alpha))) / 15;
+
+ uint16_t fg_g = (fg & 0x0000FF00) >> 8;
+ uint16_t bg_g = (bg & 0x0000FF00) >> 8;
+ uint16_t g = (fg_g * alpha + (bg_g * (15 - alpha))) / 15;
+
+ uint16_t fg_b = (fg & 0x000000FF) >> 0;
+ uint16_t bg_b = (bg & 0x000000FF) >> 0;
+ uint16_t b = (fg_b * alpha + (bg_b * (15 - alpha))) / 15;
+
+ return gdc_color32_rgb(r, g, b);
+}
+
+#else
+#error "GDC_COLOR_16BIT/32BIT not specified"
+#endif
+
+// Returns a gradient as an array of 16 consecutive 16-bit colors
+//
+// Each element in the array represents a color, with retval[0] being
+// the background (bg) color and retval[15] the foreground (fg) color
+const gdc_color16_t* gdc_color16_gradient_a4(gdc_color_t fg, gdc_color_t bg);
+
+// Returns a gradient as an array of 16 consecutive 32-bit colors
+//
+// Each element in the array represents a color, with retval[0] being
+// the background (bg) color and retval[15] the foreground (fg) color
+const gdc_color32_t* gdc_color32_gradient_a4(gdc_color_t fg, gdc_color_t bg);
+
+#endif // TREZORHAL_GDC_COLOR_H
diff --git a/core/embed/gdc/gdc_core.c b/core/embed/gdc/gdc_core.c
new file mode 100644
index 000000000..4eabecf9a
--- /dev/null
+++ b/core/embed/gdc/gdc_core.c
@@ -0,0 +1,185 @@
+/*
+ * This file is part of the Trezor project, https://trezor.io/
+ *
+ * Copyright (c) SatoshiLabs
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#include "gdc_core.h"
+#include "gdc_clip.h"
+
+#include "dma2d.h"
+
+void gdc_release(gdc_t* gdc) {
+ if (gdc != NULL) {
+ gdc_wait_for_pending_ops(gdc);
+
+ gdc_vmt_t* vmt = *(gdc_vmt_t**)gdc;
+ if (vmt != NULL && vmt->release != NULL) {
+ vmt->release(gdc);
+ }
+ }
+}
+
+gdc_size_t gdc_get_size(const gdc_t* gdc) {
+ if (gdc != NULL) {
+ gdc_vmt_t* vmt = *(gdc_vmt_t**)gdc;
+ if (vmt != NULL && vmt->get_bitmap != NULL) {
+ return vmt->get_bitmap((gdc_t*)gdc)->size;
+ }
+ }
+
+ return gdc_size(0, 0);
+}
+
+void gdc_wait_for_pending_ops(gdc_t* gdc) {
+#if defined(USE_DMA2D) && !defined(TREZOR_EMULATOR)
+ if (gdc != NULL) {
+ dma2d_wait();
+ }
+#endif
+}
+
+bool gdc_fill_rect(gdc_t* gdc, gdc_rect_t rect, gdc_color_t color) {
+ if (gdc != NULL) {
+ gdc_vmt_t* vmt = *(gdc_vmt_t**)gdc;
+
+ gdc_bitmap_t* bitmap = (gdc_bitmap_t*)gdc;
+
+ gdc_clip_t clip = gdc_clip(rect, bitmap->size, NULL);
+
+ if (clip.width <= 0 || clip.height <= 0) {
+ return true;
+ }
+
+ dma2d_params_t dp = {
+ // Destination bitmap
+ .height = clip.height,
+ .width = clip.width,
+ .dst_row = (uint8_t*)bitmap->ptr + bitmap->stride * clip.dst_y,
+ .dst_x = clip.dst_x,
+ .dst_y = clip.dst_y,
+ .dst_stride = bitmap->stride,
+
+ // Source bitmap
+ .src_fg = color,
+ .src_alpha = 255,
+ };
+
+ gdc_wait_for_pending_ops(gdc);
+
+ if (vmt->fill != NULL) {
+ return vmt->fill(gdc, &dp);
+ }
+ }
+
+ return false;
+}
+
+bool gdc_draw_bitmap(gdc_t* gdc, gdc_rect_t rect, const gdc_bitmap_ref_t* src) {
+ if (gdc != NULL) {
+ gdc_vmt_t* vmt = *(gdc_vmt_t**)gdc;
+
+ gdc_bitmap_t* bitmap = vmt->get_bitmap(gdc);
+
+ gdc_clip_t clip = gdc_clip(rect, bitmap->size, src);
+
+ if (clip.width <= 0 || clip.height <= 0) {
+ return true;
+ }
+
+ dma2d_params_t dp = {
+ // Destination bitmap
+ .height = clip.height,
+ .width = clip.width,
+ .dst_row = (uint8_t*)bitmap->ptr + bitmap->stride * clip.dst_y,
+ .dst_x = clip.dst_x,
+ .dst_y = clip.dst_y,
+ .dst_stride = bitmap->stride,
+
+ // Source bitmap
+ .src_row =
+ (uint8_t*)src->bitmap->ptr + src->bitmap->stride * clip.src_y,
+ .src_x = clip.src_x,
+ .src_y = clip.src_y,
+ .src_stride = src->bitmap->stride,
+ .src_fg = src->fg_color,
+ .src_bg = src->bg_color,
+ .src_alpha = 255,
+ };
+
+ gdc_wait_for_pending_ops(gdc);
+
+ if (src->bitmap->format == GDC_FORMAT_MONO4) {
+ if (vmt->copy_mono4 != NULL) {
+ return vmt->copy_mono4(gdc, &dp);
+ }
+ } else if (src->bitmap->format == GDC_FORMAT_RGB565) {
+ if (vmt->copy_rgb565 != NULL) {
+ return vmt->copy_rgb565(gdc, &dp);
+ }
+ } else if (src->bitmap->format == GDC_FORMAT_RGBA8888) {
+ if (vmt->copy_rgba8888 != NULL) {
+ return vmt->copy_rgba8888(gdc, &dp);
+ }
+ }
+ }
+
+ return false;
+}
+
+bool gdc_draw_blended(gdc_t* gdc, gdc_rect_t rect,
+ const gdc_bitmap_ref_t* src) {
+ if (gdc != NULL) {
+ gdc_vmt_t* vmt = *(gdc_vmt_t**)gdc;
+
+ gdc_bitmap_t* bitmap = vmt->get_bitmap(gdc);
+
+ gdc_clip_t clip = gdc_clip(rect, bitmap->size, src);
+
+ if (clip.width <= 0 || clip.height <= 0) {
+ return true;
+ }
+
+ dma2d_params_t dp = {
+ // Destination rectangle
+ .height = clip.height,
+ .width = clip.width,
+ .dst_row = (uint8_t*)bitmap->ptr + bitmap->stride * clip.dst_y,
+ .dst_x = clip.dst_x,
+ .dst_y = clip.dst_y,
+ .dst_stride = bitmap->stride,
+
+ // Source bitmap
+ .src_row =
+ (uint8_t*)src->bitmap->ptr + src->bitmap->stride * clip.src_y,
+ .src_x = clip.src_x,
+ .src_y = clip.src_y,
+ .src_stride = src->bitmap->stride,
+ .src_fg = src->fg_color,
+ .src_alpha = 255,
+ };
+
+ gdc_wait_for_pending_ops(gdc);
+
+ if (src->bitmap->format == GDC_FORMAT_MONO4) {
+ if (vmt->blend_mono4 != NULL) {
+ return vmt->blend_mono4(gdc, &dp);
+ }
+ }
+ }
+
+ return false;
+}
diff --git a/core/embed/gdc/gdc_core.h b/core/embed/gdc/gdc_core.h
new file mode 100644
index 000000000..7c8b97beb
--- /dev/null
+++ b/core/embed/gdc/gdc_core.h
@@ -0,0 +1,107 @@
+/*
+ * This file is part of the Trezor project, https://trezor.io/
+ *
+ * Copyright (c) SatoshiLabs
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#ifndef GDC_CORE_H
+#define GDC_CORE_H
+
+#include "gdc_bitmap.h"
+#include "gdc_color.h"
+#include "gdc_dma2d.h"
+#include "gdc_geom.h"
+
+#include
+#include
+#include
+
+// ------------------------------------------------------------------------
+// GDC - Graphics Device Context
+//
+
+typedef void gdc_t;
+
+// ------------------------------------------------------------------------
+// GDC (Graphic Device Context) Virtual Method Table
+//
+// GDC structure is implementation specific. Only requirement is that
+// it starts with a field of type gdc_vmt_t* vmt.
+//
+// typedef struct
+// {
+// gdc_vmt_t* vmt;
+//
+// // GDC specific data
+//
+// } gdc_impl_specific_t;
+//
+
+typedef void (*gdc_release_t)(gdc_t* gdc);
+typedef gdc_bitmap_t* (*gdc_get_bitmap_t)(gdc_t* gdc);
+typedef bool (*gdc_fill_t)(gdc_t* gdc, dma2d_params_t* params);
+typedef bool (*gdc_copy_mono4_t)(gdc_t* gdc, dma2d_params_t* params);
+typedef bool (*gdc_copy_rgb565_t)(gdc_t* gdc, dma2d_params_t* params);
+typedef bool (*gdc_copy_rgba8888_t)(gdc_t* gdc, dma2d_params_t* params);
+typedef bool (*gdc_blend_mono4_t)(gdc_t* gdc, dma2d_params_t* params);
+
+// GDC virtual methods
+struct gdc_vmt {
+ gdc_release_t release;
+ gdc_get_bitmap_t get_bitmap;
+ gdc_fill_t fill;
+ gdc_copy_mono4_t copy_mono4;
+ gdc_copy_rgb565_t copy_rgb565;
+ gdc_copy_rgba8888_t copy_rgba8888;
+ gdc_blend_mono4_t blend_mono4;
+};
+
+// ------------------------------------------------------------------------
+// GDC (Graphic Device Context) Public API
+
+// Releases reference to GDC
+void gdc_release(gdc_t* gdc);
+
+// Gets size of GDC bounding rectangle
+gdc_size_t gdc_get_size(const gdc_t* gdc);
+
+// Wait for pending DMA operation applied on this GDC
+// (used by high level code before accessing GDC's framebuffer/bitmap)
+void gdc_wait_for_pending_ops(gdc_t* gdc);
+
+// Fills a rectangle with a specified color
+bool gdc_fill_rect(gdc_t* gdc, gdc_rect_t rect, gdc_color_t color);
+
+// Draws a bitmap into the specified rectangle
+// The destination rectangle may not be fully filled if the source bitmap
+// is smaller then destination rectangle or if the bitmap is translated by
+// an offset partially or completely outside the destination rectangle.
+bool gdc_draw_bitmap(gdc_t* gdc, gdc_rect_t rect, const gdc_bitmap_ref_t* src);
+
+// Blends a bitmap with the gdc background in the specified rectangle.
+// The destination rectangle may not be fully filled if the source bitmap
+// is smaller then destination rectangle or if the bitmap is translated by
+// an offset partially or completely outside the destination rectangle.
+bool gdc_draw_blended(gdc_t* gdc, gdc_rect_t rect, const gdc_bitmap_ref_t* src);
+
+// ------------------------------------------------------------------------
+// this will be defined elsewhere::
+
+// Gets GDC for the hardware display
+// Returns NULL if display gdc was already acquired and not released
+gdc_t* display_acquire_gdc(void);
+
+#endif // GDC_CORE_H
diff --git a/core/embed/gdc/gdc_dma2d.h b/core/embed/gdc/gdc_dma2d.h
new file mode 100644
index 000000000..13d922c00
--- /dev/null
+++ b/core/embed/gdc/gdc_dma2d.h
@@ -0,0 +1,51 @@
+/*
+ * This file is part of the Trezor project, https://trezor.io/
+ *
+ * Copyright (c) SatoshiLabs
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#ifndef GDC_DMA2D_H
+#define GDC_DMA2D_H
+
+#include
+#include
+
+#include "gdc_color.h"
+
+typedef struct {
+ // Destination bitma[
+ // Following fields are used for all operations
+ uint16_t height;
+ uint16_t width;
+ void* dst_row;
+ uint16_t dst_x;
+ uint16_t dst_y;
+ uint16_t dst_stride;
+
+ // Source bitmap
+ // Used for copying and blending, but src_fg & src_alpha
+ // fields are also used for fill operation
+ void* src_row;
+ uint16_t src_x;
+ uint16_t src_y;
+ uint16_t src_stride;
+ gdc_color_t src_fg;
+ gdc_color_t src_bg;
+ uint8_t src_alpha;
+
+} dma2d_params_t;
+
+#endif // GDC_DMA2D_H
\ No newline at end of file
diff --git a/core/embed/gdc/gdc_geom.h b/core/embed/gdc/gdc_geom.h
new file mode 100644
index 000000000..2ffd11c0c
--- /dev/null
+++ b/core/embed/gdc/gdc_geom.h
@@ -0,0 +1,88 @@
+/*
+ * This file is part of the Trezor project, https://trezor.io/
+ *
+ * Copyright (c) SatoshiLabs
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#ifndef GDC_GEOM_H
+#define GDC_GEOM_H
+
+// ------------------------------------------------------------------------
+// GDC Rectangle
+//
+// used for simplified manipulation with rectangle coordinates
+
+typedef struct {
+ int16_t x0;
+ int16_t y0;
+ int16_t x1;
+ int16_t y1;
+} gdc_rect_t;
+
+// Creates a rectangle from top-left coordinates and dimensions
+static inline gdc_rect_t gdc_rect_wh(int16_t x, int16_t y, int16_t w,
+ int16_t h) {
+ gdc_rect_t rect = {
+ .x0 = x,
+ .y0 = y,
+ .x1 = x + w,
+ .y1 = y + h,
+ };
+
+ return rect;
+}
+
+// Creates a rectangle from top-left and bottom-right coordinates
+static inline gdc_rect_t gdc_rect(int16_t x0, int16_t y0, int16_t x1,
+ int16_t y1) {
+ gdc_rect_t rect = {
+ .x0 = x0,
+ .y0 = y0,
+ .x1 = x1,
+ .y1 = y1,
+ };
+
+ return rect;
+}
+
+// ------------------------------------------------------------------------
+// GDC Size
+//
+// used for simplified manipulation with size of objects
+
+typedef struct {
+ int16_t x;
+ int16_t y;
+} gdc_size_t;
+
+// Creates a rectangle from top-left and bottom-right coordinates
+static inline gdc_size_t gdc_size(int16_t x, int16_t y) {
+ gdc_size_t size = {
+ .x = x,
+ .y = y,
+ };
+
+ return size;
+}
+
+// ------------------------------------------------------------------------
+// GDC Offset
+//
+// used for simplified manipulation with size of objects
+
+typedef gdc_size_t gdc_offset_t;
+
+#endif // GDC_GEOM_H
diff --git a/core/embed/gdc/gdc_mono8_ops.c b/core/embed/gdc/gdc_mono8_ops.c
new file mode 100644
index 000000000..86f479aa6
--- /dev/null
+++ b/core/embed/gdc/gdc_mono8_ops.c
@@ -0,0 +1,121 @@
+/*
+ * This file is part of the Trezor project, https://trezor.io/
+ *
+ * Copyright (c) SatoshiLabs
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#include "gdc_color.h"
+#include "gdc_ops.h"
+
+bool mono8_fill(const dma2d_params_t* dp) {
+ uint8_t* dst_ptr = (uint8_t*)dp->dst_row + dp->dst_x;
+ uint16_t height = dp->height;
+
+ uint8_t fg = gdc_color_lum(dp->src_fg);
+
+ while (height-- > 0) {
+ for (int x = 0; x < dp->width; x++) {
+ dst_ptr[x] = fg;
+ }
+ dst_ptr += dp->dst_stride / sizeof(*dst_ptr);
+ }
+
+ return true;
+}
+
+bool mono8_copy_mono1p(const dma2d_params_t* dp) {
+ uint8_t* dst_ptr = (uint8_t*)dp->dst_row + dp->dst_x;
+ uint8_t* src = (uint8_t*)dp->src_row;
+ uint16_t src_ofs = dp->src_stride * dp->src_y + dp->src_x;
+ uint16_t height = dp->height;
+
+ uint8_t fg = gdc_color_lum(dp->src_fg);
+ uint8_t bg = gdc_color_lum(dp->src_bg);
+
+ while (height-- > 0) {
+ for (int x = 0; x < dp->width; x++) {
+ uint8_t mask = 1 << (7 - ((src_ofs + x) & 7));
+ uint8_t data = src[(src_ofs + x) / 8];
+ dst_ptr[x] = (data & mask) ? fg : bg;
+ }
+ dst_ptr += dp->dst_stride / sizeof(*dst_ptr);
+ src_ofs += dp->src_stride;
+ }
+
+ return true;
+}
+
+bool mono8_copy_mono4(const dma2d_params_t* dp) {
+ uint8_t* dst_ptr = (uint8_t*)dp->dst_row + dp->dst_x;
+ uint8_t* src_row = (uint8_t*)dp->src_row;
+ uint16_t height = dp->height;
+
+ uint8_t fg = gdc_color_lum(dp->src_fg);
+ uint8_t bg = gdc_color_lum(dp->src_bg);
+
+ while (height-- > 0) {
+ for (int x = 0; x < dp->width; x++) {
+ uint8_t src_data = src_row[(x + dp->src_x) / 2];
+ uint8_t src_lum = (x + dp->src_x) & 1 ? src_data >> 4 : src_data & 0xF;
+ dst_ptr[x] = (fg * src_lum + bg * (15 - src_lum)) / 15;
+ }
+ dst_ptr += dp->dst_stride / sizeof(*dst_ptr);
+ src_row += dp->src_stride / sizeof(*src_row);
+ }
+
+ return true;
+}
+
+bool mono8_blend_mono1p(const dma2d_params_t* dp) {
+ uint8_t* dst_ptr = (uint8_t*)dp->dst_row + dp->dst_x;
+ uint8_t* src = (uint8_t*)dp->src_row;
+ uint16_t src_ofs = dp->src_stride * dp->src_y + dp->src_x;
+ uint16_t height = dp->height;
+
+ uint8_t fg = gdc_color_lum(dp->src_fg);
+
+ while (height-- > 0) {
+ for (int x = 0; x < dp->width; x++) {
+ uint8_t mask = 1 << (7 - ((src_ofs + x) & 7));
+ uint8_t data = src[(src_ofs + x) / 8];
+ dst_ptr[x] = (data & mask) ? fg : dst_ptr[x];
+ }
+ dst_ptr += dp->dst_stride / sizeof(*dst_ptr);
+ src_ofs += dp->src_stride;
+ }
+
+ return true;
+}
+
+bool mono8_blend_mono4(const dma2d_params_t* dp) {
+ uint8_t* dst_ptr = (uint8_t*)dp->dst_row + dp->dst_x;
+ uint8_t* src_row = (uint8_t*)dp->src_row;
+ uint16_t height = dp->height;
+
+ uint8_t fg = gdc_color_lum(dp->src_fg);
+
+ while (height-- > 0) {
+ for (int x = 0; x < dp->width; x++) {
+ uint8_t src_data = src_row[(x + dp->src_x) / 2];
+ uint8_t src_alpha = (x + dp->src_x) & 1 ? src_data >> 4 : src_data & 0x0F;
+ dst_ptr[x] = (fg * src_alpha + dst_ptr[x] * (15 - src_alpha)) / 15;
+ }
+ dst_ptr += dp->dst_stride / sizeof(*dst_ptr);
+ src_row += dp->src_stride / sizeof(*src_row);
+ }
+
+ return true;
+}
diff --git a/core/embed/gdc/gdc_ops.h b/core/embed/gdc/gdc_ops.h
new file mode 100644
index 000000000..7efd32e20
--- /dev/null
+++ b/core/embed/gdc/gdc_ops.h
@@ -0,0 +1,40 @@
+/*
+ * This file is part of the Trezor project, https://trezor.io/
+ *
+ * Copyright (c) SatoshiLabs
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#include "gdc_dma2d.h"
+
+bool rgb565_fill(const dma2d_params_t* dp);
+bool rgb565_copy_mono4(const dma2d_params_t* dp);
+bool rgb565_copy_rgb565(const dma2d_params_t* dp);
+bool rgb565_blend_mono4(const dma2d_params_t* dp);
+
+bool rgba8888_fill(const dma2d_params_t* dp);
+bool rgba8888_copy_mono4(const dma2d_params_t* dp);
+bool rgba8888_copy_rgb565(const dma2d_params_t* dp);
+bool rgba8888_copy_rgba8888(const dma2d_params_t* dp);
+bool rgba8888_blend_mono4(const dma2d_params_t* dp);
+
+bool mono8_fill(const dma2d_params_t* dp);
+bool mono8_copy_mono1p(const dma2d_params_t* dp);
+bool mono8_copy_mono4(const dma2d_params_t* dp);
+bool mono8_blend_mono1p(const dma2d_params_t* dp);
+bool mono8_blend_mono4(const dma2d_params_t* dp);
+
+bool wnd565_fill(const dma2d_params_t* dp);
+bool wnd565_copy_rgb565(const dma2d_params_t* dp);
diff --git a/core/embed/gdc/gdc_rgb565.c b/core/embed/gdc/gdc_rgb565.c
new file mode 100644
index 000000000..72bce2002
--- /dev/null
+++ b/core/embed/gdc/gdc_rgb565.c
@@ -0,0 +1,74 @@
+/*
+ * This file is part of the Trezor project, https://trezor.io/
+ *
+ * Copyright (c) SatoshiLabs
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#include "gdc_core.h"
+#include "gdc_dma2d.h"
+#include "gdc_ops.h"
+
+#include
+
+static void gdc_rgb565_release(gdc_t* gdc) {
+ /* gdc_bitmap_t* bitmap = (gdc_bitmap_t*) gdc;
+
+ if (bitmap->release != NULL) {
+ bitmap->release(bitmap->context);
+ }*/
+}
+
+static gdc_bitmap_t* gdc_rgb565_get_bitmap(gdc_t* gdc) {
+ return (gdc_bitmap_t*)gdc;
+}
+
+static bool gdc_rgb565_fill(gdc_t* gdc, dma2d_params_t* params) {
+ return rgb565_fill(params);
+}
+
+static bool gdc_rgb565_copy_mono4(gdc_t* gdc, dma2d_params_t* params) {
+ return rgb565_copy_mono4(params);
+}
+
+static bool gdc_rgb565_copy_rgb565(gdc_t* gdc, dma2d_params_t* params) {
+ return rgb565_copy_rgb565(params);
+}
+
+static bool gdc_rgb565_blend_mono4(gdc_t* gdc, dma2d_params_t* params) {
+ return rgb565_blend_mono4(params);
+}
+
+gdc_bitmap_t gdc_bitmap_rgb565(void* data_ptr, size_t stride, gdc_size_t size,
+ uint8_t attrs) {
+ static const gdc_vmt_t gdc_rgb565 = {
+ .release = gdc_rgb565_release,
+ .get_bitmap = gdc_rgb565_get_bitmap,
+ .fill = gdc_rgb565_fill,
+ .copy_mono4 = gdc_rgb565_copy_mono4,
+ .copy_rgb565 = gdc_rgb565_copy_rgb565,
+ .copy_rgba8888 = NULL,
+ .blend_mono4 = gdc_rgb565_blend_mono4,
+ };
+
+ gdc_bitmap_t bitmap = {.vmt = &gdc_rgb565,
+ .ptr = data_ptr,
+ .stride = stride,
+ .size = size,
+ .format = GDC_FORMAT_RGB565,
+ .attrs = attrs};
+
+ return bitmap;
+}
diff --git a/core/embed/gdc/gdc_rgb565_ops.c b/core/embed/gdc/gdc_rgb565_ops.c
new file mode 100644
index 000000000..48eb3f1e3
--- /dev/null
+++ b/core/embed/gdc/gdc_rgb565_ops.c
@@ -0,0 +1,133 @@
+/*
+ * This file is part of the Trezor project, https://trezor.io/
+ *
+ * Copyright (c) SatoshiLabs
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#include "gdc_dma2d.h"
+#include "gdc_ops.h"
+
+#if USE_DMA2D
+#include "dma2d.h"
+#endif
+
+bool rgb565_fill(const dma2d_params_t* dp) {
+#if defined(USE_DMA2D) && !defined(TREZOR_EMULATOR)
+ if (dma2d_accessible(dp->dst_row)) {
+ return dma2d_rgb565_fill(dp);
+ } else
+#endif
+ {
+ uint16_t* dst_ptr = (uint16_t*)dp->dst_row + dp->dst_x;
+ uint16_t height = dp->height;
+
+ if (dp->src_alpha == 255) {
+ while (height-- > 0) {
+ for (int x = 0; x < dp->width; x++) {
+ dst_ptr[x] = dp->src_fg;
+ }
+ dst_ptr += dp->dst_stride / sizeof(*dst_ptr);
+ }
+ }
+ else {
+ uint8_t alpha = dp->src_alpha;
+ while (height-- > 0) {
+ for (int x = 0; x < dp->width; x++) {
+ dst_ptr[x] = gdc_color16_blend_a8(dp->src_fg, dst_ptr[x], alpha);
+ }
+ dst_ptr += dp->dst_stride / sizeof(*dst_ptr);
+ }
+ }
+ return true;
+ }
+}
+
+bool rgb565_copy_mono4(const dma2d_params_t* dp) {
+#if defined(USE_DMA2D) && !defined(TREZOR_EMULATOR)
+ if (dma2d_accessible(dp->dst_row) && dma2d_accessible(dp->src_row)) {
+ return dma2d_rgb565_copy_mono4(dp);
+ } else
+#endif
+ {
+ const gdc_color16_t* gradient =
+ gdc_color16_gradient_a4(dp->src_fg, dp->src_bg);
+
+ uint16_t* dst_ptr = (uint16_t*)dp->dst_row + dp->dst_x;
+ uint8_t* src_row = (uint8_t*)dp->src_row;
+ uint16_t height = dp->height;
+
+ while (height-- > 0) {
+ for (int x = 0; x < dp->width; x++) {
+ uint8_t fg_data = src_row[(x + dp->src_x) / 2];
+ uint8_t fg_lum = (x + dp->src_x) & 1 ? fg_data >> 4 : fg_data & 0xF;
+ dst_ptr[x] = gradient[fg_lum];
+ }
+ dst_ptr += dp->dst_stride / sizeof(*dst_ptr);
+ src_row += dp->src_stride / sizeof(*src_row);
+ }
+
+ return true;
+ }
+}
+
+bool rgb565_copy_rgb565(const dma2d_params_t* dp) {
+#if defined(USE_DMA2D) && !defined(TREZOR_EMULATOR)
+ if (dma2d_accessible(dp->dst_row) && dma2d_accessible(dp->src_row)) {
+ return dma2d_rgb565_copy_rgb565(dp);
+ } else
+#endif
+ {
+ uint16_t* dst_ptr = (uint16_t*)dp->dst_row + dp->dst_x;
+ uint16_t* src_ptr = (uint16_t*)dp->src_row + dp->src_x;
+ uint16_t height = dp->height;
+
+ while (height-- > 0) {
+ for (int x = 0; x < dp->width; x++) {
+ dst_ptr[x] = src_ptr[x];
+ }
+ dst_ptr += dp->dst_stride / sizeof(*dst_ptr);
+ src_ptr += dp->src_stride / sizeof(*src_ptr);
+ }
+
+ return true;
+ }
+}
+
+bool rgb565_blend_mono4(const dma2d_params_t* dp) {
+#if defined(USE_DMA2D) && !defined(TREZOR_EMULATOR)
+ if (dma2d_accessible(dp->dst_row) && dma2d_accessible(dp->src_row)) {
+ return dma2d_rgb565_blend_mono4(dp);
+ } else
+#endif
+ {
+ uint16_t* dst_ptr = (uint16_t*)dp->dst_row + dp->dst_x;
+ uint8_t* src_row = (uint8_t*)dp->src_row;
+ uint16_t height = dp->height;
+
+ while (height-- > 0) {
+ for (int x = 0; x < dp->width; x++) {
+ uint8_t fg_data = src_row[(x + dp->src_x) / 2];
+ uint8_t fg_alpha = (x + dp->src_x) & 1 ? fg_data >> 4 : fg_data & 0x0F;
+ dst_ptr[x] = gdc_color16_blend_a4(
+ dp->src_fg, gdc_color16_to_color(dst_ptr[x]), fg_alpha);
+ }
+ dst_ptr += dp->dst_stride / sizeof(*dst_ptr);
+ src_row += dp->src_stride / sizeof(*src_row);
+ }
+
+ return true;
+ }
+}
diff --git a/core/embed/gdc/gdc_rgba8888.c b/core/embed/gdc/gdc_rgba8888.c
new file mode 100644
index 000000000..f4481816d
--- /dev/null
+++ b/core/embed/gdc/gdc_rgba8888.c
@@ -0,0 +1,58 @@
+/*
+ * This file is part of the Trezor project, https://trezor.io/
+ *
+ * Copyright (c) SatoshiLabs
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#include "gdc_core.h"
+
+#include
+
+static void gdc_rgba8888_release(gdc_t* gdc) {
+ /* gdc_bitmap_t* bitmap = (gdc_bitmap_t*) gdc;
+
+ if (bitmap->release != NULL) {
+ bitmap->release(bitmap->context);
+ }*/
+}
+
+static gdc_bitmap_t* gdc_rgba8888_get_bitmap(gdc_t* gdc) {
+ return (gdc_bitmap_t*)gdc;
+}
+
+gdc_bitmap_t gdc_bitmap_rgba8888(void* data_ptr, size_t stride, gdc_size_t size,
+ uint8_t attrs) {
+ static const gdc_vmt_t gdc_rgba8888 = {
+ .release = gdc_rgba8888_release,
+ .get_bitmap = gdc_rgba8888_get_bitmap,
+ .fill = NULL, // dma2d_rgba8888_fill,
+ .copy_mono4 = NULL, // dma2d_rgba8888_copy_mono4,
+ .copy_rgb565 = NULL, // dma2d_rgba8888_copy_rgb565,
+ .copy_rgba8888 = NULL, // dma2d_rgba8888_copy_rgba8888,
+ .blend_mono4 = NULL, // dma2d_rgba8888_blend_mono4_mono4,
+ };
+
+ gdc_bitmap_t bitmap = {
+ .vmt = &gdc_rgba8888,
+ .ptr = data_ptr,
+ .stride = stride,
+ .size = size,
+ .format = GDC_FORMAT_RGBA8888,
+ .attrs = attrs,
+ };
+
+ return bitmap;
+}
diff --git a/core/embed/gdc/gdc_text.c b/core/embed/gdc/gdc_text.c
new file mode 100644
index 000000000..3d98e9501
--- /dev/null
+++ b/core/embed/gdc/gdc_text.c
@@ -0,0 +1,174 @@
+/*
+ * This file is part of the Trezor project, https://trezor.io/
+ *
+ * Copyright (c) SatoshiLabs
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#include "gdc_text.h"
+
+#include "fonts/fonts.h"
+
+#if TREZOR_FONT_BPP == 1
+#define GLYPH_FORMAT GDC_FORMAT_MONO1
+#define GLYPH_STRIDE(w) (((w) + 7) / 8)
+#elif TREZOR_FONT_BPP == 2
+#error Unsupported TREZOR_FONT_BPP value
+#define GLYPH_FORMAT GDC_FORMAT_MONO2
+#define GLYPH_STRIDE(w) (((w) + 3) / 4)
+#elif TREZOR_FONT_BPP == 4
+#define GLYPH_FORMAT GDC_FORMAT_MONO4
+#define GLYPH_STRIDE(w) (((w) + 1) / 2)
+#elif TREZOR_FONT_BPP == 8
+#error Unsupported TREZOR_FONT_BPP value
+#define GLYPH_FORMAT GDC_FORMAT_MONO8
+#define GLYPH_STRIDE(w) (w)
+#else
+#error Unsupported TREZOR_FONT_BPP value
+#endif
+
+#define GLYPH_WIDTH(g) ((g)[0])
+#define GLYPH_HEIGHT(g) ((g)[1])
+#define GLYPH_ADVANCE(g) ((g)[2])
+#define GLYPH_BEARING_X(g) ((g)[3])
+#define GLYPH_BEARING_Y(g) ((g)[4])
+#define GLYPH_DATA(g) ((void*)&(g)[5])
+
+bool gdc_draw_opaque_text(gdc_t* gdc, gdc_rect_t rect, const char* text,
+ size_t maxlen, const gdc_text_attr_t* attr) {
+ if (text == NULL) {
+ return false;
+ }
+
+ gdc_bitmap_t glyph_bitmap;
+ glyph_bitmap.vmt = NULL;
+ glyph_bitmap.format = GLYPH_FORMAT;
+
+ gdc_bitmap_ref_t glyph_ref;
+ glyph_ref.bitmap = &glyph_bitmap;
+ glyph_ref.fg_color = attr->fg_color;
+ glyph_ref.bg_color = attr->bg_color;
+
+ int max_height = font_max_height(attr->font);
+ int baseline = font_baseline(attr->font);
+
+ int offset_x = attr->offset.x;
+
+ if (offset_x < 0) {
+ rect.x0 -= attr->offset.x;
+ offset_x = 0;
+ }
+
+ for (int i = 0; i < maxlen; i++) {
+ uint8_t ch = (uint8_t)text[i];
+
+ if (ch == 0 || rect.x0 >= rect.x1) {
+ break;
+ }
+
+ const uint8_t* glyph = font_get_glyph(attr->font, ch);
+
+ if (glyph == NULL) {
+ continue;
+ }
+
+ if (offset_x >= GLYPH_ADVANCE(glyph)) {
+ offset_x -= GLYPH_ADVANCE(glyph);
+ continue;
+ }
+
+ glyph_bitmap.ptr = GLYPH_DATA(glyph);
+ glyph_bitmap.stride = GLYPH_STRIDE(GLYPH_WIDTH(glyph));
+ glyph_bitmap.size.x = GLYPH_WIDTH(glyph);
+ glyph_bitmap.size.y = GLYPH_HEIGHT(glyph);
+
+ glyph_ref.offset.x = attr->offset.x - GLYPH_BEARING_X(glyph);
+ glyph_ref.offset.y =
+ attr->offset.y - (max_height - baseline - GLYPH_BEARING_Y(glyph));
+
+ if (!gdc_draw_bitmap(gdc, rect, &glyph_ref)) {
+ return false;
+ }
+
+ rect.x0 += GLYPH_ADVANCE(glyph) - offset_x;
+ offset_x = 0;
+ }
+
+ return true;
+}
+
+bool gdc_draw_blended_text(gdc_t* gdc, gdc_rect_t rect, const char* text,
+ size_t maxlen, const gdc_text_attr_t* attr) {
+ if (text == NULL) {
+ return false;
+ }
+
+ gdc_bitmap_t glyph_bitmap;
+ glyph_bitmap.vmt = NULL;
+ glyph_bitmap.format = GLYPH_FORMAT;
+
+ gdc_bitmap_ref_t glyph_ref;
+ glyph_ref.bitmap = &glyph_bitmap;
+ glyph_ref.fg_color = attr->fg_color;
+ glyph_ref.bg_color = attr->bg_color;
+
+ int max_height = font_max_height(attr->font);
+ int baseline = font_baseline(attr->font);
+
+ int offset_x = attr->offset.x;
+
+ if (offset_x < 0) {
+ rect.x0 -= attr->offset.x;
+ offset_x = 0;
+ }
+
+ for (int i = 0; i < maxlen; i++) {
+ uint8_t ch = (uint8_t)text[i];
+
+ if (ch == 0 || rect.x0 >= rect.x1) {
+ break;
+ }
+
+ const uint8_t* glyph = font_get_glyph(attr->font, ch);
+
+ if (glyph == NULL) {
+ continue;
+ }
+
+ if (offset_x >= GLYPH_ADVANCE(glyph)) {
+ offset_x -= GLYPH_ADVANCE(glyph);
+ continue;
+ } else {
+ }
+
+ glyph_bitmap.ptr = GLYPH_DATA(glyph);
+ glyph_bitmap.stride = GLYPH_STRIDE(GLYPH_WIDTH(glyph));
+ glyph_bitmap.size.x = GLYPH_WIDTH(glyph);
+ glyph_bitmap.size.y = GLYPH_HEIGHT(glyph);
+
+ glyph_ref.offset.x = offset_x - GLYPH_BEARING_X(glyph);
+ glyph_ref.offset.y =
+ attr->offset.y - (max_height - baseline - GLYPH_BEARING_Y(glyph));
+
+ if (!gdc_draw_blended(gdc, rect, &glyph_ref)) {
+ return false;
+ }
+
+ rect.x0 += GLYPH_ADVANCE(glyph) - offset_x;
+ offset_x = 0;
+ }
+
+ return true;
+}
diff --git a/core/embed/gdc/gdc_text.h b/core/embed/gdc/gdc_text.h
new file mode 100644
index 000000000..e881e5e33
--- /dev/null
+++ b/core/embed/gdc/gdc_text.h
@@ -0,0 +1,48 @@
+/*
+ * This file is part of the Trezor project, https://trezor.io/
+ *
+ * Copyright (c) SatoshiLabs
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#ifndef GDC_TEXT_H
+#define GDC_TEXT_H
+
+#include "gdc_color.h"
+#include "gdc_core.h"
+#include "gdc_geom.h"
+
+#include
+#include
+
+typedef struct {
+ int font;
+
+ gdc_color_t fg_color;
+ gdc_color_t bg_color;
+
+ // TODO: horizontal/vertical alignment??
+ // TODO: extra offset???
+ gdc_offset_t offset;
+
+} gdc_text_attr_t;
+
+bool gdc_draw_opaque_text(gdc_t* gdc, gdc_rect_t rect, const char* text,
+ size_t maxlen, const gdc_text_attr_t* attr);
+
+bool gdc_draw_blended_text(gdc_t* gdc, gdc_rect_t rect, const char* text,
+ size_t maxlen, const gdc_text_attr_t* attr);
+
+#endif // GDC_TEXT_H
diff --git a/core/embed/gdc/gdc_wnd565.c b/core/embed/gdc/gdc_wnd565.c
new file mode 100644
index 000000000..6e008ea81
--- /dev/null
+++ b/core/embed/gdc/gdc_wnd565.c
@@ -0,0 +1,79 @@
+/*
+ * This file is part of the Trezor project, https://trezor.io/
+ *
+ * Copyright (c) SatoshiLabs
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#include "gdc_wnd565.h"
+#include "gdc_ops.h"
+
+#include
+
+#include "display.h"
+
+static void gdc_wnd565_release(gdc_t* gdc) {
+ // gdc_wnd565_t* wnd = (gdc_wnd565_t*)gdc;
+
+ // if (wnd->config.release != NULL) {
+ // wnd->config.release(wnd->config.context);
+ // }
+}
+
+static gdc_bitmap_t* gdc_wnd565_get_bitmap(gdc_t* gdc) {
+ return &((gdc_wnd565_t*)gdc)->bitmap;
+}
+
+static bool gdc_wnd565_fill(gdc_t* gdc, dma2d_params_t* params) {
+ return wnd565_fill(params);
+}
+
+static bool gdc_wnd565_copy_rgb565(gdc_t* gdc, dma2d_params_t* params) {
+ return wnd565_copy_rgb565(params);
+}
+
+gdc_t* gdc_wnd565_init(gdc_wnd565_t* gdc, gdc_wnd565_config_t* config) {
+ static const gdc_vmt_t gdc_wnd565 = {
+ .release = gdc_wnd565_release,
+ .get_bitmap = gdc_wnd565_get_bitmap,
+ .fill = gdc_wnd565_fill,
+ .copy_mono4 = NULL, // gdc_wnd565_copy_mono4,
+ .copy_rgb565 = gdc_wnd565_copy_rgb565,
+ .copy_rgba8888 = NULL,
+ .blend_mono4 = NULL,
+ };
+
+ memset(gdc, 0, sizeof(gdc_wnd565_t));
+ gdc->vmt = &gdc_wnd565;
+ gdc->bitmap.format = GDC_FORMAT_RGB565;
+ gdc->bitmap.size = config->size;
+ gdc->bitmap.ptr = (void*)config->reg_address;
+
+ return (gdc_t*)&gdc->vmt;
+}
+
+gdc_t* display_acquire_gdc(void) {
+ static gdc_wnd565_t wnd = {};
+
+ if (wnd.vmt == NULL) {
+ gdc_wnd565_config_t config = {
+ .size.x = 240,
+ .size.y = 240,
+ };
+ gdc_wnd565_init(&wnd, &config);
+ }
+
+ return (gdc_t*)&wnd.vmt;
+}
diff --git a/core/embed/gdc/gdc_wnd565.h b/core/embed/gdc/gdc_wnd565.h
new file mode 100644
index 000000000..e55bfb883
--- /dev/null
+++ b/core/embed/gdc/gdc_wnd565.h
@@ -0,0 +1,70 @@
+/*
+ * This file is part of the Trezor project, https://trezor.io/
+ *
+ * Copyright (c) SatoshiLabs
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#ifndef GDC_WND565_H
+#define GDC_WND565_H
+
+#include "gdc_core.h"
+
+// ------------------------------------------------------------------------
+// GDC for displays RGB565 with register/window API like ST7789
+//
+// This module serves as a driver for specific types of displays with
+// internal memory acting as a frame buffer and a specific interface
+// for writing to this framebuffer by putting pixels into the
+// specified rectangle window.
+
+typedef void (*gdc_release_cb_t)(void* context);
+
+// Driver configuration
+typedef struct {
+ // TODO
+ uintptr_t reg_address;
+
+ // GDC size in pixels
+ gdc_size_t size;
+
+ // Release callback invoked when gdc_release() is called
+ gdc_release_cb_t release;
+ // Context for release callback
+ void* context;
+
+} gdc_wnd565_config_t;
+
+// Driver-specific GDC structure
+typedef struct {
+ // GDC virtual method table
+ // (Must be the first field of the structure)
+ const gdc_vmt_t* vmt;
+
+ // Fake bitmap structure
+ gdc_bitmap_t bitmap;
+
+ // Current drawing window/rectangle
+ gdc_rect_t rect;
+ // Cursor position in the window
+ int cursor_x;
+ int cursor_y;
+
+} gdc_wnd565_t;
+
+// Initializes GDC context
+gdc_t* gdc_wnd565_init(gdc_wnd565_t* gdc, gdc_wnd565_config_t* config);
+
+#endif // GDC_WND565_H
diff --git a/core/embed/gdc/gdc_wnd565_ops.c b/core/embed/gdc/gdc_wnd565_ops.c
new file mode 100644
index 000000000..d894f7316
--- /dev/null
+++ b/core/embed/gdc/gdc_wnd565_ops.c
@@ -0,0 +1,57 @@
+/*
+ * This file is part of the Trezor project, https://trezor.io/
+ *
+ * Copyright (c) SatoshiLabs
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#include "gdc_ops.h"
+
+#include "display.h"
+
+static void set_window(const dma2d_params_t* dp) {
+ display_set_window(dp->dst_x, dp->dst_y, dp->dst_x + dp->width - 1,
+ dp->dst_y + dp->height + 1);
+}
+
+bool wnd565_fill(const dma2d_params_t* dp) {
+ set_window(dp);
+
+ uint16_t height = dp->height;
+
+ while (height-- > 0) {
+ for (int x = 0; x < dp->width; x++) {
+ PIXELDATA(dp->src_fg);
+ }
+ }
+
+ return true;
+}
+
+bool wnd565_copy_rgb565(const dma2d_params_t* dp) {
+ set_window(dp);
+
+ uint16_t* src_ptr = (uint16_t*)dp->src_row + dp->src_x;
+ uint16_t height = dp->height;
+
+ while (height-- > 0) {
+ for (int x = 0; x < dp->width; x++) {
+ PIXELDATA(src_ptr[x]);
+ }
+ src_ptr += dp->src_stride / sizeof(*src_ptr);
+ }
+
+ return true;
+}
diff --git a/core/embed/rust/Cargo.lock b/core/embed/rust/Cargo.lock
index 977bd97b6..882571d8f 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"
@@ -297,6 +303,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"
@@ -326,7 +341,10 @@ dependencies = [
"num-traits",
"qrcodegen",
"serde_json",
+ "static-alloc",
"trezor-tjpgdec",
+ "unsize",
+ "without-alloc",
"zeroize",
]
@@ -336,6 +354,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"
@@ -358,6 +385,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 17521bf78..5de592a8f 100644
--- a/core/embed/rust/Cargo.toml
+++ b/core/embed/rust/Cargo.toml
@@ -18,6 +18,9 @@ framebuffer = []
framebuffer32bit = []
ui_debug = []
ui_bounds = []
+ui_antialiasing = []
+ui_blurring = []
+ui_jpeg_decoder = []
bootloader = []
button = []
touch = []
@@ -101,6 +104,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..96391af6c 100644
--- a/core/embed/rust/build.rs
+++ b/core/embed/rust/build.rs
@@ -338,6 +338,24 @@ fn generate_trezorhal_bindings() {
.allowlist_var("DISPLAY_FRAMEBUFFER_OFFSET_Y")
.allowlist_var("DISPLAY_RESX")
.allowlist_var("DISPLAY_RESY")
+ // dma2d
+ .allowlist_type("dma2d_params_t")
+ .allowlist_function("rgb565_fill")
+ .allowlist_function("rgb565_copy_mono4")
+ .allowlist_function("rgb565_copy_rgb565")
+ .allowlist_function("rgb565_blend_mono4")
+ .allowlist_function("rgba8888_fill")
+ .allowlist_function("rgba8888_copy_mono4")
+ .allowlist_function("rgba8888_copy_rgb565")
+ .allowlist_function("rgba8888_copy_rgba8888")
+ .allowlist_function("rgba8888_blend_mono4")
+ .allowlist_function("mono8_fill")
+ .allowlist_function("mono8_copy_mono1p")
+ .allowlist_function("mono8_copy_mono4")
+ .allowlist_function("mono8_blend_mono1p")
+ .allowlist_function("mono8_blend_mono4")
+ .allowlist_function("wnd565_fill")
+ .allowlist_function("wnd565_copy_rgb565")
// fonts
.allowlist_function("font_height")
.allowlist_function("font_max_height")
diff --git a/core/embed/rust/src/trezorhal/bitmap.rs b/core/embed/rust/src/trezorhal/bitmap.rs
new file mode 100644
index 000000000..344a407ef
--- /dev/null
+++ b/core/embed/rust/src/trezorhal/bitmap.rs
@@ -0,0 +1,611 @@
+use super::ffi;
+
+use crate::ui::{
+ display::Color,
+ geometry::{Offset, Rect},
+};
+
+use core::{cell::Cell, marker::PhantomData};
+
+#[derive(Copy, Clone)]
+pub enum BitmapFormat {
+ MONO1,
+ MONO1P,
+ MONO4,
+ MONO8,
+ RGB565,
+ 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;
+ return 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
+ }
+
+ 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
+ }
+ }
+
+ /// 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) {
+ if let Some(dma2d) = Dma2d::new_fill(r, clip, color, alpha) {
+ let dma2d = dma2d.with_dst(self);
+ unsafe {
+ ffi::rgb565_fill(&dma2d);
+ }
+ self.dma_pending.set(true);
+ }
+ }
+
+ //
+ pub fn rgb565_copy(&mut self, r: Rect, clip: Rect, src: &BitmapView) {
+ if let Some(dma2d) = Dma2d::new_copy(r, clip, src) {
+ let dma2d = dma2d.with_dst(self);
+ unsafe {
+ match src.bitmap.format {
+ BitmapFormat::MONO4 => {
+ ffi::rgb565_copy_mono4(&dma2d);
+ }
+ BitmapFormat::RGB565 => {
+ ffi::rgb565_copy_rgb565(&dma2d);
+ }
+ _ => panic!(),
+ }
+ }
+ self.dma_pending.set(true);
+ src.bitmap.dma_pending.set(true);
+ }
+ }
+
+ pub fn rgb565_blend(&mut self, r: Rect, clip: Rect, src: &BitmapView) {
+ if let Some(dma2d) = Dma2d::new_copy(r, clip, src) {
+ let dma2d = dma2d.with_dst(self);
+ unsafe {
+ match src.bitmap.format {
+ BitmapFormat::MONO1P => {
+ // TODO
+ }
+ BitmapFormat::MONO4 => {
+ ffi::rgb565_blend_mono4(&dma2d);
+ }
+ _ => panic!(),
+ }
+ }
+ self.dma_pending.set(true);
+ src.bitmap.dma_pending.set(true);
+ }
+ }
+
+ /// 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) {
+ if let Some(dma2d) = Dma2d::new_fill(r, clip, color, alpha) {
+ let dma2d = dma2d.with_dst(self);
+ unsafe {
+ ffi::rgba8888_fill(&dma2d);
+ }
+ self.dma_pending.set(true);
+ }
+ }
+
+ pub fn rgba8888_copy(&mut self, r: Rect, clip: Rect, src: &BitmapView) {
+ if let Some(dma2d) = Dma2d::new_copy(r, clip, src) {
+ let dma2d = dma2d.with_dst(self);
+ unsafe {
+ match src.bitmap.format {
+ BitmapFormat::MONO4 => {
+ ffi::rgba8888_copy_mono4(&dma2d);
+ }
+ BitmapFormat::RGB565 => {
+ ffi::rgba8888_copy_rgb565(&dma2d);
+ }
+ BitmapFormat::RGBA8888 => {
+ ffi::rgba8888_copy_rgba8888(&dma2d);
+ }
+ _ => panic!(),
+ }
+ }
+ self.dma_pending.set(true);
+ src.bitmap.dma_pending.set(true);
+ }
+ }
+
+ pub fn rgba8888_blend(&mut self, r: Rect, clip: Rect, src: &BitmapView) {
+ if let Some(dma2d) = Dma2d::new_copy(r, clip, src) {
+ let dma2d = dma2d.with_dst(self);
+ unsafe {
+ match src.bitmap.format {
+ BitmapFormat::MONO4 => {
+ ffi::rgba8888_blend_mono4(&dma2d);
+ }
+ _ => panic!(),
+ }
+ }
+ self.dma_pending.set(true);
+ src.bitmap.dma_pending.set(true);
+ }
+ }
+
+ /// 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) {
+ if let Some(dma2d) = Dma2d::new_fill(r, clip, color, alpha) {
+ let dma2d = dma2d.with_dst(self);
+ unsafe {
+ ffi::mono8_fill(&dma2d);
+ }
+ self.dma_pending.set(true);
+ }
+ }
+
+ //
+ pub fn mono8_copy(&mut self, r: Rect, clip: Rect, src: &BitmapView) {
+ if let Some(dma2d) = Dma2d::new_copy(r, clip, src) {
+ let dma2d = dma2d.with_dst(self);
+ unsafe {
+ match src.bitmap.format {
+ BitmapFormat::MONO1P => {
+ ffi::mono8_copy_mono1p(&dma2d);
+ }
+ BitmapFormat::MONO4 => {
+ ffi::mono8_copy_mono4(&dma2d);
+ }
+ _ => panic!(),
+ }
+ }
+ self.dma_pending.set(true);
+ src.bitmap.dma_pending.set(true);
+ }
+ }
+
+ pub fn mono8_blend(&mut self, r: Rect, clip: Rect, src: &BitmapView) {
+ if let Some(dma2d) = Dma2d::new_copy(r, clip, src) {
+ let dma2d = dma2d.with_dst(self);
+ unsafe {
+ match src.bitmap.format {
+ BitmapFormat::MONO1P => {
+ ffi::mono8_blend_mono1p(&dma2d);
+ }
+ BitmapFormat::MONO4 => {
+ ffi::mono8_blend_mono4(&dma2d);
+ }
+ _ => panic!(),
+ }
+ }
+ self.dma_pending.set(true);
+ src.bitmap.dma_pending.set(true);
+ }
+ }
+
+ /// Waits until DMA operation is finished
+ fn wait_for_dma(&self) {
+ if self.dma_pending.get() {
+ #[cfg(feature = "dma2d")]
+ unsafe {
+ ffi::dma2d_wait_for_transfer();
+ }
+ self.dma_pending.set(false);
+ }
+ }
+}
+
+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.into()).into(),
+ ..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: fg_color.into(),
+ ..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: bg_color.into(),
+ ..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)
+ }
+}
+
+pub type Dma2d = ffi::dma2d_params_t;
+
+impl Dma2d {
+ pub fn new_fill(r: Rect, clip: Rect, color: Color, alpha: u8) -> Option {
+ let r = r.intersect(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.intersect(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(
+ Dma2d::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.ptr.add(dst.stride * self.dst_y as usize) as *mut cty::c_void },
+ dst_stride: dst.stride as u16,
+ ..self
+ }
+ }
+
+ 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,
+ }
+ }
+
+ 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.size.x as u16, // packed bits
+ _ => bitmap.stride as u16,
+ };
+
+ Self {
+ src_row: unsafe { bitmap.ptr.add(bitmap.stride * y as usize) as *mut cty::c_void },
+ 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
+ }
+ }
+}
+
+impl Dma2d {
+ pub fn wnd565_fill(r: Rect, clip: Rect, color: Color) {
+ if let Some(dma2d) = Dma2d::new_fill(r, clip, color, 255) {
+ unsafe { ffi::wnd565_fill(&dma2d) };
+ }
+ }
+
+ pub fn wnd565_copy(r: Rect, clip: Rect, src: &BitmapView) {
+ if let Some(dma2d) = Dma2d::new_copy(r, clip, src) {
+ unsafe {
+ match src.bitmap.format {
+ BitmapFormat::RGB565 => {
+ ffi::wnd565_copy_rgb565(&dma2d);
+ }
+ _ => panic!(),
+ }
+ }
+ src.bitmap.dma_pending.set(true);
+ }
+ }
+}
diff --git a/core/embed/rust/src/trezorhal/mod.rs b/core/embed/rust/src/trezorhal/mod.rs
index 39de95151..340f38a9d 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 bitmap;
#[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/canvas/algo/blur.rs b/core/embed/rust/src/ui/canvas/algo/blur.rs
new file mode 100644
index 000000000..25270bf94
--- /dev/null
+++ b/core/embed/rust/src/ui/canvas/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/canvas/algo/circle.rs b/core/embed/rust/src/ui/canvas/algo/circle.rs
new file mode 100644
index 000000000..2e56a295e
--- /dev/null
+++ b/core/embed/rust/src/ui/canvas/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/canvas/algo/line.rs b/core/embed/rust/src/ui/canvas/algo/line.rs
new file mode 100644
index 000000000..eeae577cb
--- /dev/null
+++ b/core/embed/rust/src/ui/canvas/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/canvas/algo/mod.rs b/core/embed/rust/src/ui/canvas/algo/mod.rs
new file mode 100644
index 000000000..d9e23288c
--- /dev/null
+++ b/core/embed/rust/src/ui/canvas/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/canvas/algo/trigo.rs b/core/embed/rust/src/ui/canvas/algo/trigo.rs
new file mode 100644
index 000000000..fb43dfa79
--- /dev/null
+++ b/core/embed/rust/src/ui/canvas/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!(angle >= 0 && angle <= PI4);
+ 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/canvas/common.rs b/core/embed/rust/src/ui/canvas/common.rs
new file mode 100644
index 000000000..ace34425f
--- /dev/null
+++ b/core/embed/rust/src/ui/canvas/common.rs
@@ -0,0 +1,874 @@
+use crate::ui::{
+ display::Color,
+ geometry::{Offset, Point, Rect},
+};
+
+use super::{BitmapView, Viewport};
+
+#[cfg(feature = "ui_blurring")]
+use crate::ui::shape::DrawingCache;
+
+use super::algo::{circle_points, line_points, sin_i16, PI4};
+
+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;
+
+ loop {
+ if let Some(p) = iter.next() {
+ if p.u > u2 {
+ break;
+ }
+ p2_start = p;
+ } else {
+ break;
+ }
+ }
+
+ // 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 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/canvas/mod.rs b/core/embed/rust/src/ui/canvas/mod.rs
new file mode 100644
index 000000000..7207a106a
--- /dev/null
+++ b/core/embed/rust/src/ui/canvas/mod.rs
@@ -0,0 +1,15 @@
+pub mod algo;
+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;
+
+use crate::trezorhal::bitmap;
+pub use bitmap::{Bitmap, BitmapFormat, BitmapView};
diff --git a/core/embed/rust/src/ui/canvas/mono8.rs b/core/embed/rust/src/ui/canvas/mono8.rs
new file mode 100644
index 000000000..5408c0243
--- /dev/null
+++ b/core/embed/rust/src/ui/canvas/mono8.rs
@@ -0,0 +1,97 @@
+use crate::ui::{
+ display::Color,
+ geometry::{Offset, Point, Rect},
+};
+
+#[cfg(feature = "ui_blurring")]
+use crate::ui::shape::DrawingCache;
+
+use super::{BasicCanvas, Bitmap, BitmapFormat, BitmapView, Canvas, Viewport};
+
+/// 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, min_height: Option, buff: &'a mut [u8]) -> Option {
+ let bitmap = Bitmap::new_mut(BitmapFormat::MONO8, None, 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/canvas/rgb565.rs b/core/embed/rust/src/ui/canvas/rgb565.rs
new file mode 100644
index 000000000..46aff8e57
--- /dev/null
+++ b/core/embed/rust/src/ui/canvas/rgb565.rs
@@ -0,0 +1,122 @@
+use crate::ui::{
+ display::Color,
+ geometry::{Offset, Point, Rect},
+};
+
+use super::{BasicCanvas, Bitmap, BitmapFormat, BitmapView, Canvas, Viewport};
+
+#[cfg(feature = "ui_blurring")]
+use crate::ui::shape::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, min_height: Option, buff: &'a mut [u8]) -> Option {
+ let bitmap = Bitmap::new_mut(BitmapFormat::RGB565, None, 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)
+ .intersect(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/canvas/rgba8888.rs b/core/embed/rust/src/ui/canvas/rgba8888.rs
new file mode 100644
index 000000000..3f6b07518
--- /dev/null
+++ b/core/embed/rust/src/ui/canvas/rgba8888.rs
@@ -0,0 +1,90 @@
+use crate::ui::{
+ display::Color,
+ geometry::{Offset, Point, Rect},
+};
+
+use super::{BasicCanvas, Bitmap, BitmapFormat, BitmapView, Canvas, Viewport};
+
+#[cfg(feature = "ui_blurring")]
+use crate::ui::shape::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, min_height: Option, buff: &'a mut [u8]) -> Option {
+ let bitmap = Bitmap::new_mut(BitmapFormat::RGBA8888, None, 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.into();
+ }
+ }
+ }
+
+ fn blend_pixel(&mut self, _pt: Point, _color: Color, _alpha: u8) {
+ // TODO: not implemented yet, requires 32-bit color blending routines
+ }
+
+ 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/canvas/viewport.rs b/core/embed/rust/src/ui/canvas/viewport.rs
new file mode 100644
index 000000000..99db7ca17
--- /dev/null
+++ b/core/embed/rust/src/ui/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).has_intersection(self.clip)
+ }
+
+ 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.intersect(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).intersect(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).intersect(self.clip);
+ let origin = self.origin + (clip.top_left() - self.clip.top_left());
+ Self { clip, origin }
+ }
+}
diff --git a/core/embed/rust/src/ui/display/font.rs b/core/embed/rust/src/ui/display/font.rs
index 942d9af90..29f2baebf 100644
--- a/core/embed/rust/src/ui/display/font.rs
+++ b/core/embed/rust/src/ui/display/font.rs
@@ -1,6 +1,7 @@
use crate::{
trezorhal::display,
ui::{
+ canvas::{Bitmap, BitmapFormat},
constant,
geometry::{Offset, Point, Rect},
},
@@ -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
@@ -226,6 +249,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 07b1b965e..0a36fd280 100644
--- a/core/embed/rust/src/ui/mod.rs
+++ b/core/embed/rust/src/ui/mod.rs
@@ -2,6 +2,7 @@
pub mod macros;
pub mod animation;
+pub mod canvas;
pub mod component;
pub mod constant;
pub mod display;
@@ -9,6 +10,7 @@ pub mod event;
pub mod geometry;
pub mod lerp;
pub mod screens;
+pub mod shape;
#[macro_use]
pub mod util;
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..0c1df5dcd
--- /dev/null
+++ b/core/embed/rust/src/ui/shape/bar.rs
@@ -0,0 +1,128 @@
+use crate::ui::{canvas::Canvas, display::Color, geometry::Rect};
+
+use super::{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..301fd1674
--- /dev/null
+++ b/core/embed/rust/src/ui/shape/base.rs
@@ -0,0 +1,53 @@
+use crate::ui::{canvas::Canvas, geometry::Rect};
+
+use super::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/blur.rs b/core/embed/rust/src/ui/shape/blur.rs
new file mode 100644
index 000000000..5d85b4327
--- /dev/null
+++ b/core/embed/rust/src/ui/shape/blur.rs
@@ -0,0 +1,45 @@
+use crate::ui::{canvas::Canvas, geometry::Rect};
+
+use super::{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..a4255d65e
--- /dev/null
+++ b/core/embed/rust/src/ui/shape/cache/blur_cache.rs
@@ -0,0 +1,57 @@
+use crate::ui::{
+ canvas::algo::{BlurAlgorithm, BlurBuff},
+ 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))
+ }
+}
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..147bfe80d
--- /dev/null
+++ b/core/embed/rust/src/ui/shape/cache/drawing_cache.rs
@@ -0,0 +1,99 @@
+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;
+
+const ZLIB_CACHE_SLOTS: usize = 4;
+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>,
+
+ 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")),
+ 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
+ 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()
+ }
+}
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..fc8d11677
--- /dev/null
+++ b/core/embed/rust/src/ui/shape/cache/jpeg_cache.rs
@@ -0,0 +1,364 @@
+use crate::ui::{
+ canvas::{BasicCanvas, Bitmap, BitmapFormat, BitmapView, Canvas, Rgb565Canvas},
+ display::tjpgd,
+ geometry::{Offset, Point, Rect},
+};
+
+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,
+ 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)
+ }
+ }
+}
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..4e637761d
--- /dev/null
+++ b/core/embed/rust/src/ui/shape/cache/zlib_cache.rs
@@ -0,0 +1,166 @@
+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(())
+ }
+}
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..669332f22
--- /dev/null
+++ b/core/embed/rust/src/ui/shape/circle.rs
@@ -0,0 +1,140 @@
+use crate::ui::{
+ canvas::Canvas,
+ display::Color,
+ geometry::{Point, Rect},
+};
+
+use super::{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/jpeg.rs b/core/embed/rust/src/ui/shape/jpeg.rs
new file mode 100644
index 000000000..72a08e3b3
--- /dev/null
+++ b/core/embed/rust/src/ui/shape/jpeg.rs
@@ -0,0 +1,199 @@
+use crate::ui::{
+ canvas::{Bitmap, BitmapFormat, BitmapView, Canvas},
+ geometry::{Alignment2D, Offset, Point, Rect},
+};
+
+use super::{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,
+ &mut 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/memory.md b/core/embed/rust/src/ui/shape/memory.md
new file mode 100644
index 000000000..ec7093dac
--- /dev/null
+++ b/core/embed/rust/src/ui/shape/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/mod.rs b/core/embed/rust/src/ui/shape/mod.rs
new file mode 100644
index 000000000..63f5fe786
--- /dev/null
+++ b/core/embed/rust/src/ui/shape/mod.rs
@@ -0,0 +1,27 @@
+mod bar;
+mod base;
+#[cfg(feature = "ui_blurring")]
+mod blur;
+mod cache;
+mod circle;
+#[cfg(feature = "ui_jpeg_decoder")]
+mod jpeg;
+mod model;
+mod qrcode;
+mod render;
+mod text;
+mod toif;
+
+pub use bar::Bar;
+pub use base::{Shape, ShapeClone};
+#[cfg(feature = "ui_blurring")]
+pub use blur::Blurring;
+pub use cache::drawing_cache::DrawingCache;
+pub use circle::Circle;
+#[cfg(feature = "ui_jpeg_decoder")]
+pub use jpeg::JpegImage;
+pub use model::render_on_display;
+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/model/mod.rs b/core/embed/rust/src/ui/shape/model/mod.rs
new file mode 100644
index 000000000..7a041b13d
--- /dev/null
+++ b/core/embed/rust/src/ui/shape/model/mod.rs
@@ -0,0 +1,9 @@
+#[cfg(feature = "model_tr")]
+pub mod model_tr;
+#[cfg(feature = "model_tr")]
+pub use model_tr::render_on_display;
+
+#[cfg(feature = "model_tt")]
+pub mod model_tt;
+#[cfg(feature = "model_tt")]
+pub use model_tt::render_on_display;
diff --git a/core/embed/rust/src/ui/shape/model/model_tr.rs b/core/embed/rust/src/ui/shape/model/model_tr.rs
new file mode 100644
index 000000000..8f1e91d6c
--- /dev/null
+++ b/core/embed/rust/src/ui/shape/model/model_tr.rs
@@ -0,0 +1,58 @@
+use crate::ui::{
+ canvas::{BasicCanvas, Canvas, Mono8Canvas, Viewport},
+ display,
+ display::Color,
+ geometry::{Offset, Rect},
+ shape::{DirectRenderer, DrawingCache},
+};
+
+use static_alloc::Bump;
+
+pub fn render_on_display<'a, F>(clip: Option, bg_color: Option, func: F)
+where
+ F: FnOnce(&mut DirectRenderer<'_, 'a, Mono8Canvas<'a>>),
+{
+ // TODO: do not use constants 128 & 64 directly
+
+ static mut FRAME_BUFFER: [u8; 128 * 64] = [0u8; 128 * 64];
+
+ let fb = &mut unsafe { &mut *core::ptr::addr_of_mut!(FRAME_BUFFER) }[..];
+
+ static mut BUMP: Bump<[u8; 40 * 1024]> = Bump::uninit();
+
+ let bump = unsafe { &mut *core::ptr::addr_of_mut!(BUMP) };
+ {
+ bump.reset();
+
+ let cache = DrawingCache::new(bump, bump);
+ let mut canvas = unwrap!(Mono8Canvas::new(Offset::new(128, 64), 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);
+
+ refresh_display(&canvas);
+ }
+}
+
+fn refresh_display(canvas: &Mono8Canvas) {
+ // TODO: optimize
+
+ display::set_window(canvas.bounds());
+
+ let view = canvas.view();
+
+ for y in 0..canvas.size().y {
+ let row = unwrap!(view.row(y));
+ for value in row.iter() {
+ let c = Color::rgb(*value, *value, *value);
+ display::pixeldata(c.into());
+ }
+ }
+
+ display::refresh();
+}
diff --git a/core/embed/rust/src/ui/shape/model/model_tt.rs b/core/embed/rust/src/ui/shape/model/model_tt.rs
new file mode 100644
index 000000000..c080878cc
--- /dev/null
+++ b/core/embed/rust/src/ui/shape/model/model_tt.rs
@@ -0,0 +1,78 @@
+use crate::ui::{
+ canvas::{BasicCanvas, Viewport},
+ display::Color,
+ geometry::{Offset, Rect},
+ shape::{DrawingCache, ProgressiveRenderer},
+};
+
+use crate::trezorhal::bitmap::{BitmapView, Dma2d};
+
+use static_alloc::Bump;
+
+pub fn render_on_display<'a, F>(clip: Option, bg_color: Option, func: F)
+where
+ F: FnOnce(&mut ProgressiveRenderer<'_, 'a, Bump<[u8; 40 * 1024]>, DisplayModelT>),
+{
+ #[link_section = ".no_dma_buffers"]
+ static mut BUMP_A: Bump<[u8; 40 * 1024]> = Bump::uninit();
+
+ #[link_section = ".buf"]
+ static mut BUMP_B: Bump<[u8; 16 * 1024]> = 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 = DisplayModelT::acquire().unwrap();
+
+ if let Some(clip) = clip {
+ canvas.set_viewport(Viewport::new(clip));
+ }
+
+ let mut target = ProgressiveRenderer::new(&mut canvas, bg_color, &cache, bump_a, 45);
+
+ func(&mut target);
+
+ target.render(16);
+ }
+}
+
+pub struct DisplayModelT {
+ size: Offset,
+ viewport: Viewport,
+}
+
+impl DisplayModelT {
+ pub fn acquire() -> Option {
+ let size = Offset::new(240, 240); // TODO
+ let viewport = Viewport::from_size(size);
+ Some(Self { size, viewport })
+ }
+}
+
+impl BasicCanvas for DisplayModelT {
+ 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);
+ Dma2d::wnd565_fill(r, self.viewport.clip, color);
+ }
+
+ fn draw_bitmap(&mut self, r: Rect, bitmap: BitmapView) {
+ let r = r.translate(self.viewport.origin);
+ Dma2d::wnd565_copy(r, self.viewport.clip, &bitmap);
+ }
+}
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..52460ab9f
--- /dev/null
+++ b/core/embed/rust/src/ui/shape/qrcode.rs
@@ -0,0 +1,168 @@
+use crate::ui::{
+ canvas::{algo::line_points, Bitmap, BitmapFormat, Canvas},
+ display::Color,
+ geometry::{Offset, Rect},
+};
+
+use qrcodegen::QrCode;
+
+use super::{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..e7e839275
--- /dev/null
+++ b/core/embed/rust/src/ui/shape/render.rs
@@ -0,0 +1,249 @@
+use crate::ui::{
+ canvas::{BasicCanvas, Canvas, Rgb565Canvas, Viewport},
+ display::Color,
+ geometry::{Offset, Point, Rect},
+ shape::{DrawingCache, Shape, ShapeClone},
+};
+
+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),
+ 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..4f0a0618b
--- /dev/null
+++ b/core/embed/rust/src/ui/shape/text.rs
@@ -0,0 +1,135 @@
+use crate::ui::{
+ canvas::{BitmapView, Canvas},
+ display::{Color, Font},
+ geometry::{Alignment, Offset, Point, Rect},
+};
+
+use super::{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..15bafdcec
--- /dev/null
+++ b/core/embed/rust/src/ui/shape/toif.rs
@@ -0,0 +1,175 @@
+use crate::ui::{
+ canvas::{Bitmap, BitmapFormat, Canvas},
+ display::{toif::Toif, Color},
+ geometry::{Alignment2D, Offset, Point, Rect},
+};
+
+use super::{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)
+ .intersect(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)
+ .intersect(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/trezorhal.h b/core/embed/rust/trezorhal.h
index 8425398b2..5e9ee471b 100644
--- a/core/embed/rust/trezorhal.h
+++ b/core/embed/rust/trezorhal.h
@@ -1,4 +1,6 @@
#include TREZOR_BOARD
+#include "../gdc/gdc.h"
+
#include "buffers.h"
#include "button.h"
#include "common.h"
diff --git a/core/embed/trezorhal/dma2d.h b/core/embed/trezorhal/dma2d.h
index 3f8d8f79b..20019cf4e 100644
--- a/core/embed/trezorhal/dma2d.h
+++ b/core/embed/trezorhal/dma2d.h
@@ -20,6 +20,8 @@
#ifndef TREZORHAL_DMA2D_H
#define TREZORHAL_DMA2D_H
+#include "../gdc/gdc_core.h"
+
#include "common.h"
void dma2d_init(void);
@@ -40,4 +42,12 @@ void dma2d_start_blend(uint8_t* overlay_addr, uint8_t* bg_addr,
void dma2d_wait_for_transfer(void);
+void dma2d_wait(void);
+bool dma2d_rgb565_fill(const dma2d_params_t* dp);
+bool dma2d_rgb565_copy_mono4(const dma2d_params_t* dp);
+bool dma2d_rgb565_copy_rgb565(const dma2d_params_t* dp);
+bool dma2d_rgb565_blend_mono4(const dma2d_params_t* dp);
+
+bool dma2d_accessible(const void* ptr);
+
#endif // TREZORHAL_DMA2D_H
diff --git a/core/embed/trezorhal/stm32f4/dma2d_gdc.c b/core/embed/trezorhal/stm32f4/dma2d_gdc.c
new file mode 100644
index 000000000..c3a643db9
--- /dev/null
+++ b/core/embed/trezorhal/stm32f4/dma2d_gdc.c
@@ -0,0 +1,324 @@
+/*
+ * This file is part of the Trezor project, https://trezor.io/
+ *
+ * Copyright (c) SatoshiLabs
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#include STM32_HAL_H
+
+#include
+#include "dma2d.h"
+
+#include "../gdc/gdc_color.h"
+
+static DMA2D_HandleTypeDef dma2d_handle = {
+ .Instance = (DMA2D_TypeDef*)DMA2D_BASE,
+};
+
+bool dma2d_accessible(const void* ptr) {
+ // TODO:: valid only for STM32F42x
+ const void* ccm_start = (const void*)0x10000000;
+ const void* ccm_end = (const void*)0x1000FFFF;
+ return !(ptr >= ccm_start && ptr <= ccm_end);
+}
+
+void dma2d_wait(void) {
+ while (HAL_DMA2D_PollForTransfer(&dma2d_handle, 10) != HAL_OK)
+ ;
+}
+
+bool dma2d_rgb565_fill(const dma2d_params_t* dp) {
+ dma2d_wait();
+
+ if (dp->src_alpha == 255) {
+ dma2d_handle.Init.ColorMode = DMA2D_OUTPUT_RGB565;
+ dma2d_handle.Init.Mode = DMA2D_R2M;
+ dma2d_handle.Init.OutputOffset =
+ dp->dst_stride / sizeof(uint16_t) - dp->width;
+ HAL_DMA2D_Init(&dma2d_handle);
+
+ HAL_DMA2D_Start(&dma2d_handle, gdc_color_to_color32(dp->src_fg),
+ (uint32_t)dp->dst_row + dp->dst_x * sizeof(uint16_t),
+ dp->width, dp->height);
+ } else {
+ // STM32F4 can not accelerate blending with the fixed color
+ uint16_t* dst_ptr = (uint16_t*)dp->dst_row + dp->dst_x;
+ uint16_t height = dp->height;
+ uint8_t alpha = dp->src_alpha;
+ while (height-- > 0) {
+ for (int x = 0; x < dp->width; x++) {
+ dst_ptr[x] = gdc_color16_blend_a8(dp->src_fg, dst_ptr[x], alpha);
+ }
+ dst_ptr += dp->dst_stride / sizeof(*dst_ptr);
+ }
+ }
+
+ return true;
+}
+
+/*
+static void dma2d_config_clut(uint32_t layer, gdc_color_t fg, gdc_color_t bg) {
+
+ #define LAYER_COUNT 2
+ #define GRADIENT_STEPS 16
+
+ static struct {
+ gdc_color32_t gradient[GRADIENT_STEPS];
+ } cache[LAYER_COUNT] = { 0 };
+
+ if (layer >= LAYER_COUNT) {
+ return;
+ }
+
+ uint32_t c_fg = gdc_color_to_color32(fg);
+ uint32_t c_bg = gdc_color_to_color32(bg);
+
+ uint32_t* gradient = cache[layer].gradient;
+
+ if (c_bg != gradient[0] || c_fg != gradient[GRADIENT_STEPS - 1]) {
+ for (int step = 0; step < GRADIENT_STEPS; step++) {
+ gradient[step] = gdc_color32_blend_a4(fg, bg, step);
+ }
+
+ DMA2D_CLUTCfgTypeDef clut;
+ clut.CLUTColorMode = DMA2D_CCM_ARGB8888;
+ clut.Size = GRADIENT_STEPS - 1;
+ clut.pCLUT = gradient;
+
+ HAL_DMA2D_ConfigCLUT(&dma2d_handle, clut, layer);
+
+ while (HAL_DMA2D_PollForTransfer(&dma2d_handle, 10) != HAL_OK)
+ ;
+ }
+}*/
+
+static void dma2d_config_clut(uint32_t layer, gdc_color_t fg, gdc_color_t bg) {
+#define LAYER_COUNT 2
+#define GRADIENT_STEPS 16
+
+ static struct {
+ gdc_color_t c_fg;
+ gdc_color_t c_bg;
+ } cache[LAYER_COUNT] = {0};
+
+ if (layer >= LAYER_COUNT) {
+ return;
+ }
+
+ volatile uint32_t* clut =
+ layer ? dma2d_handle.Instance->FGCLUT : dma2d_handle.Instance->BGCLUT;
+
+ if (fg != cache[layer].c_fg || bg != cache[layer].c_bg) {
+ cache[layer].c_fg = fg;
+ cache[layer].c_bg = bg;
+
+ for (int step = 0; step < GRADIENT_STEPS; step++) {
+ clut[step] = gdc_color32_blend_a4(fg, bg, step);
+ }
+
+ DMA2D_CLUTCfgTypeDef clut;
+ clut.CLUTColorMode = DMA2D_CCM_ARGB8888;
+ clut.Size = GRADIENT_STEPS - 1;
+ clut.pCLUT = 0; // ???
+
+ HAL_DMA2D_ConfigCLUT(&dma2d_handle, clut, layer);
+ }
+}
+
+static void dma2d_rgb565_copy_mono4_first_col(dma2d_params_t* dp,
+ const gdc_color16_t* gradient) {
+ uint16_t* dst_ptr = (uint16_t*)dp->dst_row + dp->dst_x;
+ uint8_t* src_ptr = (uint8_t*)dp->src_row + dp->src_x / 2;
+
+ int height = dp->height;
+
+ while (height-- > 0) {
+ uint8_t fg_lum = src_ptr[0] >> 4;
+ dst_ptr[0] = gradient[fg_lum];
+ dst_ptr += dp->dst_stride / sizeof(*dst_ptr);
+ src_ptr += dp->src_stride / sizeof(*src_ptr);
+ }
+}
+
+static void dma2d_rgb565_copy_mono4_last_col(dma2d_params_t* dp,
+ const gdc_color16_t* gradient) {
+ uint16_t* dst_ptr = (uint16_t*)dp->dst_row + (dp->dst_x + dp->width - 1);
+ uint8_t* src_ptr = (uint8_t*)dp->src_row + (dp->src_x + dp->width - 1) / 2;
+
+ int height = dp->height;
+
+ while (height-- > 0) {
+ uint8_t fg_lum = src_ptr[0] & 0x0F;
+ dst_ptr[0] = gradient[fg_lum];
+ dst_ptr += dp->dst_stride / sizeof(*dst_ptr);
+ src_ptr += dp->src_stride / sizeof(*src_ptr);
+ }
+}
+
+bool dma2d_rgb565_copy_mono4(const dma2d_params_t* params) {
+ const gdc_color16_t* src_gradient = NULL;
+
+ dma2d_params_t dp_copy = *params;
+ dma2d_params_t* dp = &dp_copy;
+
+ dma2d_wait();
+
+ if (dp->src_x & 1) {
+ // First column of mono4 bitmap is odd
+ // Use the CPU to draw the first column
+ src_gradient = gdc_color16_gradient_a4(dp->src_fg, dp->src_bg);
+ dma2d_rgb565_copy_mono4_first_col(dp, src_gradient);
+ dp->dst_x += 1;
+ dp->src_x += 1;
+ dp->width -= 1;
+ }
+
+ if (dp->width > 0 && dp->width & 1) {
+ // The width is odd
+ // Use the CPU to draw the last column
+ if (src_gradient == NULL) {
+ src_gradient = gdc_color16_gradient_a4(dp->src_fg, dp->src_bg);
+ }
+ dma2d_rgb565_copy_mono4_last_col(dp, src_gradient);
+ dp->width -= 1;
+ }
+
+ dma2d_handle.Init.ColorMode = DMA2D_OUTPUT_RGB565;
+ dma2d_handle.Init.Mode = DMA2D_M2M_PFC;
+ dma2d_handle.Init.OutputOffset =
+ dp->dst_stride / sizeof(uint16_t) - dp->width;
+ HAL_DMA2D_Init(&dma2d_handle);
+
+ dma2d_handle.LayerCfg[1].InputColorMode = DMA2D_INPUT_L4;
+ dma2d_handle.LayerCfg[1].InputOffset = dp->src_stride * 2 - dp->width;
+ dma2d_handle.LayerCfg[1].AlphaMode = 0;
+ dma2d_handle.LayerCfg[1].InputAlpha = 0;
+ HAL_DMA2D_ConfigLayer(&dma2d_handle, 1);
+
+ dma2d_config_clut(1, dp->src_fg, dp->src_bg);
+
+ HAL_DMA2D_Start(&dma2d_handle, (uint32_t)dp->src_row + dp->src_x / 2,
+ (uint32_t)dp->dst_row + dp->dst_x * sizeof(uint16_t),
+ dp->width, dp->height);
+
+ return true;
+}
+
+bool dma2d_rgb565_copy_rgb565(const dma2d_params_t* dp) {
+ dma2d_wait();
+
+ dma2d_handle.Init.ColorMode = DMA2D_OUTPUT_RGB565;
+ dma2d_handle.Init.Mode = DMA2D_M2M_PFC;
+ dma2d_handle.Init.OutputOffset =
+ dp->dst_stride / sizeof(uint16_t) - dp->width;
+ HAL_DMA2D_Init(&dma2d_handle);
+
+ dma2d_handle.LayerCfg[1].InputColorMode = DMA2D_INPUT_RGB565;
+ dma2d_handle.LayerCfg[1].InputOffset =
+ dp->src_stride / sizeof(uint16_t) - dp->width;
+ dma2d_handle.LayerCfg[1].AlphaMode = 0;
+ dma2d_handle.LayerCfg[1].InputAlpha = 0;
+ HAL_DMA2D_ConfigLayer(&dma2d_handle, 1);
+
+ HAL_DMA2D_Start(&dma2d_handle,
+ (uint32_t)dp->src_row + dp->src_x * sizeof(uint16_t),
+ (uint32_t)dp->dst_row + dp->dst_x * sizeof(uint16_t),
+ dp->width, dp->height);
+
+ return true;
+}
+
+static void dma2d_rgb565_blend_mono4_first_col(const dma2d_params_t* dp) {
+ uint16_t* dst_ptr = (uint16_t*)dp->dst_row + dp->dst_x;
+ uint8_t* src_ptr = (uint8_t*)dp->src_row + dp->src_x / 2;
+
+ int height = dp->height;
+
+ while (height-- > 0) {
+ uint8_t fg_alpha = src_ptr[0] >> 4;
+ dst_ptr[0] = gdc_color16_blend_a4(
+ dp->src_fg, gdc_color16_to_color(dst_ptr[0]), fg_alpha);
+ dst_ptr += dp->dst_stride / sizeof(*dst_ptr);
+ src_ptr += dp->src_stride / sizeof(*src_ptr);
+ }
+}
+
+static void dma2d_rgb565_blend_mono4_last_col(const dma2d_params_t* dp) {
+ uint16_t* dst_ptr = (uint16_t*)dp->dst_row + (dp->dst_x + dp->width - 1);
+ uint8_t* src_ptr = (uint8_t*)dp->src_row + (dp->src_x + dp->width - 1) / 2;
+
+ int height = dp->height;
+
+ while (height-- > 0) {
+ uint8_t fg_alpha = src_ptr[0] & 0x0F;
+ dst_ptr[0] = gdc_color16_blend_a4(
+ dp->src_fg, gdc_color16_to_color(dst_ptr[0]), fg_alpha);
+ dst_ptr += dp->dst_stride / sizeof(*dst_ptr);
+ src_ptr += dp->src_stride / sizeof(*src_ptr);
+ }
+}
+
+bool dma2d_rgb565_blend_mono4(const dma2d_params_t* params) {
+ dma2d_wait();
+
+ dma2d_params_t dp_copy = *params;
+ dma2d_params_t* dp = &dp_copy;
+
+ if (dp->src_x & 1) {
+ // First column of mono4 bitmap is odd
+ // Use the CPU to draw the first column
+ dma2d_rgb565_blend_mono4_first_col(dp);
+ dp->dst_x += 1;
+ dp->src_x += 1;
+ dp->width -= 1;
+ }
+
+ if (dp->width > 0 && dp->width & 1) {
+ // The width is odd
+ // Use the CPU to draw the last column
+ dma2d_rgb565_blend_mono4_last_col(dp);
+ dp->width -= 1;
+ }
+
+ if (dp->width > 0) {
+ dma2d_handle.Init.ColorMode = DMA2D_OUTPUT_RGB565;
+ dma2d_handle.Init.Mode = DMA2D_M2M_BLEND;
+ dma2d_handle.Init.OutputOffset =
+ dp->dst_stride / sizeof(uint16_t) - dp->width;
+ HAL_DMA2D_Init(&dma2d_handle);
+
+ dma2d_handle.LayerCfg[1].InputColorMode = DMA2D_INPUT_A4;
+ dma2d_handle.LayerCfg[1].InputOffset = dp->src_stride * 2 - dp->width;
+ dma2d_handle.LayerCfg[1].AlphaMode = 0;
+ dma2d_handle.LayerCfg[1].InputAlpha = gdc_color_to_color32(dp->src_fg);
+ HAL_DMA2D_ConfigLayer(&dma2d_handle, 1);
+
+ dma2d_handle.LayerCfg[0].InputColorMode = DMA2D_INPUT_RGB565;
+ dma2d_handle.LayerCfg[0].InputOffset =
+ dp->dst_stride / sizeof(uint16_t) - dp->width;
+ dma2d_handle.LayerCfg[0].AlphaMode = 0;
+ dma2d_handle.LayerCfg[0].InputAlpha = 0;
+ HAL_DMA2D_ConfigLayer(&dma2d_handle, 0);
+
+ HAL_DMA2D_BlendingStart(
+ &dma2d_handle, (uint32_t)dp->src_row + dp->src_x / 2,
+ (uint32_t)dp->dst_row + dp->dst_x * sizeof(uint16_t),
+ (uint32_t)dp->dst_row + dp->dst_x * sizeof(uint16_t), dp->width,
+ dp->height);
+ }
+
+ return true;
+}
diff --git a/core/site_scons/boards/trezor_t.py b/core/site_scons/boards/trezor_t.py
index 1bf39befb..b53cd2c45 100644
--- a/core/site_scons/boards/trezor_t.py
+++ b/core/site_scons/boards/trezor_t.py
@@ -87,6 +87,7 @@ def configure(
if "dma2d" in features_wanted:
defines += ["USE_DMA2D"]
sources += ["embed/trezorhal/stm32f4/dma2d.c"]
+ sources += ["embed/trezorhal/stm32f4/dma2d_gdc.c"]
sources += [
"vendor/micropython/lib/stm32lib/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_dma2d.c"
]