diff options
| author | Adam Malczewski <[email protected]> | 2026-06-13 21:00:07 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-13 21:00:07 +0900 |
| commit | be5f67f7c7cf2710b0e73df5d92be98c758c47a4 (patch) | |
| tree | 857e0e9df72675d223b2a2f643c4d49ff01a5b50 /packages/kernel/include | |
| parent | 4f5779cec17b0e9173d2b1de634c31c516069670 (diff) | |
| download | unbox-be5f67f7c7cf2710b0e73df5d92be98c758c47a4.tar.gz unbox-be5f67f7c7cf2710b0e73df5d92be98c758c47a4.zip | |
kernel: ui surfaces composite with per-pixel alpha + set_size resizes the target
Two substrate capabilities the stage dock forced (both verified real-seat nested
and on the gles2 headless path):
1. Per-pixel alpha. A ui surface composited opaque, so any overlay (the dock)
occluded the toplevels beneath it. Root cause: a stray opaque
render_iface->Clear() (glClearColor 0,0,0,1) in render_surface overrode the
transparent BeginFrame clear, and EndFrame's premultiplied composite carried
the opaque base to the buffer. Fix: drop the stray Clear(); clear the OUTPUT
FBO to (0,0,0,0) once before BeginFrame. Blend was already correct
premultiplied; the substrate never sets an opaque region (now guarded by a
probe); ARGB8888 alpha survives end to end. A document whose <body> is
transparent now shows the scene through its un-painted pixels.
2. set_size resizes the render target. Previously logical-only (the slice-5
documented change-request): set_size re-laid-out the RmlUi document but did
NOT realloc the GL target, so a surface created small and grown rendered into
its original buffer (the dock, created as a 1px placeholder and grown on
minimize, was invisible). Fix: set_size now reallocs the FBO + dmabuf
swapchain/shm + EGLImage + texture + scene buffer on an ACTUAL size change
(no-op same-size, cheap; set_position still cheap). Grow and shrink both
render fully; alpha/upright-flip/blend/fence-sync preserved.
ui.hpp documents both. kernel 45 cases/182 assertions green on build + build-asan
(no new suppressions). Edits confined to packages/kernel/.
Diffstat (limited to 'packages/kernel/include')
| -rw-r--r-- | packages/kernel/include/unbox/kernel/server.hpp | 13 | ||||
| -rw-r--r-- | packages/kernel/include/unbox/kernel/ui.hpp | 23 |
2 files changed, 34 insertions, 2 deletions
diff --git a/packages/kernel/include/unbox/kernel/server.hpp b/packages/kernel/include/unbox/kernel/server.hpp index 4cb5230..4af327a 100644 --- a/packages/kernel/include/unbox/kernel/server.hpp +++ b/packages/kernel/include/unbox/kernel/server.hpp @@ -103,6 +103,19 @@ public: // known source color reached the expected spot inside an <img>. [[nodiscard]] auto ui_pixel(int x, int y) const -> unsigned int; + // Whether the first ui surface's scene_buffer node carries a non-empty + // opaque region. The per-pixel-alpha contract requires FALSE: a forced + // opaque region would make wlr_scene skip alpha-blending the buffer, so the + // scene below would be occluded by un-painted pixels. Test instrumentation; + // single-thread only. + [[nodiscard]] auto ui_surface_has_opaque_region() const -> bool; + + // Number of UiSurface::set_size GL-target reallocations performed so far. + // A same-size set_size is a no-op (does not bump this); a grow/shrink + // reallocates the FBO/swapchain/EGLImage/texture and bumps it. Lets the + // suite prove the only-on-change guard. Test instrumentation; single-thread. + [[nodiscard]] auto ui_resize_realloc_count() const -> int; + // Count elements with the given tag name in the first ui surface's loaded // document. 0 if no surface / no document yet. Lets the suite assert that a // data-for list rendered the expected number of rows (slice 10 / b2 list diff --git a/packages/kernel/include/unbox/kernel/ui.hpp b/packages/kernel/include/unbox/kernel/ui.hpp index 7f7609a..da5986c 100644 --- a/packages/kernel/include/unbox/kernel/ui.hpp +++ b/packages/kernel/include/unbox/kernel/ui.hpp @@ -47,6 +47,15 @@ namespace unbox::kernel { // the document AND its scene node (so hold it as a member; it dies with you, // in reverse declaration order, while the kernel's scene is still alive). // All methods are event-loop-thread only. +// +// PER-PIXEL ALPHA (transparency). The surface composites with per-pixel alpha: +// any pixel your document does NOT paint is fully transparent, and the scene +// BELOW the surface composites through it (the substrate never marks the buffer +// opaque). So a document with a transparent <body> that paints, say, only a +// card in one corner shows the windows beneath it everywhere else. If you want +// a solid panel, paint an opaque background in your RCSS (e.g. body { +// background-color: #rrggbb; }) — a fully-opaque document is pixel-identical to +// a fully-opaque surface and occludes whatever it covers, as before. class UiSurface { public: virtual ~UiSurface() = default; @@ -54,9 +63,19 @@ public: auto operator=(const UiSurface&) -> UiSurface& = delete; // ---- Geometry & visibility (layout coordinates) ---- - // Move/resize the surface. The document is laid out to w×h; the node sits - // at (x,y) in layout space. Cheap; takes effect on the next frame. + // Move the surface: the node sits at (x,y) in layout space. Cheap (no + // realloc); takes effect on the next frame. virtual void set_position(int x, int y) = 0; + // Resize the surface to w×h. This RESIZES THE RENDER TARGET — the document + // is laid out to w×h AND draws into a buffer of matching size, so the + // surface renders fully at the new size (grow AND shrink both work; the + // composited node and the input hit-test rect track the new size). It is + // HEAVIER than set_position: on an actual size change it reallocates GL + // resources (the offscreen FBO + dmabuf swapchain / shm buffer), so call it + // on size changes, not every frame. A no-op same-size call is cheap (no + // realloc). Non-positive w/h is rejected (the surface keeps its size). Takes + // effect on the next frame; resizing a hidden surface is fine (it still is + // not composited until shown). virtual void set_size(int width, int height) = 0; // Show/hide without destroying. Hidden surfaces are not composited and do // not receive input. Default after create is the spec's `visible`. |
