summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--notes/plan.md2
-rw-r--r--notes/rml-compositing-phase1.md257
-rw-r--r--tasks.md27
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.
diff --git a/tasks.md b/tasks.md
index 4cd757a..ba8ffe1 100644
--- a/tasks.md
+++ b/tasks.md
@@ -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)