// SPIKE (rml-compositing, Phase 0) — RUNNABLE GO/NO-GO target. THROWAWAY.
//
// Proves the "RML compositing" mechanism: a LIVE client toplevel/layer surface
// is imported zero-copy as a SHARED GL texture and drawn as a SURFACE ELEMENT
// (an RML
) in an RmlUi document; an RCSS 3D transform + transition is
// applied to it; input is routed back to the client through RmlUi picking; the
// composite is presented via the RmlUi-FBO -> wlr_scene_buffer bridge.
//
// It is its OWN compositor (display/backend/renderer/allocator/scene/seat +
// xdg-shell + layer-shell) so it can map real clients, NOT the shipped Server
// (which names no feature and exposes none of this). It reuses the kernel's
// proven pieces: the wlr.hpp extern-"C" wrapper, the adapted RenderInterface_GL3
// (with SetOutputFramebuffer + the upright V-flip), and the slice-3 dmabuf ->
// EGLImage import discipline. RMLUi is kernel-private and this lives IN the
// kernel unit, so including the private renderer header is in-bounds.
//
// Two modes:
// --verify : headless + gles2, NO real client. A synthetic client dmabuf
// (known quadrant pattern) is imported LIVE; a known 3D transform
// is applied; the presented buffer is read back and asserted
// against the projected pattern; the idle dirty-gate is asserted
// (zero renders over N idle turns); the screen->surface-local
// input inversion is asserted through the transform; then a SECOND
// bring-up composites a surface TREE (toplevel + subsurface +
// popup) plus a layer-shell WALLPAPER as per-subsurface elements
// and reads back each surface's footprint + stack order (criteria
// 4 + 5). Exit 0 = pass.
// --run : a real seat (DRM) or nested (labwc) run that spawns a client
// (default `foot`), composites it live as a 3D surface element,
// routes input back, and prints per-frame perf + idle metrics for
// the user's visual/touch/perf GO-NO-GO. Ctrl-C to quit.
//
// wlroots only via unbox/kernel/wlr.hpp (.unbox/rules/wlroots-include.md).
#include
#include "spike_gl.hpp"
#include "spike_input_core.hpp"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
namespace spike = unbox::kernel::spike;
namespace {
int g_fail = 0;
void check(bool cond, const char* what) {
std::fprintf(stderr, "[verify] %-58s %s\n", what, cond ? "PASS" : "FAIL");
if (!cond) {
++g_fail;
}
}
// Allocate a real client dmabuf of (w,h) through the wlr allocator and paint it
// a single solid color via the wlr render pass — exactly the GPU path a client
// produces. Returns the locked wlr_buffer (caller drops it) or nullptr. `gl`
// must NOT be current while the wlr renderer runs, so we restore it around the
// pass and re-make-current after (mirrors the existing criterion-1 painter).
auto make_solid_client_buffer(spike::GlBridge& gl, wlr_renderer* renderer,
wlr_allocator* allocator, int w, int h, float r, float g, float b)
-> wlr_buffer* {
wlr_drm_format cfmt{};
cfmt.format = spike::kArgb8888;
std::uint64_t cmods[] = {0};
cfmt.len = 1;
cfmt.capacity = 1;
cfmt.modifiers = cmods;
wlr_buffer* buf = wlr_allocator_create_buffer(allocator, w, h, &cfmt);
if (buf == nullptr) {
return nullptr;
}
gl.restore_current();
wlr_buffer_pass_options po{};
wlr_render_pass* pass = wlr_renderer_begin_buffer_pass(renderer, buf, &po);
if (pass != nullptr) {
wlr_render_rect_options ro{};
ro.box = {0, 0, w, h};
ro.color = {r, g, b, 1};
ro.blend_mode = WLR_RENDER_BLEND_MODE_NONE;
wlr_render_pass_add_rect(pass, &ro);
wlr_render_pass_submit(pass);
}
gl.make_current();
return buf;
}
// The verify document: a single surface element (the live client texture) the
// size of the surface, with an RCSS 3D transform + transition. No body margin so
// the
fills the 256x256 surface 1:1 before transform.
const char* kVerifyRmlTemplate = R"RML(
)RML";
auto run_verify() -> int {
setenv("WLR_BACKENDS", "headless", 1);
setenv("WLR_RENDERER", "gles2", 1);
wlr_log_init(WLR_ERROR, nullptr);
wl_display* display = wl_display_create();
wl_event_loop* loop = wl_display_get_event_loop(display);
wlr_backend* backend = wlr_backend_autocreate(loop, nullptr);
wlr_renderer* renderer = wlr_renderer_autocreate(backend);
wlr_allocator* allocator = wlr_allocator_autocreate(backend, renderer);
wlr_scene* scene = wlr_scene_create();
if (!wlr_renderer_is_gles2(renderer)) {
std::fprintf(stderr, "[verify] SKIP: renderer is not gles2 (no GL path on this box)\n");
return 0;
}
EGLDisplay egl = wlr_egl_get_display(wlr_gles2_renderer_get_egl(renderer));
spike::GlBridge gl;
if (!gl.init(egl)) {
std::fprintf(stderr, "[verify] SKIP: sibling GL bridge unavailable\n");
return 0;
}
check(gl.dmabuf_ok, "criterion 1: dmabuf import path available on this GPU");
check(gl.fence_ok, "criterion 7: EGL fence-sync (no glFinish) present path active");
gl.make_current();
// The "live client buffer": a 256x256 dmabuf allocated through the wlr
// allocator (a real dmabuf the client path produces), painted with a quadrant
// pattern (TL red, TR green, BL blue, BR white) via the wlr renderer the way
// a GPU client would. Imported zero-copy as the live surface element.
const int W = 256;
spike::LiveTexture live;
live.gl = ≷
live.uri = "unbox-live://win";
wlr_drm_format cfmt{};
cfmt.format = spike::kArgb8888;
std::uint64_t cmods[] = {0};
cfmt.len = 1;
cfmt.capacity = 1;
cfmt.modifiers = cmods;
wlr_buffer* client_buf = wlr_allocator_create_buffer(allocator, W, W, &cfmt);
bool live_zero_copy = false;
if (client_buf != nullptr) {
gl.restore_current();
wlr_buffer_pass_options po{};
wlr_render_pass* pass = wlr_renderer_begin_buffer_pass(renderer, client_buf, &po);
if (pass != nullptr) {
const wlr_render_color quad[4] = {
{1, 0, 0, 1}, {0, 1, 0, 1}, {0, 0, 1, 1}, {1, 1, 1, 1}};
const wlr_box boxes[4] = {{0, 0, W / 2, W / 2},
{W / 2, 0, W / 2, W / 2},
{0, W / 2, W / 2, W / 2},
{W / 2, W / 2, W / 2, W / 2}};
for (int i = 0; i < 4; ++i) {
wlr_render_rect_options r{};
r.box = boxes[i];
r.color = quad[i];
r.blend_mode = WLR_RENDER_BLEND_MODE_NONE;
wlr_render_pass_add_rect(pass, &r);
}
wlr_render_pass_submit(pass);
}
gl.make_current();
live_zero_copy = live.adopt(client_buf, /*seq=*/1) && live.is_dmabuf;
}
check(client_buf != nullptr && live.tex != 0,
"criterion 1: live client buffer imported as a sampled texture");
check(live_zero_copy, "criterion 1: live import is ZERO-COPY dmabuf (not a CPU copy)");
// Re-adopting the SAME buffer at the SAME commit seq is the idle-gate case:
// no commit happened, so the seq is unchanged and we must NOT re-import.
const int reimports_before = live.reimports;
live.adopt(client_buf, /*seq=*/1);
live.adopt(client_buf, /*seq=*/1);
check(live.reimports == reimports_before,
"criterion 1: unchanged surface state (same seq) is NOT re-imported (cached)");
// But a NEW commit (seq advances) of the SAME pooled buffer pointer with new
// contents MUST re-import — the frozen-frame fix. Proven here directly.
live.adopt(client_buf, /*seq=*/2);
check(live.reimports == reimports_before + 1,
"criterion 1: a new commit (seq++) of a reused buffer pointer DOES re-import "
"(frozen-frame fix: pool reuse no longer skips the update)");
std::string rml = kVerifyRmlTemplate;
rml.replace(rml.find("LIVE_URI"), 8, live.uri);
Rml::Context* ctx = Rml::CreateContext("verify", Rml::Vector2i(W, W), gl.render);
Rml::ElementDocument* doc = (ctx != nullptr) ? ctx->LoadDocumentFromMemory(rml) : nullptr;
check(doc != nullptr, "verify document loaded");
if (doc != nullptr) {
doc->Show();
}
spike::PresentTarget present;
const bool present_ok = present.init(&gl, allocator, W, W);
present.scene_buffer = wlr_scene_buffer_create(&scene->tree, nullptr);
check(present_ok, "criterion 7: present FBO -> wlr_buffer target built");
check(present.dmabuf, "criterion 7: present buffer is a dmabuf (Plan A swapchain)");
auto is_color = [](const std::uint8_t p[4], int r, int g, int b) {
return std::abs(int(p[0]) - r) < 60 && std::abs(int(p[1]) - g) < 60 &&
std::abs(int(p[2]) - b) < 60;
};
if (doc != nullptr) {
// ---- Criterion 1+7 untransformed: the live pattern presents UPRIGHT
// with the quadrant colors in the right corners (present path renders the
// LIVE texture). ----
present.render(ctx);
std::uint8_t tl[4], tr[4], bl[4], br[4];
present.pixel(40, 40, tl);
present.pixel(W - 40, 40, tr);
present.pixel(40, W - 40, bl);
present.pixel(W - 40, W - 40, br);
check(is_color(tl, 255, 0, 0), "criterion 1: live TL quadrant red, upright, correct corner");
check(is_color(tr, 0, 255, 0), "criterion 1: live TR quadrant green");
check(is_color(bl, 0, 0, 255), "criterion 1: live BL quadrant blue");
check(is_color(br, 255, 255, 255), "criterion 1: live BR quadrant white");
// ---- Criterion 2: rotateY(180) (a deterministic endpoint of the 3D
// transform+transition) mirrors X about the 50% origin: TL red -> TOP-
// RIGHT, TR green -> TOP-LEFT. Reading the swapped corners proves the
// LIVE pixels rendered THROUGH the RCSS 3D transform. ----
doc->GetElementById("win")->SetProperty("transform", "rotateY(180deg)");
ctx->Update();
present.render(ctx);
std::uint8_t t_left[4], t_right[4];
present.pixel(40, 40, t_left);
present.pixel(W - 40, 40, t_right);
check(is_color(t_right, 255, 0, 0),
"criterion 2: rotateY(180) moved live TL-red to the TOP-RIGHT");
check(is_color(t_left, 0, 255, 0),
"criterion 2: rotateY(180) moved live TR-green to the TOP-LEFT");
// Mid-rotation under perspective must still SHOW the texture (alpha>0).
doc->GetElementById("win")->SetProperty("transform", "rotateY(60deg)");
ctx->Update();
present.render(ctx);
std::uint8_t center[4];
present.pixel(W / 2, W / 2, center);
check(center[3] > 0, "criterion 2: live texture visible under perspective rotateY(60deg)");
// Reset to the flat state for the idle-gate measurement.
doc->GetElementById("win")->SetProperty("transform", "rotateY(0deg)");
ctx->Update();
present.render(ctx);
}
// ---- Criterion 6 idle gate: with NO new commit, NO animation, NO input,
// OUR gate renders ZERO frames over N event-loop turns. The gate renders only
// when a dirty signal fires (client commit / active RCSS animation / input).
// ----
// The gate's animation signal is RmlUi's own GetNextUpdateDelay(): finite =>
// an animation needs the next frame; +inf => nothing is animating (idle). We
// gate on (our dirty) OR (animation pending), exactly the design's three
// dirty sources (client commit / RCSS animation / input).
auto anim_pending = [&]() -> bool {
ctx->Update();
return std::isfinite(ctx->GetNextUpdateDelay());
};
if (doc != nullptr) {
// Drain any settle frames so the document is fully at rest before we
// measure idle (a freshly-shown doc may request one more update).
for (int i = 0; i < 8; ++i) {
if (anim_pending()) {
present.render(ctx);
}
}
int idle_renders = 0;
bool dirty = false;
for (int turn = 0; turn < 120; ++turn) {
wl_event_loop_dispatch(loop, 0);
if (dirty || anim_pending()) {
present.render(ctx);
++idle_renders;
dirty = false;
}
}
check(idle_renders == 0, "criterion 6: idle dirty-gate renders ZERO frames over 120 turns");
int gated_renders = 0;
dirty = true; // simulate a single client buffer commit
for (int turn = 0; turn < 10; ++turn) {
if (dirty || anim_pending()) {
present.render(ctx);
++gated_renders;
dirty = false;
}
}
check(gated_renders == 1, "criterion 6: a single commit gates exactly ONE render");
}
// ---- Criterion 3 geometry: screen->surface-local inversion through the SAME
// transform RCSS applies (perspective(800) about the 50% origin, rotateY).
// Project a known surface-local point to its screen landing, invert, and
// confirm round-trip identity to sub-pixel — the math the runtime
// RmlUi-pick -> wl_seat translation rides on. ----
{
const double origin = W / 2.0;
const spike::Mat4 t = spike::rcss_transform_about_origin(
spike::mul(spike::perspective(800.0), spike::rotate_y(35.0 * M_PI / 180.0)), origin,
origin);
const double lx = 64.0, ly = 96.0;
const spike::ScreenPoint s = spike::project_to_screen(t, lx, ly);
const auto back = spike::unproject_to_local(t, s.x, s.y);
check(back.has_value(), "criterion 3: inversion solvable through perspective+rotateY");
if (back) {
const double err = std::hypot(back->x - lx, back->y - ly);
std::fprintf(stderr, "[verify] criterion 3 round-trip error = %.6f px\n", err);
check(err < 0.01, "criterion 3: screen->surface-local round-trip < 0.01px");
}
}
present.teardown();
live.destroy();
if (ctx != nullptr) {
Rml::RemoveContext("verify");
}
gl.restore_current();
gl.teardown();
if (client_buf != nullptr) {
wlr_buffer_drop(client_buf);
}
wlr_scene_node_destroy(&scene->tree.node);
wlr_allocator_destroy(allocator);
wlr_renderer_destroy(renderer);
wlr_backend_destroy(backend);
wl_display_destroy(display);
return 0;
}
// ---- Criteria 4 + 5: surface trees + wallpaper (per-subsurface elements) -----
//
// THE #1 unknown (criterion 4): a toplevel that owns a POPUP and a SUBSURFACE,
// composited correctly. This prototypes the PER-SUBSURFACE-ELEMENT answer: every
// node of the surface tree (toplevel, subsurface, popup) is its OWN RML
// sampling its OWN live shared texture, positioned in RCSS at its offset, with
// document order giving the stack (parent first, child/popup above). The
// alternative (per-window render-to-texture: flatten the whole tree to one
// texture off-screen, sample that as ONE element) is ANALYSED in the report;
// here we prove the per-subsurface path objectively by readback.
//
// Criterion 5 (wallpaper): a layer-shell client is just another surface element
// behind the stage — imported through the SAME LiveTexture::adopt path as the
// toplevel (criterion 1). We prove it by importing a full-output wallpaper
// buffer the identical way and reading it back where the toplevel does not cover
// it. "Mechanically identical to the toplevel path" is therefore shown, not
// asserted by hand-wave.
//
// Layout (output W x W), all flat (no 3D) so readback geometry is deterministic
// and each surface's screen footprint is exactly its element box:
// wallpaper : full output, BLUE, behind everything
// toplevel : (TLX,TLY) sized TW, RED
// subsurface: offset (+SOFF,+SOFF) inside the toplevel, GREEN (occludes RED)
// popup : at the toplevel's top-right, partly past it, WHITE (above all)
auto run_verify_surface_trees() -> int {
setenv("WLR_BACKENDS", "headless", 1);
setenv("WLR_RENDERER", "gles2", 1);
wl_display* display = wl_display_create();
wlr_backend* backend = wlr_backend_autocreate(wl_display_get_event_loop(display), nullptr);
wlr_renderer* renderer = wlr_renderer_autocreate(backend);
wlr_allocator* allocator = wlr_allocator_autocreate(backend, renderer);
wlr_scene* scene = wlr_scene_create();
if (!wlr_renderer_is_gles2(renderer)) {
std::fprintf(stderr, "[verify] SKIP surface-tree: renderer is not gles2\n");
wlr_scene_node_destroy(&scene->tree.node);
wlr_allocator_destroy(allocator);
wlr_renderer_destroy(renderer);
wlr_backend_destroy(backend);
wl_display_destroy(display);
return 0;
}
EGLDisplay egl = wlr_egl_get_display(wlr_gles2_renderer_get_egl(renderer));
spike::GlBridge gl;
if (!gl.init(egl)) {
std::fprintf(stderr, "[verify] SKIP surface-tree: GL bridge unavailable\n");
wlr_scene_node_destroy(&scene->tree.node);
wlr_allocator_destroy(allocator);
wlr_renderer_destroy(renderer);
wlr_backend_destroy(backend);
wl_display_destroy(display);
return 0;
}
gl.make_current();
const int W = 512;
const int TLX = 128, TLY = 96, TW = 256, TH = 256; // toplevel box
const int SOFF = 48, SW = 96, SH = 96; // subsurface: inside toplevel
const int PW = 96, PH = 64; // popup: at toplevel top-right edge
const int PX = TLX + TW - 32, PY = TLY - 16; // hangs past the toplevel corner
// Four real client dmabufs, painted like a GPU client would.
wlr_buffer* wall_buf = make_solid_client_buffer(gl, renderer, allocator, W, W, 0, 0, 1); // blue
wlr_buffer* top_buf = make_solid_client_buffer(gl, renderer, allocator, TW, TH, 1, 0, 0); // red
wlr_buffer* sub_buf = make_solid_client_buffer(gl, renderer, allocator, SW, SH, 0, 1, 0); // green
wlr_buffer* pop_buf = make_solid_client_buffer(gl, renderer, allocator, PW, PH, 1, 1, 1); // white
spike::LiveTexture wall, top, sub, pop;
for (auto* t : {&wall, &top, &sub, &pop}) {
t->gl = ≷
}
wall.uri = "unbox-live://wall";
top.uri = "unbox-live://top";
sub.uri = "unbox-live://sub";
pop.uri = "unbox-live://pop";
bool zero_copy = true;
struct Pair {
spike::LiveTexture* t;
wlr_buffer* b;
};
for (const Pair& p : {Pair{&wall, wall_buf}, Pair{&top, top_buf}, Pair{&sub, sub_buf},
Pair{&pop, pop_buf}}) {
const bool ok = p.b != nullptr && p.t->adopt(p.b, /*seq=*/1);
zero_copy = zero_copy && ok && p.t->is_dmabuf;
}
check(zero_copy, "criterion 4/5: tree (toplevel+subsurface+popup) + wallpaper imported zero-copy");
// ONE document, FOUR surface elements (per-subsurface answer): wallpaper
// first (behind), then the toplevel, then its subsurface, then the popup —
// document order is the composite stack. Each
samples its own live
// texture and is positioned in RCSS at its surface-tree offset.
char rml[2048];
std::snprintf(rml, sizeof(rml),
""
""
"

"
""
"

"
""
"

"
""
"

"
"",
W, W, W, W, wall.uri.c_str(), TLX, TLY, TW, TH, top.uri.c_str(), TLX + SOFF,
TLY + SOFF, SW, SH, sub.uri.c_str(), PX, PY, PW, PH, pop.uri.c_str());
Rml::Context* ctx = Rml::CreateContext("vtree", Rml::Vector2i(W, W), gl.render);
Rml::ElementDocument* doc = (ctx != nullptr) ? ctx->LoadDocumentFromMemory(rml) : nullptr;
check(doc != nullptr, "criterion 4: surface-tree document loaded");
if (doc != nullptr) {
doc->Show();
}
spike::PresentTarget present;
const bool present_ok = present.init(&gl, allocator, W, W);
present.scene_buffer = wlr_scene_buffer_create(&scene->tree, nullptr);
check(present_ok, "criterion 4/5: present target for the tree built");
auto is_color = [](const std::uint8_t p[4], int r, int g, int b) {
return std::abs(int(p[0]) - r) < 60 && std::abs(int(p[1]) - g) < 60 &&
std::abs(int(p[2]) - b) < 60;
};
if (doc != nullptr && present_ok) {
present.render(ctx);
std::uint8_t px[4];
// Wallpaper shows in a corner no other surface covers (criterion 5).
present.pixel(16, 16, px);
check(is_color(px, 0, 0, 255), "criterion 5: wallpaper (layer surface) visible behind all");
// Toplevel RED shows where neither subsurface nor popup covers it: a spot
// inside the toplevel but outside the (TLX+SOFF..+SW) subsurface box.
present.pixel(TLX + 16, TLY + TH - 16, px);
check(is_color(px, 255, 0, 0), "criterion 4: toplevel surface composited over wallpaper");
// Subsurface GREEN occludes the toplevel at its offset box centre
// (per-subsurface element drawn ABOVE its parent by document order).
present.pixel(TLX + SOFF + SW / 2, TLY + SOFF + SH / 2, px);
check(is_color(px, 0, 255, 0),
"criterion 4: subsurface element occludes the toplevel at its offset");
// Popup WHITE at its own box centre — drawn above everything, and where it
// hangs PAST the toplevel it sits directly on the wallpaper (proves popups
// are not clipped to the parent element).
present.pixel(PX + PW / 2, PY + PH / 2, px);
check(is_color(px, 255, 255, 255), "criterion 4: popup element composited above the tree");
// Stacking integrity: the popup's TOP edge (above the toplevel's top) is
// popup-white over wallpaper-blue, NOT toplevel-red — order is correct.
present.pixel(PX + PW / 2, PY + 6, px);
check(is_color(px, 255, 255, 255),
"criterion 4: surface-tree stack order correct (popup top over wallpaper)");
}
present.teardown();
for (auto* t : {&wall, &top, &sub, &pop}) {
t->destroy();
}
if (ctx != nullptr) {
Rml::RemoveContext("vtree");
}
gl.restore_current();
gl.teardown();
for (wlr_buffer* b : {wall_buf, top_buf, sub_buf, pop_buf}) {
if (b != nullptr) {
wlr_buffer_drop(b);
}
}
wlr_scene_node_destroy(&scene->tree.node);
wlr_allocator_destroy(allocator);
wlr_renderer_destroy(renderer);
wlr_backend_destroy(backend);
wl_display_destroy(display);
return 0;
}
} // namespace
// The real-seat run mode lives in rml_compositing_spike_run.cpp (its own TU).
// `demo` selects the curated 4-window perf-load scenario (3 foot + 1 firefox,
// one per inward-angled corner, with a live FPS HUD + per-5s min/max fps log and
// a 120s default dead-man) over the plain single-client `--run`.
auto run_real_seat(const char* startup_cmd, bool demo) -> int;
int main(int argc, char** argv) {
const char* mode = (argc > 1) ? argv[1] : "--verify";
if (std::strcmp(mode, "--verify") == 0) {
// Two independent compositor bring-ups (each its own display/renderer/GL
// bridge) so one cannot corrupt the other's GL/RmlUi global state: first
// the live-texture/3D/input/idle/present criteria (1,2,3,6,7), then the
// surface-tree + wallpaper criteria (4,5). g_fail accumulates across both;
// ALL PASS is printed once for the whole run.
run_verify();
run_verify_surface_trees();
std::fprintf(stderr, "\n[verify] %s (%d failures)\n",
g_fail == 0 ? "ALL PASS" : "FAILURES", g_fail);
return g_fail == 0 ? 0 : 1;
}
if (std::strcmp(mode, "--run") == 0) {
const char* cmd = (argc > 2) ? argv[2] : "foot";
return run_real_seat(cmd, /*demo=*/false);
}
if (std::strcmp(mode, "--demo") == 0) {
// The curated perf scenario spawns its OWN fixed client set (3 foot + 1
// firefox), so no startup-cmd is taken.
return run_real_seat(nullptr, /*demo=*/true);
}
std::fprintf(stderr,
"usage: %s [--verify | --run [startup-cmd] | --demo]\n"
" --verify headless self-check of criteria 1,2,3,4,5,6,7 (exit 0 = pass)\n"
" --run real/nested seat: spawn a client, composite it as a 3D\n"
" surface element, route input back, print perf/idle metrics\n"
" --demo real-seat perf load: 4 windows (3 foot + 1 firefox), one per\n"
" screen corner angled INWARD, a live FPS HUD + per-5s min/max\n"
" fps log; 120s default dead-man (P resets, Esc quits)\n",
argv[0]);
return 2;
}