From c97758f912cd1ad66af623437bedbba4915f1899 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sat, 28 Aug 2021 17:17:40 +0100 Subject: [PATCH] Fast color text rendering via PR7682. --- writer/DRIVERS.md | 9 +- writer/WRITER.md | 39 +++----- writer/framebuf_utils/Makefile | 13 --- writer/framebuf_utils/framebuf_utils.c | 118 ----------------------- writer/framebuf_utils/framebuf_utils.mpy | Bin 739 -> 0 bytes writer/framebuf_utils/test.py | 32 ------ writer/writer.py | 58 +++++------ 7 files changed, 52 insertions(+), 217 deletions(-) delete mode 100644 writer/framebuf_utils/Makefile delete mode 100644 writer/framebuf_utils/framebuf_utils.c delete mode 100644 writer/framebuf_utils/framebuf_utils.mpy delete mode 100644 writer/framebuf_utils/test.py diff --git a/writer/DRIVERS.md b/writer/DRIVERS.md index f776adf..f14d371 100644 --- a/writer/DRIVERS.md +++ b/writer/DRIVERS.md @@ -178,7 +178,7 @@ The index holds two integers (each occupying 2 bytes) per character. The index has an entry for every character in the specified range, whether or not that character exists. -Index entries are offsets into the `_font` bytearray represnting the start and +Index entries are offsets into the `_font` bytearray representing the start and end of the glyph. If the font comprises a set of characters which is not contiguous, missing characters have an index entry which points to the first glyph in the `_font` bytearray. This ensures that the default glyph is @@ -253,6 +253,13 @@ each of the mapping options. They are used with drivers for SSD1306 OLEDs, SSD1963 LCD displays, the official LCD160CR and the Digital Artists 2.7 inch e-paper display. +# Writing display drivers + +A guide to writing suitable drivers may be found +[here](https://github.com/peterhinch/micropython-nano-gui/blob/master/DRIVERS.md#7-writing-device-drivers). +These can be very simple as the `FrameBuffer` base class provides much of the +functionality. + # Appendix 1. The -i --iterate argument This specialist arg causes extra code to be included in the font file, to diff --git a/writer/WRITER.md b/writer/WRITER.md index 40d8c30..28a3af2 100644 --- a/writer/WRITER.md +++ b/writer/WRITER.md @@ -60,7 +60,7 @@ Labels and Fields (from nanogui.py). 2.2 [The CWriter class](./WRITER.md#22-the-cwriter-class) For colour displays.      2.2.1 [Constructor](./WRITER.md#221-constructor)      2.2.2 [Methods](./WRITER.md#222-methods) -      2.2.3 [A performance boost](./WRITER.md#223-a-performance-boost) +      2.2.3 [Performance](./WRITER.md#223-performance) A firmware enhancement. 3. [Notes](./WRITER.md#3-notes) ###### [Main README](../README.md) @@ -88,6 +88,10 @@ very simple version still exists as `writer_minimal.py`. ## 1.1 Release Notes +V0.4.3 Aug 2021 +Supports fast rendering of glyphs to color displays (PR7682). See +[Performance](./WRITER.md#223-performance). + V0.4.0 Jan 2021 Improved handling of the `col_clip` and `wrap` options. Improved accuracy avoids needless word wrapping. The clip option now displays as much of the last @@ -115,8 +119,6 @@ for a non-Pyboard target. 4. `writer_tests.py` Test/demo scripts. Import to see usage information. 5. `writer_minimal.py` A minimal version for highly resource constrained devices. - 6. `framebuf_utils.framebuf_utils.mpy` A means of improving rendering speed - on color displays. Discussed [in 2.2.3](./WRITER.md#223-a-performance-boost) Sample fonts: 1. `freesans20.py` Variable pitch font file. @@ -264,31 +266,16 @@ The `printstring` method works as per the base class except that the string is rendered in foreground color on background color (or reversed if `invert` is `True`). -### 2.2.3 A performance boost +### 2.2.3 Performance -Rendering performance of the `Cwriter` class is slow: owing to limitations in -the `framebuf.blit` method the class renders glyphs one pixel at a time. There -is a way to improve performance. It was developed by Jim Mussared (@jimmo) and -consists of a native C module. +A firmware change [PR7682](https://github.com/micropython/micropython/pull/7682) +has enabled a substantial improvement to text rendering speed. This will be +incorporated in V1.17 and is available in daily builds. The `Writer` class +checks for suitable firmware. If the firmware lacks this enhancement a slower +method of rendering is used. -This works well on Pyboards (1.x and D) but I have had no success on other -platforms including the Raspberry Pi Pico. The code will silently ignore this -module on other platforms. The following applies only when run on a Pyboard. - -On import, `writer.py` attempts to import a module `framebuf_utils`. If this -succeeds, glyph rendering will be substantially faster. If the file is not -present the class will work using normal rendering. If the file is missing or -invalid a harmless advisory note is printed and the code will run using normal -rendering. - -The directory `framebuf_utils` contains the source file, the makefile and a -version of `framebuf_utils.mpy` for `armv7m` architecture (e.g. Pyboards). -This allows for recompiling for other architectures if anyone feels like -experimenting. However the fact that it crashes the Pico suggests that the code -is highly specific to the Pybaord. - -The module has a `fast_mode` variable which is set `True` on import if the mode -was successfully engaged. User code should treat this as read-only. +The module has a `fast_mode` variable which is set `True` on import if the +firmware supports fast rendering. User code should treat this as read-only. # 3. Notes diff --git a/writer/framebuf_utils/Makefile b/writer/framebuf_utils/Makefile deleted file mode 100644 index 3443442..0000000 --- a/writer/framebuf_utils/Makefile +++ /dev/null @@ -1,13 +0,0 @@ -# Location of top-level MicroPython directory -MPY_DIR = ../../.. - -# Name of module (different to built-in framebuf so it can coexist) -MOD = framebuf_utils - -# Source files (.c or .py) -SRC = framebuf_utils.c - -# Architecture to build for (x86, x64, armv7m, xtensa, xtensawin) -ARCH = armv7m - -include $(MPY_DIR)/py/dynruntime.mk diff --git a/writer/framebuf_utils/framebuf_utils.c b/writer/framebuf_utils/framebuf_utils.c deleted file mode 100644 index 8bf0e1c..0000000 --- a/writer/framebuf_utils/framebuf_utils.c +++ /dev/null @@ -1,118 +0,0 @@ -#define MICROPY_PY_FRAMEBUF (1) - -#include "py/dynruntime.h" - -#if !defined(__linux__) -void *memset(void *s, int c, size_t n) { - return mp_fun_table.memset_(s, c, n); -} -#endif - -// Match definition from modframebuf.c. -typedef struct _mp_obj_framebuf_t { - mp_obj_base_t base; - mp_obj_t buf_obj; // need to store this to prevent GC from reclaiming buf - void *buf; - uint16_t width, height, stride; - uint8_t format; -} mp_obj_framebuf_t; - -// This points to the real mp_type_framebuf from modframebuf.c. -mp_obj_type_t *mp_type_framebuf; - -// Unbound FrameBuffer.pixel function. -mp_obj_t framebuf_pixel_obj; - -// render(dest, src, x, y, fgcolor, bgcolor=0) -STATIC mp_obj_t framebuf_render(size_t n_args, const mp_obj_t *args) { - // Convert dest/src subclass to the native mp_type_framebuf. - mp_obj_t dest_in = mp_obj_cast_to_native_base(args[0], MP_OBJ_FROM_PTR(mp_type_framebuf)); - if (dest_in == MP_OBJ_NULL) { - mp_raise_TypeError(NULL); - } - mp_obj_framebuf_t *dest = MP_OBJ_TO_PTR(dest_in); - - mp_obj_t source_in = mp_obj_cast_to_native_base(args[1], MP_OBJ_FROM_PTR(mp_type_framebuf)); - if (source_in == MP_OBJ_NULL) { - mp_raise_TypeError(NULL); - } - mp_obj_framebuf_t *source = MP_OBJ_TO_PTR(source_in); - - // Pre-build args list for calling framebuf.pixel(). - mp_obj_t args_getpixel[3] = { source_in }; - mp_obj_t args_setpixel[4] = { dest_in }; - - mp_int_t x = mp_obj_get_int(args[2]); - mp_int_t y = mp_obj_get_int(args[3]); - mp_int_t fgcol = mp_obj_get_int(args[4]); - mp_int_t bgcol = 0; - if (n_args > 5) { - bgcol = mp_obj_get_int(args[5]); - } - - if ( - (x >= dest->width) || - (y >= dest->height) || - (-x >= source->width) || - (-y >= source->height) - ) { - // Out of bounds, no-op. - return mp_const_none; - } - - // Clip. - int x0 = MAX(0, x); - int y0 = MAX(0, y); - int x1 = MAX(0, -x); - int y1 = MAX(0, -y); - int x0end = MIN(dest->width, x + source->width); - int y0end = MIN(dest->height, y + source->height); - - for (; y0 < y0end; ++y0) { - int cx1 = x1; - for (int cx0 = x0; cx0 < x0end; ++cx0) { - // source.pixel(cx1, y1) - args_getpixel[1] = MP_OBJ_NEW_SMALL_INT(cx1); - args_getpixel[2] = MP_OBJ_NEW_SMALL_INT(y1); - uint32_t col = mp_obj_get_int(mp_call_function_n_kw(framebuf_pixel_obj, 3, 0, args_getpixel)); - - // dest.pixel(cx0, y0, bgcol/fgcol) - args_setpixel[1] = MP_OBJ_NEW_SMALL_INT(cx0); - args_setpixel[2] = MP_OBJ_NEW_SMALL_INT(y0); - if (col == 0) { - args_setpixel[3] = MP_OBJ_NEW_SMALL_INT(bgcol); - } else { - args_setpixel[3] = MP_OBJ_NEW_SMALL_INT(fgcol); - } - - mp_call_function_n_kw(framebuf_pixel_obj, 4, 0, args_setpixel); - - ++cx1; - } - ++y1; - } - return mp_const_none; -} -STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(framebuf_render_obj, 5, 6, framebuf_render); - -mp_obj_t mpy_init(mp_obj_fun_bc_t *self, size_t n_args, size_t n_kw, mp_obj_t *args) { - MP_DYNRUNTIME_INIT_ENTRY - - // import framebuf - mp_obj_t modframebuf = mp_import_name(MP_QSTR_framebuf, mp_const_none, 0); - // mp_type_framebuf = framebuf.FrameBuffer - mp_type_framebuf = MP_OBJ_TO_PTR(mp_load_attr(modframebuf, MP_QSTR_FrameBuffer)); - - // framebuf_pixel_obj = mp_type_framebuf.pixel - mp_obj_t dest[2]; - mp_load_method(MP_OBJ_FROM_PTR(mp_type_framebuf), MP_QSTR_pixel, dest); - - // The resulting reference might be heap allocated due to MICROPY_BUILTIN_METHOD_CHECK_SELF_ARG. - // So store a reference to it in globals so the GC knows about it. - framebuf_pixel_obj = dest[0]; - mp_store_global(MP_QSTR_pixel, framebuf_pixel_obj); - - mp_store_global(MP_QSTR_render, MP_OBJ_FROM_PTR(&framebuf_render_obj)); - - MP_DYNRUNTIME_INIT_EXIT -} diff --git a/writer/framebuf_utils/framebuf_utils.mpy b/writer/framebuf_utils/framebuf_utils.mpy deleted file mode 100644 index 6bb1398d8799551b4b5fd61aadf0c7a71f7eb5a2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 739 zcmYjOO-vI(6rSB~e`!k%0tHRzDgmM)u8BVwIc(PLjIa``r@{%8Hd|2Alpqq+Kt0I8 z#M-XO+BK%cn4nxZsIfuV9z0+Si3UsrB=%%!YfKvtZj4EtN)NuneD8PO_ulK#*EPAM z(4RB_9WTv-xqFgVyf(O|r5Os!XpW;koEe_y_Nt({IpBgFOF+Fwf`8JUEu6Q*ubMRL z6CNxOk>7lurehhA7E8Z2N&~_)#I}b?<^v~OpR_NmjJjX~(P9wC-<57dCYh^5BB&P@ zGoU#y+9D*qnzi$***$!Xw5qX@1rfw#MAAT+f(21VKrup+W&$!I6cXwW@82MBO)vly z?1?8eYwE4rzMa(U>1DTNdr`BpD@8!ea@6LcW@m13k2(ETn^?QBv{Sg}g5NT)i=cW% z1$0|(XA&Gq@K|qY&1ELi?k1jxbLt=*cb~>M3NHd3X z@4@%AbQNYM_yjr&Q>X)yWxY-3S*g9nNv%}X9X5;yrN5(cRA;Op(IZo3=LfM yabteJFC=*#{<3vx%rEs@f`RM4E0Smp`9=nPArJoD7UqaBZwy27X}FUJv!%b-qyifN diff --git a/writer/framebuf_utils/test.py b/writer/framebuf_utils/test.py deleted file mode 100644 index 1957590..0000000 --- a/writer/framebuf_utils/test.py +++ /dev/null @@ -1,32 +0,0 @@ -import framebuf_utils -import framebuf - -# Emulate a display driver subclassed from framebuf.FrameBuffer -class Display(framebuf.FrameBuffer): - def __init__(self): - self.buf = bytearray(4 * 4 * 2) - super().__init__(self.buf, 4, 4, framebuf.RGB565) - -device = Display() - -def foo(): - width = 2 # Glyph dimensions - height = 2 - i = 0 - - while True: - buf = bytearray(width * height // 8 + 1) - fbc = framebuf.FrameBuffer(buf, width, height, framebuf.MONO_HMSB) - fbc.pixel(0, 0, 1) - print(buf) - - framebuf_utils.render(device, fbc, 1, 1, 0x5555, 0xaaaa) - print(device.buf) - print(device.pixel(0, 0)) - print(device.pixel(1, 1)) - print(device.pixel(2, 1)) - - i += 1 - print(i) - -foo() diff --git a/writer/writer.py b/writer/writer.py index befa7b5..2ad69a7 100644 --- a/writer/writer.py +++ b/writer/writer.py @@ -1,9 +1,10 @@ # writer.py Implements the Writer class. # Handles colour, word wrap and tab stops -# V0.40 Jan 2021 Improved handling of word wrap and line clip. Upside-down +# V0.4.3 Aug 2021 Support for fast blit to color displays (PR7682). +# V0.4.0 Jan 2021 Improved handling of word wrap and line clip. Upside-down # rendering no longer supported: delegate to device driver. -# V0.35 Sept 2020 Fast rendering option for color displays +# V0.3.5 Sept 2020 Fast rendering option for color displays # Released under the MIT License (MIT). See LICENSE. # Copyright (c) 2019-2021 Peter Hinch @@ -14,35 +15,35 @@ # Timings based on a 20 pixel high proportional font, run on a pyboard V1.0. # Using CWriter's slow rendering: _printchar 9.5ms typ, 13.5ms max. -# Using Writer's fast rendering: _printchar 115μs min 480μs typ 950μs max. -# CWriter on Pyboard D SF2W at standard clock rate -# Fast method 500-600μs typical, up to 1.07ms on larger fonts -# Revised fast method 691μs avg, up to 2.14ms on larger fonts -# Slow method 2700μs typical, up to 11ms on larger fonts import framebuf from uctypes import bytearray_at, addressof -from sys import platform +from sys import implementation +import os -__version__ = (0, 4, 2) +__version__ = (0, 4, 3) -fast_mode = platform == 'pyboard' -if fast_mode: +def buildcheck(device): + if not hasattr(device, 'palette'): + return False + i0, i1, _ = implementation[1] + if i0 > 1 or i1 > 16: + return True + # After release of V1.17 require that build. Until then check for date. + # TODO simplify this once V1.17 is released. try: - try: - from framebuf_utils import render - except ImportError: # May be running in GUI. Try relative import. - try: - from .framebuf_utils import render - except ImportError: - fast_mode = False - except ValueError: - fast_mode = False - if not fast_mode: - print('Ignoring missing or invalid framebuf_utils.mpy.') + datestring = os.uname()[3] + date = datestring.split(' on')[1] + date = date.lstrip()[:10] + idate = tuple([int(x) for x in date.split('-')]) + return idate >= (2021, 8, 25) + except AttributeError: + return False +fast_mode = False # False for mono displays although actually these render fast + class DisplayState(): def __init__(self): self.text_row = 0 @@ -270,6 +271,8 @@ class CWriter(Writer): def __init__(self, device, font, fgcolor=None, bgcolor=None, verbose=True): super().__init__(device, font, verbose) + global fast_mode + fast_mode = buildcheck(device) if bgcolor is not None: # Assume monochrome. self.bgcolor = bgcolor if fgcolor is not None: @@ -285,11 +288,12 @@ class CWriter(Writer): if self.glyph is None: return # All done buf = bytearray_at(addressof(self.glyph), len(self.glyph)) - fbc = framebuf.FrameBuffer(buf, self.char_width, self.char_height, self.map) - fgcolor = self.bgcolor if invert else self.fgcolor - bgcolor = self.fgcolor if invert else self.bgcolor - # render clips a glyph if outside bounds of destination - render(self.device, fbc, s.text_col, s.text_row, fgcolor, bgcolor) + fbc = framebuf.FrameBuffer(buf, self.clip_width, self.char_height, self.map) + palette = self.device.palette + palette.bg(self.fgcolor if invert else self.bgcolor) + palette.fg(self.bgcolor if invert else self.fgcolor) + + self.device.blit(fbc, s.text_col, s.text_row, -1, palette) s.text_col += self.char_width self.cpos += 1