detached d251ec8

fix(drm): probe the overlay with the cursor state it remembers

testOverlay read the cursor position twice: once for the cache key and
once in the TEST_ONLY request. A move on the input goroutine in between
cached a state KMS never tested. Take one snapshot and use it for the key,
both probes and the colour verdict.

Share the overlay commit result handling (colour refusal, overlay conflict,
queued buffer) between the composed and overlay-only paths. Cover overlay
probe and commit refusals on the overlay-only path, layer content and
rewritten scene slices, and the cursor in the probe cache.
commit
d251ec83e580f98d75617bca74677d501c47aa58
author
brice · · 2026-10-14 08:00 UTC
parent
915e135
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +125 -27
M internal/adapters/drm/frame_submit.go +11 -13
@@ -219,16 +219,7 @@ func (o *Output) submitFrame(ctx context.Context, r ports.Renderer, scene ports.
219219 done.Close()
220220 }
221221 ov.close()
222- if errors.Is(err, errColorRefused) {
223- // Cached for the overlay's buffer format: the next frame composes it.
224- o.setOverlayReason("color_refused")
225- return false, errOverlayDropped
226- }
227- if err != nil && o.overlayConflict(err, ov.buf) {
228- return false, errOverlayDropped
229- }
230- if err == nil {
231- o.queued = ov.buf
222+ if err = o.finishOverlayCommit(err, ov.buf); err == nil {
232223 // The image now holds what was drawn last: composed, or the whole
233224 // scene after an overlay refusal.
234225 if ov.fb != 0 {
@@ -250,16 +241,23 @@ func (o *Output) commitOverlayOnly(front uint32, scene ports.Scene, f pendingFra
250241 }
251242 f.queued, f.zeroCopy = ov.buf, ov.id
252243 game := o.vrrProp != 0 && fullscreenShown(&scene)
253- err := o.commitWith(front, nil, false, o.wantVRR(game), f, ov)
244+ return o.finishOverlayCommit(o.commitWith(front, nil, false, o.wantVRR(game), f, ov), ov.buf)
245+}
246+
247+// finishOverlayCommit applies the result of a frame commit with buf on the
248+// overlay (0: none): a refusal the overlay causes asks for a frame without
249+// it (errOverlayDropped); a commit marks buf queued.
250+func (o *Output) finishOverlayCommit(err error, buf uint64) error {
254251 if errors.Is(err, errColorRefused) {
252+ // Cached for the overlay's buffer format: the next frame composes it.
255253 o.setOverlayReason("color_refused")
256254 return errOverlayDropped
257255 }
258- if err != nil && o.overlayConflict(err, ov.buf) {
256+ if err != nil && o.overlayConflict(err, buf) {
259257 return errOverlayDropped
260258 }
261259 if err == nil {
262- o.queued = ov.buf
260+ o.queued = buf
263261 }
264262 return err
265263 }
@@ -219,16 +219,7 @@ func (o *Output) submitFrame(ctx context.Context, r ports.Renderer, scene ports.
219 done.Close()219 done.Close()
220 }220 }
221 ov.close()221 ov.close()
222- if errors.Is(err, errColorRefused) {222+ if err = o.finishOverlayCommit(err, ov.buf); err == nil {
223- // Cached for the overlay's buffer format: the next frame composes it.
224- o.setOverlayReason("color_refused")
225- return false, errOverlayDropped
226- }
227- if err != nil && o.overlayConflict(err, ov.buf) {
228- return false, errOverlayDropped
229- }
230- if err == nil {
231- o.queued = ov.buf
232 // The image now holds what was drawn last: composed, or the whole223 // The image now holds what was drawn last: composed, or the whole
233 // scene after an overlay refusal.224 // scene after an overlay refusal.
234 if ov.fb != 0 {225 if ov.fb != 0 {
@@ -250,16 +241,23 @@ func (o *Output) commitOverlayOnly(front uint32, scene ports.Scene, f pendingFra
250 }241 }
251 f.queued, f.zeroCopy = ov.buf, ov.id242 f.queued, f.zeroCopy = ov.buf, ov.id
252 game := o.vrrProp != 0 && fullscreenShown(&scene)243 game := o.vrrProp != 0 && fullscreenShown(&scene)
253- err := o.commitWith(front, nil, false, o.wantVRR(game), f, ov)244+ return o.finishOverlayCommit(o.commitWith(front, nil, false, o.wantVRR(game), f, ov), ov.buf)
245+}
246+
247+// finishOverlayCommit applies the result of a frame commit with buf on the
248+// overlay (0: none): a refusal the overlay causes asks for a frame without
249+// it (errOverlayDropped); a commit marks buf queued.
250+func (o *Output) finishOverlayCommit(err error, buf uint64) error {
254 if errors.Is(err, errColorRefused) {251 if errors.Is(err, errColorRefused) {
252+ // Cached for the overlay's buffer format: the next frame composes it.
255 o.setOverlayReason("color_refused")253 o.setOverlayReason("color_refused")
256 return errOverlayDropped254 return errOverlayDropped
257 }255 }
258- if err != nil && o.overlayConflict(err, ov.buf) {256+ if err != nil && o.overlayConflict(err, buf) {
259 return errOverlayDropped257 return errOverlayDropped
260 }258 }
261 if err == nil {259 if err == nil {
262- o.queued = ov.buf260+ o.queued = buf
263 }261 }
264 return err262 return err
265 }263 }
M internal/adapters/drm/front_test.go +103 -5
@@ -2,6 +2,7 @@ package drm
22
33 import (
44 "context"
5+ "errors"
56 "os"
67 "sync/atomic"
78 "testing"
@@ -10,11 +11,12 @@ import (
1011 portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
1112 "github.com/bnema/neferwl/internal/ports"
1213 "github.com/stretchr/testify/mock"
14+ "golang.org/x/sys/unix"
1315 )
1416
1517 // overlayFrames submits the overlay scene once (composed), then with the
1618 // overlay window's next buffers: the frames after the first only flip the
17-// overlay buffer. It returns the windows each render drew.
19+// overlay buffer.
1820 func overlayFrames(t *testing.T, o *Output, r *portsmocks.MockRenderer, s ports.Scene, c map[ports.WindowID]ports.SurfaceContent, frames int) {
1921 t.Helper()
2022 for i := range frames {
@@ -120,7 +122,10 @@ func TestRunVideoOnOverlayDoesNotRender(t *testing.T) {
120122 o.flipped = flips
121123 r := portsmocks.NewMockRenderer(t)
122124 pipeBuf := func() ports.DMABuf {
123- f, w, _ := os.Pipe()
125+ f, w, err := os.Pipe()
126+ if err != nil {
127+ t.Fatal(err)
128+ }
124129 w.Close()
125130 return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
126131 }
@@ -260,10 +265,85 @@ func TestRunVideoOnOverlayDoesNotRender(t *testing.T) {
260265 }
261266 }
262267
268+// Refusals on the overlay-only path: a refused probe composes the whole
269+// scene at once; a refused commit drops the overlay and asks for a new frame,
270+// leaving the images and the queue as they were.
271+func TestSubmitFrameOverlayOnlyRefused(t *testing.T) {
272+ for _, tc := range []struct {
273+ name string
274+ refuse func(r *atomicReq, flags uint32) bool
275+ wantErr error
276+ renders int
277+ }{
278+ {"probe", func(_ *atomicReq, flags uint32) bool { return flags&atomicTestOnly != 0 }, nil, 1},
279+ {"commit", func(_ *atomicReq, flags uint32) bool { return flags&atomicTestOnly == 0 }, errOverlayDropped, 0},
280+ } {
281+ t.Run(tc.name, func(t *testing.T) {
282+ refusing := false
283+ o, k, _, _ := testOutputRules(t, func(r *atomicReq, flags uint32) error {
284+ if refusing && tc.refuse(r, flags) {
285+ return unix.EINVAL
286+ }
287+ return nil
288+ })
289+ props := map[string]uint32{"zpos": pZpos}
290+ for n, v := range planeProps {
291+ props[n] = v
292+ }
293+ o.primary.props, o.primary.formats = props, nil
294+ o.stray = []*plane{{id: tOverlay, typ: planeOverlay, props: props, zposValue: 2, formats: []ports.DMABufFormat{{Format: fourccXRGB}}}}
295+ o.pickOverlay()
296+ o.cursor, o.vrrProp = nil, 0
297+ k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).RunAndReturn(func(b *ports.DMABuf, _ uint32) (uint32, error) {
298+ return uint32(80 + b.ID), nil
299+ })
300+ s, c := overlayScene()
301+ r := portsmocks.NewMockRenderer(t)
302+ r.EXPECT().UseTarget(mock.Anything).Return()
303+ var drawn []int
304+ r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(s ports.Scene, _ map[ports.WindowID]ports.SurfaceContent) (*os.File, error) {
305+ drawn = append(drawn, len(s.Windows))
306+ return nil, nil
307+ })
308+ overlayFrames(t, o, r, s, c, 1) // composed, front image held
309+ back, queued := o.back, o.queued
310+ refusing = true
311+ win := c[2]
312+ buf := *win.DMABuf
313+ buf.ID, win.DMABuf, win.Seq = 20, &buf, 2
314+ c[2] = win
315+ _, err := o.submitFrame(context.Background(), r, s, c, map[ports.WindowID]uint64{}, nil, nil)
316+ if !errors.Is(err, tc.wantErr) && err != tc.wantErr {
317+ t.Fatalf("err %v, want %v", err, tc.wantErr)
318+ }
319+ if got := len(drawn) - 1; got != tc.renders {
320+ t.Fatalf("%d renders, want %d", got, tc.renders)
321+ }
322+ if tc.renders == 1 && drawn[1] != 2 {
323+ t.Fatalf("composed %d windows, want the whole scene", drawn[1])
324+ }
325+ if tc.wantErr != nil {
326+ if o.back != back || o.queued != queued || o.clientFBs[20].overlayFailed != "cursor_conflict" {
327+ t.Fatalf("back %d queued %d reason %q", o.back, o.queued, o.clientFBs[20].overlayFailed)
328+ }
329+ // The retry composes the window.
330+ refusing = false
331+ if _, err := o.submitFrame(context.Background(), r, s, c, map[ports.WindowID]uint64{}, nil, nil); err != nil {
332+ t.Fatal(err)
333+ }
334+ if drawn[len(drawn)-1] != 2 {
335+ t.Fatalf("retry composed %d windows", drawn[len(drawn)-1])
336+ }
337+ }
338+ })
339+ }
340+}
341+
263342 // A frame whose buffer KMS took before is not probed again.
264343 func TestOverlayProbeCached(t *testing.T) {
265344 o, k, commits := overlayOutput(t)
266- o.cursor = nil
345+ o.cursor.image = true
346+ o.cursor.Move(10, 10)
267347 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once()
268348 o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
269349 s, c := overlayScene()
@@ -281,8 +361,14 @@ func TestOverlayProbeCached(t *testing.T) {
281361 moved.rect.X++
282362 o.testOverlay(70, moved)
283363 o.testOverlay(71, ov)
284- if len(*commits) != 3 {
285- t.Fatalf("%d probes, want 3", len(*commits))
364+ o.cursor.Move(20, 10)
365+ o.testOverlay(70, ov)
366+ if len(*commits) != 4 {
367+ t.Fatalf("%d probes, want 4", len(*commits))
368+ }
369+ // The probe tested the cursor where the cache remembers it.
370+ if v, _ := (*commits)[3].req.value(tCursor, 14); v != 20-uint64(o.cursor.hotX) {
371+ t.Fatalf("probe cursor x %d", v)
286372 }
287373 // A modeset (VT resume) forgets them.
288374 k.EXPECT().createBlob(mock.Anything).Return(99, nil).Once()
@@ -312,6 +398,16 @@ func TestSubmitFrameOverlayOnlyNeedsSameImage(t *testing.T) {
312398 "image forgotten": func(o *Output, _ *ports.Scene, _ map[ports.WindowID]ports.SurfaceContent) {
313399 o.front.forget()
314400 },
401+ "layer content": func(_ *Output, _ *ports.Scene, c map[ports.WindowID]ports.SurfaceContent) {
402+ l := c[5]
403+ l.Seq++
404+ c[5] = l
405+ },
406+ // The scene's slices rewritten in place by their owner: what the
407+ // image holds is a copy.
408+ "scene slice rewritten": func(_ *Output, s *ports.Scene, _ map[ports.WindowID]ports.SurfaceContent) {
409+ s.Windows[0].Rect.W = 90
410+ },
315411 } {
316412 t.Run(name, func(t *testing.T) {
317413 o, k, _ := overlayOutput(t)
@@ -319,7 +415,9 @@ func TestSubmitFrameOverlayOnlyNeedsSameImage(t *testing.T) {
319415 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Maybe()
320416 o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
321417 s, c := overlayScene()
418+ s.Layers = []ports.SceneLayer{{ID: 5, Layer: ports.LayerBottom, Rect: ports.Rect{W: 10, H: 10}}}
322419 c[1] = ports.SurfaceContent{ID: 1, Seq: 1, Width: 100, Height: 100, Opaque: true, Solid: &ports.SolidColor{A: 1}}
420+ c[5] = ports.SurfaceContent{ID: 5, Seq: 1, Width: 10, Height: 10, Opaque: true, Solid: &ports.SolidColor{A: 1}}
323421 r := portsmocks.NewMockRenderer(t)
324422 r.EXPECT().UseTarget(mock.Anything).Return().Twice()
325423 var drawn []int
@@ -2,6 +2,7 @@ package drm
2 2
3 import (3 import (
4 "context"4 "context"
5+ "errors"
5 "os"6 "os"
6 "sync/atomic"7 "sync/atomic"
7 "testing"8 "testing"
@@ -10,11 +11,12 @@ import (
10 portsmocks "github.com/bnema/neferwl/internal/mocks/ports"11 portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
11 "github.com/bnema/neferwl/internal/ports"12 "github.com/bnema/neferwl/internal/ports"
12 "github.com/stretchr/testify/mock"13 "github.com/stretchr/testify/mock"
14+ "golang.org/x/sys/unix"
13 )15 )
14 16
15 // overlayFrames submits the overlay scene once (composed), then with the17 // overlayFrames submits the overlay scene once (composed), then with the
16 // overlay window's next buffers: the frames after the first only flip the18 // overlay window's next buffers: the frames after the first only flip the
17-// overlay buffer. It returns the windows each render drew.19+// overlay buffer.
18 func overlayFrames(t *testing.T, o *Output, r *portsmocks.MockRenderer, s ports.Scene, c map[ports.WindowID]ports.SurfaceContent, frames int) {20 func overlayFrames(t *testing.T, o *Output, r *portsmocks.MockRenderer, s ports.Scene, c map[ports.WindowID]ports.SurfaceContent, frames int) {
19 t.Helper()21 t.Helper()
20 for i := range frames {22 for i := range frames {
@@ -120,7 +122,10 @@ func TestRunVideoOnOverlayDoesNotRender(t *testing.T) {
120 o.flipped = flips122 o.flipped = flips
121 r := portsmocks.NewMockRenderer(t)123 r := portsmocks.NewMockRenderer(t)
122 pipeBuf := func() ports.DMABuf {124 pipeBuf := func() ports.DMABuf {
123- f, w, _ := os.Pipe()125+ f, w, err := os.Pipe()
126+ if err != nil {
127+ t.Fatal(err)
128+ }
124 w.Close()129 w.Close()
125 return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}130 return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
126 }131 }
@@ -260,10 +265,85 @@ func TestRunVideoOnOverlayDoesNotRender(t *testing.T) {
260 }265 }
261 }266 }
262 267
268+// Refusals on the overlay-only path: a refused probe composes the whole
269+// scene at once; a refused commit drops the overlay and asks for a new frame,
270+// leaving the images and the queue as they were.
271+func TestSubmitFrameOverlayOnlyRefused(t *testing.T) {
272+ for _, tc := range []struct {
273+ name string
274+ refuse func(r *atomicReq, flags uint32) bool
275+ wantErr error
276+ renders int
277+ }{
278+ {"probe", func(_ *atomicReq, flags uint32) bool { return flags&atomicTestOnly != 0 }, nil, 1},
279+ {"commit", func(_ *atomicReq, flags uint32) bool { return flags&atomicTestOnly == 0 }, errOverlayDropped, 0},
280+ } {
281+ t.Run(tc.name, func(t *testing.T) {
282+ refusing := false
283+ o, k, _, _ := testOutputRules(t, func(r *atomicReq, flags uint32) error {
284+ if refusing && tc.refuse(r, flags) {
285+ return unix.EINVAL
286+ }
287+ return nil
288+ })
289+ props := map[string]uint32{"zpos": pZpos}
290+ for n, v := range planeProps {
291+ props[n] = v
292+ }
293+ o.primary.props, o.primary.formats = props, nil
294+ o.stray = []*plane{{id: tOverlay, typ: planeOverlay, props: props, zposValue: 2, formats: []ports.DMABufFormat{{Format: fourccXRGB}}}}
295+ o.pickOverlay()
296+ o.cursor, o.vrrProp = nil, 0
297+ k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).RunAndReturn(func(b *ports.DMABuf, _ uint32) (uint32, error) {
298+ return uint32(80 + b.ID), nil
299+ })
300+ s, c := overlayScene()
301+ r := portsmocks.NewMockRenderer(t)
302+ r.EXPECT().UseTarget(mock.Anything).Return()
303+ var drawn []int
304+ r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(s ports.Scene, _ map[ports.WindowID]ports.SurfaceContent) (*os.File, error) {
305+ drawn = append(drawn, len(s.Windows))
306+ return nil, nil
307+ })
308+ overlayFrames(t, o, r, s, c, 1) // composed, front image held
309+ back, queued := o.back, o.queued
310+ refusing = true
311+ win := c[2]
312+ buf := *win.DMABuf
313+ buf.ID, win.DMABuf, win.Seq = 20, &buf, 2
314+ c[2] = win
315+ _, err := o.submitFrame(context.Background(), r, s, c, map[ports.WindowID]uint64{}, nil, nil)
316+ if !errors.Is(err, tc.wantErr) && err != tc.wantErr {
317+ t.Fatalf("err %v, want %v", err, tc.wantErr)
318+ }
319+ if got := len(drawn) - 1; got != tc.renders {
320+ t.Fatalf("%d renders, want %d", got, tc.renders)
321+ }
322+ if tc.renders == 1 && drawn[1] != 2 {
323+ t.Fatalf("composed %d windows, want the whole scene", drawn[1])
324+ }
325+ if tc.wantErr != nil {
326+ if o.back != back || o.queued != queued || o.clientFBs[20].overlayFailed != "cursor_conflict" {
327+ t.Fatalf("back %d queued %d reason %q", o.back, o.queued, o.clientFBs[20].overlayFailed)
328+ }
329+ // The retry composes the window.
330+ refusing = false
331+ if _, err := o.submitFrame(context.Background(), r, s, c, map[ports.WindowID]uint64{}, nil, nil); err != nil {
332+ t.Fatal(err)
333+ }
334+ if drawn[len(drawn)-1] != 2 {
335+ t.Fatalf("retry composed %d windows", drawn[len(drawn)-1])
336+ }
337+ }
338+ })
339+ }
340+}
341+
263 // A frame whose buffer KMS took before is not probed again.342 // A frame whose buffer KMS took before is not probed again.
264 func TestOverlayProbeCached(t *testing.T) {343 func TestOverlayProbeCached(t *testing.T) {
265 o, k, commits := overlayOutput(t)344 o, k, commits := overlayOutput(t)
266- o.cursor = nil345+ o.cursor.image = true
346+ o.cursor.Move(10, 10)
267 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once()347 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once()
268 o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}}348 o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
269 s, c := overlayScene()349 s, c := overlayScene()
@@ -281,8 +361,14 @@ func TestOverlayProbeCached(t *testing.T) {
281 moved.rect.X++361 moved.rect.X++
282 o.testOverlay(70, moved)362 o.testOverlay(70, moved)
283 o.testOverlay(71, ov)363 o.testOverlay(71, ov)
284- if len(*commits) != 3 {364+ o.cursor.Move(20, 10)
285- t.Fatalf("%d probes, want 3", len(*commits))365+ o.testOverlay(70, ov)
366+ if len(*commits) != 4 {
367+ t.Fatalf("%d probes, want 4", len(*commits))
368+ }
369+ // The probe tested the cursor where the cache remembers it.
370+ if v, _ := (*commits)[3].req.value(tCursor, 14); v != 20-uint64(o.cursor.hotX) {
371+ t.Fatalf("probe cursor x %d", v)
286 }372 }
287 // A modeset (VT resume) forgets them.373 // A modeset (VT resume) forgets them.
288 k.EXPECT().createBlob(mock.Anything).Return(99, nil).Once()374 k.EXPECT().createBlob(mock.Anything).Return(99, nil).Once()
@@ -312,6 +398,16 @@ func TestSubmitFrameOverlayOnlyNeedsSameImage(t *testing.T) {
312 "image forgotten": func(o *Output, _ *ports.Scene, _ map[ports.WindowID]ports.SurfaceContent) {398 "image forgotten": func(o *Output, _ *ports.Scene, _ map[ports.WindowID]ports.SurfaceContent) {
313 o.front.forget()399 o.front.forget()
314 },400 },
401+ "layer content": func(_ *Output, _ *ports.Scene, c map[ports.WindowID]ports.SurfaceContent) {
402+ l := c[5]
403+ l.Seq++
404+ c[5] = l
405+ },
406+ // The scene's slices rewritten in place by their owner: what the
407+ // image holds is a copy.
408+ "scene slice rewritten": func(_ *Output, s *ports.Scene, _ map[ports.WindowID]ports.SurfaceContent) {
409+ s.Windows[0].Rect.W = 90
410+ },
315 } {411 } {
316 t.Run(name, func(t *testing.T) {412 t.Run(name, func(t *testing.T) {
317 o, k, _ := overlayOutput(t)413 o, k, _ := overlayOutput(t)
@@ -319,7 +415,9 @@ func TestSubmitFrameOverlayOnlyNeedsSameImage(t *testing.T) {
319 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Maybe()415 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Maybe()
320 o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}}416 o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
321 s, c := overlayScene()417 s, c := overlayScene()
418+ s.Layers = []ports.SceneLayer{{ID: 5, Layer: ports.LayerBottom, Rect: ports.Rect{W: 10, H: 10}}}
322 c[1] = ports.SurfaceContent{ID: 1, Seq: 1, Width: 100, Height: 100, Opaque: true, Solid: &ports.SolidColor{A: 1}}419 c[1] = ports.SurfaceContent{ID: 1, Seq: 1, Width: 100, Height: 100, Opaque: true, Solid: &ports.SolidColor{A: 1}}
420+ c[5] = ports.SurfaceContent{ID: 5, Seq: 1, Width: 10, Height: 10, Opaque: true, Solid: &ports.SolidColor{A: 1}}
323 r := portsmocks.NewMockRenderer(t)421 r := portsmocks.NewMockRenderer(t)
324 r.EXPECT().UseTarget(mock.Anything).Return().Twice()422 r.EXPECT().UseTarget(mock.Anything).Return().Twice()
325 var drawn []int423 var drawn []int
M internal/adapters/drm/overlay.go +11 -9
@@ -237,18 +237,20 @@ func (ov overlayWin) close() {
237237 // cursor; the fence does not count) is not probed again: a refusal of the
238238 // commit itself still drops the overlay (overlayConflict).
239239 func (o *Output) testOverlay(fb uint32, ov overlayWin) bool {
240+ // One cursor snapshot: the input goroutine may move it meanwhile, and
241+ // what is remembered must be what KMS was asked.
240242 key := o.overlayProbeOf(fb, ov)
241243 if slices.Contains(o.overlayPassed[:], key) {
242244 return true
243245 }
244- err := o.probeOverlay(fb, ov, ov.color.mode)
246+ err := o.probeOverlay(fb, ov, ov.color.mode, key.cursor)
245247 if err == nil {
246248 o.overlayPassed[o.overlayNext] = key
247249 o.overlayNext = (o.overlayNext + 1) % len(o.overlayPassed)
248250 return true
249251 }
250- if ov.color.mode != colorBypass && refused(err) && o.probeOverlay(fb, ov, colorBypass) == nil {
251- o.overlay.setVerdict(ov.color.format, o.cursorShown(), false)
252+ if ov.color.mode != colorBypass && refused(err) && o.probeOverlay(fb, ov, colorBypass, key.cursor) == nil {
253+ o.overlay.setVerdict(ov.color.format, key.cursor.on, false)
252254 o.setOverlayReason("color_refused")
253255 o.renderLog.Info().Err(err).Uint32("format", ov.color.format).Str("connector", o.conn.name).Msg("overlay colour pipeline refused")
254256 return false
@@ -277,17 +279,17 @@ type overlayProbe struct {
277279
278280 func (o *Output) overlayProbeOf(fb uint32, ov overlayWin) overlayProbe {
279281 p := overlayProbe{primary: fb, overlay: ov.fb, rect: ov.rect, w: ov.w, h: ov.h, color: ov.color, mult: o.colorMult, ctm: o.ctmBlob}
280- if o.cursor != nil {
282+ if o.cursor != nil && !o.protected {
281283 p.cursor = o.cursor.desired()
282284 }
283285 return p
284286 }
285287
286288 // probeOverlay is a TEST_ONLY of the frame: the composed image on the
287-// primary plane (Bypass), ov on the overlay with colour mode, the cursor.
288-// Both planes' colour state is written whatever they applied: the test must
289-// not rely on the state of the last commit.
290-func (o *Output) probeOverlay(fb uint32, ov overlayWin, mode colorMode) error {
289+// primary plane (Bypass), ov on the overlay with colour mode, the cursor in
290+// state cur. Both planes' colour state is written whatever they applied:
291+// the test must not rely on the state of the last commit.
292+func (o *Output) probeOverlay(fb uint32, ov overlayWin, mode colorMode, cur cursorState) error {
291293 req := &o.probeReq // no frame request is alive: the frame commit follows
292294 req.reset()
293295 o.primaryProps(req, fb)
@@ -295,7 +297,7 @@ func (o *Output) probeOverlay(fb uint32, ov overlayWin, mode colorMode) error {
295297 o.overlayProps(req, ov)
296298 o.overlay.colorProps(req, mode, o.colorMult, o.ctmBlob)
297299 if o.cursor != nil {
298- o.cursor.props(req, o.crtc, o.cursor.desired())
300+ o.cursor.props(req, o.crtc, cur)
299301 }
300302 return o.k.commit(req, atomicTestOnly, 0)
301303 }
@@ -237,18 +237,20 @@ func (ov overlayWin) close() {
237 // cursor; the fence does not count) is not probed again: a refusal of the237 // cursor; the fence does not count) is not probed again: a refusal of the
238 // commit itself still drops the overlay (overlayConflict).238 // commit itself still drops the overlay (overlayConflict).
239 func (o *Output) testOverlay(fb uint32, ov overlayWin) bool {239 func (o *Output) testOverlay(fb uint32, ov overlayWin) bool {
240+ // One cursor snapshot: the input goroutine may move it meanwhile, and
241+ // what is remembered must be what KMS was asked.
240 key := o.overlayProbeOf(fb, ov)242 key := o.overlayProbeOf(fb, ov)
241 if slices.Contains(o.overlayPassed[:], key) {243 if slices.Contains(o.overlayPassed[:], key) {
242 return true244 return true
243 }245 }
244- err := o.probeOverlay(fb, ov, ov.color.mode)246+ err := o.probeOverlay(fb, ov, ov.color.mode, key.cursor)
245 if err == nil {247 if err == nil {
246 o.overlayPassed[o.overlayNext] = key248 o.overlayPassed[o.overlayNext] = key
247 o.overlayNext = (o.overlayNext + 1) % len(o.overlayPassed)249 o.overlayNext = (o.overlayNext + 1) % len(o.overlayPassed)
248 return true250 return true
249 }251 }
250- if ov.color.mode != colorBypass && refused(err) && o.probeOverlay(fb, ov, colorBypass) == nil {252+ if ov.color.mode != colorBypass && refused(err) && o.probeOverlay(fb, ov, colorBypass, key.cursor) == nil {
251- o.overlay.setVerdict(ov.color.format, o.cursorShown(), false)253+ o.overlay.setVerdict(ov.color.format, key.cursor.on, false)
252 o.setOverlayReason("color_refused")254 o.setOverlayReason("color_refused")
253 o.renderLog.Info().Err(err).Uint32("format", ov.color.format).Str("connector", o.conn.name).Msg("overlay colour pipeline refused")255 o.renderLog.Info().Err(err).Uint32("format", ov.color.format).Str("connector", o.conn.name).Msg("overlay colour pipeline refused")
254 return false256 return false
@@ -277,17 +279,17 @@ type overlayProbe struct {
277 279
278 func (o *Output) overlayProbeOf(fb uint32, ov overlayWin) overlayProbe {280 func (o *Output) overlayProbeOf(fb uint32, ov overlayWin) overlayProbe {
279 p := overlayProbe{primary: fb, overlay: ov.fb, rect: ov.rect, w: ov.w, h: ov.h, color: ov.color, mult: o.colorMult, ctm: o.ctmBlob}281 p := overlayProbe{primary: fb, overlay: ov.fb, rect: ov.rect, w: ov.w, h: ov.h, color: ov.color, mult: o.colorMult, ctm: o.ctmBlob}
280- if o.cursor != nil {282+ if o.cursor != nil && !o.protected {
281 p.cursor = o.cursor.desired()283 p.cursor = o.cursor.desired()
282 }284 }
283 return p285 return p
284 }286 }
285 287
286 // probeOverlay is a TEST_ONLY of the frame: the composed image on the288 // probeOverlay is a TEST_ONLY of the frame: the composed image on the
287-// primary plane (Bypass), ov on the overlay with colour mode, the cursor.289+// primary plane (Bypass), ov on the overlay with colour mode, the cursor in
288-// Both planes' colour state is written whatever they applied: the test must290+// state cur. Both planes' colour state is written whatever they applied:
289-// not rely on the state of the last commit.291+// the test must not rely on the state of the last commit.
290-func (o *Output) probeOverlay(fb uint32, ov overlayWin, mode colorMode) error {292+func (o *Output) probeOverlay(fb uint32, ov overlayWin, mode colorMode, cur cursorState) error {
291 req := &o.probeReq // no frame request is alive: the frame commit follows293 req := &o.probeReq // no frame request is alive: the frame commit follows
292 req.reset()294 req.reset()
293 o.primaryProps(req, fb)295 o.primaryProps(req, fb)
@@ -295,7 +297,7 @@ func (o *Output) probeOverlay(fb uint32, ov overlayWin, mode colorMode) error {
295 o.overlayProps(req, ov)297 o.overlayProps(req, ov)
296 o.overlay.colorProps(req, mode, o.colorMult, o.ctmBlob)298 o.overlay.colorProps(req, mode, o.colorMult, o.ctmBlob)
297 if o.cursor != nil {299 if o.cursor != nil {
298- o.cursor.props(req, o.crtc, o.cursor.desired())300+ o.cursor.props(req, o.crtc, cur)
299 }301 }
300 return o.k.commit(req, atomicTestOnly, 0)302 return o.k.commit(req, atomicTestOnly, 0)
301 }303 }

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 25

Tags 10

↑↓ moveenter openesc close