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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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) { | |||
| 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 | 204 | // render draws a frame and closes its fence. | |
| 180 | 205 | func render(r *Renderer, s ports.Scene, contents map[ports.WindowID]ports.SurfaceContent) error { | |
| 181 | 206 | 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 | |||
| 157 | 157 | ||
| 158 | 158 | // opaqueChildren gives the physical rects shown by the opaque subsurfaces | |
| 159 | 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 | 163 | func (w *sceneWalk) opaqueChildren(content *ports.SurfaceContent, ox, oy int, clip image.Rectangle) []image.Rectangle { | |
| 162 | 164 | var covers []image.Rectangle | |
| 163 | 165 | for i := range content.Children { | |
| @@ -165,13 +167,30 @@ func (w *sceneWalk) opaqueChildren(content *ports.SurfaceContent, ox, oy int, cl | |||
| 165 | 167 | if ch.Below || !ch.Opaque { | |
| 166 | 168 | continue | |
| 167 | 169 | } | |
| 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 | |
| 170 | 173 | } | |
| 174 | + | covers = append(covers, dst) | |
| 171 | 175 | } | |
| 172 | 176 | 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 | 194 | // surfaceRects gives the physical rect of a surface buffer with its origin | |
| 176 | 195 | // at (x, y) logical, and the part of it clip (logical) shows. ok is false | |
| 177 | 196 | // 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 subsurfaces | 158 | // 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.Rectangle | 164 | 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 | continue | 168 | 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 covers | 176 | 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 origin | 194 | // 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 false | 195 | // 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. | ||