Sprint 5.5 — Shell → Raylib 6.0 Migration
Goal: Resolve the divergence between Sprint 5's specified rendering framework (Raylib, per ADR-0006 and playos-shell-spec.md) and the actual shell implementation (raw EGL/GLES2 with a hand-rolled shader + bitmap font). Upgrade the vendored Raylib from 5.5 to 6.0, implement the custom rcore_playos.c platform backend against the 6.0 backend contract, and port the shell's rendering to the Raylib draw API — without changing the shell's UX, input model, or lifecycle behaviour.
Primary Outcome: The ROG Ally boots into the same four-screen, controller-first shell, but every frame is now drawn through Raylib 6.0 via a custom Wayland/EGL platform backend. The raw-GLES2 renderer in render_util.c is retired. PLAYOS_SHELL_USE_RAYLIB=ON is the Buildroot default, and Raylib 6.0 is pinned in versions.lock.
Status: 🟢 Complete — Raylib 6.0 shell verified in QEMU and on the ROG Ally
Prerequisites: Sprint 5 complete — the shell renders and navigates on the Ally through the raw EGL/GLES2 path (4 screens, controller-first input, frame-callback vsync, lifecycle handling, per-call trusted IPC). Raylib 5.5 is vendored at playos-shell/external/raylib/ but unused. The custom backend contract is specified in ADR-0006.
Why This Sprint Exists
Sprint 5 ended in a spec/implementation divergence that, left unaddressed, will compound through Sprint 6 (storage/game discovery), Sprint 7 (launch/lifecycle), and Sprint 8 (overlay):
- Spec says Raylib; code says raw GLES2.
playos-shell-spec.mdand ADR-0006 commit to a Raylib shell with a customrcore_playos.cbackend. The implementation deferred Raylib and instead wrote a bespoke single-shader GLES2 renderer plus a hand-embedded 5×7 bitmap font inrender_util.c. - The vendored Raylib is stale and inert.
playos-shell/external/raylib/vendors Raylib 5.5, butPLAYOS_SHELL_USE_RAYLIBisOFFin bothCMakeLists.txtand the Buildroot package (playos-shell.mk). Theversions.lockentriesRAYLIB_COMMIT/RAYLIB_SOURCEare empty. - Raylib 6.0 changes the platform backend contract. The backend split introduced in Raylib 5.0 was reworked in 6.0, along with module-level compile toggles and symbol renames/removals. The custom PlayOS backend must be written against 6.0, not 5.5.
- A hand-rolled renderer becomes a maintenance trap. The raw-GLES2 path (custom shaders, bitmap font, manual text metrics) must be re-implemented in Raylib eventually anyway. Migrating now, before Sprint 6/7 build on the rendering path, avoids double work and keeps the shell aligned with ADR-0006's "Raylib for shell, overlay, and games" decision.
This is a pure migration sprint — no new screens, no new features, no UX changes. The user-visible result must be identical (or strictly better) to Sprint 5.
Start Condition Checklist
- Sprint 5 shell renders and navigates on the Ally via raw EGL/GLES2. (Verified at Sprint 5 exit gate.)
-
Raylib 5.5 is vendored at
playos-shell/external/raylib/and builds (or thePLAYOS_SHELL_USE_RAYLIBpath is understood). -
versions.lockhas emptyRAYLIB_COMMIT/RAYLIB_SOURCEentries ready to be filled. - Raylib 6.0 release source/tag is available to vendor (exact commit SHA obtainable).
- Nested Wayland dev environment and QEMU headless path are usable for iteration.
Decisions Locked for This Sprint
- Raylib version: 6.0, vendored into
playos-shell/external/raylib/and pinned with a full commit SHA inversions.lockunderRAYLIB_COMMIT. - Backend: a custom
rcore_playos.cplatform backend (not GLFW/SDL backends). It owns the fullscreenxdg_toplevel,wl_egl_window, EGL/GLES2 context, and frame-callback vsync — the same primitivesmain.ccurrently manages directly. - Raylib is rendering-only. Input remains shell-owned direct evdev (
input.c) so the reserved SYSTEM/QUICK_MENU buttons are preserved. Raylib's gamepad abstraction is not used for navigation because it strips reserved buttons. - Module stripping: disable unused Raylib subsystems via
config.hSUPPORT_MODULE_*toggles (audioraudio, modelsrmodels, camerarcamera, and networking if present), keepingrcore,rlgl,rshapes,rtext, andrtextures. This avoids conflicts with the Sprint 8 ALSA stack and keeps the shell binary small. - Font: adopt Raylib's default font (
GetFontDefault()); remove the embedded 5×7 bitmap font fromrender_util.c. - No new features. UX, screens, navigation, and lifecycle behaviour are unchanged from Sprint 5.
Scope
In Scope
- Upgrade vendored Raylib 5.5 → 6.0 and pin it in
versions.lock - Reconcile Raylib 6.0 breaking changes (platform backend contract, module flags, renamed/removed symbols)
- Implement
rcore_playos.cagainst the Raylib 6.0 backend contract - Port rendering from raw GLES2 (
render_util.c) to the Raylib draw API across all four screens - Wire shell-owned evdev input and lifecycle polling into the Raylib frame loop
- Buildroot packaging: flip
PLAYOS_SHELL_USE_RAYLIB=ON, add the vendored Raylib dependency - Spec/doc reconciliation (
playos-shell-spec.md,playos-shell/AGENTS.md, ADR-0006 note) - Validation in the nested Wayland dev path, QEMU headless path, and on the Ally
Explicitly Out of Scope
- New screens, navigation flows, or UX changes
- Game launch/resume/background lifecycle (Sprint 7)
- Overlay UI (Sprint 8)
- Audio via Raylib
raudio(Sprint 8 uses ALSA) - 3D/model rendering (
rmodelsis stripped) - Intel graphics expansion (Sprint 13)
- Store/network/account features
Required Repository Changes
| Repo | Required work |
|---|---|
playos-shell | Vendor Raylib 6.0, implement rcore_playos.c, port render_util.c + screen_*_draw() to the Raylib API, wire input/lifecycle, flip PLAYOS_SHELL_USE_RAYLIB, update AGENTS.md |
playos-refdistro | Pin RAYLIB_COMMIT in versions.lock, flip playos-shell.mk to USE_RAYLIB=ON, ensure the vendored Raylib builds/links in the Buildroot image |
playos-spec | Add Sprint-5.5.md, update playos-shell-spec.md, add ADR-0006 follow-up note, update cross-links |
Expected Files and Directories
playos-shell (changed/added)
external/raylib/
├── src/raylib.h ← UPDATE: RAYLIB_VERSION "6.0"
└── src/platforms/
└── rcore_playos.c ← NEW: custom PlayOS Wayland/EGL backend (6.0 contract)
src/
├── main.c ← UPDATE: delegate surface/EGL to the raylib backend
├── input.c ← UNCHANGED: shell-owned direct evdev (trusted reserved buttons)
├── render_util.c ← REWRITE: thin Raylib wrappers (no raw GLES2 shader/font)
├── screen_home.c ← UPDATE: draw via Raylib API
├── screen_library.c ← UPDATE: draw via Raylib API
├── screen_game_detail.c ← UPDATE: draw via Raylib API
└── screen_settings.c ← UPDATE: draw via Raylib API
CMakeLists.txt ← UPDATE: build Raylib 6.0 with minimal SUPPORT_MODULE_* config
playos-refdistro
versions.lock ← UPDATE: RAYLIB_COMMIT=<sha>, RAYLIB_SOURCE confirmed
br2-external/package/playos-shell/
└── playos-shell.mk ← UPDATE: -DPLAYOS_SHELL_USE_RAYLIB=ON
playos-spec
src/playos-shell-spec.md ← UPDATE: reflect Raylib 6.0 + rcore_playos.c
src/adr/ADR-0006-*.md ← ADD: follow-up note (or new superseding ADR) if the decision changed
src/sprints/Sprint-5.5.md ← NEW: this document
Agent Task Breakdown
Every task below is independently checkable.
Task Status Grid
| Task ID | Task | Primary repo | Status | Notes / evidence |
|---|---|---|---|---|
| S5.5-T1 | Upgrade vendored Raylib 5.5 → 6.0 and pin it | playos-shell, playos-refdistro | done | RAYLIB_COMMIT=dbc56a87 (6.0) in versions.lock |
| S5.5-T2 | Reconcile Raylib 6.0 breaking changes | playos-shell | done | Migration list applied |
| S5.5-T3 | Implement rcore_playos.c platform backend (6.0) | playos-shell | done | Custom Wayland/EGL backend; non-blocking render loop (1046262) |
| S5.5-T4 | Port rendering from raw GLES2 to Raylib draw API | playos-shell | done | render_util.c helpers map to Raylib draw API |
| S5.5-T5 | Wire controller input (rendering-only Raylib) | playos-shell | done | input.c evdev unchanged; no Raylib gamepad |
| S5.5-T6 | Integrate lifecycle polling into the Raylib frame loop | playos-shell | done | playos_lifecycle_poll() per frame |
| S5.5-T7 | Buildroot packaging: flip to Raylib 6.0 | playos-refdistro | done | USE_RAYLIB=ON; libraylib.so.6.0.0 in image |
| S5.5-T8 | Spec and docs reconciliation | playos-spec, playos-shell | done | Sprint doc updated |
| S5.5-T9 | Validation and runtime evidence | playos-shell, playos-refdistro | done | QEMU validated; Ally on-device re-test passed |
S5.5-T1 — Upgrade Vendored Raylib 5.5 → 6.0 and Pin It
Finding: playos-shell/external/raylib/ vendors Raylib 5.5 (RAYLIB_VERSION "5.5" in src/raylib.h). versions.lock has RAYLIB_COMMIT= and RAYLIB_SOURCE=https://github.com/raysan5/raylib but no pinned commit. PLAYOS_SHELL_USE_RAYLIB is OFF everywhere.
Steps:
- Replace the vendored source. Swap
external/raylib/with the Raylib 6.0 release (exact tag or commit), keeping the repo's vendoring layout intact. - Pin it. Set
RAYLIB_COMMIT=<full sha>inplayos-refdistro/versions.lock(confirmRAYLIB_SOURCEis correct). Do not use a branch orlatest. - Update the CMake integration. In
playos-shell/CMakeLists.txt, adjust theadd_subdirectory(external/raylib)path and the library target name if Raylib 6.0 renamed its CMake target (e.g.,raylibvsraylib_playos). KeepPLAYOS_SHELL_USE_RAYLIBas the gating option. - Apply a minimal module config. Configure
config.hSUPPORT_MODULE_*toggles to strip unused subsystems (see Decisions). This keeps the vendored build lean and avoids pulling inraudio/rmodelsdeps. - Standalone build check. Build the vendored Raylib alone (
cmake -B build && cmake --build build) to confirm it compiles with the stripped config before touching the shell.
Done when:
external/raylib/src/raylib.hreportsRAYLIB_VERSION "6.0".versions.lockhas a non-emptyRAYLIB_COMMIT.- The vendored Raylib builds standalone with the minimal module config.
S5.5-T2 — Reconcile Raylib 6.0 Breaking Changes
Finding: Raylib 6.0 reworks the platform-backend contract introduced in 5.0, adds module-level compile toggles, and renames/removes a number of symbols. The shell cannot adopt 6.0 blindly — it must know exactly what changed between 5.5 and 6.0.
Steps:
- Diff 5.5 → 6.0. Read the upstream
CHANGELOG(and the platform-backend headers) and produce a concrete reconciliation list covering:- platform backend entry points /
PLATFORM_*contract changes config.hflag renames and newSUPPORT_MODULE_*toggles- renamed/removed public symbols (window, input, drawing, text, math)
- GLES2 /
rlglrenderer API changes relevant to the custom backend
- platform backend entry points /
- Apply the list. Update any shell references (current or planned) that use a renamed/removed 5.5 symbol. Since Raylib is not yet wired into the shell, this mostly informs T3/T4, but any
external/integration glue is corrected here. - Record it. Capture the final list in this sprint's evidence (or an ADR-0006 follow-up note) so Sprint 6/7 do not re-discover the same changes.
Done when:
- A reviewed 5.5 → 6.0 breaking-change list exists.
- No shell or integration code references a removed 5.5-only symbol.
S5.5-T3 — Implement the rcore_playos.c Platform Backend (Raylib 6.0)
Finding: ADR-0006 mandates a custom Raylib backend that integrates with the PlayOS Wayland/EGL environment and the libplayos lifecycle API. It must now be written against the 6.0 backend contract in src/platforms/rcore_playos.c.
Steps:
- Implement
rcore_playos.cagainst Raylib 6.0's platform backend contract:- create a fullscreen
xdg_toplevel(reuse the shell'sxdg_wm_base/wl_compositorsetup) - create a
wl_egl_window+ EGL/GLES2 context and make it current - pace frames with Wayland frame callbacks (
wl_surface_frame) and present witheglSwapBuffers
- create a fullscreen
- Disable desktop features as no-ops: window decorations, free resize, drag-and-drop, clipboard, multi-window.
- Wire the trusted-shell registration. Keep the
playos_manager_v1"register as trusted shell" + ShellReady handshake frommain.c, now invoked through/alongside the backend init. - Expose dimensions. Ensure
GetScreenWidth()/GetScreenHeight()reflect the toplevel configure size (the shell'sdpi_scaleconvention can map onto Raylib scaling). - Trim
main.c. Remove the now-duplicated direct EGL/wl_surface/frame-callback management that the backend owns.
Done when:
- A Raylib frame draws through
rcore_playos.cin the nested Wayland dev environment. main.cno longer manages the EGL surface/context directly.
On-device fix (frame pacing): The first two Ally bring-ups froze in the frame loop — the shell logged no FPS lines and the screen either showed only the compositor's background (frame 1) or froze right after the first painted frame (frame 2). Root cause: SwapScreenBuffer() blocked on a wl_surface_frame callback whose associated commit could never be presented by this compositor — arming the callback before eglSwapBuffers left the surface unmapped on frame 1, and arming it after (without a commit) left the callback unarmed on frame 2. Either way wl_display_dispatch blocked forever, stalling the main loop (and therefore evdev input + the FPS counter).
Final fix (playos-shell@1046262): drop wl_surface_frame pacing entirely and mirror the proven test-client render loop — eglSwapInterval(0) keeps eglSwapBuffers non-blocking, and PollInputEvents() pumps Wayland events each frame with wl_display_dispatch_pending + wl_display_flush. The compositor remains the presentation authority. Validated in QEMU (main loop advances, FPS lines emitted) and ready for Ally re-test.
S5.5-T4 — Port Rendering from Raw GLES2 to the Raylib Draw API
Finding: render_util.c implements a single-shader GLES2 renderer plus an embedded 5×7 bitmap font. The four screen_*_draw() functions call render_draw_rect() / render_draw_text(). These must map onto Raylib.
Steps:
- Map each
render_*helper to Raylib:
| Current helper | Raylib equivalent |
|---|---|
render_init(w, h) | backend window/context init (T3) |
render_begin_frame(r,g,b,a) | BeginDrawing() + ClearBackground() |
render_draw_rect(x,y,w,h,r,g,b,a) | DrawRectangle() / DrawRectangleRec() |
render_draw_text(text,x,y,scale,r,g,b,a) | DrawTextEx() |
render_end_frame(s) | EndDrawing() |
render_screen_dims(&w,&h) | GetScreenWidth() / GetScreenHeight() |
render_text_width(text, scale) | MeasureTextEx() |
- Replace the font. Use
GetFontDefault(); delete the embeddedfont_dataarray and GLSL shader sources fromrender_util.c. - Update the four
screen_*_draw()functions. Either keep the thinrender_*wrappers (now backed by Raylib) or call Raylib directly. Preserve each screen's layout and focus-visibility rules. - Remove dead code. Delete the now-unused GLES2 quad/shader/text-metric code.
Done when:
- All four screens render through Raylib.
render_util.ccontains no raw GLES2 shader or bitmap-font code.
S5.5-T5 — Wire Controller Input (Rendering-Only Raylib)
Finding: The shell reads controller input directly from evdev in input.c because the reserved SYSTEM/QUICK_MENU buttons must survive (they are stripped by the libplayos input API). Raylib must be used for rendering only.
Steps:
- Keep
shell_input_poll()and edge detection ininput.cas the single source of controller state — do not route navigation through Raylib's gamepad abstraction. - Confirm the invariants after the rendering migration:
Aconfirms,Bbacks out, d-pad moves focus, focus is always visible. - Verify SYSTEM/QUICK_MENU still arrive (these buttons do not exist in Raylib's gamepad mapping and must stay on the shell's evdev path).
Done when:
- Controller navigation on all four screens is unchanged while rendering through Raylib.
- Reserved buttons remain available to the trusted shell.
S5.5-T6 — Integrate Lifecycle Polling into the Raylib Frame Loop
Finding: main.c polls playos_lifecycle_poll() every frame and handles SUSPEND/RESUME/FOREGROUND/BACKGROUND/TERMINATE. This must survive the Raylib migration.
Steps:
- Keep lifecycle polling in the Raylib main loop (or in the backend's per-frame hook).
- Suspend/background → skip
BeginDrawing()/EndDrawing()(no rendering work). Foreground/resume → resume normal rendering. - TERMINATE → exit cleanly through the Raylib window-close path (
s->running = false). - Confirm the shell remains a persistent supervised process (no
exit()except on unrecoverable init failure).
Done when:
- Lifecycle state changes affect rendering predictably.
- The shell exits cleanly on TERMINATE and remains compatible with
playos-initsupervision.
S5.5-T7 — Buildroot Packaging: Flip to Raylib 6.0
Finding: playos-shell.mk passes -DPLAYOS_SHELL_USE_RAYLIB=OFF. The vendored Raylib is inside the playos-shell source tree but not built into the image.
Steps:
- Flip the flag. Set
-DPLAYOS_SHELL_USE_RAYLIB=ONinbr2-external/package/playos-shell/playos-shell.mk. - Ensure the vendored Raylib builds and links. Confirm the
playos-shellCMake builds the vendoredexternal/raylibstatic library and links it intoplayos-shell. Add any missing dependency toPLAYOS_SHELL_DEPENDENCIES(Raylib stays vendored insideplayos-shell, so it should not need a separate Buildroot package). - Keep libplayos + GLES/EGL/Wayland through the backend. Update the
.mkheader comment (it currently says "Uses EGL/GLES2 for rendering" — change to "Raylib 6.0 via custom PlayOS backend"). - Pin in
versions.lock(done in T1) and confirmmake setupvendored the pinned commit. - Build. Run
make qemu-buildand confirm the image contains the Raylib-linked shell.
Done when:
playos-shell.mksetsUSE_RAYLIB=ON.- The QEMU image builds and boots with the Raylib-backed shell.
S5.5-T8 — Spec and Docs Reconciliation
Finding: playos-shell-spec.md says "Rendering with Raylib PlayOS backend", but playos-shell/AGENTS.md says "Raylib integration deferred; direct EGL/GLES2 used instead". ADR-0006's consequences require the rcore_playos.c backend to be maintained across Raylib updates.
Steps:
playos-shell-spec.md: update the rendering responsibilities and cross-references to name Raylib 6.0 +rcore_playos.c; remove any "deferred" wording.playos-shell/AGENTS.md: update the implementation-status banner and the "Rendering" section to state Raylib 6.0 is active via the custom backend (raw GLES2 path retired).- ADR-0006: add a follow-up note (or a superseding ADR if the decision materially changed — e.g., the explicit "rendering-only Raylib, input stays shell-owned evdev" ruling) recording the 6.0 migration.
- Cross-links: this document's footer already links Previous/Next; add any in-doc references from Sprint 5/6 where useful (mirroring how Sprint 2.5 is referenced by Sprint 2).
Done when:
- No documentation claims "Raylib deferred" or describes the raw-GLES2 renderer as the active path.
S5.5-T9 — Validation and Runtime Evidence
Steps:
- Nested Wayland dev run: shell boots, draws all four screens through Raylib, navigates with controller, logs manifest load + navigation + fps.
- QEMU headless path: shell starts without crashing (visual output limited, but must not regress from Sprint 5).
- On-device (Ally): visual parity with Sprint 5, ≥ 60 fps, controller navigation, persistence under supervision.
- Capture evidence:
/data/log/shell.logshowing Raylib version, backend init, GPU string, and fps.
Done when:
- Evidence is captured across dev / QEMU / device paths.
- All acceptance criteria below are verified.
Implementation Guidance
Order of execution
- T1 first (vendor + pin Raylib 6.0) — everything depends on the new source.
- T2 second (reconciliation list) — informs the backend and port work.
- T3 third (backend) — the foundation the port sits on.
- T4 fourth (rendering port) — the largest surface change, across all screens.
- T5, T6 next (input, lifecycle) — wire the shell's existing behaviours into the Raylib loop.
- T7 sixth (Buildroot) — build the whole image.
- T8 seventh (docs) — document reality after the code lands.
- T9 last (validation) — verify across all three runtime paths.
Atomic commits
Each task is a separate commit (or small group) referencing the task ID:
S5.5-T1: vendor raylib 6.0 and pin in versions.lock
S5.5-T3: implement rcore_playos.c backend for raylib 6.0
S5.5-T4: port shell rendering to raylib draw API
Do not break the shell
After T1–T7, the shell must still render all four screens, navigate with a controller, and stay alive under supervision. If the raw-GLES2 path was already broken before this sprint, document it and fix only what this sprint touches.
Verification and Evidence
| Evidence | How it is produced |
|---|---|
| Raylib 6.0 proof | external/raylib/src/raylib.h shows RAYLIB_VERSION "6.0"; versions.lock RAYLIB_COMMIT non-empty |
| Backend proof | a Raylib frame draws through rcore_playos.c in the nested Wayland dev path |
| Rendering port proof | render_util.c contains no raw GLES2 shader/bitmap-font code; all four screens use the Raylib API |
| Input proof | controller navigation unchanged; reserved SYSTEM/QUICK_MENU buttons still delivered to the shell |
| Lifecycle proof | suspend skips rendering, foreground resumes, TERMINATE exits cleanly |
| Packaging proof | playos-shell.mk sets USE_RAYLIB=ON; QEMU image boots the Raylib shell |
| Docs proof | playos-shell-spec.md, AGENTS.md, ADR-0006 no longer describe Raylib as deferred |
| Runtime proof | shell log shows Raylib version + backend init + ≥ 60 fps on the Ally |
Acceptance Criteria
-
vendored Raylib reports
RAYLIB_VERSION "6.0"and is pinned inversions.lock -
Raylib builds with the minimal
SUPPORT_MODULE_*module config - a Raylib 5.5 → 6.0 breaking-change reconciliation list exists and is applied
-
rcore_playos.cimplements the Raylib 6.0 platform backend (fullscreen Wayland + EGL + frame callback) -
all four screens render through the Raylib draw API (no raw GLES2 shader/bitmap font in
render_util.c) - controller navigation is unchanged and uses shell-owned evdev (not Raylib's gamepad abstraction)
- lifecycle suspend skips rendering; foreground resumes; TERMINATE exits cleanly
-
PLAYOS_SHELL_USE_RAYLIB=ONin Buildroot; the image builds and boots the Raylib shell -
playos-shell-spec.md,playos-shell/AGENTS.md, and ADR-0006 reflect Raylib 6.0 (no "deferred") - ≥ 60 fps on the Ally; nested dev and QEMU paths remain usable for iteration
Handoff to Sprint 6
Sprint 6 may assume:
- The shell renders through Raylib 6.0 via
rcore_playos.c— no raw-GLES2 fallback remains. - The custom backend owns Wayland surface + EGL context + frame pacing;
main.cdelegates to it. - Input remains shell-owned direct evdev (reserved buttons preserved); Raylib is rendering-only.
- Raylib is pinned in
versions.lockand builds with a minimal module config. - All spec/docs describe the active Raylib path, so Sprint 6 (storage/game discovery) builds on Raylib rendering rather than a hand-rolled renderer.
Sprint 6 should not need to touch the rendering layer except to display discovered game metadata through the existing Raylib draw path.
Previous: Sprint 5 | Next: Sprint 5.6