detached dfa298b

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 VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · 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
253253 area = area.Union(phys(a.Rect)).Union(phys(b.Rect))
254254 }
255255 }
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+ }
260287 }
288+ return area
261289 }
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) {
264292 area = area.Union(phys(sep.Rect))
265293 }
266294 }
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+ }
269299 }
270- return area, true
300+ return area
271301 }
272302
273303 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{}, false296+ if countSeparator(old, sep) != countSeparator(cur, sep) {
297+ area = area.Union(phys(sep.Rect))
298+ }
269 }299 }
270- return area, true300+ 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) {
150150 }
151151
152152 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
153157 old := deltaScene(1, win(1, 0, 0, 300, 500), win(2, 300, 0, 300, 500), win(3, 600, 0, 400, 500))
154158 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}}
157161 old.Separators = append(old.Separators, s)
158162 cur.Separators = append(cur.Separators, s)
159163 }
@@ -161,10 +165,34 @@ func TestSceneDeltaAllocations(t *testing.T) {
161165 phys := func(r ports.Rect) image.Rectangle { return image.Rect(r.X, r.Y, r.X+r.W, r.Y+r.H) }
162166 var area image.Rectangle
163167 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) })
165169 require.True(t, ok)
166170 assert.False(t, area.Empty())
171+ assert.True(t, image.Rect(50, 0, 52, 500).In(area), "the changed line is damaged: %v", area)
167172 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))
168196 }
169197
170198 // 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.Rectangle166 var area image.Rectangle
163 var ok bool167 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: when198 // The paint order splits at the first window that opens the floats: when

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