diff options
| author | Adam Malczewski <[email protected]> | 2026-06-13 00:17:33 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-13 00:17:33 +0900 |
| commit | 803fd2687a5f6ead0644f9c952bed6e3e4ef7ed9 (patch) | |
| tree | 68d727df9c0f08a7a08c2c464f95d8c82fb8789e /packages/kernel/src/ui_spike.cpp | |
| parent | c102a1b67a70149b6f9c9b2cfd8b31ceb52c09b7 (diff) | |
| download | unbox-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.cpp | 688 |
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 |
