X11 display server written in x86_64 Linux assembly. No libc, no toolkits, no FreeType, no Mesa, no Xlib. Just NASM source, direct syscalls, the X11 wire protocol on a Unix socket, and the kernel's DRM/KMS + evdev interfaces.
Long-range goal: serve enough of the X11 wire protocol (core + SHAPE + RENDER + XKB + COMPOSITE + DAMAGE + RANDR + MIT-SHM + XInput2 + XVideo) to host the whole CHasm desktop plus arbitrary X clients — Firefox, VS Code, GIMP, Inkscape — all software-rendered, all on a stack written end-to-end in asm.
The whole stack, end to end in assembly: pointer (a Rust TUI file manager) running inside glass (the asm terminal) on frame (the asm X server), shown on the laptop's panel via DRM/KMS — two-pane layout, syntax-highlighted preview, colour, keyboard-driven. No libc, no Xlib, no Mesa anywhere in the path.
| # | Phase | Status |
|---|---|---|
| 1 | Connection setup + Unix socket bind | ✓ shipped |
| 2 | DRM/KMS probe (read-only ioctls, no master) | ✓ shipped |
| 2b | DRM/KMS modeset (CreateDumb + AddFB + SetCRTC) | ✓ shipped |
| 3 | evdev input + KeyPress / Motion routing | ✓ shipped |
| 4a | Multi-client serve loop + dispatch + 16 opcode handlers | ✓ shipped |
| 4b | Window tree + CreateWindow / MapWindow / ConfigureWindow | ✓ shipped |
| 4c | Properties — ChangeProperty / GetProperty / GetAtomName | ✓ shipped |
| 4d.1 | Input API — GrabKey / GrabKeyboard / real US keysym table | ✓ shipped |
| 4d.2 | evdev → KeyPress event delivery | ✓ shipped |
| 4e | SubstructureRedirect routing + ReparentWindow | ✓ shipped |
| 4f | Software compositor — solid-colour rects on the panel via --display |
✓ shipped |
| 4g | GCs + window backing store + PolyFillRectangle / PutImage | ✓ shipped |
| 4h | Pixmaps + CopyArea / ClearArea / PolyRectangle | ✓ shipped |
| 5 | Atoms + GetProperty / ChangeProperty / selections | ✓ shipped |
| 6 | SHAPE extension | ✓ shipped (bounding + input regions — spot runs) |
| 7 | GCs + drawing primitives | |
| 8 | DRM/KMS atomic modeset upgrade | |
| 9 | RENDER subset for glass emoji + ARGB | |
| 10 | Cursor sprite + keyboard layout + clipboard | |
| 11 | XKB (Firefox-compatible) | |
| 12 | DAMAGE + COMPOSITE + FIXES | |
| 13 | RANDR + XInput2 + MIT-SHM | RANDR + XI2 + MIT-SHM ✓ shipped |
| 14 | First Firefox launch |
Phase 4 is the "tile runs on frame" milestone — self-hosting CHasm. Phase 14 is the "Firefox runs on a 50k-line asm X server" milestone.
make
./frame # listens on display :7 (configurable: ./frame N)
DISPLAY=:7 xdpyinfo # connects, gets setup reply, sends QueryExtensionframe accepts an X11 client, validates its 12-byte connection-setup
request (byte-order l, protocol 11.0, drains any auth tail), and
emits a structurally valid setup reply describing:
- One screen, 1920×1080, root window XID
0x80 - Two depths: 24 (TrueColor RGB) and 32 (TrueColor ARGB for glass transparency)
- One pixmap format (depth 24 in 32 bpp)
Subsequent requests are logged to stderr (req opcode=N len=M) and
silently dropped. Real dispatch lands in phase 4 once the wire is
proven and the DRM backend is in.
./frame --probeOpens /dev/dri/cardN, enumerates resources, lists connectors:
frame: opened /dev/dri/card1, driver i915 v1.6.0
frame: resources: 4 CRTCs, 5 connectors, 21 encoders
frame: framebuffer range 0x0 to 16384x16384
connector 507: eDP-1 → connected, 1 modes, preferred 1920x1200 @ 120 Hz
connector 516: DisplayPort-1 → disconnected, 0 modes
...
Uses three read-only ioctls — DRM_IOCTL_VERSION,
DRM_IOCTL_MODE_GETRESOURCES, DRM_IOCTL_MODE_GETCONNECTOR. None
require DRM master, so this runs safely alongside an active Xorg.
sudo ./frame --modesetPure-asm path: takes DRM master, picks the first connected
connector, queries its preferred mode, allocates a dumb buffer
(DRM_IOCTL_MODE_CREATE_DUMB), mmap's it (DRM_IOCTL_MODE_MAP_DUMB
mmap), fills it solid purple, binds it as a framebuffer (DRM_IOCTL_MODE_ADDFB), and points the CRTC at it (DRM_IOCTL_MODE_SETCRTC). Holds the picture for 5 seconds, then restores the CRTC's prior state and frees everything in reverse.
Needs DRM master, which Xorg holds while it's running. From a TTY, after stopping the display manager:
# 1. Switch to a TTY
Ctrl+Alt+F2
# 2. Login as geir, navigate to the repo
cd ~/Main/G/GIT-isene/frame
# 3. Stop X (whichever applies — pick one)
sudo systemctl stop sddm # if KDE plasma
sudo systemctl stop gdm # if GNOME
sudo systemctl stop lightdm # if XFCE/MATE
# or, if X was started manually from this TTY, just kill it
# 4. Run the modeset
sudo ./frame --modeset
# 5. Wait ~5 seconds, screen turns solid purple, then restores
# 6. Restart your session
sudo systemctl start sddm # or however you startExpected stderr:
frame: opened /dev/dri/card1
frame: SET_MASTER OK
frame: resources: 4 CRTCs, 5 connectors, 21 encoders
frame: framebuffer range 0x0 to 16384x16384
frame: using connector 507 on CRTC 79, mode 1920x1200
frame: created dumb buffer, 9216000 bytes
frame: filled with purple
frame: added framebuffer 84
frame: SETCRTC OK — displaying for 5 seconds
frame: restored original CRTC, cleanup done
The seconds in between are the proof: a CHasm asm binary putting pixels on the physical panel through nothing but raw DRM ioctls. No libdrm, no Mesa, no display server — just frame talking directly to the kernel through the same ABI Xorg uses.
The full cleanup path always runs (RMFB → munmap → DESTROY_DUMB → DROP_MASTER → close), and the CRTC is restored to its prior state. Worst case if something goes wrong: the screen stays purple until the kernel reprograms it (the next X start does this).
./frame --probe-input # enumerate /dev/input/event*
./frame --watch-input /dev/input/event3 # live decode events--probe-input scans /dev/input/event0..31, opens each one read-only,
calls EVIOCGNAME (=_IOC(_IOC_READ, 'E', 0x06, 64)), and lists what
came back:
frame: input devices:
event0: Power Button
event1: Lid Switch
event3: AT Translated Set 2 keyboard
event5: SynPS/2 Synaptics TouchPad
event7: SHK Bluetooth (Touch)
...
--watch-input PATH opens the named device and decodes each
input_event record (24 bytes: tv_sec, tv_usec, type, code,
value). Output line format is one of:
KEY 30 press # keyboard: 'A' down
KEY 30 release # 'A' up
BTN 272 press # mouse: BTN_LEFT down
REL 0 value=-3 # mouse moved 3 left
REL 1 value=2 # mouse moved 2 down
ABS 0 value=512 # touchpad position
SW 0 value=1 # switch (lid open/close, etc.)
Keycodes match /usr/include/linux/input-event-codes.h. Phase 4
layers the evdev→XKB→keysym translation on top so X clients see
real keysyms.
/dev/input/event* is crw-rw---- root:input on most distros. Either
sudo, or sudo usermod -aG input geir && reboot. From a VT (where
phase 2b runs anyway) this is automatic if you use sudo.
frame reads an optional ~/.framerc (line-based key = value, the CHasm
rc convention):
keymap = no # keyboard layout: us (default), no (Norwegian), be (Belgian)
sensitivity = 75 # pointer speed, percent of raw (default 100)
dwt = 400 # ms the touchpad ignores motion+taps after a keystroke (0 = off)
nightlight = 60 # Mod4+n warmth strength 0..100 (default 60)
sunlight = 40 # Mod4+b contrast strength 0..100 (default 40)
shake_find = 1 # wiggle the pointer to briefly enlarge it (0 = off)
magnify = 2 # Mod4+z magnifier lens zoom factor 2..8 (default 2)
cursor_color = ffffff # cursor fill colour, RRGGBB hex (default ffffff)
cursor_transparency = 50 # cursor % transparent: 0 solid .. 100 invisible (default 50)
cursor_accent = 00c800 # arrow fill over pressable items, RRGGBB (default green)
background = ~/.framebg # desktop wallpaper: a raw BGRX file at panel res
blank_timeout = 600 # idle seconds before the panel powers off (0 = never)
blank_key = Mod4+Escape # hotkey that powers the panel off NOW (none = off)
keymap: us (the default, or no file) is the standard US layout. no
is Norwegian: Shift+6 = &, the ø æ å keys, < > on the ISO key left
of Z, Norwegian punctuation, and AltGr (right Alt = ISO_Level3_Shift
= Mod5) for @ £ $ { } [ ] \ € ~. be is Belgian AZERTY: the
a z q w m letter moves, é è ç à ù µ , ² ³ left of 1, and AltGr for
| @ # { [ } \ € ~. Delivered to clients via
GetKeyboardMapping (6 keysyms/keycode, AltGr at level 3), so any X
client (glass, xterm, …) picks it up. Note: setxkbmap does not work on
frame — the layout comes from this key instead.
sensitivity: scales pointer motion (touchpad + mouse) by this percent.
100 is raw 1:1; lower values slow the cursor for finer control.
dwt (disable-while-typing / palm rejection): after any non-modifier
keystroke the touchpad ignores motion and taps for this many ms, and a
touch that begins inside that window stays ignored until the finger
lifts — a palm resting on the pad can't steer or click, even after you
stop typing. Modifier-only presses (Ctrl+click, Mod4+drag) don't mute,
and physical clickpad button presses always work. 0 disables.
nightlight / sunlight: colour-temperature presets applied through the
CRTC gamma LUT — one ioctl at scanout, zero per-pixel CPU. Mod4+n
toggles night-light (warms the screen: blue and green pulled down by the
nightlight strength); Mod4+b toggles sunlight mode (steepens
contrast by the sunlight strength for reading in bright light). Press the
same combo again to return to normal. Both strengths are 0..100.
shake_find: wiggle the pointer quickly left-right and the arrow briefly
grows (3x), so a lost cursor is easy to spot, then shrinks back on its own.
Pure gesture, no key. Detection is a few compares per motion event and
nothing at all when idle. 0 disables.
magnify / Mod4+z: a magnifier lens that follows the cursor,
showing the area under it enlarged by the magnify factor (2..8). Toggle
again to hide. It's drawn as a compositor overlay — moving it just damages
the old and new rects, so the desktop under it restores for free with no
backing store. Runs only while the lens is up; off costs one compare.
cursor_color / cursor_transparency: the arrow's interior fill and how
see-through it is. The black outline is always kept for contrast. Colour
is RRGGBB hex; transparency 0 is solid, 100 fully invisible. Free
alpha — the DRM cursor plane blends it in hardware.
cursor_accent: apps set a "hand" cursor over links and buttons; frame
shows the same arrow with this fill instead of swapping shapes. Text
fields get a real I-beam, scrot -s a crosshair, and the pointer hides
while you type (the blank cursor). All sprite swaps happen only on
crossings — zero idle cost. Sessions should export XCURSOR_CORE=1 so
toolkits ask for classic cursor-font glyphs frame can classify.
blank_timeout: screen auto-off, like xset dpms on Xorg. After this many
seconds without keyboard/mouse/touchpad input the compositor disables the
CRTC — the display engine and eDP panel power down completely. Any input
wakes it back up with a full repaint. Battery-tight by design: frame wakes
exactly once at the deadline (the idle poll timeout IS the deadline), and
while dark it sleeps indefinitely, swallowing client redraws. Default 600
(10 minutes); 0 disables.
blank_key: blank the panel immediately, without waiting for the timeout.
Modifiers (Shift, Ctrl, Alt/Mod1, Super/Mod4, Mod5) joined
with +, ending in a keysym name (same names as the keycode remap
lines). Handled server-side like the Ctrl+Alt+Fn VT switch, so it works
regardless of focus or grabs and the combo never reaches clients. Any
key press or pointer motion re-lights the panel. Default Mod4+Escape;
none disables.
background: a desktop wallpaper, drawn natively by the compositor (no feh,
no root-pixmap). frame carries no image decoder, so the value points at a
raw BGRX buffer at panel resolution (screen_w * screen_h * 4 bytes),
not a PNG/JPEG. The chasm-bg <image> helper (ships with
bolt) pre-renders any image to ~/.framebg
at the panel size — plus the bolt locker's ~/.lockbg.rgb — and adds this
line for you. Decode happens once, offline; frame just mmaps the buffer and
blits it. A wrong-sized file is ignored (falls back to the solid colour).
frame drives a second connected output natively: one wide framebuffer
spans both screens side by side (panel left, external right), each CRTC
scans its own slice via the SETCRTC pan offset, and page flips go to
both. RandR reports two monitors ("default" + "ext"), so tile pins
WS 10 to the external out of the box and xrandr --listmonitors shows
both. Hotplug is event-driven: a netlink uevent socket sits in the
serve loop's poll set (zero idle cost); plugging or unplugging a
display rebuilds the framebuffers at the new combined size, reprograms
the CRTCs and sends RRScreenChangeNotify so tile rediscovers and
retiles. ./frame N --fbtest2 fakes a second 1920x1080 output for
headless dual-head testing.
Note: with two outputs the background file no longer matches the
framebuffer size and is ignored (solid colour fallback) — re-render it
at the combined resolution if wallpaper-on-dual matters.
Pure NASM, no libc, single static ELF. Following CHasm conventions:
nasm -f elf64 frame.asm -o frame.o && ld frame.o -o frameState is BSS-allocated (no malloc). Per-client connection state lives in fixed slots; multi-client work in phase 4.
Debian/Ubuntu: every release ships a dependency-free static amd64
.deb — grab it from the
latest release and:
sudo apt install ./frame_*_amd64.debArch: a PKGBUILD is included in the repo root:
git clone https://github.com/isene/frame.git && cd frame && makepkg -siUnlicense - public domain.
Created by Geir Isene (https://isene.org) with pair-programming via Claude Code.
