diff --git a/vm/README.md b/vm/README.md
new file mode 100644
index 0000000..db1a27a
--- /dev/null
+++ b/vm/README.md
@@ -0,0 +1,142 @@
+# WayRay VM Testing Setup
+
+Visual testing environment for the WayRay compositor using an Arch Linux
+aarch64 VM on macOS Apple Silicon via UTM.
+
+## Prerequisites
+
+- MacBook with Apple Silicon (M1/M2/M3/M4)
+- [UTM](https://mac.getutm.app/) installed (free download from website)
+
+## Create the VM
+
+### 1. Download the Arch Linux ARM ISO
+
+Go to and download the latest aarch64
+ISO image.
+
+### 2. Create a new VM in UTM
+
+1. Open UTM, click **Create a New Virtual Machine**
+2. Select **Virtualize**
+3. Select **Linux**
+4. Browse to the downloaded ISO
+5. Configure hardware:
+ - **CPU:** 4 cores
+ - **RAM:** 8192 MB (8 GB)
+6. Configure storage:
+ - **Disk size:** 30 GB
+7. Give it a name (e.g., "WayRay Dev")
+8. Click **Save**
+
+The VM defaults to VirtIO display and shared networking, which is what
+we need.
+
+### 3. Install Arch Linux
+
+1. Boot the VM from the ISO
+2. Run `archinstall`
+3. Recommended settings:
+ - **Mirrors:** Select your region
+ - **Disk configuration:** Use entire disk, ext4
+ - **Bootloader:** systemd-boot
+ - **Profile:** Minimal
+ - **User:** Create a user with sudo privileges
+ - **Network:** NetworkManager
+4. Complete the installation and reboot
+5. Remove the ISO from the VM's CD drive in UTM settings
+
+### 4. Run the setup script
+
+Boot into the installed system and log in, then:
+
+```bash
+# Install git first (needed to clone the repo)
+sudo pacman -S git --noconfirm
+
+# Clone WayRay and run the setup script
+git clone ~/wayray
+bash ~/wayray/vm/setup.sh
+
+# Reboot to apply auto-login and start Sway
+sudo reboot
+```
+
+Note: the script skips cloning if `~/wayray` already exists, so passing
+the repo URL to `setup.sh` is only needed if you haven't cloned yet.
+
+## Testing the Compositor
+
+After reboot, the VM auto-logs in and starts Sway. A foot terminal
+opens automatically.
+
+```bash
+# Build and run the compositor
+cd ~/wayray
+cargo run --bin wrsrvd
+```
+
+The wrsrvd window opens inside Sway. Note the Wayland socket name from
+the log output (e.g., `wayland-1`).
+
+Open another terminal with `Super+Return`, then:
+
+```bash
+# Launch a client inside the compositor
+WAYLAND_DISPLAY=wayland-1 foot
+```
+
+A foot terminal should appear inside the wrsrvd window. You can:
+- Type in it (keyboard input works)
+- Click on it (pointer input works)
+- See it render (compositor rendering works)
+
+## Keyboard Shortcuts (Sway)
+
+| Shortcut | Action |
+|----------|--------|
+| `Super+Return` | Open a new terminal |
+| `Super+Shift+Q` | Close focused window |
+| `Super+Shift+E` | Exit Sway |
+
+## Troubleshooting
+
+### Sway fails to start
+
+If Sway fails with a GPU error, the VirtIO GPU may not be working
+with Apple Virtualization.framework. Try switching UTM to QEMU backend:
+
+1. Shut down the VM
+2. In UTM, edit the VM settings
+3. Under **System**, uncheck "Use Apple Virtualization"
+4. Under **Display**, ensure VirtIO GPU is selected
+5. Boot again
+
+### Black screen after reboot
+
+The auto-login or Sway autostart may have failed. Switch to TTY2 with
+`Ctrl+Alt+F2`, log in, and check:
+
+```bash
+# Check if seatd is running
+systemctl status seatd
+
+# Check if user is in seat group
+groups
+
+# Try starting Sway manually
+sway
+```
+
+### Build fails
+
+```bash
+# Make sure Rust is up to date
+rustup update
+
+# Check Rust version (need 1.85+ for edition 2024)
+rustc --version
+
+# Retry
+cargo build --workspace
+```
diff --git a/vm/setup.sh b/vm/setup.sh
new file mode 100755
index 0000000..90967b8
--- /dev/null
+++ b/vm/setup.sh
@@ -0,0 +1,167 @@
+#!/usr/bin/env bash
+# WayRay VM Provisioning Script
+# Run as a regular user inside a fresh Arch Linux aarch64 install.
+# Idempotent — safe to run multiple times.
+
+set -euo pipefail
+
+REPO_URL="${1:-}"
+WAYRAY_DIR="$HOME/wayray"
+
+info() { printf '\033[1;34m==> %s\033[0m\n' "$*"; }
+warn() { printf '\033[1;33m==> WARNING: %s\033[0m\n' "$*"; }
+ok() { printf '\033[1;32m==> %s\033[0m\n' "$*"; }
+
+# ── 1. System packages ───────────────────────────────────────────────
+
+info "Updating system packages..."
+sudo pacman -Syu --noconfirm
+
+PACKAGES=(
+ # Build tools
+ base-devel
+
+ # Wayland
+ wayland
+ wayland-protocols
+ libxkbcommon
+
+ # Graphics (Mesa provides EGL/OpenGL for Winit's GlesRenderer)
+ mesa
+
+ # Session
+ sway
+ seatd
+ foot
+
+ # Tools
+ git
+ openssh
+)
+
+info "Installing packages..."
+sudo pacman -S --needed --noconfirm "${PACKAGES[@]}"
+
+# ── 2. Services ──────────────────────────────────────────────────────
+
+info "Enabling seatd..."
+sudo systemctl enable --now seatd
+
+# Add user to seat group if not already a member.
+if ! groups | grep -q '\bseat\b'; then
+ info "Adding $USER to seat group..."
+ sudo usermod -aG seat "$USER"
+ warn "Group change requires re-login. Re-run this script after reboot."
+fi
+
+# ── 3. Rust toolchain ────────────────────────────────────────────────
+
+if command -v cargo &>/dev/null; then
+ ok "Rust toolchain already installed ($(rustc --version))"
+else
+ info "Installing Rust via rustup..."
+ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable
+ # shellcheck disable=SC1091
+ source "$HOME/.cargo/env"
+fi
+
+# Verify edition 2024 support (Rust 1.85+).
+RUST_VERSION=$(rustc --version | grep -oP '\d+\.\d+')
+RUST_MAJOR=$(echo "$RUST_VERSION" | cut -d. -f1)
+RUST_MINOR=$(echo "$RUST_VERSION" | cut -d. -f2)
+if (( RUST_MAJOR < 1 || (RUST_MAJOR == 1 && RUST_MINOR < 85) )); then
+ warn "Rust $RUST_VERSION may not support edition 2024. Run: rustup update"
+fi
+
+# ── 4. Clone and build WayRay ────────────────────────────────────────
+
+if [ -d "$WAYRAY_DIR" ]; then
+ ok "WayRay repo already cloned at $WAYRAY_DIR"
+else
+ if [ -z "$REPO_URL" ]; then
+ echo ""
+ echo "WayRay repo URL not provided."
+ echo "Usage: $0 "
+ echo "Example: $0 https://github.com/user/wayray.git"
+ echo " $0 git@github.com:user/wayray.git"
+ exit 1
+ fi
+ info "Cloning WayRay..."
+ git clone "$REPO_URL" "$WAYRAY_DIR"
+fi
+
+info "Building WayRay..."
+cd "$WAYRAY_DIR"
+cargo build --workspace
+ok "Build complete"
+
+# ── 5. Auto-login on TTY1 ───────────────────────────────────────────
+
+GETTY_OVERRIDE="/etc/systemd/system/getty@tty1.service.d/override.conf"
+if [ -f "$GETTY_OVERRIDE" ]; then
+ ok "Auto-login already configured"
+else
+ info "Configuring auto-login on TTY1..."
+ sudo mkdir -p "$(dirname "$GETTY_OVERRIDE")"
+ sudo tee "$GETTY_OVERRIDE" > /dev/null </dev/null; then
+ info "Adding Sway auto-start to $PROFILE..."
+ echo "" >> "$PROFILE"
+ echo "# Start Sway on TTY1" >> "$PROFILE"
+ echo "$SWAY_LAUNCH" >> "$PROFILE"
+fi
+
+# ── 6. Sway config ──────────────────────────────────────────────────
+
+SWAY_CONFIG="$HOME/.config/sway/config"
+if [ -f "$SWAY_CONFIG" ]; then
+ ok "Sway config already exists at $SWAY_CONFIG"
+else
+ info "Writing minimal Sway config..."
+ mkdir -p "$(dirname "$SWAY_CONFIG")"
+ cat > "$SWAY_CONFIG" <<'EOF'
+# Minimal Sway config for WayRay development/testing.
+# This exists solely as a Wayland session host for the Winit backend.
+
+set $mod Mod4
+output * bg #333333 solid_color
+default_border none
+
+# Launch a terminal
+bindsym $mod+Return exec foot
+
+# Close focused window
+bindsym $mod+Shift+q kill
+
+# Exit Sway
+bindsym $mod+Shift+e exit
+
+# Autostart a terminal on login
+exec foot
+EOF
+fi
+
+# ── Done ─────────────────────────────────────────────────────────────
+
+echo ""
+ok "Setup complete!"
+echo ""
+echo "Next steps:"
+echo " 1. Reboot: sudo reboot"
+echo " 2. VM will auto-login and start Sway"
+echo " 3. In the foot terminal:"
+echo " cd ~/wayray && cargo run --bin wrsrvd"
+echo " 4. Note the socket name from the log, then:"
+echo " WAYLAND_DISPLAY= foot"
+echo " 5. A foot terminal should appear inside the wrsrvd window"
+echo ""