summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-15 15:16:57 +0900
committerAdam Malczewski <[email protected]>2026-06-15 15:16:57 +0900
commit38f0091c64523cbda8c080c0525c74e725f40c87 (patch)
tree8bca6aec1e10038f6f4fb983c5d17c8a6622519f
parent432f5a0d482a2999e8d20ce8b8283e4b1ad9ab0c (diff)
downloadunbox-38f0091c64523cbda8c080c0525c74e725f40c87.tar.gz
unbox-38f0091c64523cbda8c080c0525c74e725f40c87.zip
rml-compositing: make the window field actually render (2 real bugs)
Diagnosed live (nested unbox + grim screenshots). Two bugs stopped any window from showing; both fixed and visually confirmed (multi-window, live-updating, focus highlight, RCSS tiling). 1. kernel (ui_substrate.cpp, adopt_node) — STABLE TEXTURE ID across re-imports. The live import did glDeleteTextures+glGenTextures every commit, minting a NEW GL id each frame. But RmlUi's image()/<img> decorator caches the texture HANDLE it gets from LoadTexture(uri) ONCE, so after the first re-import it drew a DELETED texture -> blank. Now node.tex is allocated ONCE and just re-pointed at the new EGLImage (dmabuf) / re-uploaded (shm) on each re-import, destroying the previous EGLImage after the rebind. The id stays constant => RmlUi's cached handle stays valid => the window updates live. (Reimport counting unchanged.) 2. ext-window-field assets — the window texture is painted via an RCSS image() DECORATOR bound with data-style-decorator (the stage-dock pattern), NOT an <img src="{{ w.live_uri }}">: RmlUi does not substitute a data binding inside an img src, so the literal "{{ w.live_uri }}" was reaching the texture loader ("Could not load texture"). Also `contain` (was `cover`) so the whole window shows instead of cropping its center. Verified: foot composites as a surface element in the window field and updates live (a ticking clock); a 2nd window tiles with the focused one highlighted. kernel + ext-window-field suites green; build-asan kernel green (no leak/UB). Follow-up: a kernel regression test asserting the texture id is stable across re-imports (no test caught this — RmlUi caching is the subtlety).
-rw-r--r--assets/ext-window-field/field.rcss8
-rw-r--r--assets/ext-window-field/field.rml16
-rw-r--r--packages/kernel/src/ui_substrate.cpp33
3 files changed, 35 insertions, 22 deletions
diff --git a/assets/ext-window-field/field.rcss b/assets/ext-window-field/field.rcss
index da84358..5e7d5e6 100644
--- a/assets/ext-window-field/field.rcss
+++ b/assets/ext-window-field/field.rcss
@@ -65,9 +65,11 @@ div.win.focused {
transform: scale(1.0);
}
-/* The live window texture fills its tile 1:1 (the <img src=live_uri> of the
- surface element). The tile's rounded overflow:hidden clips it to the card. */
-div.win img.live {
+/* The live window texture fills its tile (the surface element's live_uri, painted
+ via an RCSS `image()` decorator bound in field.rml — the proven dynamic-texture
+ pattern, since RmlUi does NOT substitute data bindings in an <img src>). The
+ tile's rounded overflow:hidden clips it to the card. */
+div.win .live {
display: block;
position: absolute;
left: 0;
diff --git a/assets/ext-window-field/field.rml b/assets/ext-window-field/field.rml
index 15f031e..7fc2d95 100644
--- a/assets/ext-window-field/field.rml
+++ b/assets/ext-window-field/field.rml
@@ -8,14 +8,18 @@
flag. LAYOUT + ANIMATION are RCSS (field.rcss) — the user's contract
decision: C++ never computes a window's on-screen rectangle.
- Each row renders one <img src="{{ w.live_uri }}"> = the window's live
- texture (UiSubstrate::create_surface_element -> SurfaceElement::source_uri()).
- The kernel manages the toplevel's whole subsurface/popup tree behind that
- single <img>; the document addresses only the root. data-class-focused keys
- the RCSS highlight/raise off the focused window. -->
+ Each row paints the window's LIVE texture via an RCSS `image()` decorator
+ bound with data-style-decorator (the proven dynamic-texture pattern — RmlUi
+ does NOT substitute a data binding inside an <img src>, so the URI must reach
+ RCSS, not the src attribute). live_uri = SurfaceElement::source_uri() from
+ UiSubstrate::create_surface_element. `contain` shows the whole window
+ (letterboxed) rather than cropping. The kernel manages the toplevel's whole
+ subsurface/popup tree behind that single element; the document addresses only
+ the root. data-class-focused keys the RCSS highlight/raise off the focused
+ window. -->
<body data-model="ui" class="field">
<div data-for="w : wins" class="win" data-class-focused="w.focused">
-<img class="live" src="{{ w.live_uri }}"/>
+<div class="live" data-style-decorator="'image( ' + w.live_uri + ' contain center center )'"/>
</div>
</body>
</rml>
diff --git a/packages/kernel/src/ui_substrate.cpp b/packages/kernel/src/ui_substrate.cpp
index fd42a44..5971c56 100644
--- a/packages/kernel/src/ui_substrate.cpp
+++ b/packages/kernel/src/ui_substrate.cpp
@@ -1613,15 +1613,18 @@ bool Substrate::Impl::adopt_node(SurfaceElementState& el, SurfaceNode& node) {
EGLImageKHR img =
gl.egl_create_image(gl.egl_display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, ia);
if (img != EGL_NO_IMAGE_KHR) {
- // Drop the old GL objects, build the new texture from the EGLImage.
- if (node.tex != 0) {
- glDeleteTextures(1, &node.tex);
- node.tex = 0;
+ // STABLE TEXTURE ID across re-imports. RmlUi's image()/<img> decorator
+ // caches the texture HANDLE it gets from LoadTexture(uri) ONCE (at load
+ // time) and draws that handle every frame — so if we regenerated the GL
+ // id on each re-import, a live window would show its FIRST frame then go
+ // blank (it would draw a DELETED texture). Allocate node.tex ONCE and
+ // just re-point it at the NEW EGLImage on every re-import, destroying the
+ // PREVIOUS image after the rebind. The id stays constant => the cached
+ // RmlUi handle stays valid => the window updates live.
+ EGLImageKHR old_image = node.image;
+ if (node.tex == 0) {
+ glGenTextures(1, &node.tex);
}
- if (node.image != EGL_NO_IMAGE_KHR && gl.egl_destroy_image != nullptr) {
- gl.egl_destroy_image(gl.egl_display, node.image);
- }
- glGenTextures(1, &node.tex);
glBindTexture(GL_TEXTURE_2D, node.tex);
gl.gl_image_target_texture(GL_TEXTURE_2D, static_cast<GLeglImageOES>(img));
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
@@ -1629,6 +1632,9 @@ bool Substrate::Impl::adopt_node(SurfaceElementState& el, SurfaceNode& node) {
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glBindTexture(GL_TEXTURE_2D, 0);
+ if (old_image != EGL_NO_IMAGE_KHR && gl.egl_destroy_image != nullptr) {
+ gl.egl_destroy_image(gl.egl_display, old_image);
+ }
node.image = img;
node.width = attribs.width;
node.height = attribs.height;
@@ -1647,15 +1653,16 @@ bool Substrate::Impl::adopt_node(SurfaceElementState& el, SurfaceNode& node) {
wlr_buffer_unlock(buf); // import failed: drop the lock we just took
return node.tex != 0;
}
- if (node.tex != 0) {
- glDeleteTextures(1, &node.tex);
- node.tex = 0;
- }
+ // STABLE TEXTURE ID (see the dmabuf path above): reuse node.tex so RmlUi's
+ // cached decorator handle stays valid; a re-upload (glTexImage2D) into the
+ // same id refreshes the pixels for an shm client's live updates.
if (node.image != EGL_NO_IMAGE_KHR && gl.egl_destroy_image != nullptr) {
gl.egl_destroy_image(gl.egl_display, node.image);
node.image = EGL_NO_IMAGE_KHR;
}
- glGenTextures(1, &node.tex);
+ if (node.tex == 0) {
+ glGenTextures(1, &node.tex);
+ }
glBindTexture(GL_TEXTURE_2D, node.tex);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, GL_BLUE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B, GL_RED);