Skip to content

Commit b7ef2ea

Browse files
committed
docs: add a docs/ book served by GitHub Pages, slim the README
Mirror cocoon's docs layout: topic-named pages plus an index.md landing (tagline, CLI diagram, Guides + Features lists) under jekyll-theme-primer; keep the README to a tagline, quickstart, and links. Refresh the image pull section for the parallel Range download.
1 parent b942f22 commit b7ef2ea

13 files changed

Lines changed: 520 additions & 212 deletions

README.md

Lines changed: 31 additions & 212 deletions
Large diffs are not rendered by default.

docs/_config.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
title: cocoon-macos
2+
description: macOS VM engine for x86 Linux/KVM, built on cocoon
3+
theme: jekyll-theme-primer

docs/cli.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# CLI Reference
2+
3+
The CLI mirrors cocoon's `vm` / `image` command surface, trimmed to the macOS VM path.
4+
5+
## Images
6+
7+
```bash
8+
# pull the golden qcow2 from ghcr into the local store (cocoon cloudimg; /var/lib/cocoon-macos)
9+
cocoon-macos image pull ghcr.io/cocoonstack/cocoon-macos/tahoe:26
10+
cocoon-macos image list # table (NAME TYPE SIZE DIGEST CREATED); -o json for JSON
11+
cocoon-macos image inspect <ref>
12+
cocoon-macos image rm <ref>
13+
```
14+
15+
See [Images](images.md) for the store layout and the parallel-Range download.
16+
17+
## VMs
18+
19+
```bash
20+
# clone the golden image into a per-VM overlay and boot it (x86 Linux + /dev/kvm).
21+
# IMAGE is a store ref or a direct qcow2 path; firmware defaults to the doctor's install.
22+
cocoon-macos vm run ghcr.io/cocoonstack/cocoon-macos/tahoe:26 \
23+
--name m1 --cpus 4 --memory 8192 --ssh-port 2222 --vnc 1 --random-smbios
24+
25+
cocoon-macos vm list # table (NAME STATE CPU MEM NET VNC SSH IMAGE CREATED); -o json for JSON
26+
cocoon-macos vm inspect m1 # full record as JSON
27+
cocoon-macos vm stop m1
28+
cocoon-macos vm rm m1
29+
# also: create (no boot), start, console
30+
```
31+
32+
- `create` scaffolds the VM (overlay, identity, network, record) without booting; `run` = `create` +
33+
boot; `start` boots a created/stopped VM.
34+
- `run` is atomic: if the boot fails it removes everything it just created (no half-made VM left
35+
behind).
36+
37+
Networking (`--net`) and VNC (`--vnc` / `--vnc-password`) are covered in
38+
[Networking & VNC](networking.md); snapshot/clone and `--data-disk` in
39+
[Snapshot, Clone & Data Disks](snapshots.md).
40+
41+
## What `vm run` does
42+
43+
1. `qemu-img create -b <golden> overlay.qcow2` — instant copy-on-write clone of the golden image.
44+
2. Copy a per-VM `OVMF_VARS`.
45+
3. With `--random-smbios`, copy OpenCore per-VM and inject a generated identity into its
46+
`config.plist` `PlatformInfo/Generic` via a `qemu-nbd` mount — model stays `iMac19,1` (proven to
47+
boot Tahoe), only serial/MLB/UUID/ROM are randomized. The identity is recorded and shown by
48+
`vm inspect`.
49+
4. Launch `qemu-system-x86_64` daemonized with the boot recipe (a `Skylake-Client-v4` CPU spoofing
50+
`GenuineIntel`, `isa-applesmc` OSK, OVMF, the LongQT OpenCore loader, and the macOS qcow2). The
51+
same recipe boots macOS identically on Intel and AMD; on AMD it also sets `kvm.ignore_msrs=1`
52+
(macOS reads MSRs an AMD host lacks). See [Boot, Firmware & GUI](vm.md).
53+
54+
State is recorded under `--state-dir` / `$COCOON_MACOS_HOME` (default `/var/lib/cocoon-macos`).

