#include "ui_spike.hpp" #include "rmlui_renderer_gl3.h" #include #include #include #include #include // The kernel owns GL; system EGL/GLES headers are allowed here (brief). // wlr.hpp (via ui_spike.hpp) already pulled + // through wlr/render/egl.h, and GLES through the adapted renderer; we add // the dmabuf import entrypoints explicitly. #include #include #include #include // glEGLImageTargetTexture2DOES #include #include #include #include #include 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(

unbox ui spike

frame {{frame}}

)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(now.tv_sec) + now.tv_nsec / 1e9; } return (static_cast(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 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(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(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(width) * 4; buf->data.assign(buf->stride * static_cast(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 readback; // glReadPixels scratch // RMLUi. std::unique_ptr system; std::unique_ptr 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(eglGetProcAddress("eglCreateImageKHR")); egl_destroy_image = reinterpret_cast(eglGetProcAddress("eglDestroyImageKHR")); gl_image_target_texture = reinterpret_cast( 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(); 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(); 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(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(attribs.format), EGL_DMA_BUF_PLANE0_FD_EXT, attribs.fd[0], EGL_DMA_BUF_PLANE0_OFFSET_EXT, static_cast(attribs.offset[0]), EGL_DMA_BUF_PLANE0_PITCH_EXT, static_cast(attribs.stride[0]), EGL_NONE, }; egl_image = egl_create_image(egl_display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, static_cast(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(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(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(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 { auto impl = std::make_unique(); 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(); disabled->plan = Plan::Disabled; return std::unique_ptr(new UiSpike(std::move(disabled))); } return std::unique_ptr(new UiSpike(std::move(impl))); } UiSpike::UiSpike(std::unique_ptr 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(sx), static_cast(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(p[0]) - c[0]; const int dg = static_cast(p[1]) - c[1]; const int db = static_cast(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(top_row) * w + x) * 4; const std::uint8_t* pb = px + (static_cast(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