← Blog

USB HID Input Router: CAP_HID Gate + Overlay Hit-Test

June 22, 2026 · sigil-drivers · Sigil-Docs
drivers hid input security 0.6.0

sigilOS Drivers lands hid_router.sg: a unified USB HID polling entry point that gates keyboard and mouse access behind CAP_HID, accumulates relative mouse deltas into a clamped absolute cursor position, and routes events into Direct-Scene Overlay regions when the cursor lands inside one. Integration-verified: hid_router + ev_overlay + usbhidkbd compile clean.

Cursor plane rendering on x86
Cursor plane on x86 — absolute position output from hid_router accumulator

Why a router?

sigilOS has separate drivers for USB HID keyboard (usbhidkbd, op hk_op) and USB HID mouse (usbhidmouse, op hm_op). Both expose a poll interface — call once per frame tick, get the current key/button/delta state. The problem: anything that needed input had to know about both drivers separately, and both drivers expose the full device state to any caller.

hid_router.sg solves both issues. It wraps both drivers behind a single hid_poll() call and gates the whole thing behind CAP_HID. Only a process that holds the HID capability can receive input events. No capability, no input — the same pattern as Cap<Display> for screen access and Cap<Network> for network sockets.

The literal op codes (hk_op, hm_op) are used rather than function names because when both HID driver files are linked together, the Sigil linker sees 10 functions with overlapping names (poll, reset, init…). Literal op codes sidestep the naming collision entirely — each driver's op table is indexed by a unique integer, not by name.


Cursor accumulation and screen-clamp

USB HID mice report relative motion — "moved 3 pixels right, 1 pixel down" — not absolute screen coordinates. The router accumulates these deltas into a running absolute cursor position (cursor_x, cursor_y):

cursor_x = cursor_x + delta_x
cursor_y = cursor_y + delta_y
if cursor_x < 0        then cursor_x = 0
if cursor_x >= scr_w   then cursor_x = scr_w - 1
if cursor_y < 0        then cursor_y = 0
if cursor_y >= scr_h   then cursor_y = scr_h - 1

The clamp keeps the cursor inside the screen rectangle on every frame — no floating cursor, no wrap-around. The WM reads (cursor_x, cursor_y) from the router each frame to position the Lumen cursor sprite and perform hit-testing against window chrome.


ev_overlay hit-test and local coordinate dispatch

After accumulating the cursor position, the router runs ev_overlay_hittest(cursor_x, cursor_y) against the live overlay registry at 0x929000. If the cursor is inside an overlay region:

1
Hit detected ev_overlay_hittest returns the overlay slot index and the overlay's bounding rect (ox, oy, ow, oh).
2
Local coordinates The router subtracts the overlay origin: local_x = cursor_x - ox, local_y = cursor_y - oy. The overlay gets coordinates relative to its own top-left corner, not the screen.
3
Overlay dispatch ev_overlay_dispatch(slot, local_x, local_y, buttons) sends the event to the overlay's registered handler. The handler receives input as if the overlay were the whole screen.
4
WM fallthrough If no overlay is hit, the event propagates normally to the WM input queue. The WM picks up cursor_x/cursor_y and the key/button state each frame.

This is the correct compositing-aware input model: a pixel on screen that belongs to an overlay should deliver input to the overlay's handler, not to whatever window is behind it. The hit-test uses the same slot data the Lumen compositor reads when punching holes in the scanout blit, so geometry is always consistent.


What's next

The HID router is the input side of the HID→WM wiring. The remaining seam is connecting hid_poll() output to the active Lumen window's input channel — routing keyboard events to the focused window's cap-gated event queue, and mouse events to the WM's window-chrome hit-tester. That's the next OS-side item: once hid_poll feeds the WM, sigilOS has a fully interactive desktop.