Monorepo for Aesthetic.Computer
aesthetic.computer
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
484 │
485 ▼
486BOOTX64.EFI (Linux 6.14.2 + EFI stub)
487 │
488 ▼
489Kernel unpacks LZ4 initramfs
490 │
491 ▼
492/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
498 │
499 ▼
500 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()
510 │
511 ├── 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
523 │
524 └── 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```