Merge pull request 'perf(vulkan): copy each wl_shm pixel once when damage rects overlap' (#15) from perf/cpu-control-plane into main
Reviewed-on: https://git.bnema.dev/bnema/neferwl/pulls/15
3 files · +102 -7
M internal/adapters/vulkan/renderer.go +2 -0
| @@ -83,6 +83,8 @@ type Renderer struct { | |||
| 83 | 83 | // the bytes copied into them, for tests. | |
| 84 | 84 | shm map[shmKey]*shmSurface | |
| 85 | 85 | copied int | |
| 86 | + | // shmRects is scratch for shmCopyFor. | |
| 87 | + | shmRects []image.Rectangle | |
| 86 | 88 | // marks date the frames for Trim's wall-clock eviction (trim.go). | |
| 87 | 89 | marks []trimMark | |
| 88 | 90 | // 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]*shmSurface | 84 | shm map[shmKey]*shmSurface | ||
| 85 | copied int | 85 | 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 []trimMark | 89 | 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 | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "fmt" | |
| 5 | + | "image" | |
| 5 | 6 | "runtime/debug" | |
| 6 | 7 | "unsafe" | |
| 7 | 8 | ||
| @@ -247,14 +248,15 @@ func (r *Renderer) shmCopyFor(key shmKey, st shmState, pixels []byte, offset, st | |||
| 247 | 248 | } | |
| 248 | 249 | // The client may shrink its pool under the mapping: its window then | |
| 249 | 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 | 254 | 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 | |
| 258 | 260 | copy(dst[d:d+n], pixels[s:s+n]) | |
| 259 | 261 | 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 | + | } | |
| @@ -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 then | 249 | // 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*4 | 259 | + | 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 += n | 261 | 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 | + | } | |||