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" ]