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:

  1. Box blur the backbuffer region under the element (draw_box_blur, radius 10)
  2. Color tint via alpha-blended rectangle (draw_blend_rect)
  3. 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:

  1. Before drawing: save the framebuffer pixels under the cursor position to s_save[]
  2. Alpha-blend the cursor sprite onto the framebuffer
  3. 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):

  1. Context menu hit testing (click on menu item, or outside to close)
  2. Top bar “Aegis” click (opens/closes context menu)
  3. Dock item click (spawns terminal, widget test, etc.)
  4. Desktop selection box (drag on empty space)
  5. Content drag (text selection in terminal)
  6. Titlebar drag (window move)
  7. Window close button (traffic-light red circle)
  8. Window focus change + raise
  9. 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.

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:

  1. Ctrl+Alt+L in Lumen sets s_input_frozen = 1 and sends SIGUSR1 to parent (Bastion)
  2. Bastion’s SIGUSR1 handler sets s_locked = 1, re-enters its own input loop showing the lock form
  3. On successful re-authentication, Bastion sends SIGUSR2 to Lumen
  4. 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)