Skip to main content

C engine

er_scene.h is the API both flows end at. Flow A reaches it through the bridge; Flow B's runtime calls it directly; a firmware author calls a handful of it from the host loop. This page is a map of the header, not a replacement for it: the header's doc comments are the reference for each function's exact contract.

A host in miniature​

#include "er_scene.h"
#include "native_renderer.h"

embedded_renderer_set_backend(&my_backend); /* fill/copy/blend/wait/frame_ready */

ERNode* root = er_node_create(ER_NODE_VIEW);
ERProps p;
er_props_default(&p);
p.width = 240; p.height = 320; p.background_color = 0xFF07111F;
er_node_set_props(root, &p);
er_tree_set_root(root);

for (;;) {
embedded_renderer_touch(id, phase, x, y); /* from the panel driver */
er_commit(); /* layout + paint the dirty region */
my_backend_present(); /* flush it */
embedded_renderer_tick(dt_ms); /* advance the clock, run animations */
}

A Flow B firmware replaces the node building with erb_load(), which verifies a package and builds its tree, and adds erb_tick(dt) for the app's timers. A Flow A firmware replaces it with er_runtime, which builds the tree from JavaScript.

Nodes and the tree​

FunctionPurpose
er_node_create(type)Allocate a node from the fixed pool (ERUI_MAX_NODES). Types: view, text, image, scroll view, flat list, pressable, text input, activity indicator, switch, modal, vector (Svg), arc (Dial)
er_node_destroy(node)Return it to the pool, cancelling its animations
er_node_ref(node), er_node_deref(ref)A reference that stops resolving once the node is destroyed, even after its slot is reused. Hold one of these across app code, not an ERNode*
er_tree_set_root, er_tree_append_child, er_tree_insert_before, er_tree_remove_childThe tree
er_node_first_child, er_node_next_sibling, er_node_get_typeTraversal
er_node_in_use_count()Live nodes, for watching the pool
er_reset()Free every node and clear the root: a fresh scene
er_set_screen_bounds(w, h)Limit the scene to the panel, for a scene you don't trust to fit it. Flow B sets it from the panel it loads

Props​

ERProps is one flat struct covering every node type; er_props_default() initialises it and er_node_set_props(node, &props) applies it, marking the node dirty. Its fields follow the header's groups: layout (the Yoga properties, with _pct companions for percentage width, height and insets, plus display), view visuals (background, borders, radii, opacity), interaction (pointer_events, hit_slop_*, long_press_ms), text (text, font_size, font_weight, colour, alignment, ellipsis), image (name, resize mode, tint), transform (transform_translate_x/y, _scale_x/y, _rotate_z, _rotate_x/y, _perspective, _origin_x/y), shadow, and the per-widget groups for activity indicator, switch, text input, modal, arc and gradient.

Two calls set what a prop bag cannot hold: er_node_set_text_spans(node, spans, count) for per-run text styling, and er_node_set_vector_ops(node, ops, n_ops, paints, n_paints, grads, n_grads) for an Svg node's shape tape, with er_node_set_vector_dirty_rect to limit its next repaint to a sub-region.

Commit and damage​

er_commit() runs the layout pass and paints every dirty node through the backend. Afterwards, er_get_dirty_rect() returns the bounding box of what was repainted and er_get_dirty_rects(out, max) the up-to-16 disjoint rectangles, both reporting the last commit that actually painted, so a host can flush one transfer window per region. er_layout_pass_count() and er_text_measure_count() count work done, for tests and tuning.

A page-flipping display tells the engine how many buffers it rotates with er_set_display_buffer_count(n) (read back with er_get_display_buffer_count()) and reports each flip with er_display_present(); the engine then replays damage into every buffer so none is stale.

Events and input​

er_event_set(node, type, fn, user_data) registers a handler for one event type on a node: press, long press, press in/out, touch start/move/end/cancel, the responder lifecycle, layout, scroll, value change (Switch, Dial), text input events. er_responder_query_set(node, ...) registers the should-set predicate the engine asks during gesture negotiation. Touches arrive from the host through embedded_renderer_touch(id, phase, x, y); er_touch_active_count() reports fingers down. Moves are coalesced to the newest one per finger per frame; embedded_renderer_set_touch_coalescing(false) dispatches every sample instead, and embedded_renderer_flush_touch() / embedded_renderer_has_pending_touch() let a host dispatch them before its own commit. A physical keyboard feeds the focused text input through embedded_renderer_key(keycode, utf8). er_scroll_view_set_offset scrolls programmatically; er_text_input_focus/blur/get_text/ set_text drive a text input; er_keyboard_set_config swaps the on-screen keyboard; er_arc_get_value(node) reads a Dial's current value.

