2025 · Microcontroller · Espressif · M5Stack

Espressif ESP32-P4

Rendered by damage: an RGB565 raster path and a PPA hardware backend at 1280×720. QuickJS in 304 KB.

Renderer integrationNative rendererhosts/esp32p4/ ↗

The ESP32-P4 is Espressif's high-performance MCU: two RISC-V cores at up to 400 MHz, a 40 MHz low-power core, 768 KB of on-chip SRAM, a Pixel Processing Accelerator, 2D-DMA, MIPI-DSI and CSI, and an H.264 encoder — Wi-Fi comes from a companion chip. M5Stack's Tab5 puts it behind a 5-inch 1280×720 IPS touchscreen with 32 MB of PSRAM and an ESP32-C6 for Wi-Fi 6.

PocketJS meets it twice. engine/backends/esp32p4-ppa is a no_std Rust renderer that interprets DrawLists, batches A8 coverage and hands FILL, BLEND and SRM transactions to the PPA with an ordered RGB565 software fallback — contributed by @HalfSweet and hardware-verified on a Tab5 at 1280×720. And Pocket Pi runs the complete Pi coding agent on the chip: QuickJS in a 304 KB profile, with files, schedules and hot-pluggable apps on board.

Processor
RISC-V ×2 · 400 MHz
Memory
768 KB + 32 MB PSRAM
Display
1280 × 720 (Tab5)
Close-up of an Espressif ESP32-P4 SoC on a small development board
Close-up of an Espressif ESP32-P4 SoC on a small development board. Photo: Pathfinbird · CC BY-SA 4.0 · The SoC on a development board; no M5Stack Tab5 photo is available under a free licence.
01

Hardware

curated here · sources below

ESP32-P4 SoC

