Fast color text rendering via PR7682.

pull/45/head
Peter Hinch 2021-08-28 17:17:40 +01:00
rodzic ff08d3526f
commit c97758f912
7 zmienionych plików z 52 dodań i 217 usunięć

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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
}

Plik binarny nie jest wyświetlany.

Wyświetl plik

@ -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()

Wyświetl plik

@ -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