diff options
| author | Adam Malczewski <[email protected]> | 2026-06-15 15:16:57 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-15 15:16:57 +0900 |
| commit | 38f0091c64523cbda8c080c0525c74e725f40c87 (patch) | |
| tree | 8bca6aec1e10038f6f4fb983c5d17c8a6622519f | |
| parent | 432f5a0d482a2999e8d20ce8b8283e4b1ad9ab0c (diff) | |
| download | unbox-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.rcss | 8 | ||||
| -rw-r--r-- | assets/ext-window-field/field.rml | 16 | ||||
| -rw-r--r-- | 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 <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); |
