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 pixel coordinates based on the current terminal size. This runs every frame, so widgets automatically adapt to terminal resizing.
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. It fills the buffer with blank pixels (white space on black background), then iterates through the active scene's buffers in creation order. For each buffer, it clips to screen bounds and copies pixels into the final buffer via memcpy.
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.
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.