Animation​

The engine animates two things: a node property directly, and a standalone value bound to one or more properties.

  • er_animation_start(node, prop, to, &cfg) / er_animation_cancel(node, prop) animate a property in place.
  • er_animation_value_create() makes a standalone float; er_animation_value_bind(v, node, prop) and er_animation_value_bind_interpolated(v, node, prop, &map) attach it, and a color prop takes er_animation_value_bind_color(v, node, prop, &colors) only. er_animation_value_animate(v, to, &cfg) drives it, er_animation_value_set/get read and write it, er_animation_value_unbind_all and er_animation_unbind_prop detach, er_animation_value_destroy frees it. This is what an Animated.Value is.
  • er_animation_sequence, er_animation_parallel, er_animation_stagger group animations; er_animation_stop(handle) stops any of them. er_interpolate maps a float through a breakpoint table, and er_interpolate_color maps one onto colors.

ERAnimationConfig selects the algorithm (ER_ANIMATION_TIMING, ER_ANIMATION_SPRING, ER_ANIMATION_DECAY), the easing (ER_EASE_LINEAR through ER_EASE_ELASTIC_OUT, or ER_EASE_BEZIER with four control points), duration_ms and delay_ms, the spring's stiffness, damping, mass and velocity, decay's deceleration, loop and loop_reverse, and an on_complete callback.

Layout animations take their own ERLayoutAnimationConfig (timing or spring, a uint16_t duration_ms, easing and spring constants): er_layout_animation_configure_next(&cfg) arms it for the next commit, and ER_LAYOUT_ANIMATION_EASE_IN_EASE_OUT, ER_LAYOUT_ANIMATION_LINEAR and ER_LAYOUT_ANIMATION_SPRING are ready-made. er_layout_animation_has_pending() reports an armed config the next commit has not consumed yet.

Assets​

er_image_load(name, argb, w, h) and er_image_load_rgb565(name, rgb565, w, h) register an image under a name; er_font_register(family, font) registers one baked size of a font (a BitmapFont, which carries its pixel size). These reference the bytes in place, so flash-resident assets cost no RAM; er_font_load(name, buf, len) instead copies a font blob into the ERUI_FONT_POOL_BYTES pool. A Flow B package and a Flow A container register their packs on load.

Time​

The engine has one clock, advanced by the host: embedded_renderer_tick(dt_ms) moves it and runs due animations. er_now_ms() and er_now_ms64() read it; JavaScript's performance.now() and Date.now() are the 64-bit one.

The backend struct​

native_renderer.h declares EmbeddedRenderBackend: fill_rect, copy_rect, blend_rect, wait, frame_ready and a context pointer, plus the optional banded rendering (band_height, band_begin, band_flush) and format-aware copy (copy_rect_format) extensions. A multicore host also passes its render threads with embedded_renderer_set_workers() and calls er_render_worker_execute(k) on worker k when dispatched (only with ERUI_RENDER_WORKERS above 1). Engine and backends explains the contract and Writing a backend the conventions.

Instrumentation​

er_perf.h times each frame's layout and raster phases, keeps the worst frame seen with its full split, and counts repainted pixels and pool usage; perf_overlay.h draws it on the panel. The Performance guide shows how to read it.

The error screen​

er_error_screen.h draws one full-screen panel for any failure an app causes, in both flows: a title, the reason, the amounts involved, the source line, a hint, and a detail such as a stack trace. er_error_screen_show() copies the text, and while it is up er_commit() paints it with fills and the built-in font instead of the scene and drops touch input. er_error_screen_hide() gives the scene back. Flow A's er_runtime_show_error() and Flow B's erb_show_error() both use it.

A fault in native code can't draw when it happens. The firmware keeps an ERFaultRecord in RAM the boot leaves alone, fills it from its fault handler with er_fault_record(), and calls er_fault_show_recorded() at the next boot. The RP2040 example does this for a hard fault and a watchdog reset.

Building the engine​

cmake -S engine -B build -DBUILD_TESTING=ON
cmake --build build
ctest --test-dir build --output-on-failure

The engine is a CMake static library named embedded-react and needs only a C99 compiler and <math.h>. Its feature flags are listed in Memory.