diff options
| author | Adam Malczewski <[email protected]> | 2026-06-15 05:47:13 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-15 05:47:13 +0900 |
| commit | 19771fdae1e6ee953022584ab1697beec1ab5e5c (patch) | |
| tree | 1531853f3e2bbcb59a73dcad163bd2dc83caffc2 | |
| parent | 7a3786a598236a638facf1755fcfcef791131938 (diff) | |
| download | unbox-spike/rml-compositing.tar.gz unbox-spike/rml-compositing.zip | |
docs: close Phase 0 (GO) + Phase 1 architecture design for RML compositingspike/rml-compositing
Phase 0 spike is closed GO (real-seat confirmed: input accurate through the 3D
transform, ~30fps fill-bound under the 4-window load). Records the design that
Phase 1 settles, per the user's contract decision.
- notes/rml-compositing-phase1.md (NEW): the Phase 1 design doc.
- Contract principle: RCSS is the single source of truth for ALL layout +
animation; C++ drives the document via a TYPED substrate API (reconciled
with the constitution's typed-symbol rule, which governs cross-extension
dependency discovery, not the RCSS vocabulary).
- New kernel primitive: SurfaceElement = a LIVE analogue of Preview
(zero-copy seq-gated import, frame-callback duty, per-subsurface tree).
- Window layout = existing bind_list + RCSS pattern (stage-dock's, generalized).
- Unified input-back folded into the substrate (the Element::Project mapping).
- Damage limiting = Option B (buffer-age + wlr_damage_ring + scissor +
set_buffer_with_damage), built properly here; scanout bypass deferred.
- Cross-unit contract changes + Phase-2 wave plan.
- User boundary decision: a NEW ext-window-field core extension owns the
window field + layout; ext-xdg-shell only supplies toplevel handles +
root wl_surface (Toplevel::scene_tree retired, wl_surface() added).
- notes/plan.md row 71: DIRECTION -> ADOPTED/GO + the Option-2 contract.
- tasks.md: slice 13 Phase 0 CLOSED; Phase 2 Wave 1 (kernel substrate) next.
| -rw-r--r-- | notes/plan.md | 2 | ||||
| -rw-r--r-- | notes/rml-compositing-phase1.md | 257 | ||||
| -rw-r--r-- | tasks.md | 27 |
3 files changed, 275 insertions, 11 deletions
diff --git a/notes/plan.md b/notes/plan.md index b0ea7cd..6e1d824 100644 --- a/notes/plan.md +++ b/notes/plan.md @@ -68,7 +68,7 @@ solves), and the trigger that would reopen it. | **VT switching (Ctrl+Alt+Fn) is kernel-hardwired** before the key_filter (`wlr_session_change_vt`) | It is the session escape hatch — must work even if an extension throws or greedily consumes keys; not a rebindable feature (user decision) | — | | **Stage dock** (ext-stage-dock, standard) = the Stage-Manager-style left-edge dock of minimized-window **previews**, revealed by a left-edge **swipe**. **Fork B**: previews are toplevel snapshots imported as textures INTO the ui substrate's RMLUi context and shown as `<img>` in ONE RML document | Closest to the iPad Stage Manager north star; one ui surface animates as a unit via RCSS; reuses the slice-3 dmabuf/EGLImage bridge in reverse (wlr pixels → RMLUi texture) instead of two-layer scene/RML lockstep | Cross-context texture import proves infeasible on crocus → fall back to Fork A (previews as `wlr_scene` snapshot nodes) | | **Mechanism in kernel/core, policy in ext-stage-dock.** Kernel ui substrate gains: preview-snapshot, list/container bindings, a gesture-CLAIM input path. ext-xdg-shell gains: `Toplevel::hide()/show()` (≠ unmap), `geometry()`, `scene_tree()`. ext-stage-dock owns: the "minimized" set, dock layout, gesture recognition, easing | Keeps "kernel names no feature" — snapshot/claim/list-bindings are generic primitives; minimize-to-dock is the only policy and lives in one standard extension | — | -| **RML compositing (DIRECTION, gated by a spike — reopens the compositing half of row 51).** RMLUi becomes the CONTENT compositor: toplevels, layer-shell clients (incl. wallpaper), and UI chrome are all RMLUi elements backed by LIVE, SHARED GL textures (zero per-frame copy via the existing shared EGLDisplay). Layout/animation/3D effects are RCSS. wlroots stays the foundation + plane manager (backend/output/renderer/seat, hardware cursor plane, fullscreen-video scanout bypass). Lost wlr_scene damage/scanout is mitigated by OUR dirty-gated rendering (idle ≈ no work) + a deferred fullscreen scanout bypass. Full design + spike acceptance criteria: `notes/rml-compositing.md` | The appeal is Hyprland-grade window effects via DECLARATIVE RCSS (layout, transitions, 3D transforms, blur/shaders) with hot-reload — and RmlUi already does transform-aware hit-testing + RTT/filters, while slice 3 + the stage dock already proved dmabuf/EGLImage texture handoff on this exact crocus HW. Doing windows in RMLUi means tiling/effects/stage-manager are all RCSS policy on top | The spike fails on the CF-AX3 (no zero-copy shared-texture path, input-routing dead-end, surface-tree composition impractical, or idle/perf out of budget) → fall back to wlr_scene compositing + transient snapshot-through-RMLUi effects | +| **RML compositing (ADOPTED — Phase 0 spike PASSED GO on real Haswell+crocus; supersedes the compositing half of row 51).** RMLUi is the CONTENT compositor: toplevels, layer-shell clients (incl. wallpaper), and UI chrome are all RMLUi elements backed by LIVE, SHARED GL textures (zero per-frame copy via the existing shared EGLDisplay). Layout/animation/3D effects are RCSS. wlroots stays the foundation + plane manager (backend/output/renderer/seat, hardware cursor plane, fullscreen-video scanout bypass). Lost wlr_scene damage/scanout is mitigated by OUR dirty-gated rendering (idle ≈ no work) + damage-limited compositing + a deferred fullscreen scanout bypass. **Contract decision (user): RCSS is the single source of truth for ALL layout + animation; C++/extensions DRIVE the document through a TYPED substrate API but never own placement geometry directly — the typed-symbol rule governs cross-extension dependency discovery, not the RCSS styling vocabulary.** Spike report: `reports/rml-compositing-spike.md`; Phase-1 design: `notes/rml-compositing-phase1.md` | The appeal is Hyprland-grade window effects via DECLARATIVE RCSS (layout, transitions, 3D transforms, blur/shaders) with hot-reload — and RmlUi already does transform-aware hit-testing + RTT/filters, while slice 3 + the stage dock already proved dmabuf/EGLImage texture handoff on this exact crocus HW. Doing windows in RMLUi means tiling/effects/stage-manager are all RCSS policy on top. **Phase 0 proved on the real CF-AX3 GPU class: zero-copy live import, RCSS 3D transform on live pixels, input accurate through the transform, per-subsurface surface trees, idle dirty-gate, FBO→dmabuf present; ~30fps under a 4-window load, fill-bound (~10–15ms whole-output composite) with damage limiting as the known recovery** | A Phase-2 implementation blocker on the real seat (perf regresses below budget after damage limiting + scanout bypass, or the RCSS-only contract proves unworkable for a real tiling/effects extension) → fall back to wlr_scene compositing + transient snapshot-through-RMLUi effects | ## 3. Architecture diff --git a/notes/rml-compositing-phase1.md b/notes/rml-compositing-phase1.md new file mode 100644 index 0000000..45c8e8c --- /dev/null +++ b/notes/rml-compositing-phase1.md @@ -0,0 +1,257 @@ +# RML compositing — Phase 1 architecture (the design doc) + +> **Status: ACTIVE design, gated only by Phase-2 implementation.** Phase 0 (the +> spike) **PASSED GO** on the real CF-AX3 GPU class — see +> `reports/rml-compositing-spike.md`. This doc settles the architecture the +> direction note (`notes/rml-compositing.md`) deferred to Phase 1. Phase 2 then +> implements it wave-by-wave, behind a flag, per `ORCHESTRATOR.md`. + +## 0. Inputs that are already settled (do not relitigate) +- **GO**: live zero-copy import, RCSS 3D transform on live pixels, input accurate + through the transform, per-subsurface surface trees, idle dirty-gate, and the + FBO→dmabuf→`wlr_scene_buffer` present path all proven on Haswell+crocus. +- **Perf reality (Phase 0 Stage-0 measurement)**: ~30fps under a 4-window load, + **fill-bound** — the whole-output composite is ~10–15ms GPU, CPU work ~2ms, + present dominated by the fence wait. Damage limiting is the recovery lever. +- **Surface-tree answer (spike §3)**: **per-subsurface elements** by default, with + a **per-window render-to-texture (RTT) hook** for the one case that needs it + (a single effect that must treat a whole window tree as one flat surface — + genie warp, cross-seam blur). +- **Contract decision (user)**: **RCSS is the single source of truth for ALL + layout and animation.** C++/extensions DRIVE the document (what exists, which + data, which classes) through a **typed** substrate API, but never own placement + geometry imperatively. Tiling, stage-manager, floating, effects = RCSS. +- **Naming (GLOSSARY)**: *RML compositing* (the approach), *surface element* (an + RML element backed by a live client surface's shared texture). + +## 1. The contract principle (Option 2), reconciled with the constitution +AGENTS.md forbids **string-keyed cross-extension dependency discovery** ("a +missing dependency must be a compile/link error"). RCSS-as-layout does **not** +violate this: that rule governs how *units find each other*, not the styling +vocabulary. So: + +- **Units couple through typed C++ symbols** (service handles, hook descriptors, + the substrate API) — unchanged. A missing `ext-xdg-shell` is still a link error. +- **Within a surface, layout/animation is RCSS.** C++ pushes *data* (typed + `bind_*`/`bind_list_*`) and *intent* (set a class, dirty a binding); the + document decides geometry and tweens. This is already how `UiSurface` works + (`notes`/the `ui.hpp` contract) — Phase 1 does not invent a paradigm, it adds a + **live** surface primitive and **input-back** to the existing one. + +Litmus: an extension may never read another extension's state by string name, and +may never compute a window's on-screen rectangle and command "draw it there." It +provides the window list + per-window data; RCSS lays them out and animates. + +## 2. What already exists and is REUSED verbatim +The `kernel` `UiSubstrate`/`UiSurface` contract (`packages/kernel/include/unbox/ +kernel/ui.hpp`) already provides everything Option 2 needs **except live windows**: +- `create_surface(UiSurfaceSpec)` → a `UiSurface` = one RML document = one + composited node; per-pixel alpha; `SceneLayer`. +- Typed data bindings: `bind_int/double/bool/string`, **`bind_list` + + `bind_list_string/int/...` + `bind_list_event`** (the list pattern), `dirty()`. +- Interaction: `bind_event`, **`bind_drag`** (captured drag stream in surface-local + px), `on_touch_mode_changed`. +- **`transition_timing(element_id, property)`** — read RCSS-authored + duration/delay/easing from C++ so animators reuse hot-reloadable RCSS values. +- **`Preview`** = a FROZEN toplevel snapshot imported as a texture, shown via + `<img src=source_uri()>` in any ui surface. `create_preview(wlr_scene_tree*)`. +- Dev **hot-reload** of RML/RCSS, error-isolated. + +**Phase 1 = make `Preview` LIVE + route input back into it.** That is the whole +new mechanism; the layout/animation/contract machinery is already shipped. + +## 3. New kernel primitive: the live surface element +A live analogue of `Preview`. Proposed contract (in `ui.hpp`, kernel-owned): + + class SurfaceElement { // GLOSSARY: "surface element" + public: + // The <img src> URI resolving to this surface's LIVE shared texture inside + // any ui surface of this substrate (e.g. "unbox-surface://7"). Stable for life. + virtual auto source_uri() const -> std::string = 0; + virtual auto width() const -> int = 0; // current surface px (tracks commits) + virtual auto height() const -> int = 0; + // NO refresh(): unlike Preview, this updates itself every client commit + // (seq-gated re-import) and drives the client's frame callbacks. + virtual ~SurfaceElement() = default; + }; + + // On UiSubstrate: + virtual auto create_surface_element(wlr_surface* client) // a BORROW + -> std::unique_ptr<SurfaceElement> = 0; + +Semantics (all proven in the spike, generalized from `spike_gl.hpp`): +- **Zero-copy, seq-gated**: re-imports the client's current buffer only when + `wlr_surface_state.seq` advances (pool-reuse-proof); double-buffered + `wlr_buffer_lock`/unlock; idle client ⇒ zero work. +- **Drives the client loop**: the substrate sends `wl_surface` frame callbacks + each composited frame (a live element, unlike `Preview`, is responsible for the + client's progress — the spike's "stuck-frame" fix). +- **Surface tree** (see §6): one `create_surface_element(toplevel root)` manages + the toplevel + its subsurfaces + popups as **child elements**, each its own live + texture at its tree offset. +- **Lifetime**: owned by the contributing extension via `unique_ptr`; destroying + it drops the import + frame-callback duty. The `wlr_surface*` is a borrow valid + until the owner drops the element (extensions already track map/unmap). + +Wallpaper/layer-shell surfaces use the **same** `create_surface_element` (spike +criterion 5). + +## 4. The compositor document + window-layout model (RCSS-driven) +This is the heart of Option 2. Windows are not per-window scene nodes; they are +**surface elements inside a ui surface document**, laid out by RCSS. + +- A window-management extension (today `ext-xdg-shell`; later a tiling/stage + extension) owns **one ui surface** at `SceneLayer` for app content (call it the + *window field*). It does NOT compute geometry. +- It feeds windows through the **existing list binding**: `bind_list("wins", …)` + with per-row fields — crucially a `live_uri` string field returning each + window's `SurfaceElement::source_uri()`, plus whatever the RCSS layout keys off + (focused bool, app_id string, a layout-slot int/percent, z-order, etc.). +- The RML authors the layout: + + <div data-model="wm"> + <div class="field tiling"> <!-- class chosen by C++ intent --> + <div data-for="w : wins" class="win" + data-class-focused="w.focused" + style="--slot: {{ w.slot }};"> + <img src="{{ w.live_uri }}"/> + </div> + </div> + </div> + + Tiling = RCSS flex/grid keyed on `--slot`; stage-manager = the same list under a + `.stage` class with `transform`/perspective per card; floating = absolutely + positioned from bound `x/y`. **Switching layout = swapping a class / changing + bound data**, animated by RCSS `transition` — the user's "everything is laid out + and animated in RCSS." +- **Animation timing** comes from RCSS; C++ that must coordinate (e.g. a gesture) + reads it via `transition_timing()` (already shipped) and drives progress with + bound values — never hand-rolled geometry. + +This makes tiling/stage/effects *policies expressed as RML+RCSS + a window list*, +exactly the constitution's "kernel names no feature." + +## 5. Unified input (pick → wl_seat), folded into the substrate +The substrate already routes `data-event*`/`bind_drag` for ui surfaces. Phase 1 +adds **client input-back** for surface elements: +- On pointer/touch, the substrate feeds the screen point to `Context::Process*` + (transform-aware pick). If the hovered element is a surface element, it maps the + point to surface-local via **`Element::Project()`** (the spike's fix — projects + through the element's real 3D transform, no-op when untransformed) and forwards + via `wlr_seat_pointer/touch_notify_*`. +- Keyboard focus follows the focused window (the wm extension calls a focus path; + `wlr_seat_keyboard_notify_*`). +- **Cursor stays a wlr hardware plane**, never drawn in RMLUi (recompose-on-move + would be fatal). +- Implicit grab / click-to-focus stays wm-extension policy; the substrate only + provides the pick→local→seat translation as a typed primitive. + +Contract sketch (kernel): a surface element created from a `wlr_surface` is +**automatically** input-routed by the substrate (it knows the element↔surface +map); the wm extension does not wire seat calls itself. This subsumes +`ext-xdg-shell`'s current pointer/touch routing. + +## 6. Surface trees: per-subsurface + RTT hook +- Default: `create_surface_element(root)` builds **one child element per + subsurface/popup**, positioned at its tree offset; DOM order = composite order; + popups are not parent-clipped (own absolutely-positioned elements). This is the + spike's criterion-4 result. +- A small **"place child relative to parent's resolved box"** layout helper is + needed so a moving/transformed parent drags its children (spike §3 edge note) — + pure layout glue. +- **RTT hook** (do not build until an effect needs it): a per-element opt-in that + flattens a window's whole tree to one texture (RmlUi `SaveLayerAsTexture`) so a + tree-spanning effect transforms/filters one surface. Element-level policy, not a + global mode. + +## 7. Present + performance posture +- **Present path**: reuse Phase-0 Plan A — RMLUi composites into an FBO on a + `wlr_swapchain` dmabuf, handed to a `wlr_scene_buffer`; EGL fence, no `glFinish`. + `wlr_scene` is reduced to **presenter of one full-output buffer + the cursor + plane + (later) scanout bypass**. +- **Dirty-gate (ours)**: schedule + `Render()` only on a real signal — a client + commit (wlroots), an active RCSS animation (`GetNextUpdateDelay()` finite), or an + input-driven state change. Idle ≈ no GPU. Proven in the spike. +- **Damage-limited compositing — Option B (build here, correctly).** Now that we + own the real compositor (not a throwaway), do it the production way: + 1. Per-element dirt → screen-space damage region: project each changed surface's + `wl_surface` damage rect through its element transform (forward of the spike's + `project_to_screen`) → AABB → union (cap; fall back to full-frame when it + explodes); static-transform elements use AABB, animating ones are full-damage + for the animation. + 2. Render damage into the swapchain with **buffer-age accumulation** + (`wlr_damage_ring` keyed on the presented buffer; repaint the union over the + buffer's age) and a **scissor** on RmlUi's draw; redraw all elements + intersecting the region in z-order (blending-correct). + 3. Feed the region to **`wlr_scene_buffer_set_buffer_with_damage`** so output + + KMS partial-update benefit (battery/thermal on a 15W fanless panel). + - A **damage-debug tint** toggle (same trick as the spike's click crosshair) to + watch the reshaded region shrink and catch buffer-age staleness. +- **Fullscreen-video scanout bypass (deferred, separate from damage).** When one + opaque, untransformed, fullscreen surface has nothing composited on top, pull it + out of the RMLUi composite and hand it to `wlr_scene`/scanout directly (RMLUi + draws nothing that frame). Trigger = the fullscreen STATE. Damage limiting can't + help a full-rate video; this can. Size it by measurement; not a blocker. + +## 8. Cross-unit contract changes (what Phase 2 touches) +| Unit | Change | Contract impact | +|---|---|---| +| **kernel** (`ui.hpp`, present/frames, input) | add `SurfaceElement` + `create_surface_element`; auto input-back for surface elements; damage-limited present; dirty-gate as the scheduler | NEW public surface in `ui.hpp`; present internals private | +| **ext-window-field** (NEW core unit; user decision §10.1) | owns the window-field ui surface + the window list (each row a `SurfaceElement` `live_uri` + layout data) + layout policy (tiling/stage/floating as RCSS); subscribes to `ext-xdg-shell`'s map/unmap/focus; drives focus + click-to-focus policy | NEW contract: the layout/tiling service (typed); consumes `ext-xdg-shell::Service` + kernel `SurfaceElement` | +| **ext-xdg-shell** | stop owning a `wlr_scene_tree` per toplevel for COMPOSITING; expose each toplevel's **root `wlr_surface`** so `ext-window-field` can make a `SurfaceElement`; `hide()/show()` become list membership / a hidden class, not scene-node toggles; pointer/touch routing moves to the substrate | `Toplevel::scene_tree()` **retired**; add `Toplevel::wl_surface()` (typed borrow); `geometry()` becomes the RCSS-resolved element box (read-back) — a real change-request | +| **ext-layer-shell** | layer surfaces become surface elements at the right `SceneLayer`; wallpaper via the identical path | analogous to xdg-shell | +| **ext-stage-dock** | minimize/restore re-expressed as **RCSS over the live window list** (a `.minimized` class / a dock list of live URIs); drop the frozen-`Preview` snapshot path (previews can stay live now) | consumes the new list/`SurfaceElement`; coordinates with `ext-window-field`; `Preview` may remain for thumbnails of hidden windows | +| **host-bin** (orchestrator-owned) | composition-root wiring (+ the NEW `ext-window-field` unit) + the **Phase-2 feature flag** to switch compositing path | — | + +The biggest contract churn is **`ext-xdg-shell`** losing window compositing +(`scene_tree()`/`hide()`/`show()`/`geometry()` redesigned around exposing the root +`wlr_surface`) **and the new `ext-window-field` unit** owning layout. Settle both +contracts before Wave 2 fans out. + +## 9. Unit/ownership map + Phase 2 wave plan +Topological, disjoint-where-possible (per `ORCHESTRATOR.md` §2). Behind a flag so +the session stays usable each wave. + +1. **Wave 1 — kernel substrate.** `SurfaceElement` + `create_surface_element` + (live import, seq-gate, frame-callback duty, surface-tree children) + auto + input-back. Damage-limited present + dirty-gate scheduler. (Pure-core damage + math is doctested; glue tested on the headless backend.) *opus agent.* +2. **Wave 2 — ext-xdg-shell + ext-layer-shell** (disjoint): retire per-window + scene-tree compositing; `ext-xdg-shell` exposes `Toplevel::wl_surface()` + the + new focus/geometry contract; `ext-layer-shell` exposes its surfaces likewise. + Depends on Wave 1's contract. (These two are disjoint and can summon together.) +3. **Wave 3 — ext-window-field** (NEW): owns the window-field ui surface, the + `bind_list` of live windows, and RCSS layout (tiling/stage/floating); subscribes + to ext-xdg-shell map/unmap/focus; drives focus policy. Depends on Wave 2. +4. **Wave 4 — ext-stage-dock**: minimize/restore as RCSS over the live window + list; coordinate with ext-window-field; thumbnails via live elements or + `Preview`. Depends on Wave 3. +5. **Wave 5 — perf hardening**: damage-debug tooling, scanout bypass, real-seat + numbers; refine tiling (now an RCSS layout over the window field). + +## 10. Open sub-decisions (USER — boundary calls before/within Phase 2) +1. **RESOLVED (user): a NEW `ext-window-field` / `ext-tiling` core extension owns + the window-field ui surface, the window list, and layout policy** (tiling / + stage / floating as RCSS). `ext-xdg-shell` keeps the xdg protocol and only + supplies toplevel handles + their root `wlr_surface`s; it no longer owns window + compositing. This is the new unit added in Wave 2. +2. **One window-field document vs one per output/workspace?** Recommendation: one + per output to start; workspaces = bound class/data on it. +3. **`Preview` retirement vs coexistence.** Keep `Preview` for thumbnails of + *hidden* windows (no live buffer), use live `SurfaceElement` everywhere else? + Recommendation: coexist. +4. **Flag strategy for Phase 2** (config `unbox.toml` key vs build flag) to run + old `wlr_scene` compositing and new RML compositing side-by-side during the + migration. Recommendation: `unbox.toml` runtime key. + +## 11. Risks & fallback +- **Perf after damage limiting + scanout bypass still below budget on the real + panel** → fall back to `wlr_scene` compositing with transient + snapshot-through-RMLUi effects (the stage-dock `Preview` path already proves that + half). This is the row-71 reopen trigger. +- **RCSS-only layout proves unworkable for a real tiling/effects extension** + (e.g. needs imperative geometry RCSS can't express) → revisit the contract + decision with a typed-placement-service escape hatch for that one case (kept out + unless earned, per the rules discipline). +- **`ext-xdg-shell` contract churn** is the integration risk; settle its new + window-list/geometry contract with its owner-agent before Wave 2 fans out. @@ -5,7 +5,7 @@ ## Now -**ACTIVE (core, user-driven) — Slice 13: RML COMPOSITING SPIKE.** Big direction +**ACTIVE (core, user-driven) — Slice 13: RML COMPOSITING (Phase 0 GO → Phase 2 impl).** Big direction change: RMLUi becomes the content compositor — toplevels + layer-shell (incl. wallpaper) + chrome are RML elements backed by LIVE, SHARED GL textures, with layout/animation/3D effects in RCSS; wlroots stays foundation + cursor plane + @@ -13,14 +13,21 @@ layout/animation/3D effects in RCSS; wlroots stays foundation + cursor plane + by OUR dirty-gated rendering (NOT a RMLUi built-in) + a deferred scanout bypass. GATED BY A SPIKE before commit. Full spec + acceptance criteria: `notes/rml-compositing.md`; decision row in `notes/plan.md` §2. -SPIKE RESULT: code-complete + self-verified **GO** on real Haswell+crocus (the -CF-AX3's GPU class) — all 7 criteria `ALL PASS` headless; surface trees resolved -to **per-subsurface elements** (RTT escape-hatch for tree-spanning effects); -present path = FBO→dmabuf swapchain→wlr_scene_buffer + EGL fence. Throwaway -target `packages/kernel/rml-compositing-spike` (`--verify` / `--run`), kept out -of the shipped binary. NEXT ACTION: **USER real-seat GO/NO-GO** — 3D/touch feel, -frame-time @4 windows+video, idle power (runbook in -`reports/rml-compositing-spike.md` §5). Then Phase 1 (architecture). +SPIKE RESULT: **PHASE 0 CLOSED — GO, real-seat CONFIRMED.** All 7 criteria +`ALL PASS` headless on Haswell+crocus (CF-AX3 GPU class); on the real seat: +input accurate through the 3D transform (after the `Element::Project()` routing +fix), and ~30fps under the 4-window `--demo` load. Stage-0 instrumentation +(per-phase split + GPU timer) shows it is **fill-bound** (~10–15ms whole-output +composite, ~2ms CPU) → damage limiting is the recovery lever, built properly in +Phase 1 (not the throwaway). Surface trees = **per-subsurface elements** (RTT +hook); present = FBO→dmabuf swapchain→wlr_scene_buffer + EGL fence. Throwaway +target `packages/kernel/rml-compositing-spike` (`--verify`/`--run`/`--demo`), +out of the shipped binary. **CONTRACT DECISION (user): RCSS is the single source +of truth for ALL layout + animation; C++ drives the document via a TYPED +substrate API.** NEXT ACTION: **Phase 2 implementation** per the Phase-1 design +doc `notes/rml-compositing-phase1.md` — Wave 1 = kernel substrate +(`SurfaceElement` live import + input-back + damage-limited present). 4 user +boundary calls open (design doc §10) before Wave 2 fans out. Tiling (slice 7) is DEFERRED behind this (becomes RCSS over surface elements; pure layout core in `notes/tiling-spec.md` carries over). Stage dock (slice 10) real-seat feel check is paused under this pivot. @@ -123,7 +130,7 @@ deprecated no-op `Options::ui_spike`, retiring host-bin's demo ui. | 10 | **Stage dock** (ext-stage-dock): minimized-window previews on a left-edge swipe (Fork B) | **a1–d1 landed; previews real-seat-verified** | DONE: Super+M minimize→RMLUi-imported preview snapshot→dock slot→hide (previews confirmed rendering on hardware); RCSS dock slide-in + slot settle. NEXT: confirm tap-to-restore + animation feel; 1 boundary call (input-transparent UiSurface flag) → c1 gesture-claim → e1 gesture reveal/drag-out; then config-driven minimize keybind + favicon (XDG icon dep) | | 11 | **Status bar** (tent. ext-statusbar): iPad/iOS top bar — clock (left), configurable left/middle/right sections, tray (right) wifi/volume/battery | **IDEA — needs design** | sequenced AFTER slice 7 (tiling); replaces cut taskbar. Details + open questions: `notes/status-bar-home-screen.md` | | 12 | **Home screen** (tent. ext-home, iPad springboard): app grid; tap = launch-or-raise (instance picker if >1 open); add/remove apps; swipe-up-from-bottom to enter | **IDEA — needs design** | sequenced AFTER slice 7 (tiling); replaces cut taskbar. Details + open questions: `notes/status-bar-home-screen.md` | -| 13 | **THE SPIKE: RML compositing** — RMLUi becomes the content compositor (toplevels + layer-shell incl. wallpaper + chrome = RML elements backed by LIVE, SHARED GL textures; layout/animation/3D effects in RCSS). wlroots = foundation + cursor plane + (deferred) fullscreen scanout bypass. | **spike code-complete; GO (self-verified); pending USER real-seat GO/NO-GO** | All 7 criteria `ALL PASS` headless on Haswell+crocus: (1) zero-copy live dmabuf texture (cached when unchanged); (2) RCSS perspective+rotateY on live pixels (readback); (3) screen→surface-local inversion through the transform = 0.000000px; (4) surface tree composited → **per-subsurface elements** (RTT hook for tree-spanning effects); (5) wallpaper via identical import path; (6) idle dirty-gate = 0 idle renders / 1-per-commit (frame-time @load = real-seat); (7) FBO→dmabuf→wlr_scene_buffer + EGL fence. Spike target `rml-compositing-spike` (`--verify`/`--run`). Report + runbook: `reports/rml-compositing-spike.md`. | +| 13 | **RML compositing** — RMLUi becomes the content compositor (toplevels + layer-shell incl. wallpaper + chrome = RML elements backed by LIVE, SHARED GL textures; layout/animation/3D effects in RCSS). wlroots = foundation + cursor plane + (deferred) fullscreen scanout bypass. | **Phase 0 spike CLOSED — GO, real-seat CONFIRMED; Phase 1 design DONE (`notes/rml-compositing-phase1.md`); Phase 2 impl NEXT** | All 7 criteria `ALL PASS` headless on Haswell+crocus: (1) zero-copy live dmabuf texture (cached when unchanged); (2) RCSS perspective+rotateY on live pixels (readback); (3) screen→surface-local inversion through the transform = 0.000000px; (4) surface tree composited → **per-subsurface elements** (RTT hook for tree-spanning effects); (5) wallpaper via identical import path; (6) idle dirty-gate = 0 idle renders / 1-per-commit (frame-time @load = real-seat); (7) FBO→dmabuf→wlr_scene_buffer + EGL fence. Spike target `rml-compositing-spike` (`--verify`/`--run`). Report + runbook: `reports/rml-compositing-spike.md`. | ## Deferred decisions (decide when reached — see notes/plan.md §7) |
