← Back to Blog

GUI Desktop for FRISC OS

November 20, 2025 friscguiosdev

Building a graphical desktop environment for FRISC OS — window manager, compositor, and applications. From raw framebuffer pixels to a fully functional retro-aesthetic desktop.

◉ Framebuffer Architecture

FRISC GUI is built on direct framebuffer access. No GPU drivers (yet), just a linear block of memory mapped to screen pixels:

struct Framebuffer {
    addr: *mut u32,     // Physical address of pixel buffer
    width: u32,         // Screen width in pixels
    height: u32,        // Screen height in pixels
    pitch: u32,         // Bytes per row (may include padding)
    bpp: u32,           // Bits per pixel (32 for BGRA)
}

Double-buffering prevents tearing:

fn render_frame(fb: &Framebuffer) {
    // Draw to back buffer
    back_buffer
        |> clear(COLOR_BG)
        |> draw_desktop()
        |> draw_windows()
        |> draw_cursor(mouse_x, mouse_y)

    // Swap buffers (single memcpy)
    fb.swap(back_buffer)
}

◉ The Rendering Pipeline

GUI rendering is a natural fit for Lateralus pipelines. Each frame:

fn desktop_render_loop() {
    loop {
        // Event handling pipeline
        events()
            |> filter(fn(e) { e.is_input() })
            |> dispatch_to_focused_window()

        // Render pipeline
        windows
            |> filter(fn(w) { w.visible })
            |> sort_by(fn(w) { w.z_order })
            |> each(fn(w) { w.render() })

        // Composite and display
        compositor.present()

        // Aim for 60 FPS
        sleep_until_vsync()
    }
}

◉ Window Manager

The window manager is tiling by default, with floating mode optional. Windows are arranged automatically based on rules:

// Window layout configuration (in Lateralus)
let layout_rules = [
    { app: "terminal", position: "left-half" },
    { app: "editor", position: "right-half" },
    { app: "browser", position: "maximized" },
    { app: "menu", position: "floating" },
]

fn apply_layout(window: Window) {
    layout_rules
        |> find(fn(r) { r.app == window.app_name })
        |> match {
            Some(rule) => window.set_position(rule.position),
            None => window.tile_next_available(),
        }
}

Keyboard-driven navigation with Vim-style bindings:

◉ The Retro Theme

FRISC desktop embraces a retro-futuristic aesthetic matching the Lateralus website:

// Color palette
const COLOR_BG       = 0xFF0a0a12  // Deep charcoal
const COLOR_PANEL    = 0xFF12121c  // Slightly lighter
const COLOR_BORDER   = 0xFF2a2a3a  // Subtle borders
const COLOR_LIME     = 0xFF00ff88  // Accent (window focus)
const COLOR_PINK     = 0xFFff4081  // Secondary accent
const COLOR_YELLOW   = 0xFFffff00  // Highlights
const COLOR_CYAN     = 0xFF00d4ff  // Links/buttons

Fonts are rendered using a built-in 8x16 bitmap font for that authentic terminal aesthetic. We may add TrueType support later, but bitmap fonts are fast and pixel-perfect.

◉ Built-in Applications

FRISC ships with essential applications, all written in Lateralus:

Terminal (frisc-term): VT100-compatible terminal emulator with 256 colors.

File Manager (frisc-files): Two-panel file browser inspired by Midnight Commander.

Text Editor (frisc-edit): Modal editor with Vim keybindings. Can edit system config files.

System Monitor (frisc-top): Real-time CPU, memory, and process monitoring.

// System monitor is just a pipeline over process data
fn update_display() {
    processes()
        |> filter(fn(p) { p.state != "zombie" })
        |> sort_by(fn(p) { p.cpu_percent })
        |> reverse()
        |> take(20)
        |> each(fn(p) {
            draw_row(p.pid, p.name, p.cpu_percent, p.mem_mb)
        })
}

◉ Compositor Details

The compositor handles window rendering order, transparency, and damage tracking:

struct Compositor {
    windows: list[Window],
    damage_regions: list[Rect],
    back_buffer: Buffer,
}

fn composite(self) {
    // Only redraw damaged regions for efficiency
    let regions = self.damage_regions |> merge_overlapping()

    for region in regions {
        // Draw background
        self.back_buffer.fill_rect(region, wallpaper.sample(region))

        // Draw windows that intersect this region
        self.windows
            |> filter(fn(w) { w.bounds.intersects(region) })
            |> each(fn(w) { w.blit_to(self.back_buffer, region) })
    }

    // Swap to front
    framebuffer.blit(self.back_buffer)
    self.damage_regions.clear()
}

◉ Mouse Support

PS/2 mouse driver (or USB HID on newer hardware) provides cursor movement:

fn mouse_handler(packet: MousePacket) {
    // Update cursor position
    cursor_x = clamp(cursor_x + packet.dx, 0, screen_width - 1)
    cursor_y = clamp(cursor_y - packet.dy, 0, screen_height - 1)  // Y inverted

    // Hit testing for clicks
    if packet.left_click {
        windows
            |> find(fn(w) { w.bounds.contains(cursor_x, cursor_y) })
            |> match {
                Some(w) => w.handle_click(cursor_x, cursor_y),
                None => desktop.handle_click(cursor_x, cursor_y),
            }
    }

    // Mark cursor region as damaged
    compositor.damage(cursor_rect())
}

◉ Performance

On the HiFive Unmatched (no GPU acceleration):

Operation Time
Full frame redraw (1920x1080)14ms
Partial redraw (single window)2-4ms
Cursor movement<1ms
Window drag~8ms

We achieve 60+ FPS for normal usage with damage tracking. Full-screen video playback would need GPU acceleration.

◉ Try It

Boot FRISC with GUI support:

qemu-system-riscv64 -machine virt -m 512M \
    -device virtio-gpu-device \
    -kernel build/frisc.elf \
    -append "console=ttyS0 gui=1"

See the OS page for full build instructions.