perf(vulkan): limit focus flash damage to its rect
4 files · +66 -16
M internal/adapters/vulkan/damage.go +8 -1
| @@ -276,7 +276,6 @@ func sceneDelta(old, cur ports.Scene, phys func(ports.Rect) image.Rectangle, lim | |||
| 276 | 276 | old.Capture != nil || cur.Capture != nil || old.CaptureScene != nil || cur.CaptureScene != nil || | |
| 277 | 277 | !slices.Equal(old.Layers, cur.Layers) || !slices.Equal(old.DropHints, cur.DropHints) || | |
| 278 | 278 | !slices.Equal(old.CaptureIndicators, cur.CaptureIndicators) || | |
| 279 | - | (old.Flash == nil) != (cur.Flash == nil) || (old.Flash != nil && *old.Flash != *cur.Flash) || | |
| 280 | 279 | len(old.Windows) != len(cur.Windows) { | |
| 281 | 280 | return image.Rectangle{}, false | |
| 282 | 281 | } | |
| @@ -287,6 +286,14 @@ func sceneDelta(old, cur ports.Scene, phys func(ports.Rect) image.Rectangle, lim | |||
| 287 | 286 | return image.Rectangle{}, false | |
| 288 | 287 | } | |
| 289 | 288 | var area image.Rectangle | |
| 289 | + | if (old.Flash == nil) != (cur.Flash == nil) || (old.Flash != nil && cur.Flash != nil && *old.Flash != *cur.Flash) { | |
| 290 | + | if old.Flash != nil { | |
| 291 | + | area = area.Union(phys(old.Flash.Rect)) | |
| 292 | + | } | |
| 293 | + | if cur.Flash != nil { | |
| 294 | + | area = area.Union(phys(cur.Flash.Rect)) | |
| 295 | + | } | |
| 296 | + | } | |
| 290 | 297 | for i := range old.Windows { | |
| 291 | 298 | a, b := old.Windows[i], cur.Windows[i] | |
| 292 | 299 | if a.ID != b.ID { | |
| @@ -276,7 +276,6 @@ func sceneDelta(old, cur ports.Scene, phys func(ports.Rect) image.Rectangle, lim | |||||
| 276 | old.Capture != nil || cur.Capture != nil || old.CaptureScene != nil || cur.CaptureScene != nil || | 276 | old.Capture != nil || cur.Capture != nil || old.CaptureScene != nil || cur.CaptureScene != nil || | ||
| 277 | !slices.Equal(old.Layers, cur.Layers) || !slices.Equal(old.DropHints, cur.DropHints) || | 277 | !slices.Equal(old.Layers, cur.Layers) || !slices.Equal(old.DropHints, cur.DropHints) || | ||
| 278 | !slices.Equal(old.CaptureIndicators, cur.CaptureIndicators) || | 278 | !slices.Equal(old.CaptureIndicators, cur.CaptureIndicators) || | ||
| 279 | - | (old.Flash == nil) != (cur.Flash == nil) || (old.Flash != nil && *old.Flash != *cur.Flash) || | |||
| 280 | len(old.Windows) != len(cur.Windows) { | 279 | len(old.Windows) != len(cur.Windows) { | ||
| 281 | return image.Rectangle{}, false | 280 | return image.Rectangle{}, false | ||
| 282 | } | 281 | } | ||
| @@ -287,6 +286,14 @@ func sceneDelta(old, cur ports.Scene, phys func(ports.Rect) image.Rectangle, lim | |||||
| 287 | return image.Rectangle{}, false | 286 | return image.Rectangle{}, false | ||
| 288 | } | 287 | } | ||
| 289 | var area image.Rectangle | 288 | var area image.Rectangle | ||
| 289 | + | if (old.Flash == nil) != (cur.Flash == nil) || (old.Flash != nil && cur.Flash != nil && *old.Flash != *cur.Flash) { | |||
| 290 | + | if old.Flash != nil { | |||
| 291 | + | area = area.Union(phys(old.Flash.Rect)) | |||
| 292 | + | } | |||
| 293 | + | if cur.Flash != nil { | |||
| 294 | + | area = area.Union(phys(cur.Flash.Rect)) | |||
| 295 | + | } | |||
| 296 | + | } | |||
| 290 | for i := range old.Windows { | 297 | for i := range old.Windows { | ||
| 291 | a, b := old.Windows[i], cur.Windows[i] | 298 | a, b := old.Windows[i], cur.Windows[i] | ||
| 292 | if a.ID != b.ID { | 299 | if a.ID != b.ID { | ||
M internal/adapters/vulkan/damage_delta_test.go +32 -0
| @@ -407,3 +407,35 @@ func TestDamageDeltaFadeAndLeavingEntry(t *testing.T) { | |||
| 407 | 407 | float.Windows[1].Floating, float.Windows[1].Fade = true, 0.1 | |
| 408 | 408 | assert.True(t, newDamage(held(gone), float, bounds).all()) | |
| 409 | 409 | } | |
| 410 | + | ||
| 411 | + | func TestSceneDeltaFlashRectDamage(t *testing.T) { | |
| 412 | + | a := &ports.FocusFlash{Rect: ports.Rect{X: 10, Y: 20, W: 100, H: 80}, Width: 3, Color: "#ff0000", Alpha: 1} | |
| 413 | + | faded := *a | |
| 414 | + | faded.Alpha = 0.5 | |
| 415 | + | moved := faded | |
| 416 | + | moved.Rect.X = 150 | |
| 417 | + | phys := func(r ports.Rect) image.Rectangle { return image.Rect(r.X*2, r.Y*2, (r.X+r.W)*2, (r.Y+r.H)*2) } | |
| 418 | + | for _, tc := range []struct { | |
| 419 | + | name string | |
| 420 | + | old, cur *ports.FocusFlash | |
| 421 | + | want image.Rectangle | |
| 422 | + | }{ | |
| 423 | + | {"appears", nil, a, phys(a.Rect)}, | |
| 424 | + | {"alpha", a, &faded, phys(a.Rect)}, | |
| 425 | + | {"moves", a, &moved, phys(a.Rect).Union(phys(moved.Rect))}, | |
| 426 | + | {"removed", a, nil, phys(a.Rect)}, | |
| 427 | + | {"idle identical", a, a, image.Rectangle{}}, | |
| 428 | + | } { | |
| 429 | + | t.Run(tc.name, func(t *testing.T) { | |
| 430 | + | old, cur := deltaScene(1), deltaScene(2) | |
| 431 | + | old.Flash, cur.Flash = tc.old, tc.cur | |
| 432 | + | area, ok := sceneDelta(old, cur, phys, 1000000) | |
| 433 | + | require.True(t, ok) | |
| 434 | + | assert.Equal(t, tc.want, area) | |
| 435 | + | }) | |
| 436 | + | } | |
| 437 | + | old, cur := deltaScene(1), deltaScene(2) | |
| 438 | + | cur.Flash = a | |
| 439 | + | _, ok := sceneDelta(old, cur, phys, 1) | |
| 440 | + | require.False(t, ok, "large damage must retain full-redraw fallback") | |
| 441 | + | } | |
| @@ -407,3 +407,35 @@ func TestDamageDeltaFadeAndLeavingEntry(t *testing.T) { | |||||
| 407 | float.Windows[1].Floating, float.Windows[1].Fade = true, 0.1 | 407 | float.Windows[1].Floating, float.Windows[1].Fade = true, 0.1 | ||
| 408 | assert.True(t, newDamage(held(gone), float, bounds).all()) | 408 | assert.True(t, newDamage(held(gone), float, bounds).all()) | ||
| 409 | } | 409 | } | ||
| 410 | + | ||||
| 411 | + | func TestSceneDeltaFlashRectDamage(t *testing.T) { | |||
| 412 | + | a := &ports.FocusFlash{Rect: ports.Rect{X: 10, Y: 20, W: 100, H: 80}, Width: 3, Color: "#ff0000", Alpha: 1} | |||
| 413 | + | faded := *a | |||
| 414 | + | faded.Alpha = 0.5 | |||
| 415 | + | moved := faded | |||
| 416 | + | moved.Rect.X = 150 | |||
| 417 | + | phys := func(r ports.Rect) image.Rectangle { return image.Rect(r.X*2, r.Y*2, (r.X+r.W)*2, (r.Y+r.H)*2) } | |||
| 418 | + | for _, tc := range []struct { | |||
| 419 | + | name string | |||
| 420 | + | old, cur *ports.FocusFlash | |||
| 421 | + | want image.Rectangle | |||
| 422 | + | }{ | |||
| 423 | + | {"appears", nil, a, phys(a.Rect)}, | |||
| 424 | + | {"alpha", a, &faded, phys(a.Rect)}, | |||
| 425 | + | {"moves", a, &moved, phys(a.Rect).Union(phys(moved.Rect))}, | |||
| 426 | + | {"removed", a, nil, phys(a.Rect)}, | |||
| 427 | + | {"idle identical", a, a, image.Rectangle{}}, | |||
| 428 | + | } { | |||
| 429 | + | t.Run(tc.name, func(t *testing.T) { | |||
| 430 | + | old, cur := deltaScene(1), deltaScene(2) | |||
| 431 | + | old.Flash, cur.Flash = tc.old, tc.cur | |||
| 432 | + | area, ok := sceneDelta(old, cur, phys, 1000000) | |||
| 433 | + | require.True(t, ok) | |||
| 434 | + | assert.Equal(t, tc.want, area) | |||
| 435 | + | }) | |||
| 436 | + | } | |||
| 437 | + | old, cur := deltaScene(1), deltaScene(2) | |||
| 438 | + | cur.Flash = a | |||
| 439 | + | _, ok := sceneDelta(old, cur, phys, 1) | |||
| 440 | + | require.False(t, ok, "large damage must retain full-redraw fallback") | |||
| 441 | + | } | |||
A internal/adapters/vulkan/flash_perf_test.go +19 -0
| @@ -0,0 +1,19 @@ | |||
| 1 | + | package vulkan | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "image" | |
| 5 | + | "testing" | |
| 6 | + | ||
| 7 | + | "github.com/bnema/neferwl/internal/ports" | |
| 8 | + | ) | |
| 9 | + | ||
| 10 | + | func BenchmarkFlashSceneWalk(b *testing.B) { | |
| 11 | + | r := &Renderer{width: 1000, height: 600} | |
| 12 | + | w := sceneWalk{r: r, s: ports.Scene{Flash: &ports.FocusFlash{Rect: ports.Rect{X: 10, Y: 10, W: 300, H: 200}, Width: 4, Color: "#ff0000", Alpha: 0.5}}, scale: 1, zoom: 1, alpha: 1, bounds: image.Rect(0, 0, 1000, 600), draws: make([]draw, 0, 4)} | |
| 13 | + | b.ReportAllocs() | |
| 14 | + | b.ResetTimer() | |
| 15 | + | for range b.N { | |
| 16 | + | w.draws = w.draws[:0] | |
| 17 | + | w.focusFlash() | |
| 18 | + | } | |
| 19 | + | } | |
| @@ -0,0 +1,19 @@ | |||||
| 1 | + | package vulkan | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "image" | |||
| 5 | + | "testing" | |||
| 6 | + | ||||
| 7 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 8 | + | ) | |||
| 9 | + | ||||
| 10 | + | func BenchmarkFlashSceneWalk(b *testing.B) { | |||
| 11 | + | r := &Renderer{width: 1000, height: 600} | |||
| 12 | + | w := sceneWalk{r: r, s: ports.Scene{Flash: &ports.FocusFlash{Rect: ports.Rect{X: 10, Y: 10, W: 300, H: 200}, Width: 4, Color: "#ff0000", Alpha: 0.5}}, scale: 1, zoom: 1, alpha: 1, bounds: image.Rect(0, 0, 1000, 600), draws: make([]draw, 0, 4)} | |||
| 13 | + | b.ReportAllocs() | |||
| 14 | + | b.ResetTimer() | |||
| 15 | + | for range b.N { | |||
| 16 | + | w.draws = w.draws[:0] | |||
| 17 | + | w.focusFlash() | |||
| 18 | + | } | |||
| 19 | + | } | |||
M internal/adapters/vulkan/scene.go +7 -15
| @@ -73,10 +73,12 @@ func (w *sceneWalk) focusFlash() { | |||
| 73 | 73 | } | |
| 74 | 74 | alpha := w.alpha | |
| 75 | 75 | w.alpha = float32(f.Alpha) | |
| 76 | - | defer func() { w.alpha = alpha }() | |
| 77 | - | t := f.Rect | |
| 78 | - | col := parseColor(f.Color) | |
| 79 | - | b := min(f.Width, t.W/2, t.H/2) | |
| 76 | + | w.innerBorder(f.Rect, min(f.Width, f.Rect.W/2, f.Rect.H/2), parseColor(f.Color)) | |
| 77 | + | w.alpha = alpha | |
| 78 | + | } | |
| 79 | + | ||
| 80 | + | // innerBorder draws four non-overlapping strips, or fills a thin target. | |
| 81 | + | func (w *sceneWalk) innerBorder(t ports.Rect, b int, col [3]uint8) { | |
| 80 | 82 | if b <= 0 { | |
| 81 | 83 | w.fill(w.physRect(t.X, t.Y, t.W, t.H), col) | |
| 82 | 84 | return | |
| @@ -107,17 +109,7 @@ func (w *sceneWalk) captureIndicators() { | |||
| 107 | 109 | w.pill(w.physRect(t.X, t.Y, t.W, t.H), col) | |
| 108 | 110 | continue | |
| 109 | 111 | } | |
| 110 | - | b := ports.CaptureBorderOf(t.W, t.H) | |
| 111 | - | if b <= 0 { | |
| 112 | - | // Too thin for a border (core inflates such marks, so this is a | |
| 113 | - | // 1-pixel output): a listed mark always draws something. | |
| 114 | - | w.fill(w.physRect(t.X, t.Y, t.W, t.H), col) | |
| 115 | - | continue | |
| 116 | - | } | |
| 117 | - | w.fill(w.physRect(t.X, t.Y, t.W, b), col) | |
| 118 | - | w.fill(w.physRect(t.X, t.Y+t.H-b, t.W, b), col) | |
| 119 | - | w.fill(w.physRect(t.X, t.Y+b, b, t.H-2*b), col) | |
| 120 | - | w.fill(w.physRect(t.X+t.W-b, t.Y+b, b, t.H-2*b), col) | |
| 112 | + | w.innerBorder(t, ports.CaptureBorderOf(t.W, t.H), col) | |
| 121 | 113 | } | |
| 122 | 114 | } | |
| 123 | 115 | ||
| @@ -73,10 +73,12 @@ func (w *sceneWalk) focusFlash() { | |||||
| 73 | } | 73 | } | ||
| 74 | alpha := w.alpha | 74 | alpha := w.alpha | ||
| 75 | w.alpha = float32(f.Alpha) | 75 | w.alpha = float32(f.Alpha) | ||
| 76 | - | defer func() { w.alpha = alpha }() | 76 | + | w.innerBorder(f.Rect, min(f.Width, f.Rect.W/2, f.Rect.H/2), parseColor(f.Color)) |
| 77 | - | t := f.Rect | 77 | + | w.alpha = alpha |
| 78 | - | col := parseColor(f.Color) | 78 | + | } |
| 79 | - | b := min(f.Width, t.W/2, t.H/2) | 79 | + | |
| 80 | + | // innerBorder draws four non-overlapping strips, or fills a thin target. | |||
| 81 | + | func (w *sceneWalk) innerBorder(t ports.Rect, b int, col [3]uint8) { | |||
| 80 | if b <= 0 { | 82 | if b <= 0 { | ||
| 81 | w.fill(w.physRect(t.X, t.Y, t.W, t.H), col) | 83 | w.fill(w.physRect(t.X, t.Y, t.W, t.H), col) | ||
| 82 | return | 84 | return | ||
| @@ -107,17 +109,7 @@ func (w *sceneWalk) captureIndicators() { | |||||
| 107 | w.pill(w.physRect(t.X, t.Y, t.W, t.H), col) | 109 | w.pill(w.physRect(t.X, t.Y, t.W, t.H), col) | ||
| 108 | continue | 110 | continue | ||
| 109 | } | 111 | } | ||
| 110 | - | b := ports.CaptureBorderOf(t.W, t.H) | 112 | + | w.innerBorder(t, ports.CaptureBorderOf(t.W, t.H), col) |
| 111 | - | if b <= 0 { | |||
| 112 | - | // Too thin for a border (core inflates such marks, so this is a | |||
| 113 | - | // 1-pixel output): a listed mark always draws something. | |||
| 114 | - | w.fill(w.physRect(t.X, t.Y, t.W, t.H), col) | |||
| 115 | - | continue | |||
| 116 | - | } | |||
| 117 | - | w.fill(w.physRect(t.X, t.Y, t.W, b), col) | |||
| 118 | - | w.fill(w.physRect(t.X, t.Y+t.H-b, t.W, b), col) | |||
| 119 | - | w.fill(w.physRect(t.X, t.Y+b, b, t.H-2*b), col) | |||
| 120 | - | w.fill(w.physRect(t.X+t.W-b, t.Y+b, b, t.H-2*b), col) | |||
| 121 | } | 113 | } | ||
| 122 | } | 114 | } | ||
| 123 | 115 | ||||