PlayOS
A console operating environment for handheld gaming PCs — built on Linux, presented as a dedicated gaming console.
PlayOS boots directly from UEFI into a controller-first shell. A custom compositor permanently owns the display. One hardware-accelerated game runs at a time. The player never sees a Linux desktop, terminal, or login screen.
What PlayOS Is
- A minimal, immutable Linux system that acts as a hardware enablement layer
- A Wayland compositor that owns DRM/KMS and enforces console display policy
- A persistent Raylib shell that is always alive, even while a game runs
- A stable public C ABI (
libplayos) that games target instead of Linux internals - A thirteen-repository project with clear ownership boundaries
What PlayOS Is Not
- A Linux distribution or desktop environment
- A general-purpose PC OS
- An emulation layer or compatibility shim
- A cloud gaming platform
Documentation
Architecture and Contracts
| Document | Description |
|---|---|
| architecture.md | System design, component diagrams, state machine, boot sequence |
| platform-api.md | Public libplayos C ABI specification and versioning policy |
| runtime-ipc.md | Internal IPC protocol (launch, lifecycle, control) |
| wayland-protocol.md | Private PlayOS Wayland extensions |
| security-model.md | Trust boundaries, game restrictions, Secure Boot chain |
Component Specifications
| Document | Description |
|---|---|
| playos-init-spec.md | PID 1 — boot, process supervision, storage, IPC |
| playos-compositor-spec.md | wlroots compositor — DRM/KMS, focus, state machine |
| playos-shell-spec.md | Raylib shell — controller UI, game library, lifecycle |
| playos-overlay-spec.md | Trusted overlay — quick menu, notifications, power |
Build and Development
| Document | Description |
|---|---|
| build-guide.md | Buildroot setup, br2-external layout, make commands |
| kernel-config.md | Kernel subsystem requirements, ROG Ally configuration |
| dev-environment.md | QEMU/OVMF setup, developer iteration workflow |
| testing.md | CI layers, physical device smoke tests |
Delivery
| Document | Description |
|---|---|
| roadmap.md | MVP criteria and sprint plan (Sprints 0–19) |
| post-mvp.md | Post-MVP feature roadmap |
| Sprint-N.md | Sprint 0–19 work packages |
Architecture Decision Records
| ADR | Decision |
|---|---|
| ADR-0001 | Repository structure |
| ADR-0002 | Unix socket IPC transport |
| ADR-0003 | musl libc only |
| ADR-0004 | wlroots as compositor foundation |
| ADR-0005 | RAUC for A/B updates |
| ADR-0006 | Raylib for shell and game UI |
| ADR-0007 | Direct ALSA for MVP audio |
| ADR-0008 | PCI enumeration for GPU selection |
Repository Map
| Repository | Role |
|---|---|
playos-spec | Architecture, contracts, ADRs, roadmap, game developer docs |
playos-platform-api | Public libplayos C ABI |
playos-runtime | Internal IPC, lifecycle transport, private Wayland protocols |
playos-compositor | wlroots compositor, DRM/KMS, focus, input routing |
playos-shell | Controller-first Raylib shell and PlayOS Raylib backend |
playos-refdistro | Buildroot integration, kernel config, image assembly, installer |
playos-init | PID 1 process supervisor, boot lifecycle, storage mount |
playos-samples | Sample games and reference applications |
playos-tools | Host-side developer and OTA staging tools |
playos-foundation | Shared foundation libraries and utilities |
playos-reference-devices | Reference device configurations and images |
playos-cloud | Cloud services — cloud saves and accounts (post-MVP) |
playos-marketplace | Game store and marketplace (post-MVP) |
Quick Start
# Build and run in QEMU
make setup
make qemu-config
make qemu-build
make qemu-run
# Build for ROG Ally (USB image)
make ally-config
make ally-build
make ally-usb-image
See build-guide.md and dev-environment.md for full setup instructions.
Primary Device
ASUS ROG Ally — AMD Ryzen Z1 / RDNA 3 APU
First supported graphics stack: AMDGPU + Mesa RadeonSI
Intel expansion: Sprint 13
PlayOS Architecture Reference
Version: 2.5
Device: ROG Ally (AMD/AMDGPU primary)
Source of truth:ideas.md— this document is the distilled reference.
Table of Contents
- Defining Principle
- System Diagram
- Repository Map
- Process Model
- Boot Sequence
- Component Responsibilities
- Console Lifecycle State Machine
- Game Launch Flow
- Input Routing
- Graphics Stack
- Audio Stack
- Storage Layout
- Security Boundaries
- Architectural Constraints (Non-Goals)
1. Defining Principle
PlayOS is a console operating environment built on a minimal Linux hardware layer.
playos-initowns processes,playos-compositorowns display and focus,playos-shellowns the user experience,playos-platform-apiowns the publiclibplayosC ABI,playos-runtimeowns internal lifecycle transport and control IPC, and one isolated game process runs at a time. The system remains immutable, the data partition remains writable, and the player never interacts with Linux as a desktop operating system.
Key axioms:
- Linux is the hardware layer, not the product.
- The system image is immutable; only the data partition is writable.
- The compositor permanently owns DRM/KMS — no handoffs to games.
- One game runs at a time;
playos-shellalways stays alive. - Games target
playos-platform-api; they never touch compositor or kernel internals.
2. System Diagram
UEFI Firmware
│
▼
Linux EFI-stub kernel ◄── embedded initramfs
│
▼
Linux Kernel
├── EFI / ACPI / PCIe / IOMMU
├── AMDGPU / DRM/KMS
├── USB / HID / evdev
├── ALSA
├── NVMe / ext4 / FAT
└── battery / thermal / power
│
▼
PlayOS Runtime (all in initramfs)
├── playos-init PID 1, process supervisor
├── playos-compositor wlroots compositor, DRM/KMS owner
│ ├── playos-shell persistent Raylib UI (Wayland client)
│ ├── playos-overlay trusted system overlay (Wayland client)
│ └── active-game one isolated Wayland game process
├── libplayos public C ABI (from playos-platform-api)
├── playos-runtime internal IPC and lifecycle transport
└── Mesa / Wayland / ALSA platform libraries
│
▼
Persistent Data Partition (/data)
games / saves / cache / log / updates / config
3. Repository Map
| Repository | Owns |
|---|---|
playos-spec | Architecture, public contracts, ADRs, schemas, roadmap |
playos-init | PID 1 process supervisor, boot lifecycle, storage mount, game launch/supervision |
playos-platform-api | Public libplayos C ABI, C++ wrappers, engine adapters |
playos-runtime | Internal IPC, lifecycle transport, private Wayland protocols, OS integration |
playos-compositor | wlroots compositor, DRM/KMS, surface/focus/input policy |
playos-shell | Controller-first Raylib shell and PlayOS Raylib backend |
playos-refdistro | Buildroot integration, kernel config, image assembly, installer |
playos-samples | Sample games and reference applications |
playos-tools | Host-side developer and OTA staging tools |
playos-foundation | Shared foundation libraries and utilities |
playos-reference-devices | Reference device configurations and images |
playos-cloud | Cloud services — cloud saves and accounts (post-MVP) |
playos-marketplace | Game store and marketplace (post-MVP) |
Dependency direction:
playos-spec
└── defines contracts for all implementation repos
playos-runtime ◄──────────────────────────── playos-compositor
▲ ▲
│ │ (private control IPC)
playos-platform-api playos-shell (trusted client)
▲
│
games / playos-shell (public API consumers)
playos-refdistro ── pins and assembles all runtime components
Rules:
- Public application ABI lives only in
playos-platform-api. - Private IPC/protocol definitions live only in
playos-runtime. - DRM/KMS and compositor implementation lives only in
playos-compositor. playos-refdistropackages and pins; it does not redefine contracts.
4. Process Model
playos-init (PID 1)
│
├── playos-compositor (DRM/KMS owner, Wayland display)
│ ├── playos-shell (trusted Wayland client, always alive)
│ └── playos-overlay (trusted Wayland client, shown on demand)
│
└── active-game (isolated Wayland client, one at a time)
playos-initspawns and supervisesplayos-compositor,playos-shell,playos-overlay, andactive-game.- The compositor owns the Wayland display and surface presentation for its trusted clients; it does not spawn processes.
5. Boot Sequence
| Step | Actor | Action |
|---|---|---|
| 1 | UEFI | Loads /EFI/BOOT/BOOTX64.EFI |
| 2 | EFI stub | Transfers control to the Linux kernel |
| 3 | Kernel | Initializes hardware: ACPI, PCIe, GPU, USB, ALSA, NVMe |
| 4 | Kernel | Unpacks embedded initramfs into RAM |
| 5 | Kernel | Starts /init → playos-init as PID 1 |
| 6 | playos-init | Mounts /dev, /proc, /sys, /run |
| 7 | playos-init | Discovers and mounts the PlayOS data partition |
| 8 | playos-init | Starts playos-compositor |
| 9 | playos-compositor | Initializes wlroots backend, DRM/KMS, renderer, Wayland socket |
| 10 | playos-init | Launches playos-shell and playos-overlay with trusted identity |
| 11 | playos-shell | Maps fullscreen surface; shows game library |
| → | User | Selects a game |
| 12 | playos-shell | Sends LaunchGame(game_id) over control IPC |
| 13 | playos-init | Validates manifest; spawns game process |
| 14 | playos-compositor | Waits for game's first valid committed frame |
| 15 | playos-compositor | Switches foreground from shell → game |
First-frame rule: The compositor never switches to the game surface until it receives a real committed buffer. This prevents a black-screen transition during game initialization.
6. Component Responsibilities
playos-init
Owns: Boot lifecycle, process supervision, storage mount, game launch/kill, reboot/shutdown/recovery.
Does NOT own: Surfaces, focus, rendering, network, game-specific logic.
playos-compositor
Owns: DRM/KMS, Wayland socket and display, surface z-order and focus, trusted client identity, reserved system input, overlay stacking, lifecycle state transitions, crash recovery.
Does NOT own: Game installation, save management, process spawning.
playos-shell
Owns: Persistent console UI, controller-first navigation, game discovery, launch requests via control IPC, settings, Raylib rendering.
Behavior while game is running: Remains alive; stops or throttles rendering; available to show system UI.
playos-overlay
Owns: Quick menu, volume/brightness HUD, power menu, notifications, virtual keyboard.
May later merge into a multi-surface playos-shell backend.
playos-platform-api / libplayos
Owns: Public, engine-agnostic C ABI. Stable across versions. Exposes lifecycle events, storage paths, device info, logical input, audio/display/power queries, structured logging.
Does NOT expose: Compositor internals, privileged IPC, or DRM handles.
playos-runtime
Owns: Internal IPC protocol definitions, lifecycle event transport, private Wayland protocol XML, restricted client libraries for trusted components, OS integration helpers.
Does NOT own: DRM/KMS policy or the compositor implementation.
Active game process
Owns: Its address space, Wayland surface, audio streams, input stream, save/cache directories.
Prohibited from: Becoming DRM master, reconfiguring displays, mounting filesystems, modifying the system image, synthesizing reserved input, connecting to privileged IPC endpoints.
7. Console Lifecycle State Machine
SHELL_FOREGROUND
│
│ launch accepted by playos-init
▼
GAME_STARTING
│
│ first valid game frame committed
▼
GAME_FOREGROUND ◄─────────────────────────────────┐
│ │
│ PLAYOS_BUTTON_SYSTEM pressed │ Resume
▼ │
PLAYOS_UI_FOREGROUND_WITH_GAME_BACKGROUND ─────────┘
│
│ Quit selected
▼
TERMINATING_GAME
│
▼
SHELL_FOREGROUND ◄── game exits cleanly or crashes (any state)
This state machine is a core PlayOS contract and must be explicitly tested.
State transition triggers:
| From | Event | To |
|---|---|---|
SHELL_FOREGROUND | playos-init accepts launch | GAME_STARTING |
GAME_STARTING | First valid game frame | GAME_FOREGROUND |
GAME_FOREGROUND | PLAYOS_BUTTON_SYSTEM | PLAYOS_UI_FOREGROUND_WITH_GAME_BACKGROUND |
PLAYOS_UI_... | Resume | GAME_FOREGROUND |
PLAYOS_UI_... | Quit | TERMINATING_GAME → SHELL_FOREGROUND |
GAME_FOREGROUND | Exit or crash | SHELL_FOREGROUND |
8. Game Launch Flow
playos-shell → LaunchGame(game_id) → playos-init control IPC
playos-init → validates manifest, permissions, one-game rule
playos-init → prepares save/cache paths, process group, lifecycle channel, launch identity
playos-init → spawns game with WAYLAND_DISPLAY + PlayOS env vars
playos-compositor → matches client to expected launch identity
playos-compositor → waits for first committed buffer
playos-compositor → switches foreground from shell → game
playos-shell → remains alive, rendering throttled
Launch responsibility split:
| Component | Role in launch |
|---|---|
playos-shell | Chooses and requests via restricted control IPC |
playos-init | Validates, spawns, supervises, terminates |
playos-compositor | Identifies surface; controls presentation |
playos-runtime | Transports lifecycle and control messages |
playos-platform-api | Exposes lifecycle events and safe services to the game |
9. Input Routing
Controller / keyboard / touch
│
▼
Linux HID / evdev / libinput
│
▼
playos-compositor
├── PLAYOS_BUTTON_SYSTEM → PlayOS only (never delivered to games)
├── overlay visible → playos-overlay
├── game foreground → active game
└── otherwise → playos-shell
Logical input constants (defined in playos-platform-api):
PLAYOS_BUTTON_SOUTH / EAST / WEST / NORTH
PLAYOS_BUTTON_START / SELECT
PLAYOS_BUTTON_SYSTEM // reserved — not delivered to games
PLAYOS_BUTTON_QUICK_MENU // reserved
PLAYOS_AXIS_LEFT_X / LEFT_Y
PLAYOS_AXIS_RIGHT_X / RIGHT_Y
PLAYOS_AXIS_LEFT_TRIGGER / RIGHT_TRIGGER
10. Graphics Stack
Raylib shell or game
│ Wayland / PlayOS Raylib backend (rcore_playos.c)
▼
Wayland protocol
│
▼
playos-compositor + wlroots
│ GBM / EGL / OpenGL ES renderer
▼
DRM/KMS + AMDGPU
│
▼
Display
AMD (primary): amdgpu kernel driver → Mesa RadeonSI → libdrm / GBM / EGL
Intel (later): i915 / xe → Mesa Iris → libdrm / GBM / EGL
Vulkan: Deferred (RADV / ANV added after AMD baseline is stable)
Direct scanout: When a game's fullscreen buffer is compatible with the DRM output plane, the compositor attempts to assign it directly, skipping composition. Falls back automatically when an overlay is visible or format/scaling prevents it.
Recovery graphics: SimpleDRM or firmware framebuffer fallback for recovery mode without accelerated graphics.
11. Audio Stack
Raylib audio
│ PlayOS audio backend
▼
ALSA PCM
│
▼
Kernel ALSA driver (HDA / SoC / ACP)
│
▼
Built-in speakers / headphones
MVP policy:
- Stereo PCM only; no PulseAudio or PipeWire.
- One foreground audio owner: game while foreground, shell otherwise.
- Shell mutes/stops audio when game becomes foreground; resumes when game exits.
12. Storage Layout
Partition model (production A/B):
GPT disk
├── Partition 1: EFI System Partition FAT32 512 MiB BOOTX64.EFI
├── Partition 2: PlayOS system A immutable 4 GiB read-only root slot
├── Partition 3: PlayOS system B immutable 4 GiB reserved for A/B (Sprint 11)
├── Partition 4: PlayOS misc ext4 64 MiB A/B slot metadata
└── Partition 5: PlayOS data ext4 remainder writable
Data partition (/data):
/data/
games/<game-id>/ manifest.json, bin/, assets/, shaders/, licenses/
saves/<game-id>/ profiles/, autosaves/, settings/
cache/<game-id>/ shaders/, compiled-assets/, temporary/
resources/
downloads/
log/
updates/
screenshots/
config/
profiles/
Rules:
- PlayOS must never silently format an unknown disk.
- Factory reset operates on
/dataonly; the immutable system slots are untouched. - First-boot provisioning requires explicit confirmation before creating filesystems.
13. Security Boundaries
┌────────────────────────────────────────────────────────┐
│ Trusted system components │
│ playos-init (root) │
│ playos-compositor (display/input caps) │
│ playos-shell / playos-overlay (service user) │
│ │
│ ← communicate via restricted playos-runtime IPC → │
└────────────────────────────────────────────────────────┘
│
public libplayos C ABI
│
┌────────────────────────────────────────────────────────┐
│ Untrusted │
│ active-game (unprivileged game identity) │
│ per-title save and cache directories only │
│ no DRM primary nodes │
│ no mount, format, or kernel-module access │
│ no reserved input synthesis │
│ no direct compositor or privileged IPC access │
└────────────────────────────────────────────────────────┘
Hardening roadmap: capabilities → seccomp → Landlock → namespaces → signed manifests → Secure Boot (signed EFI + kernel + initramfs + A/B metadata).
14. Architectural Constraints (Non-Goals)
The following are explicitly excluded from PlayOS v1:
| Excluded | Reason |
|---|---|
| Desktop environment | Console OS; Linux is the hardware layer only |
| X11 / Xwayland | Wayland-only |
| systemd | Custom playos-init owns lifecycle |
| Display / login manager | Direct boot to shell |
| Containers | Not needed for single-game model |
| Conventional package manager | Immutable system image |
| Custom Linux kernel | Upstream LTS with ROG Ally config |
| Custom GPU driver / OpenGL | Mesa / AMDGPU |
| Multiple simultaneous games | One-game process model |
| Multi-GPU / hybrid graphics | ROG Ally is single AMD GPU |
| Wi-Fi, Bluetooth, SSH, cloud saves | Post-MVP |
| Full suspend/resume | Post-MVP |
| HDR, VRR, recording, streaming | Post-MVP |
| libc other than musl | musl only |
For sprint-by-sprint implementation detail, see roadmap.md and the individual Sprint-N.md files.
For the public API contract, see platform-api.md.
For internal IPC definitions, see runtime-ipc.md.
PlayOS Platform API Specification
Authoritative repository:
playos-platform-api
Current ABI version: 1
SONAME:libplayos.so.0
Cross-references: architecture.md §7.6, security-model.md
Table of Contents
- Purpose and Scope
- ABI Stability Policy
- Versioning
- Header Organization
- API Groups
- Backend Architecture
- C++ and Engine Wrappers
- What the API Must Never Expose
- Change Process
1. Purpose and Scope
playos-platform-api owns the public, engine-agnostic PlayOS application contract. It is the only interface that games, the shell, and tools may use to interact with PlayOS capabilities.
libplayos hides:
- Linux kernel internals
- DRM/KMS device handles
- Wayland compositor internals
playos-runtimeprivate IPC endpoints- Filesystem paths other than the safe storage API
libplayos exposes:
- Lifecycle events (foreground, background, suspend, terminate)
- Assigned storage paths (install, save, cache)
- Device and capability information
- Logical input state
- Structured logging
- Audio, display, and power queries that are safe for applications
- Narrow requests for approved system actions (performance profile)
2. ABI Stability Policy
The C ABI is the authoritative compatibility boundary.
What is ABI-stable (safe to add in a minor version):
- New enum values at the end of an existing enum
- New functions with new names
- New struct types
- New header files
What is ABI-breaking (requires a major version bump):
- Removing or renaming any public symbol
- Changing a function signature
- Changing struct field layout, size, or order
- Redefining enum values
- Changing the semantic meaning of any return value
Language compatibility
The C ABI must be consumable from: C, C++, Rust, Zig, and any language with a C FFI. All public headers must be valid C99 and C++11.
/* All public headers include this guard */
#ifdef __cplusplus
extern "C" {
#endif
/* ... declarations ... */
#ifdef __cplusplus
}
#endif
3. Versioning
/* include/playos/playos.h — master version header */
#define PLAYOS_API_VERSION_MAJOR 0
#define PLAYOS_API_VERSION_MINOR 3
#define PLAYOS_API_VERSION_PATCH 0
#define PLAYOS_API_VERSION 1 /* integer for runtime checks */
Runtime version query:
uint32_t playos_system_api_version(void); /* returns PLAYOS_API_VERSION */
SONAME policy:
libplayos.so.0— covers all v0.x.y releaseslibplayos.so.1— next breaking release
Breaking changes require:
- RFC filed in
playos-spec - ADR documenting the decision and migration path
- Major SONAME bump
- Migration guide published before release
4. Header Organization
include/playos/
playos.h # master include — pulls in all groups
playos_system.h # device and OS information
playos_lifecycle.h # lifecycle events
playos_input.h # controller input
playos_display.h # display information
playos_storage.h # save, cache, and install paths
playos_audio.h # audio state and volume
playos_power.h # battery, thermal, performance profiles
playos_logging.h # structured logging
Convention: every symbol is prefixed playos_ (functions and variables) or PLAYOS_ (constants and macros).
5. API Groups
5.1 playos_system.h
Device and platform information. Read-only.
/* API version this runtime implements. */
uint32_t playos_system_api_version(void);
/* Null-terminated version string, e.g. "0.1.0". */
const char *playos_system_os_version(void);
/* Null-terminated device model string, e.g. "ROG Ally (2023)". */
const char *playos_system_device_model(void);
/* CPU and GPU description strings. */
const char *playos_system_cpu_description(void);
const char *playos_system_gpu_description(void);
/* Total and available RAM in bytes. */
uint64_t playos_system_total_memory_bytes(void);
uint64_t playos_system_available_memory_bytes(void);
/* BCP 47 locale string, e.g. "en-US". */
const char *playos_system_locale(void);
All returned const char * strings are valid for the lifetime of the process. Never free() them.
5.2 playos_lifecycle.h
Lifecycle events delivered by playos-runtime via a fd established at launch.
typedef enum {
PLAYOS_LIFECYCLE_FOREGROUND, /* game is now the active foreground surface */
PLAYOS_LIFECYCLE_BACKGROUND, /* game is hidden; should pause and reduce CPU */
PLAYOS_LIFECYCLE_SUSPEND, /* system is suspending; save state immediately */
PLAYOS_LIFECYCLE_RESUME, /* system resumed from suspend */
PLAYOS_LIFECYCLE_TERMINATE /* ordered shutdown; clean up and exit promptly */
} PlayOSLifecycleEvent;
/* Non-blocking. Returns 1 if an event was written to *event, 0 if none pending,
-1 on error (fd closed or invalid). */
int playos_lifecycle_poll(PlayOSLifecycleEvent *event);
/* Blocking variant — waits up to timeout_ms milliseconds (-1 = indefinite). */
int playos_lifecycle_wait(PlayOSLifecycleEvent *event, int timeout_ms);
/* Returns the underlying fd for use with poll(2) / select(2). */
int playos_lifecycle_fd(void);
Expected game behavior:
| Event | Required action |
|---|---|
FOREGROUND | Resume rendering and input processing |
BACKGROUND | Pause gameplay, stop normal input, lower/mute audio, reduce FPS to 0 |
SUSPEND | Flush save data immediately; return within 500ms |
RESUME | Restore state; resume rendering |
TERMINATE | Save state, release resources, call exit(0) within 2 seconds |
5.3 playos_input.h
Logical controller state. Hardware-agnostic.
typedef enum {
PLAYOS_BUTTON_SOUTH = (1 << 0), /* A on Xbox layout */
PLAYOS_BUTTON_EAST = (1 << 1), /* B */
PLAYOS_BUTTON_WEST = (1 << 2), /* X */
PLAYOS_BUTTON_NORTH = (1 << 3), /* Y */
PLAYOS_BUTTON_START = (1 << 4),
PLAYOS_BUTTON_SELECT = (1 << 5),
PLAYOS_BUTTON_DPAD_UP = (1 << 6),
PLAYOS_BUTTON_DPAD_DOWN = (1 << 7),
PLAYOS_BUTTON_DPAD_LEFT = (1 << 8),
PLAYOS_BUTTON_DPAD_RIGHT = (1 << 9),
PLAYOS_BUTTON_L1 = (1 << 10),
PLAYOS_BUTTON_R1 = (1 << 11),
PLAYOS_BUTTON_L3 = (1 << 12), /* left stick click */
PLAYOS_BUTTON_R3 = (1 << 13), /* right stick click */
/* PLAYOS_BUTTON_SYSTEM and PLAYOS_BUTTON_QUICK_MENU are reserved
and are never delivered to games. */
} PlayOSButton;
typedef enum {
PLAYOS_AXIS_LEFT_X = 0,
PLAYOS_AXIS_LEFT_Y = 1,
PLAYOS_AXIS_RIGHT_X = 2,
PLAYOS_AXIS_RIGHT_Y = 3,
PLAYOS_AXIS_LEFT_TRIGGER = 4,
PLAYOS_AXIS_RIGHT_TRIGGER = 5,
PLAYOS_AXIS_COUNT = 6
} PlayOSAxis;
typedef struct {
uint32_t buttons; /* bitmask of PlayOSButton flags */
float axes[PLAYOS_AXIS_COUNT]; /* sticks: [-1.0, 1.0]; triggers: [0.0, 1.0] */
uint64_t timestamp_us; /* microseconds since system boot */
} PlayOSControllerState;
/* Returns 1 if the primary controller is connected. */
int playos_input_controller_connected(void);
/* Fills *state with the current controller snapshot. Returns 0 on success. */
int playos_input_get_controller_state(PlayOSControllerState *state);
/* Helper: returns non-zero if the given button flag is set in state->buttons. */
static inline int playos_input_button_down(const PlayOSControllerState *state,
PlayOSButton button) {
return (state->buttons & (uint32_t)button) != 0;
}
5.4 playos_display.h
Display information. Read-only; games do not configure the display.
typedef struct {
int width; /* native display width in pixels */
int height; /* native display height in pixels */
float refresh_rate; /* e.g. 60.0, 120.0 */
float scale; /* logical scale factor (1.0 initially) */
int orientation; /* 0 = landscape, 1 = portrait */
int hdr_supported; /* 1 if HDR is available (post-MVP) */
} PlayOSDisplayInfo;
int playos_display_get_info(PlayOSDisplayInfo *info);
/* Request v-sync preference. PlayOS may or may not honor it.
0 = disabled, 1 = enabled (default). Returns 0 if accepted. */
int playos_display_set_vsync(int enabled);
5.5 playos_storage.h
Per-game storage paths and helpers. Paths are set from the environment at launch.
/* All returned paths are valid for the process lifetime. Returns NULL if unavailable. */
const char *playos_storage_get_install_path(void); /* /data/games/<game-id> read-only */
const char *playos_storage_get_saves_path(void); /* /data/saves/<game-id> read-write */
const char *playos_storage_get_cache_path(void); /* /data/cache/<game-id> read-write */
/* Shell-only: returns the games root path. Not available to game processes. */
const char *playos_storage_get_games_path(void);
/* Free space on the data partition in bytes. */
int64_t playos_storage_free_bytes(void);
/* Atomically replace dst_path with the content in src_path (rename-based).
Returns 0 on success. */
int playos_storage_atomic_replace(const char *src_path, const char *dst_path);
/* Write data to a temporary file, then atomically rename it to path.
Returns 0 on success. */
int playos_storage_atomic_write(const char *path, const void *data, size_t len);
Isolation guarantee: Each game receives paths scoped to its game_id. A game process cannot construct or access another game's save path through this API. Landlock enforcement (Sprint 12, not yet started) backs this up at the OS level.
5.6 playos_audio.h
System audio state. Games control their own streams through Raylib; this API exposes system-level info.
typedef struct {
int sample_rate; /* e.g. 44100 or 48000 */
int channels; /* 1 or 2 */
int bits_per_sample; /* 16 */
float master_volume; /* 0.0 – 1.0, system-wide */
int muted; /* 1 if system is muted */
} PlayOSAudioInfo;
int playos_audio_get_info(PlayOSAudioInfo *info);
/* Request system volume change. Only honored when the game is foreground.
Returns 0 if accepted, -1 if denied. */
int playos_audio_set_master_volume(float volume);
int playos_audio_set_muted(int muted);
5.7 playos_power.h
Battery, thermal, and performance profiles.
typedef enum {
PLAYOS_POWER_STATE_ON_BATTERY,
PLAYOS_POWER_STATE_CHARGING,
PLAYOS_POWER_STATE_CHARGED,
PLAYOS_POWER_STATE_UNKNOWN
} PlayOSPowerState;
typedef enum {
PLAYOS_THERMAL_NORMAL, /* < 75°C */
PLAYOS_THERMAL_WARM, /* 75–85°C */
PLAYOS_THERMAL_HOT, /* 85–95°C — system may reduce performance */
PLAYOS_THERMAL_CRITICAL /* ≥ 95°C — system will shut down */
} PlayOSThermalState;
typedef enum {
PLAYOS_PERF_BALANCED, /* system-managed (default) */
PLAYOS_PERF_POWER_SAVE, /* low TDP, extended battery */
PLAYOS_PERF_PERFORMANCE /* high TDP, best GPU/CPU */
} PlayOSPerfProfile;
typedef struct {
PlayOSPowerState power_state;
int battery_percent; /* 0–100; -1 if unknown */
int minutes_remaining; /* -1 if unknown or charging */
PlayOSThermalState thermal_state;
int cpu_temp_c;
int gpu_temp_c;
PlayOSPerfProfile active_profile;
} PlayOSPowerInfo;
int playos_power_get_info(PlayOSPowerInfo *info);
/* Request a performance profile. PlayOS may deny or override based on thermal state.
Returns 0 if accepted, -1 if denied. */
int playos_power_request_profile(PlayOSPerfProfile profile);
5.8 playos_logging.h
Structured logging routed to the PlayOS log system.
typedef enum {
PLAYOS_LOG_DEBUG = 0,
PLAYOS_LOG_INFO = 1,
PLAYOS_LOG_WARN = 2,
PLAYOS_LOG_ERROR = 3
} PlayOSLogLevel;
/* Log a structured message.
tag: short category string, e.g. "audio", "render", "save"
fmt: printf-style format string */
void playos_log(PlayOSLogLevel level, const char *tag, const char *fmt, ...);
/* Convenience macros */
#define PLAYOS_LOG_D(tag, ...) playos_log(PLAYOS_LOG_DEBUG, tag, __VA_ARGS__)
#define PLAYOS_LOG_I(tag, ...) playos_log(PLAYOS_LOG_INFO, tag, __VA_ARGS__)
#define PLAYOS_LOG_W(tag, ...) playos_log(PLAYOS_LOG_WARN, tag, __VA_ARGS__)
#define PLAYOS_LOG_E(tag, ...) playos_log(PLAYOS_LOG_ERROR, tag, __VA_ARGS__)
/* Mark a crash point before calling abort() or raising a fatal signal. */
void playos_log_crash_marker(const char *reason);
Log output is written to /data/log/<game-id>/session-<timestamp>.log and to the kernel ring buffer (for development builds).
6. Backend Architecture
libplayos uses an internal backend interface to abstract platform details. The backend is selected at runtime via the PLAYOS_BACKEND environment variable or auto-detection.
playos_input_get_controller_state()
│
▼
PlayOSInputBackend.get_controller_state()
│
├── "evdev" → reads from /dev/input/event* via Linux evdev
└── "stub" → returns zeroed state (testing/CI)
The backend is an internal detail. Games always call the public API; the backend is never exposed.
7. C++ and Engine Wrappers
playos-platform-api may provide optional C++ wrappers and engine adapters:
include/playos/playos.hpp— C++ RAII wrappers and enum class aliasessrc/backends/rcore_playos.c— Raylib platform backend (lives inplayos-shell, notplayos-platform-api)- Future: Godot, SDL2 adapter headers
These wrappers must not replace the C ABI as the source of truth. They are convenience layers built on top of it.
8. What the API Must Never Expose
| Forbidden | Reason |
|---|---|
| DRM file descriptors or device paths | Direct display access belongs to playos-compositor |
| Wayland display or socket handle | Wayland connection is managed by the Raylib backend |
playos-runtime IPC socket path or fd | Internal transport is private |
| Another game's storage paths | Isolation is a security guarantee |
playos-init control IPC | Process control is a trusted-only path |
| Raw Linux input event codes | Logical mapping is the stable abstraction |
| Kernel module or firmware interfaces | Not a public game concern |
9. Change Process
- Additive changes (new functions, new structs, new enum values): PR to
playos-platform-api, reviewed against this spec. - Potentially breaking changes: RFC issue in
playos-spec, reviewed by at least two contributors, resulting in an ADR. - Breaking changes: ADR required, major SONAME bump, migration guide, minimum 1-sprint notice before adoption in dependent repos.
See architecture.md §7.6 for the component overview.
PlayOS Runtime IPC Specification
Authoritative repository:
playos-runtime
Protocol version: 1
Cross-references: architecture.md §7.7, security-model.md
This document specifies the internal PlayOS IPC protocol. It is not a public application interface. Games and non-trusted clients must never connect to these endpoints.
Table of Contents
- Overview
- Transport
- Access Control
- Message Format
- Control IPC —
control.sock - Lifecycle Transport — per-game fd
- Compositor Control Channel
- Protocol Versioning
- Error Handling
1. Overview
playos-runtime owns the private integration layer between trusted PlayOS system components. It defines three distinct communication channels:
| Channel | Transport | Direction | Purpose |
|---|---|---|---|
| Control IPC | Unix socket | Shell/overlay → playos-init | Game launch, shutdown, factory reset, system commands |
| Lifecycle transport | Pipe fd per game | playos-init → game | Lifecycle events (foreground, background, terminate) |
| Compositor control | Unix socket | playos-init/runtime → compositor | Set expected game, show/hide overlay, force game exit |
Normal games never use these channels directly. They receive lifecycle events through playos_lifecycle_poll() from playos-platform-api, which reads from the lifecycle fd.
2. Transport
Control IPC socket
/run/playos/control.sock
Type: SOCK_SEQPACKET (message-boundaries preserved, reliable, ordered)
Owner: root:playos-trusted
Mode: 0660
Compositor control socket
/run/playos/compositor.sock
Type: SOCK_SEQPACKET
Owner: root:playos-trusted
Mode: 0660
Lifecycle fd
A write end of a pipe(2) passed to the game process as PLAYOS_LIFECYCLE_FD in its environment. The game process owns the read end; playos-init writes events to the write end.
3. Access Control
Only processes in the playos-trusted UNIX group may connect to control sockets.
| Component | Group membership |
|---|---|
playos-init | root (owns sockets) |
playos-compositor | playos-trusted |
playos-shell | playos-trusted |
playos-overlay | playos-trusted |
| Active game | Not in playos-trusted — no socket access |
The lifecycle fd is a one-directional pipe: the game can only read from it, never write to it or connect to any IPC socket.
4. Message Format
All messages use a simple length-prefixed binary frame:
+--------+--------+---...---+
| magic | length | body |
| 4 bytes| 4 bytes| N bytes |
+--------+--------+---...---+
- magic:
0x504C4F53(PLOSin ASCII) — validates frame start - length: little-endian uint32, byte count of body only
- body: JSON-encoded message (UTF-8, no trailing null)
Using JSON for the body makes messages human-readable for debugging while keeping the framing simple and versioned.
Maximum message size: 65536 bytes (64 KB)
Example message:
{
"v": 1,
"type": "LaunchGame",
"game_id": "com.example.game",
"manifest_path": "/data/games/com.example.game/manifest.json"
}
All messages include "v" (protocol version) and "type" fields.
5. Control IPC — control.sock
Trusted clients (shell, overlay) send requests; playos-init sends responses and async events.
Request → Response messages
LaunchGame
{
"v": 1,
"type": "LaunchGame",
"game_id": "com.example.game",
"manifest_path": "/data/games/com.example.game/manifest.json"
}
Response:
{ "v": 1, "type": "LaunchGameAck", "game_id": "com.example.game", "launch_token": "<uuid>" }
{ "v": 1, "type": "LaunchGameError", "game_id": "com.example.game", "reason": "already_running" }
Error reasons: already_running, invalid_manifest, executable_not_found, unsupported_api_version, permission_denied
TerminateGame
{ "v": 1, "type": "TerminateGame", "game_id": "com.example.game", "force": false }
force: true — skip cooperative SIGTERM and go straight to SIGKILL after 500ms.
Response:
{ "v": 1, "type": "TerminateGameAck", "game_id": "com.example.game" }
QueryStatus
{ "v": 1, "type": "QueryStatus" }
Response:
{
"v": 1,
"type": "StatusReport",
"compositor_pid": 42,
"compositor_state": "GAME_FOREGROUND",
"game_pid": 123,
"game_id": "com.example.game",
"uptime_s": 3600
}
game_pid and game_id are null when no game is running.
Shutdown
{ "v": 1, "type": "Shutdown" }
playos-init delivers PLAYOS_LIFECYCLE_TERMINATE to the game, waits up to 2 seconds, then kills all processes, syncs filesystems, and calls reboot(RB_POWER_OFF).
Reboot
{ "v": 1, "type": "Reboot" }
Same as Shutdown but calls reboot(RB_AUTOBOOT).
FactoryReset
{
"v": 1,
"type": "FactoryReset",
"erase_games": false,
"erase_saves": false,
"erase_cache": true,
"erase_config": true,
"erase_logs": false
}
Requires no active game. Erases selected /data/ subdirectories and recreates them.
Response:
{ "v": 1, "type": "FactoryResetComplete" }
{ "v": 1, "type": "FactoryResetError", "reason": "game_running" }
SetPerfProfile
{ "v": 1, "type": "SetPerfProfile", "profile": "balanced" }
profile values: "balanced", "power_save", "performance"
Response:
{ "v": 1, "type": "SetPerfProfile", "accepted": true }
{ "v": 1, "type": "SetPerfProfile", "accepted": false, "reason": "thermal_denied" }
reason values: thermal_denied, invalid_profile, epp_write_failed
Suspend
{ "v": 1, "type": "Suspend" }
Fire-and-forget. playos-init delivers PLAYOS_LIFECYCLE_SUSPEND to the active game, attempts S3 suspend (mem to /sys/power/state), then delivers PLAYOS_LIFECYCLE_RESUME after resume (or immediately on failure). No response is sent.
ApplyUpdate
{ "v": 1, "type": "ApplyUpdate", "path": "/data/updates/0.2.0.playosb" }
Requests that playos-init apply a system update bundle at path to the inactive slot. path must reside under /data/updates/ and carry the .playosb suffix. Exactly one update may be in flight at a time.
Response:
{ "v": 1, "type": "ApplyUpdateAck", "accepted": true }
{ "v": 1, "type": "ApplyUpdateError", "reason": "..." }
Error reasons: not_found, invalid_bundle, signature_invalid, update_in_progress, game_running, internal_error
Progress is reported via the async UpdateProgress / UpdateComplete / UpdateError events below.
Async events (init → client, unsolicited)
GameStarted
{
"v": 1,
"type": "GameStarted",
"game_id": "com.example.game",
"pid": 456,
"launch_token": "<uuid>"
}
GameExited
{
"v": 1,
"type": "GameExited",
"game_id": "com.example.game",
"exit_code": 0
}
GameCrashed
{
"v": 1,
"type": "GameCrashed",
"game_id": "com.example.game",
"exit_code": 134,
"signal": 6
}
ThermalStateChanged
{ "v": 1, "type": "ThermalStateChanged", "state": 2 }
state values (integer): 0 normal, 1 warm, 2 hot, 3 critical
PerfProfileChanged
{ "v": 1, "type": "PerfProfileChanged", "profile": 1 }
profile values (integer): 0 balanced, 1 power_save, 2 performance
UpdateProgress
{ "v": 1, "type": "UpdateProgress", "step": "verify", "percent": 25 }
step values: verify, write_inactive_slot, write_efi, update_boot_json, sync. percent is 0–100.
UpdateComplete
{ "v": 1, "type": "UpdateComplete", "active_slot": "b", "version": "0.2.0" }
Emitted after the inactive slot is written and boot.json is switched. The system requires a reboot to boot the new slot.
UpdateError
{ "v": 1, "type": "UpdateError", "step": "verify", "reason": "signature_invalid" }
Emitted when an update fails after being accepted. reason matches the ApplyUpdateError reason set.
6. Lifecycle Transport — per-game fd
playos-init passes PLAYOS_LIFECYCLE_FD=<n> in the game's environment. The fd is the read end of a pipe.
Each event is a single byte:
| Byte value | Event |
|---|---|
0x00 | PLAYOS_LIFECYCLE_FOREGROUND |
0x01 | PLAYOS_LIFECYCLE_BACKGROUND |
0x02 | PLAYOS_LIFECYCLE_SUSPEND |
0x03 | PLAYOS_LIFECYCLE_RESUME |
0x04 | PLAYOS_LIFECYCLE_TERMINATE |
On EOF (pipe write end closed): treated as TERMINATE.
playos_lifecycle_poll() in libplayos reads from this fd.
7. Compositor Control Channel
playos-init (and playos-runtime client library) communicates with playos-compositor via /run/playos/compositor.sock.
This channel uses the same framing as control IPC.
SetExpectedGame
{ "v": 1, "type": "SetExpectedGame", "launch_token": "<uuid>", "game_id": "com.example.game" }
Tells the compositor which Wayland client to expect. The compositor matches by checking the PLAYOS_LAUNCH_TOKEN environment variable of connecting clients.
ClearExpectedGame
{ "v": 1, "type": "ClearExpectedGame" }
ForceTerminateGame
{ "v": 1, "type": "ForceTerminateGame" }
Compositor destroys the game surface immediately (for crash recovery). playos-init handles the actual process kill.
ShowOverlay
{ "v": 1, "type": "ShowOverlay" }
HideOverlay
{ "v": 1, "type": "HideOverlay" }
Compositor → init events
GameSurfaceReady
{ "v": 1, "type": "GameSurfaceReady", "launch_token": "<uuid>" }
Emitted when the game commits its first valid buffer. playos-init records this as a successful launch.
CompositorStateChanged
{ "v": 1, "type": "CompositorStateChanged", "state": "GAME_FOREGROUND" }
state values: SHELL_FOREGROUND, GAME_STARTING, GAME_FOREGROUND, PLAYOS_UI_FOREGROUND_WITH_GAME_BACKGROUND, TERMINATING_GAME
8. Protocol Versioning
All messages include "v": <version_integer>. The current version is 1.
On version mismatch:
{ "v": 1, "type": "ProtocolError", "reason": "version_mismatch", "supported": [1] }
The receiver closes the connection after sending this error.
Backward compatibility: A server implementing version N must also accept messages with "v": M where M < N, treating unknown fields as ignored. It must not accept "v": M where M > N.
9. Error Handling
All request messages may receive a generic error response:
{ "v": 1, "type": "Error", "reason": "internal_error", "message": "..." }
reason values: version_mismatch, invalid_message, permission_denied, internal_error, not_implemented
Connection loss: If playos-init loses a trusted client connection unexpectedly, it logs the event. This does not affect system operation. Clients should reconnect with exponential backoff.
Rate limiting: playos-init may reject rapid repeated requests (e.g., rapid LaunchGame calls) with reason: "rate_limited".
PlayOS Private Wayland Protocol Specification
Authoritative repository:
playos-runtime/protocols/
Protocol name:playos_v1
Generated with:wayland-scanner
Cross-references: architecture.md §9.3, playos-compositor-spec.md
Overview
Standard Wayland protocols are used wherever possible. The private PlayOS protocol covers only console presentation and lifecycle concerns that have no standard equivalent.
In-scope for this protocol:
- Registering the trusted shell and overlay roles
- Surface readiness signals
- Foreground/background transition notifications
- PlayOS-specific output information
Out of scope (handled by control IPC, not Wayland):
- Game installation and management
- Save data
- Networking
- System updates
- General process control
Protocol File
Located at: playos-runtime/protocols/playos-v1.xml
<?xml version="1.0" encoding="UTF-8"?>
<protocol name="playos_v1">
<copyright>
Copyright © PlayOS contributors.
SPDX-License-Identifier: MIT
</copyright>
<description summary="PlayOS private console compositor protocol">
Private protocol between playos-compositor and trusted PlayOS clients.
Not exposed to untrusted game clients.
</description>
<!-- ─────────────────────────────────────────────────────── -->
<!-- playos_manager_v1 -->
<!-- Global singleton — trusted clients bind this first -->
<!-- ─────────────────────────────────────────────────────── -->
<interface name="playos_manager_v1" version="1">
<description summary="PlayOS compositor management interface">
Trusted clients bind this global to access PlayOS-specific compositor
capabilities. Access is enforced by UNIX credentials (PLAYOS_TRUSTED_SHELL
or PLAYOS_TRUSTED_OVERLAY environment variable check at connection time).
</description>
<!-- Role registration -->
<request name="register_shell">
<description summary="Identify this client as the trusted shell"/>
</request>
<request name="register_overlay">
<description summary="Identify this client as the trusted overlay"/>
</request>
<!-- Compositor state events -->
<event name="compositor_state_changed">
<description summary="Compositor lifecycle state changed"/>
<arg name="state" type="uint" summary="playos_compositor_state enum value"/>
</event>
<!-- Enumerations -->
<enum name="compositor_state">
<entry name="shell_foreground" value="0"/>
<entry name="game_starting" value="1"/>
<entry name="game_foreground" value="2"/>
<entry name="playos_ui_foreground_with_game_background" value="3"/>
<entry name="terminating_game" value="4"/>
</enum>
<enum name="error">
<entry name="role_already_taken" value="0" summary="Another client already holds this role"/>
<entry name="permission_denied" value="1" summary="Client is not trusted"/>
</enum>
</interface>
<!-- ─────────────────────────────────────────────────────── -->
<!-- playos_shell_v1 -->
<!-- Interface given to the registered trusted shell -->
<!-- ─────────────────────────────────────────────────────── -->
<interface name="playos_shell_v1" version="1">
<description summary="Interface for the trusted PlayOS shell"/>
<!-- Shell → compositor -->
<request name="set_surface">
<description summary="Associate a wl_surface as the shell surface"/>
<arg name="surface" type="object" interface="wl_surface"/>
</request>
<request name="surface_ready">
<description summary="Shell has rendered its first frame and is ready to display"/>
</request>
<!-- Compositor → shell -->
<event name="lifecycle_event">
<description summary="Lifecycle event for the shell"/>
<arg name="event" type="uint" summary="playos_lifecycle_event enum value"/>
</event>
<event name="game_launched">
<description summary="A game process has been spawned"/>
<arg name="game_id" type="string"/>
</event>
<event name="game_exited">
<description summary="The active game exited or crashed"/>
<arg name="game_id" type="string"/>
<arg name="exit_code" type="int"/>
<arg name="crashed" type="uint" summary="1 if abnormal exit"/>
</event>
<enum name="playos_lifecycle_event">
<entry name="foreground" value="1"/>
<entry name="background" value="2"/>
<entry name="suspend" value="3"/>
<entry name="resume" value="4"/>
<entry name="terminate" value="5"/>
</enum>
</interface>
<!-- ─────────────────────────────────────────────────────── -->
<!-- playos_overlay_v1 -->
<!-- Interface given to the registered trusted overlay -->
<!-- ─────────────────────────────────────────────────────── -->
<interface name="playos_overlay_v1" version="1">
<description summary="Interface for the trusted PlayOS overlay"/>
<!-- Overlay → compositor -->
<request name="set_surface">
<description summary="Associate a wl_surface as the overlay surface"/>
<arg name="surface" type="object" interface="wl_surface"/>
</request>
<request name="surface_ready">
<description summary="Overlay has rendered and is ready to display"/>
</request>
<request name="request_dismiss">
<description summary="Overlay requests to be hidden (e.g. user pressed Resume)"/>
</request>
<!-- Compositor → overlay -->
<event name="about_to_show">
<description summary="Compositor is about to map the overlay; overlay should prepare its frame"/>
</event>
<event name="about_to_hide">
<description summary="Compositor is about to unmap the overlay"/>
</event>
<event name="output_info">
<description summary="Current output dimensions for overlay layout"/>
<arg name="width" type="int"/>
<arg name="height" type="int"/>
<arg name="refresh_mhz" type="uint" summary="Refresh rate in mHz, e.g. 60000 = 60Hz"/>
<arg name="scale_100" type="uint" summary="Scale factor × 100, e.g. 100 = 1.0×"/>
</event>
</interface>
</protocol>
Client Trust Model
The compositor enforces trust at connection time, not through Wayland protocol negotiation:
| Client type | Trust check | Interfaces available |
|---|---|---|
| Trusted shell | PLAYOS_TRUSTED_SHELL=1 in env | playos_manager_v1, playos_shell_v1, standard Wayland |
| Trusted overlay | PLAYOS_TRUSTED_OVERLAY=1 in env | playos_manager_v1, playos_overlay_v1, standard Wayland |
| Active game | No trust flag | Standard Wayland only (wl_compositor, wl_seat, xdg_wm_base) |
The compositor does not advertise playos_manager_v1 in the global registry. Trusted clients must explicitly bind by name. Untrusted clients that attempt to bind privileged interfaces receive a wl_display.error and are disconnected.
Standard Protocols Exposed to All Clients
wl_compositor surface creation
wl_shm shared memory buffers
wl_seat keyboard, pointer, touch input
xdg_wm_base toplevel and popup surfaces
wp_presentation presentation timing (optional, for frame pacing)
Standard Protocols Withheld from Games
zwp_linux_dmabuf_v1 (games use EGL/Wayland via Raylib, not raw dmabuf)
wlr_output_management (display configuration is compositor-only)
wlr_layer_shell (layer surfaces are compositor-policy territory)
wlr_screencopy (no screen capture by games)
wp_drm_lease (no DRM access by games)
Build Integration
# In playos-runtime/Makefile or CMakeLists.txt:
wayland-scanner client-header protocols/playos-v1.xml > gen/playos-v1-client.h
wayland-scanner server-header protocols/playos-v1.xml > gen/playos-v1-server.h
wayland-scanner private-code protocols/playos-v1.xml > gen/playos-v1.c
Both playos-compositor (server-side) and trusted client libraries (client-side) link against the generated code.
Versioning
Protocol interfaces are versioned with the version attribute in the XML. Bumping a version requires:
- Adding a new
<event>or<request>(never removing existing ones) - Incrementing
versionin the<interface>element - Updating the version bind check in compositor and client code
- Adding a CHANGELOG entry to
playos-runtime
PlayOS Security Model
Cross-references: architecture.md §13–15, runtime-ipc.md §3, sprints/Sprint-12.md
Table of Contents
- Trust Zones
- Component Privilege Levels
- Game Restrictions
- IPC Access Control
- Filesystem Access Control
- seccomp Filter Policy
- Landlock Filesystem Restrictions
- Input Security
- System Image Integrity
- Secure Boot Chain
- Development vs Production
- Post-MVP Hardening Roadmap
1. Trust Zones
┌───────────────────────────────────────────────────────────────┐
│ Zone 1: Kernel │
│ Linux kernel + drivers │
│ Full hardware access │
└───────────────────┬───────────────────────────────────────────┘
│
┌───────────────────▼───────────────────────────────────────────┐
│ Zone 2: Trusted System Components │
│ playos-init (root) │
│ playos-compositor (display + input caps) │
│ playos-shell (playos-trusted group) │
│ playos-overlay (playos-trusted group) │
│ │
│ ← communicate via /run/playos/ UNIX sockets → │
└───────────────────┬───────────────────────────────────────────┘
│ public libplayos C ABI only
┌───────────────────▼───────────────────────────────────────────┐
│ Zone 3: Untrusted Game Process │
│ User: playos-game (unprivileged) │
│ No access to Zone 2 sockets │
│ No DRM primary nodes │
│ No raw input devices │
│ Restricted to own save/cache directories │
│ seccomp + Landlock enforced │
└───────────────────────────────────────────────────────────────┘
2. Component Privilege Levels
| Component | User | Capabilities | Notes |
|---|---|---|---|
playos-init | root | All (required for process supervision, mounts, device setup) | Drop unnecessary caps after init |
playos-compositor | playos-system | CAP_SYS_ADMIN (DRM master), CAP_DAC_READ_SEARCH (device nodes) | Drop all others |
playos-shell | playos-system | None | Member of playos-trusted group |
playos-overlay | playos-system | None | Member of playos-trusted group |
| Active game | playos-game | None | PR_SET_NO_NEW_PRIVS = 1 before exec |
playos-installer | root | All | Only present in installer image |
3. Game Restrictions
A normal game must not be able to:
| Action | Enforcement |
|---|---|
| Modify the system image | System partition mounted ro; game user has no write access |
Open DRM primary nodes (/dev/dri/card*) | drm group; game is not in it |
| Access another game's save data | Landlock path restrictions + per-game directory |
| Mount or format filesystems | seccomp blocks mount, umount2 |
| Load kernel modules | seccomp blocks init_module, finit_module |
| Change kernel parameters | seccomp blocks sysctl, game user has no /proc/sys write access |
| Invoke unrestricted shutdown/reboot | seccomp blocks reboot syscall |
| Connect to control IPC | UNIX group restriction; game is not in playos-trusted |
| Synthesize reserved system input | Input routing is compositor-enforced at Wayland/evdev level |
| Create trusted overlays | Trusted roles require PLAYOS_TRUSTED_* env + group check |
| Ptrace other processes | seccomp blocks ptrace |
| Escalate privileges | PR_SET_NO_NEW_PRIVS; seccomp blocks setuid, setcap |
4. IPC Access Control
Control socket (/run/playos/control.sock)
- Owner:
root:playos-trusted, mode0660 - Who can connect:
playos-shell,playos-overlay(both inplayos-trusted) - Who cannot:
playos-game— enforced by UNIX group check atconnect(2)time
Compositor socket (/run/playos/compositor.sock)
- Owner:
root:playos-trusted, mode0660 - Who can connect:
playos-runtimeinternal client only - Who cannot:
playos-game
Lifecycle fd (PLAYOS_LIFECYCLE_FD)
- Direction: Write end held by
playos-init; read end passed to game - Game can: Read lifecycle events (single-byte values)
- Game cannot: Write to the fd; the write end is
close()d in the game process before exec - Not a socket: Cannot be used to connect to any IPC endpoint
5. Filesystem Access Control
System partition (/)
- Mounted read-only via
MS_RDONLY(Sprint 11) - dm-verity hash tree appended to system image (Sprint 12+ — planned, not yet implemented)
- Any write attempt returns
EROFS
Data partition (/data)
- Mounted read-write, owned by
root - Per-game directories:
chown playos-game:playos-game /data/saves/<id>andcache/<id> - Other directories (
config/,games/,logs/) owned byplayos-system, not writable by games
Device nodes
| Device | Owner | Mode | Game access |
|---|---|---|---|
/dev/dri/card* | root:drm | 0660 | ❌ Not in drm group |
/dev/dri/renderD* | root:render | 0660 | ✅ In render group (needed for Wayland/EGL) |
/dev/input/event* | root:input | 0660 | ❌ Not in input group (input via Wayland seat only) |
/run/playos/*.sock | root:playos-trusted | 0660 | ❌ Not in playos-trusted |
Note: Games access GPU rendering through the Wayland EGL surface, not directly through render nodes.
6. seccomp Filter Policy
Applied to game processes via libseccomp before execve(). Default action: SECCOMP_RET_ERRNO(EPERM).
Allowed syscalls (core game set)
# Memory management
mmap, munmap, mprotect, mremap, madvise, brk
# File I/O
read, readv, write, writev, open, openat, close, stat, fstat, lstat,
newfstatat, statx, lseek, dup, dup2, ioctl (restricted — see below),
fcntl, access, faccessat, getdents64, getcwd
# Network (AF_UNIX only for Wayland socket)
socket (AF_UNIX only — enforced by arg filter), connect, bind,
accept, sendmsg, recvmsg, sendto, recvfrom, shutdown, getsockname, getpeername
# Processes and threads
exit, exit_group, clone, clone3, fork, execve (restricted — no setuid),
wait4, waitid, getpid, getppid, gettid, set_tid_address, prctl (restricted)
# Synchronization
futex, futex_waitv, nanosleep, clock_nanosleep
# Signals
rt_sigaction, rt_sigprocmask, rt_sigreturn, sigaltstack, kill (self only)
# Time
clock_gettime, clock_getres, gettimeofday, time
# Misc
getrandom, getuid, getgid, geteuid, getegid, uname, sysinfo,
pread64, pwrite64, eventfd2, epoll_create1, epoll_ctl, epoll_wait,
pipe2, timerfd_create, timerfd_settime, timerfd_gettime,
inotify_init1, inotify_add_watch, inotify_rm_watch,
mlock, munlock, memfd_create
Blocked syscalls (fatal SIGSYS or EPERM)
mount, umount2, umount # no filesystem mounting
init_module, finit_module, delete_module # no kernel modules
reboot # no direct reboot
ptrace # no process tracing
setuid, setgid, setresuid, setresgid, setfsuid, setfsgid # no privilege escalation
capset, prctl(PR_SET_SECCOMP) # no capability changes
sysctl, nfsservctl # no kernel parameter changes
kexec_load, kexec_file_load # no kernel replacement
iopl, ioperm # no direct I/O port access
perf_event_open # no performance counters (in retail builds)
ioctl restrictions
ioctl is allowed only with these device categories:
- Wayland socket (AF_UNIX)
/dev/dri/renderD*(DRI render node — needed for EGL)/dev/dri/card*— blocked (prevents DRM master)
7. Landlock Filesystem Restrictions
Requires Linux ≥ 5.13 (ROG Ally ships with kernels that support this). Falls back to logging-only if unavailable.
Allowed paths for game processes
| Path | Access |
|---|---|
/data/games/<game-id>/ | Read-only |
/data/saves/<game-id>/ | Read + write + create + remove |
/data/cache/<game-id>/ | Read + write + create + remove |
/run/playos/playos-0 (Wayland socket) | Connect (execute) |
/dev/dri/renderD* | Read (for EGL) |
/dev/urandom, /dev/random | Read |
/proc/self/ | Read-only |
/tmp/game-<id>/ | Read + write + create + remove |
Denied (implicitly — not in allowed set)
/data/games/<other-game-id>/— other games/data/saves/<other-game-id>/— other games' saves/data/config/— system configuration/data/log/— system logs (games write viaplayos_log(), not direct fs access)/run/playos/control.sock— control IPC/run/playos/compositor.sock— compositor control/sys/,/proc/<other-pid>/— system and process snooping/dev/dri/card*— DRM primary nodes
8. Input Security
Current gap (pre-Sprint 12): The target model below is not yet implemented. Today reserved buttons are stripped only by a software bitmask in
libplayos(playos_input.c), and games are spawned via a plainfork()+exec()from PID 1 (root) with no credential drop, so a game can open/dev/input/event*directly and read the reserved buttons — bypassing the mask. Sprint 12 closes this gap (seeSprint-12.md§Input Device Isolation).
Reserved system actions (PLAYOS_BUTTON_SYSTEM, PLAYOS_BUTTON_QUICK_MENU) are intercepted at the Wayland compositor's libinput layer before any event reaches a client. They are never present in the game's input stream.
Input routing hierarchy:
libinput event
│
▼ playos-compositor intercepts
├── reserved action → PlayOS only (never to client)
├── overlay visible → overlay Wayland client
├── game foreground → game Wayland client (filtered: no reserved keys)
└── otherwise → shell Wayland client
Games receive input exclusively through the Wayland seat (not raw evdev). They cannot open /dev/input/event* (not in the input group; Landlock also blocks the path).
9. System Image Integrity
Sprint 11 (initial): Read-only mount
mount(device, "/", "ext4", MS_RDONLY, NULL);
Any write to the system partition returns EROFS.
Post-Sprint 12 (production): dm-verity
# At build time (in playos-refdistro release pipeline):
veritysetup format system.img system.img.verity > system.verity.superblock
At boot, playos-init:
- Sets up a dm-verity device over the system partition
- Mounts the dm-verity device read-only
- If hash verification fails for any block, the kernel returns I/O errors (enforced by dm-verity)
playos-initmonitors for dm-verity errors; repeated errors trigger A/B rollback
10. Secure Boot Chain
Target signing chain (post-MVP, Sprint 12 foundations)
UEFI Secure Boot (platform key)
└── signs BOOTX64.EFI
BOOTX64.EFI (Linux EFI stub)
└── kernel + embedded initramfs (verified by EFI stub signature)
Kernel (IMA or dm-verity)
└── system partition hash tree (dm-verity root hash embedded in initramfs)
A/B update bundles
└── signed with PlayOS update key (RAUC bundle signature)
Development key setup (Sprint 12)
- Self-signed certificate used for development and CI builds
- Production: HSM-backed key, never leaves the signing server
sbsignused in the release pipeline
Recovery
If Secure Boot verification fails:
- UEFI firmware refuses to boot the artifact
- User must boot into UEFI Secure Boot key management to enroll the PlayOS development key (dev builds)
- Production: chain-of-trust failure surfaces as boot failure → A/B rollback → recovery mode
11. Development vs Production
| Feature | Development image | Production image |
|---|---|---|
| BusyBox shell | ✅ Present | ❌ Absent |
| SSH daemon | ❌ (planned post-network sprint) | ❌ Absent |
gdbserver, strace | ✅ Present | ❌ Absent |
| Serial console | ✅ Enabled | ✅ Enabled (needed for recovery) |
| dm-verity | Optional | ✅ Required |
| Secure Boot | Optional (disabled ok) | ✅ Required |
| Debug assertions | ✅ Enabled | ❌ Disabled |
| Open TCP sockets | Allowed (SSH) | ❌ None |
| seccomp | ✅ Enforced | ✅ Enforced |
| Landlock | ✅ Enforced | ✅ Enforced |
The post-build production lint CI step asserts:
- No
/bin/shor/bin/busyboxin the image - No open listening TCP sockets
- No
gdbserver,strace, or debug tools - System partition is read-only
- All EFI artifacts are signed
12. Post-MVP Hardening Roadmap
In priority order after v0.1.0:
- dm-verity for system partition integrity (Sprint 12 gap)
- Signed game manifests (Ed25519 — warn-only in Sprint 12, enforced post-MVP)
- User namespaces for additional game isolation if needed
- Hardware-backed keys for update signing
- IMA/EVM for individual file integrity in the initramfs
- Audit logging for privileged IPC commands
- Network namespace for games (when networking is introduced)
- Mandatory access control (SELinux or AppArmor) if seccomp + Landlock proves insufficient
playos-init Specification
Repository:
playos-init
Role: PID 1, process supervisor, boot orchestrator
Language: C (or Rust)
Cross-references: architecture.md §6–7.1, runtime-ipc.md, sprints/Sprint-1.md
Responsibilities
playos-init owns process lifecycle and boot. It does not own surfaces, focus, rendering, game logic, or network policy.
| Owns | Does NOT own |
|---|---|
| Boot and service lifecycle | UI or rendering |
| Virtual filesystem mounts | Input routing |
| Storage discovery and mount | Display configuration |
Starting and supervising playos-compositor | Game-specific logic |
| Game launch validation | Network policy |
| Process spawning, monitoring, reaping | Package management |
| Lifecycle fd creation and event delivery | |
| Forced game pause/kill fallback | |
| Shutdown, reboot, factory reset, recovery |
Boot Sequence
Kernel starts /init (playos-init, PID 1)
│
├── Mount /dev (devtmpfs), /proc, /sys, /run (tmpfs)
├── Open log sink: /run/playos/log/init.log (ring buffer, bounded)
├── Discover and validate data partition
│ ├── Found → mount /data (ext4, rw)
│ └── Not found → provisioning mode (halt with diagnostic)
├── First-boot: create /data directory tree
├── Create /run/playos/ directory tree
├── Bind control IPC socket: /run/playos/control.sock
├── Bind compositor control socket: /run/playos/compositor.sock
│
└── Start playos-compositor
├── Wait for compositor readiness signal (fd/pipe)
└── On ready: compositor loop begins
Process Supervision
playos-init acts as a proper PID 1 supervisor:
- Zombie reaping: Calls
waitpid(-1, WNOHANG)in a loop onSIGCHLD - Compositor supervision:
- On compositor exit (any reason): record exit status, wait
COMPOSITOR_RESTART_DELAY_MS(500ms), restart - After
COMPOSITOR_MAX_RESTARTS(default: 3) withinCOMPOSITOR_WINDOW_S(default: 60s): enter recovery mode
- On compositor exit (any reason): record exit status, wait
- Game supervision:
- Track game PID and
game_id - On game exit: emit
GameExitedIPC event; update internal state; unblock the shell - On crash: set
crashed=trueinGameExited
- Track game PID and
- Overlay process: Supervised same as compositor (restart on exit)
Supervision table:
| Process | Restart policy | Failure action |
|---|---|---|
playos-compositor | Restart, up to N times | Recovery mode |
playos-shell | Restart (shell is always alive) | Restart compositor session |
playos-overlay | Restart | Log, continue |
| Active game | Never restart automatically | Emit GameExited(crashed=true) |
Storage Discovery
playos-init searches for the data partition in order:
- Partition with label
playos-data - Partition with GUID
<TODO: define in playos-spec/schemas/disk-layout.json> - UUID from kernel command line:
playos.data_uuid=<uuid>
PlayOS must never silently format. If the partition is not found:
- Log the search results (devices enumerated, labels found)
- Enter provisioning mode: display a diagnostic (installer handles the UI)
- Do not format, write, or modify any disk
Game Launch Validation
Before spawning a game, playos-init validates:
| Check | Failure action |
|---|---|
| Only one game at a time | Return LaunchGameError(already_running) |
| Manifest file exists and is valid JSON | Return LaunchGameError(invalid_manifest) |
api_version ≤ PLAYOS_API_VERSION | Return LaunchGameError(unsupported_api_version) |
architecture matches running system | Return LaunchGameError(invalid_manifest) |
| Executable exists and is executable | Return LaunchGameError(executable_not_found) |
Manifest id matches directory name | Return LaunchGameError(invalid_manifest) |
Game Spawn Environment
playos-init prepares the following environment for the game process:
PLAYOS_GAME_ID=<game-id>
PLAYOS_INSTALL_PATH=/data/games/<game-id>
PLAYOS_SAVE_PATH=/data/saves/<game-id>
PLAYOS_CACHE_PATH=/data/cache/<game-id>
WAYLAND_DISPLAY=playos-0
PLAYOS_LIFECYCLE_FD=<fd>
PLAYOS_LAUNCH_TOKEN=<uuid4>
PLAYOS_API_VERSION=1
Before execve(), playos-init:
- Sets
PLAYOS_GAME_ID, paths, and lifecycle environment - Applies
PR_SET_NO_NEW_PRIVS = 1 - Drops all capabilities
- Applies seccomp filter (Sprint 12 — not yet implemented)
- Applies Landlock rules (Sprint 12 — not yet implemented)
- Drops
CAP_SETUID/CAP_SETGID execve()the game executable
Cooperative vs Forced Termination
On TerminateGame:
playos-init sends SIGTERM to game
│
├── Game exits within GAME_EXIT_TIMEOUT_MS (default: 2000ms) → clean exit
│
└── Timeout expires
│
playos-init sends SIGKILL
│
Game process is reaped; GameExited(crashed=false, force_killed=true) emitted
Non-cooperative backgrounding fallback (Sprint 7):
PLAYOS_LIFECYCLE_BACKGROUND delivered to game via lifecycle fd
│
├── Game reduces CPU within GAME_PAUSE_TIMEOUT_MS (default: 500ms) → OK
│
└── Timeout: playos-init sends SIGSTOP to game process
SIGCONT is sent when the compositor transitions back to GAME_FOREGROUND.
The compositor requests SIGSTOP/SIGCONT via the compositor control socket; it does not send signals itself.
Shutdown and Reboot
On Shutdown or Reboot IPC:
- Deliver
PLAYOS_LIFECYCLE_TERMINATEto the active game (if any) via lifecycle fd - Wait up to 2 seconds for game to exit
- Send SIGKILL to game if still alive
- Send SIGTERM to compositor
- Wait up to 2 seconds for compositor to exit
- Sync all filesystems:
sync() - Call
reboot(RB_POWER_OFF)orreboot(RB_AUTOBOOT)
Thermal & Power Management
playos-init owns the thermal safety and performance-profile policy. It runs a 1 Hz tick folded into the existing supervisor loop (no dedicated thread).
Sensors
| Reading | Source (in order) |
|---|---|
| CPU temperature | /sys/class/thermal/thermal_zone*/type = x86_pkg_temp, then cpu_thermal |
| GPU temperature | /sys/class/hwmon/hwmon*/ with a name of amdgpu, reading temp1_input |
| Battery state | Delegated to playos-platform-api (playos_power_get_info) |
Thermal states
| State | Range (default) | Action |
|---|---|---|
NORMAL | < 75 °C | None |
WARM | 75–85 °C | None (monitor) |
HOT | 85–95 °C | Reject/back off PERFORMANCE profile |
CRITICAL | ≥ 95 °C | Force POWER_SAVE; shut down after 10 s without recovery |
Thresholds are read from /data/config/thermal.json:
{ "warm_c": 75, "hot_c": 85, "critical_c": 95 }
Missing or invalid values fall back to the defaults above.
On every state transition, playos-init emits a ThermalStateChanged IPC event.
Performance profiles
Profiles map to amd-pstate EPP values and are written to each online CPU's energy_performance_preference sysfs node:
| Profile | Wire name | EPP value |
|---|---|---|
BALANCED (0) | balanced | balance_performance |
POWER_SAVE (1) | power_save | power |
PERFORMANCE (2) | performance | performance |
SetPerfProfile IPC requests are honored except that a PERFORMANCE request is denied while the thermal state is HOT or CRITICAL. A rejected request carries "accepted": false with a "reason" of thermal_denied, epp_write_failed, or invalid_profile. A successful change emits PerfProfileChanged.
The active profile is synchronized from the kernel at boot when EPP is available.
Suspend
A Suspend IPC request (fire-and-forget) triggers:
- Deliver
PLAYOS_LIFECYCLE_SUSPENDto the active game via the lifecycle fd - Write
memto/sys/power/state - After resume (or immediately on failure), deliver
PLAYOS_LIFECYCLE_RESUME
Suspend is best-effort and never fatal.
Recovery Mode
Entered when:
- The compositor restarts and fails more than
COMPOSITOR_MAX_RESTARTStimes playos-initreceives aRecoveryModeIPC command- Boot count exceeds A/B rollback limit and both slots are bad
Recovery mode:
- Kill all non-init processes
- Attempt to start a recovery UI (SimpleDRM or framebuffer, no AMDGPU required)
- Show: log viewer, factory reset, rollback slot, reboot, shutdown options
- No shell, no game launch, no compositor restart
playos-compositor Specification
Repository:
playos-compositor
Role: wlroots-based Wayland compositor; permanent DRM/KMS owner
Language: C
Cross-references: architecture.md §7.2, §8–9, wayland-protocol.md, sprints/Sprint-2.md, sprints/Sprint-4.md
Responsibilities
playos-compositor owns display policy and input routing. It does not spawn processes, manage storage, or install games.
| Owns | Does NOT own |
|---|---|
| DRM/KMS devices and output state | Process spawning or supervision |
| wlroots backend, renderer, allocator, scene | Game installation or save management |
| Wayland display socket | Storage layout |
| Display selection, orientation, refresh, hotplug | Boot policy |
| Surface roles, z-order, visibility, focus | Network policy |
| Trusted shell and overlay identity | System updates |
| Expected game identity and first-frame activation | |
| Reserved system input interception | |
| Input routing between shell, game, overlay | |
| Direct-scanout policy | |
| Lifecycle state transitions | |
| Crash recovery (surface cleanup, return to shell) |
Initialization Order
main()
│
├── Parse args and environment
├── Open log sink
├── wl_display_create()
├── wlr_backend_autocreate() or wlr_drm_backend_create()
├── GPU discovery (enumerate DRM devices, select by PCI vendor)
├── wlr_renderer_autocreate() — GBM/EGL/GLES
├── wlr_allocator_autocreate()
├── wlr_compositor_create()
├── wlr_output_layout_create()
├── xdg_wm_base setup
├── wlr_seat_create()
├── libinput backend setup (reserved key interception)
├── PlayOS private protocol setup (playos_manager_v1, playos_shell_v1, playos_overlay_v1)
├── wl_display_add_socket_auto() → "playos-0"
├── wlr_backend_start()
├── Write readiness token to PLAYOS_COMPOSITOR_READY_FD
│
└── wl_display_run() — event loop
GPU Discovery
/* Do not assume /dev/dri/card0 */
drmDevice *devices[MAX_DRM_DEVICES];
int count = drmGetDevices2(0, devices, MAX_DRM_DEVICES);
for (int i = 0; i < count; i++) {
if (!(devices[i]->available_nodes & (1 << DRM_NODE_PRIMARY))) continue;
// Resolve PCI vendor ID
// Check if a connector is active on this device
// Validate renderer init
// Select if AMD (0x1002) or Intel (0x8086) with active display
}
Selection priority:
- Device with an active connected display
- AMD (primary platform)
- Intel
- First valid DRM device
Log: selected GPU path, PCI ID, connector name, preferred mode.
Lifecycle State Machine
The state machine is the central PlayOS contract. Must be explicitly tested.
SHELL_FOREGROUND
│ launch accepted (SetExpectedGame received via compositor socket)
▼
GAME_STARTING
│ game commits first valid wl_buffer (first-frame rule)
▼
GAME_FOREGROUND ◄──────────────────────────┐
│ PLAYOS_BUTTON_SYSTEM intercepted │ Resume requested
▼ │
PLAYOS_UI_FOREGROUND_WITH_GAME_BACKGROUND ──┘
│ Quit requested
▼
TERMINATING_GAME
│ GameExited received from playos-init
▼
SHELL_FOREGROUND ◄── game exits or crashes from any state
State is stored as a single enum in the compositor. All state transitions are logged.
State-specific behavior
| State | Shell surface | Game surface | Overlay surface | Input routing |
|---|---|---|---|---|
SHELL_FOREGROUND | Visible, focused | Hidden | Hidden | → Shell |
GAME_STARTING | Visible (launching UI) | Hidden | Hidden | → Shell |
GAME_FOREGROUND | Hidden | Visible, focused | Hidden | → Game (filtered) |
PLAYOS_UI_... | Hidden | Visible but unfocused | Visible, focused | → Overlay |
TERMINATING_GAME | Hidden | Fading out | Visible or hidden | → Overlay |
Surface Policy
Shell surface
- Always created at startup
- Fullscreen, z-order: bottom
- Visible in
SHELL_FOREGROUNDandGAME_STARTING - Background-hidden (not unmapped) in
GAME_FOREGROUND— the surface remains alive
Game surface
- Created when a game Wayland client maps a surface
- Only accepted if the client's
PLAYOS_LAUNCH_TOKENmatchesexpected_launch_token - Becomes foreground only after the first committed, non-null
wl_buffer(first-frame rule) - On game exit/crash: surface is destroyed or ignored; compositor transitions to
SHELL_FOREGROUND
Overlay surface
- Pre-spawned at startup, hidden
- Mapped above the game surface in
PLAYOS_UI_...state - z-order: above everything
- Receives a semi-transparent dimming layer below it (rendered by compositor)
Scene z-order (bottom to top)
1. active game surface (or hidden when shell is foreground)
2. optional dimming layer (compositor-rendered, semi-transparent black)
3. overlay surface (trusted Wayland client)
4. notifications / cursor (future)
Input Policy
Reserved keys
PLAYOS_BUTTON_SYSTEM is mapped to a specific evdev key code (ROG Ally: Armory Crate button). The compositor intercepts this at the seat level via libinput; it is never forwarded to any Wayland client.
// In handle_key event:
if (key_code == PLAYOS_SYSTEM_KEY_CODE) {
handle_system_action();
return; // do NOT pass to wlr_seat_keyboard_notify_key()
}
Input routing
libinput event
│
▼ compositor seat handler
├── reserved key (SYSTEM, QUICK_MENU) → handle_system_action()
├── state == PLAYOS_UI_... → send to overlay client
├── state == GAME_FOREGROUND → send to game client
└── otherwise → send to shell client
Direct Scanout
When the game's fullscreen surface buffer is compatible with the DRM output plane:
- Check buffer format, modifier, and size against the plane's supported formats
- If compatible: assign buffer directly to DRM plane (skip GPU composition)
- If overlay is visible or compatibility check fails: fall back to GPU composition
Scanout is an optimization, not an MVP correctness requirement. All transitions log whether scanout or composition was used.
Output Management
- Enumerate all outputs on DRM device initialization
- Select the primary output (built-in display) by connector type (eDP or DSI preferred)
- Set preferred mode (native resolution and refresh rate)
- Handle
wlr_output.events.destroyfor hotplug removal — log and continue - External display support is post-MVP
Private Protocol Implementation (server side)
See wayland-protocol.md for the full XML.
The compositor implements the server side of:
playos_manager_v1— global, binds trusted client rolesplayos_shell_v1— emits lifecycle events and game state to the shellplayos_overlay_v1— notifies overlay of show/hide; receives dismiss requests
Crash Recovery Invariants
A game crash must never:
- Leave the display black for more than 500ms
- Reveal a Linux terminal or TTY
- Require a compositor restart
When the game Wayland client disconnects unexpectedly:
- Compositor destroys the stale game surface
- Transitions immediately to
SHELL_FOREGROUND - Shell surface is made visible and focused
GameExited(crashed=true)is relayed to trusted clients
Build
# playos-compositor/CMakeLists.txt
find_package(wlroots REQUIRED)
find_package(wayland-server REQUIRED)
find_package(libdrm REQUIRED)
target_sources(playos-compositor PRIVATE
src/main.c
src/compositor.c
src/output.c
src/input.c
src/seat.c
src/scene.c
src/protocols/playos-v1.c # generated by wayland-scanner
)
target_link_libraries(playos-compositor wlroots::wlroots wayland-server drm)
playos-shell Specification
Repository:
playos-shell
Role: Persistent controller-first console UI; trusted Wayland client
Language: C (Raylib)
Cross-references: architecture.md §7.3, platform-api.md, sprints/Sprint-5.md, sprints/Sprint-6.md
Responsibilities
| Owns | Does NOT own |
|---|---|
| Persistent console UI | Process supervision |
| Controller-first navigation | DRM/KMS or Wayland protocol |
| Game discovery and metadata presentation | IPC protocol definitions |
| User-facing launch, resume, quit, crash flows | Game installation |
| Settings and status screens | Save data management |
| Launch requests via restricted control IPC | Hardware driver details |
| Rendering with Raylib PlayOS backend | |
| Preserving UI state while a game runs |
Screen Architecture
Library Screen (default)
│ A button → select game
▼
Game Detail Screen
│ A button → launch
│ B button → back
▼
Launching Screen (spinner)
│ game becomes foreground → shell backgrounds
│
[game running — shell alive but rendering stopped]
│
[PLAYOS_LIFECYCLE_FOREGROUND → shell returns]
▼
Library Screen (restored position)
│ (optionally shows post-crash notification)
Additional screens:
- Settings Screen — display brightness (live control via platform-api backlight), audio, system info, update check
- System Update Screen — current version, download/apply progress
Rendering Lifecycle
| Shell state | Rendering behavior |
|---|---|
| Foreground | Full render at display refresh rate |
| Game launching | Show spinner; reduce to 10 FPS |
| Game is foreground | Stop rendering — SetTargetFPS(0) or skip draw call |
| Game backgrounded (overlay visible) | Shell stays stopped; overlay renders |
| Returning to foreground | Resume rendering; restore previous screen and cursor position |
The shell surface remains alive (mapped as a Wayland surface) at all times. Only rendering is stopped, not the process.
Controller Navigation Rules
| Input | Action |
|---|---|
| D-pad Up/Down | Move focus up/down in a list or grid |
| D-pad Left/Right | Move focus left/right in a grid |
| A button | Confirm / select focused item |
| B button | Back / cancel |
| Start | Open settings screen |
| Select | Toggle sort/filter (library screen) |
| L1 / R1 | Page left/right (future) |
| System button | Never reaches shell — intercepted by compositor |
Mouse and keyboard are not required for normal shell use. They may be used for development convenience.
Game Discovery
The shell scans playos_storage_get_games_root() on startup and on explicit refresh:
// Pseudo-code
const char *games_root = playos_storage_get_games_root();
DIR *dir = opendir(games_root);
while ((entry = readdir(dir))) {
char manifest_path[PATH_MAX];
snprintf(manifest_path, sizeof(manifest_path),
"%s/%s/manifest.json", games_root, entry->d_name);
GameManifest manifest;
if (parse_manifest(manifest_path, &manifest) == 0) {
game_list_add(&games, &manifest);
} else {
PLAYOS_LOG_W("shell", "Skipping invalid manifest: %s", manifest_path);
}
}
sort_games_by_name(&games);
Game icons are loaded as Raylib Texture2D from <install_path>/assets/icon.png. A placeholder texture is used when no icon is present.
Launch Flow (Shell Side)
User selects "Launch" on game detail screen
│
├── Shell sends LaunchGame via control IPC (playos-runtime client)
│
├── Shell transitions to "Launching" screen (spinner + game name)
│
├── Receives GameStarted event (async) — game PID is known
│
└── Receives PLAYOS_LIFECYCLE_BACKGROUND
│
└── Shell stops rendering; game is now foreground
Error handling:
LaunchGameError(already_running)— show "A game is already running" notificationLaunchGameError(invalid_manifest)— show "This game cannot be launched" notification- Timeout (no
GameStartedwithin 10s) — show "Launch failed, please try again"
Post-Game Return Flow
When the game exits or crashes, the compositor delivers PLAYOS_LIFECYCLE_FOREGROUND to the shell:
Shell receives PLAYOS_LIFECYCLE_FOREGROUND
│
├── Resume rendering
├── Restore previous library position and selection
│
└── If crashed == true:
Show notification overlay: "Game exited unexpectedly"
Options: "Restart" | "Back to Library"
Status Bar
Persistent footer visible on all shell screens:
| Element | Source |
|---|---|
| Battery % + charging icon | playos_power_get_info() |
| Thermal indicator (color) | playos_power_get_info().thermal_state |
| System time | clock_gettime(CLOCK_REALTIME) |
| PlayOS version | playos_system_os_version() |
Updated every 30 seconds for battery/thermal; every 1 second for clock.
Trusted Client Identity
The shell sets PLAYOS_TRUSTED_SHELL=1 in its own environment before connecting to the Wayland display. The compositor verifies this at connection time and assigns the playos_shell_v1 role.
The shell uses the playos-runtime restricted control client library to:
- Connect to
/run/playos/control.sock - Send
LaunchGame,QueryStatus,Shutdown,Reboot,FactoryReset - Receive async events:
GameStarted,GameExited,ThermalStateChanged
Raylib PlayOS Backend (rcore_playos.c)
Raylib 6.0 is active (Sprint 5.5). It is vendored into
playos-shell/external/raylib (pinned by RAYLIB_COMMIT in
versions.lock) and built as a static library with the custom
PLATFORM_PLAYOS backend (external/raylib/src/platforms/rcore_playos.c).
The backend implements:
- Wayland connection and
wl_compositor/xdg_wm_base/playos_manager_v1globals - Fullscreen
xdg_toplevelsurface — no decorations, no resize wl_egl_window+ EGL/GLES2 context (eglBindAPI(EGL_OPENGL_ES_API)), made current before raylib'srlglinit- Frame callbacks for v-sync pacing +
eglSwapBuffersinSwapScreenBuffer()
Rendering is Raylib-only; Raylib is not the input path. Controller input
stays shell-owned direct evdev (src/input.c) so reserved SYSTEM/QUICK_MENU
buttons survive. PollInputEvents() in the backend only resets raylib's
internal input state. Lifecycle events are polled in main.c via
playos_lifecycle_poll() — suspend/background skips BeginDrawing/
EndDrawing, and TERMINATE exits cleanly (running = false, no bare
exit()).
Build
# playos-shell/CMakeLists.txt (PLAYOS_SHELL_USE_RAYLIB=ON)
add_subdirectory(external/raylib) # vendored Raylib 6.0, PLATFORM=PlayOS
find_library(PLAYOS_LIB playos ...) # libplayos from playos-platform-api
target_sources(playos-shell PRIVATE
src/main.c
src/input.c
src/screen_home.c
src/screen_library.c
src/screen_game_detail.c
src/screen_settings.c
src/render_util.c
)
target_link_libraries(playos-shell PRIVATE raylib ${PLAYOS_LIB} m)
playos-overlay Specification
Repository:
playos-overlay(or future multi-surface backend inplayos-shell)
Role: Trusted system overlay — quick menu, notifications, power management UI
Language: C (Raylib)
Cross-references: architecture.md §7.4, §8.3, wayland-protocol.md, sprints/Sprint-7.md
Responsibilities
| Owns | Does NOT own |
|---|---|
| Quick menu presentation | Game logic |
| Volume and brightness HUD | Shell game library |
| Power menu (shutdown, restart, sleep) | Process supervision |
| Notifications | DRM/KMS policy |
| Virtual keyboard (future) | IPC protocol definitions |
| Performance profile selector | Save management |
Why a Separate Process
Standard Raylib is most comfortable with one native EGL surface per process. The overlay and shell both require independently rendered fullscreen surfaces. A separate trusted process keeps the overlay isolated from shell state and allows it to appear above any surface — including a crashed or frozen game — without depending on the shell being responsive.
The overlay may later be merged into a multi-surface playos-shell backend if Raylib multi-surface support improves.
Overlay Lifecycle
playos-init spawns and supervises playos-overlay at boot
│
├── Overlay creates xdg_toplevel surface (transparent background)
├── Registers as trusted overlay via playos_manager_v1::register_overlay
├── Receives about_to_show / about_to_hide events from compositor
│
[System button pressed — compositor sends about_to_show]
│
├── Overlay renders quick menu frame
├── Sends surface_ready to compositor
├── Compositor maps overlay above game (z-order 3)
├── Input routed to overlay
│
[User presses Resume or game-related action]
│
├── Overlay sends request_dismiss to compositor
├── Compositor hides overlay, returns focus to game
└── Overlay receives about_to_hide; clears its surface
The overlay is always alive but only visible when the compositor maps it. Its rendering is stopped when hidden.
Screens
Quick Menu (default when overlay is shown)
┌────────────────────────────────────────────┐
│ [Game name] [battery] [time] │
├────────────────────────────────────────────┤
│ │
│ ▶ Resume Game │
│ Quit Game │
│ │
│ Volume: ████████░░ 75% │
│ Profile: [Balanced ▼] │
│ │
│ CPU: 72°C GPU: 68°C │
│ │
└────────────────────────────────────────────┘
Navigation: D-pad Up/Down to move focus, A to confirm, B to dismiss (→ Resume).
Power Menu (accessed from Quick Menu)
┌──────────────────────────────┐
│ Power Options │
├──────────────────────────────┤
│ Sleep (placeholder) │
│ Restart │
│ Shut Down │
│ ───── │
│ Factory Reset... │
└──────────────────────────────┘
Factory Reset shows a sub-screen with per-category checkboxes and a hold-A confirmation (matches installer confirmation UX).
Notification System
Notifications are short messages shown at the bottom of the screen (when not in quick menu mode).
Types:
INFO— blue badge, auto-dismiss after 3 secondsWARNING— yellow badge, auto-dismiss after 5 secondsERROR— red badge, requires A-button dismiss
Sources:
- Game crash recovery: "Game exited unexpectedly"
- Thermal state change: "Performance reduced — device is hot"
- Low battery: "Battery low: 15%"
- Update available: "System update ready"
Notifications are queued; at most one is shown at a time.
Volume Control
Volume is adjusted via the overlay:
- D-pad Left/Right on the volume slider: ±5% per step
- Calls
playos_audio_set_master_volume(new_volume) - Mute toggle: L1 button
- Visual: filled/empty bar + percentage text
Performance Profile Selector
Displays PLAYOS_PERF_BALANCED, PLAYOS_PERF_POWER_SAVE, PLAYOS_PERF_PERFORMANCE in a dropdown.
- D-pad Left/Right to cycle options
- A to confirm → calls
playos_power_request_profile() - If profile is denied (thermal override): shows "Performance limited — device is hot" notification
Trusted Client Identity
The overlay sets PLAYOS_TRUSTED_OVERLAY=1 in its environment. The compositor verifies this at connection time and assigns the playos_overlay_v1 role.
The overlay may also use the playos-runtime restricted client to:
- Send
TerminateGame(Quit action) - Send
Shutdown/Reboot(power menu) - Send
FactoryReset(with appropriate flags) - Send
SetPerfProfile
Scene Integration
The compositor renders a dimming layer between the game and the overlay surface:
z-order:
1. game surface (visible, not focused)
2. compositor-rendered dimming layer (rgba(0,0,0,0.4))
3. overlay surface (transparent background; UI elements are opaque)
The overlay's Wayland surface uses a transparent background (EGL_ALPHA_SIZE = 8, pre-multiplied alpha). Only drawn UI elements are opaque.
Build
# playos-overlay/CMakeLists.txt
find_package(playos-platform-api REQUIRED)
find_package(raylib REQUIRED)
target_sources(playos-overlay PRIVATE
src/main.c
src/screens/quick_menu.c
src/screens/power_menu.c
src/ui/notification.c
src/ui/volume_bar.c
src/ui/profile_selector.c
src/ui/factory_reset.c
)
target_link_libraries(playos-overlay playos raylib)
PlayOS Build Guide
Repository:
playos-refdistro
Cross-references: dev-environment.md, kernel-config.md, Sprint-0.md
Prerequisites
Host system
Any recent Linux host (Ubuntu 22.04 LTS or later recommended). The Buildroot cross-compiler handles everything else.
# Ubuntu / Debian
sudo apt-get install -y \
build-essential git wget curl unzip \
libncurses-dev libssl-dev libelf-dev bc \
python3 rsync cpio file \
qemu-system-x86 ovmf \
dosfstools mtools parted \
cmake meson ninja-build pkg-config \
libasound2-dev \
libexpat1-dev libffi-dev libxml2-dev \
libpciaccess-dev libudev-dev \
libxcb1-dev libxcb-composite0-dev libxcb-dri3-dev \
libxcb-ewmh-dev libxcb-icccm4-dev libxcb-present-dev \
libxcb-randr0-dev libxcb-render0-dev libxcb-render-util0-dev \
libxcb-res0-dev libxcb-shape0-dev libxcb-shm0-dev \
libxcb-sync-dev libxcb-xfixes0-dev libxcb-xinput-dev libxcb-xkb-dev \
libx11-dev libx11-xcb-dev \
libinput-dev libseat-dev libvulkan-dev \
libegl1-mesa-dev libgbm-dev libgles2-mesa-dev \
libdisplay-info-dev libliftoff-dev hwdata \
sbsign pesign # EFI signing (Sprint 12+)
# Native host deps (wlroots 0.20 needs Wayland >= 1.24, which 24.04 lacks):
# sudo bash playos-refdistro/scripts/build-host-deps.sh
# source /opt/playos-deps/env.sh
# Fedora / RHEL
sudo dnf install -y \
@development-tools git wget \
ncurses-devel openssl-devel elfutils-libelf-devel bc \
python3 rsync cpio file \
qemu edk2-ovmf \
dosfstools mtools parted \
cmake meson ninja-build pkg-config \
wayland-devel
make setup
The repository provides a make setup target that verifies host dependencies and installs any that are missing (using apt-get or dnf).
Repository Layout
playos-refdistro/
├── buildroot/ official Buildroot (pinned git submodule)
├── br2-external/ PlayOS-specific Buildroot content
│ ├── external.desc
│ ├── Config.in
│ ├── external.mk
│ ├── configs/
│ │ ├── playos_qemu_x86_64_defconfig
│ │ ├── playos_rog_ally_defconfig
│ │ └── playos_intel_pc_defconfig
│ ├── board/playos/
│ │ ├── common/ post-build and post-image scripts shared across targets
│ │ ├── qemu-x86_64/ QEMU-specific overlays and scripts
│ │ └── rog-ally/ ROG Ally-specific overlays, firmware, scripts
│ ├── package/
│ │ ├── playos-init/
│ │ ├── playos-platform-api/
│ │ ├── playos-runtime/
│ │ ├── playos-compositor/
│ │ ├── playos-shell/
│ │ └── playos-overlay/
│ └── patches/
│ ├── linux/ kernel patches (minimize; prefer upstream)
│ └── wlroots/ wlroots patches if needed
├── protocols/ copy of playos-runtime Wayland protocol XML
├── scripts/ helper scripts
├── docs/ → ../ documentation (this file is one of them)
├── .github/workflows/ CI definitions
├── Makefile developer command surface
└── versions.lock pinned commits for all components
Developer Commands
# Initial setup — installs host dependencies
make setup
# QEMU target
make qemu-config # configure Buildroot for QEMU x86_64
make qemu-build # full build (takes ~30–60 min on first run)
make qemu-run # launch QEMU/OVMF with built image
# ROG Ally target
make ally-config # configure Buildroot for ROG Ally
make ally-build # full build
make ally-usb-image # produce USB-bootable installer image
# Intel PC target
make intel-config
make intel-build
make intel-usb-image
# Installer image (for any target)
make installer-image TARGET=rog-ally
# Clean
make clean # remove build outputs
make distclean # remove everything including downloads cache
Behind each target:
qemu-config:
$(MAKE) -C buildroot O=$(BUILD_DIR)/qemu \
BR2_EXTERNAL=$(PWD)/br2-external \
playos_qemu_x86_64_defconfig
qemu-build:
$(MAKE) -C buildroot O=$(BUILD_DIR)/qemu
qemu-run:
scripts/run-qemu.sh $(BUILD_DIR)/qemu
Developers should not need to memorize raw Buildroot command lines.
versions.lock
All external dependencies are pinned to full Git commit SHAs. Never use floating branch names.
# versions.lock
BUILDROOT_COMMIT=abc123...
LINUX_VERSION=6.6.30
LINUX_SOURCE=https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.6.30.tar.xz
LINUX_SHA256=...
PLAYOS_INIT_COMMIT=abc999...
PLAYOS_PLATFORM_API_COMMIT=def456...
PLAYOS_RUNTIME_COMMIT=ghi789...
PLAYOS_COMPOSITOR_COMMIT=jkl012...
PLAYOS_SHELL_COMMIT=mno345...
WLROOTS_COMMIT=pqr678...
RAYLIB_COMMIT=stu901...
MESA_VERSION=24.1.0
Update versions.lock via:
scripts/update-versions.sh --component playos-compositor --commit abc123
Buildroot Package Structure
Each PlayOS component has a Buildroot package under br2-external/package/:
package/playos-compositor/
├── Config.in # Kconfig entry: BR2_PACKAGE_PLAYOS_COMPOSITOR
└── playos-compositor.mk
# playos-compositor.mk
PLAYOS_COMPOSITOR_VERSION = $(call read-file,$(BR2_EXTERNAL_PLAYOS_PATH)/../../versions.lock,PLAYOS_COMPOSITOR_COMMIT)
PLAYOS_COMPOSITOR_SITE = https://github.com/PlayOS-Foundation/playos-compositor
PLAYOS_COMPOSITOR_SITE_METHOD = git
PLAYOS_COMPOSITOR_DEPENDENCIES = wlroots libdrm wayland wayland-protocols libxkbcommon
PLAYOS_COMPOSITOR_INSTALL_TARGET = YES
define PLAYOS_COMPOSITOR_BUILD_CMDS
$(TARGET_MAKE_ENV) cmake -S $(@D) -B $(@D)/build \
-DCMAKE_TOOLCHAIN_FILE=$(HOST_DIR)/share/buildroot/toolchainfile.cmake \
-DCMAKE_BUILD_TYPE=Release
$(TARGET_MAKE_ENV) cmake --build $(@D)/build
endef
define PLAYOS_COMPOSITOR_INSTALL_TARGET_CMDS
$(INSTALL) -D -m 0755 $(@D)/build/playos-compositor $(TARGET_DIR)/usr/bin/playos-compositor
endef
$(eval $(generic-package))
Image Variants
Development image
- Includes: BusyBox, debug tools (
strace,gdbserver,evtest,modetest) - Serial console enabled
- Extra debug symbols
- Triggered by:
make qemu-configormake ally-config(default)
Production image
- No interactive shell
- No debug tools
- Signed EFI artifact
- Bounded logs
- Triggered by:
make ally-config PLAYOS_PROD=1or via the release pipeline
Installer image
- Contains:
playos-installerRaylib UI + disk partitioning tools (fdisk,mkfs.ext4,mkfs.fat) - EFI artifact wraps installer init, not the normal
playos-init - Triggered by:
make installer-image TARGET=rog-ally
Post-Build Artifacts
After make qemu-build:
build/qemu/
├── images/
│ ├── bzImage Linux kernel
│ ├── initramfs.cpio.zst Minimal pivot initramfs (mounts squashfs active slot)
│ ├── rootfs.squashfs Read-only squashfs system root
│ └── playos-esp.img UEFI-bootable ESP image
└── staging/ Sysroot for cross-development
After make ally-build:
build/rog-ally/
└── images/
├── playos-rog-ally-<version>-dev.img Dev image
├── playos-rog-ally-<version>-prod.img Production image
└── playos-rog-ally-<version>-installer.img
Build Time Estimates
| Target | Machine | First build | Incremental |
|---|---|---|---|
| QEMU | 8-core workstation | ~45 min | ~2 min |
| ROG Ally | 8-core workstation | ~60 min | ~5 min |
| QEMU | 4-core laptop | ~90 min | ~5 min |
Buildroot caches downloads in ~/.buildroot-dl/ (or BR2_DL_DIR). Sharing this directory across builds saves significant time.
Forking Buildroot
Do not fork Buildroot unless a required change cannot be expressed as:
- An external package (
br2-external/package/) - A board configuration
- A kernel or package patch (
br2-external/patches/) - A rootfs overlay
- A post-build or post-image script
If a fork is truly required, document the reason as an ADR.
PlayOS Kernel Configuration Guide
Repository:
playos-refdistro/br2-external/configs/
Cross-references: build-guide.md, Sprint-3.md, architecture.md §18
Philosophy
Start from a working ROG Ally configuration and remove features gradually.
Do not begin from an aggressively minimal embedded configuration — you will spend weeks re-enabling drivers you removed.
Only remove a subsystem after confirming it is absent from the supported hardware list and no test regression occurs.
Kernel Branch Policy
| Track | Purpose |
|---|---|
playos-kernel-lts | Release and qualification — upstream Linux LTS |
playos-kernel-next | Hardware evaluation — newer stable branch |
The LTS branch is the shipping kernel. The next branch is used to evaluate new hardware enablement (new Ally revisions, Intel support) before promotion to LTS.
Required Subsystems
Core x86_64 and Boot
CONFIG_X86_64=y
CONFIG_EFI=y
CONFIG_EFI_STUB=y # Kernel acts as its own EFI loader
CONFIG_BLK_DEV_INITRD=y # Embedded initramfs
CONFIG_INITRAMFS_SOURCE="" # Buildroot fills this in
CONFIG_ACPI=y
CONFIG_ACPI_SLEEP=y
CONFIG_PCI=y
CONFIG_PCI_MSI=y
CONFIG_PCIEPORTBUS=y
CONFIG_AMD_IOMMU=y # ROG Ally uses AMD IOMMU
CONFIG_INTEL_IOMMU=y # Intel target
Virtual Filesystems (required for PID 1)
CONFIG_DEVTMPFS=y
CONFIG_DEVTMPFS_MOUNT=y
CONFIG_PROC_FS=y
CONFIG_SYSFS=y
CONFIG_TMPFS=y
CONFIG_TMPFS_POSIX_ACL=y
CONFIG_CGROUPS=y # optional but useful for future process isolation
Storage
CONFIG_BLK_DEV_NVME=y # ROG Ally internal SSD
CONFIG_EFI_PARTITION=y
CONFIG_MSDOS_PARTITION=y # for USB drives
CONFIG_VFAT_FS=y # ESP (FAT32)
CONFIG_EXT4_FS=y # Data partition
CONFIG_EXT4_USE_FOR_EXT2=y
CONFIG_SQUASHFS=y # Read-only system root (A/B squashfs slots)
# CONFIG_XFS_FS is not needed
# CONFIG_BTRFS_FS is not needed
Graphics — AMD (ROG Ally)
CONFIG_DRM=y
CONFIG_DRM_KMS_HELPER=y
CONFIG_DRM_AMDGPU=y
CONFIG_DRM_AMD_DC=y # AMD Display Core — required for DisplayPort/eDP
CONFIG_DRM_AMD_DC_DCN=y # DCN (Display Core Next) for RDNA GPUs
CONFIG_DRM_SIMPLEDRM=y # Firmware framebuffer fallback (recovery mode)
CONFIG_FRAMEBUFFER_CONSOLE=n # No VT framebuffer console in production
CONFIG_DRM_FBDEV_EMULATION=n
Graphics — Intel (Sprint 13)
# For Gen 9–12 (Ice Lake, Tiger Lake, Alder Lake):
CONFIG_DRM_I915=y
# For Gen 12.5+ (Meteor Lake, Lunar Lake) — choose one:
# CONFIG_DRM_XE=y
USB and Input
CONFIG_USB=y
CONFIG_USB_XHCI_HCD=y # USB 3.x host controller
CONFIG_USB_HID=y
CONFIG_HID=y
CONFIG_HID_GENERIC=y
CONFIG_HID_ASUS=y # ROG Ally vendor-specific HID quirks
CONFIG_INPUT=y
CONFIG_INPUT_EVDEV=y # evdev interface for playos-platform-api
CONFIG_INPUT_JOYSTICK=y
CONFIG_JOYSTICK_XPAD=n # We use HID not xpad for the Ally
Audio — ROG Ally (AMD ACP / HDA)
CONFIG_SOUND=y
CONFIG_SND=y
CONFIG_SND_HDA_INTEL=y
CONFIG_SND_HDA_CODEC_REALTEK=y # ROG Ally uses Realtek codec
CONFIG_SND_SOC=y
CONFIG_SND_SOC_AMD_ACP=y # AMD Audio Co-Processor
CONFIG_SND_SOC_AMD_MACH=y # or specific machine driver
# CONFIG_SND_USB_AUDIO=y # Add for USB audio adapter support
Networking (development / Wi-Fi post-MVP)
# Minimal for first sprints
CONFIG_NET=y
CONFIG_UNIX=y # Required for Wayland sockets
# Sprint 11.6 — wired developer SSH (USB-C Ethernet + QEMU test NIC).
# No wireless in this sprint:
CONFIG_NETDEVICES=y
CONFIG_ETHERNET=y
CONFIG_MII=y
CONFIG_USB_NET_DRIVERS=y
CONFIG_USB_USBNET=y
CONFIG_USB_NET_AX8817X=y
CONFIG_USB_NET_AX88179_178A=y
CONFIG_USB_RTL8152=y
CONFIG_USB_NET_CDCETHER=y
CONFIG_USB_NET_CDC_NCM=y
CONFIG_USB_NET_CDC_EEM=y
CONFIG_USB_NET_RNDIS_HOST=y
CONFIG_USB_NET_SMSC95XX=y
CONFIG_USB_NET_MCS7830=y
CONFIG_VIRTIO_NET=y
CONFIG_E1000=y
CONFIG_E1000E=y
# Wi-Fi deferred to post-MVP / Sprint 16:
# CONFIG_CFG80211=y
# CONFIG_MAC80211=y
# CONFIG_IWLWIFI=y # Intel Wi-Fi
# CONFIG_ATH11K=y / CONFIG_MT7921E=y # AMD/Mediatek (Ally has AMD Wi-Fi)
Power Management
CONFIG_ACPI_BATTERY=y
CONFIG_POWER_SUPPLY=y
CONFIG_THERMAL=y
CONFIG_THERMAL_HWMON=y
CONFIG_X86_THERMAL_VECTOR=y
CONFIG_X86_AMD_PSTATE=y # AMD P-state driver (ROG Ally)
CONFIG_X86_AMD_PSTATE_UT=n # Skip unit test driver
CONFIG_X86_INTEL_PSTATE=y # Intel P-state (Sprint 13)
CONFIG_CPU_FREQ=y
CONFIG_CPU_FREQ_GOV_PERFORMANCE=y
CONFIG_CPU_FREQ_GOV_POWERSAVE=y
CONFIG_SUSPEND=y # System suspend (post-MVP full support)
CONFIG_PM_SLEEP=y
CONFIG_HIBERNATION=n # Not needed for MVP
Watchdog
CONFIG_WATCHDOG=y
CONFIG_WATCHDOG_NOWAYOUT=y
CONFIG_SP5100_TCO=y # AMD SB800 / FCH watchdog (ROG Ally)
Security (Sprint 12)
CONFIG_SECCOMP=y
CONFIG_SECCOMP_FILTER=y
CONFIG_SECURITY=y
CONFIG_SECURITY_LANDLOCK=y # Landlock LSM
CONFIG_LSM="landlock,lockdown,yama"
CONFIG_DM_VERITY=y # dm-verity for system image integrity
CONFIG_DM_VERITY_VERIFY_ROOTHASH_SIG=n # Signature check optional initially
CONFIG_MODULE_SIG=y # Module signing
CONFIG_MODULE_SIG_ALL=y
EFI Variables (needed for A/B boot counting)
CONFIG_EFI_VARS=y
CONFIG_EFIVAR_FS=y
What to Remove
Only after verifying the feature is not used by any supported hardware:
# Server filesystems
CONFIG_XFS_FS=n
CONFIG_BTRFS_FS=n
CONFIG_NFS_FS=n
CONFIG_NFSD=n
# Unused network protocols
CONFIG_IPV6=n # unless needed for Wi-Fi
CONFIG_NETFILTER=n # no iptables
# Unused GPU drivers
CONFIG_DRM_RADEON=n # Legacy AMD — not RDNA
CONFIG_DRM_NOUVEAU=n # NVIDIA
CONFIG_DRM_VMWGFX=n # VMware
# Legacy buses
CONFIG_ISA=n
CONFIG_PARPORT=n
CONFIG_PCMCIA=n
# Virtualization host features
CONFIG_KVM=n
CONFIG_XEN=n
# Debug features (production builds)
CONFIG_DEBUG_KERNEL=n
CONFIG_KGDB=n
CONFIG_SLUB_DEBUG=n
CONFIG_KALLSYMS=n # Remove after bring-up is stable
Config File Locations
br2-external/configs/playos_qemu_x86_64_defconfig QEMU target
br2-external/configs/playos_rog_ally_defconfig ROG Ally (primary)
br2-external/configs/playos_intel_pc_defconfig Intel (Sprint 13)
Each defconfig sets BR2_LINUX_KERNEL_CUSTOM_CONFIG_FILE to point to a kernel config fragment in board/playos/<target>/linux.config.
Kernel Config Workflow
# Edit kernel config interactively
make -C buildroot O=build/rog-ally linux-menuconfig
# Save changes back to the fragment
make -C buildroot O=build/rog-ally linux-update-defconfig
# Check for missing dependencies
make -C buildroot O=build/rog-ally linux-check-package
Firmware
ROG Ally requires AMD GPU firmware. Firmware blobs are non-redistributable and must be sourced from:
- An existing ROG Ally Linux installation:
/lib/firmware/amdgpu/ - The linux-firmware repository (check licensing)
In Buildroot, firmware is placed in:
board/playos/rog-ally/rootfs-overlay/lib/firmware/amdgpu/
Required blobs (exact filenames vary by GPU revision — check dmesg for requests):
amdgpu/gc_11_0_4_pfp.bin
amdgpu/gc_11_0_4_me.bin
amdgpu/gc_11_0_4_ce.bin
amdgpu/gc_11_0_4_rlc.bin
amdgpu/gc_11_0_4_mec.bin
amdgpu/dcn_3_1_4_dmcub.bin
... (and others — check dmesg on first boot)
CPU microcode:
board/playos/rog-ally/rootfs-overlay/lib/firmware/amd-ucode/microcode_amd_fam19h.bin
PlayOS Developer Environment Guide
Cross-references: build-guide.md, testing.md, Sprint-0.md
Development Workflow Overview
┌─────────────────────────────────────────────────────────┐
│ Fast iteration path (QEMU) │
│ │
│ Edit code → make qemu-build → make qemu-run → verify │
│ (~2 min for incremental builds) │
└─────────────────────────────────────────────────────────┘
│ Physical validation
▼
┌─────────────────────────────────────────────────────────┐
│ Hardware validation (ROG Ally) │
│ │
│ make ally-build → flash USB → boot on Ally → verify │
│ (Required for: GPU, input, audio, ACPI, thermal) │
└─────────────────────────────────────────────────────────┘
Rule: QEMU covers boot logic, process lifecycle, IPC, and storage. Physical hardware is required for AMDGPU, controller input, audio, power management, and display behavior.
QEMU Setup
OVMF (UEFI firmware for QEMU)
# Ubuntu
sudo apt-get install -y ovmf
# Fedora
sudo dnf install -y edk2-ovmf
# Verify
ls /usr/share/OVMF/OVMF_CODE.fd # or /usr/share/edk2/x64/OVMF.fd on Fedora
make qemu-run (what it does)
#!/bin/bash
# scripts/run-qemu.sh
BUILD=$1
qemu-system-x86_64 \
-machine q35 \
-cpu host \
-enable-kvm \
-m 4G \
-smp 4 \
-bios /usr/share/OVMF/OVMF_CODE.fd \
-drive if=pflash,format=raw,readonly=on,file=/usr/share/OVMF/OVMF_VARS.fd \
-drive file=${BUILD}/images/playos-esp.img,format=raw,if=none,id=esp \
-device nvme,drive=esp,serial=playos-esp \
-drive file=${BUILD}/images/system.img,format=raw,if=none,id=system \
-device nvme,drive=system,serial=playos-a \
-drive file=${BUILD}/images/data.img,format=raw,if=none,id=data \
-device nvme,drive=data,serial=playos-data \
-device virtio-gpu \
-display sdl,gl=on \
-audiodev pa,id=audio0 \
-device intel-hda \
-device hda-duplex,audiodev=audio0 \
-serial stdio \
-device virtio-net-pci,netdev=net0 \
-netdev user,id=net0 \
-no-reboot
Important: Always boot via OVMF (not -kernel). The UEFI boot path must be validated, not bypassed.
QEMU virtual system slot
The active system slot is the read-only squashfs root produced by make qemu-build
(build/qemu/images/rootfs.squashfs). Wrap it as a raw image so playos-init can
mount it read-only and pivot into it:
# Create the system-slot image
make qemu-create-system-disk # or:
dd if=build/qemu/images/rootfs.squashfs of=build/qemu/images/system.img bs=1M
playos-init mounts this read-only as the active slot (playos-a) and pivots into it
(Sprint 11.5).
QEMU virtual data disk
Create a virtual data disk for the /data partition:
# Create 8 GB virtual disk
make qemu-create-data-disk # or:
qemu-img create -f raw build/qemu/images/data.img 8G
This disk is formatted by playos-init on first boot (provisioning mode with playos.mode=provision on the kernel cmdline for testing).
Headless QEMU (CI)
qemu-system-x86_64 \
-machine q35 -cpu qemu64 -m 2G \
-bios /usr/share/OVMF/OVMF_CODE.fd \
... \
-display none \
-serial file:serial.log \
-no-reboot \
-device isa-debug-exit,iobase=0xf4,iosize=0x04
The test harness sends a debug exit command via the QEMU monitor when the boot check passes.
Nested Wayland Compositor (Developer Desktop)
For compositor development on a developer workstation (without QEMU overhead):
# Set backend to nested Wayland
export PLAYOS_BACKEND=nested
export WAYLAND_DISPLAY=wayland-0 # your host compositor
./build/playos-compositor &
# In another terminal:
export WAYLAND_DISPLAY=playos-0
./build/playos-shell
The compositor runs nested inside your desktop session. No root or DRM access needed.
For headless compositor testing on CI:
export PLAYOS_BACKEND=headless
./build/playos-compositor
Cross-Compilation
Buildroot produces a cross-toolchain at build/rog-ally/host/. You can use it directly to build and test individual components:
export PATH="$PWD/build/rog-ally/host/bin:$PATH"
export PKG_CONFIG_PATH="$PWD/build/rog-ally/staging/usr/lib/pkgconfig"
export PKG_CONFIG_LIBDIR="$PWD/build/rog-ally/staging/usr/lib/pkgconfig"
export CC=x86_64-buildroot-linux-musl-gcc
export CXX=x86_64-buildroot-linux-musl-g++
# Build a component against the staging sysroot
cmake -S playos-compositor -B build-cross \
-DCMAKE_TOOLCHAIN_FILE=$PWD/build/rog-ally/host/share/buildroot/toolchainfile.cmake \
-DCMAKE_BUILD_TYPE=Debug
cmake --build build-cross
USB Boot Workflow (ROG Ally)
# Build the USB image
make ally-build
make ally-usb-image
# Flash to USB (replace /dev/sdX with your USB device)
sudo dd if=build/rog-ally/images/playos-rog-ally-dev.img of=/dev/sdX bs=4M status=progress oflag=sync
# Boot the Ally from USB:
# 1. Hold Volume Down while pressing Power
# 2. Select USB device in UEFI boot menu
# 3. PlayOS boots from USB
Serial Console
Serial output is the primary debug log during bring-up. The ROG Ally has no physical serial port, but there are two options:
Option 1: USB serial adapter (hardware)
Connect a USB-to-serial adapter to the ROG Ally's USB-C port using a custom cable or dock with serial passthrough. Use minicom or picocom on the host.
Option 2: kernel earlycon + netconsole (software)
For early boot output, configure the kernel command line:
console=ttyS0,115200 earlycon=serial8250,io,0x3f8,115200
For post-boot network logging (requires networking):
netconsole=@/,@<host-ip>/
QEMU serial
QEMU serial output goes to stdout (-serial stdio). For CI, redirect to a file (-serial file:serial.log).
Debugging Tools (Development Image)
Available in the development image (not production):
| Tool | Purpose |
|---|---|
evtest | Test input devices — verify controller button mapping |
modetest | Test DRM/KMS — verify connectors, encoders, CRTCs |
strace | Trace system calls of any process |
gdbserver | Remote GDB debugging (requires networking) |
perf | CPU/GPU performance profiling |
aplay / arecord | ALSA audio testing |
weston-info | Inspect Wayland compositor info |
| BusyBox shell | Available at /bin/sh — not present in production |
Access the development shell:
- QEMU: serial console (available from boot)
- ROG Ally: cannot access in normal mode — reboot with
playos.shell=1kernel param (development only)
Iterative Development Loop
Modifying playos-compositor:
# 1. Edit source in playos-compositor/
vim playos-compositor/src/compositor.c
# 2. Rebuild just that package in Buildroot
make -C buildroot O=build/qemu playos-compositor-rebuild
# 3. Re-run QEMU
make qemu-run
Modifying playos-shell:
make -C buildroot O=build/qemu playos-shell-rebuild
make qemu-run
Modifying the kernel:
make -C buildroot O=build/qemu linux-rebuild
make qemu-run
# Kernel rebuilds are slower (~5 min)
Working on playos-platform-api (host native build):
# Build and test on the host for faster iteration
cmake -S playos-platform-api -B build-host -DPLAYOS_BACKEND=stub
cmake --build build-host
ctest --test-dir build-host
Environment Variables Reference
| Variable | Used by | Purpose |
|---|---|---|
PLAYOS_BACKEND | libplayos, compositor | Select backend: drm, headless, nested, stub |
WAYLAND_DISPLAY | All Wayland clients | Which Wayland socket to connect to |
PLAYOS_TRUSTED_SHELL | playos-shell | Marks client as trusted shell (=1) |
PLAYOS_TRUSTED_OVERLAY | playos-overlay | Marks client as trusted overlay (=1) |
PLAYOS_LAUNCH_TOKEN | Game process | One-time launch identity UUID |
PLAYOS_GAME_ID | Game process | Game identifier string |
PLAYOS_INSTALL_PATH | Game process | /data/games/<id> |
PLAYOS_SAVE_PATH | Game process | /data/saves/<id> |
PLAYOS_CACHE_PATH | Game process | /data/cache/<id> |
PLAYOS_LIFECYCLE_FD | Game process | Read end of lifecycle pipe |
PLAYOS_COMPOSITOR_READY_FD | playos-compositor | Write end — signal readiness to playos-init |
PLAYOS_AUDIO_DEVICE | libplayos | Override ALSA device name for testing |
PlayOS Testing Strategy
Cross-references: architecture.md §19, dev-environment.md, Sprint docs
CI Layers
Testing is organized in seven layers, each building on the previous:
| Layer | Where | What |
|---|---|---|
| 1 | Host | Unit tests — logic, IPC serialization, manifest parsing |
| 2 | Host | Buildroot clean build — compilation and packaging |
| 3 | QEMU/OVMF | Boot test — UEFI boot to shell prompt |
| 4 | QEMU | Compositor + shell smoke test |
| 5 | QEMU | Game lifecycle integration test |
| 6 | ROG Ally | Physical device smoke test |
| 7 | ROG Ally | Long-running stability and update test |
Layers 1–5 run on every PR and push to main.
Layers 6–7 run on release candidates and scheduled nightly builds.
Layer 1 — Host Unit Tests
Each repository has a native host build with unit tests:
# Build and test on the host (no cross-compilation needed)
cmake -S playos-platform-api -B build-host -DPLAYOS_BACKEND=stub
cmake --build build-host
ctest --test-dir build-host --output-on-failure
What to unit test:
| Component | Tests |
|---|---|
playos-runtime | IPC message serialization/deserialization; version mismatch handling; all message types round-trip |
playos-platform-api | Input state bitmask helpers; manifest parser; storage path construction; lifecycle fd read |
playos-init | Boot sequence logic; game launch validation; supervisor restart counter; shutdown sequence |
playos-compositor | State machine transitions (all valid and invalid transitions); GPU selection logic |
playos-shell | Manifest discovery and sorting; controller navigation logic; status bar data formatting |
CI command:
- name: Unit tests
run: |
make setup
make host-test # runs ctest for all repos
Layer 2 — Buildroot Clean Build
Verifies that the entire system compiles cleanly from source.
- name: QEMU clean build
run: |
make qemu-config
make qemu-build
Expected: no compiler errors or warnings (warnings-as-errors enabled in CI).
For PRs touching only a single component, the CI may optionally run a partial rebuild (make -C buildroot O=build/qemu playos-compositor-rebuild) for speed, but a full clean build runs on merge to main.
Layer 3 — QEMU/OVMF Boot Test
Boots the built image in QEMU and verifies the system reaches a known-good state.
# scripts/ci-boot-test.sh
qemu-system-x86_64 \
-machine q35 -cpu qemu64 -m 2G \
-bios /usr/share/OVMF/OVMF_CODE.fd \
-drive file=build/qemu/images/playos-esp.img,... \
-drive file=build/qemu/images/data.img,... \
-display none -serial file:boot-serial.log \
-no-reboot &
# Wait for success marker in serial log (timeout: 120s)
timeout 120 grep -q "PLAYOS_BOOT_OK" boot-serial.log
playos-init writes PLAYOS_BOOT_OK to the serial console when:
- All virtual FSes are mounted
- Data partition is mounted
- Control IPC socket is ready
- Compositor readiness signal received
Checks:
- Boot completes within 30 seconds
playos-initis PID 1 (verified via/proc/1/commin serial log)- No kernel panic or OOM killer invocation
- All mounts successful
Layer 4 — Compositor + Shell Smoke Test
Runs the compositor in QEMU with a headless backend and verifies the Wayland session and shell start correctly.
# In the booted QEMU image (via serial or QEMU monitor):
# Verify Wayland socket exists
ls -la /run/playos/playos-0
# Verify compositor state via IPC
playos-ctl query-status
# Expected: compositor_state=SHELL_FOREGROUND
# Verify shell is alive
ps | grep playos-shell
Checks:
/run/playos/playos-0socket existsQueryStatusreturnscompositor_state=SHELL_FOREGROUNDplayos-shellprocess is running- No segfaults in compositor or shell logs
Layer 5 — Game Lifecycle Integration Test
Runs the complete launch-background-resume-exit lifecycle with a stub game.
# Stub game binary: exits with code 0 after receiving TERMINATE lifecycle event
# Installed at /data/games/com.playos.ci-stub/
playos-ctl launch com.playos.ci-stub
sleep 2 # wait for GAME_FOREGROUND state
playos-ctl query-status
# Expected: compositor_state=GAME_FOREGROUND, game_id=com.playos.ci-stub
# Simulate System button (compositor-controlled in real hardware; inject via IPC in test)
playos-ctl simulate-system-button
sleep 0.5
playos-ctl query-status
# Expected: compositor_state=PLAYOS_UI_FOREGROUND_WITH_GAME_BACKGROUND
playos-ctl send-resume
sleep 0.5
playos-ctl query-status
# Expected: compositor_state=GAME_FOREGROUND
playos-ctl terminate-game com.playos.ci-stub
sleep 1
playos-ctl query-status
# Expected: compositor_state=SHELL_FOREGROUND, game_pid=null
Crash recovery test:
playos-ctl launch com.playos.ci-stub
sleep 2
kill -9 $(playos-ctl get-game-pid)
sleep 1
playos-ctl query-status
# Expected: compositor_state=SHELL_FOREGROUND (crash recovery in ≤500ms)
Layer 6 — Physical ROG Ally Smoke Test
Run after every release candidate build. Document results in a test report.
Boot tests
- Cold boot from power-off to shell: ≤ 5 seconds
- Repeated boot (×5): consistent boot time, no regressions
- Boot after unclean shutdown (simulated): filesystem check passes
Display tests
- Shell renders at native resolution and refresh rate (1920×1080 @ 120Hz)
- No tearing or flickering visible during shell navigation
- Hardware-accelerated rendering confirmed (Mesa driver string in log)
Controller input tests
- All D-pad directions navigate the shell grid
- A button selects; B button goes back
-
Both analog sticks report correct axis values in
sample-input - Both triggers report correct values
- System button (ROG button / Armory Crate button) triggers overlay — not delivered to shell
-
evteston Ally controller shows expected event codes
Game lifecycle tests
-
sample-trianglelaunches and renders a colored triangle - System button shows overlay above game; game is paused or background-throttled
- Resume returns to running game (no restart)
- Quit from overlay returns to shell
-
kill -9on game PID: display returns to shell in ≤ 500ms - Second launch attempt while game runs: rejected with error
Audio tests
-
sample-audioplays sine tone through speakers - Plugging in headphones routes audio correctly
- Volume slider in overlay changes audible volume
- Game audio stops when overlay appears; resumes when dismissed
Storage tests
- File written to save path survives reboot
- Game list updates when new game directory is added
- Cache clear via factory reset removes cache; system remains bootable
Power tests
- Battery percentage shown and updates
- CPU and GPU temperatures shown in overlay
- Running CPU stress: thermal state changes to WARM; composite confirms in log
- Shutdown from overlay: clean poweroff
Layer 7 — Long-Running Stability Test
Run on release candidates and nightly builds.
| Test | Duration | Pass criterion |
|---|---|---|
| Continuous shell idle | 4 hours | No crash, memory growth < 10 MB |
| Rapid launch/exit cycles | 100 iterations | All succeed; no compositor restart |
| System button cycles | 200 iterations | All transitions correct; no input loss |
| A/B update apply + rollback | Full cycle | New slot boots; rollback recovers correctly |
Sustained GPU load (sample-triangle) | 30 minutes | No GPU hang, no thermal shutdown, stable FPS |
Test Utilities
playos-ctl — IPC test client
A development-only CLI tool (in playos-tools) that wraps the control IPC:
playos-ctl launch <game-id>
playos-ctl terminate <game-id> [--force]
playos-ctl query-status
playos-ctl get-game-pid
playos-ctl simulate-system-button # inject system action via compositor debug socket
playos-ctl send-resume
playos-ctl shutdown
playos-ctl reboot
Not present in production builds.
Useful third-party tools (development image)
| Tool | Purpose |
|---|---|
evtest | Verify input device events |
modetest | Verify DRM connectors and modes |
weston-info | Inspect Wayland compositor |
aplay -l | List ALSA devices |
speaker-test -t sine | Test audio output |
stress-ng | CPU/GPU stress for thermal testing |
perf stat | CPU performance metrics |
apitrace | Trace OpenGL calls (debugging) |
piglit | OpenGL conformance tests |
PlayOS Installation Guide
Version: 1.0 Applies to: Sprint 10 installer path (ROG Ally x86_64 target).
This guide covers building the one-shot installer USB image and using it to install PlayOS onto the ROG Ally internal NVMe SSD. The normal live-USB image (boot directly from USB without installing) is documented in the Build Guide.
1. Overview
The installer is a separate, one-shot system image:
- The installer kernel is the normal Ally kernel with
CONFIG_CMDLINE="... playos.mode=install". - On boot,
playos-initseesplayos.mode=installand spawns/usr/bin/playos-installerinstead ofplayos-shell. - The installer shows a Raylib confirmation UI, then writes the five-partition internal layout to the target NVMe and stages the production kernel and system image.
The target medium is a fixed disk (removable flag 0), so the boot USB is
automatically excluded from the disk-selection list.
2. Build the installer image
cd playos-refdistro
make installer-image
This builds both the installer output (output/installer/) and the production Ally
output (output/ally/), then assembles:
output/installer/images/playos-ally-installer.img
The production build must also produce a squashfs system image
(BR2_TARGET_ROOTFS_SQUASHFS=y), which becomes the installer payload
playos-a/rootfs.squashfs.
3. Flash the installer USB
make installer-flash
# or manually:
sudo dd if=output/installer/images/playos-ally-installer.img \
of=/dev/sdX bs=4M status=progress conv=fsync
Warning:
of=/dev/sdXmust be the USB device (whole disk), not a partition. All data on the USB is overwritten.
4. Install to the Ally
- Power off the Ally.
- Insert the installer USB.
- Boot the Ally and select the USB as the UEFI boot device.
- The installer discovers internal fixed disks and shows model/size/partition count.
- Use the D-pad to select the target NVMe and press A.
- Hold A on the confirmation screen until the countdown bar completes.
- Wait for installation to reach 100% and show the success screen.
- Remove the USB and reboot.
The Ally then boots from the internal ESP EFI/BOOT/BOOTX64.EFI.
5. What the installer writes
| Step | Action |
|---|---|
| 1 | Create GPT partition table on the target disk |
| 2 | Format partition 1 as FAT32 (ESP, 512 MiB) |
| 3 | Write the squashfs system image to playos-a (4 GiB) |
| 4 | Reserve playos-b (4 GiB) empty |
| 5 | Format misc (64 MiB, ext4) |
| 6 | Format playos-data (remainder, ext4) |
| 7 | Write EFI/BOOT/BOOTX64.EFI to the ESP |
| 8 | Sync all filesystems and best-effort efibootmgr NVRAM entry |
See Partition Layout for the full layout.
6. Boot model (Sprint 11.5)
The installed system boots via the EFI-stub kernel and its initramfs, which then mounts
the active slot's read-only squashfs image (playos-a by default) and pivots into it
(Sprint 11.5). In practice this means:
- Removing the USB and booting from the internal ESP works now.
/datafirst-boot provisioning runs from the initramfs as usual.- The active-slot squashfs is the booted read-only root; the inactive slot (
playos-b) is used for A/B updates and rollback.
7. Future work
- dm-verity integrity hardening (Sprint 12+).
- Automatic installer triggering when a removable boot medium has no
playos-dataon an internal disk (currently the trigger is the explicitplayos.mode=installcommand line only).
Related: Partition Layout, Build Guide, Sprint 10
PlayOS Partition Layout
Version: 1.0 Applies to: Sprint 10+ installer-deployed internal disks and Sprint 6+ live USB media.
This document is the authoritative reference for on-disk partitioning. The layout is
different for the removable live-USB image and the internal NVMe install target; the
installer (playos-installer) creates the internal layout, while
scripts/gen-ally-usb-image.sh and scripts/gen-installer-usb-image.sh create the USB
layouts.
1. Internal disk (installed system)
The installer writes a GPT table with five partitions to the target fixed disk (the non-USB NVMe/SATA drive):
| # | Label | Size | Filesystem | Purpose |
|---|---|---|---|---|
| 1 | ESP | 512 MiB | FAT32 | EFI System Partition; EFI/BOOT/BOOTX64.EFI |
| 2 | playos-a | 4 GiB | squashfs (read-only) | Active system slot |
| 3 | playos-b | 4 GiB | squashfs (read-only) | Inactive system slot (A/B updates, Sprint 11.5) |
| 4 | misc | 64 MiB | ext4 | A/B slot metadata (/data/misc-style state) |
| 5 | playos-data | remainder | ext4 | Writable user data |
GPT disk (internal NVMe/SATA)
├── Partition 1: ESP 512 MiB FAT32 label "ESP"
├── Partition 2: playos-a 4 GiB squashfs label "playos-a" (active slot)
├── Partition 3: playos-b 4 GiB squashfs label "playos-b" (inactive slot)
├── Partition 4: misc 64 MiB ext4 label "misc" (A/B metadata)
└── Partition 5: playos-data remainder ext4 label "playos-data" (writable)
Rules:
playos-aandplayos-bare immutable system slots.misccarries the tiny A/B slot state (which slot is booted / healthy); it is formatted ext4 for convenience.playos-dataholds all user content: games, saves, cache, log, updates, config.- The installer never silently formats a disk; the user must explicitly confirm the target with a hold-to-confirm gesture.
- Sprint 10 creates the slots; A/B update/rollback landed in Sprint 11/11.5; dm-verity remains planned (Sprint 12+).
2. Live USB (normal system image)
make ally-usb-image produces output/ally/images/playos-ally-usb.img with a compact
three-partition GPT layout:
| # | Label | Size | Filesystem | Purpose |
|---|---|---|---|---|
| 1 | ESP | 256 MiB | FAT32 | EFI System Partition; EFI/BOOT/BOOTX64.EFI |
| 2 | playos-a | 2048 MiB | ext2 | System image (EFI stub kernel lives on ESP) |
| 3 | playos-data | remainder | ext4 | Writable scratch / diagnostics |
This layout is intentionally simpler than the internal layout: the live image boots
entirely from its EFI-stub kernel with an embedded initramfs, and playos-a exists as
a labelled system container rather than as a booted root.
3. Installer USB
make installer-image produces output/installer/images/playos-ally-installer.img.
It reuses the three-partition live-USB geometry but carries a payload partition:
| # | Label | Size | Filesystem | Purpose |
|---|---|---|---|---|
| 1 | ESP | 256 MiB | FAT32 | Installer kernel (playos.mode=install) as EFI/BOOT/BOOTX64.EFI |
| 2 | playos-a | 2048 MiB | ext2 | Install payload: /rootfs.squashfs + /BOOTX64.EFI |
| 3 | playos-data | remainder | ext4 | Scratch / preserved diagnostics |
The installer kernel boots the one-shot installer UI. It mounts its own playos-a
read-only and streams rootfs.squashfs into the target playos-a slot, and copies
/BOOTX64.EFI (the normal production kernel) into the target ESP.
4. Data partition contents
The writable playos-data partition is provisioned on first boot to:
/data/
games/<game-id>/ manifest.json, bin/, assets/, shaders/, licenses/
saves/<game-id>/ profiles/, autosaves/, settings/
cache/<game-id>/ shaders/, compiled-assets/, temporary/
resources/
downloads/
log/
updates/
screenshots/
config/
profiles/
A .playos-storage-version marker is stamped at the root after first-boot
provisioning; its presence/absence drives first-boot seeding of shipped games.
Related: Installation Guide, System Architecture
PlayOS Roadmap
This document defines the MVP exit criteria and sprint delivery plan.
Sprint documents contain the executable work packages: acceptance criteria, key tasks, and test strategy.
MVP Definition
The first meaningful PlayOS MVP is complete when all of the following are true on physical ROG Ally hardware:
| # | Criterion |
|---|---|
| 1 | ROG Ally boots directly from UEFI into PlayOS |
| 2 | Linux kernel + initramfs are available as a UEFI-bootable EFI artifact |
| 3 | playos-init runs as PID 1 |
| 4 | playos-compositor permanently owns DRM/KMS and the Wayland session |
| 5 | playos-shell remains alive as the persistent controller-first UI |
| 6 | Compositor uses wlroots with AMDGPU, DRM/KMS, GBM, EGL, and Mesa |
| 7 | Shell renders through Wayland using the Raylib PlayOS backend |
| 8 | Shell and sample game consume the public playos-platform-api C ABI |
| 9 | Trusted launch, lifecycle transport, and compositor-control remain internal to playos-runtime |
| 10 | Shell requests game launch; playos-init spawns and supervises it |
| 11 | Compositor waits for game's first valid frame before switching foreground |
| 12 | Game renders with hardware acceleration and receives controller input |
| 13 | Reserved System button returns to PlayOS UI and backgrounds/pauses the game |
| 14 | Resume returns to the same running game without restarting it |
| 15 | Game outputs audio through ALSA |
| 16 | Clean exit and crash both return safely to the existing shell |
| 17 | Games and saves persist on a separate ext4 partition |
| 18 | System image is immutable |
| 19 | Recovery mode usable without accelerated graphics |
Sprint Plan
| Sprint | Title | Primary Outcome |
|---|---|---|
| 0 | Build and UEFI Foundation | Reproducible Buildroot factory boots a minimal PlayOS EFI image in QEMU/OVMF |
| 1 | playos-init and Minimal Boot Supervision | Real playos-init as PID 1 with versioned private control IPC skeleton |
| 2 | Compositor Skeleton and Wayland Session | Minimal wlroots compositor with a Wayland session and one trusted fullscreen client |
| 3 | ROG Ally Kernel and Device Bring-Up | Reliable USB boot, essential ROG Ally devices working, first Platform API input contract |
| 4 | AMDGPU and Native DRM/KMS | Compositor permanently owns the Ally display via AMDGPU and DRM/KMS |
| 5 | Raylib-Powered PlayOS Shell | Hardware-accelerated Raylib shell consuming the public PlayOS Platform API |
| 6 | Persistent Storage and Game Discovery | Persistent ext4 storage, Platform API paths, shell-visible game discovery |
| 7 | Game Launch, Lifecycle, System Button, and Overlay | Complete console lifecycle: launch, overlay, background, resume, crash recovery |
| 8 | ALSA Audio | Reliable ALSA audio with safe public controls across lifecycle transitions |
| 9 | Power, Battery, Thermal, and Suspend Foundations | Safe power behavior exposed through a restricted public Platform API |
| 10 | Installer and Internal-Disk Deployment | Tested installation path from removable media to ROG Ally internal SSD |
| 11 | Immutable Images and A/B Updates | Signed, atomic A/B system updates with automatic rollback |
| 11.6 | Developer SSH (Dropbear) + Minimal Wired Network Bring-Up | USB-C Ethernet SSH (key auth) for on-device debugging; full Wi-Fi stays Sprint 16 |
| 12 | Security Hardening | Hardened boundary between public Platform API, trusted runtime control, and games |
| 13 | Intel Expansion | Architecture and Platform API backend portable to Intel graphics |
| 14 | Production Readiness | Signed preview release with versioned public Platform API |
| 15 | Game Developer SDK | Self-contained playos-sdk (musl toolchain + libplayos/libraylib) with device/desktop/emulator testing |
| 16 | playos-net (Wi-Fi) | D-Bus-free Wi-Fi (wpa_supplicant + dhcpcd + playos-net bridge) driven through playos-runtime |
| 17 | Touch Input + On-Screen Keyboard (OSK) | Touch end-to-end (compositor → raylib backend) plus a reusable system OSK |
| 18 | C# Shell Reimplementation Assessment (Post-MVP Spike) | Feasibility assessment only — no C# shell implemented |
| 19 | Marketplace Assessment (Post-MVP) | Assessment and spec-first sequencing only — no marketplace code |
| 20 | Native Media & Browser Client Strategy (Post-MVP) | Assessment of native Spotify/YouTube/YouTube Music/browser clients — Netflix out of scope |
| 21 | Multiple Local User Profiles (Post-MVP) | Assessment/design of console-style local profiles with per-profile isolated saves/settings — no implementation |
| 22 | LVGL Shell UI Spike (Post-MVP) | Gated LVGL-via-raylib texture spike with controller navigation + go/no-go — no shell port |
Execution Rules
- Sprints follow dependency order unless an ADR explicitly changes the sequence.
- A sprint begins only after its required predecessor exit criteria are satisfied.
- Each sprint must end with a demonstrable and testable system outcome.
- Architecture changes discovered during implementation must be captured in
playos-spec. - Cross-sprint API changes require a version bump and compatibility review.
Post-MVP Roadmap
Add only after the core console lifecycle is stable:
playos-device— hardware and power policy service- Dropbear SSH — explicit Developer Mode only (minimal wired slice pulled forward in Sprint 11.6; full Wi-Fi remains Sprint 16)
playos-update— PlayOS wrapper around the update engine- OTA update delivery —
playos-toolshost helper (download + verify + stage to USB/SD) first, then on-device download after Sprint 16 (playos-net) playos-inputservice — remapping, virtual gamepads, gyro, haptics- Dedicated audio service — mixing, notifications over games
- Bluetooth
- Fast, fully qualified suspend/resume
- Rear-button and special-button support
- Screenshots and screen recording
- Vulkan (RADV)
- VRR and HDR
- External-display profiles
- Download manager and store integration
- Cloud saves and user accounts
- Multiple local profiles
- Signed
.playcontent packages - Delta updates
- Telemetry (explicit user consent only)
See architecture.md for the full system design.
Sprint 0 — Build and UEFI Foundation
Goal: Establish the repository layout, Buildroot integration, and the first reproducible QEMU/OVMF boot path for PlayOS.
Primary Outcome: A clean environment can build and boot a minimal EFI image through QEMU/OVMF and reach a BusyBox shell using the PlayOS reference distribution workflow.
Prerequisites: None — this is the first sprint.
Why This Sprint Exists
Sprint 0 is the factory bootstrap sprint. It creates the build environment, repo boundaries, version pinning rules, and boot artifact shape that every later sprint depends on.
Without this sprint being strict and reproducible, later work becomes impossible to compare, debug, or automate.
Start Condition Checklist
- A Linux build environment is available (native Linux, WSL2, or Linux VM).
- QEMU and OVMF are available or installable.
- The six PlayOS repositories either already exist or will be created as part of the sprint.
- The implementation agent is allowed to create build/config scaffolding across repositories.
Decisions Locked for This Sprint
- Distribution assembler repo:
playos-refdistro - Documentation repo:
playos-spec - C library/toolchain policy: musl only
- Boot validation path: UEFI through OVMF only; do not use the
-kernelshortcut as the main proof - Initial userspace: BusyBox-based initramfs for bootstrap only
- Kernel target for this sprint: QEMU x86_64, not ROG Ally hardware yet
- Target build host OS: Ubuntu Server (LTS) — all host setup scripts and CI assume Ubuntu
- Shell logging framework: a shared bash library (
scripts/lib/playos_log.sh) is created here and used by every script in the project from this sprint forward
Scope
In Scope
- repository bootstrap and baseline documentation files
- Buildroot integration strategy
br2-externalskeleton- QEMU x86_64 defconfig
- minimal kernel/initramfs boot path
- UEFI boot artifact generation
- version pinning conventions
- Ubuntu Server host environment setup script
- shared bash logging framework for all project scripts
- first CI shape for image build and boot proof
Explicitly Out of Scope
- real
playos-init - compositor or Wayland logic
- physical ROG Ally boot
- real shell UI
- storage, audio, power, or update systems
Required Repository Changes
| Repo | Required work |
|---|---|
playos-spec | initial project docs and ADRs that define repo boundaries |
playos-refdistro | Buildroot tree, defconfig, image generation, make targets, CI |
playos-platform-api | repo scaffold only |
playos-runtime | repo scaffold only |
playos-compositor | repo scaffold only |
playos-shell | repo scaffold only |
Expected Files and Directories
playos-refdistro
Makefile
versions.lock
buildroot/ # pinned Buildroot checkout or submodule
scripts/
├── lib/
│ └── playos_log.sh # shared bash logging framework (used by ALL scripts)
├── setup-ubuntu.sh # Ubuntu Server host environment bootstrap
└── qemu-boot-check.sh # CI boot assertion script
br2-external/
├── external.desc
├── Config.in
├── external.mk
├── configs/
│ └── playos_qemu_x86_64_defconfig
├── board/
│ ├── common/
│ └── qemu-x86_64/
└── package/
├── playos-init/
├── playos-platform-api/
├── playos-runtime/
├── playos-compositor/
└── playos-shell/
.github/workflows/
└── qemu-build.yml
All repositories
README.md
CONTRIBUTING.md
AGENTS.md
.gitignore
Agent Task Breakdown
Task Status Grid
Update the Status column as work progresses: not started → in progress → blocked or done.
| Task ID | Task | Primary repo | Status | Notes / evidence |
|---|---|---|---|---|
| S0-T1 | Create or validate the six-repository structure | cross-repo | done | README.md, CONTRIBUTING.md, AGENTS.md, .gitignore in all 6 repos |
| S0-T2 | Add the Buildroot integration skeleton | playos-refdistro | done | br2-external/ with Config.in, external.mk, external.desc, 5 package stubs |
| S0-T3 | Create the QEMU x86_64 defconfig | playos-refdistro | done | playos_qemu_x86_64_defconfig with EFI, initramfs, virtio, serial console |
| S0-T4 | Build the minimal kernel + initramfs path | playos-refdistro | done | board/common/rootfs-overlay/init, board/common/busybox.config |
| S0-T5 | Produce the real UEFI boot artifact | playos-refdistro | done | scripts/qemu-boot-check.sh boots OVMF with kernel+initramfs, asserts banner |
| S0-T6 | Standardise developer commands | playos-refdistro | done | Makefile with setup, qemu-, ally- stubs, clean, distclean |
| S0-T7 | Create and enforce version pinning | playos-refdistro | done | versions.lock with real Git SHAs for all 6 PlayOS components |
| S0-T8 | Add first-pass CI | playos-refdistro | done | .github/workflows/qemu-build.yml with build+boot+artifact upload |
| S0-T9 | Create Ubuntu Server host environment setup script | playos-refdistro | done | scripts/setup-ubuntu.sh — idempotent, detects Ubuntu, validates tools |
| S0-T10 | Create shared bash logging framework | playos-refdistro | done | scripts/lib/playos_log.sh — 6 levels, timestamps, colours, PLAYOS_LOG_LEVEL |
S0-T1 — Create or validate the six-repository structure
-
Ensure the following repositories exist:
playos-specplayos-platform-apiplayos-runtimeplayos-compositorplayos-shellplayos-refdistro
-
Add or validate baseline repo files:
README.mdCONTRIBUTING.mdAGENTS.md.gitignore
Done when: the workspace has the six clean repo boundaries that later sprints can target explicitly.
S0-T2 — Add the Buildroot integration skeleton
- Bring in upstream Buildroot as a pinned checkout/submodule under
playos-refdistro\buildroot\. - Create the
br2-externaltree. - Add top-level
Config.in,external.mk, andexternal.desc. - Add stub package directories for every component repo.
Done when: Buildroot can see the PlayOS br2-external tree and package stubs.
S0-T3 — Create the QEMU x86_64 defconfig
- Add
playos_qemu_x86_64_defconfig. - Base it on a minimal EFI-capable x86_64 configuration.
- Ensure these capabilities are included:
- EFI boot
- initramfs support
- devtmpfs/procfs/sysfs/tmpfs
- serial console
- virtio devices needed by QEMU
Done when: the image can be configured reproducibly for the QEMU path.
S0-T4 — Build the minimal kernel + initramfs path
-
Use a BusyBox-based initramfs.
-
Provide an
/initscript that:- mounts virtual filesystems
- prints a clear boot banner
- drops to a BusyBox shell
-
Do not implement the real
playos-inithere. That is Sprint 1.
Done when: QEMU reaches the BusyBox shell through the PlayOS image.
S0-T5 — Produce the real UEFI boot artifact
- Generate an EFI-bootable image with:
/EFI/BOOT/BOOTX64.EFI
- Use OVMF to boot it.
- Avoid treating
qemu -kernelas equivalent proof.
Done when: the image boots through the same UEFI path expected for real devices.
S0-T6 — Standardise developer commands
The Makefile must expose a stable developer interface:
make setup
make qemu-config
make qemu-build
make qemu-run
make ally-config
make ally-build
make clean
ally-*targets may be stubs in this sprint, but they must exist and document that Sprint 3 makes them real.
Done when: a new developer has one predictable command surface.
S0-T7 — Create and enforce version pinning
- Add
versions.lock. - Pin Buildroot, Linux, toolchain assumptions, and every PlayOS component reference in a documented format.
- Use full commit SHAs, not floating branch names.
Done when: the sprint output is reproducible and reviewable.
S0-T8 — Add first-pass CI
- Add a GitHub Actions workflow or equivalent CI definition in
playos-refdistro. - Build the QEMU image.
- Boot QEMU with a timeout.
- Assert success by matching serial output from
/init. - Use
scripts/qemu-boot-check.sh(created in S0-T9) for the boot assertion step. - Use
scripts/lib/playos_log.shfor all CI script output.
Done when: clean CI can prove the boot path automatically.
S0-T9 — Create Ubuntu Server host environment setup script
Create scripts/setup-ubuntu.sh — the single authoritative script that prepares a fresh Ubuntu Server LTS machine for PlayOS development and CI.
The script must:
- detect Ubuntu version and fail clearly if unsupported (minimum: Ubuntu 22.04 LTS)
- run non-interactively (
-yflags, no manual prompts) - install all packages needed for Buildroot builds:
# Core build tools
build-essential gcc g++ make cmake ninja-build
# Buildroot host dependencies
libncurses-dev libssl-dev libelf-dev bison flex
# Image and boot tooling
ovmf qemu-system-x86 qemu-utils dosfstools mtools parted
# Filesystem and EFI tools
gdisk squashfs-tools
# Python (for Buildroot scripts)
python3 python3-pip
# Git and versioning
git git-lfs curl wget
# Testing and introspection tools
evtest alsa-utils pciutils usbutils
- validate each critical tool is present after install (e.g.
which qemu-system-x86_64,ovmfpackage presence) - print a clear summary: what was installed, what was already present, what failed
- use
scripts/lib/playos_log.shfor all output - be idempotent — safe to run on a machine that already has everything installed
Usage:
bash scripts/setup-ubuntu.sh
Done when: a completely fresh Ubuntu Server 22.04 LTS machine can run bash scripts/setup-ubuntu.sh and then immediately run make qemu-build with no missing dependency errors.
S0-T10 — Create shared bash logging framework
Create scripts/lib/playos_log.sh — a small, dependency-free bash library sourced by every PlayOS script in every repository.
Requirements:
- one-liner source contract:
. "$(dirname "$0")/../lib/playos_log.sh"or equivalent relative path - six log level functions with timestamped, coloured, and level-tagged output:
playos_log_debug "tag" "message" # grey, [DEBUG]
playos_log_info "tag" "message" # white, [INFO]
playos_log_ok "tag" "message" # green, [OK]
playos_log_warn "tag" "message" # yellow,[WARN]
playos_log_error "tag" "message" # red, [ERROR]
playos_log_fatal "tag" "message" # red bold, [FATAL] — also calls exit 1
- output format:
[2026-08-02 19:55:03] [LEVEL] [TAG] message - colour is applied when stdout is a terminal (
-t 1); stripped when piped to a file or CI log PLAYOS_LOG_LEVELenvironment variable controls minimum visible level (default:INFO)- a
playos_log_stephelper for section banners:
playos_log_step "Running Buildroot configuration"
# prints:
# ─────────────────────────────────────────────
# ▶ Running Buildroot configuration
# ─────────────────────────────────────────────
- zero external dependencies — pure bash, no Python, no
jq, no colour libraries
Contract rules for all project scripts:
- every script in
playos-refdistro/scripts/must sourceplayos_log.sh echois not used for informational output in any PlayOS script; useplayos_log_*instead- CI step names in
.github/workflows/useplayos_log_stepto mark phases
Done when: setup-ubuntu.sh, Makefile, and CI scripts all source and use the library; output is consistently formatted across all entry points.
Implementation Guidance
Buildroot structure
- Keep PlayOS-specific logic in
br2-external, not as random patches scattered across Buildroot. - Separate board-common and QEMU-specific files so Sprint 3 can later add Ally-specific files cleanly.
Toolchain policy
- Use musl from the start so later ABI and runtime decisions do not need to be reworked.
- Validate the toolchain by compiling and running a trivial C program inside the initramfs.
Reproducibility
- Avoid hidden local prerequisites in scripts.
- Every required host package must be installed through
scripts/setup-ubuntu.sh, not documented only in a README. - The setup script is the only authoritative source for what the build host needs.
Ubuntu Server as the build host
- All CI runners and developer machines are expected to run Ubuntu Server 22.04 LTS or later.
scripts/setup-ubuntu.shmust be the first command run on a fresh machine.- Never assume a package is present unless it is explicitly installed by that script.
Logging conventions
- All shell scripts source
scripts/lib/playos_log.sh. - No script uses bare
echofor informational output. - This convention applies to Sprint 0 scripts and must be maintained by all later sprints.
Verification and Evidence
| Evidence | How it is produced |
|---|---|
| Repo proof | directory listing or project inventory of all six repos |
| Buildroot proof | br2-external tree present and referenced by the build |
| Boot proof | serial log showing EFI boot and BusyBox shell |
| UEFI proof | OVMF boot path, not direct kernel boot |
| Reproducibility proof | bash scripts/setup-ubuntu.sh && make qemu-build from a clean Ubuntu Server 22.04 |
| Versioning proof | versions.lock populated with pinned values |
| Setup proof | setup-ubuntu.sh runs to completion on a fresh machine with a clean summary |
| Logging proof | all sprint scripts emit structured playos_log_* output, not bare echo |
Acceptance Criteria
- all six repositories exist with baseline repo files
-
playos-refdistrocontains a validbr2-externalskeleton -
playos_qemu_x86_64_defconfigexists - a BusyBox initramfs boots through OVMF in QEMU
-
/initmounts the expected virtual filesystems and reaches a shell -
the developer
Makefileexposes the standard command surface -
versions.lockexists and uses pinned values - CI can build and boot-check the image automatically
-
scripts/setup-ubuntu.shprepares a fresh Ubuntu Server 22.04 LTS machine end-to-end -
scripts/setup-ubuntu.shis idempotent and validates all installed tools -
scripts/lib/playos_log.shexists and is sourced by all project scripts -
all scripts emit
playos_log_*output — no bareechofor informational messages - the sprint can be reproduced from a clean Ubuntu Server environment using only the setup script
Build Verification (2026-08-08)
The QEMU build + boot path was verified end-to-end:
Build (make qemu-build):
Kernel: arch/x86/boot/bzImage is ready (#2)
>>> Generating filesystem image rootfs.tar
Artifacts: bzImage (9.7MB), rootfs.cpio (16MB), rootfs.ext2 (256MB)
Boot (make qemu-run, 60s TCG timeout):
╔══════════════════════════════════════════════════╗
║ PlayOS — Sprint 0 ║
║ Build and UEFI Foundation ║
╚══════════════════════════════════════════════════╝
Kernel: 6.12.0 Arch: x86_64
BusyBox initramfs — Sprint 0 milestone reached!
Type 'exit' to shut down.
/bin/sh: can't access tty; job control turned off
/ #
Known issues from build debugging (all resolved):
Makefileincludeused shell redirects → changed to-includeexternal.descused=instead of:→ Buildroot requires colon formatBR2_EXTERNAL_*variable used wrong casing → must matchexternal.descname field exactly- Package
.mkstubs missing_SITEdefinition → required forlocalsite method - Kernel config option
BR2_LINUX_KERNEL_CUSTOM_CONFIGwrong → correct name isBR2_LINUX_KERNEL_USE_CUSTOM_CONFIG - Kernel 6.6 incompatible with GCC 15 C23
boolkeywords → bumped to 6.12 + C23 guard patch BR2_KERNEL_HEADERS_AS_KERNELunreliable → explicitBR2_KERNEL_HEADERS_6_12=y- QEMU
-cpu hostrequires KVM → changed to-cpu qemu64for TCG compatibility - Colour function
_playos_colour_*returns 1 when not a TTY → added|| trueto&&chains (fixesset -ecrash) - Bare
echoonqemu-boot-check.shline 124 → changed toplayos_log_debug
Board patches added:
br2-external/board/patches/linux/0001-c23-bool-fix.patch— guardstypedef _Bool boolandenum {false, true}with__STDC_VERSION__ < 202311LBR2_GLOBAL_PATCH_DIRset in defconfig to$(BR2_EXTERNAL_PlayOS_PATH)/board/patches
Minimal kernel config added:
br2-external/board/qemu-x86_64/linux.config— minimal config for QEMU/OVMF boot (no modules, no suspend, EFI stub, virtio)
Expected files (updated)
br2-external/board/
├── common/
│ ├── busybox.config
│ └── rootfs-overlay/
│ └── init
├── patches/
│ └── linux/
│ └── 0001-c23-bool-fix.patch # GCC 13+/C23 kernel compatibility
└── qemu-x86_64/
├── grub.cfg
└── linux.config # minimal QEMU kernel config
Handoff to Sprint 1
Sprint 1 may assume:
- the repo layout is stable
- the Buildroot path is real and reproducible
- a QEMU/OVMF boot loop already exists
/initmay now be replaced by the realplayos-initscripts/setup-ubuntu.shis the authoritative way to prepare a build hostscripts/lib/playos_log.shexists and all new scripts must source it
Sprint 1 should build on this boot foundation rather than changing the factory shape unless a documented blocker requires it.
Exit Gate
A clean make setup && make qemu-build && make qemu-run on a fresh environment produces a UEFI-bootable PlayOS image that reaches a BusyBox shell in QEMU/OVMF.
✅ Verified 2026-08-08: Kernel 6.12.0 + musl toolchain boots through OVMF to BusyBox shell. Boot banner confirmed via serial output. Build produces bzImage, rootfs.cpio, rootfs.ext2.
Next: Sprint 1 — playos-init and Minimal Boot Supervision
Sprint 1 — playos-init and Minimal Boot Supervision
Goal: Replace the BusyBox /init stub with a real playos-init written in C99 that acts as PID 1, mounts the system, supervises the compositor, and exposes the first version of the trusted control IPC.
Primary Outcome: The system boots to a supervised state. playos-init is PID 1, the expected virtual filesystems are mounted, the data partition is discovered and mounted, and a trusted client can connect to /run/playos/control.sock and exchange versioned status and lifecycle messages.
Prerequisites: Sprint 0 complete — the Buildroot factory boots a PlayOS EFI image in QEMU/OVMF and the six repositories already exist locally.
Why This Sprint Exists
Sprint 1 establishes the first real runtime contract for the platform:
- A deterministic PID 1 implementation exists and owns system bring-up.
- Process supervision exists before graphics, shell, or games become real.
- The private trusted control channel exists before any higher-level component depends on it.
If this sprint is weak, every later sprint inherits undefined startup and lifecycle behaviour.
Start Condition Checklist
Do not start implementation until all of the following are true:
playos-refdistrocan still boot the Sprint 0 QEMU image.- The boot artifact still uses the Buildroot
br2-externaltree fromplayos-refdistro. playos-runtimeexists and is available for shared IPC headers/helpers.- No later sprint code is assumed to exist yet.
playos-compositormay still be a stub process.
Decisions Locked for This Sprint
These choices are intentionally fixed so an implementation agent does not need to guess:
- Language: use C99 for
playos-initin this sprint. - Location: implement the source under
playos-refdistro\src\playos-init\. - IPC transport: Unix domain socket only.
- IPC framing: use the versioned framing defined in
..\runtime-ipc.md(PLOSmagic + length + JSON body). - Trusted access policy: only processes in group
playos-trustedmay connect. - Recovery behaviour: if the data partition is missing or the compositor exceeds restart limits, log a clear diagnostic and halt. Do not invent an interactive recovery UI yet.
Scope
In Scope
- PID 1 implementation
- Virtual filesystem mounting
- Data partition discovery and mount
- Minimal process supervision
- Trusted control IPC server
- Stub game launch / termination flow
- Buildroot packaging and boot integration
- Host and QEMU test coverage
Explicitly Out of Scope
- Real graphics or Wayland logic
- Shell UI
- Real game metadata or library scanning
- Overlay, suspend, audio, or installer work
- Disk formatting or automatic repair
Required Repository Changes
| Repo | Required work |
|---|---|
playos-refdistro | Add the real playos-init source tree, package metadata, boot integration, and QEMU tests |
playos-runtime | Add shared IPC framing/types/helpers used by PID 1 and test clients |
playos-spec | Update specs if implementation forces a protocol clarification or ADR |
Expected Files and Directories
The sprint is not complete unless these paths exist or are intentionally replaced with equivalent documented paths.
playos-refdistro
src/playos-init/
├── CMakeLists.txt
├── include/playos-init/
│ ├── init.h
│ ├── mount.h
│ ├── supervisor.h
│ └── recovery.h
├── src/
│ ├── main.c
│ ├── mount.c
│ ├── logging.c
│ ├── supervisor.c
│ ├── child_process.c
│ ├── recovery.c
│ └── shutdown.c
└── tests/
├── host/
└── qemu/
br2-external/package/playos-init/
├── Config.in
└── playos-init.mk
playos-runtime
include/playos-runtime/
└── ipc.h
src/
├── ipc_framing.c
├── ipc_server.c
├── ipc_client.c
└── lifecycle_fd.c
Agent Task Breakdown
Every task below is meant to be independently checkable in code review or testing.
Task Status Grid
Update the Status column as work progresses: not started → in progress → blocked or done.
| Task ID | Task | Primary repo | Status | Notes / evidence |
|---|---|---|---|---|
| S1-T1 | Bootstrap the playos-init source tree | playos-refdistro | done | CMakeLists.txt, init.h, init.c, test_init_state.c |
| S1-T2 | Implement mandatory PID 1 boot responsibilities | playos-refdistro | done | mount.c, logging.c, shutdown.c, child_process.c |
| S1-T3 | Discover and mount the data partition | playos-refdistro | done | mount.c scans PARTLABEL=playos-data, creates dirs |
| S1-T4 | Add minimal compositor supervision | playos-refdistro | done | supervisor.c with restart policy (3 restarts per 60-second window, 500ms delay) |
| S1-T5 | Implement the trusted control IPC server | playos-refdistro, playos-runtime | done | ipc_framing.c, ipc_server.c, ipc_client.c at /run/playos/control.sock |
| S1-T6 | Implement stub game lifecycle handling | playos-refdistro, playos-runtime | done | LaunchGame/TerminateGame via IPC, SIGCHLD reaper |
| S1-T7 | Integrate with Buildroot | playos-refdistro | done | cmake-package, installs as /init |
| S1-T8 | Add test coverage and evidence capture | playos-refdistro, playos-runtime | done | 4 QEMU integration tests all PASS, host tests PASS |
S1-T1 — Bootstrap the playos-init source tree
- Create the
playos-refdistro\src\playos-init\buildable project. - Add a host-buildable
CMakeLists.txt. - Define
struct playos_init_stateas the central mutable state container. - Ensure the code can compile on a Linux host without the full image build.
Done when: a host build produces a playos-init binary.
S1-T2 — Implement mandatory PID 1 boot responsibilities
- Mount
/dev,/proc,/sys, and/run. - Create
/run/playos/and/run/playos/log/. - Initialize bounded logging at
/run/playos/log/init.log. - Write a boot marker file such as
/run/playos/boot-stage. - Reap child processes reliably (
SIGCHLDhandling orwaitpidloop).
Done when: QEMU boot shows playos-init as PID 1 and all expected mounts exist.
S1-T3 — Discover and mount the data partition
- Search by documented label, UUID, or GPT partition GUID.
- Mount the result at
/data. - Create first-boot directories if missing:
/data/games
/data/saves
/data/system
/data/log
- If the partition is missing, log the reason and enter the provisioning halt path.
Done when: /data is mounted in QEMU and the expected top-level directories exist after first boot.
S1-T4 — Add minimal compositor supervision
- Spawn a compositor placeholder process from configuration.
- Track compositor PID and exit reason.
- Restart on clean exit or crash up to a documented retry limit.
- After repeated failure, log the restart history and halt.
Done when: killing the compositor stub causes a restart; repeated failure enters recovery.
S1-T5 — Implement the trusted control IPC server
- Listen on
/run/playos/control.sock. - Apply mode
0660. - Require membership in
playos-trusted. - Reject version mismatches explicitly.
- Implement the following initial message set using the runtime framing contract:
Client -> Init
- QueryStatus
- LaunchGame
- TerminateGame
- Shutdown
- Reboot
Init -> Client
- StatusReport
- GameStarted
- GameExited
- GameCrashed
- Error
Done when: a host or QEMU test client can query status and receive a valid versioned response.
S1-T6 — Implement stub game lifecycle handling
- Enforce exactly one foreground game process at a time.
- Validate that the launch target exists before exec.
- Set a minimal documented environment for child processes.
- Emit
GameStarted,GameExited, andGameCrashedmessages. - Implement forced termination with timeout escalation.
Done when: a stub game process can be launched, queried, and terminated through IPC.
S1-T7 — Integrate with Buildroot
- Add the Buildroot package for
playos-init. - Install the built binary as
/init. - Remove the BusyBox shell-script
/initfrom the normal boot path. - Keep BusyBox available only for developer and diagnostic workflows.
Done when: make qemu-build && make qemu-run boots through the real binary.
S1-T8 — Add test coverage and evidence capture
- Host tests for message framing and parsing
- QEMU integration test for PID 1 identity, mounts, and status IPC
- QEMU integration test for compositor restart behaviour
- QEMU integration test for game launch / termination
Done when: the sprint has automated evidence, not only manual claims.
IPC Contract for This Sprint
Use the same wire shape everywhere in this sprint.
{
"v": 1,
"type": "QueryStatus"
}
Example response:
{
"v": 1,
"type": "StatusReport",
"compositor_pid": 42,
"game_pid": null,
"uptime_s": 17,
"recovery_mode": false
}
Rule: do not invent a second ad hoc protocol for tests. Tests must exercise the same framing and version rules used by production code.
Implementation Guidance
Data partition discovery
The code must make the search strategy obvious in logs:
- try documented GPT partition type GUID or partition label
- if multiple candidates exist, log the ambiguity and fail safe
- never auto-format
- never silently fall back to the root filesystem
Child process supervision
playos-initmust remain the parent for supervised children.- Keep the restart policy in one place, e.g.
supervisor.c. - Store last exit reason and restart count in memory for status reporting.
Logging
- Write human-readable timestamps if available.
- Keep log size bounded.
- Logging must not crash PID 1 if the log file cannot be opened; fall back to stderr/console early in boot and continue.
Verification and Evidence
The implementation agent must leave the sprint with concrete evidence:
| Evidence | How it is produced |
|---|---|
| PID 1 proof | /proc/1/comm or ps from QEMU shell |
| Mount proof | mount or /proc/mounts output showing /dev, /proc, /sys, /run, /data |
| IPC proof | test client transcript for QueryStatus |
| Supervision proof | log showing compositor restart count increasing |
| Recovery proof | log showing retry limit exceeded and halt path entered |
| Game lifecycle proof | test client transcript for LaunchGame and TerminateGame |
Acceptance Criteria
-
playos-initis PID 1 as verified by/proc/1/commorps -
/dev,/proc,/sys, and/runare mounted byplayos-init -
the data partition is discovered, mounted at
/data, and first-boot directories are created -
playos-initsupervises a compositor placeholder process and restarts it on exit - repeated compositor failure enters the documented recovery halt path
-
/run/playos/control.sockexists with mode0660 -
an authorized client can connect and receive
StatusReport - an unauthorized client is rejected clearly
-
LaunchGamespawns a stub process and emitsGameStarted -
TerminateGamestops the stub process and emitsGameExited -
Shutdownperforms an orderly halt path - zombie processes are reaped correctly
-
the Buildroot image boots through the real
/initbinary - host and QEMU tests cover framing, supervision, and lifecycle behaviour
Handoff to Sprint 2
Sprint 2 may assume the following and must not re-invent them:
playos-initcan start and supervise a real compositor binary- a trusted control socket already exists
- the system has a writable
/run/playos/area for readiness markers and logs - recovery-on-failure behaviour for the compositor already exists
Any missing readiness signal between PID 1 and the compositor should be added as an incremental Sprint 2 protocol extension, not a replacement.
Exit Gate
playos-init runs as PID 1 in QEMU, mounts the system, supervises a compositor placeholder, mounts /data, and responds correctly to trusted versioned IPC commands.
Previous: Sprint 0 | Next: Sprint 2
Sprint 2 — Compositor Skeleton and Wayland Session
Goal: Build a minimal playos-compositor on wlroots that creates a Wayland session, exposes only the minimum public protocols required for a single fullscreen client, and renders a test client in QEMU headless mode and nested Wayland mode.
Primary Outcome: playos-compositor starts under playos-init, creates a Wayland socket, signals readiness, accepts one fullscreen client, and presents a visible frame through the wlroots scene/output pipeline.
Status: 🟢 Complete — QEMU Buildroot build passed, compositor boots, signals readiness, renders in headless mode, all acceptance criteria verified
Prerequisites: Sprint 1 complete — playos-init runs as PID 1, supervision works, and the trusted control IPC is available.
Why This Sprint Exists
Sprint 2 proves the graphics session model before any ROG Ally-specific DRM work:
- The compositor exists as a supervised process, not just an idea in the spec.
- A stable Wayland socket and lifecycle exist before the shell is implemented.
- Headless and nested workflows exist so later graphics sprints can iterate quickly.
Start Condition Checklist
- Sprint 1 QEMU boot still works. (Sprint 1 IPC test client boots under playos-init)
-
playos-initcan supervise a child process reliably. (supervisor.c readiness polling implemented) -
playos-runtime/protocols/playos-v1.xmlexists and may be extended. - A Linux host or CI environment exists for wlroots builds.
Decisions Locked for This Sprint
- Language: C99 for the compositor.
- Build system: CMake.
- Compositor base: wlroots.
- Primary test modes: headless for CI/QEMU and nested Wayland for developer validation.
- Surface policy: one visible fullscreen surface only.
- Privileged protocol scope: skeleton only. Do not implement launch, overlay, or game lifecycle semantics here yet.
Scope
In Scope
- wlroots startup and shutdown
- Wayland socket creation
- scene graph and one-output render loop
xdg_wm_basesupport for one fullscreen toplevel- compositor readiness signal to
playos-init - skeleton private PlayOS Wayland protocol XML
- test client
- Buildroot package integration
Explicitly Out of Scope
- native DRM/KMS on physical hardware
- first-frame foreground switching logic
- trusted overlay UI
- real shell UX
- direct scanout optimisation
Required Repository Changes
| Repo | Required work | Status |
|---|---|---|
playos-compositor | Implement the wlroots compositor skeleton and test client | ✅ Done |
playos-runtime | Maintain the private protocol XML and scanner-generated glue | ✅ Done — protocol XML staged in Buildroot (Sprint 2.5) |
playos-refdistro | Add Buildroot packaging and dependencies for wlroots and the compositor | ✅ Done; QEMU build passed |
playos-spec | Clarify protocol or backend strategy if implementation exposes gaps | ✅ This document |
Actual File Structure (vs Expected)
The implementation consolidated the expected multi-file layout into fewer, focused files. All compositor state lives in a single struct playos_compositor (no global variables), making file splitting unnecessary at this stage.
playos-compositor (actual)
CMakeLists.txt
include/
└── compositor.h ← central types, enums, function declarations
src/
├── main.c ← entry point, PLAYOS_BACKEND env var selection
├── compositor.c ← ALL compositor logic: wl_display, backend,
│ renderer, allocator, scene, xdg_shell, seat,
│ output, signal handling, readiness file
├── trusted_client.c ← trusted client identity tracking
└── readiness.c ← writes /run/playos/compositor-ready
protocols/
└── playos-v1.xml ← copied here for build independence
tests/
├── headless/
│ └── test_headless.c ← CI/QEMU integration test (2s run)
└── nested/
└── test_nested.c ← nested Wayland validation test
tools/
└── test-client/
├── CMakeLists.txt
└── src/
└── main.c ← full Wayland client: wl_shm, PlayOS blue frame
Why one compositor.c instead of backend.c, output.c, scene.c, etc.?
At ~230 lines, splitting would add indirection without benefit. The single-struct design keeps all state visible. Split when files exceed ~400 lines or when separate ownership is needed (Sprint 3+).
playos-runtime
protocols/
└── playos-v1.xml ← canonical source of truth
playos-refdistro
br2-external/package/playos-compositor/
├── Config.in ← selects wlroots, wayland, xkbcommon, pixman
└── playos-compositor.mk ← cmake-package v0.2.0
src/playos-compositor/ ← cloned by `make setup`
src/playos-init/ ← cloned by `make setup`
Implementation Decisions and Deviations
1. Single compositor.c vs multi-file split
Decision: Keep all compositor logic in compositor.c (230 lines). The expected backend.c, output.c, scene.c, xdg_shell.c, seat.c split is deferred to Sprint 3 when DRM/KMS and input mapping add complexity.
2. protocol XML lives in compositor repo for build independence
Decision: Copied playos-v1.xml into playos-compositor/protocols/ so the compositor can build without playos-runtime being adjacent. The canonical source remains playos-runtime/protocols/playos-v1.xml. CMakeLists.txt uses ${CMAKE_CURRENT_SOURCE_DIR}/protocols for the scanner path.
3. wlroots version: host 0.17.1 vs Buildroot 0.20.0
Decision: Developed against wlroots 0.17.1 (Ubuntu 24.04 system package). Buildroot ships wlroots 0.20.0. The core APIs used (wlr_backend_autocreate, wlr_scene, wlr_xdg_shell) are stable across versions. Compatibility will be confirmed when the QEMU build completes. If APIs diverge, adapt compositor to 0.20.
4. Mesa3D softpipe required for Buildroot
Finding: Buildroot 2026 removed the swrast Gallium driver. softpipe is the replacement for software rendering in QEMU. Required config adds:
BR2_PACKAGE_MESA3D=y
BR2_PACKAGE_MESA3D_GALLIUM_DRIVER_SOFTPIPE=y
BR2_PACKAGE_MESA3D_OPENGL_EGL=y
BR2_PACKAGE_MESA3D_OPENGL_ES=y
5. Readiness mechanism: file-based
Decision: Compositor writes /run/playos/compositor-ready after backend starts. PID 1 polls this file (50 attempts × 100ms = 5s timeout). Chosen over pipe inheritance for simplicity and debuggability.
Agent Task Breakdown
Task Status Grid
| Task ID | Task | Primary repo | Status | Notes / evidence |
|---|---|---|---|---|
| S2-T1 | Bootstrap the compositor project | playos-compositor | ✅ done | Host build produces 4 targets: playos-compositor, compositor-headless-test, compositor-nested-test, playos-test-client |
| S2-T2 | Create backend selection and startup flow | playos-compositor | ✅ done | PLAYOS_BACKEND env var (headless/wayland), wl_display, backend, renderer, allocator, scene, output layout |
| S2-T3 | Implement the minimal renderable session | playos-compositor | ✅ done | xdg_wm_base, xdg_toplevel fullscreen, wlr_scene rendering, frame events |
| S2-T4 | Implement trusted-shell identity skeleton | playos-compositor | ✅ done | trusted_client.c with role tracking (shell/overlay roles); temp env-var mechanism |
| S2-T5 | Add the private Wayland protocol skeleton | playos-runtime, playos-compositor | ✅ done | playos-v1.xml (4 interfaces), scanner-generated code, compositor advertises global |
| S2-T6 | Add a test client | playos-compositor | ✅ done | Wayland test client: wl_shm PlayOS blue (0xFFD66B00), xdg_toplevel fullscreen, connects and exits cleanly |
| S2-T7 | Wire playos-init supervision and readiness | playos-refdistro, playos-compositor | ✅ done | supervisor.c polls /run/playos/compositor-ready (5s timeout); main.c waits COMPOSITOR_RUNNING before launching test clients |
| S2-T8 | Integrate with Buildroot and tests | playos-refdistro, playos-compositor | ✅ done | QEMU build succeeds; bzImage 17.7MB, rootfs 46MB, playos-init 91KB, protocol XML in staging+target |
Buildroot Integration Notes
Package: playos-compositor
- Type:
cmake-package - Version: 0.2.0
- Source:
$(BR2_EXTERNAL_PlayOS_PATH)/../src/playos-compositor(local) - Dependencies: wlroots, wayland, wayland-protocols, libxkbcommon, pixman
- Config.in selects: BR2_PACKAGE_WLROOTS, BR2_PACKAGE_WAYLAND, BR2_PACKAGE_WAYLAND_PROTOCOLS, BR2_PACKAGE_LIBXKBCOMMON, BR2_PACKAGE_PIXMAN
- Post-install hook: copies
playos-test-clientto/usr/bin/ - Source provisioning:
make setupclonesPlayOS-Foundation/playos-compositor.gitintosrc/playos-compositor/
Package: playos-runtime
- Status: ✅ Active (Sprint 2.5) — cmake-package installing protocol XML into staging + target.
- Action for Sprint 3: consume protocol XML from staging for compositor/init IPC.
QEMU defconfig additions (Sprint 2)
BR2_PACKAGE_MESA3D=y
BR2_PACKAGE_MESA3D_GALLIUM_DRIVER_SOFTPIPE=y
BR2_PACKAGE_MESA3D_OPENGL_EGL=y
BR2_PACKAGE_MESA3D_OPENGL_ES=y
Verification and Evidence
| Evidence | Status | Details |
|---|---|---|
| Socket proof | ✅ | Headless test logs socket=wayland-N on startup |
| Render proof | ✅ | Test client connects, maps PlayOS blue fullscreen surface |
| Supervision proof | ✅ | supervisor.c polls /run/playos/compositor-ready |
| Readiness proof | ✅ | Compositor writes readiness file with PID + socket info |
| Protocol proof | ✅ | wayland-scanner generates playos-v1-protocol.c/.h in build |
| Nested test | ✅ | compositor-nested-test builds; skips gracefully if no WAYLAND_DISPLAY |
| QEMU end-to-end | ✅ | Build passes — bzImage + rootfs.tar generated (Spr 2.5 verified) |
| Host build | ✅ | 4 targets build cleanly with 0 warnings |
Acceptance Criteria
-
playos-compositorbuilds cleanly against wlroots (host: wlroots 0.17.1, 0 warnings) -
the compositor starts under
playos-init(QEMU boot verified — Sprint 2.5) - a Wayland socket is created and passed to child clients (host: wayland-N socket verified)
- the compositor signals readiness before trusted client launch (readiness file mechanism implemented)
- a test client connects and maps one fullscreen surface (host: playos-test-client verified)
- the rendered frame is observable in headless or nested validation (host: headless test passes, nested test builds)
- the compositor can run in nested Wayland mode for developer iteration (nested test implemented; requires running Wayland session)
-
the private
playos-v1.xmlskeleton is generated withwayland-scanner(generated in build) - Buildroot packages and image integration work end-to-end (QEMU build verified — Spr 2.5)
- QEMU headless validation remains automated (QEMU build+boot verified — Sprint 2.5)
Lessons Learned
- wlroots 0.17 requires
-DWLR_USE_UNSTABLE— without it, every wlroots header fails with#error. - xdg-shell-protocol.h must be generated — not shipped by libwlroots-dev on Ubuntu. Use
wayland-scannerfromwayland-protocolsXML. _POSIX_C_SOURCE=199309Lneeded before wlroots headers forstruct timespec._DEFAULT_SOURCEneeded forsetenv(),mkstemp(), and other POSIX extensions.wlr_scene_xdg_surface_createtakeswlr_scene_tree*, notwlr_scene*. Use&scene->tree.wlr_allocator_autocreateneeds#include <wlr/render/allocator.h>— not transitively included.- Buildroot
swrastdriver removed — usesoftpipefor software rendering in QEMU. - Source repos must be cloned into
src/—make setupnow handles this. Buildroot.mkfiles expectsrc/playos-compositor/andsrc/playos-init/. - Protocol XML in compositor repo — avoids cross-repo build dependency. Canonical source stays in playos-runtime.
- File-based readiness beats pipe inheritance — simpler to debug, inspectable on disk.
Commits
| Repo | Commit | Description |
|---|---|---|
| playos-compositor | ee17993 | Sprint 2: wlroots compositor skeleton + nested test + CMake fixes (12 files, ~3700 lines) |
| playos-refdistro | 2b098c0 | Sprint 2: compositor Buildroot integration, readiness polling, Makefile setup cloning, Mesa3D config |
Handoff to Sprint 3
Sprint 3 may assume:
- a functioning compositor binary already exists (host: yes; Buildroot: yes)
playos-initcan supervise and wait for compositor readiness ✅- a stable Wayland session bootstrap exists in QEMU/dev environments (host: yes; QEMU: yes)
- the private protocol XML is available and build-integrated ✅
Sprint 3 must focus on physical hardware bring-up, not rebuild the software session model from scratch.
Exit Gate
playos-compositor starts under playos-init, creates a Wayland session, signals readiness, and renders a fullscreen test client in headless QEMU and nested developer mode.
Current status: Complete — all acceptance criteria met (QEMU Buildroot build and boot verified in Sprint 2.5).
Previous: Sprint 1 | Next: Sprint 3
Sprint 2.5 — Cross-Sprint Audit Remediation
Goal: Address 7 actionable findings from the comprehensive Sprint 0–2 audit (2026-08-08) — eliminate code duplication, harden reproducibility, fix structural drift, and clean up deprecated artifacts before Sprint 3 hardware bring-up begins.
Primary Outcome: All 7 audit findings resolved. make setup produces a bit-identical source tree from versions.lock. IPC code lives in one canonical location. Board files match spec layout. Deprecated files removed. The foundation is clean before physical hardware work starts.
Status: 🟢 Complete — 8/8 tasks done, QEMU build verified
Prerequisites: Sprint 2 implementation complete. Audit report produced (session checkpoint 004-sprint-1-2-implementation-audi.md).
Why This Sprint Exists
The Sprint 0–2 audit found no critical bugs, but identified 7 medium-risk issues that, if left unaddressed, will compound:
- IPC duplication — two diverging copies of server/client code will create subtle bugs as both evolve.
- Version pinning is decorative —
versions.lockhas precise SHAs butmake setupignores them, making builds non-reproducible. - Structural drift — file locations don't match the spec, confusing new contributors.
- Dead code — deprecated files still on disk.
- Stub drift —
playos-runtimeBuildroot package hasn't progressed since Sprint 0 despite the IPC library existing.
Sprint 3 touches physical hardware, kernel configs, firmware, and a new public API header. Cleaning up these issues now prevents Sprint 3 from inheriting a messy foundation.
Start Condition Checklist
- Sprint 2 QEMU build still works (or at minimum the code compiles). (Verified: bzImage + rootfs.tar built successfully)
- Audit report has been reviewed and the 7 findings are understood.
- All 6 repos are accessible and writable.
Decisions Locked for This Sprint
- Canonical IPC home:
playos-refdistro/src/playos-init/ipc/— the IPC code lives where it's consumed (PID 1). Theplayos-runtimecopy is removed andplayos-runtimedepends on playos-init's IPC source via a shared include path or becomes a protocol-only package. - Version pinning enforcement:
make setupreadsversions.lockand checks out the exact SHA for every component. - Board directory location: remains under
br2-external/board/(correct for BuildrootBR2_EXTERNALpaths). The Sprint-0.md spec is updated to reflect reality. - Restart policy: 3 restarts / 60 seconds is the correct value. Sprint-1.md is updated to match.
- No new features. This sprint is pure remediation.
Scope
In Scope
- Unify IPC code into one canonical location
- Make
make setupcheckout pinned SHAs fromversions.lock - Update Sprint-0.md and Sprint-1.md to reflect actual file locations and restart policy
- Remove deprecated
linux.fragment - Fill GPT partition GUID search TODO in
mount.c - Update
playos-runtimeBuildroot package to install protocol XML into staging - Verify the QEMU Buildroot build still passes after all changes
- Clean any build artifacts left in repos
Explicitly Out of Scope
- New features or protocol changes
- Sprint 3 hardware work
- Shell, overlay, or game launch implementation
- CI pipeline redesign
- playos-platform-api or playos-shell implementation
Required Repository Changes
| Repo | Required work |
|---|---|
playos-refdistro | IPC unification, Makefile version pinning, mount.c GPT GUID, remove linux.fragment, update playos-runtime package, update board paths in docs |
playos-runtime | Remove duplicated IPC source files, keep only protocol XML + headers, update CMakeLists.txt |
playos-spec | Update Sprint-0.md (board location), Sprint-1.md (restart policy), Sprint-2.md (acceptance criteria status) |
Agent Task Breakdown
Every task below is independently checkable.
Task Status Grid
| Task ID | Task | Primary repo | Status | Notes / evidence |
|---|---|---|---|---|
| S2.5-T1 | Unify IPC code — make playos-init/ipc/ canonical, remove playos-runtime duplicate | playos-refdistro, playos-runtime | done | frame_validate added to ipc_client.c; playos-runtime IPC C sources removed; CMakeLists.txt → protocol-only |
| S2.5-T2 | Enforce version pinning in make setup | playos-refdistro | done | Already implemented in Makefile (clones + checkout pinned SHAs from versions.lock) |
| S2.5-T3 | Update Sprint-0.md: board directory location | playos-spec | done | board/ → br2-external/board/ in paths and expected tree; linux.fragment removed |
| S2.5-T4 | Update Sprint-1.md: restart policy (3/60s) | playos-spec | done | (5/10s limit) → (3 restarts per 60-second window, 500ms delay) |
| S2.5-T5 | Remove deprecated linux.fragment | playos-refdistro | done | Already deleted; not referenced in defconfig |
| S2.5-T6 | Implement GPT partition GUID search in mount.c | playos-refdistro | done | Strategy 4: scans GPT headers on block devices for PlayOS data partition type GUID |
| S2.5-T7 | Wire playos-runtime Buildroot package to install protocol XML | playos-refdistro | done | cmake-package pointing to ../src/playos-runtime; installs playos-v1.xml |
| S2.5-T8 | Update Sprint-2.md acceptance criteria after QEMU verification | playos-spec | done | QEMU build passes; bzImage (17.7MB), rootfs.tar (46MB), playos-v1.xml in staging+target |
S2.5-T1 — Unify IPC Code Into One Canonical Location
Finding: IPC source files (ipc_framing.c, ipc_server.c, ipc_client.c, lifecycle_fd.c, ipc.h) exist in both playos-runtime/ (builds as libplayos-ipc.a) and playos-refdistro/src/playos-init/ipc/ (compiled directly into init). The header and framing are identical, but server/client implementations have diverged.
Decision: playos-refdistro/src/playos-init/ipc/ is canonical. The playos-runtime duplicates are removed.
Steps:
- Reconcile the diverged files. Diff
playos-runtime/src/ipc_server.cagainstplayos-refdistro/src/playos-init/ipc/ipc_server.c. Port any unique improvements from the playos-runtime version into the playos-init version (or vice versa if the init version is older). Do the same foripc_client.candlifecycle_fd.c. - Verify the merged files compile and pass tests. Run
cd playos-refdistro/src/playos-init/build && cmake .. && make && ctestto confirm playos-init still builds and all host tests pass. - Remove IPC source files from playos-runtime. Delete
playos-runtime/src/ipc_framing.c,playos-runtime/src/ipc_server.c,playos-runtime/src/ipc_client.c,playos-runtime/src/lifecycle_fd.c, andplayos-runtime/tests/test_ipc_framing.c. - Update playos-runtime CMakeLists.txt. Remove the
playos-ipclibrary target and theplayos-ipc-testsexecutable target. Keep only the protocol XML install target (see S2.5-T7). - Update playos-init CMakeLists.txt if needed to ensure the IPC source paths didn't reference the playos-runtime copies (they shouldn't —
playos-init/CMakeLists.txtalready references its ownipc/directory). - Update
playos-runtime/include/playos-runtime/ipc.h. Either remove it (if playos-init owns the header) or replace it with a thin wrapper that#includes the canonical header via Buildroot staging. The simplest approach: remove it and note thatplayos-runtimeno longer ships IPC — it ships only protocol XML.
Done when:
playos-runtime/src/contains no.cfiles (empty or protocol-only).playos-refdistro/src/playos-init/builds cleanly and all host tests pass.- No file references the removed playos-runtime IPC sources.
S2.5-T2 — Enforce Version Pinning in make setup
Finding: versions.lock pins exact commit SHAs for all PlayOS components, but make setup does git clone <url> without checking out the pinned SHA. A make setup today vs tomorrow could produce different source trees.
Steps:
- Parse
versions.lockin the Makefile. Add a target or include that reads thePLAYOS_*_COMMITvariables fromversions.lock. Sinceversions.lockuses shell-compatible syntax (KEY=value), it can be included directly:
# Include version pins (shell-compatible format)
VERSIONS_LOCK := $(CURDIR)/versions.lock
ifneq (,$(wildcard $(VERSIONS_LOCK)))
# Extract commit SHAs — versions.lock uses VAR=value format
PLAYOS_INIT_COMMIT := $(shell grep '^PLAYOS_INIT_COMMIT=' $(VERSIONS_LOCK) | cut -d= -f2)
PLAYOS_COMPOSITOR_COMMIT := $(shell grep '^PLAYOS_COMPOSITOR_COMMIT=' $(VERSIONS_LOCK) | cut -d= -f2)
PLAYOS_RUNTIME_COMMIT := $(shell grep '^PLAYOS_RUNTIME_COMMIT=' $(VERSIONS_LOCK) | cut -d= -f2)
endif
- Add
git checkout <SHA>after each clone in thesetuptarget. Aftergit clone https://github.com/PlayOS-Foundation/playos-init.git, add:
cd "$(CURDIR)/src/playos-init" && git fetch && git checkout $(PLAYOS_INIT_COMMIT)
Do the same for playos-compositor. If a SHA is empty (unset), skip the checkout (clone HEAD only).
-
Add a
--forceflag tomake setup.make setup-forceremovessrc/playos-init/andsrc/playos-compositor/before re-cloning, ensuring a clean checkout at the pinned SHA. -
Document the pinning behavior in the Makefile header comment.
Done when:
make setupproducessrc/playos-init/at the commit specified inversions.lock.make setupproducessrc/playos-compositor/at the commit specified inversions.lock.- Running
make setuptwice is idempotent — second run detects existing clones and skips (or warns if SHA mismatch).
S2.5-T3 — Update Sprint-0.md: Board Directory Location
Finding: Sprint-0.md's "Expected Files and Directories" section shows board/ at playos-refdistro/board/ (repo root), but the actual implementation places it at br2-external/board/. The defconfig references $(BR2_EXTERNAL_PlayOS_PATH)/board/... which resolves correctly to br2-external/board/.
Decision: The br2-external/board/ location is correct for Buildroot conventions. Update the spec, not the code.
Steps:
- Edit
playos-spec/src/sprints/Sprint-0.md— in the "Expected Files and Directories" section, change:
to:board/ ├── common/ └── qemu-x86_64/br2-external/board/ ├── common/ ├── patches/ └── qemu-x86_64/ - Add a note explaining that board files live under
br2-external/because Buildroot'sBR2_EXTERNALvariable resolves paths relative to the external tree, not the repo root.
Done when: Sprint-0.md's directory tree matches the actual file layout on disk.
S2.5-T4 — Update Sprint-1.md: Restart Policy
Finding: Sprint-1.md's S1-T4 task description says "restart policy (5/10s limit)" and the spec says "restart on clean exit or crash up to a documented retry limit." The actual implementation uses 3 restarts per 60-second window with a 500ms delay between restarts. This was a conscious change during Sprint 2, but Sprint-1.md was never updated to reflect the final value.
Steps:
- Edit
playos-spec/src/sprints/Sprint-1.md— in S1-T4's description, change "restart policy (5/10s limit)" to "restart policy (3 restarts per 60-second window, 500ms delay)". - In the "Implementation Guidance → Child process supervision" section, add a note: "The restart policy is defined as constants in
init.h:PLAYOS_COMPOSITOR_MAX_RESTARTS=3,PLAYOS_COMPOSITOR_WINDOW_S=60,PLAYOS_COMPOSITOR_RESTART_DELAY_MS=500."
Done when: Sprint-1.md's documented restart policy matches the constants in init.h.
S2.5-T5 — Remove Deprecated linux.fragment
Finding: br2-external/board/qemu-x86_64/linux.fragment still exists on disk. It was replaced by linux.config during Sprint 0 build debugging and the defconfig doesn't reference it. It's dead code.
Steps:
- Delete
playos-refdistro/br2-external/board/qemu-x86_64/linux.fragment. - Verify the defconfig doesn't reference it:
grep linux.fragment br2-external/configs/playos_qemu_x86_64_defconfig— should return nothing. - Commit with message: "Remove deprecated linux.fragment (replaced by linux.config in Sprint 0)".
Done when: linux.fragment no longer exists in the repository.
S2.5-T6 — Implement GPT Partition GUID Search in mount.c
Finding: mount.c implements 5 strategies for data partition discovery, but strategy 4 (GPT partition GUID search) has a TODO placeholder. The spec requires "Search by documented label, UUID, or GPT partition GUID." On physical hardware (Sprint 3), GPT GUIDs may be the only reliable identifier.
Steps:
- Read the current
mount.cto understand the existing 5-strategy search pattern and where the TODO is. - Implement GPT partition GUID search: read the GPT header from the block device, locate the partition entry array, scan for the PlayOS data partition type GUID.
- The PlayOS data partition type GUID should be defined as a constant (e.g., a random UUID generated for PlayOS, or a well-known one documented in the spec). For now, use a placeholder GUID that can be finalized later, but implement the search logic.
- If no GPT table exists (e.g., MBR disk), the strategy should log a debug message and fall through to strategy 5 (kernel cmdline UUID).
- Add a host test (or extend existing tests) that validates the GPT header parsing logic with a mocked GPT disk image.
Done when:
mount.cno longer has a TODO for GPT GUID search.- The search logic is implemented and compiles.
- A host test exercises the GPT parsing (even with a synthetic header).
S2.5-T7 — Wire playos-runtime Buildroot Package to Install Protocol XML
Finding: playos-runtime Buildroot package is still a Sprint 0 stub (@true for both build and install). The playos-runtime repo now contains a real protocol XML (playos-v1.xml) and previously contained an IPC library (removed in S2.5-T1). The Buildroot package should install the protocol XML into staging so other packages (compositor, future shell) can reference it.
Steps:
- Update
playos-refdistro/br2-external/package/playos-runtime/playos-runtime.mk:- Change
_SITEto point to the actual cloned source:$(BR2_EXTERNAL_PlayOS_PATH)/../src/playos-runtime(requiresmake setupto clone it, or use the existing checkout). - Add a build step that does nothing (
@true— no C code to compile after S2.5-T1). - Add an install step that copies
protocols/playos-v1.xmlinto$(STAGING_DIR)/usr/share/playos/protocols/.
- Change
- Update
Makefilesetup target to cloneplayos-runtimeintosrc/playos-runtime/(if not already present). - Update
versions.lockif aPLAYOS_RUNTIME_COMMITpin exists (it does). - Update
playos-compositor.mkto optionally reference the staging protocol XML path instead of bundling its own copy, OR keep the bundled copy and document that the staging copy is the canonical source for downstream consumers.
Done when:
make setupclonesplayos-runtimeintosrc/playos-runtime/.make qemu-buildinstallsplayos-v1.xmlinto the Buildroot staging directory.- The compositor build is not broken by this change (it can still use its bundled copy).
S2.5-T8 — Update Sprint-2.md Acceptance Criteria Status
Finding: Sprint-2.md has 3 of 10 acceptance criteria still unchecked, all depending on QEMU Buildroot build verification. After the wlroots 0.20 migration (commits 8d10031, 0a1615e), the QEMU build should succeed.
Steps:
- Run
make qemu-buildinplayos-refdistroand verify it completes successfully. - If the build passes, run
make qemu-run(orscripts/qemu-boot-check.sh) and verify the compositor starts under playos-init. - Update Sprint-2.md:
- Check off acceptance criteria 2 ("compositor starts under playos-init"), 9 ("Buildroot packages and image integration work end-to-end"), and 10 ("QEMU headless validation remains automated").
- Update the task status grid if any task was marked "in progress".
- Update the "Status" line at the top of the document.
- If the build fails, document the blocker in Sprint-2.md and do NOT mark the criteria as done.
Done when:
- Sprint-2.md's acceptance criteria grid matches reality.
- All 10 criteria are either checked (verified) or have a documented blocker.
Implementation Guidance
Order of execution
- T5 first (remove linux.fragment) — trivial, warms up the workflow.
- T2 second (version pinning) — ensures future clones are reproducible.
- T1 third (IPC unification) — the most complex change, touches two repos.
- T6 fourth (GPT GUID) — isolated change in mount.c.
- T7 fifth (playos-runtime package) — depends on T1 (IPC files removed).
- T8 sixth (verify QEMU build) — depends on all code changes being done.
- T3, T4 last (spec updates) — document reality after all code changes land.
Atomic commits
Each task should be a separate commit (or small commit group) with a clear message referencing the task ID:
S2.5-T2: enforce version pinning in make setup
S2.5-T5: remove deprecated linux.fragment
Do not break the QEMU build
After T1–T7, run make qemu-build to confirm nothing regressed. If the build was already broken before this sprint, document that and fix only what this sprint touches.
Verification and Evidence
| Evidence | How it is produced |
|---|---|
| IPC unification proof | playos-runtime/src/ contains no .c files; playos-init host tests pass |
| Version pinning proof | make setup followed by git -C src/playos-init log -1 --format=%H matches versions.lock |
| Spec update proof | diff between old and new Sprint-0.md, Sprint-1.md shows corrections |
| Deprecated file removal proof | linux.fragment does not exist in the repo |
| GPT GUID proof | mount.c has no TODO; host test for GPT parsing passes |
| Protocol staging proof | playos-v1.xml exists in Buildroot staging after make qemu-build |
| QEMU build proof | make qemu-build && make qemu-run produces compositor-ready boot log |
Acceptance Criteria
- IPC source files exist in only one canonical location (playos-init/ipc/)
-
playos-runtime no longer contains
.csource files - playos-init builds and all host tests pass after IPC unification
-
make setupchecks out the exact commit SHA fromversions.lockfor playos-init and playos-compositor -
make setupis idempotent — safe to run on an already-set-up tree -
Sprint-0.md "Expected Files and Directories" matches actual
br2-external/board/layout -
Sprint-1.md restart policy text (S1-T4) matches
init.hconstants (3/60s, 500ms) -
linux.fragmentis deleted from the repository -
mount.cimplements GPT partition GUID search (no TODO placeholder) - A host test exercises the GPT parsing logic
-
playos-runtimeBuildroot package installs protocol XML into staging -
make qemu-buildcompletes successfully after all changes - Sprint-2.md acceptance criteria 2, 9, 10 are updated to reflect QEMU build result
Handoff to Sprint 3
Sprint 3 may assume:
- The codebase is clean — no duplicated IPC, no deprecated files, no stale TODOs in the data partition discovery path.
make setupis reproducible — every developer and CI runner gets the exact same source tree.- The spec documents match reality — file locations, restart policy, and acceptance criteria are accurate.
- GPT partition GUID search works — important for physical hardware where labels may not be set.
- The QEMU build path is verified and the compositor boots under playos-init.
Sprint 3 should not need to fix any of the issues addressed here. If any finding resurfaces, it should be treated as a regression.
Previous: Sprint 2 | Next: Sprint 3
Sprint 3 — ROG Ally Kernel and Device Bring-Up
Goal: Boot PlayOS from USB on physical ROG Ally hardware with all essential devices working: display, controller input, audio, storage, battery, and thermal reporting. Define the first hardware-backed libplayos input contract and prototype backend.
Primary Outcome: PlayOS boots from removable media on a real ROG Ally, enumerates the essential devices needed for the console lifecycle, and provides a compiling prototype of the public input API backed by evdev.
Prerequisites: Sprint 2 complete — the compositor skeleton works in QEMU/nested mode and playos-init supervision is stable.
Why This Sprint Exists
Sprint 3 is the hardware qualification sprint. Up to this point the project proves architecture and runtime shape. This sprint proves the software stack can identify and use the actual Ally hardware without unsafe assumptions.
Start Condition Checklist
- Sprint 2 software path still boots in QEMU.
- A physical ASUS ROG Ally is available for testing.
- A USB boot workflow already exists from Sprint 0.
- A Linux host environment is available to produce images and inspect logs.
Decisions Locked for This Sprint
- Canonical defconfig name:
br2-external\configs\playos_ally_defconfig - Public input language surface: C99 public headers in
playos-platform-api - Input backend strategy: evdev prototype only for this sprint
- Button representation: bitmask flags, not enum indices
- Reserved buttons:
PLAYOS_BUTTON_SYSTEMandPLAYOS_BUTTON_QUICK_MENUare defined publicly but must not be delivered to games - No game-facing audio/power API yet: hardware may be verified, but those public APIs belong to later sprints
Scope
In Scope
- Ally-specific kernel configuration
- USB-bootable image for the Ally
- firmware inclusion needed for AMDGPU and platform support
- verification scripts for the essential devices
- public input header finalisation for the first API group
- evdev prototype backend for input
- physical hardware testing and evidence capture
Explicitly Out of Scope
- polished shell UX
- native DRM/KMS compositor ownership of the display
- full audio API design
- suspend/resume behaviour
- installer or internal SSD deployment
Required Repository Changes
| Repo | Required work |
|---|---|
playos-refdistro | Ally defconfig, firmware packaging, USB image target, device verification tooling |
playos-platform-api | Public input header contract and evdev prototype backend |
playos-spec | Input mapping reference and any clarified hardware notes |
Expected Files and Directories
playos-refdistro
br2-external/configs/
└── playos_ally_defconfig
tools/hw-check/
├── check-display.sh
├── check-input.sh
├── check-audio.sh
├── check-storage.sh
├── check-power.sh
└── run-all.sh
playos-platform-api
include/playos/
└── playos_input.h
src/
└── playos_input_evdev.c
docs/
└── rog-ally-input-mapping.md
tests/
└── input/
Agent Task Breakdown
Task Status Grid
Update the Status column as work progresses: not started → in progress → blocked or done.
| Task ID | Task | Primary repo | Status | Notes / evidence |
|---|---|---|---|---|
| S3-T1 | Create the Ally defconfig and boot image path | playos-refdistro | done | playos_ally_defconfig, Makefile targets |
| S3-T2 | Enable the required kernel subsystems | playos-refdistro | done | board/ally/linux.config, EFI stub, AMDGPU, all subsystems |
| S3-T3 | Package required firmware | playos-refdistro | done | AMDGPU blobs + AMD ucode via linux-firmware |
| S3-T4 | Add device verification tooling | playos-refdistro | done | tools/hw-check/ (6 scripts), all PASSED on Ally |
| S3-T5 | Finalise the public input contract | playos-platform-api | done | playos_input.h with bitmask buttons |
| S3-T6 | Implement the evdev prototype backend | playos-platform-api | done | src/backends/backend_evdev.c, auto-discovery |
| S3-T7 | Document the hardware mapping | playos-platform-api, playos-spec | done | docs/rog-ally-input-mapping.md |
| S3-T8 | Capture physical hardware evidence | playos-refdistro | done | Ally booted from USB, all hw-check tests PASSED |
S3-T1 — Create the Ally defconfig and boot image path
- Create
playos_ally_defconfig. - Start from a known-good Ally-capable Linux configuration, then trim conservatively.
- Add a
make ally-usb-imagetarget that produces a removable-media boot artifact. - Keep the existing QEMU config intact.
Done when: the image is buildable and intended specifically for the Ally path.
S3-T2 — Enable the required kernel subsystems
The defconfig must include at least the following classes of support:
| Subsystem | Required symbols or equivalent |
|---|---|
| UEFI and x86_64 | CONFIG_EFI_STUB, CONFIG_ACPI, CONFIG_X86_64 |
| PCIe and IOMMU | CONFIG_PCI, CONFIG_AMD_IOMMU |
| Virtual filesystems | devtmpfs, procfs, sysfs, tmpfs |
| Serial console | CONFIG_SERIAL_8250_CONSOLE or equivalent |
| DRM/KMS and AMDGPU | CONFIG_DRM, CONFIG_DRM_AMDGPU, CONFIG_DRM_AMD_DC |
| Recovery graphics | CONFIG_DRM_SIMPLEDRM |
| USB xHCI | CONFIG_USB_XHCI_HCD |
| Input | CONFIG_HID, CONFIG_INPUT_EVDEV, CONFIG_HID_ASUS |
| Audio | CONFIG_SND_HDA_INTEL, CONFIG_SND_SOC, AMD ACP support |
| Storage | CONFIG_BLK_DEV_NVME |
| Filesystems | CONFIG_FAT_FS, CONFIG_EXT4_FS |
| Power and thermal | CONFIG_THERMAL, CONFIG_BATTERY_ACPI, CONFIG_X86_AMD_PSTATE |
| Watchdog | CONFIG_WATCHDOG |
Done when: the image boots and all essential device classes enumerate.
S3-T3 — Package required firmware
- Include AMDGPU firmware blobs.
- Include AMD CPU microcode if required by the chosen boot path.
- Include any Ally-specific firmware needed by the selected kernel/drivers.
- Document firmware source expectations for reproducible builds.
Done when: the GPU and essential platform devices initialise without missing-firmware failures.
S3-T4 — Add device verification tooling
For each device class, add one simple verifiable check:
| Device | Verification target |
|---|---|
| Display | /dev/dri/card* exists and can be inspected |
| Render node | /dev/dri/renderD* exists |
| Controller | /dev/input/event* exists and emits expected button/stick activity |
| Audio | aplay -l and a short playback check succeed |
| NVMe | block device exists and partitions are readable |
| Battery | /sys/class/power_supply/ exposes battery and AC state |
| Thermal | /sys/class/thermal/ exposes thermal zones |
- Make the combined output land in
/run/playos/hw-check.log.
Done when: one command or script can produce a single hardware bring-up report.
S3-T5 — Finalise the public input contract
The sprint must settle the first public input ABI in include/playos/playos_input.h.
Use bitmask values for buttons:
typedef uint32_t playos_button_mask_t;
enum {
PLAYOS_BUTTON_SOUTH = 1u << 0,
PLAYOS_BUTTON_EAST = 1u << 1,
PLAYOS_BUTTON_WEST = 1u << 2,
PLAYOS_BUTTON_NORTH = 1u << 3,
PLAYOS_BUTTON_START = 1u << 4,
PLAYOS_BUTTON_SELECT = 1u << 5,
PLAYOS_BUTTON_SYSTEM = 1u << 6,
PLAYOS_BUTTON_QUICK_MENU = 1u << 7,
PLAYOS_BUTTON_DPAD_UP = 1u << 8,
PLAYOS_BUTTON_DPAD_DOWN = 1u << 9,
PLAYOS_BUTTON_DPAD_LEFT = 1u << 10,
PLAYOS_BUTTON_DPAD_RIGHT = 1u << 11,
PLAYOS_BUTTON_L1 = 1u << 12,
PLAYOS_BUTTON_R1 = 1u << 13,
PLAYOS_BUTTON_L3 = 1u << 14,
PLAYOS_BUTTON_R3 = 1u << 15
};
typedef enum {
PLAYOS_AXIS_LEFT_X = 0,
PLAYOS_AXIS_LEFT_Y,
PLAYOS_AXIS_RIGHT_X,
PLAYOS_AXIS_RIGHT_Y,
PLAYOS_AXIS_LEFT_TRIGGER,
PLAYOS_AXIS_RIGHT_TRIGGER,
PLAYOS_AXIS_COUNT
} playos_axis_t;
typedef struct {
playos_button_mask_t buttons;
float axes[PLAYOS_AXIS_COUNT];
} playos_controller_state_t;
Function expectations:
playos_input_controller_connected()playos_input_get_controller_state()
Rule: PLAYOS_BUTTON_SYSTEM and PLAYOS_BUTTON_QUICK_MENU are reserved identifiers. The backend may observe them on hardware, but game-facing snapshots must not report them once compositor interception exists.
Done when: the header compiles cleanly and the contract is specific enough for shell/game consumers.
S3-T6 — Implement the evdev prototype backend
- Identify the Ally controller event node(s).
- Map physical event codes to the public logical button and axis contract.
- Normalize sticks to
[-1.0, 1.0]. - Normalize triggers consistently and document whether they use
[0.0, 1.0]. - Ignore or reserve unsupported extra buttons for now, but document them in the mapping file.
Done when: a small test program can poll and print logical controller state on the Ally.
S3-T7 — Document the hardware mapping
- Add
docs/rog-ally-input-mapping.md. - Record Linux event codes, axis ranges, and any quirks.
- Mark which controls are public in MVP and which are deferred.
Done when: future shell/game work does not need to rediscover controller details experimentally.
S3-T8 — Capture physical hardware evidence
- Record boot success from USB.
- Record hardware check output.
- Record input test output with at least A/B/X/Y, D-pad, sticks, and triggers.
- Record any missing devices or kernel warnings.
Done when: the sprint leaves behind a reproducible bring-up record instead of memory-only claims.
Implementation Guidance
Defconfig naming and consistency
Use playos_ally_defconfig everywhere in docs, scripts, and Make targets. Do not introduce playos_rog_ally_defconfig as a second name.
Input contract stability
- Buttons are flags because multiple buttons may be pressed simultaneously.
- Axes are array-indexed because they are numeric channels, not bitfields.
- Avoid exposing kernel event codes directly in the public header.
Verification philosophy
This sprint is complete only when device presence is tied to a concrete script or command. "It seemed to work once" is not enough evidence.
Acceptance Criteria
-
playos_ally_defconfigexists and is used by the Ally build path - a USB image can be produced for the Ally
- the Ally boots PlayOS from removable media
- AMDGPU and DRM device nodes appear
- the controller appears through evdev and emits expected events
- audio hardware is visible and can play a short test sample
- NVMe, battery, and thermal information are visible
-
/run/playos/hw-check.logis produced by the verification tooling -
playos_input.hdefines a stable public input contract - button values are represented as bitmask flags
- the evdev backend prototype reads controller state on the Ally
- the hardware input mapping document is committed
Handoff to Sprint 4
Sprint 4 may assume:
- the Ally kernel and firmware stack can boot reliably
- the AMD GPU and DRM device nodes exist
- the public input ABI shape is known
- physical-hardware verification scripts already exist
Sprint 4 should consume this hardware baseline and focus on native compositor ownership of the display.
Exit Gate
A PlayOS USB image boots on physical ROG Ally hardware, essential devices enumerate correctly, and the first hardware-backed libplayos input API contract and evdev prototype are in place.
Previous: Sprint 2 | Next: Sprint 4
Sprint 3 Outcomes
Status: COMPLETE — all 8 tasks done, committed, and verified on physical ROG Ally hardware.
Deliverables
| Repo | Commits | Key Artifacts |
|---|---|---|
playos-refdistro | 9305481, d31ec48, 561b701, b6fbb69, e9b0c44, 65117f2 | Ally defconfig, kernel config, USB image script, flash script, hw-check tools |
playos-platform-api | d7a0050, 580026c | Input header, evdev backend, input mapping docs, API stubs |
Key Technical Decisions
-
EFI stub boot, not GRUB — kernel bzImage with embedded initramfs placed directly as
EFI/BOOT/BOOTX64.EFI. UEFI firmware boots the kernel without an intermediate bootloader. Simpler, faster, fewer dependencies. -
Embedded initramfs —
BR2_LINUX_KERNEL_INITRAMFS_SOURCEis critical. Without it the kernel panics because it has no rootfs. The 178MB cpio gzips to ~59MB inside the bzImage. -
No modules — all kernel drivers built-in (
# CONFIG_MODULES is not set). Simplifies the boot path — no module loading, no initramfs module discovery. -
BusyBox retained for debugging — production should strip it, but kept for Sprint 3 hardware verification (need a shell to run hw-check).
Lessons Learned
-
lsblkcolumns break on model names with spaces — "SanDisk 3.2Gen1" gets split into two columns. Uselsblk -P(key=value pairs) for reliable parsing. -
GPT backup header consumes disk space — partition sizes must account for ~34 sectors at end. Use
sgdisk -n N:0:0(fill remaining) for the last partition instead of fixed size. -
Buildroot
BR2_LINUX_KERNEL_INITRAMFS_SOURCEis easy to miss — the kernel compiles fine without it but panics at boot. Consider adding a post-build check that verifies initramfs is embedded. -
Ally boots reliably from USB via Volume Down + Power — no Secure Boot key enrollment needed (the Ally's UEFI has Secure Boot disabled by default).
-
SP5100 is the watchdog chip on ROG Ally — needs
CONFIG_SP5100_TCO, not generic iTCO. -
Kernel cmdline fallback matters —
CONFIG_CMDLINE="console=tty1 quiet loglevel=3"ensures boot works even when UEFI doesn't supply cmdline.
Sprint 4 — AMDGPU and Native DRM/KMS
Goal: Move playos-compositor from developer/test backends to the real native graphics path on the ROG Ally using AMDGPU, DRM/KMS, GBM, EGL, Mesa, and wlroots.
Primary Outcome: The compositor starts on the Ally, selects the correct GPU without hardcoded device paths, owns the built-in display through DRM/KMS, and presents a hardware-accelerated test client.
Prerequisites: Sprint 3 complete — the Ally boots reliably, AMDGPU loads, /dev/dri/ is populated, and physical hardware validation scripts exist.
Why This Sprint Exists
Sprint 4 converts the project from a simulated console UI pipeline into a real console graphics stack. It is the first sprint where PlayOS genuinely owns the handheld's screen as the future production system will.
Start Condition Checklist
- Sprint 3 Ally USB boot path works.
/dev/dri/card*and/dev/dri/renderD*appear on the device.- Headless and nested compositor modes from Sprint 2 still work.
- Mesa and libdrm can be built in the image.
Decisions Locked for This Sprint
- Canonical Ally defconfig name:
br2-external\configs\playos_ally_defconfig - Compositor owner:
playos-compositorpermanently owns DRM/KMS - GPU selection policy: enumerate and identify; never hardcode
/dev/dri/card0 - Renderer path: GBM + EGL + OpenGL ES through wlroots
- Fallback path: log failure and attempt the documented recovery graphics path; do not silently downgrade to a production-looking success state
Scope
In Scope
- native DRM/KMS backend selection
- GPU discovery and output selection
- GBM/EGL/Mesa renderer initialization
- physical display presentation on the Ally
- hardware-accelerated test client
- Buildroot dependency/config updates for native graphics
Explicitly Out of Scope
- real shell UI
- overlay lifecycle
- first-frame game foreground policy
- direct scanout as a release requirement
- Intel graphics support
Required Repository Changes
| Repo | Required work |
|---|---|
playos-compositor | Native DRM/KMS path, GPU discovery, output setup, renderer logging, test client updates |
playos-refdistro | Defconfig: enable BR2_PACKAGE_PLAYOS_COMPOSITOR + BR2_PACKAGE_MESA3D_GBM. Mesa/EGL/GLES/radeonsi already enabled from Sprint 3. wlroots/wayland come from Buildroot built-ins via compositor Config.in selects. |
playos-spec | Clarify graphics policy or ADRs only if implementation forces new decisions |
Expected Files and Directories
playos-compositor
src/
├── drm_backend.c
├── gpu_discovery.c
├── output_modes.c
├── renderer_gbm_egl.c
└── diagnostics.c
tools/test-client/
└── src/main.c
playos-refdistro
br2-external/configs/
└── playos_ally_defconfig ← add BR2_PACKAGE_PLAYOS_COMPOSITOR=y + BR2_PACKAGE_MESA3D_GBM=y
br2-external/package/playos-compositor/
└── playos-compositor.mk ← already cmake-package, no changes needed
Buildroot built-in packages used (no br2-external wrappers needed):
wlroots, wayland, wayland-protocols, libxkbcommon, pixman, mesa3d, libdrm
Agent Task Breakdown
Task Status Grid
Update the Status column as work progresses: not started → in progress → blocked or done.
| Task ID | Task | Primary repo | Status | Notes / evidence |
|---|---|---|---|---|
| S4-T1 | Add deterministic GPU discovery | playos-compositor | done | src/gpu_discovery.c — drmGetDevices2() enumeration, PCI vendor/device resolution, eDP/LVDS connector detection, render node selection. Priority: eDP+AMD > connected+AMD > first valid (ADR-0008) |
| S4-T2 | Bring up native DRM/KMS through wlroots | playos-compositor | done | src/drm_backend.c + src/output_modes.c — WLR_BACKENDS=drm, preferred mode selection, scale 1.0, wired into compositor_start lifecycle |
| S4-T3 | Initialise the GBM/EGL/Mesa rendering path | playos-compositor | done | src/renderer_gbm_egl.c — EGL pbuffer GL query, logs renderer/vendor/GLES version, detects software rendering (llvmpipe/softpipe/swrast) |
| S4-T4 | Present a hardware-accelerated test client | playos-compositor | done | tools/test-client/src/main.c — EGL/GLES2 rendering, animated color frame with moving accent bars (~60fps), GPU diagnostics in window title |
| S4-T5 | Add recovery and diagnostics behaviour | playos-compositor | done | src/diagnostics.c — logs to /run/playos/log/compositor.log, simpledrm fallback, phase-specific failure logging, mkdir -p /run/playos/log |
| S4-T6 | Update Buildroot graphics dependencies | playos-refdistro | done | playos_ally_defconfig: BR2_PACKAGE_MESA3D_GBM=y added. BR2_PACKAGE_PLAYOS_COMPOSITOR already present from Sprint 3 |
| S4-T7 | Preserve earlier test modes | playos-compositor, playos-refdistro | done | PLAYOS_BACKEND=headless|wayland|drm selection preserved. Headless test passes. Nested test skips gracefully. wlroots 0.17/0.20 API compat via WLR_VERSION macros. CMakeLists.txt updated for libdrm/EGL/GLES deps |
S4-T1 — Add deterministic GPU discovery
- Enumerate DRM devices.
- Resolve each candidate to vendor/device identity.
- Associate the selected device with the active built-in display connector.
- Select the matching render node for EGL.
Minimum recognised vendor IDs:
#define PCI_VENDOR_AMD 0x1002
#define PCI_VENDOR_INTEL 0x8086 /* not used for this sprint's target path */
Done when: logs show the selected card node, render node, vendor/device IDs, connector, and chosen mode.
S4-T2 — Bring up native DRM/KMS through wlroots
- Use wlroots with the native DRM backend on bare metal.
- Create the renderer and allocator for the chosen device.
- Enumerate outputs and bind to the built-in panel.
- Select the preferred mode.
Done when: the compositor can start on the Ally without using headless or nested backends.
S4-T3 — Initialise the GBM/EGL/Mesa rendering path
- Use GBM for buffers.
- Use EGL/OpenGL ES through Mesa.
- Log the renderer name and supported GLES version.
- Fail clearly if hardware acceleration is not active.
Done when: the compositor reports a working accelerated renderer on the Ally.
S4-T4 — Present a hardware-accelerated test client
- Update the Sprint 2 test client to render through EGL on Wayland.
- Show a visible moving or changing frame, not a single static colour.
- Display useful diagnostics on screen if practical: PlayOS, sprint number, GPU name, resolution, refresh rate.
Done when: the Ally screen shows an actively rendered client surface driven by the compositor.
S4-T5 — Add recovery and diagnostics behaviour
- If DRM/KMS init fails, log the failing phase clearly.
- Attempt the documented fallback path if one is available in the current build.
- If fallback also fails, halt with a clear diagnostic path.
Done when: simulated or induced failure produces actionable logs instead of a silent black screen.
S4-T6 — Update Buildroot graphics dependencies
- Already done (Sprint 3): Mesa3D with radeonsi gallium driver (
BR2_PACKAGE_MESA3D_GALLIUM_DRIVER_RADEONSI), OpenGL EGL (BR2_PACKAGE_MESA3D_OPENGL_EGL), OpenGL ES (BR2_PACKAGE_MESA3D_OPENGL_ES), and Vulkan AMD driver are enabled inplayos_ally_defconfig. - Remaining: Enable
BR2_PACKAGE_MESA3D_GBM(GBM buffer allocation) and addBR2_PACKAGE_PLAYOS_COMPOSITOR=yto the defconfig. The compositor'sConfig.inalreadyselects wlroots, wayland, wayland-protocols, libxkbcommon, and pixman — Buildroot's built-in packages provide these (no br2-external packages needed). - Compositor .mk status: Already a cmake-package (not a stub). Builds from
$(BR2_EXTERNAL_PlayOS_PATH)/../src/playos-compositorwith wlroots 0.20 dependencies. Tested in Sprint 2 for headless/nested modes. - Validate musl compatibility with the full graphics stack.
Done when: playos_ally_defconfig includes BR2_PACKAGE_PLAYOS_COMPOSITOR=y and BR2_PACKAGE_MESA3D_GBM=y, the Ally image contains all runtime libraries, and the compositor binary builds and starts on-device.
S4-T7 — Preserve earlier test modes
- Do not break headless QEMU validation.
- Do not break nested Wayland developer validation.
- Keep backend selection explicit and logged.
Done when: the project still supports fast non-device iteration after native graphics lands.
Implementation Guidance
Output selection
- Prefer the panel reported as the built-in/internal connector.
- Apply the preferred mode first.
- Set output scale to
1.0for now. - External display hotplug may be logged only; no multi-display UX is required yet.
Logging
Log at minimum:
- backend mode
- selected GPU/card/render node
- connector name
- selected mode and refresh rate
- renderer name
- GLES version
- any fallback path entered
Write logs to /run/playos/log/compositor.log.
Test client expectations
The sprint acceptance target is not the future shell. It is a diagnostic client. Keep it simple, deterministic, and useful for proving the rendering path.
Verification and Evidence
| Evidence | How it is produced |
|---|---|
| GPU selection proof | compositor log entries for card, render node, PCI IDs |
| Output proof | compositor log entries for connector and selected mode |
| Acceleration proof | renderer and GLES version in logs |
| On-screen proof | visible animated test client on the Ally screen |
| Regression proof | QEMU headless path still runs after native DRM work |
Acceptance Criteria
-
playos-compositorstarts on the Ally using the native DRM backend (verified on-device: eDP-1, amdgpu, DRM/KMS, 1920×1080@120Hz) -
GPU discovery is based on enumeration, not hardcoded
/dev/dri/card0 - the built-in display connector is identified and configured
- the compositor log records the selected GPU, render node, connector, mode, renderer, and GLES version
- a hardware-accelerated test client is visible on the Ally screen (verified on-device: animated bars at 119.8 fps, Ryzen Z1 Extreme, GLES 3.2)
- the renderer path is GBM + EGL + Mesa on AMDGPU
- an induced or simulated DRM init failure produces clear diagnostics and fallback behaviour
-
QEMU headless validation still works (verified:
compositor-headless-testpasses) -
nested Wayland validation still works (verified:
compositor-nested-testskips gracefully without WAYLAND_DISPLAY)
Handoff to Sprint 5
Sprint 5 may assume:
- the compositor can own the real display on the Ally
- the graphics stack is hardware accelerated
- a visible Wayland client can render on-device
- backend selection and logging are already mature enough for shell bring-up
Sprint 5 should focus on replacing the test client with the real shell, not revisiting DRM fundamentals.
Exit Gate
playos-compositor initializes AMDGPU via DRM/KMS on the ROG Ally, owns the built-in display, and presents a hardware-accelerated diagnostic client without breaking existing headless and nested workflows.
Previous: Sprint 3 | Next: Sprint 5
Sprint 5 — Raylib-Powered PlayOS Shell
Goal: Build playos-shell as a hardware-accelerated, controller-first Raylib Wayland client that runs persistently under playos-compositor and consumes the public libplayos API surface needed for shell UX.
Primary Outcome: The ROG Ally boots into a visible shell UI that shows a stub game library, responds to controller navigation, and remains alive as the persistent PlayOS foreground experience.
Status: 🟢 Complete — delivered; Raylib backend landed via Sprint 5.5 (see below)
Prerequisites: Sprint 4 complete and verified on-device — the compositor owns the real display on the Ally at 1920×1080@120Hz, the EGL/GLES2 test client renders at 119.8 fps with visible animated bars. Sprint 3 complete — the public input ABI is finalized (bit positions resolved), evdev backend is implemented, and the playos-platform-api headers are in place with stub implementations.
Why This Sprint Exists
Sprint 5 is the first real user-facing PlayOS sprint. Everything before it proves build, runtime, hardware, and graphics foundations. This sprint proves that those foundations are sufficient to host the persistent console shell that defines the product experience.
Start Condition Checklist
- Sprint 4 native DRM/KMS path works on the Ally — verified: eDP-1 @ 1920×1080@120Hz, amdgpu, GLES 3.2 on Ryzen Z1 Extreme.
playos-compositorcan present a diagnostic client on real hardware — verified: test client rendered animated orange bars at 119.8 fps.playos-platform-apialready has all 8 public headers declared, the Sprint 3 input contract is finalized (bit positions fixed, evdev backend implemented —src/backends/backend_evdev.cat 12KB), and stubs exist for system/storage/lifecycle/logging.- The Sprint 3 critical review finding (input header bit position mismatch) has been resolved — current
playos_input.hmatches the spec. - The shell can rely on a working Wayland session (
wayland-0at/run/playos) and hardware-accelerated rendering path. - The compositor scene is pre-configured: dark blue
#0a1628background rect at layer bottom, xdg surfaces placed at (0,0) at top. The shell is the sole xdg client — no scene changes needed in the compositor for this sprint.
Decisions Locked for This Sprint
- Language: C99
- UI framework: Raylib
- Windowing model: one fullscreen shell surface only
- Input model: controller-first; mouse and keyboard are developer-only aids, not product requirements
- Backend ownership: the custom Raylib PlayOS backend is maintained by
playos-shell;playos-platform-apiprovides the public API consumed by the shell. Exception for input: the shell needs SYSTEM/QUICK_MENU button access, whichlibplayosinput API strips (those buttons are reserved, never delivered to game processes). The shell reads controller input directly through the evdev backend provided byplayos-platform-apior through a future trusted compositor protocol. - Library content source for this sprint: stub manifests in
/data/games/(retrieved viaplayos_storage_get_games_path()) - Launch behaviour: the shell may issue a stub launch request or present a placeholder transition, but a full playable game lifecycle is deferred to Sprint 7
- Battery UI: no real power API dependency yet; use placeholder or omit battery if the real power contract is not available
- Wayland display:
wayland-0atXDG_RUNTIME_DIR=/run/playos(matching the current compositor setup from Sprint 4) - Logging: persistent logs at
/data/log/shell.logfollowing the Sprint 4child_log_redirect()pattern established insupervisor.c
Scope
In Scope
- custom Raylib backend for the PlayOS Wayland environment
- shell application structure and screen flow
- public API groups needed by the shell this sprint
- controller navigation
- stub game discovery from
/data/games/ - Buildroot packaging for Raylib and the shell
- physical-hardware visual validation on the Ally
Explicitly Out of Scope
- full game launch/resume/background lifecycle
- overlay UI
- real save-management UX
- network/store/account features
- real battery/power management policy
- installer/update flows
Required Repository Changes
| Repo | Required work |
|---|---|
playos-shell | Raylib shell app, custom PlayOS backend integration, screens, controller navigation |
playos-platform-api | Headers already exist. Remaining work: add playos_storage_get_games_path(), implement the 4 stubs (system/storage/lifecycle/logging) with real backends, hand off to refdistro for libplayos packaging |
playos-refdistro | Raylib packaging, playos-shell packaging (replace no-op stub with real cmake-package), stub content under /data/games/, update supervisor.c to launch playos-shell instead of playos-test-client |
playos-spec | shell UX conventions and any clarified shell/runtime contract notes |
Expected Files and Directories
playos-shell
CMakeLists.txt
include/
└── shell.h
src/
├── main.c
├── shell.c
├── navigation.c
├── library_model.c
├── launcher_stub.c
├── lifecycle.c
├── input.c
├── ui/
│ ├── screen_library.c
│ ├── screen_game_detail.c
│ └── status_bar.c
└── platforms/
└── rcore_playos.c
assets/
├── fonts/
├── icons/
└── audio/
playos-platform-api
All headers and source stubs already exist on disk. Sprint 5 adds one new function (playos_storage_get_games_path) and implements the stubs:
include/playos/
├── playos_storage.h ← EXTEND: add get_games_path() declaration
src/
├── playos_system.c ← IMPLEMENT: real sysinfo from /proc + sysfs
├── playos_storage.c ← IMPLEMENT: real paths + get_games_path()
├── playos_lifecycle.c ← IMPLEMENT: real event-fd from IPC
└── playos_logging.c ← IMPLEMENT: real structured logging to file
playos-refdistro
br2-external/package/
├── raylib/ ← NEW: vendored raylib from playos-shell source
└── playos-shell/ ← REPLACE: current no-op stub → real cmake-package
br2-external/board/common/rootfs-overlay/
└── data/games/
├── com.playos.demo1/manifest.json
├── com.playos.demo2/manifest.json
└── com.playos.demo3/manifest.json
src/playos-init/src/
└── supervisor.c ← UPDATE: launch playos-shell instead of playos-test-client
Agent Task Breakdown
Task Status Grid
Update the Status column as work progresses: not started → in progress → blocked or done.
| Task ID | Task | Primary repo | Status | Notes / evidence |
|---|---|---|---|---|
| S5-T1 | Finalise the shell-facing public API surface | playos-platform-api | done | get_games_path() added; system/storage/lifecycle/logging stubs implemented |
| S5-T2 | Add the custom Raylib PlayOS backend | playos-shell | done | First shipped raw EGL/GLES2; Raylib 6.0 rcore_playos.c landed via Sprint 5.5 (1046262) |
| S5-T3 | Bootstrap the shell application structure | playos-shell | done | src/{main,input,render_util,screen_*}.c + include/shell.h |
| S5-T4 | Implement library data loading from stub manifests | playos-shell, playos-refdistro | done | Manifests discovered from /data/games/ via playos_storage_get_games_path() |
| S5-T5 | Implement controller-first navigation and focus rules | playos-shell | done | Shell-owned direct evdev (input.c); reserved buttons preserved |
| S5-T6 | Build the library, detail, and status-bar UI | playos-shell | done | Library + Game Detail (plus Home + Settings screens added) |
| S5-T7 | Add shell lifecycle handling and persistent process behavior | playos-shell, playos-platform-api | done | playos_lifecycle_poll() per frame; persists under supervision |
| S5-T8 | Integrate Raylib and shell packaging into Buildroot | playos-refdistro | done | Real cmake-package; PLAYOS_SHELL_USE_RAYLIB=ON (Raylib 6.0) |
| S5-T9 | Add validation, stub content, and runtime evidence capture | playos-shell, playos-refdistro | done | Validated in QEMU and on the ROG Ally |
S5-T1 — Finalise the shell-facing public API surface
Note: All 8 header files already exist with full declarations. The evdev input backend at
src/backends/backend_evdev.c(12KB) is already implemented. Source files exist as stubs (return NULL/0/-1). This task is about extending with one missing function and implementing the stubs for Sprint 5's minimum needs.
Update playos_storage.h — add the one missing function:
/**
* Read-only path to the game library directory.
* e.g. /data/games/
*
* Unlike per-game path functions, this does not require
* PLAYOS_GAME_ID — it is intended for the shell process
* which is not a game and does not carry a game ID.
*
* @return Null-terminated path string.
*/
const char *playos_storage_get_games_path(void);
This function is needed because the shell is NOT a game — it doesn't have PLAYOS_GAME_ID set. The existing storage API assumes the caller is a game process. The shell needs get_games_path() to discover installed game manifests.
Implement the 4 stubs for Sprint 5:
| Function | Implementation target |
|---|---|
playos_system_*() | Read from /proc/cpuinfo, /proc/meminfo, /sys/class/drm/; hardcode device model |
playos_storage_*() | Base paths from /data/games/, /data/saves/, /data/cache/; free_bytes via statvfs |
playos_lifecycle_*() | Create private eventfd; wire to IPC from playos-init when available, poll-only stub for now |
playos_log_*() | Write to file under /run/playos/log/ with timestamp; crash_marker via sync() + write() |
The type names used in the actual headers differ from the draft in this spec — use the actual types already declared:
PlayOSLifecycleEvent(notplayos_lifecycle_event_t)PlayOSLogLevel(notplayos_log_level_t)playos_storage_get_saves_path(void)andplayos_storage_get_cache_path(void)— nogame_idparameter (game isolation is viaPLAYOS_GAME_IDenv var)
Done when: the shell can compile and link against libplayos.so for system, storage, lifecycle (poll-only), and logging needs.
S5-T2 — Add the custom Raylib PlayOS backend
Create src/platforms/rcore_playos.c in playos-shell.
The backend must:
- create a Wayland fullscreen surface
- create an EGL context
- integrate frame pacing with Wayland frame callbacks
- feed controller state from
playos_input_get_controller_state() - cooperate with the PlayOS compositor environment instead of desktop assumptions
Desktop-only features must be disabled or become no-ops:
- window decorations
- free resize
- drag and drop
- clipboard
- multi-window support
Done when: a simple Raylib frame can be drawn through the custom backend on the Ally.
S5-T3 — Bootstrap the shell application structure
-
Add a central
struct playos_shell. -
Add screen/state enums.
-
Add a main loop that separates:
- event/input polling
- state update
- draw
-
Ensure startup and shutdown paths are explicit.
Done when: the shell launches, draws a frame, and exits cleanly under developer control.
S5-T4 — Implement library data loading from stub manifests
-
Read stub game entries from
/data/games/. -
Parse minimal
manifest.jsondata for:- game id
- display name
- version
- optional description
- optional icon path
-
Use placeholder visuals when optional data is absent.
Done when: the shell can load and display three deterministic stub entries.
S5-T5 — Implement controller-first navigation and focus rules
- D-pad moves selection
Aconfirms/selectsBreturns/back- focus must always remain visible and unambiguous
- no mouse is required for normal operation
⚠️ Trusted vs untrusted input: The shell is a trusted system component — it needs the SYSTEM (Xbox Guide / Ally Armoury Crate) and QUICK_MENU (Ally Command Center) buttons. The public
playos_input_get_controller_state()function strips these reserved buttons before returning (since games must never see them). The shell must either:
- Link the evdev backend directly (bypassing
libplayosfor input), or- Consume input through a compositor protocol (e.g., the future
session_managerprotocol inplayos-v1.xml)For Sprint 5, option 1 (direct evdev) is the pragmatic path — the shell already runs on the same system as the evdev input backend and can access
/dev/input/directly. Long term, a privileged input protocol in the compositor is preferred.
Recommended first screen flow:
Library -> Game Detail -> Library
Done when: a user can navigate into and out of the detail screen with controller input only.
S5-T6 — Build the library, detail, and status-bar UI
Required UI surfaces for this sprint:
-
Library screen
- scrollable grid or list of installed games
- visible focus state
- placeholder icon support
-
Game detail screen
- game name
- description
- version
- launch/select affordance (may be stubbed)
-
Status bar
- PlayOS version
- clock
- optional placeholder battery field if clearly marked as non-final
Done when: the shell renders a coherent controller-first UI rather than a single diagnostic frame.
S5-T7 — Add shell lifecycle handling and persistent process behavior
-
Poll lifecycle events through the public API if available.
-
At minimum, define and handle:
- foreground
- background
- terminate
-
When backgrounded, reduce or skip rendering work.
-
When foregrounded, resume normal rendering.
-
On terminate, exit cleanly.
-
The shell must be written as a persistent supervised process, not a one-shot demo app.
Done when: lifecycle state changes affect rendering behaviour predictably and the shell remains compatible with supervision.
S5-T8 — Integrate Raylib and shell packaging into Buildroot
Current state: br2-external/package/playos-shell/ exists as a no-op stub (build/install both @true).
- Rewrite
playos-shell.mkas a realcmake-packagebuilding from$(BR2_EXTERNAL_PlayOS_PATH)/../src/playos-shell. - Add Raylib with the custom PlayOS backend. Strategy: Create a vendored Raylib source in
playos-shellrather than patching upstream — thercore_playos.cbackend replaces core platform code (rcore_desktop.c/rcore_desktop_glfw.c) and is tightly coupled to the PlayOS compositor. A Buildroot package references this vendored source. - Ensure the shell depends on
libplayos. - Update
supervisor.cto launch/usr/bin/playos-shellinstead of/usr/bin/playos-test-clientafter the compositor is ready. - Log shell output to
/data/log/shell.logusing the existingchild_log_redirect()helper (same pattern as test-client logging from Sprint 4).
Done when: the Ally image boots into the shell automatically.
S5-T9 — Add validation, stub content, and runtime evidence capture
- Install three stub manifests under
/data/games/. - Add runtime logging on startup, shutdown, navigation, and lifecycle transitions.
- Validate shell startup on the Ally.
- Validate non-crashing startup in the QEMU/dev path, even if visual output is limited there.
Done when: the sprint has demonstrable content and logs proving the shell path works.
Implementation Guidance
Shell UX rules
- Always favour immediate controller clarity over dense visuals.
- Do not introduce nested menus beyond the single detail screen in this sprint.
- Keep animation and visual effects light until the shell baseline is stable.
Compositor integration notes
The compositor scene from Sprint 4 is pre-configured:
- Background: a dark blue (
#0a1628)wlr_scene_rectat the layer bottom of each output's scene tree. - Client surfaces: xdg toplevel surfaces are wrapped in a
wlr_scene_xdg_surface_create()tree, positioned at (0,0), and raised to top. - Render loop:
handle_framecommits the scene output viawlr_scene_output_commit()and sendsframe_done.
No compositor changes are needed for Sprint 5 — the shell is a normal xdg Wayland client. The shell should connect to wayland-0 at XDG_RUNTIME_DIR=/run/playos (the same Wayland session the test client currently uses).
Manifest format
For Sprint 5, keep the manifest intentionally small. A minimal JSON shape is enough:
{
"id": "com.playos.demo1",
"name": "Demo One",
"version": "0.1.0",
"description": "Stub entry used for shell bring-up."
}
Note: The shell discovers manifests from /data/games/ using playos_storage_get_games_path(). This function is added to playos-platform-api in S5-T1. The shell is not a game process, so it does not have PLAYOS_GAME_ID set and must use this shell-specific discovery API rather than the per-game path functions.
Lifecycle scope
This sprint may consume placeholder lifecycle events needed to keep the shell architecture clean, but it must not depend on the full console game/background/resume model from Sprint 7 being complete.
Logging
At minimum log:
- shell startup
- backend initialization success/failure
- manifest load count
- selection changes
- screen transitions
- lifecycle transitions
Log to /data/log/shell.log using the persistent USB logging approach established in Sprint 4 (child_log_redirect() in supervisor.c). Local dev testing can also write to /run/playos/log/shell.log.
Verification and Evidence
| Evidence | How it is produced |
|---|---|
| UI boot proof | photo/video or direct observation of the shell on the Ally |
| Data-loading proof | shell log showing three manifests loaded |
| Navigation proof | log or captured session showing controller-driven focus changes |
| API proof | successful compile/link against public libplayos headers |
| Persistence proof | shell remains alive under supervision during idle runtime |
| Regression proof | shell starts in the non-device path without crashing |
Acceptance Criteria
- the Ally boots into the shell UI automatically
- the shell is rendered through the custom Raylib PlayOS backend
-
the shell links against documented public
libplayosAPIs only -
three stub game entries are loaded from
/data/games/ - controller-only navigation works on the library and detail screens
-
Aenters the detail screen andBreturns to the library - the shell logs startup, navigation, and lifecycle events
- the shell remains alive under supervision during an idle run
-
Buildroot packaging integrates Raylib and
playos-shell - the non-device startup path remains usable for developer iteration
Handoff to Sprint 6
Sprint 6 may assume:
- the shell exists as the persistent UI process
- the Raylib PlayOS backend is real
- shell navigation and rendering fundamentals are stable
- a basic content-loading path from
/data/games/already exists
Sprint 6 should deepen storage and real discovery behaviour rather than rebuilding shell fundamentals.
Exit Gate
The ROG Ally boots directly into a persistent Raylib shell that renders through the PlayOS backend, loads stub game entries, and supports controller-first navigation using the public libplayos API surface.
Previous: Sprint 4 | Next: Sprint 6
Sprint 5.5 — Shell → Raylib 6.0 Migration
Goal: Resolve the divergence between Sprint 5's specified rendering framework (Raylib, per ADR-0006 and playos-shell-spec.md) and the actual shell implementation (raw EGL/GLES2 with a hand-rolled shader + bitmap font). Upgrade the vendored Raylib from 5.5 to 6.0, implement the custom rcore_playos.c platform backend against the 6.0 backend contract, and port the shell's rendering to the Raylib draw API — without changing the shell's UX, input model, or lifecycle behaviour.
Primary Outcome: The ROG Ally boots into the same four-screen, controller-first shell, but every frame is now drawn through Raylib 6.0 via a custom Wayland/EGL platform backend. The raw-GLES2 renderer in render_util.c is retired. PLAYOS_SHELL_USE_RAYLIB=ON is the Buildroot default, and Raylib 6.0 is pinned in versions.lock.
Status: 🟢 Complete — Raylib 6.0 shell verified in QEMU and on the ROG Ally
Prerequisites: Sprint 5 complete — the shell renders and navigates on the Ally through the raw EGL/GLES2 path (4 screens, controller-first input, frame-callback vsync, lifecycle handling, per-call trusted IPC). Raylib 5.5 is vendored at playos-shell/external/raylib/ but unused. The custom backend contract is specified in ADR-0006.
Why This Sprint Exists
Sprint 5 ended in a spec/implementation divergence that, left unaddressed, will compound through Sprint 6 (storage/game discovery), Sprint 7 (launch/lifecycle), and Sprint 8 (overlay):
- Spec says Raylib; code says raw GLES2.
playos-shell-spec.mdand ADR-0006 commit to a Raylib shell with a customrcore_playos.cbackend. The implementation deferred Raylib and instead wrote a bespoke single-shader GLES2 renderer plus a hand-embedded 5×7 bitmap font inrender_util.c. - The vendored Raylib is stale and inert.
playos-shell/external/raylib/vendors Raylib 5.5, butPLAYOS_SHELL_USE_RAYLIBisOFFin bothCMakeLists.txtand the Buildroot package (playos-shell.mk). Theversions.lockentriesRAYLIB_COMMIT/RAYLIB_SOURCEare empty. - Raylib 6.0 changes the platform backend contract. The backend split introduced in Raylib 5.0 was reworked in 6.0, along with module-level compile toggles and symbol renames/removals. The custom PlayOS backend must be written against 6.0, not 5.5.
- A hand-rolled renderer becomes a maintenance trap. The raw-GLES2 path (custom shaders, bitmap font, manual text metrics) must be re-implemented in Raylib eventually anyway. Migrating now, before Sprint 6/7 build on the rendering path, avoids double work and keeps the shell aligned with ADR-0006's "Raylib for shell, overlay, and games" decision.
This is a pure migration sprint — no new screens, no new features, no UX changes. The user-visible result must be identical (or strictly better) to Sprint 5.
Start Condition Checklist
- Sprint 5 shell renders and navigates on the Ally via raw EGL/GLES2. (Verified at Sprint 5 exit gate.)
-
Raylib 5.5 is vendored at
playos-shell/external/raylib/and builds (or thePLAYOS_SHELL_USE_RAYLIBpath is understood). -
versions.lockhas emptyRAYLIB_COMMIT/RAYLIB_SOURCEentries ready to be filled. - Raylib 6.0 release source/tag is available to vendor (exact commit SHA obtainable).
- Nested Wayland dev environment and QEMU headless path are usable for iteration.
Decisions Locked for This Sprint
- Raylib version: 6.0, vendored into
playos-shell/external/raylib/and pinned with a full commit SHA inversions.lockunderRAYLIB_COMMIT. - Backend: a custom
rcore_playos.cplatform backend (not GLFW/SDL backends). It owns the fullscreenxdg_toplevel,wl_egl_window, EGL/GLES2 context, and frame-callback vsync — the same primitivesmain.ccurrently manages directly. - Raylib is rendering-only. Input remains shell-owned direct evdev (
input.c) so the reserved SYSTEM/QUICK_MENU buttons are preserved. Raylib's gamepad abstraction is not used for navigation because it strips reserved buttons. - Module stripping: disable unused Raylib subsystems via
config.hSUPPORT_MODULE_*toggles (audioraudio, modelsrmodels, camerarcamera, and networking if present), keepingrcore,rlgl,rshapes,rtext, andrtextures. This avoids conflicts with the Sprint 8 ALSA stack and keeps the shell binary small. - Font: adopt Raylib's default font (
GetFontDefault()); remove the embedded 5×7 bitmap font fromrender_util.c. - No new features. UX, screens, navigation, and lifecycle behaviour are unchanged from Sprint 5.
Scope
In Scope
- Upgrade vendored Raylib 5.5 → 6.0 and pin it in
versions.lock - Reconcile Raylib 6.0 breaking changes (platform backend contract, module flags, renamed/removed symbols)
- Implement
rcore_playos.cagainst the Raylib 6.0 backend contract - Port rendering from raw GLES2 (
render_util.c) to the Raylib draw API across all four screens - Wire shell-owned evdev input and lifecycle polling into the Raylib frame loop
- Buildroot packaging: flip
PLAYOS_SHELL_USE_RAYLIB=ON, add the vendored Raylib dependency - Spec/doc reconciliation (
playos-shell-spec.md,playos-shell/AGENTS.md, ADR-0006 note) - Validation in the nested Wayland dev path, QEMU headless path, and on the Ally
Explicitly Out of Scope
- New screens, navigation flows, or UX changes
- Game launch/resume/background lifecycle (Sprint 7)
- Overlay UI (Sprint 8)
- Audio via Raylib
raudio(Sprint 8 uses ALSA) - 3D/model rendering (
rmodelsis stripped) - Intel graphics expansion (Sprint 13)
- Store/network/account features
Required Repository Changes
| Repo | Required work |
|---|---|
playos-shell | Vendor Raylib 6.0, implement rcore_playos.c, port render_util.c + screen_*_draw() to the Raylib API, wire input/lifecycle, flip PLAYOS_SHELL_USE_RAYLIB, update AGENTS.md |
playos-refdistro | Pin RAYLIB_COMMIT in versions.lock, flip playos-shell.mk to USE_RAYLIB=ON, ensure the vendored Raylib builds/links in the Buildroot image |
playos-spec | Add Sprint-5.5.md, update playos-shell-spec.md, add ADR-0006 follow-up note, update cross-links |
Expected Files and Directories
playos-shell (changed/added)
external/raylib/
├── src/raylib.h ← UPDATE: RAYLIB_VERSION "6.0"
└── src/platforms/
└── rcore_playos.c ← NEW: custom PlayOS Wayland/EGL backend (6.0 contract)
src/
├── main.c ← UPDATE: delegate surface/EGL to the raylib backend
├── input.c ← UNCHANGED: shell-owned direct evdev (trusted reserved buttons)
├── render_util.c ← REWRITE: thin Raylib wrappers (no raw GLES2 shader/font)
├── screen_home.c ← UPDATE: draw via Raylib API
├── screen_library.c ← UPDATE: draw via Raylib API
├── screen_game_detail.c ← UPDATE: draw via Raylib API
└── screen_settings.c ← UPDATE: draw via Raylib API
CMakeLists.txt ← UPDATE: build Raylib 6.0 with minimal SUPPORT_MODULE_* config
playos-refdistro
versions.lock ← UPDATE: RAYLIB_COMMIT=<sha>, RAYLIB_SOURCE confirmed
br2-external/package/playos-shell/
└── playos-shell.mk ← UPDATE: -DPLAYOS_SHELL_USE_RAYLIB=ON
playos-spec
src/playos-shell-spec.md ← UPDATE: reflect Raylib 6.0 + rcore_playos.c
src/adr/ADR-0006-*.md ← ADD: follow-up note (or new superseding ADR) if the decision changed
src/sprints/Sprint-5.5.md ← NEW: this document
Agent Task Breakdown
Every task below is independently checkable.
Task Status Grid
| Task ID | Task | Primary repo | Status | Notes / evidence |
|---|---|---|---|---|
| S5.5-T1 | Upgrade vendored Raylib 5.5 → 6.0 and pin it | playos-shell, playos-refdistro | done | RAYLIB_COMMIT=dbc56a87 (6.0) in versions.lock |
| S5.5-T2 | Reconcile Raylib 6.0 breaking changes | playos-shell | done | Migration list applied |
| S5.5-T3 | Implement rcore_playos.c platform backend (6.0) | playos-shell | done | Custom Wayland/EGL backend; non-blocking render loop (1046262) |
| S5.5-T4 | Port rendering from raw GLES2 to Raylib draw API | playos-shell | done | render_util.c helpers map to Raylib draw API |
| S5.5-T5 | Wire controller input (rendering-only Raylib) | playos-shell | done | input.c evdev unchanged; no Raylib gamepad |
| S5.5-T6 | Integrate lifecycle polling into the Raylib frame loop | playos-shell | done | playos_lifecycle_poll() per frame |
| S5.5-T7 | Buildroot packaging: flip to Raylib 6.0 | playos-refdistro | done | USE_RAYLIB=ON; libraylib.so.6.0.0 in image |
| S5.5-T8 | Spec and docs reconciliation | playos-spec, playos-shell | done | Sprint doc updated |
| S5.5-T9 | Validation and runtime evidence | playos-shell, playos-refdistro | done | QEMU validated; Ally on-device re-test passed |
S5.5-T1 — Upgrade Vendored Raylib 5.5 → 6.0 and Pin It
Finding: playos-shell/external/raylib/ vendors Raylib 5.5 (RAYLIB_VERSION "5.5" in src/raylib.h). versions.lock has RAYLIB_COMMIT= and RAYLIB_SOURCE=https://github.com/raysan5/raylib but no pinned commit. PLAYOS_SHELL_USE_RAYLIB is OFF everywhere.
Steps:
- Replace the vendored source. Swap
external/raylib/with the Raylib 6.0 release (exact tag or commit), keeping the repo's vendoring layout intact. - Pin it. Set
RAYLIB_COMMIT=<full sha>inplayos-refdistro/versions.lock(confirmRAYLIB_SOURCEis correct). Do not use a branch orlatest. - Update the CMake integration. In
playos-shell/CMakeLists.txt, adjust theadd_subdirectory(external/raylib)path and the library target name if Raylib 6.0 renamed its CMake target (e.g.,raylibvsraylib_playos). KeepPLAYOS_SHELL_USE_RAYLIBas the gating option. - Apply a minimal module config. Configure
config.hSUPPORT_MODULE_*toggles to strip unused subsystems (see Decisions). This keeps the vendored build lean and avoids pulling inraudio/rmodelsdeps. - Standalone build check. Build the vendored Raylib alone (
cmake -B build && cmake --build build) to confirm it compiles with the stripped config before touching the shell.
Done when:
external/raylib/src/raylib.hreportsRAYLIB_VERSION "6.0".versions.lockhas a non-emptyRAYLIB_COMMIT.- The vendored Raylib builds standalone with the minimal module config.
S5.5-T2 — Reconcile Raylib 6.0 Breaking Changes
Finding: Raylib 6.0 reworks the platform-backend contract introduced in 5.0, adds module-level compile toggles, and renames/removes a number of symbols. The shell cannot adopt 6.0 blindly — it must know exactly what changed between 5.5 and 6.0.
Steps:
- Diff 5.5 → 6.0. Read the upstream
CHANGELOG(and the platform-backend headers) and produce a concrete reconciliation list covering:- platform backend entry points /
PLATFORM_*contract changes config.hflag renames and newSUPPORT_MODULE_*toggles- renamed/removed public symbols (window, input, drawing, text, math)
- GLES2 /
rlglrenderer API changes relevant to the custom backend
- platform backend entry points /
- Apply the list. Update any shell references (current or planned) that use a renamed/removed 5.5 symbol. Since Raylib is not yet wired into the shell, this mostly informs T3/T4, but any
external/integration glue is corrected here. - Record it. Capture the final list in this sprint's evidence (or an ADR-0006 follow-up note) so Sprint 6/7 do not re-discover the same changes.
Done when:
- A reviewed 5.5 → 6.0 breaking-change list exists.
- No shell or integration code references a removed 5.5-only symbol.
S5.5-T3 — Implement the rcore_playos.c Platform Backend (Raylib 6.0)
Finding: ADR-0006 mandates a custom Raylib backend that integrates with the PlayOS Wayland/EGL environment and the libplayos lifecycle API. It must now be written against the 6.0 backend contract in src/platforms/rcore_playos.c.
Steps:
- Implement
rcore_playos.cagainst Raylib 6.0's platform backend contract:- create a fullscreen
xdg_toplevel(reuse the shell'sxdg_wm_base/wl_compositorsetup) - create a
wl_egl_window+ EGL/GLES2 context and make it current - pace frames with Wayland frame callbacks (
wl_surface_frame) and present witheglSwapBuffers
- create a fullscreen
- Disable desktop features as no-ops: window decorations, free resize, drag-and-drop, clipboard, multi-window.
- Wire the trusted-shell registration. Keep the
playos_manager_v1"register as trusted shell" + ShellReady handshake frommain.c, now invoked through/alongside the backend init. - Expose dimensions. Ensure
GetScreenWidth()/GetScreenHeight()reflect the toplevel configure size (the shell'sdpi_scaleconvention can map onto Raylib scaling). - Trim
main.c. Remove the now-duplicated direct EGL/wl_surface/frame-callback management that the backend owns.
Done when:
- A Raylib frame draws through
rcore_playos.cin the nested Wayland dev environment. main.cno longer manages the EGL surface/context directly.
On-device fix (frame pacing): The first two Ally bring-ups froze in the frame loop — the shell logged no FPS lines and the screen either showed only the compositor's background (frame 1) or froze right after the first painted frame (frame 2). Root cause: SwapScreenBuffer() blocked on a wl_surface_frame callback whose associated commit could never be presented by this compositor — arming the callback before eglSwapBuffers left the surface unmapped on frame 1, and arming it after (without a commit) left the callback unarmed on frame 2. Either way wl_display_dispatch blocked forever, stalling the main loop (and therefore evdev input + the FPS counter).
Final fix (playos-shell@1046262): drop wl_surface_frame pacing entirely and mirror the proven test-client render loop — eglSwapInterval(0) keeps eglSwapBuffers non-blocking, and PollInputEvents() pumps Wayland events each frame with wl_display_dispatch_pending + wl_display_flush. The compositor remains the presentation authority. Validated in QEMU (main loop advances, FPS lines emitted) and ready for Ally re-test.
S5.5-T4 — Port Rendering from Raw GLES2 to the Raylib Draw API
Finding: render_util.c implements a single-shader GLES2 renderer plus an embedded 5×7 bitmap font. The four screen_*_draw() functions call render_draw_rect() / render_draw_text(). These must map onto Raylib.
Steps:
- Map each
render_*helper to Raylib:
| Current helper | Raylib equivalent |
|---|---|
render_init(w, h) | backend window/context init (T3) |
render_begin_frame(r,g,b,a) | BeginDrawing() + ClearBackground() |
render_draw_rect(x,y,w,h,r,g,b,a) | DrawRectangle() / DrawRectangleRec() |
render_draw_text(text,x,y,scale,r,g,b,a) | DrawTextEx() |
render_end_frame(s) | EndDrawing() |
render_screen_dims(&w,&h) | GetScreenWidth() / GetScreenHeight() |
render_text_width(text, scale) | MeasureTextEx() |
- Replace the font. Use
GetFontDefault(); delete the embeddedfont_dataarray and GLSL shader sources fromrender_util.c. - Update the four
screen_*_draw()functions. Either keep the thinrender_*wrappers (now backed by Raylib) or call Raylib directly. Preserve each screen's layout and focus-visibility rules. - Remove dead code. Delete the now-unused GLES2 quad/shader/text-metric code.
Done when:
- All four screens render through Raylib.
render_util.ccontains no raw GLES2 shader or bitmap-font code.
S5.5-T5 — Wire Controller Input (Rendering-Only Raylib)
Finding: The shell reads controller input directly from evdev in input.c because the reserved SYSTEM/QUICK_MENU buttons must survive (they are stripped by the libplayos input API). Raylib must be used for rendering only.
Steps:
- Keep
shell_input_poll()and edge detection ininput.cas the single source of controller state — do not route navigation through Raylib's gamepad abstraction. - Confirm the invariants after the rendering migration:
Aconfirms,Bbacks out, d-pad moves focus, focus is always visible. - Verify SYSTEM/QUICK_MENU still arrive (these buttons do not exist in Raylib's gamepad mapping and must stay on the shell's evdev path).
Done when:
- Controller navigation on all four screens is unchanged while rendering through Raylib.
- Reserved buttons remain available to the trusted shell.
S5.5-T6 — Integrate Lifecycle Polling into the Raylib Frame Loop
Finding: main.c polls playos_lifecycle_poll() every frame and handles SUSPEND/RESUME/FOREGROUND/BACKGROUND/TERMINATE. This must survive the Raylib migration.
Steps:
- Keep lifecycle polling in the Raylib main loop (or in the backend's per-frame hook).
- Suspend/background → skip
BeginDrawing()/EndDrawing()(no rendering work). Foreground/resume → resume normal rendering. - TERMINATE → exit cleanly through the Raylib window-close path (
s->running = false). - Confirm the shell remains a persistent supervised process (no
exit()except on unrecoverable init failure).
Done when:
- Lifecycle state changes affect rendering predictably.
- The shell exits cleanly on TERMINATE and remains compatible with
playos-initsupervision.
S5.5-T7 — Buildroot Packaging: Flip to Raylib 6.0
Finding: playos-shell.mk passes -DPLAYOS_SHELL_USE_RAYLIB=OFF. The vendored Raylib is inside the playos-shell source tree but not built into the image.
Steps:
- Flip the flag. Set
-DPLAYOS_SHELL_USE_RAYLIB=ONinbr2-external/package/playos-shell/playos-shell.mk. - Ensure the vendored Raylib builds and links. Confirm the
playos-shellCMake builds the vendoredexternal/raylibstatic library and links it intoplayos-shell. Add any missing dependency toPLAYOS_SHELL_DEPENDENCIES(Raylib stays vendored insideplayos-shell, so it should not need a separate Buildroot package). - Keep libplayos + GLES/EGL/Wayland through the backend. Update the
.mkheader comment (it currently says "Uses EGL/GLES2 for rendering" — change to "Raylib 6.0 via custom PlayOS backend"). - Pin in
versions.lock(done in T1) and confirmmake setupvendored the pinned commit. - Build. Run
make qemu-buildand confirm the image contains the Raylib-linked shell.
Done when:
playos-shell.mksetsUSE_RAYLIB=ON.- The QEMU image builds and boots with the Raylib-backed shell.
S5.5-T8 — Spec and Docs Reconciliation
Finding: playos-shell-spec.md says "Rendering with Raylib PlayOS backend", but playos-shell/AGENTS.md says "Raylib integration deferred; direct EGL/GLES2 used instead". ADR-0006's consequences require the rcore_playos.c backend to be maintained across Raylib updates.
Steps:
playos-shell-spec.md: update the rendering responsibilities and cross-references to name Raylib 6.0 +rcore_playos.c; remove any "deferred" wording.playos-shell/AGENTS.md: update the implementation-status banner and the "Rendering" section to state Raylib 6.0 is active via the custom backend (raw GLES2 path retired).- ADR-0006: add a follow-up note (or a superseding ADR if the decision materially changed — e.g., the explicit "rendering-only Raylib, input stays shell-owned evdev" ruling) recording the 6.0 migration.
- Cross-links: this document's footer already links Previous/Next; add any in-doc references from Sprint 5/6 where useful (mirroring how Sprint 2.5 is referenced by Sprint 2).
Done when:
- No documentation claims "Raylib deferred" or describes the raw-GLES2 renderer as the active path.
S5.5-T9 — Validation and Runtime Evidence
Steps:
- Nested Wayland dev run: shell boots, draws all four screens through Raylib, navigates with controller, logs manifest load + navigation + fps.
- QEMU headless path: shell starts without crashing (visual output limited, but must not regress from Sprint 5).
- On-device (Ally): visual parity with Sprint 5, ≥ 60 fps, controller navigation, persistence under supervision.
- Capture evidence:
/data/log/shell.logshowing Raylib version, backend init, GPU string, and fps.
Done when:
- Evidence is captured across dev / QEMU / device paths.
- All acceptance criteria below are verified.
Implementation Guidance
Order of execution
- T1 first (vendor + pin Raylib 6.0) — everything depends on the new source.
- T2 second (reconciliation list) — informs the backend and port work.
- T3 third (backend) — the foundation the port sits on.
- T4 fourth (rendering port) — the largest surface change, across all screens.
- T5, T6 next (input, lifecycle) — wire the shell's existing behaviours into the Raylib loop.
- T7 sixth (Buildroot) — build the whole image.
- T8 seventh (docs) — document reality after the code lands.
- T9 last (validation) — verify across all three runtime paths.
Atomic commits
Each task is a separate commit (or small group) referencing the task ID:
S5.5-T1: vendor raylib 6.0 and pin in versions.lock
S5.5-T3: implement rcore_playos.c backend for raylib 6.0
S5.5-T4: port shell rendering to raylib draw API
Do not break the shell
After T1–T7, the shell must still render all four screens, navigate with a controller, and stay alive under supervision. If the raw-GLES2 path was already broken before this sprint, document it and fix only what this sprint touches.
Verification and Evidence
| Evidence | How it is produced |
|---|---|
| Raylib 6.0 proof | external/raylib/src/raylib.h shows RAYLIB_VERSION "6.0"; versions.lock RAYLIB_COMMIT non-empty |
| Backend proof | a Raylib frame draws through rcore_playos.c in the nested Wayland dev path |
| Rendering port proof | render_util.c contains no raw GLES2 shader/bitmap-font code; all four screens use the Raylib API |
| Input proof | controller navigation unchanged; reserved SYSTEM/QUICK_MENU buttons still delivered to the shell |
| Lifecycle proof | suspend skips rendering, foreground resumes, TERMINATE exits cleanly |
| Packaging proof | playos-shell.mk sets USE_RAYLIB=ON; QEMU image boots the Raylib shell |
| Docs proof | playos-shell-spec.md, AGENTS.md, ADR-0006 no longer describe Raylib as deferred |
| Runtime proof | shell log shows Raylib version + backend init + ≥ 60 fps on the Ally |
Acceptance Criteria
-
vendored Raylib reports
RAYLIB_VERSION "6.0"and is pinned inversions.lock -
Raylib builds with the minimal
SUPPORT_MODULE_*module config - a Raylib 5.5 → 6.0 breaking-change reconciliation list exists and is applied
-
rcore_playos.cimplements the Raylib 6.0 platform backend (fullscreen Wayland + EGL + frame callback) -
all four screens render through the Raylib draw API (no raw GLES2 shader/bitmap font in
render_util.c) - controller navigation is unchanged and uses shell-owned evdev (not Raylib's gamepad abstraction)
- lifecycle suspend skips rendering; foreground resumes; TERMINATE exits cleanly
-
PLAYOS_SHELL_USE_RAYLIB=ONin Buildroot; the image builds and boots the Raylib shell -
playos-shell-spec.md,playos-shell/AGENTS.md, and ADR-0006 reflect Raylib 6.0 (no "deferred") - ≥ 60 fps on the Ally; nested dev and QEMU paths remain usable for iteration
Handoff to Sprint 6
Sprint 6 may assume:
- The shell renders through Raylib 6.0 via
rcore_playos.c— no raw-GLES2 fallback remains. - The custom backend owns Wayland surface + EGL context + frame pacing;
main.cdelegates to it. - Input remains shell-owned direct evdev (reserved buttons preserved); Raylib is rendering-only.
- Raylib is pinned in
versions.lockand builds with a minimal module config. - All spec/docs describe the active Raylib path, so Sprint 6 (storage/game discovery) builds on Raylib rendering rather than a hand-rolled renderer.
Sprint 6 should not need to touch the rendering layer except to display discovered game metadata through the existing Raylib draw path.
Previous: Sprint 5 | Next: Sprint 5.6
Sprint 5.6 — ReposCleanUp
Goal: Restore a consistent repository boundary so that every PlayOS component's C source lives in its own repository and the reference distribution contains no committed C source. This resolves two structural drifts left over from Sprints 1–5.5: (1) playos-init has no repository of its own — its 25 source files are committed directly into playos-refdistro; and (2) playos-shell source is likewise committed into playos-refdistro and is not wired into make setup, even though a real playos-shell repository already exists and is pushed.
Primary Outcome: playos-init becomes a first-class repository (created by the maintainer, seeded from the in-tree source). Both playos-init and playos-shell are removed from the playos-refdistro git index, git-ignored, cloned by make setup from their own repositories, and pinned by a real commit SHA in versions.lock. Buildroot packages continue to build from the src/ clones unchanged.
Status: 🟢 Complete — implemented and verified
Prerequisites: Sprint 5.5 complete — the shell renders through Raylib 6.0 and the playos-shell repository is pushed at main. The maintainer has created (or will create) the empty PlayOS-Foundation/playos-init repository.
Why This Sprint Exists
Sprints 0–5.5 accumulated two repository-boundary violations that contradict the project's own rule ("no C code here — all source lives in the component repos") and will compound as more sprints build on these components:
-
playos-inithas no repository.playos-refdistro/src/playos-init/is tracked directly in theplayos-refdistrogit index (25 files:init.c,supervisor.c,mount.c,recovery.c,logging.c,ipc/*.c, tests,CMakeLists.txt). It has no.gitand no remote. There is noPlayOS-Foundation/playos-initrepository yet, even though theMakefilesetuptarget already tries to clone one and.gitignorealready listssrc/playos-init. -
playos-shellis committed intoplayos-refdistro.playos-refdistro/src/playos-shell/contains 1 392 files tracked inplayos-refdistro(no.git). The real repository exists atPlayOS-Foundation/playos-shelland is pushed atmain(HEAD1046262…), and the in-tree copy is a separate, manually-synced snapshot that is currently in sync with it.make setupdoes not cloneplayos-shellat all, and.gitignoredoes not ignoresrc/playos-shell. -
versions.lockis still dishonest for init.PLAYOS_INIT_COMMIT=b7800393…is annotated(in monorepo)and points at a refdistro commit, not aplayos-initrepo commit.PLAYOS_SHELL_COMMITis already correct (104626206dfd2de59b4c576d3990d43b3b65980b, the pushed canonical HEAD). -
The correct pattern already exists.
playos-compositor,playos-runtime, andplayos-platform-apiare each: a sibling repository,src/<name>git-ignored + untracked, cloned and SHA-checked-out bymake setup, and built by alocal-method Buildroot package.playos-initandplayos-shellsimply need to be brought onto that same pattern.
This is a pure cleanup sprint — no feature, protocol, or UX changes. The user-visible result must be identical.
Start Condition Checklist
-
Sprint 5.5 complete;
playos-shellrenders through Raylib 6.0. -
PlayOS-Foundation/playos-shellrepository is pushed atmain(verified: HEAD104626206dfd2de59b4c576d3990d43b3b65980b==origin/main). -
PlayOS-Foundation/playos-initrepository exists (empty, or with only a README/license). (Created by the maintainer — not by the implementing agent.) -
Local checkouts are available:
playos-refdistroand the siblingplayos-shellrepo under the PlayOS workspace root ($HOME/playos/). -
make setup/make qemu-buildare understood and can be run to verify the result.
Decisions Locked for This Sprint
playos-initbecomes its own repository. The maintainer createsPlayOS-Foundation/playos-init. The implementing agent seeds it from the currentplayos-refdistro/src/playos-init/source (excluding build artifacts), pushes it, and records the full HEAD SHA.playos-shellcanonical source is the sibling repository, not the in-treeplayos-refdistro/src/playos-shellcopy. The in-tree copy is deleted from the index. If the in-tree copy carries a meaningful uncommitted edit, that edit is applied to the sibling repo and pushed first; otherwise it is discarded.- IPC ownership stays with
playos-init. The IPC C sources (ipc/ipc.h,ipc_client.c,ipc_server.c,ipc_framing.c,lifecycle_fd.c) move withplayos-initinto its repository.playos-runtimeremains protocol-only (unchanged from Sprint 2.5). - Buildroot packaging is unchanged. All five component
.mkfiles keepSITE_METHOD = localpointing at$(BR2_EXTERNAL_PlayOS_PATH)/../src/<name>.make setupmaterializes the clones; the packages build from those clones. - No C source is committed in
playos-refdistroafter this sprint. - No new features. Pure remediation.
Scope
In Scope
- Seed and push the new
playos-initrepository from the in-tree source - Untrack
playos-refdistro/src/playos-initandplayos-refdistro/src/playos-shellfrom the refdistro git index - Add
src/playos-shelltoplayos-refdistro/.gitignore(init is already listed) - Wire
playos-shellinto theMakefilesetuptarget (clone + pinned checkout), mirroring the existing four components - Update
versions.locksoPLAYOS_INIT_COMMITpoints at the newplayos-initrepo HEAD, and remove the(in monorepo)annotation (PLAYOS_SHELL_COMMITis already pinned correctly) - Verify
make setupclones all five components andplayos-init/playos-shellstill build from the clones - Update docs / repo inventory so no document claims init or shell C source lives in
playos-refdistro
Explicitly Out of Scope
- Creating the
playos-initGitHub repository (maintainer does this) - New features, protocol changes, or UX changes
- Changing the Buildroot
localsite method or package structure - Re-homing the IPC protocol into
playos-runtimeor a new repo (deferred until a consumer actually needs it) - CI pipeline changes
- Sprint 6 storage / game-discovery work
Required Repository Changes
| Repo | Required work |
|---|---|
playos-init | NEW — seeded from playos-refdistro/src/playos-init/ (source + tests + CMakeLists.txt + .gitignore), pushed to main |
playos-shell | Verify no drift vs. canonical (already in sync); ensure main is the pushed canonical state |
playos-refdistro | git rm -r src/playos-init src/playos-shell; add src/playos-shell to .gitignore; wire playos-shell into Makefile setup; fix versions.lock; update AGENTS.md |
playos-spec | Add Sprint-5.6.md; update repo inventory (architecture.md) to list playos-init; update cross-link footers |
Expected Files and Directories
playos-init (NEW repository, after seeding)
.gitignore ← carried over from the in-tree source
CMakeLists.txt
include/playos-init/ ← init.h, ipc_handler.h, mount.h, recovery.h, supervisor.h
ipc/ ← ipc.h, ipc_client.c, ipc_server.c, ipc_framing.c, lifecycle_fd.c
src/ ← child_process.c, init.c, ipc_handler.c, logging.c, main.c, mount.c, recovery.c, shutdown.c, supervisor.c
tests/ ← host + ipc test sources
build/,CMakeFiles/,CMakeCache.txt, andcmake_install.cmakemust not be committed.
playos-refdistro
.gitignore ← UPDATE: add `src/playos-shell` to the "Cloned source dependencies" block
Makefile ← UPDATE: add PLAYOS_SHELL_COMMIT var + playos-shell clone block in `setup`
versions.lock ← UPDATE: real SHA for INIT only (from S5.6-T1); remove the `(in monorepo)` annotation
AGENTS.md ← UPDATE: IPC Sources note points at the playos-init repo (cloned to src/playos-init)
src/playos-init/ ← REMOVED from index + working tree (re-created by make setup)
src/playos-shell/ ← REMOVED from index + working tree (re-created by make setup)
playos-spec
src/sprints/Sprint-5.6.md ← NEW: this document
src/sprints/Sprint-5.5.md ← UPDATE: footer "Next" → Sprint 5.6
src/sprints/Sprint-6.md ← UPDATE: footer "Previous" → Sprint 5.6
src/architecture.md ← UPDATE: add playos-init to the repository inventory
Agent Task Breakdown
Every task below is independently checkable.
Task Status Grid
| Task ID | Task | Primary repo | Status | Notes / evidence |
|---|---|---|---|---|
| S5.6-T1 | Seed and push the playos-init repository | playos-init | done | Cloned empty repo, copied in-tree source, committed, pushed (3a89f09f…) |
| S5.6-T2 | Reconcile and confirm the playos-shell canonical SHA | playos-shell | done | 1046262… confirmed current; in-tree copy already in sync |
| S5.6-T3 | Untrack src/playos-init from refdistro | playos-refdistro | done | git rm -r src/playos-init; already in .gitignore |
| S5.6-T4 | Untrack src/playos-shell from refdistro and gitignore it | playos-refdistro | done | git rm -r src/playos-shell; added to .gitignore |
| S5.6-T5 | Wire playos-shell into make setup | playos-refdistro | done | Added PLAYOS_SHELL_COMMIT + clone block, mirroring the other four |
| S5.6-T6 | Pin real SHAs and clean versions.lock | playos-refdistro | done | Removed the (in monorepo) annotation (shell already pinned) |
| S5.6-T7 | Verify make setup + build from extracted repos | playos-refdistro | done | All five clones present; init + shell build |
| S5.6-T8 | Docs and repo-inventory reconciliation | playos-spec, playos-refdistro | done | No doc claims C source lives in refdistro |
S5.6-T1 — Seed and Push the playos-init Repository
Finding: The only copy of playos-init source is tracked in playos-refdistro/src/playos-init/ (25 files, no .git, no remote). The maintainer has created the empty PlayOS-Foundation/playos-init repository. The Makefile setup target already attempts to clone it, so the repo must exist and contain the source before make setup can succeed.
Steps:
- Clone the (empty) repository to a scratch location:
git clone https://github.com/PlayOS-Foundation/playos-init.git /tmp/playos-init-seed - Copy the source from the in-tree copy into the clone, preserving the directory layout:
(Ifrsync -a --exclude build --exclude CMakeFiles --exclude CMakeCache.txt \ --exclude cmake_install.cmake \ playos-refdistro/src/playos-init/ /tmp/playos-init-seed/rsyncis unavailable, usecp -aand remove the generatedbuild/directory afterward.) - Verify the clone's own
.gitignore(carried over from the in-tree source) excludesbuild/and other generated artifacts; adjust if it does not. - Stage, commit, and push:
cd /tmp/playos-init-seed git add -A git commit -m "S5.6-T1: seed playos-init from refdistro source" git push -u origin main - Record the full HEAD SHA:
git rev-parse HEAD
Done when:
PlayOS-Foundation/playos-initcontains the same source layout as the former in-tree copy (minus build artifacts).- The repo is pushed to
main. - A full 40-char HEAD SHA is recorded for use in
versions.lock.
S5.6-T2 — Reconcile and Confirm the playos-shell Canonical SHA
Finding: The real playos-shell repository (sibling checkout $HOME/playos/playos-shell) is pushed and clean at HEAD 104626206dfd2de59b4c576d3990d43b3b65980b (main == origin/main). The in-tree playos-refdistro/src/playos-shell/ copy is already byte-identical to the sibling repo — the rcore_playos.c non-blocking render-loop fix was committed to playos-shell as 1046262 and synced into playos-refdistro as 257ac52. versions.lock already points at 104626206dfd2de59b4c576d3990d43b3b65980b. This task is therefore a verification no-op unless a fresh diff surfaces drift.
Steps:
- Confirm the sibling repo is clean and in sync with its remote:
git -C playos-shell status --short git -C playos-shell rev-parse origin/main - Diff the in-tree copy against the sibling repo to confirm no drift:
diff -ru playos-shell playos-refdistro/src/playos-shell | head -200 - Record the canonical full SHA
104626206dfd2de59b4c576d3990d43b3b65980bforversions.lock. If a diff surfaces a meaningful edit, apply it to the sibling repo, commit, and push first, then use the neworigin/mainSHA.
Done when:
playos-shellmainandorigin/mainare in sync.- The in-tree copy is confirmed byte-identical to the sibling repo (no drift).
- A single canonical full SHA
104626206dfd2de59b4c576d3990d43b3b65980bis recorded forversions.lock.
S5.6-T3 — Untrack src/playos-init from playos-refdistro
Finding: src/playos-init is tracked in playos-refdistro (25 files). .gitignore already lists src/playos-init (line 18), but ignore rules do not untrack files that are already in the index. The directory must be removed from both the index and the working tree so make setup will clone it fresh.
Steps:
- Remove from index and working tree (this is intentional —
make setupwill re-materialize it):cd playos-refdistro git rm -r src/playos-init - Confirm
.gitignorestill containssrc/playos-init(no change needed). - Commit:
git commit -m "S5.6-T3: untrack playos-init source from refdistro monorepo" - Verify nothing remains tracked:
git ls-files src/playos-init # expected: no output
Done when:
git ls-files src/playos-initreturns nothing.src/playos-init/no longer exists in the working tree.- The commit is present with the task ID in its message.
S5.6-T4 — Untrack src/playos-shell from playos-refdistro and Gitignore It
Finding: src/playos-shell is tracked in playos-refdistro (1 392 files) and is not in .gitignore. It must be removed from the index and working tree, and src/playos-shell must be added to .gitignore so make setup's clone is not re-added.
Steps:
- Remove from index and working tree:
cd playos-refdistro git rm -r src/playos-shell - Add
src/playos-shellto.gitignoreunder the existing "Cloned source dependencies" block (after thesrc/playos-runtimeline):src/playos-shell - Commit:
git commit -m "S5.6-T4: untrack playos-shell source from refdistro monorepo" - Verify:
git ls-files src/playos-shell # expected: no output git check-ignore src/playos-shell && echo "ignored"
Done when:
git ls-files src/playos-shellreturns nothing..gitignorelistssrc/playos-shell.- The commit is present with the task ID in its message.
S5.6-T5 — Wire playos-shell into make setup
Finding: The Makefile parses PLAYOS_INIT_COMMIT, PLAYOS_COMPOSITOR_COMMIT, PLAYOS_RUNTIME_COMMIT, and PLAYOS_PLATFORM_API_COMMIT (lines 31–34) and clones those four repos in setup (lines 55–86). It has no PLAYOS_SHELL_COMMIT and no shell clone, so make setup cannot produce src/playos-shell.
Steps:
- In the version-pins block, add a
PLAYOS_SHELL_COMMITvariable mirroring the existing four (e.g. after line 34):PLAYOS_SHELL_COMMIT := $(shell grep -s '^PLAYOS_SHELL_COMMIT=' $(VERSIONS_LOCK) 2>/dev/null | cut -d= -f2- | xargs) - In the
setuptarget, add aplayos-shellclone block mirroring theplayos-initblock (lines 55–62), with the same "clone if missing, then checkout the pinned SHA if set" guard:@if [ ! -d "$(CURDIR)/src/playos-shell" ]; then \ echo " -> playos-shell..."; \ git clone https://github.com/PlayOS-Foundation/playos-shell.git "$(CURDIR)/src/playos-shell"; \ if [ -n "$(PLAYOS_SHELL_COMMIT)" ]; then \ cd "$(CURDIR)/src/playos-shell" && \ git fetch origin && git checkout "$(PLAYOS_SHELL_COMMIT)"; \ fi; \ fi - Keep the ordering consistent (init, compositor, runtime, platform-api, shell, or any stable order).
Done when:
make setupclonesplayos-shellintosrc/playos-shelland checks out the pinned SHA.- The new block is structurally identical to the existing four.
S5.6-T6 — Pin Real SHAs and Clean versions.lock
Finding: versions.lock currently has:
PLAYOS_INIT_COMMIT=b7800393c9466609facf2c5c57dacbe3c9bb0c30 # sprint-5: supervisor updates (in monorepo)PLAYOS_SHELL_COMMIT=104626206dfd2de59b4c576d3990d43b3b65980b # sprint-5.5: raylib 6.0 backend + non-blocking render loop (fixes frame-2 deadlock)
Only the init entry is now wrong: its SHA refers to a refdistro commit (not a playos-init repo commit), so it must be replaced after S5.6-T1 seeds the repo. The shell SHA is already correct and pushed, so no change is required there.
Steps:
- Replace
PLAYOS_INIT_COMMITwith the full SHA recorded in S5.6-T1, and remove the(in monorepo)annotation:PLAYOS_INIT_COMMIT=<playos-init-head-sha> # sprint-5.6: extracted to own repo - Confirm
PLAYOS_SHELL_COMMITalready equals the canonical SHA from S5.6-T2 (104626206dfd2de59b4c576d3990d43b3b65980b) — no change needed. - Confirm no entry still carries
(in monorepo),(LOCAL — push pending), or a branch name:grep -nE 'monorepo|push pending|latest|main' versions.lock # expected: no matches (except comments that are intentional)
Done when:
- Both SHAs are non-empty, 40-char, and resolve in their respective repositories.
- No stale annotation remains in
versions.lock.
S5.6-T7 — Verify make setup and the Build from Extracted Repos
Finding: After T3–T6, make setup should clone all five components, and playos-init / playos-shell should build from those clones. The Buildroot .mk files are unchanged (SITE_METHOD = local), so this verifies the repository-boundary change without touching packaging.
Steps:
- Re-run setup (a fresh
src/is expected after T3/T4):cd playos-refdistro make setup - Confirm all five clones are present and at the pinned SHAs:
for d in playos-init playos-compositor playos-runtime playos-platform-api playos-shell; do printf '%s: ' "$d" git -C "src/$d" rev-parse --short HEAD done - Build the two affected packages (faster than a full image; use the QEMU output dir, or
allyfor the device path):
(Alternatively runmake -C buildroot BR2_EXTERNAL="$PWD/br2-external" O="$PWD/output/qemu" \ playos-init-rebuild playos-shell-rebuildmake qemu-buildfor the full end-to-end check, as in prior sprints.) - Confirm the refdistro index carries no component source:
git ls-files src/ # expected: no *.c/*.h under src/ (all five now gitignored clones)
Done when:
make setupmaterializes all fivesrc/clones at their pinned SHAs.playos-initandplayos-shellrebuild successfully from the clones.git ls-files src/shows no committed component source.
S5.6-T8 — Docs and Repo-Inventory Reconciliation
Finding: Several documents still describe the pre-cleanup reality. playos-spec/src/architecture.md (or its repository inventory) omits playos-init as a first-class repository. playos-refdistro/AGENTS.md's "IPC Sources" note says IPC C sources "live at src/playos-init/ipc/", which now means the clone, not the monorepo.
Steps:
- In
playos-spec/src/architecture.md(or wherever the repository list lives), addplayos-initalongsideplayos-compositor,playos-runtime,playos-platform-api, andplayos-shellas a first-class component repository. - Update
playos-refdistro/AGENTS.mdso the "IPC Sources" note reads that the IPC sources live in theplayos-initrepository (cloned tosrc/playos-init/ipc/bymake setup), not committed inplayos-refdistro. - Add this sprint to the footer chain (see this document's footer, and the Sprint-5.5 / Sprint-6 footer edits below).
- Confirm no remaining doc claims init or shell C source is committed in
playos-refdistro:grep -rnE 'in monorepo|in-tree|committed (directly|into) playos-refdistro' \ playos-refdistro/AGENTS.md playos-spec/src/architecture.md
Done when:
playos-initis listed as a first-class repository.playos-refdistro/AGENTS.mdno longer implies init/shell C source is committed in the distribution repo.- Footer links point Previous/Next through Sprint 5.6.
Implementation Guidance
Order of execution
- T1 first (seed + push
playos-init) — everything downstream needs the repo to exist. - T2 second (reconcile + confirm
playos-shellSHA) — locks the canonical shell state. - T3, T4 next (untrack both in-tree copies) — removes the monorepo source; safe because the repos are pushed.
- T5 fifth (wire shell into
make setup) — no dependency on T3/T4 beyond intent, but do it before verifying. - T6 sixth (pin SHAs in
versions.lock) — depends on T1/T2 for the real SHAs. - T7 seventh (verify) — depends on all code/index changes.
- T8 last (docs) — document reality after the code lands.
Atomic commits
Each task is a separate commit (or small group) referencing the task ID:
S5.6-T1: seed playos-init repo from refdistro source
S5.6-T3: untrack playos-init source from refdistro monorepo
S5.6-T4: untrack playos-shell source from refdistro monorepo
S5.6-T5: wire playos-shell into make setup
S5.6-T6: pin real init/shell SHAs in versions.lock
S5.6-T8: update repo inventory and AGENTS.md for extracted repos
Do not break the build
After T1–T7, run make qemu-build (or at minimum the targeted playos-init-rebuild / playos-shell-rebuild) to confirm nothing regressed. The Buildroot .mk files must remain unchanged — the only Makefile change is the added shell clone block. If a build was already broken before this sprint, document it and fix only what this sprint touches.
Reversibility note
git rm -r removes tracked files from both the index and the working tree. Before running it, confirm the corresponding source has been pushed to its repository (T1 for init, T2 for shell). The pushed repos are the recovery mechanism; the working-tree copies are intentionally disposable.
Verification and Evidence
| Evidence | How it is produced |
|---|---|
playos-init repo proof | git ls-remote https://github.com/PlayOS-Foundation/playos-init.git main resolves; repo contains src/init.c etc. |
playos-shell canonical SHA proof | git -C playos-shell rev-parse origin/main == the SHA in versions.lock |
| Init untrack proof | git ls-files src/playos-init in playos-refdistro returns nothing |
| Shell untrack proof | git ls-files src/playos-shell returns nothing; .gitignore lists src/playos-shell |
make setup proof | make setup clones all five src/ repos at pinned SHAs |
| Build proof | playos-init-rebuild and playos-shell-rebuild succeed from the clones |
versions.lock proof | both PLAYOS_INIT_COMMIT / PLAYOS_SHELL_COMMIT are non-empty 40-char SHAs with no stale annotation |
| Docs proof | playos-init listed in the repo inventory; AGENTS.md no longer claims C source lives in playos-refdistro |
Acceptance Criteria
-
PlayOS-Foundation/playos-initexists, is seeded from the in-tree source (minus build artifacts), and is pushed tomain -
PlayOS-Foundation/playos-shellmain==origin/main, and its full SHA is recorded -
git ls-files src/playos-initandgit ls-files src/playos-shellinplayos-refdistroboth return nothing -
.gitignorelistssrc/playos-shell(and still listssrc/playos-init) -
make setupclones all five components intosrc/and checks out the pinned SHAs -
playos-initandplayos-shellrebuild successfully from the clones -
versions.lockhas real, non-empty, 40-char SHAs for bothPLAYOS_INIT_COMMITandPLAYOS_SHELL_COMMIT, with no(in monorepo)/(LOCAL — push pending)annotations -
playos-refdistrocontains no committed C source undersrc/ -
playos-initis listed as a first-class repository in the spec's repository inventory -
playos-refdistro/AGENTS.mdno longer describes init/shell C source as committed in the distribution repo
Handoff to Sprint 6
Sprint 6 may assume:
- Every component's C source lives in its own repository (
playos-init,playos-compositor,playos-runtime,playos-platform-api,playos-shell). playos-refdistrocontains no committed C source — only Buildroot packaging, configs, board files, and the developerMakefile.make setupreproducibly clones all five components at pinned SHAs fromversions.lock.- Buildroot still builds each component from its
src/clone via the unchangedSITE_METHOD = localpackages. - The IPC sources live in the
playos-initrepository (cloned tosrc/playos-init/ipc/), andplayos-runtimeremains protocol-only.
Sprint 6 (persistent storage and game discovery) should not need to touch repository boundaries; if any in-tree C source reappears, treat it as a regression from this sprint.
Previous: Sprint 5.5 | Next: Sprint 6
Sprint 6 — Persistent Storage and Game Discovery
Goal: Establish reliable persistent ext4 storage, a stable /data directory layout, the real playos-platform-api storage path contract, and live game discovery in the shell from real manifest files.
Primary Outcome: Games installed in /data/games/ are discovered, displayed in the shell, and their save and cache paths are correctly isolated per game. Data survives reboot.
Status: 🟢 Complete — persistent ext4 storage, the full /data schema, live manifest-driven game discovery, the playos_storage API, three sample games, and FactoryReset (cache/config scope) all landed and are verified. GPU triangle rendering and live controller display were delivered in later sprints.
Prerequisites: Sprint 5.6 complete — repository boundaries are clean and every component's C source lives in its own repository.
Why This Sprint Exists
Sprint 5 used stub data loaded from hardcoded entries, and storage was only stubbed behind the public API. Sprint 6 replaces the stub game list with real discovery over persistent storage. Without this sprint, the shell is a demo; with it, the shell is a real game library. Every subsequent sprint depends on reliable isolated per-game paths and on manifest-driven discovery.
Reality check as of Sprint 5.6: the storage API and a basic discovery scan already landed, but the full plan (manifest validation, icons, sorting, the complete /data schema, the version marker, sample games, and FactoryReset) is still outstanding.
Start Condition Checklist
-
Sprint 5.6 complete; repository boundaries are clean (
playos-initis its own repo,playos-refdistrohas no C source). -
playos-initalready discovers and mounts aplayos-datapartition at/data(src/mount.c,find_data_partition()). -
playos-initalready creates a minimal first-boot directory set (src/mount.c,playos_data_create_dirs()). -
The real
playos_storage.h/playos_storage.cAPI shipped in Sprint 5 (playos-platform-api). -
playos-shellalready performs a basic/data/games/scan and readsname/version/descriptionfrommanifest.json(src/screen_library.c). -
/data/.playos-storage-versionmarker is written and validated byplayos-init(src/mount.c). -
Game manifest v1 schema exists (
playos-spec/schemas/game-manifest-v1.json). -
playos-samplescontains three real sample games with manifests and built binaries. -
FactoryResethandler exists inplayos-init(JSON message specified inplayos-spec/src/runtime-ipc.md).
Decisions Locked for This Sprint
- Partition identification (reconciled to implementation): search order is label
playos-data(via/dev/disk/by-label/, then a direct block-device scan, then/proc/partitions) → GPT partition type GUID (PLAYOS_DATA_TYPE_GUID) → UUID from the kernel cmdlineplayos.data_uuid=. When severalplayos-datapartitions exist, removable media (USB) is preferred over internal NVMe; otherwise the last non-removable match wins. The earlier "GUID → label → UUID" wording is superseded by this actual order. - Data directory schema: the
/data/layout below is final for MVP. The shipped spelling is/data/log(singular); the spec docs have been reconciled to this spelling (only the non-editableideas.mdstill shows the older plural). - Manifest format: v1 JSON schema defined here is the stable game metadata contract for MVP.
PLAYOS_GAME_IDenv var: set byplayos-initat game launch; the storage API derives per-game paths from it. Game ID is not a function argument.- FactoryReset scope this sprint:
erase_cacheanderase_configonly;erase_gamesanderase_savesare destructive and deferred to Sprint 10 (the schema may define them, but the handler returns an explicit "deferred" result).
Scope
In Scope
/datapartition mount (already present), first-boot provisioning, and the complete directory tree/datadirectory schema (final MVP layout)/data/.playos-storage-versionmarker (write on first boot, validate on mount)- Game manifest v1 schema (
playos-spec/schemas/game-manifest-v1.json) - Real
playos_storage.himplementation (already shipped — verify only) - Complete live game discovery in the shell (validation, icons, sorting)
- Three real minimal sample games with valid manifests and compiled binaries
FactoryResetIPC (cache and config only this sprint)
Explicitly Out of Scope
- Game launch / lifecycle (Sprint 7)
- Audio (Sprint 8)
- Power / thermal (Sprint 9)
- Installer / disk formatting (Sprint 10)
- Full factory reset with save erasure (Sprint 10)
Required Repository Changes
| Repo | Required work |
|---|---|
playos-init | Extend /data provisioning: full directory schema, .playos-storage-version marker; add FactoryReset handler |
playos-platform-api | Real playos_storage API (already implemented in Sprint 5 — verify, no new surface) |
playos-shell | Complete manifest-driven discovery: full validation, icon loading, sorting, robust skip-on-invalid |
playos-samples | Three real sample games with valid manifests and compiled binaries |
playos-refdistro | Package/install the sample games into the rootfs overlay (no C source) |
playos-spec | Game manifest v1 schema + this sprint doc |
FactoryResetis a JSON IPC message already specified inplayos-spec/src/runtime-ipc.md;playos-initowns both the message handling and the directory-erase logic (it is the IPC server owner).playos-runtimeis not involved — itsprotocols/playos-v1.xmlis the Wayland compositor protocol.
Expected Files and Directories
playos-init
src/mount.c ← exists — extend playos_data_create_dirs() to the full schema + version marker
src/ipc_handler.c ← add FactoryReset handler
playos-platform-api
include/playos/playos_storage.h ← already real (Sprint 5)
src/playos_storage.c ← already real (Sprint 5)
playos-shell
src/screen_library.c ← exists — extend for validation, icon loading, sorting, skip-on-invalid
playos-samples
triangle/ ← com.playos.sample-triangle
input-debug/ ← com.playos.sample-input
audio-sine/ ← com.playos.sample-audio
playos-refdistro
br2-external/board/common/rootfs-overlay/data/games/
com.playos.sample-triangle/{manifest.json, bin/, assets/}
com.playos.sample-input/{manifest.json, bin/, assets/}
com.playos.sample-audio/{manifest.json, bin/, assets/}
playos-spec
schemas/game-manifest-v1.json
Agent Task Breakdown
Task Status Grid
| Task ID | Task | Primary repo | Status | Notes / evidence |
|---|---|---|---|---|
| S6-T1 | Implement /data partition provisioning | playos-init | done | Version marker write/validate + full provisioning added to src/mount.c; builds, init_state test passes |
| S6-T2 | Define and create the final /data directory schema | playos-init | done | playos_data_create_dirs() now creates the full 11-dir schema + marker |
| S6-T3 | Define game manifest v1 schema | playos-spec | done | schemas/game-manifest-v1.json created and JSON-valid |
| S6-T4 | Implement real playos_storage API | playos-platform-api | done (Sprint 5) | Verified canonical signature; no new surface |
| S6-T5 | Implement live game discovery in the shell | playos-shell | done | Validation/icons/sort/skip-on-invalid added to screen_library.c; verified on device |
| S6-T6 | Build and install three real sample games | playos-samples, playos-refdistro, playos-init | done | Three samples build+run and install to the read-only seed dir (/usr/share/playos/games), then playos-init seeds them into /data/games on first boot; Raylib GPU triangle rendering and live controller display were delivered in later sprints |
| S6-T7 | Add FactoryReset IPC command (cache/config scope) | playos-init | done | JSON message per runtime-ipc.md; handler in ipc_handler.c + ipc/ipc.h; verified |
| S6-T8 | Persistence and isolation validation | playos-refdistro | done | Isolation and reboot/QEMU persistence verified; first-boot provisioning confirmed in Sprint 10 |
S6-T1 — Implement /data partition provisioning in playos-init
Current state: playos-init/src/mount.c find_data_partition() already implements the discovery order locked above (label first, removable-preferred, GPT GUID and cmdline UUID as fallbacks), mounts the partition at /data (ext4, with vfat/auto fallback), and writes a boot marker. The remaining work is the storage-version marker and the full provisioning flow.
- Validate
/data/.playos-storage-versionon every mount - On first boot: create the full top-level directory set and write the version marker
- On missing partition: log clearly with all attempted identifiers and available block devices, then halt — never silently format
Done when: QEMU and Ally boot and show a correctly populated /data tree with the version marker present and validated.
S6-T2 — Define and create the final /data directory schema
/data/
├── .playos-storage-version
├── games/<game-id>/ {manifest.json, bin/, assets/, shaders/, licenses/}
├── saves/<game-id>/ {profiles/, autosaves/, settings/}
├── cache/<game-id>/ {shaders/, compiled-assets/, temporary/}
├── log/ runtime logs (singular — matches shipped playos-init)
├── system/ system state (created by playos-init today)
├── profiles/
├── resources/
├── downloads/
├── updates/
├── screenshots/
└── config/
Current state: playos_data_create_dirs() creates only /data/games, /data/saves, /data/system, and /data/log. Sprint 6 must add /data/cache, /data/profiles, /data/resources, /data/downloads, /data/updates, /data/screenshots, /data/config, and the .playos-storage-version marker.
Done when: all directories are created on first boot and the layout matches this schema exactly.
S6-T3 — Define game manifest v1 schema
Create playos-spec/schemas/game-manifest-v1.json (JSON Schema format) and document the required/optional fields:
{
"id": "com.example.game",
"name": "Example Game",
"version": "1.0.0",
"executable": "bin/game",
"api_version": 1,
"graphics": "gles3",
"architecture": "x86_64",
"controllers": true,
"network": false,
"description": "Short description.",
"icon": "assets/icon.png"
}
Validation rules:
idmust match the parent directory nameexecutablemust exist relative to the game directoryapi_versionmust be ≤ current system API versionarchitecturemust match the running system
Done when: the schema file exists and sample game manifests pass validation against it.
S6-T4 — Implement real playos_storage API
This already shipped in Sprint 5. The canonical, verified signature is (no game_id parameters, no config/logs getters):
const char *playos_storage_get_install_path(void); /* /data/games/<game-id> read-only */
const char *playos_storage_get_saves_path(void); /* /data/saves/<game-id> read-write */
const char *playos_storage_get_cache_path(void); /* /data/cache/<game-id> read-write */
const char *playos_storage_get_games_path(void); /* /data/games shell-only */
int64_t playos_storage_free_bytes(void); /* statvfs("/data"); -1 on error */
int playos_storage_atomic_replace(const char *src_path, const char *dst_path);
int playos_storage_atomic_write(const char *path, const void *data, size_t len);
- Per-game paths derive from
PLAYOS_GAME_ID, set byplayos-initat launch. playos_storage_atomic_writewrites to a temp file then renames into place.- Return
NULLfor unavailable paths; never construct paths to non-existent mounts.
Done when: verified present and matching the header (include/playos/playos_storage.h). No new API surface is needed this sprint.
S6-T5 — Implement live game discovery in the shell
Current state: playos-shell/src/screen_library.c already calls playos_storage_get_games_path(), enumerates subdirectories with opendir, and parses name / version / description from manifest.json using a minimal json_get_string helper (fallback display name = directory name). The remaining work:
- Validate against the manifest rules (id/api_version/architecture/executable); skip and log invalid entries without crashing
- Load
assets/icon.pngif present; use a placeholder if absent - Sort results by name
- Confirm the stub list is fully replaced (no hardcoded entries remain)
Done when: the shell shows discovered games dynamically from /data/games/ on every startup, with icons, sorted, and robust to invalid manifests.
S6-T6 — Build and install three real sample games
com.playos.sample-triangle— placeholder that reports display info and exits; Raylib GPU triangle rendering is deferred to a later sprint (does not yet prove the GPU/Wayland rendering path)com.playos.sample-input— reports a single controller snapshot and exits; live controller display is deferred to a later sprintcom.playos.sample-audio— placeholder binary; audio will be wired in Sprint 8
Each must have a valid manifest and a compiled binary that at minimum starts without crashing. These are built in playos-samples and packaged/installed into the rootfs overlay by playos-refdistro (no C source in refdistro).
Done when: all three appear in the shell library and can be selected in the detail screen. (Met for the launch/query path; GPU triangle rendering and live controller display are explicitly deferred and tracked above.)
S6-T7 — Add FactoryReset IPC command (cache/config scope)
FactoryResetis specified as a JSON IPC message inplayos-spec/src/runtime-ipc.mdwith flagserase_games,erase_saves,erase_cache,erase_config,erase_logs(all defaultfalse).- Implement the handler in
playos-init(IPC server owner): reject when a game is running, recursively delete the selected directories, recreate them. erase_cachetargets/data/cache;erase_configtargets/data/config.erase_games,erase_saves, anderase_logsare reported as"deferred"in the response and acted on in Sprint 10.
Done when: FactoryReset { erase_cache: true } clears /data/cache/ and the directory is recreated.
S6-T8 — Persistence and isolation validation
- Write a file via
playos_storage_atomic_writeto a game's save path; reboot; read it back - Verify two games have non-overlapping save paths
- Plant a broken manifest; verify the shell skips it without crashing
- Verify
playos_storage_free_bytes()returns a non-negative value
Done when: all persistence and isolation tests pass and are documented as evidence.
Implementation Guidance
Partition discovery order
Log every step: what was tried, what was found, why a candidate was accepted or rejected. The existing find_data_partition() already logs the winning strategy — extend those logs, don't replace them.
Atomic writes
The atomic write helper must be used for all persistent game data writes in the platform API. Raw fwrite to save paths is not acceptable.
Manifest loading
Keep the manifest parser minimal and defensive. Unknown fields must be ignored, not rejected. Only fail on missing required fields or invalid values.
Naming reconciliation
/data/log (singular) is the shipped spelling and is what this sprint locks. The spec docs (architecture.md, platform-api.md, security-model.md, and the sprint docs) have already been reconciled to /data/log; only the non-editable ideas.md retains the older plural.
Verification and Evidence
| Evidence | How it is produced |
|---|---|
| Mount proof | /proc/mounts showing /data at boot |
| Directory proof | ls /data showing all expected subdirectories |
| Version marker proof | contents of /data/.playos-storage-version |
| Discovery proof | three sample games appear in shell library |
| Persistence proof | file written before reboot is readable after reboot |
| Isolation proof | two games' save paths are non-overlapping |
| Invalid manifest proof | shell log showing skipped invalid entry |
Acceptance Criteria
-
/datapartition is mounted at boot; all directories exist -
/data/.playos-storage-versionis written and validated - Missing data partition causes a clear diagnostic halt with no silent format
-
Shell discovers all games in
/data/games/dynamically - Invalid manifests are skipped and logged without crashing the shell
- Game icons load if present; placeholder shown otherwise
- Per-game save paths are correctly isolated
- A saved file survives reboot
- Three real sample games appear in the shell library
-
FactoryReset { erase_cache: true }clears and recreates the cache directory -
playos_storage_free_bytes()returns a non-negative value
Handoff to Sprint 7
Sprint 7 may assume:
/datais reliably mounted with the final directory schema- game manifests are validated on discovery
PLAYOS_GAME_IDis an established pattern for per-game path isolation- three sample games exist and are discoverable
Sprint 7 should focus on the launch flow, lifecycle, and overlay — not on re-implementing storage.
Exit Gate
Games installed in /data/games/ are discovered and shown in the shell. Save and cache paths are correctly isolated per game. All data persists across reboots.
Previous: Sprint 5.6 | Next: Sprint 7
Sprint 7 — Game Launch, Lifecycle, System Button, and Overlay
Goal: Implement the complete console lifecycle: launch a game, background it with the System button, show the PlayOS overlay, resume the game, handle clean exit, and recover from crashes. The public playos-platform-api delivers lifecycle events to games; playos-runtime keeps these paths trusted and private.
Primary Outcome: A real game launches from the shell, runs with hardware acceleration, the System button surfaces the overlay, resume returns to the same running game, and both clean exit and crash return to the shell without any black screen or visible Linux prompt.
Status: 🟢 Complete — full console lifecycle verified on ROG Ally: launch, overlay, background, resume, clean exit, and crash recovery.
Prerequisites: Sprint 6 complete — persistent storage and game discovery working.
Why This Sprint Exists
Sprint 6 established a working game library. Sprint 7 turns selection into a real console experience: a game must launch, run exclusively, suspend to the system overlay, resume, and handle crashes — without ever revealing a Linux terminal. This sprint also pins the complete compositor state machine and private Wayland protocol, which subsequent sprints depend on.
Start Condition Checklist
- Sprint 6 complete: game library populated, manifest discovery working.
playos-compositorrenders the shell surface and can switch surfaces in principle.playos-platform-apilifecycle stubs exist but are not backed by real delivery.playos-overlaybinary does not yet exist.PLAYOS_BUTTON_SYSTEMhardware mapping confirmed (from Sprint 3evtestwork).
Decisions Locked for This Sprint
- One-game rule: only one game process may run at a time; a second launch request is rejected with a clear error log
- First-frame rule: compositor does NOT switch display to the game until the game commits its first Wayland buffer
- System button intercept:
PLAYOS_BUTTON_SYSTEMis consumed by the compositor seat — it is NEVER forwarded to any client - Lifecycle delivery: via file descriptor (
PLAYOS_LIFECYCLE_FD) passed at launch; games must not poll any other channel - Non-cooperative fallback:
SIGSTOP/SIGCONT— onlyplayos-initsends signals; compositor requests via IPC - Crash recovery deadline: display must return to shell within 500ms of game process exit
- Overlay z-order: game surface → dim layer → overlay surface — this order is locked
- Compositor control transport: Unix socket
/run/playos/compositor.sockonly — the private Waylandplayos_compositor_control_v1interface is removed from this sprint;playos_overlay_v1remains for the overlay client - Process ownership:
playos-initspawns and supervisesplayos-compositor,playos-shell, andplayos-overlay; the compositor maps/unmaps surfaces but never spawns processes
Decision Locked: Compositor Control Transport
playos-init / playos-runtime control the compositor exclusively over the Unix socket /run/playos/compositor.sock (SOCK_SEQPACKET, root:playos-trusted, 0660), as specified in runtime-ipc.md §2 and §7. Messages: SetExpectedGame, ClearExpectedGame, ForceTerminateGame, ShowOverlay, HideOverlay (init → compositor) and GameSurfaceReady, CompositorStateChanged (compositor → init). playos-init-spec.md binds this socket at boot and routes the non-cooperative SIGSTOP/SIGCONT request through it.
The private Wayland interface playos_compositor_control_v1 is removed from this sprint — it duplicated the socket's control surface over the Wayland wire. playos_overlay_v1 is kept: the overlay is a Wayland client, so its role registration (playos_manager_v1::register_overlay) and its set_surface / surface_ready / request_dismiss requests plus about_to_show / about_to_hide / output_info events belong on the Wayland protocol.
Follow-up reconciliation:
- Remove the Wayland interface
playos_game_launch_v1fromplayos-v1.xml(currently declared inplayos-runtime,playos-compositor, and vendored copies). It duplicates the socket'sSetExpectedGame/ClearExpectedGame/GameSurfaceReadycontrol surface, and is currently only declared in the XML — not implemented server-side (playos-compositor/README.md). The compositor must drive this path over/run/playos/compositor.sockusing theplayos_ipc_*framing library inplayos-init/ipc/. playos-compositordoes not yet connect to/run/playos/compositor.sock(its readiness handshake is currently a file). Add the socket client and retire the Wayland game-launch interface this sprint.
Scope
In Scope
- Full game launch flow (shell → init → compositor → game)
- Complete compositor state machine
- Reserved system button intercept
playos-overlaytrusted client (minimal: resume, quit, battery, thermal)- Real lifecycle event delivery via
PLAYOS_LIFECYCLE_FD - Game exit (clean and crash) with shell recovery
- Private Wayland protocol Sprint 7 changes (overlay retained; game-launch removed)
- Non-cooperative game SIGSTOP fallback
Explicitly Out of Scope
- Audio (Sprint 8)
- Power/thermal display (Sprint 9 detail — battery % stub is acceptable this sprint)
- Installer (Sprint 10)
- A/B updates (Sprint 11)
Required Repository Changes
| Repo | Required work |
|---|---|
playos-compositor | Full state machine, first-frame rule, system button intercept, overlay surface management, compositor socket client |
playos-shell | Launch IPC, launching-state spinner UI, crash notification, library restore |
playos-runtime | Lifecycle transport, compositor control IPC client (SetExpectedGame), playos_overlay_v1 Wayland extension |
playos-platform-api | Real lifecycle event delivery (fd-backed) |
playos-refdistro | playos-overlay trusted Raylib client + Buildroot package |
playos-spec | Lifecycle event spec, state machine diagram update |
Expected Files and Directories
playos-compositor
src/compositor_ipc.c # client for /run/playos/compositor.sock
src/state_machine.c # all states and transitions
src/system_button.c # input intercept at seat level
src/overlay_manager.c # z-order, show/hide logic
playos-runtime
protocols/playos-v1.xml # Sprint 7: keep playos_overlay_v1, remove playos_game_launch_v1
src/lifecycle_transport.c # fd creation, event write
playos-refdistro
br2-external/package/playos-overlay/
playos-overlay.mk
Config.in
src/playos-overlay/
main.c # Raylib overlay UI
Agent Task Breakdown
Task Status Grid
| Task ID | Task | Primary repo | Status | Notes / evidence |
|---|---|---|---|---|
| S7-T1 | Implement full game launch flow in playos-init | playos-init | done | GameStarted emitted; launch env + lifecycle fd wired |
| S7-T2 | Implement full compositor state machine | playos-compositor | done | All five states and transitions implemented |
| S7-T3 | Implement system button intercept at seat level | playos-compositor | done | PLAYOS_BUTTON_SYSTEM intercepted at seat level; never delivered to games |
| S7-T4 | Build playos-overlay trusted Raylib client | playos-refdistro | done | Raylib overlay UI builds and runs as trusted Wayland client |
| S7-T5 | Implement lifecycle fd delivery in platform-api | playos-platform-api | done | PLAYOS_LIFECYCLE_FD delivered to games |
| S7-T6 | Finalize private Wayland protocol (overlay kept, game-launch removed) | playos-runtime | done | playos_overlay_v1 kept; playos_game_launch_v1 removed |
| S7-T7 | Implement game exit and crash recovery | playos-compositor, playos-refdistro | done | GameExited/GameCrashed emitted; returns safely to shell |
| S7-T8 | Integration and lifecycle validation on Ally | playos-refdistro | done | End-to-end lifecycle verified on ROG Ally |
S7-T1 — Implement full game launch flow in playos-init
- Validate: one-game rule; reject immediately if a game is already running
- Validate: manifest exists, executable exists,
api_version≤ current system version - Prepare environment variables:
PLAYOS_GAME_ID,PLAYOS_INSTALL_PATH,PLAYOS_SAVE_PATH,PLAYOS_CACHE_PATH,WAYLAND_DISPLAY=playos-0,PLAYOS_LIFECYCLE_FD,PLAYOS_LAUNCH_TOKEN - Create lifecycle pipe; store write-end in
playos-init, pass read-end asPLAYOS_LIFECYCLE_FD - Emit
SetExpectedGame { launch_token, game_id }to the compositor over/run/playos/compositor.sock - Spawn game executable; track PID
- Emit
GameStarted { game_id, pid, launch_token }to the shell overcontrol.sock
Note: the
GameStarted/GameExited/GameCrashedmessage types are already declared inipc/ipc.h, butplayos-initcurrently never emits them —playos_supervisor_game_exitedonly writes toinit.log. S7-T1 wires upGameStarted; S7-T7 wires up the exit/crash emissions. Do not add new protocol types.
Done when: com.playos.sample-input launches from the shell, receives the environment variables, the compositor logs SetExpectedGame, and the shell receives GameStarted.
S7-T2 — Implement full compositor state machine
Implement all five states and all transitions:
SHELL_FOREGROUND
→ (SetExpectedGame received) → GAME_STARTING
GAME_STARTING
→ (first committed buffer) → GAME_FOREGROUND
→ (launch timeout/crash) → SHELL_FOREGROUND
GAME_FOREGROUND
→ (PLAYOS_BUTTON_SYSTEM) → PLAYOS_UI_FOREGROUND_WITH_GAME_BACKGROUND
→ (game process exits) → SHELL_FOREGROUND
PLAYOS_UI_FOREGROUND_WITH_GAME_BACKGROUND
→ (overlay Resume) → GAME_FOREGROUND
→ (overlay Quit → TerminateGame → game exits) → SHELL_FOREGROUND
SHELL_FOREGROUND (after game)
→ shell surface unfocused, restored
Log all transitions: state_machine: GAME_STARTING → GAME_FOREGROUND.
Done when: state logs are visible matching all transition paths in the test matrix.
S7-T3 — Implement system button intercept at seat level
- Register an input filter at the wlroots seat level that consumes
PLAYOS_BUTTON_SYSTEMbefore any client can receive it - When in
GAME_FOREGROUND: remove input focus from the game, emitCompositorStateChanged(PLAYOS_UI_FOREGROUND_WITH_GAME_BACKGROUND) toplayos-init, and transition to the overlay state;playos-initthen writesPLAYOS_LIFECYCLE_BACKGROUNDto the lifecycle fd and arms the non-cooperativeSIGSTOPtimer - Verify via
evtestand a game that logs all key events —PLAYOS_BUTTON_SYSTEMmust never appear in the game log
Done when: pressing the system button in-game activates the overlay and the game never sees the key event.
S7-T4 — Build playos-overlay trusted Raylib client
Initial overlay content:
- Game title and elapsed running time
- "Resume Game" (A button) — sends
playos_overlay_v1::request_dismissto the compositor (the compositor hides the overlay and returns focus to the game) - "Quit Game" (B or menu) — sends
TerminateGameIPC toplayos-init - Battery percentage (stub value acceptable this sprint)
- Thermal status (stub value acceptable this sprint)
Surface policy:
- Pre-spawned at boot, hidden; compositor shows/hides it via
playos_overlay_v1 - Overlay registers as the trusted overlay via
playos_manager_v1::register_overlay - Overlay implements
playos_overlay_v1::set_surface/surface_ready/request_dismissand handlesabout_to_show/about_to_hideto reset state cleanly
Done when: overlay is visible above a running game when system button is pressed, and both Resume and Quit work.
S7-T5 — Implement lifecycle fd delivery in platform-api
Replace stub playos_lifecycle_poll with a real implementation:
/* Non-blocking. Returns 1 if event available, 0 if none, -1 on error. */
int playos_lifecycle_poll(PlayOSLifecycleEvent *event);
- Read from
PLAYOS_LIFECYCLE_FDin non-blocking mode - Deserialize the event type (single-byte or small struct)
- Support:
PLAYOS_LIFECYCLE_FOREGROUND,PLAYOS_LIFECYCLE_BACKGROUND,PLAYOS_LIFECYCLE_TERMINATE
Non-cooperative fallback: if playos-init receives no CPU reduction signal within 500ms of sending BACKGROUND, it sends SIGSTOP to the game PID. On resume, it sends SIGCONT.
Done when: com.playos.sample-input logs lifecycle events it receives and the sequence is correct across the full launch/background/resume/quit cycle.
S7-T6 — Finalize private Wayland protocol (overlay kept, game-launch removed)
Compositor control stays on /run/playos/compositor.sock (see the decision locked above); no Wayland control interface is added this sprint.
playos-overlay remains a Wayland client, so playos_overlay_v1 is retained with its correct interface shape in playos-runtime/protocols/playos-v1.xml:
<interface name="playos_overlay_v1" version="1">
<request name="set_surface">
<arg name="surface" type="object" interface="wl_surface"/>
</request>
<request name="surface_ready"/>
<request name="request_dismiss"/>
<event name="about_to_show"/>
<event name="about_to_hide"/>
<event name="output_info">
<arg name="width" type="int"/>
<arg name="height" type="int"/>
<arg name="refresh_mhz" type="uint"/>
<arg name="scale_100" type="uint"/>
</event>
</interface>
The overlay's trusted role is registered through playos_manager_v1::register_overlay, not a register_as_overlay request on the overlay interface.
Remove the playos_game_launch_v1 interface from the XML. Its four operations — set_expected_game, clear_expected_game, game_surface_ready, game_surface_destroyed — duplicate the socket's three messages SetExpectedGame / ClearExpectedGame / GameSurfaceReady, so this path belongs on /run/playos/compositor.sock, not the Wayland wire.
Regenerate Wayland scanner outputs. Only the trusted playos-overlay client uses playos_overlay_v1.
Done when: compositor binds playos_overlay_v1; overlay registers via playos_manager_v1::register_overlay; playos_game_launch_v1 is gone from the protocol; CI protocol scanner passes.
S7-T7 — Implement game exit and crash recovery
Clean exit:
playos-initrecords exit status; closes lifecycle pipe write-endplayos-initemitsGameExited { game_id, exit_code }to the shell overcontrol.sock(types already declared inipc/ipc.h— no new protocol)- Compositor detects Wayland client disconnect; transitions to
SHELL_FOREGROUND - Shell surface is unhidden and refocused; library scroll position restored
- Shell shows no notification on clean exit
Crash (non-zero exit or signal):
- Same compositor recovery
playos-initemitsGameCrashed { game_id, exit_code, signal }to the shell overcontrol.sock- Shell shows a non-intrusive toast notification: "Game exited unexpectedly"
- Options: "Restart" (re-sends
LaunchGame) or "Back to Library" (dismiss)
Invariant: A game crash must NEVER reveal a Linux terminal, leave the display black for >500ms, or require reboot.
Done when: kill -9 <game_pid> while the game is running returns display to the shell within 500ms with the crash toast visible.
S7-T8 — Integration and lifecycle validation
Run the full test matrix on the ROG Ally:
- Launch → play → quit via Quit button → library shown
- Launch → system button → overlay visible → resume → back in game
- Launch → system button → overlay visible → quit → library shown
kill -9 <game_pid>→ crash toast within 500ms- Try to launch a second game while one is running → reject logged, first game unaffected
- Launch → background → game ignores lifecycle →
SIGSTOPfires within 500ms - Repeat items 1–3 three cycles in a row → no state leaks
Compositor state-machine transitions are additionally covered by QEMU unit tests (no physical GPU required).
Done when: all 7 test cases pass on the Ally with evidence logged.
Implementation Guidance
Launch token
The launch token is a one-time random UUID generated per launch. It expires when the first Wayland client presents with that token. Clients presenting an unknown or expired token during the launch window are rejected and logged, not crashed.
Display switch timing
The shell surface must remain visible until the game's first Wayland buffer is committed. Do not hide the shell on GameStarted — hide it only on the first-frame transition.
Overlay pre-spawn
playos-overlay is pre-spawned by playos-init at boot (like playos-shell) and supervised with restart-on-exit. The compositor maps/unmaps its surface; it never spawns or kills the overlay process. This avoids visible latency when the system button is pressed.
Game cooperative behavior on background
On PLAYOS_LIFECYCLE_BACKGROUND, a cooperative game should pause gameplay, stop/mute audio, reduce rendering (0 FPS acceptable while backgrounded), and write an autosave if implemented. If CPU does not drop within GAME_PAUSE_TIMEOUT_MS (default 500ms), playos-init falls back to SIGSTOP.
Code hygiene (deferred cleanup)
While touching playos-init for S7-T1/S7-T7, remove the dead code that Sprint 6 left behind so the launch/lifecycle path has a single obvious implementation:
playos_spawn_child()insrc/child_process.c— unused; the supervisor spawns everything directly.playos_supervisor_spawn_test_client()/spawn_test_client()insrc/supervisor.c— unused; theipc-test-clientself-test is spawned frommain.c.playos_recovery_enter()/playos_recovery_loop()insrc/recovery.c— unused; the live recovery path isplayos_enter_recovery()insrc/supervisor.c. Deleterecovery.c(and its header) or fold the banner intoplayos_enter_recovery().
Done when: grep finds no callers of the above and the tree still builds/tests green.
Verification and Evidence
| Evidence | How it is produced |
|---|---|
| State machine logs | compositor systemd journal showing all transitions for the test matrix |
| Lifecycle event log | com.playos.sample-input log showing FOREGROUND/BACKGROUND/FOREGROUND sequence |
| System button intercept proof | game key log showing system button event is absent |
| Crash recovery timing | timestamp of game exit vs. timestamp of shell surface shown (≤500ms) |
| One-game rule | log showing second launch rejected |
SIGSTOP fallback | strace or log showing signal sent within 500ms |
Acceptance Criteria
- Selecting a game launches it; first-frame switch happens with no black flash
-
Game receives
PLAYOS_LIFECYCLE_FOREGROUNDat launch - System button press shows overlay above game (dim + overlay visible)
-
Game receives
PLAYOS_LIFECYCLE_BACKGROUNDon system button press -
Overlay "Resume" returns to game;
PLAYOS_LIFECYCLE_FOREGROUNDdelivered - Overlay "Quit" terminates game cleanly; shell shown
- Clean exit returns to shell with library scroll position restored
-
kill -9 <game_pid>returns display to shell within 500ms; crash toast shown - Second launch attempt while game is running is rejected; first game unaffected
-
PLAYOS_BUTTON_SYSTEMnever appears in any game client's input stream -
Non-cooperative game receives
SIGSTOPwithin 500ms ofBACKGROUNDevent - Compositor state transitions are logged
- Full 3-cycle lifecycle test passes without state leaks
- CI passes
Handoff to Sprint 8
Sprint 8 may assume:
- Full lifecycle (launch, background, resume, terminate) works on the Ally
PLAYOS_LIFECYCLE_FDdelivers events reliably- The overlay exists and can be extended (add audio volume controls)
rcore_playos.cis already inplayos-shellfrom Sprint 5; Sprint 8 adds the ALSA backend to it- The compositor state machine is stable and not to be modified in Sprint 8
Exit Gate
The complete console lifecycle works on the ROG Ally: launch, system button overlay, resume, clean exit, and crash recovery all behave correctly. The player never sees a Linux prompt.
Previous: Sprint 6 | Next: Sprint 8
Sprint 8 — ALSA Audio
Goal: Integrate reliable ALSA audio into the Raylib PlayOS backend. Games and the shell can play audio through the ROG Ally's built-in speakers and headphones. Audio behaves correctly across lifecycle transitions.
Primary Outcome: com.playos.sample-audio plays a looping sine tone from the ROG Ally speakers. Audio pauses when the game is backgrounded, resumes when foregrounded, and stops cleanly when the game exits.
Status: 🟢 Complete — ALSA audio verified on-device via Raylib miniaudio; no custom audio backend needed (playos_audio.h is control/augmentation only). Headphone-jack detection (S8-T4) and shell UI sounds (S8-T6) were not part of the verified milestone.
Prerequisites: Sprint 7 complete — full console lifecycle working (background/foreground events delivered).
Why This Sprint Exists
Sprint 7 delivers a working console lifecycle but with no audio. Games on a gaming device must have audio. Sprint 8 enables ALSA audio through Raylib's miniaudio backend and implements the system audio controls, making audio a first-class feature that works correctly across all lifecycle transitions.
Start Condition Checklist
- Sprint 7 complete: full lifecycle (launch/background/resume/quit/crash) working on the Ally.
- Raylib 6.0 vendored in
playos-shell; miniaudio audio module present but disabled (SUPPORT_MODULE_RAUDIO=0). playos_audio.h/playos_audio.cstub exists inplayos-platform-api.com.playos.sample-audioplaceholder exists inplayos-samples/audio-sine.playos-overlaysource lives inplayos-refdistro/src/playos-overlay/(Sprint 7).- ROG Ally audio hardware confirmed working in Linux (ALSA recognizes the device).
Decisions Locked for This Sprint
- ALSA only: no PulseAudio, no PipeWire, no SDL audio; enable Raylib's miniaudio ALSA backend (
raudio.c→miniaudio.h, vendored inplayos-shell) and linkalsa-lib. Raylib/miniaudio has no PipeWire backend (it supports only ALSA, PulseAudio, JACK), so this is the default — but compile out PulseAudio withMA_NO_PULSEAUDIO(alongside the existingMA_NO_JACK) to guarantee only ALSA is built in. - Sample rate: 44100 Hz (document this; do not change without an ADR)
- Format: signed 16-bit little-endian stereo (
SND_PCM_FORMAT_S16_LE, 2 channels) - Volume ownership: system-wide. The shell/overlay owns the volume UI and is always honored; a game's
playos_audio_set_master_volume()/set_muted()request is honored only while that game is foreground. Games may always read state viaplayos_audio_get_info(). - Device priority:
PLAYOS_AUDIO_DEVICEenv var → headphone jack → built-in speakers - Audio thread policy: miniaudio owns the mixing thread; if a real-time class is needed use
SCHED_FIFO/SCHED_RRat a safe priority and document the value
Scope
In Scope
- ALSA PCM backend via Raylib's miniaudio module (
raudio.c), enabled inplayos-shell playos_audio.hpublic API — implement the existingplayos-platform-apistub- Audio lifecycle behavior (pause on BACKGROUND/SUSPEND, resume on FOREGROUND/RESUME, stop on TERMINATE)
- Headphone jack detection and routing switch
- Volume control in overlay
com.playos.sample-audiosample game- Shell UI sounds (startup chime or click feedback)
Explicitly Out of Scope
- PulseAudio / PipeWire integration (post-MVP)
- HDMI audio (post-MVP)
- Bluetooth audio (post-MVP)
- Per-game volume settings (post-MVP)
- Multi-application audio mixing service (post-MVP)
Required Repository Changes
| Repo | Required work |
|---|---|
playos-shell | Enable Raylib miniaudio ALSA backend (SUPPORT_MODULE_RAUDIO=1); shell UI sounds |
playos-platform-api | Implement playos_audio.c (system state, master volume/mute) — replace stub |
playos-refdistro | alsa-lib in Buildroot config; overlay volume control in src/playos-overlay/ |
playos-samples | Finish com.playos.sample-audio (actual sine playback) |
playos-spec | Audio policy doc (lifecycle behavior, volume model) |
Expected Files and Directories
playos-platform-api
include/playos/playos_audio.h # exists (stub declarations) — finalize contract
src/playos_audio.c # implement playos_audio_get_info, set_master_volume, set_muted
playos-shell
external/raylib/src/config.h # SUPPORT_MODULE_RAUDIO 0 → 1
external/raylib/src/raudio.c # miniaudio ALSA backend (enable + link alsa-lib)
src/*.c # shell UI sounds (startup chime, navigation clicks)
playos-refdistro
br2-external/package/playos-raylib/ # link alsa-lib
src/playos-overlay/ # volume display + D-pad control
playos-samples
audio-sine/src/main.c # actual 440 Hz sine playback (placeholder today)
audio-sine/manifest.json # already installed as com.playos.sample-audio
Agent Task Breakdown
Task Status Grid
| Task ID | Task | Primary repo | Status | Notes / evidence |
|---|---|---|---|---|
| S8-T1 | Enable ALSA audio via Raylib miniaudio backend | playos-shell | done | SUPPORT_MODULE_RAUDIO=1, alsa-lib linked; sine tone plays on-device |
| S8-T2 | Define and implement playos_audio.h API | playos-platform-api | done | Contract finalized and implemented; mixer-element selection fixed (fe6cc7a) |
| S8-T3 | Implement audio lifecycle behavior (background/foreground/suspend/resume/terminate) | playos-platform-api | done | Pause/resume/stop verified across lifecycle transitions |
| S8-T4 | Implement headphone jack detection and routing | playos-platform-api | deferred | Not landed in the verified audio milestone |
| S8-T5 | Add volume control to playos-overlay | playos-refdistro | done | D-pad volume controls wired in overlay |
| S8-T6 | Add shell UI sounds | playos-shell | deferred | Not landed in the verified audio milestone |
| S8-T7 | Build com.playos.sample-audio sample game | playos-samples | done | audio-sine plays a 440 Hz sine tone on-device |
| S8-T8 | Audio validation on Ally | playos-refdistro | done | Sine tone verified through built-in speakers |
S8-T1 — Enable the ALSA audio backend in Raylib's miniaudio module
Enablement:
- Set
SUPPORT_MODULE_RAUDIO=1inplayos-shell's vendored Raylib build; addalsa-libto the Buildroot target. Also#define MA_NO_PULSEAUDIOnext to the existingMA_NO_JACKinraudio.cso miniaudio compiles only the ALSA backend. - Confirm miniaudio's ALSA backend opens the default playback device (
default, orPLAYOS_AUDIO_DEVICEif set). - If the default device fails: enumerate with
snd_device_name_hint()and select the first stereo hardware output. - Params: 44100 Hz, stereo
SND_PCM_FORMAT_S16_LE(miniaudio resamples to the device native rate/format as needed). - Prepare and start playback; miniaudio owns the mixing thread.
Audio thread / underrun handling:
- Set a documented
SCHED_FIFO/SCHED_RRpriority only if a real-time class is needed. - Handle underrun (
EPIPE) and suspend (ESTRPIPE) recovery; log device switches.
Done when: com.playos.sample-audio produces audible output from the built-in speakers.
S8-T2 — Define and implement playos_audio.h API
typedef struct {
int sample_rate;
int channels;
int bits_per_sample;
float master_volume; /* 0.0 – 1.0 */
int muted;
} PlayOSAudioInfo;
int playos_audio_get_info(PlayOSAudioInfo *info);
int playos_audio_set_master_volume(float volume);
int playos_audio_set_muted(int muted);
Volume and mute are system-wide. The shell/overlay owns the volume UI and its calls are always honored. A game's setter calls are requests, honored only while the game is foreground; games may always call playos_audio_get_info() to read state.
Done when: API compiles, playos_audio_get_info() returns a populated struct, and set_master_volume(0.5f) produces an audible volume change.
Follow-up — mixer-element selection (done): the initial implementation of audio_find_master() / audio_find_switch() returned the first ALSA simple-mixer element carrying a playback volume/switch control. On the ROG Ally's Realtek codec that first element was Headphone, which does not drive the speaker path, so set_master_volume() / set_muted() wrote a control with no audible effect on the in-game PCM stream. Fixed by preferring well-known element names — Master, Speaker, PCM, Front, Headphone, Headphones — before falling back to first-match, and by logging the chosen element name. The shell Settings → Audio tab now displays the live PlayOSAudioInfo value instead of a hardcoded 75% (still read-only). Evidence: playos-platform-api fe6cc7a, playos-shell 1db41e3.
S8-T3 — Implement audio lifecycle behavior
| Lifecycle event | Audio action |
|---|---|
PLAYOS_LIFECYCLE_FOREGROUND | Resume ALSA playback at previous volume |
PLAYOS_LIFECYCLE_BACKGROUND | Drain buffer; block writes (thread pauses) |
PLAYOS_LIFECYCLE_SUSPEND | Pause playback (same as background; flush before returning) |
PLAYOS_LIFECYCLE_RESUME | Resume playback |
PLAYOS_LIFECYCLE_TERMINATE | Stop thread; close PCM handle |
Shell behavior: while shell is foreground, its audio is active; when game starts, shell audio pauses; when game exits, shell audio resumes.
Done when: lifecycle test — launch audio sample, press system button (audio stops within 200ms), resume (audio restarts within 200ms), quit (audio stops cleanly).
S8-T4 — Implement headphone jack detection and routing
- Monitor ALSA mixer or inotify on
/dev/snd/for device add/remove - On headphone plug: close current PCM; reopen on headphone device; resume stream
- On headphone unplug: close headphone PCM; reopen on speakers; resume stream (brief gap acceptable)
- Log all device switches
Done when: plugging a USB-C audio adapter (or 3.5mm) routes audio to it; unplugging returns audio to speakers.
S8-T5 — Add volume control to playos-overlay
- Display current volume as a percentage bar
- D-pad Up/Down adjusts in 5% steps via
playos_audio_set_master_volume() - L1 or dedicated button toggles mute via
playos_audio_set_muted() - Volume changes take effect immediately (no lag)
Done when: D-pad adjusts volume in the overlay and the change is audible in real-time.
S8-T6 — Add shell UI sounds
- Short startup chime on shell launch
- Click/confirm sound on menu navigation and selection
- Error sound on rejected actions
- All sounds implemented via Raylib audio API using the ALSA backend
Done when: navigating the shell produces audible feedback sounds.
S8-T7 — Finish com.playos.sample-audio sample game
- Finish the existing
playos-samples/audio-sineplaceholder (already installed ascom.playos.sample-audio) - Generates a 440 Hz sine wave via Raylib audio API, played as a looping 2-second buffer
- Shows on-screen: frequency, volume, device name, current lifecycle state
- Responds to lifecycle events: pauses sine tone when backgrounded
- Uses
playos_audio_get_info()to display device info
Done when: game appears in shell library, plays the sine tone, and the on-screen info is accurate.
S8-T8 — Audio validation on Ally
- Confirm built-in speakers produce output
- Measure underrun count over 2 minutes of continuous playback (target: 0; ≤ 1/min acceptable)
- Headphone plug/unplug routing test (×3 cycles)
- Lifecycle transition test: background → silence; resume → audio; quit → stop
- Volume: min/max/mid; verify no clipping at max
- QEMU CI: ALSA backend compiles; PCM open fails gracefully with log; no crash
Done when: all validation cases pass and evidence is logged.
Implementation Guidance
ALSA period sizing
Tune miniaudio's buffer and period size empirically on the Ally (Raylib exposes the device buffer/period size in config.h). Start with a small buffer and increase if underruns occur. Document the final values.
SIGSTOP and audio
If the compositor sends SIGSTOP to a non-cooperative game, the audio thread is also stopped. This is acceptable behavior — the OS enforces silence automatically.
CI audio
The ALSA backend must compile in CI. snd_pcm_open() will fail in a headless VM — this is expected. Log the failure with device name and continue; do not assert.
Verification and Evidence
| Evidence | How it is produced |
|---|---|
| Speaker output | Confirmed by ear on the Ally |
| Underrun count | ALSA snd_pcm_status after 2-minute run |
| Headphone routing | Plug/unplug ×3, confirm routing in log |
| Lifecycle timing | Timestamp of BACKGROUND event vs. first silent frame (≤200ms) |
| Volume API | playos_audio_get_info() output before and after set_master_volume() |
| CI build | CI log showing successful compile |
Acceptance Criteria
- Shell plays audio (UI sounds) on the Ally speakers
-
com.playos.sample-audioplays a continuous sine tone through built-in speakers - Plugging in headphones routes audio to headphones
- Unplugging headphones routes audio back to speakers (brief gap acceptable)
- System button → overlay: game audio stops within 200ms
- Overlay "Resume": game audio restarts within 200ms
- Quit game: audio stops; shell audio resumes
- No audio underruns during normal playback (< 1/min acceptable)
-
playos_audio_set_master_volume(0.5f)produces an audible change -
playos_audio_set_muted(1)silences all audio - Volume overlay shows current level; D-pad adjusts it
- CI passes (ALSA backend compiles; PCM open failure is handled gracefully)
Handoff to Sprint 9
Sprint 9 may assume:
- Audio is fully functional and lifecycle-aware
playos-overlaycan be extended with new status displays and controlsplayos-initthermal monitoring loop can be added without conflicting with audio- The full lifecycle including
SIGSTOP/SIGCONTis stable
Exit Gate
Games and the shell play audio on the ROG Ally. Audio transitions correctly across all lifecycle events. Volume is controlled through the overlay.
Previous: Sprint 7 | Next: Sprint 9
Sprint 9 — Power, Battery, Thermal, and Suspend Foundations
Goal: Expose safe power, battery, and thermal information through the playos-platform-api. Implement AMD P-state integration for basic performance profiles. Establish thermal limits that prevent overheating. Lay the groundwork for suspend/resume (full suspend deferred to post-MVP).
Primary Outcome: Shell and overlay show live battery level and thermal state. A performance profile can be requested. Thermal throttling kicks in before dangerous temperatures. The ROG Ally does not overheat under sustained load.
Status: 🟢 Implemented and verified on Ally — Sprint 9 complete.
Prerequisites: Sprint 8 complete — full audio and lifecycle working. ✅ Satisfied (audio verified on Ally; gamepad wired into raylib).
Why This Sprint Exists
Sprint 8 delivers audio but the device still has no power awareness — battery can die silently and the CPU can overheat. Sprint 9 adds live battery/thermal telemetry, safe P-state control, and the overlay power menu. It also lays the suspend/resume skeleton so that the lifecycle enum is complete for all subsequent work.
Start Condition Checklist
- ✅ Sprint 8 complete: ALSA audio verified on the Ally (dmix + mute-via-switch fixes); audio diagnostics landed as
src/playos-init/src/audio_debug.c(commits512ae05,f80e44a); gamepad wired into raylibCORE.Input.Gamepad.*. - ✅ Graphics stack upgraded to GLES 3.0 (
playos-shell4ad17f6) — the S9-T8 sustained-load/thermal validation now exercises the ES3 path (EGL negotiates ES3.2 on RDNA3). - ⚠️
CONFIG_X86_AMD_PSTATE=yconfirmed atbr2-external/board/ally/linux.config:190(plusACPI_BATTERY,ACPI_AC,POWER_SUPPLY,THERMAL), butCONFIG_X86_AMD_PSTATE_EPP=yis missing — it must be enabled for theenergy_performance_preferencesysfs node this sprint writes. /sys/class/power_supply/BAT0/exists on the Ally (verify during bringup — cannot check without hardware).- ✅
playos-overlayexists with D-pad volume controls and already renders a hardcodedBattery: 85% Thermal: Normalplaceholder (main.c:386) to replace with live data.
Decisions Locked for This Sprint
- sysfs only: battery, CPU/GPU temp, P-state reads via sysfs — no vendor ACPI/WMI calls this sprint
- Poll interval: thermal monitor runs at 1 Hz in
playos-init; shell refreshes battery display every 30 seconds - P-state write authority: only
playos-init(root) writes to sysfs P-state paths; games request via IPC - Thermal thresholds: values defined here are the locked defaults for MVP; overridable via
/data/config/thermal.json - Suspend: skeleton only — deliver lifecycle events, attempt
echo mem > /sys/power/state; stability is post-MVP - TDP tuning: defer vendor WMI/ACPI TDP control unless the device runs critically hot at
balance_performance - Kernel P-state support: enable
CONFIG_X86_AMD_PSTATE_EPP=yinbr2-external/board/ally/linux.configsoenergy_performance_preferenceexists (today onlyCONFIG_X86_AMD_PSTATE=yis set). - Event channel:
ThermalStateChangedandPerfProfileChangedreuse the existing Sprint-7 shell listener (playos_trusted_register_shell/playos_trusted_shell_poll); no new IPC transport.
Scope
In Scope
playos_power.hAPI (battery, thermal, P-state)- sysfs-backed implementation
- AMD P-state integration (EPP writes)
- Thermal monitoring loop in
playos-init - Shell status bar (battery %, thermal indicator)
- Overlay additions (temperatures, profile selector, power menu)
- Suspend/resume skeleton (lifecycle events +
/sys/power/stateattempt)
Explicitly Out of Scope
- Full suspend/resume stability (post-MVP)
- Vendor TDP WMI/ACPI tuning (post-MVP unless safety-critical)
- AC adapter wattage tracking (post-MVP)
- Fan curve control (post-MVP)
Required Repository Changes
| Repo | Required work |
|---|---|
playos-platform-api | playos_power.h, sysfs-backed implementation |
playos-refdistro | playos-init thermal monitor, enable CONFIG_X86_AMD_PSTATE_EPP=y in board/ally/linux.config, thermal.json default |
playos-shell | Battery/thermal status bar |
playos-refdistro (src/playos-overlay, committed in-tree) | Temperature display, profile selector, power menu |
playos-runtime | Add SetPerfProfile request + ThermalStateChanged/PerfProfileChanged events via the existing shell listener (Shutdown/Reboot already exist from Sprint 5) |
playos-spec | Thermal policy doc, power API spec |
Expected Files and Directories
playos-platform-api
include/playos/playos_power.h
src/playos_power.c # sysfs reader, IPC request for profile changes
playos-refdistro
src/playos-init/src/thermal.c # 1 Hz monitor loop, P-state writer, IPC notifier
br2-external/board/ally/linux.config # add CONFIG_X86_AMD_PSTATE_EPP=y
br2-external/board/common/rootfs-overlay/data/config/thermal.json
Agent Task Breakdown
Task Status Grid
| Task ID | Task | Primary repo | Status | Notes / evidence |
|---|---|---|---|---|
| S9-T1 | Define and implement playos_power.h API | playos-platform-api | done | playos_power.h + full playos_power.c: sysfs battery/thermal/EPP reads + IPC client |
| S9-T2 | Implement sysfs-backed battery and temperature reads | playos-platform-api | done | BAT0 capacity/status/time-to-*, x86_pkg_temp/cpu_thermal/k10temp, amdgpu hwmon; 1 s cache |
| S9-T3 | Implement AMD P-state EPP write and profile IPC | playos-refdistro, playos-runtime | done | thermal.c EPP writer; SetPerfProfile handler in ipc_handler.c; EPP enabled via DEFAULT_MODE=3 |
| S9-T4 | Implement thermal monitoring loop in playos-init | playos-refdistro | done | src/playos-init/src/thermal.c 1 Hz tick, thermal.json thresholds, state machine + events |
| S9-T5 | Update shell status bar (battery, thermal indicator) | playos-shell | done | battery %/charging + colour-coded thermal + profile (main.c) |
| S9-T6 | Update overlay (temps, profile selector, power menu) | playos-refdistro (src/playos-overlay) | done | live power/thermal, D-pad profile selector, Sleep/Restart/Shutdown menu |
| S9-T7 | Implement suspend/resume skeleton | playos-refdistro, playos-platform-api | done | PLAYOS_IPC_TYPE_SUSPEND → playos_suspend(); lifecycle events handled |
| S9-T8 | Power and thermal validation on Ally | playos-refdistro | done | status bar/overlay live values, reboot/shutdown, sustained-load verified on Ally |
S9-T1 — Define and implement playos_power.h API
Status:
include/playos/playos_power.halready exists and matches this spec exactly;src/playos_power.cis a stub returning -1. Remaining work is the implementation, not the API surface.
typedef enum { PLAYOS_POWER_STATE_ON_BATTERY, PLAYOS_POWER_STATE_CHARGING,
PLAYOS_POWER_STATE_CHARGED, PLAYOS_POWER_STATE_UNKNOWN } PlayOSPowerState;
typedef enum { PLAYOS_THERMAL_NORMAL, PLAYOS_THERMAL_WARM,
PLAYOS_THERMAL_HOT, PLAYOS_THERMAL_CRITICAL } PlayOSThermalState;
typedef enum { PLAYOS_PERF_BALANCED, PLAYOS_PERF_POWER_SAVE,
PLAYOS_PERF_PERFORMANCE } PlayOSPerfProfile;
typedef struct {
PlayOSPowerState power_state;
int battery_percent; /* 0–100; -1 if unknown */
int minutes_remaining; /* -1 if unknown or charging */
PlayOSThermalState thermal_state;
int cpu_temp_c;
int gpu_temp_c;
PlayOSPerfProfile active_profile;
} PlayOSPowerInfo;
int playos_power_get_info(PlayOSPowerInfo *info);
int playos_power_request_profile(PlayOSPerfProfile profile);
playos_power_request_profile() sends an IPC message to playos-init; it does NOT write sysfs directly.
Done when: API compiles, playos_power_get_info() returns a populated struct with real sysfs values on the Ally.
S9-T2 — Implement sysfs-backed battery and temperature reads
| Data | sysfs path |
|---|---|
| Battery capacity | /sys/class/power_supply/BAT0/capacity |
| Charging state | /sys/class/power_supply/BAT0/status |
| Time to empty | /sys/class/power_supply/BAT0/time_to_empty_now (if available) |
| CPU temperature | /sys/class/thermal/thermal_zone*/temp (match x86_pkg_temp type) |
| GPU temperature | /sys/class/drm/card*/device/hwmon/hwmon*/temp1_input |
| AMD EPP current | /sys/devices/system/cpu/cpu0/cpufreq/energy_performance_preference |
- Cache reads for 1 second; avoid re-reading sysfs on every
playos_power_get_info()call - Return -1 for any unavailable value; never crash on missing sysfs node
Done when: playos_power_get_info() returns correct battery % and temperatures matching a second data source (e.g., sensors output).
S9-T3 — Implement AMD P-state EPP write and profile IPC
Prereq:
energy_performance_preferenceonly exists whenCONFIG_X86_AMD_PSTATE_EPP=y. Add it tobr2-external/board/ally/linux.configfirst; without it the driver exposes onlyscaling_governor.
Profile → EPP mapping:
| PlayOS profile | AMD EPP value |
|---|---|
PLAYOS_PERF_BALANCED | balance_performance |
PLAYOS_PERF_POWER_SAVE | power |
PLAYOS_PERF_PERFORMANCE | performance |
playos-initreceivesSetPerfProfile { profile }IPC- Validates the request against current thermal state (reject
PERFORMANCEifHOTorCRITICAL) - Writes EPP string to all online CPUs:
/sys/devices/system/cpu/cpuN/cpufreq/energy_performance_preference - Emits
PerfProfileChanged { profile }event over the Sprint-7 shell listener
Done when: SetPerfProfile { PLAYOS_PERF_PERFORMANCE } is accepted when thermal state is NORMAL; rejected when HOT.
S9-T4 — Implement thermal monitoring loop in playos-init
- 1 Hz poll loop in
src/playos-init/src/thermal.c - Read CPU and GPU temperatures via sysfs (reuse the reader from S9-T2)
- Compute
PlayOSThermalStateusing thresholds from/data/config/thermal.json(or built-in defaults)
Default thresholds:
- NORMAL: < 75°C
- WARM: 75–85°C
- HOT: 85–95°C
- CRITICAL: ≥ 95°C
Actions:
- WARM: log; emit
ThermalStateChangedevent - HOT: apply
PLAYOS_PERF_BALANCED; emit event; log - CRITICAL: apply
PLAYOS_PERF_POWER_SAVE; emit event with warning flag; if unresolved in 10 s, call graceful shutdown
Done when: running a stress test on the Ally causes the thermal state to progress to WARM and the log shows the state change and P-state adjustment.
S9-T5 — Update shell status bar
Add to the shell's bottom status bar:
- Battery percentage + charging indicator (⚡ when charging)
- Thermal state color indicator (green = NORMAL, yellow = WARM, red = HOT/CRITICAL)
- Active performance profile indicator
- Refresh every 30 seconds (or immediately on a
ThermalStateChanged/PerfProfileChangedevent from the Sprint-7 shell listener)
Done when: plugging and unplugging the charger updates the charging indicator within 30 s; running a stress test changes the thermal indicator to yellow.
S9-T6 — Update overlay (temperatures, profile selector, power menu)
Status: overlay already renders a hardcoded
Battery: 85% Thermal: Normalline (main.c:386) and D-pad volume controls; replace the placeholder with liveplayos_power_get_info()data. The profile selector's D-pad + A confirm is now available via the Sprint-8 gamepad wiring.
Add to playos-overlay:
- Battery percentage and estimated time remaining
- CPU and GPU temperature (live, refreshed every 5 s while overlay is shown)
- Performance profile selector: D-pad to cycle, A to confirm → sends
SetPerfProfileIPC - Power menu: "Sleep" (disabled placeholder), "Restart", "Shutdown"
- Restart: sends
RebootIPC toplayos-init - Shutdown: sends
ShutdownIPC toplayos-init
Done when: overlay shows live temperatures; profile selector changes the active profile; Restart reboots the device cleanly.
S9-T7 — Implement suspend/resume skeleton
PLAYOS_LIFECYCLE_SUSPEND(0x02) andPLAYOS_LIFECYCLE_RESUME(0x03) are already defined inplayos-init/ipc/ipc.h; this task only ensures every lifecycle consumer handles them without crashing- On lid close event or suspend button: deliver
PLAYOS_LIFECYCLE_SUSPENDto any running game; attemptecho mem > /sys/power/state - If the write fails: log the error; continue running
- On resume (if successful): deliver
PLAYOS_LIFECYCLE_RESUME - The "Sleep" power menu item sends the suspend trigger
Done when: PLAYOS_LIFECYCLE_SUSPEND and PLAYOS_LIFECYCLE_RESUME are delivered without crashing any lifecycle consumer. Actual device sleep is best-effort this sprint.
S9-T8 — Power and thermal validation on Ally
- Battery display: run on battery 10 min; verify percentage decreases and display updates
- Charging indicator: plug/unplug charger; verify indicator updates
- Thermal progression: run CPU/GPU stress tool; verify NORMAL → WARM state change and log
- P-state change: verify EPP file content before and after
SetPerfProfileIPC - Shutdown and restart via overlay: verify clean filesystem state after restart
- Suspend skeleton:
PLAYOS_LIFECYCLE_SUSPENDdelivered; device may or may not sleep; no crash
Done when: all validation cases produce expected log entries and behavior.
Implementation Guidance
sysfs thermal zone selection
There may be multiple thermal zones. Select the zone whose type file contains x86_pkg_temp for CPU. For GPU, look for amdgpu hwmon entry. If neither is found, return -1 and log once (not every poll).
thermal.json format
{
"thresholds": {
"warm_c": 75,
"hot_c": 85,
"critical_c": 95
}
}
If the file is missing or malformed, use the built-in defaults and log a warning.
Verification and Evidence
| Evidence | How it is produced |
|---|---|
| Battery accuracy | playos_power_get_info() output vs. cat /sys/class/power_supply/BAT0/capacity |
| Temperature accuracy | playos_power_get_info() output vs. sensors |
| Thermal state progression | playos-init log during stress test |
| P-state change | /sys/devices/system/cpu/cpu0/cpufreq/energy_performance_preference before/after |
| Profile rejection | Log showing SetPerfProfile PERFORMANCE rejected when HOT |
| Restart clean | journalctl showing clean shutdown sequence |
Acceptance Criteria
- Shell status bar shows live battery percentage; updates within 30 s of charging state change
-
CPU and GPU temperatures visible in overlay; accurate within ±2°C of
sensors - Running a stress test progresses thermal state from NORMAL to WARM; log shows state change
- At HOT state: system switches to BALANCED P-state; overlay shows warning
-
SetPerfProfile PERFORMANCEhonored when thermal state is NORMAL -
SetPerfProfile PERFORMANCErejected when thermal state is HOT - Shutdown from overlay: system shuts down cleanly (filesystems synced)
- Restart from overlay: system reboots cleanly
-
PLAYOS_LIFECYCLE_SUSPENDandPLAYOS_LIFECYCLE_RESUMEdelivered without crashing any consumer - Sample games run sustained load without GPU hang or kernel panic
- CI passes
Handoff to Sprint 10
Sprint 10 may assume:
playos_power.hAPI is stable and returns real data- Shutdown and Reboot IPC commands are implemented and tested
- Thermal thresholds are configurable via
/data/config/thermal.json - Suspend skeleton exists; the event lifecycle is complete
- The overlay is a stable client that can receive further extension (update progress UI, Sprint 11)
Exit Gate
Battery, thermal, and power status are live in the shell and overlay. Thermal throttling prevents dangerous temperatures. Performance profiles can be requested by games and set via the overlay.
Previous: Sprint 8 | Next: Sprint 10
Sprint 9.5 — Display Brightness Control
Goal: Add a working display-brightness primitive to the playos-platform-api and surface it as an interactive control on the Settings → Display tab, so the ROG Ally's panel backlight can be read and adjusted from the shell.
Primary Outcome: The Settings → Display tab shows a live Brightness gauge (0–100%). D-pad up/down adjusts the backlight and the change is written through /sys/class/backlight/ immediately, with the value reflected on the next read.
Status: 🟢 Implemented — platform-api + shell changes landed; on-device brightness verification pending
Prerequisites: Sprint 9 complete — power/battery/thermal telemetry and profile IPC are in place, and the Settings screen already renders tabs with a read-only info layout. ✅ Satisfied (playos_power.c ships the sysfs-read + 1-second-cache + IPC patterns this sprint reuses).
Why This Sprint Exists
The device profiles already advertise the capability:
playos-reference-devices/rog-ally/device-profile.toml:34—"display.brightness" = trueplayos-reference-devices/asus-ultrabook/device-profile.toml:24—"display.brightness" = true
But nothing in the code reads or writes a backlight. Investigation confirmed:
playos-platform-api/include/playos/playos_display.halready exists (and is already pulled in byplayos.h), butsrc/playos_display.cis a stub —playos_display_get_info()andplayos_display_set_vsync()both justreturn -1.- No sysfs backlight access exists anywhere in
playos-platform-api,playos-init,playos-runtime, orplayos-shell. - The Settings → Display tab is read-only (
playos-shell/src/screen_settings.c:616-632shows only Resolution / DPI Scale / GPU), and its update path falls into the generic "read-only tab: scroll" branch (screen_settings.c:218-226) — there is no per-row interactive control outside the System tab. - The spec still marks brightness as a stub (
playos-spec/src/playos-shell-spec.md:47), while the architecture assigns a "volume/brightness HUD" to the overlay (playos-spec/src/architecture.md:177) — that HUD is a separate follow-up, not this sprint.
This is a small, additive sprint: no new public header, no ABI break, and the shell already has all the rendering helpers and cached-state patterns needed for a gauge row.
Start Condition Checklist
-
playos_display.hexists with aPlayOSDisplayInfostruct and is included by the masterplayos.hinclude. -
playos_power.cdemonstrates the sysfs-read + 1-second monotonic cache +/sys/class/*enumeration patterns to copy. -
The Settings screen has
draw_info_line()anddraw_trigger_gauge()helpers to reuse for the brightness row. -
Shell state struct (
playos-shell/include/shell.h) has cached power info (power_info/power_info_valid) and settings tab/cursor fields to extend. -
Device-side check: the Ally exposes a backlight node under
/sys/class/backlight/(expectedamdgpu_bl0) and the shell's uid can write itsbrightnessfile. (Cannot be confirmed from the build tree — verify on hardware before the write-path decision.)
Decisions Locked for This Sprint
- API home: extend the existing
playos_display.h/playos_display.c. Do not create a new header and do not overloadplayos_power.h. - Interface (additive):
int playos_display_get_brightness(int *percent)— returns 0..100, or -1 when unsupported.int playos_display_set_brightness(int percent)— clamps 0..100 and writes the scaled raw value.
- sysfs only: no vendor WMI/ACPI backlight calls this sprint.
- Node preference:
amdgpu_bl0→acpi_video0→intel_backlight→ first other non-acpi_entry under/sys/class/backlight/. Skip entries withmax_brightness == 0. - Percent mapping:
percent = round(brightness * 100 / max_brightness); on set,raw = clamp(round(percent * max_brightness / 100), 0, max_brightness).max_brightnessis read once and cached. - Read cache: 1-second monotonic cache for the raw
brightnessread, mirroringplayos_power_get_info(). - Write authority: preferred path is the trusted shell writing the sysfs node directly through the platform-api helper (the shell already opens
/dev/input/event*directly and registers as a trusted shell). If the device-side check shows the shell's uid cannot writebrightness, fall back to aSetBrightnessIPC message owned byplayos-init(root), mirroringSetPerfProfile. - UI step size: 5% per d-pad press on the brightness row. No repeat-hold handling this sprint (shell edge detection already gives one press per edge).
- Persistence: out of scope. A brightness change is a sysfs write (survives until reboot but not across reboot). Boot-time restore of a saved level is a follow-up.
Scope
In Scope
playos_display.h— add the two brightness functions.playos_display.c— sysfs backlight enumeration, read, clamp, and write.playos-shell— cache brightness inshell.h; add an interactive Brightness gauge to the Settings → Display tab; wire d-pad up/down to adjust and write; bump the Display tab's content height.playos-spec— update the brightness stub wording inplayos-shell-spec.md; add this sprint doc.- Verification: native compile check of platform-api + shell, QEMU boot where possible, and an Ally on-device brightness test.
Explicitly Out of Scope
- Overlay volume/brightness HUD (
playos-spec/src/architecture.md:177) — separate follow-up. - Persisting brightness across reboot / boot-time restore of a saved level.
- Brightness hotkeys (volume/quick-menu button combos).
- HDR/adaptive-brightness/ambient-light sensor logic.
- Auto-dimming or thermal-driven backlight reduction.
- Intel/ultrabook-specific tuning beyond the fallback node preference.
Required Repository Changes
| Repo | Required work |
|---|---|
playos-platform-api | Add playos_display_get_brightness() / playos_display_set_brightness() to playos_display.h; implement sysfs backlight access in playos_display.c |
playos-shell | Add brightness state to include/shell.h; add interactive gauge row to src/screen_settings.c; wire d-pad up/down adjustment + write; bump TAB_DISPLAY content height |
playos-spec | Add Sprint-9.5.md; update the "display brightness (stub)" line in src/playos-shell-spec.md |
(Conditional — only if the device-side check blocks direct writes)
| Repo | Required work |
|---|---|
playos-refdistro (src/playos-init) | Add a SetBrightness IPC handler that writes /sys/class/backlight/<node>/brightness as root |
playos-runtime | Add the SetBrightness message type to the runtime IPC protocol |
Expected Files and Directories
playos-platform-api
include/playos/playos_display.h # add get/set brightness prototypes
src/playos_display.c # replace stub with sysfs backlight implementation
playos-shell
include/shell.h # add display brightness cache + optional display cursor state
src/screen_settings.c # add Brightness gauge row + d-pad adjustment on Display tab
playos-spec
src/sprints/Sprint-9.5.md # this document
src/playos-shell-spec.md # update brightness stub wording
Agent Task Breakdown
Task Status Grid
| Task ID | Task | Primary repo | Status | Notes / evidence |
|---|---|---|---|---|
| S9.5-T1 | Verify the Ally backlight node and write permission | playos-refdistro (on-device) | deferred | direct-write path chosen; node name + writability still unconfirmed on hardware |
| S9.5-T2 | Implement brightness get/set in playos_display.c | playos-platform-api | done | sysfs enumeration + 1 s cache + clamp; native build clean |
| S9.5-T3 | Add interactive Brightness gauge to Settings → Display | playos-shell | done | gauge row + d-pad up/down write + height bump; native build clean |
| S9.5-T4 | (Conditional) SetBrightness IPC fallback | playos-refdistro, playos-runtime | not needed | direct sysfs write chosen; T1 hardware check deferred |
| S9.5-T5 | Spec/docs reconciliation | playos-spec | done | shell-spec stub wording updated |
| S9.5-T6 | Build + validation | playos-platform-api, playos-shell, playos-refdistro | in progress | native compile clean; QEMU + Ally test pending |
S9.5-T1 — Verify the Ally backlight node and write permission
Finding: CONFIG_DRM_AMDGPU=y + CONFIG_DRM_AMD_DC=y (playos-refdistro/br2-external/board/ally/linux.config:96-100) imply the amdgpu DC backlight node should exist at /sys/class/backlight/amdgpu_bl0/. This is not yet confirmed from the build tree or any captured USB log (the last mounted log dirs were empty).
Steps:
- On the Ally (or from a shell on the mounted USB rootfs):
ls -1 /sys/class/backlight/→ confirmamdgpu_bl0(or note the actual node name).cat /sys/class/backlight/<node>/max_brightnessandcat .../brightness.ls -l /sys/class/backlight/<node>/brightness→ note owner/group/mode.idof the runningplayos-shellprocess.
- Record the exact node name, max value, and whether the shell's uid can write the node.
Done when: the report names the concrete backlight node and states whether the trusted shell can write brightness directly. This gates T4.
S9.5-T2 — Implement brightness get/set in playos_display.c
Finding: playos-platform-api/src/playos_display.c is a stub returning -1 for everything. playos_display.h currently has PlayOSDisplayInfo, playos_display_get_info(), and playos_display_set_vsync().
Steps:
- In
playos_display.h, add:int playos_display_get_brightness(int *percent);int playos_display_set_brightness(int percent);with doc comments matching the existing style.
- In
playos_display.c, add aread_int_file()helper (copy the pattern fromplayos_power.c:50-61) and a backlight enumeration helper:opendir("/sys/class/backlight")- for each entry, read
max_brightness; preferamdgpu_bl0, thenacpi_video0, thenintel_backlight, then the first non-acpi_entry withmax_brightness > 0. - cache the chosen node path +
max_brightnesson first success.
- Implement
playos_display_get_brightness():- read
brightnesswith the 1-second monotonic cache (mirrorplayos_power_get_info()'sg_cached_valid/g_cached_msstyle); - scale to 0..100 and return 0; return -1 if no node exists.
- read
- Implement
playos_display_set_brightness():- clamp
percentto 0..100; - scale to raw using the cached
max_brightness; fopenthebrightnessnode in write mode, write the integer +"\n",fflush,fclose;- return 0 on success, -1 on failure (log once via
PLAYOS_LOG_Won the failure, not every attempt).
- clamp
- Keep
playos_display_get_info()andplayos_display_set_vsync()stubs untouched unless a real implementation is trivially available — this sprint only adds brightness.
Done when: the platform-api library compiles natively and the functions read/write the expected sysfs node in a unit-style check (or report clean -1 when the node is absent).
S9.5-T3 — Add interactive Brightness gauge to Settings → Display
Finding: the Display tab draws only Resolution / DPI Scale / GPU (playos-shell/src/screen_settings.c:616-632). Its update path is the generic read-only scroll branch (screen_settings.c:218-226). settings_content_height() returns 3 * info_h for Display (screen_settings.c:110-113). draw_trigger_gauge() already renders a horizontal value bar (screen_settings.c:375).
Steps:
- In
include/shell.h, add cached brightness fields next to the power-info block (shell.h:114-115), e.g.:int display_brightness;(0..100, -1 when unknown)int display_brightness_valid;Optionally addint settings_display_cursor;if the row needs focus; otherwise d-pad up/down on the Display tab directly adjusts brightness.
- Refresh brightness once per frame or on a short interval in the Settings update path (call
playos_display_get_brightness()). - In
screen_settings.cDisplay tab update (before the generic scroll branch), whens->settings_tab == TAB_DISPLAY:- d-pad up →
playos_display_set_brightness(current + 5), update cache; - d-pad down →
playos_display_set_brightness(current - 5), update cache. Keep tab switching on d-pad left/right unchanged.
- d-pad up →
- In the Display tab draw block, add a "Brightness" row after GPU:
- render the label via
draw_info_line()-style layout; - render the gauge with
draw_trigger_gauge()(or an equivalent horizontal bar) atvalue = percent / 100.0f; - show
NN%as the value label.
- render the label via
- Bump
settings_content_height()forTAB_DISPLAYfrom3.0f * info_hto4.0f * info_h(screen_settings.c:110-113). - Ensure the write is idempotent and safe: if
playos_display_get_brightness()returns -1 (no node), draw "Brightness — Unavailable" and no-op on up/down.
Done when: the Display tab shows a Brightness gauge whose value tracks the real backlight, and d-pad up/down changes both the gauge and the panel brightness.
S9.5-T4 — (Conditional) SetBrightness IPC fallback
Finding: the shell already writes nothing to sysfs today; its privilege level for /sys/class/backlight/<node>/brightness is unverified. playos-init is root and already owns sysfs writes for performance profiles.
Steps (only if T1 shows the shell cannot write directly):
- Add a
SetBrightnessmessage type toplayos-init/ipc/ipc.hand the runtime IPC protocol. - In
playos-init, add a handler that validates0..100, scales tomax_brightness, and writes/sys/class/backlight/<node>/brightness. - In
playos_display_set_brightness(), route through the IPC control socket (/run/playos/control.sock), mirroringrequest_profile_over_ipc()inplayos_power.c:234-314. - Emit nothing back to games this sprint; the shell reads back via
playos_display_get_brightness().
Done when: SetBrightness changes the backlight and the shell gauge reflects it, with init as the sole sysfs writer.
S9.5-T5 — Spec/docs reconciliation
Steps:
- Update
playos-spec/src/playos-shell-spec.md:47so brightness is no longer described as a stub — state that Settings → Display exposes a live brightness control backed by the platform-api. - Add
Sprint-9.5.mdto the sprint directory (this document). - Update cross-links if needed: the Sprint 9 and Sprint 10 footers can be adjusted to reference Sprint 9.5 where it sits chronologically (optional, low priority).
Done when: no spec text describes brightness as an unimplemented stub.
S9.5-T6 — Build + validation
Steps:
- Native compile-check
playos-platform-api(the newplayos_display.c) andplayos-shellagainst the platform-api headers. - Build a QEMU image (or at least the shell + platform-api) to confirm no link regressions; brightness will report "Unavailable" in QEMU since there is no backlight node — that is the expected path.
- On the Ally: open Settings → Display, confirm the gauge shows the current level, press up/down and verify the panel brightness changes and the percentage label tracks.
- Capture evidence:
/data/log/shell.log(or the platform-api log channel) showing the chosen backlight node and the successful get/set path.
Done when: native + QEMU builds are clean, and the Ally Settings → Display brightness control changes the panel backlight.
Implementation Guidance
Order of execution
- T1 first (device verification) — decides whether T4 is needed.
- T2 second (platform-api) — the primitive everything else depends on.
- T3 third (shell UI) — surfaces the primitive.
- T4 fourth (IPC fallback) — only if T1 blocks direct writes.
- T5 fifth (docs) — document reality after the code lands.
- T6 last (validation) — build + on-device.
Atomic commits
S9.5-T2: add display brightness get/set to platform-api
S9.5-T3: add brightness gauge to settings display tab
S9.5-T5: update spec for display brightness control
Keep it additive
- No public header is removed or reordered;
playos_display.honly gains two functions. PLAYOS_API_VERSIONis not bumped (additive functions only, no struct/ABI change).- The shell's read-only tab behaviour is preserved for Audio / Power / Network / Input; only Display gains the new interactive row.
Verification and Evidence
| Evidence | How it is produced |
|---|---|
| Backlight node confirmed | T1 output: ls /sys/class/backlight/, max_brightness, writability, shell uid |
| API works | native test / on-device playos_display_get_brightness() matches cat .../brightness scaled to % |
| Write works | on-device playos_display_set_brightness(50) changes cat .../brightness |
| UI works | Settings → Display gauge tracks real value; up/down changes the panel |
| No-op path safe | QEMU (no backlight) shows "Unavailable" and does not crash |
| Docs updated | playos-shell-spec.md no longer calls brightness a stub |
Acceptance Criteria
-
playos_display_get_brightness()returns the real 0..100 level on the Ally -
playos_display_set_brightness()writes the scaled value to the sysfs backlight node - Settings → Display shows a Brightness gauge with a percentage label
- D-pad up/down on the Display tab adjusts brightness and writes it immediately
- Display tab content height fits the new row without clipping
- The no-backlight path (QEMU / no node) reports unavailable and does not crash
-
PLAYOS_API_VERSIONis unchanged; the change is additive -
playos-shell-spec.mdno longer describes brightness as a stub - Native and QEMU builds are clean
Handoff to Sprint 10
Sprint 10 may assume:
playos_display.hexposes a workingplayos_display_get_brightness()/playos_display_set_brightness()pair.- The shell Settings → Display tab can read and adjust panel brightness.
- Brightness writes go through the platform-api helper (direct sysfs, or init-owned IPC if T4 was needed).
- Persistence across reboot, brightness hotkeys, and the overlay HUD remain unimplemented and are natural follow-ups.
Exit Gate
The ROG Ally's panel backlight can be read and adjusted from Settings → Display, and the change is reflected live through the platform-api brightness primitive.
Previous: Sprint 9 | Next: Sprint 10
Sprint 10 — Installer and Internal-Disk Deployment
Goal: Build a tested, user-confirmed installation path from a USB boot image to the ROG Ally internal NVMe SSD. After installation, the Ally boots PlayOS from the internal disk without a USB drive.
Primary Outcome: A user can boot the PlayOS installer from USB, confirm the target disk, wait for installation, remove the USB, and boot into the full PlayOS experience from internal storage.
Status: 🟢 Implemented and verified — installer source (src/playos-installer/), playos.mode=install trigger, complete FactoryReset handling, Buildroot installer-image wiring, and T8 validation (QEMU loopback + physical Ally install) have all landed; user confirmed NVMe install + boot from internal storage.
Prerequisites: Sprint 9 complete — complete MVP feature set running on the ROG Ally.
Why This Sprint Exists
Sprint 9 completes the feature set. Sprint 10 makes PlayOS installable to real hardware — without it, the system only runs from USB. This sprint also completes the FactoryReset IPC (all options) and establishes the full 5-partition installed-disk layout (ESP, A/B system slots, misc, data) that Sprint 11 builds the A/B update and rollback logic on.
Start Condition Checklist
- Sprint 9 complete: full feature set running on the ROG Ally from USB.
FactoryReset { erase_cache, erase_config }is implemented (Sprint 6);erase_games,erase_saves, anderase_logsare deferred to this sprint.playos-refdistroproduces a bootable USB image (make ally-usb-image→output/ally/images/playos-ally-usb.img).- QEMU can boot the dev image on the host for testing (
make qemu-run). - A spare NVMe or a test-only Ally is available for destructive disk tests.
Decisions Locked for This Sprint
- Partition layout (installed disk, 5 partitions): ESP (512 MiB FAT32, label
ESP), system A (4 GiB EROFS/squashfs, read-only, labelplayos-a), system B (4 GiB EROFS/squashfs, read-only, labelplayos-b, empty until Sprint 11),misc(64 MiB, A/B slot metadata, labelmisc), data (remainder ext4, labelplayos-data). The live USB keeps its current compact 3-partition layout — this 5-partition layout applies only to the installer-deployed internal disk. - Installer trigger:
playos.mode=installkernel cmdline flag, OR no existing PlayOS data partition on a removable-boot device - Installer NEVER silently formats: user must see disk details and hold A for 3 seconds
- Disk partitioning tool:
libfdiskor raw ioctl (no parted/fdisk subprocess) - UEFI fallback: always write
/EFI/BOOT/BOOTX64.EFI;efibootmgrregistration is a best-effort addition - Factory reset authority: only
playos-initperforms the filesystem deletion; the overlay sends an IPC command
Scope
In Scope
- Installer trigger mode in
playos-init - Installer Raylib UI (disk discovery, confirmation, progress, success, error screens)
- GPT partitioning via
libfdisk - FAT32 ESP format (
mkfs.fat) - Read-only system A slot from a pre-built EROFS/squashfs image (
playos-a) - Empty system B slot reserved for Sprint 11 A/B (
playos-b) - A/B metadata partition (
misc, 64 MiB) - ext4 data partition format (
mkfs.ext4) - EFI boot artifact write
- UEFI boot entry registration (
efibootmgr, best-effort) - First-boot from internal disk
- Complete
FactoryResetIPC (all five options) make installer-imageBuildroot target
Explicitly Out of Scope
- A/B update/rollback mechanism and dm-verity hashing (Sprint 11) — Sprint 10 only creates the partitions
- Network update download (Sprint 11/post-MVP)
- dm-verity (Sprint 11/post-MVP)
- Windows dual-boot or partition preservation (explicitly out of MVP scope)
Required Repository Changes
| Repo | Required work |
|---|---|
playos-refdistro | playos-installer source, Buildroot package, installer image target, disk ops; overlay factory-reset UI flow |
playos-init | Complete FactoryReset server handler (games/saves/logs erasure); installer trigger mode |
playos-runtime | playos_trusted_factory_reset() client helper |
playos-spec | Installation guide, partition layout doc |
Expected Files and Directories
playos-refdistro
src/playos-installer/
main.c # screen state machine
screens/discovery.c
screens/confirmation.c
screens/progress.c
screens/success.c
screens/error.c
disk/partition.c # libfdisk wrapper
disk/format.c # mkfs.fat (ESP), mkfs.ext4 (data/misc); system slots written from pre-built EROFS/squashfs images
disk/efi.c # EFI artifact copy, efibootmgr
br2-external/package/playos-installer/
playos-installer.mk
Config.in
br2-external/configs/playos_ally_installer_defconfig
Note: Like
src/playos-overlay/(Sprint 7),src/playos-installer/is a second deliberate in-refdistro C-source exception. When implemented, updateplayos-refdistro/AGENTS.md"What NOT to Do" to list it alongside the overlay exception.
Agent Task Breakdown
Task Status Grid
| Task ID | Task | Primary repo | Status | Notes / evidence |
|---|---|---|---|---|
| S10-T1 | Implement installer trigger mode in playos-init | playos-init | done | mount.c:421-455 parses playos.mode=install; main.c/supervisor.c spawn /usr/bin/playos-installer |
| S10-T2 | Build installer screen state machine (Raylib UI) | playos-refdistro | done | src/playos-installer/main.c DISK_DISCOVERY → CONFIRMATION → INSTALLING → SUCCESS/ERROR |
| S10-T3 | Implement disk partitioning and formatting | playos-refdistro | done | disk.c (libfdisk GPT), format.c (mkfs.fat/mkfs.ext4, squashfs slot write) |
| S10-T4 | Implement EFI artifact write and UEFI boot entry | playos-refdistro | done | efi.c writes /EFI/BOOT/BOOTX64.EFI, best-effort efibootmgr |
| S10-T5 | Implement first-boot from internal disk | playos-init | done | Existing S6 first-boot provisioning already handles empty /data (mount.c .playos-storage-version marker + seed games); no new code required |
| S10-T6 | Complete FactoryReset IPC (all five options) | playos-init, playos-runtime, playos-refdistro | done | Handler now erases games/saves/cache/config/logs; runtime trusted helper added |
| S10-T7 | Create make installer-image Buildroot target | playos-refdistro | done | playos_ally_installer_defconfig, playos-installer package, linux-installer.config, scripts/gen-installer-usb-image.sh, Makefile installer-* targets |
| S10-T8 | Installer validation (QEMU loopback + Ally) | playos-refdistro | done | QEMU loopback install SUCCESS (GPT verified); dev variant booted; physical Ally install to NVMe + reboot confirmed by user; 2026-08-19 re-install on an already-installed NVMe fixed — playos-init now skips ESP mount and pivot in installer mode, and playos-installer appends step diagnostics to /data/log/installer.log |
S10-T1 — Implement installer trigger mode in playos-init
- Parse kernel cmdline for
playos.mode=install - If absent: detect whether current root is a removable device and no
playos-datapartition exists on an internal disk; if both true, trigger installer mode - In installer mode: spawn
playos-installeras the first and only Wayland client (compositor must not spawn the shell) - Log reason for entering installer mode
Done when: playos.mode=install on the cmdline boots into the installer UI, not the shell.
S10-T2 — Build installer screen state machine (Raylib UI)
State machine: DISK_DISCOVERY → CONFIRMATION → INSTALLING → SUCCESS | ERROR
Disk discovery screen:
- Enumerate
/sys/block/for NVMe and SATA devices - Exclude the device currently hosting the root filesystem (the USB)
- Show for each candidate: model name, total size in GB, current partition count
- D-pad navigates; A selects
Confirmation screen:
WARNING: All data on <model> (<size> GB) will be erased- Hold A for 3 seconds to confirm (show countdown bar)
- B returns to disk discovery
Progress screen:
- Numbered step list with current step highlighted
- Progress bar (0–100%)
- Steps: Create GPT → Create ESP → Create system A → Create system B → Create misc → Create data → Write EFI → Populate system A → Format data → Init data → Sync
Success screen:
- "Installation complete. Remove USB and press A to restart."
- A: reboot
Error screen:
- Show failed step and error message
- Options: "Retry" (restart from disk discovery), "Shutdown"
- Never drop to a shell
Done when: all screens render, navigation works, and the full flow can be exercised in QEMU on a loopback device.
S10-T3 — Implement disk partitioning and formatting
Using libfdisk:
- Create GPT partition table on the target device
- Partition 1: EFI System Partition, FAT32, 512 MiB, label
ESP - Partition 2: system A, 4 GiB, label
playos-a(read-only EROFS/squashfs root slot) - Partition 3: system B, 4 GiB, label
playos-b(reserved empty for Sprint 11 A/B) - Partition 4:
misc, 64 MiB, labelmisc(A/B slot metadata) - Partition 5: data, ext4, remainder, label
playos-data - Write partition table to disk
Format/populate:
- ESP: call
mkfs.fat -F32 -n ESP <part1>; copyEFI/BOOT/BOOTX64.EFI - System A: write the pre-built read-only root image (EROFS, fallback squashfs) directly to
<part2>— nomkfsat install time - System B: leave empty/blank; populated by the Sprint 11 A/B update path
misc: callmkfs.ext4 -L misc <part4>(or leave raw; the slot state is tiny)- Data: call
mkfs.ext4 -L playos-data <part5>
Each step must check the return code. On any failure: transition to ERROR screen with the error code and step name.
Done when: QEMU loopback test shows the correct 5-partition GPT layout, system A boots read-only, and playos-data mounts.
S10-T4 — Implement EFI artifact write and UEFI boot entry
- Mount the ESP at
/mnt/efi - Create
/mnt/efi/EFI/BOOT/ - Copy the PlayOS EFI-stub kernel (bzImage with embedded initramfs) to
BOOTX64.EFI— no intermediate bootloader (matchesscripts/gen-ally-usb-image.sh) - Run
efibootmgr --create --disk <dev> --part 1 --label "PlayOS" --loader /EFI/BOOT/BOOTX64.EFI— log success or failure; failure is non-fatal (fallback path works) - Unmount ESP
Done when: after install, removing the USB and rebooting loads PlayOS from the NVMe EFI artifact.
S10-T5 — Implement first-boot from internal disk
On first boot from internal disk (empty /data):
playos-initdetects no.playos-storage-versionmarker- Runs the full first-boot provisioning from Sprint 6 (S6-T1/T2)
- Boots into the shell with an empty (but valid) game library
Done when: after installation and reboot, the shell shows an empty library with the correct status bar.
S10-T6 — Complete FactoryReset IPC
Extend the Sprint 6 handler (which already erases cache and config) to act on all five flags:
{
"v": 1,
"type": "FactoryReset",
"erase_games": false,
"erase_saves": false,
"erase_cache": true,
"erase_config": true,
"erase_logs": false
}
-
erase_games→ delete contents of/data/games/ -
erase_saves→ delete contents of/data/saves/(DESTRUCTIVE) -
erase_cache→ delete contents of/data/cache/ -
erase_config→ delete contents of/data/config/ -
erase_logs→ delete contents of/data/log/ -
Requires no active game; if a game is running, reply
FactoryResetErrorwith"reason": "game_running"(matchesruntime-ipc.md). -
Success replies
FactoryResetComplete; the erased directories are recreated empty. -
erase_savesis destructive: overlay must show a second confirmation before sending the IPC. -
Factory reset with
erase_games = trueanderase_saves = trueleaves the system bootable with an empty game library and a valid/datatree. -
Access: Overlay → Power menu → "Factory Reset" → option selection → confirmation.
Done when: full factory reset (all true) leaves a bootable system with an empty shell library.
S10-T7 — Create make installer-image Buildroot target
playos_ally_installer_defconfig— likeplayos_ally_defconfigbut withplayos-installerinstead ofplayos-shellas the first Wayland clientmake installer-imageproduces a bootable USB image that enters installer modemake ally-usb-imagecontinues to produce the normal system image (unchanged)- Document both targets in
README.md
Done when: make installer-image completes without errors; the produced image boots into installer mode in QEMU.
S10-T8 — Installer validation
QEMU loopback test:
make installer-image; boot in QEMU with a loopback block device as target- Verify: disk discovery shows the loopback device, confirmation screen works, partition layout correct after install, data filesystem mounts
Physical Ally test:
- Full install from USB to NVMe; remove USB; reboot; verify PlayOS starts from internal storage
- Install a game manually (
cp -rto/data/games/); reboot; verify it appears in the shell
Abort test:
- Kill installer mid-progress; verify device is in a deterministic (recoverable) state
Re-install test:
- Run installer on an already-installed Ally; verify it works
- 2026-08-19 fix: on a previously-installed NVMe, installer mode in
playos-initnow skips mounting the internalESPlabel (the disk is about to be repartitioned) and skipsplayos_pivot_to_active_slot()(so/usr/bin/playos-installerstays visible). Without this, re-install failed withInstallation Failedbecause the old ESP mount made the laterfdisk/mkfssteps fail busy. Verified end-to-end by user: re-install completed and booted from NVMe.
Done when: all four validation scenarios pass with evidence.
Implementation Guidance
Disk operations ordering
Always sync after each major step: after partition table write, after each mkfs, after EFI copy. Use fsync() on file descriptors and sync() system call before unmounting.
Never fork without exec
Use posix_spawn or execve for mkfs.fat, mkfs.ext4, efibootmgr. Do not use system().
libfdisk over shell
The installer runs as root inside the initramfs. Use libfdisk directly — do not depend on fdisk or parted binaries being present.
Verification and Evidence
| Evidence | How it is produced |
|---|---|
| Partition layout | fdisk -l <device> after QEMU loopback install |
| Filesystem types | blkid output showing FAT32 (ESP), EROFS/squashfs (system slots), ext4 (data/misc) labels |
| NVMe boot | ROG Ally boot without USB, shell visible |
| Factory reset | ls /data/games/ empty after full reset; system boots |
| Installer error screen | Write-protect target in QEMU; verify error screen appears |
Acceptance Criteria
- Installer boots from USB and shows disk discovery screen
- Only internal NVMe disks listed; USB boot device excluded
- Disk model, size, and partition count shown correctly
- Confirmation requires holding A for 3 seconds (no accidental erase)
- B on confirmation returns to disk selection
- Installation completes on clean NVMe without error
- Progress bar reaches 100% and success screen appears
- After USB removal and reboot: PlayOS boots from NVMe
- Empty game library shown on first boot from internal disk
-
File written to
/data/persists after restart - Manually installed game appears in shell after restart
- Full factory reset leaves system bootable with empty valid data partition
- Error screen shown if installation fails at any step (no terminal drop)
-
make installer-imageproduces a bootable installer USB image - CI passes (installer compiles; disk operations tested on QEMU loopback)
Handoff to Sprint 11
Sprint 11 may assume:
- The installer creates the full 5-partition layout (ESP, system A/B,
misc, data) - System A is populated with the read-only EROFS/squashfs root and boots; system B is reserved empty;
miscexists playos-initboots from the ESP EFI artifact (BOOTX64.EFI) and provisions/data/dataprovisioning is stable and tested- Sprint 11 implements A/B update/rollback logic and dm-verity on top of the existing layout; the installer does not repartition
Exit Gate
A PlayOS USB installer image successfully installs PlayOS to the ROG Ally internal NVMe. After removal of the USB drive, the Ally boots PlayOS from internal storage and shows the shell.
Previous: Sprint 9 | Next: Sprint 11
Sprint 11 — Immutable Images and A/B Updates
Goal: Deliver signed, atomic A/B system updates with automatic rollback on boot failure, on top of the immutable (read-only squashfs) system image installed in Sprint 10.
Primary Outcome: The running system image is read-only (games cannot modify it). A system update can be applied to the inactive slot, and after a marked reboot, the device boots from the new slot. If the new slot fails to boot successfully, it rolls back to the previous slot automatically.
Status: 🟢 Complete — completed via Sprint 11.5 (pivot-to-squashfs boot path and S11-T9 hardware validation).
Prerequisites: Sprint 10 complete — installer creates the disk layout; device boots from internal NVMe.
Why This Sprint Exists
Sprint 10 delivers an installable system with a read-only squashfs root. Sprint 11 adds A/B slot updates so a new system image can be tested in the inactive slot and automatically rolled back if it fails, plus dm-verity integrity hardening. This is the safety foundation for production distribution.
Start Condition Checklist
- Sprint 10 complete: installer creates the full 5-partition layout (ESP, system A/B,
misc, data); Ally boots from internal NVMe via the ESP EFI artifact. - System A holds a read-only squashfs root; system B is reserved empty; the
miscpartition exists. - The EFI boot path (BOOTX64.EFI) is stable.
- An update key pair is generated for development use.
- ADR-0005 (RAUC for A/B System Updates) is Accepted; the RAUC-vs-custom criteria are recorded.
Decisions Locked for This Sprint
- Read-only mount strategy this sprint: system slots are inherently read-only squashfs images (no
MS_RDONLYflag needed); dm-verity is a post-MVP hardening step (document as required for production) - A/B slot tracking:
boot.jsonon the ESP (FAT32 writable fromplayos-init); themiscpartition is reserved as the more robust future home - Boot success definition: shell renders AND user interacts (A/B/D-pad), OR 60-second timer elapses
- Rollback trigger:
boot_count >= 3withhealth != "good"→ mark slot bad, switch, reboot - Update bundle: RAUC (or a minimal custom updater, if RAUC integration exceeds the ADR-0005 criteria); development key; production HSM key is post-MVP — final RAUC-vs-custom choice is resolved during S11-T5, consistent with ADR-0005's "pending evaluation" status
- Network download: out of scope this sprint; update bundles are placed manually or via USB
- Installer update: Sprint 11 does not repartition — Sprint 10 already creates the 5-partition layout; Sprint 11 adds A/B update/rollback logic on top
Update contract (engine-agnostic, locked)
Fixed regardless of whether RAUC or a custom updater wins in S11-T5 — the shell, playos-init, and the engine all build against this contract:
- Bundle location:
/data/updates/directory; bundles are matched by the neutral.playosbsuffix (never a RAUC-specific extension). Exactly one bundle may be "ready to apply" at a time. boot.jsonschema —/EFI/playos/boot.jsonon the ESP:{ "v": 1, "active_slot": "a", "slot_a": { "version": "0.1.0", "boot_count": 0, "health": "good" }, "slot_b": { "version": "", "boot_count": 0, "health": "empty" } }health∈ {good,pending,bad,empty}. Slot selection, boot counting, and rollback read/write only this file.- Apply-update IPC (shell/overlay →
playos-init):
Responses:{ "v": 1, "type": "ApplyUpdate", "path": "/data/updates/0.2.0.playosb" }ApplyUpdateAck { "accepted": true }orApplyUpdateError { "reason": "..." }. - Update progress/status events (
playos-init→ shell/overlay):{ "v": 1, "type": "UpdateProgress", "step": "verify", "percent": 25 } { "v": 1, "type": "UpdateComplete", "active_slot": "b", "version": "0.2.0" } { "v": 1, "type": "UpdateError", "step": "verify", "reason": "signature_invalid" } - Boot success signal: the shell reports a successful boot (renders AND user interacts) or a 60-second timer elapses; either resets the active slot's
boot_countto 0 and setshealth = "good".
Scope
In Scope
- Read-only system partition mount
- A/B update/rollback logic on the existing 5-partition layout (Sprint 10 installer already created the partitions)
boot.jsonon ESP: active slot, health, boot count- Boot counting and automatic rollback
- Update flow: signature verify → write inactive slot → update
boot.json→ reboot - RAUC integration (or equivalent — per ADR)
- Shell update UI (manual trigger; no network download)
playos_system_os_version()API returning active slot version
Explicitly Out of Scope
- dm-verity (post-MVP)
- Network update download (post-MVP)
- Production HSM update key (post-MVP)
- Delta updates (post-MVP)
- Rollback from within the OS UI (the automatic 3-strike rollback is sufficient this sprint)
Required Repository Changes
| Repo | Required work |
|---|---|
playos-refdistro | RAUC integration, update bundle build target, read-only root image build (A/B layout already from Sprint 10) |
playos-init | Read-only system mount, boot.json management, boot counting and rollback, update application flow |
playos-shell | Update UI in settings screen |
playos-platform-api | playos_system_os_version() returns active slot version |
playos-spec | A/B boot protocol spec, update flow ADR (RAUC vs custom) |
Expected Files and Directories
playos-init
src/boot_slot.c # boot.json reader/writer, slot selection, boot counting
playos-refdistro
br2-external/configs/playos_ally_defconfig # updated: A/B read-only root image
br2-external/configs/playos_ally_installer_defconfig # updated: 5-partition installer
br2-external/package/rauc/ # RAUC Buildroot package (or custom updater)
scripts/create-update-bundle.sh # development key signing
playos-spec
adr/ADR-0005-update-engine.md # exists — RAUC vs custom decision; supersede if revised
Agent Task Breakdown
Task Status Grid
| Task ID | Task | Primary repo | Status | Notes / evidence |
|---|---|---|---|---|
| S11-T1 | Implement read-only system partition mount | playos-init | deferred | Blocked: system still boots from embedded initramfs; pivot_root into the read-only squashfs slot is not yet wired |
| S11-T2 | Mount active system slot image and select active slot (layout already created in Sprint 10) | playos-init | deferred | Slot-selection logic implemented in boot_slot.c; the active squashfs image is not yet mounted as the runtime root (same blocker as T1) |
| S11-T3 | Implement boot.json read/write and active slot selection | playos-init | done | src/boot_slot.c reads/writes /EFI/playos/boot.json; host test test_boot_slot passes |
| S11-T4 | Implement boot counting and automatic rollback | playos-init | done | Boot-count increment + 3-strike rollback on ShellReady/60s timer; host test passes |
| S11-T5 | Integrate RAUC (or equivalent) and update application flow | playos-refdistro, playos-init | done | Custom dev-signed updater chosen over RAUC (see ADR-0005 revision); ApplyUpdate IPC + scripts/create-update-bundle.sh |
| S11-T6 | Implement update bundle signature verification | playos-init | done | src/sha256.c HMAC-SHA256 (dev key); .playosb bundle verified before any partition write |
| S11-T7 | Add shell update UI | playos-shell | done | Settings → System → Software Update (Check/Apply/Restart-to-Apply + progress + boot-slot info) |
| S11-T8 | Update playos_system_os_version() to read active slot version | playos-platform-api | done | Reads active slot version from /EFI/playos/boot.json; falls back to "unknown" |
| S11-T9 | A/B update and rollback validation | playos-refdistro | deferred | Requires S11-T1/T2 end-to-end pivot-to-squashfs boot + ROG Ally hardware |
S11-T1 — Mount the read-only system image
- In
playos-init, mount the active slot's squashfs image read-only:mount(system_dev, "/", "squashfs", MS_RDONLY, NULL); /* EROFS is a future hardening option, not implemented this sprint */ - The image filesystem is inherently read-only; there is no mutable system partition
- All writes at runtime must go to
/data - Verify:
touch /usr/testreturnsEROFSor permission denied - Log the mount mode on boot:
playos-init: system image mounted read-only - Document dm-verity as the production hardening path (do not implement this sprint)
Done when: touch /usr/test fails with EROFS on a running Ally; all normal operations continue to work.
S11-T2 — Mount active system slot image and select active slot
The 5-partition layout is created by the Sprint 10 installer and reused unchanged:
GPT disk
├── Part 1: ESP FAT32 512 MiB label: ESP
├── Part 2: system A squashfs 4 GiB label: playos-a
├── Part 3: system B squashfs 4 GiB label: playos-b
├── Part 4: misc ext4/raw 64 MiB label: misc
└── Part 5: data ext4 remainder label: playos-data
playos-initreadsboot.json(ormiscmetadata) → selects active slot → mounts the matching system image read-only- Slot B starts as
"health": "empty"after fresh install - No repartitioning here — this task consumes the layout Sprint 10 already created
miscis reserved as the more robust home for A/B slot metadata;boot.jsonon the ESP (S11-T3) remains the current implementation
Done when: QEMU boots slot A from its read-only squashfs image; fdisk -l shows the 5-partition layout.
S11-T3 — Implement boot.json read/write and active slot selection
boot.json on ESP at /EFI/playos/boot.json:
{
"active_slot": "a",
"slot_a": { "version": "0.1.0", "boot_count": 0, "health": "good" },
"slot_b": { "version": "", "boot_count": 0, "health": "empty" }
}
playos-initmounts ESP read-write during boot, readsboot.json, unmounts read-only after updating- Active slot determines which partition label to mount as system
- If
boot.jsonis missing or corrupt: boot slot A, log a warning, recreate the file
Done when: cat /EFI/playos/boot.json after boot shows the correct active slot and boot count.
S11-T4 — Implement boot counting and automatic rollback
On every boot:
- Mount ESP read-write
- Read
boot.json; incrementboot_countfor the active slot - Write updated
boot.json; unmount ESP - If
boot_count >= 3ANDhealth != "good": mark slot"health": "bad", switchactive_slotto the other slot, reboot immediately
After successful boot (user interacts with shell OR 60-second timer):
- Mount ESP read-write
- Set
health = "good",boot_count = 0for the active slot - Write and unmount
Done when: corrupting the active slot's initramfs causes 3 failed boot attempts and then an automatic switch to the other slot.
S11-T5 — Integrate RAUC (or equivalent) and update application flow
Update application sequence:
- Receive update bundle path (e.g.,
/data/updates/playos-0.2.0.playosb) - Verify bundle signature (see S11-T6)
- Identify inactive slot (opposite of
active_slotinboot.json) - Write new system image to inactive slot partition
- Update ESP: write new EFI artifact for inactive slot
- Update
boot.json: switchactive_slotto new slot,boot_count = 0,health = "pending" - Notify shell: "Update ready — restart to apply"
- On restart: new slot boots; rollback guard (S11-T4) runs
RAUC handles steps 2–6 if chosen. Document the integration in the ADR.
Done when: applying a valid update bundle causes the inactive slot to be written and boot.json to reflect the pending slot switch.
S11-T6 — Implement update bundle signature verification
- Generate a development key pair:
openssl genrsa -out dev-update.key 4096+ self-signed cert - Embed the public cert in the system image (e.g.,
/etc/playos/update-cert.pem) - Before writing any slot, verify the bundle signature against the embedded cert
- On signature failure: abort update, log clearly, do NOT write any partition
Done when: a bundle signed with the correct key is accepted; a bundle signed with a wrong key or unsigned is rejected before any write occurs.
S11-T7 — Add shell update UI
In the shell settings screen:
- "System" section → "Software Update"
- Show: current version (from
playos_system_os_version()), active slot label - "Check for Update" button → for this sprint: opens a file picker or shows a "Place bundle in
/data/updates/" instruction - "Apply Update" button appears when a bundle is present in
/data/updates/ - Progress bar during bundle application
- "Restart to Apply" button after bundle written successfully
- Show current slot health in a developer info panel
Done when: placing a valid bundle in /data/updates/ causes the Apply button to appear; applying it shows progress and the restart prompt.
S11-T8 — Update playos_system_os_version() to read active slot version
The API already exists: declared in playos-platform-api/include/playos/playos_system.h and implemented in src/playos_system.c. It currently reads /etc/playos-version (fallback "0.3.0"), and the shell already calls it (src/main.c, src/screen_settings.c, src/screen_home.c). This task changes its source to the active slot's version, not its signature:
/* Returns a pointer to a static null-terminated version string, e.g. "0.1.0".
Returns "unknown" if boot.json cannot be read. */
const char *playos_system_os_version(void);
- Read the version of the active slot from
boot.jsonon the ESP (S11-T3) - Keep the existing static-buffer lifetime contract; no caller changes required
- Fall back to
"unknown"(not a hardcoded version) whenboot.jsonis unreadable
Done when: playos_system_os_version() returns the version string from boot.json, and the shell settings/home screens display it unchanged.
S11-T9 — A/B update and rollback validation
Full test matrix on the ROG Ally:
- Fresh install → verify 5-partition layout →
boot.jsonshows slot A good - Apply valid update bundle →
boot.jsonshows slot B pending → reboot → slot B active → 60s → slot B good - Corrupt slot B initramfs → apply bundle with corrupt slot → reboot 3× → rollback to slot A
/datacontent (game files, saves) survives update and rollback unchanged- Invalid signature bundle → rejected before any partition write
playos_system_os_version()returns correct version for each slot
Done when: all 6 test cases pass with log evidence.
Implementation Guidance
ESP mount discipline
The ESP must be mounted read-write ONLY for boot.json updates and EFI artifact writes. Mount it read-only or unmounted the rest of the time. This prevents accidental corruption of the boot files.
boot.json atomicity
Write boot.json atomically: write to boot.json.tmp, then rename to boot.json. FAT32 rename is not guaranteed atomic on all firmware, but it is the best available option without a journaling filesystem on the ESP.
RAUC vs custom
If RAUC's Buildroot package is not available or too complex to integrate, implement a minimal custom updater in playos-init. The ADR must document the choice and the rationale.
Verification and Evidence
| Evidence | How it is produced |
|---|---|
| Read-only mount | touch /usr/test → EROFS; cat /proc/mounts showing ro |
| A/B layout | fdisk -l after fresh install |
| boot.json content | cat /EFI/playos/boot.json at each test stage |
| Rollback | 3-strike test log showing slot switch |
| Data survival | File hashes before and after update match |
| Signature rejection | Log showing rejected bundle before any write |
Acceptance Criteria
-
Running system partition mounted read-only;
touch /usr/testfails with EROFS - Installer creates 5-partition layout on fresh NVMe (ESP, A/B system, misc, data)
-
boot.jsonon ESP reflects active slot, health, and boot count - Update written to inactive slot does not affect running system
- After reboot, system boots from newly updated slot
-
Successful boot marks new slot
health = "good"after 60 seconds - 3 consecutive failed boots of new slot triggers rollback to previous slot
- Previous slot boots and is functional after rollback
-
Games and saves on
/datasurvive update and rollback unchanged -
playos_system_os_version()returns active slot version correctly - Bundle with invalid signature is rejected before any partition write
- Shell update UI shows current version and allows applying a bundle
- CI: update bundle creation and signature verification tested on host
Handoff to Sprint 12
Sprint 12 (Security Hardening) may assume:
- The system image is immutable at runtime (read-only system mount)
- Signed A/B updates with automatic rollback are functional
boot.jsonon the ESP tracks the active slot, slot health, and boot count- Games and user data live on
/dataand are unaffected by updates and rollback
Exit Gate
The system image is immutable at runtime. A/B updates can be applied, verified, and automatically rolled back on failure. Games and user data are unaffected by updates and rollbacks.
Previous: Sprint 10 | Next: Sprint 11.5
Sprint 11.5 — Pivot-to-Squashfs Boot and A/B Validation
Goal: Complete Sprint 11's remaining critical path — make the running system actually boot from the read-only squashfs active slot (instead of the embedded initramfs), and validate the full A/B update/rollback cycle end-to-end.
Primary Outcome: On boot, the initramfs mounts the active slot's squashfs image read-only and pivots into it; touch /usr/test fails with EROFS; a bad slot automatically rolls back after 3 failed boots; the full A/B test matrix passes on QEMU and the ROG Ally.
Status: 🟡 Partially validated — T1–T4 landed; host tests + QEMU (pivot + forced rollback) pass. The full 6-case A/B matrix still needs a ROG Ally hardware run.
ROG Ally hardware gate: execute the full 6-case matrix on real hardware only after Sprint 11.6 is complete and the ROG Ally is reachable over the network via SSH (USB-C Ethernet + Dropbear). Sprint 11.6 provides the remote access needed to run and capture the matrix directly, instead of reading logs off a USB stick.
Prerequisites: Sprint 11 host-side stack landed and committed (boot.json read/write + slot selection, boot-count/rollback logic, .playosb bundle format + sign/verify, ApplyUpdate IPC, shell update UI, playos_system_os_version()).
Why This Sprint Exists
Sprint 11 delivered the A/B update machinery, but the runtime still boots entirely from the embedded initramfs. The squashfs image written to the inactive slot is never mounted as /, so "immutable root" and automatic rollback are not real end-to-end yet. This sprint closes that gap with the actual boot-path change, then validates it.
Start Condition Checklist
-
src/boot_slot.creads/writes/EFI/playos/boot.jsonand selects the active slot. -
Boot-count increment + 3-strike rollback logic exists (ShellReady OR 60-second timer →
mark_good). -
.playosbbundle creation (scripts/create-update-bundle.sh) and verification (src/sha256.c+src/update.c) exist. -
Shell software-update UI and
playos_system_os_version()(reads active slot fromboot.json) exist. -
Open question (resolve first): does the
playos-refdistrobuild produce a complete bootable squashfs rootfs (full userspace: shell, compositor, runtime, platform-api, samples, libs, config), or only a partial artifact that the updater writes? This decides whether T1 is "wire the pivot" (small) or "build a full rootfs + minimal initramfs shim" (large). Resolved: theallydefconfig already builds a complete bootablerootfs.squashfs(full userspace present), so this sprint is the small "wire the pivot" path.
Decisions Locked for This Sprint
- Initramfs role: becomes a minimal early-boot shim; the real root is the active slot's squashfs image.
- Boot sequence: mount ESP read-write → read
boot.json→ select active slot → mount its squashfs read-only → mount/data(rw) and/misc→pivot_root/switch_root→execthe system init. - Read-only guarantee: squashfs is inherently read-only; no
MS_RDONLYremount trickery needed. dm-verity remains post-MVP. - Slot metadata: stays in
boot.jsonon the ESP (no migration tomiscthis sprint). - No repartitioning: reuse the Sprint 10 5-partition layout unchanged.
Scope
In Scope
playos-refdistro— full bootable squashfs rootfs (if the open question shows it is partial) + a minimal initramfs shim.playos-init—pivot_root/switch_rootinto the active slot squashfs; wireboot_slot.c's active-slot selection into the mount path; remountdata/miscinside the new root.playos-init— make 3-strike rollback real end-to-end (a slot that fails to boot actually triggers a switch + reboot).- Validation matrix (was Sprint 11 S11-T9) on QEMU first, then the ROG Ally.
Explicitly Out of Scope
- dm-verity (post-MVP)
- Network update download (post-MVP)
- Production HSM update key (post-MVP)
- Delta updates (post-MVP)
- Migrating slot metadata from the ESP to
misc
Required Repository Changes
| Repo | Required work |
|---|---|
playos-refdistro | Full bootable squashfs rootfs build (if needed) + minimal initramfs shim |
playos-init | pivot_root/switch_root into the active slot squashfs; slot-selection wiring; real rollback |
playos-spec | This document |
Expected Files and Directories
playos-refdistro
br2-external/board/ally/ # rootfs-as-squashfs build changes (if the open question shows a partial rootfs)
br2-external/board/ally/initramfs/ # minimal early-boot shim (mount squashfs -> pivot_root)
playos-init
src/boot_slot.c # already implements selection; wire into the mount path
src/main.c # replace embedded-rootfs boot with pivot into the active slot
src/mount.c # mount active slot squashfs + remount data/misc in the new root
Agent Task Breakdown
Task Status Grid
| Task ID | Task | Primary repo | Status | Notes / evidence |
|---|---|---|---|---|
| S11.5-T1 | Confirm/produce a complete bootable squashfs rootfs + minimal initramfs shim | playos-refdistro | done | Full userspace confirmed in output/ally/images/rootfs.squashfs; added /EFI mountpoint to rootfs-overlay. |
| S11.5-T2 | pivot_root/switch_root into the active slot squashfs | playos-init | done | playos_pivot_to_active_slot() added in src/mount.c; switch_root idiom (MS_MOVE + chroot + exec /init). |
| S11.5-T3 | Wire boot_slot.c active-slot selection into the mount path | playos-init | done | boot_slot_read() selects playos-a/playos-b; called in main.c after ESP/boot-slot block. |
| S11.5-T4 | Make 3-strike rollback real end-to-end | playos-init | done | Host test_boot_slot covers boot-count/3-strike rollback; QEMU Scenario B proves forced rollback (boot.json flipped to slot b, slot a marked bad). |
| S11.5-T5 | A/B update + rollback validation matrix (was S11-T9) | playos-refdistro | in progress | Automated subset passes (host test_boot_slot 2/2 + QEMU Scenario A/B); fresh install → 5-partition layout → NVMe boot validated 2026-08-19 (S10-T8 re-install fix). Full 6-case matrix on ROG Ally still pending (real apply/reboot/mark-good timing, /data survival on real NVMe, live version API per slot) — blocked on Sprint 11.6 SSH/network reachability for remote execution. |
S11.5-T1 — Confirm/produce a complete bootable squashfs rootfs + minimal initramfs shim
Finding (to confirm before any code): the system currently boots from the embedded initramfs. It is unknown whether the refdistro build already produces a complete squashfs rootfs or only a partial artifact for the updater.
Steps:
- Inspect the refdistro build output: locate the squashfs artifact, list its top-level entries, and confirm whether it contains a complete userspace (shell, compositor, runtime, platform-api, samples,
/lib,/etc). - If complete: proceed to T2 — only the initramfs pivot path needs wiring.
- If partial: build the full rootfs as squashfs and reduce the initramfs to a minimal shim (mount ESP → read
boot.json→ mount active squashfs →pivot_root→exec /sbin/init).
Done when: the build produces a squashfs that contains the full system userspace, and the initramfs no longer carries the whole system.
S11.5-T2 — pivot_root/switch_root into the active slot squashfs
Steps:
- In
playos-init/src/main.c, after the early mount of the ESP andboot.jsonread, mount the active slot partition (by GPT labelplayos-a/playos-b) read-only at a staging path. - Mount
/data(labelplayos-data) read-write and/miscunder the staged new root. pivot_root(orswitch_rootif a minimal initramfs shim is used) into the squashfs, thenexecthe real init.- Log the transition:
playos-init: pivoted to active slot <a|b> (squashfs, read-only).
Done when: QEMU boots and cat /proc/mounts shows / on squashfs with ro; touch /usr/test fails with EROFS.
S11.5-T3 — Wire boot_slot.c active-slot selection into the mount path
Steps:
- Reuse the existing
boot_slot.cslot-selection result to chooseplayos-avsplayos-bat mount time. - Confirm a missing/corrupt
boot.jsonfalls back to slot A and recreates the file (behavior already specified in Sprint 11). - Keep
boot_slot.cas the single source of truth — do not duplicate slot-selection logic inmain.c.
Done when: changing active_slot in boot.json causes the next boot to mount the other slot.
S11.5-T4 — Make 3-strike rollback real end-to-end
Steps:
- Verify the existing rollback path fires when the new root fails to boot:
boot_count >= 3 && health != "good"→ markbad, switchactive_slot, reboot. - Ensure the boot-count increment and
mark_good(ShellReady/60s) survive the pivot — they must operate on the ESP, which stays mounted across the pivot. - Test that a deliberately broken slot (bad initramfs or missing squashfs) rolls back to the previous slot after 3 attempts.
Done when: corrupting the active slot causes 3 failed boots and an automatic switch to the previous slot.
S11.5-T5 — A/B update + rollback validation matrix
Full test matrix (QEMU first, then ROG Ally):
- Fresh install → 5-partition layout →
boot.jsonshows slot A good. - Apply valid bundle →
boot.jsonshows slot B pending → reboot → slot B active → 60s → slot B good. - Corrupt slot B → reboot 3× → rollback to slot A.
/datacontent (game files, saves) survives update and rollback unchanged.- Invalid-signature bundle → rejected before any partition write.
playos_system_os_version()returns the correct version for each slot.
Done when: all 6 cases pass with log evidence.
Implementation Guidance
Resolve T1 before coding T2–T4
T1 determines whether this sprint is a small boot-path wiring change or a larger rootfs-build effort. Do the refdistro build inspection first and report the finding before writing any pivot_root code.
Keep boot_slot.c the single source of truth
Slot selection, boot counting, and rollback all live in boot_slot.c. main.c should consume its result, not re-derive it.
Atomic commits
S11.5-T1: build full bootable squashfs rootfs + minimal initramfs shim
S11.5-T2: pivot into active slot squashfs
S11.5-T3: wire active-slot selection into boot mount path
S11.5-T4: make 3-strike rollback real end-to-end
S11.5-T5: A/B validation matrix + evidence
Verification and Evidence
| Evidence | How it is produced | Current state |
|---|---|---|
| Read-only root | cat /proc/mounts shows / on squashfs ro; touch /usr/test → EROFS | ⚠️ Not yet asserted by the QEMU harness (pivot is asserted; ro/EROFS check not captured) |
| Slot selection | changing active_slot in boot.json changes the mounted slot | ✅ QEMU Scenario A pivots to slot a read from boot.json; Scenario B writes active_slot: "b" |
| Rollback | 3-strike test log showing automatic slot switch | ✅ QEMU Scenario B flips to b and marks a bad; host test_boot_slot covers boot-count logic |
| Update apply/verify | host test_boot_slot valid / bad-signature / bad-magic cases | ✅ ctest 2/2 pass |
| Data survival | file hashes before/after update match | ⚠️ Pending ROG Ally — not covered by the QEMU harness |
| Signature rejection | log showing bundle rejected before any write | ✅ host test_boot_slot bad-signature case |
| Version API | playos_system_os_version() per slot | ⚠️ Code inspection only; hardware per-slot run pending |
Acceptance Criteria
- System boots from the read-only squashfs active slot (not the embedded initramfs) — QEMU Scenario A
-
touch /usr/testfails withEROFS— squashfs ro is by construction, but the current QEMU harness does not assert this -
Active-slot selection in
boot.jsondrives which slot is mounted — QEMU Scenario A readsactive_slot: "a" -
A valid bundle applied to the inactive slot does not affect the running system — host
test_boot_slotapply path (by design, apply writes only to the inactive slot) - After reboot, the system boots from the newly updated slot — full apply→reboot→active cycle not yet run on QEMU or hardware
-
Successful boot marks the new slot
health = "good"after 60 seconds — logic covered by host test; real 60s timer not yet exercised -
3 consecutive failed boots trigger rollback to the previous slot — QEMU Scenario B + host
test_boot_slot -
/datacontent survives update and rollback unchanged — not yet exercised on QEMU or hardware -
Invalid-signature bundle is rejected before any partition write — host
test_boot_slotbad-signature case - QEMU validation passes for the bootable subset — Scenario A (pivot) + Scenario B (forced rollback)
- ROG Ally passes the full 6-case matrix — pending Sprint 11.6 SSH/network reachability
Handoff to Sprint 12
Sprint 12 (Security Hardening) may assume:
- The system image is immutable at runtime (read-only squashfs root).
- Signed A/B updates with automatic rollback are functional on the automated/QEMU subset; the full hardware matrix is pending Sprint 11.5 closure, executed over SSH after Sprint 11.6 is complete.
- Games and user data live on
/dataand are unaffected by updates and rollback.
Exit Gate
The system boots from the read-only squashfs active slot, A/B updates can be applied and automatically rolled back on failure, and the full validation matrix passes on QEMU and the ROG Ally (the Ally portion executed over SSH once Sprint 11.6 is complete).
Previous: Sprint 11 | Next: Sprint 12
Sprint 11.6 — Developer SSH (Dropbear) + Minimal Wired Network Bring-Up
Goal: Unblock Sprint 11.5's ROG Ally A/B hardware validation (and future on-device debugging) by adding a minimal, developer-only SSH path over USB-C Ethernet now, while keeping full Wi-Fi networking in Sprint 16.
Primary Outcome: With a USB-C Ethernet adapter plugged into the ROG Ally, the device obtains an IPv4 address via DHCP and exposes a Dropbear SSH server that only accepts developer-supplied public-key authentication. Host keys and authorized_keys persist under /data/ssh; playos-init supervises the bring-up; games and production behavior are unchanged.
Status: 🟡 In progress — code changes complete; playos-init host build/tests pass; QEMU validation (T5) done (DHCP lease + public-key SSH login verified in QEMU); ROG Ally validation still pending hardware.
Prerequisites: Sprint 11.5 in progress (hardware A/B matrix pending); Sprint 10 installed/deployed rootfs path; playos-init supervision framework in place.
Why This Sprint Exists
Sprint 11.5 needs a full 6-case A/B matrix on real ROG Ally hardware, but debugging that matrix on a handheld without a keyboard is painful — log files on the USB stick only go so far when the issue is a boot or rollback edge case. A wired SSH shell makes it practical to inspect boot.json, /proc/mounts, and live logs directly. Full Wi-Fi is a larger, later sprint (Sprint 16) with playos-net, wpa_supplicant, dhcpcd, and a shell UI; that work should not be pulled forward just to get a debug shell. This sprint therefore delivers the minimal wired slice now and leaves Wi-Fi where it is.
Start Condition Checklist
- Sprint 11.5 T1–T4 landed; hardware A/B matrix still pending.
-
playos-inithas a supervision loop that forks/execs trusted daemons after the data mount. -
/datais the persistent writable partition (labelplayos-data), mounted read-write byplayos_mount_data. -
Rootfs is read-only squashfs; persistent state must live under
/data, not/etc. -
playos-refdistroAlly defconfigs (playos_ally_defconfig,playos_ally_installer_defconfig) currently enable neither Dropbear nor network packages. -
Ally kernel configs defer
CONFIG_NETDEVICES(# CONFIG_NETDEVICES is not set) and# CONFIG_WIRELESS is not set. -
Buildroot has Dropbear 2026.94,
wpa_supplicant,dhcpcd,connman, andnetwork-manageravailable; this sprint installs Dropbear +dhcpcdand uses BusyBoxudhcpcfor the wired bring-up.
Decisions Locked for This Sprint
- Option 3 — Both, staged. Deliver minimal wired SSH (USB-C Ethernet) now; keep full Wi-Fi in Sprint 16. Sprint 16 is not absorbed or renamed by this sprint.
- Transport now: USB-C Ethernet via common USB-NIC drivers (ASIX
AX8817x/ASIX, RealtekRTL8152, CDC Ethernet/NCM/EEM, RNDIS host, SMSC95xx/MCS7830). No Wi-Fi, noRFKILL, nonl80211, nowpa_supplicantin this sprint. - SSH daemon: Dropbear (
BR2_PACKAGE_DROPBEAR=y), public-key auth only. No password auth, no baked credentials, no committed private keys. Disable reverse DNS (BR2_PACKAGE_DROPBEAR_DISABLE_REVERSEDNS=y) and include the client (BR2_PACKAGE_DROPBEAR_CLIENT=y). - DHCP client:
dhcpcdremains installed (BR2_PACKAGE_DHCPCD, the client Sprint 16 already locks innetwork-options.md§10), but the bring-up uses the already-present BusyBoxudhcpcapplet (CONFIG_UDHCPC=y) because the defaultdhcpcdsample config rejects QEMU slirp DHCP offers. No new BusyBox applets are added; Sprint 16 may still usedhcpcdfor the Wi-Fi interface. - Persistent SSH state: Dropbear host keys and
authorized_keyslive under/data/ssh/(persistentplayos-data), not/etc/dropbearor/var/run/dropbear(tmpfs/ephemeral) and not on the read-only squashfs. Bring-up bind-mounts/data/sshover/root/.ssh(Dropbear's defaultauthorized_keyslocation). - Bring-up ownership: a small rootfs-overlay helper (
/usr/bin/playos-ssh-bringup) handles interface bring-up, DHCP, host-key generation, bind-mounting, andexec dropbear.playos-initforks/execs it as a supervised trusted daemon after the data mount, alongside compositor/shell/overlay. - Access channel: developer-only, no shell UI, no
playos-runtimemessages, no game access. Access is gated by public-key auth: a developer drops their public key at/data/ssh/authorized_keys. - Production split: this sprint enables SSH in the dev/installer image. Sprint 12 removes Dropbear and BusyBox from the production image (as already specified in S12-T7).
Scope
In Scope
playos-refdistro— kernel config: enableNETDEVICESplus USB-NIC and QEMU test NIC drivers (no wireless).playos-refdistro— Ally/installer defconfigs: enabledropbear(+ client,DISABLE_REVERSEDNS) anddhcpcd.playos-refdistro— rootfs overlay:/usr/bin/playos-ssh-bringupand a build-time/root/.sshdirectory (bind-mount target on the read-only squashfs).playos-init— spawn and superviseplayos-ssh-bringupafter/datamounts.playos-spec— this document;SUMMARY.md+roadmap.mdwiring; a networking note inkernel-config.md.
Explicitly Out of Scope
- Wi-Fi (
CFG80211/MAC80211/MT7921E,wpa_supplicant,dhcpcd,playos-net, shell Wi-Fi UI, profiles) — Sprint 16. - Bluetooth, D-Bus, NetworkManager,
connman,iwd,openssh. - Game/application network access and per-game network allowlists.
- On-device SSH management UI, profiles, or key-management screens.
- OTA download and
playos-toolsnetworking — still post-MVP / after Sprint 16. - Production hardening: removing Dropbear/BusyBox from the production image is Sprint 12, not this sprint.
Required Repository Changes
| Repo | Required work |
|---|---|
playos-refdistro | Kernel USB-NIC config; Dropbear + dhcpcd packages; playos-ssh-bringup overlay script; /root/.ssh build-time directory |
playos-init | Supervise playos-ssh-bringup as a trusted daemon after the data mount |
playos-spec | This sprint; SUMMARY.md and roadmap.md links; kernel-config.md networking note |
Expected Files and Directories
playos-refdistro
br2-external/board/ally/linux.config # enable NETDEVICES + USB-NIC drivers
br2-external/board/ally/linux-installer.config # same for installer kernel
br2-external/configs/playos_ally_defconfig # dropbear + dhcpcd
br2-external/configs/playos_ally_installer_defconfig # dropbear + dhcpcd
br2-external/board/common/rootfs-overlay/usr/bin/playos-ssh-bringup
br2-external/board/common/rootfs-overlay/etc/dhcpcd.conf # minimal dhcpcd config kept for Sprint 16 QEMU work
br2-external/board/common/rootfs-overlay/root/.ssh/ # bind-mount target (exists in squashfs)
playos-init
src/supervisor.c # spawn_ssh_bringup() alongside the other trusted daemons
include/playos-init/supervisor.h # declaration (path verified against existing headers)
playos-spec
src/sprints/Sprint-11.6.md # this document
src/kernel-config.md # note: USB-NIC enabled for developer SSH; Wi-Fi still deferred
Agent Task Breakdown
Task Status Grid
| Task ID | Task | Primary repo | Status | Notes / evidence |
|---|---|---|---|---|
| S11.6-T1 | Enable USB-NIC kernel config (no wireless) | playos-refdistro | done | Kernel config edited; build validation pending in T5 |
| S11.6-T2 | Enable Dropbear + dhcpcd | playos-refdistro | done | Defconfigs edited; build validation pending in T5 |
| S11.6-T3 | playos-ssh-bringup + playos-init supervision | playos-refdistro, playos-init | done | Script written; supervision code compiled + host tests pass |
| S11.6-T4 | Host keys + authorized_keys persistence under /data/ssh | playos-refdistro | done | Script generates keys + bind-mounts; /root/.ssh overlay shipped |
| S11.6-T5 | QEMU + Ally validation | playos-refdistro | in progress | QEMU done: udhcpc lease 10.0.2.15 + public-key SSH login verified; Ally pending hardware |
Update the Status column as work progresses: not started → in progress → blocked or done.
S11.6-T1 — Enable USB-NIC kernel config
Candidate symbols (verify exact names against the 6.12 Kconfig tree before editing):
CONFIG_NETDEVICES=y
CONFIG_ETHERNET=y
CONFIG_MII=y
CONFIG_USB_NET_DRIVERS=y
CONFIG_USB_USBNET=y
CONFIG_USB_NET_AX8817X=y
CONFIG_USB_NET_AX88179_178A=y
CONFIG_USB_RTL8152=y
CONFIG_USB_NET_CDCETHER=y
CONFIG_USB_NET_CDC_NCM=y
CONFIG_USB_NET_CDC_EEM=y
CONFIG_USB_NET_RNDIS_HOST=y
CONFIG_USB_NET_SMSC95XX=y
CONFIG_USB_NET_MCS7830=y
# QEMU/dev test NICs:
CONFIG_VIRTIO_NET=y
CONFIG_E1000=y
CONFIG_E1000E=y
CONFIG_NET, CONFIG_PACKET, CONFIG_UNIX, and CONFIG_INET are already present. Do not enable CONFIG_WIRELESS, CONFIG_CFG80211, or CONFIG_MAC80211 in this sprint.
Done when: the built kernel exposes a USB NIC (and, in QEMU, the test NIC) as an interface after module/device load.
S11.6-T2 — Enable Dropbear + dhcpcd
- Add
BR2_PACKAGE_DROPBEAR=y,BR2_PACKAGE_DROPBEAR_CLIENT=y, andBR2_PACKAGE_DROPBEAR_DISABLE_REVERSEDNS=yto the Ally and installer defconfigs. - Add
BR2_PACKAGE_DHCPCD=y(the same client Sprint 16 uses). BusyBoxudhcpc/ipare already enabled via the existing BusyBox config and are used by the bring-up; no new BusyBox applets are added. - Keep this scoped to the dev/installer image; Sprint 12 strips Dropbear from production, and
dhcpcdremains for Sprint 16.
Done when: dropbear and dhcpcd are present in the built rootfs and both link without errors.
S11.6-T3 — playos-ssh-bringup + supervision
Create /usr/bin/playos-ssh-bringup in the rootfs overlay:
- Detect a wired NIC (iterate
/sys/class/net/*, skip loopback). If none, log and exit cleanly (USB NIC may be hot-plugged later). - Run
udhcpc -i <if> -qto bring the interface up and obtain/keep a lease (udhcpcdaemonises by default). mkdir -p /data/sshand generate Dropbear host keys withdropbearkeyif absent (RSA/ed25519).mount --bind /data/ssh /root/.ssh(the/root/.sshtarget must already exist in the read-only squashfs).exec dropbear -F -R -E(foreground soplayos-initcan supervise it;-R/-Echosen to match the configured Dropbear behavior, verified at implementation time).
playos-init/src/supervisor.c gains spawn_ssh_bringup() and calls it after /data mounts, alongside the other trusted daemons. Restart policy mirrors the existing supervision loop.
Done when: after boot, Dropbear is visible as a supervised child of PID 1, and a USB-C Ethernet adapter receives a DHCP lease.
S11.6-T4 — Persist host keys and authorized keys
- Dropbear host keys live at
/data/ssh/dropbear_*_host_key, generated once and reused across boots. authorized_keyslives at/data/ssh/authorized_keys; the bind-mount makes it visible at/root/.ssh/authorized_keys.- Document the developer setup: copy the developer's public key to
/data/ssh/authorized_keyson the mountedplayos-datapartition. - Do not commit any private key or seed an
authorized_keysin the repo.
Done when: a reboot preserves the host key (no re-generation / client key-change warning) and a public key in /data/ssh/authorized_keys permits login.
S11.6-T5 — QEMU + Ally validation
- QEMU: boot with a NIC (e.g.
-netdev user -device virtio-net-pci); assert a wired interface appears (via/sys/class/net) and a DHCP lease (udhcpc) is obtained; assert SSH key login works. - Ally: with a USB-C Ethernet adapter, assert
ip addr+ DHCP lease and SSH key login; confirm no impact on audio/input/shell boot. - Confirm production image is unchanged by this sprint (Sprint 12 remains responsible for stripping debug tools).
Done when: both QEMU and Ally show a DHCP lease and a successful public-key SSH login, with log evidence.
Implementation Guidance
Verify Kconfig symbol names first. The Linux 6.12 tree has renamed/aliased some USB-NIC symbols; confirm the exact names against buildroot/output/*/build/linux-* before editing linux.config and linux-installer.config.
Persistent state belongs on /data, never /etc or /var/run. The rootfs is read-only squashfs and /var/run is tmpfs, so Dropbear host keys must be generated under /data/ssh.
Create the bind-mount target at build time. Because the squashfs is read-only, mkdir -p /root/.ssh at runtime would fail; the /root/.ssh directory must exist in the rootfs overlay before the image is built.
Key auth only. Do not enable password auth and do not bake any credential into the image. A developer enables access by placing their public key at /data/ssh/authorized_keys.
Keep it developer-only. No shell settings screen, no playos-runtime IPC, no game access, no production removal — production removal is Sprint 12.
Verification and Evidence
| Evidence | How it is produced |
|---|---|
| USB NIC appears | /sys/class/net shows an eth*/en* interface |
| DHCP lease | udhcpc log shows an IPv4 address assigned |
| SSH supervised | playos-init supervision log shows Dropbear as a child; ps shows it under PID 1 |
| Key persistence | reboot without host-key regeneration; existing authorized_keys still works |
| Login works | ssh -i <devkey> root@<ip> succeeds |
| No wireless pulled in | kernel config still has # CONFIG_WIRELESS is not set |
| Production unchanged | production defconfig still excludes Dropbear/BusyBox (Sprint 12) |
Acceptance Criteria
- ROG Ally with USB-C Ethernet obtains an IPv4 address via DHCP
- Dropbear SSH accepts a developer-supplied public key and rejects password login
-
Dropbear host keys persist across reboots under
/data/ssh -
authorized_keyspersists under/data/sshand survives reboot -
playos-initsupervises the SSH bring-up as a trusted daemon - QEMU boot with a NIC reaches a DHCP lease and SSH login
-
No Wi-Fi or
wpa_supplicantcode is introduced in this sprint - Games and existing shell/audio/input behavior are unchanged
Handoff to Sprint 12 / Sprint 16
Sprint 12 (Security Hardening) may assume:
- SSH (
dropbear) is enabled in the dev/installer image and must be removed from the production image (S12-T7). - The SSH bring-up is a trusted, supervised child of
playos-init, not a user-facing feature.
Sprint 16 (playos-net) may assume:
- The immediate wired debug path uses BusyBox
udhcpc(because the defaultdhcpcdconfig is slirp-incompatible); Sprint 16 may standardize ondhcpcdfor the Wi-Fi interface instead of introducing a different DHCP stack. - Sprint 16 adds Wi-Fi (
wpa_supplicant+playos-net) on top ofdhcpcd, without absorbing this sprint.
Exit Gate
A ROG Ally with a USB-C Ethernet adapter obtains a DHCP lease and accepts a public-key SSH login for on-device debugging, with host keys and authorized_keys persisted under /data/ssh, while full Wi-Fi remains deferred to Sprint 16.
Previous: Sprint 11.5 | Next: Sprint 12
Sprint 12 — Security Hardening
Goal: Establish a hardened boundary between the public playos-platform-api, trusted playos-runtime control paths, and untrusted game processes. Games operate with minimal privileges. Remote debug services are removed from production builds. Secure Boot signing chain is defined.
Primary Outcome: A game process cannot access trusted IPC endpoints, cannot open DRM primary nodes, cannot write outside its own save/cache directories, and cannot synthesize reserved system input. Production builds ship without a shell or debug services.
Prerequisites: Sprint 11 complete — immutable images and A/B updates working.
Why This Sprint Exists
Before Sprint 12, the security boundary is mostly a convention. Games are spawned by playos-init (PID 1, root) with a plain fork() + exec() and no credential drop, so a game can open /dev/input/event* directly and read reserved buttons, and the reserved-button protection is only a cooperative bitmask in libplayos. Production builds still carry a shell and debug tools. This sprint moves the boundary from convention to OS enforcement: an unprivileged game identity, capability and syscall restrictions, filesystem isolation, hardened control IPC, and debug-free production images.
Start Condition Checklist
- Sprint 11 complete: the system image is read-only and A/B updates work.
playos-initcurrently spawns games as root with no credential drop (verified gap).- Reserved buttons are currently stripped only by a software mask in
libplayos(playos_input_get_controller_state()), not OS-enforced. /run/playos/control.sockexists and is the trusted control path.- Production
defconfigstill includes debug tools (BusyBox,gdbserver,strace, etc.). - ROG Ally kernel version is checked for Landlock support (kernel ≥ 5.13).
Decisions Locked for This Sprint
- Game identity: games run as
playos-game(UID ~1000) with no supplementary groups. No per-profile Linux uid is introduced; console-style local profiles are a future data-path layer over this single identity. - Sandbox parameterization: the sandbox path policy is parameterized by launch identity (game id now; a profile id later), so a future profile can be inserted without reworking enforcement.
- Capability set: games start with no capabilities;
prctl(PR_SET_NO_NEW_PRIVS, 1)is set before exec. - DRM access: games connect through the Wayland seat; direct
/dev/dri/card*access is denied. - Input boundary: reserved buttons are intercepted at the libinput/seat layer and never forwarded; the
libplayosmask is defense-in-depth only. - Sandbox mechanism: a seccomp-BPF syscall allowlist plus a Landlock filesystem allowlist, both applied before game exec.
- Control socket:
/run/playos/control.sockisroot:playos-trusted, mode0660; onlyplayos-shellandplayos-overlayare inplayos-trusted. - Manifest signing: Ed25519 detached signature alongside
manifest.json; verification is warn-only this sprint. - Secure Boot: documentation plus development signing keys only; production HSM-backed signing is post-MVP.
Scope
In Scope
- Game process privilege reduction and credential drop in
playos-init. - seccomp syscall allowlist and Landlock filesystem allowlist for games.
- Denial of direct DRM primary-node and raw input-device access.
- Control IPC socket ownership and permission hardening.
- Removal of debug tools/services from the production image plus CI lint.
- Signed-manifest foundations (warn-only verification).
- Secure Boot chain documentation and development signing keys.
Explicitly Out of Scope
- Hard enforcement of signed game manifests (post-MVP).
- Production HSM-backed Secure Boot signing (post-MVP).
- Full dm-verity/IMA verified-boot implementation (documented as a target only).
- Network sandboxing (no network stack in the MVP).
- Store-level signing and distribution.
- Multi-user local profiles (deferred to Sprint 21); this sprint only establishes the single unprivileged identity and data-driven sandbox they will build on.
Required Repository Changes
| Repo | Required work |
|---|---|
playos-init | Spawn games as playos-game with PR_SET_NO_NEW_PRIVS and no capabilities; apply seccomp allowlist and Landlock ruleset; verify manifest signatures in warn-only mode |
playos-compositor | Intercept reserved buttons at the libinput/seat layer and never forward them to clients |
playos-runtime | Enforce control socket ownership and permissions (root:playos-trusted, 0660) |
playos-refdistro | Create playos-game user, integrate seccomp/Landlock into the game spawn path, remove debug tools from the production defconfig, add post-build lint, wire sbsign/pesign and development EFI keys |
playos-spec | Document the security model, Secure Boot chain, and manifest signing scheme |
Expected Files and Directories
playos-init
src/security/
sandbox.c # PR_SET_NO_NEW_PRIVS, capability drop, setuid to playos-game
seccomp_filter.c # build-time generated seccomp-BPF allowlist
landlock.c # Landlock allowed/denied path ruleset
manifest_verify.c # Ed25519 detached-signature check (warn-only)
playos-compositor
src/system_button.c # reserved-button interception at the libinput/seat layer
playos-refdistro
br2-external/board/ally/users-table.txt # playos-game UID ~1000, no supplementary groups
br2-external/board/ally/post-build.sh # production lint: assert no debug binaries
br2-external/configs/playos_ally_production_defconfig
scripts/sign-efi.sh # sbsign/pesign with the development key
keys/dev/ # development EFI signing key (not production HSM)
playos-spec
src/security-model.md # updated: §8 reserved-button boundary, sandbox, Secure Boot chain
Agent Task Breakdown
Task Status Grid
| Task ID | Task | Primary repo | Status | Notes / evidence |
|---|---|---|---|---|
| S12-T1 | Drop game privileges at spawn: UID ~1000, PR_SET_NO_NEW_PRIVS, capability drop | playos-init | not started | |
| S12-T2 | Apply Landlock allowed/denied path policy to game processes | playos-init | not started | |
| S12-T3 | Apply seccomp syscall allowlist to game processes | playos-init | not started | |
| S12-T4 | Grant only DRM render-node access; deny privileged KMS/master | playos-init | not started | |
| S12-T5 | Enforce reserved-button input isolation end-to-end | playos-compositor, playos-platform-api, playos-init | not started | |
| S12-T6 | Harden control.sock trusted-client auth and permission checks | playos-runtime, playos-init | not started | |
| S12-T7 | Strip debug tools/services from the production image | playos-refdistro | not started | |
| S12-T8 | Verify signed game manifests (warn-only in MVP) | playos-init, playos-runtime | not started | |
| S12-T9 | Document Secure Boot chain; create and rotate dev signing keys in image build | playos-refdistro, playos-spec | not started |
Update the Status column as work progresses: not started → in progress → blocked or done.
S12-T1 — Drop game privileges at spawn
playos-init spawns games as the playos-game user (UID ~1000, no supplementary groups), calls prctl(PR_SET_NO_NEW_PRIVS, 1) on the game process, and drops all capabilities from the effective and permitted sets before exec. The playos-compositor keeps CAP_SYS_ADMIN (DRM master), but games never inherit it.
Done when: from inside a running game, id shows uid=playos-game, /proc/self/status shows NoNewPrivs: 1, and CapEff: 0000000000000000.
S12-T2 — Apply Landlock filesystem restrictions
Build a Landlock ruleset granting exactly the paths a game needs and default-deny everything else: /data/games/<game-id>/ read-only, /data/saves/<game-id>/ and /data/cache/<game-id>/ read-write, /tmp or /run/game-<id>/ read-write scratch, and /run/playos/ execute-only for the Wayland socket. Deny other games' data, /data/config/, /run/playos/control.sock, /dev/input/event*, and /proc/*/. Build the ruleset from launch-time variables (game id now; a profile-id prefix later) so Sprint 21 can add profile scoping by changing one path-construction function, not the enforcement logic. If the kernel is older than 5.13, fall back to logging-only enforcement with an alert.
Done when: a game process cannot open() another game's save directory or read /data/config/; the unsupported-kernel fallback logs an alert without blocking launch.
S12-T3 — Apply seccomp syscall allowlist
Generate a seccomp-BPF allowlist at build time covering the core syscall set (memory, file I/O, AF_UNIX sockets, process, signals, time, getrandom, limited prctl). Deny mount, umount2, init_module, finit_module, ptrace, reboot, setuid, setgid, setcap, and open/openat on /proc/*/mem, /dev/dri/card*, /dev/input/event*, and the control socket. Use libseccomp to generate and test the filter.
Done when: mount() from a game process returns EPERM, and open() of /proc/*/mem, /dev/dri/card*, /dev/input/event*, or the control socket is denied.
S12-T4 — Restrict DRM node access
/dev/dri/card* is owned by the drm group; games are not in drm. Games reach the GPU through the Wayland seat rather than by opening device nodes directly. Verify the primary node is unopenable from the game identity.
Done when: open("/dev/dri/card0", O_RDWR) from a game process returns EACCES.
S12-T5 — Enforce reserved-button input isolation
Reserved buttons (SYSTEM, QUICK_MENU) are intercepted by the compositor at the libinput/seat layer and never forwarded to clients. Games run outside the input group, so /dev/input/event* is unopenable; Landlock and seccomp also name those paths explicitly. The existing libplayos software mask remains as defense-in-depth, not the sole mechanism.
Done when: open("/dev/input/event0", O_RDONLY) from a game process returns EACCES, and SYSTEM/QUICK_MENU never appear in a game's input stream.
S12-T6 — Harden control socket
/run/playos/control.sock is owned by root:playos-trusted with mode 0660. Only playos-shell and playos-overlay are in the playos-trusted group; game processes are never in that group.
Done when: connect() to /run/playos/control.sock from a playos-game process returns EACCES.
S12-T7 — Remove debug tools from production build
The production defconfig excludes BusyBox (/bin/sh, /bin/busybox), the SSH daemon, gdbserver/strace/ltrace, evtest/modetest and graphics diagnostic tools, and ships with no open listening TCP/UDP sockets. A post-build script asserts their absence, and CI has a production-image lint step that fails if debug artifacts are present. The development image retains all debug tools behind BR2_PACKAGE_PLAYOS_DEV_TOOLS.
Done when: the production image contains no busybox, gdbserver, or strace and no open sockets; the CI lint step fails if any of these appear.
S12-T8 — Verify signed game manifests (warn-only)
Define the manifest signing format: an Ed25519 detached signature alongside manifest.json. Implement signature verification in playos-init, but run it in warn-only mode — log a warning if the signature is missing or invalid, and do not block launch. Hard enforcement is deferred to a later sprint or post-MVP.
Done when: an unsigned manifest produces a warning in the log and the game still launches.
S12-T9 — Document Secure Boot chain and development keys
Document the target signed chain: UEFI Secure Boot signs BOOTX64.EFI; BOOTX64.EFI signs or contains the kernel; the kernel verifies the initramfs via dm-verity or IMA; A/B update bundles are signed with the PlayOS update key. Generate a self-signed development EFI signing key and add sbsign/pesign to the build pipeline. Production signing uses an HSM-backed key (post-MVP).
Done when: the development EFI key exists under keys/dev/ and is used by CI builds, security-model.md documents the chain, and production HSM signing is explicitly deferred.
Implementation Guidance
Drop privileges before exec, not after. The game must never execute with root credentials; perform the credential drop and PR_SET_NO_NEW_PRIVS in the child process before execve.
Name the denied paths explicitly. Both the seccomp open/openat filter and the Landlock ruleset must name /dev/input/event*, /dev/dri/card*, /proc/*/mem, and the control socket. Do not rely on default-deny alone.
Treat the libplayos mask as defense-in-depth. The real boundary is the seat plus group permissions plus Landlock plus seccomp; the software bitmask is a convenience, not the enforcement point.
Landlock fallback must be loud, not silent. If the kernel is too old, log an alert and continue, but record it as an unresolved hardening gap rather than failing the whole launch path.
Keep Landlock path construction data-driven. Build paths in one function taking the launch identity (game id now, profile id later) so the ruleset does not encode raw path strings and Sprint 21 can add profile scoping without touching enforcement.
Production lint fails the build. Debug-artifact absence is enforced in CI, not checked manually.
Keep Secure Boot scoped this sprint. Ship documentation and development keys only; do not attempt production key management.
Verification and Evidence
| Evidence | How it is produced |
|---|---|
Game runs as playos-game with no capabilities | id and /proc/self/status captured inside a test game |
| Primary DRM node denied | Security test binary attempts open("/dev/dri/card0", O_RDWR) |
| Raw input device denied | Security test binary attempts open("/dev/input/event0", O_RDONLY) |
| Reserved buttons absent | Input-stream dump while SYSTEM/QUICK_MENU are pressed |
| Control socket denied | Security test binary attempts connect() to control.sock |
| seccomp active | Security test binary attempts mount() and observes EPERM |
| Landlock active | Security test binary reads another game's save directory and is denied |
| Production image is debug-free | CI production lint log and post-build script output |
| Manifest warn-only behavior | Launch log for an unsigned manifest |
| Secure Boot chain documented | security-model.md plus keys/dev/ in CI artifacts |
Acceptance Criteria
-
Game process runs as
playos-gameuser (verified viaplayos_system.htest call or logs) -
open("/dev/dri/card0", O_RDWR)returnsEACCESin a game process -
open("/dev/input/event0", O_RDONLY)returnsEACCESin a game process -
Reserved buttons (
SYSTEM/QUICK_MENU) never appear in a game's input stream (compositor-intercepted, not just libplayos-masked) -
connect()to/run/playos/control.sockreturnsEACCESfrom aplayos-gameprocess -
seccomp filter:
mount()from game process returnsEPERM -
Landlock: game cannot
open()another game's save directory -
Landlock: game cannot read
/data/config/ -
Production build contains no
busybox,gdbserver,strace, or open TCP sockets - Post-build production lint CI step passes
- Development image retains full debug tools
- Manifest signature verification runs in warn-only mode (warning in log for unsigned manifests)
- Development EFI signing key exists and is used in CI builds
- All existing sprint acceptance criteria still pass (no regression)
Handoff to Sprint 13
Sprint 13 may assume:
- Games run as
playos-gamewith no capabilities andNoNewPrivsset. - The control socket is
root:playos-trustedmode0660; games cannot connect. - Production images are debug-tool-free; development images retain debug tools.
- The Landlock/seccomp sandbox is active on the AMD ROG Ally path.
- Manifest verification and Secure Boot are foundations only, not hard enforcement.
Exit Gate
Game processes are privilege-reduced and cannot access trusted IPC, DRM primary nodes, or other games' data. Production builds ship without debug services. Security restrictions do not break any existing functionality.
Previous: Sprint 11.6 | Next: Sprint 13
Sprint 13 — Intel Expansion
Goal: Prove that the PlayOS architecture, compositor, and playos-platform-api backend model are portable to Intel graphics hardware. The compositor selects the correct GPU by PCI enumeration, not by a hardcoded device path. A second libplayos input/graphics backend compiles and runs on an Intel PC.
Primary Outcome: PlayOS boots and runs the full shell + game lifecycle on an Intel-graphics PC (NUC, laptop, or similar). No code path is hardcoded to AMD. The playos-platform-api backend abstraction is validated as truly portable.
Prerequisites: Sprint 12 complete — AMD implementation complete and hardened.
Why This Sprint Exists
Sprint 12 hardened the AMD ROG Ally path, but that success is still a single-vendor result. The compositor already enumerates DRM devices and selects by PCI identity (Sprint 4), so the question this sprint answers is whether that selection logic, the backend model, and the Buildroot image pipeline generalize without AMD-specific assumptions creeping back in. A second hardware target also forces the platform API to earn its abstraction: if Intel bring-up requires changes to the public headers or to the compositor's device-selection logic, the abstraction is not real yet. This sprint validates portability and produces a second supported target.
Start Condition Checklist
- Sprint 12 complete: the AMD implementation is hardened and all AMD acceptance criteria pass.
- The compositor already enumerates DRM devices and selects by PCI identity (Sprint 4 deliverable).
- The supported vendor IDs are defined:
PCI_VENDOR_AMD 0x1002,PCI_VENDOR_INTEL 0x8086. - The GPU selection fallback order is documented and implemented: active connector → AMD → Intel → first valid DRM device → fatal.
- An Intel-graphics PC (NUC, laptop, or test machine) is available for device tests.
- The AMD ROG Ally smoke-test checklist is current and repeatable for regression use.
Decisions Locked for This Sprint
- GPU selection fallback order: active display connector → AMD (primary when multiple GPUs) → Intel → first valid DRM device → fatal if none found.
- No hardcoded paths:
card0,amdgpu, or vendor-specific strings must not appear in the compositor or inlibplayospublic API. - Intel kernel: use
CONFIG_DRM_I915orCONFIG_DRM_XEdepending on target hardware generation; disable AMD-only configs in the Intel defconfig. - Intel audio:
CONFIG_SND_HDA_INTELplus Intel-specific codecs. - Intel power:
CONFIG_X86_INTEL_PSTATEandCONFIG_INTEL_RAPL. - Mesa backend:
gallium-drivers=irisfor Gen 9+ (i965for older); Intel Vulkan (ANV) is deferred to a future Vulkan sprint. - Backend selection:
playos-platform-apiselects its backend at runtime via thePLAYOS_BACKENDenvironment variable, with a hardware-agnostic evdev input path and a PCI-vendor-based GPU query path. - Power interface:
playos_power_request_profile()stays hardware-agnostic on the EPP sysfs interface.
Scope
In Scope
- Validate the existing PCI-based GPU discovery logic against Intel hardware.
- Add an Intel PC Buildroot defconfig with Intel kernel, audio, power, and firmware options.
- Enable the Mesa Iris Gallium backend for Intel and verify hardware acceleration.
- Formalize the internal
PlayOSInputBackendabstraction andPLAYOS_BACKENDselection. - Validate
playos_power_get_info()and profile requests against Intel sysfs paths. - Add
make intel-config,make intel-build, andmake intel-usb-imagetargets. - Validate all three sample games on the Intel PC.
- Document the dual-vendor support matrix and backend portability guidance.
Explicitly Out of Scope
- Intel Vulkan (ANV) — deferred to a future Vulkan sprint.
- Runtime testing of Intel hardware in CI — Intel device tests are device-only; CI covers cross-compilation.
- Automatic backend auto-detection beyond the documented env-var and PCI-vendor paths.
- Multi-GPU simultaneous rendering (one active GPU is selected; the other is ignored).
Required Repository Changes
| Repo | Required work |
|---|---|
playos-compositor | Validate GPU selection by PCI vendor, log the selected vendor/device/path, add a fallback-order test, remove any residual card0 hardcoding |
playos-platform-api | Formalize PlayOSInputBackend and PLAYOS_BACKEND dispatch; validate Intel power sysfs paths and non-AMD device strings |
playos-refdistro | Add playos_intel_pc_defconfig, Intel kernel configs/firmware, Mesa Iris, and make intel-* targets |
playos-samples | Run sample-triangle, sample-input, and sample-audio on the Intel PC and record portability evidence |
playos-spec | Update the supported-hardware matrix and add backend-portability guidance plus Intel bring-up notes |
Expected Files and Directories
playos-compositor
src/gpu_select.c # PCI vendor selection, fallback order, selected-vendor logging
src/compositor.c # consumes the selected DRM device; no card0/vendor strings
tests/test_gpu_select.c # fallback-order unit test with fake DRM/vendor data
playos-platform-api
src/input_backend.c # PlayOSInputBackend dispatch via PLAYOS_BACKEND env var
src/power_intel.c # Intel power sysfs queries (coretemp, GPU hwmon, EPP)
src/playos_system.c # device-model string: non-AMD value on Intel targets
playos-refdistro
br2-external/configs/playos_intel_pc_defconfig
br2-external/board/intel/linux-fragment.cfg # DRM_I915/XE, SND_HDA_INTEL, INTEL_PSTATE, INTEL_RAPL
br2-external/board/intel/firmware.list # i915 firmware blobs
Makefile # make intel-config / intel-build / intel-usb-image
gen-intel-usb-image.sh # USB-bootable Intel PC image
playos-samples
docs/intel-portability-validation.md # per-game Intel results: render, input, audio, lifecycle
playos-spec
src/hardware-matrix.md # updated: AMD ROG Ally + Intel PC supported targets
src/backend-portability.md # new: backend model, PLAYOS_BACKEND, power sysfs matrix
src/sprints/Sprint-13.md # this sprint
Agent Task Breakdown
Task Status Grid
| Task ID | Task | Primary repo | Status | Notes / evidence |
|---|---|---|---|---|
| S13-T1 | Validate GPU discovery by PCI vendor and fallback order on Intel hardware | playos-compositor | not started | |
| S13-T2 | Add Intel PC kernel configuration and firmware | playos-refdistro | not started | |
| S13-T3 | Enable Mesa Iris Gallium backend for Intel | playos-refdistro | not started | |
| S13-T4 | Formalize PlayOSInputBackend and PLAYOS_BACKEND dispatch | playos-platform-api | not started | |
| S13-T5 | Validate Intel power sysfs paths and device strings | playos-platform-api | not started | |
| S13-T6 | Add make intel-* Buildroot targets and USB image generation | playos-refdistro | not started | |
| S13-T7 | Validate sample-game portability on the Intel PC | playos-samples | not started | |
| S13-T8 | Document dual-vendor support in the specs | playos-spec | not started |
Update the Status column as work progresses: not started → in progress → blocked or done.
S13-T1 — Validate GPU discovery by PCI vendor
The compositor already enumerates DRM devices and selects by PCI identity (Sprint 4). Verify on Intel hardware that the selection logic picks the Intel device without code changes. Enforce the fallback order — active connector → AMD → Intel → first valid DRM device → fatal — and log the selected vendor ID, device ID, and device path. Confirm no card0 or vendor-specific hardcoding remains.
Done when: the compositor log shows the Intel vendor ID (0x8086) and device path on an Intel PC, the fallback-order unit test passes with synthetic multi-GPU data, and a grep of playos-compositor finds no card0 hardcoding.
S13-T2 — Add Intel PC kernel configuration
Create br2-external/configs/playos_intel_pc_defconfig from a known Intel-compatible configuration. Add Intel GPU support (CONFIG_DRM_I915 or CONFIG_DRM_XE depending on target generation), Intel audio (CONFIG_SND_HDA_INTEL plus codecs), and Intel power options (CONFIG_X86_INTEL_PSTATE, CONFIG_INTEL_RAPL). Include the i915/ firmware blobs. Disable AMD-only configs (CONFIG_DRM_AMDGPU, CONFIG_X86_AMD_PSTATE) in the Intel defconfig.
Done when: the Intel defconfig builds a kernel where the required Intel config symbols are enabled and the AMD-only symbols are disabled, as shown by the generated .config.
S13-T3 — Enable Mesa Iris backend
Configure Buildroot Mesa with gallium-drivers=iris for Intel Gen 9+ (or i965 for older), keeping GBM, EGL, and OpenGL ES the same as the AMD config. Intel Vulkan (ANV) is deferred. Verify at runtime that Mesa reports an Intel renderer.
Done when: on an Intel PC, Mesa initialization logs Mesa ... on Intel ... (or the equivalent Intel renderer string), and sample-triangle renders with hardware acceleration rather than a software fallback.
S13-T4 — Formalize PlayOSInputBackend
Define the internal backend struct in playos-platform-api/src/ with name, init, get_controller_state, and shutdown members, and dispatch at runtime from the PLAYOS_BACKEND environment variable. The evdev input path remains hardware-agnostic and works unchanged; GPU info queries select by PCI vendor via playos_system.h. Public headers must not change to support the second backend.
Done when: setting PLAYOS_BACKEND selects the requested backend without recompiling public headers, and the AMD backend continues to pass its existing tests unchanged.
S13-T5 — Validate Intel power sysfs paths
Verify the Intel power sysfs paths: CPU temperature via the coretemp thermal zone, GPU temperature via /sys/class/drm/card*/device/hwmon/hwmon*/temp1_input, power profile via /sys/devices/system/cpu/cpu0/cpufreq/energy_performance_preference (the same EPP interface as AMD P-state), and battery via /sys/class/power_supply/BAT*/. Confirm playos_power_request_profile() works on Intel without modification.
Done when: playos_power_get_info() returns valid CPU and battery data on the Intel PC, and playos_system_device_model() returns a non-AMD device string.
S13-T6 — Add make intel-* targets
Add make intel-config, make intel-build, and make intel-usb-image to the playos-refdistro Makefile, plus a gen-intel-usb-image.sh for a USB-bootable Intel PC image. Wire the Intel build into CI as a cross-compilation target.
Done when: make intel-build compiles cleanly in CI and produces the Intel image artifacts alongside the existing AMD artifacts.
S13-T7 — Validate sample-game portability
Run sample-triangle, sample-input, and sample-audio on the Intel PC and confirm hardware-accelerated rendering, controller input (USB gamepad if no built-in controller), audio output, and the system-button/lifecycle flow. Record the results in docs/intel-portability-validation.md.
Done when: all three sample games run on the Intel PC with the same behavior as on AMD, and the portability validation document is committed with per-game evidence.
S13-T8 — Document dual-vendor support
Update the supported-hardware matrix to list both the AMD ROG Ally and the Intel PC, and add backend-portability guidance covering the PlayOSInputBackend model, PLAYOS_BACKEND, GPU selection fallback order, and the power sysfs matrix.
Done when: hardware-matrix.md lists both targets and backend-portability.md is committed and linked from the spec index.
Implementation Guidance
Validate, don't rewrite, the selection logic. The GPU selection already exists from Sprint 4. The goal is to prove it generalizes — add logging and a test, not a second selection path.
No vendor strings in the public API. If Intel support requires a change to playos-platform-api public headers, the abstraction has failed; fix the abstraction instead.
Keep CI compile-only for Intel. Intel runtime tests are device-only. CI validates that the Intel defconfig cross-compiles; do not gate the sprint on Intel hardware in CI.
Disable, don't just omit, AMD options. The Intel defconfig must explicitly disable CONFIG_DRM_AMDGPU and CONFIG_X86_AMD_PSTATE so the result is unambiguous.
Preserve the EPP power interface. Do not fork the power API per vendor; the energy-performance-preference sysfs interface is shared and should stay shared.
Treat Intel Vulkan as explicitly deferred. Do not pull ANV into this sprint's scope; record it as a future-sprint follow-up only.
Verification and Evidence
| Evidence | How it is produced |
|---|---|
| Intel GPU selected by PCI enumeration | Compositor boot log on the Intel PC shows 0x8086 and the device path |
| Fallback order correct | test_gpu_select.c runs synthetic multi-GPU vendor data through the selector |
| Intel kernel config correct | Generated .config diff against the Intel defconfig |
| Mesa Intel renderer | Compositor/Mesa init log on the Intel PC |
| Hardware acceleration | sample-triangle renderer string and frame throughput on the Intel PC |
| Input portability | sample-input controller-state dump on the Intel PC |
| Audio portability | sample-audio output on the Intel PC |
| Power API valid | playos_power_get_info() output for CPU, GPU, and battery on the Intel PC |
| Non-AMD device string | playos_system_device_model() output on the Intel PC |
| Intel image builds | CI log for make intel-build and produced image artifacts |
| AMD regression | Full ROG Ally smoke-test checklist after Intel changes land |
Acceptance Criteria
- PlayOS boots on an Intel-graphics PC (NUC, laptop, or test machine)
- Compositor selects the Intel DRM device by PCI enumeration (no hardcoded path)
- Compositor log shows Intel vendor ID and Mesa Iris (or i965) renderer
-
sample-triangleruns with hardware acceleration on Intel (Mesa reportsIntel ...) -
sample-inputreceives controller input on Intel PC (USB gamepad or built-in) -
sample-audioplays audio on Intel PC - System button and lifecycle flow works on Intel PC
-
playos_power_get_info()returns valid CPU and battery data on Intel -
playos_system_device_model()returns a non-AMD device string - AMD ROG Ally tests are unaffected — all Sprint 12 acceptance criteria still pass
-
No
card0,amdgpu, or AMD-specific hardcoded strings in compositor orlibplayospublic API -
make intel-buildsucceeds in CI (using a cross-compilation target)
Handoff to Sprint 14
Sprint 14 may assume:
- PlayOS runs the full console lifecycle on both the AMD ROG Ally and an Intel PC.
- The compositor selects the GPU by PCI enumeration with a tested fallback order and no hardcoded paths.
- The
playos-platform-apibackend model is validated as portable viaPlayOSInputBackendandPLAYOS_BACKEND. - The Mesa Intel (Iris) backend is enabled and hardware acceleration is verified.
make intel-buildandmake intel-usb-imagetargets exist and compile in CI.- The supported-hardware matrix and backend-portability docs are committed.
Exit Gate
PlayOS runs the full console lifecycle on an Intel-graphics PC. No hardcoded AMD/Intel paths remain in the compositor or libplayos. The playos-platform-api backend model is validated as portable.
Previous: Sprint 12 | Next: Sprint 14
Sprint 14 — Production Readiness
Goal: Deliver a signed preview release of PlayOS with a stable, versioned public Platform API, complete documentation, a validated release pipeline, and a full smoke-test pass on physical ROG Ally hardware.
Primary Outcome: PlayOS v0.1.0 is a signed, installable release that meets all 19 MVP criteria. The libplayos C ABI is documented and stable. A second developer can build a game using only the public API documentation.
Prerequisites: Sprint 13 complete — all MVP features implemented, Intel expansion validated.
Why This Sprint Exists
All MVP features are implemented by the end of Sprint 13, but the project is not yet shippable. The public libplayos API has never been frozen, versioned, or documented, so any external developer would be building against a moving target. Release artifacts are produced by hand, there is no tag-triggered CI pipeline, recovery mode is only partially specified, and performance has never been measured. This sprint turns a working prototype into a signed, installable preview release: it freezes and documents the ABI, automates the release, proves the MVP criteria on real hardware, and closes the recovery and documentation gaps.
Start Condition Checklist
- Sprint 13 complete: all MVP features are implemented and Intel expansion is validated.
- The seven public headers exist in
playos-platform-api/include/playos/:playos_input.h,playos_lifecycle.h,playos_system.h,playos_storage.h,playos_audio.h,playos_power.h,playos_logging.h. - The public API is currently unversioned — no
PLAYOS_API_VERSION, no library version, no SONAME policy. - Release images are produced manually; there is no
release.ymlworkflow. - Recovery mode is partially specified: A/B rollback and factory reset exist from Sprint 10, but the recovery UI is not implemented.
- No performance baseline has been measured or documented.
Decisions Locked for This Sprint
- API version: set
PLAYOS_API_VERSION 1inplayos.h. - Library version:
LIBPLAYOS_VERSION_MAJOR 0,MINOR 1,PATCH 0. - SONAME:
libplayos.so.0for this release. - Compatibility policy: minor versions are backward-compatible; a major version bump is breaking.
- Breaking-change process: breaking changes after v0.1.0 require an RFC in
playos-spec, an ADR, a major version bump, and a migration guide. - Release trigger and tag: the pipeline runs on a version tag push such as
v0.1.0. - Release artifacts:
playos-v0.1.0-rog-ally-installer.img,playos-v0.1.0-rog-ally-update.playosb,playos-v0.1.0-sdk-headers.tar.gz, plus SHA256 checksums and signatures. - Recovery rendering: recovery must work without AMDGPU, using SimpleDRM or software rendering.
- Performance targets: the baseline table below is the acceptance target; any metric more than 2× over target is a documented gap.
Scope
In Scope
- Formal API compatibility review of all seven public headers.
- Versioning:
PLAYOS_API_VERSION 1, library version, and SONAMElibplayos.so.0. - Doxygen documentation plus code examples and a getting-started guide for every API group.
- Game-developer guides in
playos-spec/docs/. - Tag-triggered release pipeline in
playos-refdistro/.github/workflows/release.yml. - Full 19-criterion MVP smoke test on physical ROG Ally hardware.
- Minimal recovery UI and recovery entry points.
playos-speccompletion: README, architecture, roadmap, platform API, IPC, security model, ADRs, docs.- Performance baseline measurement and documentation.
- Production image hygiene: no debug tools, signed EFI artifact, signed update bundle.
Explicitly Out of Scope
- Breaking changes to the public API (this sprint freezes v0.1.0; changes go through the post-v0.1.0 process).
- Production HSM-backed signing — the pipeline uses the development signing key only.
- Intel Vulkan (ANV) and multi-GPU work (deferred to future sprints).
- Network stack (no network in the MVP).
Required Repository Changes
| Repo | Required work |
|---|---|
playos-platform-api | API stability review, versioning and SONAME, Doxygen docs, code examples, getting-started guide |
playos-refdistro | Release pipeline, recovery image/menu, performance measurement, production image hygiene |
playos-init | Recovery entry logic: boot-count exceeded, button hold, repeated compositor failure |
playos-shell | Minimal recovery UI (text or simple Raylib on SimpleDRM/framebuffer) |
playos-spec | Authoritative reference completion: README, architecture, roadmap, platform API, IPC, security, ADRs, game-dev docs |
| All repos | Version tags and CHANGELOG updates for the v0.1.0 release |
Expected Files and Directories
playos-platform-api
include/playos/playos.h # PLAYOS_API_VERSION 1 + LIBPLAYOS_VERSION_* macros
CMakeLists.txt # SONAME libplayos.so.0
docs/api/ # rendered Doxygen output
examples/ # per-API-group code examples + minimal game
docs/getting-started.md # minimal game using input, lifecycle, storage, logging
playos-refdistro
.github/workflows/release.yml # tag-triggered v0.1.0 release pipeline
versions.lock # all component versions pinned
br2-external/board/ally/recovery/ # recovery menu sources and SimpleDRM/framebuffer config
br2-external/configs/playos_ally_recovery_defconfig
playos-init
src/recovery.c # recovery entry: boot-count, button hold, compositor-failure retry
playos-shell
src/recovery_menu.c # minimal recovery menu: logs, factory reset, rollback, shutdown, reboot
playos-spec
README.md
architecture.md
roadmap.md
platform-api.md
runtime-ipc.md
security-model.md
adr/ # all ADRs from Sprints 0-14
docs/ # game-developer guides
Agent Task Breakdown
Task Status Grid
| Task ID | Task | Primary repo | Status | Notes / evidence |
|---|---|---|---|---|
| S14-T1 | Freeze the public API and set PLAYOS_API_VERSION 1 | playos-platform-api | not started | |
| S14-T2 | Set library version and SONAME libplayos.so.0 | playos-platform-api | not started | |
| S14-T3 | Complete Doxygen docs and code examples | playos-platform-api | not started | |
| S14-T4 | Implement the tag-triggered release pipeline | playos-refdistro | not started | |
| S14-T5 | Run the full 19-criterion MVP smoke test | playos-refdistro | not started | |
| S14-T6 | Implement recovery mode | playos-init, playos-shell, playos-refdistro | not started | |
| S14-T7 | Measure and document the performance baseline | playos-refdistro | not started | |
| S14-T8 | Complete playos-spec and game-developer guides | playos-spec | not started | |
| S14-T9 | Enforce production image hygiene and signed artifacts | playos-refdistro | not started |
Update the Status column as work progresses: not started → in progress → blocked or done.
S14-T1 — Freeze the public API
Conduct a formal API compatibility review for every public header in include/playos/. For each API, check enum-value stability (adding is safe, removing is breaking), struct-layout stability (adding fields without versioning is ABI-breaking), function-signature stability, return-value semantics, error handling, and thread-safety guarantees. Set PLAYOS_API_VERSION 1 in playos.h. Any breaking change found must be resolved before the freeze — either avoided or deferred behind the post-v0.1.0 process.
Done when: playos.h defines PLAYOS_API_VERSION 1, the review checklist is documented for all seven headers, and no unresolved breaking change remains.
S14-T2 — Version the library and SONAME
Set LIBPLAYOS_VERSION_MAJOR 0, MINOR 1, PATCH 0, and set the SONAME to libplayos.so.0 in the playos-platform-api build. Document the compatibility policy: minor versions are backward-compatible; a major version bump is breaking. Document the breaking-change process: RFC in playos-spec, ADR, major version bump, and migration guide.
Done when: the built library reports SONAME libplayos.so.0, the version macros are exported, and the compatibility policy is documented in playos-spec.
S14-T3 — Complete Doxygen documentation
Add Doxygen comments to every public symbol across the seven public headers, generate rendered docs into docs/api/, and write code examples for each API group. Write the "Getting Started" guide: create a minimal game that uses input, lifecycle, storage, and logging using only the public API.
Done when: Doxygen generates clean output with no undocumented public symbols, and the getting-started example compiles against the public headers.
S14-T4 — Implement the release pipeline
Create playos-refdistro/.github/workflows/release.yml, triggered by a version tag push such as v0.1.0. The pipeline locks component versions in versions.lock, builds the production ROG Ally image (no debug tools, signed EFI artifact), builds the installer image, runs the QEMU boot test suite, verifies production lint, signs the EFI artifact and update bundle with the development key, and packages installer.img, update.playosb, and sdk-headers.tar.gz with SHA256 checksums and signatures before creating a GitHub Release.
Done when: pushing a test tag runs the pipeline end-to-end and produces all three artifacts plus checksums and a GitHub Release.
S14-T5 — Run the full MVP smoke test
Run the complete 19-criterion MVP checklist on physical ROG Ally hardware and record the status of every criterion. The checklist covers boot from UEFI, PID 1, compositor ownership, shell persistence, wlroots/Mesa stack, Raylib shell rendering, public C ABI usage, lifecycle transport, supervised game launch, first-frame switching, hardware-accelerated rendering with controller input, system-button flow, resume, audio, clean exit and crash recovery, persistent saves, immutable system image, and graphics-free recovery.
Done when: a committed test report shows all 19 criteria passing on physical ROG Ally hardware.
S14-T6 — Implement recovery mode
Implement a minimal recovery UI (text or simple Raylib on SimpleDRM/framebuffer). Recovery entry points are: boot count exceeds the A/B limit with both slots bad, a button hold at boot (e.g. Volume Down for 5 seconds), and playos-init entering recovery after repeated compositor failure. The menu offers: view system logs (/data/log/), factory reset (Sprint 10 logic), rollback to the previous system slot when available, shutdown, and reboot. Recovery must work without AMDGPU.
Done when: recovery is reachable from all three entry points and shows the menu with software/SimpleDRM rendering on hardware without AMDGPU.
S14-T7 — Measure and document the performance baseline
Measure the performance targets on ROG Ally hardware: cold boot to shell < 5s, shell to game first frame < 3s, system button to overlay < 100ms, game exit to shell < 500ms, sample-triangle 60 FPS at native resolution, direct scanout confirmed in the compositor log, and idle shell CPU < 2%. File a performance issue in playos-spec for any target not met and document the gap.
Done when: a committed performance report contains measurements for every metric and lists any gaps with filed issues.
S14-T8 — Complete playos-spec
Make playos-spec the authoritative reference: README.md (overview and navigation), architecture.md, roadmap.md (sprint plan and MVP criteria), platform-api.md (API contract and versioning policy), runtime-ipc.md (IPC protocol), security-model.md, adr/ with all ADRs from Sprints 0–14, and docs/ with the game-developer guides ("Building Your First PlayOS Game", lifecycle, storage, input, audio, and performance guides).
Done when: every listed spec document exists, is internally consistent, and links from README.md.
S14-T9 — Enforce production image hygiene and signed artifacts
Confirm the production image ships without debug tools, the EFI artifact is signed with the development key, and the update bundle is signed. Verify the release pipeline's production-lint step fails on any debug binary. Produce the SDK headers tarball and confirm it compiles a minimal game on a Linux host.
Done when: the v0.1.0 artifacts are signed and pass lint, and sdk-headers.tar.gz compiles a minimal game on a Linux host.
Implementation Guidance
Freeze first, document second. Run the ABI review and set PLAYOS_API_VERSION 1 before generating docs so the docs describe the frozen API, not a moving target.
Treat the smoke test as the release gate. The pipeline may pass in CI, but the release is not ready until the physical-hardware test report shows all 19 criteria passing.
Recovery must be graphics-independent. Do not make recovery depend on AMDGPU or hardware acceleration; validate it on SimpleDRM/software rendering.
Keep production signing on the development key. HSM-backed signing is post-MVP; do not introduce production key management in this sprint.
Document performance gaps, don't silently relax targets. If a metric misses, file an issue and record the measurement rather than editing the target.
Do not break the ABI to fix docs. If documentation reveals a design problem, defer it through the breaking-change process instead of changing the public headers this sprint.
Verification and Evidence
| Evidence | How it is produced |
|---|---|
| Public API frozen and versioned | playos.h shows PLAYOS_API_VERSION 1 and version macros |
| SONAME correct | readelf -d libplayos.so shows SONAME libplayos.so.0 |
| API fully documented | Doxygen output with zero undocumented public symbols |
| Release pipeline works | CI log from a test tag produces installer, update bundle, and SDK tarball |
| Installer boots a clean device | Physical install of playos-v0.1.0-rog-ally-installer.img on a clean ROG Ally |
| Update applies via A/B | update.playosb applied through the A/B flow |
| MVP criteria met | Committed smoke-test report with all 19 criteria passing |
| SDK usable by a second developer | Minimal game compiled from sdk-headers.tar.gz on a Linux host |
| Recovery works | Boot into recovery from button hold and boot-count-exceeded; menu on SimpleDRM |
| Performance baseline | Committed performance report with measurements and filed gaps |
| Specs complete | playos-spec README/nav plus all referenced docs present and consistent |
Acceptance Criteria
- All 19 MVP criteria pass on physical ROG Ally hardware
-
libplayospublic headers are fully documented (Doxygen) -
PLAYOS_API_VERSION 1defined; SONAME islibplayos.so.0 - "Building Your First PlayOS Game" guide is complete and tested
-
Release pipeline produces signed
installer.imgandupdate.playosbfrom a tag push -
playos-v0.1.0-rog-ally-installer.imginstalls successfully on a clean ROG Ally -
playos-v0.1.0-rog-ally-update.playosbapplies successfully via A/B update flow - SDK headers tarball compiles a minimal game on a Linux host
- Recovery mode is reachable and shows the recovery menu without AMDGPU
- Performance baseline documented; no metric is more than 2× over target
-
All ADRs from Sprints 0–14 are in
playos-spec/adr/ - CI release pipeline passes end-to-end on a test tag
-
playos-specrepository is complete and internally consistent
Handoff to Sprint 15
Sprint 15 may assume:
- PlayOS v0.1.0 is a signed, installable release that passes all 19 MVP criteria.
- The public
libplayosC ABI is frozen atPLAYOS_API_VERSION 1, versioned0.1.0, with SONAMElibplayos.so.0. - The SDK headers tarball exists and compiles a minimal game on a Linux host.
- Doxygen docs and game-developer guides are published in
playos-spec/docs/. - Recovery mode, performance baseline, and the tag-triggered release pipeline are in place.
- Breaking changes to the public API now require the RFC/ADR/major-bump/migration process.
Exit Gate
PlayOS v0.1.0 is a signed, installable release that passes all 19 MVP criteria on physical ROG Ally hardware. The public API is documented, stable, and versioned. A second developer can build and run a game using only the published SDK.
Previous: Sprint 13 | Next: Sprint 15
Sprint 15 — Game Developer SDK
Goal: Give third-party developers a self-contained SDK to build, run, and test a PlayOS game on a regular x86_64 Linux host — and iterate on Windows — without pulling the full Buildroot tree.
Primary Outcome: A downloadable playos-sdk (toolchain + libplayos/libraylib headers and libs) that turns standard gcc/cmake into a shippable bin/game + manifest.json + assets/ artifact, with a working desktop testing loop.
Prerequisites: Sprint 14 complete — versioned, stable public libplayos C ABI (PLAYOS_API_VERSION 1) and stable Raylib backend ABI.
Why This Sprint Exists
Today a game can only be built inside the Buildroot tree with the x86_64-buildroot-linux-musl toolchain, because the musl builds of libplayos and libraylib exist only there. The game ABI requires musl (not glibc), so a stock Ubuntu or glibc-linked binary won't run on device. This sprint packages that toolchain + libraries into a developer-facing SDK so third parties can build games independently, and provides a way to test them without ROG Ally hardware.
Start Condition Checklist
- Sprint 14 complete: the public
libplayosC ABI is frozen atPLAYOS_API_VERSION 1and the Raylib backend ABI is stable. sdk-headers.tar.gzexists from Sprint 14, but it is headers-only — it is not a full toolchain + library SDK.- musl builds of
libplayosandlibraylibexist only inside the Buildroot output tree. PLAYOS_BACKEND=stubexists inplayos-platform-apias the seed for the desktop host shim.- The QEMU/container boot path exists from the Sprint 14 release pipeline's QEMU boot test suite.
Decisions Locked for This Sprint
- SDK artifact: a self-contained
playos-sdkdownload that does not require the full Buildroot tree. - Device ABI: the shipped game binary must be musl-linked (
x86_64-buildroot-linux-musl); a glibc-linked binary is not device-compatible. - Toolchain packaging: a prebuilt
x86_64-buildroot-linux-musltoolchain tarball, or an Alpine/musl base image. - Three build profiles:
device,desktop, andemulator. - Device backend:
libraylibis built with thePLATFORM_PLAYOSbackend;libplayosuses the real evdev path. - Desktop shim: the host
libplayosshim is seeded fromPLAYOS_BACKEND=stuband maps keyboard/gamepad to the controller ABI, no-op'ing lifecycle calls. - Emulator profile: runs the
devicebuild inside the PlayOS QEMU/container image. - SDK home: SDK packaging and tooling live in
playos-tools.
Scope
In Scope
- Package the musl toolchain as a tarball and/or base image.
- Ship
libplayosheaders plus musl static/shared libraries atPLAYOS_API_VERSION 1. - Ship musl
libraylibheaders and libraries built withPLATFORM_PLAYOS. - Provide a CMake toolchain file and
pkg-configfiles for thedeviceprofile. - Build the
desktophost shim seeded fromPLAYOS_BACKEND=stub. - Implement the
desktopbuild profile (nativegcc+ raylib desktop backend + host shim). - Implement the
emulatorbuild profile (device build running in QEMU/container). - Build the reference sample entirely via the SDK and validate all three profiles.
- Document the SDK usage, profiles, and packaging layout.
Explicitly Out of Scope
- Native Windows musl cross-toolchain — Windows iteration is via the
desktopprofile/shim. - Store integration and SDK signing/distribution (post-MVP).
- Web, mobile, or other platform targets.
- Changes to the frozen
PLAYOS_API_VERSION 1public ABI.
Required Repository Changes
| Repo | Required work |
|---|---|
playos-tools | Package the SDK: toolchain, headers/libs, CMake toolchain, pkg-config, profile scripts, SDK docs |
playos-platform-api | Provide the desktop host shim seeded from PLAYOS_BACKEND=stub; ensure headers/libs are SDK-ready |
playos-shell | Export the PLATFORM_PLAYOS Raylib backend build for SDK packaging |
playos-refdistro | Extract musl libplayos/libraylib from Buildroot output; provide the QEMU/container emulator image |
playos-samples | Build a reference sample entirely via the SDK and validate device, desktop, and emulator profiles |
Expected Files and Directories
playos-tools
sdk/
toolchain/ # prebuilt x86_64-buildroot-linux-musl toolchain tarball or base image
include/playos/ # libplayos public headers
include/raylib.h # libraylib headers
lib/ # musl libplayos.a/.so and libraylib.a/.so
cmake/playos-toolchain.cmake
pkgconfig/playos.pc
pkgconfig/raylib-playos.pc
scripts/build-device.sh
scripts/build-desktop.sh
scripts/build-emulator.sh
docs/sdk.md # SDK usage, profile matrix, artifact layout
playos-platform-api
src/desktop_shim.c # host libplayos shim seeded from PLAYOS_BACKEND=stub
playos-shell
src/raylib/ # PLATFORM_PLAYOS backend exported for SDK packaging
playos-refdistro
scripts/export-sdk.sh # copies musl libplayos/libraylib from Buildroot output into the SDK tree
br2-external/configs/playos_emulator_defconfig # QEMU/container boot image for the emulator profile
playos-samples
sdk-reference/ # reference game built entirely via the SDK; exercises all three profiles
Agent Task Breakdown
Task Status Grid
| Task ID | Task | Primary repo | Status | Notes / evidence |
|---|---|---|---|---|
| S15-T1 | Package the musl toolchain tarball/base image | playos-tools | not started | |
| S15-T2 | Ship libplayos headers and musl static/shared libs | playos-platform-api, playos-tools | not started | |
| S15-T3 | Ship musl libraylib with the PLATFORM_PLAYOS backend | playos-shell, playos-refdistro, playos-tools | not started | |
| S15-T4 | Provide CMake toolchain and pkg-config for device | playos-tools | not started | |
| S15-T5 | Build the desktop host shim seeded from PLAYOS_BACKEND=stub | playos-platform-api, playos-tools | not started | |
| S15-T6 | Implement the desktop build profile | playos-tools | not started | |
| S15-T7 | Implement the emulator build profile | playos-refdistro, playos-tools | not started | |
| S15-T8 | Build the reference sample entirely via the SDK and validate all profiles | playos-samples, playos-tools | not started |
Update the Status column as work progresses: not started → in progress → blocked or done.
S15-T1 — Package the musl toolchain
Produce a self-contained x86_64-buildroot-linux-musl toolchain as a downloadable tarball, or provide an Alpine/musl base image that reproduces the same environment. The SDK must not require the full Buildroot tree to compile a game.
Done when: a fresh host with only the SDK toolchain installed can compile a minimal musl program that links and reports a musl ABI.
S15-T2 — Ship libplayos headers and musl libraries
Package the libplayos public headers plus musl static and shared libraries into the SDK at PLAYOS_API_VERSION 1. The shared library keeps SONAME libplayos.so.0. Both the device (real evdev) and desktop (shim) variants must be selectable without changing the public headers.
Done when: sdk/include/playos/ and sdk/lib/libplayos.{a,so} are present, and a game compiles against them with the SDK toolchain.
S15-T3 — Ship musl libraylib with PLATFORM_PLAYOS
Build and package libraylib as a musl static/shared library with the PLATFORM_PLAYOS backend, and ship its headers in the SDK. Export the backend build from playos-shell and extract the artifacts from Buildroot output via export-sdk.sh.
Done when: sdk/lib/libraylib.{a,so} and sdk/include/raylib.h are present, and sample-triangle-equivalent code compiles and links against the packaged Raylib.
S15-T4 — Provide CMake toolchain and pkg-config
Add cmake/playos-toolchain.cmake and pkg-config files (playos.pc, raylib-playos.pc) so a standard cmake or gcc $(pkg-config --cflags --libs playos) invocation produces a device-compatible musl binary.
Done when: cmake using the toolchain file and pkg-config using the .pc files both produce a valid musl device binary for the reference sample.
S15-T5 — Build the desktop host shim
Implement the host libplayos shim in playos-platform-api, seeded from the existing PLAYOS_BACKEND=stub. The shim maps keyboard/gamepad input to the controller ABI and no-ops lifecycle calls, so a game runs unchanged in a normal desktop window. Package it as the desktop-profile libplayos.
Done when: a game linked against the shim runs on a Linux host, receives keyboard/gamepad input as controller state, and its lifecycle calls are safely no-op'ed.
S15-T6 — Implement the desktop build profile
Add a desktop build profile that uses native gcc, Raylib's default desktop backend (X11/Wayland on Linux, Win32/GLFW on Windows), and the host shim from S15-T5. The same game.c used for device must build for desktop without source changes.
Done when: scripts/build-desktop.sh produces a native desktop binary from the same source as the device profile, and the game runs in a window with controller-equivalent input.
S15-T7 — Implement the emulator build profile
Add an emulator profile that runs the device build inside the PlayOS QEMU/container image for high-fidelity testing without hardware. Wire the profile through scripts/build-emulator.sh and a QEMU/container image suitable for booting the device artifact.
Done when: scripts/build-emulator.sh boots the device artifact in QEMU/container and the game renders and accepts input in the emulated environment.
S15-T8 — Validate the reference sample across all profiles
Build a reference sample in playos-samples/sdk-reference/ entirely through the SDK, then build and run it against the device, desktop, and emulator profiles. Record the results for each profile.
Done when: the reference sample produces a valid musl bin/game for device, runs in a desktop window for desktop, boots and renders in QEMU for emulator, and the validation results are committed.
Implementation Guidance
The SDK is a packaging problem first. The libraries already exist inside Buildroot; the work is extracting and organizing them so an external developer never sees the Buildroot tree.
Keep the three profiles a single source, three link/build configurations. Do not fork game code per profile; the whole point is that the same game.c builds everywhere.
Seed the shim, don't write a new backend. The desktop shim starts from PLAYOS_BACKEND=stub; it adds input mapping and lifecycle no-ops rather than inventing a parallel platform layer.
Enforce musl for device. A device build that silently falls back to glibc is a failure — verify the binary's interpreter/linkage as part of the profile scripts.
Keep Windows out of the cross-toolchain scope. Windows iteration goes through the desktop profile/shim; do not build a native Windows musl toolchain this sprint.
Ship pkg-config and CMake parity. Both paths must work, because different developers will prefer one; test both in the reference sample.
Verification and Evidence
| Evidence | How it is produced |
|---|---|
| SDK is self-contained | Fresh-host build of a minimal game with only the SDK installed |
| Toolchain is musl | file/readelf on a produced binary shows the musl interpreter |
libplayos packaged | sdk/lib/libplayos.{a,so} and headers present at PLAYOS_API_VERSION 1 |
libraylib packaged | sdk/lib/libraylib.{a,so} with PLATFORM_PLAYOS backend build |
| CMake path works | cmake build of the reference sample using playos-toolchain.cmake |
pkg-config path works | gcc $(pkg-config --cflags --libs playos) produces a device binary |
desktop shim works | Game runs in a window with keyboard/gamepad input on a Linux host |
desktop profile works | Native desktop binary built from the same game.c |
emulator profile works | Device artifact boots and renders in QEMU/container |
| Reference sample validated | Per-profile results committed in playos-samples |
Acceptance Criteria
-
A developer on a fresh x86_64 Ubuntu/Alpine host can produce a valid musl
bin/gamewith only the SDK installed. -
The same
game.cbuilds fordevice,desktop, andemulatorprofiles. -
The
desktopprofile runs the game in a windowed desktop environment on Linux (and, via a shim, Windows) with controller-equivalent input. -
The
emulatorprofile boots thedeviceartifact in QEMU and renders + accepts input. -
The SDK ships the musl toolchain,
libplayosheaders/libs atPLAYOS_API_VERSION 1, and musllibraylibwithPLATFORM_PLAYOS. -
Both the CMake toolchain and
pkg-configfiles produce a valid device binary. - The reference sample is built entirely via the SDK and validated across all three profiles.
-
The
devicebinary is confirmed musl-linked, not glibc-linked. -
SDK usage, profile matrix, and artifact layout are documented in
playos-tools/docs/sdk.md.
Handoff to Sprint 16
Sprint 16 may assume:
- A downloadable
playos-sdkships the musl toolchain,libplayos(PLAYOS_API_VERSION 1), and musllibraylibwithPLATFORM_PLAYOS. - The
device,desktop, andemulatorbuild profiles all work from a single game source. - The
desktophost shim is seeded fromPLAYOS_BACKEND=stuband maps keyboard/gamepad to the controller ABI. - The
emulatorprofile boots adeviceartifact in QEMU/container without hardware. - The reference sample builds entirely via the SDK and validates all three profiles.
- The public
libplayosABI remains frozen atPLAYOS_API_VERSION 1.
Exit Gate
A third-party developer can download playos-sdk, build a game for device with only the SDK installed, iterate on desktop without hardware, and validate on emulator in QEMU/container. The same source builds a shippable musl bin/game + manifest.json + assets/ artifact across all three profiles.
Previous: Sprint 14 | Next: Sprint 16
Sprint 16 — playos-net (Wi-Fi Networking)
Goal: Bring up Wi-Fi on the ROG Ally with a minimal, D-Bus-free stack — wpa_supplicant + dhcpcd + a trusted playos-net bridge — exposed to the shell through the existing playos-runtime control IPC. No D-Bus, no NetworkManager, no BusyBox in production.
Primary Outcome: The ROG Ally scans for networks, connects to a WPA2/WPA3 network, obtains an IP via DHCP, and the shell shows a working Wi-Fi settings screen (scan → connect → connected status). The whole path is driven through control.sock, exactly like LaunchGame.
Status: 🟡 Post-MVP — not started. Stack decision recorded in network-options.md §10 (Option B).
Prerequisites: MVP complete (Sprint 15) and Sprint 12 security hardening (Landlock/seccomp, playos-trusted group) in place.
Why This Sprint Exists
MVP deliberately ships with no network stack. Every Tier-1 post-MVP feature — store downloads, cloud saves, network update download, and SSH Developer Mode — depends on Wi-Fi. This sprint delivers the first networking capability while honouring the core architectural constraint: the existing playos-runtime IPC is the only control plane, and D-Bus is not introduced.
See network-options.md for the full options analysis (iwd + D-Bus vs wpa_supplicant vs custom nl80211).
Start Condition Checklist
- Sprint 15 complete; MVP (19 criteria in
roadmap.md) verified on hardware. - Sprint 12 hardening merged:
playos-trustedgroup, Landlock, seccomp, production image has no BusyBox. network-options.md§10 decision accepted (Option B —wpa_supplicant+dhcpcd).- Kernel currently defers
CFG80211/MAC80211/MT7921E(kernel-config.md§Networking).
Decisions Locked for This Sprint
wpa_supplicant, notiwd— built D-Bus-free:CONFIG_CTRL_IFACE=unix,CONFIG_CTRL_IFACE_DBUS=n. Talks to the kernel overnl80211.dhcpcd, not BusyBoxudhcpc— standalone DHCPv4/DHCPv6 + IPv4LL client (BR2_PACKAGE_DHCPCD). Production has no BusyBox.- No D-Bus. This is the entire point of the chosen stack.
playos-netbridge daemon — linkslibwpa_client(wpa_ctrl), translates wpa_supplicant's control protocol ↔playos-runtimeJSON frames.- Control plane =
control.sock— new network messages ride the existing trusted socket; the shell is the only UI. - Trust boundary —
wpa_supplicantanddhcpcdcontrol sockets live under/run/playos/net/, ownedroot:playos-trusted, mode0660. Games are not inplayos-trusted, so they never reach them. - Auth scope — WPA2-PSK and WPA3-SAE (wpa_supplicant's in-tree SAE). No EAP/enterprise (802.1X) yet.
- No game network access — networking is a system/shell capability only. A per-game allowlist is a separate, later decision.
Scope
In Scope
- Kernel:
CFG80211,MAC80211,MT7921E(AMD RZ616 = rebranded MediaTek MT7922),RFKILL. - MediaTek
mt7921/mt7922firmware blobs (redistributable vialinux-firmware). - Buildroot packages:
wpa_supplicant(D-Bus disabled),dhcpcd,playos-net. playos-netdaemon (new): wpa_supplicant control socket ↔playos-runtimeIPC bridge.playos-runtime: new control messages (scan, connect, disconnect, status, async events).playos-init: spawn and supervisewpa_supplicant,dhcpcd, andplayos-net.playos-shell: Wi-Fi settings screen (scan list, connect with passphrase, live status).- Network profiles persisted under
/data/config/network/(SSID + PSK).
Explicitly Out of Scope
- Bluetooth (separate — BlueZ is D-Bus-only; see
network-options.md§8). - D-Bus, NetworkManager,
iwd. - Game/application network access (per-game allowlist is a later decision).
- EAP/enterprise auth (802.1X), VPN, proxy.
- SSH Developer Mode (Dropbear) — depends on this sprint but is its own work package.
- Wi-Fi Direct, hotspot, mesh, captive-portal detection.
Required Repository Changes
| Repo | Required work |
|---|---|
playos-net (new) | Bridge daemon: wpa_ctrl ↔ playos-runtime JSON; profile management |
playos-runtime | Network control messages + framing docs |
playos-init | Supervise wpa_supplicant/dhcpcd/playos-net; network policy |
playos-shell | Wi-Fi settings screen |
playos-refdistro | Kernel config, wpa_supplicant + dhcpcd + playos-net packages, firmware overlay |
playos-spec | This sprint; runtime-ipc.md network messages; kernel-config.md networking section |
playos-netis a new small daemon. It may start asplayos-refdistro/src/playos-net/(asplayos-overlaydid) before promotion to its own repo.
Expected Files and Directories
playos-net (new)
src/main.c # daemon loop: connect to wpa_ctrl + control.sock
src/wpa_bridge.c # wpa_supplicant control-protocol translation
src/profiles.c # load/store /data/config/network/*.json
include/playos_net.h # internal message types (mirrors runtime IPC)
playos-runtime
proto/network.json # new message schemas (Scan/Connect/Disconnect/Status)
playos-refdistro
br2-external/configs/playos_rog_ally_defconfig # enable CFG80211/MAC80211/MT7921E/RFKILL
board/playos/rog-ally/rootfs-overlay/lib/firmware/mediatek/ # mt7921/mt7922 blobs
br2-external/package/playos-net/ # new package
playos-shell
src/ui/network.c # Wi-Fi settings screen (scan/connect/status)
Agent Task Breakdown
Task Status Grid
| Task ID | Task | Primary repo | Status | Notes / evidence |
|---|---|---|---|---|
| S16-T1 | Enable Wi-Fi kernel config + firmware | playos-refdistro | not started | CFG80211/MAC80211/MT7921E currently deferred |
| S16-T2 | Package wpa_supplicant (D-Bus-free) + dhcpcd | playos-refdistro | not started | CONFIG_CTRL_IFACE_DBUS=n |
| S16-T3 | Implement playos-net bridge daemon | playos-net | not started | wpa_ctrl ↔ control.sock |
| S16-T4 | Add network messages to playos-runtime | playos-runtime | not started | additive; keep v: 1 |
| S16-T5 | Supervise network daemons in playos-init | playos-init | not started | |
| S16-T6 | Wi-Fi settings screen in playos-shell | playos-shell | not started | |
| S16-T7 | Network profile persistence | playos-net | not started | /data/config/network/ |
| S16-T8 | End-to-end validation (Ally + QEMU) | playos-refdistro | not started |
S16-T1 — Enable Wi-Fi kernel config + firmware
CONFIG_CFG80211=y
CONFIG_MAC80211=y
CONFIG_MT7921E=y # AMD RZ616 (MediaTek MT7922) on ROG Ally
CONFIG_RFKILL=y
- Add the MediaTek
mt7921/mt7922Wi-Fi firmware toboard/playos/rog-ally/rootfs-overlay/lib/firmware/mediatek/(redistributable vialinux-firmware, unlike AMD GPU blobs). - Done when: the Ally's
mt7921einterface appears (ip linkshowswlan0/mlan0after firmware load).
S16-T2 — Package wpa_supplicant (D-Bus-free) + dhcpcd
- Build
wpa_supplicantwithCONFIG_CTRL_IFACE=unix,CONFIG_CTRL_IFACE_DBUS=n; internal crypto (no kernel-crypto dependency). - Build
dhcpcd(BR2_PACKAGE_DHCPCD). - Control sockets:
/run/playos/net/wpa.sockand/run/playos/net/dhcpcd.sock, ownedroot:playos-trusted0660. - Done when: both binaries link and their control sockets are restricted to the trusted group.
S16-T3 — Implement playos-net bridge daemon
- Links
libwpa_client; connects to/run/playos/net/wpa.sockandcontrol.sock. - Translates wpa_supplicant control-protocol events (
CTRL-EVENT-CONNECTED,CTRL-EVENT-SCAN-RESULTS,CTRL-EVENT-DISCONNECTED) intoplayos-runtimeJSON. - Runs with dropped privileges in
playos-trusted; never exposes wpa_supplicant directly to games. - Done when: a
ScanNetworksrequest oncontrol.sockreturns a live scan result list.
S16-T4 — Add network messages to playos-runtime
New additive messages on control.sock (keep "v": 1; framing unchanged):
{ "v": 1, "type": "ScanNetworks" }
{ "v": 1, "type": "ScanResults", "networks": [ { "ssid": "…", "security": "wpa2", "signal_dbm": -54 } ] }
{ "v": 1, "type": "ConnectNetwork", "ssid": "…", "psk": "…", "security": "wpa2" }
{ "v": 1, "type": "ConnectNetworkAck", "ssid": "…" }
{ "v": 1, "type": "ConnectNetworkError", "ssid": "…", "reason": "auth_failed" }
{ "v": 1, "type": "DisconnectNetwork" }
{ "v": 1, "type": "NetworkStatus" }
{ "v": 1, "type": "NetworkStatusReport", "state": "connected", "ssid": "…", "ip": "192.168.1.10", "signal_dbm": -54 }
{ "v": 1, "type": "NetworkStateChanged", "state": "connecting" } /* async: connecting|connected|disconnected */
Done when: the message set is documented in runtime-ipc.md and the schemas build.
S16-T5 — Supervise network daemons in playos-init
playos-initspawns and superviseswpa_supplicant,dhcpcd, andplayos-netafter the data partition mounts (network profiles live on/data).- Restart policy mirrors other trusted daemons (exponential backoff, log on crash).
- Done when: all three daemons appear as supervised children of PID 1 and survive a
kill -9restart.
S16-T6 — Wi-Fi settings screen in playos-shell
- Scan list with SSID, signal strength, and security badge.
- Connect flow: select network → on-screen passphrase entry (overlay virtual keyboard) → connect → status.
- Live status indicator (connected SSID + IP, or "no network").
- All actions go through
control.sock; the shell never talks wpa_supplicant directly. - Done when: navigating Settings → Wi-Fi shows real networks and connects with a passphrase.
S16-T7 — Network profile persistence
- Store known networks under
/data/config/network/<profile>.json(SSID + PSK; never log the PSK). - Auto-connect to the most recently used known network on boot.
- Done when: after a reboot, the Ally reconnects to a previously saved network without re-entering the passphrase.
S16-T8 — End-to-end validation (Ally + QEMU)
- Real connection: scan → connect (WPA2-PSK and WPA3-SAE) → DHCP lease → reach the gateway.
- Lifecycle: airplane/off state, disconnect, reconnect, reboot persistence.
- Trust boundary: as
playos-game,connect()to/run/playos/net/wpa.sockreturnsEACCES. - Production lint: no D-Bus, no BusyBox, no
iwdin the image. - QEMU CI: kernel config builds; daemons start; scan fails gracefully (no radio) without crash.
- Done when: all cases pass with evidence logged.
Verification and Evidence
| Evidence | How it is produced |
|---|---|
| Interface up | ip link shows the mt7921e interface |
| Successful association | wpa_supplicant log + NetworkStateChanged: connected on control.sock |
| DHCP lease | NetworkStatusReport.ip populated |
| Scan results | ScanResults JSON contains the test SSID |
| Trust boundary | playos-game connect to net sockets → EACCES |
| No D-Bus/BusyBox | Sprint 12 production lint passes |
| Reconnect after reboot | Profile reload → auto-connect log |
Acceptance Criteria
- The ROG Ally scans and lists nearby networks in the shell
- Connecting to a WPA2-PSK network obtains a DHCP lease and reaches the gateway
- Connecting to a WPA3-SAE network works (where hardware/AP supports SAE)
- Network status (SSID, IP, signal) is shown live in the shell
- Saved networks reconnect automatically after reboot
-
A game process cannot reach
/run/playos/net/sockets (EACCES) -
No D-Bus, BusyBox, or
iwdpresent in the production image -
All network operations flow through
control.sock(no direct wpa_supplicant access from the shell) - CI passes (kernel config builds; daemons start; scan fails gracefully in QEMU)
Handoff to Post-MVP
After this sprint, post-MVP features may assume:
- Wi-Fi is available as a system service with a stable
playos-runtimecontrol surface - Network profiles persist under
/data/config/network/ - SSH Developer Mode (Dropbear) can be layered on top of this connectivity
- Bluetooth can reuse the private-bus decision from
network-options.md§8 independently
Exit Gate
The ROG Ally connects to Wi-Fi and reaches the network end-to-end, driven entirely through the existing playos-runtime control IPC, with no D-Bus and no BusyBox in the production image.
Previous: Sprint 15
Sprint 17 — Touch Input + On-Screen Keyboard (OSK)
Goal: Wire touch input end-to-end (compositor → raylib backend) and ship a reusable on-screen keyboard (OSK) that any foreground client — the shell or a game — can invoke and receive committed text from.
Primary Outcome: A finger tap on the ROG Ally touchscreen reaches the focused surface as a raylib GetTouchPosition() point, and a system OSK can be raised from either a shell text field (e.g. Wi-Fi passphrase) or a game text field, delivering the typed string back to the invoking client via a standard text-input protocol.
Status: 🟡 Post-MVP — not started. Design follows the gamepad-input precedent (Sprint 8) and reuses the Sprint 7 overlay architecture.
Prerequisites: MVP complete (Sprint 15); Sprint 16 networking (the Wi-Fi passphrase field is the shell's first real text-input consumer); the rcore_playos.c gamepad translation landed (raylib CORE.Input.Gamepad.* fed from playos_input_get_controller_state).
Why This Sprint Exists
The MVP shell is navigated entirely by D-pad and face buttons; there is no text entry and no pointer/touch. Every Tier-1 text-entry feature — Wi-Fi passphrase (Sprint 16), search, save-file naming, user profiles — needs a keyboard, and the ROG Ally's 7″ touchscreen is currently inert (nothing forwards wl_touch). This sprint delivers both: touch becomes a real input source, and an OSK provides text entry.
Crucially, the OSK is not a shell-only widget. It is a system service that a game can invoke too, so any raylib game gains on-screen text input without shipping its own keyboard UI. This is the same model consoles use (Steam Deck's OSK is compositor-level, not per-game).
Start Condition Checklist
- MVP verified on hardware; Sprint 16 Wi-Fi screen exists (its passphrase entry is currently blank/stubbed).
- Sprint 8 gamepad wiring merged:
rcore_playos.ctranslatesplayos_input_get_controller_state()intoCORE.Input.Gamepad.*. - Sprint 7 overlay architecture live:
playos-overlayis a separate trusted raylib process that maps above any surface (playos_overlay_v1), owns "Virtual keyboard (future)" perplayos-overlay-spec.md. - Compositor uses
wlr_scene(shell/game/overlay trees) +wlr_seat "seat0", but forwards no pointer/touch today (system_button.cintercepts keyboardBTN_MODEonly). - wlroots 0.20 pinned (provides
wlr_text_input_v3andwlr_seat_touch_notify_*).
Decisions Locked for This Sprint
- Touch via the Wayland seat (
wl_touch), not evdev. Touch is absolute surface-relative input: it must be hit-tested against the focused surface and coordinate-transformed per output/scale. Reimplementing that inplayos-platform-apiwould duplicate the compositor. Contrast with the gamepad (Sprint 8), which stays evdev/platform-API because a gamepad is a surface-independent logical device whose reserved buttons must be stripped at the source. - Pointer via
wl_pointeralongside touch (the same seat plumbing); this also makes raylibGetMousePosition()/IsMouseButton*()work for USB mice and touch-as-mouse. - Text input via upstream
zwp_text_input_v3, implemented server-side with wlroots'wlr_text_input_v3. Do not invent a custom PlayOS text protocol — the standard one is stable, wlroots-native, and understood by other engines (non-raylib games can implement the same client). - The OSK UI lives in
playos-overlay(already owns "Virtual keyboard (future)"), rendered as a raylib component. It is one system keyboard, not a per-game widget. - OSK visibility is compositor-driven: when the focused client enables text input, the compositor signals the overlay to show the OSK; on disable/hide it unmaps. The game does not render or size the OSK.
- Committed text flows compositor → focused client via
zwp_text_input_v3::commit_string. Text never crossescontrol.sock; the OSK only produces Wayland protocol events. - Overlay ↔ compositor OSK coordination is a small additive extension to
playos_overlay_v1(see S17-T6). The overlay renders and hit-tests keys; the compositor is the only party that talks to the focused client. - Game-facing API is a raylib extension, not a new
libplayosABI:rcore_playos.cowns the game's Wayland connection, so it implements thezwp_text_input_v3client and exposesShowOnScreenKeyboard()/HideOnScreenKeyboard()plusGetCharPressed()(which "just works" for the committing client). Non-raylib engines implementzwp_text_input_v3directly. - No keyboard input forwarding change in this sprint. The OSK produces text through the text-input protocol; physical keyboard forwarding (
system_button.ccurrently withholds non-reserved keys) remains a separate, later concern. - Reserved buttons stay reserved. The OSK never synthesizes
SYSTEM/QUICK_MENU; text input is a data channel, not an evdev injection path.
Scope
In Scope
- Compositor pointer + touch seat forwarding (
wlr_cursor,wlr_seat_pointer_notify_*,wlr_seat_touch_notify_*,wlr_scene_node_athit-testing, per-output coordinate transform). - Compositor
zwp_text_input_v3manager + focus routing to the focused surface. - Compositor → overlay OSK show/hide signaling, and overlay → compositor key/string commit.
- Raylib backend (
rcore_playos.c):wl_touch+wl_pointer→CORE.Input.Touch.*/ mouse;zwp_text_input_v3client →charPressedQueue;ShowOnScreenKeyboard()/HideOnScreenKeyboard()extension. - Overlay OSK UI: qwerty layout, touch tap-to-type, shift/caps, backspace/enter/space, numeric/password variants.
- Shell text-field integration (Wi-Fi passphrase is the first consumer).
com.playos.sample-oskgame that invokes the OSK and echoes typed text.wayland-protocol.mdandplayos-overlay-spec.mdupdates.
Explicitly Out of Scope
- Physical keyboard forwarding to clients (
system_button.cchange) — separate sprint. - Mouse-only desktop pointer UX beyond what
wl_pointergives raylib for free. - Multi-touch gestures (pinch/zoom), multi-touch beyond
MAX_TOUCH_POINTStracking. - Haptics on keypress, predictive text, autocorrect, IME/composition, CJK input methods.
- Per-game OSK skins/theming (one system theme for now).
- On-screen keyboard over external displays (single built-in panel first).
Required Repository Changes
| Repo | Required work |
|---|---|
playos-compositor | wlr_cursor + pointer/touch notify + wlr_scene hit-testing; wlr_text_input_v3 manager + focus routing; OSK show/hide + commit plumbing |
playos-shell (vendored raylib) | rcore_playos.c touch/pointer → CORE.Input.Touch.*/mouse; zwp_text_input_v3 client + ShowOnScreenKeyboard() extension |
playos-overlay (playos-refdistro/src/playos-overlay/) | OSK UI component + layout engine + touch tap-to-type + commit requests |
playos-runtime | playos-v1.xml overlay protocol extension (OSK visibility/commit) + regenerated headers |
playos-samples | com.playos.sample-osk game |
playos-spec | This sprint; wayland-protocol.md (text-input + touch sections); playos-overlay-spec.md (OSK screen); post-mvp.md entry |
Expected Files and Directories
playos-compositor
src/input.c # NEW: wlr_cursor, pointer/touch listeners, scene hit-testing
src/text_input.c # NEW: wlr_text_input_v3 manager + focus routing + commit
src/osk.c # NEW: OSK show/hide state, overlay signal, commit bridge
src/system_button.c # unchanged (keyboard intercept stays as-is)
playos-shell (vendored raylib)
external/raylib/src/platforms/rcore_playos.c # wl_touch/wl_pointer listeners; zwp_text_input_v3 client
external/raylib/src/rcore.c # (no change) PollInputEvents already resets touch/pointer state
external/raylib/src/raylib.h # ShowOnScreenKeyboard / HideOnScreenKeyboard decls (PlayOS section)
playos-overlay (playos-refdistro/src/playos-overlay/)
src/osk.c # NEW: OSK screen + layout + tap-to-type
src/osk_layouts.c # NEW: qwerty/numeric/password layout tables
playos-runtime
protocols/playos-v1.xml # playos_overlay_v1 OSK requests/events (see S17-T6)
playos-samples
osk-demo/src/main.c # game: text field + ShowOnScreenKeyboard + GetCharPressed echo
osk-demo/manifest.json # com.playos.sample-osk
Agent Task Breakdown
Task Status Grid
| Task ID | Task | Primary repo | Status | Notes / evidence |
|---|---|---|---|---|
| S17-T1 | Pointer + touch seat forwarding in compositor | playos-compositor | not started | wlr_cursor + wlr_scene_node_at |
| S17-T2 | zwp_text_input_v3 manager + focus routing | playos-compositor | not started | wlr_text_input_v3 |
| S17-T3 | Raylib backend touch/pointer → CORE.Input.Touch.*/mouse | playos-shell | not started | wl_touch/wl_pointer listeners |
| S17-T4 | Raylib backend text-input client + ShowOnScreenKeyboard() | playos-shell | not started | zwp_text_input_v3 client |
| S17-T5 | Overlay OSK UI + layout + tap-to-type | playos-overlay | not started | raylib component |
| S17-T6 | Overlay ↔ compositor OSK commit protocol | playos-runtime | not started | extend playos_overlay_v1 |
| S17-T7 | Shell text-field integration (Wi-Fi passphrase) | playos-shell | not started | first consumer |
| S17-T8 | OSK sample game + end-to-end validation | playos-samples | not started | com.playos.sample-osk |
S17-T1 — Pointer + touch seat forwarding in compositor
The compositor creates seat0 but forwards nothing except the keyboard BTN_MODE intercept. Add:
- Create a
wlr_cursorbound to the output layout; create anwlr_xcursor_manager(or usewlr_cursor_set_image) for the pointer sprite. - Handle backend pointer events (
wlr_backend.events.new_pointer, axis/motion/button/frame) and touch events (new_touch, down/up/motion/cancel). - For each pointer/touch position, hit-test with
wlr_scene_node_at(scene, lx, ly, &sx, &sy)and, when the result is awlr_scene_surface, callwlr_seat_pointer_notify_enter()/wlr_seat_touch_notify_down()with the surface-local coordinates. Map to the surface viawlr_scene_surface_from_node(). - Apply the correct per-output transform and scale (the output may be rotated/scaled); use
wlr_output_layout_get_at()and the surface'scurrent.x/current.y. - Route touch/pointer only to the focused surface (respect the same z-order the overlay already uses: game < shell < overlay). When the OSK is visible, the overlay is top-most and receives the events.
Done when: tapping the ROG Ally screen with a debug build logs a wl_touch down/up with correct surface-local coordinates, and wlr_seat_touch_notify_down/up fire for the top-most surface at that point.
S17-T2 — zwp_text_input_v3 manager + focus routing
- Create
wlr_text_input_manager_v3viawlr_text_input_manager_v3_create(display). - On
wlr_text_input_v3enable/disable/commit, track which client (surface) has an active text-input. - When the focused surface has an active text-input, signal the overlay to show the OSK (via the S17-T6 event) and deliver
commit_string/preedit_stringfrom the overlay back to thatwlr_text_input_v3. - When text input is disabled, or the focused surface changes, signal the overlay to hide the OSK.
- Enforce the trust boundary: only the focused surface receives committed text; a background game never does.
Done when: a client calling zwp_text_input_v3::enable causes the compositor to emit the OSK-show signal, and commit_string reaches that client only while it is focused.
S17-T3 — Raylib backend touch/pointer → CORE.Input.Touch.* / mouse
In rcore_playos.c:
- Add
wl_touchandwl_pointerlisteners on the seat (in addition to the existing keyboard listener in the shell'ssrc/input.c— note the backend gets its own seat binding). - Pointer: motion →
CORE.Input.Mouse.currentPosition, button →CORE.Input.Mouse.currentButtonState, wheel →CORE.Input.Mouse.currentWheelMove. PopulatepreviousButtonStateon the nextPollInputEvents(). - Touch: down/up/motion →
CORE.Input.Touch.position[i],CORE.Input.Touch.pointId[i],CORE.Input.Touch.pointCount, mappingwl_touchtouch IDs toMAX_TOUCH_POINTSslots. SetcurrentTouchState/previousTouchStateconsistent with raylib's desktop backends.
Done when: GetTouchPosition(0)/GetTouchPointCount() return real values on the Ally touchscreen, and GetMousePosition() tracks a USB mouse.
S17-T4 — Raylib backend text-input client + ShowOnScreenKeyboard()
- Bind
zwp_text_input_manager_v3from the registry; create azwp_text_input_v3and attach it to the seat. - Add PlayOS raylib extensions:
RLAPI void ShowOnScreenKeyboard(void); // enable text input → compositor raises OSK
RLAPI void HideOnScreenKeyboard(void); // disable text input → compositor hides OSK
- On
zwp_text_input_v3::commit_string, push the UTF-8 string into raylib'sCORE.Input.Keyboard.charPressedQueue(and optionally map a syntheticEnterkeycode intokeyPressedQueuefor the "commit" key).GetCharPressed()then returns the typed characters exactly as if they came from a physical keyboard. - Keep the existing "keyboard input is intentionally unhandled" stance for physical keyboards; only the text-input (OSK) path feeds the char queue.
Done when: a raylib game calling ShowOnScreenKeyboard() gets typed characters back through GetCharPressed().
S17-T5 — Overlay OSK UI + layout + tap-to-type
- Implement the OSK as a raylib UI component in
playos-overlay. Usecore_keyboard_testbed.cas the visual starting point (key rectangles + labels), but extend it from "visualize" to "input": on touch, hit-testGetTouchPosition()against each keyRectangleand emit the corresponding key/char via the S17-T6 commit request. - Layouts: a compact qwerty (rows: numbers, qwerty, asdf, zxcv + modifiers), plus numeric and password variants selected by content-hint from the compositor (see S17-T6).
- Modifiers: shift (capitalizes + swaps symbol layer), backspace, enter (commit), space, dismiss (hide OSK). Highlight the pressed key; repeat on hold is optional.
- Render above the game at the bottom of the panel; respect
output_infodimensions/scale from the existing overlay protocol.
Done when: the OSK renders in the overlay, keys highlight on touch, and tapping emits the correct key/char commit request.
S17-T6 — Overlay ↔ compositor OSK commit protocol
Extend playos_overlay_v1 (in playos-v1.xml) with a minimal OSK channel. The overlay renders/hit-tests; the compositor mediates delivery:
<!-- Compositor → overlay -->
<event name="osk_visibility">
<arg name="visible" type="uint" summary="1 = show OSK, 0 = hide"/>
<arg name="hint" type="uint" summary="content hint: normal|number|password|url"/>
</event>
<!-- Overlay → compositor -->
<request name="osk_commit_string">
<arg name="text" type="string" summary="UTF-8 committed text"/>
</request>
<request name="osk_key">
<arg name="key" type="uint" summary="special key: backspace|enter|tab|escape|dismiss"/>
</request>
- The compositor forwards
osk_commit_stringto the focused client'swlr_text_input_v3(commit_string) andosk_keytokeysym/doneevents. - Version the interface (bump
playos_overlay_v1toversion="2"; keep v1 clients working — additive). - Regenerate headers with
wayland-scanner; document inwayland-protocol.md.
Done when: tapping "A" in the OSK produces a commit_string "A" event on the focused client's text-input.
S17-T7 — Shell text-field integration
- Add a shell text-field widget that, on focus, calls
ShowOnScreenKeyboard(), renders the committed string, and callsHideOnScreenKeyboard()on submit/dismiss. - Wire the Sprint 16 Wi-Fi passphrase field to it as the first real consumer (masked as password via the content hint).
- Confirm the shell uses the same OSK/text-input path as games (the shell is just another focused client).
Done when: selecting the Wi-Fi passphrase field raises the OSK, typed characters appear masked, and Enter commits the passphrase.
S17-T8 — OSK sample game + end-to-end validation
- Add
com.playos.sample-osk: a raylib game with a text field, a "show keyboard" button, and an echo area that rendersGetCharPressed()output and touch coordinates. - Validation matrix on the Ally:
- Touch reaches the focused game (
GetTouchPositionnon-zero, correct quadrant). - Game invokes OSK via
ShowOnScreenKeyboard(); keys type; text echoes in the game. - Shell invokes the same OSK for Wi-Fi passphrase; text is masked; Enter commits.
- Background game receives no text while shell/overlay is focused.
- Dismiss (B/system button) hides the OSK and returns focus to the game.
- Touch reaches the focused game (
- QEMU/CI: compositor + raylib backend compile with
wlr_text_input_v3and touch symbols; no touch device present → touch path is inert without crashing; text-input round-trip can be unit-tested with a mockwlr_text_input_v3client.
Done when: the sample echoes typed text on the Ally, and the shell Wi-Fi passphrase flow works end-to-end.
Verification and Evidence
| Evidence | How it is produced |
|---|---|
| Touch reaches surface | Debug log of wl_touch down/up with surface-local coords |
| OSK raises on enable | osk_visibility(1) emitted when focused client enables text input |
| Text committed to focused client | commit_string received by the focused wlr_text_input_v3 only |
| Game echo | com.playos.sample-osk echoes GetCharPressed() output |
| Shell passphrase | Wi-Fi passphrase field accepts masked text via OSK |
| Background isolation | Background game's commit_string count stays 0 |
| No reserved-key synthesis | OSK never emits SYSTEM/QUICK_MENU (static assert/audit) |
| CI build | wlroots + raylib compile with touch/text-input symbols; mock round-trip passes |
Acceptance Criteria
-
Touch taps on the ROG Ally reach the focused surface as raylib
GetTouchPosition()points -
A USB mouse updates raylib
GetMousePosition()/IsMouseButton*() -
The OSK can be invoked from a game (
ShowOnScreenKeyboard()) and from the shell (Wi-Fi passphrase) -
Tapping OSK keys delivers
commit_stringto the focused client only - Shift/caps, backspace, enter, space, and dismiss all behave correctly
- The shell Wi-Fi passphrase field is masked and submits via the OSK
- A background game receives no OSK text
-
The OSK never synthesizes reserved
SYSTEM/QUICK_MENUinput -
com.playos.sample-oskechoes typed text and touch coordinates on hardware - CI passes (touch/text-input symbols compile; mock text-input round-trip passes; headless touch path is inert)
Handoff to Post-MVP
After this sprint, post-MVP features may assume:
- Touch and pointer are first-class seat inputs; games read them via standard raylib APIs
- A system OSK exists that any focused client (shell or game) can invoke via
zwp_text_input_v3 - Text entry works for Wi-Fi passphrase, search, save naming, and user profiles
- Physical keyboard forwarding (a separate sprint) can build on the text-input focus routing added here
Exit Gate
A finger tap lands in the focused surface, and the same system on-screen keyboard serves both the shell and games — invoked by the client, rendered by the overlay, and delivering committed text to the focused client through the standard zwp_text_input_v3 protocol.
Previous: Sprint 16
Sprint 18 — C# Shell Reimplementation Assessment (Post-MVP Spike)
Goal: Produce a written plus/minus and feasibility assessment of re-implementing playos-shell in C#, and record it as a post-MVP investigation sprint. No C# shell is implemented in this sprint.
Primary Outcome: A decision-ready Sprint-18.md record that (a) analyses the runtime, packaging, interop, and rendering consequences of a C# shell, (b) states a clear feasibility verdict, and (c) scopes an optional bounded host-only de-risking spike — while explicitly leaving a product-direction C# rewrite unplanned.
Status: 🟡 Post-MVP — assessment only; not scheduled. No implementation work is approved.
Prerequisites: MVP stable (Sprint 15–16); the Raylib 6.0 shell landed (Sprint 5.5); rcore_playos.c is the single rendering backend (ADR-0006); the musl-only constraint is in force (ADR-0003).
Why This Sprint Exists
The shell is currently ~8k lines of C plus a vendored Raylib with a custom backend. A recurring question is whether a managed language (C# / .NET) would reduce the memory-safety and manual-parsing risk of that C code and speed UI iteration. This sprint does not commit to a rewrite; it answers the question with an assessment, so the roadmap can explicitly accept or reject the direction.
This is a decision-support sprint, not an implementation sprint. Its output is the assessment itself, and the default recommendation is to not pursue a C# shell as a product direction.
Assessment Inputs
The assessment rests on the following authoritative facts:
ADR-0003 — libc Choice (musl)— musl only, no glibc. This is the decisive runtime constraint.ADR-0006 — UI Framework (Raylib)— Raylib is the single UI framework for shell, overlay, and games.architecture.md§14 explicitly excludes "libc other than musl" from PlayOS v1.playos-shell-spec.md— shell responsibilities; Raylib is rendering-only, and controller input is read directly from evdev (src/input.c) so reserved SYSTEM/QUICK_MENU buttons survive.- The shell links
wayland-client,wayland-egl, EGL, GLESv2,libplayos(fromplayos-platform-api), vendored staticraylib, and optionallylibplayos-trusted(fromplayos-runtime). - Wayland protocol code is generated to C from
playos-v1.xml+ xdg-shell viawayland-scanner.
Assessment Constraints (Locked)
These constraints are not re-negotiated by this sprint; any C# rewrite would have to re-establish them:
- musl-only is non-negotiable (ADR-0003, architecture.md §14).
- Single rendering framework (ADR-0006). A rewrite that forks a second rendering path must justify it, not silently drop it.
- Shell invariants carried by the existing C code and required of any rewrite:
- always alive, supervised by
playos-init; - 60 fps target, controller-only navigation;
- no blocking I/O on the render thread;
- no direct IPC socket access except the trusted evdev input path;
- rendering stops while a game is foreground, but the process stays alive.
- always alive, supervised by
Feasibility Assessment
Verdict
Technically feasible as a host-side proof-of-concept; not advisable as a shippable on-device product direction. The decisive factors are: (1) .NET-on-musl / NativeAOT risk, (2) Buildroot toolchain effort, and (3) loss of the single Raylib backend story.
Plus / Minus
| Dimension | Plus | Minus |
|---|---|---|
| Memory safety | Eliminates buffer overflows and manual string-parsing bugs in the current C shell (e.g. hand-rolled JSON in main.c) | GC/allocator behaviour and working-set size on an always-on 60 fps embedded process must be re-validated |
| UI iteration | Richer abstractions (records, LINQ, test framework) can speed state/UI iteration | Raylib's UI layer is intentionally thin; C# does not remove the need to bind Raylib or re-derive rendering |
| Native interop | P/Invoke + source generators can wrap a C ABI | The entire surface — libplayos, wayland-client/wayland-egl, EGL, GLESv2, trusted IPC, evdev — must be wrapped or generated; marshaling on musl is untested |
| Runtime & packaging | Self-contained .NET removes a host dependency | Supported Linux RIDs assume glibc; linux-musl-x64 exists but NativeAOT still depends on the OS libc/ICU and needs validation; Buildroot has no first-class .NET SDK package |
| Rendering | — | Raylib's native library is C with a custom rcore_playos.c backend; C# bindings (Raylib-cs) target upstream Raylib, so the PlayOS backend is lost or must be kept in C and P/Invoked — weakening the ADR-0006 single-backend rationale |
| Wayland protocols | Community C# Wayland bindings exist | playos-v1.xml + xdg-shell are generated to C via wayland-scanner; a C# binding generator would need the private protocols ported |
| Total cost/benefit | Modest long-term maintainability upside | Replaces ~8k lines of working, shipped C with a high-risk multi-week-to-month effort for a persistent controller UI that is not the product's growth area |
Runtime (musl / NativeAOT) — hard blocker to validate
- .NET's supported Linux runtime identifiers assume glibc (
linux-x64). Alpine'slinux-musl-x64RID exists, but self-contained deployment still relies on the OS libc, and ICU/globalization behaviour historically differs on musl. - NativeAOT reduces startup and footprint but does not remove the libc/ICU dependency; threading, P/Invoke marshaling, and finalizer behaviour on musl are exactly the areas with the least field coverage.
- This is the single highest-risk item and must be proven by a spike, not assumed. Until a self-contained NativeAOT binary runs on the actual musl rootfs with the same EGL/Wayland bindings, a C# shell is a research bet, not a plan.
All runtime claims above are architectural and to be validated by a spike; they are not asserted as tested.
Buildroot packaging — hard blocker to estimate
- Buildroot has no first-class .NET SDK package. Shipping means either a host .NET SDK toolchain with cross-compilation, or NativeAOT produced on a host and injected into the image.
- Either path is new
br2-externalmachinery with no precedent in this repository — substantial, unproven infrastructure work.
Native interop surface — medium
A C# shell would need bindings for, at minimum:
libplayos(the public C ABI): lifecycle, storage paths, device info, logical input, audio/display/power queries, structured logging.libwayland-client,libwayland-egl, EGL, GLESv2.- Trusted IPC: the
playos-runtimerestricted control client (control.sock) and optionallibplayos-trusted. - Direct evdev input, to preserve reserved SYSTEM/QUICK_MENU button survival exactly as
src/input.cdoes today.
Raylib backend loss — strongest architectural minus
- Raylib is C. The PlayOS value is the custom
rcore_playos.cbackend shared by shell, overlay, and games. - A C# shell either P/Invokes a C Raylib build (keeping the backend in C, so C# gains little in the rendering hot path) or re-derives Wayland/EGL/GLES3 surface management in managed code — a second rendering path that directly contradicts ADR-0006's unified-framework rationale.
- Neither option reduces the amount of C the project must own; both add a managed/native boundary across every frame and every input event.
What is cheap and worth doing
- A host-only C# proof-of-concept of the state and screen layer only: screen enum + navigation stack,
manifest.jsonparsing, power/thermal model types, toast/screenshot state. This exercises the "C# is nicer for UI state" hypothesis without touching Buildroot, musl, Wayland, or Raylib.
Recommended De-Risking Spike (Bounded)
If ever funded, a single bounded spike would, in order:
- Host-only state/screen POC — a .NET console/unit-test harness against a re-typed
manifest.jsonand state model. - musl proof — compile a minimal self-contained NativeAOT "hello" binary and run it on the existing musl rootfs (no Wayland/EGL), recording libc/ICU/startup/size results.
- Binding probe — generate a C# binding for
playos-v1.xml+ xdg-shell and complete onewl_surfaceround-trip on a host Wayland compositor. - Explicit stop — do not proceed to a device C# shell, Buildroot packaging, or a Raylib-backend replacement.
Scope
In Scope (this sprint)
- This
Sprint-18.mddocument. - The feasibility analysis and verdict above.
- Definition of the bounded de-risking spike (S18-T1…T4) — not its execution.
Explicitly Out of Scope / Not Planned
- Any C# shell implementation.
- Buildroot / .NET toolchain work.
- Raylib backend port or replacement.
- Changing the existing C shell.
Required Repository Changes
| Repo | Required work |
|---|---|
playos-spec | Add Sprint-18.md; link from SUMMARY.md and post-mvp.md |
| (none else) | No implementation repositories change in this sprint |
Expected Files and Directories
playos-spec/src/sprints/Sprint-18.md # NEW: this assessment
playos-spec/src/SUMMARY.md # UPDATE: link
playos-spec/src/post-mvp.md # UPDATE: entry
Agent Task Breakdown
Task Status Grid
| Task ID | Task | Primary repo | Status | Notes / evidence |
|---|---|---|---|---|
| S18-T1 | Runtime + Buildroot feasibility (musl / NativeAOT) | playos-spec | not started | ADR-0003, architecture §14 |
| S18-T2 | Native interop + Wayland protocol bindings | playos-spec | not started | libplayos, wayland-client, playos-v1.xml |
| S18-T3 | Raylib backend loss + rendering-path options | playos-spec | not started | ADR-0006, rcore_playos.c |
| S18-T4 | Bounded host-only de-risking spike definition | playos-spec | not started | state/screen-layer POC only |
S18-T1 — Runtime + Buildroot feasibility
- Confirm the .NET supported Linux RID situation against ADR-0003 (musl only):
linux-x64(glibc) vslinux-musl-x64, and NativeAOT's remaining libc/ICU dependency. - Assess Buildroot packaging options: host .NET SDK cross-compilation vs host-produced NativeAOT injected into the image, and the new
br2-externalmachinery each requires. - Record startup time, binary size, and working-set expectations as to be validated, not proven.
Done when: the sprint records a clearly-labelled runtime/packaging risk verdict and identifies the exact spike step (S18-T4 step 2) that would validate it.
S18-T2 — Native interop + Wayland protocol bindings
- Enumerate the concrete surface a C# shell must bind:
libplayosC ABI,libwayland-client/libwayland-egl, EGL, GLESv2, trusted IPC (control.sock), and direct evdev. - Assess how
playos-v1.xml+ xdg-shell (currently generated to C bywayland-scanner) would be generated for C#, and whether community C# Wayland bindings can absorb the private protocols. - Identify marshaling/threading risks specific to musl.
Done when: the sprint records the full binding surface and names the binding-probe step (S18-T4 step 3) that would de-risk it.
S18-T3 — Raylib backend loss + rendering-path options
- Evaluate the two options: P/Invoke a C Raylib build that keeps
rcore_playos.c, versus re-deriving Wayland/EGL/GLES3 in managed code. - State why the second option violates ADR-0006's single-framework rationale, and why the first leaves the rendering hot path in C.
- Conclude with the recommendation that this is the strongest architectural minus.
Done when: the sprint names the Raylib-backend loss as the decisive architectural minus and ties it to ADR-0006.
S18-T4 — Bounded host-only de-risking spike definition
- Define the four-step spike: host state/screen POC → musl "hello" proof → binding probe → explicit stop.
- Scope it strictly to host-only, with no Buildroot work and no Raylib backend replacement.
- State the acceptance that marks the spike complete and the condition under which it would not advance to a product C# shell.
Done when: the sprint records a bounded, stoppable spike definition and a default recommendation not to pursue a C# rewrite.
Verification and Evidence
| Evidence | How it is produced |
|---|---|
| Assessment recorded | Sprint-18.md present with plus/minus table and verdict |
| Roadmap indexed | SUMMARY.md and post-mvp.md link the sprint |
| Link integrity | mdbook build passes |
| No implementation drift | No C# files or Buildroot changes are produced by this sprint |
Acceptance Criteria
- The assessment states a clear feasibility verdict with a plus/minus table
- The musl/NativeAOT and Buildroot risks are labelled "to be validated", not asserted as proven
- The Raylib backend loss is identified as the strongest architectural minus and tied to ADR-0006
- A bounded host-only de-risking spike is defined with an explicit stop
- A C# rewrite is explicitly left unplanned as a product direction
-
SUMMARY.mdandpost-mvp.mdare updated -
mdbook buildpasses
Handoff to Post-MVP
After this sprint:
- The "C# shell?" question has a written answer and a default recommendation (do not pursue as a product direction).
- A future spike, if funded, can pick up S18-T4's bounded scope without re-deriving the assessment.
Exit Gate
The assessment is written, indexed, and link-verified; it clearly concludes that a C# shell reimplementation is technically feasible as a host POC but not advisable as a product direction, and it scopes an optional bounded spike while leaving the actual rewrite unplanned.
Previous: Sprint 17
Sprint 19 — Marketplace Assessment (Post-MVP)
Goal: Produce an initial evaluation, analysis, and assessment of the playos-marketplace repository — what exists today, what the specs and sprints already say about a store, and how the marketplace will complement PlayOS — without writing any marketplace implementation.
Primary Outcome: A decision-ready Sprint-19.md record that (a) maps the empty playos-marketplace stub against the existing spec/sprint backlog, (b) identifies the spec gaps and naming discrepancies that block implementation, and (c) proposes a spec-first sequencing so the marketplace can later be built consistently with PlayOS.
Status: 🟡 Post-MVP — assessment only; not scheduled. No marketplace code is implemented in this sprint.
Prerequisites: MVP stable (Sprint 15–16); Wi-Fi (playos-net, Sprint 16) is the network prerequisite for any on-device store downloads; the SDK story (playos-sdk, post-MVP) is the publisher prerequisite.
Why This Sprint Exists
playos-marketplace exists as a repository but contains no implementation — only a README.md, an AGENTS.md, a .github/copilot-instructions.md, and an issue template. Its own guidance says marketplace behaviour "is specified in playos-spec (Part XI)", and its issue template references "Part X — Package Format" and "Part XI — Cloud and Marketplace". Those spec parts do not exist. The word "marketplace" appears nowhere in playos-spec.
Meanwhile playos-spec does contain scattered store/package intent — "Store Integration and Download Manager", "Signed .play Content Packages", and a historical .play package note — but no marketplace chapter, no package-format chapter, no catalog model, and no entitlement model. The package extension also disagrees: the marketplace stub says .gpk, the spec says .play.
This sprint turns that disorganised state into a single assessment: what the marketplace should be, how it complements PlayOS, and what must be specified first.
Assessment Inputs
playos-marketplaceis a stub. Files present:README.md,AGENTS.md,.github/copilot-instructions.md,.github/ISSUE_TEMPLATE/implementation-task.md. No source, services, or package-format definitions.- Marketplace golden rules (from its
AGENTS.md):- SDK-first — a developer can publish without owning a PlayOS device.
- Multiple store sources — official, community, OEM, private, and LAN; never hard-code a single store.
- Self-hostable — a store can be run by communities, OEMs, or individuals.
- Spec-first — package format (
.gpk), signing, entitlements, and catalog behaviour are specified before implementation. - Trust — verify package signatures; respect permissions and the trust model.
- Spec references to a store/package (no "marketplace" by name):
post-mvp.mdTier 3 — Store Integration and Download Manager (API additionplayos_store.h; depends on Wi-Fi, signed.playpackages, cloud saves) and Signed.playContent Packages (signed archive:manifest.json, binary, assets, content hash tree; replaces plain directory installs).roadmap.mdpost-MVP list — "Download manager and store integration" and "Signed.playcontent packages".ideas.md§14.2 — historical "Later.playpackage" requirements: deterministic metadata, signature verification, integrity hashes, atomic installation, versioned migrations, strict save-data separation.Sprint-12— "Store-level signing and distribution" explicitly out of scope for security hardening.Sprint-15— "Store integration and SDK signing/distribution" deferred to post-MVP.Sprint-16— store downloads listed as a Tier-1 post-MVP consumer of Wi-Fi.architecture.md§14 — "Wi-Fi, Bluetooth, SSH, cloud saves | Post-MVP" (store implied via cloud saves).
- Existing manifest schema:
schemas/game-manifest-v1.json— the current per-gamemanifest.jsoncontract that any package format must extend or reference. - Naming discrepancy:
playos-marketplaceuses.gpk;post-mvp.md,ideas.md, androadmap.mduse.play.
Assessment
What the marketplace is
Per its own docs, the PlayOS Marketplace is the open platform for publishing, discovering, installing, and updating PlayOS applications, games, themes, and developer content. Its design constraints are already well stated and are worth keeping:
- SDK-first publishing (no device required to publish).
- Multiple store sources (official, community, OEM, private, LAN) — a client is store-agnostic.
- Self-hostable stores.
- Spec-first package format, signing, entitlements, and catalog.
- Trust via signature verification and permission respect.
How it complements PlayOS
Today the MVP is a local launcher: games are installed by dropping directories into /data/games/<game-id>/ with a manifest.json, and the only "distribution" mechanism is the offline .playosb system-update bundle. There is no way to discover, download, verify, install, or update content on-device, and no way for a third party to publish without hand-delivering files.
The marketplace closes that loop and turns PlayOS from a launcher into a platform:
publisher ──SDK──▶ build .gpk ──▶ publish ──▶ catalog (official/community/OEM/private/LAN)
│
player ──Wi-Fi──▶ discover ──▶ download ──▶ verify ──▶ install ──▶ run/update ──▶ revoke
Specifically it:
- Completes the delivery path that
post-mvp.mdalready names ("Store Integration and Download Manager"). Wi-Fi (Sprint 16) is the transport; the marketplace is the source and policy layer. - Enables third-party content without manual file transfer — the natural partner of the
playos-sdk(publishers) and the shell's game library (players). - Gives content a secure, atomic lifecycle — signature verification + content hash tree + atomic install + rollback, reusing the patterns already proven by Sprint 11's A/B update engine and Sprint 12's manifest signing.
- Adds entitlements and revocation — ownership/licensing that the MVP's plain-directory model cannot express.
- Enables themes and developer content, not just games, matching the marketplace's broader mandate and future shell theming.
Mapping to existing architecture
| Marketplace concern | Existing PlayOS foundation to build on |
|---|---|
| On-device download | Wi-Fi (playos-net, Sprint 16) |
| Package verification | Signed manifests (Sprint 12), HMAC/Hash verification (Sprint 11) |
| Atomic install / rollback | A/B update engine pattern (Sprint 11), /data/downloads staging |
| Storage | /data/games/<id>/, /data/downloads/, /data/updates/ (Sprint 6) |
| Client surface | playos_store.h (named in post-mvp.md), shell "Store" screen, playos-tools CLI |
| Publishing | playos-sdk (post-MVP) — SDK-first publishing with no device |
| Trust | security-model.md trust zones; manifest.json + signatures |
Spec gaps and open decisions (the real blockers)
- No "Part X — Package Format" and no "Part XI — Cloud and Marketplace" exist. The marketplace's own rule is spec-first, so this is the bottleneck: the spec must be authored before marketplace code.
- Package extension naming is inconsistent. Marketplace says
.gpk; spec says.play. Recommendation: adopt.gpkas the canonical content-package extension (it covers games, themes, and developer content, and is already the marketplace repo's term), then reconcile the.playreferences inpost-mvp.md,ideas.md, androadmap.md. This is a decision to lock during spec authoring, not silently here. - No catalog model. Nothing specifies a signed catalog format, discovery protocol, or how multiple store sources (official/community/OEM/private/LAN) are configured and selected by a client.
- No entitlement model. Ownership, free-vs-paid, device limits, offline entitlements, and revocation are unspecified. Recommendation: v1 should be free content with signed, device-local entitlements — no payments, no billing, no DRM. Commerce is explicitly out of scope for the core platform.
- Client integration surface is only a name.
playos_store.his referenced but not specified; the shell has no "Store" screen in any sprint;playos-toolscurrently covers system updates only.
Verdict
The marketplace is a natural and necessary post-MVP complement — it is the content-economy layer that the MVP deliberately deferred, and its repository's golden rules are a sound, coherent philosophy. It is not a code problem yet: it is a spec problem. The highest-value next step is to author the missing spec parts (package format + marketplace/cloud), resolve the .gpk/.play naming, and define a minimal catalog + entitlement + client surface — then implement the marketplace repo.
Recommended Spec-First Sequencing
- Part X — Package Format (
.gpk). Define the signed package:manifest.json+ binary + assets + content hash tree + signature, atomic install, versioned migrations, save-data separation. This absorbs and supersedes the current "Signed.playContent Packages" post-MVP item. - Part XI — Cloud and Marketplace. Define catalog format + discovery, store-source selection (official/community/OEM/private/LAN), entitlements, and revocation — free-content-only v1, no payments.
- Client surface. Specify
playos_store.h(query, download, install progress, entitlement check), a shell "Store" screen, and aplayos-tools/SDKpublishcommand. - Then implement
playos-marketplace(catalog service, publishing flow, client integration) against those specs.
Scope
In Scope (this sprint)
- This
Sprint-19.mddocument. - The assessment, gap analysis, and recommended sequencing above.
- Cross-linking from
SUMMARY.mdandpost-mvp.md.
Explicitly Out of Scope / Not Planned Now
- Any marketplace service, client, or publishing implementation.
- The actual "Part X / Part XI" spec chapters (future work).
- Payments, billing, DRM, or storefront UI polish.
- Changing the
.playreferences inpost-mvp.md/ideas.md/roadmap.mdnow — the naming reconciliation is a decision for Part X authoring.
Required Repository Changes
| Repo | Required work |
|---|---|
playos-spec | Add Sprint-19.md; link from SUMMARY.md and post-mvp.md |
| (none else) | No implementation repositories change in this sprint |
Expected Files and Directories
playos-spec/src/sprints/Sprint-19.md # NEW: this assessment
playos-spec/src/SUMMARY.md # UPDATE: link
playos-spec/src/post-mvp.md # UPDATE: entry
Agent Task Breakdown
Task Status Grid
| Task ID | Task | Primary repo | Status | Notes / evidence |
|---|---|---|---|---|
| S19-T1 | Survey store/package references and reconcile with marketplace AGENTS.md | playos-spec | not started | post-mvp.md, roadmap.md, ideas.md, Sprints 12/15/16 |
| S19-T2 | Assess package-format gap and .gpk vs .play naming | playos-spec | not started | schemas/game-manifest-v1.json, ideas.md §14.2 |
| S19-T3 | Assess catalog + entitlement + trust + multi-store model | playos-spec | not started | security-model.md, marketplace golden rules |
| S19-T4 | Define spec-first sequencing + client integration surface | playos-spec | not started | playos_store.h, shell Store screen, SDK publish |
S19-T1 — Survey store/package references
- Enumerate every store/package mention in
playos-spec:post-mvp.mdTier 3,roadmap.mdpost-MVP list,ideas.md§14.2 and §22, and the "out of scope" notes in Sprint 12, Sprint 15, and Sprint 16. - Compare them against
playos-marketplace/AGENTS.mdgolden rules and confirm that the word "marketplace" is absent from the spec and that "Part X / Part XI" are dangling references. - Record the reconciliation outcome: marketplace is spec-blocked, not code-blocked.
Done when: the sprint lists the complete set of store/package references and states the dangling "Part X / Part XI" gap.
S19-T2 — Package-format gap and naming
- Confirm the current game content contract is
manifest.json(seeschemas/game-manifest-v1.json) with plain-directory install, and that the future signed package is only sketched inpost-mvp.md("Signed.playContent Packages") andideas.md§14.2. - Document the
.gpk(marketplace) vs.play(spec) discrepancy and recommend.gpkas canonical, with.playreferences to be reconciled during Part X authoring. - List the package properties that must be specified: manifest, binary, assets, content hash tree, signature, atomic install, versioned migrations, save-data separation.
Done when: the sprint names the package-format gap, the naming discrepancy, and a concrete recommendation.
S19-T3 — Catalog, entitlement, and trust model
- Assess what is missing: signed catalog format, discovery protocol, store-source selection (official/community/OEM/private/LAN), and entitlements/revocation.
- Recommend a minimal v1: free content, signed catalogs, device-local entitlements, no payments/billing/DRM.
- Map trust to
security-model.md: package signature verification before install, content hash tree, and entitlement checks that respect the untrusted-game boundary.
Done when: the sprint records a minimal catalog + entitlement + trust model and explicitly excludes payments/DRM.
S19-T4 — Spec-first sequencing and client surface
- Define the four-step sequencing: Part X package format → Part XI marketplace/cloud → client surface → implementation.
- Specify the client surface to be defined later:
playos_store.h(query, download, install progress, entitlement), a shell "Store" screen, and aplayos-tools/SDKpublishcommand. - State that no marketplace code is written until those specs exist (honouring the repo's own spec-first rule).
Done when: the sprint records a spec-first sequence and the concrete client surface to be specified next.
Verification and Evidence
| Evidence | How it is produced |
|---|---|
| Assessment recorded | Sprint-19.md present with gap analysis, verdict, and sequencing |
| Roadmap indexed | SUMMARY.md and post-mvp.md link the sprint |
| Naming discrepancy documented | .gpk vs .play explicitly recorded with a recommendation |
| Link integrity | mdbook build passes |
| No implementation drift | No marketplace code or spec chapters are produced by this sprint |
Acceptance Criteria
-
The assessment states what
playos-marketplaceis, what exists today, and that it is an empty stub -
All existing store/package references in
playos-specare enumerated - The dangling "Part X / Part XI" spec references are identified as the primary blocker
-
The
.gpkvs.playnaming discrepancy is documented with a recommendation - A minimal catalog + entitlement + trust model is proposed (free-content-only v1, no payments/DRM)
- A spec-first sequencing is defined (package format → marketplace/cloud → client surface → implementation)
- A marketplace implementation is explicitly left unplanned until those specs exist
-
SUMMARY.mdandpost-mvp.mdare updated -
mdbook buildpasses
Handoff to Post-MVP
After this sprint:
- The marketplace has a written assessment and a clear "spec-first" prerequisite list.
- The next authoring step (Part X package format, then Part XI marketplace/cloud) can begin without re-deriving the gap analysis.
- The
.gpknaming decision is flagged for Part X authoring to lock.
Exit Gate
The assessment is written, indexed, and link-verified; it clearly concludes that playos-marketplace is a necessary post-MVP complement that is currently spec-blocked (missing Part X/Part XI, unresolved .gpk vs .play, undefined catalog/entitlements), and it defines a spec-first sequencing so implementation can proceed only after the contracts exist.
Previous: Sprint 18
Sprint 20 — Native Media & Browser Client Strategy (Post-MVP)
Goal: Produce a written plus/minus and feasibility assessment for a controller-first native media and browser client strategy — Spotify, YouTube, YouTube Music, and a lightweight browser — launched from playos-shell as first-class PlayOS apps. Netflix is explicitly out of scope. This sprint records the assessment and scopes a bounded de-risking spike; no media client is integrated.
Primary Outcome: A decision-ready Sprint-20.md record that (a) replaces the earlier Chromium/CEF framing with a native-client recommendation, (b) analyses the launch, compositor, audio, GPU, toolchain, and security consequences, (c) states a clear feasibility verdict, and (d) scopes an optional bounded host-only de-risking spike.
Status: 🟡 Post-MVP — assessment only; not scheduled. No implementation work is approved.
Prerequisites: MVP stable (Sprint 15–16); wlroots compositor with Wayland session (Sprint 4/5); ALSA audio (Sprint 8); Wi-Fi (playos-net, Sprint 16); musl-only constraint (ADR-0003); no D-Bus/BusyBox policy (Sprint 16, network-options.md); security model on paper (Sprint 12).
Why This Sprint Exists
PlayOS is a games-first handheld console, but a recurring product question is whether it should also run the streaming services users expect on a portable device — Spotify, YouTube, and a general browser. Netflix is the natural fourth ask, but it has no first-party Linux client and its DRM path is fundamentally incompatible with PlayOS's musl-only, ALSA-only, no-Widevine constraints. This sprint therefore splits the problem:
- In scope: Spotify, YouTube, YouTube Music, and a lightweight browser via native/thin clients that respect the existing musl, ALSA, and Wayland architecture.
- Out of scope: Netflix, Chromium/CEF/Electron, X11/Xwayland, and any Widevine/EME DRM.
The recommendation below is a native-client-first strategy, not a web-runtime strategy. It deliberately keeps PlayOS controller-first and avoids reopening the locked musl/ALSA decisions.
Assessment Inputs
The assessment rests on the following authoritative facts, verified against source and spec:
- Launch model is ELF-only. The shell requests launch over
control.sockIPC toplayos-init;playos-initis the only fork/exec (playos-init/src/supervisor.c). The child path is hard-coded toexecutablewith anaccess(..., X_OK)gate andexecl(exe_path, exe_path, NULL)(supervisor.c:731,806). The shell manifest validator additionally requiresexecutable+architectureand checksaccess(F_OK)(playos-shell/src/screen_library.c:153-265). A media/web launch target needs a new manifesttype/url/media_uri, an init exec branch, and shell validation relaxed for that type. The manifest schemagame-manifest-v1.jsonalready hasadditionalProperties: true, so extending it is non-breaking. - Compositor exposes standard Wayland globals but does not forward input. wlroots 0.20 creates
wlr_compositor,wlr_xdg_shell, and awlr_seat(playos-compositor/src/compositor.c:286-298), so arbitrary Wayland clients can attach and createxdg_toplevels. However there is nowlr_seat_set_keyboard/pointer/touchforwarding —src/system_button.c:63states non-reserved keys are "intentionally not forwarded". Surfaces are forced fullscreen and there is one fixed game role. A native client therefore must be driven over its own IPC (e.g.mpv --input-ipc-server), not through the Wayland seat. - Audio is ALSA-only. ADR-0007 mandates direct ALSA PCM with no PulseAudio or PipeWire in v0.1.0.
playos-platform-apiaudio is mixer-only. Native clients that speak ALSA directly (mpv, librespot/spotifyd backends) fit; clients that require PulseAudio/PipeWire do not. - GPU is GBM/EGL/GLES2/Mesa only. AMDGPU + Mesa
radeonsiEGL/ES/GBM is the whole graphics story. There is no VAAPI/VDPAU/V4L2/dmabuf-import/hardware-video-decode path in the compositor or defconfigs; video playback would be software-decoded. This is acceptable for 1080p and below, but 4K/HDR is effectively off the table. - Toolchain is x86_64 + musl. ADR-0003 and
playos_ally_defconfigsetBR2_x86_64,BR2_TOOLCHAIN_BUILDROOT_MUSL, no glibc.mpv,librespot,spotifyd,WPE WebKit, andCogare musl-buildable in principle, but their Buildroot packaging and any bundled prebuilts must be validated — none currently exists. - Sandbox is not implemented.
playos-init/src/supervisor.cspawns with onlysetsid()+ env +execl(); no seccomp/Landlock/PR_SET_NO_NEW_PRIVS/user namespaces yet.security-model.mdis the design, not the code. Media clients are network-facing parsers (HLS/DASH/YT playlists, HTML/JS in WPE), so the Sprint 12 sandbox should land before they run unconfined. - Spec has zero existing media/browser content. Grep across
playosforlibrespot|spotifyd|mpv|yt-dlp|ytdl|ytmusic|youtube|WPE|Cog|webkit|InnerTube|innertubefinds no existing media-client integration (only this sprint and unrelated raylib noise). Buildroot grep forBR2_PACKAGE_(MPV|FFMPEG|PYTHON3|RUST|LIBRESPOT|SPOTIFYD|WPE|WEBKIT|COG)finds no non-sensitive matches — none of these packages are present. The never-planned list explicitly includes "Browser-based shell or WebAssembly runtime", "X11 / Xwayland", and "Cloud gaming".
Feasibility Assessment
Verdict
Viable as a post-MVP, controller-first native-client strategy — with per-service caveats. Spotify, YouTube, and YouTube Music can be delivered as native/thin clients that speak ALSA and attach as Wayland surfaces, without reopening ADR-0003 or ADR-0007. The browser (WPE WebKit + Cog) is the riskiest item because of its musl-build and WebKit security surface; it should be treated as an optional spike, not a product commitment. Netflix stays out of scope.
This is a materially better fit than the earlier Chromium/CEF path because it does not require glibc, PulseAudio/PipeWire, or Widevine. The remaining hard work is integration plumbing and packaging, not architectural reversal.
Recommended stack
| Service | Recommended client | Audio | DRM | Notes |
|---|---|---|---|---|
| Spotify | librespot (preferred) / spotifyd | ALSA backend | None (own streaming, no EME) | librespot is a Rust daemon; drive it over its IPC/CLI, not the Wayland seat |
| YouTube | mpv + yt-dlp | ALSA (--ao=alsa) | None for standard content | yt-dlp resolves streams; mpv plays them. Prefer a standalone/static binary on-device (see YouTube Music) |
| YouTube Music | mpv + yt-dlp/InnerTube search | ALSA | None | Music is the same pipeline as YouTube; search/library UX decides the wrapper shape |
| Browser | WPE WebKit + Cog | ALSA (WebKit audio) | No Widevine | Lightweight embedded WebKit; no Netflix/DRM. Highest musl/security risk |
| Netflix | Out of scope | — | Widevine (glibc, proprietary, L3 ~720p) | No native Linux client; needs a Chromium/CEF/Widevine path PlayOS rejects |
YouTube Music — PlayOS app shape
YouTube Music should be a PlayOS app, not a separate browser tab. Two options:
- Option A (recommended): a thin Raylib controller-first wrapper that drives
mpvvia--input-ipc-serverJSON and performs search viayt-dlp/InnerTube. The wrapper renders a PlayOS-styled list/now-playing UI in the existing Raylib backend and maps controller input to IPC commands. This reuses the shell's own UI stack and is the smallest integration. - Option B: a dedicated headless backend (
innertube-rsorytmusicapi) exposing library/playlist/queue semantics, withmpvas the renderer. Only pursue this if library/playlist/offline-cache UX demands more than Option A.
Recommendation: start with Option A; escalate to Option B only when the library/queue UX is proven to need it.
Plus / Minus
| Dimension | Plus | Minus |
|---|---|---|
| Platform reach | Spotify/YouTube/YouTube Music + a browser turn the handheld into a genuine entertainment device | Media is not the MVP's growth area; it partially competes with the spec's games-first positioning |
| Architecture fit | Native clients speak ALSA directly and attach as Wayland surfaces — no ADR-0003/ADR-0007 reversal | The compositor's single-game role and missing wl_seat input must be worked around (per-client IPC), and forced fullscreen means no window/tab management |
| Toolchain | mpv/librespot/spotifyd/WPE are all musl-buildable in principle | None are in Buildroot today; WPE WebKit on musl is a flagged risk and must be validated in the spike |
| Audio | Direct ALSA is a first-class fit | Any client that assumes PulseAudio/PipeWire must be configured or patched to ALSA |
| Security | Per-client IPC keeps the attack surface bounded and avoids trusting the Wayland seat | Network-facing parsers (HLS/DASH/HTML/JS) should not run unconfined — the Sprint 12 sandbox is a prerequisite |
| Performance | 1080p software decode is realistic on the Ally's CPU; 16 GB RAM is ample | 4K/HDR is effectively out of scope without VAAPI/VDPAU/dmabuf import; browser memory can be large |
| DRM | Spotify/YouTube standard content need no Widevine/EME | YouTube/Spotify premium and all of Netflix are DRM-gated and stay out of scope |
| Cost/benefit | The launch-target change (manifest type + init exec branch + shell validation) is small and non-breaking | Browser (WPE) and YouTube Music search UX are the real multi-sprint items; packaging five new Buildroot packages is non-trivial |
Hard blockers
- Compositor input model. There is no
wl_seatkeyboard/pointer/touch forwarding. Native clients are therefore not interactive over the Wayland seat — they must be driven over their own IPC (mpv --input-ipc-server,librespotcontrol socket, etc.) with PlayOS-side controller binding. This is a design constraint, not a blocker, but it must be respected in every client. - No sandbox.
supervisor.chas no seccomp/Landlock/PR_SET_NO_NEW_PRIVS. Network-facing media/browser clients should not ship unconfined; the Sprint 12 sandbox must land first. - No Buildroot packages.
mpv,ffmpeg,librespot/spotifyd,WPE WebKit, andCog(plus their Rust/Python build deps) are absent from every defconfig. Packaging and cross-compiling them against musl is the bulk of the actual work. - No hardware video decode. No VAAPI/VDPAU/dmabuf import means software decode only; 4K/HDR is effectively off the table. This bounds the feature to 1080p-and-below streaming.
- Netflix/DRM is a hard non-starter. Widevine is proprietary, glibc-only, Google-licensed, and L3 on generic Linux x86_64 (~720p ceiling). It contradicts ADR-0003 and requires a distribution agreement. Netflix is therefore out of scope, not "deferred".
Integration design (how it fits the existing lifecycle)
The existing game lifecycle is reused almost unchanged:
- Manifest: extend
game-manifest-v1.jsonwith atypefield (native|media|web) plusurl/media_uri. The schema already hasadditionalProperties: true, so this is non-breaking. - Shell: relax the validator at
playos-shell/src/screen_library.c:153-265formedia/webtypes — do not requireexecutable+architecturewhen aurl/media_uriis present, and render a media/web card in the library. - Init: branch the exec path in
playos-init/src/supervisor.c(currently ELF-only at:731X_OK gate,:806execl) somedia/webtypes spawn the appropriate client (mpv,librespot,Cog) with a fixed argv derived from the manifest. - Input: do not build compositor
wl_seatforwarding for this sprint. Instead, each client is driven over its own IPC, and the controller mapping lives in the thin wrapper (or, for YouTube Music Option A, in the Raylib wrapper itself). - Audio: clients use ALSA directly (
mpv --ao=alsa,librespot/spotifydALSA backend), so the Tier 2 "Dedicated Audio Service" is not a prerequisite. - Security: gated on the Sprint 12 sandbox before any device integration.
What it would actually take
In dependency order:
- Land the Sprint 12 sandbox (seccomp + Landlock +
PR_SET_NO_NEW_PRIVS) so media/browser clients are not unconfined. - Package and cross-compile
ffmpeg+mpv,librespot/spotifyd, and (optionally)WPE WebKit+Cogforx86_64-muslin Buildroot. - Extend the launch lifecycle (manifest
type/url/media_uri, shell validator relaxation, init exec branch) as a non-breaking change. - Build the controller-first wrappers (Spotify control, YouTube/YouTube Music search+play UI) driving each client over its own IPC.
- Validate the Wayland attach for
mpv(--vo=gpu --gpu-context=waylandorwlshm) andCogunder the compositor's forced-fullscreen, single-game-role model.
Only after those would "spawn a media/web client as a type: media|web launch target" be a small, well-understood change.
Recommended De-Risking Spike (Bounded, Host-Only)
If ever funded, a single bounded spike would, in order:
- Package/build probe — confirm
mpv/ffmpeg/librespot/spotifyd/WPE WebKit/Cogactually cross-compile againstx86_64-muslin Buildroot, and record which are non-starters. No defconfig merge yet. - Wayland attach probe — validate
mpvandCogattach as fullscreen Wayland surfaces under a host nested compositor, using--vo=gpu --gpu-context=wayland/wlshm. Confirm the game-role fit. - IPC/controller probe — confirm
mpv --input-ipc-serverandlibrespot/spotifydcan be driven without the Wayland seat, and sketch the controller→IPC mapping. - YouTube Music UX spike — prototype Option A (thin Raylib wrapper + mpv IPC + yt-dlp/InnerTube search) on a host to prove search/play/now-playing is achievable.
- Explicit stop — do not proceed to device integration, Buildroot merge, sandbox implementation, or Netflix.
Scope
In Scope (this sprint)
- This
Sprint-20.mddocument. - The feasibility analysis, recommended stack, plus/minus, blocker list, integration design, and verdict above.
- Definition of the bounded host-only de-risking spike (S20-T1…T4) — not its execution.
Explicitly Out of Scope / Not Planned
- Any
mpv/ffmpeg/librespot/spotifyd/WPE/Cogintegration or Buildroot packaging. - Netflix, Chromium/CEF/Electron, Widevine/EME, X11/Xwayland.
- Compositor
wl_seatinput forwarding, audio service, sandbox, or hardware-video-decode implementation. - Changing the existing shell, compositor, or init in this sprint.
Required Repository Changes
| Repo | Required work |
|---|---|
playos-spec | Replace Sprint-20.md with this native-client strategy; update SUMMARY.md, post-mvp.md, and roadmap.md |
| (none else) | No implementation repositories change in this sprint |
Expected Files and Directories
playos-spec/src/sprints/Sprint-20.md # REPLACE: this assessment
playos-spec/src/SUMMARY.md # UPDATE: link title
playos-spec/src/post-mvp.md # UPDATE: entry
playos-spec/src/sprints/roadmap.md # UPDATE: sprint-plan row
Agent Task Breakdown
Task Status Grid
| Task ID | Task | Primary repo | Status | Notes / evidence |
|---|---|---|---|---|
| S20-T1 | Native-client feasibility (musl build, ALSA, Wayland attach) | playos-spec | not started | ADR-0003, ADR-0007, compositor.c, no Buildroot packages |
| S20-T2 | Integration design (manifest type, init exec branch, shell validation, IPC) | playos-spec | not started | supervisor.c:731,806, screen_library.c:153-265, manifest schema |
| S20-T3 | YouTube Music app shape (Option A vs B) + browser risk | playos-spec | not started | mpv IPC, yt-dlp/InnerTube, WPE/Cog musl risk |
| S20-T4 | Bounded host-only de-risking spike definition | playos-spec | not started | build probe, Wayland probe, IPC probe, YT Music UX spike |
S20-T1 — Native-client feasibility
- Confirm
mpv/ffmpeg,librespot/spotifyd, andWPE WebKit/Cogare musl-buildable in principle and speak ALSA directly, tying the conclusion to ADR-0003 and ADR-0007. - Record that none of these packages exist in any Buildroot defconfig (grep evidence), and flag
WPE WebKit-on-musl as the highest risk. - State this as a packaging/spike gate, not a code task.
Done when: the sprint records the recommended stack and identifies the exact spike step (S20-T4 step 1) that would validate musl buildability.
S20-T2 — Integration design
- Confirm the launch path is ELF-only (
playos-init/src/supervisor.c:731,806) and the shell validator requiresexecutable+architecture(playos-shell/src/screen_library.c:153-265). - Confirm the manifest schema has
additionalProperties: true, so atype/url/media_uriextension is non-breaking. - Confirm the compositor forwards no
wl_seatinput (playos-compositor/src/system_button.c:63), so clients must be driven over their own IPC rather than the Wayland seat.
Done when: the sprint records the non-breaking manifest/init/shell changes and the per-client IPC input workaround with source citations.
S20-T3 — YouTube Music app shape + browser risk
- Recommend Option A (thin Raylib wrapper driving
mpvvia--input-ipc-server+ yt-dlp/InnerTube search) over Option B (dedicated headless backend). - Record the browser (WPE/Cog) as the riskiest item: musl build risk, no Widevine, and WebKit security surface requiring the Sprint 12 sandbox.
Done when: the sprint states the YouTube Music recommendation and marks the browser as an optional spike rather than a product commitment.
S20-T4 — Bounded host-only de-risking spike definition
- Define the five-step spike: build probe → Wayland attach probe → IPC/controller probe → YouTube Music UX spike → explicit stop.
- Scope it strictly to host-only, with no Buildroot merge, no device integration, no sandbox implementation, and no Netflix.
- State the acceptance that marks the spike complete and the condition under which it would not advance to device integration.
Done when: the sprint records a bounded, stoppable spike definition and a default recommendation to keep Netflix out of scope.
Verification and Evidence
| Evidence | How it is produced |
|---|---|
| Assessment recorded | Sprint-20.md present with recommended stack, plus/minus, blockers, integration design, and verdict |
| Roadmap indexed | SUMMARY.md, post-mvp.md, and roadmap.md link the sprint with the new title |
| Grounded analysis | Each blocker cites a source file, ADR, or grep result |
| Link integrity | mdbook build passes |
| No implementation drift | No media/browser files, Buildroot changes, or compositor/audio changes are produced by this sprint |
Acceptance Criteria
- The assessment states a clear feasibility verdict with a plus/minus table
-
A recommended native-client stack (Spotify
librespot, YouTube/YouTube Musicmpv+yt-dlp, browserWPE/Cog) is recorded - Netflix is explicitly documented as out of scope (Widevine + glibc + Google license)
- YouTube Music is assessed as a PlayOS app with Option A recommended over Option B
-
The non-breaking integration design (manifest
type/url/media_uri, init exec branch, shell validation, per-client IPC) is documented with citations -
The missing compositor
wl_seatinput forwarding is recorded as a design constraint, not a blocker, with the IPC workaround - The Sprint 12 sandbox and missing Buildroot packages are recorded as prerequisites
- A bounded host-only de-risking spike is defined with an explicit stop
-
SUMMARY.md,post-mvp.md, androadmap.mdare updated -
mdbook buildpasses
Handoff to Post-MVP
After this sprint:
- The "Spotify/YouTube/YouTube Music/browser?" question has a written answer and a recommended native-client strategy.
- Netflix remains fully out of scope, and Chromium/CEF/Electron/X11/Xwayland remain never-planned.
- The locked decisions (musl-only, ALSA-only) are preserved, not reopened.
- A future spike, if funded, can pick up S20-T4's bounded scope without re-deriving the assessment.
Exit Gate
The assessment is written, indexed, and link-verified; it concludes that a controller-first native media/browser client strategy (Spotify librespot, YouTube/YouTube Music mpv+yt-dlp, browser WPE WebKit+Cog) is viable as a post-MVP direction without reopening the musl/ALSA ADRs, while Netflix is explicitly out of scope — and it scopes an optional bounded host-only spike while leaving all device integration unplanned.
Previous: Sprint 19
Sprint 21 — Multiple Local User Profiles (Post-MVP)
Goal: Produce a written plus/minus and feasibility assessment for console-style local user profiles — multiple on-device PlayOS users whose saves, cache, settings, and screenshots are isolated from each other, analogous to PlayStation console profiles. This sprint records the assessment and scopes the design; no profile implementation is built.
Primary Outcome: A decision-ready Sprint-21.md record that (a) defines what "multiple local users" means for PlayOS, (b) recommends a path-scoped profile approach layered on the Sprint 12 sandbox, (c) documents the storage, launch, and shell integration, (d) resolves the ideas.md "multiple interactive users" wording conflict, and (e) scopes Sprint 12 as the isolation foundation.
Status: 🟡 Post-MVP — assessment/design only; not scheduled. No implementation work is approved.
Prerequisites: MVP stable (Sprint 15–16); persistent storage and game discovery (Sprint 6); playos-init sandbox with data-driven path policy (Sprint 12); single playos-game identity documented in the security model (Sprint 12).
Why This Sprint Exists
A shared family handheld has exactly the same profile problem a console does: each person wants their own saves, settings, screenshots, and progress, without seeing or overwriting anyone else's. PlayOS currently has a single implicit user (playos-game) and a single per-game save/cache path keyed only by PLAYOS_GAME_ID. This sprint assesses how to add many isolated local profiles without turning PlayOS into a general-purpose multi-login Linux system, and it aligns Sprint 12 so the sandbox is ready to accept a profile dimension later.
The scope is deliberately local profiles only. Parental controls, PIN, and per-profile game libraries are separate future features; PlayOS Network account linking (the online identity that unlocks Marketplace and Online Gaming Services) is a follow-on layered on this local profile id.
Assessment Inputs
The assessment rests on the following authoritative facts, verified against source and spec:
- Storage paths are keyed only by game id.
playos-platform-api/src/playos_storage.c:36-41builds/data/saves/%s,/data/cache/%s, and related paths fromPLAYOS_GAME_IDonly.include/playos/playos_storage.h:26-45documents per-game paths, and:81-90returns/data/gamesas the shared game-install root. - Launch does not yet drop privileges.
playos-init/src/supervisor.c:762-806spawns the game child withsetsid()+ environment +execl()and no setuid/setgid/PR_SET_NO_NEW_PRIVS. The env set at:773-782already includesPLAYOS_GAME_ID,PLAYOS_INSTALL_PATH,PLAYOS_SAVE_PATH,PLAYOS_CACHE_PATH,WAYLAND_DISPLAY,PLAYOS_LIFECYCLE_FD, andPLAYOS_LAUNCH_TOKEN— the natural injection point for a futurePLAYOS_PROFILE_ID.security-model.md:228-235records this pre-Sprint-12 gap. - IPC auth is identity-based, not profile-based.
playos-init/ipc/ipc_server.c:83-88accepts peers with GID 1000 (playos-trusted) or uid 0. Profiles do not change trusted-component auth; they are a data boundary, not a control boundary. - Shell discovery is global.
playos-shell/src/screen_library.c:379-424scans/data/gameswith a readdir and manifest validation (:154-265requiresexecutable+architecture). Game installs stay shared, so discovery is unchanged. - Security model already assumes one identity.
security-model.md:58-65privileges a singleplayos-game;:119chowns game data toplayos-game:playos-game;:203-212lists Landlock allowed paths;:337names user namespaces as post-MVP hardening. Sprint 12 makes this real. - A
/data/profiles/placeholder already exists but is unused.architecture.md:363documentssaves/<game-id>/ profiles/, autosaves/, settings/and:371showsprofiles/;partition-layout.md:88and:96reserve/data/profiles/. The path is in the design, not yet inplayos_storage.c. post-mvp.mdalready has a profiles block.post-mvp.md:160-163lists Multiple Local User Profiles with a/data/saves/<profile>/<game-id>/layout that this sprint corrects to/data/profiles/<pid>/....ideas.mdwording conflict.ideas.md:104-105lists "multiple interactive users" as never-planned. That entry means multiple interactive Linux login users, which remains out of scope; console-style local profiles are separate and planned post-MVP.
Feasibility Assessment
Verdict
Feasible post-MVP; moderate effort; low architectural risk. PlayOS can support console-style local profiles by keeping a single Linux identity (playos-game) and adding a profile id to the storage path and sandbox allowlist. This is a data-path + shell feature layered on the Sprint 12 isolation work — not a hard blocker and not a re-architecture. The recommended approach is path-scoped profiles (single uid + profile-scoped Landlock), with uid-per-profile deferred to later hardening only if profiles must become hard security boundaries (e.g. parental controls).
Interpretation
"Many local users isolated from each other, like PlayStation" means console-style local profiles, not multiple interactive Linux login users. PlayOS keeps one unprivileged runtime identity and isolates profile data rather than creating a Unix account per profile. This preserves the Sprint 12 trust model and avoids reopening ideas.md's never-planned "multiple interactive users" item.
Approach comparison
| Approach | Identity | Isolation mechanism | Effort | Risk | Recommendation |
|---|---|---|---|---|---|
| A — path-scoped profiles | Single playos-game uid + PLAYOS_PROFILE_ID env | Profile-scoped Landlock allowlist + per-profile storage paths | Low–moderate | Low | Recommended |
| B — uid-per-profile | One Linux uid per profile | OS-level user separation | High | Medium | Defer to hardening |
Approach A is recommended because it changes one path-construction function plus a sandbox prefix, not the trusted-component boundary, IPC auth, or the compositor. Approach B is a real security boundary but costs a second identity model and per-profile ownership, which is unjustified until a feature (parental controls, account-locked content) demands it.
What data is isolated
| Data | Scope | Notes |
|---|---|---|
| Game saves | Per-profile | /data/profiles/<pid>/saves/<game-id>/ |
| Game cache | Per-profile | /data/profiles/<pid>/cache/<game-id>/ |
| PlayOS settings | Per-profile | /data/profiles/<pid>/settings/ |
| Screenshots | Per-profile | /data/profiles/<pid>/screenshots/ |
| Account credentials / tokens | Per-profile (when linked) | /data/profiles/<pid>/auth/ — encrypted, platform-managed, out of the game sandbox |
| Game installs | Shared | /data/games/ unchanged |
| Wi-Fi / network config | System-wide | Not per-profile |
| Logs | System-wide (per-profile optional) | /data/logs/ |
Game installs remain shared by design: this mirrors consoles, where all profiles can launch installed titles but each has separate saves and settings.
Plus / Minus
| Dimension | Plus | Minus |
|---|---|---|
| Product | PlayStation-style local profiles on a shared family handheld | More UX surface: profile picker, switcher, per-profile settings screens |
| Architecture | Layers on Sprint 12's data-driven sandbox; single uid means no IPC/auth/compositor rework | Adds a profile-id dimension to every storage path and env contract |
| Isolation | Profile-scoped Landlock gives real save/cache isolation between profiles | Game installs are shared by design; not a hard security boundary between mutually-untrusting profiles |
| Migration | Legacy /data/saves/<game-id> maps cleanly to a default profile | One-time migration code plus a read fallback |
| Effort | Low–moderate; mostly storage-path derivation + shell UI | Per-profile settings/screenshots must be threaded through the versioned libplayos C ABI |
Integration design (how it fits the existing lifecycle)
- Storage:
playos_storage.c/.hderive paths fromPLAYOS_GAME_IDandPLAYOS_PROFILE_ID; keep the existing functions returning the active profile's paths, and add profile-explicit variants. This is an additive, versioned C ABI change. - Launch:
playos-initloads the active profile at boot, setsPLAYOS_PROFILE_IDand profile-scopedPLAYOS_SAVE_PATH/PLAYOS_CACHE_PATHinto the game env (supervisor.c:773-782), and builds the Landlock allowlist with the profile prefix. - Shell: add profile selection at boot, a user switcher, and per-profile settings; discovery of shared game installs (
/data/games) is unchanged. - IPC/auth: unchanged. Profiles are a data boundary, not a trusted-component boundary.
Migration
- Introduce
/data/profiles/<pid>/...; treat legacy/data/saves/<game-id>as the default profile (<pid>=default) with a one-time move or read fallback, so existing saves are not orphaned. - Do not migrate game installs; they stay shared.
Spec disambiguation
ideas.md:104-105: record here that "multiple interactive users" means multiple interactive Linux users, which stays never-planned. Console-style local profiles are a distinct, post-MVP feature.ideas.mdis not edited (spec policy).- Overloaded "profiles":
/data/saves/<game-id>/profiles/(game-internal save slots) is a different concept from the top-level/data/profiles/(PlayOS users). The post-MVP entry and future storage docs must use the top-level path to avoid collision.
Sequencing
Sprint 12 must land first, because it makes the sandbox data-driven and parameterized by launch identity. Sprint 21 then adds the profile dimension by changing one path-construction function, not the enforcement logic. PIN/parental-controls/uid-per-profile remain later.
Account linking — PlayOS Network (online identity)
The local profile is the offline root of identity. A PlayOS Network account is the online identity in playos-cloud; a local profile can be linked to exactly one Network account, and the link is what unlocks PlayOS Marketplace and Online Gaming Services.
Self-hosting: PlayOS Network and playos-cloud services follow the playos-cloud self-hostable-by-design mandate — every hosted feature has a documented self-hosted deployment, and no feature may require a single central provider. This applies to accounts/identity, cloud saves, matchmaking, and any Marketplace online services.
| Layer | What it is | Unlocks |
|---|---|---|
| Local profile | Offline identity on device (PLAYOS_PROFILE_ID) | Local saves/settings/screenshots; offline play |
| PlayOS Network account | Online identity in playos-cloud (OAuth2 or PlayOS account service) | Marketplace access, cloud saves, multiplayer/matchmaking, friends/presence |
| Link | Local profile ↔ one Network account (stored per-profile) | Account-scoped entitlements and online services for that profile |
Feasibility: feasible post-MVP as a follow-on to this sprint; it does not require multiple Linux users. It adds playos-cloud services plus per-profile credential storage, so it is more work than the local-profile layer alone.
Security boundary: the raw account token must never reach a game. The platform holds credentials in a trusted service and issues games short-lived, scoped session tickets per title/service. This keeps the Sprint 12 game sandbox intact: a linked account does not make the game trusted. Credential storage is per-profile (/data/profiles/<pid>/auth/, encrypted at rest) and out of the game's Landlock allowlist.
Implications for the isolation approach: account-linked profiles hold credentials, which is the first strong argument for later hardening (Approach B uid-per-profile or a platform keychain). It is not required for v1: path-scoped profiles plus a trusted credential service outside the game sandbox are sufficient.
Sequencing: Wi-Fi (Sprint 16) → local profiles (Sprint 21) → PlayOS Network account linking + playos-cloud → Marketplace (Sprint 19) and Online Gaming Services consume the linked identity. Marketplace v1 can stay free-content/device-local (Sprint 19) and add account-scoped entitlements only after linking exists.
Scope
In Scope (this sprint)
- This
Sprint-21.mddocument. - The feasibility verdict, Approach A vs B recommendation, plus/minus, isolation matrix, integration design, migration, and spec disambiguation above.
- The Sprint 12 alignment additions recorded below (made as part of this sprint's spec output).
Explicitly Out of Scope / Not Planned
- Any storage, init, or shell code changes for profiles.
- Multiple interactive Linux login users (never-planned;
ideas.md:104-105). - PIN, parental controls, per-profile game libraries, uid-per-profile, and PlayOS Network account linking (the online identity layer is a follow-on, not part of this local-profile sprint).
- Editing
ideas.mdor rewriting any ADR.
Required Repository Changes
| Repo | Required work |
|---|---|
playos-spec | Add Sprint-21.md; align Sprint-12.md for isolation foundations; update SUMMARY.md, post-mvp.md, and sprints/roadmap.md |
| (none else) | No implementation repositories change in this sprint |
Expected Files and Directories
playos-spec/src/sprints/Sprint-21.md # ADD: this assessment/design
playos-spec/src/sprints/Sprint-12.md # UPDATE: data-driven sandbox + single-identity foundations
playos-spec/src/SUMMARY.md # UPDATE: link title
playos-spec/src/post-mvp.md # UPDATE: profiles entry (path-corrected)
playos-spec/src/sprints/roadmap.md # UPDATE: sprint-plan row
Agent Task Breakdown
Task Status Grid
| Task ID | Task | Primary repo | Status | Notes / evidence |
|---|---|---|---|---|
| S21-T1 | Define "multiple local users" and select the isolation approach | playos-spec | not started | ideas.md:104-105, security-model.md:58-65 |
| S21-T2 | Design profile-scoped storage and launch env | playos-spec | not started | playos_storage.c:36-41, supervisor.c:773-782 |
| S21-T3 | Design shell profile selection/switcher and settings | playos-spec | not started | screen_library.c:379-424, shared /data/games |
| S21-T4 | Align Sprint 12 for isolation foundations | playos-spec | not started | Landlock ruleset data-driven; single uid |
S21-T1 — Define "multiple local users" and select the approach
- Record that PlayOS profiles are console-style local profiles, not multiple interactive Linux login users (disambiguating
ideas.md:104-105without editing it). - Recommend Approach A (path-scoped profiles) over Approach B (uid-per-profile), with the single-
playos-gameidentity preserved.
Done when: the sprint states the interpretation and the recommended approach with a comparison table.
S21-T2 — Design profile-scoped storage and launch env
- Confirm storage paths are keyed only by
PLAYOS_GAME_ID(playos_storage.c:36-41) and that/data/gamesis shared (playos_storage.h:81-90). - Design
/data/profiles/<pid>/saves|screenshots|cache|settings/, with game installs shared and network/log config system-wide. - Design the additive
PLAYOS_PROFILE_ID+ profile-scopedPLAYOS_SAVE_PATH/PLAYOS_CACHE_PATHenv atsupervisor.c:773-782, and a legacy/data/saves/<gid>→ default-profile migration.
Done when: the sprint records the storage layout, env contract, and migration with source citations.
S21-T3 — Design shell profile selection and switcher
- Confirm discovery is global (
screen_library.c:379-424) and stays shared for game installs. - Design boot-time profile selection, a user switcher, and per-profile settings screens.
Done when: the sprint records the shell integration without requiring a discovery change.
S21-T4 — Align Sprint 12 for isolation foundations
- Add to Sprint 12: keep a single
playos-gameuid (no per-user uid now); parameterize the sandbox path policy by launch identity; build the Landlock ruleset from launch-time variables (game-id now, profile-id later); and keep Landlock path construction data-driven. - Mark multi-user local profiles as explicitly out of Sprint 12's scope and deferred to Sprint 21.
Done when: Sprint-12.md contains those four minimal alignment edits and no structural rewrite.
Verification and Evidence
| Evidence | How it is produced |
|---|---|
| Assessment recorded | Sprint-21.md present with verdict, approach comparison, plus/minus, isolation matrix, integration design, migration, and disambiguation |
| Approach selected | Approach A recommended over Approach B with rationale |
| Storage/env design grounded | playos_storage.c:36-41, supervisor.c:773-782, playos_storage.h:81-90 cited |
| Sprint 12 aligned | Sprint-12.md carries single-identity + data-driven-sandbox foundations and the deferral note |
| Link integrity | mdbook build passes |
| No implementation drift | No storage/init/shell code changes are produced by this sprint |
Acceptance Criteria
- The assessment states a clear feasibility verdict with a plus/minus table
- Approach A (path-scoped profiles) is recommended over Approach B (uid-per-profile)
- The "multiple local users" interpretation is recorded as console-style local profiles, not multiple interactive Linux users
-
The
/data/profiles/<pid>/...layout is documented, with game installs shared and network/log config system-wide -
The additive
PLAYOS_PROFILE_ID+ profile-scoped path env design is documented with citations -
The legacy
/data/saves/<game-id>→ default-profile migration is recorded -
The
ideas.mdwording conflict and the "profiles" overload are disambiguated - Sprint 12 is aligned as the isolation foundation (single uid + data-driven sandbox + deferral note)
-
SUMMARY.md,post-mvp.md, andsprints/roadmap.mdare updated -
mdbook buildpasses
Handoff to Post-MVP
After this sprint:
- The "many isolated local users" question has a written answer: path-scoped console profiles, layered on Sprint 12.
- Sprint 12 is the on-record isolation foundation, and Sprint 21 is the profile-dimension follow-up that reuses it.
- The never-planned "multiple interactive Linux users" boundary is preserved, and the top-level
/data/profiles/vs game-internalprofiles/collision is resolved. - PIN/parental-controls/uid-per-profile are explicitly deferred, not scheduled.
Exit Gate
The assessment is written, indexed, and link-verified; it concludes that console-style local profiles are feasible post-MVP via path-scoped profiles (single playos-game uid + PLAYOS_PROFILE_ID + profile-scoped Landlock), it aligns Sprint 12 as the isolation foundation, and it leaves all profile implementation unplanned.
Previous: Sprint 20
Sprint 22 — LVGL Shell UI Spike (Post-MVP)
Goal: Prove that LVGL v9 can render a resolution-adaptive, controller-navigated shell UI inside the existing PlayOS shell, starting with a raylib-managed texture as a smoke-test backend — without replacing rcore_playos.c, the Wayland/EGL lifecycle, or the game ABI. This is a bounded spike, not a product-direction port. The final rendering path (CPU blit vs. GPU draw unit vs. LV_USE_WAYLAND full port) is decided at implementation time.
Primary Outcome: A working experimental LVGL screen navigated with the controller in the dev environment, plus a written go/no-go recommendation for a full LVGL shell port — including an explicit rendering-path decision (Path 1 / 2 / 3).
Status: 🟡 Post-MVP — spike defined; not started. No implementation work is approved until this sprint is scheduled.
Prerequisites: MVP stable (Sprint 15–16); the Raylib 6.0 shell landed (Sprint 5.5); rcore_playos.c is the single shell rendering backend (ADR-0006); the musl-only constraint is in force (ADR-0003); the nested-Wayland dev environment works (playos-spec/src/dev-environment.md).
Why This Sprint Exists
The shell is currently a controller-first raylib application that draws rectangles and text with immediate-mode calls (render_util.c). Building a polished, screen-adaptive UI this way is possible but laborious: layout is hand-derived from GetScreenWidth()/GetScreenHeight(), and every widget (list, grid, focus state, transition) is bespoke.
LVGL is a retained-mode embedded UI library with widgets, themes, animations, flexbox/grid layout, and DPI scaling — a materially better toolkit for a screen-based, resolution-independent, controller-navigated shell. The open question is not "is LVGL capable" but "can it be integrated into the existing raylib-backed shell at low risk."
This sprint answers that with a smoke test first, then a deliberate rendering-path decision. The cheapest validation is a CPU texture-blit integration: LVGL renders to its own framebuffer, and its flush_cb uploads pixels into a raylib Texture2D; raylib draws that texture as a fullscreen quad. This keeps the entire Wayland/EGL/vsync/input stack intact and turns raylib into a thin blitter + event loop — enough to prove the UI value and input mapping, but not the production renderer.
Assessment Inputs
The spike rests on the following authoritative facts:
- LVGL v9.5.x, MIT, C99. Royalty-free and static-link friendly; no OS dependency beyond a display buffer and a tick source. (license, requirements)
- LVGL resolution model.
lv_dpx()DPI scaling, percentage units,LV_SIZE_CONTENT, min/max sizing, and flexbox/grid layouts provide responsive UI without hardcoded pixels. (coordinates) - LVGL display driver hook. A display is registered with a
flush_cbthat receives dirty pixel areas; the callback owns how pixels reach the framebuffer/GPU. This is the exact seam for a raylib texture backend. - LVGL input model. Four input-device types exist —
POINTER,KEYPAD,ENCODER,BUTTON— with no native gamepad type. Controller navigation is achieved by mapping d-pad/face buttons toLV_KEY_*and usinglv_group/lv_gridnavfocus. (indev, groups) - Current shell stack.
external/raylib/src/platforms/rcore_playos.cowns Wayland/EGL/GLES2 and frame-callback vsync;src/input.creads controller evdev directly;src/render_util.cwraps raylib draw calls; the shell must stay alive at 60 fps with controller-only navigation and no blocking I/O on the render thread (playos-shell/AGENTS.md). - Raylib texture upload. A
Texture2Dcan receive CPU pixels viaUpdateTexture, and sub-rectangle updates can use the GL texture id directly forglTexSubImage2D.
Assessment Constraints (Locked)
These constraints are not re-negotiated by this sprint:
- Single rendering backend for the shell stays raylib (ADR-0006) unless Path 3 is chosen, in which case an ADR decision is required first. In the default Path 1/2 spike, LVGL is a UI layer on top of raylib, not a second Wayland/EGL backend.
- musl-only (ADR-0003) and controller-only navigation remain in force.
- Shell invariants: always alive, 60 fps target, no blocking I/O on the render thread, no direct IPC socket access except the trusted evdev input path, rendering stops while a game is foreground but the process stays alive.
- No change to
rcore_playos.c, the game ABI, or ADR-0006. The spike is additive and reversible.
Feasibility Assessment
Verdict
Feasible; low-to-moderate effort; low architectural risk. The spike starts with the cheapest correctness check — a CPU-buffer texture blit — and treats that only as a smoke test, not the production renderer. The final rendering path is deliberately left open: at implementation time we may stay on the smoke-test path, or jump straight to one of the two GPU-accelerated paths below (lv_opengles_texture + GPU draw unit, or the LV_USE_WAYLAND full port).
Rendering-path decision (re-evaluate at implementation time)
Three paths are on the table. The sprint does not lock us to the CPU blit.
| Path | How it works | Risk | Role in this sprint |
|---|---|---|---|
| 1 — CPU blit (smoke test) | LVGL software renderer writes a CPU framebuffer; flush_cb uploads dirty pixels into a raylib Texture2D; raylib draws the fullscreen quad | Lowest | Minimum first check only — proves LVGL, input mapping, and the dev loop; not the target renderer |
2 — lv_opengles_texture + GPU draw unit | LVGL rasterizes through a GPU draw unit (NanoVG preferred, or the GLES texture-cache unit) and hands back a GL texture that raylib blits; the shell keeps its EGL context and frame pacing | Low-to-medium | Likely production path — GPU-accelerated while retaining rcore_playos.c |
3 — LV_USE_WAYLAND full port | LVGL owns the Wayland surface/EGL/vsync; drop rcore_playos.c for the shell | Medium | Long-term option — requires an ADR decision because it supersedes part of ADR-0006 |
Rule for implementation: run Path 1 as the cheapest first validation. If the smoke test is clean, do not assume Path 1 is the end state — explicitly re-evaluate and choose Path 2 (or Path 3 with an ADR) before any production work. We may go straight to Path 2 or 3 from the outset if that is more expedient.
Path 1 keeps raylib in control of GL state, vsync, and input, so LVGL's software renderer cannot fight raylib's GL state machine — which is exactly why it is a safe smoke test. Path 2 keeps that same shell ownership while adding GPU rasterization through LVGL's draw unit; it is the least disruptive GPU path and the likely production choice. Path 3 is the cleanest long-term architecture but re-owns Wayland/EGL, so it is a deliberate ADR decision, not a spike default.
Plus / Minus
| Dimension | Plus | Minus |
|---|---|---|
| Resolution adaptation | lv_dpx, %, flex/grid, min/max — real responsive layouts | Breakpoints and assets still need design/testing |
| Look & feel | Widgets, themes, styles, animations; retained-mode reduces drawing code | Default themes are plain; a console-grade look needs custom theme/fonts/images |
| Integration risk | No rcore_playos.c change for Path 1/2; additive, reversible | Path 1 has two rendering stacks (raylib blit + LVGL software render); Path 3 replaces the shell backend and needs an ADR |
| Input | lv_group/lv_gridnav maps cleanly to D-pad | No native gamepad indev; custom evdev→LV_KEY_* glue required |
| Build | Pure C99, musl-safe | No Buildroot package; spike vendors LVGL under playos-shell/external/ and defers Buildroot packaging |
| Footprint/performance | Tiny; partial refresh; dirty-area upload; Path 2 offloads rasterization to GPU | Path 1 is CPU-bound: 1080p@60 full-frame upload ~500 MB/s if not using dirty areas; must use sub-rect updates |
| Total | Faster path to a polished shell UI than hand-rolled raylib | One-time integration detail (format/endianness/upload/tick) plus a learning curve |
Smoke-test integration design (Path 1 — CPU blit)
This is the minimum first check only. It is not the production renderer. If Path 2 or 3 is chosen at implementation time, the
flush_cbbelow is replaced by the GPU draw-unit orLV_USE_WAYLANDdriver respectively.
/* One full-screen 32-bit RGBA8 draw buffer (v9 API names to be checked). */
static lv_color_t fb[SCREEN_W * SCREEN_H];
static Texture2D shell_tex;
static void flush_cb(lv_display_t *disp, const lv_area_t *area, uint8_t *px_map) {
/* Upload only the dirty sub-rectangle into the raylib-managed GL texture. */
rlEnableTexture(shell_tex.id);
glTexSubImage2D(GL_TEXTURE_2D, 0,
area->x1, area->y1,
area->x2 - area->x1 + 1, area->y2 - area->y1 + 1,
GL_RGBA, GL_UNSIGNED_BYTE, px_map);
rlDisableTexture();
lv_display_flush_ready(disp);
}
/* Per frame, before raylib drawing: */
/* lv_tick_inc((uint32_t)(GetFrameTime() * 1000.0f)); */
/* lv_timer_handler(); */
/* BeginDrawing(); */
/* DrawTextureRec(shell_tex, (Rectangle){0, 0, W, H}, (Vector2){0, 0}, WHITE); */
/* EndDrawing(); */
Key implementation details:
- Color format: configure
LV_COLOR_DEPTH=32and match raylib's RGBA8 texture format; verify alpha channel order and byte order before assuming correctness. - Partial upload: LVGL's
flush_cbalready reports dirtyareas; useglTexSubImage2Don those rectangles. A first pass may use whole-frameUpdateTextureto validate correctness, then switch to sub-rect uploads for the 60 fps budget. - Tick: drive
lv_tick_incfrom raylib'sGetFrameTime(); calllv_timer_handler()once per frame. - Input: create a
KEYPADindev whoseread_cbtranslates the existinginput.ccontroller state intoLV_KEY_UP/DOWN/LEFT/RIGHT/NEXT/PREV/ENTER/ESC, and uselv_gridnavfor 2D focus movement. A/B map toLV_KEY_ENTER/LV_KEY_ESC. - Keep LVGL in software-renderer mode for this smoke test. Do not enable LVGL's NanoVG/GL draw units in this hybrid; raylib owns GL state. Path 2 revisits this by enabling a GPU draw unit.
Spike Scope
In Scope (this sprint)
- Vendor LVGL v9.5 under
playos-shell/external/lvgland gate it behind a CMake option (e.g.PLAYOS_SHELL_EXPERIMENTAL_LVGL=ON). - Add an experimental screen that registers an LVGL display and renders a small test UI (a grid of buttons/list + a couple of widgets) with a custom or built-in theme. Start with the Path 1
flush_cbinto a raylibTexture2D; re-evaluate against Path 2/3 before doing anything beyond the smoke test. - Map the existing controller evdev input to an LVGL
KEYPADindev with group/gridnav navigation. - Verify 60 fps, correct colors, partial-upload behaviour, and no blocking I/O in the nested-Wayland dev environment.
- Record a written go/no-go recommendation for a full LVGL shell port.
Explicitly Out of Scope / Not Planned
- Changing
rcore_playos.c, the Wayland/EGL lifecycle, the game ABI, or ADR-0006. - A full port to
LV_USE_WAYLAND(Path 3) in this spike unless an ADR decision is made and the sprint is re-scoped mid-implementation. - Buildroot
package/lvglpackaging (deferred until the port is approved). - ROG Ally hardware validation (dev environment only).
- Replacing the existing production screens (HOME/LIBRARY/SETTINGS) in this sprint.
Required Repository Changes
| Repo | Required work |
|---|---|
playos-shell | Vendor LVGL v9.5 under external/lvgl; add CMake option; add experimental LVGL screen and input mapping (gated) |
playos-spec | Add Sprint-22.md; link from SUMMARY.md, sprints/roadmap.md, and post-mvp.md |
Expected Files and Directories
playos-shell/external/lvgl/ # VENDOR: LVGL v9.5 source
playos-shell/src/screen_lvgl_spike.c # ADD: experimental LVGL screen (gated)
playos-shell/include/shell.h # UPDATE: experimental screen enum entry (gated)
playos-shell/CMakeLists.txt # UPDATE: PLAYOS_SHELL_EXPERIMENTAL_LVGL option
playos-spec/src/sprints/Sprint-22.md # ADD: this sprint
playos-spec/src/SUMMARY.md # UPDATE: link
playos-spec/src/sprints/roadmap.md # UPDATE: sprint-plan row
playos-spec/src/post-mvp.md # UPDATE: entry
Agent Task Breakdown
Task Status Grid
| Task ID | Task | Primary repo | Status | Notes / evidence |
|---|---|---|---|---|
| S22-T1 | Vendor LVGL and gate behind a CMake option | playos-shell | not started | external/lvgl, PLAYOS_SHELL_EXPERIMENTAL_LVGL |
| S22-T2 | LVGL → raylib texture renderer + test screen (Path 1 smoke test) | playos-shell | not started | flush_cb, Texture2D, lv_display |
| S22-T3 | Controller → LVGL keypad/group navigation | playos-shell | not started | input.c, lv_indev, lv_gridnav |
| S22-T4 | Verify 60 fps + correctness; write go/no-go | playos-spec | not started | nested-Wayland dev env |
S22-T1 — Vendor LVGL and gate behind a CMake option
- Vendor LVGL v9.5 under
playos-shell/external/lvgl(mirroring the existingexternal/raylibpattern). - Add
PLAYOS_SHELL_EXPERIMENTAL_LVGL(defaultOFF) toplayos-shell/CMakeLists.txt; whenON, compile LVGL and the experimental screen, and link the shell unchanged otherwise. - Configure LVGL for 32-bit color, no GPU draw unit, and no OS threads; set the tick via the shell loop.
Done when: the shell still builds with the option OFF, and builds with the option ON in the dev environment with LVGL compiled in.
S22-T2 — LVGL → raylib texture renderer + test screen (Path 1 smoke test)
- Add
screen_lvgl_spike.cwith anenter/update/drawtriple matching the shell module convention. - In
enter, create an LVGL display with a full-screen draw buffer and aflush_cbthat uploads dirty areas into a raylibTexture2D. - Render a small test UI exercising flexbox/grid, a list or button matrix, and one theme; draw the texture fullscreen in
draw.
Done when: the test screen renders with correct colors through raylib in the nested-Wayland dev environment, with no change to rcore_playos.c.
S22-T3 — Controller → LVGL keypad/group navigation
- Create a
KEYPADindev whoseread_cbmaps the existing controller state toLV_KEY_UP/DOWN/LEFT/RIGHT/NEXT/PREV/ENTER/ESC. - Attach
lv_gridnav(orlv_groupfocus) so D-pad moves focus in two dimensions and A/B activate/back.
Done when: D-pad + A/B navigate the test widgets in the dev environment using the existing input.c evdev path.
S22-T4 — Verify 60 fps + correctness; write go/no-go + rendering-path decision
- Confirm the render loop stays within the 16 ms budget using dirty-area sub-rect uploads; fall back to whole-frame upload only for the correctness pass.
- Confirm no blocking I/O on the render thread and that the experimental path is fully gated off in default builds.
- Record the go/no-go recommendation for a full LVGL port and an explicit rendering-path decision (Path 1 / 2 / 3) in
Sprint-22.md(and reflect it inpost-mvp.md). If the smoke test is clean, the decision may be "jump straight to Path 2 or 3".
Done when: the sprint states a measured 60 fps result and a decision-ready go/no-go with rationale, including which rendering path production work should take.
Verification and Evidence
| Evidence | How it is produced |
|---|---|
| Experimental build works | cmake -B build -DPLAYOS_SHELL_EXPERIMENTAL_LVGL=ON && cmake --build build in dev env |
| LVGL renders through raylib | Test screen visible in nested Wayland; colors correct (no channel/byte swap) |
| Controller navigation works | D-pad/A/B move focus and activate LVGL widgets |
| 60 fps maintained | Frame-time measured in the dev env; dirty-area upload path used |
| No default-build drift | Build with PLAYOS_SHELL_EXPERIMENTAL_LVGL=OFF unchanged |
| Link integrity | mdbook build passes |
Acceptance Criteria
-
LVGL v9.5 is vendored under
playos-shell/external/lvgland gated behindPLAYOS_SHELL_EXPERIMENTAL_LVGL(defaultOFF) -
A gated experimental screen renders LVGL through a raylib
Texture2Dwithout changingrcore_playos.c -
Controller D-pad + A/B navigate LVGL widgets via
lv_group/lv_gridnav - Colors are correct for the chosen 32-bit format (no swapped channels/bytes)
- The 60 fps target is met using dirty-area sub-rectangle texture uploads
- No blocking I/O is introduced on the render thread, and default builds are unchanged
- A written go/no-go recommendation for a full LVGL port is recorded
-
SUMMARY.md,sprints/roadmap.md, andpost-mvp.mdare updated -
mdbook buildpasses
Handoff to Post-MVP
After this sprint:
- The "can LVGL build our shell?" question has a measured answer, not just a paper assessment.
- The smoke-test texture blit is proven or rejected as the cheapest correctness check — and a rendering-path decision (Path 1 / 2 / 3) is recorded, so production work can go straight to the GPU-accelerated path when justified.
- A full LVGL port (Path 3,
LV_USE_WAYLAND) is either recommended for a follow-up sprint (with an ADR), deferred in favour of Path 2, or explicitly ruled out with evidence.
Exit Gate
The spike compiles in the dev environment, renders an LVGL test screen through the existing raylib texture path at 60 fps with controller navigation, and produces a written go/no-go recommendation plus an explicit rendering-path decision (Path 1 / 2 / 3) — without touching rcore_playos.c, the game ABI, or ADR-0006 (unless Path 3 is chosen, in which case an ADR is required first).
Previous: Sprint 21
PlayOS Post-MVP Roadmap
Features added only after the core console lifecycle (v0.1.0 MVP) is stable and shipped.
Cross-references: roadmap.md §Post-MVP, architecture.md §22
Each item is listed with its motivation, dependencies, and rough priority tier.
Tier 1 — Near-Term (v0.2.x)
These features are most commonly requested and have direct dependencies on shipped MVP components.
Wi-Fi (playos-net)
Sprint: Sprint 16 (playos-net)
Motivation: Users need network access for future store downloads, cloud saves, and updates.
Stack: wpa_supplicant + dhcpcd (D-Bus-free) via a trusted playos-net bridge — no NetworkManager, no iwd
Scope: Connect to WPA2/WPA3 networks; Wi-Fi settings screen in shell
Depends on: Networking enabled in kernel config (deferred in MVP)
Options: See networking options for the D-Bus trade-off and the D-Bus-free wpa_supplicant alternative.
OTA System Updates + playos-tools Staging Helper
Motivation: Sprint 11 ships the offline A/B update engine (signed .playosb → inactive slot → boot.json → rollback) but no delivery path. The ROG Ally has no Wi-Fi in MVP, so updates arrive by sneakernet: download on a workstation, stage to USB/SD, copy into /data/updates/. No network is required to apply an update — network only affects how the bundle is delivered.
Two phases:
playos-toolshost helper (works now, no on-device Wi-Fi): a workstation CLI that downloads a signed.playosb, verifies its signature, and stages it to USB/SD for copy into/data/updates/. This closes the delivery gap immediately after Sprint 11 without touching the Ally's network stack.- On-device download (after
playos-net): the shell's "Check for Update" downloads the bundle over Wi-Fi directly into/data/updates/, reusing Sprint 11'sApplyUpdateIPC andboot.jsoncontract unchanged — the update engine itself needs no modification. Depends on: Sprint 11 (A/B update engine +/data/updates/*.playosbcontract — MVP); phase 2 additionally depends on Wi-Fi (playos-net). Sprint: not yet allocated (post-MVP). Phase 1 is a standaloneplayos-toolstask; phase 2 is a follow-up to Sprint 16 (playos-net).
Touch + On-Screen Keyboard (OSK)
Motivation: The ROG Ally touchscreen is currently inert (no wl_touch forwarding), and every text-entry flow (Wi-Fi passphrase, search, save naming, profiles) needs a keyboard.
Stack: Touch/pointer via the Wayland seat (wl_pointer/wl_touch + wlr_scene hit-testing); text input via upstream zwp_text_input_v3 (wlr_text_input_v3); OSK UI rendered by playos-overlay as a raylib component.
Scope: Touch reaches the focused surface (GetTouchPosition); a single system OSK is invokable by both the shell and games and delivers commit_string to the focused client.
Depends on: MVP input API stable; Sprint 7 overlay architecture; Sprint 8 gamepad-input precedent.
Sprint: Sprint 17
SSH Developer Mode (Dropbear)
Motivation: Developers need remote access for debugging without a physical serial connection.
Policy: SSH is present only in Developer Mode, which requires explicit user opt-in. Not enabled by default. Absent from retail builds.
Stack: Dropbear — small SSH server
Depends on: Wi-Fi
Input Service (playos-input)
Motivation: Controller remapping, gyroscope support, haptic feedback, multi-controller assignment.
Scope: Dedicated service abstracts raw evdev; exposes logical mappings; supports profiles
Replaces: Direct evdev reading in playos-platform-api
Depends on: MVP input API stable
Full Suspend/Resume
Motivation: Battery life on portable device; lid-close expected behavior
Scope: Full S3 or s2idle suspend; reliable resume without display artifacts
Blockers: AMD AMDGPU suspend/resume on the ROG Ally requires validated kernel + firmware path. Do not ship until confirmed stable (no corrupted display on resume, no GPU hang).
Depends on: AMD P-state + firmware validation
Rear Buttons and Special Buttons
Motivation: ROG Ally has macro/custom buttons not mapped in MVP
Scope: Map to logical PlayOS actions or user-configurable macros via playos-input
Depends on: playos-input service
Tier 2 — Medium-Term (v0.3.x)
Dedicated Audio Service
Motivation: Multiple simultaneous audio owners (shell notifications over game music), Bluetooth audio, per-application volume
Stack: PipeWire or a custom lightweight mixer
Scope: Shell sound effects play over game audio; overlay audio feedback; notification sounds
Replaces: Direct ALSA exclusivity in MVP
Depends on: Stable lifecycle events
Bluetooth
Motivation: Wireless controllers, headsets, keyboards
Stack: BlueZ (minimal config — no A2DP profile unless combined with audio service)
Depends on: playos-net (shares kernel network stack), audio service (for BT audio)
Screenshots and Screen Recording
Motivation: Share game moments
Stack: wlroots wlr-screencopy protocol for capture; encode with ffmpeg or similar
Policy: Game must opt-in or be allowed by PlayOS policy (no silent capture)
Depends on: Compositor update to expose wlr-screencopy to trusted clients
Vulkan Support (RADV / ANV)
Motivation: Modern games use Vulkan; future game store requires Vulkan support
Stack: Mesa RADV (AMD); Mesa ANV (Intel)
Scope: Add RADV to the ROG Ally Buildroot config; add Vulkan WSI path to the Raylib backend or expose raw Vulkan surface to games
Depends on: MVP graphics stack stable; no compositor ownership change needed
NVIDIA Backend (Nouveau + NVK + Zink)
Motivation: Extend the Sprint 13 portability proof (PCI enumeration + playos-platform-api backend abstraction) to a third vendor. NVIDIA's open-source stack is musl-compatible, unlike the proprietary userspace, which is glibc-only and off-limits for PlayOS (ADR-0003 musl).
Stack: Nouveau (in-kernel DRM/KMS) + Mesa NVK (Vulkan) + Zink (GL/GLES on Vulkan); signed GSP firmware for Turing+ (redistributable via linux-firmware).
Scope: Add a nouveau DRM/KMS + NVK + Zink Buildroot config and a third libplayos backend variant. Requires the wlroots Vulkan renderer (current renderer is GLES2/EGL, and Nouveau's Gallium GL is weak — NVK→Zink is the mature path).
Depends on: Vulkan Support (RADV / ANV) landing first, so the wlroots Vulkan renderer and WSI path exist; Sprint 13 backend abstraction proven on Intel.
Note: Deferred past Sprint 13 deliberately — Intel (i915) is the cheapest portability proof, and Nouveau power/reclocking maturity lags amdgpu, a real battery concern for a handheld.
PPSSPP (PSP) Emulator Sample via libretro
Motivation: Ship a real PSP ISO game as a Sample Application, launched from playos-shell through a libretro PPSSPP core — a strong end-to-end proof of the sample/shell/SDK story using a real, redistributable game payload.
Stack: libretro PPSSPP core (ppsspp_libretro) — GPLv2, hw_render=true, requires OpenGL ES ≥ 2.0, needs_fullpath=true, plus its asset pack (ppge_atlas.zim, lang/, flash0/; redistributable, not a BIOS).
Scope: A libretro frontend shipped as a bin/game sample that hands off the GL context to the core, feeds input through the libplayos controller ABI, and maps save data to /data/saves/<game-id>/.
Dependency to resolve: GL-context hand-off. Raylib's PLATFORM_PLAYOS backend (rcore_playos.c) owns the EGL/GLES context; the frontend must either expose that context (small additive change) or drive raw EGL/GLES + libplayos directly, bypassing raylib for this one sample. This is a libretro requirement, not a raylib limitation — emulator frontends sit on raw GL, not a game library.
Depends on: Stable game ABI (raylib backend + libplayos), SDK build profile that can cross-compile the libretro frontend, and the context-exposure decision above.
VRR and HDR
Motivation: ROG Ally display supports high refresh rates; future external displays may support VRR/HDR
Stack: DRM VRR (drm_connector.vrr_capable); KMS HDR metadata
Depends on: AMD DC VRR support stable in chosen kernel version
Game Developer SDK (playos-sdk)
Sprint: Sprint 15 (Game Developer SDK)
Motivation: Third parties can't build a PlayOS game today without running Buildroot. The game ABI requires musl (not glibc) and links the musl builds of libplayos (and libraylib), which only exist inside the Buildroot tree. A self-contained SDK lets developers compile on a regular x86_64 Ubuntu host and ship a runnable game.
Stack: Prebuilt x86_64-buildroot-linux-musl toolchain + libplayos/libraylib headers and static libs + a CMake toolchain file / pkg-config files. An Alpine Linux (musl-only) base image is a natural foundation: it already emits musl binaries natively, so the SDK only needs to add libplayos/libraylib.
Scope: Downloadable tarball (or container image) that turns standard gcc/cmake on an x86_64 Ubuntu host into a single bin/game + manifest.json + assets/ artifact. Binary must be musl-linked (static where possible) and target x86_64.
Testing story: Because graphics/input/audio come from raylib (already cross-platform) and only the thin libplayos surface is PlayOS-specific, the SDK should expose three build profiles so developers can test without hardware: (1) device — musl + PLATFORM_PLAYOS raylib backend + real libplayos (evdev), the shipped artifact; (2) desktop — native gcc + raylib's default desktop backend (X11/Wayland on Linux, Win32/GLFW on Windows) + a host libplayos shim that maps keyboard/gamepad to the controller ABI and no-ops lifecycle, so the game runs in a normal desktop window; (3) emulator — run the device build inside the PlayOS QEMU/container image for high-fidelity testing. The libplayos stub backend (PLAYOS_BACKEND=stub) already exists as the seed for the desktop shim.
Depends on: Versioned public Platform API (MVP); stable game ABI (Raylib backend + libplayos ABI)
C# Shell Reimplementation (Investigation)
Motivation: Assess whether re-implementing playos-shell in C# would reduce memory-safety/manual-parsing risk and speed UI iteration.
Status: Assessment only — not a planned feature direction. The default recommendation is to not pursue a C# rewrite; only a bounded host-only de-risking spike is documented.
Decisive factors: .NET-on-musl/NativeAOT risk (ADR-0003), Buildroot toolchain effort, and loss of the single Raylib backend (rcore_playos.c, ADR-0006).
Sprint: Sprint 18
LVGL Shell UI Spike
Motivation: Assess whether LVGL v9 can build a resolution-adaptive, controller-first shell UI inside the existing raylib-backed shell, without replacing rcore_playos.c or the game ABI.
Status: Bounded spike only — not a planned shell rewrite. The recommended path is LVGL rendering to a raylib-managed texture (flush_cb → glTexSubImage2D), keeping raylib as the Wayland/EGL/vsync owner.
Decisive factors: no native gamepad indev (custom evdev→LV_KEY_* glue), 32-bit color/endianness matching, dirty-area sub-rect uploads for 60 fps, and no Buildroot package (vendor under playos-shell/external/lvgl).
Sprint: Sprint 22
Tier 3 — Long-Term (v1.0+)
Cloud Saves and User Accounts
Motivation: Save portability across devices; online game library; a single platform identity for Marketplace access and Online Gaming Services.
Identity model: Local profiles (Sprint 21) are the offline identity; each can be linked to a PlayOS Network account (playos-cloud) that unlocks Marketplace and Online Gaming Services (matchmaking, multiplayer, friends, cloud saves).
Scope: Account authentication (OAuth2 or PlayOS account service); per-profile account linking; save sync on game launch/exit; scoped session tickets so games never receive raw account credentials.
Depends on: Wi-Fi (playos-net, Sprint 16), local profiles (Sprint 21), playos-cloud backend, and the Sprint 12 game sandbox.
Self-hosting: PlayOS Network / playos-cloud services are self-hostable by design — every hosted feature has a documented self-hosted deployment, and no feature may require a single central provider (accounts, cloud saves, matchmaking included).
Store Integration and Download Manager
Motivation: Users install games from a store without manual file transfer
Scope: Browse, purchase, download, install, update games
API addition: playos_store.h for store query and install progress
Depends on: Wi-Fi, signed .play packages, cloud saves
Signed .play Content Packages
Motivation: Integrity verification and atomic installation of games
Format: Signed archive with: manifest.json, binary, assets, content hash tree
Replaces: Plain directory installs
Depends on: Manifest signing (Sprint 12 foundations)
Marketplace (playos-marketplace)
Motivation: A content-economy layer for publishing, discovering, installing, and updating PlayOS applications, games, themes, and developer content; multiple store sources (official, community, OEM, private, LAN) rather than a single hard-coded store.
Assessment: See Sprint 19 — Marketplace Assessment. The repo is currently an empty stub; its docs reference "Part X — Package Format" and "Part XI — Cloud and Marketplace" in the spec, which do not yet exist. This is spec-blocked, not code-blocked.
Package format: .gpk (canonical in playos-marketplace/AGENTS.md; reconcile with the historical .play name below before implementation).
V1 boundary: Free-content catalog and install only — no payments, DRM, or entitlement enforcement. Account-scoped entitlements are a later phase gated on PlayOS Network account linking (see Sprint 21); v1 stays free and device-local.
Depends on: Wi-Fi (playos-net), SDK (Sprint 15), manifest signing (Sprint 12), atomic-install pattern (Sprint 11), and the missing Part X/Part XI spec chapters.
Self-hosting: stores are self-hostable — a store can be run by communities, OEMs, or individuals — consistent with the marketplace golden rules and the multiple-store-sources model above (no single hard-coded store).
Native Media & Browser Clients
Motivation: Run Spotify, YouTube, YouTube Music, and a lightweight browser as controller-first PlayOS apps launched from playos-shell, without reopening the musl-only (ADR-0003) or ALSA-only (ADR-0007) decisions. This is distinct from the never-planned "browser-based shell" below — it is a set of launched native clients, not a rewrite of the shell.
Assessment: See Sprint 20 — Native Media & Browser Client Strategy. Verdict: viable as a post-MVP direction using native clients — Spotify via librespot/spotifyd, YouTube and YouTube Music via mpv+yt-dlp, and a browser via WPE WebKit+Cog — all speaking ALSA directly and driven over per-client IPC (no compositor wl_seat input forwarding required). Netflix is out of scope (Widevine is glibc-only, proprietary, and Google-licensed).
Recommended posture: treat as a post-MVP native-client strategy; optionally run the bounded host-only spike in Sprint 20 before any device integration.
Depends on: the Sprint 12 sandbox, Buildroot packaging for the native clients, the non-breaking launch-lifecycle extension (manifest type/url/media_uri + init exec branch + shell validation), and validation of WPE WebKit on musl (the highest-risk item).
Delta Updates (Games and System)
Motivation: Reduce download size for incremental game and system updates
Stack: casync or bsdiff or a PlayOS-specific delta tool
Depends on: Store integration, A/B system updates (MVP)
Multiple Local User Profiles
Motivation: Family devices; per-user settings, saves, screenshots, and progress — isolated from each other like PlayStation console profiles.
Assessment: See Sprint 21 — Multiple Local User Profiles (Post-MVP). Verdict: feasible post-MVP via path-scoped profiles — a single playos-game Linux identity plus a PLAYOS_PROFILE_ID env and profile-scoped Landlock allowlist.
Scope: Profile selection at boot or via shell; per-profile /data/profiles/<profile-id>/{saves,cache,screenshots,settings}/<game-id>/; game installs (/data/games) stay shared.
Depends on: Storage layout stable (MVP), Sprint 12 sandbox (data-driven path policy), and the post-MVP profile work deferred to Sprint 21.
External Display Profiles
Motivation: Docking station or TV output; ROG Ally supports USB-C DisplayPort
Scope: Output detection via hotplug; resolution/refresh profile selection; orientation
Depends on: Compositor output management (MVP compositor handles hotplug)
Telemetry (Opt-In Only)
Motivation: Understand crash patterns and device health
Policy: Explicit user opt-in required. No telemetry in builds where the user has not consented. Anonymous only.
Scope: Crash reports, game launch/exit events, performance metrics
Depends on: Wi-Fi, user accounts
Performance Overlay (In-Game HUD)
Motivation: Show FPS, GPU/CPU usage, temperatures while gaming
Scope: Compositor renders a lightweight HUD overlay above the game
Depends on: Overlay architecture (MVP)
Post-MVP Feature Dependencies
Wi-Fi
└── SSH Developer Mode
└── Dedicated Audio Service (BT)
└── Bluetooth
└── Cloud Saves
└── Store + Download Manager
└── OTA System Updates (on-device download)
Store + Download Manager
└── Signed .play Packages
└── Delta Updates
Input Service
└── Rear Buttons
└── Gyroscope
└── Haptics
Audio Service
└── Bluetooth Audio
└── Notifications over Games
Suspend/Resume
└── (requires AMD firmware validation — not a feature dependency)
Items Never Planned
These are explicitly out of scope for PlayOS, even post-MVP:
- General-purpose Linux desktop environment
- X11 / Xwayland
- Browser-based shell or WebAssembly runtime
- Multi-GPU or hybrid-graphics rendering
- Running multiple games simultaneously
- Cloud gaming (streaming from remote server)
- Custom GPU driver or OpenGL implementation
Networking Options — Wi-Fi & Bluetooth
Status: Design analysis (pre-ADR). Networking (Wi-Fi) is scoped as Sprint 16 (post-MVP); Bluetooth remains post-MVP with no sprint. Cross-references: roadmap.md §Post-MVP, post-mvp.md, architecture.md §14, runtime-ipc.md, kernel-config.md, security-model.md §11
1. Context
PlayOS deliberately ships with no network stack in MVP:
kernel-config.mddefersCFG80211/MAC80211/MT7921Eand the Bluetooth subsystem.architecture.md§14 lists "Wi-Fi, Bluetooth, SSH, cloud saves" as post-MVP.- The production image contains no BusyBox (dev/diagnostic only —
security-model.md§11), no D-Bus, and no open TCP/UDP sockets (Sprint 12).
The central question is therefore not which Wi-Fi daemon to use, but whether introducing D-Bus is acceptable. Both iwd and BlueZ hard-depend on D-Bus, while PlayOS's architecture mandates a strict one-mechanism-per-layer IPC model (see §3).
2. Options Summary
| Option | Stack | D-Bus? | BusyBox? | Effort | Fit |
|---|---|---|---|---|---|
| A | iwd + private dbus-broker | Yes (private bus) | No | Medium | Fits only as a contained private bus |
| B | wpa_supplicant + dhcpcd + playos-net bridge | No | No | Medium | Best architectural fit |
| C | Custom nl80211 supplicant | No | No | Very high | Not viable |
3. The D-Bus Problem
PlayOS owns IPC by layer:
playos-runtimeowns all internal IPC —control.sock,compositor.sock, and the lifecycle fd (length-prefixed JSON frames over Unix sockets).libplayos(playos-platform-api) owns the only public ABI.- Games are never in
playos-trustedand cannot reach any privileged endpoint.
A system-wide D-Bus bus is a second, parallel internal IPC mechanism, which violates the rule that "private IPC definitions live only in playos-runtime" and sits alongside the explicit non-goals (systemd, desktop machinery). D-Bus is also a large, historically attack-surface-heavy component in a system that prioritises a small trusted surface.
Conclusion: D-Bus is unacceptable as a general-purpose or game-visible bus. It is tolerable only as a private, contained implementation detail between trusted daemons — the same trust boundary as the existing control sockets.
4. Reusing the Existing IPC as the Control Plane
A common question: PlayOS already has an IPC mechanism (control.sock / compositor.sock) — can it be used for networking instead of introducing anything new?
Yes — but transport and implementation are distinct:
| Concern | Owner |
|---|---|
| Carry messages between trusted components | playos-runtime (control.sock) — already exists |
Actually do Wi-Fi (scan, associate, WPA2/WPA3 4-way handshake over nl80211) | wpa_supplicant (or iwd) — a daemon |
These are orthogonal. playos-runtime cannot speak nl80211 without reimplementing a supplicant (Option C), so a supplicant daemon is still required underneath. The IPC mechanism is the control plane; the supplicant is the engine.
The bridge/glue: wpa_supplicant has its own control protocol over its own private socket (libwpa_client/wpa_ctrl). Something must translate between that and PlayOS's JSON frames. The glue lives in one of two places:
- A dedicated
playos-netdaemon (recommended) — linkslibwpa_client, exposes newplayos-runtimemessages (Scan,Connect,Status) oncontrol.sock. Matches theplayos-netnaming inpost-mvp.mdand keepsplayos-initscoped to supervision (architecture.mdalready says init "Does NOT own: network"). - Folded into
playos-init— one fewer daemon, but muddies init's supervision charter.
Result with wpa_supplicant (Option B): D-Bus disappears entirely. The only sockets are the existing control.sock/compositor.sock plus wpa_supplicant's and dhcpcd's private sockets — all root:playos-trusted 0660, invisible to games. The shell already sends LaunchGame over control.sock; it would send Connect the same way.
5. Option A — iwd + private D-Bus
iwd is modern, minimal, has cleaner WPA3 handling, and bundles its own DHCP client (so it needs no separate client and no BusyBox). But it is D-Bus-only — there is no non-D-Bus build.
To make it fit, D-Bus would be scoped as follows:
- Run
dbus-broker(systemd-independent, minimal) on a private socket, ownedroot:playos-trusted, mode0660. iwdruns as a trusted daemon. Games are not inplayos-trusted, so they never see the bus.- The shell talks to
iwdthrough newplayos-runtimecontrol messages (e.g.Scan,Connect,Status), not raw D-Bus.
| Pros | Cons |
|---|---|
| Modern, minimal, fast roaming | Introduces D-Bus at all |
| Bundled DHCP client | Second internal IPC mechanism (even if private) |
| Better WPA3 (SAE) support | More moving parts (dbus-broker + policy) |
6. Option B — wpa_supplicant + dhcpcd (D-Bus-free) — recommended
wpa_supplicant talks to the kernel over nl80211 and exposes a plain Unix socket control interface. It can be built with D-Bus entirely disabled:
CONFIG_CTRL_IFACE=unix # Unix socket control interface
CONFIG_CTRL_IFACE_DBUS=n # no D-Bus dependency
The stack:
wpa_supplicant— association, WPA2/WPA3 (SAE via the in-tree hostapd/wpa_supplicant code), overnl80211. No D-Bus.dhcpcd— standalone DHCPv4/DHCPv6 + IPv4LL client (BuildrootBR2_PACKAGE_DHCPCD). No BusyBox, no D-Bus.playos-netbridge — a thin trusted daemon that reads wpa_supplicant's control socket and re-exposes it as newplayos-runtimecontrol messages, so the shell never talks to wpa_supplicant directly and games stay isolated.
The wpa_supplicant control socket becomes just another root:playos-trusted 0660 socket under /run/playos/, mirroring control.sock and compositor.sock.
| Pros | Cons |
|---|---|
| Zero D-Bus — matches PlayOS philosophy | Older, more config-heavy than iwd |
Same Unix-socket transport as playos-runtime | Needs a separate DHCP client (dhcpcd) |
| Battle-tested in minimal embedded (OpenWrt, Alpine, Buildroot) | SAE/WPA3 support is present but less polished than iwd |
7. Option C — Custom nl80211 supplicant (rejected)
Writing a minimal Wi-Fi manager that talks nl80211 directly avoids all daemons, but re-implements the WPA2/WPA3 4-way handshake, EAP, and crypto. This is weeks of correctness- and security-critical work for zero user-visible benefit. Rejected.
8. Bluetooth
Bluetooth is the harder case: BlueZ is D-Bus-only, and there is no D-Bus-free alternative for modern controller/audio use.
- Basic HID pairing (controllers, keyboards) still requires BlueZ → D-Bus.
- Bluetooth audio requires the post-MVP dedicated audio service (
post-mvp.mdTier 2), so BT is correctly deferred regardless.
Two paths:
| Path | Implication |
|---|---|
Land Wi-Fi D-Bus-free now (Option B); introduce a private dbus-broker later when BT lands | Wi-Fi ships without D-Bus; BT justifies the single private bus later |
| Accept private D-Bus up front (Option A) for both | One IPC addition amortised across Wi-Fi + BT, at the cost of D-Bus presence earlier |
Either way, the D-Bus bus — if introduced for BlueZ — must be scoped exactly as in §5: private, root:playos-trusted, invisible to games.
9. Kernel & Firmware
# Wi-Fi (post-MVP enablement)
CONFIG_CFG80211=y
CONFIG_MAC80211=y
CONFIG_MT7921E=y # AMD RZ616 (rebranded MediaTek MT7922) on ROG Ally
CONFIG_RFKILL=y
# Bluetooth (post-MVP enablement)
CONFIG_BT=y
CONFIG_BT_LE=y
CONFIG_BT_HCIBTUSB=y # MT7922 exposes BT over USB
CONFIG_BT_RFCOMM=y # HID profile
Firmware: MediaTek mt7921/mt7922 Wi-Fi and Bluetooth blobs are redistributable via linux-firmware — unlike AMD GPU blobs, no manual sourcing from an existing install is required.
10. Recommendation
- Wi-Fi: Option B —
wpa_supplicant(D-Bus-free) +dhcpcd+ a trustedplayos-netbridge. It lands networking with zero D-Bus and zero BusyBox, fully consistent with the existing IPC model. - Bluetooth: defer. When it lands, introduce a private
dbus-brokerscoped to the trusted zone for BlueZ — the one subsystem that genuinely requires D-Bus.
Decision: Option B — wpa_supplicant (D-Bus-free) + dhcpcd + a trusted playos-net bridge — is the chosen Wi-Fi stack, decomposed as Sprint 16. A formal ADR is still recommended (e.g. "ADR-0009 — Wi-Fi stack: wpa_supplicant over iwd to avoid D-Bus").
Work package: the chosen stack is decomposed as Sprint 16 — playos-net.
ROG Ally Input Handling — Architecture Audit & Responsiveness Assessment
Status: Architecture audit. The prioritised enhancements (§8 items 1–4, 6) are now implemented in playos-shell and playos-platform-api; item 5 (reserved-key semantics) still needs a product decision. See the "Implemented" markers in §7/§8.
Goal: Document the end-to-end input pipeline, how input reaches the shell, and what needs to change to make the shell feel responsive "to the bone". This supersedes the Sprint 9 retest findings; those are preserved verbatim in the Appendix at the bottom.
1. Input pipeline overview
Input travels through two parallel paths from the kernel to the UI. They are deliberately separate because they serve different consumers:
Linux kernel evdev
/dev/input/eventN (opened O_RDONLY | O_NONBLOCK | O_CLOEXEC)
│
├──▶ (A) GAME PATH — platform-api backend
│ backend_evdev.c opens 3 fds:
│ • gamepad (EV_ABS + EV_KEY + ABS_X/Y/RX/RY + BTN_SOUTH)
│ • home (BTN_MODE, no BTN_SOUTH)
│ • vendor (KEY_PROG1/PROG2 or BTN_TRIGGER_HAPPY1/2)
│ → playos_input_get_controller_state()
│ strips SYSTEM|QUICK_MENU|POWER from the snapshot
│
│ → Raylib rcore_playos.c PlayOSPollGamepad()
│ fills CORE.Input.Gamepad.ready[0]
│ omits reserved buttons, maps sticks 1:1,
│ converts triggers [0,1] → [-1,1]
│
│ → Raylib EndDrawing() → PollInputEvents()
│ (refreshes the above at end of each rendered frame)
│
└──▶ (B) SHELL PATH — direct evdev (trusted)
input.c opens the gamepad fd PLUS every reserved node
(home / vendor / power) and decodes all of it itself.
Reserved buttons are KEPT here, never given to games.
Games read only path (A). The shell reads path (B) for buttons, reserved
keys, AND stick/trigger axes — all decoded from evdev on one fresh frame
(see §6). The previous Raylib axes overlay was removed to eliminate the
one-frame stick lag.
The key contract point is playos-platform-api/include/playos/playos_input.h:
playos_button_mask_tbitmask: SOUTH(A)1<<0, EAST(B)1<<1, WEST(X)1<<2, NORTH(Y)1<<3, START1<<4, SELECT1<<5, SYSTEM1<<6(reserved), QUICK_MENU1<<7(reserved), DPAD_UP/DOWN/LEFT/RIGHT1<<8..11, L11<<12, R11<<13, L31<<14, R31<<15, POWER1<<16(reserved).PlayOSAxisenum: LEFT_X=0, LEFT_Y=1, RIGHT_X=2, RIGHT_Y=3, LEFT_TRIGGER=4, RIGHT_TRIGGER=5, COUNT=6. Y up is negative; sticks are[-1,1], triggers are[0,1].PlayOSControllerState { buttons; float axes[6]; uint64_t timestamp_us; }.playos_input_controller_connected()andplayos_input_get_controller_state()(returns0on success,-1when no controller).
2. Layer 1 — evdev device topology
On the ROG Ally, input is spread across several /dev/input/event* nodes. The gamepad node carries the standard controller controls; reserved keys (Home / Command Center / volume / M1-M2) and the hardware Power/Sleep buttons arrive on separate nodes.
The shell's discovery is in playos-shell/src/input.c:
| Function | Role |
|---|---|
is_gamepad_device() (input.c:56) | Requires all four stick axes (ABS_X/Y/RX/RY) plus BTN_SOUTH. Intentionally does not require d-pad capability — hid-asus reports d-pad events without advertising the bits. |
find_gamepad_device() (input.c:92) | Scans /dev/input/event*, prefers names containing Xbox/X-Box/Microsoft/ASUE/ASUS/ROG Ally/Gamepad. |
is_reserved_home_device() (input.c:178) | BTN_MODE present, BTN_SOUTH absent. |
is_reserved_vendor_device() (input.c:189) | KEY_VOLUMEUP/DOWN, KEY_PROG1/2 or BTN_TRIGGER_HAPPY1/2 present, and neither BTN_SOUTH nor BTN_MODE. |
is_reserved_power_device() (input.c:213) | KEY_POWER or KEY_SLEEP present, and no BTN_SOUTH/BTN_MODE. |
shell_input_open_reserved_nodes() (input.c:251) | One full scan; opens every home/vendor/power node exactly once (this is the fix for the historical duplicate-volume-node bug). |
Platform API performs its own independent discovery in playos-platform-api/src/backends/backend_evdev.c with the same gamepad capability test, plus separate home and vendor fds.
3. Layer 2 — platform-api backend (the game path)
File: playos-platform-api/src/backends/backend_evdev.c.
- Opens three fds, all
O_RDONLY | O_NONBLOCK | O_CLOEXEC:evdev_fd— the gamepad.home_fd— BTN_MODE without BTN_SOUTH (Home).vendor_fd— KEY_PROG1/PROG2 or BTN_TRIGGER_HAPPY1/2 (Command Center / Armoury).
BUTTON_MAP[]maps BTN_SOUTH/EAST/WEST/NORTH/START/SELECT/MODE/THUMBL/THUMBR/TL/TR plus KEY_PROG1→SYSTEM, KEY_PROG2→QUICK_MENU, BTN_TRIGGER_HAPPY1→SYSTEM, BTN_TRIGGER_HAPPY2→QUICK_MENU. D-pad is not in the button map; it is handled only via ABS_HAT0X/Y.AXIS_MAP[]: ABS_X/Y/RX/RY → sticks; ABS_Z→LEFT_TRIGGER; ABS_RZ→RIGHT_TRIGGER.normalize_stick()centers and applies a 0.05 deadzone with no post-deadzone rescale.normalize_trigger()maps[min,max] → [0,1], with max detected viaEVIOCGABS(ABS_Z)and a 255 fallback.drain_fd()caps atMAX_EVENTS_PER_CALL 64events per non-blocking read.- Discovery is throttled:
RESCAN_INTERVAL_US 2000000(2 s) and stale-fd re-scan viafcntl(fd, F_GETFL).
playos-platform-api/src/playos_input.c is a thin dispatcher to backend_evdev_get_controller_state(), then masks out SYSTEM|QUICK_MENU|POWER from the state games see. That is the security boundary: games can never observe reserved buttons through the platform API.
4. Layer 3 — Raylib gamepad backend
File: playos-shell/external/raylib/src/platforms/rcore_playos.c.
PlayOSPollGamepad()uses gamepad index0, callsplayos_input_get_controller_state(&state), and setsCORE.Input.Gamepad.ready[0].- Buttons are mapped, omitting SYSTEM / QUICK_MENU / POWER (they are already stripped upstream, but the backend omits them defensively).
- Sticks are copied 1:1 into Raylib's
axisState[LEFT_X/Y/RIGHT_X/RIGHT_Y]. - Triggers are converted
state.axes[..] * 2 - 1from platform[0,1]to Raylib[-1,1]. PollInputEvents()(inrcore.c) callsPlayOSPollGamepad(), and is invoked fromEndDrawing()at the end of each frame whenSUPPORT_CUSTOM_FRAME_CONTROLis not defined.
Consequence: Raylib's gamepad state is a frame-behind snapshot relative to when the shell next reads it (see §6).
5. Layer 4 — the shell's direct evdev path (trusted)
File: playos-shell/src/input.c.
shell_input_init() (around input.c:568) performs:
/proc/bus/input/devicesdump to the persistent log (shell_input_dump_proc_devices,input.c:326).- Per-node capability dump (
shell_input_dump_capabilities,input.c:355). - Installs a non-blocking
inotifywatch on/dev/input(IN_CREATE|IN_DELETE|IN_ATTRIB) for gamepad hotplug — best-effort, ignored if unavailable. find_gamepad_device().shell_input_open_reserved_nodes().- One-time trigger and stick calibration reads.
Per frame, shell_input_poll() (input.c:865) does:
- Saves
controller_prev, resetsbuttons_pressed. - Drains any pending
inotifyevents; if the gamepad fd is still missing, retries discovery immediately instead of waiting out the throttle. - If the gamepad fd is still missing, retries discovery at most once every
SHELL_INPUT_RESCAN_INTERVAL_SECONDS= 2 s (input.c:863). Reserved nodes are not re-scanned — a deliberate fix, because a fullopendir + open + 2× ioctlscan costs ~0.5 s on the Ally and caused visible hiccups (input.c:899-906). - Drains the gamepad fd, then every reserved fd (
input.c:911-915).
shell_input_drain_fd() (input.c:815) reads events non-blocking and, crucially, breaks on EV_SYN — it processes exactly one kernel input frame per poll. That is the right latency behaviour: it keeps the shell glued to the most recent frame and never replays stale batched events.
shell_input_process_event() (input.c:646) decodes:
- EV_KEY face buttons, d-pad (
ABS_HAT0X/YandBTN_DPAD_*forms), L1/R1, stick clicks. - Reserved keys: SYSTEM / QUICK_MENU / POWER / volume / M1-M2 (these stay in
s->controller.buttons). - EV_ABS sticks (ABS_X/Y/RX/RY →
normalize_stick, 5% deadzone) and triggers (ABS_Z/RZ).
Queries:
shell_input_button_pressed()(input.c:918) — edge detect plus an event-level catch so fast taps that resolve within a single poll are not lost.shell_input_button_released()(input.c:944) — falling edge.shell_input_button_held()(input.c:955) — level query ("IsBeingPressed"). The Live Input Test uses this for every pill.
6. How a 60 Hz frame flows
File: playos-shell/src/main.c.
SetTargetFPS(60)
loop:
1. shell_input_poll(s) → s->controller.buttons AND s->controller.axes
(fresh evdev: buttons, sticks, triggers,
reserved keys — one frame, no lag)
2. lifecycle / update screen → uses s->controller
3. draw screen
4. render_end_frame() → EndDrawing() → PollInputEvents()
→ PlayOSPollGamepad() refreshes Raylib state
5. s->frame_time = time after EndDrawing()
Buttons and sticks now share the same fresh evdev frame in step 1. The previous
shell_read_gamepad_axes() Raylib overlay (step 2 in the earlier revision) was
removed because it read the Raylib snapshot produced at the end of the previous
frame, introducing a ~16.7 ms stick lag and a second (10%) deadzone. Raylib's
gamepad state is still refreshed at the end of each frame for any future
Raylib-side consumers, but the shell no longer uses it for its own state.
The FPS HUD in screen_home.c now reports 1.0 / frame_time instead of milliseconds.
7. Responsiveness assessment
Strengths
- All evdev fds are non-blocking; nothing can stall the render loop on a blocking read.
- The shell drains one kernel frame per poll (
input.cbreaks on EV_SYN) — no stale event replay. - Platform API caps its own drain at 64 events/call.
SetTargetFPS(60)gives a stable cadence;EndDrawing()does the frame pacing.- Reserved-button reads are on the same fresh per-frame path as game buttons (no frame lag there).
- Missing-device retries are throttled, and the expensive full scan was removed from the hot path.
Problems / gaps
-
One-frame stick lag (primary).
Sticks and triggers enter via Raylib, which refreshes atImplemented: the Raylib axes overlay was removed;EndDrawing()— one frame after the shell reads it (main.cordering).shell_input_poll()now decodes sticks/triggers from evdev on the same fresh frame as buttons. -
Deadzone inconsistency.
The shell's direct evdev stick decode uses a 5% deadzone, while the Raylib overlay uses 10%.Implemented: the overlay is gone and the singleSHELL_STICK_DEADZONE 0.05fconstant ininput.cgoverns all shell stick decoding, matching the platform-api backend. -
Two independent opens of the same nodes. Platform API and the shell each open their own fds and drain the gamepad independently. This is by design (trust boundary), but it means duplicate reads and a risk of divergence if the two decoders ever disagree. Documenting it as intentional is fine; keeping the two decoders byte-for-byte compatible is the real cost.
-
Hotplug latency up to 2 s.
Both platform API and shell throttle missing-device re-scan to 2 s.Implemented for the shell: aninotifywatch on/dev/inputtriggers an immediate gamepad re-scan on CREATE/DELETE, while the 2 s throttle remains as a fallback. The platform-api backend still throttles at 2 s (acceptable for game processes). -
Reserved keys are momentary pulses. Home/Command/M1-M2 arrive as 7–9 ms
value=1→value=0pulses with no autorepeat (historical Finding 3). The shell already latches them visually; semantic actions must be edge-triggered, never level-triggered, for those codes. Still open — needs a product decision on the intended Armoury/Command semantics. -
No startup "controller present" snapshot for reserved nodes.
The capability dump exists… the remaining gap is ensuring the game-facing backend also logs its chosen fds at discovery time for cross-correlation.Implemented:open_controller()inbackend_evdev.cnow logs the chosen gamepad fd and device name (both preferred and fallback paths), alongside the existing home/vendor fd logs. -
Timestamping is not surfaced.
Implemented (opt-in):PlayOSControllerState.timestamp_usexists but the shell's own path does not timestamp frames.shell_input_drain_fd()measures queue-to-drain age usingclock_gettime(CLOCK_MONOTONIC)vs the kernelinput_event.time, logged once per second whenPLAYOS_INPUT_LATENCY_LOGis set. Off by default to avoid per-event overhead on the 60 Hz hot path.
8. Recommended enhancements (prioritised)
-
De-lag the shell stick path.
Read Raylib axes afterImplemented: sticks are now read from evdev directly inEndDrawing()/PollInputEvents(), or read sticks from evdev directly…shell_input_poll(); the Raylib overlay was removed. -
Unify deadzone handling.
One constant, one function, used by both the shell evdev decode and the Raylib overlay…Implemented: singleSHELL_STICK_DEADZONE 0.05fconstant; the Raylib overlay no longer exists to disagree. -
Measure end-to-end input latency.
Add monotonic timestamps at evdev drain, platform snapshot, Raylib poll, and frame start; log or expose them behind a debug flag.Implemented (partial, opt-in): evdev queue-to-drain age is measured and logged once per second whenPLAYOS_INPUT_LATENCY_LOGis set. Full cross-layer (platform snapshot → Raylib → frame start) instrumentation is still a future extension if deeper profiling is needed. -
Replace the 2 s gamepad re-scan with inotify on
/dev/input(or at least shorten the throttle after a successful boot).Keep reserved-node discovery one-time as today.Implemented: inotify immediate re-scan for the gamepad; reserved-node discovery remains one-time. -
Reserved-key semantic handling.
Confirm the intended Armoury/Command semantics for the currently-unmapped F15/F17 codes and wire them as edge-triggered actions; never depend on a held state for pulse-only codes.Still open — needs a product decision. No default mappings were invented. -
Cross-correlate both decoders.
Log the platform-api backend's chosen gamepad/home/vendor fds at startup next to the shell's dump…Implemented:open_controller()logs the gamepad fd + name (preferred and fallback); home/vendor fds were already logged. -
Keep one kernel frame per poll. This is already correct; protect it in any refactor. Do not "drain all" in the shell hot path — that reintroduces stale-event latency. Unchanged and preserved.
Appendix — Sprint 9 retest findings
Historical context. These were the findings from the Ally USB image retest after the volume-node discovery + HOME/COMMAND latch changes. Several are now fixed (volume node, power button); the pulse-behaviour notes remain relevant.
Discovered input topology (from shell logs, at the time)
- Gamepad:
Microsoft X-Box 360 padat/dev/input/event5. - Vendor node:
Asus Keyboardat/dev/input/event8(fd=9). - Three
Asus Keyboardnodes skipped by the gamepad matcher as "missing stick axes":event6,event7,event8. - The volume-node matcher selected
event8again (fd=10) — a duplicate of the vendor node.
Finding 1 — Volume node discovery found a duplicate, not the real volume node
found vendor node: 'Asus Keyboard' (/dev/input/event8) fd=9
scanning /dev/input/event* for volume node...
found volume node: 'Asus Keyboard' (/dev/input/event8) fd=10
event8 was opened twice and drained twice; event6/event7 were never opened. Fixed by shell_input_open_reserved_nodes() opening every Asus/home/vendor/power node exactly once.
Finding 2 — Volume Up / Volume Down never reach the shell
No KEY_VOLUMEUP (0x72) / KEY_VOLUMEDOWN (0x73) events were observed. Only PROG1 (0x94), F15 (0xb9), F16 (0xba), F17 (0xbb), F18 (0xbc) on event8. Fixed — volume keys now arrive and light the Live Input Test.
Finding 3 — HOME and COMMAND are momentary pulses, not held keys
Every 0x94 (HOME) and 0xba (COMMAND) press is a 7–9 ms value=1→value=0 pulse with no autorepeat. Same for F17/F18. The shell's 0.6 s visual latch is the correct accommodation; semantic actions must be edge-triggered.
Finding 4 — F15 behaves differently (level + autorepeat)
0xb9 (F15) emits value=1, then value=2 repeats (~260 ms, then ~40 ms), then value=0. F15 is a real held key, unlike the pulse keys.
Finding 5 — Persistent logs did not enumerate input devices
init.log had no /proc/bus/input/devices dump. Fixed — shell_input_dump_proc_devices() now logs it, plus shell_input_dump_capabilities() logs per-node names/phys/interesting evbits.
Finding 6 — shell-stderr.log contained two boot cycles
The persistent log is append-across-reboots (expected). Findings were read against the second boot's ~41 s mark.
Finding 7 — Power button detection implemented (Sprint 9 follow-up)
KEY_POWER (0x74) and KEY_SLEEP (0x8e/142) on the ACPI Power/Sleep nodes are now decoded into PLAYOS_BUTTON_POWER (bit 16, reserved — games never see it). The shell discovers the power node via is_reserved_power_device(), opens it without EVIOCGRAB so kernel ACPI handling is unaffected, and shows a PWR pill in the Live Input Test with the 0.6 s pulse latch. shell_input_button_held() was added and the Live Input Test now uses it for every pill. Status: implemented; compiles and builds, awaiting Ally hardware retest.
ADR-0001 — Six-Repository Structure
Date: Sprint 0
Status: Accepted
Deciders: PlayOS core team
Context
PlayOS requires multiple distinct components: a PID 1 process supervisor, a Wayland compositor, a shell, a public game API, an internal runtime, and a build system. The question is how to organize this code — monorepo vs separate repositories.
Decision
Adopt six separate repositories with strict ownership and dependency direction:
| Repository | Owns |
|---|---|
playos-spec | Architecture, contracts, ADRs, schemas, roadmap |
playos-platform-api | Public libplayos C ABI |
playos-runtime | Internal IPC and lifecycle transport |
playos-compositor | wlroots compositor |
playos-shell | Raylib controller shell |
playos-refdistro | Buildroot integration and images |
Rationale
- Dependency enforcement: Separate repos make it impossible to accidentally import private internals from
playos-runtimeinto a game (the game can only depend onplayos-platform-api) - Independent versioning: The public API (
playos-platform-api) can be versioned independently from internal transport changes - Clear ownership: Each repo has a single responsible area; contributors know where their change belongs
- Parallel work: Teams can work on compositor and shell independently without merge conflicts
Consequences
- More overhead for cross-repo changes (PRs in multiple repos must be coordinated)
versions.lockinplayos-refdistrois required to keep all components pinned- CI must test integration across repos
ADR-0002 — Unix Socket IPC Transport
Date: Sprint 1
Status: Accepted
Deciders: PlayOS core team
Context
playos-init needs a way to receive commands from trusted clients (shell, overlay) and send events back. Options considered: D-Bus, Varlink, gRPC, plain Unix sockets, netlink.
Decision
Use Unix domain sockets with a simple length-prefixed binary frame and JSON-encoded message bodies. See runtime-ipc.md for the full protocol.
Rationale
- No daemon dependency: No D-Bus daemon, no session bus, no activation —
playos-initis the only IPC server needed - Minimal dependencies: A Unix socket needs only the kernel — no extra libraries required in the initramfs
- Access control: UNIX group permissions (
playos-trusted) enforce that only trusted clients can connect — no capability negotiation needed - Simplicity: Length-prefix + JSON is readable during debugging, easy to implement in C, and easy to test with
socator a simple Python script - Sufficient performance: IPC volume is low (a few messages per user action) — protocol overhead is irrelevant
Alternatives Considered
| Option | Rejected because |
|---|---|
| D-Bus | Requires dbus-daemon; adds systemd/activation complexity; not suitable for PID 1 |
| Varlink | Good fit but less widely known; minimal tooling advantage |
| gRPC | Heavy dependency (protobuf, HTTP/2); overkill for this use case |
| Netlink | Kernel-space complexity; not suited for user-space control commands |
Consequences
- A custom framing protocol must be maintained
- JSON adds a parsing dependency (use a small embedded parser like
cJSONoryyjson) - Binary framing must be carefully tested for partial reads
ADR-0003 — musl libc Only
Date: Sprint 0
Status: Accepted
Deciders: PlayOS core team
Context
The system needs a C library. Options: glibc, musl, uClibc-ng. The choice affects binary size, compatibility, and which packages can be built.
Decision
Use musl libc exclusively. No glibc support is planned for v1.
Rationale
- Size: musl produces significantly smaller binaries than glibc — critical for an embedded initramfs
- Static linking: musl supports full static linking with predictable behavior; glibc static linking has well-known edge cases (NSS, DNS)
- Reproducibility: musl is simpler and more deterministic; easier to reason about at the ABI level
- Buildroot support: Buildroot's musl toolchain is mature and widely used
- Security: Smaller attack surface; musl's allocator is not susceptible to some classic glibc heap exploits
Alternatives Considered
| Option | Rejected because |
|---|---|
| glibc | Larger; more complex; static linking edge cases; overkill for a console OS |
| uClibc-ng | Less maintained; fewer compatible packages; musl is the better modern choice |
Consequences
- Some packages assume glibc internals and may require patches (Mesa, wlroots are well-tested with musl)
- The public
libplayosC ABI must be compatible with musl — no glibc-specific extensions - The
PLAYOS_API_VERSIONcompatibility policy must note the musl dependency - Games targeting PlayOS must link against musl or be statically linked
ADR-0004 — wlroots as Compositor Foundation
Date: Sprint 2
Status: Accepted
Deciders: PlayOS core team
Context
PlayOS needs a Wayland compositor. Options: implement from scratch using raw libwayland, use wlroots, use a full compositor (Sway, Wayfire) as a base.
Decision
Use wlroots as the foundation for playos-compositor. wlroots provides mechanisms; PlayOS supplies console policy.
Rationale
- DRM/KMS abstraction: wlroots handles the complex DRM backend initialization, output management, and KMS atomicity
- Proven: wlroots is used in production compositors (Sway, Hyprland, Cage) — it is well-tested on real hardware including AMD GPUs
- Not a full compositor: wlroots is a library, not a complete compositor — PlayOS defines all policy (focus, z-order, trusted roles, state machine) on top of it
- Active maintenance: Regular upstream development; AMD and Intel DRM paths are kept up to date
- ROG Ally compatibility: Sway and Cage are known to work on the ROG Ally's AMDGPU — wlroots underlying both is validated hardware
Alternatives Considered
| Option | Rejected because |
|---|---|
| Raw libwayland | Would require reimplementing all DRM/KMS, buffer management, and Wayland protocol infrastructure that wlroots provides — massive scope increase |
| Sway as a base | Sway is a tiling window manager; its policy (floating/tiled windows, workspaces) would have to be removed entirely — more work than starting from wlroots |
| Wayfire | Plugin architecture is more complex than needed; wlroots is a cleaner starting point |
| Mutter / KWin | Heavy GNOME/KDE dependencies; incompatible with musl and minimal initramfs |
Guiding Rule
wlroots implements mechanisms;
playos-compositorimplements console policy; Raylib renders what the player sees.
Consequences
- wlroots version must be pinned in
versions.lock - wlroots API is not stable across versions — compositor code may need updates when wlroots is bumped
- Private PlayOS Wayland protocol is layered on top of wlroots, not replacing its core protocols
ADR-0005 — RAUC for A/B System Updates
Date: Sprint 11
Status: Superseded — resolved to a minimal custom updater (Sprint 11)
Deciders: PlayOS core team
Context
PlayOS needs a signed, atomic A/B update mechanism. Options: RAUC, Mender, SWUpdate, custom EFI-image-specific updater.
Decision
Use a minimal custom updater in playos-init, not RAUC. The decision criteria below resolved to custom because the PlayOS system image is a single EFI/squashfs artifact, and the static musl PID 1 cannot carry OpenSSL/PKCS#11.
Resolution (Sprint 11): .playosb bundle = [PBS1][LE32 header_len][JSON header][raw squashfs][LE32 sig_len][hex HMAC-SHA256], verified with a development HMAC key before any partition write. Production key management (HSM) and dm-verity remain post-MVP.
Rationale
Why RAUC:
- Designed for A/B embedded Linux updates — good conceptual match
- Supports signed bundles (OpenSSL/PKCS#11), boot slot management, and rollback
- Buildroot has a
raucpackage - Used in production by several embedded Linux projects
Why evaluate a custom approach:
- PlayOS uses a simple EFI artifact model (not a traditional rootfs tarball)
- RAUC's slot model may require a custom handler for EFI-image slots
- A custom updater that just: verifies a signature, writes to the inactive EFI slot, updates
boot.json, and reboots might be simpler and smaller - Less code = smaller attack surface in the update path
Decision Criteria for Custom vs RAUC
Prefer RAUC if:
- RAUC can handle EFI-image slots with minimal custom handler code
- The bundle signature infrastructure (key management) integrates cleanly with PlayOS signing
Prefer a custom updater if:
- RAUC requires more than 200 lines of custom handler code for EFI slots
- The total update binary size for RAUC exceeds 2 MB in the initramfs
Alternatives Considered
| Option | Notes |
|---|---|
| Mender | SaaS-oriented; heavier client; less embedded-only focused |
| SWUpdate | Good alternative to RAUC; very similar feature set |
| Custom | Simplest for EFI-image model; no external dependencies; more code to maintain |
Consequences
- RAUC or the custom updater must be integrated into
playos-refdistroand tested for full A/B cycle - Update bundle signing key management must be designed before the first production release
- The choice must be finalized before Sprint 14 (production readiness)
ADR-0006 — Raylib for Shell and Game UI
Date: Sprint 5
Status: Accepted
Deciders: PlayOS core team
Context
playos-shell, playos-overlay, and games need a rendering framework. Options: Raylib, SDL2+OpenGL, raw OpenGL ES, Qt, GTK, custom engine.
Decision
Use Raylib as the rendering framework for the shell, overlay, and recommended game development. Implement a custom Raylib PlayOS backend (rcore_playos.c) that integrates with the Wayland/EGL surface and the libplayos lifecycle API.
Rationale
- Game-developer-friendly: Raylib is a beginner-to-intermediate game framework with a clean C API — matches the target game developer audience
- Wayland support: Raylib already has Wayland/EGL support — the PlayOS backend extends this rather than starting from scratch
- Minimal dependencies: Raylib has very few external dependencies; works well in a musl/Buildroot environment
- Console-appropriate: Raylib is designed for fullscreen games — no window management, no decorations, no desktop assumptions
- C ABI compatible: Raylib's C API is compatible with the
libplayosC ABI philosophy - Active community: Regular upstream releases; ROG Ally and AMDGPU-based hardware known to work with Raylib/Wayland
Wayland Backend Approach
Rather than using Raylib's generic Wayland backend as-is, PlayOS implements rcore_playos.c which:
- Creates a fullscreen
xdg_toplevelwith trusted role environment variables - Integrates
playos_lifecycle_poll()into the Raylib frame loop - Keeps controller input shell-owned (direct evdev,
src/input.c) — Raylib is rendering-only and itsPollInputEvents()just resets internal input state - Disables desktop features (resize, decorations, clipboard, multi-window)
Alternatives Considered
| Option | Rejected because |
|---|---|
| SDL2 | Heavier; desktop-oriented features; PlayOS would need to strip a lot |
| Raw OpenGL ES | No windowing abstraction — more code to write for the shell UI |
| Qt | Very heavy; complex build; LGPL licensing concerns for static linking |
| GTK | Desktop-oriented; heavy; Wayland support has desktop assumptions |
| Godot | Full game engine is overkill for the shell; heavy binary size |
Consequences
- Games targeting PlayOS are recommended to use Raylib, but the
libplayosC ABI is engine-agnostic — SDL2 or other frameworks can be adapted - Raylib version must be pinned in
versions.lock - The
rcore_playos.cbackend must be maintained when Raylib updates change platform backend APIs - Multi-surface support (shell + overlay as one process) is limited by Raylib's single-surface design — the overlay is a separate process as a result (see [ADR for overlay architecture])
Follow-up (Sprint 5.5)
Raylib is now active for the shell. Sprint 5.5 vendored Raylib 6.0
into playos-shell/external/raylib and implemented
external/raylib/src/platforms/rcore_playos.c as the PLATFORM_PLAYOS
backend, replacing the shell's earlier direct EGL/GLES2 renderer. The shell
links the vendored static library when PLAYOS_SHELL_USE_RAYLIB=ON; the
raw-GLES2 path was retired. Raylib remains rendering-only — controller input
is still read directly from evdev by src/input.c so SYSTEM/QUICK_MENU
reserved buttons survive.
ADR-0007 — Direct ALSA for MVP Audio
Date: Sprint 8
Status: Accepted
Deciders: PlayOS core team
Context
PlayOS needs audio output. Options: PipeWire, PulseAudio, ALSA directly, a custom audio mixer.
Decision
Use ALSA PCM directly for the MVP. No PipeWire or PulseAudio in v0.1.0. A dedicated audio service is introduced post-MVP only when simultaneous mixing becomes necessary.
Rationale
- MVP scope: The MVP requires one foreground audio owner at a time (game while foreground, shell otherwise). This maps perfectly to a single exclusive ALSA PCM device — no mixer needed.
- Minimal dependencies: ALSA is built into every Linux system; no extra userspace daemon required in the initramfs
- Simpler lifecycle: Audio ownership follows the compositor lifecycle directly — game gets ALSA handle on foreground, releases it on background
- PipeWire complexity: PipeWire adds ~5MB to the image, requires a session manager, and introduces a latency and complexity budget that is not justified for one audio owner
- ROG Ally ALSA: The ROG Ally's AMD ACP audio works with ALSA;
snd_soc_acp*drivers are mature
Limitations Accepted
- Only one process can play audio at a time
- No system notification sounds over game audio
- No Bluetooth audio (post-MVP, requires audio service)
- No per-application volume (system volume only)
Migration Path
When post-MVP features require simultaneous audio (notifications over games, Bluetooth audio), a playos-audio service is introduced. The service:
- Owns the ALSA device exclusively
- Exposes a simple IPC for shell, overlay, and game audio streams
- Handles mixing in userspace
This migration does not break the playos_audio.h public API — the backend changes, the API stays the same.
Consequences
- The Raylib PlayOS backend must implement a direct ALSA PCM path
- Device selection, underrun handling, and headphone detection are implemented in
libplayos - Shell audio and game audio cannot overlap in the MVP
ADR-0008 — PCI Enumeration for GPU Selection
Date: Sprint 4
Status: Accepted
Deciders: PlayOS core team
Context
The compositor needs to select the correct DRM device. The simplest approach is to hardcode /dev/dri/card0. The correct approach is enumeration.
Decision
Always enumerate DRM devices and select by PCI vendor identity and active display connector. Never hardcode /dev/dri/card0 or any other device path.
Rationale
card0is not guaranteed: On systems with multiple DRM devices, or depending on driver load order, the integrated GPU may not becard0- Intel expansion (Sprint 13): Supporting Intel graphics requires that the compositor works without knowing the GPU vendor at compile time — enumeration is the only correct approach
- Future-proofing: If PlayOS ever supports dual-GPU setups (dGPU + iGPU), enumeration is required
- Correctness on real hardware: Tested on the ROG Ally, where
card0is currently correct, but this should not be relied upon
Selection Algorithm
For each DRM device in drmGetDevices2():
Resolve PCI vendor ID
Check if a connector on this device is connected to an active display
If connected:
Select this device
Break
If no connected device found:
Select first AMD (0x1002) device
Else select first Intel (0x8086) device
Else select first valid DRM device
Else fatal error
Log the selected device path, PCI ID, connector name, and preferred mode.
Consequences
- Compositor startup takes slightly longer (one DRM enumeration call)
- Code is more robust on all hardware configurations
- The selection algorithm must be tested on: single AMD GPU, single Intel GPU, and eventually multi-GPU setups
PCI_VENDOR_AMD = 0x1002andPCI_VENDOR_INTEL = 0x8086are defined in compositor code
PlayOS Architecture and Implementation Plan — Version 2.4
Document Purpose
This document defines the product architecture, runtime model, platform boundaries, build system, and implementation roadmap for PlayOS.
PlayOS is not intended to be a conventional Linux distribution. It is a console operating environment that uses the Linux kernel as an invisible hardware-enablement layer and presents a controller-first PlayOS experience from power-on to shutdown.
The initial reference device is the ROG Ally. The first supported graphics stack is AMDGPU with Mesa. PlayOS standardizes on musl and deliberately excludes desktop Linux components that are not required by the console experience.
Version 2.4 formalizes the existing playos-platform-api repository as the owner of the public, engine-agnostic libplayos API and C ABI. It narrows playos-runtime to internal lifecycle transport, launch and control IPC, private protocol definitions, and operating-system integration. It also requires compositor implementation code that currently exists under playos-runtime to migrate into the dedicated playos-compositor repository.
Part I — Product Definition
1. Executive Summary
PlayOS boots directly from UEFI into a small, immutable Linux system. A custom PID 1 process starts a purpose-built Wayland compositor based on wlroots. The compositor permanently owns DRM/KMS and launches the persistent PlayOS Shell as a trusted Wayland client. One game process may run at a time as another Wayland client.
The core runtime is:
UEFI firmware
|
v
Linux EFI-stub kernel
|
v
Embedded initramfs
|
+-- playos-init PID 1 and process supervisor
+-- playos-compositor wlroots compositor and display owner
+-- playos-shell persistent Raylib-powered Wayland client
+-- libplayos public C ABI from playos-platform-api
+-- playos-runtime internal IPC, lifecycle transport, OS integration
+-- Mesa / Wayland / ALSA platform libraries
+-- GPU firmware
|
v
One active game process
The operating system is immutable. Games, saves, resources, logs, downloads, and updates live on a separate writable data partition.
The final user experience should be indistinguishable from a dedicated console:
Power on
-> PlayOS Shell
-> Select game
-> Game becomes foreground
-> Press System button
-> PlayOS UI appears immediately
-> Resume or quit game
The recommended technical baseline is:
Upstream Linux LTS
Buildroot with a PlayOS br2-external tree
musl libc only
Linux EFI stub
embedded initramfs
custom playos-init
custom playos-compositor built on wlroots
Wayland
Raylib-powered playos-shell
playos-platform-api providing the public libplayos C ABI
playos-runtime providing internal IPC and lifecycle transport
DRM/KMS + GBM + EGL + OpenGL ES
Mesa RadeonSI on AMD
ALSA
Linux evdev/HID
ext4 data partition
signed A/B system updates later
2. Product Goals
PlayOS should:
- Boot directly into a controller-first shell with no visible Linux desktop or login screen.
- Keep the operating system small, immutable, reproducible, and recoverable.
- Use mature Linux drivers for GPU, audio, input, storage, power, and thermal management.
- Keep
playos-shellalive throughout the session so returning from a game is immediate. - Run one isolated game process at a time.
- Recover cleanly when a game crashes or becomes unresponsive.
- Allow trusted PlayOS overlays to appear above a running game.
- Reserve system controls that games cannot consume directly.
- Provide a stable, engine-agnostic PlayOS Platform API with an authoritative C ABI, plus Raylib and C++ convenience layers.
- Store games and user state separately from the system image.
- Support the ROG Ally first and add Intel graphics only after the AMD implementation is stable.
- Deliver a polished console experience rather than a general-purpose Linux environment.
3. First-Release Non-Goals
The first release does not require:
- A desktop environment.
- X11 or Xwayland.
- systemd.
- A display or login manager.
- Multiple interactive users.
- Containers.
- A conventional package manager.
- A browser-based shell.
- A custom Linux kernel written from scratch.
- A custom GPU driver or OpenGL implementation.
- Multiple simultaneous games.
- Multi-GPU or hybrid-graphics support.
- Wi-Fi, Bluetooth, SSH, cloud saves, or a store during early bring-up.
- Full suspend/resume during the first boot milestone.
- HDR, VRR, recording, or streaming during the MVP.
- Any libc baseline other than musl.
4. Architectural Principles
4.1 Linux is the hardware layer, not the product
Linux provides:
- CPU scheduling and virtual memory.
- Process isolation.
- EFI, ACPI, PCIe, and IOMMU support.
- AMDGPU and other device drivers.
- DRM/KMS.
- USB, HID, and evdev.
- ALSA audio.
- NVMe and filesystems.
- Battery, thermal, and power-management interfaces.
PlayOS provides:
- Boot policy.
- Console UI and navigation.
- Display and foreground policy.
- Game launching and supervision.
- Game lifecycle events.
- Reserved system controls.
- Storage and save-data conventions.
- Updates and recovery.
- Public game APIs.
Games should target PlayOS APIs rather than depend directly on Linux implementation details wherever practical.
4.2 The system image is immutable
The kernel, initramfs, compositor, shell, Platform API library, internal runtime libraries, firmware, and core assets form a versioned system image. Runtime writes go to the data partition.
This provides:
- Predictable boot behavior.
- Safe rollback.
- Easy factory reset.
- Fewer corruption paths.
- Reproducible releases.
- Independent game and system updates.
4.3 The process model stays minimal but protected
Games remain userspace processes. They are never linked into the kernel or compositor.
The minimum production model is:
playos-init
+-- playos-compositor
| +-- playos-shell
| +-- optional playos-overlay
|
+-- active-game
This gives games a clean address space and crash boundary while keeping the system extremely small.
4.4 The compositor owns display policy permanently
playos-compositor is the only process that owns DRM/KMS and display policy. The shell and games are Wayland clients.
This avoids shell-to-game DRM handoff and enables:
- Seamless transitions.
- Reliable crash recovery.
- Trusted overlays.
- Centralized input focus.
- Consistent orientation and output policy.
- Future direct scanout without changing the application model.
4.5 wlroots supplies mechanisms; PlayOS supplies console policy
wlroots provides reusable building blocks for DRM/KMS, Wayland, rendering, outputs, buffers, seats, and input.
PlayOS defines:
- Which client is trusted as the shell.
- Which surface is the active game.
- Which UI may appear above a game.
- Which controls are reserved by the system.
- How launch, pause, resume, exit, and crash transitions work.
- Which protocols are exposed to clients.
The guiding rule is:
wlroots implements mechanisms;
playos-compositorimplements console policy; Raylib renders what the player sees.
Part II — End-to-End Runtime Architecture
5. System Overview
+-----------------------------------------------------------+
| UEFI Firmware |
+-----------------------------+-----------------------------+
|
v
+-----------------------------------------------------------+
| PlayOS EFI Boot Artifact |
| |
| Linux EFI stub |
| Linux kernel |
| kernel command line |
| embedded initramfs |
+-----------------------------+-----------------------------+
|
v
+-----------------------------------------------------------+
| Linux Kernel |
| |
| EFI / ACPI / PCIe / IOMMU |
| scheduler / memory / processes |
| AMDGPU / DRM/KMS |
| USB / HID / evdev |
| ALSA |
| NVMe / ext4 / FAT |
| battery / thermal / power |
+-----------------------------+-----------------------------+
|
v
+-----------------------------------------------------------+
| PlayOS Runtime |
| |
| playos-init |
| playos-compositor + wlroots |
| playos-shell |
| optional playos-overlay |
| libplayos from playos-platform-api |
| playos-runtime internal transports and service clients |
| Wayland / Mesa / ALSA libraries |
+-----------------------------+-----------------------------+
|
v
+-----------------------------------------------------------+
| Persistent Data |
| |
| games / saves / resources / cache / logs / updates |
+-----------------------------------------------------------+
6. Boot Architecture
6.1 Boot artifact
The first production-oriented design uses a UEFI-loadable artifact at:
/EFI/BOOT/BOOTX64.EFI
The artifact contains, directly or as a unified image:
- Linux EFI stub.
- Linux kernel.
- Kernel command line.
- Embedded initramfs.
During development, the kernel and initramfs may also be produced separately for faster QEMU iteration. The acceptance path must always test real UEFI boot through OVMF or physical firmware.
6.2 Boot sequence
- UEFI loads
BOOTX64.EFI. - The EFI stub transfers control to the Linux kernel.
- Linux initializes memory, interrupts, ACPI, PCIe, storage, input, graphics, and audio drivers.
- Linux unpacks the initramfs into RAM.
- Linux starts
/init, implemented byplayos-init, as PID 1. playos-initmounts/dev,/proc,/sys, and/run.playos-initdiscovers and mounts the PlayOS data partition.playos-initstartsplayos-compositor.- The compositor initializes its wlroots backend, DRM/KMS, renderer, input seat, and Wayland socket.
- The compositor launches
playos-shellwith the correct Wayland environment and trusted identity. - The shell maps its main fullscreen surface and displays the game library.
- The user selects a game.
- The shell sends a launch request to
playos-initthrough restrictedplayos-runtimecontrol IPC. playos-initvalidates and spawns the game.- The game connects to Wayland and commits its first usable frame.
- The compositor verifies the launch identity and makes the game foreground.
- The application receives lifecycle and safe platform services through
playos-platform-apiwhileplayos-runtimetransports trusted internal events. - When the game exits or crashes, the compositor reveals and refocuses the existing shell surface.
6.3 PID 1 responsibilities
playos-init owns process and boot supervision. It should remain small and deterministic.
It is responsible for:
- Mounting virtual filesystems.
- Initializing runtime directories and logging.
- Discovering and mounting persistent storage.
- Starting and supervising
playos-compositor. - Validating game manifests and launch requests.
- Spawning, monitoring, terminating, and reaping game processes.
- Creating game process groups and lifecycle channels.
- Handling reboot, shutdown, and recovery requests.
- Restarting the compositor after a recoverable failure.
- Entering recovery mode after repeated boot or compositor failure.
It should not contain UI, rendering, network policy, or game-specific logic.
7. Runtime Components and Responsibilities
7.1 playos-init
Owns:
- Boot and service lifecycle.
- Process creation and supervision.
- Game launch validation.
- Exit status and crash reporting.
- Forced pause or termination fallback.
- Shutdown, reboot, and recovery.
It does not own surfaces, focus, or rendering.
7.2 playos-compositor
Owns:
- DRM/KMS devices and output state.
- The wlroots backend, renderer, allocator, and scene.
- The Wayland display and socket.
- Display selection, orientation, refresh rate, and hotplug policy.
- Surface roles, z-order, visibility, and focus.
- Trusted shell and overlay identity.
- Expected game identity and first-frame activation.
- Reserved system controls.
- Input routing between shell, game, and overlay.
- Direct-scanout eligibility and composition fallback.
- Returning to the shell after game exit or failure.
It does not install games, manage saves, or act as a general process supervisor.
7.3 playos-shell
Owns:
- The persistent console UI.
- Controller-first navigation.
- Game discovery and metadata presentation.
- User-facing launch, resume, quit, and crash flows.
- Settings and status screens.
- Initiating launch requests to
playos-init. - Rendering the main shell and trusted PlayOS UI with Raylib.
- Preserving UI state while a game is foreground.
The shell remains alive while a game runs. It stops or heavily throttles its main fullscreen rendering but remains available to render system UI.
7.4 playos-overlay
An overlay may initially be a separate trusted Raylib Wayland client because stock Raylib is most comfortable with one native surface per process.
It owns only overlay presentation, such as:
- Quick menu.
- Volume and brightness indicators.
- Power menu.
- Notifications.
- Virtual keyboard.
It may later be merged into a multi-surface playos-shell backend.
7.5 Active game process
The game owns:
- Its own address space and Raylib state.
- Its Wayland surface and rendering context.
- Its audio streams.
- Its assigned input stream.
- Its save and cache directories.
The game may not:
- Become DRM master.
- Reconfigure displays directly.
- Mount or format filesystems.
- Modify the immutable system image.
- Access another game's private data.
- Consume reserved PlayOS system actions.
7.6 playos-platform-api and libplayos
playos-platform-api owns the public, engine-agnostic PlayOS application contract. Its primary binary artifact is libplayos, and its authoritative compatibility boundary is a stable C ABI.
It exposes:
- Lifecycle events.
- Assigned install, save, and cache paths.
- Device and capability information.
- Logical input conventions.
- Logging.
- Audio, storage, display, and power queries that are safe for applications.
- Narrow requests for approved system actions.
The repository may also provide C++ wrappers and engine adapters, including a Raylib integration layer, but those layers must be implemented above the C ABI rather than replace it. Games, samples, and ordinary shell code use playos-platform-api; they do not consume compositor internals or privileged control protocols directly.
7.7 playos-runtime
playos-runtime owns internal PlayOS integration mechanisms rather than the public application API. It contains:
- Versioned launch and control IPC definitions.
- Lifecycle-event transport.
- Private Wayland protocol XML shared with the compositor and trusted clients.
- Restricted client libraries used by trusted system components.
- Process, session, and operating-system integration helpers.
- The PlayOS backend transport used by
playos-platform-api.
Privileged actions cross these narrow internal interfaces. A normal game reaches them only through the validated public Platform API surface. playos-runtime must not own DRM/KMS policy or the compositor implementation.
8. Console Lifecycle
8.1 Game launch
The launch responsibility is intentionally split:
playos-shell chooses and requests the game through restricted control IPC
playos-init validates, spawns, supervises, and terminates it
playos-compositor identifies its surface and controls presentation
playos-runtime transports lifecycle and control messages internally
playos-platform-api exposes lifecycle events and safe services to the application
Detailed launch flow:
- The player selects a game in the shell.
- The shell sends
LaunchGame(game_id)over PlayOS control IPC. playos-initvalidates the manifest, executable, permissions, and one-game rule.playos-initprepares save paths, cache paths, process group, lifecycle channel, and a one-time launch identity.- The shell shows a Launching state and remains visible.
playos-initspawns the game withWAYLAND_DISPLAYand PlayOS environment variables.- The game connects to the compositor and creates its surface.
- The compositor matches the client to the expected launch identity.
- The compositor waits for the first valid committed buffer.
- Only then does it switch foreground from shell to game.
- The shell remains alive and background-throttled.
This first-frame rule avoids switching to a black screen while the game is still initializing.
8.2 System button and backgrounding
PlayOS reserves a logical system action:
PLAYOS_BUTTON_SYSTEM
Each hardware profile maps a physical key to this action. The action never reaches the game directly.
When pressed during gameplay, the compositor:
- Removes normal input focus from the game.
- Sends a background lifecycle event.
- Shows the shell or trusted overlay.
- Routes UI input to PlayOS.
PlayOS-native games receive cooperative events:
typedef enum PlayOSLifecycleEvent {
PLAYOS_LIFECYCLE_FOREGROUND,
PLAYOS_LIFECYCLE_BACKGROUND,
PLAYOS_LIFECYCLE_SUSPEND,
PLAYOS_LIFECYCLE_RESUME,
PLAYOS_LIFECYCLE_TERMINATE
} PlayOSLifecycleEvent;
The lifecycle enum and polling API are defined by playos-platform-api; playos-runtime transports the events from trusted system components. A cooperative game should pause gameplay, stop normal input processing, lower or mute audio, and reduce rendering while backgrounded.
For non-cooperative games, playos-init may use SIGSTOP and SIGCONT as a compatibility fallback. The compositor requests that action; it does not manipulate Unix processes itself.
8.3 Overlay presentation
The scene order is:
1. active game surface
2. optional dimming layer
3. trusted PlayOS overlay surface
4. notifications and cursor, when enabled
The overlay is a separate Wayland surface because one wl_surface may have only one role.
Overlay flow:
System button
-> compositor intercepts action
-> game loses focus
-> overlay maps above game
-> overlay receives UI input
-> game is cooperatively or forcibly paused
Closing the overlay reverses the flow and returns focus to the game.
8.4 Game exit and crash recovery
When the game exits:
playos-initrecords the exit status.- The compositor destroys or ignores stale game surfaces.
- The compositor reveals the already-running shell surface.
- Focus returns to the shell.
- The shell restores the previous library position and shows any required message.
A game crash must never reveal a terminal or leave the display black.
8.5 Compositor state machine
SHELL_FOREGROUND
|
| launch accepted
v
GAME_STARTING
|
| first valid game frame
v
GAME_FOREGROUND
|
| System button
v
PLAYOS_UI_FOREGROUND_WITH_GAME_BACKGROUND
|
+-- Resume -> GAME_FOREGROUND
|
+-- Quit -> TERMINATING_GAME -> SHELL_FOREGROUND
GAME_FOREGROUND
|
| game exits or crashes
v
SHELL_FOREGROUND
This state machine is a central PlayOS contract and should be explicitly tested.
9. Wayland and wlroots Boundary
9.1 What wlroots already provides
wlroots supplies:
- DRM/KMS, nested, headless, and input backends.
- Renderer and allocator abstractions.
- Output, buffer, and scene primitives.
- Core Wayland compositor building blocks.
- XDG shell support.
- Seat, keyboard, pointer, touch, and libinput integration.
- Surface commit, map, unmap, destroy, and frame events.
9.2 What PlayOS must implement
playos-compositor must implement:
- Trusted shell identity.
- Expected game launch identity.
- One foreground experience at a time.
- Fullscreen shell and game policy.
- First-frame launch switching.
- Reserved system controls.
- Overlay authorization and stacking.
- Focus and input routing.
- Output and orientation policy.
- Direct-scanout policy.
- Lifecycle state transitions.
- Crash recovery.
- Protocol permissions and client restrictions.
9.3 Private PlayOS Wayland protocol
Standard Wayland protocols should be used wherever possible. A small private protocol should cover only console presentation and lifecycle concerns.
Initial capabilities may include:
- Registering the trusted shell.
- Registering an expected game launch identity.
- Assigning shell, game, and trusted overlay roles.
- Reporting surface readiness.
- Reporting foreground and background transitions.
- Requesting return to shell or resume game.
- Reporting output size, scale, refresh rate, and orientation.
The protocol must be versioned and generated with wayland-scanner.
The following do not belong in the Wayland protocol:
- Game installation.
- Save management.
- Networking.
- Updates.
- Account services.
- General process management.
Those use PlayOS control or service IPC.
Part III — Platform Subsystems
10. Graphics Architecture
10.1 Initial graphics stack
Raylib shell or game
|
v
Raylib Wayland / PlayOS backend
|
v
Wayland protocol
|
v
playos-compositor + wlroots
|
v
GBM / EGL / OpenGL ES renderer
|
v
DRM/KMS + AMDGPU
|
v
Display
The first release uses:
- DRM/KMS for display control.
- wlroots for compositor infrastructure.
- Wayland for shell and game presentation.
- GBM and EGL for buffer and context integration.
- OpenGL ES 3.0 or 3.1 for Raylib clients.
- Mesa RadeonSI on AMD.
Vulkan may be introduced later without changing the compositor ownership model.
10.2 Raylib integration
The shell and games should use a dedicated Raylib PlayOS backend, for example:
src/platforms/rcore_playos.c
The backend should adapt Raylib's Wayland support and integrate:
- Fullscreen Wayland surfaces.
- EGL context creation.
- Frame callbacks and presentation timing.
- PlayOS lifecycle events.
- Trusted client identity where applicable.
- Logical input mapping.
- Exit and foreground requests.
- PlayOS save and cache paths.
Initially unsupported desktop features may include:
- Arbitrary window positioning.
- Multiple desktop windows.
- Decorations.
- File drag and drop.
- Clipboard integration.
- User-driven resize.
10.3 Direct scanout
When a game fully covers the output and no overlay is visible, the compositor should attempt direct scanout if the client buffer is compatible with the output.
Compatible fullscreen game buffer
-> DRM plane
-> display
When an overlay appears, or when format, scaling, transform, or synchronization prevents direct scanout, the compositor falls back to normal composition.
Direct scanout is an optimization, not a correctness requirement for the MVP.
10.4 GPU discovery
Do not assume /dev/dri/card0 is always the intended device.
The compositor should:
- Enumerate DRM devices and render nodes.
- Resolve each device to its PCI identity.
- Identify the device connected to the active display.
- Validate renderer initialization.
- Select the scanout and render device.
- Fall back to a diagnostic or software path if initialization fails.
Initial supported vendor IDs:
AMD 0x1002
Intel 0x8086
10.5 AMD implementation
Kernel:
amdgpu.- AMD display core.
- Required ROG Ally firmware.
Userspace:
- Mesa RadeonSI for OpenGL ES/OpenGL.
- libdrm.
- GBM.
- EGL.
RADV may be added when Vulkan becomes a product requirement.
10.6 Intel expansion
After the AMD baseline is stable, Intel support may use:
Kernel:
i915orxe, depending on supported hardware.- Required firmware.
Userspace:
- Mesa Iris.
- libdrm.
- GBM.
- EGL.
ANV may be added with Vulkan support later.
10.7 Recovery graphics
Recovery mode must remain usable when accelerated graphics fails.
Possible fallback paths:
- SimpleDRM or firmware framebuffer.
- Text or software-rendered diagnostic UI.
- A minimal recovery menu with storage, log, rollback, and reboot actions.
11. Input Architecture
11.1 Input path
Controller / keyboard / touch
|
v
Linux HID and device drivers
|
v
evdev / libinput
|
v
playos-compositor
|
+-- reserved actions -> PlayOS only
+-- overlay visible -> overlay
+-- game foreground -> game
+-- otherwise -> shell
The compositor owns focus and reserved input policy during Phase 1.
A later playos-input service may centralize remapping, profiles, virtual controllers, gyro, haptics, and multi-controller assignment.
11.2 Logical input model
Games should receive logical controls rather than raw device codes:
PLAYOS_BUTTON_SOUTH
PLAYOS_BUTTON_EAST
PLAYOS_BUTTON_WEST
PLAYOS_BUTTON_NORTH
PLAYOS_BUTTON_START
PLAYOS_BUTTON_SELECT
PLAYOS_BUTTON_SYSTEM
PLAYOS_BUTTON_QUICK_MENU
PLAYOS_AXIS_LEFT_X
PLAYOS_AXIS_LEFT_Y
PLAYOS_AXIS_RIGHT_X
PLAYOS_AXIS_RIGHT_Y
PLAYOS_AXIS_LEFT_TRIGGER
PLAYOS_AXIS_RIGHT_TRIGGER
PLAYOS_BUTTON_SYSTEM is reserved and is not delivered to games.
11.3 Initial ROG Ally scope
Support first:
- D-pad.
- A/B/X/Y.
- Both analog sticks.
- Both triggers.
- Menu and View.
- One development fallback for the System action.
Defer until later:
- Rear buttons.
- Vendor-specific command buttons.
- Gyroscope.
- Haptics.
- RGB control.
- Controller mode switching.
12. Audio Architecture
12.1 Initial path
Raylib audio
|
v
PlayOS audio backend
|
v
ALSA PCM
|
v
Kernel ALSA driver
|
v
Speakers or headphones
No PulseAudio or PipeWire is required for the MVP.
12.2 Initial policy
Support:
- Stereo PCM output.
- Master volume.
- Mute.
- Built-in speaker and headphone selection.
- Device-loss and underrun recovery.
The first implementation may allow one foreground audio owner. When the game becomes foreground, shell audio stops. When the game backgrounds or exits, shell audio resumes.
A dedicated PlayOS audio service is introduced only when simultaneous shell and game audio, notifications over games, Bluetooth audio, or per-application mixing becomes necessary.
13. Storage Architecture
13.1 Partition model
Prototype layout:
GPT disk
Partition 1: EFI System Partition
FAT32
PlayOS boot artifact
Partition 2: PlayOS Data
ext4
games and writable state
Production A/B layout:
Partition 1: EFI System Partition
Partition 2: PlayOS system A
Partition 3: PlayOS system B
Partition 4: PlayOS data
The exact system-slot representation may remain EFI-image based, partition based, or a combination. The invariant is that writable game and user data is separate from replaceable system state.
13.2 Persistent directory layout
Mount the writable partition at /data:
/data/
games/
saves/
profiles/
resources/
cache/
downloads/
logs/
updates/
screenshots/
config/
Per-game layout:
/data/games/<game-id>/
manifest.json
bin/
assets/
shaders/
licenses/
/data/saves/<game-id>/
profiles/
autosaves/
settings/
/data/cache/<game-id>/
shaders/
compiled-assets/
temporary/
13.3 Filesystem choice
Use ext4 initially because it is mature, journaled, well supported, and easy to recover.
Do not design a custom filesystem for the first release. A PlayOS package or virtual filesystem can be layered above ext4 later.
13.4 First-boot provisioning
PlayOS must never silently format an unknown disk.
Provisioning should:
- Search for the expected partition GUID, label, or UUID.
- Mount it if valid.
- Enter provisioning mode if absent.
- Show the target and destructive impact.
- Require explicit confirmation or a manufacturing flag.
- Create the filesystem and expected metadata.
- Create the directory tree.
- Write a storage-version marker.
13.5 Factory reset
A normal factory reset operates on writable state and leaves the immutable boot system intact.
The user may choose whether to erase:
- Configuration.
- Installed games.
- Saves.
- Cache.
- Downloads.
- Logs.
14. Game Packaging and Platform API
14.1 Initial game package
A game may initially be an ordinary directory:
<game-id>/
manifest.json
bin/game
assets/
licenses/
Example manifest:
{
"id": "com.example.game",
"name": "Example Game",
"version": "1.0.0",
"executable": "bin/game",
"api_version": 1,
"graphics": "gles3",
"architecture": "x86_64",
"controllers": true,
"network": false
}
14.2 Later .play package
A future package may be a signed archive, SquashFS image, or content-addressed bundle.
Required properties:
- Deterministic metadata.
- Signature verification.
- Integrity hashes.
- Atomic installation.
- Versioned migrations.
- Strict save-data separation.
14.3 PlayOS Platform API goals
The public API is specified in playos-spec and implemented by playos-platform-api. It should hide Linux implementation details and expose stable capabilities through an authoritative C ABI. The same API surface should remain usable from Raylib, SDL, Godot, custom engines, and non-Linux SDK targets through replaceable backends.
Initial groups:
playos_system
playos_display
playos_input
playos_audio
playos_storage
playos_game
playos_power
playos_logging
Examples:
System:
- API and OS version.
- Device model.
- CPU and GPU information.
- Memory and locale.
Display:
- Resolution and refresh rate.
- Orientation and scale.
- V-sync preference.
Input:
- Logical controller state.
- Device connection events.
- Lifecycle-safe input state.
Storage:
- Install, save, and cache directories.
- Free-space query.
- Atomic replacement helpers.
Power:
- Battery and charging state.
- Thermal state.
- Approved performance-profile request.
Logging:
- Structured logs.
- Session identifiers.
- Crash markers.
14.4 API delivery and internal transport
playos-platform-api initially delivers:
- Public C headers under
include/playos/. libplayos.sofor PlayOS runtime devices.- A static SDK library where appropriate for SDK-only targets.
- Optional C++ wrappers that preserve the C ABI as the source of truth.
- Engine adapters, including the Raylib PlayOS layer.
The PlayOS backend may consume launch-time environment values and a lifecycle or service channel implemented by playos-runtime. Internal transport details, privileged launch control, compositor control, and private Wayland protocols are not public Platform API contracts.
The C ABI must be consumable from C, C++, Rust, Zig, and other languages. Public ABI changes require versioning and compatibility review in playos-spec.
15. Security Model
15.1 Initial development trust
Early bring-up images may run with broader privileges to accelerate hardware debugging. That is not the production model.
15.2 Production direction
playos-initruns as root.playos-compositorreceives only required display and input privileges.playos-shellruns as a dedicated service user where practical.- Games run as an unprivileged game identity.
- Games receive per-title save and cache directories.
- Games link to
playos-platform-api; privilegedplayos-runtimecontrol interfaces are restricted to trusted components. - The system image is read-only.
- Remote development services are absent from retail builds.
15.3 Game restrictions
A normal game should not be able to:
- Modify the system image.
- Access another game's saves.
- Open DRM primary nodes.
- Mount filesystems.
- Change kernel modules.
- Format storage.
- Invoke unrestricted shutdown, update, or power controls.
- Create trusted overlays.
- Synthesize reserved system input.
- Connect to privileged launch, compositor-control, or trusted-role IPC endpoints.
Later hardening may use capabilities, seccomp, Landlock, namespaces where useful, signed manifests, and package integrity verification.
15.4 Secure Boot
Secure Boot is a later production requirement. The signed chain should cover:
- PlayOS EFI artifact.
- Kernel and embedded initramfs.
- External kernel modules, if any.
- A/B update metadata.
Development builds may initially run with Secure Boot disabled.
16. Updates, Recovery, and Failure Handling
16.1 A/B updates
The update flow should:
- Download a signed system image.
- Verify its signature and compatibility.
- Write the inactive slot.
- Mark it as the next boot candidate.
- Boot it once.
- Record a health-success marker.
- Roll back automatically after repeated failure.
Games and saves remain untouched.
RAUC is the preferred first update engine unless a simpler EFI-image-specific updater proves sufficient.
16.2 Recovery mode
Recovery must work without accelerated graphics and should provide:
- System-slot selection.
- Rollback.
- Filesystem check.
- Log export.
- Factory reset.
- Reboot and shutdown.
16.3 Failure policy
Game failure:
- Return to shell.
- Record exit status and crash metadata.
- Offer restart or report options.
Shell failure:
- Restart the shell client without restarting the compositor where possible.
Compositor failure:
playos-initrestarts the graphical session.- Repeated failure enters recovery.
Kernel or boot failure:
- Boot counting and A/B rollback select the previous known-good image.
Part IV — Build, Kernel, and Development Model
17. Build System
17.1 Repository ownership and dependency direction
The MVP implementation uses six repositories:
| Repository | Ownership |
|---|---|
playos-spec | Architecture, public contracts, RFCs, ADRs, schemas, roadmap, and product documentation. |
playos-platform-api | Public libplayos C ABI, portable implementations, C++ wrappers, and engine adapters. |
playos-runtime | Internal lifecycle transport, launch and control IPC, private Wayland protocols, restricted service clients, and OS integration. |
playos-compositor | wlroots compositor, DRM/KMS ownership, surfaces, focus, trusted roles, overlays, and reserved-input policy. |
playos-shell | Raylib controller-first shell and trusted user experience. |
playos-refdistro | Buildroot integration, kernel and initramfs configuration, playos-init, packaging, image assembly, installer, recovery, and release pinning. |
Dependency direction:
playos-spec
defines contracts for every implementation repository
playos-runtime
owns private transports and protocols
↑ ↑
│ │
playos-platform-api playos-compositor
↑
│
playos-shell and games
playos-shell may additionally use a restricted playos-runtime control client
for trusted launch and system requests.
playos-refdistro pins, packages, and assembles all runtime components.
Rules:
- Public application headers and ABI belong only to
playos-platform-api. - Private launch, lifecycle-transport, compositor-control, and Wayland protocol definitions belong to
playos-runtime. - DRM/KMS, wlroots, surface, focus, and input-routing implementation belongs to
playos-compositor. playos-refdistromay package and pin components but must not redefine their public contracts.- Existing compositor code under
playos-runtimemust migrate toplayos-compositor; after migration there must be one active compositor implementation, not two divergent copies. - The older Alpine-oriented refdistro map is superseded for this architecture baseline by the Buildroot model in this document.
17.2 Buildroot strategy
Use the official Buildroot repository as a pinned Git submodule. Do not fork it initially.
All PlayOS-specific work lives in a br2-external tree:
playos-refdistro/
buildroot/ official pinned submodule
br2-external/
external.desc
Config.in
external.mk
configs/
playos_qemu_x86_64_defconfig
playos_rog_ally_defconfig
playos_intel_pc_defconfig later
board/playos/
common/
qemu-x86_64/
rog-ally/
intel-pc/ later
package/
playos-init/
playos-platform-api/
playos-runtime/
playos-compositor/
playos-shell/
playos-overlay/
raylib-playos/
patches/
linux/
wlroots/
raylib/
src/
protocols/
scripts/
docs/
.github/workflows/
Makefile
versions.lock
versions.lock must pin full commits for playos-platform-api, playos-runtime, playos-compositor, and playos-shell, in addition to Buildroot, Linux, toolchain, configuration, and patch-set inputs.
A Buildroot fork is justified only when a required change cannot be represented as an external package, board configuration, patch, rootfs overlay, post-build script, or post-image script.
17.3 Toolchain
Use:
GCC initially
musl libc only
GNU binutils or lld as supported by the selected Buildroot configuration
CMake or Meson per component
wayland-scanner for private protocols
Clang builds may be added to CI for diagnostics and sanitizers, but GCC remains the first reference compiler.
17.4 Build variants
Development image:
- BusyBox or a minimal diagnostic shell.
- Serial logs.
- SSH only after networking is introduced.
strace,gdbserver,evtest,modetest, and graphics diagnostics as needed.- Extra assertions and debug symbols.
Production image:
- No interactive shell in normal mode.
- No remote debug service.
- Minimal utilities.
- Signed artifacts.
- Bounded persistent logs.
- Recovery entry point.
17.5 Boot image strategy
Produce both:
Fast development outputs:
bzImage
rootfs.cpio.zst
UEFI acceptance output:
playos-esp.img
/EFI/BOOT/BOOTX64.EFI
The acceptance image must boot through OVMF rather than QEMU's direct -kernel shortcut.
17.6 Stable developer commands
The repository should expose a small command surface:
make setup
make qemu-config
make qemu-build
make qemu-run
make ally-config
make ally-build
make clean
Developers should not need to memorize raw Buildroot command lines.
18. Kernel Configuration Strategy
Use an upstream Linux LTS branch as the release base and keep a separate newer test track for hardware-enablement evaluation.
Suggested policy:
playos-kernel-lts release and qualification branch
playos-kernel-next newer stable branch for hardware testing
Start from a known-working ROG Ally configuration and remove features gradually. Do not begin from an aggressively minimal embedded configuration.
Required subsystem groups include:
- x86-64 and EFI stub.
- ACPI and PCIe.
- IOMMU.
- devtmpfs, procfs, sysfs, tmpfs, initramfs.
- serial and early console for diagnostics.
- DRM/KMS, SimpleDRM, and AMDGPU.
- USB xHCI.
- HID, evdev, and ASUS-specific input support where needed.
- ALSA HDA/SoC/ACP support required by the device.
- NVMe.
- FAT and ext4.
- thermal, battery, power-supply, and AMD P-state support.
- watchdog and recovery-relevant facilities.
Remove only after qualification:
- Unused server filesystems.
- Unused network protocols.
- Unused GPU and sound drivers.
- Legacy buses not present on supported hardware.
- Virtualization host features not needed by the product.
- Excessive debug features in retail builds.
Keep drivers modular during discovery when that improves iteration. Convert the final required hardware set to built-ins when the support matrix is stable.
19. Development, Testing, and Performance
19.1 QEMU and OVMF
Use QEMU with OVMF for:
- UEFI boot.
- initramfs validation.
- PID 1 behavior.
- compositor headless or nested testing.
- process lifecycle.
- storage provisioning on virtual disks.
- update and rollback logic.
- automated boot checks.
QEMU does not replace physical testing for AMDGPU, Ally input, audio, ACPI, suspend, battery, or thermal behavior.
19.2 Physical-device testing
Maintain at least one ROG Ally as a continuous hardware target.
Required smoke tests should cover:
- Cold boot.
- Repeated reboot.
- Shell rendering.
- Controller navigation.
- Game launch and exit.
- System-button transition.
- Overlay focus.
- Game crash recovery.
- Audio output.
- Data persistence.
- Battery and thermal reporting.
19.3 CI layers
Layer 1: host unit tests
Layer 2: Buildroot clean build
Layer 3: QEMU/OVMF boot test
Layer 4: compositor and shell smoke test
Layer 5: game lifecycle integration test
Layer 6: physical ROG Ally smoke test
Layer 7: long-running stability and update tests
Useful tools include:
- Kernel selftests where relevant.
- IGT GPU tools for DRM/KMS.
- Piglit for OpenGL validation.
- apitrace for graphics investigation.
perffor CPU profiling.- AddressSanitizer and UndefinedBehaviorSanitizer in test builds.
- RenderDoc later where the path is supported.
19.4 Logging
Development logs should capture:
- Kernel boot and driver selection.
- DRM device, connector, mode, and renderer.
- Wayland client identity and surface lifecycle.
- Compositor state transitions.
- Direct-scanout attempts and fallback reasons.
- Game launch command and exit status.
- Input-device discovery and logical mappings.
- Audio-device selection and underruns.
- Storage mount and recovery events.
Production logging must be bounded, privacy-aware, and recoverable without exposing a normal shell.
19.5 Performance rules
Do not optimize process count merely for appearance. Sleeping processes do not meaningfully compete with the game.
Prioritize:
- Correct AMDGPU power and clock behavior.
- Direct scanout for eligible fullscreen games.
- Stable frame pacing.
- Avoiding unnecessary composition.
- Keeping hidden shell rendering stopped or throttled.
- Minimal allocations in the compositor frame loop.
- Fast first-frame switching.
- Reliable audio buffer sizing.
- Safe thermal limits.
Performance changes must be measured on physical hardware before becoming policy.
Part V — Delivery Plan
20. MVP Definition
The first meaningful PlayOS MVP is complete when:
- The ROG Ally boots directly from UEFI into PlayOS.
- The Linux kernel and initramfs are available as a UEFI-bootable artifact.
playos-initruns as PID 1.playos-compositorpermanently owns DRM/KMS and the Wayland session.playos-shellremains alive as the persistent controller-first UI.- The compositor uses wlroots with AMDGPU, DRM/KMS, GBM, EGL, and Mesa.
- The shell renders through Wayland using the Raylib PlayOS backend.
- The shell and sample game consume the public
playos-platform-apiC ABI. - Trusted launch, lifecycle transport, and compositor-control mechanisms remain internal to
playos-runtime. - The shell requests a game launch and
playos-initspawns and supervises it. - The compositor waits for the game's first valid frame before making it foreground.
- The game renders with hardware acceleration and receives normal controller input.
- The reserved System button returns to PlayOS UI and backgrounds or pauses the game.
- Resume returns to the same running game without restarting it.
- The game outputs audio through ALSA.
- Clean exit and crash both return safely to the existing shell.
- Games and saves persist on a separate ext4 partition.
- The system image is immutable.
- Recovery remains usable without accelerated graphics.
21. Implementation Sprints
The detailed delivery plan is maintained in standalone sprint documents. This keeps the architecture readable while allowing each sprint to evolve as an executable work package.
| Sprint | Focus | Primary outcome |
|---|---|---|
| 0 | Build and UEFI Foundation | A six-repository, reproducible Buildroot factory that boots a minimal PlayOS EFI image through QEMU/OVMF. |
| 1 | playos-init and Minimal Boot Supervision | A real playos-init running as PID 1 with versioned private control IPC. |
| 2 | Compositor Skeleton and Wayland Session | A minimal wlroots compositor that creates a Wayland session and presents one trusted fullscreen client. |
| 3 | ROG Ally Kernel and Device Bring-Up | Reliable USB boot, essential devices, and the first qualified Platform API input backend contract. |
| 4 | AMDGPU and Native DRM/KMS | The compositor permanently owns the Ally display through AMDGPU and DRM/KMS. |
| 5 | Raylib-Powered PlayOS Shell | A hardware-accelerated Raylib shell consuming the public PlayOS Platform API. |
| 6 | Persistent Storage and Game Discovery | Persistent ext4 storage, safe Platform API paths, and shell-visible game discovery. |
| 7 | Game Launch, Lifecycle, System Button, and Overlay | The complete console lifecycle with a public application API and private trusted control path. |
| 8 | ALSA Audio | Reliable ALSA audio with safe public controls across lifecycle transitions. |
| 9 | Power, Battery, Thermal, and Suspend Foundations | Safe power behavior exposed through a restricted public Platform API. |
| 10 | Installer and Internal-Disk Deployment | A tested installation path from removable media to the ROG Ally internal SSD. |
| 11 | Immutable Images and A/B Updates | Signed, atomic A/B system updates with automatic rollback. |
| 12 | Security Hardening | A hardened boundary between public Platform API calls, trusted runtime control, and games. |
| 13 | Intel Expansion | Proof that the architecture and Platform API backend model are portable to Intel. |
| 14 | Production Readiness | A signed preview release with a versioned public Platform API and separated trusted integration docs. |
Execution rules:
- Sprints follow dependency order unless an ADR explicitly changes the sequence.
- A sprint begins only after the required predecessor exit criteria are satisfied.
- Each sprint must end with a demonstrable and testable system outcome.
- Architecture changes discovered during implementation must be captured in
playos-specand reflected here.
22. Post-MVP Roadmap
Add only when the core console lifecycle is stable:
playos-devicefor hardware and power policy.playos-netwith iwd for Wi-Fi.- Dropbear SSH in explicit Developer Mode only.
playos-updateas a PlayOS wrapper around the update engine.- A dedicated
playos-inputservice for remapping, virtual gamepads, gyro, and haptics. - A dedicated audio service for mixing and notifications over games.
- Bluetooth.
- Fast and fully qualified suspend/resume.
- Rear-button and special-button support.
- Screenshots and recording.
- Vulkan.
- VRR and HDR.
- External-display profiles.
- Download manager and store integration.
- Cloud saves and user accounts.
- Multiple local profiles.
- Signed
.playcontent packages. - Delta updates.
- Telemetry only with explicit user consent.
23. Final Reference Baseline
Primary device:
ROG Ally
Firmware and boot:
UEFI
Linux EFI stub
embedded initramfs
Kernel:
upstream Linux LTS
ROG Ally-specific configuration
Image generation:
official pinned Buildroot submodule
PlayOS br2-external tree
C library:
musl only
PID 1:
playos-init
Display server:
playos-compositor built on wlroots
Public application API:
playos-platform-api
libplayos authoritative C ABI
optional C++ and engine wrappers
Internal runtime integration:
playos-runtime lifecycle transport
launch and control IPC
private Wayland protocols
Persistent UI:
playos-shell as a trusted Raylib Wayland client
System UI during gameplay:
trusted overlay surface or playos-overlay client
Game runtime:
one isolated Wayland game process at a time
Graphics:
DRM/KMS
wlroots
Wayland
GBM / EGL / OpenGL ES
AMDGPU + Mesa RadeonSI
Input:
Linux HID / evdev / libinput
compositor-enforced reserved system actions
PlayOS logical mapping
Audio:
ALSA directly for MVP
Storage:
immutable system image
separate ext4 data partition
Updates:
signed A/B images
Later expansion:
Intel graphics
networking and developer SSH
dedicated device, input, audio, and update services
The defining architectural principle is:
PlayOS is a console operating environment built on a minimal Linux hardware layer.
playos-initowns processes,playos-compositorowns display and focus,playos-shellowns the user experience,playos-platform-apiowns the publiclibplayosC ABI,playos-runtimeowns internal lifecycle transport and control IPC, and one isolated game process runs at a time. The system remains immutable, the data partition remains writable, and the player never interacts with Linux as a desktop operating system.