detached f958185

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 VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · 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) {
294294 }
295295 }
296296
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+
297301 // 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.
299305 func disjointRects(dst []image.Rectangle, rects []ports.Rect, w, h int) []image.Rectangle {
300306 bounds := image.Rect(0, 0, w, h)
307+ start := len(dst)
301308 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+ }
302316 pending := len(dst)
303317 dst = append(dst, image.Rect(rc.X, rc.Y, rc.X+rc.W, rc.Y+rc.H).Intersect(bounds))
304318 // 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, with301 // 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 are318 // 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 @@
11 package vulkan
22
33 import (
4+ "image"
45 "math/rand"
56 "testing"
67
@@ -46,6 +47,26 @@ func TestDisjointRectsCoverUnionOnce(t *testing.T) {
4647 }
4748 }
4849
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+
4970 // With a reused scratch slice the helper allocates nothing.
5071 func TestDisjointRectsNoAllocs(t *testing.T) {
5172 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 vulkan1 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}}

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