A 16-bit render canvas on ESP32: 1200x1600 renders on 8 MB of PSRAM (reTerminal E1004) - #375
Open
mariusandra wants to merge 1 commit into
Open
A 16-bit render canvas on ESP32: 1200x1600 renders on 8 MB of PSRAM (reTerminal E1004)#375mariusandra wants to merge 1 commit into
mariusandra wants to merge 1 commit into
Conversation
The scene canvas the embedded renderer composites into is now a pixie pfRgb565 image (2 B/px) instead of RGBA, claimed once at boot from a PSRAM block the firmware reserves before Wi-Fi (frameos_nim_reserve_canvas) and reused every render. A 1200x1600 canvas is 3.7 MiB instead of 7.3 MiB, which is what lets the 13.3" Spectra 6 panel render on-device on a stock 8 MB module — the Seeed reTerminal E1004 — where it was thin-client only. Why it is not a quality compromise: every panel the firmware drives is a dithered e-paper, and the dither now streams with its own full-precision error rows beside the canvas (forEachPaletteDithered / forEachGrayDithered in utils/dither.nim, bit-exact with the in-place version they replace, incl. its clip-per-step semantics — tested against the old algorithm). The 5/6-bit colour the canvas holds is below what the output can show: a side-by-side of a photo + gradients + text dithered to Spectra 6 through both canvases lands the same palette population within 0.05% per colour. The Pi runtime, HDMI/framebuffer and the wasm preview are untouched and keep full RGBA; -d:frameosCanvasRgbx keeps the RGBA canvas on ESP32 for A/B. The packers read the canvas through pixie's accessors and never write it (the grey packer lost its 8-byte-per-pixel float scratch, the two-plane packer its full RGBA copy). FrameOS raw-pixel sites (rotateDegrees, the embedded nearest scaler, error marker, spill) go through the accessors too; the interpreter's cache limits use byteSize/bufferPointer so a 565 canvas neither miscounts nor false-aliases. The budget math moves 4->2 in its mirrors: FOS_RENDER_CANVAS_BYTES_PER_PIXEL (frameos_display, the boot fit check), EMBEDDED_RENDER_CANVAS_BYTES_PER_PIXEL (backend build guard + deploy drawer), and EmbeddedReserveBytes is aligned to the 1536*1024 the other two always were. Seeed reTerminal E1004 preset (seeed_reterminal_e1004): EPD_13in3e on the E-series bus with cs2=2, rst=38, pwr=12, in the backend table, the console presets[], both frontends and the cloud flasher. Its T133A01 panel wants the vendor's analogue tuning, so EPD_13in3e.c gains a runtime variant (EPD_13IN3E_SetVariant: 0x74/0x50/0x05/0x06 values, an extra 0xA5, no CCSET) that frameos_display selects from the preset — the panel key stays EPD_13in3e everywhere, like the PhotoPainter PMIC selection. pixie pin -> FrameOS/pixie 64ce3c9 (rgb565-surface): the 565 image format. EMBEDDED_FIRMWARE_VERSION 48. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A 16-bit render canvas on ESP32: 1200×1600 renders on 8 MB of PSRAM
The goal: the Seeed reTerminal E1004 (13.3" 1200×1600 Spectra 6, ESP32-S3, 8 MB PSRAM) rendering scenes on-device, full resolution, no quality compromise. Before this PR the 13.3" class needed the 16 MB module: the RGBA canvas alone is 7.3 MiB.
What changed
Leg 1 — pixie (FrameOS/pixie#rgb565-surface, pinned here).
Imagegains aformat:pfRgbx(everything as before) orpfRgb565, a packed 16-bit RGB surface with no alpha. Every drawing operation works on one — fills, antialiased paths and text,drawwith any blend mode, gradients, opacity, views, copies, the scaled and streamed decoders writing straight into it.shadow/spreadraise; minify/magnify convert; the opaque/transparent predicates answer truthfully. RGBX output is untouched — every xray score in the pixie suite is identical to before.tests/test_rgb565.nimis an oracle: each scenario is drawn onto an RGBX and a 565 canvas and the quantised RGBX result must match — bit-exact for a single layer over a representable backdrop, one 5-bit step for compound cases. The SIMD variants guard on the format too (on amd64/arm64hasSimdreplaces the scalar body outright).Leg 2 — the dither streams (
utils/dither.nim).forEachPaletteDithered/forEachGrayDitheredwalk any image with two rows of error state beside it, reading through pixie's accessors and never writing the canvas. Bit-exact with the in-place version they replace — including its clip-per-step semantics — tested against the old algorithm (kept verbatim in the test) across bit depths, RGBX/565 canvases and views. The ESP32 packers are rewritten on top of them: the grey packer lost an 8-byte-per-pixel float scratch, the two-plane packer a full RGBA copy, the palette packer stops mutating the canvas. The Pi drivers get the same streamedditherPaletteIndexed(same bytes out, one less full RGBA copy).Leg 3 — the canvas is 565, claimed once at boot.
embedded_runtime.nim'srenderCanvas()wraps a PSRAM block the firmware reserves inmain.cright after the fit check and beforefos_wifi_init(frameos_nim_reserve_canvas), so the multi-MB contiguous run exists before Wi-Fi/TLS fragment the heap, and reuses it every render — no per-render allocate/collect churn. Nothing aliases the canvas across renders (the value pipeline never caches the live canvas), so reuse is safe. Error frames and the demo scene render into it too.-d:frameosCanvasRgbxkeeps the RGBA canvas for A/B. The Pi runtime, HDMI/framebuffer and the wasm preview are untouched and keep full RGBA.Leg 4 — the E1004 itself. Preset
seeed_reterminal_e1004(EPD_13in3e on the E-series bus: cs2=2, rst=38, pwr=12 — the pins ESPHome's integrated-board definition bakes in) in the backend table, the consolepresets[], both frontends and the cloud flasher. Its T133A01 panel wants the vendor's analogue tuning, soEPD_13in3e.cgains a runtime variant (EPD_13IN3E_SetVariant: 0x74/0x50/0x05/0x06 values, an extra 0xA5, no CCSET) thatframeos_displayselects from the preset — the panel key staysEPD_13in3eeverywhere, the same way the PhotoPainter PMIC is selected.Budget math moved 4→2 in its mirrors:
FOS_RENDER_CANVAS_BYTES_PER_PIXEL(boot fit check),EMBEDDED_RENDER_CANVAS_BYTES_PER_PIXEL(backend guard + deploy drawer,canvasBufferByteskey;rgbaBufferByteskept as an alias), andEmbeddedReserveBytesaligned to the1536*1024the other two always were.EMBEDDED_FIRMWARE_VERSION48.Why it is not a quality compromise
Every panel the firmware drives is a dithered e-paper, and the dither keeps its own full-precision error rows; the 5/6-bit colour the canvas holds is below what the output can show (a 6-colour Floyd–Steinberg field has per-pixel error dozens of times the 565 step). Measured on a 1200×1600 scene (mandrill photo + colour and grey gradients + AA text) dithered to Spectra 6 through both canvases: canvas delta before dither max 8/255, mean 2.4/255; palette population per colour identical within 0.05%; the panel renders are visually indistinguishable at 1:1 — including the grey ramp, where the 565 canvas shows visible steps and the dithered output does not. (Dithered dot positions differ on 38% of pixels, which is what error diffusion does to any input perturbation; the picture is the same.)
Verified
test_rgb565(31 scenarios); scalar path with-d:pixieNoSimd.test_ditherbit-exactness suite.test_embedded_firmware.py83 passed (incl. "13.3" fits 8 MB", E1004 preset + provisioning plan).tscclean; cloud flasher vitest 25 passed; auth-web typecheck clean.idf.py build(S3 32 MB profile) clean;ci_build_image.sh+ QEMU boot smoke.Not verified (no hardware on this desk)
Notes
git rebase --exec 'git commit --amend --no-edit -S' mainre-signs them.🤖 Generated with Claude Code