Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

  1. Kernel WRITERprintln!() renders to the BGA framebuffer via an IrqMutex-protected Framebuffer struct.
  2. User shell — reads from the console input buffer, writes to stdout (which goes through WRITER).
  3. 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:

  1. The compositor gets an shmem fd wrapping the BGA LFB (existing behaviour).
  2. 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:

  1. /bin/kbd calls irq_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 by irq_fd_dispatch, which reads port 0x60 and delivers the scancode in completion.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.
  2. Creates a registration channel and calls svc_register("keyboard").
  3. Event loop on CompletionPort:
    • OP_IRQ_WAIT → receives scancode → decodes via scancode set 1 state machine → produces key events
    • OP_IPC_RECV on registration channel → new client connecting (compositor sends a channel send-end) → stores client
  4. For each decoded key event, sends an IpcMessage to 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:

  1. 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.
  2. Arms OP_IRQ_WAIT on the IRQ fd in its completion port event loop.
  3. On each IRQ completion, feeds the raw byte into an inline MouseDecoder that 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/shell via clone(CLONE_VM|CLONE_VFORK) + execve (child_stack=0 shares the parent’s stack — safe because the parent is blocked until the child calls execve or _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_open is 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_shell polls DISPLAY_SUPPRESSED every 50ms (up to 20 iterations / 1 second) before falling back to standalone shell

Fallback Matrix

kbdcompositortermResult
yesyesyesFull graphical: kbd→compositor (mouse integrated)→term→shell
yesyesnoCompositor up, no terminal (display-only)
nono-Classic fallback: kernel kbd actor + shell on console

No New Syscalls

This design uses only existing kernel primitives:

PrimitiveSyscallUse
irq_create504keyboard driver claims IRQ 1, compositor claims IRQ 12 (mouse)
svc_register / svc_lookup513/514keyboard and compositor service discovery
ipc_create / ipc_send / ipc_recv505-507key/mouse event delivery with fd passing
shmem_create508window buffers, resize buffer allocation
framebuffer_open515display ownership (with suppression side effect)
pipe2293terminal↔shell communication
clone / execve / dup256/59/33terminal 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_dispatch reads port 0x60 for GSI 12 (mouse) in addition to GSI 1 (keyboard)