Skip to main content

Roadmap

The bulk of Embedded React is built and verified — see the status table in the introduction. This file is the single home for what's left: known issues, planned work toward 1.0, the performance backlog, and the longer-term vision.

It is intentionally honest. If something here looks unfinished, it is — the engine, both flows, the backends that run on hardware, and the simulators all work today, but this is a beta. Flow B is being rebuilt around loadable packages (see Flow B); everything else is fixes and feature additions.


Known issues & caveats​

  • AOT animation toValue uses a snapshot, not live state. A Flow B (Animated) animation whose toValue depends on current state may animate toward the value captured at compile/setup time rather than the live one. Tracked for the imperative / animation hardening pass.
  • Simulator asset-pack reload leaks buffers. The engine has no asset unregister, so each hot-reload of an image/font keeps the previous pack's buffers alive (a few KB per changed asset). Harmless for a dev session; never happens on-device.
  • Browser host re-creates its HEAPU8 view every frame. WebAssembly memory growth detaches typed-array views, so the WASM simulator's present layer re-derives the view on each present. This is a workaround, in place and working — noted so it isn't "simplified" away.
  • Text is re-measured whenever a commit runs layout. The layout-dirty flag gates the layout pass, but any commit that does run layout re-measures every <Text> node, even if the change was unrelated. This is the top remaining per-frame layout cost (see the performance backlog).
  • Static RAM footprint is desktop-sized by default. The opacity/transform/shadow scratch buffers default to generous dims. Opacity now composites through band strips (ERUI_SCRATCH_BAND_HEIGHT) — shrink the strip height for a large RAM saving with no size cap on fades — and the transform source has its own dims (ERUI_TRANSFORM_WIDTH and ERUI_TRANSFORM_HEIGHT). Tune these plus ERUI_MAX_OPACITY_DEPTH and shadows/transforms per board — the defaults are not board defaults.
  • Performance ceiling on JS-driven drag (Flow A). A drag whose handler runs in JS still costs a React render per frame — around ~24 fps on PSRAM-QuickJS hardware, bounded by per-event dispatch rather than rendering. The common cases are now covered: touch-move coalescing collapses a frame's samples into one dispatch, and <Dial> drags natively without entering JS at all (the thermostat dial measures ~47 fps on an ESP32-S3). Anything still driven from onTouchMove keeps the old ceiling; the AOT flow avoids it entirely. Native-driver animations were never affected.

Near-term (beta → 1.0)​

Backends​

The backends that run on hardware (sdl, esp32-lcd, esp32-spi-lcd, pico-spi-lcd, dma2d) and power the simulators (software, web) are done. The remaining backends are stubs / README-only:

  • backends/framebuffer/ — Linux /dev/fb0 direct framebuffer (embedded Linux SBCs).
  • backends/opengl/ — OpenGL ES 2.0 (Raspberry Pi, Android).

Examples / board bring-up​

Five examples run end-to-end (see the board guides). These are README-only and need wiring:

  • examples/stm32h7/ — first STM32 bring-up. The backend (backends/dma2d/) is done; what is missing is a board project that wires it to an LTDC panel end to end.
  • examples/raspberry-pi/ — RPi reference app (with backends/framebuffer/ or opengl).
  • examples/dashboard-demo/, examples/marine-display/ — larger reference apps.

Flow A — React on QuickJS (runtime)​

  • Wireless hot reload. On-device hot reload ships over USB serial; a Wi-Fi transport would give the same edit-save-see loop without a cable.
  • Runtime asset registration. A NativeUI.loadImage / loadFont path for dynamically generated or OTA-delivered assets. er_font_load (blob → pool) already exists for this future case; the JS-facing API does not.
  • SDL keyboard → input. Mouse is forwarded as touch; hardware keyboard feed into the engine is still TODO (needed for TextInput on the desktop host).

Flow B — compiled, loadable packages​

