detached da55fba

perf(vulkan): copy each wl_shm pixel once when damage rects overlap

Damage accumulated since a surface's double buffer was last written is merged into disjoint rects before the copy. weston-simple-shm copied 16.7 GB for 8.7 GB of buffers; CPU over 12 s drops from 1.22 s to 0.88 s.
commit
da55fba1fc933b8e9b7977ea302b8d46982385cf
author
brice · · 2026-10-07 21:04 UTC
parent
2732965
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +102 -7
M internal/adapters/vulkan/renderer.go +2 -0
@@ -83,6 +83,8 @@ type Renderer struct {
8383 // the bytes copied into them, for tests.
8484 shm map[shmKey]*shmSurface
8585 copied int
86+ // shmRects is scratch for shmCopyFor.
87+ shmRects []image.Rectangle
8688 // marks date the frames for Trim's wall-clock eviction (trim.go).
8789 marks []trimMark
8890 // redrawn counts the target pixels drawn (TakeRedrawn).
@@ -83,6 +83,8 @@ type Renderer struct {
83 // the bytes copied into them, for tests.83 // the bytes copied into them, for tests.
84 shm map[shmKey]*shmSurface84 shm map[shmKey]*shmSurface
85 copied int85 copied int
86+ // shmRects is scratch for shmCopyFor.
87+ shmRects []image.Rectangle
86 // marks date the frames for Trim's wall-clock eviction (trim.go).88 // marks date the frames for Trim's wall-clock eviction (trim.go).
87 marks []trimMark89 marks []trimMark
88 // redrawn counts the target pixels drawn (TakeRedrawn).90 // redrawn counts the target pixels drawn (TakeRedrawn).
M internal/adapters/vulkan/shm.go +44 -7
@@ -2,6 +2,7 @@ package vulkan
22
33 import (
44 "fmt"
5+ "image"
56 "runtime/debug"
67 "unsafe"
78
@@ -247,14 +248,15 @@ func (r *Renderer) shmCopyFor(key shmKey, st shmState, pixels []byte, offset, st
247248 }
248249 // The client may shrink its pool under the mapping: its window then
249250 // shows garbage for this frame, never a crash, and is copied again.
251+ // Damage of the frames since this buffer was written can overlap: each
252+ // pixel is copied once.
253+ r.shmRects = disjointRects(r.shmRects[:0], rects, st.w, st.h)
250254 c.valid = copyGuarded(func() {
251- for _, rc := range rects {
252- x0, y0 := max(rc.X, 0), max(rc.Y, 0)
253- x1, y1 := min(rc.X+rc.W, st.w), min(rc.Y+rc.H, st.h)
254- for y := y0; y < y1; y++ {
255- d := y*row + x0*4
256- s := offset + y*stride + x0*4
257- n := (x1 - x0) * 4
255+ for _, rc := range r.shmRects {
256+ for y := rc.Min.Y; y < rc.Max.Y; y++ {
257+ d := y*row + rc.Min.X*4
258+ s := offset + y*stride + rc.Min.X*4
259+ n := rc.Dx() * 4
258260 copy(dst[d:d+n], pixels[s:s+n])
259261 r.copied += n
260262 }
@@ -291,3 +293,38 @@ func (r *Renderer) retireShm(s *shmSurface) {
291293 }
292294 }
293295 }
296+
297+// 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.
299+func disjointRects(dst []image.Rectangle, rects []ports.Rect, w, h int) []image.Rectangle {
300+ bounds := image.Rect(0, 0, w, h)
301+ for _, rc := range rects {
302+ pending := len(dst)
303+ 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
305+ // appended after it and cut in turn.
306+ for i := pending; i < len(dst); i++ {
307+ if dst[i].Empty() {
308+ dst[i] = dst[len(dst)-1]
309+ dst = dst[:len(dst)-1]
310+ i--
311+ continue
312+ }
313+ for _, o := range dst[:pending] {
314+ in := dst[i].Intersect(o)
315+ if in.Empty() {
316+ continue
317+ }
318+ r := dst[i]
319+ dst[i] = image.Rect(r.Min.X, r.Min.Y, r.Max.X, in.Min.Y)
320+ dst = append(dst,
321+ image.Rect(r.Min.X, in.Max.Y, r.Max.X, r.Max.Y),
322+ image.Rect(r.Min.X, in.Min.Y, in.Min.X, in.Max.Y),
323+ image.Rect(in.Max.X, in.Min.Y, r.Max.X, in.Max.Y))
324+ i--
325+ break
326+ }
327+ }
328+ }
329+ return dst
330+}
@@ -2,6 +2,7 @@ package vulkan
2 2
3 import (3 import (
4 "fmt"4 "fmt"
5+ "image"
5 "runtime/debug"6 "runtime/debug"
6 "unsafe"7 "unsafe"
7 8
@@ -247,14 +248,15 @@ func (r *Renderer) shmCopyFor(key shmKey, st shmState, pixels []byte, offset, st
247 }248 }
248 // The client may shrink its pool under the mapping: its window then249 // The client may shrink its pool under the mapping: its window then
249 // shows garbage for this frame, never a crash, and is copied again.250 // shows garbage for this frame, never a crash, and is copied again.
251+ // Damage of the frames since this buffer was written can overlap: each
252+ // pixel is copied once.
253+ r.shmRects = disjointRects(r.shmRects[:0], rects, st.w, st.h)
250 c.valid = copyGuarded(func() {254 c.valid = copyGuarded(func() {
251- for _, rc := range rects {255+ for _, rc := range r.shmRects {
252- x0, y0 := max(rc.X, 0), max(rc.Y, 0)256+ for y := rc.Min.Y; y < rc.Max.Y; y++ {
253- x1, y1 := min(rc.X+rc.W, st.w), min(rc.Y+rc.H, st.h)257+ d := y*row + rc.Min.X*4
254- for y := y0; y < y1; y++ {258+ s := offset + y*stride + rc.Min.X*4
255- d := y*row + x0*4259+ n := rc.Dx() * 4
256- s := offset + y*stride + x0*4
257- n := (x1 - x0) * 4
258 copy(dst[d:d+n], pixels[s:s+n])260 copy(dst[d:d+n], pixels[s:s+n])
259 r.copied += n261 r.copied += n
260 }262 }
@@ -291,3 +293,38 @@ func (r *Renderer) retireShm(s *shmSurface) {
291 }293 }
292 }294 }
293 }295 }
296+
297+// 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.
299+func disjointRects(dst []image.Rectangle, rects []ports.Rect, w, h int) []image.Rectangle {
300+ bounds := image.Rect(0, 0, w, h)
301+ for _, rc := range rects {
302+ pending := len(dst)
303+ 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
305+ // appended after it and cut in turn.
306+ for i := pending; i < len(dst); i++ {
307+ if dst[i].Empty() {
308+ dst[i] = dst[len(dst)-1]
309+ dst = dst[:len(dst)-1]
310+ i--
311+ continue
312+ }
313+ for _, o := range dst[:pending] {
314+ in := dst[i].Intersect(o)
315+ if in.Empty() {
316+ continue
317+ }
318+ r := dst[i]
319+ dst[i] = image.Rect(r.Min.X, r.Min.Y, r.Max.X, in.Min.Y)
320+ dst = append(dst,
321+ image.Rect(r.Min.X, in.Max.Y, r.Max.X, r.Max.Y),
322+ image.Rect(r.Min.X, in.Min.Y, in.Min.X, in.Max.Y),
323+ image.Rect(in.Max.X, in.Min.Y, r.Max.X, in.Max.Y))
324+ i--
325+ break
326+ }
327+ }
328+ }
329+ return dst
330+}
A internal/adapters/vulkan/shm_rects_test.go +56 -0
@@ -0,0 +1,56 @@
1+package vulkan
2+
3+import (
4+ "math/rand"
5+ "testing"
6+
7+ "github.com/bnema/neferwl/internal/ports"
8+)
9+
10+// disjointRects keeps the union of the damage, clipped to the buffer, and
11+// covers no pixel twice.
12+func TestDisjointRectsCoverUnionOnce(t *testing.T) {
13+ const w, h = 24, 16
14+ rng := rand.New(rand.NewSource(1))
15+ var scratch = disjointRects(nil, nil, w, h)
16+ for range 500 {
17+ rects := make([]ports.Rect, 1+rng.Intn(6))
18+ var want [w * h]bool
19+ for i := range rects {
20+ rects[i] = ports.Rect{X: rng.Intn(w+8) - 4, Y: rng.Intn(h+8) - 4, W: rng.Intn(14), H: rng.Intn(10)}
21+ for y := rects[i].Y; y < rects[i].Y+rects[i].H; y++ {
22+ for x := rects[i].X; x < rects[i].X+rects[i].W; x++ {
23+ if x >= 0 && x < w && y >= 0 && y < h {
24+ want[y*w+x] = true
25+ }
26+ }
27+ }
28+ }
29+ scratch = disjointRects(scratch[:0], rects, w, h)
30+ var got [w * h]int
31+ for _, r := range scratch {
32+ if r.Empty() {
33+ t.Fatalf("empty rect in %v for %v", scratch, rects)
34+ }
35+ for y := r.Min.Y; y < r.Max.Y; y++ {
36+ for x := r.Min.X; x < r.Max.X; x++ {
37+ got[y*w+x]++
38+ }
39+ }
40+ }
41+ for i := range got {
42+ if (got[i] > 0) != want[i] || got[i] > 1 {
43+ t.Fatalf("pixel %d covered %d times (want %v) for %v -> %v", i, got[i], want[i], rects, scratch)
44+ }
45+ }
46+ }
47+}
48+
49+// With a reused scratch slice the helper allocates nothing.
50+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}}
52+ scratch := disjointRects(nil, rects, 32, 32)
53+ if n := testing.AllocsPerRun(100, func() { scratch = disjointRects(scratch[:0], rects, 32, 32) }); n != 0 {
54+ t.Fatalf("%v allocs", n)
55+ }
56+}
@@ -0,0 +1,56 @@
1+package vulkan
2+
3+import (
4+ "math/rand"
5+ "testing"
6+
7+ "github.com/bnema/neferwl/internal/ports"
8+)
9+
10+// disjointRects keeps the union of the damage, clipped to the buffer, and
11+// covers no pixel twice.
12+func TestDisjointRectsCoverUnionOnce(t *testing.T) {
13+ const w, h = 24, 16
14+ rng := rand.New(rand.NewSource(1))
15+ var scratch = disjointRects(nil, nil, w, h)
16+ for range 500 {
17+ rects := make([]ports.Rect, 1+rng.Intn(6))
18+ var want [w * h]bool
19+ for i := range rects {
20+ rects[i] = ports.Rect{X: rng.Intn(w+8) - 4, Y: rng.Intn(h+8) - 4, W: rng.Intn(14), H: rng.Intn(10)}
21+ for y := rects[i].Y; y < rects[i].Y+rects[i].H; y++ {
22+ for x := rects[i].X; x < rects[i].X+rects[i].W; x++ {
23+ if x >= 0 && x < w && y >= 0 && y < h {
24+ want[y*w+x] = true
25+ }
26+ }
27+ }
28+ }
29+ scratch = disjointRects(scratch[:0], rects, w, h)
30+ var got [w * h]int
31+ for _, r := range scratch {
32+ if r.Empty() {
33+ t.Fatalf("empty rect in %v for %v", scratch, rects)
34+ }
35+ for y := r.Min.Y; y < r.Max.Y; y++ {
36+ for x := r.Min.X; x < r.Max.X; x++ {
37+ got[y*w+x]++
38+ }
39+ }
40+ }
41+ for i := range got {
42+ if (got[i] > 0) != want[i] || got[i] > 1 {
43+ t.Fatalf("pixel %d covered %d times (want %v) for %v -> %v", i, got[i], want[i], rects, scratch)
44+ }
45+ }
46+ }
47+}
48+
49+// With a reused scratch slice the helper allocates nothing.
50+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}}
52+ scratch := disjointRects(nil, rects, 32, 32)
53+ if n := testing.AllocsPerRun(100, func() { scratch = disjointRects(scratch[:0], rects, 32, 32) }); n != 0 {
54+ t.Fatalf("%v allocs", n)
55+ }
56+}

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 25

Tags 10

↑↓ moveenter openesc close