perf(vulkan): compare separator lists index by index before counting
sceneDelta counted every separator in both lists (O(S²) per frame). Equal lists are one slices.Equal; same-length lists (core emits them in layout order) only count the pairs that differ by index; the full multiset count remains for lists of different lengths. Same damage as before. The alloc test runs with 80 lines, and with a dropped line.
- commit
- dfa298b9c0cc5a68c6dfc4b93f3a66f1bb2d0933
- author
- brice · · 2026-10-05 01:55 UTC
- parent
- 6ab8757
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +70 -12
M internal/adapters/vulkan/damage.go +39 -9
| @@ -253,21 +253,51 @@ func sceneDelta(old, cur ports.Scene, phys func(ports.Rect) image.Rectangle, lim | |||
| 253 | 253 | area = area.Union(phys(a.Rect)).Union(phys(b.Rect)) | |
| 254 | 254 | } | |
| 255 | 255 | } | |
| 256 | - | // Separators: a line in one list more often than in the other. | |
| 257 | - | for _, sep := range old.Separators { | |
| 258 | - | if countSeparator(old.Separators, sep) != countSeparator(cur.Separators, sep) { | |
| 259 | - | area = area.Union(phys(sep.Rect)) | |
| 256 | + | area = area.Union(separatorDelta(old.Separators, cur.Separators, phys)) | |
| 257 | + | if area.Dx()*area.Dy() > limit { | |
| 258 | + | return image.Rectangle{}, false | |
| 259 | + | } | |
| 260 | + | return area, true | |
| 261 | + | } | |
| 262 | + | ||
| 263 | + | // separatorDelta is the region where the separator lists differ: a line in | |
| 264 | + | // one list more often than in the other. Core builds the lists in layout | |
| 265 | + | // order (one pass over the placements, sides in a fixed order), so between | |
| 266 | + | // two frames of the same windows they are index-aligned: equal lists cost | |
| 267 | + | // one comparison, and with lists of the same length only the pairs that | |
| 268 | + | // differ by index are counted in the other list (a focus change lights a | |
| 269 | + | // few lines, not all). Lists of different lengths (a line scrolled on or | |
| 270 | + | // off) count every line. | |
| 271 | + | func separatorDelta(old, cur []ports.Separator, phys func(ports.Rect) image.Rectangle) image.Rectangle { | |
| 272 | + | var area image.Rectangle | |
| 273 | + | if slices.Equal(old, cur) { | |
| 274 | + | return area | |
| 275 | + | } | |
| 276 | + | if len(old) == len(cur) { | |
| 277 | + | for i := range old { | |
| 278 | + | if old[i] == cur[i] { | |
| 279 | + | continue | |
| 280 | + | } | |
| 281 | + | if countSeparator(old, old[i]) != countSeparator(cur, old[i]) { | |
| 282 | + | area = area.Union(phys(old[i].Rect)) | |
| 283 | + | } | |
| 284 | + | if countSeparator(old, cur[i]) != countSeparator(cur, cur[i]) { | |
| 285 | + | area = area.Union(phys(cur[i].Rect)) | |
| 286 | + | } | |
| 260 | 287 | } | |
| 288 | + | return area | |
| 261 | 289 | } | |
| 262 | - | for _, sep := range cur.Separators { | |
| 263 | - | if countSeparator(old.Separators, sep) != countSeparator(cur.Separators, sep) { | |
| 290 | + | for _, sep := range old { | |
| 291 | + | if countSeparator(old, sep) != countSeparator(cur, sep) { | |
| 264 | 292 | area = area.Union(phys(sep.Rect)) | |
| 265 | 293 | } | |
| 266 | 294 | } | |
| 267 | - | if area.Dx()*area.Dy() > limit { | |
| 268 | - | return image.Rectangle{}, false | |
| 295 | + | for _, sep := range cur { | |
| 296 | + | if countSeparator(old, sep) != countSeparator(cur, sep) { | |
| 297 | + | area = area.Union(phys(sep.Rect)) | |
| 298 | + | } | |
| 269 | 299 | } | |
| 270 | - | return area, true | |
| 300 | + | return area | |
| 271 | 301 | } | |
| 272 | 302 | ||
| 273 | 303 | func countSeparator(list []ports.Separator, sep ports.Separator) int { | |
| @@ -253,21 +253,51 @@ func sceneDelta(old, cur ports.Scene, phys func(ports.Rect) image.Rectangle, lim | |||||
| 253 | area = area.Union(phys(a.Rect)).Union(phys(b.Rect)) | 253 | area = area.Union(phys(a.Rect)).Union(phys(b.Rect)) | ||
| 254 | } | 254 | } | ||
| 255 | } | 255 | } | ||
| 256 | - | // Separators: a line in one list more often than in the other. | 256 | + | area = area.Union(separatorDelta(old.Separators, cur.Separators, phys)) |
| 257 | - | for _, sep := range old.Separators { | 257 | + | if area.Dx()*area.Dy() > limit { |
| 258 | - | if countSeparator(old.Separators, sep) != countSeparator(cur.Separators, sep) { | 258 | + | return image.Rectangle{}, false |
| 259 | - | area = area.Union(phys(sep.Rect)) | 259 | + | } |
| 260 | + | return area, true | |||
| 261 | + | } | |||
| 262 | + | ||||
| 263 | + | // separatorDelta is the region where the separator lists differ: a line in | |||
| 264 | + | // one list more often than in the other. Core builds the lists in layout | |||
| 265 | + | // order (one pass over the placements, sides in a fixed order), so between | |||
| 266 | + | // two frames of the same windows they are index-aligned: equal lists cost | |||
| 267 | + | // one comparison, and with lists of the same length only the pairs that | |||
| 268 | + | // differ by index are counted in the other list (a focus change lights a | |||
| 269 | + | // few lines, not all). Lists of different lengths (a line scrolled on or | |||
| 270 | + | // off) count every line. | |||
| 271 | + | func separatorDelta(old, cur []ports.Separator, phys func(ports.Rect) image.Rectangle) image.Rectangle { | |||
| 272 | + | var area image.Rectangle | |||
| 273 | + | if slices.Equal(old, cur) { | |||
| 274 | + | return area | |||
| 275 | + | } | |||
| 276 | + | if len(old) == len(cur) { | |||
| 277 | + | for i := range old { | |||
| 278 | + | if old[i] == cur[i] { | |||
| 279 | + | continue | |||
| 280 | + | } | |||
| 281 | + | if countSeparator(old, old[i]) != countSeparator(cur, old[i]) { | |||
| 282 | + | area = area.Union(phys(old[i].Rect)) | |||
| 283 | + | } | |||
| 284 | + | if countSeparator(old, cur[i]) != countSeparator(cur, cur[i]) { | |||
| 285 | + | area = area.Union(phys(cur[i].Rect)) | |||
| 286 | + | } | |||
| 260 | } | 287 | } | ||
| 288 | + | return area | |||
| 261 | } | 289 | } | ||
| 262 | - | for _, sep := range cur.Separators { | 290 | + | for _, sep := range old { |
| 263 | - | if countSeparator(old.Separators, sep) != countSeparator(cur.Separators, sep) { | 291 | + | if countSeparator(old, sep) != countSeparator(cur, sep) { |
| 264 | area = area.Union(phys(sep.Rect)) | 292 | area = area.Union(phys(sep.Rect)) | ||
| 265 | } | 293 | } | ||
| 266 | } | 294 | } | ||
| 267 | - | if area.Dx()*area.Dy() > limit { | 295 | + | for _, sep := range cur { |
| 268 | - | return image.Rectangle{}, false | 296 | + | if countSeparator(old, sep) != countSeparator(cur, sep) { |
| 297 | + | area = area.Union(phys(sep.Rect)) | |||
| 298 | + | } | |||
| 269 | } | 299 | } | ||
| 270 | - | return area, true | 300 | + | return area |
| 271 | } | 301 | } | ||
| 272 | 302 | ||||
| 273 | func countSeparator(list []ports.Separator, sep ports.Separator) int { | 303 | func countSeparator(list []ports.Separator, sep ports.Separator) int { | ||
M internal/adapters/vulkan/damage_delta_test.go +31 -3
| @@ -150,10 +150,14 @@ func TestDamageDeltaSeparators(t *testing.T) { | |||
| 150 | 150 | } | |
| 151 | 151 | ||
| 152 | 152 | func TestSceneDeltaAllocations(t *testing.T) { | |
| 153 | + | // Many separators (twenty tiles' worth): the same-length lists are | |
| 154 | + | // compared index by index, and a list that lost a line still allocates | |
| 155 | + | // nothing. | |
| 156 | + | const seps = 80 | |
| 153 | 157 | old := deltaScene(1, win(1, 0, 0, 300, 500), win(2, 300, 0, 300, 500), win(3, 600, 0, 400, 500)) | |
| 154 | 158 | cur := deltaScene(2, win(1, 0, 0, 300, 500), win(2, 300, 0, 300, 500), win(3, 600, 0, 380, 500)) | |
| 155 | - | for i := range 6 { | |
| 156 | - | s := ports.Separator{Rect: ports.Rect{X: i * 100, W: 2, H: 500}} | |
| 159 | + | for i := range seps { | |
| 160 | + | s := ports.Separator{Rect: ports.Rect{X: i * 10, W: 2, H: 500}} | |
| 157 | 161 | old.Separators = append(old.Separators, s) | |
| 158 | 162 | cur.Separators = append(cur.Separators, s) | |
| 159 | 163 | } | |
| @@ -161,10 +165,34 @@ func TestSceneDeltaAllocations(t *testing.T) { | |||
| 161 | 165 | phys := func(r ports.Rect) image.Rectangle { return image.Rect(r.X, r.Y, r.X+r.W, r.Y+r.H) } | |
| 162 | 166 | var area image.Rectangle | |
| 163 | 167 | var ok bool | |
| 164 | - | allocs := testing.AllocsPerRun(50, func() { area, ok = sceneDelta(old, cur, phys, 450000) }) | |
| 168 | + | allocs := testing.AllocsPerRun(50, func() { area, ok = sceneDelta(old, cur, phys, 1<<30) }) | |
| 165 | 169 | require.True(t, ok) | |
| 166 | 170 | assert.False(t, area.Empty()) | |
| 171 | + | assert.True(t, image.Rect(50, 0, 52, 500).In(area), "the changed line is damaged: %v", area) | |
| 167 | 172 | assert.Zero(t, allocs, "sceneDelta must not allocate") | |
| 173 | + | ||
| 174 | + | short := cur | |
| 175 | + | short.Separators = cur.Separators[:seps-1] | |
| 176 | + | allocs = testing.AllocsPerRun(50, func() { area, ok = sceneDelta(old, short, phys, 1<<30) }) | |
| 177 | + | require.True(t, ok) | |
| 178 | + | assert.True(t, image.Rect((seps-1)*10, 0, (seps-1)*10+2, 500).In(area), "the dropped line is damaged: %v", area) | |
| 179 | + | assert.Zero(t, allocs, "sceneDelta must not allocate with lists of different lengths") | |
| 180 | + | } | |
| 181 | + | ||
| 182 | + | // Separators are damaged where a line is in one list more often than in | |
| 183 | + | // the other, whatever the order: equal lists add nothing, a changed pair | |
| 184 | + | // adds both rects, a rotated list adds nothing, a duplicated line that | |
| 185 | + | // replaces another adds both, a line added or dropped adds that line. | |
| 186 | + | func TestSceneDeltaSeparators(t *testing.T) { | |
| 187 | + | phys := func(r ports.Rect) image.Rectangle { return image.Rect(r.X, r.Y, r.X+r.W, r.Y+r.H) } | |
| 188 | + | line := func(x int) ports.Separator { return ports.Separator{Rect: ports.Rect{X: x, W: 2, H: 10}} } | |
| 189 | + | a, b, c := line(0), line(100), line(200) | |
| 190 | + | assert.True(t, separatorDelta([]ports.Separator{a, b}, []ports.Separator{a, b}, phys).Empty()) | |
| 191 | + | assert.Equal(t, image.Rect(100, 0, 202, 10), separatorDelta([]ports.Separator{a, b}, []ports.Separator{a, c}, phys)) | |
| 192 | + | assert.True(t, separatorDelta([]ports.Separator{a, b, c}, []ports.Separator{b, c, a}, phys).Empty()) | |
| 193 | + | assert.Equal(t, image.Rect(0, 0, 102, 10), separatorDelta([]ports.Separator{a, b, c}, []ports.Separator{a, a, c}, phys)) | |
| 194 | + | assert.Equal(t, image.Rect(200, 0, 202, 10), separatorDelta([]ports.Separator{a, b}, []ports.Separator{a, b, c}, phys)) | |
| 195 | + | assert.Equal(t, image.Rect(0, 0, 2, 10), separatorDelta([]ports.Separator{a, b, c}, []ports.Separator{b, c}, phys)) | |
| 168 | 196 | } | |
| 169 | 197 | ||
| 170 | 198 | // The paint order splits at the first window that opens the floats: when | |
| @@ -150,10 +150,14 @@ func TestDamageDeltaSeparators(t *testing.T) { | |||||
| 150 | } | 150 | } | ||
| 151 | 151 | ||||
| 152 | func TestSceneDeltaAllocations(t *testing.T) { | 152 | func TestSceneDeltaAllocations(t *testing.T) { | ||
| 153 | + | // Many separators (twenty tiles' worth): the same-length lists are | |||
| 154 | + | // compared index by index, and a list that lost a line still allocates | |||
| 155 | + | // nothing. | |||
| 156 | + | const seps = 80 | |||
| 153 | old := deltaScene(1, win(1, 0, 0, 300, 500), win(2, 300, 0, 300, 500), win(3, 600, 0, 400, 500)) | 157 | old := deltaScene(1, win(1, 0, 0, 300, 500), win(2, 300, 0, 300, 500), win(3, 600, 0, 400, 500)) | ||
| 154 | cur := deltaScene(2, win(1, 0, 0, 300, 500), win(2, 300, 0, 300, 500), win(3, 600, 0, 380, 500)) | 158 | cur := deltaScene(2, win(1, 0, 0, 300, 500), win(2, 300, 0, 300, 500), win(3, 600, 0, 380, 500)) | ||
| 155 | - | for i := range 6 { | 159 | + | for i := range seps { |
| 156 | - | s := ports.Separator{Rect: ports.Rect{X: i * 100, W: 2, H: 500}} | 160 | + | s := ports.Separator{Rect: ports.Rect{X: i * 10, W: 2, H: 500}} |
| 157 | old.Separators = append(old.Separators, s) | 161 | old.Separators = append(old.Separators, s) | ||
| 158 | cur.Separators = append(cur.Separators, s) | 162 | cur.Separators = append(cur.Separators, s) | ||
| 159 | } | 163 | } | ||
| @@ -161,10 +165,34 @@ func TestSceneDeltaAllocations(t *testing.T) { | |||||
| 161 | phys := func(r ports.Rect) image.Rectangle { return image.Rect(r.X, r.Y, r.X+r.W, r.Y+r.H) } | 165 | phys := func(r ports.Rect) image.Rectangle { return image.Rect(r.X, r.Y, r.X+r.W, r.Y+r.H) } | ||
| 162 | var area image.Rectangle | 166 | var area image.Rectangle | ||
| 163 | var ok bool | 167 | var ok bool | ||
| 164 | - | allocs := testing.AllocsPerRun(50, func() { area, ok = sceneDelta(old, cur, phys, 450000) }) | 168 | + | allocs := testing.AllocsPerRun(50, func() { area, ok = sceneDelta(old, cur, phys, 1<<30) }) |
| 165 | require.True(t, ok) | 169 | require.True(t, ok) | ||
| 166 | assert.False(t, area.Empty()) | 170 | assert.False(t, area.Empty()) | ||
| 171 | + | assert.True(t, image.Rect(50, 0, 52, 500).In(area), "the changed line is damaged: %v", area) | |||
| 167 | assert.Zero(t, allocs, "sceneDelta must not allocate") | 172 | assert.Zero(t, allocs, "sceneDelta must not allocate") | ||
| 173 | + | ||||
| 174 | + | short := cur | |||
| 175 | + | short.Separators = cur.Separators[:seps-1] | |||
| 176 | + | allocs = testing.AllocsPerRun(50, func() { area, ok = sceneDelta(old, short, phys, 1<<30) }) | |||
| 177 | + | require.True(t, ok) | |||
| 178 | + | assert.True(t, image.Rect((seps-1)*10, 0, (seps-1)*10+2, 500).In(area), "the dropped line is damaged: %v", area) | |||
| 179 | + | assert.Zero(t, allocs, "sceneDelta must not allocate with lists of different lengths") | |||
| 180 | + | } | |||
| 181 | + | ||||
| 182 | + | // Separators are damaged where a line is in one list more often than in | |||
| 183 | + | // the other, whatever the order: equal lists add nothing, a changed pair | |||
| 184 | + | // adds both rects, a rotated list adds nothing, a duplicated line that | |||
| 185 | + | // replaces another adds both, a line added or dropped adds that line. | |||
| 186 | + | func TestSceneDeltaSeparators(t *testing.T) { | |||
| 187 | + | phys := func(r ports.Rect) image.Rectangle { return image.Rect(r.X, r.Y, r.X+r.W, r.Y+r.H) } | |||
| 188 | + | line := func(x int) ports.Separator { return ports.Separator{Rect: ports.Rect{X: x, W: 2, H: 10}} } | |||
| 189 | + | a, b, c := line(0), line(100), line(200) | |||
| 190 | + | assert.True(t, separatorDelta([]ports.Separator{a, b}, []ports.Separator{a, b}, phys).Empty()) | |||
| 191 | + | assert.Equal(t, image.Rect(100, 0, 202, 10), separatorDelta([]ports.Separator{a, b}, []ports.Separator{a, c}, phys)) | |||
| 192 | + | assert.True(t, separatorDelta([]ports.Separator{a, b, c}, []ports.Separator{b, c, a}, phys).Empty()) | |||
| 193 | + | assert.Equal(t, image.Rect(0, 0, 102, 10), separatorDelta([]ports.Separator{a, b, c}, []ports.Separator{a, a, c}, phys)) | |||
| 194 | + | assert.Equal(t, image.Rect(200, 0, 202, 10), separatorDelta([]ports.Separator{a, b}, []ports.Separator{a, b, c}, phys)) | |||
| 195 | + | assert.Equal(t, image.Rect(0, 0, 2, 10), separatorDelta([]ports.Separator{a, b, c}, []ports.Separator{b, c}, phys)) | |||
| 168 | } | 196 | } | ||
| 169 | 197 | ||||
| 170 | // The paint order splits at the first window that opens the floats: when | 198 | // The paint order splits at the first window that opens the floats: when | ||