Architecture Overview
Blinc is a high-performance UI framework built from the ground up for GPU-accelerated rendering without virtual DOM overhead. This chapter explains how the major systems work together.
Design Philosophy
Blinc follows several key principles:
- Fine-grained Reactivity - Signal-based state management with automatic dependency tracking eliminates the need for virtual DOM diffing
- Layout as Separate Concern - Tree structure is independent from visual properties, enabling visual-only updates without layout recomputation
- GPU-First Rendering - SDF shaders provide resolution-independent, smooth rendering with glass/blur effects
- Incremental Updates - Hash-based diffing with change categories minimizes recomputation
- Background Thread Animations - Animation scheduler runs independently from the UI thread
System Architecture
Each frame runs through five named phases. The phase numbering is load-bearing: code, traces, and runbooks all reference it.
┌─────────────────────────────────────────────────────────────┐
│ WindowedApp Event Loop (Platform abstraction) │
├─────────────────────────────────────────────────────────────┤
│ • Receives pointer, keyboard, lifecycle events │
│ • Routes through EventRouter -> StateMachines │
│ • Triggers reactive signal updates │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Phase 1: Diff │
├─────────────────────────────────────────────────────────────┤
│ • incremental_update() compares hashes │
│ • VisualOnly: apply prop updates │
│ • ChildrenChanged: rebuild subtree, invalidate cache │
│ • Most signal updates skip rebuild; they land as │
│ dynamic bindings the compositor patches in Phase 4 │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Phase 2: Layout (Taffy Flexbox) │
├─────────────────────────────────────────────────────────────┤
│ • compute_layout() on dirty nodes only │
│ • Stylesheet overrides applied before compute_layout │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Phase 3: Tick │
├─────────────────────────────────────────────────────────────┤
│ • Spring physics, CSS keyframes, CSS transitions │
│ • Scroll physics, motion bindings │
│ • Layout-prop animations re-run compute_layout │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Phase 4: Paint │
├─────────────────────────────────────────────────────────────┤
│ • try_fast_paint? (cache + no rebuild / layout / scroll) │
│ yes → apply_binding_deltas + apply_css_deltas │
│ render_static_layer_damaged (scissor + redraw) │
│ no → full walker → repopulate cache │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Phase 5: Composite & Present │
├─────────────────────────────────────────────────────────────┤
│ • Blit static cache → swapchain │
│ • Overlay pass: dynamic batch + motion subtree blits │
│ • Submit, present │
│ • request_redraw() if any visible animation mid-flight │
└─────────────────────────────────────────────────────────────┘
What makes this fast
After the first paint of a tree state, the static layer lives in a GPU-side texture cache. Most subsequent frames (hover, focus, CSS animation tick, spring tick) never re-walk the tree. The compositor patches a small set of primitive fields in place (translate, scale, opacity, colour, corner radius) and the GPU pass scissors a damage rect, re-dispatching only the SDF primitives that intersect.
Signal-bound props (bg(my_signal), w(my_signal)) go through the same
in-place patch path. A signal update doesn’t necessarily rebuild any
subtree; it can simply patch the GPU primitive’s relevant field.
A signal read inside a branched expression is the case that rebuilds the reading component’s subtree (not the whole UI). And off-screen content opted into viewport culling emits zero primitives, so it costs nothing.
The full re-walk only fires when the cache invalidates: structural rebuilds, layout-affecting changes, scroll, overlay transitions. See GPU Rendering for the cache, damage, and culling details.
Core Crates
| Crate | Purpose |
|---|---|
blinc_core | Reactive signals, FSM, core types, event system |
blinc_layout | Element builders, Taffy integration, diff system, stateful elements |
blinc_animation | Spring physics, keyframe timelines, animation scheduler |
blinc_gpu | wgpu renderer, SDF shaders, glass effects, text rendering |
blinc_text | Font loading, glyph shaping, text atlas |
blinc_app | WindowedApp, render context, platform integration |
Why No Virtual DOM?
Traditional frameworks (React, Vue) use a virtual DOM to diff the entire component tree on every state change. This has overhead:
- Creating VDOM objects for every render
- Diffing the full tree to find changes
- Patching the real DOM with changes
Blinc avoids this with:
- Fine-grained signals - Only dependent code re-runs when state changes
- Stateful elements - UI state managed at the element level, not rebuilt from scratch
- Hash-based diffing - Quick equality checks without deep comparison
- Change categories - Visual vs layout vs structural changes handled differently
The result: updates proportional to what changed, not to tree size.
Chapter Contents
- GPU Rendering - SDF primitives, glass effects, text rendering
- Reactive State - Signal system, dependency tracking, effects
- Layout & Diff - Taffy integration, incremental updates
- Animation - Spring physics, timelines, scheduler
- Stateful Elements - FSM-driven interactive widgets