The compiler produces a package in the same .erpkg container Flow A uses, and a fixed Flow B runtime in the firmware checks and runs it. There is no JS engine and no heap, and changing the UI needs no firmware rebuild. Tracked under the flow-b label.

  • One firmware for every panel. A package holds a layout for each of the app's breakpoints, and declares no sizes. The firmware passes the panel size at boot and the package picks its layout. A layout that doesn't compile fails the build.
  • Fixed memory. The firmware reserves one block of RAM, and the package states exactly how much it needs. The program, text, vector data, and assets are read in place from flash. A package that does not fit shows the error screen instead of loading.
  • Nothing in a package can crash the device. The runtime checks the whole package before it runs: every offset and index in range, every jump valid, every native import matched by signature. While running, node references are checked handles, handlers have an instruction budget, and every fixed limit reports overflow. Any failure stops the app and shows a red error screen with the reason and the source location.
  • On-device hot reload over the Flow A reload transport. The new package is checked in a staging slot before the swap, and the last good package is kept for rollback.
  • Parity with Flow A through bounded, native versions of what Flow B lacks: full list and string operations, structs, useReducer, useContext, custom hooks, and Button / ImageBackground / SectionList.
  • Native modules for work outside the UI (network, sensors, storage). A typed TypeScript interface is implemented in C by the firmware, and the same interface works in Flow A.

Order: restructure the compiler (split aot/compile.mts, then add a typed intermediate representation), then build the runtime and the package output, then multi-panel packages and hot reload, then parity. Boards with no spare flash partition link the package into the firmware instead.

Still to do:

  • Resolve the live-vs-snapshot toValue gap (see Known issues).
  • Imperative refs / Animated sequence / parallel / loop / interpolate hardening on the no-PSRAM hardware path.
  • Thermostat capstone under a frame-time gate; ping-pong band buffers for banded RGB565.

Engine features​

  • Canvas API (canvas_bindings.c) — deliberately a stub; land only if the bundled React surface needs it.
  • Ellipsize modes — tail and clip ship; head and middle are deferred.
  • textAlign: justify — left/center/right ship; justified text deferred.
  • Per-container layout-dirty. The dirty flag is whole-tree and conservative (er_node_set_props raises it even for a pure visual change). Next: compare layout-relevant fields and re-solve only the changed subtrees.

