venusian-surface / embedded-displays
Surface embedded displays: an IC display panel as a drawing output.
Package info
github.com/venusian-surface/embedded-displays
pkg:composer/venusian-surface/embedded-displays
Requires
- php: ^8.4|^8.5|^8.6
- gpio/contracts: ^0.10.0
- venusian-surface/contracts: ^0.10.0
- venusian-surface/framebuffers: ^0.10.0
- venusian-voyager/contracts: ^0.10.1
- venusian-voyager/nuts-and-bolts: ^0.10.1
Requires (Dev)
None
Suggests
- dept-of-scrapyard-robotics/jd79661: ^0.10.0 - JD79661 four-colour ePaper panels.
- dept-of-scrapyard-robotics/spectra6: ^0.10.0 - E Ink Spectra 6 panels.
- dept-of-scrapyard-robotics/ssd1306: ^0.10.0 - SSD1306 monochrome OLED panels over I2C or SPI.
- dept-of-scrapyard-robotics/ssd1680: ^0.10.0 - SSD1680 black/white and black/white/red ePaper panels.
- dept-of-scrapyard-robotics/st77xx: ^0.10.0 - ST7735, ST7789 and ST7796 colour TFT panels over SPI.
- scrapyard-io/framework: ^0.10.0 - conjure panels from config/circuits.
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-15 16:59:44 UTC
README
An IC display panel as a drawing output, beside a toolkit canvas. A display
hands out a framebuffer in the panel's own format; draw into it with a
rendering engine, then present() sends the panel what changed.
$display = app('displays')->panel('st7789'); // conjured from config/circuits/st7789.php $engine = app('drawing')->renderer('velvet', ['framebuffer' => $display->framebuffer()]); $face = app('fonts')->face(); while (true) { $engine->frame(function ($g) use ($face) { $g->clear(Color::rgb(0, 0, 32)); $g->strokeRect(1, 1, 238, 238, Color::rgb(255, 255, 255)); $g->scale(4)->text(date('H:i:s'), 6, 26, Color::rgb(255, 255, 0), $face); }); $display->present(); // the first frame whole, then only the time sleep(1); }
A chip the sketch built itself: app('displays')->attach($st7789, 'tft'). It
must have booted and answer formatSpec().
What present() sends
| Panel | Framebuffer framebuffer() picks |
Sent |
|---|---|---|
| ePaper (refreshes on command) | epaper |
the whole frame, then one refresh() |
| Region writes (OLED, TFT) | dirty |
the whole frame first, then the damage, snapped to what the panel addresses (8-row pages, byte columns) |
| Whole frames only (LED matrix) | full |
the whole frame |
config/embedded-displays.php defaults changes the kind per panel
behaviour. framebuffer('ring', frames: 3) and
framebuffer('paged', page_rows: 8) name one; a paged framebuffer is sent page
by page, the refresh after the last.
A rendering engine draws only what changed since its last frame, so a dirty framebuffer under it records only that, and the panel gets only that.
bind($framebuffer) shows a framebuffer made elsewhere, in any format: its
bytes are converted to the panel's when sent.
show() / hide() switch a panel that can. A panel call that throws stops the
display and posts one DisplayFaulted mail (display.faulted.<name>) to the
event loop; detach() and attach again to recover. close() switches the panel
off and leaves the chip and its bus to the sketch.
Piping from memory
direct: true builds a DirectEDisplay, which sends the same regions without
passing the pixels through PHP: for each region the chip opens a window, and its
bus reads the region's rows straight out of the framebuffer's ext-fb memory.
$tft = app('displays')->panel('st7796', direct: true); $engine = app('drawing')->renderer('velvet', ['output' => $tft]);
It needs a panel that implements gpio/contracts PipeablePanel on a bus that
writes from memory (today the ST7735, ST7789 and ST7796 on the Pi's spidev), and
an ext-fb framebuffer in the panel's own format with whole bytes a pixel.
Anything else is refused with the reason, never sent another way. On the Pi 5's
ST7796 at 10 MHz a whole frame takes 246 ms, the wire time, against 298 ms
through PHP.
GPU engines
Every display says its pixelSize() and pixelFormat(). A GPU engine's
framebuffer binds like any other and is sent as what changed, converted to the
panel's format. On a direct display, bind() gives it an ext-fb staging copy in
the panel's format; present() brings each region of the copy up to date, then
pipes the copy's memory. A panel whose pixels share bytes or planes, or whose
rows run bottom-up, is refused with the reason.
$tft = app('displays')->panel('st7796', direct: true); $engine = app('drawing')->renderer('vulkan', ['output' => $tft]); $engine->frame(fn ($g) => $g->clear(Color::rgb(0, 0, 0))->fillEllipse(160, 240, 80, 80, Color::rgb(255, 128, 0))); $tft->present();
Panels
dept-of-scrapyard-robotics/ssd1306, /st77xx (ST7735, ST7789, ST7796),
/ssd1680 (black/white, black/white/red), /jd79661, /spectra6, over
scrapyard-io/framework buses.