Real CSS, compiled to a native layout engine
You write flexbox, transforms, @keyframes and media queries — real CSS. GEA compiles it into a tiny native engine that lays out and paints every frame on the device. No browser, no interpreter. Here is how it works, and what it costs.
A stylesheet usually implies a browser behind it — a parser, a cascade, a layout engine, a rasteriser, megabytes of machinery. None of that fits on a chip with half a megabyte of RAM. So the usual answer is to give up CSS and hand-place pixels in C. GEA keeps the CSS and drops the browser instead.
The layout engine is a slice of CSS written from scratch in C++: flexbox, a small grid, 3D transforms, keyframe animation and media queries, laid out and painted natively every frame. The slice is chosen for impact, not completeness — and the whole of it is about six thousand lines.
CSS compiles at build time
Your CSS is read at build time and turned into native registration calls that run once at boot. After that there is no stylesheet text on the device and nothing to re-parse. A rule is typed data, matched against the live element tree.
.cube-title {
display: flex;
flex-direction: column;
align-items: center;
gap: 5px;
}
auto& sheet = StyleSheet::instance();
sheet.registerRule("cube-title", "display", "flex");
sheet.registerRule("cube-title", "flex-direction", "column");
sheet.registerRule("cube-title", "align-items", "center");
sheet.registerRule("cube-title", "gap", "5px");
Colours never travel as strings into a frame. A literal is quantised once, at build time, to the
panel's pixel format — on a 16-bit RGB565 display, #12130f becomes a single
constant the rasteriser writes straight to the framebuffer.
.cube-app { background-color: #12130f; }
// quantised once, at boot, to the panel's RGB565
constexpr native_t bg = rgb565FromRgb888(0x12, 0x13, 0x0f); // 0x1081
Layout, from scratch
Layout is a reimplementation of flexbox: flex-direction, wrap,
justify-content, align-items, gap, flex-grow and
a real box model. Next to it sit a small grid (auto-placement, up to eight tracks per axis), block
flow, and absolute/relative positioning with z-index. The
whole layout pass is about 1,300 lines.
Sizes resolve against the live panel: px, %, vw,
vh, vmin, clamp(), min()/max() and
var() all work, and custom properties are dependency-tracked — change
--accent and only the nodes that read it recompute, not the whole tree. Media queries
are evaluated at runtime against the real panel, so one stylesheet reflows across a 410×502 AMOLED,
a 480×480 dial and an e-paper panel without a rebuild.
Transforms and animation
The 3D transforms are real. Each node keeps rotate, translate and scale components with a
perspective and back-face culling, projected per-point every frame. That is what makes the demo cube
an actual cube, not a sprite. @keyframes run on-device through a runtime animation
engine that decomposes a transform into per-component tracks and eases them —
cubic-bezier() and steps() included — applying the result each tick.
@keyframes cube-spin {
0% { transform: rotateX(-18deg) rotateY(24deg) rotateZ(0deg); }
100% { transform: rotateX(342deg) rotateY(384deg) rotateZ(0deg); }
}
sheet.registerKeyframeRule("cube-spin", 0, "transform", "rotateX(-18deg) rotateY(24deg) rotateZ(0deg)");
sheet.registerKeyframeRule("cube-spin", 1000, "transform", "rotateX(342deg) rotateY(384deg) rotateZ(0deg)");
A compact data model
The engine is small because the data model is. Each node carries one typed
ComputedStyle struct — no hash map, no per-property allocation. The hot struct is
136 bytes. Cold properties like transforms, gradients and shadows move into a pooled
RareStyle, so a plain node pays a two-byte handle and nothing more. It's the same
hot/cold split Blink and WebKit use, at a fraction of the size.
RareStyle behind a 2-byte handle. Across one screen, retained tree state fell from 803 KB to 159 KB.Rendering is compute-bound
Painting goes through a retained display list and dirty rectangles. Only the commands that intersect a changed region are replayed, in paint order, into an RGB565 framebuffer that is then DMA'd to the panel in chunks. The honest bottleneck is the CPU, not the bus. Most of a frame is spent rasterising fills, gradients, anti-aliased shapes and text — not moving bytes.
Write the CSS you already know, and GEA runs it natively on the device — the same stylesheet, from a microcontroller to a Mac.
What works, and what doesn't
The boundary is deliberate, and worth stating plainly. What compiles and runs today:
- Flexbox, a small grid, block flow, absolute/relative positioning
- 3D transforms,
@keyframeswithcubic-bezier()/steps()easing - Media queries against the live panel,
vw/vh/vmin/clamp(), dependency-trackedvar() - border-radius and ellipses,
filter: blur(), linear and radial gradients, shadows - Anti-aliased text from fonts rasterised at build time — no TTF parser on the device
And what it deliberately leaves out:
- Authored CSS
transition:— use@keyframesor the runtime animation engine instead - Full CSS Grid placement,
position: fixed/sticky,em/remunits backdrop-filter, scroll-snap, real stacking contexts, forms, tables and SVG
CSS, compiled to native
The same engine lays out the examples on this site for a microcontroller, embedded Linux, macOS and iOS — from one stylesheet. The code is the best place to see it for yourself.