Search by

venusian-surface / embedded-displays

projectsaturnstudios

Surface embedded displays: an IC display panel as a drawing output.

Package info

github.com/venusian-surface/embedded-displays

Homepage

Issues

pkg:composer/venusian-surface/embedded-displays

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 0

v0.10.1 2026-10-15 16:16 UTC

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.