From 38f0091c64523cbda8c080c0525c74e725f40c87 Mon Sep 17 00:00:00 2001 From: Adam Malczewski Date: Mon, 15 Jun 2026 15:16:57 +0900 Subject: rml-compositing: make the window field actually render (2 real bugs) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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()/ 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 : 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). --- assets/ext-window-field/field.rcss | 8 +++++--- assets/ext-window-field/field.rml | 16 ++++++++++------ packages/kernel/src/ui_substrate.cpp | 33 ++++++++++++++++++++------------- 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 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 ). 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 = 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 ; 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 , 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. -->
- +
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()/ 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(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); -- cgit v1.2.3