fix(vulkan): bound the damage split of wl_shm copies to a bounding box
- commit
- f958185cb8cc55c79ac5f947a2c02975d31f3ab4
- author
- brice · · 2026-10-08 19:49 UTC
- parent
- d6a3812
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +36 -1
M internal/adapters/vulkan/shm.go +15 -1
| @@ -294,11 +294,25 @@ func (r *Renderer) retireShm(s *shmSurface) { | |||
| 294 | 294 | } | |
| 295 | 295 | } | |
| 296 | 296 | ||
| 297 | + | // maxDisjointRects bounds the pieces disjointRects cuts the damage into: the | |
| 298 | + | // rects come from the client, and each cut can add three pieces. | |
| 299 | + | const maxDisjointRects = 64 | |
| 300 | + | ||
| 297 | 301 | // disjointRects appends to dst the parts of rects inside a w×h buffer, with | |
| 298 | - | // no pixel in two of them. It reuses dst's capacity. | |
| 302 | + | // no pixel in two of them. Past maxDisjointRects pieces it appends their | |
| 303 | + | // bounding box instead: some undamaged pixels are copied again. It reuses | |
| 304 | + | // dst's capacity. | |
| 299 | 305 | func disjointRects(dst []image.Rectangle, rects []ports.Rect, w, h int) []image.Rectangle { | |
| 300 | 306 | bounds := image.Rect(0, 0, w, h) | |
| 307 | + | start := len(dst) | |
| 301 | 308 | for _, rc := range rects { | |
| 309 | + | if len(dst)-start > maxDisjointRects { | |
| 310 | + | var box image.Rectangle | |
| 311 | + | for _, rc := range rects { | |
| 312 | + | box = box.Union(image.Rect(rc.X, rc.Y, rc.X+rc.W, rc.Y+rc.H).Intersect(bounds)) | |
| 313 | + | } | |
| 314 | + | return append(dst[:start], box) | |
| 315 | + | } | |
| 302 | 316 | pending := len(dst) | |
| 303 | 317 | dst = append(dst, image.Rect(rc.X, rc.Y, rc.X+rc.W, rc.Y+rc.H).Intersect(bounds)) | |
| 304 | 318 | // Cut the new rect by each earlier one; the pieces it leaves are | |
| @@ -294,11 +294,25 @@ func (r *Renderer) retireShm(s *shmSurface) { | |||||
| 294 | } | 294 | } | ||
| 295 | } | 295 | } | ||
| 296 | 296 | ||||
| 297 | + | // maxDisjointRects bounds the pieces disjointRects cuts the damage into: the | |||
| 298 | + | // rects come from the client, and each cut can add three pieces. | |||
| 299 | + | const maxDisjointRects = 64 | |||
| 300 | + | ||||
| 297 | // disjointRects appends to dst the parts of rects inside a w×h buffer, with | 301 | // disjointRects appends to dst the parts of rects inside a w×h buffer, with | ||
| 298 | - | // no pixel in two of them. It reuses dst's capacity. | 302 | + | // no pixel in two of them. Past maxDisjointRects pieces it appends their |
| 303 | + | // bounding box instead: some undamaged pixels are copied again. It reuses | |||
| 304 | + | // dst's capacity. | |||
| 299 | func disjointRects(dst []image.Rectangle, rects []ports.Rect, w, h int) []image.Rectangle { | 305 | func disjointRects(dst []image.Rectangle, rects []ports.Rect, w, h int) []image.Rectangle { | ||
| 300 | bounds := image.Rect(0, 0, w, h) | 306 | bounds := image.Rect(0, 0, w, h) | ||
| 307 | + | start := len(dst) | |||
| 301 | for _, rc := range rects { | 308 | for _, rc := range rects { | ||
| 309 | + | if len(dst)-start > maxDisjointRects { | |||
| 310 | + | var box image.Rectangle | |||
| 311 | + | for _, rc := range rects { | |||
| 312 | + | box = box.Union(image.Rect(rc.X, rc.Y, rc.X+rc.W, rc.Y+rc.H).Intersect(bounds)) | |||
| 313 | + | } | |||
| 314 | + | return append(dst[:start], box) | |||
| 315 | + | } | |||
| 302 | pending := len(dst) | 316 | pending := len(dst) | ||
| 303 | dst = append(dst, image.Rect(rc.X, rc.Y, rc.X+rc.W, rc.Y+rc.H).Intersect(bounds)) | 317 | dst = append(dst, image.Rect(rc.X, rc.Y, rc.X+rc.W, rc.Y+rc.H).Intersect(bounds)) | ||
| 304 | // Cut the new rect by each earlier one; the pieces it leaves are | 318 | // Cut the new rect by each earlier one; the pieces it leaves are | ||
M internal/adapters/vulkan/shm_rects_test.go +21 -0
| @@ -1,6 +1,7 @@ | |||
| 1 | 1 | package vulkan | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "image" | |
| 4 | 5 | "math/rand" | |
| 5 | 6 | "testing" | |
| 6 | 7 | ||
| @@ -46,6 +47,26 @@ func TestDisjointRectsCoverUnionOnce(t *testing.T) { | |||
| 46 | 47 | } | |
| 47 | 48 | } | |
| 48 | 49 | ||
| 50 | + | // Client damage that would cut into too many pieces falls back to its | |
| 51 | + | // bounding box: the work stays bounded and every damaged pixel is copied. | |
| 52 | + | func TestDisjointRectsBoundsAdversarialDamage(t *testing.T) { | |
| 53 | + | const w, h = 1024, 1024 | |
| 54 | + | rects := make([]ports.Rect, 128) | |
| 55 | + | for i := range rects { | |
| 56 | + | rects[i] = ports.Rect{X: i * 3, Y: i * 3, W: w - i*6, H: 4 + i} | |
| 57 | + | } | |
| 58 | + | got := disjointRects(nil, rects, w, h) | |
| 59 | + | if len(got) != 1 { | |
| 60 | + | t.Fatalf("%d pieces, want the bounding box", len(got)) | |
| 61 | + | } | |
| 62 | + | for _, rc := range rects { | |
| 63 | + | r := image.Rect(rc.X, rc.Y, rc.X+rc.W, rc.Y+rc.H) | |
| 64 | + | if !r.In(got[0]) { | |
| 65 | + | t.Fatalf("damage %v outside %v", r, got[0]) | |
| 66 | + | } | |
| 67 | + | } | |
| 68 | + | } | |
| 69 | + | ||
| 49 | 70 | // With a reused scratch slice the helper allocates nothing. | |
| 50 | 71 | func TestDisjointRectsNoAllocs(t *testing.T) { | |
| 51 | 72 | rects := []ports.Rect{{X: 0, Y: 0, W: 10, H: 10}, {X: 5, Y: 5, W: 10, H: 10}, {X: 2, Y: 8, W: 4, H: 4}} | |
| @@ -1,6 +1,7 @@ | |||||
| 1 | package vulkan | 1 | package vulkan | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | + | "image" | |||
| 4 | "math/rand" | 5 | "math/rand" | ||
| 5 | "testing" | 6 | "testing" | ||
| 6 | 7 | ||||
| @@ -46,6 +47,26 @@ func TestDisjointRectsCoverUnionOnce(t *testing.T) { | |||||
| 46 | } | 47 | } | ||
| 47 | } | 48 | } | ||
| 48 | 49 | ||||
| 50 | + | // Client damage that would cut into too many pieces falls back to its | |||
| 51 | + | // bounding box: the work stays bounded and every damaged pixel is copied. | |||
| 52 | + | func TestDisjointRectsBoundsAdversarialDamage(t *testing.T) { | |||
| 53 | + | const w, h = 1024, 1024 | |||
| 54 | + | rects := make([]ports.Rect, 128) | |||
| 55 | + | for i := range rects { | |||
| 56 | + | rects[i] = ports.Rect{X: i * 3, Y: i * 3, W: w - i*6, H: 4 + i} | |||
| 57 | + | } | |||
| 58 | + | got := disjointRects(nil, rects, w, h) | |||
| 59 | + | if len(got) != 1 { | |||
| 60 | + | t.Fatalf("%d pieces, want the bounding box", len(got)) | |||
| 61 | + | } | |||
| 62 | + | for _, rc := range rects { | |||
| 63 | + | r := image.Rect(rc.X, rc.Y, rc.X+rc.W, rc.Y+rc.H) | |||
| 64 | + | if !r.In(got[0]) { | |||
| 65 | + | t.Fatalf("damage %v outside %v", r, got[0]) | |||
| 66 | + | } | |||
| 67 | + | } | |||
| 68 | + | } | |||
| 69 | + | ||||
| 49 | // With a reused scratch slice the helper allocates nothing. | 70 | // With a reused scratch slice the helper allocates nothing. | ||
| 50 | func TestDisjointRectsNoAllocs(t *testing.T) { | 71 | func TestDisjointRectsNoAllocs(t *testing.T) { | ||
| 51 | rects := []ports.Rect{{X: 0, Y: 0, W: 10, H: 10}, {X: 5, Y: 5, W: 10, H: 10}, {X: 2, Y: 8, W: 4, H: 4}} | 72 | rects := []ports.Rect{{X: 0, Y: 0, W: 10, H: 10}, {X: 5, Y: 5, W: 10, H: 10}, {X: 2, Y: 8, W: 4, H: 4}} | ||