summaryrefslogtreecommitdiffhomepage
path: root/packages/kernel/src/ui_spike.cpp
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-13 00:17:33 +0900
committerAdam Malczewski <[email protected]>2026-06-13 00:17:33 +0900
commit803fd2687a5f6ead0644f9c952bed6e3e4ef7ed9 (patch)
tree68d727df9c0f08a7a08c2c464f95d8c82fb8789e /packages/kernel/src/ui_spike.cpp
parentc102a1b67a70149b6f9c9b2cfd8b31ceb52c09b7 (diff)
downloadunbox-803fd2687a5f6ead0644f9c952bed6e3e4ef7ed9.tar.gz
unbox-803fd2687a5f6ead0644f9c952bed6e3e4ef7ed9.zip
Slice 5: real ui substrate + unified input routing + touch-mode; spike retired
The ui substrate is now the extension-facing contract (unbox/kernel/ui.hpp): Host::ui() -> UiSubstrate::create_surface(spec) -> UiSurface with typed scalar bindings (int/double/bool/string getters), data-event callbacks (error-isolated per extension), dirty(), geometry/visibility — RMLUi and GL stay kernel-private. Production sync: glFinish replaced by EGL_KHR_fence_sync + 2-deep wlr_swapchain. ui_spike retired (orientation guard + dirty-cycle coverage live on as substrate tests). Input: ONE kernel routing path feeds pointer AND touch into ui surfaces with consume-or-pass semantics and implicit-grab ownership (the consumer of a press owns the matching release; per touch point too) — fixes drag-release-over-ui sticking. touch-mode: state machine + debounce + on_touch_mode_changed notification, NO visual scaling (user decision after hardware hands-on; dp-ratio stays 1.0; see plan §2). ext-xdg-shell: GrabMachine generalized to pointer-OR-touch interaction source (touch titlebar drag works; originating-point pinning); fixed the seat implicit-grab leak (suppressed release after forwarded press swallowed all later touch-downs — pointer/touch alternation doctested); factory renamed create(). ext-layer-shell: on_demand keyboard interactivity via scene hit resolution. host-bin: --ui-demo extension (temporary acceptance demo on the public contract, dies in slice 6). User hands-on verified: same surface by mouse and finger, tap counter, touch-mode neutrality, no click-through, drag alternation, fuzzel on_demand. 113 doctest cases green, ASan/UBSan clean (our code), idle RSS ≈78 MiB. Harness: UX-feel hands-on lesson (ORCHESTRATOR §2.6), nested-run pkill/setsid notes, touch-mode glossary redefinition.
Diffstat (limited to 'packages/kernel/src/ui_spike.cpp')
-rw-r--r--packages/kernel/src/ui_spike.cpp688
1 files changed, 0 insertions, 688 deletions
diff --git a/packages/kernel/src/ui_spike.cpp b/packages/kernel/src/ui_spike.cpp
deleted file mode 100644
index 5d18c07..0000000
--- a/packages/kernel/src/ui_spike.cpp
+++ /dev/null
@@ -1,688 +0,0 @@
-#include "ui_spike.hpp"
-
-#include "rmlui_renderer_gl3.h"
-
-#include <RmlUi/Core/Context.h>
-#include <RmlUi/Core/Core.h>
-#include <RmlUi/Core/DataModelHandle.h>
-#include <RmlUi/Core/ElementDocument.h>
-#include <RmlUi/Core/SystemInterface.h>
-
-// The kernel owns GL; system EGL/GLES headers are allowed here (brief).
-// wlr.hpp (via ui_spike.hpp) already pulled <EGL/egl.h>+<EGL/eglext.h>
-// through wlr/render/egl.h, and GLES through the adapted renderer; we add
-// the dmabuf import entrypoints explicitly.
-#include <EGL/egl.h>
-#include <EGL/eglext.h>
-#include <GLES3/gl32.h>
-#include <GLES2/gl2ext.h> // glEGLImageTargetTexture2DOES
-
-#include <cstdint>
-#include <cstdlib>
-#include <cstring>
-#include <ctime>
-#include <vector>
-
-namespace unbox::kernel {
-
-namespace {
-
-constexpr int kSpikeWidth = 320;
-constexpr int kSpikeHeight = 200;
-constexpr int kSpikeX = 40; // layout-space origin of the node
-constexpr int kSpikeY = 40;
-
-// DRM FourCC for the buffers we allocate / wrap. ARGB8888 is universally
-// render+sample-able and matches RMLUi's premultiplied RGBA8 output once
-// channel order is accounted for. (FourCC AR24 = little-endian B,G,R,A.)
-constexpr std::uint32_t kDrmFormatArgb8888 = 0x34325241; // 'AR24'
-
-// Distinctive solid bands at the document's top and bottom edges. They are
-// full-width, unique colors that appear NOWHERE else in the document, so the
-// orientation assertion can prove the submitted buffer is upright: the top
-// band must land in the TOP rows of the buffer, the bottom band in the
-// BOTTOM rows. (A vertical flip would swap them — the bug.)
-constexpr int kBandHeight = 12; // px, each band
-// Top band #18e0a0 (teal-green); bottom band #e09018 (amber). Stored as the
-// RGB byte triplets the Plan-B readback produces (R,G,B order).
-constexpr std::uint8_t kTopBandRGB[3] = {0x18, 0xe0, 0xa0};
-constexpr std::uint8_t kBottomBandRGB[3] = {0xe0, 0x90, 0x18};
-
-// In-memory hello-world document. Distinctive top/bottom bands (orientation
-// proof), a title, a live frame counter via data binding, and a button that
-// reacts to hover/click (input proof).
-const char* kHelloRml = R"RML(<rml>
-<head>
-<style>
-body { font-family: "Noto Sans"; background: #1e2230; color: #e8ecff;
- width: 320px; height: 200px; }
-#topband { display: block; width: 320px; height: 12px; background: #18e0a0; }
-#bottomband { display: block; width: 320px; height: 12px; background: #e09018;
- position: absolute; bottom: 0px; left: 0px; }
-h1 { font-size: 22px; margin: 16px; color: #9ecbff; }
-p { font-size: 15px; margin: 0 16px 12px 16px; }
-button { font-size: 15px; margin: 16px; padding: 8px 16px;
- background: #3a4670; color: #ffffff; border-radius: 6px; }
-button:hover { background: #5468b0; }
-button:active { background: #7e93e0; }
-</style>
-</head>
-<body data-model="spike">
-<div id="topband"></div>
-<h1>unbox ui spike</h1>
-<p>frame {{frame}}</p>
-<button>{{label}}</button>
-<div id="bottomband"></div>
-</body>
-</rml>)RML";
-
-// --- SystemInterface: elapsed time + route RmlUi logs to wlr_log ----------
-
-class SpikeSystemInterface final : public Rml::SystemInterface {
-public:
- auto GetElapsedTime() -> double override {
- timespec now{};
- clock_gettime(CLOCK_MONOTONIC, &now);
- if (start_ == 0.0) {
- start_ = static_cast<double>(now.tv_sec) + now.tv_nsec / 1e9;
- }
- return (static_cast<double>(now.tv_sec) + now.tv_nsec / 1e9) - start_;
- }
-
- auto LogMessage(Rml::Log::Type type, const Rml::String& message) -> bool override {
- const wlr_log_importance imp = (type == Rml::Log::LT_ERROR || type == Rml::Log::LT_ASSERT)
- ? WLR_ERROR
- : (type == Rml::Log::LT_WARNING ? WLR_INFO : WLR_DEBUG);
- wlr_log(imp, "[rmlui] %s", message.c_str());
- return true;
- }
-
-private:
- double start_ = 0.0;
-};
-
-// --- A data-ptr wlr_buffer wrapping heap memory (Plan B target) -----------
-//
-// The wlr GLES2 renderer can sample a WLR_BUFFER_CAP_DATA_PTR buffer (it
-// uploads via begin/end_data_ptr_access). Works on both the headless/pixman
-// and GPU/gles2 backends, which is why this is the robust spike landing.
-
-struct ShmBuffer {
- wlr_buffer base{};
- std::vector<std::uint8_t> data;
- std::uint32_t format = kDrmFormatArgb8888;
- std::size_t stride = 0;
- bool dropped = false;
-};
-
-void shm_buffer_destroy(wlr_buffer* wlr_buf) {
- auto* buf = reinterpret_cast<ShmBuffer*>(wlr_buf);
- wlr_buffer_finish(&buf->base);
- delete buf;
-}
-
-auto shm_buffer_begin_data_ptr_access(wlr_buffer* wlr_buf, std::uint32_t /*flags*/, void** data,
- std::uint32_t* format, std::size_t* stride) -> bool {
- auto* buf = reinterpret_cast<ShmBuffer*>(wlr_buf);
- *data = buf->data.data();
- *format = buf->format;
- *stride = buf->stride;
- return true;
-}
-
-void shm_buffer_end_data_ptr_access(wlr_buffer* /*wlr_buf*/) {}
-
-const wlr_buffer_impl kShmBufferImpl = {
- .destroy = shm_buffer_destroy,
- .get_dmabuf = nullptr,
- .get_shm = nullptr,
- .begin_data_ptr_access = shm_buffer_begin_data_ptr_access,
- .end_data_ptr_access = shm_buffer_end_data_ptr_access,
-};
-
-auto make_shm_buffer(int width, int height) -> ShmBuffer* {
- auto* buf = new ShmBuffer();
- buf->stride = static_cast<std::size_t>(width) * 4;
- buf->data.assign(buf->stride * static_cast<std::size_t>(height), 0);
- wlr_buffer_init(&buf->base, &kShmBufferImpl, width, height);
- return buf;
-}
-
-} // namespace
-
-// --- Impl -----------------------------------------------------------------
-
-struct UiSpike::Impl {
- EGLDisplay egl_display = EGL_NO_DISPLAY;
- EGLContext egl_context = EGL_NO_CONTEXT;
- EGLContext saved_context = EGL_NO_CONTEXT;
- EGLSurface saved_draw = EGL_NO_SURFACE;
- EGLSurface saved_read = EGL_NO_SURFACE;
-
- wlr_allocator* allocator = nullptr; // borrowed (server-owned)
- wlr_renderer* renderer = nullptr; // borrowed (server-owned)
-
- // Sibling-context GL objects.
- GLuint fbo = 0;
- GLuint color_tex = 0;
-
- // Plan A (dmabuf) state — populated only if A engages.
- wlr_buffer* dmabuf = nullptr; // the swapchain-acquired render target
- EGLImageKHR egl_image = EGL_NO_IMAGE_KHR;
-
- // Plan B (shm copy) state.
- ShmBuffer* shm = nullptr;
- std::vector<std::uint8_t> readback; // glReadPixels scratch
-
- // RMLUi.
- std::unique_ptr<SpikeSystemInterface> system;
- std::unique_ptr<RenderInterface_GL3> render_iface;
- Rml::Context* context = nullptr; // owned by Rml (RemoveContext)
- Rml::ElementDocument* document = nullptr;
- Rml::DataModelHandle model;
-
- // Data-bound document state.
- int frame = 0;
- Rml::String label = "hover me";
-
- // Scene.
- wlr_scene_buffer* scene_buffer = nullptr;
-
- Plan plan = Plan::Disabled;
- int frame_count = 0;
-
- // EGL extension entrypoints (loaded once).
- PFNEGLCREATEIMAGEKHRPROC egl_create_image = nullptr;
- PFNEGLDESTROYIMAGEKHRPROC egl_destroy_image = nullptr;
- PFNGLEGLIMAGETARGETTEXTURE2DOESPROC gl_image_target_texture = nullptr;
-
- bool make_current() {
- saved_context = eglGetCurrentContext();
- saved_draw = eglGetCurrentSurface(EGL_DRAW);
- saved_read = eglGetCurrentSurface(EGL_READ);
- return eglMakeCurrent(egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, egl_context) == EGL_TRUE;
- }
-
- void restore_current() {
- eglMakeCurrent(egl_display, saved_draw, saved_read, saved_context);
- }
-
- bool init(wlr_scene_tree* parent, EGLDisplay display, wlr_allocator* alloc,
- wlr_renderer* rend);
- bool try_plan_a();
- void setup_plan_b();
- void render_locked();
- void teardown();
-};
-
-bool UiSpike::Impl::init(wlr_scene_tree* parent, EGLDisplay display, wlr_allocator* alloc,
- wlr_renderer* rend) {
- egl_display = display;
- allocator = alloc;
- renderer = rend;
-
- // 1. Sibling GLES 3.2 context sharing the wlr EGLDisplay. No GL object
- // sharing — buffers cross via dmabuf/EGLImage or CPU copy only, so we
- // do NOT pass the wlr context as share_context.
- if (eglBindAPI(EGL_OPENGL_ES_API) != EGL_TRUE) {
- wlr_log(WLR_ERROR, "ui-spike: eglBindAPI(ES) failed");
- return false;
- }
- const EGLint config_attribs[] = {
- EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT,
- EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8, EGL_ALPHA_SIZE, 8,
- EGL_NONE,
- };
- EGLConfig config = nullptr;
- EGLint num_config = 0;
- if (eglChooseConfig(egl_display, config_attribs, &config, 1, &num_config) != EGL_TRUE ||
- num_config < 1) {
- wlr_log(WLR_ERROR, "ui-spike: eglChooseConfig found no ES3 config");
- return false;
- }
- const EGLint ctx_attribs[] = {
- EGL_CONTEXT_MAJOR_VERSION, 3, EGL_CONTEXT_MINOR_VERSION, 2, EGL_NONE,
- };
- egl_context = eglCreateContext(egl_display, config, EGL_NO_CONTEXT, ctx_attribs);
- if (egl_context == EGL_NO_CONTEXT) {
- wlr_log(WLR_ERROR, "ui-spike: eglCreateContext(ES 3.2) failed (0x%x)", eglGetError());
- return false;
- }
-
- if (!make_current()) {
- wlr_log(WLR_ERROR, "ui-spike: eglMakeCurrent (surfaceless) failed (0x%x)", eglGetError());
- restore_current();
- return false;
- }
-
- // Load EGLImage entrypoints for the Plan-A attempt.
- egl_create_image =
- reinterpret_cast<PFNEGLCREATEIMAGEKHRPROC>(eglGetProcAddress("eglCreateImageKHR"));
- egl_destroy_image =
- reinterpret_cast<PFNEGLDESTROYIMAGEKHRPROC>(eglGetProcAddress("eglDestroyImageKHR"));
- gl_image_target_texture = reinterpret_cast<PFNGLEGLIMAGETARGETTEXTURE2DOESPROC>(
- eglGetProcAddress("glEGLImageTargetTexture2DOES"));
-
- // 2. RMLUi render interface (our GLES3-adapted GL3 backend).
- Rml::String gl_msg;
- if (!RmlGL3::Initialize(&gl_msg)) {
- wlr_log(WLR_ERROR, "ui-spike: RmlGL3::Initialize failed");
- restore_current();
- return false;
- }
- wlr_log(WLR_INFO, "ui-spike: %s", gl_msg.c_str());
-
- render_iface = std::make_unique<RenderInterface_GL3>();
- if (!*render_iface) {
- wlr_log(WLR_ERROR, "ui-spike: RenderInterface_GL3 construction failed");
- restore_current();
- return false;
- }
- render_iface->SetViewport(kSpikeWidth, kSpikeHeight);
-
- // 3. Offscreen FBO + color target. Plan A first, Plan B on any failure.
- glGenFramebuffers(1, &fbo);
- if (!try_plan_a()) {
- setup_plan_b();
- }
- // flip_y: the FBO color attachment (dmabuf in Plan A, GL texture read
- // back in Plan B) is sampled/scanned-out row 0 = top, but GL renders with
- // a bottom-left origin. Flip the final composite so the submitted buffer
- // is upright; display then matches document coords, so pointer input is
- // forwarded unflipped (on-screen button == document button).
- render_iface->SetOutputFramebuffer(fbo, /*flip_y=*/true);
-
- // Verify the FBO is complete before committing to RMLUi init.
- glBindFramebuffer(GL_FRAMEBUFFER, fbo);
- const GLenum fb_status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
- glBindFramebuffer(GL_FRAMEBUFFER, 0);
- if (fb_status != GL_FRAMEBUFFER_COMPLETE) {
- wlr_log(WLR_ERROR, "ui-spike: output FBO incomplete (0x%x)", fb_status);
- restore_current();
- return false;
- }
-
- // 4. RMLUi core + font + context + document.
- system = std::make_unique<SpikeSystemInterface>();
- Rml::SetSystemInterface(system.get());
- Rml::SetRenderInterface(render_iface.get());
- if (!Rml::Initialise()) {
- wlr_log(WLR_ERROR, "ui-spike: Rml::Initialise failed");
- restore_current();
- return false;
- }
-
- if (!Rml::LoadFontFace("/usr/share/fonts/noto/NotoSans-Regular.ttf")) {
- wlr_log(WLR_INFO, "ui-spike: NotoSans not found; disabling spike gracefully");
- Rml::Shutdown();
- restore_current();
- return false;
- }
-
- context = Rml::CreateContext("spike", Rml::Vector2i(kSpikeWidth, kSpikeHeight));
- if (context == nullptr) {
- wlr_log(WLR_ERROR, "ui-spike: CreateContext failed");
- Rml::Shutdown();
- restore_current();
- return false;
- }
-
- if (Rml::DataModelConstructor ctor = context->CreateDataModel("spike")) {
- ctor.Bind("frame", &frame);
- ctor.Bind("label", &label);
- model = ctor.GetModelHandle();
- }
-
- document = context->LoadDocumentFromMemory(kHelloRml);
- if (document == nullptr) {
- wlr_log(WLR_ERROR, "ui-spike: LoadDocumentFromMemory failed");
- Rml::Shutdown();
- restore_current();
- return false;
- }
- document->Show();
-
- // 5. Scene node. Start with a transparent/empty buffer; tick() fills it.
- scene_buffer = wlr_scene_buffer_create(parent, nullptr);
- if (scene_buffer == nullptr) {
- wlr_log(WLR_ERROR, "ui-spike: wlr_scene_buffer_create failed");
- Rml::Shutdown();
- restore_current();
- return false;
- }
- wlr_scene_node_set_position(&scene_buffer->node, kSpikeX, kSpikeY);
-
- restore_current();
- wlr_log(WLR_INFO, "ui-spike: bridge up (plan %s, %dx%d)",
- plan == Plan::Dmabuf ? "A/dmabuf" : "B/shm-copy", kSpikeWidth, kSpikeHeight);
- return true;
-}
-
-// Plan A: allocate a dmabuf wlr_buffer via the server allocator, import it
-// into the sibling context as an EGLImage, bind that as the FBO color
-// attachment. Returns false (cleaning up) on any failure so init() falls to B.
-bool UiSpike::Impl::try_plan_a() {
- // Spike instrumentation: force the Plan-B fallback for testing the CPU
- // copy path even on hardware where Plan A works. Harmless in production.
- if (std::getenv("UNBOX_UI_SPIKE_FORCE_SHM") != nullptr) {
- wlr_log(WLR_INFO, "ui-spike: plan A skipped — UNBOX_UI_SPIKE_FORCE_SHM set");
- return false;
- }
- if ((allocator->buffer_caps & WLR_BUFFER_CAP_DMABUF) == 0) {
- wlr_log(WLR_INFO, "ui-spike: plan A skipped — allocator has no DMABUF cap");
- return false;
- }
- if (egl_create_image == nullptr || gl_image_target_texture == nullptr) {
- wlr_log(WLR_INFO, "ui-spike: plan A skipped — no EGLImage dmabuf-import entrypoints");
- return false;
- }
- const char* exts = eglQueryString(egl_display, EGL_EXTENSIONS);
- if (exts == nullptr || std::strstr(exts, "EGL_EXT_image_dma_buf_import") == nullptr) {
- wlr_log(WLR_INFO, "ui-spike: plan A skipped — no EGL_EXT_image_dma_buf_import");
- return false;
- }
-
- // Allocate one dmabuf via the allocator using a LINEAR/INVALID modifier
- // list (legacy-driver-safe; crocus is fine with linear).
- wlr_drm_format fmt{};
- fmt.format = kDrmFormatArgb8888;
- const std::uint64_t modifiers[] = {0 /* DRM_FORMAT_MOD_LINEAR */};
- fmt.len = 1;
- fmt.capacity = 1;
- fmt.modifiers = const_cast<std::uint64_t*>(modifiers);
-
- wlr_buffer* buf = wlr_allocator_create_buffer(allocator, kSpikeWidth, kSpikeHeight, &fmt);
- if (buf == nullptr) {
- wlr_log(WLR_INFO, "ui-spike: plan A — allocator could not create dmabuf");
- return false;
- }
-
- wlr_dmabuf_attributes attribs{};
- if (!wlr_buffer_get_dmabuf(buf, &attribs) || attribs.n_planes < 1) {
- wlr_log(WLR_INFO, "ui-spike: plan A — buffer has no dmabuf attrs");
- wlr_buffer_drop(buf);
- return false;
- }
-
- // Build the EGLImage from the dmabuf (single-plane fast path).
- EGLint img_attribs[] = {
- EGL_WIDTH, attribs.width,
- EGL_HEIGHT, attribs.height,
- EGL_LINUX_DRM_FOURCC_EXT, static_cast<EGLint>(attribs.format),
- EGL_DMA_BUF_PLANE0_FD_EXT, attribs.fd[0],
- EGL_DMA_BUF_PLANE0_OFFSET_EXT, static_cast<EGLint>(attribs.offset[0]),
- EGL_DMA_BUF_PLANE0_PITCH_EXT, static_cast<EGLint>(attribs.stride[0]),
- EGL_NONE,
- };
- egl_image = egl_create_image(egl_display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT,
- static_cast<EGLClientBuffer>(nullptr), img_attribs);
- if (egl_image == EGL_NO_IMAGE_KHR) {
- wlr_log(WLR_INFO, "ui-spike: plan A — eglCreateImageKHR failed (0x%x)", eglGetError());
- wlr_buffer_drop(buf);
- return false;
- }
-
- glGenTextures(1, &color_tex);
- glBindTexture(GL_TEXTURE_2D, color_tex);
- gl_image_target_texture(GL_TEXTURE_2D, static_cast<GLeglImageOES>(egl_image));
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
-
- glBindFramebuffer(GL_FRAMEBUFFER, fbo);
- glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, color_tex, 0);
- const GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
- glBindFramebuffer(GL_FRAMEBUFFER, 0);
- if (status != GL_FRAMEBUFFER_COMPLETE) {
- wlr_log(WLR_INFO, "ui-spike: plan A — FBO from EGLImage incomplete (0x%x)", status);
- egl_destroy_image(egl_display, egl_image);
- egl_image = EGL_NO_IMAGE_KHR;
- glDeleteTextures(1, &color_tex);
- color_tex = 0;
- wlr_buffer_drop(buf);
- return false;
- }
-
- dmabuf = buf;
- plan = Plan::Dmabuf;
- wlr_log(WLR_INFO, "ui-spike: plan A engaged (dmabuf-backed FBO)");
- return true;
-}
-
-// Plan B: a plain GL texture color attachment; results read back to a
-// data-ptr wlr_buffer with glReadPixels each frame.
-void UiSpike::Impl::setup_plan_b() {
- glGenTextures(1, &color_tex);
- glBindTexture(GL_TEXTURE_2D, color_tex);
- glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kSpikeWidth, kSpikeHeight, 0, GL_RGBA,
- GL_UNSIGNED_BYTE, nullptr);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
- glBindFramebuffer(GL_FRAMEBUFFER, fbo);
- glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, color_tex, 0);
- glBindFramebuffer(GL_FRAMEBUFFER, 0);
-
- shm = make_shm_buffer(kSpikeWidth, kSpikeHeight);
- readback.assign(static_cast<std::size_t>(kSpikeWidth) * kSpikeHeight * 4, 0);
- plan = Plan::ShmCopy;
- wlr_log(WLR_INFO, "ui-spike: plan B engaged (FBO + glReadPixels -> shm)");
-}
-
-// Render one dirty frame. Caller holds the sibling context current.
-void UiSpike::Impl::render_locked() {
- // Tick the bound state; dirtying drives the repeated render proof.
- frame += 1;
- if (model) {
- model.DirtyVariable("frame");
- }
-
- context->Update();
- render_iface->BeginFrame();
- render_iface->Clear();
- context->Render();
- render_iface->EndFrame(); // composites into `fbo` (SetOutputFramebuffer)
-
- if (plan == Plan::Dmabuf) {
- // The wlr renderer will sample the dmabuf directly; ensure all GL
- // writes have landed before the compositor reads it. A spike uses
- // glFinish; the real substrate will use an EGL fence.
- glFinish();
- wlr_scene_buffer_set_buffer(scene_buffer, dmabuf);
- } else {
- // Plan B: read the FBO back into the data-ptr buffer.
- glBindFramebuffer(GL_FRAMEBUFFER, fbo);
- glReadPixels(0, 0, kSpikeWidth, kSpikeHeight, GL_RGBA, GL_UNSIGNED_BYTE, readback.data());
- glBindFramebuffer(GL_FRAMEBUFFER, 0);
-
- // RMLUi outputs premultiplied RGBA8 (R,G,B,A byte order). The shm
- // buffer is FourCC AR24 = little-endian {B,G,R,A}. Swap R<->B; the
- // result is already premultiplied which wlroots expects.
- const std::size_t px = static_cast<std::size_t>(kSpikeWidth) * kSpikeHeight;
- std::uint8_t* dst = shm->data.data();
- const std::uint8_t* src = readback.data();
- for (std::size_t i = 0; i < px; ++i) {
- dst[i * 4 + 0] = src[i * 4 + 2]; // B
- dst[i * 4 + 1] = src[i * 4 + 1]; // G
- dst[i * 4 + 2] = src[i * 4 + 0]; // R
- dst[i * 4 + 3] = src[i * 4 + 3]; // A
- }
- wlr_scene_buffer_set_buffer(scene_buffer, &shm->base);
- }
-
- frame_count += 1;
-}
-
-void UiSpike::Impl::teardown() {
- // RMLUi teardown needs the sibling context current (GL deletes).
- const bool ok = make_current();
-
- if (scene_buffer != nullptr) {
- wlr_scene_node_destroy(&scene_buffer->node);
- scene_buffer = nullptr;
- }
-
- if (context != nullptr) {
- // Document is owned by the context; Shutdown tears everything down.
- Rml::Shutdown();
- context = nullptr;
- document = nullptr;
- }
- render_iface.reset();
-
- if (color_tex != 0) {
- glDeleteTextures(1, &color_tex);
- color_tex = 0;
- }
- if (fbo != 0) {
- glDeleteFramebuffers(1, &fbo);
- fbo = 0;
- }
- if (egl_image != EGL_NO_IMAGE_KHR && egl_destroy_image != nullptr) {
- egl_destroy_image(egl_display, egl_image);
- egl_image = EGL_NO_IMAGE_KHR;
- }
- if (dmabuf != nullptr) {
- wlr_buffer_drop(dmabuf);
- dmabuf = nullptr;
- }
- if (shm != nullptr) {
- wlr_buffer_drop(&shm->base); // triggers shm_buffer_destroy -> delete
- shm = nullptr;
- }
-
- if (ok) {
- restore_current();
- }
- if (egl_context != EGL_NO_CONTEXT) {
- eglDestroyContext(egl_display, egl_context);
- egl_context = EGL_NO_CONTEXT;
- }
-}
-
-// --- UiSpike (public-ish private surface) ---------------------------------
-
-auto UiSpike::create(wlr_scene_tree* parent, EGLDisplay egl_display, wlr_allocator* allocator,
- wlr_renderer* renderer) -> std::unique_ptr<UiSpike> {
- auto impl = std::make_unique<Impl>();
- if (!impl->init(parent, egl_display, allocator, renderer)) {
- impl->teardown(); // safe to call after partial init
- // Hand back a Disabled bridge (never throws, never aborts the server).
- auto disabled = std::make_unique<Impl>();
- disabled->plan = Plan::Disabled;
- return std::unique_ptr<UiSpike>(new UiSpike(std::move(disabled)));
- }
- return std::unique_ptr<UiSpike>(new UiSpike(std::move(impl)));
-}
-
-UiSpike::UiSpike(std::unique_ptr<Impl> impl) : impl_(std::move(impl)) {}
-
-UiSpike::~UiSpike() {
- if (impl_->plan != Plan::Disabled) {
- impl_->teardown();
- }
-}
-
-void UiSpike::tick() {
- if (impl_->plan == Plan::Disabled) {
- return;
- }
- // Render every tick (spike fidelity: the frame counter dirties the doc
- // each call, so the context is always dirty — proving repeated cycles).
- if (!impl_->make_current()) {
- return;
- }
- impl_->render_locked();
- impl_->restore_current();
-}
-
-void UiSpike::on_pointer_motion(double sx, double sy) {
- if (impl_->plan == Plan::Disabled || impl_->context == nullptr) {
- return;
- }
- impl_->context->ProcessMouseMove(static_cast<int>(sx), static_cast<int>(sy), 0);
-}
-
-void UiSpike::on_pointer_button(bool pressed) {
- if (impl_->plan == Plan::Disabled || impl_->context == nullptr) {
- return;
- }
- if (pressed) {
- impl_->context->ProcessMouseButtonDown(0, 0);
- } else {
- impl_->context->ProcessMouseButtonUp(0, 0);
- }
-}
-
-auto UiSpike::node() const -> wlr_scene_node* {
- return impl_->scene_buffer != nullptr ? &impl_->scene_buffer->node : nullptr;
-}
-
-auto UiSpike::plan() const -> Plan {
- return impl_->plan;
-}
-
-auto UiSpike::frame_count() const -> int {
- return impl_->frame_count;
-}
-
-auto UiSpike::check_orientation() const -> int {
- // Only the shm path keeps a CPU readback to inspect, and only after a
- // frame has been submitted.
- if (impl_->plan != Plan::ShmCopy || impl_->frame_count == 0) {
- return 0;
- }
- const std::uint8_t* px = impl_->readback.data(); // R,G,B,A, row 0 = top
- const int w = kSpikeWidth;
- const int h = kSpikeHeight;
-
- auto matches = [](const std::uint8_t* p, const std::uint8_t (&c)[3]) {
- const int dr = static_cast<int>(p[0]) - c[0];
- const int dg = static_cast<int>(p[1]) - c[1];
- const int db = static_cast<int>(p[2]) - c[2];
- return dr * dr + dg * dg + db * db < 24 * 24; // tolerant of AA edges
- };
-
- // Count band pixels in the top kBandHeight rows vs the bottom kBandHeight
- // rows, sampling the full width. Upright => top band dominates the top
- // rows and bottom band the bottom rows; a flip swaps them.
- int top_band_in_top = 0;
- int top_band_in_bottom = 0;
- int bottom_band_in_top = 0;
- int bottom_band_in_bottom = 0;
- for (int row = 0; row < kBandHeight; ++row) {
- const int top_row = row;
- const int bot_row = h - 1 - row;
- for (int x = 0; x < w; ++x) {
- const std::uint8_t* pt = px + (static_cast<std::size_t>(top_row) * w + x) * 4;
- const std::uint8_t* pb = px + (static_cast<std::size_t>(bot_row) * w + x) * 4;
- if (matches(pt, kTopBandRGB)) {
- ++top_band_in_top;
- }
- if (matches(pb, kTopBandRGB)) {
- ++top_band_in_bottom;
- }
- if (matches(pt, kBottomBandRGB)) {
- ++bottom_band_in_top;
- }
- if (matches(pb, kBottomBandRGB)) {
- ++bottom_band_in_bottom;
- }
- }
- }
-
- // Need a clear, unambiguous signal in one orientation.
- const bool upright = top_band_in_top > 100 && bottom_band_in_bottom > 100 &&
- top_band_in_top > top_band_in_bottom &&
- bottom_band_in_bottom > bottom_band_in_top;
- const bool flipped = top_band_in_bottom > 100 && bottom_band_in_top > 100 &&
- top_band_in_bottom > top_band_in_top &&
- bottom_band_in_top > bottom_band_in_bottom;
- if (upright) {
- return 1;
- }
- if (flipped) {
- return -1;
- }
- return 0;
-}
-
-} // namespace unbox::kernel