CPU
Dual-core 32-bit RISC-V HP system, up to 400 MHz (360 MHz on the Tab5's ESP32-P4NRW32) plus a single-core 40 MHz LP RISC-V
SRAM
768 KB on-chip L2 + 8 KB TCM; 32 MB octal PSRAM in-package on the Tab5
Graphics
Pixel Processing Accelerator (fill, blend, scale-rotate-mirror), 2D-DMA, MIPI-DSI 2-lane, MIPI-CSI, H.264 encoder
Wireless
None on-chip; Tab5 pairs an ESP32-C6-MINI-1U for Wi-Fi 6 / BLE / Thread

M5Stack Tab5 (reference board)

Display
5″ IPS TFT, 1280 × 720, MIPI-DSI, capacitive touch (GT911 / ST7123 / ST7121)
Memory
16 MB flash, 32 MB PSRAM, microSD
Camera
SC2356 2 MP over MIPI-CSI
Audio
ES8388 codec, ES7210 AEC with dual microphones, 1 W speaker, 3.5 mm
I/O
USB-C OTG, USB-A host, RS-485, Grove, M5-Bus; BMI270 IMU; RX8130CE RTC
Power
NP-F550 7.4 V 2000 mAh (Kit); about 6 hours
Body
128 × 80 × 12 mm, 118 g; released 9 May 2025
02

PocketJS on this machine

Native renderer

What runs

hosts/esp32p4/components/pocketjs_ppa is the reusable ESP-IDF half: it registers one client each for FILL, BLEND and SRM and executes blocking transactions, while the product BSP owns display initialisation, presentation buffers and vblank scheduling. The Rust crate is enabled with its esp-idf feature and driven by one EspIdfPpaOps on the rendering task. The adapter is build-tested with ESP-IDF release/v6.0 and v6.1; it is a backend, not a stock application target.

What is proven

Reusable RGB565/PPA backend with a strip-equals-full parity suite and an ESP-IDF component smoke build; hardware-verified on an M5Stack Tab5 at 1280×720. Pocket Pi ships the QuickJS guest on the chip in a separate repository.

Record

  1. v0.8.0

    ESP32-P4, rendered by damage: RGB565 raster path and hybrid PPA backend, hardware-verified on an M5Stack Tab5 at 1280×720.

  2. Pocket Pi ports the Pi coding agent to the ESP32-P4 — QuickJS in a 304 KB profile.

  3. Hot-pluggable .pocketapp packages arrive over the network or UART on a running Pocket Pi.

Profile · as declared by the demo manifestsource ↗
Profile
esp32p4 (renderer integration)

Acquisition reports

04

Bring-up guide

upstream documents · rendered verbatim

Toolchain, build, deploy and acceptance are owned by pocket-stack/pocketjs. The documents below are rendered from the pinned checkout without edits; relative links point back into the repository at the same revision.

Render backends

7 min read · 1,618 words

Where the PPA path sits among the portable backends.

rendered verbatim fromdocs/BACKENDS.md@ 2d20ddaraw ↗

The core's output is one contract: the DrawList, a flat Vec<u32> of draw ops pinned in contracts/spec/spec.ts ("DRAWLIST op format"). Everything above it — JSX + Tailwind, the Solid/Vue Vapor/Octane renderers, the frame transaction (docs/DETERMINISM.md), the animation engine — is identical on every backend. Backends differ in how the DrawList becomes pixels, and in exactly one capability: who measures and shapes text.

The portable backend

The portable backend is the baked-text pipeline every fixed-function host shares: pocket-ui-wgpu on the desktop, the core software rasterizer (engine/core/src/raster.rs) behind the wasm, Apple, PocketBook and sim hosts, the PSP GE walker, the ESP32-P4 PPA and Symbian GLES2 ports.

  • Text is baked at compile time. framework/compiler/bake-font.ts rasterizes the app's collected codepoints into FONT ATLAS v3 blobs; the core measures runs from the atlas advance tables (engine/core/src/text.rs::measure_run) and emits GLYPH_RUN ops — glyph ids and cell positions, nothing else.
  • Pixels are byte-deterministic. Same bundle + pak + input tape produce the identical framebuffer on every portable host at the same density — that is what tests/golden.ts, the PSP/Vita emulator goldens and tools/tape.ts session hashes pin.
  • The capability id is text.glyphs.baked; hosts that extend atlases at runtime (system-font rasterization + loadFontAtlas reload, note-widget's cjk.rs) add text.glyphs.runtime.

The gpui backend

engine/backends/gpui (pocket-ui-gpui) paints the same DrawList through gpui — Zed's native GPU renderer — and is embedded by hosts/desktop, the stock host of the macos-app and linux-app targets. Boxes, gradients and images become antialiased vector quads instead of upscaled raster; text can come from the host text system.

  • Text layout is a host capability, opted into per app. An app that enhances: ["text.layout.native"] gets a core text measurer installed before the guest mounts (Ui::set_text_measure): taffy leaf sizes, the measureText op and painted glyphs all come from one provider — CoreText through gpui's platform text system. Codepoint coverage is the OS font fallback chain (CJK, emoji, everything), with no runtime atlas baking and no tofu.
  • The op is TEXT_RUN (9). With a measurer installed, translation-only tracking-0 runs pack the run string's UTF-8 bytes INTO the word stream (8 header words + payload) — the DrawList stays the complete Vec<u32> pixel truth, so snapshots, demand-render hashes and damage word-diffs are exact by construction. The provider is chosen once per node at layout build and recorded (Node::text_native); layout and paint gate on ONE shared predicate (Resolved::declares_transform), and when a paint-only transform changes the answer, Ui::draw relayouts and repaints before returning — every frame that leaves draw() is provider-correct, in both directions, with no oscillation on canceling transforms. Tracked, scaled and rotated runs keep the baked GLYPH_RUN pair — measurement and glyphs always come from the same provider per node.
  • Monospace is a slot family. font-mono resolves to dedicated slots (16..18; framework/compiler/tailwind.ts MONO_FONT_PX) baked from JetBrains Mono on the portable side and mapped to the same family through the host text system on gpui — the note's code blocks are monospace on every backend.
  • Prefix additivity is preserved. Ligatures, contextual alternates and kerning are disabled in the native shaping configuration (engine/backends/gpui/src/fonts.rs), because app editor math measures caret positions as prefix widths through measureText (apps/note/layout.ts, apps/im/wrap.ts) and prefix sums only equal shaped positions when advances are additive.
  • Soft-wrap breaks are a host op. wrapText (spec op 43) returns the break columns for one line under a pixel width. The core computes greedy word wrap over the slot's measure provider; a native-text app gets gpui's own LineWrapper instead (native_wrap, installed next to the measurer through the same TextConfig — Zed's editor WrapMap consumes the same machinery). The wrapped COORDINATE SPACE — visual rows, caret/selection mapping and hit testing — stays app-side: the op is only the "where does this line break" half, matching the platform/editor split Zed uses.
  • Two ops keep a pixel-exact escape hatch. Gouraud TRI and TEX_TRI batches (rotated gradients and images, 3D subtrees) have no gpui vector equivalent, so consecutive batches raster through pocketjs_core::raster into cached local images at the target density — the portable rasterizer used as a sub-backend.

What the gpui backend guarantees, and what it does not

The frame transaction is unchanged: the host ticks the guest at the fixed declared rate (one guest.frame() + one surface.tick() per virtual tick, never from a paint callback), rendering is a pure function of the DrawList, and paints are demand-armed off the DrawList content hash — the pocket-widget governor discipline. state[n] is exactly as deterministic as on every other host.

Pixels are not byte-comparable across hosts in native-text mode: glyph rasterization, metrics and fallback fonts belong to the OS. That is the "different guarantee gets a different id" rule — text.layout.native instead of text.glyphs.baked — and why gpui-hosted apps verify like the note does (pure-math unit tests over an injected measurer, sim traces, --proof acceptance runs) instead of joining tests/golden-specs.ts.

Native desktop targets

contracts/spec/platforms.ts registers macos-app and linux-app at hostAbi 4 with form: "window", a dynamic viewport and acceptsFixed. Both profiles use the same generic host. macOS resolves density 2; Linux resolves density 1. Fixed-viewport console apps run size-locked and letterboxed with their baked glyph pipeline intact.

bun run macos note        # dynamic viewport, native text, svc editor protocol
bun run macos hero        # fixed 480x272, size-locked, baked glyphs
bun run macos note --proof

tools/macos.ts resolves the manifest against macos-app, writes the plan, builds the bundle + pak, and derives the capability-shaped host flags (--fixed, --native-text, --companions) from the resolved plan. If the selected app directory also contains pocket.system.json, the tool resolves every installed package and starts the host with one complete ResolvedSystemPlan; it does not project child plans into command-line viewport or title fields. --editor is NOT a capability: it enables the note's companion svc adapter (an app protocol — the profile deliberately registers no pointer/text/IME/clipboard ids, see contracts/spec/platforms.ts). On exit the host prints its governor receipt (pocket-desktop-host: N ticks, M frames rendered); a settled app shows M ≪ N.

Browser System host

web-app runs every package in an independent same-origin iframe JavaScript Realm with its own wasm Ui. hosts/web/system-engine.js derives the package catalog, surface handles and lifecycle policy from one ResolvedSystemPlan. Live shell bindings create and remove AppInstances; focus and visible painter order determine scheduling.

The wasm software compositor retains each visible child framebuffer and replaces SURFACE_QUAD with a texture quad only during the host render. Full bounds determine the child coordinate origin, clip bounds constrain the visible pixels, and the instruction remains at its original DrawList offset. Shell chrome emitted after the surface therefore stays above the child. A missing child raster leaves the shell's loading fallback visible.

System UI companion input

The host speaks the system-ui svc dialect when the resolved System UI plan declares that companion. The protocol extends the note dialect's input lines with right-button mouse lines (b:2), alt/ctl key modifiers, F1–F12, cmd-flagged ⌘ chords, a boot epoch in the hello, a {t:"cursor"} guest intent that sets the window's pointer shape, and a {t:"paste-req"} guest intent the host answers with a paste line (menu-driven Paste). ⌘Q quits and ⌘V pastes host-side; every other ⌘ chord reaches the guest, so the compositor owns its shortcuts (⌘W close, ⌘M minimize, ⌘cycle windows, ⌘Esc Start menu, ⌘A/C/X editing). Plain typing arrives only through the IME input handler (insertText:→ onech` line per keypress). This companion carries shell UI input, clipboard requests and cursor intents. It does not carry package lifecycle, focus, per-frame visibility or button routing.

The themeable Pocket Desktop product is maintained separately and consumes these contracts as an external Pocket System. Its manifest owns the app catalog, installation snapshot, System UI role and background-execution policy. Every installed entry reaches the native host as a complete ResolvedBuildPlan; ordinary applications resolve without the System UI-only compositor capability.

  • hosts/desktop implements a generic AppSupervisor; the System contract does not expose that implementation. The host contains no product catalog or package-name rules. Live <CompositorSurface package> bindings create one AppInstance with its own Guest, QuickJS Runtime, QuickJS Context, UiSurface and GpuiRenderer inside the existing process. Their globals, node trees, textures, clocks and button state are isolated.
  • Compositor surfaces use SURFACE_QUAD, not TEX_QUAD. The instruction carries the package-surface handle, unclipped bounds, clipped visible bounds and focused state. GPUI invokes the native compositor at that exact DrawList position, so shell content before and after it keeps its painter order and clipping never changes the child coordinate origin.
  • AppInstance lifecycle and scheduling come from the shell core's live surface bindings. Destroying a binding removes its instance. Hidden instances become Suspended under backgroundExecution: "suspend"; "continue" keeps them Running. This policy does not govern memory residency. Focused visible instances run first, and hardware-neutral buttons go only to the top focused surface.
  • A child exception marks only that AppInstance as Failed. The shell and sibling instances continue; the host records the package failure without a companion hot path.

Scripted acceptance drives the same dialect from flags: --mouse X,Y[,d|u|r]@TICK (drags, right clicks), --key [cmd+][alt+][ctl+]NAME@TICK, --type TEXT@TICK.

Choosing a backend

portablegpui
hostsPSP, Vita, PocketBook, ESP32-P4, Symbian, web, sim, macOS widgetmacOS and Linux (hosts/desktop)
text measurementcore, atlas advance tablesgpui platform text system, per-app opt-in
codepoint coveragebaked charset (+ runtime extension)OS fallback chain, color emoji
pixel determinismbyte-exact across hostsper-host; transactions still deterministic
pixel goldenstests/golden-specs.ts, tape hashesopted out (note-style verification)
rotated/3D contentnativeportable rasterizer as a local sub-backend

The desktop benchmark against Tauri and Electron (harness, comparison apps, results) lives in its own stacked PR — pocket-stack/pocketjs#294.

Not in this build's glob list: hosts/esp32p4/README.md, engine/backends/esp32p4-ppa/README.md, hosts/esp32p4/examples/data-smoke/README.md

Photo

Wikimedia Commons · Espressif ESP32-P4.jpg — Pathfinbird, CC BY-SA 4.0. The SoC on a development board; no M5Stack Tab5 photo is available under a free licence.