docs/e2e.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# E2E Regression
2+
3+
`scripts/e2e.sh` is a testbed lifecycle regression (modeled on cocoon's), in two tiers.
4+
5+
## `[DUMMY]` — file-level, no macOS boot
6+
7+
Runs on any x86 Linux/KVM host against a tiny throwaway qcow2 (no macOS boot):
8+
9+
- Image store (`pull`-less `list`/`rm`).
10+
- `vm create` overlay-on-shared-base.
11+
- `--random-smbios` serial/MAC uniqueness + MAC == ROM.
12+
- `snapshot`/`restore` (qcow2-internal tag rollback).
13+
- `clone` (CoW on the shared base + distinct identity + clone-of-clone backing).
14+
- `stop` PID-reuse-safe teardown.
15+
- `--net bridge` auto-TAP create + `tap_owned` + teardown-on-`rm` + user-`--tap` preservation +
16+
negative paths.
17+
18+
Post-conditions assert no leaked TAPs/procs and backing-chain integrity. The `--random-smbios` rows
19+
need `root` + `nbd` + a real `CM_OPENCORE`; the `--net` rows need `root` + a test bridge (they `SKIP`,
20+
not `FAIL`, otherwise).
21+
22+
## `[REAL]` — boots the golden image
23+
24+
Boots `tahoe:26` from the store, passes the OpenCore picker over the HMP monitor, and asserts
25+
SSH-ready (`sw_vers` 26.x) + in-guest serial == injected. Testbed-only.
26+
27+
## Running
28+
29+
```bash
30+
sudo CM_BIN=./cocoon-macos CM_OPENCORE=.../OpenCore.qcow2 ./scripts/e2e.sh # [DUMMY] (30 rows)
31+
sudo CM_HOME=~/cm-demo CM_OPENCORE=... CM_OVMF_CODE=... CM_OVMF_VARS=... \
32+
./scripts/e2e.sh --real-only
33+
```
34+
35+
The `[DUMMY]` tier is the behavioral gate today's CI structurally can't give (CI is pure `go test` /
36+
`vet` / build — nothing opens `/dev/kvm` or mutates netlink); it belongs on a privileged self-hosted
37+
KVM runner. `[REAL]` stays a manual/nightly testbed run (~15 GB + cold boot).

docs/image-pipeline.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# CI Image Pipeline
2+
3+
The golden images are built entirely by CI:
4+
`.github/workflows/build-macos-image.yml` + `scripts/build-qemu-macos.sh`.
5+
6+
It is a `workflow_dispatch` with a `macos` version and a `stage`. This pipeline is **image-only**
7+
(no Go); the CLI end-to-end (`vm run` + `--random-smbios`) is exercised separately on a KVM testbed,
8+
keeping image and Go CI apart.
9+
10+
## `macos` — which OS
11+
12+
`macos` selects the OS to build and where it lands on ghcr (the build script derives
13+
`MACOS_SHORTNAME` / `GHCR_REPO` / `GHCR_TAG` from it):
14+
15+
| `macos` | fetch-macOS shortname | ghcr repo:tag |
16+
|---------|-----------------------|---------------|
17+
| `tahoe` (default) | `tahoe` | `…/tahoe:26` — macOS 26, the **last Intel** macOS |
18+
| `sequoia` | `sequoia` | `…/sequoia:15` — macOS 15 (n-1, still security-supported) |
19+
20+
The same OSX-KVM multi-version OpenCore and the same provision recipe build either OS — only the
21+
recovery shortname and tag differ.
22+
23+
## `stage` — how far the build goes
24+
25+
Shown for `tahoe:26`; the actual `repo:tag` follows `macos`.
26+
27+
| stage | what |
28+
|-------|------|
29+
| `boot` | Smoke: boot OpenCore → macOS Recovery (proves KVM + OpenCore + recovery). |
30+
| `install` | Full install from scratch → capture → push `<repo>:<tag>-base` (~65 min). |
31+
| `setup` | Pull `<repo>:<tag>-base` → boot Recovery → `provision-macos.sh` (SA-skip recipe + user + SSH) → push `<repo>:<tag>`. |
32+
| `slim` | Pull `<repo>:<tag>` → boot → reclaim stale clusters → re-push `<repo>:<tag>` (smaller). |
33+
| `verify` | Pull `<repo>:<tag>` → boot → confirm login + SSH (`cocoon@localhost`). |
34+
35+
## Automation primitives
36+
37+
- **`scripts/qmp-input.py`** — QMP absolute mouse click/move, keyboard type/chord, **Tesseract + PIL
38+
OCR-click and title routing** (drives the macOS GUI installer where buttons can't be reached by the
39+
keyboard), and HMP `screendump`.
40+
- **`scripts/provision-macos.sh`** — runs in the Recovery Terminal against the installed Data volume:
41+
offline `dscl -f` user, `.AppleSetupDone`, and a first-boot LaunchDaemon that enables Remote Login.
42+
43+
## Host facts
44+
45+
GitHub `ubuntu-latest` exposes `/dev/kvm` (needs `chmod 666`). macOS Tahoe 26 is the last
46+
Intel-supporting macOS, so this x86 path has a finite shelf life.

docs/images.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Images
2+
3+
## Golden images on ghcr
4+
5+
The CI pipeline (see [CI Image Pipeline](image-pipeline.md)) publishes two tiers per OS:
6+
7+
| Ref | Contents |
8+
|-----|----------|
9+
| `…/tahoe:26-base` | Installed macOS at the first-run Setup Assistant. |
10+
| `…/tahoe:26` | Provisioned & **SSH-ready**: admin `cocoon`/`cocoon`, complete home, Remote Login on first boot. |
11+
12+
`sequoia:15` / `sequoia:15-base` are the macOS 15 (n-1) equivalents. The image is a single immutable
13+
qcow2 blob — **qcow2-only**, no OCI layer filesystem.
14+
15+
## Local store
16+
17+
`image pull` imports the golden qcow2 into a **content-addressed store** under
18+
`<state-dir>/cloudimg` (cocoon's `cloudimg` backend, imported directly). `vm run <ref>` then bakes a
19+
copy-on-write overlay on the immutable shared base, so many VMs share one on-disk copy.
20+
21+
```bash
22+
cocoon-macos image pull ghcr.io/cocoonstack/cocoon-macos/tahoe:26
23+
cocoon-macos image list
24+
```
25+
26+
## Parallel Range download
27+
28+
A single HTTP/2 stream to ghcr is throttled and often reset mid-transfer on multi-GB blobs. `image
29+
pull` therefore downloads the qcow2 layer in **8 concurrent HTTP Range chunks** to a temp file, then
30+
verifies the SHA-256 against the layer digest before importing:
31+
32+
- The `oras-go` SDK resolves the manifest, credentials (from the user's Docker config; public images
33+
pull anonymously), and the qcow2 layer descriptor — **no external `oras` binary** is needed.
34+
- Each chunk is fetched with a `Range` request that follows ghcr's redirect to its presigned CDN URL;
35+
the response's `Content-Range` and length are validated per chunk (a misbehaving CDN can't silently
36+
corrupt the file), and the assembled file's digest must match before import.
37+
- If the registry doesn't support `Range`, it falls back to a single stream. The whole pull retries
38+
up to 3 times on transient drops.
39+
40+
This is both faster (parallel chunks saturate the link) and more robust than a single stream.

docs/index.md

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# cocoon-macos
2+
3+
macOS VM engine for x86 Linux/KVM, built on
4+
[cocoon](https://github.com/cocoonstack/cocoon). Boots real macOS guests
5+
(Sequoia 15, Tahoe 26) as fully-virtualized [QEMU](https://www.qemu.org/)/KVM
6+
VMs via [OpenCore](https://github.com/acidanthera/OpenCorePkg) + OVMF. A GitHub
7+
Action installs macOS from scratch and publishes a golden qcow2 to ghcr; a thin
8+
Go CLI clones that image and boots VMs from it.
9+
10+
```
11+
cocoon-macos CLI ──► image: pull the golden macOS qcow2 from ghcr (parallel Range, sha256)
12+
──► vm: create/run/console/clone ── QEMU + OpenCore + OVMF, CoW disk, CNI real IP
13+
──► snapshot: save/restore ── offline qcow2-internal, per-VM Apple identity
14+
```
15+
16+
## Guides
17+
18+
- [Installation](install.md) — host prerequisites, the doctor script (deps +
19+
firmware), the state directory, and building the CLI
20+
- [CLI reference](cli.md) — the `image` and `vm` commands, key flags, and what
21+
`vm run` does under the hood
22+
- [Images](images.md) — golden images on ghcr, the local cloudimg store, and the
23+
parallel-Range `image pull`
24+
- [VM boot & firmware](vm.md) — the OpenCore/OVMF/CPU recipe, the desktop/GUI
25+
status, and the Setup-Assistant blocker
26+
- [Networking & VNC](networking.md)`--net user/tap/bridge/cni`, TC-redirect
27+
real LAN IP, and per-mode / per-start VNC exposure
28+
- [Snapshots, clone & data disks](snapshots.md) — offline snapshots, CoW clones
29+
with a fresh identity, and extra data disks
30+
- [CI image pipeline](image-pipeline.md) — how the GitHub Action installs macOS
31+
and publishes the golden images
32+
- [E2E regression](e2e.md) — the `e2e.sh` lifecycle regression (`[DUMMY]` +
33+
`[REAL]` tiers)
34+
- [Known issues](known-issues.md) — no-GPU video, display-sleep VNC blanking,
35+
and the Setup Assistant
36+
- [Roadmap](roadmap.md) — what's planned, and what's intentionally out of scope
37+
38+
## Features
39+
40+
- **Fully-automated macOS install** — CI boots OpenCore, erases APFS, drives the
41+
installer by OCR, and publishes a golden qcow2 (Tahoe 26 / Sequoia 15) to ghcr
42+
- **SSH-ready golden images**`tahoe:26` ships an admin `cocoon`/`cocoon` user,
43+
a complete home, and Remote Login on first boot (plus a `-base` tier)
44+
- **Parallel Range image pull** — the multi-GB qcow2 is pulled in 8 concurrent
45+
HTTP Range chunks with an sha256 digest check (oras-go for auth; no `oras` binary)
46+
- **COW overlays** — an instant copy-on-write clone of the immutable golden base
47+
per VM
48+
- **Per-VM Apple identity**`--random-smbios` injects a unique serial/MLB/UUID/ROM
49+
(guest MAC = ROM) into a per-VM OpenCore, so clones never share a serial
50+
- **CNI networking with TC redirect**`--net cni` joins cocoon's forwarding
51+
plane so the guest DHCPs a real LAN IP; also `user`/`tap`/`bridge`
52+
- **Reachable VNC** — loopback VNC on user/tap/bridge; a host-side proxy fronts
53+
CNI VNC on the host port (password required); launch-scoped, off by default
54+
- **Snapshot, clone & restore** — offline qcow2-internal snapshots and CoW clones
55+
that cold-boot a fresh Apple identity
56+
- **Data disks** — up to 4 extra qcow2 data disks on the AHCI ports the OS disk
57+
and OpenCore leave free (macOS has no virtio-blk driver)
58+
- **Intel & AMD** — one boot recipe (Skylake-Client spoofing GenuineIntel + the
59+
LongQT OpenCore) boots identically on both; `ignore_msrs` auto-set on AMD
60+
- **Docker-like CLI**`create`, `run`, `start`, `stop`, `list`, `inspect`,
61+
`console`, `rm`, `snapshot`, `restore`, `clone`
62+
- **Built on cocoon** — imports cocoon's `cloudimg` store, `network` plane, and
63+
copy-on-write conventions rather than reimplementing them
64+
65+
## Repository
66+
67+
Source and issue tracker:
68+
[github.com/cocoonstack/cocoon-macos](https://github.com/cocoonstack/cocoon-macos).
69+
Upstream engine:
70+
[github.com/cocoonstack/cocoon](https://github.com/cocoonstack/cocoon)
71+
(Lightweight MicroVM engine with Cloud Hypervisor and Firecracker backends).

docs/install.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Installation
2+
3+
## Prerequisites
4+
5+
- **x86-64 Linux** with **KVM** (`/dev/kvm` present and accessible). Both Intel and AMD hosts work —
6+
the same boot recipe applies to both; you never choose "OpenCore vs LongQT".
7+
- `qemu-system-x86_64`, `qemu-nbd` (from `qemu-utils`), and NBD kernel module support.
8+
- Enough disk for the golden image (~15 GB) plus per-VM CoW overlays.
9+
10+
## Build the CLI
11+
12+
```bash
13+
go build -o cocoon-macos .
14+
```
15+
16+
## Provision the host (`scripts/doctor.sh`)
17+
18+
Run once per host. `doctor.sh`:
19+
20+
1. Checks `/dev/kvm` and installs any missing dependencies (`qemu`, OVMF, `gdisk`, `dosfstools`),
21+
2. Loads the `nbd` kernel module,
22+
3. Provisions the **shared firmware** into `<state-dir>/firmware` — it downloads the LongQT OpenCore
23+
release and bakes a GPT/ESP `OpenCore.qcow2` plus the 4 MB OVMF `CODE`/`VARS`. Every VM reuses this
24+
one firmware install.
25+
26+
```bash
27+
sudo scripts/doctor.sh
28+
```
29+
30+
`vm run` fails with a "run scripts/doctor.sh" message when the firmware is absent. You can override
31+
any piece with `--opencore` / `--ovmf-code` / `--ovmf-vars` on the VM commands.
32+
33+
## State directory
34+
35+
All persistent state lives under `--state-dir` / `$COCOON_MACOS_HOME`, default
36+
`/var/lib/cocoon-macos` (mirroring cocoon's `/var/lib/cocoon`):
37+
38+
```
39+
/var/lib/cocoon-macos/
40+
├── firmware/ # shared OpenCore.qcow2 + OVMF_CODE/VARS (from doctor.sh)
41+
├── cloudimg/ # content-addressed golden-image store (image pull)
42+
└── vms/<name>/ # per-VM: disk.qcow2 overlay, OVMF_VARS, OpenCore overlay, vm.json, sockets
43+
```

docs/known-issues.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Known Issues
2+
3+
## No GPU → hardware-accelerated video renders black
4+
5+
The display is `-device vmware-svga` (macOS's only supported virtual adapter): a **2D software
6+
framebuffer with no Metal / GPU / VideoToolbox / IOSurface**. CPU-drawn content renders fine —
7+
desktop, widgets, window chrome, and *static* images (e.g. a YouTube thumbnail) — but **live video
8+
playback** uses a GPU-composited hardware overlay (AVPlayerLayer + IOSurface + VideoToolbox) with no
9+
backing on a GPU-less VM, so the moving picture stays **black even though audio plays and the scrubber
10+
advances**. Safari has no software-rendering fallback.
11+
12+
- **Workaround:** Chrome with hardware acceleration off (Settings → System → uncheck "Use hardware
13+
acceleration when available", relaunch) composites video in software into a layer the framebuffer
14+
can draw.
15+
- **No display-device fix:** `qxl` / `virtio-gpu` / `std` have no macOS driver, so swapping `-vga` /
16+
`-device` cannot add acceleration.
17+
- **Real fix:** GPU passthrough (VFIO `-device vfio-pci`) — see [Roadmap](roadmap.md).
18+
19+
## VNC blanks on display sleep
20+
21+
macOS only repaints the emulated framebuffer while the display is **awake**; once it sleeps (~idle),
22+
VNC shows a blank white/black screen with just the cursor even though the guest is healthy (SSH works,
23+
WindowServer is up). A mouse move repaints it — it is *not* a GPU/driver problem. The golden image's
24+
first-boot daemon runs `pmset -a displaysleep 0 disablesleep 1` system-wide (covering the pre-login
25+
loginwindow) to keep the framebuffer painted; older images need a `setup`-stage rebuild.
26+
27+
## GUI lands at the Setup Assistant
28+
29+
A fresh macOS 26 clone boots to the system Setup Assistant, which resists every offline marker-based
30+
skip tried (macOS 14+ broke the classic `.AppleSetupDone` skip). `:26` is SSH/VNC-login-usable, but a
31+
fully unattended boot-to-desktop needs a mouse/OCR click-through of the SA wizard — see the WIP detail
32+
in [VM Boot & Firmware](vm.md) and the [Roadmap](roadmap.md).

docs/networking.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Networking & VNC
2+
3+
## Network modes (`--net`)
4+
5+
| Mode | What it does |
6+
|------|--------------|
7+
| `user` (default) | User-mode SLIRP. Combine with `--ssh-port N` to forward `localhost:N → guest:22`. Works everywhere. |
8+
| `tap` | Attach to a pre-created host TAP verbatim (`--tap tap0`) — the bridge/plane owns IP + forwarding. |
9+
| `bridge` | Auto-create a TAP on an existing Linux bridge (`--bridge br0`) via cocoon's `network/bridge`. |
10+
| `cni` | Auto-create a TAP inside a per-VM netns via cocoon's `network/cni` (TC-redirect plane). |
11+
12+
`tap`/`bridge`/`cni` make a macOS VM join the **same** forwarding plane as cocoon's Cloud
13+
Hypervisor / Firecracker VMs on the node, so the guest can DHCP a **real LAN IP** from the upstream
14+
network. The guest NIC MAC stays equal to the SMBIOS ROM. Auto-create (`bridge`/`cni`) is Linux-only
15+
(needs `CAP_NET_ADMIN`); `user` and a pre-created `--tap` work everywhere.
16+
17+
### `--net cni` and TC redirect
18+
19+
CNI runs QEMU inside a per-VM network namespace. cocoon's CNI wires the netns veth to the QEMU TAP
20+
with **TC ingress redirect** (`mirred`), not a bridge — the guest's DHCP/traffic flows
21+
tap → eth0 → `cni0` → upstream, and it comes back the same way. The guest gets a real ToR IP; SSH
22+
goes straight to that IP (no port-forward).
23+
24+
## VNC exposure
25+
26+
VNC exposure depends on the net mode:
27+
28+
- **`--net user | tap | bridge`** — QEMU binds VNC **loopback-only** (`127.0.0.1:590<vnc>`). Reach it
29+
by tunnelling: `ssh -L 5901:127.0.0.1:5901 <host>`.
30+
- **`--net cni`** — QEMU runs in a netns, so its `127.0.0.1` bind would be unreachable off-box.
31+
Instead VNC rides a unix socket fronted by a small host-side proxy on **all interfaces**
32+
(`0.0.0.0:590<vnc>`). Because that is reachable off the host, **`--vnc-password` is required** for
33+
`--net cni` — the launch is rejected without it.
34+
35+
VNC is **launch-scoped**: it is off unless `--vnc` is given for that launch, it is cleared on
36+
`vm stop`, and it is re-enabled per start:
37+
38+
```bash
39+
cocoon-macos vm start m1 --vnc 1 --vnc-password s3cret # this run only
40+
cocoon-macos vm stop m1 # VNC gone with the qemu it belonged to
41+
```
42+
43+
The VNC password is never written to disk — it is read from the flag on each start.
44+
45+
### macOS Screen Sharing
46+
47+
QEMU's default `None` auth **hangs macOS Screen Sharing**. Pass `--vnc-password <≤8 chars>` (applied
48+
via the QEMU monitor post-launch, capped at 8 chars by the VNC DES scheme) so Screen Sharing prompts
49+
and connects. Plain VNC clients (RealVNC/TigerVNC) work without a password on the loopback modes.
50+
51+
> In-guest macOS Screen Sharing (to the guest's own IP) is **not** enabled headlessly — macOS
52+
> requires the Screen Recording TCC grant from the GUI or MDM. Use QEMU's built-in VNC above instead.
53+
54+
## Display sleep blanks VNC
55+
56+
macOS only repaints the emulated framebuffer while the display is **awake**; once it sleeps (~idle),
57+
VNC shows a blank white/black screen with just the cursor even though the guest is healthy (SSH works,
58+
WindowServer is up). It is *not* a GPU/driver problem — a mouse move repaints it. The golden image's
59+
first-boot daemon runs `pmset -a displaysleep 0 disablesleep 1` system-wide (covering the pre-login
60+
loginwindow) to keep the framebuffer painted; older images need a `setup`-stage rebuild. See also the
61+
GPU/video note in [Boot, Firmware & GUI](vm.md).

0 commit comments

Comments
 (0)