detached f21738b

perf(vulkan): bound the strips a root quad splits into around opaque covers

commit
f21738b127abd0836704711ae8eef0d68b882595
author
brice · · 2026-10-05 08:32 UTC
committer
brice · · 2026-10-05 08:32 UTC
parent
eb19005
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +126 -7
M internal/adapters/vulkan/scene.go +45 -7
@@ -431,25 +431,63 @@ func (w *sceneWalk) surface(content *ports.SurfaceContent, x, y float64, clip im
431431 // The quad's mapping comes from full: only its rect shrinks.
432432 d := w.draws[n]
433433 w.draws = w.draws[:n]
434- w.uncovered(d, dst, covers)
434+ w.split(d, dst, covers)
435435 }
436436 }
437437
438+// Splitting a quad around covers is bounded: covers come from the client, and
439+// each can cut a quad into four strips, recursively. Past these bounds the
440+// quad is drawn whole: the covers above it hide the rest, at the cost of
441+// overdraw.
442+const (
443+ maxSplitCovers = 8 // covers crossing the quad that are still split around
444+ maxSplitStrips = 32 // draws one quad may become
445+)
446+
447+// split appends d, a quad over rect, minus the covers, or the whole quad
448+// when the covers cut it into too many strips.
449+func (w *sceneWalk) split(d draw, rect image.Rectangle, covers []image.Rectangle) {
450+ crossing := 0
451+ for _, c := range covers {
452+ if !rect.Intersect(c).Empty() {
453+ crossing++
454+ }
455+ }
456+ n := len(w.draws)
457+ if crossing <= maxSplitCovers {
458+ w.uncovered(d, rect, covers, n+maxSplitStrips)
459+ if len(w.draws) <= n+maxSplitStrips {
460+ return
461+ }
462+ w.draws = w.draws[:n]
463+ }
464+ w.quad(d, rect)
465+}
466+
438467 // uncovered appends d, a quad over rect, minus the covers: the strips of
439-// rect around each cover, in turn. d keeps its mapping.
440-func (w *sceneWalk) uncovered(d draw, rect image.Rectangle, covers []image.Rectangle) {
468+// rect around each cover, in turn. d keeps its mapping. It stops once the
469+// draws pass limit, for the caller to drop them.
470+func (w *sceneWalk) uncovered(d draw, rect image.Rectangle, covers []image.Rectangle, limit int) {
471+ if len(w.draws) > limit {
472+ return
473+ }
441474 for i, c := range covers {
442475 in := rect.Intersect(c)
443476 if in.Empty() {
444477 continue
445478 }
446479 rest := covers[i+1:]
447- w.uncovered(d, image.Rect(rect.Min.X, rect.Min.Y, rect.Max.X, in.Min.Y), rest)
448- w.uncovered(d, image.Rect(rect.Min.X, in.Max.Y, rect.Max.X, rect.Max.Y), rest)
449- w.uncovered(d, image.Rect(rect.Min.X, in.Min.Y, in.Min.X, in.Max.Y), rest)
450- w.uncovered(d, image.Rect(in.Max.X, in.Min.Y, rect.Max.X, in.Max.Y), rest)
480+ w.uncovered(d, image.Rect(rect.Min.X, rect.Min.Y, rect.Max.X, in.Min.Y), rest, limit)
481+ w.uncovered(d, image.Rect(rect.Min.X, in.Max.Y, rect.Max.X, rect.Max.Y), rest, limit)
482+ w.uncovered(d, image.Rect(rect.Min.X, in.Min.Y, in.Min.X, in.Max.Y), rest, limit)
483+ w.uncovered(d, image.Rect(in.Max.X, in.Min.Y, rect.Max.X, in.Max.Y), rest, limit)
451484 return
452485 }
486+ w.quad(d, rect)
487+}
488+
489+// quad appends d over rect, unless it is empty.
490+func (w *sceneWalk) quad(d draw, rect image.Rectangle) {
453491 if rect.Empty() {
454492 return
455493 }
@@ -431,25 +431,63 @@ func (w *sceneWalk) surface(content *ports.SurfaceContent, x, y float64, clip im
431 // The quad's mapping comes from full: only its rect shrinks.431 // The quad's mapping comes from full: only its rect shrinks.
432 d := w.draws[n]432 d := w.draws[n]
433 w.draws = w.draws[:n]433 w.draws = w.draws[:n]
434- w.uncovered(d, dst, covers)434+ w.split(d, dst, covers)
435 }435 }
436 }436 }
437 437
438+// Splitting a quad around covers is bounded: covers come from the client, and
439+// each can cut a quad into four strips, recursively. Past these bounds the
440+// quad is drawn whole: the covers above it hide the rest, at the cost of
441+// overdraw.
442+const (
443+ maxSplitCovers = 8 // covers crossing the quad that are still split around
444+ maxSplitStrips = 32 // draws one quad may become
445+)
446+
447+// split appends d, a quad over rect, minus the covers, or the whole quad
448+// when the covers cut it into too many strips.
449+func (w *sceneWalk) split(d draw, rect image.Rectangle, covers []image.Rectangle) {
450+ crossing := 0
451+ for _, c := range covers {
452+ if !rect.Intersect(c).Empty() {
453+ crossing++
454+ }
455+ }
456+ n := len(w.draws)
457+ if crossing <= maxSplitCovers {
458+ w.uncovered(d, rect, covers, n+maxSplitStrips)
459+ if len(w.draws) <= n+maxSplitStrips {
460+ return
461+ }
462+ w.draws = w.draws[:n]
463+ }
464+ w.quad(d, rect)
465+}
466+
438 // uncovered appends d, a quad over rect, minus the covers: the strips of467 // uncovered appends d, a quad over rect, minus the covers: the strips of
439-// rect around each cover, in turn. d keeps its mapping.468+// rect around each cover, in turn. d keeps its mapping. It stops once the
440-func (w *sceneWalk) uncovered(d draw, rect image.Rectangle, covers []image.Rectangle) {469+// draws pass limit, for the caller to drop them.
470+func (w *sceneWalk) uncovered(d draw, rect image.Rectangle, covers []image.Rectangle, limit int) {
471+ if len(w.draws) > limit {
472+ return
473+ }
441 for i, c := range covers {474 for i, c := range covers {
442 in := rect.Intersect(c)475 in := rect.Intersect(c)
443 if in.Empty() {476 if in.Empty() {
444 continue477 continue
445 }478 }
446 rest := covers[i+1:]479 rest := covers[i+1:]
447- w.uncovered(d, image.Rect(rect.Min.X, rect.Min.Y, rect.Max.X, in.Min.Y), rest)480+ w.uncovered(d, image.Rect(rect.Min.X, rect.Min.Y, rect.Max.X, in.Min.Y), rest, limit)
448- w.uncovered(d, image.Rect(rect.Min.X, in.Max.Y, rect.Max.X, rect.Max.Y), rest)481+ w.uncovered(d, image.Rect(rect.Min.X, in.Max.Y, rect.Max.X, rect.Max.Y), rest, limit)
449- w.uncovered(d, image.Rect(rect.Min.X, in.Min.Y, in.Min.X, in.Max.Y), rest)482+ w.uncovered(d, image.Rect(rect.Min.X, in.Min.Y, in.Min.X, in.Max.Y), rest, limit)
450- w.uncovered(d, image.Rect(in.Max.X, in.Min.Y, rect.Max.X, in.Max.Y), rest)483+ w.uncovered(d, image.Rect(in.Max.X, in.Min.Y, rect.Max.X, in.Max.Y), rest, limit)
451 return484 return
452 }485 }
486+ w.quad(d, rect)
487+}
488+
489+// quad appends d over rect, unless it is empty.
490+func (w *sceneWalk) quad(d draw, rect image.Rectangle) {
453 if rect.Empty() {491 if rect.Empty() {
454 return492 return
455 }493 }
M internal/adapters/vulkan/scene_opaque_region_test.go +81 -0
@@ -249,3 +249,84 @@ func BenchmarkComposeOpaqueRegionChild(b *testing.B) {
249249 })
250250 }
251251 }
252+
253+// crossingCovers is a root of w×h pixels under n opaque 10×10 children
254+// staggered over it, so that each cover crosses the root and the others.
255+func crossingCovers(t testing.TB, w, h, n int) ports.SurfaceContent {
256+ root := solidContent(t, w, h, color.RGBA{R: 255, A: 255})
257+ root.ID, root.Surface, root.Seq, root.Version = 1, 1, 1, 1
258+ for i := range n {
259+ child := solidContent(t, 10, 10, color.RGBA{G: 255, A: 255})
260+ child.Surface, child.Version, child.Opaque = uint64(2+i), 1, true
261+ root.Children = append(root.Children, ports.Subsurface{SurfaceContent: child, X: (i * 7) % (w - 10), Y: (i * 5) % (h - 10)})
262+ }
263+ return root
264+}
265+
266+// A client chooses how many opaque subsurfaces cross its root, and each one
267+// cuts the root quad into strips: the split stops at maxSplitCovers covers
268+// or maxSplitStrips draws, and the root then draws whole (overdraw only, the
269+// covers above it still hide the rest).
270+func TestSceneSplitIsBounded(t *testing.T) {
271+ r, err := New(64, 48)
272+ if err != nil {
273+ t.Skipf("Vulkan unavailable: %v", err)
274+ }
275+ defer r.Close()
276+ const w, h = 60, 40
277+ scene := ports.Scene{Seq: 1, Scale: 1, Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: w, H: h}}}}
278+ full := image.Rect(0, 0, w, h)
279+ for _, n := range []int{1, maxSplitCovers, maxSplitCovers + 1, 40} {
280+ contents := map[ports.WindowID]ports.SurfaceContent{1: crossingCovers(t, w, h, n)}
281+ ds := r.draws(scene, contents, newDamage(r.target(), scene, image.Rect(0, 0, 64, 48)))
282+ rects := drawRects(ds)
283+ root := len(rects) - n // the children draw once each, after the root
284+ if root < 1 || root > maxSplitStrips {
285+ t.Errorf("%d covers: %d root draws, want 1..%d", n, root, maxSplitStrips)
286+ }
287+ if n > maxSplitCovers && (root != 1 || rects[0] != full) {
288+ t.Errorf("%d covers: root draws %v, want the whole quad %v", n, rects[:root], full)
289+ }
290+ if n == 1 && root < 2 {
291+ t.Errorf("one cover: %d root draws, want the quad split around it", root)
292+ }
293+ bounds := image.Rect(0, 0, 64, 48)
294+ frame := func() { _ = r.draws(scene, contents, r.frameDamage(r.target(), scene, bounds)) }
295+ frame()
296+ if allocs := testing.AllocsPerRun(20, frame); allocs != 0 {
297+ t.Errorf("%d covers: %.1f allocs/frame, want 0", n, allocs)
298+ }
299+ }
300+}
301+
302+// With the split given up on, the covers above the whole root quad still
303+// show: the window's pixels are the children's inside them and the root's
304+// elsewhere.
305+func TestRendererSplitBoundKeepsRoot(t *testing.T) {
306+ r, err := New(64, 48)
307+ if err != nil {
308+ t.Skipf("Vulkan unavailable: %v", err)
309+ }
310+ defer r.Close()
311+ const w, h, n = 60, 40, 40
312+ scene := ports.Scene{Scale: 1, Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: w, H: h}}}}
313+ root := crossingCovers(t, w, h, n)
314+ if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: root}); err != nil {
315+ t.Fatal(err)
316+ }
317+ got := readPixels(t, r)
318+ red, green := color.RGBA{R: 255, A: 255}, color.RGBA{G: 255, A: 255}
319+ for y := range h {
320+ for x := range w {
321+ want := red
322+ for _, c := range root.Children {
323+ if image.Pt(x, y).In(image.Rect(c.X, c.Y, c.X+10, c.Y+10)) {
324+ want = green
325+ }
326+ }
327+ if g := got.At(x, y); g != want {
328+ t.Fatalf("At(%d,%d)=%v want %v", x, y, g, want)
329+ }
330+ }
331+ }
332+}
@@ -249,3 +249,84 @@ func BenchmarkComposeOpaqueRegionChild(b *testing.B) {
249 })249 })
250 }250 }
251 }251 }
252+
253+// crossingCovers is a root of w×h pixels under n opaque 10×10 children
254+// staggered over it, so that each cover crosses the root and the others.
255+func crossingCovers(t testing.TB, w, h, n int) ports.SurfaceContent {
256+ root := solidContent(t, w, h, color.RGBA{R: 255, A: 255})
257+ root.ID, root.Surface, root.Seq, root.Version = 1, 1, 1, 1
258+ for i := range n {
259+ child := solidContent(t, 10, 10, color.RGBA{G: 255, A: 255})
260+ child.Surface, child.Version, child.Opaque = uint64(2+i), 1, true
261+ root.Children = append(root.Children, ports.Subsurface{SurfaceContent: child, X: (i * 7) % (w - 10), Y: (i * 5) % (h - 10)})
262+ }
263+ return root
264+}
265+
266+// A client chooses how many opaque subsurfaces cross its root, and each one
267+// cuts the root quad into strips: the split stops at maxSplitCovers covers
268+// or maxSplitStrips draws, and the root then draws whole (overdraw only, the
269+// covers above it still hide the rest).
270+func TestSceneSplitIsBounded(t *testing.T) {
271+ r, err := New(64, 48)
272+ if err != nil {
273+ t.Skipf("Vulkan unavailable: %v", err)
274+ }
275+ defer r.Close()
276+ const w, h = 60, 40
277+ scene := ports.Scene{Seq: 1, Scale: 1, Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: w, H: h}}}}
278+ full := image.Rect(0, 0, w, h)
279+ for _, n := range []int{1, maxSplitCovers, maxSplitCovers + 1, 40} {
280+ contents := map[ports.WindowID]ports.SurfaceContent{1: crossingCovers(t, w, h, n)}
281+ ds := r.draws(scene, contents, newDamage(r.target(), scene, image.Rect(0, 0, 64, 48)))
282+ rects := drawRects(ds)
283+ root := len(rects) - n // the children draw once each, after the root
284+ if root < 1 || root > maxSplitStrips {
285+ t.Errorf("%d covers: %d root draws, want 1..%d", n, root, maxSplitStrips)
286+ }
287+ if n > maxSplitCovers && (root != 1 || rects[0] != full) {
288+ t.Errorf("%d covers: root draws %v, want the whole quad %v", n, rects[:root], full)
289+ }
290+ if n == 1 && root < 2 {
291+ t.Errorf("one cover: %d root draws, want the quad split around it", root)
292+ }
293+ bounds := image.Rect(0, 0, 64, 48)
294+ frame := func() { _ = r.draws(scene, contents, r.frameDamage(r.target(), scene, bounds)) }
295+ frame()
296+ if allocs := testing.AllocsPerRun(20, frame); allocs != 0 {
297+ t.Errorf("%d covers: %.1f allocs/frame, want 0", n, allocs)
298+ }
299+ }
300+}
301+
302+// With the split given up on, the covers above the whole root quad still
303+// show: the window's pixels are the children's inside them and the root's
304+// elsewhere.
305+func TestRendererSplitBoundKeepsRoot(t *testing.T) {
306+ r, err := New(64, 48)
307+ if err != nil {
308+ t.Skipf("Vulkan unavailable: %v", err)
309+ }
310+ defer r.Close()
311+ const w, h, n = 60, 40, 40
312+ scene := ports.Scene{Scale: 1, Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: w, H: h}}}}
313+ root := crossingCovers(t, w, h, n)
314+ if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: root}); err != nil {
315+ t.Fatal(err)
316+ }
317+ got := readPixels(t, r)
318+ red, green := color.RGBA{R: 255, A: 255}, color.RGBA{G: 255, A: 255}
319+ for y := range h {
320+ for x := range w {
321+ want := red
322+ for _, c := range root.Children {
323+ if image.Pt(x, y).In(image.Rect(c.X, c.Y, c.X+10, c.Y+10)) {
324+ want = green
325+ }
326+ }
327+ if g := got.At(x, y); g != want {
328+ t.Fatalf("At(%d,%d)=%v want %v", x, y, g, want)
329+ }
330+ }
331+ }
332+}

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 26

Tags 10

↑↓ moveenter openesc close