detached a126bb4

fix(vulkan): copy the held scene's slices and split full redraws at the floats start

Output adapters reuse their scene slices in place (DRM overlay frames,
capture), so a target holding them by reference compared a mutated old
scene. hold now copies Windows, Separators, Layers, DropHints and
CaptureIndicators into per-target buffers (no allocation once warm).

sceneDelta also redraws everything when the first window that opens the
floats changes, since the tile lines and the Dim veil are painted there.
Tests cover reused slices, fractional scale, a rotated output, scene and
window Dim, and the Scene field count.
commit
a126bb47f23bc736e4fe9de4ecfc719a08cc85ca
author
brice · · 2026-10-04 20:57 UTC
parent
8967242
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +209 -21
M internal/adapters/vulkan/damage.go +30 -1
@@ -189,11 +189,34 @@ func (d *damageRegion) clip(ds []draw) []draw {
189189 }
190190
191191 // hold records what the target holds after this frame: only the windows
192-// it drew.
192+// it drew, and the scene with its compared slices copied into the target's
193+// buffers (zero allocations once they are large enough): the caller may
194+// rewrite its slices in place later.
193195 func (tg *target) hold(s ports.Scene, d *damageRegion) {
196+ tg.heldWindows = append(tg.heldWindows[:0], s.Windows...)
197+ tg.heldSeparators = append(tg.heldSeparators[:0], s.Separators...)
198+ tg.heldLayers = append(tg.heldLayers[:0], s.Layers...)
199+ tg.heldHints = append(tg.heldHints[:0], s.DropHints...)
200+ tg.heldIndicators = append(tg.heldIndicators[:0], s.CaptureIndicators...)
194201 tg.valid, tg.sceneSeq, tg.scene, tg.windows = true, s.Seq, s, d.drawn
202+ tg.scene.Windows, tg.scene.Separators, tg.scene.Layers = tg.heldWindows, tg.heldSeparators, tg.heldLayers
203+ tg.scene.DropHints, tg.scene.CaptureIndicators = tg.heldHints, tg.heldIndicators
204+ // Only whether they are set matters (any capture redraws everything):
205+ // sentinels, so no pointer into the caller's capture state is kept.
206+ tg.scene.Capture, tg.scene.CaptureScene = nil, nil
207+ if s.Capture != nil {
208+ tg.scene.Capture = &heldCapture
209+ }
210+ if s.CaptureScene != nil {
211+ tg.scene.CaptureScene = &heldCaptureScene
212+ }
195213 }
196214
215+var (
216+ heldCapture ports.SceneCapture
217+ heldCaptureScene ports.Scene
218+)
219+
197220 // sceneDelta is the region (physical, through phys) where cur differs from
198221 // old, two scenes of one output; ok is false when the whole output must be
199222 // redrawn: anything but the windows and separators differs, the windows
@@ -214,6 +237,12 @@ func sceneDelta(old, cur ports.Scene, phys func(ports.Rect) image.Rectangle, lim
214237 len(old.Windows) != len(cur.Windows) {
215238 return image.Rectangle{}, false
216239 }
240+ // windows() paints the tile lines and the Dim veil under the first
241+ // window that opens the floats: when that moves, the paint order of the
242+ // whole output changed, not only the rects of the windows that differ.
243+ if floatsStart(old) != floatsStart(cur) {
244+ return image.Rectangle{}, false
245+ }
217246 var area image.Rectangle
218247 for i := range old.Windows {
219248 a, b := old.Windows[i], cur.Windows[i]
@@ -189,11 +189,34 @@ func (d *damageRegion) clip(ds []draw) []draw {
189 }189 }
190 190
191 // hold records what the target holds after this frame: only the windows191 // hold records what the target holds after this frame: only the windows
192-// it drew.192+// it drew, and the scene with its compared slices copied into the target's
193+// buffers (zero allocations once they are large enough): the caller may
194+// rewrite its slices in place later.
193 func (tg *target) hold(s ports.Scene, d *damageRegion) {195 func (tg *target) hold(s ports.Scene, d *damageRegion) {
196+ tg.heldWindows = append(tg.heldWindows[:0], s.Windows...)
197+ tg.heldSeparators = append(tg.heldSeparators[:0], s.Separators...)
198+ tg.heldLayers = append(tg.heldLayers[:0], s.Layers...)
199+ tg.heldHints = append(tg.heldHints[:0], s.DropHints...)
200+ tg.heldIndicators = append(tg.heldIndicators[:0], s.CaptureIndicators...)
194 tg.valid, tg.sceneSeq, tg.scene, tg.windows = true, s.Seq, s, d.drawn201 tg.valid, tg.sceneSeq, tg.scene, tg.windows = true, s.Seq, s, d.drawn
202+ tg.scene.Windows, tg.scene.Separators, tg.scene.Layers = tg.heldWindows, tg.heldSeparators, tg.heldLayers
203+ tg.scene.DropHints, tg.scene.CaptureIndicators = tg.heldHints, tg.heldIndicators
204+ // Only whether they are set matters (any capture redraws everything):
205+ // sentinels, so no pointer into the caller's capture state is kept.
206+ tg.scene.Capture, tg.scene.CaptureScene = nil, nil
207+ if s.Capture != nil {
208+ tg.scene.Capture = &heldCapture
209+ }
210+ if s.CaptureScene != nil {
211+ tg.scene.CaptureScene = &heldCaptureScene
212+ }
195 }213 }
196 214
215+var (
216+ heldCapture ports.SceneCapture
217+ heldCaptureScene ports.Scene
218+)
219+
197 // sceneDelta is the region (physical, through phys) where cur differs from220 // sceneDelta is the region (physical, through phys) where cur differs from
198 // old, two scenes of one output; ok is false when the whole output must be221 // old, two scenes of one output; ok is false when the whole output must be
199 // redrawn: anything but the windows and separators differs, the windows222 // redrawn: anything but the windows and separators differs, the windows
@@ -214,6 +237,12 @@ func sceneDelta(old, cur ports.Scene, phys func(ports.Rect) image.Rectangle, lim
214 len(old.Windows) != len(cur.Windows) {237 len(old.Windows) != len(cur.Windows) {
215 return image.Rectangle{}, false238 return image.Rectangle{}, false
216 }239 }
240+ // windows() paints the tile lines and the Dim veil under the first
241+ // window that opens the floats: when that moves, the paint order of the
242+ // whole output changed, not only the rects of the windows that differ.
243+ if floatsStart(old) != floatsStart(cur) {
244+ return image.Rectangle{}, false
245+ }
217 var area image.Rectangle246 var area image.Rectangle
218 for i := range old.Windows {247 for i := range old.Windows {
219 a, b := old.Windows[i], cur.Windows[i]248 a, b := old.Windows[i], cur.Windows[i]
M internal/adapters/vulkan/damage_delta_test.go +139 -11
@@ -5,6 +5,7 @@ import (
55 "image"
66 "image/color"
77 "reflect"
8+ "slices"
89 "testing"
910
1011 "github.com/stretchr/testify/assert"
@@ -13,6 +14,14 @@ import (
1314 "github.com/bnema/neferwl/internal/ports"
1415 )
1516
17+// TestSceneFieldCount fails when Scene gains or loses a field: sceneDelta
18+// lists the fields that force a full redraw.
19+func TestSceneFieldCount(t *testing.T) {
20+ const fields = 20
21+ assert.Equal(t, fields, len(reflect.VisibleFields(reflect.TypeOf(ports.Scene{}))),
22+ "Scene fields changed: check sceneDelta (a field it does not compare must force a full redraw or be covered), then this count")
23+}
24+
1625 // TestSceneWindowFieldCount fails when SceneWindow gains or loses a field:
1726 // sceneDelta compares windows by struct equality, so a new field is covered
1827 // as long as it only changes where or how that window draws (inside Rect).
@@ -158,11 +167,125 @@ func TestSceneDeltaAllocations(t *testing.T) {
158167 assert.Zero(t, allocs, "sceneDelta must not allocate")
159168 }
160169
170+// The paint order splits at the first window that opens the floats: when
171+// that window changes, the whole output is redrawn.
172+func TestSceneDeltaFloatsStartMoves(t *testing.T) {
173+ phys := func(r ports.Rect) image.Rectangle { return image.Rect(r.X, r.Y, r.X+r.W, r.Y+r.H) }
174+ tile := func(id ports.WindowID, x int) ports.SceneWindow { return win(id, x, 0, 100, 100) }
175+ float := func(id ports.WindowID, x int) ports.SceneWindow {
176+ w := win(id, x, 0, 100, 100)
177+ w.Floating = true
178+ return w
179+ }
180+ delta := func(a, b []ports.SceneWindow) bool {
181+ _, ok := sceneDelta(deltaScene(1, a...), deltaScene(2, b...), phys, 1<<30)
182+ return ok
183+ }
184+ // A tile becomes a float: the tile lines and the veil move under it.
185+ assert.False(t, delta([]ports.SceneWindow{tile(1, 0), tile(2, 100)}, []ports.SceneWindow{tile(1, 0), float(2, 100)}))
186+ // The first float moves in the list.
187+ assert.False(t, delta([]ports.SceneWindow{float(1, 0), float(2, 100)}, []ports.SceneWindow{tile(1, 0), float(2, 100)}))
188+ // A hidden or popup float opens nothing; showing it does.
189+ hidden := float(2, 100)
190+ hidden.Hidden = true
191+ assert.False(t, delta([]ports.SceneWindow{tile(1, 0), hidden}, []ports.SceneWindow{tile(1, 0), float(2, 100)}))
192+ popup := float(2, 100)
193+ popup.Popup = true
194+ assert.False(t, delta([]ports.SceneWindow{tile(1, 0), popup}, []ports.SceneWindow{tile(1, 0), float(2, 100)}))
195+ // A below float or an overview preview does not open the floats.
196+ below := float(2, 100)
197+ below.Below = true
198+ assert.True(t, delta([]ports.SceneWindow{tile(1, 0), tile(2, 100)}, []ports.SceneWindow{tile(1, 0), below}))
199+ preview := float(2, 100)
200+ preview.Preview = 0.5
201+ assert.True(t, delta([]ports.SceneWindow{tile(1, 0), tile(2, 100)}, []ports.SceneWindow{tile(1, 0), preview}))
202+ // The same first float with another rect stays a delta.
203+ assert.True(t, delta([]ports.SceneWindow{tile(1, 0), float(2, 100)}, []ports.SceneWindow{tile(1, 0), float(2, 130)}))
204+}
205+
206+// The target keeps its own copy of the compared slices: a caller reusing
207+// its slices in place (DRM overlay frames, capture) must not change what the
208+// target holds.
209+func TestTargetHoldCopiesSlices(t *testing.T) {
210+ ws := []ports.SceneWindow{win(1, 0, 0, 100, 100), win(2, 100, 0, 100, 100)}
211+ seps := []ports.Separator{{Rect: ports.Rect{X: 100, W: 2, H: 100}}}
212+ layers := []ports.SceneLayer{{ID: 9, Rect: ports.Rect{W: 10, H: 10}}}
213+ hints := []ports.Rect{{W: 5, H: 5}}
214+ marks := []ports.CaptureIndicator{{Rect: ports.Rect{W: 5, H: 5}}}
215+ s := deltaScene(1)
216+ s.Windows, s.Separators, s.Layers, s.DropHints, s.CaptureIndicators = ws, seps, layers, hints, marks
217+ tg := &target{}
218+ d := &damageRegion{drawn: map[ports.WindowID]heldWindow{}}
219+ tg.hold(s, d)
220+ ws[1].Rect.X, seps[0].Rect.X, layers[0].ID, hints[0].W, marks[0].Rect.W = 150, 150, 8, 6, 6
221+ phys := func(r ports.Rect) image.Rectangle { return image.Rect(r.X, r.Y, r.X+r.W, r.Y+r.H) }
222+ // Compared with the mutated slices as the new scene, the held copy
223+ // differs in each; with a held reference nothing would differ.
224+ cur := s
225+ cur.Seq = 2
226+ _, ok := sceneDelta(tg.scene, cur, phys, 1<<30)
227+ assert.False(t, ok, "layers, hints and indicators changed: full redraw")
228+ cur.Layers, cur.DropHints, cur.CaptureIndicators = tg.heldLayers, tg.heldHints, tg.heldIndicators
229+ area, ok := sceneDelta(tg.scene, cur, phys, 1<<30)
230+ require.True(t, ok)
231+ assert.Equal(t, image.Rect(100, 0, 250, 100), area, "window 2, old and new")
232+ // Zero allocations once the buffers are warm.
233+ allocs := testing.AllocsPerRun(50, func() { tg.hold(s, d) })
234+ assert.Zero(t, allocs, "hold must not allocate once warm")
235+}
236+
237+// deltaCase is a frame sequence of two windows whose second one resizes.
238+type deltaCase struct {
239+ name string
240+ scale float64
241+ out ports.BufferTransform
242+ sceneDim float64
243+ floating bool // window 2 is a float: the veil and tile lines go under it
244+ windowDim bool // window 1 is dimmed on some frames
245+}
246+
247+func (c deltaCase) scene(seq uint64, i, w2 int, windows []ports.SceneWindow, seps []ports.Separator) ports.Scene {
248+ windows[0], windows[1] = win(1, 0, 0, 20, 48), win(2, 20, 0, w2, 48)
249+ windows[1].Floating = c.floating
250+ if c.windowDim && i%3 == 0 {
251+ windows[0].Dim = 0.5
252+ }
253+ seps[0] = ports.Separator{Rect: ports.Rect{X: 20, W: 1, H: 48}}
254+ return ports.Scene{Seq: seq, Transform: c.out, OutputWidth: 64, OutputHeight: 48, Scale: c.scale, Background: "#101010",
255+ Dim: c.sceneDim, Windows: windows, Separators: seps}
256+}
257+
258+func deltaCases() []deltaCase {
259+ return []deltaCase{
260+ {name: "scale 1", scale: 1},
261+ {name: "scale 1.5", scale: 1.5},
262+ {name: "transform 90", scale: 1, out: 1},
263+ {name: "scene dim over tiles", scale: 1, sceneDim: 0.4, floating: true},
264+ {name: "window dim", scale: 1, windowDim: true},
265+ {name: "scale 1.5 rotated, all", scale: 1.5, out: 1, sceneDim: 0.3, floating: true, windowDim: true},
266+ }
267+}
268+
161269 // A partial redraw after a window resize is pixel-identical to a full
162-// redraw, on both targets of a double-buffered output.
270+// redraw, on both targets of a double-buffered output. With reuse the scene
271+// slices are rewritten in place for every frame, like an output adapter
272+// that reuses them: the target must not have kept a reference.
163273 func TestRendererSceneDeltaMatchesFullRedraw(t *testing.T) {
274+ for _, reuse := range []bool{false, true} {
275+ for _, tc := range deltaCases() {
276+ name := tc.name
277+ if reuse {
278+ name += " reused slices"
279+ }
280+ t.Run(name, func(t *testing.T) { sceneDeltaMatchesFullRedraw(t, tc, reuse) })
281+ }
282+ }
283+}
284+
285+func sceneDeltaMatchesFullRedraw(t *testing.T, tc deltaCase, reuse bool) {
286+ tw, th := tc.out.Size(int(64*tc.scale+0.5), int(48*tc.scale+0.5))
164287 newR := func() *Renderer {
165- r, err := New(64, 48)
288+ r, err := New(tw, th)
166289 if err != nil {
167290 t.Skipf("Vulkan unavailable: %v", err)
168291 }
@@ -184,23 +307,28 @@ func TestRendererSceneDeltaMatchesFullRedraw(t *testing.T) {
184307 green := solidContent(t, 8, 8, color.RGBA{G: 255, A: 255})
185308 green.ID, green.Surface, green.Seq, green.Version = 2, 2, 1, 1
186309 contents := map[ports.WindowID]ports.SurfaceContent{1: red, 2: green}
187- mk := func(seq uint64, w2 int) ports.Scene {
188- s := ports.Scene{Seq: seq, OutputWidth: 64, OutputHeight: 48, Scale: 1, Background: "#101010",
189- Windows: []ports.SceneWindow{win(1, 0, 0, 20, 48), win(2, 20, 0, w2, 48)}}
190- s.Separators = []ports.Separator{{Rect: ports.Rect{X: 20, W: 1, H: 48}}}
191- return s
192- }
310+ shared := make([]ports.SceneWindow, 2)
311+ sharedSeps := make([]ports.Separator, 1)
193312 var partial int
194- for i, w2 := range []int{20, 20, 30, 24, 24, 40, 20} {
195- s := mk(uint64(10+i), w2)
313+ for i, w2 := range []int{20, 20, 30, 24, 24, 40, 20, 22} {
314+ windows, seps := shared, sharedSeps
315+ if !reuse {
316+ windows, seps = make([]ports.SceneWindow, 2), make([]ports.Separator, 1)
317+ }
318+ if reuse {
319+ clear(windows)
320+ }
321+ s := tc.scene(uint64(10+i), i, w2, windows, seps)
196322 damaged.UseTarget(i % 2)
197323 before := damaged.redrawn
198324 require.NoError(t, render(damaged, s, contents))
199- if damaged.redrawn-before < 64*48 {
325+ if damaged.redrawn-before < tw*th {
200326 partial++
201327 }
202328 plain := s
203329 plain.Seq = 0
330+ plain.Windows = slices.Clone(s.Windows)
331+ plain.Separators = slices.Clone(s.Separators)
204332 require.NoError(t, render(full, plain, contents))
205333 a, b := readPixels(t, damaged), readPixels(t, full)
206334 if !bytes.Equal(a.Pix, b.Pix) {
@@ -5,6 +5,7 @@ import (
5 "image"5 "image"
6 "image/color"6 "image/color"
7 "reflect"7 "reflect"
8+ "slices"
8 "testing"9 "testing"
9 10
10 "github.com/stretchr/testify/assert"11 "github.com/stretchr/testify/assert"
@@ -13,6 +14,14 @@ import (
13 "github.com/bnema/neferwl/internal/ports"14 "github.com/bnema/neferwl/internal/ports"
14 )15 )
15 16
17+// TestSceneFieldCount fails when Scene gains or loses a field: sceneDelta
18+// lists the fields that force a full redraw.
19+func TestSceneFieldCount(t *testing.T) {
20+ const fields = 20
21+ assert.Equal(t, fields, len(reflect.VisibleFields(reflect.TypeOf(ports.Scene{}))),
22+ "Scene fields changed: check sceneDelta (a field it does not compare must force a full redraw or be covered), then this count")
23+}
24+
16 // TestSceneWindowFieldCount fails when SceneWindow gains or loses a field:25 // TestSceneWindowFieldCount fails when SceneWindow gains or loses a field:
17 // sceneDelta compares windows by struct equality, so a new field is covered26 // sceneDelta compares windows by struct equality, so a new field is covered
18 // as long as it only changes where or how that window draws (inside Rect).27 // as long as it only changes where or how that window draws (inside Rect).
@@ -158,11 +167,125 @@ func TestSceneDeltaAllocations(t *testing.T) {
158 assert.Zero(t, allocs, "sceneDelta must not allocate")167 assert.Zero(t, allocs, "sceneDelta must not allocate")
159 }168 }
160 169
170+// The paint order splits at the first window that opens the floats: when
171+// that window changes, the whole output is redrawn.
172+func TestSceneDeltaFloatsStartMoves(t *testing.T) {
173+ phys := func(r ports.Rect) image.Rectangle { return image.Rect(r.X, r.Y, r.X+r.W, r.Y+r.H) }
174+ tile := func(id ports.WindowID, x int) ports.SceneWindow { return win(id, x, 0, 100, 100) }
175+ float := func(id ports.WindowID, x int) ports.SceneWindow {
176+ w := win(id, x, 0, 100, 100)
177+ w.Floating = true
178+ return w
179+ }
180+ delta := func(a, b []ports.SceneWindow) bool {
181+ _, ok := sceneDelta(deltaScene(1, a...), deltaScene(2, b...), phys, 1<<30)
182+ return ok
183+ }
184+ // A tile becomes a float: the tile lines and the veil move under it.
185+ assert.False(t, delta([]ports.SceneWindow{tile(1, 0), tile(2, 100)}, []ports.SceneWindow{tile(1, 0), float(2, 100)}))
186+ // The first float moves in the list.
187+ assert.False(t, delta([]ports.SceneWindow{float(1, 0), float(2, 100)}, []ports.SceneWindow{tile(1, 0), float(2, 100)}))
188+ // A hidden or popup float opens nothing; showing it does.
189+ hidden := float(2, 100)
190+ hidden.Hidden = true
191+ assert.False(t, delta([]ports.SceneWindow{tile(1, 0), hidden}, []ports.SceneWindow{tile(1, 0), float(2, 100)}))
192+ popup := float(2, 100)
193+ popup.Popup = true
194+ assert.False(t, delta([]ports.SceneWindow{tile(1, 0), popup}, []ports.SceneWindow{tile(1, 0), float(2, 100)}))
195+ // A below float or an overview preview does not open the floats.
196+ below := float(2, 100)
197+ below.Below = true
198+ assert.True(t, delta([]ports.SceneWindow{tile(1, 0), tile(2, 100)}, []ports.SceneWindow{tile(1, 0), below}))
199+ preview := float(2, 100)
200+ preview.Preview = 0.5
201+ assert.True(t, delta([]ports.SceneWindow{tile(1, 0), tile(2, 100)}, []ports.SceneWindow{tile(1, 0), preview}))
202+ // The same first float with another rect stays a delta.
203+ assert.True(t, delta([]ports.SceneWindow{tile(1, 0), float(2, 100)}, []ports.SceneWindow{tile(1, 0), float(2, 130)}))
204+}
205+
206+// The target keeps its own copy of the compared slices: a caller reusing
207+// its slices in place (DRM overlay frames, capture) must not change what the
208+// target holds.
209+func TestTargetHoldCopiesSlices(t *testing.T) {
210+ ws := []ports.SceneWindow{win(1, 0, 0, 100, 100), win(2, 100, 0, 100, 100)}
211+ seps := []ports.Separator{{Rect: ports.Rect{X: 100, W: 2, H: 100}}}
212+ layers := []ports.SceneLayer{{ID: 9, Rect: ports.Rect{W: 10, H: 10}}}
213+ hints := []ports.Rect{{W: 5, H: 5}}
214+ marks := []ports.CaptureIndicator{{Rect: ports.Rect{W: 5, H: 5}}}
215+ s := deltaScene(1)
216+ s.Windows, s.Separators, s.Layers, s.DropHints, s.CaptureIndicators = ws, seps, layers, hints, marks
217+ tg := &target{}
218+ d := &damageRegion{drawn: map[ports.WindowID]heldWindow{}}
219+ tg.hold(s, d)
220+ ws[1].Rect.X, seps[0].Rect.X, layers[0].ID, hints[0].W, marks[0].Rect.W = 150, 150, 8, 6, 6
221+ phys := func(r ports.Rect) image.Rectangle { return image.Rect(r.X, r.Y, r.X+r.W, r.Y+r.H) }
222+ // Compared with the mutated slices as the new scene, the held copy
223+ // differs in each; with a held reference nothing would differ.
224+ cur := s
225+ cur.Seq = 2
226+ _, ok := sceneDelta(tg.scene, cur, phys, 1<<30)
227+ assert.False(t, ok, "layers, hints and indicators changed: full redraw")
228+ cur.Layers, cur.DropHints, cur.CaptureIndicators = tg.heldLayers, tg.heldHints, tg.heldIndicators
229+ area, ok := sceneDelta(tg.scene, cur, phys, 1<<30)
230+ require.True(t, ok)
231+ assert.Equal(t, image.Rect(100, 0, 250, 100), area, "window 2, old and new")
232+ // Zero allocations once the buffers are warm.
233+ allocs := testing.AllocsPerRun(50, func() { tg.hold(s, d) })
234+ assert.Zero(t, allocs, "hold must not allocate once warm")
235+}
236+
237+// deltaCase is a frame sequence of two windows whose second one resizes.
238+type deltaCase struct {
239+ name string
240+ scale float64
241+ out ports.BufferTransform
242+ sceneDim float64
243+ floating bool // window 2 is a float: the veil and tile lines go under it
244+ windowDim bool // window 1 is dimmed on some frames
245+}
246+
247+func (c deltaCase) scene(seq uint64, i, w2 int, windows []ports.SceneWindow, seps []ports.Separator) ports.Scene {
248+ windows[0], windows[1] = win(1, 0, 0, 20, 48), win(2, 20, 0, w2, 48)
249+ windows[1].Floating = c.floating
250+ if c.windowDim && i%3 == 0 {
251+ windows[0].Dim = 0.5
252+ }
253+ seps[0] = ports.Separator{Rect: ports.Rect{X: 20, W: 1, H: 48}}
254+ return ports.Scene{Seq: seq, Transform: c.out, OutputWidth: 64, OutputHeight: 48, Scale: c.scale, Background: "#101010",
255+ Dim: c.sceneDim, Windows: windows, Separators: seps}
256+}
257+
258+func deltaCases() []deltaCase {
259+ return []deltaCase{
260+ {name: "scale 1", scale: 1},
261+ {name: "scale 1.5", scale: 1.5},
262+ {name: "transform 90", scale: 1, out: 1},
263+ {name: "scene dim over tiles", scale: 1, sceneDim: 0.4, floating: true},
264+ {name: "window dim", scale: 1, windowDim: true},
265+ {name: "scale 1.5 rotated, all", scale: 1.5, out: 1, sceneDim: 0.3, floating: true, windowDim: true},
266+ }
267+}
268+
161 // A partial redraw after a window resize is pixel-identical to a full269 // A partial redraw after a window resize is pixel-identical to a full
162-// redraw, on both targets of a double-buffered output.270+// redraw, on both targets of a double-buffered output. With reuse the scene
271+// slices are rewritten in place for every frame, like an output adapter
272+// that reuses them: the target must not have kept a reference.
163 func TestRendererSceneDeltaMatchesFullRedraw(t *testing.T) {273 func TestRendererSceneDeltaMatchesFullRedraw(t *testing.T) {
274+ for _, reuse := range []bool{false, true} {
275+ for _, tc := range deltaCases() {
276+ name := tc.name
277+ if reuse {
278+ name += " reused slices"
279+ }
280+ t.Run(name, func(t *testing.T) { sceneDeltaMatchesFullRedraw(t, tc, reuse) })
281+ }
282+ }
283+}
284+
285+func sceneDeltaMatchesFullRedraw(t *testing.T, tc deltaCase, reuse bool) {
286+ tw, th := tc.out.Size(int(64*tc.scale+0.5), int(48*tc.scale+0.5))
164 newR := func() *Renderer {287 newR := func() *Renderer {
165- r, err := New(64, 48)288+ r, err := New(tw, th)
166 if err != nil {289 if err != nil {
167 t.Skipf("Vulkan unavailable: %v", err)290 t.Skipf("Vulkan unavailable: %v", err)
168 }291 }
@@ -184,23 +307,28 @@ func TestRendererSceneDeltaMatchesFullRedraw(t *testing.T) {
184 green := solidContent(t, 8, 8, color.RGBA{G: 255, A: 255})307 green := solidContent(t, 8, 8, color.RGBA{G: 255, A: 255})
185 green.ID, green.Surface, green.Seq, green.Version = 2, 2, 1, 1308 green.ID, green.Surface, green.Seq, green.Version = 2, 2, 1, 1
186 contents := map[ports.WindowID]ports.SurfaceContent{1: red, 2: green}309 contents := map[ports.WindowID]ports.SurfaceContent{1: red, 2: green}
187- mk := func(seq uint64, w2 int) ports.Scene {310+ shared := make([]ports.SceneWindow, 2)
188- s := ports.Scene{Seq: seq, OutputWidth: 64, OutputHeight: 48, Scale: 1, Background: "#101010",311+ sharedSeps := make([]ports.Separator, 1)
189- Windows: []ports.SceneWindow{win(1, 0, 0, 20, 48), win(2, 20, 0, w2, 48)}}
190- s.Separators = []ports.Separator{{Rect: ports.Rect{X: 20, W: 1, H: 48}}}
191- return s
192- }
193 var partial int312 var partial int
194- for i, w2 := range []int{20, 20, 30, 24, 24, 40, 20} {313+ for i, w2 := range []int{20, 20, 30, 24, 24, 40, 20, 22} {
195- s := mk(uint64(10+i), w2)314+ windows, seps := shared, sharedSeps
315+ if !reuse {
316+ windows, seps = make([]ports.SceneWindow, 2), make([]ports.Separator, 1)
317+ }
318+ if reuse {
319+ clear(windows)
320+ }
321+ s := tc.scene(uint64(10+i), i, w2, windows, seps)
196 damaged.UseTarget(i % 2)322 damaged.UseTarget(i % 2)
197 before := damaged.redrawn323 before := damaged.redrawn
198 require.NoError(t, render(damaged, s, contents))324 require.NoError(t, render(damaged, s, contents))
199- if damaged.redrawn-before < 64*48 {325+ if damaged.redrawn-before < tw*th {
200 partial++326 partial++
201 }327 }
202 plain := s328 plain := s
203 plain.Seq = 0329 plain.Seq = 0
330+ plain.Windows = slices.Clone(s.Windows)
331+ plain.Separators = slices.Clone(s.Separators)
204 require.NoError(t, render(full, plain, contents))332 require.NoError(t, render(full, plain, contents))
205 a, b := readPixels(t, damaged), readPixels(t, full)333 a, b := readPixels(t, damaged), readPixels(t, full)
206 if !bytes.Equal(a.Pix, b.Pix) {334 if !bytes.Equal(a.Pix, b.Pix) {
M internal/adapters/vulkan/scene.go +26 -2
@@ -161,6 +161,30 @@ func (w *sceneWalk) layers(afterWindows bool) {
161161 }
162162 }
163163
164+// drawsAsWindow reports whether windows() paints win as a window (not
165+// hidden, a popup or empty).
166+func drawsAsWindow(win ports.SceneWindow) bool {
167+ return !win.Hidden && !win.Popup && win.Rect.W > 0 && win.Rect.H > 0
168+}
169+
170+// opensFloats reports whether win, once reached, closes the tiles: the tile
171+// lines and the dim veil are painted under it (an overview preview of a
172+// float is not one).
173+func opensFloats(win ports.SceneWindow) bool {
174+ return !win.Below && win.Floating && win.Preview == 0
175+}
176+
177+// floatsStart is the index of the first window that opens the floats in s,
178+// or -1 when none does: where windows() splits its paint order.
179+func floatsStart(s ports.Scene) int {
180+ for i, win := range s.Windows {
181+ if drawsAsWindow(win) && opensFloats(win) {
182+ return i
183+ }
184+ }
185+ return -1
186+}
187+
164188 // windows draws ordered placements with borders. Tile lines go after
165189 // the column group, before the first float above it; an overview preview
166190 // of a float is not one. The veil goes under that float, or under every
@@ -171,10 +195,10 @@ func (w *sceneWalk) windows() {
171195 }
172196 tileLines := false
173197 for _, win := range w.s.Windows {
174- if win.Hidden || win.Popup || win.Rect.W <= 0 || win.Rect.H <= 0 {
198+ if !drawsAsWindow(win) {
175199 continue
176200 }
177- if !win.Below && win.Floating && win.Preview == 0 && !tileLines {
201+ if opensFloats(win) && !tileLines {
178202 w.separators(0)
179203 tileLines = true
180204 if w.s.Dim > 0 && !w.s.DimBehind {
@@ -161,6 +161,30 @@ func (w *sceneWalk) layers(afterWindows bool) {
161 }161 }
162 }162 }
163 163
164+// drawsAsWindow reports whether windows() paints win as a window (not
165+// hidden, a popup or empty).
166+func drawsAsWindow(win ports.SceneWindow) bool {
167+ return !win.Hidden && !win.Popup && win.Rect.W > 0 && win.Rect.H > 0
168+}
169+
170+// opensFloats reports whether win, once reached, closes the tiles: the tile
171+// lines and the dim veil are painted under it (an overview preview of a
172+// float is not one).
173+func opensFloats(win ports.SceneWindow) bool {
174+ return !win.Below && win.Floating && win.Preview == 0
175+}
176+
177+// floatsStart is the index of the first window that opens the floats in s,
178+// or -1 when none does: where windows() splits its paint order.
179+func floatsStart(s ports.Scene) int {
180+ for i, win := range s.Windows {
181+ if drawsAsWindow(win) && opensFloats(win) {
182+ return i
183+ }
184+ }
185+ return -1
186+}
187+
164 // windows draws ordered placements with borders. Tile lines go after188 // windows draws ordered placements with borders. Tile lines go after
165 // the column group, before the first float above it; an overview preview189 // the column group, before the first float above it; an overview preview
166 // of a float is not one. The veil goes under that float, or under every190 // of a float is not one. The veil goes under that float, or under every
@@ -171,10 +195,10 @@ func (w *sceneWalk) windows() {
171 }195 }
172 tileLines := false196 tileLines := false
173 for _, win := range w.s.Windows {197 for _, win := range w.s.Windows {
174- if win.Hidden || win.Popup || win.Rect.W <= 0 || win.Rect.H <= 0 {198+ if !drawsAsWindow(win) {
175 continue199 continue
176 }200 }
177- if !win.Below && win.Floating && win.Preview == 0 && !tileLines {201+ if opensFloats(win) && !tileLines {
178 w.separators(0)202 w.separators(0)
179 tileLines = true203 tileLines = true
180 if w.s.Dim > 0 && !w.s.DimBehind {204 if w.s.Dim > 0 && !w.s.DimBehind {
M internal/adapters/vulkan/targets.go +14 -7
@@ -31,13 +31,20 @@ type target struct {
3131 // exported images are shared with the display: ownership goes to the
3232 // foreign queue family after each frame, and KMS reads them.
3333 exported bool
34- // What the image holds (damage.go): the scene (an immutable snapshot,
35- // held by reference), its Seq and each window's content Seq; valid
36- // once a frame was drawn whole.
37- valid bool
38- sceneSeq uint64
39- scene ports.Scene
40- windows map[ports.WindowID]heldWindow
34+ // What the image holds (damage.go): the scene, its Seq and each
35+ // window's content Seq; valid once a frame was drawn whole. Scenes are
36+ // immutable for core, but output adapters reuse their slices in place
37+ // (DRM overlay frames, capture), so the compared slices of scene point
38+ // into the target's own buffers, copied by hold.
39+ valid bool
40+ sceneSeq uint64
41+ scene ports.Scene
42+ heldWindows []ports.SceneWindow
43+ heldSeparators []ports.Separator
44+ heldLayers []ports.SceneLayer
45+ heldHints []ports.Rect
46+ heldIndicators []ports.CaptureIndicator
47+ windows map[ports.WindowID]heldWindow
4148 }
4249
4350 // fourccXRGB is DRM_FORMAT_XRGB8888: B8G8R8A8 in memory, alpha ignored.
@@ -31,13 +31,20 @@ type target struct {
31 // exported images are shared with the display: ownership goes to the31 // exported images are shared with the display: ownership goes to the
32 // foreign queue family after each frame, and KMS reads them.32 // foreign queue family after each frame, and KMS reads them.
33 exported bool33 exported bool
34- // What the image holds (damage.go): the scene (an immutable snapshot,34+ // What the image holds (damage.go): the scene, its Seq and each
35- // held by reference), its Seq and each window's content Seq; valid35+ // window's content Seq; valid once a frame was drawn whole. Scenes are
36- // once a frame was drawn whole.36+ // immutable for core, but output adapters reuse their slices in place
37- valid bool37+ // (DRM overlay frames, capture), so the compared slices of scene point
38- sceneSeq uint6438+ // into the target's own buffers, copied by hold.
39- scene ports.Scene39+ valid bool
40- windows map[ports.WindowID]heldWindow40+ sceneSeq uint64
41+ scene ports.Scene
42+ heldWindows []ports.SceneWindow
43+ heldSeparators []ports.Separator
44+ heldLayers []ports.SceneLayer
45+ heldHints []ports.Rect
46+ heldIndicators []ports.CaptureIndicator
47+ windows map[ports.WindowID]heldWindow
41 }48 }
42 49
43 // fourccXRGB is DRM_FORMAT_XRGB8888: B8G8R8A8 in memory, alpha ignored.50 // fourccXRGB is DRM_FORMAT_XRGB8888: B8G8R8A8 in memory, alpha ignored.

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