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

DocumentDescription
architecture.mdSystem design, component diagrams, state machine, boot sequence
platform-api.mdPublic libplayos C ABI specification and versioning policy
runtime-ipc.mdInternal IPC protocol (launch, lifecycle, control)
wayland-protocol.mdPrivate PlayOS Wayland extensions
security-model.mdTrust boundaries, game restrictions, Secure Boot chain

Component Specifications

DocumentDescription
playos-init-spec.mdPID 1 — boot, process supervision, storage, IPC
playos-compositor-spec.mdwlroots compositor — DRM/KMS, focus, state machine
playos-shell-spec.mdRaylib shell — controller UI, game library, lifecycle
playos-overlay-spec.mdTrusted overlay — quick menu, notifications, power

Build and Development

DocumentDescription
build-guide.mdBuildroot setup, br2-external layout, make commands
kernel-config.mdKernel subsystem requirements, ROG Ally configuration
dev-environment.mdQEMU/OVMF setup, developer iteration workflow
testing.mdCI layers, physical device smoke tests

Delivery

DocumentDescription
roadmap.mdMVP criteria and sprint plan (Sprints 0–19)
post-mvp.mdPost-MVP feature roadmap
Sprint-N.mdSprint 0–19 work packages

Architecture Decision Records

ADRDecision
ADR-0001Repository structure
ADR-0002Unix socket IPC transport
ADR-0003musl libc only
ADR-0004wlroots as compositor foundation
ADR-0005RAUC for A/B updates
ADR-0006Raylib for shell and game UI
ADR-0007Direct ALSA for MVP audio
ADR-0008PCI enumeration for GPU selection

Repository Map

RepositoryRole
playos-specArchitecture, contracts, ADRs, roadmap, game developer docs
playos-platform-apiPublic libplayos C ABI
playos-runtimeInternal IPC, lifecycle transport, private Wayland protocols
playos-compositorwlroots compositor, DRM/KMS, focus, input routing
playos-shellController-first Raylib shell and PlayOS Raylib backend
playos-refdistroBuildroot integration, kernel config, image assembly, installer
playos-initPID 1 process supervisor, boot lifecycle, storage mount
playos-samplesSample games and reference applications
playos-toolsHost-side developer and OTA staging tools
playos-foundationShared foundation libraries and utilities
playos-reference-devicesReference device configurations and images
playos-cloudCloud services — cloud saves and accounts (post-MVP)
playos-marketplaceGame 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

  1. Defining Principle
  2. System Diagram
  3. Repository Map
  4. Process Model
  5. Boot Sequence
  6. Component Responsibilities
  7. Console Lifecycle State Machine
  8. Game Launch Flow
  9. Input Routing
  10. Graphics Stack
  11. Audio Stack
  12. Storage Layout
  13. Security Boundaries
  14. Architectural Constraints (Non-Goals)

1. Defining Principle

PlayOS is a console operating environment built on a minimal Linux hardware layer. playos-init owns processes, playos-compositor owns display and focus, playos-shell owns the user experience, playos-platform-api owns the public libplayos C ABI, playos-runtime owns 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-shell always 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

RepositoryOwns
playos-specArchitecture, public contracts, ADRs, schemas, roadmap
playos-initPID 1 process supervisor, boot lifecycle, storage mount, game launch/supervision
playos-platform-apiPublic libplayos C ABI, C++ wrappers, engine adapters
playos-runtimeInternal IPC, lifecycle transport, private Wayland protocols, OS integration
playos-compositorwlroots compositor, DRM/KMS, surface/focus/input policy
playos-shellController-first Raylib shell and PlayOS Raylib backend
playos-refdistroBuildroot integration, kernel config, image assembly, installer
playos-samplesSample games and reference applications
playos-toolsHost-side developer and OTA staging tools
playos-foundationShared foundation libraries and utilities
playos-reference-devicesReference device configurations and images
playos-cloudCloud services — cloud saves and accounts (post-MVP)
playos-marketplaceGame 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-refdistro packages 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-init spawns and supervises playos-compositor, playos-shell, playos-overlay, and active-game.
  • The compositor owns the Wayland display and surface presentation for its trusted clients; it does not spawn processes.

5. Boot Sequence

StepActorAction
1UEFILoads /EFI/BOOT/BOOTX64.EFI
2EFI stubTransfers control to the Linux kernel
3KernelInitializes hardware: ACPI, PCIe, GPU, USB, ALSA, NVMe
4KernelUnpacks embedded initramfs into RAM
5KernelStarts /initplayos-init as PID 1
6playos-initMounts /dev, /proc, /sys, /run
7playos-initDiscovers and mounts the PlayOS data partition
8playos-initStarts playos-compositor
9playos-compositorInitializes wlroots backend, DRM/KMS, renderer, Wayland socket
10playos-initLaunches playos-shell and playos-overlay with trusted identity
11playos-shellMaps fullscreen surface; shows game library
UserSelects a game
12playos-shellSends LaunchGame(game_id) over control IPC
13playos-initValidates manifest; spawns game process
14playos-compositorWaits for game's first valid committed frame
15playos-compositorSwitches 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:

FromEventTo
SHELL_FOREGROUNDplayos-init accepts launchGAME_STARTING
GAME_STARTINGFirst valid game frameGAME_FOREGROUND
GAME_FOREGROUNDPLAYOS_BUTTON_SYSTEMPLAYOS_UI_FOREGROUND_WITH_GAME_BACKGROUND
PLAYOS_UI_...ResumeGAME_FOREGROUND
PLAYOS_UI_...QuitTERMINATING_GAMESHELL_FOREGROUND
GAME_FOREGROUNDExit or crashSHELL_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:

ComponentRole in launch
playos-shellChooses and requests via restricted control IPC
playos-initValidates, spawns, supervises, terminates
playos-compositorIdentifies surface; controls presentation
playos-runtimeTransports lifecycle and control messages
playos-platform-apiExposes 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 /data only; 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:

ExcludedReason
Desktop environmentConsole OS; Linux is the hardware layer only
X11 / XwaylandWayland-only
systemdCustom playos-init owns lifecycle
Display / login managerDirect boot to shell
ContainersNot needed for single-game model
Conventional package managerImmutable system image
Custom Linux kernelUpstream LTS with ROG Ally config
Custom GPU driver / OpenGLMesa / AMDGPU
Multiple simultaneous gamesOne-game process model
Multi-GPU / hybrid graphicsROG Ally is single AMD GPU
Wi-Fi, Bluetooth, SSH, cloud savesPost-MVP
Full suspend/resumePost-MVP
HDR, VRR, recording, streamingPost-MVP
libc other than muslmusl 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

  1. Purpose and Scope
  2. ABI Stability Policy
  3. Versioning
  4. Header Organization
  5. API Groups
  6. Backend Architecture
  7. C++ and Engine Wrappers
  8. What the API Must Never Expose
  9. 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-runtime private 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 releases
  • libplayos.so.1 — next breaking release

Breaking changes require:

  1. RFC filed in playos-spec
  2. ADR documenting the decision and migration path
  3. Major SONAME bump
  4. 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:

