detached 915e135

test(drm): cover a video on the overlay plane through Output.Run

Run a video window on the overlay next to a static window for 60 frames:
no render, no primary fence, each flip reports the video shown zero-copy,
and its three buffers are probed once. The overlay probe cache keeps four
entries so a client cycling through four buffers is not probed again, and
the probe cache test invalidates it through a real modeset.
commit
915e135b6fbf68d7ba31ec0ba5eb347e180ec990
author
brice · · 2026-10-14 07:32 UTC
parent
b646535
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
5 files · +180 -10
M internal/adapters/drm/front_test.go +170 -4
@@ -3,7 +3,9 @@ package drm
33 import (
44 "context"
55 "os"
6+ "sync/atomic"
67 "testing"
8+ "time"
79
810 portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
911 "github.com/bnema/neferwl/internal/ports"
@@ -99,6 +101,165 @@ func TestSubmitFrameOverlayOnlyWaitsForHeldReads(t *testing.T) {
99101 overlayFrames(t, o, r, s, c, 1)
100102 }
101103
104+// The desktop case through Run: a video (a dmabuf window on the overlay)
105+// next to a static window. Once the overlay is up, each new video buffer is
106+// one frame commit of the same composed image with no render, and each flip
107+// reports the video shown zero-copy at its latest content.
108+func TestRunVideoOnOverlayDoesNotRender(t *testing.T) {
109+ o, k, commits, commitMu := testOutputMu(t)
110+ o.cursor, o.tearing, o.vrrProp = nil, false, 0
111+ props := map[string]uint32{"zpos": pZpos}
112+ for n, v := range planeProps {
113+ props[n] = v
114+ }
115+ o.primary.props = props
116+ o.stray = []*plane{{id: tOverlay, typ: planeOverlay, props: props, zposValue: 2, formats: []ports.DMABufFormat{{Format: fourccXRGB}}}}
117+ o.primary.formats = nil
118+ o.pickOverlay()
119+ flips := make(chan flipEvent, 1)
120+ o.flipped = flips
121+ r := portsmocks.NewMockRenderer(t)
122+ pipeBuf := func() ports.DMABuf {
123+ f, w, _ := os.Pipe()
124+ w.Close()
125+ return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
126+ }
127+ r.EXPECT().SetHDR(float64(0)).Return().Maybe()
128+ r.EXPECT().ExportTargets(2, mock.Anything, false).Return([]ports.DMABuf{pipeBuf(), pipeBuf()}, nil).Once()
129+ r.EXPECT().UseTarget(mock.Anything).Return()
130+ var renders atomic.Int32
131+ r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(ports.Scene, map[ports.WindowID]ports.SurfaceContent) (*os.File, error) {
132+ renders.Add(1)
133+ return nil, nil
134+ })
135+ r.EXPECT().Close().Return().Once()
136+ var images atomic.Uint32
137+ k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).RunAndReturn(func(b *ports.DMABuf, _ uint32) (uint32, error) {
138+ if b.ID == 0 {
139+ return 70 + images.Add(1) - 1, nil // the output images
140+ }
141+ return uint32(100 + b.ID), nil
142+ })
143+ k.EXPECT().createBlob(mock.Anything).Return(99, nil)
144+ k.EXPECT().destroyBlob(mock.Anything).Return(nil).Maybe()
145+ k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
146+ k.EXPECT().objProps(uint32(tOverlay), uint32(objPlane)).Return(map[string][2]uint64{}, nil).Maybe()
147+ frameCommits := func() []commitRec {
148+ commitMu.Lock()
149+ defer commitMu.Unlock()
150+ var out []commitRec
151+ for _, c := range *commits {
152+ if c.user&3 == userFrame {
153+ out = append(out, c)
154+ }
155+ }
156+ return out
157+ }
158+ scenes := make(chan ports.Scene, 1)
159+ contents := newContentFeed(t, 1)
160+ sink, presented := presentedSink(t, 64)
161+ ctx, cancel := context.WithCancel(context.Background())
162+ done := make(chan error, 1)
163+ go func() {
164+ done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), scenes, contents.in(), contents.ret, nil, sink, nil, nil)
165+ }()
166+ // Stop Run on every path, before the mocks check their calls.
167+ t.Cleanup(func() {
168+ cancel()
169+ select {
170+ case err := <-done:
171+ if err != nil {
172+ t.Error(err)
173+ }
174+ case <-time.After(2 * time.Second):
175+ t.Error("Run did not stop")
176+ }
177+ })
178+ static := ports.SurfaceContent{ID: 1, Seq: 1, Width: 100, Height: 100, Opaque: true, Solid: &ports.SolidColor{A: 1}}
179+ video := func(n int) ports.SurfaceContent {
180+ return ports.SurfaceContent{ID: 2, Seq: uint64(n), Width: 100, Height: 100, Opaque: true,
181+ DMABuf: &ports.DMABuf{ID: uint64(n%3 + 1), Width: 100, Height: 100, Format: fourccXRGB}}
182+ }
183+ contents.send(static, video(1))
184+ scenes <- ports.Scene{Seq: 1, Scale: 1, OutputWidth: 200, OutputHeight: 100, Windows: []ports.SceneWindow{
185+ {ID: 1, Rect: ports.Rect{W: 100, H: 100}},
186+ {ID: 2, Rect: ports.Rect{X: 100, W: 100, H: 100}},
187+ }}
188+ // play delivers video frame n and flips every frame commit until a flip
189+ // shows it; it returns that commit and its report. (Seen is what the
190+ // output took in, which may run ahead of the flip.)
191+ flipped := 0
192+ vseq := uint32(0)
193+ play := func(n int) (commitRec, ports.OutputPresented) {
194+ t.Helper()
195+ contents.send(video(n))
196+ var c commitRec
197+ var got ports.OutputPresented
198+ waitFor(t, func() bool {
199+ if cs := frameCommits(); len(cs) > flipped {
200+ c = cs[len(cs)-1]
201+ flipped = len(cs)
202+ vseq++
203+ flips <- flipEvent{crtc: tCrtc, user: c.user, when: time.Duration(vseq) * time.Millisecond, seq: vseq}
204+ }
205+ for {
206+ select {
207+ case p := <-presented:
208+ if p.Flip != nil && p.Flip.Shows[2] == uint64(n) {
209+ got = p
210+ return true
211+ }
212+ default:
213+ return false
214+ }
215+ }
216+ })
217+ return c, got
218+ }
219+ // Warm up until the video is on the overlay over a composed image.
220+ n := 1
221+ for ; n < 10; n++ {
222+ c, _ := play(n)
223+ if v, _ := c.req.value(tOverlay, pFB); v != 0 {
224+ break
225+ }
226+ }
227+ base, commitsBefore := renders.Load(), len(frameCommits())
228+ const frames = 60
229+ for i := range frames {
230+ n++
231+ c, p := play(n)
232+ if v, _ := c.req.value(tOverlay, pFB); v != uint64(100+n%3+1) {
233+ t.Fatalf("video frame %d: overlay fb %d", i, v)
234+ }
235+ if _, ok := c.req.value(tPrimary, pFence); ok {
236+ t.Fatalf("video frame %d waits on a render", i)
237+ }
238+ // The flip reports the video shown zero-copy at its content.
239+ if p.Flip.ZeroCopy != 2 || p.Flip.Shows[2] != uint64(n) || p.Seen[1] != 1 {
240+ t.Fatalf("video frame %d: report %+v flip %+v", i, p, *p.Flip)
241+ }
242+ }
243+ if got := len(frameCommits()) - commitsBefore; got != frames {
244+ t.Fatalf("%d frame commits for %d video frames", got, frames)
245+ }
246+ if got := renders.Load() - base; got != 0 {
247+ t.Fatalf("%d renders for %d video frames, want 0", got, frames)
248+ }
249+ // Three buffers in rotation: each probed once, then remembered.
250+ commitMu.Lock()
251+ probes := 0
252+ for _, c := range *commits {
253+ if c.flags&atomicTestOnly != 0 {
254+ probes++
255+ }
256+ }
257+ commitMu.Unlock()
258+ if probes > 4 {
259+ t.Fatalf("%d overlay probes for %d video frames of 3 buffers", probes, frames)
260+ }
261+}
262+
102263 // A frame whose buffer KMS took before is not probed again.
103264 func TestOverlayProbeCached(t *testing.T) {
104265 o, k, commits := overlayOutput(t)
@@ -123,11 +284,16 @@ func TestOverlayProbeCached(t *testing.T) {
123284 if len(*commits) != 3 {
124285 t.Fatalf("%d probes, want 3", len(*commits))
125286 }
126- // A modeset forgets them.
127- o.overlayPassed = [2]overlayProbe{}
287+ // A modeset (VT resume) forgets them.
288+ k.EXPECT().createBlob(mock.Anything).Return(99, nil).Once()
289+ k.EXPECT().objProps(uint32(tOverlay), uint32(objPlane)).Return(map[string][2]uint64{}, nil).Maybe()
290+ if err := o.modeset(); err != nil {
291+ t.Fatal(err)
292+ }
293+ before := len(*commits)
128294 o.testOverlay(70, ov)
129- if len(*commits) != 4 {
130- t.Fatalf("%d probes after forgetting", len(*commits))
295+ if len(*commits) != before+1 {
296+ t.Fatal("probe cached across a modeset")
131297 }
132298 }
133299
@@ -3,7 +3,9 @@ package drm
3 import (3 import (
4 "context"4 "context"
5 "os"5 "os"
6+ "sync/atomic"
6 "testing"7 "testing"
8+ "time"
7 9
8 portsmocks "github.com/bnema/neferwl/internal/mocks/ports"10 portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
9 "github.com/bnema/neferwl/internal/ports"11 "github.com/bnema/neferwl/internal/ports"
@@ -99,6 +101,165 @@ func TestSubmitFrameOverlayOnlyWaitsForHeldReads(t *testing.T) {
99 overlayFrames(t, o, r, s, c, 1)101 overlayFrames(t, o, r, s, c, 1)
100 }102 }
101 103
104+// The desktop case through Run: a video (a dmabuf window on the overlay)
105+// next to a static window. Once the overlay is up, each new video buffer is
106+// one frame commit of the same composed image with no render, and each flip
107+// reports the video shown zero-copy at its latest content.
108+func TestRunVideoOnOverlayDoesNotRender(t *testing.T) {
109+ o, k, commits, commitMu := testOutputMu(t)
110+ o.cursor, o.tearing, o.vrrProp = nil, false, 0
111+ props := map[string]uint32{"zpos": pZpos}
112+ for n, v := range planeProps {
113+ props[n] = v
114+ }
115+ o.primary.props = props
116+ o.stray = []*plane{{id: tOverlay, typ: planeOverlay, props: props, zposValue: 2, formats: []ports.DMABufFormat{{Format: fourccXRGB}}}}
117+ o.primary.formats = nil
118+ o.pickOverlay()
119+ flips := make(chan flipEvent, 1)
120+ o.flipped = flips
121+ r := portsmocks.NewMockRenderer(t)
122+ pipeBuf := func() ports.DMABuf {
123+ f, w, _ := os.Pipe()
124+ w.Close()
125+ return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
126+ }
127+ r.EXPECT().SetHDR(float64(0)).Return().Maybe()
128+ r.EXPECT().ExportTargets(2, mock.Anything, false).Return([]ports.DMABuf{pipeBuf(), pipeBuf()}, nil).Once()
129+ r.EXPECT().UseTarget(mock.Anything).Return()
130+ var renders atomic.Int32
131+ r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(ports.Scene, map[ports.WindowID]ports.SurfaceContent) (*os.File, error) {
132+ renders.Add(1)
133+ return nil, nil
134+ })
135+ r.EXPECT().Close().Return().Once()
136+ var images atomic.Uint32
137+ k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).RunAndReturn(func(b *ports.DMABuf, _ uint32) (uint32, error) {
138+ if b.ID == 0 {
139+ return 70 + images.Add(1) - 1, nil // the output images
140+ }
141+ return uint32(100 + b.ID), nil
142+ })
143+ k.EXPECT().createBlob(mock.Anything).Return(99, nil)
144+ k.EXPECT().destroyBlob(mock.Anything).Return(nil).Maybe()
145+ k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
146+ k.EXPECT().objProps(uint32(tOverlay), uint32(objPlane)).Return(map[string][2]uint64{}, nil).Maybe()
147+ frameCommits := func() []commitRec {
148+ commitMu.Lock()
149+ defer commitMu.Unlock()
150+ var out []commitRec
151+ for _, c := range *commits {
152+ if c.user&3 == userFrame {
153+ out = append(out, c)
154+ }
155+ }
156+ return out
157+ }
158+ scenes := make(chan ports.Scene, 1)
159+ contents := newContentFeed(t, 1)
160+ sink, presented := presentedSink(t, 64)
161+ ctx, cancel := context.WithCancel(context.Background())
162+ done := make(chan error, 1)
163+ go func() {
164+ done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), scenes, contents.in(), contents.ret, nil, sink, nil, nil)
165+ }()
166+ // Stop Run on every path, before the mocks check their calls.
167+ t.Cleanup(func() {
168+ cancel()
169+ select {
170+ case err := <-done:
171+ if err != nil {
172+ t.Error(err)
173+ }
174+ case <-time.After(2 * time.Second):
175+ t.Error("Run did not stop")
176+ }
177+ })
178+ static := ports.SurfaceContent{ID: 1, Seq: 1, Width: 100, Height: 100, Opaque: true, Solid: &ports.SolidColor{A: 1}}
179+ video := func(n int) ports.SurfaceContent {
180+ return ports.SurfaceContent{ID: 2, Seq: uint64(n), Width: 100, Height: 100, Opaque: true,
181+ DMABuf: &ports.DMABuf{ID: uint64(n%3 + 1), Width: 100, Height: 100, Format: fourccXRGB}}
182+ }
183+ contents.send(static, video(1))
184+ scenes <- ports.Scene{Seq: 1, Scale: 1, OutputWidth: 200, OutputHeight: 100, Windows: []ports.SceneWindow{
185+ {ID: 1, Rect: ports.Rect{W: 100, H: 100}},
186+ {ID: 2, Rect: ports.Rect{X: 100, W: 100, H: 100}},
187+ }}
188+ // play delivers video frame n and flips every frame commit until a flip
189+ // shows it; it returns that commit and its report. (Seen is what the
190+ // output took in, which may run ahead of the flip.)
191+ flipped := 0
192+ vseq := uint32(0)
193+ play := func(n int) (commitRec, ports.OutputPresented) {
194+ t.Helper()
195+ contents.send(video(n))
196+ var c commitRec
197+ var got ports.OutputPresented
198+ waitFor(t, func() bool {
199+ if cs := frameCommits(); len(cs) > flipped {
200+ c = cs[len(cs)-1]
201+ flipped = len(cs)
202+ vseq++
203+ flips <- flipEvent{crtc: tCrtc, user: c.user, when: time.Duration(vseq) * time.Millisecond, seq: vseq}
204+ }
205+ for {
206+ select {
207+ case p := <-presented:
208+ if p.Flip != nil && p.Flip.Shows[2] == uint64(n) {
209+ got = p
210+ return true
211+ }
212+ default:
213+ return false
214+ }
215+ }
216+ })
217+ return c, got
218+ }
219+ // Warm up until the video is on the overlay over a composed image.
220+ n := 1
221+ for ; n < 10; n++ {
222+ c, _ := play(n)
223+ if v, _ := c.req.value(tOverlay, pFB); v != 0 {
224+ break
225+ }
226+ }
227+ base, commitsBefore := renders.Load(), len(frameCommits())
228+ const frames = 60
229+ for i := range frames {
230+ n++
231+ c, p := play(n)
232+ if v, _ := c.req.value(tOverlay, pFB); v != uint64(100+n%3+1) {
233+ t.Fatalf("video frame %d: overlay fb %d", i, v)
234+ }
235+ if _, ok := c.req.value(tPrimary, pFence); ok {
236+ t.Fatalf("video frame %d waits on a render", i)
237+ }
238+ // The flip reports the video shown zero-copy at its content.
239+ if p.Flip.ZeroCopy != 2 || p.Flip.Shows[2] != uint64(n) || p.Seen[1] != 1 {
240+ t.Fatalf("video frame %d: report %+v flip %+v", i, p, *p.Flip)
241+ }
242+ }
243+ if got := len(frameCommits()) - commitsBefore; got != frames {
244+ t.Fatalf("%d frame commits for %d video frames", got, frames)
245+ }
246+ if got := renders.Load() - base; got != 0 {
247+ t.Fatalf("%d renders for %d video frames, want 0", got, frames)
248+ }
249+ // Three buffers in rotation: each probed once, then remembered.
250+ commitMu.Lock()
251+ probes := 0
252+ for _, c := range *commits {
253+ if c.flags&atomicTestOnly != 0 {
254+ probes++
255+ }
256+ }
257+ commitMu.Unlock()
258+ if probes > 4 {
259+ t.Fatalf("%d overlay probes for %d video frames of 3 buffers", probes, frames)
260+ }
261+}
262+
102 // A frame whose buffer KMS took before is not probed again.263 // A frame whose buffer KMS took before is not probed again.
103 func TestOverlayProbeCached(t *testing.T) {264 func TestOverlayProbeCached(t *testing.T) {
104 o, k, commits := overlayOutput(t)265 o, k, commits := overlayOutput(t)
@@ -123,11 +284,16 @@ func TestOverlayProbeCached(t *testing.T) {
123 if len(*commits) != 3 {284 if len(*commits) != 3 {
124 t.Fatalf("%d probes, want 3", len(*commits))285 t.Fatalf("%d probes, want 3", len(*commits))
125 }286 }
126- // A modeset forgets them.287+ // A modeset (VT resume) forgets them.
127- o.overlayPassed = [2]overlayProbe{}288+ k.EXPECT().createBlob(mock.Anything).Return(99, nil).Once()
289+ k.EXPECT().objProps(uint32(tOverlay), uint32(objPlane)).Return(map[string][2]uint64{}, nil).Maybe()
290+ if err := o.modeset(); err != nil {
291+ t.Fatal(err)
292+ }
293+ before := len(*commits)
128 o.testOverlay(70, ov)294 o.testOverlay(70, ov)
129- if len(*commits) != 4 {295+ if len(*commits) != before+1 {
130- t.Fatalf("%d probes after forgetting", len(*commits))296+ t.Fatal("probe cached across a modeset")
131 }297 }
132 }298 }
133 299
M internal/adapters/drm/images.go +1 -1
@@ -238,7 +238,7 @@ func (o *Output) exportImages(r ports.Renderer, mods []uint64, singlePlane bool)
238238 // freeImages removes the output images' framebuffers.
239239 func (o *Output) freeImages() {
240240 o.front.forget()
241- o.overlayPassed = [2]overlayProbe{}
241+ o.forgetOverlayProbes()
242242 for i, fb := range o.fbs {
243243 if fb != 0 {
244244 _ = o.k.rmFB(fb)
@@ -238,7 +238,7 @@ func (o *Output) exportImages(r ports.Renderer, mods []uint64, singlePlane bool)
238 // freeImages removes the output images' framebuffers.238 // freeImages removes the output images' framebuffers.
239 func (o *Output) freeImages() {239 func (o *Output) freeImages() {
240 o.front.forget()240 o.front.forget()
241- o.overlayPassed = [2]overlayProbe{}241+ o.forgetOverlayProbes()
242 for i, fb := range o.fbs {242 for i, fb := range o.fbs {
243 if fb != 0 {243 if fb != 0 {
244 _ = o.k.rmFB(fb)244 _ = o.k.rmFB(fb)
M internal/adapters/drm/output.go +4 -3
@@ -149,8 +149,9 @@ type Output struct {
149149 restWindows []ports.SceneWindow
150150 // front is what the output image on screen holds (front.go).
151151 front frontImage
152- // overlayPassed are the last overlay probes KMS took (testOverlay).
153- overlayPassed [2]overlayProbe
152+ // overlayPassed are the last overlay probes KMS took (testOverlay):
153+ // enough for a client cycling through four buffers.
154+ overlayPassed [4]overlayProbe
154155 overlayNext int
155156 // lastFrame is when the last frame was committed. cursorHeld: a cursor
156157 // move waits for the next frame (see cursorWaits).
@@ -443,7 +444,7 @@ func (o *Output) modesetImage(fb uint32, active bool) error {
443444 fb.scaleTestedOK, fb.scaleRefused = false, false
444445 }
445446 o.front.forget()
446- o.overlayPassed = [2]overlayProbe{}
447+ o.forgetOverlayProbes()
447448 o.forgetColor()
448449 o.forgetColorVerdicts()
449450 o.shown, o.queued = 0, 0
@@ -149,8 +149,9 @@ type Output struct {
149 restWindows []ports.SceneWindow149 restWindows []ports.SceneWindow
150 // front is what the output image on screen holds (front.go).150 // front is what the output image on screen holds (front.go).
151 front frontImage151 front frontImage
152- // overlayPassed are the last overlay probes KMS took (testOverlay).152+ // overlayPassed are the last overlay probes KMS took (testOverlay):
153- overlayPassed [2]overlayProbe153+ // enough for a client cycling through four buffers.
154+ overlayPassed [4]overlayProbe
154 overlayNext int155 overlayNext int
155 // lastFrame is when the last frame was committed. cursorHeld: a cursor156 // lastFrame is when the last frame was committed. cursorHeld: a cursor
156 // move waits for the next frame (see cursorWaits).157 // move waits for the next frame (see cursorWaits).
@@ -443,7 +444,7 @@ func (o *Output) modesetImage(fb uint32, active bool) error {
443 fb.scaleTestedOK, fb.scaleRefused = false, false444 fb.scaleTestedOK, fb.scaleRefused = false, false
444 }445 }
445 o.front.forget()446 o.front.forget()
446- o.overlayPassed = [2]overlayProbe{}447+ o.forgetOverlayProbes()
447 o.forgetColor()448 o.forgetColor()
448 o.forgetColorVerdicts()449 o.forgetColorVerdicts()
449 o.shown, o.queued = 0, 0450 o.shown, o.queued = 0, 0
M internal/adapters/drm/overlay.go +4 -1
@@ -244,7 +244,7 @@ func (o *Output) testOverlay(fb uint32, ov overlayWin) bool {
244244 err := o.probeOverlay(fb, ov, ov.color.mode)
245245 if err == nil {
246246 o.overlayPassed[o.overlayNext] = key
247- o.overlayNext = 1 - o.overlayNext
247+ o.overlayNext = (o.overlayNext + 1) % len(o.overlayPassed)
248248 return true
249249 }
250250 if ov.color.mode != colorBypass && refused(err) && o.probeOverlay(fb, ov, colorBypass) == nil {
@@ -261,6 +261,9 @@ func (o *Output) testOverlay(fb uint32, ov overlayWin) bool {
261261 return false
262262 }
263263
264+// forgetOverlayProbes drops the remembered probes: KMS is asked again.
265+func (o *Output) forgetOverlayProbes() { clear(o.overlayPassed[:]) }
266+
264267 // overlayProbe is what an overlay probe depends on, fences aside.
265268 type overlayProbe struct {
266269 primary, overlay uint32
@@ -244,7 +244,7 @@ func (o *Output) testOverlay(fb uint32, ov overlayWin) bool {
244 err := o.probeOverlay(fb, ov, ov.color.mode)244 err := o.probeOverlay(fb, ov, ov.color.mode)
245 if err == nil {245 if err == nil {
246 o.overlayPassed[o.overlayNext] = key246 o.overlayPassed[o.overlayNext] = key
247- o.overlayNext = 1 - o.overlayNext247+ o.overlayNext = (o.overlayNext + 1) % len(o.overlayPassed)
248 return true248 return true
249 }249 }
250 if ov.color.mode != colorBypass && refused(err) && o.probeOverlay(fb, ov, colorBypass) == nil {250 if ov.color.mode != colorBypass && refused(err) && o.probeOverlay(fb, ov, colorBypass) == nil {
@@ -261,6 +261,9 @@ func (o *Output) testOverlay(fb uint32, ov overlayWin) bool {
261 return false261 return false
262 }262 }
263 263
264+// forgetOverlayProbes drops the remembered probes: KMS is asked again.
265+func (o *Output) forgetOverlayProbes() { clear(o.overlayPassed[:]) }
266+
264 // overlayProbe is what an overlay probe depends on, fences aside.267 // overlayProbe is what an overlay probe depends on, fences aside.
265 type overlayProbe struct {268 type overlayProbe struct {
266 primary, overlay uint32269 primary, overlay uint32
M internal/adapters/drm/scanout.go +1 -1
@@ -588,7 +588,7 @@ func (o *Output) dropClientFBs(now time.Time, all bool) {
588588 if fb.fbID != 0 {
589589 _ = o.k.rmFB(fb.fbID)
590590 // KMS may give the id to another buffer.
591- o.overlayPassed = [2]overlayProbe{}
591+ o.forgetOverlayProbes()
592592 }
593593 delete(o.clientFBs, id)
594594 }
@@ -588,7 +588,7 @@ func (o *Output) dropClientFBs(now time.Time, all bool) {
588 if fb.fbID != 0 {588 if fb.fbID != 0 {
589 _ = o.k.rmFB(fb.fbID)589 _ = o.k.rmFB(fb.fbID)
590 // KMS may give the id to another buffer.590 // KMS may give the id to another buffer.
591- o.overlayPassed = [2]overlayProbe{}591+ o.forgetOverlayProbes()
592 }592 }
593 delete(o.clientFBs, id)593 delete(o.clientFBs, id)
594 }594 }

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 26

Tags 10

↑↓ moveenter openesc close