Badgeware Picovector - #116
Conversation
ST7701 inherited DisplayDriver and took a PicoGraphics in update(), which tied the panel driver to a library Presto no longer builds. Hold width, height and rotation directly and take a raw RGBA8888 source instead, converting to the RGB565 scanout buffer. The beam-racing that update() already did is kept: the conversion stays behind the line being scanned out, so a frame is never torn.
Drops hershey_fonts, bitmap_fonts, picographics, pngdec, jpegdec and the old picovector in favour of the PicoVector used by Badgeware, which brings its own PNG and JPEG decoders. PV_DUAL_CORE stays off: core1 runs the ST7701 scanout, so the rasteriser has to share core0.
PicoVector draws RGBA8888, the ST7701 scans out RGB565, so the module now owns both: the buffer protocol hands out the RGBA surface for picovector's image() to wrap, and update() converts it into the scanout buffer. Scanout is read continuously by DMA and has to stay in SRAM. The rasteriser's 80K working buffer no longer leaves room for a full-res 460K scanout buffer there, so the static pool is sized for the default half-res mode and holds both buffers; full res allocates its 1.35M from the GC heap, which is in PSRAM. Those allocations are kept reachable from the object, since the GC scans only its own heap and the stack. Palette mode goes: it was PicoGraphics PEN_P8 driving the 18-bit palette PIO, and has no equivalent here. It now raises rather than silently misrendering.
display is now a picovector image wrapping the module's RGBA buffer, so examples use image drawing calls and colour objects rather than PicoGraphics pens. Drops the palette, direct_to_fb and layers arguments, which described PicoGraphics buffer layouts that no longer exist.
fetch is unchanged. wifi loses fatal_error, which painted a Tufty error screen, and raises instead; its frame-tick timeouts become time.ticks_ms. Both are frozen alongside ezwifi, which presto.py still uses for its own connect(); fetch drives the poll-per-frame wifi module rather than asyncio.
Clones picovector-micropython (with its picovector core submodule) and passes PICOVECTOR_MICROPYTHON_DIR to cmake. MicroPython moves to feature/presto-badgeware: bw-1.29.0-gc, which picovector needs for m_malloc_no_scan, plus Presto's PSRAM linker-symbol patch.
Polygon/Transform become shape/mat3, with the transform living on the shape rather than on a global vector context. Text is positioned directly: the new text() has no max_width or align, so the launcher centres with measure_text. The rounded corner mask was two rectangles in one Polygon relying on even-odd fill. Drawing a list of shapes fills each in turn, so the mask is now a single custom shape with an outer rectangle contour and a generated rounded inner one.
The old text anchor was the baseline, so centring added half the text height; the new one is the top of the em box, so it subtracts.
The multi-contour icon becomes a single custom shape carrying its own transform, rather than a Polygon drawn through a shared vector context.
reset_pen freed a palette entry, which colours are no longer allocated from, so the pen is now built inline each frame.
It drew through its own PicoGraphics over Presto's buffer, using set_pen and pixel_span from a viper function. The surface is RGBA8888 now and there is no span primitive, so the viper function writes words into the framebuffer directly, which is closer to what it was after anyway.
jpegdec is gone; image.load decodes JPEG and PNG. Both images are decoded once at startup rather than re-decoded every second.
jpegdec's open_file/decode pair becomes image.load plus a blit.
The background was decoded into layer 0 and left there while the letter grid redrew on layer 1. Without layers it is decoded once into an image and blitted each frame. The grid used bitmap8 at scale 1; there is no bitmap font here, so it draws at a vector size close to the old cell height.
Bitmap text scales become vector point sizes at eight pixels per scale step, and measure_text returns a width/height pair rather than a width.
Button labels were placed from a baseline offset; the new text anchor is the top of the em box, so they centre in the button bounds instead.
Button labels centre in their bounds and the countdown centres on measured text, rather than both working from hardcoded offsets and a baseline anchor.
Shapes carry their own transform now, so the rotate-about-a-point that a shared Transform did per draw becomes a matrix built once. Polygon.arc's filled wedge is shape.pie; shape.arc is a ring segment and takes two radii.
The two readings were drawn through the same 45 degree transform as the diagonal band. Shapes carry a transform but text does not, so the band still rotates and the readings are now upright either side of it.
jpegdec's open_RAM/decode becomes image.load, which takes bytes as well as a path. The fizzle transition dissolved layer 1 to reveal the next image already decoded on layer 0. Without layers the next frame is composed off-screen and the dissolve copies from it pixel by pixel.
The button class no longer needs a vector context passed in, and its label centres on the new top-of-em-box anchor rather than a baseline.
breakout_scd41.init() and the module-level functions are gone; the driver became BreakoutSCD41(i2c) so the collector can trace the I2C reference. The constructor now raises when the sensor doesn't respond, where init() did not, so the name has to exist before the try or the read loop below trips over it.
The graph and indicator lines were Polygons built from point lists, which shape.custom takes directly. Font size is an argument to text() rather than context state, and measure_text returns width first rather than a bounds tuple.
The spin accumulated in a shared Transform across frames. Shapes each hold their own matrix now, so the angle is tracked in the loop and the transform rebuilt from it.
Polygons that held several sub-paths become custom shapes built from point contours, which is what the instrument mask and the pitch ladder relied on. Polygon.arc took a radius and a stroke width; shape.arc is a ring segment between an inner and outer radius.
The lone circle Polygon is just display.circle. Font size is an argument to text() rather than context state, and the anchor moved from the baseline to the top of the em box.
A shape holds one transform, and the clock draws the same tick and hand shapes at many angles per frame, so a helper rebuilds the matrix for each draw.
set_spritesheet took a raw RGB565 buffer; images are RGBA8888 and load from PNG, so the two .16bpp sheets are re-encoded. They are 128x128 of 8x8 cells, which becomes a 16x16 grid, and sprite() returns a cell to blit into a scaled destination rect. Pen 0 was the transparent colour, so those pixels get an alpha of zero. range(WIDTH / 32) fed range a float, which it will not take; the ground row never drew. display.update() went with PicoGraphics. It was presto.update() that was wanted.
Every example that called set_antialiasing lost it in the port, so all vector work was rendering with hard edges. The equivalent is display.antialias: ANTIALIAS_FAST maps to X2, BEST and X16 to X4. In the launcher the icon and its labels used to be drawn through the tile's matrix, so their sizes scaled with it. Only the positions were being scaled, leaving the text a fixed size and, at 8 and 10 points with no antialiasing, illegible. The Material Symbols glyphs are also centred on x and render about twice their nominal size, unlike the Latin text in the same font, so they need centring on their ink rather than on the advance width.
A full res frame is 460K of RGB565, which does not fit in SRAM beside the rasteriser's 80K working buffer, so it was allocated from the GC heap and the scanout DMA read PSRAM a pixel at a time. Being latency bound, that starved the rasteriser, which is also working in PSRAM: 294ms per frame. Keep the frame in PSRAM but give the PIO four lines of SRAM, refilled a line ahead of the beam by the line interrupt on core1. The panel then pulls whole lines in sequential bursts, which PSRAM is far happier with, and update() converts into the PSRAM frame instead. 189ms per frame. Half res is untouched: both buffers still fit in SRAM, so it keeps scanning the front buffer directly and stays at 15.8ms per frame. Full res remains bandwidth bound on the RGBA surface itself, which is cleared, drawn and read back every frame. This addresses how the frame reaches the panel, not that. Co-Authored-By: AndrewCapon <andrew.capon@zen.co.uk>
Gradient stops are in the shape's own space, so one brush per tile follows the carousel transform without being rebuilt for position or scale.
clear() takes a brush, so the backdrop costs about a fifth of a millisecond over filling flat. Drawing a full screen rectangle would have doubled the frame instead. Travelling further across the colour space also narrows the RGB565 banding: these stops step every 1.7 pixels where a subtler pair banded every 3. The carousel had no pull toward the middle, so a fling left an icon parked part way to the next one. Once the user lets go and the spin has slowed, ease onto the nearest icon.
This reverts commit 6036a92. The cache made full res faster but not correct: refilling a line per interrupt needs 27.6MB/s sustained out of PSRAM inside a 34.7us line, so the ISR overran, the DMA re-read a slot and rows repeated, stretching the picture vertically. Scanning straight out of PSRAM instead just moves the failure: the transfer is latency bound and underruns into noise. Full res was never rendering correctly in this port, so there was nothing here worth keeping.
Scanout is continuous, so the RGB565 frame has to keep up with the panel's pixel rate. At 480x480 that is 460K, which no longer fits in SRAM beside the rasteriser's 80K working buffer, and PSRAM cannot serve it: read per pixel by DMA it underruns into noise, and refilled a line at a time by an interrupt it overruns and repeats rows. Neither is a tuning problem. The surface is also cleared, drawn and read back in PSRAM every frame, about 2.7MB of traffic, so even the arrangement that rendered fastest managed 189ms a frame against 15.8ms at half res. full_res now raises instead of rendering garbage, as palette does. vector_clock_full and co2 asked for it; both are laid out from WIDTH and HEIGHT so they scale, and co2's text sizes halve with the rest.
It never called set_antialiasing, so the wireframe and title were both hard edged. The title was also bitmap text at scale 1, which is 8 points as a vector size; at a legible size the hardcoded x no longer centres it.
|
I just tried building this branch, and there was a build failure against |
A follow up. I did manage to get statsbadge running on this branch, with minor changes. The most significant is that this PR seems to have an issue when writing to the filesystem. The hypothesis an LLM arrived at is that "the core1 ST7701 scanout [is] not cooperating with the RP2350 flash lockout – with |
XIP paused, yes, but core1 can continue to run - and needs to on Presto not to upset the display - so long as its executing strictly out of SRAM. Highly possible I broke that! This port was very experimental. statsbadge on Presto would be awesome. Probably not too bad either with the animations left off.
I definitely broke that! The old QRCode feature got moved into a factory on |
Added as a helper in PicoVector.
This extremely experimental PR brings Badgeware's Picovector library to Presto, with the caveat that it only works in 240x240 lo-res, pixel-doubled mode since Picovector wants a working buffer in SRAM to maintain a decent level of performance.
It's a curiosity for anyone who wants to work with the newer API and play with gradients, alpha blending, texture brushes and all the other good stuff Badgeware gets.
The examples have been - badly - ported and will absolutely contain bugs, quirks, issues and outright hilarity. The menu got a little more TLC with some gradients to zazz it up.