EventRequired action
FOREGROUNDResume rendering and input processing
BACKGROUNDPause gameplay, stop normal input, lower/mute audio, reduce FPS to 0
SUSPENDFlush save data immediately; return within 500ms
RESUMERestore state; resume rendering
TERMINATESave 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 aliases
  • src/backends/rcore_playos.c — Raylib platform backend (lives in playos-shell, not playos-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

ForbiddenReason
DRM file descriptors or device pathsDirect display access belongs to playos-compositor
Wayland display or socket handleWayland connection is managed by the Raylib backend
playos-runtime IPC socket path or fdInternal transport is private
Another game's storage pathsIsolation is a security guarantee
playos-init control IPCProcess control is a trusted-only path
Raw Linux input event codesLogical mapping is the stable abstraction
Kernel module or firmware interfacesNot a public game concern

9. Change Process

  1. Additive changes (new functions, new structs, new enum values): PR to playos-platform-api, reviewed against this spec.
  2. Potentially breaking changes: RFC issue in playos-spec, reviewed by at least two contributors, resulting in an ADR.
  3. 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

  1. Overview
  2. Transport
  3. Access Control
  4. Message Format
  5. Control IPC — control.sock
  6. Lifecycle Transport — per-game fd
  7. Compositor Control Channel
  8. Protocol Versioning
  9. Error Handling

1. Overview

playos-runtime owns the private integration layer between trusted PlayOS system components. It defines three distinct communication channels:

ChannelTransportDirectionPurpose
Control IPCUnix socketShell/overlay → playos-initGame launch, shutdown, factory reset, system commands
Lifecycle transportPipe fd per gameplayos-init → gameLifecycle events (foreground, background, terminate)
Compositor controlUnix socketplayos-init/runtime → compositorSet 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.

ComponentGroup membership
playos-initroot (owns sockets)
playos-compositorplayos-trusted
playos-shellplayos-trusted
playos-overlayplayos-trusted
Active gameNot 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 (PLOS in 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 valueEvent
0x00PLAYOS_LIFECYCLE_FOREGROUND
0x01PLAYOS_LIFECYCLE_BACKGROUND
0x02PLAYOS_LIFECYCLE_SUSPEND
0x03PLAYOS_LIFECYCLE_RESUME
0x04PLAYOS_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 typeTrust checkInterfaces available
Trusted shellPLAYOS_TRUSTED_SHELL=1 in envplayos_manager_v1, playos_shell_v1, standard Wayland
Trusted overlayPLAYOS_TRUSTED_OVERLAY=1 in envplayos_manager_v1, playos_overlay_v1, standard Wayland
Active gameNo trust flagStandard 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:

  1. Adding a new <event> or <request> (never removing existing ones)
  2. Incrementing version in the <interface> element
  3. Updating the version bind check in compositor and client code
  4. 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

  1. Trust Zones
  2. Component Privilege Levels
  3. Game Restrictions
  4. IPC Access Control
  5. Filesystem Access Control
  6. seccomp Filter Policy
  7. Landlock Filesystem Restrictions
  8. Input Security
  9. System Image Integrity
  10. Secure Boot Chain
  11. Development vs Production
  12. 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

ComponentUserCapabilitiesNotes
playos-initrootAll (required for process supervision, mounts, device setup)Drop unnecessary caps after init
playos-compositorplayos-systemCAP_SYS_ADMIN (DRM master), CAP_DAC_READ_SEARCH (device nodes)Drop all others
playos-shellplayos-systemNoneMember of playos-trusted group
playos-overlayplayos-systemNoneMember of playos-trusted group
Active gameplayos-gameNonePR_SET_NO_NEW_PRIVS = 1 before exec
playos-installerrootAllOnly present in installer image

3. Game Restrictions

A normal game must not be able to:

ActionEnforcement
Modify the system imageSystem 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 dataLandlock path restrictions + per-game directory
Mount or format filesystemsseccomp blocks mount, umount2
Load kernel modulesseccomp blocks init_module, finit_module
Change kernel parametersseccomp blocks sysctl, game user has no /proc/sys write access
Invoke unrestricted shutdown/rebootseccomp blocks reboot syscall
Connect to control IPCUNIX group restriction; game is not in playos-trusted
Synthesize reserved system inputInput routing is compositor-enforced at Wayland/evdev level
Create trusted overlaysTrusted roles require PLAYOS_TRUSTED_* env + group check
Ptrace other processesseccomp blocks ptrace
Escalate privilegesPR_SET_NO_NEW_PRIVS; seccomp blocks setuid, setcap

4. IPC Access Control

Control socket (/run/playos/control.sock)

  • Owner: root:playos-trusted, mode 0660
  • Who can connect: playos-shell, playos-overlay (both in playos-trusted)
  • Who cannot: playos-game — enforced by UNIX group check at connect(2) time

Compositor socket (/run/playos/compositor.sock)

  • Owner: root:playos-trusted, mode 0660
  • Who can connect: playos-runtime internal 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> and cache/<id>
  • Other directories (config/, games/, logs/) owned by playos-system, not writable by games

Device nodes

DeviceOwnerModeGame access
/dev/dri/card*root:drm0660❌ Not in drm group
/dev/dri/renderD*root:render0660✅ In render group (needed for Wayland/EGL)
/dev/input/event*root:input0660❌ Not in input group (input via Wayland seat only)
/run/playos/*.sockroot:playos-trusted0660❌ 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

PathAccess
/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/randomRead
/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 via playos_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 plain fork()+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 (see Sprint-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:

  1. Sets up a dm-verity device over the system partition
  2. Mounts the dm-verity device read-only
  3. If hash verification fails for any block, the kernel returns I/O errors (enforced by dm-verity)
  4. playos-init monitors 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
  • sbsign used in the release pipeline

Recovery

If Secure Boot verification fails:

  1. UEFI firmware refuses to boot the artifact
  2. User must boot into UEFI Secure Boot key management to enroll the PlayOS development key (dev builds)
  3. Production: chain-of-trust failure surfaces as boot failure → A/B rollback → recovery mode

11. Development vs Production

FeatureDevelopment imageProduction image
BusyBox shell✅ Present❌ Absent
SSH daemon❌ (planned post-network sprint)❌ Absent
gdbserver, strace✅ Present❌ Absent
Serial console✅ Enabled✅ Enabled (needed for recovery)
dm-verityOptional✅ Required
Secure BootOptional (disabled ok)✅ Required
Debug assertions✅ Enabled❌ Disabled
Open TCP socketsAllowed (SSH)❌ None
seccomp✅ Enforced✅ Enforced
Landlock✅ Enforced✅ Enforced

The post-build production lint CI step asserts:

  • No /bin/sh or /bin/busybox in 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:

  1. dm-verity for system partition integrity (Sprint 12 gap)
  2. Signed game manifests (Ed25519 — warn-only in Sprint 12, enforced post-MVP)
  3. User namespaces for additional game isolation if needed
  4. Hardware-backed keys for update signing
  5. IMA/EVM for individual file integrity in the initramfs
  6. Audit logging for privileged IPC commands
  7. Network namespace for games (when networking is introduced)
  8. 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.

OwnsDoes NOT own
Boot and service lifecycleUI or rendering
Virtual filesystem mountsInput routing
Storage discovery and mountDisplay configuration
Starting and supervising playos-compositorGame-specific logic
Game launch validationNetwork policy
Process spawning, monitoring, reapingPackage 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 on SIGCHLD
  • Compositor supervision:
    • On compositor exit (any reason): record exit status, wait COMPOSITOR_RESTART_DELAY_MS (500ms), restart
    • After COMPOSITOR_MAX_RESTARTS (default: 3) within COMPOSITOR_WINDOW_S (default: 60s): enter recovery mode
  • Game supervision:
    • Track game PID and game_id
    • On game exit: emit GameExited IPC event; update internal state; unblock the shell
    • On crash: set crashed=true in GameExited
  • Overlay process: Supervised same as compositor (restart on exit)

Supervision table:

ProcessRestart policyFailure action
playos-compositorRestart, up to N timesRecovery mode
playos-shellRestart (shell is always alive)Restart compositor session
playos-overlayRestartLog, continue
Active gameNever restart automaticallyEmit GameExited(crashed=true)

Storage Discovery

playos-init searches for the data partition in order:

  1. Partition with label playos-data
  2. Partition with GUID <TODO: define in playos-spec/schemas/disk-layout.json>
  3. 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:

CheckFailure action
Only one game at a timeReturn LaunchGameError(already_running)
Manifest file exists and is valid JSONReturn LaunchGameError(invalid_manifest)
api_versionPLAYOS_API_VERSIONReturn LaunchGameError(unsupported_api_version)
architecture matches running systemReturn LaunchGameError(invalid_manifest)
Executable exists and is executableReturn LaunchGameError(executable_not_found)
Manifest id matches directory nameReturn 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:

  1. Sets PLAYOS_GAME_ID, paths, and lifecycle environment
  2. Applies PR_SET_NO_NEW_PRIVS = 1
  3. Drops all capabilities
  4. Applies seccomp filter (Sprint 12 — not yet implemented)
  5. Applies Landlock rules (Sprint 12 — not yet implemented)
  6. Drops CAP_SETUID / CAP_SETGID
  7. 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:

  1. Deliver PLAYOS_LIFECYCLE_TERMINATE to the active game (if any) via lifecycle fd
  2. Wait up to 2 seconds for game to exit
  3. Send SIGKILL to game if still alive
  4. Send SIGTERM to compositor
  5. Wait up to 2 seconds for compositor to exit
  6. Sync all filesystems: sync()
  7. Call reboot(RB_POWER_OFF) or reboot(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

ReadingSource (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 stateDelegated to playos-platform-api (playos_power_get_info)

Thermal states

StateRange (default)Action
NORMAL< 75 °CNone
WARM75–85 °CNone (monitor)
HOT85–95 °CReject/back off PERFORMANCE profile
CRITICAL≥ 95 °CForce 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:

ProfileWire nameEPP value
BALANCED (0)balancedbalance_performance
POWER_SAVE (1)power_savepower
PERFORMANCE (2)performanceperformance

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:

  1. Deliver PLAYOS_LIFECYCLE_SUSPEND to the active game via the lifecycle fd
  2. Write mem to /sys/power/state
  3. 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_RESTARTS times
  • playos-init receives a RecoveryMode IPC command
  • Boot count exceeds A/B rollback limit and both slots are bad

Recovery mode:

  1. Kill all non-init processes
  2. Attempt to start a recovery UI (SimpleDRM or framebuffer, no AMDGPU required)
  3. Show: log viewer, factory reset, rollback slot, reboot, shutdown options
  4. 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.

OwnsDoes NOT own
DRM/KMS devices and output stateProcess spawning or supervision
wlroots backend, renderer, allocator, sceneGame installation or save management
Wayland display socketStorage layout
Display selection, orientation, refresh, hotplugBoot policy
Surface roles, z-order, visibility, focusNetwork policy
Trusted shell and overlay identitySystem 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:

  1. Device with an active connected display
  2. AMD (primary platform)
  3. Intel
  4. 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

StateShell surfaceGame surfaceOverlay surfaceInput routing
SHELL_FOREGROUNDVisible, focusedHiddenHidden→ Shell
GAME_STARTINGVisible (launching UI)HiddenHidden→ Shell
GAME_FOREGROUNDHiddenVisible, focusedHidden→ Game (filtered)
PLAYOS_UI_...HiddenVisible but unfocusedVisible, focused→ Overlay
TERMINATING_GAMEHiddenFading outVisible or hidden→ Overlay

Surface Policy

Shell surface

  • Always created at startup
  • Fullscreen, z-order: bottom
  • Visible in SHELL_FOREGROUND and GAME_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_TOKEN matches expected_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:

  1. Check buffer format, modifier, and size against the plane's supported formats
  2. If compatible: assign buffer directly to DRM plane (skip GPU composition)
  3. 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.destroy for 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 roles
  • playos_shell_v1 — emits lifecycle events and game state to the shell
  • playos_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:

  1. Compositor destroys the stale game surface
  2. Transitions immediately to SHELL_FOREGROUND
  3. Shell surface is made visible and focused
  4. 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

OwnsDoes NOT own
Persistent console UIProcess supervision
Controller-first navigationDRM/KMS or Wayland protocol
Game discovery and metadata presentationIPC protocol definitions
User-facing launch, resume, quit, crash flowsGame installation
Settings and status screensSave data management
Launch requests via restricted control IPCHardware 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 stateRendering behavior
ForegroundFull render at display refresh rate
Game launchingShow spinner; reduce to 10 FPS
Game is foregroundStop renderingSetTargetFPS(0) or skip draw call
Game backgrounded (overlay visible)Shell stays stopped; overlay renders
Returning to foregroundResume 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

InputAction
D-pad Up/DownMove focus up/down in a list or grid
D-pad Left/RightMove focus left/right in a grid
A buttonConfirm / select focused item
B buttonBack / cancel
StartOpen settings screen
SelectToggle sort/filter (library screen)
L1 / R1Page left/right (future)
System buttonNever 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" notification
  • LaunchGameError(invalid_manifest) — show "This game cannot be launched" notification
  • Timeout (no GameStarted within 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:

ElementSource
Battery % + charging iconplayos_power_get_info()
Thermal indicator (color)playos_power_get_info().thermal_state
System timeclock_gettime(CLOCK_REALTIME)
PlayOS versionplayos_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_v1 globals
  • Fullscreen xdg_toplevel surface — no decorations, no resize
  • wl_egl_window + EGL/GLES2 context (eglBindAPI(EGL_OPENGL_ES_API)), made current before raylib's rlgl init
  • Frame callbacks for v-sync pacing + eglSwapBuffers in SwapScreenBuffer()

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 in playos-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

OwnsDoes NOT own
Quick menu presentationGame logic
Volume and brightness HUDShell game library
Power menu (shutdown, restart, sleep)Process supervision
NotificationsDRM/KMS policy
Virtual keyboard (future)IPC protocol definitions
Performance profile selectorSave 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 seconds
  • WARNING — yellow badge, auto-dismiss after 5 seconds
  • ERROR — 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-config or make ally-config (default)

Production image

  • No interactive shell
  • No debug tools
  • Signed EFI artifact
  • Bounded logs
  • Triggered by: make ally-config PLAYOS_PROD=1 or via the release pipeline

Installer image

  • Contains: playos-installer Raylib 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

TargetMachineFirst buildIncremental
QEMU8-core workstation~45 min~2 min
ROG Ally8-core workstation~60 min~5 min
QEMU4-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

TrackPurpose
playos-kernel-ltsRelease and qualification — upstream Linux LTS
playos-kernel-nextHardware 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):

ToolPurpose
evtestTest input devices — verify controller button mapping
modetestTest DRM/KMS — verify connectors, encoders, CRTCs
straceTrace system calls of any process
gdbserverRemote GDB debugging (requires networking)
perfCPU/GPU performance profiling
aplay / arecordALSA audio testing
weston-infoInspect Wayland compositor info
BusyBox shellAvailable 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=1 kernel 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

VariableUsed byPurpose
PLAYOS_BACKENDlibplayos, compositorSelect backend: drm, headless, nested, stub
WAYLAND_DISPLAYAll Wayland clientsWhich Wayland socket to connect to
PLAYOS_TRUSTED_SHELLplayos-shellMarks client as trusted shell (=1)
PLAYOS_TRUSTED_OVERLAYplayos-overlayMarks client as trusted overlay (=1)
PLAYOS_LAUNCH_TOKENGame processOne-time launch identity UUID
PLAYOS_GAME_IDGame processGame identifier string
PLAYOS_INSTALL_PATHGame process/data/games/<id>
PLAYOS_SAVE_PATHGame process/data/saves/<id>
PLAYOS_CACHE_PATHGame process/data/cache/<id>
PLAYOS_LIFECYCLE_FDGame processRead end of lifecycle pipe
PLAYOS_COMPOSITOR_READY_FDplayos-compositorWrite end — signal readiness to playos-init
PLAYOS_AUDIO_DEVICElibplayosOverride 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:

LayerWhereWhat
1HostUnit tests — logic, IPC serialization, manifest parsing
2HostBuildroot clean build — compilation and packaging
3QEMU/OVMFBoot test — UEFI boot to shell prompt
4QEMUCompositor + shell smoke test
5QEMUGame lifecycle integration test
6ROG AllyPhysical device smoke test
7ROG AllyLong-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:

ComponentTests
playos-runtimeIPC message serialization/deserialization; version mismatch handling; all message types round-trip
playos-platform-apiInput state bitmask helpers; manifest parser; storage path construction; lifecycle fd read
playos-initBoot sequence logic; game launch validation; supervisor restart counter; shutdown sequence
playos-compositorState machine transitions (all valid and invalid transitions); GPU selection logic
playos-shellManifest 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-init is PID 1 (verified via /proc/1/comm in 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-0 socket exists
  • QueryStatus returns compositor_state=SHELL_FOREGROUND
  • playos-shell process 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
  • evtest on Ally controller shows expected event codes

Game lifecycle tests

  • sample-triangle launches 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 -9 on game PID: display returns to shell in ≤ 500ms
  • Second launch attempt while game runs: rejected with error

Audio tests

  • sample-audio plays 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.

TestDurationPass criterion
Continuous shell idle4 hoursNo crash, memory growth < 10 MB
Rapid launch/exit cycles100 iterationsAll succeed; no compositor restart
System button cycles200 iterationsAll transitions correct; no input loss
A/B update apply + rollbackFull cycleNew slot boots; rollback recovers correctly
Sustained GPU load (sample-triangle)30 minutesNo 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)

ToolPurpose
evtestVerify input device events
modetestVerify DRM connectors and modes
weston-infoInspect Wayland compositor
aplay -lList ALSA devices
speaker-test -t sineTest audio output
stress-ngCPU/GPU stress for thermal testing
perf statCPU performance metrics
apitraceTrace OpenGL calls (debugging)
piglitOpenGL 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-init sees playos.mode=install and spawns /usr/bin/playos-installer instead of playos-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/sdX must be the USB device (whole disk), not a partition. All data on the USB is overwritten.


4. Install to the Ally

  1. Power off the Ally.
  2. Insert the installer USB.
  3. Boot the Ally and select the USB as the UEFI boot device.
  4. The installer discovers internal fixed disks and shows model/size/partition count.
  5. Use the D-pad to select the target NVMe and press A.
  6. Hold A on the confirmation screen until the countdown bar completes.
  7. Wait for installation to reach 100% and show the success screen.
  8. Remove the USB and reboot.

The Ally then boots from the internal ESP EFI/BOOT/BOOTX64.EFI.


5. What the installer writes

StepAction
1Create GPT partition table on the target disk
2Format partition 1 as FAT32 (ESP, 512 MiB)
3Write the squashfs system image to playos-a (4 GiB)
4Reserve playos-b (4 GiB) empty
5Format misc (64 MiB, ext4)
6Format playos-data (remainder, ext4)
7Write EFI/BOOT/BOOTX64.EFI to the ESP
8Sync 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.
  • /data first-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-data on an internal disk (currently the trigger is the explicit playos.mode=install command 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):

#LabelSizeFilesystemPurpose
1ESP512 MiBFAT32EFI System Partition; EFI/BOOT/BOOTX64.EFI
2playos-a4 GiBsquashfs (read-only)Active system slot
3playos-b4 GiBsquashfs (read-only)Inactive system slot (A/B updates, Sprint 11.5)
4misc64 MiBext4A/B slot metadata (/data/misc-style state)
5playos-dataremainderext4Writable 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-a and playos-b are immutable system slots. misc carries the tiny A/B slot state (which slot is booted / healthy); it is formatted ext4 for convenience.
  • playos-data holds 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:

#LabelSizeFilesystemPurpose
1ESP256 MiBFAT32EFI System Partition; EFI/BOOT/BOOTX64.EFI
2playos-a2048 MiBext2System image (EFI stub kernel lives on ESP)
3playos-dataremainderext4Writable 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:

#LabelSizeFilesystemPurpose
1ESP256 MiBFAT32Installer kernel (playos.mode=install) as EFI/BOOT/BOOTX64.EFI
2playos-a2048 MiBext2Install payload: /rootfs.squashfs + /BOOTX64.EFI
3playos-dataremainderext4Scratch / 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
1ROG Ally boots directly from UEFI into PlayOS
2Linux kernel + initramfs are available as a UEFI-bootable EFI artifact
3playos-init runs as PID 1
4playos-compositor permanently owns DRM/KMS and the Wayland session
5playos-shell remains alive as the persistent controller-first UI
6Compositor uses wlroots with AMDGPU, DRM/KMS, GBM, EGL, and Mesa
7Shell renders through Wayland using the Raylib PlayOS backend
8Shell and sample game consume the public playos-platform-api C ABI
9Trusted launch, lifecycle transport, and compositor-control remain internal to playos-runtime
10Shell requests game launch; playos-init spawns and supervises it
11Compositor waits for game's first valid frame before switching foreground
12Game renders with hardware acceleration and receives controller input
13Reserved System button returns to PlayOS UI and backgrounds/pauses the game
14Resume returns to the same running game without restarting it
15Game outputs audio through ALSA
16Clean exit and crash both return safely to the existing shell
17Games and saves persist on a separate ext4 partition
18System image is immutable
19Recovery mode usable without accelerated graphics

Sprint Plan

SprintTitlePrimary Outcome
0Build and UEFI FoundationReproducible Buildroot factory boots a minimal PlayOS EFI image in QEMU/OVMF
1playos-init and Minimal Boot SupervisionReal playos-init as PID 1 with versioned private control IPC skeleton
2Compositor Skeleton and Wayland SessionMinimal wlroots compositor with a Wayland session and one trusted fullscreen client
3ROG Ally Kernel and Device Bring-UpReliable USB boot, essential ROG Ally devices working, first Platform API input contract
4AMDGPU and Native DRM/KMSCompositor permanently owns the Ally display via AMDGPU and DRM/KMS
5Raylib-Powered PlayOS ShellHardware-accelerated Raylib shell consuming the public PlayOS Platform API
6Persistent Storage and Game DiscoveryPersistent ext4 storage, Platform API paths, shell-visible game discovery
7Game Launch, Lifecycle, System Button, and OverlayComplete console lifecycle: launch, overlay, background, resume, crash recovery
8ALSA AudioReliable ALSA audio with safe public controls across lifecycle transitions
9Power, Battery, Thermal, and Suspend FoundationsSafe power behavior exposed through a restricted public Platform API
10Installer and Internal-Disk DeploymentTested installation path from removable media to ROG Ally internal SSD
11Immutable Images and A/B UpdatesSigned, atomic A/B system updates with automatic rollback
11.6Developer SSH (Dropbear) + Minimal Wired Network Bring-UpUSB-C Ethernet SSH (key auth) for on-device debugging; full Wi-Fi stays Sprint 16
12Security HardeningHardened boundary between public Platform API, trusted runtime control, and games
13Intel ExpansionArchitecture and Platform API backend portable to Intel graphics
14Production ReadinessSigned preview release with versioned public Platform API
15Game Developer SDKSelf-contained playos-sdk (musl toolchain + libplayos/libraylib) with device/desktop/emulator testing
16playos-net (Wi-Fi)D-Bus-free Wi-Fi (wpa_supplicant + dhcpcd + playos-net bridge) driven through playos-runtime
17Touch Input + On-Screen Keyboard (OSK)Touch end-to-end (compositor → raylib backend) plus a reusable system OSK
18C# Shell Reimplementation Assessment (Post-MVP Spike)Feasibility assessment only — no C# shell implemented
19Marketplace Assessment (Post-MVP)Assessment and spec-first sequencing only — no marketplace code
20Native Media & Browser Client Strategy (Post-MVP)Assessment of native Spotify/YouTube/YouTube Music/browser clients — Netflix out of scope
21Multiple Local User Profiles (Post-MVP)Assessment/design of console-style local profiles with per-profile isolated saves/settings — no implementation
22LVGL Shell UI Spike (Post-MVP)Gated LVGL-via-raylib texture spike with controller navigation + go/no-go — no shell port

Execution Rules

  1. Sprints follow dependency order unless an ADR explicitly changes the sequence.
  2. A sprint begins only after its required predecessor exit criteria are satisfied.
  3. Each sprint must end with a demonstrable and testable system outcome.
  4. Architecture changes discovered during implementation must be captured in playos-spec.
  5. 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-tools host helper (download + verify + stage to USB/SD) first, then on-device download after Sprint 16 (playos-net)
  • playos-input service — 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 .play content 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 -kernel shortcut 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-external skeleton
  • 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

RepoRequired work
playos-specinitial project docs and ADRs that define repo boundaries
playos-refdistroBuildroot tree, defconfig, image generation, make targets, CI
playos-platform-apirepo scaffold only
playos-runtimerepo scaffold only
playos-compositorrepo scaffold only
playos-shellrepo 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 startedin progressblocked or done.

Task IDTaskPrimary repoStatusNotes / evidence
S0-T1Create or validate the six-repository structurecross-repodoneREADME.md, CONTRIBUTING.md, AGENTS.md, .gitignore in all 6 repos
S0-T2Add the Buildroot integration skeletonplayos-refdistrodonebr2-external/ with Config.in, external.mk, external.desc, 5 package stubs
S0-T3Create the QEMU x86_64 defconfigplayos-refdistrodoneplayos_qemu_x86_64_defconfig with EFI, initramfs, virtio, serial console
S0-T4Build the minimal kernel + initramfs pathplayos-refdistrodoneboard/common/rootfs-overlay/init, board/common/busybox.config
S0-T5Produce the real UEFI boot artifactplayos-refdistrodonescripts/qemu-boot-check.sh boots OVMF with kernel+initramfs, asserts banner
S0-T6Standardise developer commandsplayos-refdistrodoneMakefile with setup, qemu-, ally- stubs, clean, distclean
S0-T7Create and enforce version pinningplayos-refdistrodoneversions.lock with real Git SHAs for all 6 PlayOS components
S0-T8Add first-pass CIplayos-refdistrodone.github/workflows/qemu-build.yml with build+boot+artifact upload
S0-T9Create Ubuntu Server host environment setup scriptplayos-refdistrodonescripts/setup-ubuntu.sh — idempotent, detects Ubuntu, validates tools
S0-T10Create shared bash logging frameworkplayos-refdistrodonescripts/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-spec
    • playos-platform-api
    • playos-runtime
    • playos-compositor
    • playos-shell
    • playos-refdistro
  • Add or validate baseline repo files:

    • README.md
    • CONTRIBUTING.md
    • AGENTS.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-external tree.
  • Add top-level Config.in, external.mk, and external.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 /init script that:

    1. mounts virtual filesystems
    2. prints a clear boot banner
    3. drops to a BusyBox shell
  • Do not implement the real playos-init here. 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 -kernel as 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.sh for 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 (-y flags, 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, ovmf package presence)
  • print a clear summary: what was installed, what was already present, what failed
  • use scripts/lib/playos_log.sh for 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_LEVEL environment variable controls minimum visible level (default: INFO)
  • a playos_log_step helper 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 source playos_log.sh
  • echo is not used for informational output in any PlayOS script; use playos_log_* instead
  • CI step names in .github/workflows/ use playos_log_step to 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.sh must 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 echo for informational output.
  • This convention applies to Sprint 0 scripts and must be maintained by all later sprints.

Verification and Evidence

EvidenceHow it is produced
Repo proofdirectory listing or project inventory of all six repos
Buildroot proofbr2-external tree present and referenced by the build
Boot proofserial log showing EFI boot and BusyBox shell
UEFI proofOVMF boot path, not direct kernel boot
Reproducibility proofbash scripts/setup-ubuntu.sh && make qemu-build from a clean Ubuntu Server 22.04
Versioning proofversions.lock populated with pinned values
Setup proofsetup-ubuntu.sh runs to completion on a fresh machine with a clean summary
Logging proofall sprint scripts emit structured playos_log_* output, not bare echo

Acceptance Criteria

  • all six repositories exist with baseline repo files
  • playos-refdistro contains a valid br2-external skeleton
  • playos_qemu_x86_64_defconfig exists
  • a BusyBox initramfs boots through OVMF in QEMU
  • /init mounts the expected virtual filesystems and reaches a shell
  • the developer Makefile exposes the standard command surface
  • versions.lock exists and uses pinned values
  • CI can build and boot-check the image automatically
  • scripts/setup-ubuntu.sh prepares a fresh Ubuntu Server 22.04 LTS machine end-to-end
  • scripts/setup-ubuntu.sh is idempotent and validates all installed tools
  • scripts/lib/playos_log.sh exists and is sourced by all project scripts
  • all scripts emit playos_log_* output — no bare echo for 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):

  1. Makefile include used shell redirects → changed to -include
  2. external.desc used = instead of : → Buildroot requires colon format
  3. BR2_EXTERNAL_* variable used wrong casing → must match external.desc name field exactly
  4. Package .mk stubs missing _SITE definition → required for local site method
  5. Kernel config option BR2_LINUX_KERNEL_CUSTOM_CONFIG wrong → correct name is BR2_LINUX_KERNEL_USE_CUSTOM_CONFIG
  6. Kernel 6.6 incompatible with GCC 15 C23 bool keywords → bumped to 6.12 + C23 guard patch
  7. BR2_KERNEL_HEADERS_AS_KERNEL unreliable → explicit BR2_KERNEL_HEADERS_6_12=y
  8. QEMU -cpu host requires KVM → changed to -cpu qemu64 for TCG compatibility
  9. Colour function _playos_colour_* returns 1 when not a TTY → added || true to && chains (fixes set -e crash)
  10. Bare echo on qemu-boot-check.sh line 124 → changed to playos_log_debug

Board patches added:

  • br2-external/board/patches/linux/0001-c23-bool-fix.patch — guards typedef _Bool bool and enum {false, true} with __STDC_VERSION__ < 202311L
  • BR2_GLOBAL_PATCH_DIR set 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
  • /init may now be replaced by the real playos-init
  • scripts/setup-ubuntu.sh is the authoritative way to prepare a build host
  • scripts/lib/playos_log.sh exists 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:

  1. A deterministic PID 1 implementation exists and owns system bring-up.
  2. Process supervision exists before graphics, shell, or games become real.
  3. 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-refdistro can still boot the Sprint 0 QEMU image.
  • The boot artifact still uses the Buildroot br2-external tree from playos-refdistro.
  • playos-runtime exists and is available for shared IPC headers/helpers.
  • No later sprint code is assumed to exist yet. playos-compositor may 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-init in 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 (PLOS magic + length + JSON body).
  • Trusted access policy: only processes in group playos-trusted may 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

RepoRequired work
playos-refdistroAdd the real playos-init source tree, package metadata, boot integration, and QEMU tests
playos-runtimeAdd shared IPC framing/types/helpers used by PID 1 and test clients
playos-specUpdate 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 startedin progressblocked or done.

Task IDTaskPrimary repoStatusNotes / evidence
S1-T1Bootstrap the playos-init source treeplayos-refdistrodoneCMakeLists.txt, init.h, init.c, test_init_state.c
S1-T2Implement mandatory PID 1 boot responsibilitiesplayos-refdistrodonemount.c, logging.c, shutdown.c, child_process.c
S1-T3Discover and mount the data partitionplayos-refdistrodonemount.c scans PARTLABEL=playos-data, creates dirs
S1-T4Add minimal compositor supervisionplayos-refdistrodonesupervisor.c with restart policy (3 restarts per 60-second window, 500ms delay)
S1-T5Implement the trusted control IPC serverplayos-refdistro, playos-runtimedoneipc_framing.c, ipc_server.c, ipc_client.c at /run/playos/control.sock
S1-T6Implement stub game lifecycle handlingplayos-refdistro, playos-runtimedoneLaunchGame/TerminateGame via IPC, SIGCHLD reaper
S1-T7Integrate with Buildrootplayos-refdistrodonecmake-package, installs as /init
S1-T8Add test coverage and evidence captureplayos-refdistro, playos-runtimedone4 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_state as 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 (SIGCHLD handling or waitpid loop).

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, and GameCrashed messages.
  • 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 /init from 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:

  1. try documented GPT partition type GUID or partition label
  2. if multiple candidates exist, log the ambiguity and fail safe
  3. never auto-format
  4. never silently fall back to the root filesystem

Child process supervision

  • playos-init must 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:

EvidenceHow it is produced
PID 1 proof/proc/1/comm or ps from QEMU shell
Mount proofmount or /proc/mounts output showing /dev, /proc, /sys, /run, /data
IPC prooftest client transcript for QueryStatus
Supervision prooflog showing compositor restart count increasing
Recovery prooflog showing retry limit exceeded and halt path entered
Game lifecycle prooftest client transcript for LaunchGame and TerminateGame

Acceptance Criteria

  • playos-init is PID 1 as verified by /proc/1/comm or ps
  • /dev, /proc, /sys, and /run are mounted by playos-init
  • the data partition is discovered, mounted at /data, and first-boot directories are created
  • playos-init supervises a compositor placeholder process and restarts it on exit
  • repeated compositor failure enters the documented recovery halt path
  • /run/playos/control.sock exists with mode 0660
  • an authorized client can connect and receive StatusReport
  • an unauthorized client is rejected clearly
  • LaunchGame spawns a stub process and emits GameStarted
  • TerminateGame stops the stub process and emits GameExited
  • Shutdown performs an orderly halt path
  • zombie processes are reaped correctly
  • the Buildroot image boots through the real /init binary
  • 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-init can 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:

  1. The compositor exists as a supervised process, not just an idea in the spec.
  2. A stable Wayland socket and lifecycle exist before the shell is implemented.
  3. 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-init can supervise a child process reliably. (supervisor.c readiness polling implemented)
  • playos-runtime/protocols/playos-v1.xml exists 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_base support 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

RepoRequired workStatus
playos-compositorImplement the wlroots compositor skeleton and test client✅ Done
playos-runtimeMaintain the private protocol XML and scanner-generated glue✅ Done — protocol XML staged in Buildroot (Sprint 2.5)
playos-refdistroAdd Buildroot packaging and dependencies for wlroots and the compositor✅ Done; QEMU build passed
playos-specClarify 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 IDTaskPrimary repoStatusNotes / evidence
S2-T1Bootstrap the compositor projectplayos-compositor✅ doneHost build produces 4 targets: playos-compositor, compositor-headless-test, compositor-nested-test, playos-test-client
S2-T2Create backend selection and startup flowplayos-compositor✅ donePLAYOS_BACKEND env var (headless/wayland), wl_display, backend, renderer, allocator, scene, output layout
S2-T3Implement the minimal renderable sessionplayos-compositor✅ donexdg_wm_base, xdg_toplevel fullscreen, wlr_scene rendering, frame events
S2-T4Implement trusted-shell identity skeletonplayos-compositor✅ donetrusted_client.c with role tracking (shell/overlay roles); temp env-var mechanism
S2-T5Add the private Wayland protocol skeletonplayos-runtime, playos-compositor✅ doneplayos-v1.xml (4 interfaces), scanner-generated code, compositor advertises global
S2-T6Add a test clientplayos-compositor✅ doneWayland test client: wl_shm PlayOS blue (0xFFD66B00), xdg_toplevel fullscreen, connects and exits cleanly
S2-T7Wire playos-init supervision and readinessplayos-refdistro, playos-compositor✅ donesupervisor.c polls /run/playos/compositor-ready (5s timeout); main.c waits COMPOSITOR_RUNNING before launching test clients
S2-T8Integrate with Buildroot and testsplayos-refdistro, playos-compositor✅ doneQEMU 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-client to /usr/bin/
  • Source provisioning: make setup clones PlayOS-Foundation/playos-compositor.git into src/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

EvidenceStatusDetails
Socket proofHeadless test logs socket=wayland-N on startup
Render proofTest client connects, maps PlayOS blue fullscreen surface
Supervision proofsupervisor.c polls /run/playos/compositor-ready
Readiness proofCompositor writes readiness file with PID + socket info
Protocol proofwayland-scanner generates playos-v1-protocol.c/.h in build
Nested testcompositor-nested-test builds; skips gracefully if no WAYLAND_DISPLAY
QEMU end-to-endBuild passes — bzImage + rootfs.tar generated (Spr 2.5 verified)
Host build4 targets build cleanly with 0 warnings

Acceptance Criteria

  • playos-compositor builds 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.xml skeleton is generated with wayland-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

  1. wlroots 0.17 requires -DWLR_USE_UNSTABLE — without it, every wlroots header fails with #error.
  2. xdg-shell-protocol.h must be generated — not shipped by libwlroots-dev on Ubuntu. Use wayland-scanner from wayland-protocols XML.
  3. _POSIX_C_SOURCE=199309L needed before wlroots headers for struct timespec.
  4. _DEFAULT_SOURCE needed for setenv(), mkstemp(), and other POSIX extensions.
  5. wlr_scene_xdg_surface_create takes wlr_scene_tree*, not wlr_scene*. Use &scene->tree.
  6. wlr_allocator_autocreate needs #include <wlr/render/allocator.h> — not transitively included.
  7. Buildroot swrast driver removed — use softpipe for software rendering in QEMU.
  8. Source repos must be cloned into src/make setup now handles this. Buildroot .mk files expect src/playos-compositor/ and src/playos-init/.
  9. Protocol XML in compositor repo — avoids cross-repo build dependency. Canonical source stays in playos-runtime.
  10. File-based readiness beats pipe inheritance — simpler to debug, inspectable on disk.

Commits

RepoCommitDescription
playos-compositoree17993Sprint 2: wlroots compositor skeleton + nested test + CMake fixes (12 files, ~3700 lines)
playos-refdistro2b098c0Sprint 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-init can 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:

  1. IPC duplication — two diverging copies of server/client code will create subtle bugs as both evolve.
  2. Version pinning is decorativeversions.lock has precise SHAs but make setup ignores them, making builds non-reproducible.
  3. Structural drift — file locations don't match the spec, confusing new contributors.
  4. Dead code — deprecated files still on disk.
  5. Stub driftplayos-runtime Buildroot 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). The playos-runtime copy is removed and playos-runtime depends on playos-init's IPC source via a shared include path or becomes a protocol-only package.
  • Version pinning enforcement: make setup reads versions.lock and checks out the exact SHA for every component.
  • Board directory location: remains under br2-external/board/ (correct for Buildroot BR2_EXTERNAL paths). 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 setup checkout pinned SHAs from versions.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-runtime Buildroot 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

RepoRequired work
playos-refdistroIPC unification, Makefile version pinning, mount.c GPT GUID, remove linux.fragment, update playos-runtime package, update board paths in docs
playos-runtimeRemove duplicated IPC source files, keep only protocol XML + headers, update CMakeLists.txt
playos-specUpdate 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 IDTaskPrimary repoStatusNotes / evidence
S2.5-T1Unify IPC code — make playos-init/ipc/ canonical, remove playos-runtime duplicateplayos-refdistro, playos-runtimedoneframe_validate added to ipc_client.c; playos-runtime IPC C sources removed; CMakeLists.txt → protocol-only
S2.5-T2Enforce version pinning in make setupplayos-refdistrodoneAlready implemented in Makefile (clones + checkout pinned SHAs from versions.lock)
S2.5-T3Update Sprint-0.md: board directory locationplayos-specdoneboard/ → br2-external/board/ in paths and expected tree; linux.fragment removed
S2.5-T4Update Sprint-1.md: restart policy (3/60s)playos-specdone(5/10s limit) → (3 restarts per 60-second window, 500ms delay)
S2.5-T5Remove deprecated linux.fragmentplayos-refdistrodoneAlready deleted; not referenced in defconfig
S2.5-T6Implement GPT partition GUID search in mount.cplayos-refdistrodoneStrategy 4: scans GPT headers on block devices for PlayOS data partition type GUID
S2.5-T7Wire playos-runtime Buildroot package to install protocol XMLplayos-refdistrodonecmake-package pointing to ../src/playos-runtime; installs playos-v1.xml
S2.5-T8Update Sprint-2.md acceptance criteria after QEMU verificationplayos-specdoneQEMU 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:

  1. Reconcile the diverged files. Diff playos-runtime/src/ipc_server.c against playos-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 for ipc_client.c and lifecycle_fd.c.
  2. Verify the merged files compile and pass tests. Run cd playos-refdistro/src/playos-init/build && cmake .. && make && ctest to confirm playos-init still builds and all host tests pass.
  3. 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, and playos-runtime/tests/test_ipc_framing.c.
  4. Update playos-runtime CMakeLists.txt. Remove the playos-ipc library target and the playos-ipc-tests executable target. Keep only the protocol XML install target (see S2.5-T7).
  5. 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.txt already references its own ipc/ directory).
  6. 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 that playos-runtime no longer ships IPC — it ships only protocol XML.

Done when:

  • playos-runtime/src/ contains no .c files (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:

  1. Parse versions.lock in the Makefile. Add a target or include that reads the PLAYOS_*_COMMIT variables from versions.lock. Since versions.lock uses 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
  1. Add git checkout <SHA> after each clone in the setup target. After git 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).

  1. Add a --force flag to make setup. make setup-force removes src/playos-init/ and src/playos-compositor/ before re-cloning, ensuring a clean checkout at the pinned SHA.

  2. Document the pinning behavior in the Makefile header comment.

Done when:

  • make setup produces src/playos-init/ at the commit specified in versions.lock.
  • make setup produces src/playos-compositor/ at the commit specified in versions.lock.
  • Running make setup twice 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:

  1. Edit playos-spec/src/sprints/Sprint-0.md — in the "Expected Files and Directories" section, change:
    board/
    ├── common/
    └── qemu-x86_64/
    
    to:
    br2-external/board/
    ├── common/
    ├── patches/
    └── qemu-x86_64/
    
  2. Add a note explaining that board files live under br2-external/ because Buildroot's BR2_EXTERNAL variable 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:

  1. 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)".
  2. 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:

  1. Delete playos-refdistro/br2-external/board/qemu-x86_64/linux.fragment.
  2. Verify the defconfig doesn't reference it: grep linux.fragment br2-external/configs/playos_qemu_x86_64_defconfig — should return nothing.
  3. 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:

  1. Read the current mount.c to understand the existing 5-strategy search pattern and where the TODO is.
  2. 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.
  3. 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.
  4. 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).
  5. Add a host test (or extend existing tests) that validates the GPT header parsing logic with a mocked GPT disk image.

Done when:

  • mount.c no 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:

  1. Update playos-refdistro/br2-external/package/playos-runtime/playos-runtime.mk:
    • Change _SITE to point to the actual cloned source: $(BR2_EXTERNAL_PlayOS_PATH)/../src/playos-runtime (requires make setup to 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.xml into $(STAGING_DIR)/usr/share/playos/protocols/.
  2. Update Makefile setup target to clone playos-runtime into src/playos-runtime/ (if not already present).
  3. Update versions.lock if a PLAYOS_RUNTIME_COMMIT pin exists (it does).
  4. Update playos-compositor.mk to 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 setup clones playos-runtime into src/playos-runtime/.
  • make qemu-build installs playos-v1.xml into 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:

  1. Run make qemu-build in playos-refdistro and verify it completes successfully.
  2. If the build passes, run make qemu-run (or scripts/qemu-boot-check.sh) and verify the compositor starts under playos-init.
  3. 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.
  4. 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

  1. T5 first (remove linux.fragment) — trivial, warms up the workflow.
  2. T2 second (version pinning) — ensures future clones are reproducible.
  3. T1 third (IPC unification) — the most complex change, touches two repos.
  4. T6 fourth (GPT GUID) — isolated change in mount.c.
  5. T7 fifth (playos-runtime package) — depends on T1 (IPC files removed).
  6. T8 sixth (verify QEMU build) — depends on all code changes being done.
  7. 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

EvidenceHow it is produced
IPC unification proofplayos-runtime/src/ contains no .c files; playos-init host tests pass
Version pinning proofmake setup followed by git -C src/playos-init log -1 --format=%H matches versions.lock
Spec update proofdiff between old and new Sprint-0.md, Sprint-1.md shows corrections
Deprecated file removal prooflinux.fragment does not exist in the repo
GPT GUID proofmount.c has no TODO; host test for GPT parsing passes
Protocol staging proofplayos-v1.xml exists in Buildroot staging after make qemu-build
QEMU build proofmake 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 .c source files
  • playos-init builds and all host tests pass after IPC unification
  • make setup checks out the exact commit SHA from versions.lock for playos-init and playos-compositor
  • make setup is 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.h constants (3/60s, 500ms)
  • linux.fragment is deleted from the repository
  • mount.c implements GPT partition GUID search (no TODO placeholder)
  • A host test exercises the GPT parsing logic
  • playos-runtime Buildroot package installs protocol XML into staging
  • make qemu-build completes 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 setup is 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_SYSTEM and PLAYOS_BUTTON_QUICK_MENU are 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

RepoRequired work
playos-refdistroAlly defconfig, firmware packaging, USB image target, device verification tooling
playos-platform-apiPublic input header contract and evdev prototype backend
playos-specInput 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 startedin progressblocked or done.

Task IDTaskPrimary repoStatusNotes / evidence
S3-T1Create the Ally defconfig and boot image pathplayos-refdistrodoneplayos_ally_defconfig, Makefile targets
S3-T2Enable the required kernel subsystemsplayos-refdistrodoneboard/ally/linux.config, EFI stub, AMDGPU, all subsystems
S3-T3Package required firmwareplayos-refdistrodoneAMDGPU blobs + AMD ucode via linux-firmware
S3-T4Add device verification toolingplayos-refdistrodonetools/hw-check/ (6 scripts), all PASSED on Ally
S3-T5Finalise the public input contractplayos-platform-apidoneplayos_input.h with bitmask buttons
S3-T6Implement the evdev prototype backendplayos-platform-apidonesrc/backends/backend_evdev.c, auto-discovery
S3-T7Document the hardware mappingplayos-platform-api, playos-specdonedocs/rog-ally-input-mapping.md
S3-T8Capture physical hardware evidenceplayos-refdistrodoneAlly 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-image target 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:

SubsystemRequired symbols or equivalent
UEFI and x86_64CONFIG_EFI_STUB, CONFIG_ACPI, CONFIG_X86_64
PCIe and IOMMUCONFIG_PCI, CONFIG_AMD_IOMMU
Virtual filesystemsdevtmpfs, procfs, sysfs, tmpfs
Serial consoleCONFIG_SERIAL_8250_CONSOLE or equivalent
DRM/KMS and AMDGPUCONFIG_DRM, CONFIG_DRM_AMDGPU, CONFIG_DRM_AMD_DC
Recovery graphicsCONFIG_DRM_SIMPLEDRM
USB xHCICONFIG_USB_XHCI_HCD
InputCONFIG_HID, CONFIG_INPUT_EVDEV, CONFIG_HID_ASUS
AudioCONFIG_SND_HDA_INTEL, CONFIG_SND_SOC, AMD ACP support
StorageCONFIG_BLK_DEV_NVME
FilesystemsCONFIG_FAT_FS, CONFIG_EXT4_FS
Power and thermalCONFIG_THERMAL, CONFIG_BATTERY_ACPI, CONFIG_X86_AMD_PSTATE
WatchdogCONFIG_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:

DeviceVerification 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
Audioaplay -l and a short playback check succeed
NVMeblock 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_defconfig exists 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.log is produced by the verification tooling
  • playos_input.h defines 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

RepoCommitsKey Artifacts
playos-refdistro9305481, d31ec48, 561b701, b6fbb69, e9b0c44, 65117f2Ally defconfig, kernel config, USB image script, flash script, hw-check tools
playos-platform-apid7a0050, 580026cInput header, evdev backend, input mapping docs, API stubs

Key Technical Decisions

  1. 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.

  2. Embedded initramfsBR2_LINUX_KERNEL_INITRAMFS_SOURCE is critical. Without it the kernel panics because it has no rootfs. The 178MB cpio gzips to ~59MB inside the bzImage.

  3. No modules — all kernel drivers built-in (# CONFIG_MODULES is not set). Simplifies the boot path — no module loading, no initramfs module discovery.

  4. BusyBox retained for debugging — production should strip it, but kept for Sprint 3 hardware verification (need a shell to run hw-check).

Lessons Learned

  1. lsblk columns break on model names with spaces — "SanDisk 3.2Gen1" gets split into two columns. Use lsblk -P (key=value pairs) for reliable parsing.

  2. 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.

  3. Buildroot BR2_LINUX_KERNEL_INITRAMFS_SOURCE is easy to miss — the kernel compiles fine without it but panics at boot. Consider adding a post-build check that verifies initramfs is embedded.

  4. 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).

  5. SP5100 is the watchdog chip on ROG Ally — needs CONFIG_SP5100_TCO, not generic iTCO.

  6. Kernel cmdline fallback mattersCONFIG_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-compositor permanently 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

RepoRequired work
playos-compositorNative DRM/KMS path, GPU discovery, output setup, renderer logging, test client updates
playos-refdistroDefconfig: 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-specClarify 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 startedin progressblocked or done.

Task IDTaskPrimary repoStatusNotes / evidence
S4-T1Add deterministic GPU discoveryplayos-compositordonesrc/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-T2Bring up native DRM/KMS through wlrootsplayos-compositordonesrc/drm_backend.c + src/output_modes.c — WLR_BACKENDS=drm, preferred mode selection, scale 1.0, wired into compositor_start lifecycle
S4-T3Initialise the GBM/EGL/Mesa rendering pathplayos-compositordonesrc/renderer_gbm_egl.c — EGL pbuffer GL query, logs renderer/vendor/GLES version, detects software rendering (llvmpipe/softpipe/swrast)
S4-T4Present a hardware-accelerated test clientplayos-compositordonetools/test-client/src/main.c — EGL/GLES2 rendering, animated color frame with moving accent bars (~60fps), GPU diagnostics in window title
S4-T5Add recovery and diagnostics behaviourplayos-compositordonesrc/diagnostics.c — logs to /run/playos/log/compositor.log, simpledrm fallback, phase-specific failure logging, mkdir -p /run/playos/log
S4-T6Update Buildroot graphics dependenciesplayos-refdistrodoneplayos_ally_defconfig: BR2_PACKAGE_MESA3D_GBM=y added. BR2_PACKAGE_PLAYOS_COMPOSITOR already present from Sprint 3
S4-T7Preserve earlier test modesplayos-compositor, playos-refdistrodonePLAYOS_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 in playos_ally_defconfig.
  • Remaining: Enable BR2_PACKAGE_MESA3D_GBM (GBM buffer allocation) and add BR2_PACKAGE_PLAYOS_COMPOSITOR=y to the defconfig. The compositor's Config.in already selects 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-compositor with 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.0 for 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

EvidenceHow it is produced
GPU selection proofcompositor log entries for card, render node, PCI IDs
Output proofcompositor log entries for connector and selected mode
Acceleration proofrenderer and GLES version in logs
On-screen proofvisible animated test client on the Ally screen
Regression proofQEMU headless path still runs after native DRM work

Acceptance Criteria

  • playos-compositor starts 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-test passes)
  • nested Wayland validation still works (verified: compositor-nested-test skips 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-compositor can present a diagnostic client on real hardware — verified: test client rendered animated orange bars at 119.8 fps.
  • playos-platform-api already has all 8 public headers declared, the Sprint 3 input contract is finalized (bit positions fixed, evdev backend implemented — src/backends/backend_evdev.c at 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.h matches the spec.
  • The shell can rely on a working Wayland session (wayland-0 at /run/playos) and hardware-accelerated rendering path.
  • The compositor scene is pre-configured: dark blue #0a1628 background 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-api provides the public API consumed by the shell. Exception for input: the shell needs SYSTEM/QUICK_MENU button access, which libplayos input API strips (those buttons are reserved, never delivered to game processes). The shell reads controller input directly through the evdev backend provided by playos-platform-api or through a future trusted compositor protocol.
  • Library content source for this sprint: stub manifests in /data/games/ (retrieved via playos_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-0 at XDG_RUNTIME_DIR=/run/playos (matching the current compositor setup from Sprint 4)
  • Logging: persistent logs at /data/log/shell.log following the Sprint 4 child_log_redirect() pattern established in supervisor.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

RepoRequired work
playos-shellRaylib shell app, custom PlayOS backend integration, screens, controller navigation
playos-platform-apiHeaders 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-refdistroRaylib 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-specshell 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 startedin progressblocked or done.

Task IDTaskPrimary repoStatusNotes / evidence
S5-T1Finalise the shell-facing public API surfaceplayos-platform-apidoneget_games_path() added; system/storage/lifecycle/logging stubs implemented
S5-T2Add the custom Raylib PlayOS backendplayos-shelldoneFirst shipped raw EGL/GLES2; Raylib 6.0 rcore_playos.c landed via Sprint 5.5 (1046262)
S5-T3Bootstrap the shell application structureplayos-shelldonesrc/{main,input,render_util,screen_*}.c + include/shell.h
S5-T4Implement library data loading from stub manifestsplayos-shell, playos-refdistrodoneManifests discovered from /data/games/ via playos_storage_get_games_path()
S5-T5Implement controller-first navigation and focus rulesplayos-shelldoneShell-owned direct evdev (input.c); reserved buttons preserved
S5-T6Build the library, detail, and status-bar UIplayos-shelldoneLibrary + Game Detail (plus Home + Settings screens added)
S5-T7Add shell lifecycle handling and persistent process behaviorplayos-shell, playos-platform-apidoneplayos_lifecycle_poll() per frame; persists under supervision
S5-T8Integrate Raylib and shell packaging into Buildrootplayos-refdistrodoneReal cmake-package; PLAYOS_SHELL_USE_RAYLIB=ON (Raylib 6.0)
S5-T9Add validation, stub content, and runtime evidence captureplayos-shell, playos-refdistrodoneValidated 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:

FunctionImplementation 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 (not playos_lifecycle_event_t)
  • PlayOSLogLevel (not playos_log_level_t)
  • playos_storage_get_saves_path(void) and playos_storage_get_cache_path(void) — no game_id parameter (game isolation is via PLAYOS_GAME_ID env 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:

    1. event/input polling
    2. state update
    3. 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.json data 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
  • A confirms/selects
  • B returns/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:

  1. Link the evdev backend directly (bypassing libplayos for input), or
  2. Consume input through a compositor protocol (e.g., the future session_manager protocol in playos-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:

  1. Library screen

    • scrollable grid or list of installed games
    • visible focus state
    • placeholder icon support
  2. Game detail screen

    • game name
    • description
    • version
    • launch/select affordance (may be stubbed)
  3. 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.mk as a real cmake-package building from $(BR2_EXTERNAL_PlayOS_PATH)/../src/playos-shell.
  • Add Raylib with the custom PlayOS backend. Strategy: Create a vendored Raylib source in playos-shell rather than patching upstream — the rcore_playos.c backend 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.c to launch /usr/bin/playos-shell instead of /usr/bin/playos-test-client after the compositor is ready.
  • Log shell output to /data/log/shell.log using the existing child_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_rect at 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_frame commits the scene output via wlr_scene_output_commit() and sends frame_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

EvidenceHow it is produced
UI boot proofphoto/video or direct observation of the shell on the Ally
Data-loading proofshell log showing three manifests loaded
Navigation prooflog or captured session showing controller-driven focus changes
API proofsuccessful compile/link against public libplayos headers
Persistence proofshell remains alive under supervision during idle runtime
Regression proofshell 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 libplayos APIs only
  • three stub game entries are loaded from /data/games/
  • controller-only navigation works on the library and detail screens
  • A enters the detail screen and B returns 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):

  1. Spec says Raylib; code says raw GLES2. playos-shell-spec.md and ADR-0006 commit to a Raylib shell with a custom rcore_playos.c backend. The implementation deferred Raylib and instead wrote a bespoke single-shader GLES2 renderer plus a hand-embedded 5×7 bitmap font in render_util.c.
  2. The vendored Raylib is stale and inert. playos-shell/external/raylib/ vendors Raylib 5.5, but PLAYOS_SHELL_USE_RAYLIB is OFF in both CMakeLists.txt and the Buildroot package (playos-shell.mk). The versions.lock entries RAYLIB_COMMIT / RAYLIB_SOURCE are empty.
  3. 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.
  4. 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 the PLAYOS_SHELL_USE_RAYLIB path is understood).
  • versions.lock has empty RAYLIB_COMMIT / RAYLIB_SOURCE entries 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 in versions.lock under RAYLIB_COMMIT.
  • Backend: a custom rcore_playos.c platform backend (not GLFW/SDL backends). It owns the fullscreen xdg_toplevel, wl_egl_window, EGL/GLES2 context, and frame-callback vsync — the same primitives main.c currently 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.h SUPPORT_MODULE_* toggles (audio raudio, models rmodels, camera rcamera, and networking if present), keeping rcore, rlgl, rshapes, rtext, and rtextures. 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 from render_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.c against 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 (rmodels is stripped)
  • Intel graphics expansion (Sprint 13)
  • Store/network/account features

Required Repository Changes

RepoRequired work
playos-shellVendor 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-refdistroPin RAYLIB_COMMIT in versions.lock, flip playos-shell.mk to USE_RAYLIB=ON, ensure the vendored Raylib builds/links in the Buildroot image
playos-specAdd 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 IDTaskPrimary repoStatusNotes / evidence
S5.5-T1Upgrade vendored Raylib 5.5 → 6.0 and pin itplayos-shell, playos-refdistrodoneRAYLIB_COMMIT=dbc56a87 (6.0) in versions.lock
S5.5-T2Reconcile Raylib 6.0 breaking changesplayos-shelldoneMigration list applied
S5.5-T3Implement rcore_playos.c platform backend (6.0)playos-shelldoneCustom Wayland/EGL backend; non-blocking render loop (1046262)
S5.5-T4Port rendering from raw GLES2 to Raylib draw APIplayos-shelldonerender_util.c helpers map to Raylib draw API
S5.5-T5Wire controller input (rendering-only Raylib)playos-shelldoneinput.c evdev unchanged; no Raylib gamepad
S5.5-T6Integrate lifecycle polling into the Raylib frame loopplayos-shelldoneplayos_lifecycle_poll() per frame
S5.5-T7Buildroot packaging: flip to Raylib 6.0playos-refdistrodoneUSE_RAYLIB=ON; libraylib.so.6.0.0 in image
S5.5-T8Spec and docs reconciliationplayos-spec, playos-shelldoneSprint doc updated
S5.5-T9Validation and runtime evidenceplayos-shell, playos-refdistrodoneQEMU 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:

  1. Replace the vendored source. Swap external/raylib/ with the Raylib 6.0 release (exact tag or commit), keeping the repo's vendoring layout intact.
  2. Pin it. Set RAYLIB_COMMIT=<full sha> in playos-refdistro/versions.lock (confirm RAYLIB_SOURCE is correct). Do not use a branch or latest.
  3. Update the CMake integration. In playos-shell/CMakeLists.txt, adjust the add_subdirectory(external/raylib) path and the library target name if Raylib 6.0 renamed its CMake target (e.g., raylib vs raylib_playos). Keep PLAYOS_SHELL_USE_RAYLIB as the gating option.
  4. Apply a minimal module config. Configure config.h SUPPORT_MODULE_* toggles to strip unused subsystems (see Decisions). This keeps the vendored build lean and avoids pulling in raudio/rmodels deps.
  5. 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.h reports RAYLIB_VERSION "6.0".
  • versions.lock has a non-empty RAYLIB_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:

  1. 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.h flag renames and new SUPPORT_MODULE_* toggles
    • renamed/removed public symbols (window, input, drawing, text, math)
    • GLES2 / rlgl renderer API changes relevant to the custom backend
  2. 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.
  3. 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:

  1. Implement rcore_playos.c against Raylib 6.0's platform backend contract:
    • create a fullscreen xdg_toplevel (reuse the shell's xdg_wm_base/wl_compositor setup)
    • create a wl_egl_window + EGL/GLES2 context and make it current
    • pace frames with Wayland frame callbacks (wl_surface_frame) and present with eglSwapBuffers
  2. Disable desktop features as no-ops: window decorations, free resize, drag-and-drop, clipboard, multi-window.
  3. Wire the trusted-shell registration. Keep the playos_manager_v1 "register as trusted shell" + ShellReady handshake from main.c, now invoked through/alongside the backend init.
  4. Expose dimensions. Ensure GetScreenWidth() / GetScreenHeight() reflect the toplevel configure size (the shell's dpi_scale convention can map onto Raylib scaling).
  5. 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.c in the nested Wayland dev environment.
  • main.c no 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:

  1. Map each render_* helper to Raylib:
Current helperRaylib 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()
  1. Replace the font. Use GetFontDefault(); delete the embedded font_data array and GLSL shader sources from render_util.c.
  2. Update the four screen_*_draw() functions. Either keep the thin render_* wrappers (now backed by Raylib) or call Raylib directly. Preserve each screen's layout and focus-visibility rules.
  3. Remove dead code. Delete the now-unused GLES2 quad/shader/text-metric code.

Done when:

  • All four screens render through Raylib.
  • render_util.c contains 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:

  1. Keep shell_input_poll() and edge detection in input.c as the single source of controller state — do not route navigation through Raylib's gamepad abstraction.
  2. Confirm the invariants after the rendering migration: A confirms, B backs out, d-pad moves focus, focus is always visible.
  3. 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:

  1. Keep lifecycle polling in the Raylib main loop (or in the backend's per-frame hook).
  2. Suspend/background → skip BeginDrawing()/EndDrawing() (no rendering work). Foreground/resume → resume normal rendering.
  3. TERMINATE → exit cleanly through the Raylib window-close path (s->running = false).
  4. 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-init supervision.

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:

  1. Flip the flag. Set -DPLAYOS_SHELL_USE_RAYLIB=ON in br2-external/package/playos-shell/playos-shell.mk.
  2. Ensure the vendored Raylib builds and links. Confirm the playos-shell CMake builds the vendored external/raylib static library and links it into playos-shell. Add any missing dependency to PLAYOS_SHELL_DEPENDENCIES (Raylib stays vendored inside playos-shell, so it should not need a separate Buildroot package).
  3. Keep libplayos + GLES/EGL/Wayland through the backend. Update the .mk header comment (it currently says "Uses EGL/GLES2 for rendering" — change to "Raylib 6.0 via custom PlayOS backend").
  4. Pin in versions.lock (done in T1) and confirm make setup vendored the pinned commit.
  5. Build. Run make qemu-build and confirm the image contains the Raylib-linked shell.

Done when:

  • playos-shell.mk sets USE_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:

  1. playos-shell-spec.md: update the rendering responsibilities and cross-references to name Raylib 6.0 + rcore_playos.c; remove any "deferred" wording.
  2. 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).
  3. 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.
  4. 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:

  1. Nested Wayland dev run: shell boots, draws all four screens through Raylib, navigates with controller, logs manifest load + navigation + fps.
  2. QEMU headless path: shell starts without crashing (visual output limited, but must not regress from Sprint 5).
  3. On-device (Ally): visual parity with Sprint 5, ≥ 60 fps, controller navigation, persistence under supervision.
  4. Capture evidence: /data/log/shell.log showing 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

  1. T1 first (vendor + pin Raylib 6.0) — everything depends on the new source.
  2. T2 second (reconciliation list) — informs the backend and port work.
  3. T3 third (backend) — the foundation the port sits on.
  4. T4 fourth (rendering port) — the largest surface change, across all screens.
  5. T5, T6 next (input, lifecycle) — wire the shell's existing behaviours into the Raylib loop.
  6. T7 sixth (Buildroot) — build the whole image.
  7. T8 seventh (docs) — document reality after the code lands.
  8. 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

EvidenceHow it is produced
Raylib 6.0 proofexternal/raylib/src/raylib.h shows RAYLIB_VERSION "6.0"; versions.lock RAYLIB_COMMIT non-empty
Backend proofa Raylib frame draws through rcore_playos.c in the nested Wayland dev path
Rendering port proofrender_util.c contains no raw GLES2 shader/bitmap-font code; all four screens use the Raylib API
Input proofcontroller navigation unchanged; reserved SYSTEM/QUICK_MENU buttons still delivered to the shell
Lifecycle proofsuspend skips rendering, foreground resumes, TERMINATE exits cleanly
Packaging proofplayos-shell.mk sets USE_RAYLIB=ON; QEMU image boots the Raylib shell
Docs proofplayos-shell-spec.md, AGENTS.md, ADR-0006 no longer describe Raylib as deferred
Runtime proofshell log shows Raylib version + backend init + ≥ 60 fps on the Ally

Acceptance Criteria

  • vendored Raylib reports RAYLIB_VERSION "6.0" and is pinned in versions.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.c implements 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=ON in 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.c delegates to it.
  • Input remains shell-owned direct evdev (reserved buttons preserved); Raylib is rendering-only.
  • Raylib is pinned in versions.lock and 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:

  1. playos-init has no repository. playos-refdistro/src/playos-init/ is tracked directly in the playos-refdistro git index (25 files: init.c, supervisor.c, mount.c, recovery.c, logging.c, ipc/*.c, tests, CMakeLists.txt). It has no .git and no remote. There is no PlayOS-Foundation/playos-init repository yet, even though the Makefile setup target already tries to clone one and .gitignore already lists src/playos-init.

  2. playos-shell is committed into playos-refdistro. playos-refdistro/src/playos-shell/ contains 1 392 files tracked in playos-refdistro (no .git). The real repository exists at PlayOS-Foundation/playos-shell and is pushed at main (HEAD 1046262…), and the in-tree copy is a separate, manually-synced snapshot that is currently in sync with it. make setup does not clone playos-shell at all, and .gitignore does not ignore src/playos-shell.

  3. versions.lock is still dishonest for init. PLAYOS_INIT_COMMIT=b7800393… is annotated (in monorepo) and points at a refdistro commit, not a playos-init repo commit. PLAYOS_SHELL_COMMIT is already correct (104626206dfd2de59b4c576d3990d43b3b65980b, the pushed canonical HEAD).

  4. The correct pattern already exists. playos-compositor, playos-runtime, and playos-platform-api are each: a sibling repository, src/<name> git-ignored + untracked, cloned and SHA-checked-out by make setup, and built by a local-method Buildroot package. playos-init and playos-shell simply 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-shell renders through Raylib 6.0.
  • PlayOS-Foundation/playos-shell repository is pushed at main (verified: HEAD 104626206dfd2de59b4c576d3990d43b3b65980b == origin/main).
  • PlayOS-Foundation/playos-init repository exists (empty, or with only a README/license). (Created by the maintainer — not by the implementing agent.)
  • Local checkouts are available: playos-refdistro and the sibling playos-shell repo under the PlayOS workspace root ($HOME/playos/).
  • make setup / make qemu-build are understood and can be run to verify the result.

Decisions Locked for This Sprint

  • playos-init becomes its own repository. The maintainer creates PlayOS-Foundation/playos-init. The implementing agent seeds it from the current playos-refdistro/src/playos-init/ source (excluding build artifacts), pushes it, and records the full HEAD SHA.
  • playos-shell canonical source is the sibling repository, not the in-tree playos-refdistro/src/playos-shell copy. 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 with playos-init into its repository. playos-runtime remains protocol-only (unchanged from Sprint 2.5).
  • Buildroot packaging is unchanged. All five component .mk files keep SITE_METHOD = local pointing at $(BR2_EXTERNAL_PlayOS_PATH)/../src/<name>. make setup materializes the clones; the packages build from those clones.
  • No C source is committed in playos-refdistro after this sprint.
  • No new features. Pure remediation.

Scope

In Scope

  • Seed and push the new playos-init repository from the in-tree source
  • Untrack playos-refdistro/src/playos-init and playos-refdistro/src/playos-shell from the refdistro git index
  • Add src/playos-shell to playos-refdistro/.gitignore (init is already listed)
  • Wire playos-shell into the Makefile setup target (clone + pinned checkout), mirroring the existing four components
  • Update versions.lock so PLAYOS_INIT_COMMIT points at the new playos-init repo HEAD, and remove the (in monorepo) annotation (PLAYOS_SHELL_COMMIT is already pinned correctly)
  • Verify make setup clones all five components and playos-init / playos-shell still 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-init GitHub repository (maintainer does this)
  • New features, protocol changes, or UX changes
  • Changing the Buildroot local site method or package structure
  • Re-homing the IPC protocol into playos-runtime or a new repo (deferred until a consumer actually needs it)
  • CI pipeline changes
  • Sprint 6 storage / game-discovery work

Required Repository Changes

RepoRequired work
playos-initNEW — seeded from playos-refdistro/src/playos-init/ (source + tests + CMakeLists.txt + .gitignore), pushed to main
playos-shellVerify no drift vs. canonical (already in sync); ensure main is the pushed canonical state
playos-refdistrogit 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-specAdd 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, and cmake_install.cmake must 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 IDTaskPrimary repoStatusNotes / evidence
S5.6-T1Seed and push the playos-init repositoryplayos-initdoneCloned empty repo, copied in-tree source, committed, pushed (3a89f09f…)
S5.6-T2Reconcile and confirm the playos-shell canonical SHAplayos-shelldone1046262… confirmed current; in-tree copy already in sync
S5.6-T3Untrack src/playos-init from refdistroplayos-refdistrodonegit rm -r src/playos-init; already in .gitignore
S5.6-T4Untrack src/playos-shell from refdistro and gitignore itplayos-refdistrodonegit rm -r src/playos-shell; added to .gitignore
S5.6-T5Wire playos-shell into make setupplayos-refdistrodoneAdded PLAYOS_SHELL_COMMIT + clone block, mirroring the other four
S5.6-T6Pin real SHAs and clean versions.lockplayos-refdistrodoneRemoved the (in monorepo) annotation (shell already pinned)
S5.6-T7Verify make setup + build from extracted reposplayos-refdistrodoneAll five clones present; init + shell build
S5.6-T8Docs and repo-inventory reconciliationplayos-spec, playos-refdistrodoneNo 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:

  1. Clone the (empty) repository to a scratch location:
    git clone https://github.com/PlayOS-Foundation/playos-init.git /tmp/playos-init-seed
    
  2. Copy the source from the in-tree copy into the clone, preserving the directory layout:
    rsync -a --exclude build --exclude CMakeFiles --exclude CMakeCache.txt \
      --exclude cmake_install.cmake \
      playos-refdistro/src/playos-init/ /tmp/playos-init-seed/
    
    (If rsync is unavailable, use cp -a and remove the generated build/ directory afterward.)
  3. Verify the clone's own .gitignore (carried over from the in-tree source) excludes build/ and other generated artifacts; adjust if it does not.
  4. 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
    
  5. Record the full HEAD SHA:
    git rev-parse HEAD
    

Done when:

  • PlayOS-Foundation/playos-init contains 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:

  1. 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
    
  2. Diff the in-tree copy against the sibling repo to confirm no drift:
    diff -ru playos-shell playos-refdistro/src/playos-shell | head -200
    
  3. Record the canonical full SHA 104626206dfd2de59b4c576d3990d43b3b65980b for versions.lock. If a diff surfaces a meaningful edit, apply it to the sibling repo, commit, and push first, then use the new origin/main SHA.

Done when:

  • playos-shell main and origin/main are in sync.
  • The in-tree copy is confirmed byte-identical to the sibling repo (no drift).
  • A single canonical full SHA 104626206dfd2de59b4c576d3990d43b3b65980b is recorded for versions.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:

  1. Remove from index and working tree (this is intentional — make setup will re-materialize it):
    cd playos-refdistro
    git rm -r src/playos-init
    
  2. Confirm .gitignore still contains src/playos-init (no change needed).
  3. Commit:
    git commit -m "S5.6-T3: untrack playos-init source from refdistro monorepo"
    
  4. Verify nothing remains tracked:
    git ls-files src/playos-init   # expected: no output
    

Done when:

  • git ls-files src/playos-init returns 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:

  1. Remove from index and working tree:
    cd playos-refdistro
    git rm -r src/playos-shell
    
  2. Add src/playos-shell to .gitignore under the existing "Cloned source dependencies" block (after the src/playos-runtime line):
    src/playos-shell
    
  3. Commit:
    git commit -m "S5.6-T4: untrack playos-shell source from refdistro monorepo"
    
  4. 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-shell returns nothing.
  • .gitignore lists src/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:

  1. In the version-pins block, add a PLAYOS_SHELL_COMMIT variable 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)
    
  2. In the setup target, add a playos-shell clone block mirroring the playos-init block (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
    
  3. Keep the ordering consistent (init, compositor, runtime, platform-api, shell, or any stable order).

Done when:

  • make setup clones playos-shell into src/playos-shell and 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:

  1. Replace PLAYOS_INIT_COMMIT with 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
    
  2. Confirm PLAYOS_SHELL_COMMIT already equals the canonical SHA from S5.6-T2 (104626206dfd2de59b4c576d3990d43b3b65980b) — no change needed.
  3. 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:

  1. Re-run setup (a fresh src/ is expected after T3/T4):
    cd playos-refdistro
    make setup
    
  2. 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
    
  3. Build the two affected packages (faster than a full image; use the QEMU output dir, or ally for the device path):
    make -C buildroot BR2_EXTERNAL="$PWD/br2-external" O="$PWD/output/qemu" \
      playos-init-rebuild playos-shell-rebuild
    
    (Alternatively run make qemu-build for the full end-to-end check, as in prior sprints.)
  4. 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 setup materializes all five src/ clones at their pinned SHAs.
  • playos-init and playos-shell rebuild 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:

  1. In playos-spec/src/architecture.md (or wherever the repository list lives), add playos-init alongside playos-compositor, playos-runtime, playos-platform-api, and playos-shell as a first-class component repository.
  2. Update playos-refdistro/AGENTS.md so the "IPC Sources" note reads that the IPC sources live in the playos-init repository (cloned to src/playos-init/ipc/ by make setup), not committed in playos-refdistro.
  3. Add this sprint to the footer chain (see this document's footer, and the Sprint-5.5 / Sprint-6 footer edits below).
  4. 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-init is listed as a first-class repository.
  • playos-refdistro/AGENTS.md no 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

  1. T1 first (seed + push playos-init) — everything downstream needs the repo to exist.
  2. T2 second (reconcile + confirm playos-shell SHA) — locks the canonical shell state.
  3. T3, T4 next (untrack both in-tree copies) — removes the monorepo source; safe because the repos are pushed.
  4. T5 fifth (wire shell into make setup) — no dependency on T3/T4 beyond intent, but do it before verifying.
  5. T6 sixth (pin SHAs in versions.lock) — depends on T1/T2 for the real SHAs.
  6. T7 seventh (verify) — depends on all code/index changes.
  7. 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

EvidenceHow it is produced
playos-init repo proofgit ls-remote https://github.com/PlayOS-Foundation/playos-init.git main resolves; repo contains src/init.c etc.
playos-shell canonical SHA proofgit -C playos-shell rev-parse origin/main == the SHA in versions.lock
Init untrack proofgit ls-files src/playos-init in playos-refdistro returns nothing
Shell untrack proofgit ls-files src/playos-shell returns nothing; .gitignore lists src/playos-shell
make setup proofmake setup clones all five src/ repos at pinned SHAs
Build proofplayos-init-rebuild and playos-shell-rebuild succeed from the clones
versions.lock proofboth PLAYOS_INIT_COMMIT / PLAYOS_SHELL_COMMIT are non-empty 40-char SHAs with no stale annotation
Docs proofplayos-init listed in the repo inventory; AGENTS.md no longer claims C source lives in playos-refdistro

Acceptance Criteria

  • PlayOS-Foundation/playos-init exists, is seeded from the in-tree source (minus build artifacts), and is pushed to main
  • PlayOS-Foundation/playos-shell main == origin/main, and its full SHA is recorded
  • git ls-files src/playos-init and git ls-files src/playos-shell in playos-refdistro both return nothing
  • .gitignore lists src/playos-shell (and still lists src/playos-init)
  • make setup clones all five components into src/ and checks out the pinned SHAs
  • playos-init and playos-shell rebuild successfully from the clones
  • versions.lock has real, non-empty, 40-char SHAs for both PLAYOS_INIT_COMMIT and PLAYOS_SHELL_COMMIT, with no (in monorepo) / (LOCAL — push pending) annotations
  • playos-refdistro contains no committed C source under src/
  • playos-init is listed as a first-class repository in the spec's repository inventory
  • playos-refdistro/AGENTS.md no 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-refdistro contains no committed C source — only Buildroot packaging, configs, board files, and the developer Makefile.
  • make setup reproducibly clones all five components at pinned SHAs from versions.lock.
  • Buildroot still builds each component from its src/ clone via the unchanged SITE_METHOD = local packages.
  • The IPC sources live in the playos-init repository (cloned to src/playos-init/ipc/), and playos-runtime remains 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-init is its own repo, playos-refdistro has no C source).
  • playos-init already discovers and mounts a playos-data partition at /data (src/mount.c, find_data_partition()).
  • playos-init already creates a minimal first-boot directory set (src/mount.c, playos_data_create_dirs()).
  • The real playos_storage.h / playos_storage.c API shipped in Sprint 5 (playos-platform-api).
  • playos-shell already performs a basic /data/games/ scan and reads name / version / description from manifest.json (src/screen_library.c).
  • /data/.playos-storage-version marker is written and validated by playos-init (src/mount.c).
  • Game manifest v1 schema exists (playos-spec/schemas/game-manifest-v1.json).
  • playos-samples contains three real sample games with manifests and built binaries.
  • FactoryReset handler exists in playos-init (JSON message specified in playos-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 cmdline playos.data_uuid=. When several playos-data partitions 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-editable ideas.md still shows the older plural).
  • Manifest format: v1 JSON schema defined here is the stable game metadata contract for MVP.
  • PLAYOS_GAME_ID env var: set by playos-init at game launch; the storage API derives per-game paths from it. Game ID is not a function argument.
  • FactoryReset scope this sprint: erase_cache and erase_config only; erase_games and erase_saves are destructive and deferred to Sprint 10 (the schema may define them, but the handler returns an explicit "deferred" result).

Scope

In Scope

  • /data partition mount (already present), first-boot provisioning, and the complete directory tree
  • /data directory schema (final MVP layout)
  • /data/.playos-storage-version marker (write on first boot, validate on mount)
  • Game manifest v1 schema (playos-spec/schemas/game-manifest-v1.json)
  • Real playos_storage.h implementation (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
  • FactoryReset IPC (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

RepoRequired work
playos-initExtend /data provisioning: full directory schema, .playos-storage-version marker; add FactoryReset handler
playos-platform-apiReal playos_storage API (already implemented in Sprint 5 — verify, no new surface)
playos-shellComplete manifest-driven discovery: full validation, icon loading, sorting, robust skip-on-invalid
playos-samplesThree real sample games with valid manifests and compiled binaries
playos-refdistroPackage/install the sample games into the rootfs overlay (no C source)
playos-specGame manifest v1 schema + this sprint doc

FactoryReset is a JSON IPC message already specified in playos-spec/src/runtime-ipc.md; playos-init owns both the message handling and the directory-erase logic (it is the IPC server owner). playos-runtime is not involved — its protocols/playos-v1.xml is 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 IDTaskPrimary repoStatusNotes / evidence
S6-T1Implement /data partition provisioningplayos-initdoneVersion marker write/validate + full provisioning added to src/mount.c; builds, init_state test passes
S6-T2Define and create the final /data directory schemaplayos-initdoneplayos_data_create_dirs() now creates the full 11-dir schema + marker
S6-T3Define game manifest v1 schemaplayos-specdoneschemas/game-manifest-v1.json created and JSON-valid
S6-T4Implement real playos_storage APIplayos-platform-apidone (Sprint 5)Verified canonical signature; no new surface
S6-T5Implement live game discovery in the shellplayos-shelldoneValidation/icons/sort/skip-on-invalid added to screen_library.c; verified on device
S6-T6Build and install three real sample gamesplayos-samples, playos-refdistro, playos-initdoneThree 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-T7Add FactoryReset IPC command (cache/config scope)playos-initdoneJSON message per runtime-ipc.md; handler in ipc_handler.c + ipc/ipc.h; verified
S6-T8Persistence and isolation validationplayos-refdistrodoneIsolation 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-version on 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:

  • id must match the parent directory name
  • executable must exist relative to the game directory
  • api_version must be ≤ current system API version
  • architecture must 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 by playos-init at launch.
  • playos_storage_atomic_write writes to a temp file then renames into place.
  • Return NULL for 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.png if 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 sprint
  • com.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)

  • FactoryReset is specified as a JSON IPC message in playos-spec/src/runtime-ipc.md with flags erase_games, erase_saves, erase_cache, erase_config, erase_logs (all default false).
  • Implement the handler in playos-init (IPC server owner): reject when a game is running, recursively delete the selected directories, recreate them.
  • erase_cache targets /data/cache; erase_config targets /data/config.
  • erase_games, erase_saves, and erase_logs are 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_write to 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

EvidenceHow it is produced
Mount proof/proc/mounts showing /data at boot
Directory proofls /data showing all expected subdirectories
Version marker proofcontents of /data/.playos-storage-version
Discovery proofthree sample games appear in shell library
Persistence prooffile written before reboot is readable after reboot
Isolation prooftwo games' save paths are non-overlapping
Invalid manifest proofshell log showing skipped invalid entry

Acceptance Criteria

  • /data partition is mounted at boot; all directories exist
  • /data/.playos-storage-version is 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:

  • /data is reliably mounted with the final directory schema
  • game manifests are validated on discovery
  • PLAYOS_GAME_ID is 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-compositor renders the shell surface and can switch surfaces in principle.
  • playos-platform-api lifecycle stubs exist but are not backed by real delivery.
  • playos-overlay binary does not yet exist.
  • PLAYOS_BUTTON_SYSTEM hardware mapping confirmed (from Sprint 3 evtest work).

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_SYSTEM is 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 — only playos-init sends 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.sock only — the private Wayland playos_compositor_control_v1 interface is removed from this sprint; playos_overlay_v1 remains for the overlay client
  • Process ownership: playos-init spawns and supervises playos-compositor, playos-shell, and playos-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_v1 from playos-v1.xml (currently declared in playos-runtime, playos-compositor, and vendored copies). It duplicates the socket's SetExpectedGame / ClearExpectedGame / GameSurfaceReady control 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.sock using the playos_ipc_* framing library in playos-init/ipc/.
  • playos-compositor does 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-overlay trusted 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

RepoRequired work
playos-compositorFull state machine, first-frame rule, system button intercept, overlay surface management, compositor socket client
playos-shellLaunch IPC, launching-state spinner UI, crash notification, library restore
playos-runtimeLifecycle transport, compositor control IPC client (SetExpectedGame), playos_overlay_v1 Wayland extension
playos-platform-apiReal lifecycle event delivery (fd-backed)
playos-refdistroplayos-overlay trusted Raylib client + Buildroot package
playos-specLifecycle 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 IDTaskPrimary repoStatusNotes / evidence
S7-T1Implement full game launch flow in playos-initplayos-initdoneGameStarted emitted; launch env + lifecycle fd wired
S7-T2Implement full compositor state machineplayos-compositordoneAll five states and transitions implemented
S7-T3Implement system button intercept at seat levelplayos-compositordonePLAYOS_BUTTON_SYSTEM intercepted at seat level; never delivered to games
S7-T4Build playos-overlay trusted Raylib clientplayos-refdistrodoneRaylib overlay UI builds and runs as trusted Wayland client
S7-T5Implement lifecycle fd delivery in platform-apiplayos-platform-apidonePLAYOS_LIFECYCLE_FD delivered to games
S7-T6Finalize private Wayland protocol (overlay kept, game-launch removed)playos-runtimedoneplayos_overlay_v1 kept; playos_game_launch_v1 removed
S7-T7Implement game exit and crash recoveryplayos-compositor, playos-refdistrodoneGameExited/GameCrashed emitted; returns safely to shell
S7-T8Integration and lifecycle validation on Allyplayos-refdistrodoneEnd-to-end lifecycle verified on ROG Ally

S7-T1 — Implement full game launch flow in playos-init

  1. Validate: one-game rule; reject immediately if a game is already running
  2. Validate: manifest exists, executable exists, api_version ≤ current system version
  3. 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
  4. Create lifecycle pipe; store write-end in playos-init, pass read-end as PLAYOS_LIFECYCLE_FD
  5. Emit SetExpectedGame { launch_token, game_id } to the compositor over /run/playos/compositor.sock
  6. Spawn game executable; track PID
  7. Emit GameStarted { game_id, pid, launch_token } to the shell over control.sock

Note: the GameStarted / GameExited / GameCrashed message types are already declared in ipc/ipc.h, but playos-init currently never emits themplayos_supervisor_game_exited only writes to init.log. S7-T1 wires up GameStarted; 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_SYSTEM before any client can receive it
  • When in GAME_FOREGROUND: remove input focus from the game, emit CompositorStateChanged (PLAYOS_UI_FOREGROUND_WITH_GAME_BACKGROUND) to playos-init, and transition to the overlay state; playos-init then writes PLAYOS_LIFECYCLE_BACKGROUND to the lifecycle fd and arms the non-cooperative SIGSTOP timer
  • Verify via evtest and a game that logs all key events — PLAYOS_BUTTON_SYSTEM must 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_dismiss to the compositor (the compositor hides the overlay and returns focus to the game)
  • "Quit Game" (B or menu) — sends TerminateGame IPC to playos-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_dismiss and handles about_to_show / about_to_hide to 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_FD in 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:

  1. playos-init records exit status; closes lifecycle pipe write-end
  2. playos-init emits GameExited { game_id, exit_code } to the shell over control.sock (types already declared in ipc/ipc.h — no new protocol)
  3. Compositor detects Wayland client disconnect; transitions to SHELL_FOREGROUND
  4. Shell surface is unhidden and refocused; library scroll position restored
  5. Shell shows no notification on clean exit

Crash (non-zero exit or signal):

  1. Same compositor recovery
  2. playos-init emits GameCrashed { game_id, exit_code, signal } to the shell over control.sock
  3. Shell shows a non-intrusive toast notification: "Game exited unexpectedly"
  4. 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:

  1. Launch → play → quit via Quit button → library shown
  2. Launch → system button → overlay visible → resume → back in game
  3. Launch → system button → overlay visible → quit → library shown
  4. kill -9 <game_pid> → crash toast within 500ms
  5. Try to launch a second game while one is running → reject logged, first game unaffected
  6. Launch → background → game ignores lifecycle → SIGSTOP fires within 500ms
  7. 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() in src/child_process.c — unused; the supervisor spawns everything directly.
  • playos_supervisor_spawn_test_client() / spawn_test_client() in src/supervisor.c — unused; the ipc-test-client self-test is spawned from main.c.
  • playos_recovery_enter() / playos_recovery_loop() in src/recovery.c — unused; the live recovery path is playos_enter_recovery() in src/supervisor.c. Delete recovery.c (and its header) or fold the banner into playos_enter_recovery().

Done when: grep finds no callers of the above and the tree still builds/tests green.


Verification and Evidence

EvidenceHow it is produced
State machine logscompositor systemd journal showing all transitions for the test matrix
Lifecycle event logcom.playos.sample-input log showing FOREGROUND/BACKGROUND/FOREGROUND sequence
System button intercept proofgame key log showing system button event is absent
Crash recovery timingtimestamp of game exit vs. timestamp of shell surface shown (≤500ms)
One-game rulelog showing second launch rejected
SIGSTOP fallbackstrace 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_FOREGROUND at launch
  • System button press shows overlay above game (dim + overlay visible)
  • Game receives PLAYOS_LIFECYCLE_BACKGROUND on system button press
  • Overlay "Resume" returns to game; PLAYOS_LIFECYCLE_FOREGROUND delivered
  • 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_SYSTEM never appears in any game client's input stream
  • Non-cooperative game receives SIGSTOP within 500ms of BACKGROUND event
  • 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_FD delivers events reliably
  • The overlay exists and can be extended (add audio volume controls)
  • rcore_playos.c is already in playos-shell from 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.c stub exists in playos-platform-api.
  • com.playos.sample-audio placeholder exists in playos-samples/audio-sine.
  • playos-overlay source lives in playos-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.cminiaudio.h, vendored in playos-shell) and link alsa-lib. Raylib/miniaudio has no PipeWire backend (it supports only ALSA, PulseAudio, JACK), so this is the default — but compile out PulseAudio with MA_NO_PULSEAUDIO (alongside the existing MA_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 via playos_audio_get_info().
  • Device priority: PLAYOS_AUDIO_DEVICE env 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_RR at a safe priority and document the value

Scope

In Scope

  • ALSA PCM backend via Raylib's miniaudio module (raudio.c), enabled in playos-shell
  • playos_audio.h public API — implement the existing playos-platform-api stub
  • 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-audio sample 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

RepoRequired work
playos-shellEnable Raylib miniaudio ALSA backend (SUPPORT_MODULE_RAUDIO=1); shell UI sounds
playos-platform-apiImplement playos_audio.c (system state, master volume/mute) — replace stub
playos-refdistroalsa-lib in Buildroot config; overlay volume control in src/playos-overlay/
playos-samplesFinish com.playos.sample-audio (actual sine playback)
playos-specAudio 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 IDTaskPrimary repoStatusNotes / evidence
S8-T1Enable ALSA audio via Raylib miniaudio backendplayos-shelldoneSUPPORT_MODULE_RAUDIO=1, alsa-lib linked; sine tone plays on-device
S8-T2Define and implement playos_audio.h APIplayos-platform-apidoneContract finalized and implemented; mixer-element selection fixed (fe6cc7a)
S8-T3Implement audio lifecycle behavior (background/foreground/suspend/resume/terminate)playos-platform-apidonePause/resume/stop verified across lifecycle transitions
S8-T4Implement headphone jack detection and routingplayos-platform-apideferredNot landed in the verified audio milestone
S8-T5Add volume control to playos-overlayplayos-refdistrodoneD-pad volume controls wired in overlay
S8-T6Add shell UI soundsplayos-shelldeferredNot landed in the verified audio milestone
S8-T7Build com.playos.sample-audio sample gameplayos-samplesdoneaudio-sine plays a 440 Hz sine tone on-device
S8-T8Audio validation on Allyplayos-refdistrodoneSine tone verified through built-in speakers

S8-T1 — Enable the ALSA audio backend in Raylib's miniaudio module

Enablement:

  1. Set SUPPORT_MODULE_RAUDIO=1 in playos-shell's vendored Raylib build; add alsa-lib to the Buildroot target. Also #define MA_NO_PULSEAUDIO next to the existing MA_NO_JACK in raudio.c so miniaudio compiles only the ALSA backend.
  2. Confirm miniaudio's ALSA backend opens the default playback device (default, or PLAYOS_AUDIO_DEVICE if set).
  3. If the default device fails: enumerate with snd_device_name_hint() and select the first stereo hardware output.
  4. Params: 44100 Hz, stereo SND_PCM_FORMAT_S16_LE (miniaudio resamples to the device native rate/format as needed).
  5. Prepare and start playback; miniaudio owns the mixing thread.

Audio thread / underrun handling:

  • Set a documented SCHED_FIFO/SCHED_RR priority 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 eventAudio action
PLAYOS_LIFECYCLE_FOREGROUNDResume ALSA playback at previous volume
PLAYOS_LIFECYCLE_BACKGROUNDDrain buffer; block writes (thread pauses)
PLAYOS_LIFECYCLE_SUSPENDPause playback (same as background; flush before returning)
PLAYOS_LIFECYCLE_RESUMEResume playback
PLAYOS_LIFECYCLE_TERMINATEStop 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-sine placeholder (already installed as com.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

EvidenceHow it is produced
Speaker outputConfirmed by ear on the Ally
Underrun countALSA snd_pcm_status after 2-minute run
Headphone routingPlug/unplug ×3, confirm routing in log
Lifecycle timingTimestamp of BACKGROUND event vs. first silent frame (≤200ms)
Volume APIplayos_audio_get_info() output before and after set_master_volume()
CI buildCI log showing successful compile

Acceptance Criteria

  • Shell plays audio (UI sounds) on the Ally speakers
  • com.playos.sample-audio plays 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-overlay can be extended with new status displays and controls
  • playos-init thermal monitoring loop can be added without conflicting with audio
  • The full lifecycle including SIGSTOP/SIGCONT is 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 (commits 512ae05, f80e44a); gamepad wired into raylib CORE.Input.Gamepad.*.
  • ✅ Graphics stack upgraded to GLES 3.0 (playos-shell 4ad17f6) — the S9-T8 sustained-load/thermal validation now exercises the ES3 path (EGL negotiates ES3.2 on RDNA3).
  • ⚠️ CONFIG_X86_AMD_PSTATE=y confirmed at br2-external/board/ally/linux.config:190 (plus ACPI_BATTERY, ACPI_AC, POWER_SUPPLY, THERMAL), but CONFIG_X86_AMD_PSTATE_EPP=y is missing — it must be enabled for the energy_performance_preference sysfs node this sprint writes.
  • /sys/class/power_supply/BAT0/ exists on the Ally (verify during bringup — cannot check without hardware).
  • playos-overlay exists with D-pad volume controls and already renders a hardcoded Battery: 85% Thermal: Normal placeholder (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=y in br2-external/board/ally/linux.config so energy_performance_preference exists (today only CONFIG_X86_AMD_PSTATE=y is set).
  • Event channel: ThermalStateChanged and PerfProfileChanged reuse the existing Sprint-7 shell listener (playos_trusted_register_shell / playos_trusted_shell_poll); no new IPC transport.

Scope

In Scope

  • playos_power.h API (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/state attempt)

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

RepoRequired work
playos-platform-apiplayos_power.h, sysfs-backed implementation
playos-refdistroplayos-init thermal monitor, enable CONFIG_X86_AMD_PSTATE_EPP=y in board/ally/linux.config, thermal.json default
playos-shellBattery/thermal status bar
playos-refdistro (src/playos-overlay, committed in-tree)Temperature display, profile selector, power menu
playos-runtimeAdd SetPerfProfile request + ThermalStateChanged/PerfProfileChanged events via the existing shell listener (Shutdown/Reboot already exist from Sprint 5)
playos-specThermal 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 IDTaskPrimary repoStatusNotes / evidence
S9-T1Define and implement playos_power.h APIplayos-platform-apidoneplayos_power.h + full playos_power.c: sysfs battery/thermal/EPP reads + IPC client
S9-T2Implement sysfs-backed battery and temperature readsplayos-platform-apidoneBAT0 capacity/status/time-to-*, x86_pkg_temp/cpu_thermal/k10temp, amdgpu hwmon; 1 s cache
S9-T3Implement AMD P-state EPP write and profile IPCplayos-refdistro, playos-runtimedonethermal.c EPP writer; SetPerfProfile handler in ipc_handler.c; EPP enabled via DEFAULT_MODE=3
S9-T4Implement thermal monitoring loop in playos-initplayos-refdistrodonesrc/playos-init/src/thermal.c 1 Hz tick, thermal.json thresholds, state machine + events
S9-T5Update shell status bar (battery, thermal indicator)playos-shelldonebattery %/charging + colour-coded thermal + profile (main.c)
S9-T6Update overlay (temps, profile selector, power menu)playos-refdistro (src/playos-overlay)donelive power/thermal, D-pad profile selector, Sleep/Restart/Shutdown menu
S9-T7Implement suspend/resume skeletonplayos-refdistro, playos-platform-apidonePLAYOS_IPC_TYPE_SUSPENDplayos_suspend(); lifecycle events handled
S9-T8Power and thermal validation on Allyplayos-refdistrodonestatus 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.h already exists and matches this spec exactly; src/playos_power.c is 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

Datasysfs 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_preference only exists when CONFIG_X86_AMD_PSTATE_EPP=y. Add it to br2-external/board/ally/linux.config first; without it the driver exposes only scaling_governor.

Profile → EPP mapping:

PlayOS profileAMD EPP value
PLAYOS_PERF_BALANCEDbalance_performance
PLAYOS_PERF_POWER_SAVEpower
PLAYOS_PERF_PERFORMANCEperformance
  • playos-init receives SetPerfProfile { profile } IPC
  • Validates the request against current thermal state (reject PERFORMANCE if HOT or CRITICAL)
  • 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 PlayOSThermalState using 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 ThermalStateChanged event
  • 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 / PerfProfileChanged event 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: Normal line (main.c:386) and D-pad volume controls; replace the placeholder with live playos_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 SetPerfProfile IPC
  • Power menu: "Sleep" (disabled placeholder), "Restart", "Shutdown"
  • Restart: sends Reboot IPC to playos-init
  • Shutdown: sends Shutdown IPC to playos-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) and PLAYOS_LIFECYCLE_RESUME (0x03) are already defined in playos-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_SUSPEND to any running game; attempt echo 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 SetPerfProfile IPC
  • Shutdown and restart via overlay: verify clean filesystem state after restart
  • Suspend skeleton: PLAYOS_LIFECYCLE_SUSPEND delivered; 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

EvidenceHow it is produced
Battery accuracyplayos_power_get_info() output vs. cat /sys/class/power_supply/BAT0/capacity
Temperature accuracyplayos_power_get_info() output vs. sensors
Thermal state progressionplayos-init log during stress test
P-state change/sys/devices/system/cpu/cpu0/cpufreq/energy_performance_preference before/after
Profile rejectionLog showing SetPerfProfile PERFORMANCE rejected when HOT
Restart cleanjournalctl 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 PERFORMANCE honored when thermal state is NORMAL
  • SetPerfProfile PERFORMANCE rejected when thermal state is HOT
  • Shutdown from overlay: system shuts down cleanly (filesystems synced)
  • Restart from overlay: system reboots cleanly
  • PLAYOS_LIFECYCLE_SUSPEND and PLAYOS_LIFECYCLE_RESUME delivered 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.h API 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" = true
  • playos-reference-devices/asus-ultrabook/device-profile.toml:24"display.brightness" = true

But nothing in the code reads or writes a backlight. Investigation confirmed:

  1. playos-platform-api/include/playos/playos_display.h already exists (and is already pulled in by playos.h), but src/playos_display.c is a stub — playos_display_get_info() and playos_display_set_vsync() both just return -1.
  2. No sysfs backlight access exists anywhere in playos-platform-api, playos-init, playos-runtime, or playos-shell.
  3. The Settings → Display tab is read-only (playos-shell/src/screen_settings.c:616-632 shows 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.
  4. 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.h exists with a PlayOSDisplayInfo struct and is included by the master playos.h include.
  • playos_power.c demonstrates the sysfs-read + 1-second monotonic cache + /sys/class/* enumeration patterns to copy.
  • The Settings screen has draw_info_line() and draw_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/ (expected amdgpu_bl0) and the shell's uid can write its brightness file. (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 overload playos_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_bl0acpi_video0intel_backlight → first other non-acpi_ entry under /sys/class/backlight/. Skip entries with max_brightness == 0.
  • Percent mapping: percent = round(brightness * 100 / max_brightness); on set, raw = clamp(round(percent * max_brightness / 100), 0, max_brightness). max_brightness is read once and cached.
  • Read cache: 1-second monotonic cache for the raw brightness read, mirroring playos_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 write brightness, fall back to a SetBrightness IPC message owned by playos-init (root), mirroring SetPerfProfile.
  • 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 in shell.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 in playos-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

RepoRequired work
playos-platform-apiAdd playos_display_get_brightness() / playos_display_set_brightness() to playos_display.h; implement sysfs backlight access in playos_display.c
playos-shellAdd 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-specAdd 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)

RepoRequired work
playos-refdistro (src/playos-init)Add a SetBrightness IPC handler that writes /sys/class/backlight/<node>/brightness as root
playos-runtimeAdd 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 IDTaskPrimary repoStatusNotes / evidence
S9.5-T1Verify the Ally backlight node and write permissionplayos-refdistro (on-device)deferreddirect-write path chosen; node name + writability still unconfirmed on hardware
S9.5-T2Implement brightness get/set in playos_display.cplayos-platform-apidonesysfs enumeration + 1 s cache + clamp; native build clean
S9.5-T3Add interactive Brightness gauge to Settings → Displayplayos-shelldonegauge row + d-pad up/down write + height bump; native build clean
S9.5-T4(Conditional) SetBrightness IPC fallbackplayos-refdistro, playos-runtimenot neededdirect sysfs write chosen; T1 hardware check deferred
S9.5-T5Spec/docs reconciliationplayos-specdoneshell-spec stub wording updated
S9.5-T6Build + validationplayos-platform-api, playos-shell, playos-refdistroin progressnative 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:

  1. On the Ally (or from a shell on the mounted USB rootfs):
    • ls -1 /sys/class/backlight/ → confirm amdgpu_bl0 (or note the actual node name).
    • cat /sys/class/backlight/<node>/max_brightness and cat .../brightness.
    • ls -l /sys/class/backlight/<node>/brightness → note owner/group/mode.
    • id of the running playos-shell process.
  2. 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:

  1. 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.
  2. In playos_display.c, add a read_int_file() helper (copy the pattern from playos_power.c:50-61) and a backlight enumeration helper:
    • opendir("/sys/class/backlight")
    • for each entry, read max_brightness; prefer amdgpu_bl0, then acpi_video0, then intel_backlight, then the first non-acpi_ entry with max_brightness > 0.
    • cache the chosen node path + max_brightness on first success.
  3. Implement playos_display_get_brightness():
    • read brightness with the 1-second monotonic cache (mirror playos_power_get_info()'s g_cached_valid/g_cached_ms style);
    • scale to 0..100 and return 0; return -1 if no node exists.
  4. Implement playos_display_set_brightness():
    • clamp percent to 0..100;
    • scale to raw using the cached max_brightness;
    • fopen the brightness node in write mode, write the integer + "\n", fflush, fclose;
    • return 0 on success, -1 on failure (log once via PLAYOS_LOG_W on the failure, not every attempt).
  5. Keep playos_display_get_info() and playos_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:

  1. 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 add int settings_display_cursor; if the row needs focus; otherwise d-pad up/down on the Display tab directly adjusts brightness.
  2. Refresh brightness once per frame or on a short interval in the Settings update path (call playos_display_get_brightness()).
  3. In screen_settings.c Display tab update (before the generic scroll branch), when s->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.
  4. 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) at value = percent / 100.0f;
    • show NN% as the value label.
  5. Bump settings_content_height() for TAB_DISPLAY from 3.0f * info_h to 4.0f * info_h (screen_settings.c:110-113).
  6. 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):

  1. Add a SetBrightness message type to playos-init/ipc/ipc.h and the runtime IPC protocol.
  2. In playos-init, add a handler that validates 0..100, scales to max_brightness, and writes /sys/class/backlight/<node>/brightness.
  3. In playos_display_set_brightness(), route through the IPC control socket (/run/playos/control.sock), mirroring request_profile_over_ipc() in playos_power.c:234-314.
  4. 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:

  1. Update playos-spec/src/playos-shell-spec.md:47 so brightness is no longer described as a stub — state that Settings → Display exposes a live brightness control backed by the platform-api.
  2. Add Sprint-9.5.md to the sprint directory (this document).
  3. 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:

  1. Native compile-check playos-platform-api (the new playos_display.c) and playos-shell against the platform-api headers.
  2. 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.
  3. 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.
  4. 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

  1. T1 first (device verification) — decides whether T4 is needed.
  2. T2 second (platform-api) — the primitive everything else depends on.
  3. T3 third (shell UI) — surfaces the primitive.
  4. T4 fourth (IPC fallback) — only if T1 blocks direct writes.
  5. T5 fifth (docs) — document reality after the code lands.
  6. 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.h only gains two functions.
  • PLAYOS_API_VERSION is 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

EvidenceHow it is produced
Backlight node confirmedT1 output: ls /sys/class/backlight/, max_brightness, writability, shell uid
API worksnative test / on-device playos_display_get_brightness() matches cat .../brightness scaled to %
Write workson-device playos_display_set_brightness(50) changes cat .../brightness
UI worksSettings → Display gauge tracks real value; up/down changes the panel
No-op path safeQEMU (no backlight) shows "Unavailable" and does not crash
Docs updatedplayos-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_VERSION is unchanged; the change is additive
  • playos-shell-spec.md no longer describes brightness as a stub
  • Native and QEMU builds are clean

Handoff to Sprint 10

Sprint 10 may assume:

  • playos_display.h exposes a working playos_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, and erase_logs are deferred to this sprint.
  • playos-refdistro produces a bootable USB image (make ally-usb-imageoutput/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, label playos-a), system B (4 GiB EROFS/squashfs, read-only, label playos-b, empty until Sprint 11), misc (64 MiB, A/B slot metadata, label misc), data (remainder ext4, label playos-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=install kernel 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: libfdisk or raw ioctl (no parted/fdisk subprocess)
  • UEFI fallback: always write /EFI/BOOT/BOOTX64.EFI; efibootmgr registration is a best-effort addition
  • Factory reset authority: only playos-init performs 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 FactoryReset IPC (all five options)
  • make installer-image Buildroot 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

RepoRequired work
playos-refdistroplayos-installer source, Buildroot package, installer image target, disk ops; overlay factory-reset UI flow
playos-initComplete FactoryReset server handler (games/saves/logs erasure); installer trigger mode
playos-runtimeplayos_trusted_factory_reset() client helper
playos-specInstallation 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, update playos-refdistro/AGENTS.md "What NOT to Do" to list it alongside the overlay exception.


Agent Task Breakdown

Task Status Grid

Task IDTaskPrimary repoStatusNotes / evidence
S10-T1Implement installer trigger mode in playos-initplayos-initdonemount.c:421-455 parses playos.mode=install; main.c/supervisor.c spawn /usr/bin/playos-installer
S10-T2Build installer screen state machine (Raylib UI)playos-refdistrodonesrc/playos-installer/main.c DISK_DISCOVERY → CONFIRMATION → INSTALLING → SUCCESS/ERROR
S10-T3Implement disk partitioning and formattingplayos-refdistrodonedisk.c (libfdisk GPT), format.c (mkfs.fat/mkfs.ext4, squashfs slot write)
S10-T4Implement EFI artifact write and UEFI boot entryplayos-refdistrodoneefi.c writes /EFI/BOOT/BOOTX64.EFI, best-effort efibootmgr
S10-T5Implement first-boot from internal diskplayos-initdoneExisting S6 first-boot provisioning already handles empty /data (mount.c .playos-storage-version marker + seed games); no new code required
S10-T6Complete FactoryReset IPC (all five options)playos-init, playos-runtime, playos-refdistrodoneHandler now erases games/saves/cache/config/logs; runtime trusted helper added
S10-T7Create make installer-image Buildroot targetplayos-refdistrodoneplayos_ally_installer_defconfig, playos-installer package, linux-installer.config, scripts/gen-installer-usb-image.sh, Makefile installer-* targets
S10-T8Installer validation (QEMU loopback + Ally)playos-refdistrodoneQEMU 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 fixedplayos-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-data partition exists on an internal disk; if both true, trigger installer mode
  • In installer mode: spawn playos-installer as 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_DISCOVERYCONFIRMATIONINSTALLINGSUCCESS | 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:

  1. Create GPT partition table on the target device
  2. Partition 1: EFI System Partition, FAT32, 512 MiB, label ESP
  3. Partition 2: system A, 4 GiB, label playos-a (read-only EROFS/squashfs root slot)
  4. Partition 3: system B, 4 GiB, label playos-b (reserved empty for Sprint 11 A/B)
  5. Partition 4: misc, 64 MiB, label misc (A/B slot metadata)
  6. Partition 5: data, ext4, remainder, label playos-data
  7. Write partition table to disk

Format/populate:

  • ESP: call mkfs.fat -F32 -n ESP <part1>; copy EFI/BOOT/BOOTX64.EFI
  • System A: write the pre-built read-only root image (EROFS, fallback squashfs) directly to <part2> — no mkfs at install time
  • System B: leave empty/blank; populated by the Sprint 11 A/B update path
  • misc: call mkfs.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

  1. Mount the ESP at /mnt/efi
  2. Create /mnt/efi/EFI/BOOT/
  3. Copy the PlayOS EFI-stub kernel (bzImage with embedded initramfs) to BOOTX64.EFI — no intermediate bootloader (matches scripts/gen-ally-usb-image.sh)
  4. 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)
  5. 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-init detects no .playos-storage-version marker
  • 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 FactoryResetError with "reason": "game_running" (matches runtime-ipc.md).

  • Success replies FactoryResetComplete; the erased directories are recreated empty.

  • erase_saves is destructive: overlay must show a second confirmation before sending the IPC.

  • Factory reset with erase_games = true and erase_saves = true leaves the system bootable with an empty game library and a valid /data tree.

  • 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 — like playos_ally_defconfig but with playos-installer instead of playos-shell as the first Wayland client
  • make installer-image produces a bootable USB image that enters installer mode
  • make ally-usb-image continues 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 -r to /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-init now skips mounting the internal ESP label (the disk is about to be repartitioned) and skips playos_pivot_to_active_slot() (so /usr/bin/playos-installer stays visible). Without this, re-install failed with Installation Failed because the old ESP mount made the later fdisk/mkfs steps 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

EvidenceHow it is produced
Partition layoutfdisk -l <device> after QEMU loopback install
Filesystem typesblkid output showing FAT32 (ESP), EROFS/squashfs (system slots), ext4 (data/misc) labels
NVMe bootROG Ally boot without USB, shell visible
Factory resetls /data/games/ empty after full reset; system boots
Installer error screenWrite-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-image produces 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; misc exists
  • playos-init boots from the ESP EFI artifact (BOOTX64.EFI) and provisions /data
  • /data provisioning 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 misc partition 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_RDONLY flag needed); dm-verity is a post-MVP hardening step (document as required for production)
  • A/B slot tracking: boot.json on the ESP (FAT32 writable from playos-init); the misc partition 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 >= 3 with health != "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 .playosb suffix (never a RAUC-specific extension). Exactly one bundle may be "ready to apply" at a time.
  • boot.json schema/EFI/playos/boot.json on 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):
    { "v": 1, "type": "ApplyUpdate", "path": "/data/updates/0.2.0.playosb" }
    
    Responses: ApplyUpdateAck { "accepted": true } or ApplyUpdateError { "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_count to 0 and sets health = "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.json on 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

RepoRequired work
playos-refdistroRAUC integration, update bundle build target, read-only root image build (A/B layout already from Sprint 10)
playos-initRead-only system mount, boot.json management, boot counting and rollback, update application flow
playos-shellUpdate UI in settings screen
playos-platform-apiplayos_system_os_version() returns active slot version
playos-specA/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 IDTaskPrimary repoStatusNotes / evidence
S11-T1Implement read-only system partition mountplayos-initdeferredBlocked: system still boots from embedded initramfs; pivot_root into the read-only squashfs slot is not yet wired
S11-T2Mount active system slot image and select active slot (layout already created in Sprint 10)playos-initdeferredSlot-selection logic implemented in boot_slot.c; the active squashfs image is not yet mounted as the runtime root (same blocker as T1)
S11-T3Implement boot.json read/write and active slot selectionplayos-initdonesrc/boot_slot.c reads/writes /EFI/playos/boot.json; host test test_boot_slot passes
S11-T4Implement boot counting and automatic rollbackplayos-initdoneBoot-count increment + 3-strike rollback on ShellReady/60s timer; host test passes
S11-T5Integrate RAUC (or equivalent) and update application flowplayos-refdistro, playos-initdoneCustom dev-signed updater chosen over RAUC (see ADR-0005 revision); ApplyUpdate IPC + scripts/create-update-bundle.sh
S11-T6Implement update bundle signature verificationplayos-initdonesrc/sha256.c HMAC-SHA256 (dev key); .playosb bundle verified before any partition write
S11-T7Add shell update UIplayos-shelldoneSettings → System → Software Update (Check/Apply/Restart-to-Apply + progress + boot-slot info)
S11-T8Update playos_system_os_version() to read active slot versionplayos-platform-apidoneReads active slot version from /EFI/playos/boot.json; falls back to "unknown"
S11-T9A/B update and rollback validationplayos-refdistrodeferredRequires 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/test returns EROFS or 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-init reads boot.json (or misc metadata) → 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
  • misc is reserved as the more robust home for A/B slot metadata; boot.json on 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-init mounts ESP read-write during boot, reads boot.json, unmounts read-only after updating
  • Active slot determines which partition label to mount as system
  • If boot.json is 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:

  1. Mount ESP read-write
  2. Read boot.json; increment boot_count for the active slot
  3. Write updated boot.json; unmount ESP
  4. If boot_count >= 3 AND health != "good": mark slot "health": "bad", switch active_slot to the other slot, reboot immediately

After successful boot (user interacts with shell OR 60-second timer):

  1. Mount ESP read-write
  2. Set health = "good", boot_count = 0 for the active slot
  3. 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:

  1. Receive update bundle path (e.g., /data/updates/playos-0.2.0.playosb)
  2. Verify bundle signature (see S11-T6)
  3. Identify inactive slot (opposite of active_slot in boot.json)
  4. Write new system image to inactive slot partition
  5. Update ESP: write new EFI artifact for inactive slot
  6. Update boot.json: switch active_slot to new slot, boot_count = 0, health = "pending"
  7. Notify shell: "Update ready — restart to apply"
  8. 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.json on the ESP (S11-T3)
  • Keep the existing static-buffer lifetime contract; no caller changes required
  • Fall back to "unknown" (not a hardcoded version) when boot.json is 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:

  1. Fresh install → verify 5-partition layout → boot.json shows slot A good
  2. Apply valid update bundle → boot.json shows slot B pending → reboot → slot B active → 60s → slot B good
  3. Corrupt slot B initramfs → apply bundle with corrupt slot → reboot 3× → rollback to slot A
  4. /data content (game files, saves) survives update and rollback unchanged
  5. Invalid signature bundle → rejected before any partition write
  6. 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

EvidenceHow it is produced
Read-only mounttouch /usr/test → EROFS; cat /proc/mounts showing ro
A/B layoutfdisk -l after fresh install
boot.json contentcat /EFI/playos/boot.json at each test stage
Rollback3-strike test log showing slot switch
Data survivalFile hashes before and after update match
Signature rejectionLog showing rejected bundle before any write

Acceptance Criteria

  • Running system partition mounted read-only; touch /usr/test fails with EROFS
  • Installer creates 5-partition layout on fresh NVMe (ESP, A/B system, misc, data)
  • boot.json on 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 /data survive 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.json on the ESP tracks the active slot, slot health, and boot count
  • Games and user data live on /data and 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.c reads/writes /EFI/playos/boot.json and selects the active slot.
  • Boot-count increment + 3-strike rollback logic exists (ShellReady OR 60-second timer → mark_good).
  • .playosb bundle 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 from boot.json) exist.
  • Open question (resolve first): does the playos-refdistro build 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: the ally defconfig already builds a complete bootable rootfs.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 /miscpivot_root/switch_rootexec the system init.
  • Read-only guarantee: squashfs is inherently read-only; no MS_RDONLY remount trickery needed. dm-verity remains post-MVP.
  • Slot metadata: stays in boot.json on the ESP (no migration to misc this 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-initpivot_root/switch_root into the active slot squashfs; wire boot_slot.c's active-slot selection into the mount path; remount data/misc inside 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

RepoRequired work
playos-refdistroFull bootable squashfs rootfs build (if needed) + minimal initramfs shim
playos-initpivot_root/switch_root into the active slot squashfs; slot-selection wiring; real rollback
playos-specThis 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 IDTaskPrimary repoStatusNotes / evidence
S11.5-T1Confirm/produce a complete bootable squashfs rootfs + minimal initramfs shimplayos-refdistrodoneFull userspace confirmed in output/ally/images/rootfs.squashfs; added /EFI mountpoint to rootfs-overlay.
S11.5-T2pivot_root/switch_root into the active slot squashfsplayos-initdoneplayos_pivot_to_active_slot() added in src/mount.c; switch_root idiom (MS_MOVE + chroot + exec /init).
S11.5-T3Wire boot_slot.c active-slot selection into the mount pathplayos-initdoneboot_slot_read() selects playos-a/playos-b; called in main.c after ESP/boot-slot block.
S11.5-T4Make 3-strike rollback real end-to-endplayos-initdoneHost 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-T5A/B update + rollback validation matrix (was S11-T9)playos-refdistroin progressAutomated 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:

  1. 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).
  2. If complete: proceed to T2 — only the initramfs pivot path needs wiring.
  3. 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_rootexec /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:

  1. In playos-init/src/main.c, after the early mount of the ESP and boot.json read, mount the active slot partition (by GPT label playos-a/playos-b) read-only at a staging path.
  2. Mount /data (label playos-data) read-write and /misc under the staged new root.
  3. pivot_root (or switch_root if a minimal initramfs shim is used) into the squashfs, then exec the real init.
  4. 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:

  1. Reuse the existing boot_slot.c slot-selection result to choose playos-a vs playos-b at mount time.
  2. Confirm a missing/corrupt boot.json falls back to slot A and recreates the file (behavior already specified in Sprint 11).
  3. Keep boot_slot.c as the single source of truth — do not duplicate slot-selection logic in main.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:

  1. Verify the existing rollback path fires when the new root fails to boot: boot_count >= 3 && health != "good" → mark bad, switch active_slot, reboot.
  2. 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.
  3. 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):

  1. Fresh install → 5-partition layout → boot.json shows slot A good.
  2. Apply valid bundle → boot.json shows slot B pending → reboot → slot B active → 60s → slot B good.
  3. Corrupt slot B → reboot 3× → rollback to slot A.
  4. /data content (game files, saves) survives update and rollback unchanged.
  5. Invalid-signature bundle → rejected before any partition write.
  6. 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

EvidenceHow it is producedCurrent state
Read-only rootcat /proc/mounts shows / on squashfs ro; touch /usr/testEROFS⚠️ Not yet asserted by the QEMU harness (pivot is asserted; ro/EROFS check not captured)
Slot selectionchanging 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"
Rollback3-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/verifyhost test_boot_slot valid / bad-signature / bad-magic casesctest 2/2 pass
Data survivalfile hashes before/after update match⚠️ Pending ROG Ally — not covered by the QEMU harness
Signature rejectionlog showing bundle rejected before any write✅ host test_boot_slot bad-signature case
Version APIplayos_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/test fails with EROFS — squashfs ro is by construction, but the current QEMU harness does not assert this
  • Active-slot selection in boot.json drives which slot is mounted — QEMU Scenario A reads active_slot: "a"
  • A valid bundle applied to the inactive slot does not affect the running system — host test_boot_slot apply 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
  • /data content survives update and rollback unchanged — not yet exercised on QEMU or hardware
  • Invalid-signature bundle is rejected before any partition write — host test_boot_slot bad-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 /data and 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-init has a supervision loop that forks/execs trusted daemons after the data mount.
  • /data is the persistent writable partition (label playos-data), mounted read-write by playos_mount_data.
  • Rootfs is read-only squashfs; persistent state must live under /data, not /etc.
  • playos-refdistro Ally 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, and network-manager available; this sprint installs Dropbear + dhcpcd and uses BusyBox udhcpc for 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, Realtek RTL8152, CDC Ethernet/NCM/EEM, RNDIS host, SMSC95xx/MCS7830). No Wi-Fi, no RFKILL, no nl80211, no wpa_supplicant in 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: dhcpcd remains installed (BR2_PACKAGE_DHCPCD, the client Sprint 16 already locks in network-options.md §10), but the bring-up uses the already-present BusyBox udhcpc applet (CONFIG_UDHCPC=y) because the default dhcpcd sample config rejects QEMU slirp DHCP offers. No new BusyBox applets are added; Sprint 16 may still use dhcpcd for the Wi-Fi interface.
  • Persistent SSH state: Dropbear host keys and authorized_keys live under /data/ssh/ (persistent playos-data), not /etc/dropbear or /var/run/dropbear (tmpfs/ephemeral) and not on the read-only squashfs. Bring-up bind-mounts /data/ssh over /root/.ssh (Dropbear's default authorized_keys location).
  • Bring-up ownership: a small rootfs-overlay helper (/usr/bin/playos-ssh-bringup) handles interface bring-up, DHCP, host-key generation, bind-mounting, and exec dropbear. playos-init forks/execs it as a supervised trusted daemon after the data mount, alongside compositor/shell/overlay.
  • Access channel: developer-only, no shell UI, no playos-runtime messages, 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: enable NETDEVICES plus USB-NIC and QEMU test NIC drivers (no wireless).
  • playos-refdistro — Ally/installer defconfigs: enable dropbear (+ client, DISABLE_REVERSEDNS) and dhcpcd.
  • playos-refdistro — rootfs overlay: /usr/bin/playos-ssh-bringup and a build-time /root/.ssh directory (bind-mount target on the read-only squashfs).
  • playos-init — spawn and supervise playos-ssh-bringup after /data mounts.
  • playos-spec — this document; SUMMARY.md + roadmap.md wiring; a networking note in kernel-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-tools networking — still post-MVP / after Sprint 16.
  • Production hardening: removing Dropbear/BusyBox from the production image is Sprint 12, not this sprint.

Required Repository Changes

RepoRequired work
playos-refdistroKernel USB-NIC config; Dropbear + dhcpcd packages; playos-ssh-bringup overlay script; /root/.ssh build-time directory
playos-initSupervise playos-ssh-bringup as a trusted daemon after the data mount
playos-specThis 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 IDTaskPrimary repoStatusNotes / evidence
S11.6-T1Enable USB-NIC kernel config (no wireless)playos-refdistrodoneKernel config edited; build validation pending in T5
S11.6-T2Enable Dropbear + dhcpcdplayos-refdistrodoneDefconfigs edited; build validation pending in T5
S11.6-T3playos-ssh-bringup + playos-init supervisionplayos-refdistro, playos-initdoneScript written; supervision code compiled + host tests pass
S11.6-T4Host keys + authorized_keys persistence under /data/sshplayos-refdistrodoneScript generates keys + bind-mounts; /root/.ssh overlay shipped
S11.6-T5QEMU + Ally validationplayos-refdistroin progressQEMU done: udhcpc lease 10.0.2.15 + public-key SSH login verified; Ally pending hardware

Update the Status column as work progresses: not startedin progressblocked 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, and BR2_PACKAGE_DROPBEAR_DISABLE_REVERSEDNS=y to the Ally and installer defconfigs.
  • Add BR2_PACKAGE_DHCPCD=y (the same client Sprint 16 uses). BusyBox udhcpc/ip are 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 dhcpcd remains 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:

  1. Detect a wired NIC (iterate /sys/class/net/*, skip loopback). If none, log and exit cleanly (USB NIC may be hot-plugged later).
  2. Run udhcpc -i <if> -q to bring the interface up and obtain/keep a lease (udhcpc daemonises by default).
  3. mkdir -p /data/ssh and generate Dropbear host keys with dropbearkey if absent (RSA/ed25519).
  4. mount --bind /data/ssh /root/.ssh (the /root/.ssh target must already exist in the read-only squashfs).
  5. exec dropbear -F -R -E (foreground so playos-init can supervise it; -R/-E chosen 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_keys lives 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_keys on the mounted playos-data partition.
  • Do not commit any private key or seed an authorized_keys in 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

EvidenceHow it is produced
USB NIC appears/sys/class/net shows an eth*/en* interface
DHCP leaseudhcpc log shows an IPv4 address assigned
SSH supervisedplayos-init supervision log shows Dropbear as a child; ps shows it under PID 1
Key persistencereboot without host-key regeneration; existing authorized_keys still works
Login worksssh -i <devkey> root@<ip> succeeds
No wireless pulled inkernel config still has # CONFIG_WIRELESS is not set
Production unchangedproduction 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_keys persists under /data/ssh and survives reboot
  • playos-init supervises 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_supplicant code 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 default dhcpcd config is slirp-incompatible); Sprint 16 may standardize on dhcpcd for the Wi-Fi interface instead of introducing a different DHCP stack.
  • Sprint 16 adds Wi-Fi (wpa_supplicant + playos-net) on top of dhcpcd, 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-init currently 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.sock exists and is the trusted control path.
  • Production defconfig still 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 libplayos mask 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.sock is root:playos-trusted, mode 0660; only playos-shell and playos-overlay are in playos-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

RepoRequired work
playos-initSpawn 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-compositorIntercept reserved buttons at the libinput/seat layer and never forward them to clients
playos-runtimeEnforce control socket ownership and permissions (root:playos-trusted, 0660)
playos-refdistroCreate 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-specDocument 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 IDTaskPrimary repoStatusNotes / evidence
S12-T1Drop game privileges at spawn: UID ~1000, PR_SET_NO_NEW_PRIVS, capability dropplayos-initnot started
S12-T2Apply Landlock allowed/denied path policy to game processesplayos-initnot started
S12-T3Apply seccomp syscall allowlist to game processesplayos-initnot started
S12-T4Grant only DRM render-node access; deny privileged KMS/masterplayos-initnot started
S12-T5Enforce reserved-button input isolation end-to-endplayos-compositor, playos-platform-api, playos-initnot started
S12-T6Harden control.sock trusted-client auth and permission checksplayos-runtime, playos-initnot started
S12-T7Strip debug tools/services from the production imageplayos-refdistronot started
S12-T8Verify signed game manifests (warn-only in MVP)playos-init, playos-runtimenot started
S12-T9Document Secure Boot chain; create and rotate dev signing keys in image buildplayos-refdistro, playos-specnot started

Update the Status column as work progresses: not startedin progressblocked 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

EvidenceHow it is produced
Game runs as playos-game with no capabilitiesid and /proc/self/status captured inside a test game
Primary DRM node deniedSecurity test binary attempts open("/dev/dri/card0", O_RDWR)
Raw input device deniedSecurity test binary attempts open("/dev/input/event0", O_RDONLY)
Reserved buttons absentInput-stream dump while SYSTEM/QUICK_MENU are pressed
Control socket deniedSecurity test binary attempts connect() to control.sock
seccomp activeSecurity test binary attempts mount() and observes EPERM
Landlock activeSecurity test binary reads another game's save directory and is denied
Production image is debug-freeCI production lint log and post-build script output
Manifest warn-only behaviorLaunch log for an unsigned manifest
Secure Boot chain documentedsecurity-model.md plus keys/dev/ in CI artifacts

Acceptance Criteria

  • Game process runs as playos-game user (verified via playos_system.h test call or logs)
  • open("/dev/dri/card0", O_RDWR) returns EACCES in a game process
  • open("/dev/input/event0", O_RDONLY) returns EACCES in 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.sock returns EACCES from a playos-game process
  • seccomp filter: mount() from game process returns EPERM
  • 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-game with no capabilities and NoNewPrivs set.
  • The control socket is root:playos-trusted mode 0660; 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 in libplayos public API.
  • Intel kernel: use CONFIG_DRM_I915 or CONFIG_DRM_XE depending on target hardware generation; disable AMD-only configs in the Intel defconfig.
  • Intel audio: CONFIG_SND_HDA_INTEL plus Intel-specific codecs.
  • Intel power: CONFIG_X86_INTEL_PSTATE and CONFIG_INTEL_RAPL.
  • Mesa backend: gallium-drivers=iris for Gen 9+ (i965 for older); Intel Vulkan (ANV) is deferred to a future Vulkan sprint.
  • Backend selection: playos-platform-api selects its backend at runtime via the PLAYOS_BACKEND environment 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 PlayOSInputBackend abstraction and PLAYOS_BACKEND selection.
  • Validate playos_power_get_info() and profile requests against Intel sysfs paths.
  • Add make intel-config, make intel-build, and make intel-usb-image targets.
  • 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

RepoRequired work
playos-compositorValidate GPU selection by PCI vendor, log the selected vendor/device/path, add a fallback-order test, remove any residual card0 hardcoding
playos-platform-apiFormalize PlayOSInputBackend and PLAYOS_BACKEND dispatch; validate Intel power sysfs paths and non-AMD device strings
playos-refdistroAdd playos_intel_pc_defconfig, Intel kernel configs/firmware, Mesa Iris, and make intel-* targets
playos-samplesRun sample-triangle, sample-input, and sample-audio on the Intel PC and record portability evidence
playos-specUpdate 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 IDTaskPrimary repoStatusNotes / evidence
S13-T1Validate GPU discovery by PCI vendor and fallback order on Intel hardwareplayos-compositornot started
S13-T2Add Intel PC kernel configuration and firmwareplayos-refdistronot started
S13-T3Enable Mesa Iris Gallium backend for Intelplayos-refdistronot started
S13-T4Formalize PlayOSInputBackend and PLAYOS_BACKEND dispatchplayos-platform-apinot started
S13-T5Validate Intel power sysfs paths and device stringsplayos-platform-apinot started
S13-T6Add make intel-* Buildroot targets and USB image generationplayos-refdistronot started
S13-T7Validate sample-game portability on the Intel PCplayos-samplesnot started
S13-T8Document dual-vendor support in the specsplayos-specnot started

Update the Status column as work progresses: not startedin progressblocked 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

EvidenceHow it is produced
Intel GPU selected by PCI enumerationCompositor boot log on the Intel PC shows 0x8086 and the device path
Fallback order correcttest_gpu_select.c runs synthetic multi-GPU vendor data through the selector
Intel kernel config correctGenerated .config diff against the Intel defconfig
Mesa Intel rendererCompositor/Mesa init log on the Intel PC
Hardware accelerationsample-triangle renderer string and frame throughput on the Intel PC
Input portabilitysample-input controller-state dump on the Intel PC
Audio portabilitysample-audio output on the Intel PC
Power API validplayos_power_get_info() output for CPU, GPU, and battery on the Intel PC
Non-AMD device stringplayos_system_device_model() output on the Intel PC
Intel image buildsCI log for make intel-build and produced image artifacts
AMD regressionFull 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-triangle runs with hardware acceleration on Intel (Mesa reports Intel ...)
  • sample-input receives controller input on Intel PC (USB gamepad or built-in)
  • sample-audio plays 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 or libplayos public API
  • make intel-build succeeds 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-api backend model is validated as portable via PlayOSInputBackend and PLAYOS_BACKEND.
  • The Mesa Intel (Iris) backend is enabled and hardware acceleration is verified.
  • make intel-build and make intel-usb-image targets 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.yml workflow.
  • 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 1 in playos.h.
  • Library version: LIBPLAYOS_VERSION_MAJOR 0, MINOR 1, PATCH 0.
  • SONAME: libplayos.so.0 for 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 SONAME libplayos.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-spec completion: 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

RepoRequired work
playos-platform-apiAPI stability review, versioning and SONAME, Doxygen docs, code examples, getting-started guide
playos-refdistroRelease pipeline, recovery image/menu, performance measurement, production image hygiene
playos-initRecovery entry logic: boot-count exceeded, button hold, repeated compositor failure
playos-shellMinimal recovery UI (text or simple Raylib on SimpleDRM/framebuffer)
playos-specAuthoritative reference completion: README, architecture, roadmap, platform API, IPC, security, ADRs, game-dev docs
All reposVersion 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 IDTaskPrimary repoStatusNotes / evidence
S14-T1Freeze the public API and set PLAYOS_API_VERSION 1playos-platform-apinot started
S14-T2Set library version and SONAME libplayos.so.0playos-platform-apinot started
S14-T3Complete Doxygen docs and code examplesplayos-platform-apinot started
S14-T4Implement the tag-triggered release pipelineplayos-refdistronot started
S14-T5Run the full 19-criterion MVP smoke testplayos-refdistronot started
S14-T6Implement recovery modeplayos-init, playos-shell, playos-refdistronot started
S14-T7Measure and document the performance baselineplayos-refdistronot started
S14-T8Complete playos-spec and game-developer guidesplayos-specnot started
S14-T9Enforce production image hygiene and signed artifactsplayos-refdistronot started

Update the Status column as work progresses: not startedin progressblocked 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

EvidenceHow it is produced
Public API frozen and versionedplayos.h shows PLAYOS_API_VERSION 1 and version macros
SONAME correctreadelf -d libplayos.so shows SONAME libplayos.so.0
API fully documentedDoxygen output with zero undocumented public symbols
Release pipeline worksCI log from a test tag produces installer, update bundle, and SDK tarball
Installer boots a clean devicePhysical install of playos-v0.1.0-rog-ally-installer.img on a clean ROG Ally
Update applies via A/Bupdate.playosb applied through the A/B flow
MVP criteria metCommitted smoke-test report with all 19 criteria passing
SDK usable by a second developerMinimal game compiled from sdk-headers.tar.gz on a Linux host
Recovery worksBoot into recovery from button hold and boot-count-exceeded; menu on SimpleDRM
Performance baselineCommitted performance report with measurements and filed gaps
Specs completeplayos-spec README/nav plus all referenced docs present and consistent

Acceptance Criteria

  • All 19 MVP criteria pass on physical ROG Ally hardware
  • libplayos public headers are fully documented (Doxygen)
  • PLAYOS_API_VERSION 1 defined; SONAME is libplayos.so.0
  • "Building Your First PlayOS Game" guide is complete and tested
  • Release pipeline produces signed installer.img and update.playosb from a tag push
  • playos-v0.1.0-rog-ally-installer.img installs successfully on a clean ROG Ally
  • playos-v0.1.0-rog-ally-update.playosb applies 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-spec repository 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 libplayos C ABI is frozen at PLAYOS_API_VERSION 1, versioned 0.1.0, with SONAME libplayos.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 libplayos C ABI is frozen at PLAYOS_API_VERSION 1 and the Raylib backend ABI is stable.
  • sdk-headers.tar.gz exists from Sprint 14, but it is headers-only — it is not a full toolchain + library SDK.
  • musl builds of libplayos and libraylib exist only inside the Buildroot output tree.
  • PLAYOS_BACKEND=stub exists in playos-platform-api as 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-sdk download 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-musl toolchain tarball, or an Alpine/musl base image.
  • Three build profiles: device, desktop, and emulator.
  • Device backend: libraylib is built with the PLATFORM_PLAYOS backend; libplayos uses the real evdev path.
  • Desktop shim: the host libplayos shim is seeded from PLAYOS_BACKEND=stub and maps keyboard/gamepad to the controller ABI, no-op'ing lifecycle calls.
  • Emulator profile: runs the device build 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 libplayos headers plus musl static/shared libraries at PLAYOS_API_VERSION 1.
  • Ship musl libraylib headers and libraries built with PLATFORM_PLAYOS.
  • Provide a CMake toolchain file and pkg-config files for the device profile.
  • Build the desktop host shim seeded from PLAYOS_BACKEND=stub.
  • Implement the desktop build profile (native gcc + raylib desktop backend + host shim).
  • Implement the emulator build 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 desktop profile/shim.
  • Store integration and SDK signing/distribution (post-MVP).
  • Web, mobile, or other platform targets.
  • Changes to the frozen PLAYOS_API_VERSION 1 public ABI.

Required Repository Changes

RepoRequired work
playos-toolsPackage the SDK: toolchain, headers/libs, CMake toolchain, pkg-config, profile scripts, SDK docs
playos-platform-apiProvide the desktop host shim seeded from PLAYOS_BACKEND=stub; ensure headers/libs are SDK-ready
playos-shellExport the PLATFORM_PLAYOS Raylib backend build for SDK packaging
playos-refdistroExtract musl libplayos/libraylib from Buildroot output; provide the QEMU/container emulator image
playos-samplesBuild 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 IDTaskPrimary repoStatusNotes / evidence
S15-T1Package the musl toolchain tarball/base imageplayos-toolsnot started
S15-T2Ship libplayos headers and musl static/shared libsplayos-platform-api, playos-toolsnot started
S15-T3Ship musl libraylib with the PLATFORM_PLAYOS backendplayos-shell, playos-refdistro, playos-toolsnot started
S15-T4Provide CMake toolchain and pkg-config for deviceplayos-toolsnot started
S15-T5Build the desktop host shim seeded from PLAYOS_BACKEND=stubplayos-platform-api, playos-toolsnot started
S15-T6Implement the desktop build profileplayos-toolsnot started
S15-T7Implement the emulator build profileplayos-refdistro, playos-toolsnot started
S15-T8Build the reference sample entirely via the SDK and validate all profilesplayos-samples, playos-toolsnot started

Update the Status column as work progresses: not startedin progressblocked 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

EvidenceHow it is produced
SDK is self-containedFresh-host build of a minimal game with only the SDK installed
Toolchain is muslfile/readelf on a produced binary shows the musl interpreter
libplayos packagedsdk/lib/libplayos.{a,so} and headers present at PLAYOS_API_VERSION 1
libraylib packagedsdk/lib/libraylib.{a,so} with PLATFORM_PLAYOS backend build
CMake path workscmake build of the reference sample using playos-toolchain.cmake
pkg-config path worksgcc $(pkg-config --cflags --libs playos) produces a device binary
desktop shim worksGame runs in a window with keyboard/gamepad input on a Linux host
desktop profile worksNative desktop binary built from the same game.c
emulator profile worksDevice artifact boots and renders in QEMU/container
Reference sample validatedPer-profile results committed in playos-samples

Acceptance Criteria

  • A developer on a fresh x86_64 Ubuntu/Alpine host can produce a valid musl bin/game with only the SDK installed.
  • The same game.c builds for device, desktop, and emulator profiles.
  • The desktop profile runs the game in a windowed desktop environment on Linux (and, via a shim, Windows) with controller-equivalent input.
  • The emulator profile boots the device artifact in QEMU and renders + accepts input.
  • The SDK ships the musl toolchain, libplayos headers/libs at PLAYOS_API_VERSION 1, and musl libraylib with PLATFORM_PLAYOS.
  • Both the CMake toolchain and pkg-config files produce a valid device binary.
  • The reference sample is built entirely via the SDK and validated across all three profiles.
  • The device binary 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-sdk ships the musl toolchain, libplayos (PLAYOS_API_VERSION 1), and musl libraylib with PLATFORM_PLAYOS.
  • The device, desktop, and emulator build profiles all work from a single game source.
  • The desktop host shim is seeded from PLAYOS_BACKEND=stub and maps keyboard/gamepad to the controller ABI.
  • The emulator profile boots a device artifact in QEMU/container without hardware.
  • The reference sample builds entirely via the SDK and validates all three profiles.
  • The public libplayos ABI remains frozen at PLAYOS_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-trusted group, 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, not iwd — built D-Bus-free: CONFIG_CTRL_IFACE=unix, CONFIG_CTRL_IFACE_DBUS=n. Talks to the kernel over nl80211.
  • dhcpcd, not BusyBox udhcpc — 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-net bridge daemon — links libwpa_client (wpa_ctrl), translates wpa_supplicant's control protocol ↔ playos-runtime JSON frames.
  • Control plane = control.sock — new network messages ride the existing trusted socket; the shell is the only UI.
  • Trust boundarywpa_supplicant and dhcpcd control sockets live under /run/playos/net/, owned root:playos-trusted, mode 0660. Games are not in playos-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/mt7922 firmware blobs (redistributable via linux-firmware).
  • Buildroot packages: wpa_supplicant (D-Bus disabled), dhcpcd, playos-net.
  • playos-net daemon (new): wpa_supplicant control socket ↔ playos-runtime IPC bridge.
  • playos-runtime: new control messages (scan, connect, disconnect, status, async events).
  • playos-init: spawn and supervise wpa_supplicant, dhcpcd, and playos-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

RepoRequired work
playos-net (new)Bridge daemon: wpa_ctrlplayos-runtime JSON; profile management
playos-runtimeNetwork control messages + framing docs
playos-initSupervise wpa_supplicant/dhcpcd/playos-net; network policy
playos-shellWi-Fi settings screen
playos-refdistroKernel config, wpa_supplicant + dhcpcd + playos-net packages, firmware overlay
playos-specThis sprint; runtime-ipc.md network messages; kernel-config.md networking section

playos-net is a new small daemon. It may start as playos-refdistro/src/playos-net/ (as playos-overlay did) 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 IDTaskPrimary repoStatusNotes / evidence
S16-T1Enable Wi-Fi kernel config + firmwareplayos-refdistronot startedCFG80211/MAC80211/MT7921E currently deferred
S16-T2Package wpa_supplicant (D-Bus-free) + dhcpcdplayos-refdistronot startedCONFIG_CTRL_IFACE_DBUS=n
S16-T3Implement playos-net bridge daemonplayos-netnot startedwpa_ctrl ↔ control.sock
S16-T4Add network messages to playos-runtimeplayos-runtimenot startedadditive; keep v: 1
S16-T5Supervise network daemons in playos-initplayos-initnot started
S16-T6Wi-Fi settings screen in playos-shellplayos-shellnot started
S16-T7Network profile persistenceplayos-netnot started/data/config/network/
S16-T8End-to-end validation (Ally + QEMU)playos-refdistronot 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/mt7922 Wi-Fi firmware to board/playos/rog-ally/rootfs-overlay/lib/firmware/mediatek/ (redistributable via linux-firmware, unlike AMD GPU blobs).
  • Done when: the Ally's mt7921e interface appears (ip link shows wlan0/mlan0 after firmware load).

S16-T2 — Package wpa_supplicant (D-Bus-free) + dhcpcd

  • Build wpa_supplicant with CONFIG_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.sock and /run/playos/net/dhcpcd.sock, owned root:playos-trusted 0660.
  • 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.sock and control.sock.
  • Translates wpa_supplicant control-protocol events (CTRL-EVENT-CONNECTED, CTRL-EVENT-SCAN-RESULTS, CTRL-EVENT-DISCONNECTED) into playos-runtime JSON.
  • Runs with dropped privileges in playos-trusted; never exposes wpa_supplicant directly to games.
  • Done when: a ScanNetworks request on control.sock returns 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-init spawns and supervises wpa_supplicant, dhcpcd, and playos-net after 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 -9 restart.

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.sock returns EACCES.
  • Production lint: no D-Bus, no BusyBox, no iwd in 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

EvidenceHow it is produced
Interface upip link shows the mt7921e interface
Successful associationwpa_supplicant log + NetworkStateChanged: connected on control.sock
DHCP leaseNetworkStatusReport.ip populated
Scan resultsScanResults JSON contains the test SSID
Trust boundaryplayos-game connect to net sockets → EACCES
No D-Bus/BusyBoxSprint 12 production lint passes
Reconnect after rebootProfile 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 iwd present 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-runtime control 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.c translates playos_input_get_controller_state() into CORE.Input.Gamepad.*.
  • Sprint 7 overlay architecture live: playos-overlay is a separate trusted raylib process that maps above any surface (playos_overlay_v1), owns "Virtual keyboard (future)" per playos-overlay-spec.md.
  • Compositor uses wlr_scene (shell/game/overlay trees) + wlr_seat "seat0", but forwards no pointer/touch today (system_button.c intercepts keyboard BTN_MODE only).
  • wlroots 0.20 pinned (provides wlr_text_input_v3 and wlr_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 in playos-platform-api would 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_pointer alongside touch (the same seat plumbing); this also makes raylib GetMousePosition()/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 crosses control.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 libplayos ABI: rcore_playos.c owns the game's Wayland connection, so it implements the zwp_text_input_v3 client and exposes ShowOnScreenKeyboard() / HideOnScreenKeyboard() plus GetCharPressed() (which "just works" for the committing client). Non-raylib engines implement zwp_text_input_v3 directly.
  • No keyboard input forwarding change in this sprint. The OSK produces text through the text-input protocol; physical keyboard forwarding (system_button.c currently 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_at hit-testing, per-output coordinate transform).
  • Compositor zwp_text_input_v3 manager + 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_pointerCORE.Input.Touch.* / mouse; zwp_text_input_v3 client → 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-osk game that invokes the OSK and echoes typed text.
  • wayland-protocol.md and playos-overlay-spec.md updates.

Explicitly Out of Scope

  • Physical keyboard forwarding to clients (system_button.c change) — separate sprint.
  • Mouse-only desktop pointer UX beyond what wl_pointer gives raylib for free.
  • Multi-touch gestures (pinch/zoom), multi-touch beyond MAX_TOUCH_POINTS tracking.
  • 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

RepoRequired work
playos-compositorwlr_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-runtimeplayos-v1.xml overlay protocol extension (OSK visibility/commit) + regenerated headers
playos-samplescom.playos.sample-osk game
playos-specThis 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 IDTaskPrimary repoStatusNotes / evidence
S17-T1Pointer + touch seat forwarding in compositorplayos-compositornot startedwlr_cursor + wlr_scene_node_at
S17-T2zwp_text_input_v3 manager + focus routingplayos-compositornot startedwlr_text_input_v3
S17-T3Raylib backend touch/pointer → CORE.Input.Touch.*/mouseplayos-shellnot startedwl_touch/wl_pointer listeners
S17-T4Raylib backend text-input client + ShowOnScreenKeyboard()playos-shellnot startedzwp_text_input_v3 client
S17-T5Overlay OSK UI + layout + tap-to-typeplayos-overlaynot startedraylib component
S17-T6Overlay ↔ compositor OSK commit protocolplayos-runtimenot startedextend playos_overlay_v1
S17-T7Shell text-field integration (Wi-Fi passphrase)playos-shellnot startedfirst consumer
S17-T8OSK sample game + end-to-end validationplayos-samplesnot startedcom.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_cursor bound to the output layout; create an wlr_xcursor_manager (or use wlr_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 a wlr_scene_surface, call wlr_seat_pointer_notify_enter() / wlr_seat_touch_notify_down() with the surface-local coordinates. Map to the surface via wlr_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's current.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_v3 via wlr_text_input_manager_v3_create(display).
  • On wlr_text_input_v3 enable/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_string from the overlay back to that wlr_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_touch and wl_pointer listeners on the seat (in addition to the existing keyboard listener in the shell's src/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. Populate previousButtonState on the next PollInputEvents().
  • Touch: down/up/motion → CORE.Input.Touch.position[i], CORE.Input.Touch.pointId[i], CORE.Input.Touch.pointCount, mapping wl_touch touch IDs to MAX_TOUCH_POINTS slots. Set currentTouchState/previousTouchState consistent 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_v3 from the registry; create a zwp_text_input_v3 and 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's CORE.Input.Keyboard.charPressedQueue (and optionally map a synthetic Enter keycode into keyPressedQueue for 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. Use core_keyboard_testbed.c as the visual starting point (key rectangles + labels), but extend it from "visualize" to "input": on touch, hit-test GetTouchPosition() against each key Rectangle and 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_info dimensions/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_string to the focused client's wlr_text_input_v3 (commit_string) and osk_key to keysym/done events.
  • Version the interface (bump playos_overlay_v1 to version="2"; keep v1 clients working — additive).
  • Regenerate headers with wayland-scanner; document in wayland-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 calls HideOnScreenKeyboard() 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 renders GetCharPressed() output and touch coordinates.
  • Validation matrix on the Ally:
    • Touch reaches the focused game (GetTouchPosition non-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.
  • QEMU/CI: compositor + raylib backend compile with wlr_text_input_v3 and touch symbols; no touch device present → touch path is inert without crashing; text-input round-trip can be unit-tested with a mock wlr_text_input_v3 client.

Done when: the sample echoes typed text on the Ally, and the shell Wi-Fi passphrase flow works end-to-end.


Verification and Evidence

EvidenceHow it is produced
Touch reaches surfaceDebug log of wl_touch down/up with surface-local coords
OSK raises on enableosk_visibility(1) emitted when focused client enables text input
Text committed to focused clientcommit_string received by the focused wlr_text_input_v3 only
Game echocom.playos.sample-osk echoes GetCharPressed() output
Shell passphraseWi-Fi passphrase field accepts masked text via OSK
Background isolationBackground game's commit_string count stays 0
No reserved-key synthesisOSK never emits SYSTEM/QUICK_MENU (static assert/audit)
CI buildwlroots + 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_string to 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_MENU input
  • com.playos.sample-osk echoes 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 (from playos-platform-api), vendored static raylib, and optionally libplayos-trusted (from playos-runtime).
  • Wayland protocol code is generated to C from playos-v1.xml + xdg-shell via wayland-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.

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

DimensionPlusMinus
Memory safetyEliminates 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 iterationRicher abstractions (records, LINQ, test framework) can speed state/UI iterationRaylib's UI layer is intentionally thin; C# does not remove the need to bind Raylib or re-derive rendering
Native interopP/Invoke + source generators can wrap a C ABIThe entire surface — libplayos, wayland-client/wayland-egl, EGL, GLESv2, trusted IPC, evdev — must be wrapped or generated; marshaling on musl is untested
Runtime & packagingSelf-contained .NET removes a host dependencySupported 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
RenderingRaylib'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 protocolsCommunity C# Wayland bindings existplayos-v1.xml + xdg-shell are generated to C via wayland-scanner; a C# binding generator would need the private protocols ported
Total cost/benefitModest long-term maintainability upsideReplaces ~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's linux-musl-x64 RID 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-external machinery 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-runtime restricted control client (control.sock) and optional libplayos-trusted.
  • Direct evdev input, to preserve reserved SYSTEM/QUICK_MENU button survival exactly as src/input.c does today.

Raylib backend loss — strongest architectural minus

  • Raylib is C. The PlayOS value is the custom rcore_playos.c backend 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.json parsing, power/thermal model types, toast/screenshot state. This exercises the "C# is nicer for UI state" hypothesis without touching Buildroot, musl, Wayland, or Raylib.

If ever funded, a single bounded spike would, in order:

  1. Host-only state/screen POC — a .NET console/unit-test harness against a re-typed manifest.json and state model.
  2. 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.
  3. Binding probe — generate a C# binding for playos-v1.xml + xdg-shell and complete one wl_surface round-trip on a host Wayland compositor.
  4. 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.md document.
  • 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

RepoRequired work
playos-specAdd 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 IDTaskPrimary repoStatusNotes / evidence
S18-T1Runtime + Buildroot feasibility (musl / NativeAOT)playos-specnot startedADR-0003, architecture §14
S18-T2Native interop + Wayland protocol bindingsplayos-specnot startedlibplayos, wayland-client, playos-v1.xml
S18-T3Raylib backend loss + rendering-path optionsplayos-specnot startedADR-0006, rcore_playos.c
S18-T4Bounded host-only de-risking spike definitionplayos-specnot startedstate/screen-layer POC only

S18-T1 — Runtime + Buildroot feasibility

  • Confirm the .NET supported Linux RID situation against ADR-0003 (musl only): linux-x64 (glibc) vs linux-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-external machinery 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: libplayos C 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 by wayland-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

EvidenceHow it is produced
Assessment recordedSprint-18.md present with plus/minus table and verdict
Roadmap indexedSUMMARY.md and post-mvp.md link the sprint
Link integritymdbook build passes
No implementation driftNo 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.md and post-mvp.md are updated
  • mdbook build passes

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-marketplace is 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):
    1. SDK-first — a developer can publish without owning a PlayOS device.
    2. Multiple store sources — official, community, OEM, private, and LAN; never hard-code a single store.
    3. Self-hostable — a store can be run by communities, OEMs, or individuals.
    4. Spec-first — package format (.gpk), signing, entitlements, and catalog behaviour are specified before implementation.
    5. Trust — verify package signatures; respect permissions and the trust model.
  • Spec references to a store/package (no "marketplace" by name):
    • post-mvp.md Tier 3 — Store Integration and Download Manager (API addition playos_store.h; depends on Wi-Fi, signed .play packages, cloud saves) and Signed .play Content Packages (signed archive: manifest.json, binary, assets, content hash tree; replaces plain directory installs).
    • roadmap.md post-MVP list — "Download manager and store integration" and "Signed .play content packages".
    • ideas.md §14.2 — historical "Later .play package" 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-game manifest.json contract that any package format must extend or reference.
  • Naming discrepancy: playos-marketplace uses .gpk; post-mvp.md, ideas.md, and roadmap.md use .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:

  1. Completes the delivery path that post-mvp.md already names ("Store Integration and Download Manager"). Wi-Fi (Sprint 16) is the transport; the marketplace is the source and policy layer.
  2. Enables third-party content without manual file transfer — the natural partner of the playos-sdk (publishers) and the shell's game library (players).
  3. 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.
  4. Adds entitlements and revocation — ownership/licensing that the MVP's plain-directory model cannot express.
  5. Enables themes and developer content, not just games, matching the marketplace's broader mandate and future shell theming.

Mapping to existing architecture

Marketplace concernExisting PlayOS foundation to build on
On-device downloadWi-Fi (playos-net, Sprint 16)
Package verificationSigned manifests (Sprint 12), HMAC/Hash verification (Sprint 11)
Atomic install / rollbackA/B update engine pattern (Sprint 11), /data/downloads staging
Storage/data/games/<id>/, /data/downloads/, /data/updates/ (Sprint 6)
Client surfaceplayos_store.h (named in post-mvp.md), shell "Store" screen, playos-tools CLI
Publishingplayos-sdk (post-MVP) — SDK-first publishing with no device
Trustsecurity-model.md trust zones; manifest.json + signatures

Spec gaps and open decisions (the real blockers)

  1. 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.
  2. Package extension naming is inconsistent. Marketplace says .gpk; spec says .play. Recommendation: adopt .gpk as the canonical content-package extension (it covers games, themes, and developer content, and is already the marketplace repo's term), then reconcile the .play references in post-mvp.md, ideas.md, and roadmap.md. This is a decision to lock during spec authoring, not silently here.
  3. 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.
  4. 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.
  5. Client integration surface is only a name. playos_store.h is referenced but not specified; the shell has no "Store" screen in any sprint; playos-tools currently 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.


  1. 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 .play Content Packages" post-MVP item.
  2. 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.
  3. Client surface. Specify playos_store.h (query, download, install progress, entitlement check), a shell "Store" screen, and a playos-tools/SDK publish command.
  4. Then implement playos-marketplace (catalog service, publishing flow, client integration) against those specs.

Scope

In Scope (this sprint)

  • This Sprint-19.md document.
  • The assessment, gap analysis, and recommended sequencing above.
  • Cross-linking from SUMMARY.md and post-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 .play references in post-mvp.md/ideas.md/roadmap.md now — the naming reconciliation is a decision for Part X authoring.

Required Repository Changes

RepoRequired work
playos-specAdd 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 IDTaskPrimary repoStatusNotes / evidence
S19-T1Survey store/package references and reconcile with marketplace AGENTS.mdplayos-specnot startedpost-mvp.md, roadmap.md, ideas.md, Sprints 12/15/16
S19-T2Assess package-format gap and .gpk vs .play namingplayos-specnot startedschemas/game-manifest-v1.json, ideas.md §14.2
S19-T3Assess catalog + entitlement + trust + multi-store modelplayos-specnot startedsecurity-model.md, marketplace golden rules
S19-T4Define spec-first sequencing + client integration surfaceplayos-specnot startedplayos_store.h, shell Store screen, SDK publish

S19-T1 — Survey store/package references

  • Enumerate every store/package mention in playos-spec: post-mvp.md Tier 3, roadmap.md post-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.md golden 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 (see schemas/game-manifest-v1.json) with plain-directory install, and that the future signed package is only sketched in post-mvp.md ("Signed .play Content Packages") and ideas.md §14.2.
  • Document the .gpk (marketplace) vs .play (spec) discrepancy and recommend .gpk as canonical, with .play references 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 a playos-tools/SDK publish command.
  • 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

EvidenceHow it is produced
Assessment recordedSprint-19.md present with gap analysis, verdict, and sequencing
Roadmap indexedSUMMARY.md and post-mvp.md link the sprint
Naming discrepancy documented.gpk vs .play explicitly recorded with a recommendation
Link integritymdbook build passes
No implementation driftNo marketplace code or spec chapters are produced by this sprint

Acceptance Criteria

  • The assessment states what playos-marketplace is, what exists today, and that it is an empty stub
  • All existing store/package references in playos-spec are enumerated
  • The dangling "Part X / Part XI" spec references are identified as the primary blocker
  • The .gpk vs .play naming 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.md and post-mvp.md are updated
  • mdbook build passes

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 .gpk naming 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.sock IPC to playos-init; playos-init is the only fork/exec (playos-init/src/supervisor.c). The child path is hard-coded to executable with an access(..., X_OK) gate and execl(exe_path, exe_path, NULL) (supervisor.c:731,806). The shell manifest validator additionally requires executable + architecture and checks access(F_OK) (playos-shell/src/screen_library.c:153-265). A media/web launch target needs a new manifest type/url/media_uri, an init exec branch, and shell validation relaxed for that type. The manifest schema game-manifest-v1.json already has additionalProperties: 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 a wlr_seat (playos-compositor/src/compositor.c:286-298), so arbitrary Wayland clients can attach and create xdg_toplevels. However there is no wlr_seat_set_keyboard/pointer/touch forwarding — src/system_button.c:63 states 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-api audio 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 radeonsi EGL/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_defconfig set BR2_x86_64, BR2_TOOLCHAIN_BUILDROOT_MUSL, no glibc. mpv, librespot, spotifyd, WPE WebKit, and Cog are 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.c spawns with only setsid() + env + execl(); no seccomp/Landlock/PR_SET_NO_NEW_PRIVS/user namespaces yet. security-model.md is 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 playos for librespot|spotifyd|mpv|yt-dlp|ytdl|ytmusic|youtube|WPE|Cog|webkit|InnerTube|innertube finds no existing media-client integration (only this sprint and unrelated raylib noise). Buildroot grep for BR2_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.

ServiceRecommended clientAudioDRMNotes
Spotifylibrespot (preferred) / spotifydALSA backendNone (own streaming, no EME)librespot is a Rust daemon; drive it over its IPC/CLI, not the Wayland seat
YouTubempv + yt-dlpALSA (--ao=alsa)None for standard contentyt-dlp resolves streams; mpv plays them. Prefer a standalone/static binary on-device (see YouTube Music)
YouTube Musicmpv + yt-dlp/InnerTube searchALSANoneMusic is the same pipeline as YouTube; search/library UX decides the wrapper shape
BrowserWPE WebKit + CogALSA (WebKit audio)No WidevineLightweight embedded WebKit; no Netflix/DRM. Highest musl/security risk
NetflixOut of scopeWidevine (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 mpv via --input-ipc-server JSON and performs search via yt-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-rs or ytmusicapi) exposing library/playlist/queue semantics, with mpv as 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

DimensionPlusMinus
Platform reachSpotify/YouTube/YouTube Music + a browser turn the handheld into a genuine entertainment deviceMedia is not the MVP's growth area; it partially competes with the spec's games-first positioning
Architecture fitNative clients speak ALSA directly and attach as Wayland surfaces — no ADR-0003/ADR-0007 reversalThe 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
Toolchainmpv/librespot/spotifyd/WPE are all musl-buildable in principleNone are in Buildroot today; WPE WebKit on musl is a flagged risk and must be validated in the spike
AudioDirect ALSA is a first-class fitAny client that assumes PulseAudio/PipeWire must be configured or patched to ALSA
SecurityPer-client IPC keeps the attack surface bounded and avoids trusting the Wayland seatNetwork-facing parsers (HLS/DASH/HTML/JS) should not run unconfined — the Sprint 12 sandbox is a prerequisite
Performance1080p software decode is realistic on the Ally's CPU; 16 GB RAM is ample4K/HDR is effectively out of scope without VAAPI/VDPAU/dmabuf import; browser memory can be large
DRMSpotify/YouTube standard content need no Widevine/EMEYouTube/Spotify premium and all of Netflix are DRM-gated and stay out of scope
Cost/benefitThe launch-target change (manifest type + init exec branch + shell validation) is small and non-breakingBrowser (WPE) and YouTube Music search UX are the real multi-sprint items; packaging five new Buildroot packages is non-trivial

Hard blockers

  1. Compositor input model. There is no wl_seat keyboard/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, librespot control socket, etc.) with PlayOS-side controller binding. This is a design constraint, not a blocker, but it must be respected in every client.
  2. No sandbox. supervisor.c has no seccomp/Landlock/PR_SET_NO_NEW_PRIVS. Network-facing media/browser clients should not ship unconfined; the Sprint 12 sandbox must land first.
  3. No Buildroot packages. mpv, ffmpeg, librespot/spotifyd, WPE WebKit, and Cog (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.
  4. 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.
  5. 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:

  1. Manifest: extend game-manifest-v1.json with a type field (native | media | web) plus url/media_uri. The schema already has additionalProperties: true, so this is non-breaking.
  2. Shell: relax the validator at playos-shell/src/screen_library.c:153-265 for media/web types — do not require executable+architecture when a url/media_uri is present, and render a media/web card in the library.
  3. Init: branch the exec path in playos-init/src/supervisor.c (currently ELF-only at :731 X_OK gate, :806 execl) so media/web types spawn the appropriate client (mpv, librespot, Cog) with a fixed argv derived from the manifest.
  4. Input: do not build compositor wl_seat forwarding 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).
  5. Audio: clients use ALSA directly (mpv --ao=alsa, librespot/spotifyd ALSA backend), so the Tier 2 "Dedicated Audio Service" is not a prerequisite.
  6. Security: gated on the Sprint 12 sandbox before any device integration.

What it would actually take

In dependency order:

  1. Land the Sprint 12 sandbox (seccomp + Landlock + PR_SET_NO_NEW_PRIVS) so media/browser clients are not unconfined.
  2. Package and cross-compile ffmpeg + mpv, librespot/spotifyd, and (optionally) WPE WebKit + Cog for x86_64-musl in Buildroot.
  3. Extend the launch lifecycle (manifest type/url/media_uri, shell validator relaxation, init exec branch) as a non-breaking change.
  4. Build the controller-first wrappers (Spotify control, YouTube/YouTube Music search+play UI) driving each client over its own IPC.
  5. Validate the Wayland attach for mpv (--vo=gpu --gpu-context=wayland or wlshm) and Cog under 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.


If ever funded, a single bounded spike would, in order:

  1. Package/build probe — confirm mpv/ffmpeg/librespot/spotifyd/WPE WebKit/Cog actually cross-compile against x86_64-musl in Buildroot, and record which are non-starters. No defconfig merge yet.
  2. Wayland attach probe — validate mpv and Cog attach as fullscreen Wayland surfaces under a host nested compositor, using --vo=gpu --gpu-context=wayland/wlshm. Confirm the game-role fit.
  3. IPC/controller probe — confirm mpv --input-ipc-server and librespot/spotifyd can be driven without the Wayland seat, and sketch the controller→IPC mapping.
  4. 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.
  5. Explicit stop — do not proceed to device integration, Buildroot merge, sandbox implementation, or Netflix.

Scope

In Scope (this sprint)

  • This Sprint-20.md document.
  • 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/Cog integration or Buildroot packaging.
  • Netflix, Chromium/CEF/Electron, Widevine/EME, X11/Xwayland.
  • Compositor wl_seat input forwarding, audio service, sandbox, or hardware-video-decode implementation.
  • Changing the existing shell, compositor, or init in this sprint.

Required Repository Changes

RepoRequired work
playos-specReplace 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 IDTaskPrimary repoStatusNotes / evidence
S20-T1Native-client feasibility (musl build, ALSA, Wayland attach)playos-specnot startedADR-0003, ADR-0007, compositor.c, no Buildroot packages
S20-T2Integration design (manifest type, init exec branch, shell validation, IPC)playos-specnot startedsupervisor.c:731,806, screen_library.c:153-265, manifest schema
S20-T3YouTube Music app shape (Option A vs B) + browser riskplayos-specnot startedmpv IPC, yt-dlp/InnerTube, WPE/Cog musl risk
S20-T4Bounded host-only de-risking spike definitionplayos-specnot startedbuild probe, Wayland probe, IPC probe, YT Music UX spike

S20-T1 — Native-client feasibility

  • Confirm mpv/ffmpeg, librespot/spotifyd, and WPE WebKit/Cog are 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 requires executable+architecture (playos-shell/src/screen_library.c:153-265).
  • Confirm the manifest schema has additionalProperties: true, so a type/url/media_uri extension is non-breaking.
  • Confirm the compositor forwards no wl_seat input (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 mpv via --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

EvidenceHow it is produced
Assessment recordedSprint-20.md present with recommended stack, plus/minus, blockers, integration design, and verdict
Roadmap indexedSUMMARY.md, post-mvp.md, and roadmap.md link the sprint with the new title
Grounded analysisEach blocker cites a source file, ADR, or grep result
Link integritymdbook build passes
No implementation driftNo 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 Music mpv+yt-dlp, browser WPE/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_seat input 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, and roadmap.md are updated
  • mdbook build passes

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-41 builds /data/saves/%s, /data/cache/%s, and related paths from PLAYOS_GAME_ID only. include/playos/playos_storage.h:26-45 documents per-game paths, and :81-90 returns /data/games as the shared game-install root.
  • Launch does not yet drop privileges. playos-init/src/supervisor.c:762-806 spawns the game child with setsid() + environment + execl() and no setuid/setgid/PR_SET_NO_NEW_PRIVS. The env set at :773-782 already includes PLAYOS_GAME_ID, PLAYOS_INSTALL_PATH, PLAYOS_SAVE_PATH, PLAYOS_CACHE_PATH, WAYLAND_DISPLAY, PLAYOS_LIFECYCLE_FD, and PLAYOS_LAUNCH_TOKEN — the natural injection point for a future PLAYOS_PROFILE_ID. security-model.md:228-235 records this pre-Sprint-12 gap.
  • IPC auth is identity-based, not profile-based. playos-init/ipc/ipc_server.c:83-88 accepts 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-424 scans /data/games with a readdir and manifest validation (:154-265 requires executable + architecture). Game installs stay shared, so discovery is unchanged.
  • Security model already assumes one identity. security-model.md:58-65 privileges a single playos-game; :119 chowns game data to playos-game:playos-game; :203-212 lists Landlock allowed paths; :337 names user namespaces as post-MVP hardening. Sprint 12 makes this real.
  • A /data/profiles/ placeholder already exists but is unused. architecture.md:363 documents saves/<game-id>/ profiles/, autosaves/, settings/ and :371 shows profiles/; partition-layout.md:88 and :96 reserve /data/profiles/. The path is in the design, not yet in playos_storage.c.
  • post-mvp.md already has a profiles block. post-mvp.md:160-163 lists Multiple Local User Profiles with a /data/saves/<profile>/<game-id>/ layout that this sprint corrects to /data/profiles/<pid>/....
  • ideas.md wording conflict. ideas.md:104-105 lists "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

ApproachIdentityIsolation mechanismEffortRiskRecommendation
A — path-scoped profilesSingle playos-game uid + PLAYOS_PROFILE_ID envProfile-scoped Landlock allowlist + per-profile storage pathsLow–moderateLowRecommended
B — uid-per-profileOne Linux uid per profileOS-level user separationHighMediumDefer 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

DataScopeNotes
Game savesPer-profile/data/profiles/<pid>/saves/<game-id>/
Game cachePer-profile/data/profiles/<pid>/cache/<game-id>/
PlayOS settingsPer-profile/data/profiles/<pid>/settings/
ScreenshotsPer-profile/data/profiles/<pid>/screenshots/
Account credentials / tokensPer-profile (when linked)/data/profiles/<pid>/auth/ — encrypted, platform-managed, out of the game sandbox
Game installsShared/data/games/ unchanged
Wi-Fi / network configSystem-wideNot per-profile
LogsSystem-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

DimensionPlusMinus
ProductPlayStation-style local profiles on a shared family handheldMore UX surface: profile picker, switcher, per-profile settings screens
ArchitectureLayers on Sprint 12's data-driven sandbox; single uid means no IPC/auth/compositor reworkAdds a profile-id dimension to every storage path and env contract
IsolationProfile-scoped Landlock gives real save/cache isolation between profilesGame installs are shared by design; not a hard security boundary between mutually-untrusting profiles
MigrationLegacy /data/saves/<game-id> maps cleanly to a default profileOne-time migration code plus a read fallback
EffortLow–moderate; mostly storage-path derivation + shell UIPer-profile settings/screenshots must be threaded through the versioned libplayos C ABI

Integration design (how it fits the existing lifecycle)

  1. Storage: playos_storage.c/.h derive paths from PLAYOS_GAME_ID and PLAYOS_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.
  2. Launch: playos-init loads the active profile at boot, sets PLAYOS_PROFILE_ID and profile-scoped PLAYOS_SAVE_PATH/PLAYOS_CACHE_PATH into the game env (supervisor.c:773-782), and builds the Landlock allowlist with the profile prefix.
  3. Shell: add profile selection at boot, a user switcher, and per-profile settings; discovery of shared game installs (/data/games) is unchanged.
  4. 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.md is 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.

LayerWhat it isUnlocks
Local profileOffline identity on device (PLAYOS_PROFILE_ID)Local saves/settings/screenshots; offline play
PlayOS Network accountOnline identity in playos-cloud (OAuth2 or PlayOS account service)Marketplace access, cloud saves, multiplayer/matchmaking, friends/presence
LinkLocal 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.md document.
  • 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.md or rewriting any ADR.

Required Repository Changes

RepoRequired work
playos-specAdd 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 IDTaskPrimary repoStatusNotes / evidence
S21-T1Define "multiple local users" and select the isolation approachplayos-specnot startedideas.md:104-105, security-model.md:58-65
S21-T2Design profile-scoped storage and launch envplayos-specnot startedplayos_storage.c:36-41, supervisor.c:773-782
S21-T3Design shell profile selection/switcher and settingsplayos-specnot startedscreen_library.c:379-424, shared /data/games
S21-T4Align Sprint 12 for isolation foundationsplayos-specnot startedLandlock 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-105 without editing it).
  • Recommend Approach A (path-scoped profiles) over Approach B (uid-per-profile), with the single-playos-game identity 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/games is 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-scoped PLAYOS_SAVE_PATH/PLAYOS_CACHE_PATH env at supervisor.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-game uid (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

EvidenceHow it is produced
Assessment recordedSprint-21.md present with verdict, approach comparison, plus/minus, isolation matrix, integration design, migration, and disambiguation
Approach selectedApproach A recommended over Approach B with rationale
Storage/env design groundedplayos_storage.c:36-41, supervisor.c:773-782, playos_storage.h:81-90 cited
Sprint 12 alignedSprint-12.md carries single-identity + data-driven-sandbox foundations and the deferral note
Link integritymdbook build passes
No implementation driftNo 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.md wording 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, and sprints/roadmap.md are updated
  • mdbook build passes

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-internal profiles/ 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_cb that 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 to LV_KEY_* and using lv_group/lv_gridnav focus. (indev, groups)
  • Current shell stack. external/raylib/src/platforms/rcore_playos.c owns Wayland/EGL/GLES2 and frame-callback vsync; src/input.c reads controller evdev directly; src/render_util.c wraps 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 Texture2D can receive CPU pixels via UpdateTexture, and sub-rectangle updates can use the GL texture id directly for glTexSubImage2D.

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.

PathHow it worksRiskRole 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 quadLowestMinimum first check only — proves LVGL, input mapping, and the dev loop; not the target renderer
2 — lv_opengles_texture + GPU draw unitLVGL 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 pacingLow-to-mediumLikely production path — GPU-accelerated while retaining rcore_playos.c
3 — LV_USE_WAYLAND full portLVGL owns the Wayland surface/EGL/vsync; drop rcore_playos.c for the shellMediumLong-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

DimensionPlusMinus
Resolution adaptationlv_dpx, %, flex/grid, min/max — real responsive layoutsBreakpoints and assets still need design/testing
Look & feelWidgets, themes, styles, animations; retained-mode reduces drawing codeDefault themes are plain; a console-grade look needs custom theme/fonts/images
Integration riskNo rcore_playos.c change for Path 1/2; additive, reversiblePath 1 has two rendering stacks (raylib blit + LVGL software render); Path 3 replaces the shell backend and needs an ADR
Inputlv_group/lv_gridnav maps cleanly to D-padNo native gamepad indev; custom evdev→LV_KEY_* glue required
BuildPure C99, musl-safeNo Buildroot package; spike vendors LVGL under playos-shell/external/ and defers Buildroot packaging
Footprint/performanceTiny; partial refresh; dirty-area upload; Path 2 offloads rasterization to GPUPath 1 is CPU-bound: 1080p@60 full-frame upload ~500 MB/s if not using dirty areas; must use sub-rect updates
TotalFaster path to a polished shell UI than hand-rolled raylibOne-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_cb below is replaced by the GPU draw-unit or LV_USE_WAYLAND driver 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=32 and match raylib's RGBA8 texture format; verify alpha channel order and byte order before assuming correctness.
  • Partial upload: LVGL's flush_cb already reports dirty areas; use glTexSubImage2D on those rectangles. A first pass may use whole-frame UpdateTexture to validate correctness, then switch to sub-rect uploads for the 60 fps budget.
  • Tick: drive lv_tick_inc from raylib's GetFrameTime(); call lv_timer_handler() once per frame.
  • Input: create a KEYPAD indev whose read_cb translates the existing input.c controller state into LV_KEY_UP/DOWN/LEFT/RIGHT/NEXT/PREV/ENTER/ESC, and use lv_gridnav for 2D focus movement. A/B map to LV_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/lvgl and 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_cb into a raylib Texture2D; re-evaluate against Path 2/3 before doing anything beyond the smoke test.
  • Map the existing controller evdev input to an LVGL KEYPAD indev 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/lvgl packaging (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

RepoRequired work
playos-shellVendor LVGL v9.5 under external/lvgl; add CMake option; add experimental LVGL screen and input mapping (gated)
playos-specAdd 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 IDTaskPrimary repoStatusNotes / evidence
S22-T1Vendor LVGL and gate behind a CMake optionplayos-shellnot startedexternal/lvgl, PLAYOS_SHELL_EXPERIMENTAL_LVGL
S22-T2LVGL → raylib texture renderer + test screen (Path 1 smoke test)playos-shellnot startedflush_cb, Texture2D, lv_display
S22-T3Controller → LVGL keypad/group navigationplayos-shellnot startedinput.c, lv_indev, lv_gridnav
S22-T4Verify 60 fps + correctness; write go/no-goplayos-specnot startednested-Wayland dev env

S22-T1 — Vendor LVGL and gate behind a CMake option

  • Vendor LVGL v9.5 under playos-shell/external/lvgl (mirroring the existing external/raylib pattern).
  • Add PLAYOS_SHELL_EXPERIMENTAL_LVGL (default OFF) to playos-shell/CMakeLists.txt; when ON, 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.c with an enter/update/draw triple matching the shell module convention.
  • In enter, create an LVGL display with a full-screen draw buffer and a flush_cb that uploads dirty areas into a raylib Texture2D.
  • 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 KEYPAD indev whose read_cb maps the existing controller state to LV_KEY_UP/DOWN/LEFT/RIGHT/NEXT/PREV/ENTER/ESC.
  • Attach lv_gridnav (or lv_group focus) 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 in post-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

EvidenceHow it is produced
Experimental build workscmake -B build -DPLAYOS_SHELL_EXPERIMENTAL_LVGL=ON && cmake --build build in dev env
LVGL renders through raylibTest screen visible in nested Wayland; colors correct (no channel/byte swap)
Controller navigation worksD-pad/A/B move focus and activate LVGL widgets
60 fps maintainedFrame-time measured in the dev env; dirty-area upload path used
No default-build driftBuild with PLAYOS_SHELL_EXPERIMENTAL_LVGL=OFF unchanged
Link integritymdbook build passes

Acceptance Criteria

  • LVGL v9.5 is vendored under playos-shell/external/lvgl and gated behind PLAYOS_SHELL_EXPERIMENTAL_LVGL (default OFF)
  • A gated experimental screen renders LVGL through a raylib Texture2D without changing rcore_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, and post-mvp.md are updated
  • mdbook build passes

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:

  1. playos-tools host 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.
  2. 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's ApplyUpdate IPC and boot.json contract unchanged — the update engine itself needs no modification. Depends on: Sprint 11 (A/B update engine + /data/updates/*.playosb contract — MVP); phase 2 additionally depends on Wi-Fi (playos-net). Sprint: not yet allocated (post-MVP). Phase 1 is a standalone playos-tools task; 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_cbglTexSubImage2D), 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.md defers CFG80211/MAC80211/MT7921E and 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

OptionStackD-Bus?BusyBox?EffortFit
Aiwd + private dbus-brokerYes (private bus)NoMediumFits only as a contained private bus
Bwpa_supplicant + dhcpcd + playos-net bridgeNoNoMediumBest architectural fit
CCustom nl80211 supplicantNoNoVery highNot viable

3. The D-Bus Problem

PlayOS owns IPC by layer:

  • playos-runtime owns all internal IPCcontrol.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-trusted and 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:

ConcernOwner
Carry messages between trusted componentsplayos-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:

  1. A dedicated playos-net daemon (recommended) — links libwpa_client, exposes new playos-runtime messages (Scan, Connect, Status) on control.sock. Matches the playos-net naming in post-mvp.md and keeps playos-init scoped to supervision (architecture.md already says init "Does NOT own: network").
  2. 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, owned root:playos-trusted, mode 0660.
  • iwd runs as a trusted daemon. Games are not in playos-trusted, so they never see the bus.
  • The shell talks to iwd through new playos-runtime control messages (e.g. Scan, Connect, Status), not raw D-Bus.
ProsCons
Modern, minimal, fast roamingIntroduces D-Bus at all
Bundled DHCP clientSecond internal IPC mechanism (even if private)
Better WPA3 (SAE) supportMore moving parts (dbus-broker + policy)

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:

  1. wpa_supplicant — association, WPA2/WPA3 (SAE via the in-tree hostapd/wpa_supplicant code), over nl80211. No D-Bus.
  2. dhcpcd — standalone DHCPv4/DHCPv6 + IPv4LL client (Buildroot BR2_PACKAGE_DHCPCD). No BusyBox, no D-Bus.
  3. playos-net bridge — a thin trusted daemon that reads wpa_supplicant's control socket and re-exposes it as new playos-runtime control 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.

ProsCons
Zero D-Bus — matches PlayOS philosophyOlder, more config-heavy than iwd
Same Unix-socket transport as playos-runtimeNeeds 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.md Tier 2), so BT is correctly deferred regardless.

Two paths:

PathImplication
Land Wi-Fi D-Bus-free now (Option B); introduce a private dbus-broker later when BT landsWi-Fi ships without D-Bus; BT justifies the single private bus later
Accept private D-Bus up front (Option A) for bothOne 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 trusted playos-net bridge. 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-broker scoped 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_t bitmask: SOUTH(A) 1<<0, EAST(B) 1<<1, WEST(X) 1<<2, NORTH(Y) 1<<3, START 1<<4, SELECT 1<<5, SYSTEM 1<<6 (reserved), QUICK_MENU 1<<7 (reserved), DPAD_UP/DOWN/LEFT/RIGHT 1<<8..11, L1 1<<12, R1 1<<13, L3 1<<14, R3 1<<15, POWER 1<<16 (reserved).
  • PlayOSAxis enum: 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() and playos_input_get_controller_state() (returns 0 on success, -1 when 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:

FunctionRole
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 via EVIOCGABS(ABS_Z) and a 255 fallback.
  • drain_fd() caps at MAX_EVENTS_PER_CALL 64 events per non-blocking read.
  • Discovery is throttled: RESCAN_INTERVAL_US 2000000 (2 s) and stale-fd re-scan via fcntl(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 index 0, calls playos_input_get_controller_state(&state), and sets CORE.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 - 1 from platform [0,1] to Raylib [-1,1].
  • PollInputEvents() (in rcore.c) calls PlayOSPollGamepad(), and is invoked from EndDrawing() at the end of each frame when SUPPORT_CUSTOM_FRAME_CONTROL is 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:

  1. /proc/bus/input/devices dump to the persistent log (shell_input_dump_proc_devices, input.c:326).
  2. Per-node capability dump (shell_input_dump_capabilities, input.c:355).
  3. Installs a non-blocking inotify watch on /dev/input (IN_CREATE|IN_DELETE|IN_ATTRIB) for gamepad hotplug — best-effort, ignored if unavailable.
  4. find_gamepad_device().
  5. shell_input_open_reserved_nodes().
  6. One-time trigger and stick calibration reads.

Per frame, shell_input_poll() (input.c:865) does:

  1. Saves controller_prev, resets buttons_pressed.
  2. Drains any pending inotify events; if the gamepad fd is still missing, retries discovery immediately instead of waiting out the throttle.
  3. 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 full opendir + open + 2× ioctl scan costs ~0.5 s on the Ally and caused visible hiccups (input.c:899-906).
  4. 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/Y and BTN_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.c breaks 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

  1. One-frame stick lag (primary). Sticks and triggers enter via Raylib, which refreshes at EndDrawing() — one frame after the shell reads it (main.c ordering). Implemented: the Raylib axes overlay was removed; shell_input_poll() now decodes sticks/triggers from evdev on the same fresh frame as buttons.

  2. 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 single SHELL_STICK_DEADZONE 0.05f constant in input.c governs all shell stick decoding, matching the platform-api backend.

  3. 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.

  4. Hotplug latency up to 2 s. Both platform API and shell throttle missing-device re-scan to 2 s. Implemented for the shell: an inotify watch on /dev/input triggers 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).

  5. Reserved keys are momentary pulses. Home/Command/M1-M2 arrive as 7–9 ms value=1value=0 pulses 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.

  6. 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() in backend_evdev.c now logs the chosen gamepad fd and device name (both preferred and fallback paths), alongside the existing home/vendor fd logs.

  7. Timestamping is not surfaced. PlayOSControllerState.timestamp_us exists but the shell's own path does not timestamp frames. Implemented (opt-in): shell_input_drain_fd() measures queue-to-drain age using clock_gettime(CLOCK_MONOTONIC) vs the kernel input_event.time, logged once per second when PLAYOS_INPUT_LATENCY_LOG is set. Off by default to avoid per-event overhead on the 60 Hz hot path.


  1. De-lag the shell stick path. Read Raylib axes after EndDrawing()/PollInputEvents(), or read sticks from evdev directly… Implemented: sticks are now read from evdev directly in shell_input_poll(); the Raylib overlay was removed.

  2. Unify deadzone handling. One constant, one function, used by both the shell evdev decode and the Raylib overlay… Implemented: single SHELL_STICK_DEADZONE 0.05f constant; the Raylib overlay no longer exists to disagree.

  3. 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 when PLAYOS_INPUT_LATENCY_LOG is set. Full cross-layer (platform snapshot → Raylib → frame start) instrumentation is still a future extension if deeper profiling is needed.

  4. 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.

  5. 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.

  6. 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.

  7. 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 pad at /dev/input/event5.
  • Vendor node: Asus Keyboard at /dev/input/event8 (fd=9).
  • Three Asus Keyboard nodes skipped by the gamepad matcher as "missing stick axes": event6, event7, event8.
  • The volume-node matcher selected event8 again (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=1value=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. Fixedshell_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:

RepositoryOwns
playos-specArchitecture, contracts, ADRs, schemas, roadmap
playos-platform-apiPublic libplayos C ABI
playos-runtimeInternal IPC and lifecycle transport
playos-compositorwlroots compositor
playos-shellRaylib controller shell
playos-refdistroBuildroot integration and images

Rationale

  • Dependency enforcement: Separate repos make it impossible to accidentally import private internals from playos-runtime into a game (the game can only depend on playos-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.lock in playos-refdistro is 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-init is 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 socat or a simple Python script
  • Sufficient performance: IPC volume is low (a few messages per user action) — protocol overhead is irrelevant

Alternatives Considered

OptionRejected because
D-BusRequires dbus-daemon; adds systemd/activation complexity; not suitable for PID 1
VarlinkGood fit but less widely known; minimal tooling advantage
gRPCHeavy dependency (protobuf, HTTP/2); overkill for this use case
NetlinkKernel-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 cJSON or yyjson)
  • 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

OptionRejected because
glibcLarger; more complex; static linking edge cases; overkill for a console OS
uClibc-ngLess 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 libplayos C ABI must be compatible with musl — no glibc-specific extensions
  • The PLAYOS_API_VERSION compatibility 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

OptionRejected because
Raw libwaylandWould require reimplementing all DRM/KMS, buffer management, and Wayland protocol infrastructure that wlroots provides — massive scope increase
Sway as a baseSway is a tiling window manager; its policy (floating/tiled windows, workspaces) would have to be removed entirely — more work than starting from wlroots
WayfirePlugin architecture is more complex than needed; wlroots is a cleaner starting point
Mutter / KWinHeavy GNOME/KDE dependencies; incompatible with musl and minimal initramfs

Guiding Rule

wlroots implements mechanisms; playos-compositor implements 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 rauc package
  • 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

OptionNotes
MenderSaaS-oriented; heavier client; less embedded-only focused
SWUpdateGood alternative to RAUC; very similar feature set
CustomSimplest for EFI-image model; no external dependencies; more code to maintain

Consequences

  • RAUC or the custom updater must be integrated into playos-refdistro and 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 libplayos C 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_toplevel with 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 its PollInputEvents() just resets internal input state
  • Disables desktop features (resize, decorations, clipboard, multi-window)

Alternatives Considered

OptionRejected because
SDL2Heavier; desktop-oriented features; PlayOS would need to strip a lot
Raw OpenGL ESNo windowing abstraction — more code to write for the shell UI
QtVery heavy; complex build; LGPL licensing concerns for static linking
GTKDesktop-oriented; heavy; Wayland support has desktop assumptions
GodotFull game engine is overkill for the shell; heavy binary size

Consequences

  • Games targeting PlayOS are recommended to use Raylib, but the libplayos C ABI is engine-agnostic — SDL2 or other frameworks can be adapted
  • Raylib version must be pinned in versions.lock
  • The rcore_playos.c backend 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

  • card0 is not guaranteed: On systems with multiple DRM devices, or depending on driver load order, the integrated GPU may not be card0
  • 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 card0 is 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 = 0x1002 and PCI_VENDOR_INTEL = 0x8086 are 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:

  1. Boot directly into a controller-first shell with no visible Linux desktop or login screen.
  2. Keep the operating system small, immutable, reproducible, and recoverable.
  3. Use mature Linux drivers for GPU, audio, input, storage, power, and thermal management.
  4. Keep playos-shell alive throughout the session so returning from a game is immediate.
  5. Run one isolated game process at a time.
  6. Recover cleanly when a game crashes or becomes unresponsive.
  7. Allow trusted PlayOS overlays to appear above a running game.
  8. Reserve system controls that games cannot consume directly.
  9. Provide a stable, engine-agnostic PlayOS Platform API with an authoritative C ABI, plus Raylib and C++ convenience layers.
  10. Store games and user state separately from the system image.
  11. Support the ROG Ally first and add Intel graphics only after the AMD implementation is stable.
  12. 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-compositor implements 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

  1. UEFI loads BOOTX64.EFI.
  2. The EFI stub transfers control to the Linux kernel.
  3. Linux initializes memory, interrupts, ACPI, PCIe, storage, input, graphics, and audio drivers.
  4. Linux unpacks the initramfs into RAM.
  5. Linux starts /init, implemented by playos-init, as PID 1.
  6. playos-init mounts /dev, /proc, /sys, and /run.
  7. playos-init discovers and mounts the PlayOS data partition.
  8. playos-init starts playos-compositor.
  9. The compositor initializes its wlroots backend, DRM/KMS, renderer, input seat, and Wayland socket.
  10. The compositor launches playos-shell with the correct Wayland environment and trusted identity.
  11. The shell maps its main fullscreen surface and displays the game library.
  12. The user selects a game.
  13. The shell sends a launch request to playos-init through restricted playos-runtime control IPC.
  14. playos-init validates and spawns the game.
  15. The game connects to Wayland and commits its first usable frame.
  16. The compositor verifies the launch identity and makes the game foreground.
  17. The application receives lifecycle and safe platform services through playos-platform-api while playos-runtime transports trusted internal events.
  18. 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:

  1. The player selects a game in the shell.
  2. The shell sends LaunchGame(game_id) over PlayOS control IPC.
  3. playos-init validates the manifest, executable, permissions, and one-game rule.
  4. playos-init prepares save paths, cache paths, process group, lifecycle channel, and a one-time launch identity.
  5. The shell shows a Launching state and remains visible.
  6. playos-init spawns the game with WAYLAND_DISPLAY and PlayOS environment variables.
  7. The game connects to the compositor and creates its surface.
  8. The compositor matches the client to the expected launch identity.
  9. The compositor waits for the first valid committed buffer.
  10. Only then does it switch foreground from shell to game.
  11. 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:

  1. Removes normal input focus from the game.
  2. Sends a background lifecycle event.
  3. Shows the shell or trusted overlay.
  4. 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:

  1. playos-init records the exit status.
  2. The compositor destroys or ignores stale game surfaces.
  3. The compositor reveals the already-running shell surface.
  4. Focus returns to the shell.
  5. 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:

  1. Enumerate DRM devices and render nodes.
  2. Resolve each device to its PCI identity.
  3. Identify the device connected to the active display.
  4. Validate renderer initialization.
  5. Select the scanout and render device.
  6. 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:

  • i915 or xe, 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:

  1. Search for the expected partition GUID, label, or UUID.
  2. Mount it if valid.
  3. Enter provisioning mode if absent.
  4. Show the target and destructive impact.
  5. Require explicit confirmation or a manufacturing flag.
  6. Create the filesystem and expected metadata.
  7. Create the directory tree.
  8. 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.so for 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-init runs as root.
  • playos-compositor receives only required display and input privileges.
  • playos-shell runs 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; privileged playos-runtime control 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:

  1. Download a signed system image.
  2. Verify its signature and compatibility.
  3. Write the inactive slot.
  4. Mark it as the next boot candidate.
  5. Boot it once.
  6. Record a health-success marker.
  7. 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-init restarts 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:

RepositoryOwnership
playos-specArchitecture, public contracts, RFCs, ADRs, schemas, roadmap, and product documentation.
playos-platform-apiPublic libplayos C ABI, portable implementations, C++ wrappers, and engine adapters.
playos-runtimeInternal lifecycle transport, launch and control IPC, private Wayland protocols, restricted service clients, and OS integration.
playos-compositorwlroots compositor, DRM/KMS ownership, surfaces, focus, trusted roles, overlays, and reserved-input policy.
playos-shellRaylib controller-first shell and trusted user experience.
playos-refdistroBuildroot 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-refdistro may package and pin components but must not redefine their public contracts.
  • Existing compositor code under playos-runtime must migrate to playos-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.
  • perf for 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:

  1. The ROG Ally boots directly from UEFI into PlayOS.
  2. The Linux kernel and initramfs are available as a UEFI-bootable 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. The compositor uses wlroots with AMDGPU, DRM/KMS, GBM, EGL, and Mesa.
  7. The shell renders through Wayland using the Raylib PlayOS backend.
  8. The shell and sample game consume the public playos-platform-api C ABI.
  9. Trusted launch, lifecycle transport, and compositor-control mechanisms remain internal to playos-runtime.
  10. The shell requests a game launch and playos-init spawns and supervises it.
  11. The compositor waits for the game's first valid frame before making it foreground.
  12. The game renders with hardware acceleration and receives normal controller input.
  13. The reserved System button returns to PlayOS UI and backgrounds or pauses the game.
  14. Resume returns to the same running game without restarting it.
  15. The 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. The system image is immutable.
  19. 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.

SprintFocusPrimary outcome
0Build and UEFI FoundationA six-repository, reproducible Buildroot factory that boots a minimal PlayOS EFI image through QEMU/OVMF.
1playos-init and Minimal Boot SupervisionA real playos-init running as PID 1 with versioned private control IPC.
2Compositor Skeleton and Wayland SessionA minimal wlroots compositor that creates a Wayland session and presents one trusted fullscreen client.
3ROG Ally Kernel and Device Bring-UpReliable USB boot, essential devices, and the first qualified Platform API input backend contract.
4AMDGPU and Native DRM/KMSThe compositor permanently owns the Ally display through AMDGPU and DRM/KMS.
5Raylib-Powered PlayOS ShellA hardware-accelerated Raylib shell consuming the public PlayOS Platform API.
6Persistent Storage and Game DiscoveryPersistent ext4 storage, safe Platform API paths, and shell-visible game discovery.
7Game Launch, Lifecycle, System Button, and OverlayThe complete console lifecycle with a public application API and private trusted control path.
8ALSA AudioReliable ALSA audio with safe public controls across lifecycle transitions.
9Power, Battery, Thermal, and Suspend FoundationsSafe power behavior exposed through a restricted public Platform API.
10Installer and Internal-Disk DeploymentA tested installation path from removable media to the ROG Ally internal SSD.
11Immutable Images and A/B UpdatesSigned, atomic A/B system updates with automatic rollback.
12Security HardeningA hardened boundary between public Platform API calls, trusted runtime control, and games.
13Intel ExpansionProof that the architecture and Platform API backend model are portable to Intel.
14Production ReadinessA 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-spec and reflected here.

22. Post-MVP Roadmap

Add only when the core console lifecycle is stable:

  • playos-device for hardware and power policy.
  • playos-net with iwd for Wi-Fi.
  • Dropbear SSH in explicit Developer Mode only.
  • playos-update as a PlayOS wrapper around the update engine.
  • A dedicated playos-input service 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 .play content 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-init owns processes, playos-compositor owns display and focus, playos-shell owns the user experience, playos-platform-api owns the public libplayos C ABI, playos-runtime owns 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.