Rendering Pipeline
TUIX renders terminal UI through a multi-stage pipeline: geometry resolution, content building, compositing, and delta rendering. All stages run in C for maximum throughput.
Pipeline Stages
- Geometry Resolution — multiply proportional modifiers by terminal dimensions
- Content Building — each widget's build_content produces a pixel buffer
- Compositing — layer all scene buffers onto the final framebuffer (painter's order)
- Delta Rendering — hash rows, skip unchanged, emit only modified ANSI output
Geometry Resolution
Before building content, the geometry resolver scales each widget's float modifiers to absolute coordinates based on the current terminal size. In v0.3, buffers with a valid parent_uid are resolved relative to their parent buffer, enabling parent/child layouts.
buffer.width = terminal_width × width_mod
buffer.height = terminal_height × height_mod
buffer.margin_left = terminal_width × margin_left_mod
buffer.margin_top = terminal_height × margin_top_modIf a widget's builder defines an on_resize callback, it is called after geometry resolution with the new width and height.
Pixel Model
Every cell in a buffer is a TuixPixel containing: an 8-byte UTF-8 symbol, foreground and background RGB colors, style flags (bold, italic, underline, dim), and quantization cache fields.
| Field | Size | Description |
|---|---|---|
| sym | 8 bytes | UTF-8 character (padded with null bytes) |
| styles.fg | 3 bytes | Foreground RGB (0–255 per channel) |
| styles.bg | 3 bytes | Background RGB (0–255 per channel) |
| styles.bold | 1 byte | Bold text flag |
| styles.italic | 1 byte | Italic text flag |
| styles.underlined | 1 byte | Underline text flag |
| styles.dim | 1 byte | Dim text flag |
| q_fg, q_bg | 6 bytes | Quantized color cache (RGB565) |
| q_cached | 1 byte | Whether quantized cache is valid |
Compositing
The compositor allocates a final framebuffer sized to terminal_width × (terminal_height - 1). The bottom row is reserved for log output. In v0.3, compositing uses traversal-chain layering: root buffers are ordered by z-index and each parent/child subtree is traversed before painting, preserving hierarchical layering.
Delta Rendering
The renderer compares the current frame against the previous frame to skip unchanged content:
- Hash each row using FNV-1a on the raw (unquantized) pixel data
- Compare against the previous frame's row hashes
- Only process rows with changed hashes
- For changed rows, quantize colors (RGB → RGB565 via a 128KB precomputed LUT)
- Group consecutive pixels with identical styles into a single SGR escape sequence
- Emit cursor positioning (\e[Y;XH) only when needed (skip for sequential cells)
- Stream output in 256KB chunks
Color Output Modes
The color quantization LUT maps RGB565 values to the closest available terminal color. Three output modes are used automatically based on the best match:
| Mode | Format | Colors |
|---|---|---|
| ANSI16 | \e[30-37m / \e[90-97m | 16 basic terminal colors |
| ANSI256 | \e[38;5;Nm | 256-color palette |
| Truecolor | \e[38;2;R;G;Bm | Full 24-bit RGB (16.7M colors) |
Halfblock Rendering (Optional Module)
The source distribution includes a halfblock renderer (rendering_halfblocks.c) that uses the Unicode upper half block character (▀ U+2580) to achieve 2× vertical resolution. Two pixel rows are packed into one terminal row by using the foreground color for the top half and the background color for the bottom half.
Scene Instrumentation and Compaction
v0.3 adds scene activity instrumentation and conservative compaction APIs. Instead of eager global caching, the recommended strategy is data-first: read scene stats, then compact cold scenes when thresholds are met.
from tuix.core import scenes
stats = scenes.get_scene_stats(b"main")
freed = scenes.compact_scene_pixels(b"main")
compacted = scenes.compact_cold_scenes(120, 64 * 1024, keep_active_scene=True)Resize Handling
Terminal dimensions are read every frame via platform-specific APIs (ReadConsoleScreenBufferInfo on Windows, ioctl TIOCGWINSZ on POSIX). When a size change is detected, the compositor reallocates its static final buffer, all widget geometries are recalculated, and a full redraw is triggered.