Lumen Compositor
Direct-framebuffer window compositor with dirty-rect tracking, frosted glass compositing, and integrated terminal emulator
Lumen Compositor
Lumen is the Aegis display server and window compositor. It renders directly to the linear framebuffer mapped via sys_fb_map (syscall 513), bypassing X11, Wayland, and any display protocol entirely.
v1 note: Lumen is v1 software – functional and tested, but not production-hardened. Contributions are welcome – file issues or propose changes at exec/aegis. Lumen owns the framebuffer surface, manages window Z-order, performs dirty-rect compositing with frosted glass effects, handles mouse/keyboard input dispatch, and hosts the built-in terminal emulator.
+-------------------------------------------------------+
| Bastion (display manager) |
| authenticates user -> fork+execve -> Lumen |
+-------------------------------------------------------+
|
v
+-------------------------------------------------------+
| Lumen compositor (user/bin/lumen/) |
| +---------------------------------------------------+|
| | Framebuffer (sys_fb_map, syscall 513) ||
| | fb_info_t: addr, width, height, pitch, bpp ||
| +---------------------------------------------------+|
| | Back buffer (malloc'd, same dimensions) ||
| +---------------------------------------------------+|
| | compositor_t ||
| | window stack [MAX_WINDOWS=16] ||
| | dirty rect accumulator [MAX_DIRTY_RECTS=32] ||
| | cursor state, drag state, selection state ||
| +---------------------------------------------------+|
| | Citadel desktop shell ||
| | top bar (topbar_draw) ||
| | dock (dock_draw, frosted glass overlay) ||
| | context menu (Aegis system menu) ||
| +---------------------------------------------------+|
| | Glyph toolkit (widget windows) ||
| | Terminal emulator (PTY-backed) ||
| +---------------------------------------------------+|
+-------------------------------------------------------+
Architecture Overview
Lumen is a single-process, single-threaded compositor. All input polling, PTY I/O, compositing, and framebuffer writes happen in one for (;;) event loop running at approximately 60 fps (16ms sleep when idle).
No Display Protocol
Unlike X11 or Wayland, Lumen does not implement a client-server display protocol. Windows are not separate processes communicating over sockets. Instead, all windows (terminals, widget windows, about dialog) live in-process as glyph_window_t objects with their own offscreen surface_t buffers. The compositor blits these surfaces onto the back buffer, applies effects, and copies dirty regions to the framebuffer.
This design eliminates IPC overhead and shared-memory synchronization entirely, at the cost of requiring all GUI content to be linked into the Lumen binary. For a custom OS with a small number of window types, this tradeoff is appropriate.
Framebuffer Initialization
Lumen maps the hardware framebuffer via a custom syscall:
typedef struct {
uint64_t addr;
uint32_t width, height, pitch, bpp;
} fb_info_t;
fb_info_t fb_info;
long ret = syscall(513, &fb_info); /* sys_fb_map */
uint32_t *fb = (uint32_t *)(uintptr_t)fb_info.addr;
int pitch_px = (int)(fb_info.pitch / (fb_info.bpp / 8));
The framebuffer is a linear array of 32-bit XRGB pixels. pitch is in bytes; all internal surface operations use pitch_px (pixels per row) to handle stride correctly. A separate back buffer of identical dimensions is heap-allocated for double buffering.
Compositor State
The central data structure is compositor_t, defined in compositor.h:
typedef struct {
surface_t fb; /* framebuffer surface */
surface_t back; /* back buffer surface */
glyph_window_t *windows[MAX_WINDOWS]; /* Z-ordered window stack */
int nwindows;
glyph_window_t *focused; /* keyboard focus target */
int cursor_x, cursor_y;
int dragging; /* titlebar drag in progress */
glyph_window_t *drag_win;
int drag_dx, drag_dy; /* offset from cursor to window origin */
glyph_window_t *content_drag_win; /* window receiving mouse drag (text selection) */
int prev_buttons;
int selecting; /* desktop selection box active */
int sel_x0, sel_y0, sel_x1, sel_y1;
glyph_rect_t dirty_rects[MAX_DIRTY_RECTS];
int ndirty;
int full_redraw; /* force complete recomposite */
int bg_rendered;
wallpaper_t wallpaper;
void (*on_draw_desktop)(surface_t *back, int w, int h);
void (*on_draw_overlay)(surface_t *back, int w, int h);
} compositor_t;
Window Stack
Windows are stored in a flat array ordered by Z-depth: windows[0] is the bottom-most, windows[nwindows-1] is the top-most. The limit is MAX_WINDOWS = 16. Window operations:
| Function | Behavior |
|---|---|
comp_add_window |
Append to stack, set as focused, trigger full redraw |
comp_remove_window |
Mark old screen rect dirty, compact array, destroy window |
comp_raise_window |
Move window to top of stack, trigger full redraw |
comp_window_at |
Reverse-iterate stack to find topmost visible window at (x,y) |
Focus tracking is manual: comp_add_window sets focused_window = 1 on the new window. When focus changes, the old window’s flag is cleared and the new window’s flag is set. This flag is read by render_chrome to decide whether to draw the close button “x” glyph.
Dirty Rect Compositing
Lumen uses a dirty-rect accumulator to minimize per-frame work. The compositor maintains up to MAX_DIRTY_RECTS = 32 rectangles. When the accumulator overflows, the last entry is expanded via glyph_rect_union to absorb new rects.
Dirty Rect Pipeline
Per-frame dirty rect flow:
+--------------------------------------------------+
| 1. Collect dirty rects from windows |
| glyph_window_get_dirty_rect() -> screen coords|
+--------------------------------------------------+
|
v
+--------------------------------------------------+
| 2. Full redraw path (if full_redraw == 1) |
| - Draw wallpaper/solid bg to entire backbuf |
| - Call on_draw_desktop (top bar) |
| - Render + blit ALL visible windows |
| - Draw selection box |
| - Call on_draw_overlay (dock, context menu) |
| - memcpy entire backbuf -> framebuffer |
+--------------------------------------------------+
| (if not full redraw)
v
+--------------------------------------------------+
| 3. Partial redraw path |
| For EACH dirty rect: |
| a. Restore background in that rect |
| Then once: |
| b. Call on_draw_desktop |
| c. Re-render windows overlapping any rect |
| d. Call on_draw_overlay |
| For EACH dirty rect: |
| e. partial_flip: memcpy rect from back->fb |
+--------------------------------------------------+
The partial redraw path processes each dirty rect individually rather than unioning them into one bounding box. From compositor.c:
/* Process each dirty rect individually instead of unioning into one
* giant bounding box. This avoids redrawing the entire horizontal
* span between two small dirty regions on opposite sides. */
partial_flip
The partial_flip function copies only the dirty rectangle from the back buffer to the framebuffer, row by row:
static void
partial_flip(surface_t *fb, surface_t *back, glyph_rect_t r)
{
for (int y = r.y; y < r.y + r.h && y < fb->h; y++) {
int x0 = r.x < 0 ? 0 : r.x;
int x1 = r.x + r.w;
if (x1 > fb->w) x1 = fb->w;
int count = x1 - x0;
if (count <= 0) continue;
memcpy(&fb->buf[y * fb->pitch + x0],
&back->buf[y * back->pitch + x0],
(unsigned)count * sizeof(uint32_t));
}
}
Frosted Glass Compositing
Lumen implements a multi-layer frosted glass effect for windows, the dock, the top bar, and the context menu. The effect is achieved by:
- Box blur the backbuffer region under the element (
draw_box_blur, radius 10) - Color tint via alpha-blended rectangle (
draw_blend_rect) - Color-keyed blit of window content (
draw_blit_keyed)
Blit Modes
Window blitting uses three modes to balance visual quality and performance:
| Mode | Constant | Behavior | When Used |
|---|---|---|---|
| Full frost | BLIT_FROST |
Blur + tint + keyed blit | Normal compositing |
| Fast frost | BLIT_FAST_FROST |
Tint + keyed blit (skip blur) | Non-dragged windows during drag |
| Opaque | BLIT_OPAQUE |
Direct surface blit | Dragged window during drag |
The frosted window rendering pipeline (from blit_window_to_back):
Frosted window blit (BLIT_FROST):
+------------------------------------------+
| 1. Box blur entire window footprint |
| draw_box_blur(back, x, y, w, h, 10) |
+------------------------------------------+
| 2. Dark tint on titlebar region |
| draw_blend_rect(..., 0x101020, 160) |
+------------------------------------------+
| 3. Tint on client region |
| Terminal: dark glass (0x0A0A14, 160) |
| Widget: dark glass (0x181828, 150) |
+------------------------------------------+
| 4. Subtle border (1px highlight/shadow) |
+------------------------------------------+
| 5. Title text (centered, white) |
+------------------------------------------+
| 6. Traffic-light circles (close/min/max) |
+------------------------------------------+
| 7. Color-keyed client area blit |
| Key = C_TERM_BG (terminal) or |
| C_SHADOW (widget window) |
+------------------------------------------+
The color key mechanism allows transparent pixels in the window surface to reveal the frosted background underneath. Terminal windows use C_TERM_BG (0x000A0A14) as the key color; widget windows use C_SHADOW (0x00080810).
Chromeless Frosted Windows
The dropdown terminal uses chromeless = 1 to skip the titlebar entirely. Its frosted glass path is simpler: blur, tint with C_TERM_BG, then color-keyed blit of the terminal content.
Cursor System
The cursor is a 16x20 ARGB sprite rendered procedurally at startup (cursor.c). It uses a save-under strategy:
- Before drawing: save the framebuffer pixels under the cursor position to
s_save[] - Alpha-blend the cursor sprite onto the framebuffer
- Before the next composite: restore
s_save[]to erase the cursor
The compositor calls cursor_hide() before any framebuffer write and cursor_show() after. Mouse-only movement (no content changes) is optimized to skip the full composite cycle, using only cursor_hide() + cursor_show() to relocate the cursor.
The cursor sprite has three layers built procedurally:
- Shadow (alpha 0x40, offset +1,+1)
- Outline (black, fully opaque border pixels)
- Fill (white, fully opaque interior)
Mouse movement applies a 1.5x speed multiplier via integer math: cursor_x += dx + dx / 2.
Input Handling
Keyboard
Lumen sets stdin to raw mode (VMIN=0, VTIME=0, ICANON and ECHO disabled). Single bytes are read per iteration. Escape sequences are collected with tight retry loops (up to 80 retries per byte) to handle the keyboard ISR’s atomic multi-byte push.
Key bindings processed by the compositor:
| Sequence | Action |
|---|---|
Ctrl+Alt+T (ESC + 0x14) |
Toggle dropdown terminal visibility |
Ctrl+Alt+L (ESC + 0x0C) |
Lock screen (signals parent Bastion via SIGUSR1) |
Alt+C or Ctrl+Shift+C |
Copy terminal selection to clipboard |
Alt+V or Ctrl+Shift+V |
Paste clipboard to focused PTY |
CSI sequences (ESC [ …) |
Forward to focused PTY master fd |
| Normal keys | Forward to focused PTY master fd |
Mouse
Mouse events are read from /dev/mouse (non-blocking). Events are batched: all pending events in a single frame are accumulated into a total delta before processing. The event structure:
typedef struct __attribute__((packed)) {
uint8_t buttons;
int16_t dx;
int16_t dy;
int16_t scroll;
} mouse_event_t;
Mouse dispatch priority (from comp_handle_mouse and the main loop):
- Context menu hit testing (click on menu item, or outside to close)
- Top bar “Aegis” click (opens/closes context menu)
- Dock item click (spawns terminal, widget test, etc.)
- Desktop selection box (drag on empty space)
- Content drag (text selection in terminal)
- Titlebar drag (window move)
- Window close button (traffic-light red circle)
- Window focus change + raise
- Widget dispatch (client-area click forwarded to Glyph widget tree)
Wallpaper
Wallpaper is loaded from /usr/share/wallpaper.raw as a simple raw pixel format: 8-byte header (uint32_t width, uint32_t height) followed by XRGB pixel data. If the wallpaper dimensions match the framebuffer exactly, rows are memcpy‘d directly. Otherwise, draw_blit_scaled performs nearest-neighbor scaling.
Terminal Emulator
Lumen includes a full PTY-backed terminal emulator (terminal.c) supporting ANSI escape sequences, 256-color and truecolor output, text selection, scrollback, and a scrollbar.
Architecture
Each terminal window stores a term_priv_t in the Glyph window’s priv pointer. The terminal grid is a ring buffer of term_cell_t cells:
typedef struct {
char ch;
uint32_t fg; /* 0xRRGGBB or TERM_DEFAULT_COLOR */
uint32_t bg; /* 0xRRGGBB or TERM_DEFAULT_COLOR */
uint8_t attrs; /* ATTR_BOLD | ATTR_UNDERLINE | ATTR_REVERSE */
} term_cell_t;
The ring buffer holds rows + SCROLLBACK_LINES (500 lines of history) rows. scroll_top tracks the first visible row in the ring. When text scrolls past the bottom, scroll_top advances and the new bottom row is cleared.
PTY Creation
Terminals use sys_spawn (syscall 514) instead of fork+execve to avoid the page-copy stall caused by forking Lumen’s large address space:
long pid = syscall(SYS_SPAWN, "/bin/stsh", argv, envp, slave_fd, 0);
The PTY pair is opened via /dev/ptmx + TIOCGPTN + /dev/pts/N. The master fd is set to O_NONBLOCK and stored in win->tag for polling in the main loop.
ANSI Escape Handling
The terminal parses CSI sequences (ESC [ params final-byte) with a state machine:
| State | Meaning |
|---|---|
| 0 | Normal character processing |
| 1 | Got ESC, expecting [ |
| 2 | Inside CSI, collecting parameters and final byte |
Supported CSI sequences:
| Sequence | Code | Function |
|---|---|---|
ESC[nA |
CUU | Cursor up n lines |
ESC[nB |
CUD | Cursor down n lines |
ESC[nC |
CUF | Cursor forward n columns |
ESC[nD |
CUB | Cursor back n columns |
ESC[r;cH |
CUP | Cursor position (row, col) |
ESC[nJ |
ED | Erase in display (0=below, 2=all) |
ESC[nK |
EL | Erase in line (0=right, 2=all) |
ESC[...m |
SGR | Set graphic rendition (colors, attrs) |
ESC[?25h/l |
DECTCEM | Show/hide cursor |
ESC[?1049h/l |
Alternate screen buffer enter/leave |
SGR supports: reset (0), bold (1), underline (4), reverse (7), standard 8-color fg/bg (30-37, 40-47), bright fg/bg (90-97, 100-107), 256-color (38;5;N, 48;5;N), and truecolor (38;2;R;G;B, 48;2;R;G;B).
Text Selection and Clipboard
Mouse drag in the terminal client area initiates text selection. The selection is tracked as start/end (row, col) coordinates in the visible grid. Selected cells are drawn with a translucent blue highlight (SEL_HIGHLIGHT = 0x004488CC, alpha 80).
Copy (Alt+C) extracts selected text via terminal_copy_selection, which strips trailing whitespace per row and joins rows with newlines. The clipboard is a simple 8KB static buffer. Paste (Alt+V) writes clipboard content directly to the PTY master fd.
Dropdown Terminal
The dropdown terminal (Ctrl+Alt+T) is a chromeless, frosted-glass window spanning nearly the full screen width, positioned directly below the top bar. It starts hidden and toggles visibility. When shown, it steals focus; when hidden, focus returns to the previously focused window.
Bottom corners are rounded by overpainting background-colored pixels outside the corner arcs after the terminal grid render.
Crossfade Transition
When Lumen starts, it captures the current framebuffer content (Bastion’s login screen), composites the desktop into the back buffer, then performs a 15-step crossfade over approximately 250ms:
for (int step = 0; step < 15; step++) {
int alpha = 255 - (step * 255 / 14);
int inv = 255 - alpha;
for (size_t i = 0; i < npx; i++) {
/* Per-pixel linear interpolation */
uint32_t r = (((old >> 16) & 0xFF) * alpha +
((new_px >> 16) & 0xFF) * inv) / 255;
/* ... green, blue channels ... */
fb[i] = (r << 16) | (g << 8) | b;
}
nanosleep(&ts, NULL); /* 17ms per step */
}
Event Loop
The main loop structure (main.c):
for (;;) {
1. Poll keyboard (stdin, raw, non-blocking)
- Handle compositor shortcuts (Ctrl+Alt+T, etc.)
- Forward keys/escape sequences to focused PTY
2. Poll mouse (/dev/mouse, batched)
- Update dock hover, context menu hover
- Handle button press (menu, topbar, dock, windows)
- comp_handle_mouse() for drag, focus, widget dispatch
3. Poll PTY masters for ALL terminal windows
- Read output, feed to terminal_write()
4. Update clock (~1/sec via frame counter)
5. Composite + cursor update
- Mouse-only: skip composite, just relocate cursor
- Content change: cursor_hide -> comp_composite -> cursor_show
6. Second PTY read pass (catch late shell output)
7. Sleep 16ms if idle (no activity this frame)
}
Lock Screen
Screen locking is cooperative between Lumen and Bastion:
Ctrl+Alt+Lin Lumen setss_input_frozen = 1and sendsSIGUSR1to parent (Bastion)- Bastion’s SIGUSR1 handler sets
s_locked = 1, re-enters its own input loop showing the lock form - On successful re-authentication, Bastion sends
SIGUSR2to Lumen - Lumen’s SIGUSR2 handler clears
s_input_frozen, resuming input processing
While frozen, Lumen discards all keyboard input but continues compositing normally.
Build
Lumen links against both libglyph.a (widget toolkit) and libcitadel.a (desktop shell):
SRCS = main.c cursor.c compositor.c terminal.c widget_test.c about.c
lumen.elf: $(OBJS) $(GLYPH) $(CITADEL)
$(CC) $(CFLAGS) -o $@ $(OBJS) \
-L../../lib/citadel -lcitadel \
-L../../lib/glyph -lglyph
All sources are compiled with musl-gcc (-O2 -fno-pie -no-pie). The resulting lumen.elf is installed to /bin/lumen in the Aegis root filesystem.
Source Files
| File | Purpose |
|---|---|
main.c |
Entry point, event loop, wallpaper loading, clipboard, context menu |
compositor.c |
Window management, dirty-rect compositing, mouse/key dispatch |
compositor.h |
compositor_t struct, constants (MAX_WINDOWS, MAX_DIRTY_RECTS) |
cursor.c |
ARGB cursor sprite with save-under |
terminal.c |
PTY terminal emulator, ANSI parser, scrollback, selection |
widget_test.c |
Widget showcase window (tabs, buttons, checkboxes, etc.) |
about.c |
“About Aegis” info window (system info from /proc, logo rendering) |