{"id":51163908,"url":"https://github.com/isene/frame","last_synced_at":"2026-07-13T12:30:28.776Z","repository":{"id":366527101,"uuid":"1275879704","full_name":"isene/frame","owner":"isene","description":"Pure-asm X11 display server for the CHasm desktop. No libc, no Mesa, no Xlib — just NASM, syscalls, and the X11 wire protocol.","archived":false,"fork":false,"pushed_at":"2026-07-06T17:43:25.000Z","size":627,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-07-06T18:08:41.776Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Assembly","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"unlicense","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/isene.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-21T08:52:30.000Z","updated_at":"2026-07-06T17:43:31.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/isene/frame","commit_stats":null,"previous_names":["isene/frame"],"tags_count":56,"template":false,"template_full_name":null,"purl":"pkg:github/isene/frame","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/isene%2Fframe","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/isene%2Fframe/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/isene%2Fframe/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/isene%2Fframe/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/isene","download_url":"https://codeload.github.com/isene/frame/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/isene%2Fframe/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35423097,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-13T02:00:06.543Z","response_time":119,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-26T17:00:25.883Z","updated_at":"2026-07-13T12:30:28.770Z","avatar_url":"https://github.com/isene.png","language":"Assembly","funding_links":[],"categories":["Assembly"],"sub_categories":[],"readme":"# frame - Pure Assembly X11 Display Server\n\n\u003cimg src=\"img/frame.svg\" align=\"left\" width=\"150\" height=\"150\"\u003e\n\n![Version](https://img.shields.io/badge/version-0.0.132-blue)\n![Phase](https://img.shields.io/badge/phase-4%2F14-yellow)\n![Assembly](https://img.shields.io/badge/language-x86__64%20Assembly-purple)\n![License](https://img.shields.io/badge/license-Unlicense-green)\n![Platform](https://img.shields.io/badge/platform-Linux%20x86__64-blue)\n![Dependencies](https://img.shields.io/badge/dependencies-none-brightgreen)\n\nX11 display server written in x86_64 Linux assembly. No libc, no\ntoolkits, no FreeType, no Mesa, no Xlib. Just NASM source, direct\nsyscalls, the X11 wire protocol on a Unix socket, and the kernel's\nDRM/KMS + evdev interfaces.\n\nLong-range goal: serve enough of the X11 wire protocol (core + SHAPE +\nRENDER + XKB + COMPOSITE + DAMAGE + RANDR + MIT-SHM + XInput2 + XVideo) to host\nthe whole [CHasm](https://github.com/isene/chasm) desktop plus\narbitrary X clients — Firefox, VS Code, GIMP, Inkscape — all\nsoftware-rendered, all on a stack written end-to-end in asm.\n\n\u003cbr clear=\"left\"/\u003e\n\n![pointer (the Fe2O3 file manager) running in glass on frame](img/pointer-on-frame.jpg)\n\n*The whole stack, end to end in assembly: [pointer](https://github.com/isene/pointer)\n(a Rust TUI file manager) running inside [glass](https://github.com/isene/glass)\n(the asm terminal) on **frame** (the asm X server), shown on the laptop's\npanel via DRM/KMS — two-pane layout, syntax-highlighted preview, colour,\nkeyboard-driven. No libc, no Xlib, no Mesa anywhere in the path.*\n\n## Status: phase 4 of 14 (core protocol + compositor + drawing)\n\n| # | Phase | Status |\n|---|-------|--------|\n| 1 | Connection setup + Unix socket bind | ✓ shipped |\n| 2 | DRM/KMS probe (read-only ioctls, no master) | ✓ shipped |\n| 2b | DRM/KMS modeset (CreateDumb + AddFB + SetCRTC) | ✓ shipped |\n| 3 | evdev input + KeyPress / Motion routing | ✓ shipped |\n| 4a | Multi-client serve loop + dispatch + 16 opcode handlers | ✓ shipped |\n| 4b | Window tree + CreateWindow / MapWindow / ConfigureWindow | ✓ shipped |\n| 4c | Properties — ChangeProperty / GetProperty / GetAtomName | ✓ shipped |\n| 4d.1 | Input API — GrabKey / GrabKeyboard / real US keysym table | ✓ shipped |\n| 4d.2 | evdev → KeyPress event delivery | ✓ shipped |\n| 4e | SubstructureRedirect routing + ReparentWindow | ✓ shipped |\n| 4f | Software compositor — solid-colour rects on the panel via `--display` | ✓ shipped |\n| 4g | GCs + window backing store + PolyFillRectangle / PutImage | ✓ shipped |\n| 4h | Pixmaps + CopyArea / ClearArea / PolyRectangle | ✓ shipped |\n| 5 | Atoms + GetProperty / ChangeProperty / selections | ✓ shipped |\n| 6 | SHAPE extension | ✓ shipped (bounding + input regions — spot runs) |\n| 7 | GCs + drawing primitives | |\n| 8 | DRM/KMS atomic modeset upgrade | |\n| 9 | RENDER subset for glass emoji + ARGB | |\n| 10 | Cursor sprite + keyboard layout + clipboard | |\n| 11 | XKB (Firefox-compatible) | |\n| 12 | DAMAGE + COMPOSITE + FIXES | |\n| 13 | RANDR + XInput2 + MIT-SHM | RANDR + XI2 + MIT-SHM ✓ shipped |\n| 14 | First Firefox launch | |\n\nPhase 4 is the \"tile runs on frame\" milestone — self-hosting CHasm.\nPhase 14 is the \"Firefox runs on a 50k-line asm X server\" milestone.\n\n## Phase 1: what works\n\n```bash\nmake\n./frame                 # listens on display :7 (configurable: ./frame N)\nDISPLAY=:7 xdpyinfo     # connects, gets setup reply, sends QueryExtension\n```\n\n`frame` accepts an X11 client, validates its 12-byte connection-setup\nrequest (byte-order `l`, protocol 11.0, drains any auth tail), and\nemits a structurally valid setup reply describing:\n\n- One screen, 1920×1080, root window XID `0x80`\n- Two depths: 24 (TrueColor RGB) and 32 (TrueColor ARGB for glass\n  transparency)\n- One pixmap format (depth 24 in 32 bpp)\n\nSubsequent requests are logged to stderr (`req opcode=N len=M`) and\nsilently dropped. Real dispatch lands in phase 4 once the wire is\nproven and the DRM backend is in.\n\n## Phase 2: DRM/KMS probe\n\n```bash\n./frame --probe\n```\n\nOpens `/dev/dri/cardN`, enumerates resources, lists connectors:\n\n```\nframe: opened /dev/dri/card1, driver i915 v1.6.0\nframe: resources: 4 CRTCs, 5 connectors, 21 encoders\nframe: framebuffer range 0x0 to 16384x16384\n  connector 507: eDP-1 → connected, 1 modes, preferred 1920x1200 @ 120 Hz\n  connector 516: DisplayPort-1 → disconnected, 0 modes\n  ...\n```\n\nUses three read-only ioctls — `DRM_IOCTL_VERSION`,\n`DRM_IOCTL_MODE_GETRESOURCES`, `DRM_IOCTL_MODE_GETCONNECTOR`. None\nrequire DRM master, so this runs safely alongside an active Xorg.\n\n## Phase 2b: DRM/KMS modeset\n\n```bash\nsudo ./frame --modeset\n```\n\nPure-asm path: takes DRM master, picks the first connected\nconnector, queries its preferred mode, allocates a dumb buffer\n(`DRM_IOCTL_MODE_CREATE_DUMB`), mmap's it (`DRM_IOCTL_MODE_MAP_DUMB`\n+ `mmap`), fills it solid purple, binds it as a framebuffer\n(`DRM_IOCTL_MODE_ADDFB`), and points the CRTC at it\n(`DRM_IOCTL_MODE_SETCRTC`). Holds the picture for 5 seconds, then\nrestores the CRTC's prior state and frees everything in reverse.\n\n### Test recipe\n\nNeeds DRM master, which Xorg holds while it's running. From a TTY,\nafter stopping the display manager:\n\n```bash\n# 1. Switch to a TTY\nCtrl+Alt+F2\n\n# 2. Login as geir, navigate to the repo\ncd ~/Main/G/GIT-isene/frame\n\n# 3. Stop X (whichever applies — pick one)\nsudo systemctl stop sddm           # if KDE plasma\nsudo systemctl stop gdm            # if GNOME\nsudo systemctl stop lightdm        # if XFCE/MATE\n# or, if X was started manually from this TTY, just kill it\n\n# 4. Run the modeset\nsudo ./frame --modeset\n\n# 5. Wait ~5 seconds, screen turns solid purple, then restores\n\n# 6. Restart your session\nsudo systemctl start sddm      # or however you start\n```\n\nExpected stderr:\n\n```\nframe: opened /dev/dri/card1\nframe: SET_MASTER OK\nframe: resources: 4 CRTCs, 5 connectors, 21 encoders\nframe: framebuffer range 0x0 to 16384x16384\nframe: using connector 507 on CRTC 79, mode 1920x1200\nframe: created dumb buffer, 9216000 bytes\nframe: filled with purple\nframe: added framebuffer 84\nframe: SETCRTC OK — displaying for 5 seconds\nframe: restored original CRTC, cleanup done\n```\n\nThe seconds in between are the proof: a CHasm asm binary putting\npixels on the physical panel through nothing but raw DRM ioctls. No\nlibdrm, no Mesa, no display server — just frame talking directly to\nthe kernel through the same ABI Xorg uses.\n\n### Safety\n\nThe full cleanup path always runs (RMFB → munmap → DESTROY_DUMB →\nDROP_MASTER → close), and the CRTC is restored to its prior state.\nWorst case if something goes wrong: the screen stays purple until\nthe kernel reprograms it (the next X start does this).\n\n## Phase 3: evdev input\n\n```bash\n./frame --probe-input                       # enumerate /dev/input/event*\n./frame --watch-input /dev/input/event3     # live decode events\n```\n\n`--probe-input` scans `/dev/input/event0..31`, opens each one read-only,\ncalls `EVIOCGNAME` (=`_IOC(_IOC_READ, 'E', 0x06, 64)`), and lists what\ncame back:\n\n```\nframe: input devices:\n  event0: Power Button\n  event1: Lid Switch\n  event3: AT Translated Set 2 keyboard\n  event5: SynPS/2 Synaptics TouchPad\n  event7: SHK Bluetooth (Touch)\n  ...\n```\n\n`--watch-input PATH` opens the named device and decodes each\n`input_event` record (24 bytes: `tv_sec`, `tv_usec`, `type`, `code`,\n`value`). Output line format is one of:\n\n```\nKEY 30 press        # keyboard: 'A' down\nKEY 30 release      # 'A' up\nBTN 272 press       # mouse: BTN_LEFT down\nREL 0 value=-3      # mouse moved 3 left\nREL 1 value=2       # mouse moved 2 down\nABS 0 value=512     # touchpad position\nSW  0 value=1       # switch (lid open/close, etc.)\n```\n\nKeycodes match `/usr/include/linux/input-event-codes.h`. Phase 4\nlayers the evdev→XKB→keysym translation on top so X clients see\nreal keysyms.\n\n### Access\n\n`/dev/input/event*` is `crw-rw---- root:input` on most distros. Either\n`sudo`, or `sudo usermod -aG input geir \u0026\u0026 reboot`. From a VT (where\nphase 2b runs anyway) this is automatic if you use `sudo`.\n\n### Config (`~/.framerc`)\n\nframe reads an optional `~/.framerc` (line-based `key = value`, the CHasm\nrc convention):\n\n```\nkeymap = no                # keyboard layout: us (default) or no (Norwegian)\nsensitivity = 75           # pointer speed, percent of raw (default 100)\ndwt = 400                  # ms the touchpad ignores motion+taps after a keystroke (0 = off)\ncursor_color = ffffff      # cursor fill colour, RRGGBB hex (default ffffff)\ncursor_transparency = 50   # cursor % transparent: 0 solid .. 100 invisible (default 50)\ncursor_accent = 00c800     # arrow fill over pressable items, RRGGBB (default green)\nbackground = ~/.framebg    # desktop wallpaper: a raw BGRX file at panel res\nblank_timeout = 600        # idle seconds before the panel powers off (0 = never)\nblank_key = Mod4+Escape    # hotkey that powers the panel off NOW (none = off)\n```\n\n`keymap`: `us` (the default, or no file) is the standard US layout. `no`\nis Norwegian: `Shift+6` = `\u0026`, the `ø æ å` keys, `\u003c \u003e` on the ISO key left\nof `Z`, Norwegian punctuation, and **AltGr** (right Alt = ISO_Level3_Shift\n= Mod5) for `@ £ $ { } [ ] \\ € ~`. Delivered to clients via\n`GetKeyboardMapping` (6 keysyms/keycode, AltGr at level 3), so any X\nclient (glass, xterm, …) picks it up.\n\n`sensitivity`: scales pointer motion (touchpad + mouse) by this percent.\n`100` is raw 1:1; lower values slow the cursor for finer control.\n\n`dwt` (disable-while-typing / palm rejection): after any non-modifier\nkeystroke the touchpad ignores motion and taps for this many ms, and a\ntouch that *begins* inside that window stays ignored until the finger\nlifts — a palm resting on the pad can't steer or click, even after you\nstop typing. Modifier-only presses (Ctrl+click, Mod4+drag) don't mute,\nand physical clickpad button presses always work. `0` disables.\n\n`cursor_color` / `cursor_transparency`: the arrow's interior fill and how\nsee-through it is. The black outline is always kept for contrast. Colour\nis `RRGGBB` hex; transparency `0` is solid, `100` fully invisible. Free\nalpha — the DRM cursor plane blends it in hardware.\n\n`cursor_accent`: apps set a \"hand\" cursor over links and buttons; frame\nshows the same arrow with this fill instead of swapping shapes. Text\nfields get a real I-beam, `scrot -s` a crosshair, and the pointer hides\nwhile you type (the blank cursor). All sprite swaps happen only on\ncrossings — zero idle cost. Sessions should export `XCURSOR_CORE=1` so\ntoolkits ask for classic cursor-font glyphs frame can classify.\n\n`blank_timeout`: screen auto-off, like `xset dpms` on Xorg. After this many\nseconds without keyboard/mouse/touchpad input the compositor disables the\nCRTC — the display engine and eDP panel power down completely. Any input\nwakes it back up with a full repaint. Battery-tight by design: frame wakes\nexactly once at the deadline (the idle poll timeout IS the deadline), and\nwhile dark it sleeps indefinitely, swallowing client redraws. Default 600\n(10 minutes); `0` disables.\n\n`blank_key`: blank the panel immediately, without waiting for the timeout.\nModifiers (`Shift`, `Ctrl`, `Alt`/`Mod1`, `Super`/`Mod4`, `Mod5`) joined\nwith `+`, ending in a keysym name (same names as the `keycode` remap\nlines). Handled server-side like the Ctrl+Alt+Fn VT switch, so it works\nregardless of focus or grabs and the combo never reaches clients. Any\nkey press or pointer motion re-lights the panel. Default `Mod4+Escape`;\n`none` disables.\n\n`background`: a desktop wallpaper, drawn natively by the compositor (no feh,\nno root-pixmap). frame carries no image decoder, so the value points at a\n**raw BGRX buffer at panel resolution** (`screen_w * screen_h * 4` bytes),\nnot a PNG/JPEG. The `chasm-bg \u003cimage\u003e` helper (ships with\n[bolt](https://github.com/isene/bolt)) pre-renders any image to `~/.framebg`\nat the panel size — plus the bolt locker's `~/.lockbg.rgb` — and adds this\nline for you. Decode happens once, offline; frame just mmaps the buffer and\nblits it. A wrong-sized file is ignored (falls back to the solid colour).\n\n## External displays\n\nframe drives a second connected output natively: one wide framebuffer\nspans both screens side by side (panel left, external right), each CRTC\nscans its own slice via the SETCRTC pan offset, and page flips go to\nboth. RandR reports two monitors (\"default\" + \"ext\"), so tile pins\nWS 10 to the external out of the box and `xrandr --listmonitors` shows\nboth. Hotplug is event-driven: a netlink uevent socket sits in the\nserve loop's poll set (zero idle cost); plugging or unplugging a\ndisplay rebuilds the framebuffers at the new combined size, reprograms\nthe CRTCs and sends RRScreenChangeNotify so tile rediscovers and\nretiles. `./frame N --fbtest2` fakes a second 1920x1080 output for\nheadless dual-head testing.\n\nNote: with two outputs the `background` file no longer matches the\nframebuffer size and is ignored (solid colour fallback) — re-render it\nat the combined resolution if wallpaper-on-dual matters.\n\n## How it's built\n\nPure NASM, no libc, single static ELF. Following CHasm conventions:\n\n```bash\nnasm -f elf64 frame.asm -o frame.o \u0026\u0026 ld frame.o -o frame\n```\n\nState is BSS-allocated (no malloc). Per-client connection state lives\nin fixed slots; multi-client work in phase 4.\n\n## License\n\n[Unlicense](https://unlicense.org/) - public domain.\n\n## Credits\n\nCreated by Geir Isene (https://isene.org) with pair-programming via\nClaude Code.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fisene%2Fframe","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fisene%2Fframe","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fisene%2Fframe/lists"}