detached a0f00e0

fix(vulkan): keep the surface below a cover that will not draw

commit
a0f00e02d422b0d6f8d54b6f6649358000316de6
author
brice · · 2026-09-26 15:46 UTC
parent
693b8c9
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +47 -3
M internal/adapters/vulkan/compose_test.go +25 -0
@@ -176,6 +176,31 @@ func TestRendererSkipsOccludedSurface(t *testing.T) {
176176 }
177177 }
178178
179+// A covering child whose buffer will not draw does not hide the root:
180+// an unimportable dmabuf still shows the surface below it, not a hole.
181+func TestRendererKeepsSurfaceBelowFailedCover(t *testing.T) {
182+ r, err := New(32, 32)
183+ if err != nil {
184+ t.Skipf("Vulkan unavailable: %v", err)
185+ }
186+ defer r.Close()
187+ red := color.RGBA{255, 0, 0, 255}
188+ root := solidContent(t, 16, 16, red)
189+ root.ID, root.Seq = 1, 1
190+ bad := &ports.DMABuf{ID: 9, Width: 16, Height: 16, Format: 0xdeadbeef}
191+ root.Children = []ports.Subsurface{{SurfaceContent: ports.SurfaceContent{Width: 16, Height: 16, Opaque: true, DMABuf: bad}}}
192+ scene := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 16, H: 16}}}}
193+ if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: root}); err != nil {
194+ t.Fatal(err)
195+ }
196+ if r.copied != 16*16*4 {
197+ t.Fatalf("copied %d bytes, want the root's %d", r.copied, 16*16*4)
198+ }
199+ if got := r.Pixels().At(8, 8); got != red {
200+ t.Fatalf("At(8,8)=%v want %v", got, red)
201+ }
202+}
203+
179204 // render draws a frame and closes its fence.
180205 func render(r *Renderer, s ports.Scene, contents map[ports.WindowID]ports.SurfaceContent) error {
181206 done, err := r.Render(s, contents)
@@ -176,6 +176,31 @@ func TestRendererSkipsOccludedSurface(t *testing.T) {
176 }176 }
177 }177 }
178 178
179+// A covering child whose buffer will not draw does not hide the root:
180+// an unimportable dmabuf still shows the surface below it, not a hole.
181+func TestRendererKeepsSurfaceBelowFailedCover(t *testing.T) {
182+ r, err := New(32, 32)
183+ if err != nil {
184+ t.Skipf("Vulkan unavailable: %v", err)
185+ }
186+ defer r.Close()
187+ red := color.RGBA{255, 0, 0, 255}
188+ root := solidContent(t, 16, 16, red)
189+ root.ID, root.Seq = 1, 1
190+ bad := &ports.DMABuf{ID: 9, Width: 16, Height: 16, Format: 0xdeadbeef}
191+ root.Children = []ports.Subsurface{{SurfaceContent: ports.SurfaceContent{Width: 16, Height: 16, Opaque: true, DMABuf: bad}}}
192+ scene := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 16, H: 16}}}}
193+ if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: root}); err != nil {
194+ t.Fatal(err)
195+ }
196+ if r.copied != 16*16*4 {
197+ t.Fatalf("copied %d bytes, want the root's %d", r.copied, 16*16*4)
198+ }
199+ if got := r.Pixels().At(8, 8); got != red {
200+ t.Fatalf("At(8,8)=%v want %v", got, red)
201+ }
202+}
203+
179 // render draws a frame and closes its fence.204 // render draws a frame and closes its fence.
180 func render(r *Renderer, s ports.Scene, contents map[ports.WindowID]ports.SurfaceContent) error {205 func render(r *Renderer, s ports.Scene, contents map[ports.WindowID]ports.SurfaceContent) error {
181 done, err := r.Render(s, contents)206 done, err := r.Render(s, contents)
M internal/adapters/vulkan/scene.go +22 -3
@@ -157,7 +157,9 @@ func (w *sceneWalk) place(id ports.WindowID, content *ports.SurfaceContent, x, y
157157
158158 // opaqueChildren gives the physical rects shown by the opaque subsurfaces
159159 // above the root (e.g. a game's GPU surface over its window's shm buffer):
160-// they hide the surfaces below them.
160+// they hide the surfaces below them. A child whose buffer will not draw is
161+// left out, so the surface below it still shows: a dmabuf that fails to
162+// import (opaqueChildren pre-imports it) or an unreadable shm buffer.
161163 func (w *sceneWalk) opaqueChildren(content *ports.SurfaceContent, ox, oy int, clip image.Rectangle) []image.Rectangle {
162164 var covers []image.Rectangle
163165 for i := range content.Children {
@@ -165,13 +167,30 @@ func (w *sceneWalk) opaqueChildren(content *ports.SurfaceContent, ox, oy int, cl
165167 if ch.Below || !ch.Opaque {
166168 continue
167169 }
168- if _, dst, ok := w.surfaceRects(&ch.SurfaceContent, ox+ch.X, oy+ch.Y, clip); ok {
169- covers = append(covers, dst)
170+ _, dst, ok := w.surfaceRects(&ch.SurfaceContent, ox+ch.X, oy+ch.Y, clip)
171+ if !ok || !w.drawable(&ch.SurfaceContent) {
172+ continue
170173 }
174+ covers = append(covers, dst)
171175 }
172176 return covers
173177 }
174178
179+// drawable reports whether the child's buffer will draw. GPU buffer
180+// imports are cached, so pre-importing here costs nothing.
181+func (w *sceneWalk) drawable(c *ports.SurfaceContent) bool {
182+ if c.DMABuf != nil {
183+ _, err := w.r.importDMABuf(c.DMABuf)
184+ return err == nil
185+ }
186+ b := c.SHM
187+ if b == nil {
188+ return false
189+ }
190+ _, err := w.r.shmPixels(b, b.Offset+(c.Height-1)*b.Stride+c.Width*4)
191+ return err == nil
192+}
193+
175194 // surfaceRects gives the physical rect of a surface buffer with its origin
176195 // at (x, y) logical, and the part of it clip (logical) shows. ok is false
177196 // when there is nothing to draw.
@@ -157,7 +157,9 @@ func (w *sceneWalk) place(id ports.WindowID, content *ports.SurfaceContent, x, y
157 157
158 // opaqueChildren gives the physical rects shown by the opaque subsurfaces158 // opaqueChildren gives the physical rects shown by the opaque subsurfaces
159 // above the root (e.g. a game's GPU surface over its window's shm buffer):159 // above the root (e.g. a game's GPU surface over its window's shm buffer):
160-// they hide the surfaces below them.160+// they hide the surfaces below them. A child whose buffer will not draw is
161+// left out, so the surface below it still shows: a dmabuf that fails to
162+// import (opaqueChildren pre-imports it) or an unreadable shm buffer.
161 func (w *sceneWalk) opaqueChildren(content *ports.SurfaceContent, ox, oy int, clip image.Rectangle) []image.Rectangle {163 func (w *sceneWalk) opaqueChildren(content *ports.SurfaceContent, ox, oy int, clip image.Rectangle) []image.Rectangle {
162 var covers []image.Rectangle164 var covers []image.Rectangle
163 for i := range content.Children {165 for i := range content.Children {
@@ -165,13 +167,30 @@ func (w *sceneWalk) opaqueChildren(content *ports.SurfaceContent, ox, oy int, cl
165 if ch.Below || !ch.Opaque {167 if ch.Below || !ch.Opaque {
166 continue168 continue
167 }169 }
168- if _, dst, ok := w.surfaceRects(&ch.SurfaceContent, ox+ch.X, oy+ch.Y, clip); ok {170+ _, dst, ok := w.surfaceRects(&ch.SurfaceContent, ox+ch.X, oy+ch.Y, clip)
169- covers = append(covers, dst)171+ if !ok || !w.drawable(&ch.SurfaceContent) {
172+ continue
170 }173 }
174+ covers = append(covers, dst)
171 }175 }
172 return covers176 return covers
173 }177 }
174 178
179+// drawable reports whether the child's buffer will draw. GPU buffer
180+// imports are cached, so pre-importing here costs nothing.
181+func (w *sceneWalk) drawable(c *ports.SurfaceContent) bool {
182+ if c.DMABuf != nil {
183+ _, err := w.r.importDMABuf(c.DMABuf)
184+ return err == nil
185+ }
186+ b := c.SHM
187+ if b == nil {
188+ return false
189+ }
190+ _, err := w.r.shmPixels(b, b.Offset+(c.Height-1)*b.Stride+c.Width*4)
191+ return err == nil
192+}
193+
175 // surfaceRects gives the physical rect of a surface buffer with its origin194 // surfaceRects gives the physical rect of a surface buffer with its origin
176 // at (x, y) logical, and the part of it clip (logical) shows. ok is false195 // at (x, y) logical, and the part of it clip (logical) shows. ok is false
177 // when there is nothing to draw.196 // when there is nothing to draw.

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 23

Tags 10

↑↓ moveenter openesc close