Open API decisions​

  • Gradient style key. Gradients are an engine primitive with no React Native prop; the key is backgroundGradient (#108). Still open: its shape, {colors, angle?, positions?} or the CSS-modelled {type, angle?, to?, stops} recorded on #108.
  • Non-native-driver animation. Decide whether to support Animated without useNativeDriver (needs a JS-side rAF loop calling setValue each frame) or require useNativeDriver: true.
  • Controlled TextInput value. placeholder / placeholderTextColor / editable / cursorColor are done; a controlled value still routes through er_text_input_set_text, not ERProps.
  • Modal transparent prop. No ERProps field yet — handled via backdrop alpha at the JS layer; formalize or record as a gap.

Tooling / simulators​

  • Prebuilt / auto-built SDL simulator binary. Today npm run sim needs a one-time CMake build of the sim binary. A scaffold + prebuilt/auto-built binary would remove the step. (The WASM simulator already ships prebuilt.)
  • Fast Refresh. The SDL/WASM simulators preserve useState across reloads via a Babel transform; full per-component Fast Refresh (no keying fragility) is a later upgrade.
  • Workspace packages. The JS layer incubates in-repo at packages/embedded-react. Once the public surface stabilizes, extract it into published workspace packages.

Done: embedded-react build now produces the device artifact from a consumer project — app.erpkg (Flow A, bytecode compiled through the prebuilt .wasm, no native toolchain) and --aot → a Flow B app.erpkg.


Performance backlog (engine)​

Catalogued optimizations — correctness is fine, these are speed/RAM wins. Roughly highest-impact first.

  • Cache text measurement on the node; invalidate only when text/font/size/spacing props change (top remaining layout cost).
  • Skip clean subtrees in the render walk. Track a subtree_dirty bit and cache sorted child order instead of descending the whole tree every frame.
  • Cache blurred shadow buffers keyed on geometry (shadows are fully re-blurred every repaint).
  • Gradient color LUT. Precompute a 256-entry premultiplied LUT from the stops instead of evaluating per pixel, and rasterize only the rows and columns inside the clip (render_linear() evaluates the whole box even for a small damage rect).
  • Cache / DDA-step scaled images instead of re-sampling (per-pixel division) every repaint.
  • Resolve BitmapFont* once when text props are set and cache on the node (currently a strncmp registry scan per text op).
  • Batch anti-aliased corner and 1-bit glyph blits into a per-row buffer, one copy/blend per row, instead of per-pixel / per-span.
  • Draw only the border ring of bordered rounded rects with an opaque background too; today only a transparent or translucent background gets the ring, and an opaque one is overdrawn.
  • Clamp software-composite loops once per row instead of a per-pixel bounds check.
  • Step transform source coordinates incrementally (and consider fixed-point) instead of a float matrix-multiply + bilinear sample per output pixel.
  • memset only the live w×h scratch region, not the whole slot.
  • Right-size recursive walker stack arrays (today each recursion level allocates uint16_t[ERUI_MAX_NODES]); walk the sibling list in place or share scratch by depth.
  • er_tree_append_child is O(n) per append → O(n²) to build a sibling list; keep a last_child pointer and prev_sibling/parent back-pointers.
  • Skip the zIndex insertion-sort when all children have zIndex == 0, or cache the sorted order.
  • Bound the input-tick scan (currently scans all ERUI_MAX_NODES slots twice for momentum) with a small active-scroller list.
  • Reduce static scratch RAM: size slots to the largest composited node, tile large transformed/opacity subtrees, or share one arena across transform/opacity/shadow.
  • Scroll by moving painted pixels (deferred, #292). Every scroll step repaints the whole viewport. Moving the painted viewport by the delta and repainting only the exposed strip needs a backend "move rect" operation and a full framebuffer, so it would be opt-in for backends that have both (ESP32-S3 RGB, STM32 LTDC/DMA2D).

Vision / longer-term​

The engine is runtime-agnostic by construction — er_scene.h is a pure C ABI, and nothing in engine/ knows about React. That layering keeps Flow A and Flow B honest against one C surface and leaves the door open for other frontends without forking the engine. None of these are committed work; they're why the engine stays neutral:

  • Lua UI — drive the scene graph from Lua tables / a Lua DSL.
  • JSON UI — a serialized React-tree format loaded at runtime.
  • Visual editor runtime — a desktop GUI designer that emits that JSON format.
  • Other scripting APIs — language-agnostic C bindings other runtimes can wrap.

This does not change the project's identity: Embedded React is React Native for embedded MCUs. React is the supported developer path; the engine's neutrality is an implementation choice.


Non-goals​

Deliberately out of scope:

  • MCU SDK headers (stm32h7xx_hal.h, esp_lcd.h, …) anywhere in engine/. Hardware specifics live in backends/.
  • Flash tooling, OpenOCD scripts, deploy automation — firmware tooling, not engine concerns.
  • RTOS setup / task creation — the caller's responsibility (bare-metal, FreeRTOS, Zephyr all work).
  • Display initialization (SPI, MIPI DSI, LTDC) — the backend's responsibility.
  • Network / fetch — provided by the firmware if needed.
  • Web-only React APIs — DOM refs, portals, Suspense, Server Components.
  • Host-only optimizations — SIMD paths (AVX2, NEON on desktop) in the software backend. On x86 and desktop it is the simulator, not a target; optimized loops belong in the backend of the board that runs them.
  • List virtualization — FlatList windowing, or a native recycling list node. FlatList is a documented ScrollView alias in both flows: every row mounts and stays mounted (see FlatList). Windowing in JS would cost a React commit every time the window shifts — during a flick, every frame — the same per-event cost that caps Flow A drag, and Flow B has no runtime reconciler to recompute a window at all. A native recycling list node is the only version worth building, and it is not worth it here: these panels show a few dozen rows, not thousands. Paginate, or window by hand against a scroll offset you hold in state.
  • Pitching as a general-purpose multi-runtime UI engine. Non-React frontends could live in bridges/ later, but React is the supported path.