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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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 | |||
| 431 | 431 | // The quad's mapping comes from full: only its rect shrinks. | |
| 432 | 432 | d := w.draws[n] | |
| 433 | 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 | 467 | // 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 | + | } | |
| 441 | 474 | for i, c := range covers { | |
| 442 | 475 | in := rect.Intersect(c) | |
| 443 | 476 | if in.Empty() { | |
| 444 | 477 | continue | |
| 445 | 478 | } | |
| 446 | 479 | 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) | |
| 451 | 484 | 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 | 491 | if rect.Empty() { | |
| 454 | 492 | return | |
| 455 | 493 | } | |
| @@ -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 of | 467 | // 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 | continue | 477 | 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 | return | 484 | 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 | return | 492 | return | ||
| 455 | } | 493 | } | ||
M internal/adapters/vulkan/scene_opaque_region_test.go +81 -0
| @@ -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 | + | } | |
| @@ -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 | + | } | |||