Monorepo for Aesthetic.Computer aesthetic.computer
at main 529 lines 22 kB view raw view rendered
1# AC Native OS Internals 2 3A technical narrative of how AC Native OS boots, renders, and runs interactive 4pieces on bare metal -- from UEFI power-on to the 60 fps main loop. 5 6--- 7 8## Boot Sequence 9 10### 1. UEFI Firmware 11 12The machine's UEFI firmware loads `EFI/BOOT/BOOTX64.EFI` from a FAT32 EFI 13System Partition. This file is actually a Linux kernel (6.14.2) compiled with 14`CONFIG_EFI_STUB=y` ([config-minimal:400](kernel/config-minimal#L400)), which 15lets the kernel act as its own EFI bootloader -- no GRUB, no systemd-boot, no 16bootloader at all. The kernel image has a CPIO initramfs archive compressed 17with LZ4 embedded directly inside it, containing the entire userspace. 18 19The kernel is minimal and purpose-built. Key features enabled in the config: 20 21- **Display**: `CONFIG_DRM=y`, `CONFIG_DRM_I915=y` for Intel GPU 22 ([config-minimal:2563](kernel/config-minimal#L2563), 23 [config-minimal:2617](kernel/config-minimal#L2617)) 24- **WiFi**: `CONFIG_IWLWIFI=y`, `CONFIG_IWLMVM=y`, `CONFIG_CFG80211=y` 25 ([config-minimal:1723](kernel/config-minimal#L1723), 26 [config-minimal:1726](kernel/config-minimal#L1726)) 27- **Audio**: `CONFIG_SOUND=y`, `CONFIG_SND=y`, `CONFIG_SND_HDA=y` 28 ([config-minimal:2796](kernel/config-minimal#L2796), 29 [config-minimal:2897](kernel/config-minimal#L2897)) 30- **Input**: `CONFIG_INPUT_EVDEV=y` 31 ([config-minimal:1823](kernel/config-minimal#L1823)) 32- **Swap**: `CONFIG_ZRAM=y` for compressed RAM swap 33 ([config-minimal:1266](kernel/config-minimal#L1266)) 34- **Namespaces**: `CONFIG_NAMESPACES=y`, `CONFIG_USER_NS=y`, `CONFIG_NET_NS=y` 35 ([config-minimal:180](kernel/config-minimal#L180)) 36- **devtmpfs**: `CONFIG_DEVTMPFS_MOUNT=y` for automatic `/dev` population 37 ([config-minimal:1153](kernel/config-minimal#L1153)) 38 39### 2. Init Script 40 41After the kernel unpacks the initramfs, it runs `/init` -- a 39-line shell 42script that is the first userspace code to execute 43([init:1](initramfs/init#L1)). 44 45The init script does the minimum necessary before handing off to the native 46binary: 47 481. **Mount virtual filesystems**: `/proc`, `/sys`, `/dev` (devtmpfs), 49 `/dev/pts`, `/dev/shm`, `/tmp`, `/run` 50 ([init:4-11](initramfs/init#L4)) 51 522. **Set up zram swap**: Loads the `zram` module, creates a 1 GB compressed RAM 53 swap device. This effectively doubles available memory, which is critical 54 since Firefox and GTK need significant RAM beyond what the tmpfs-backed 55 initramfs provides ([init:18-19](initramfs/init#L18)) 56 573. **Bring up loopback**: `ip link set lo up` -- needed later for Claude Code's 58 OAuth callback server ([init:22](initramfs/init#L22)) 59 604. **Set environment**: `PATH`, SSL certificate paths for curl/OpenSSL 61 ([init:24-27](initramfs/init#L24)) 62 635. **Create identity files**: Writes minimal `/etc/group` and `/etc/passwd` for 64 seatd, which needs to look up the `root` group later during the cage 65 transition ([init:30-31](initramfs/init#L30)) 66 676. **Performance governor**: Sets all CPU cores to `performance` mode 68 ([init:34-36](initramfs/init#L34)) 69 707. **Exec ac-native**: Replaces itself with the native binary via 71 `exec /ac-native /piece.mjs` ([init:39](initramfs/init#L39)). The `exec` 72 means ac-native inherits PID 1. 73 74### 3. ac-native as PID 1 75 76The native binary starts by checking if it is PID 1 (direct boot) or running 77under a compositor (cage child) 78([ac-native.c:1535](src/ac-native.c#L1535)). This fork in logic drives the 79entire architecture: the same binary serves two roles. 80 81**When PID 1** (first boot, DRM mode): 82 831. **`mount_minimal_fs()`**: Re-mounts core filesystems. The init script 84 already mounted them, but ac-native re-mounts devtmpfs to pick up any 85 devices that appeared after the init script ran (notably `/dev/dri/card0` 86 from i915). It also re-enables zram swap and brings up loopback 87 ([ac-native.c:215-248](src/ac-native.c#L215)). Waits up to 1 second for 88 `/dev/dri/card0` or `/dev/fb0` to appear 89 ([ac-native.c:244-248](src/ac-native.c#L244)). 90 912. **Display init (DRM)**: Calls `drm_init()` which opens `/dev/dri/card0`, 92 enumerates connectors, picks the best mode, and sets up dumb buffer 93 page-flipping ([ac-native.c:1617](src/ac-native.c#L1617)). 94 953. **Framebuffer creation**: Creates a software framebuffer at 1/3 display 96 resolution (e.g., 960x640 for a 2880x1920 panel). The `pixel_scale=3` 97 default gives chunky pixels that are nearest-neighbor scaled to the display 98 ([ac-native.c:1585-1626](src/ac-native.c#L1585)). 99 1004. **Graphics and font init**: Initializes the immediate-mode 2D graphics 101 context and bitmap font renderer 102 ([ac-native.c:1640-1642](src/ac-native.c#L1640)). 103 1045. **USB log mount**: Tries to mount the EFI boot partition at `/mnt` for 105 persistent logging and config 106 ([ac-native.c:1651](src/ac-native.c#L1651)). 107 1086. **Audio init**: Opens ALSA at 192 kHz stereo, 32 voices, with reverb and 109 glitch effects. Waits up to 4 seconds for the sound card to appear (HDA 110 probe can lag behind i915 GPU init) 111 ([audio.c:616-664](src/audio.c#L616)). Plays a boot beep immediately 112 ([ac-native.c:1683](src/ac-native.c#L1683)). 113 1147. **TTS init**: Initializes Flite text-to-speech engine, fed through the audio 115 system's ring buffer 116 ([ac-native.c:1684](src/ac-native.c#L1684)). 117 1188. **Boot animation**: `draw_startup_fade()` -- a startup fade from black to 119 white that hides kernel text, displays the user's handle (read from 120 `/mnt/config.json`), and speaks a greeting via TTS 121 ([ac-native.c:1692](src/ac-native.c#L1692)). During this animation, holding 122 `W` triggers the install-to-internal-drive flow. 123 1249. **Input init (DRM path)**: `input_init()` scans `/dev/input/` for evdev 125 devices, opening anything with key, absolute axis, relative axis, or switch 126 capabilities ([input.c:315-374](src/input.c#L315)). NuPhy keyboards are 127 detected by vendor ID and flagged for analog hidraw handling 128 ([input.c:358-366](src/input.c#L358)). 129 13010. **WiFi init**: `wifi_init()` spawns a background thread that manages 131 wpa_supplicant, scanning, connection, and DHCP 132 ([wifi.c:516-535](src/wifi.c#L516)). The thread runs a 10-second 133 connectivity watchdog that auto-reconnects on link loss 134 ([wifi.c:484-503](src/wifi.c#L484)). 135 13611. **JS runtime init**: Initializes QuickJS-ng and registers all AC API 137 bindings (graphics, input, audio, wifi, networking, PTY, camera, 3D) 138 ([ac-native.c:1751](src/ac-native.c#L1751)). 139 14012. **Piece loading**: Reads `/mnt/config.json` for the configured boot piece 141 (defaults to `/piece.mjs`), resolves aliases like `"claude"` to 142 `"terminal"`, and loads the piece's JavaScript module 143 ([ac-native.c:1762-1818](src/ac-native.c#L1762)). 144 14513. **Ready melody**: After the piece loads, waits for TTS to finish, plays a 146 ready melody, and prewarms the audio engine for zero-latency first keypress 147 ([ac-native.c:1839-1848](src/ac-native.c#L1839)). 148 14914. **Call `boot()`**: Invokes the piece's `boot()` lifecycle function 150 ([ac-native.c:1855](src/ac-native.c#L1855)). 151 152**When cage child** (running under Wayland compositor): 153 154The binary detects `WAYLAND_DISPLAY` in the environment and takes a shorter 155path ([ac-native.c:1543-1558](src/ac-native.c#L1543)): skips filesystem 156mounting (parent already did it), connects to the Wayland compositor via 157`wayland_display_init()`, initializes input via `input_init_wayland()`, and 158jumps directly to the JS runtime. No boot animation, no install flow, no 159audio re-init (cage child opens its own ALSA handle). 160 161### 4. Cage Transition 162 163After the piece's `boot()` completes in DRM mode, ac-native attempts a graceful 164transition from DRM direct rendering to a Wayland compositor session. This is 165the key architectural trick: fast DRM boot (sub-second to first pixel) followed 166by a compositor that enables browser popups for OAuth and web content 167([ac-native.c:1860-1867](src/ac-native.c#L1860)). 168 169The transition proceeds as follows: 170 1711. **Close audio**: The DRM parent releases ALSA so the cage child can open it 172 ([ac-native.c:1870-1871](src/ac-native.c#L1870)) 173 1742. **Release DRM master**: Gives up exclusive GPU access so cage can take it 175 ([ac-native.c:1874](src/ac-native.c#L1874)) 176 1773. **Fork**: The child process will become the cage session 178 ([ac-native.c:1877-1878](src/ac-native.c#L1877)) 179 1804. **Child process setup**: 181 - Sets `WLR_RENDERER=pixman` (software rendering -- no GPU acceleration 182 needed since we're doing software framebuffer anyway) 183 ([ac-native.c:1882](src/ac-native.c#L1882)) 184 - Sets `WLR_BACKENDS=drm` and `WLR_LIBINPUT_NO_DEVICES=1` (cage uses DRM 185 for output but doesn't need libinput since ac-native reads evdev directly) 186 ([ac-native.c:1883-1885](src/ac-native.c#L1883)) 187 - Starts **seatd** (minimal seat manager) and waits up to 3 seconds for 188 `/run/seatd.sock` ([ac-native.c:1932-1955](src/ac-native.c#L1932)) 189 - Execs `cage -s -- /ac-native /piece.mjs` -- cage launches ac-native as 190 its Wayland client ([ac-native.c:1965](src/ac-native.c#L1965)) 191 1925. **Parent process**: Waits for the cage child to exit, copies cage stderr and 193 child logs to USB, cleans up seatd 194 ([ac-native.c:1970-1996](src/ac-native.c#L1970)). If the cage child 195 requested reboot or poweroff, the parent (still PID 1) executes it 196 ([ac-native.c:2002-2009](src/ac-native.c#L2002)). If cage failed, the 197 parent reclaims DRM master, re-inits audio, and continues in DRM mode as a 198 fallback ([ac-native.c:2015-2019](src/ac-native.c#L2015)). 199 200### 5. Main Loop 201 202The main loop runs at 60 fps with `frame_sync_60fps()` 203([ac-native.c:2030-2033](src/ac-native.c#L2030)): 204 2051. **Input poll**: `input_poll()` -- in Wayland mode, dispatches the Wayland 206 event queue (keyboard/pointer/touch listeners fire); in DRM mode, reads 207 evdev devices directly and handles DRM handoff signals 208 ([ac-native.c:2034-2145](src/ac-native.c#L2034)) 209 2102. **Hardware keys**: Processes Ctrl+=/- for pixel scale changes (dynamic 211 resolution), volume keys, power button for shutdown 212 ([ac-native.c:2189-2358](src/ac-native.c#L2189)) 213 2143. **JS lifecycle**: Calls `js_call_act()` (events), `js_call_sim()` (logic), 215 `js_call_paint()` (rendering) every frame 216 2174. **Display present**: Copies the software framebuffer to the display surface 218 with nearest-neighbor scaling 219 2205. **Performance logging**: Every 30 seconds, writes frame timing CSV to USB 221 for crash-resilient diagnostics 222 ([ac-native.c:44-49](src/ac-native.c#L44)) 223 224--- 225 226## Display Architecture 227 228### DRM Direct Mode 229 230The primary display path uses Linux DRM (Direct Rendering Manager) with **dumb 231buffers** -- no GPU acceleration, pure software rendering 232([drm-display.c:1](src/drm-display.c#L1)). The display module opens 233`/dev/dri/card0`, enumerates connectors, picks the preferred mode, and creates 234two dumb buffers for page-flipping. An SDL2/KMSDRM backend exists as an 235optional compile flag (`USE_SDL`) but is not used in production 236([drm-display.c:13-22](src/drm-display.c#L13)). 237 238Rendering happens at 1/3 display resolution by default (pixel_scale=3) into an 239`ACFramebuffer`, then nearest-neighbor scaled to the display resolution during 240`ac_display_present()`. This gives the characteristic chunky pixel aesthetic 241while keeping the rendering workload small. 242 243### Wayland SHM Mode 244 245When running under cage, the display switches to `wayland-display.c`, which 246creates a `wl_surface` with XDG shell decorations and renders to shared-memory 247buffers in `WL_SHM_FORMAT_ARGB8888` 248([wayland-display.c:1-3](src/wayland-display.c#L1)). Double-buffered SHM pools 249are allocated via `memfd_create` and `mmap` 250([wayland-display.c:152-177](src/wayland-display.c#L152)). The compositor 251(cage, using wlroots with `WLR_RENDERER=pixman`) composites the ac-native 252surface with any browser windows. 253 254The Wayland init sequence: 2551. Connect to compositor via `wl_display_connect(NULL)` 256 ([wayland-display.c:189](src/wayland-display.c#L189)) 2572. Bind registry globals: `wl_compositor`, `wl_shm`, `xdg_wm_base`, `wl_seat` 258 ([wayland-display.c:198-200](src/wayland-display.c#L198)) 2593. Create surface and XDG toplevel 260 ([wayland-display.c:211-218](src/wayland-display.c#L211)) 2614. Allocate double-buffered SHM pool 262 ([wayland-display.c:162-169](src/wayland-display.c#L162)) 263 264### DRM-to-Cage Handoff 265 266The transition from DRM to Wayland is designed to be invisible to the user. 267The DRM parent renders the boot animation and first piece frame, then 268`drm_release_master()` unlocks the GPU. Cage's wlroots backend immediately 269picks up the DRM device and ac-native (now a Wayland client) resumes rendering 270to SHM buffers. If cage fails, the parent calls `drm_acquire_master()` and 271falls back to DRM mode seamlessly 272([ac-native.c:2015-2018](src/ac-native.c#L2015)). 273 274--- 275 276## Input Architecture 277 278### Evdev Direct (DRM mode and fallback) 279 280`input_init()` scans `/dev/input/` for event devices and opens them with 281`O_RDONLY | O_NONBLOCK` ([input.c:315-374](src/input.c#L315)). It checks 282capability bits (`EV_KEY`, `EV_ABS`, `EV_REL`, `EV_SW`) to filter relevant 283devices. Special handling: 284 285- **Tablet mode switch** (`SW_TABLET_MODE`): Reads initial state from the 286 ThinkPad ACPI hotkey interface, with evdev switch events as live updates 287 ([input.c:345-356](src/input.c#L345)) 288- **NuPhy analog keyboards**: Detected by USB vendor ID, evdev key events are 289 suppressed in favor of hidraw analog pressure data 290 ([input.c:358-366](src/input.c#L358)) 291 292`input_poll()` reads all pending events from all device file descriptors each 293frame ([input.c:396-460](src/input.c#L396)). In Wayland mode with evdev 294fallback, it polls both sources. 295 296### Wayland Seat (under cage) 297 298`input_init_wayland()` binds to the compositor's `wl_seat` to receive keyboard, 299pointer, and touch events through Wayland protocol listeners 300([input.c:1032-1061](src/input.c#L1032)). Since cage runs without 301udev/libinput (`WLR_LIBINPUT_NO_DEVICES=1`), the Wayland seat often has no 302capabilities. In that case, it falls back to direct evdev polling while still 303using the Wayland event dispatch loop 304([input.c:1064-1091](src/input.c#L1064)). 305 306Software key repeat is implemented in `input_poll()` since Wayland doesn't 307send `value=2` repeat events ([input.c:434-449](src/input.c#L434)). 308 309--- 310 311## Audio 312 313The audio engine uses ALSA directly at 192 kHz stereo with a 192-sample period 314(~1 ms latency) ([audio.h:7-9](src/audio.h#L7)). It supports: 315 316- **32-voice polyphonic synthesizer**: Sine, triangle, sawtooth, square, and 317 filtered noise waveforms with per-voice frequency, volume, pan, attack, and 318 decay envelopes ([audio.h:29-49](src/audio.h#L29)) 319- **Sample playback**: 12 simultaneous sample voices with pitch shifting and 320 looping, plus microphone recording with hot-mic mode 321 ([audio.h:51-61](src/audio.h#L51)) 322- **Effects**: Room reverb (simple delay-line) and bit-crush glitch, with 323 smoothed wet/dry mix ([audio.h:88-102](src/audio.h#L88)) 324- **TTS integration**: Flite speech synthesis fed through a ring buffer into 325 the audio thread ([audio.h:109-115](src/audio.h#L109)) 326- **HDMI output**: Optional secondary audio output with downsampling and 327 low-pass filtering ([audio.h:141-148](src/audio.h#L141)) 328 329Audio init waits up to 4 seconds for the sound card to appear, since i915 GPU 330initialization can delay HDA codec probe 331([audio.c:658-664](src/audio.c#L658)). The engine writes diagnostics to 332`/mnt/ac-audio.log` on the USB partition 333([audio.c:667-668](src/audio.c#L667)). 334 335--- 336 337## Networking 338 339### WiFi 340 341The WiFi subsystem wraps `iw`, `wpa_supplicant`, and `udhcpc`/`dhcpcd` behind 342a threaded state machine ([wifi.c:1](src/wifi.c#L1)). `wifi_init()` checks for 343the `iw` binary, logs iwlwifi firmware availability, and spawns a background 344thread ([wifi.c:516-535](src/wifi.c#L516)). 345 346The thread runs a command loop with a 2-second timeout for watchdog polling 347([wifi.c:448-510](src/wifi.c#L448)): 348 349- **Scan**: Calls `iw dev wlanX scan` to enumerate available networks 350- **Connect**: Configures and launches `wpa_supplicant`, then runs DHCP 351- **Disconnect**: Kills `wpa_supplicant` and releases the IP 352- **Watchdog**: Every ~10 seconds, checks `ip -4 addr show` for a live IP 353 address. On loss, auto-reconnects with exponential backoff 354 ([wifi.c:484-503](src/wifi.c#L484)) 355 356### WebSocket and UDP 357 358The runtime includes a WebSocket client (`ws-client.c`) for session server 359connections and a UDP client (`udp-client.c`) for low-latency multiplayer data 360([js-bindings.h:13-14](src/js-bindings.h#L13)). These are exposed to pieces 361through the JS API as `net.socket()` and `net.udp()`. 362 363--- 364 365## Process Model 366 367The system has a distinctive process tree that changes shape during boot: 368 369### Phase 1: DRM Boot 370 371``` 372PID 1: ac-native (DRM direct rendering) 373``` 374 375Single process. Owns the DRM master, evdev devices, ALSA, and WiFi thread. 376 377### Phase 2: Cage Transition 378 379``` 380PID 1: ac-native (waiting, DRM released) 381 └── cage (Wayland compositor, seatd session) 382 └── ac-native (Wayland client, piece runner) 383``` 384 385PID 1 forks a child that execs cage. Cage then launches ac-native as its 386Wayland client. The parent (PID 1) blocks on `waitpid()` until the cage session 387ends ([ac-native.c:1974](src/ac-native.c#L1974)). 388 389### Browser Popup (DRM fallback path) 390 391When running in DRM mode without cage, browser popups for OAuth use a different 392pattern ([ac-native.c:2060-2133](src/ac-native.c#L2060)): 393 394``` 395PID 1: ac-native (DRM released, waiting) 396 └── child: seatd + cage + firefox (browser session) 397``` 398 399The child forks, starts seatd, launches `cage -s -- firefox --kiosk <url>`, 400and exits when the browser closes. The parent reclaims DRM master and resumes 401rendering. 402 403### Claude Code (PTY) 404 405Claude Code runs as a child process spawned via `forkpty()` 406([pty.c:523](src/pty.c#L523)). The child sets up a terminal environment 407(`TERM=xterm-256color`), loads OAuth credentials from 408`/mnt/claude-credentials.json`, fakes `DISPLAY=:0` so Claude will call 409`xdg-open` for browser-based authentication, and execs the `claude` binary 410([pty.c:531-566](src/pty.c#L531)). The parent reads PTY output via a 411non-blocking master fd and renders it through a VT100 terminal emulator 412implemented in `pty.c`. 413 414--- 415 416## Build System 417 418### ac-os Script 419 420The `ac-os` script is the single entry point for all build operations 421([ac-os:1-8](ac-os#L1)): 422 423- `ac-os build` -- compile binary, pack initramfs, build kernel 424- `ac-os flash` -- build + write to USB 425- `ac-os upload` -- **always rebuilds first**, then uploads OTA release. 426 The kernel embeds `AC_GIT_HASH` and `AC_BUILD_NAME` at compile time, so 427 uploading without rebuilding would serve a stale version string. 428- `ac-os flash+upload` -- all of the above 429 430### Makefile 431 432The Makefile compiles 17 C source files plus QuickJS-ng (the JavaScript engine) 433from source ([Makefile:60-76](Makefile#L60), 434[Makefile:93-94](Makefile#L93)): 435 436``` 437ac-native.c, drm-display.c, framebuffer.c, graph.c, graph3d.c, 438font.c, color.c, input.c, audio.c, wifi.c, tts.c, ws-client.c, 439udp-client.c, camera.c, pty.c, machines.c, js-bindings.c 440``` 441 442Plus conditionally: `wayland-display.c` (when `USE_WAYLAND=1`). 443 444Key build details: 445 446- **Compiler flags**: `-O2 -Wall -Wextra -std=gnu11` with version metadata 447 baked in via `-DAC_GIT_HASH` and `-DAC_BUILD_NAME` 448 ([Makefile:15](Makefile#L15)) 449- **Linking**: DRM, ALSA, OpenSSL, Flite TTS, optionally Wayland client and 450 SDL2 ([Makefile:30-54](Makefile#L30)) 451- **Static linking**: Supported with musl-gcc; otherwise dynamic linking with 452 `.so` files bundled in the initramfs 453 ([Makefile:18-27](Makefile#L18)) 454- **Wayland protocol**: `xdg-shell-client-protocol.c/h` generated from the 455 system's `xdg-shell.xml` via `wayland-scanner` 456 ([Makefile:48-54](Makefile#L48)) 457 458### Kernel 459 460Linux 6.14.2 compiled with GCC 15.2.1 461([config-minimal:5](kernel/config-minimal#L5)). The kernel config is 462purpose-built for Intel laptop hardware with EFI stub boot. The initramfs 463(containing ac-native, all shared libraries, Firefox, Claude Code, firmware 464blobs, and bundled pieces) is LZ4-compressed and embedded directly into the 465kernel image, producing a single `vmlinuz` file that is copied to the EFI 466partition as `BOOTX64.EFI`. 467 468### JS Runtime 469 470Pieces run on QuickJS-ng, compiled from source as part of the build 471([Makefile:93-94](Makefile#L93)). The `ACRuntime` struct holds cached 472references to piece lifecycle functions (`boot_fn`, `paint_fn`, `act_fn`, 473`sim_fn`, `leave_fn`, `beat_fn`) and all subsystem pointers 474([js-bindings.h:20-98](src/js-bindings.h#L20)). `js-bindings.c` registers 475the full AC API surface: graphics primitives, audio synthesis, WiFi control, 476WebSocket/UDP networking, camera, PTY terminal, OTA updates, and 3D rendering. 477 478--- 479 480## Lifecycle Summary 481 482``` 483UEFI firmware 484485486BOOTX64.EFI (Linux 6.14.2 + EFI stub) 487488489Kernel unpacks LZ4 initramfs 490491492/init (shell script, 39 lines) 493 ├── mount proc/sys/dev/pts/shm/tmp/run 494 ├── zram swap (1 GB compressed) 495 ├── loopback up 496 ├── CPU performance governor 497 └── exec /ac-native /piece.mjs 498499500 ac-native (PID 1, DRM mode) 501 ├── mount_minimal_fs() 502 ├── drm_init() ──────────────── first pixels on screen 503 ├── audio_init() ────────────── boot beep 504 ├── tts_init() + startup fade 505 ├── input_init() 506 ├── wifi_init() ─────────────── background thread 507 ├── js_init() + js_load_piece() 508 ├── ready melody 509 ├── js_call_boot() 510511 ├── cage transition (fork) 512 │ ├── child: seatd → cage → ac-native (Wayland client) 513 │ │ ├── wayland_display_init() 514 │ │ ├── input_init_wayland() 515 │ │ └── main loop (60 fps) 516 │ │ ├── input_poll() 517 │ │ ├── js_call_act() 518 │ │ ├── js_call_sim() 519 │ │ ├── js_call_paint() 520 │ │ └── display present (SHM buffer) 521 │ │ 522 │ └── parent: waitpid(cage) → reboot/poweroff/DRM fallback 523524 └── DRM fallback main loop (if cage unavailable) 525 ├── input_poll() (evdev) 526 ├── DRM handoff for browser popups 527 ├── js_call_act/sim/paint() 528 └── display present (dumb buffer flip) 529```