Display & Input Ownership
How the framebuffer and keyboard transition from kernel to compositor using the existing fd-passing and service-registry primitives.
Problem
At boot, three components compete for the display and keyboard:
- Kernel WRITER —
println!()renders to the BGA framebuffer via an IrqMutex-protectedFramebufferstruct. - User shell — reads from the console input buffer, writes to stdout (which goes through WRITER).
- Compositor — mmaps the same LFB via
framebuffer_open, composites client windows.
Today there is no ownership model. The kernel WRITER and compositor both
hold pointers to the same physical framebuffer memory and write
concurrently. Keyboard input routes to the user shell via
FOREGROUND_PID but the compositor has no way to receive it.
Design: Capability-Based Handoff
Ownership is expressed through who holds which fds, matching the existing IPC model.
Display Ownership
BOOT COMPOSITOR RUNNING
──── ──────────────────
WRITER ──▶ LFB (active) WRITER ──▶ serial only
Compositor ──▶ LFB (exclusive)
When the compositor calls framebuffer_open (syscall 515), two things
happen:
- The compositor gets an shmem fd wrapping the BGA LFB (existing behaviour).
- Side effect: the kernel marks the WRITER backend as
suppressed. All subsequent
println!()/log::info!()output is redirected to serial only. The kernel no longer touches the LFB. The status bar and timeline strip are also suppressed.
If the compositor exits or crashes, the kernel detects this (via process
exit cleanup in terminate_process) and unsuppresses the WRITER,
calling repaint_all() to restore kernel display output.
Implementation: DISPLAY_SUPPRESSED: AtomicBool and DISPLAY_OWNER_PID: AtomicU64 in libkernel/src/vga_buffer/mod.rs.
Input Ownership — Userspace Keyboard Driver
Instead of a kernel-level input_acquire syscall, the keyboard becomes
a userspace service (/bin/kbd). This requires no new kernel
interfaces — only existing primitives.
┌──────────┐ IRQ fd ┌──────────┐ IPC channel ┌────────────┐
│ IO APIC │───────────▶│ /bin/kbd │──────────────▶│ Compositor │
│ (GSI 1) │ scancode │ │ key events │ │
└──────────┘ in result └──────────┘ └────────────┘
How it works:
/bin/kbdcallsirq_create(1)— claims keyboard IRQ via the existing IRQ fd mechanism. This reroutes the keyboard interrupt from the hardwired kernel ISR (vector 33) to a dynamic vector handled byirq_fd_dispatch, which reads port 0x60 and delivers the scancode incompletion.result. The GSI is kept unmasked between interrupts so that edge-triggered IRQ edges are never lost; scancodes that arrive between OP_IRQ_WAIT re-arms are buffered in a 64-entry ring.- Creates a registration channel and calls
svc_register("keyboard"). - Event loop on CompletionPort:
OP_IRQ_WAIT→ receives scancode → decodes via scancode set 1 state machine → produces key eventsOP_IPC_RECVon registration channel → new client connecting (compositor sends a channel send-end) → stores client
- For each decoded key event, sends an
IpcMessageto all connected clients.
When /bin/kbd exits, close_irq restores the original IO APIC entry,
and the kernel keyboard actor resumes automatically — providing fallback.
Safety: if no client connects within 2 seconds, kbd exits to avoid capturing keyboard input with nobody listening.
Keyboard Protocol
MSG_KB_CONNECT (tag=1): client → keyboard service
data = [0, 0, 0]
fds = [event_send_fd, -1, -1, -1]
MSG_KB_KEY (tag=1): keyboard service → client (via passed channel)
data = [byte, modifiers, key_type]
fds = [-1, -1, -1, -1]
key_type: 0 = ASCII byte, 1 = special key (arrow, etc.)modifiers: bitmask (bit 0 = shift, bit 1 = ctrl, bit 2 = alt)
Input Ownership — Mouse (Integrated into Compositor)
The mouse is handled directly by the compositor — no separate mouse driver process. The compositor claims IRQ 12 itself and decodes PS/2 packets inline, eliminating an IPC round-trip per mouse event.
┌──────────┐ IRQ fd ┌────────────┐
│ IO APIC │───────────▶│ Compositor │
│ (GSI 12) │ byte │ │
└──────────┘ in result └────────────┘
How it works:
- The compositor calls
irq_create(12)— claims mouse IRQ via the existing IRQ fd mechanism. The kernel automatically initializes the PS/2 auxiliary port (i8042 controller) when GSI 12 is claimed. - Arms
OP_IRQ_WAITon the IRQ fd in its completion port event loop. - On each IRQ completion, feeds the raw byte into an inline
MouseDecoderthat collects 3-byte PS/2 packets (sync on byte 0 bit 3), decodes signed deltas using the OSDev wiki formula, and updates the absolute cursor position (clamped to screen bounds).
Compositor Key & Mouse Forwarding
The compositor connects to the keyboard service on startup using
svc_lookup_retry(). Key events are forwarded to the focused
window’s client via MSG_KEY_EVENT (tag 5). Mouse events (decoded
directly from IRQ 12) drive the cursor, focus, window movement,
and resizing.
Window Decorations (Server-Side, CDE Style)
The compositor draws server-side decorations inspired by the Common Desktop Environment (CDE) / Motif toolkit, with 3D beveled borders:
╔═══════════════════════════════╗ ─┐
║ ┌──┐ ║ │
║ │▪▪│ Win 1 (centered) ║ │ TITLE_H = 24px
║ └──┘ ║ │
╠═══════════════════════════════╣ ─┘
║ ┌───────────────────────────┐ ║
║ │ │ ║
║ │ Client Content │ ║ client buffer (w × h)
║ │ │ ║ (sunken inner bevel)
║ └───────────────────────────┘ ║
╚═══════════════════════════════╝
BORDER_W = 4px, BEVEL = 2px
- 3D bevels:
draw_bevel()renders light/dark edge pairs on all four sides to create a raised or sunken look (2px bevel width) - Title bar (24px): raised bevel, blue when focused, grey when unfocused
- Close button: raised square with inner square motif (CDE style), positioned in the top-left of the title bar
- Window title: centered text rendered with 8×16 CP437 font
- Client area: surrounded by a sunken inner bevel
- Color palette: blue-grey CDE theme (slate blue desktop, cool grey-blue window frames, blue active title bars)
Window Management
Focus: Click anywhere in a window to focus it. The focused window moves to the top of the Z-order and receives keyboard input.
Move: Drag the title bar to move a window.
Resize: Drag the bottom edge, right edge, or bottom-right corner
to resize. The cursor changes to indicate the resize direction:
diagonal double-arrow for corners, horizontal for right edge, vertical
for bottom edge. During drag, the window frame updates live. On
mouse-up, the compositor allocates a new shared buffer and sends
MSG_WINDOW_RESIZED to the client. The terminal emulator remaps the
new buffer, recalculates cols/rows, clears the screen, and nudges the
shell to redraw its prompt.
Close: Click the close button to close a window.
Compositor Double Buffering & Cursor-Only Rendering
The compositor uses an offscreen back buffer (heap-allocated, same
size as the framebuffer) to eliminate flicker. Full composite passes
clear and draw all windows (with decorations) into the back buffer,
then copy the finished frame to the LFB in a single memcpy.
Cursor-only optimization: Mouse movement that doesn’t change the scene (no window drag, no focus change) takes a fast path: restore the old cursor rectangle from the back buffer (~12x8 pixels), draw the cursor at the new position, and patch only those two small rectangles on the LFB. This avoids the full 3 MB recomposite on every mouse event.
Terminal Emulator and Shell
The terminal emulator (/bin/term) is a compositor client that spawns
the shell with pipe-connected stdin/stdout.
Compositor Terminal Emulator Shell
────────── ───────────────── ─────
MSG_KEY_EVENT stdin pipe
─────────────────▶ translate ──────────────▶ read(0)
s2c channel stdout pipe
◀───────────────── render ◀────────────── write(1)
damage notify (pipe pair)
◀─────────────────
The terminal emulator:
- Connects to compositor via
svc_lookup_retry("compositor") - Gets a 640×384 window (80×24 cells at 8×16 font)
- Creates pipe pairs for shell stdin/stdout
- Spawns
/bin/shellviaclone(CLONE_VM|CLONE_VFORK)+execve(child_stack=0shares the parent’s stack — safe because the parent is blocked until the child callsexecveor_exit) - Event loop: key events → shell stdin pipe, shell stdout → VT100 parser → glyph rendering → damage signal
Command interpreter (shell)
A plain stdin/stdout program with no knowledge of the compositor:
- Reads lines from stdin, writes output to stdout
- Works identically in both graphical and fallback modes
Fallback Path
If the compositor binary is not present or fails to start:
framebuffer_openis never called → WRITER stays active- Keyboard IRQ stays with kernel actor → routes to console buffer
- User shell reads/writes via console as it does today
The system degrades gracefully to the current behaviour.
Startup Sequence
Boot
├─ launch_keyboard_driver() [100ms VFS settle]
│ └─ /bin/kbd: irq_create(1), svc_register("keyboard")
│ keyboard IRQ rerouted → kernel actor dormant
│
├─ launch_compositor() [100ms VFS settle]
│ ├─ /bin/compositor: framebuffer_open → WRITER suppressed
│ ├─ irq_create(12) → PS/2 aux init, inline mouse decoding
│ ├─ svc_lookup_retry("keyboard") → receive key events
│ ├─ svc_register("compositor")
│ └─ kernel spawns /bin/term
│ ├─ svc_lookup_retry("compositor") → get window
│ ├─ pipe2 + clone/execve → spawn /bin/shell
│ └─ event loop (keys → shell stdin, shell stdout → render)
│
└─ launch_userspace_shell() [polls DISPLAY_SUPPRESSED every 50ms, up to 1s]
└─ if DISPLAY_SUPPRESSED → skip (compositor path active)
else → launch /bin/shell directly (fallback)
Service readiness is coordinated via polling and retry loops rather than hardcoded sleep timings:
- Userspace:
svc_lookup_retry()retries service lookup with 50ms yields - Kernel:
launch_userspace_shellpollsDISPLAY_SUPPRESSEDevery 50ms (up to 20 iterations / 1 second) before falling back to standalone shell
Fallback Matrix
| kbd | compositor | term | Result |
|---|---|---|---|
| yes | yes | yes | Full graphical: kbd→compositor (mouse integrated)→term→shell |
| yes | yes | no | Compositor up, no terminal (display-only) |
| no | no | - | Classic fallback: kernel kbd actor + shell on console |
No New Syscalls
This design uses only existing kernel primitives:
| Primitive | Syscall | Use |
|---|---|---|
irq_create | 504 | keyboard driver claims IRQ 1, compositor claims IRQ 12 (mouse) |
svc_register / svc_lookup | 513/514 | keyboard and compositor service discovery |
ipc_create / ipc_send / ipc_recv | 505-507 | key/mouse event delivery with fd passing |
shmem_create | 508 | window buffers, resize buffer allocation |
framebuffer_open | 515 | display ownership (with suppression side effect) |
pipe2 | 293 | terminal↔shell communication |
clone / execve / dup2 | 56/59/33 | terminal spawns shell |
The only kernel changes beyond the initial suppression flag are:
- PS/2 auxiliary port initialization on
irq_create(12)(libkernel/src/ps2.rs) irq_fd_dispatchreads port 0x60 for GSI 12 (mouse) in addition to GSI 1 (keyboard)