detached b646535

perf(drm): flip only the overlay buffer when the composed image is unchanged

When a frame puts a window on the overlay plane and the image on screen
already holds the rest of the scene (same scene, same content Seq for
every window and layer), commit that image again with the new overlay
buffer instead of rendering: no GPU pass, no frame fence. Any held read
of an uncommitted render forces composition.

Overlay TEST_ONLY probes that KMS accepted are remembered (last two, keyed
by images, place, colour state and cursor) and not repeated; the cache is
dropped on modeset, image teardown and client framebuffer removal.
commit
b6465355c833d82894b61935974c6057b8d91178
author
brice · · 2026-10-14 05:44 UTC
parent
e608dd7
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
9 files · +341 -0
M internal/adapters/drm/frame_submit.go +46 -0
@@ -89,6 +89,22 @@ func (o *Output) submitFrame(ctx context.Context, r ports.Renderer, scene ports.
8989 decision = o.composeFrame(scene, surfaces)
9090 }
9191 ov, composed := decision.overlay, decision.composed
92+ // A render that was not committed may still read client buffers: only a
93+ // new render's fence orders this flip after it, so the frame composes.
94+ if ov.fb != 0 && len(normal) == 0 && len(excluded) == 0 && o.readDone() {
95+ if front := o.fbs[1-o.back]; o.front.holds(front, composed, surfaces) {
96+ // Only the overlay window changed: the image on screen already
97+ // holds the rest.
98+ if o.testOverlay(front, ov) {
99+ err = o.commitOverlayOnly(front, scene, f, ov)
100+ presented = err == nil
101+ return false, err
102+ }
103+ // Refused: compose the window too (cached per buffer).
104+ ov.close()
105+ ov, composed = overlayWin{}, scene
106+ }
107+ }
92108 r.UseTarget(o.back)
93109 // Captures never show the capture indicator: their frame goes first.
94110 took, pdone, perr := pipeline.SubmitPlain(r, composed, surfaces, normal)
@@ -213,7 +229,37 @@ func (o *Output) submitFrame(ctx context.Context, r ports.Renderer, scene ports.
213229 }
214230 if err == nil {
215231 o.queued = ov.buf
232+ // The image now holds what was drawn last: composed, or the whole
233+ // scene after an overlay refusal.
234+ if ov.fb != 0 {
235+ o.front.hold(o.fbs[o.back], composed, surfaces)
236+ } else {
237+ o.front.hold(o.fbs[o.back], scene, surfaces)
238+ }
216239 o.back = 1 - o.back
217240 }
218241 return false, err
219242 }
243+
244+// commitOverlayOnly commits front, the image on screen, again with ov on
245+// the overlay plane: no render, no GPU fence. It closes ov's fence.
246+func (o *Output) commitOverlayOnly(front uint32, scene ports.Scene, f pendingFrame, ov overlayWin) error {
247+ defer ov.close()
248+ if !o.sceneCurrent(scene) || o.protected {
249+ return errSecurityScene
250+ }
251+ f.queued, f.zeroCopy = ov.buf, ov.id
252+ game := o.vrrProp != 0 && fullscreenShown(&scene)
253+ err := o.commitWith(front, nil, false, o.wantVRR(game), f, ov)
254+ if errors.Is(err, errColorRefused) {
255+ o.setOverlayReason("color_refused")
256+ return errOverlayDropped
257+ }
258+ if err != nil && o.overlayConflict(err, ov.buf) {
259+ return errOverlayDropped
260+ }
261+ if err == nil {
262+ o.queued = ov.buf
263+ }
264+ return err
265+}
@@ -89,6 +89,22 @@ func (o *Output) submitFrame(ctx context.Context, r ports.Renderer, scene ports.
89 decision = o.composeFrame(scene, surfaces)89 decision = o.composeFrame(scene, surfaces)
90 }90 }
91 ov, composed := decision.overlay, decision.composed91 ov, composed := decision.overlay, decision.composed
92+ // A render that was not committed may still read client buffers: only a
93+ // new render's fence orders this flip after it, so the frame composes.
94+ if ov.fb != 0 && len(normal) == 0 && len(excluded) == 0 && o.readDone() {
95+ if front := o.fbs[1-o.back]; o.front.holds(front, composed, surfaces) {
96+ // Only the overlay window changed: the image on screen already
97+ // holds the rest.
98+ if o.testOverlay(front, ov) {
99+ err = o.commitOverlayOnly(front, scene, f, ov)
100+ presented = err == nil
101+ return false, err
102+ }
103+ // Refused: compose the window too (cached per buffer).
104+ ov.close()
105+ ov, composed = overlayWin{}, scene
106+ }
107+ }
92 r.UseTarget(o.back)108 r.UseTarget(o.back)
93 // Captures never show the capture indicator: their frame goes first.109 // Captures never show the capture indicator: their frame goes first.
94 took, pdone, perr := pipeline.SubmitPlain(r, composed, surfaces, normal)110 took, pdone, perr := pipeline.SubmitPlain(r, composed, surfaces, normal)
@@ -213,7 +229,37 @@ func (o *Output) submitFrame(ctx context.Context, r ports.Renderer, scene ports.
213 }229 }
214 if err == nil {230 if err == nil {
215 o.queued = ov.buf231 o.queued = ov.buf
232+ // The image now holds what was drawn last: composed, or the whole
233+ // scene after an overlay refusal.
234+ if ov.fb != 0 {
235+ o.front.hold(o.fbs[o.back], composed, surfaces)
236+ } else {
237+ o.front.hold(o.fbs[o.back], scene, surfaces)
238+ }
216 o.back = 1 - o.back239 o.back = 1 - o.back
217 }240 }
218 return false, err241 return false, err
219 }242 }
243+
244+// commitOverlayOnly commits front, the image on screen, again with ov on
245+// the overlay plane: no render, no GPU fence. It closes ov's fence.
246+func (o *Output) commitOverlayOnly(front uint32, scene ports.Scene, f pendingFrame, ov overlayWin) error {
247+ defer ov.close()
248+ if !o.sceneCurrent(scene) || o.protected {
249+ return errSecurityScene
250+ }
251+ f.queued, f.zeroCopy = ov.buf, ov.id
252+ game := o.vrrProp != 0 && fullscreenShown(&scene)
253+ err := o.commitWith(front, nil, false, o.wantVRR(game), f, ov)
254+ if errors.Is(err, errColorRefused) {
255+ o.setOverlayReason("color_refused")
256+ return errOverlayDropped
257+ }
258+ if err != nil && o.overlayConflict(err, ov.buf) {
259+ return errOverlayDropped
260+ }
261+ if err == nil {
262+ o.queued = ov.buf
263+ }
264+ return err
265+}
A internal/adapters/drm/front.go +80 -0
@@ -0,0 +1,80 @@
1+package drm
2+
3+import (
4+ "slices"
5+
6+ "github.com/bnema/neferwl/internal/ports"
7+)
8+
9+// frontImage is what the output image last committed by a composed frame
10+// holds: the scene drawn into it and the content Seq of each window and
11+// layer it lists. When only the window on the overlay plane changes, the
12+// next frame commits that image again with the new overlay buffer: no GPU
13+// pass, no fence. Paths that draw into an output image outside
14+// submitFrame (security, protection, image setup, the async probe) forget
15+// it.
16+type frontImage struct {
17+ fb uint32 // 0: unknown
18+ scene ports.Scene
19+ seqs []uint64 // Seq of each window, then each layer, of scene
20+ // Backing arrays of scene's slices, reused across frames.
21+ windows []ports.SceneWindow
22+ separators []ports.Separator
23+ layers []ports.SceneLayer
24+ hints []ports.Rect
25+ indicators []ports.CaptureIndicator
26+}
27+
28+// forget drops the image's contents: the next frame composes.
29+func (f *frontImage) forget() { f.fb = 0 }
30+
31+// hold records that fb holds s drawn from surfaces. A capture scene is not
32+// recorded (SameAs never matches it).
33+func (f *frontImage) hold(fb uint32, s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent) {
34+ if s.Capture != nil || s.CaptureScene != nil {
35+ f.forget()
36+ return
37+ }
38+ f.windows = append(f.windows[:0], s.Windows...)
39+ f.separators = append(f.separators[:0], s.Separators...)
40+ f.layers = append(f.layers[:0], s.Layers...)
41+ f.hints = append(f.hints[:0], s.DropHints...)
42+ f.indicators = append(f.indicators[:0], s.CaptureIndicators...)
43+ f.scene = s
44+ f.scene.Windows, f.scene.Separators, f.scene.Layers = f.windows, f.separators, f.layers
45+ f.scene.DropHints, f.scene.CaptureIndicators = f.hints, f.indicators
46+ f.seqs = appendSeqs(f.seqs[:0], s, surfaces)
47+ f.fb = fb
48+}
49+
50+// holds reports whether fb holds exactly what drawing s from surfaces would.
51+func (f *frontImage) holds(fb uint32, s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent) bool {
52+ if f.fb == 0 || f.fb != fb || !s.SameAs(f.scene) {
53+ return false
54+ }
55+ i := 0
56+ for _, w := range s.Windows {
57+ if surfaces[w.ID].Seq != f.seqs[i] {
58+ return false
59+ }
60+ i++
61+ }
62+ for _, l := range s.Layers {
63+ if surfaces[l.ID].Seq != f.seqs[i] {
64+ return false
65+ }
66+ i++
67+ }
68+ return true
69+}
70+
71+func appendSeqs(seqs []uint64, s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent) []uint64 {
72+ seqs = slices.Grow(seqs, len(s.Windows)+len(s.Layers))
73+ for _, w := range s.Windows {
74+ seqs = append(seqs, surfaces[w.ID].Seq)
75+ }
76+ for _, l := range s.Layers {
77+ seqs = append(seqs, surfaces[l.ID].Seq)
78+ }
79+ return seqs
80+}
@@ -0,0 +1,80 @@
1+package drm
2+
3+import (
4+ "slices"
5+
6+ "github.com/bnema/neferwl/internal/ports"
7+)
8+
9+// frontImage is what the output image last committed by a composed frame
10+// holds: the scene drawn into it and the content Seq of each window and
11+// layer it lists. When only the window on the overlay plane changes, the
12+// next frame commits that image again with the new overlay buffer: no GPU
13+// pass, no fence. Paths that draw into an output image outside
14+// submitFrame (security, protection, image setup, the async probe) forget
15+// it.
16+type frontImage struct {
17+ fb uint32 // 0: unknown
18+ scene ports.Scene
19+ seqs []uint64 // Seq of each window, then each layer, of scene
20+ // Backing arrays of scene's slices, reused across frames.
21+ windows []ports.SceneWindow
22+ separators []ports.Separator
23+ layers []ports.SceneLayer
24+ hints []ports.Rect
25+ indicators []ports.CaptureIndicator
26+}
27+
28+// forget drops the image's contents: the next frame composes.
29+func (f *frontImage) forget() { f.fb = 0 }
30+
31+// hold records that fb holds s drawn from surfaces. A capture scene is not
32+// recorded (SameAs never matches it).
33+func (f *frontImage) hold(fb uint32, s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent) {
34+ if s.Capture != nil || s.CaptureScene != nil {
35+ f.forget()
36+ return
37+ }
38+ f.windows = append(f.windows[:0], s.Windows...)
39+ f.separators = append(f.separators[:0], s.Separators...)
40+ f.layers = append(f.layers[:0], s.Layers...)
41+ f.hints = append(f.hints[:0], s.DropHints...)
42+ f.indicators = append(f.indicators[:0], s.CaptureIndicators...)
43+ f.scene = s
44+ f.scene.Windows, f.scene.Separators, f.scene.Layers = f.windows, f.separators, f.layers
45+ f.scene.DropHints, f.scene.CaptureIndicators = f.hints, f.indicators
46+ f.seqs = appendSeqs(f.seqs[:0], s, surfaces)
47+ f.fb = fb
48+}
49+
50+// holds reports whether fb holds exactly what drawing s from surfaces would.
51+func (f *frontImage) holds(fb uint32, s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent) bool {
52+ if f.fb == 0 || f.fb != fb || !s.SameAs(f.scene) {
53+ return false
54+ }
55+ i := 0
56+ for _, w := range s.Windows {
57+ if surfaces[w.ID].Seq != f.seqs[i] {
58+ return false
59+ }
60+ i++
61+ }
62+ for _, l := range s.Layers {
63+ if surfaces[l.ID].Seq != f.seqs[i] {
64+ return false
65+ }
66+ i++
67+ }
68+ return true
69+}
70+
71+func appendSeqs(seqs []uint64, s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent) []uint64 {
72+ seqs = slices.Grow(seqs, len(s.Windows)+len(s.Layers))
73+ for _, w := range s.Windows {
74+ seqs = append(seqs, surfaces[w.ID].Seq)
75+ }
76+ for _, l := range s.Layers {
77+ seqs = append(seqs, surfaces[l.ID].Seq)
78+ }
79+ return seqs
80+}
A internal/adapters/drm/front_test.go +172 -0
@@ -0,0 +1,172 @@
1+package drm
2+
3+import (
4+ "context"
5+ "os"
6+ "testing"
7+
8+ portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
9+ "github.com/bnema/neferwl/internal/ports"
10+ "github.com/stretchr/testify/mock"
11+)
12+
13+// overlayFrames submits the overlay scene once (composed), then with the
14+// overlay window's next buffers: the frames after the first only flip the
15+// overlay buffer. It returns the windows each render drew.
16+func overlayFrames(t *testing.T, o *Output, r *portsmocks.MockRenderer, s ports.Scene, c map[ports.WindowID]ports.SurfaceContent, frames int) {
17+ t.Helper()
18+ for i := range frames {
19+ win := c[2]
20+ buf := *win.DMABuf
21+ buf.ID = uint64(9 + i)
22+ win.DMABuf, win.Seq = &buf, uint64(1+i)
23+ c[2] = win
24+ if _, err := o.submitFrame(context.Background(), r, s, c, map[ports.WindowID]uint64{}, nil, nil); err != nil {
25+ t.Fatalf("frame %d: %v", i, err)
26+ }
27+ o.frame.endPending()
28+ }
29+}
30+
31+// When only the overlay window's buffer changes, the image on screen is
32+// committed again with the new buffer: no render, no probe once KMS took
33+// the same frame.
34+func TestSubmitFrameOverlayOnlySkipsRender(t *testing.T) {
35+ o, k, commits := overlayOutput(t)
36+ o.cursor, o.vrrProp = nil, 0
37+ k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).RunAndReturn(func(b *ports.DMABuf, _ uint32) (uint32, error) {
38+ return uint32(80 + b.ID), nil
39+ }).Times(4)
40+ o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
41+ s, c := overlayScene()
42+ r := portsmocks.NewMockRenderer(t)
43+ r.EXPECT().UseTarget(0).Return().Once()
44+ r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil).Once()
45+ overlayFrames(t, o, r, s, c, 4)
46+ var probes, frames int
47+ for _, cm := range *commits {
48+ if cm.flags&atomicTestOnly != 0 {
49+ probes++
50+ continue
51+ }
52+ frames++
53+ if v, _ := cm.req.value(tPrimary, pFB); v != 70 {
54+ t.Fatalf("frame %d primary fb %d, want the composed image 70", frames, v)
55+ }
56+ if _, ok := cm.req.value(tPrimary, pFence); ok {
57+ t.Fatalf("frame %d waits on a render fence", frames)
58+ }
59+ }
60+ if frames != 4 {
61+ t.Fatalf("%d frame commits", frames)
62+ }
63+ // Each new overlay buffer is probed once; nothing else is.
64+ if probes != 4 {
65+ t.Fatalf("%d probes", probes)
66+ }
67+ if last := (*commits)[len(*commits)-1]; o.queued != 12 {
68+ t.Fatalf("queued %d", o.queued)
69+ } else if v, _ := last.req.value(tOverlay, pFB); v != 92 {
70+ t.Fatalf("overlay fb %d", v)
71+ }
72+ if o.back != 1 {
73+ t.Fatalf("back %d: the skipped frames drew nothing", o.back)
74+ }
75+}
76+
77+// An uncommitted render that may still read client buffers forces
78+// composition: the overlay-only flip carries no fence to order it after
79+// those reads, and its report would let wayland release them.
80+func TestSubmitFrameOverlayOnlyWaitsForHeldReads(t *testing.T) {
81+ o, k, _ := overlayOutput(t)
82+ o.cursor, o.vrrProp = nil, 0
83+ k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Maybe()
84+ o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
85+ s, c := overlayScene()
86+ r := portsmocks.NewMockRenderer(t)
87+ r.EXPECT().UseTarget(mock.Anything).Return().Twice()
88+ r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil).Twice()
89+ overlayFrames(t, o, r, s, c, 1)
90+ // An unsignalled fence of a render whose commit failed.
91+ fence, w, err := os.Pipe()
92+ if err != nil {
93+ t.Fatal(err)
94+ }
95+ defer fence.Close()
96+ defer w.Close()
97+ o.holdRead(fence)
98+ defer o.dropRead()
99+ overlayFrames(t, o, r, s, c, 1)
100+}
101+
102+// A frame whose buffer KMS took before is not probed again.
103+func TestOverlayProbeCached(t *testing.T) {
104+ o, k, commits := overlayOutput(t)
105+ o.cursor = nil
106+ k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once()
107+ o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
108+ s, c := overlayScene()
109+ ov, _ := o.overlayFrame(s, c)
110+ for range 3 {
111+ if !o.testOverlay(70, ov) {
112+ t.Fatal("refused")
113+ }
114+ }
115+ if len(*commits) != 1 {
116+ t.Fatalf("%d probes, want 1", len(*commits))
117+ }
118+ // Another place, primary image or cursor is probed.
119+ moved := ov
120+ moved.rect.X++
121+ o.testOverlay(70, moved)
122+ o.testOverlay(71, ov)
123+ if len(*commits) != 3 {
124+ t.Fatalf("%d probes, want 3", len(*commits))
125+ }
126+ // A modeset forgets them.
127+ o.overlayPassed = [2]overlayProbe{}
128+ o.testOverlay(70, ov)
129+ if len(*commits) != 4 {
130+ t.Fatalf("%d probes after forgetting", len(*commits))
131+ }
132+}
133+
134+// Anything but the overlay window changing composes: another window's
135+// content, the scene, or an image drawn by another path.
136+func TestSubmitFrameOverlayOnlyNeedsSameImage(t *testing.T) {
137+ for name, edit := range map[string]func(o *Output, s *ports.Scene, c map[ports.WindowID]ports.SurfaceContent){
138+ "other window content": func(_ *Output, _ *ports.Scene, c map[ports.WindowID]ports.SurfaceContent) {
139+ w := c[1]
140+ w.Seq++
141+ c[1] = w
142+ },
143+ "scene": func(_ *Output, s *ports.Scene, _ map[ports.WindowID]ports.SurfaceContent) {
144+ s.Background = "#ffffff"
145+ },
146+ "image forgotten": func(o *Output, _ *ports.Scene, _ map[ports.WindowID]ports.SurfaceContent) {
147+ o.front.forget()
148+ },
149+ } {
150+ t.Run(name, func(t *testing.T) {
151+ o, k, _ := overlayOutput(t)
152+ o.cursor, o.vrrProp = nil, 0
153+ k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Maybe()
154+ o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
155+ s, c := overlayScene()
156+ c[1] = ports.SurfaceContent{ID: 1, Seq: 1, Width: 100, Height: 100, Opaque: true, Solid: &ports.SolidColor{A: 1}}
157+ r := portsmocks.NewMockRenderer(t)
158+ r.EXPECT().UseTarget(mock.Anything).Return().Twice()
159+ var drawn []int
160+ r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(s ports.Scene, _ map[ports.WindowID]ports.SurfaceContent) (*os.File, error) {
161+ drawn = append(drawn, len(s.Windows))
162+ return nil, nil
163+ }).Twice()
164+ overlayFrames(t, o, r, s, c, 1)
165+ edit(o, &s, c)
166+ overlayFrames(t, o, r, s, c, 1)
167+ if len(drawn) != 2 || drawn[1] != 1 {
168+ t.Fatalf("rendered %v", drawn)
169+ }
170+ })
171+ }
172+}
@@ -0,0 +1,172 @@
1+package drm
2+
3+import (
4+ "context"
5+ "os"
6+ "testing"
7+
8+ portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
9+ "github.com/bnema/neferwl/internal/ports"
10+ "github.com/stretchr/testify/mock"
11+)
12+
13+// overlayFrames submits the overlay scene once (composed), then with the
14+// overlay window's next buffers: the frames after the first only flip the
15+// overlay buffer. It returns the windows each render drew.
16+func overlayFrames(t *testing.T, o *Output, r *portsmocks.MockRenderer, s ports.Scene, c map[ports.WindowID]ports.SurfaceContent, frames int) {
17+ t.Helper()
18+ for i := range frames {
19+ win := c[2]
20+ buf := *win.DMABuf
21+ buf.ID = uint64(9 + i)
22+ win.DMABuf, win.Seq = &buf, uint64(1+i)
23+ c[2] = win
24+ if _, err := o.submitFrame(context.Background(), r, s, c, map[ports.WindowID]uint64{}, nil, nil); err != nil {
25+ t.Fatalf("frame %d: %v", i, err)
26+ }
27+ o.frame.endPending()
28+ }
29+}
30+
31+// When only the overlay window's buffer changes, the image on screen is
32+// committed again with the new buffer: no render, no probe once KMS took
33+// the same frame.
34+func TestSubmitFrameOverlayOnlySkipsRender(t *testing.T) {
35+ o, k, commits := overlayOutput(t)
36+ o.cursor, o.vrrProp = nil, 0
37+ k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).RunAndReturn(func(b *ports.DMABuf, _ uint32) (uint32, error) {
38+ return uint32(80 + b.ID), nil
39+ }).Times(4)
40+ o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
41+ s, c := overlayScene()
42+ r := portsmocks.NewMockRenderer(t)
43+ r.EXPECT().UseTarget(0).Return().Once()
44+ r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil).Once()
45+ overlayFrames(t, o, r, s, c, 4)
46+ var probes, frames int
47+ for _, cm := range *commits {
48+ if cm.flags&atomicTestOnly != 0 {
49+ probes++
50+ continue
51+ }
52+ frames++
53+ if v, _ := cm.req.value(tPrimary, pFB); v != 70 {
54+ t.Fatalf("frame %d primary fb %d, want the composed image 70", frames, v)
55+ }
56+ if _, ok := cm.req.value(tPrimary, pFence); ok {
57+ t.Fatalf("frame %d waits on a render fence", frames)
58+ }
59+ }
60+ if frames != 4 {
61+ t.Fatalf("%d frame commits", frames)
62+ }
63+ // Each new overlay buffer is probed once; nothing else is.
64+ if probes != 4 {
65+ t.Fatalf("%d probes", probes)
66+ }
67+ if last := (*commits)[len(*commits)-1]; o.queued != 12 {
68+ t.Fatalf("queued %d", o.queued)
69+ } else if v, _ := last.req.value(tOverlay, pFB); v != 92 {
70+ t.Fatalf("overlay fb %d", v)
71+ }
72+ if o.back != 1 {
73+ t.Fatalf("back %d: the skipped frames drew nothing", o.back)
74+ }
75+}
76+
77+// An uncommitted render that may still read client buffers forces
78+// composition: the overlay-only flip carries no fence to order it after
79+// those reads, and its report would let wayland release them.
80+func TestSubmitFrameOverlayOnlyWaitsForHeldReads(t *testing.T) {
81+ o, k, _ := overlayOutput(t)
82+ o.cursor, o.vrrProp = nil, 0
83+ k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Maybe()
84+ o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
85+ s, c := overlayScene()
86+ r := portsmocks.NewMockRenderer(t)
87+ r.EXPECT().UseTarget(mock.Anything).Return().Twice()
88+ r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil).Twice()
89+ overlayFrames(t, o, r, s, c, 1)
90+ // An unsignalled fence of a render whose commit failed.
91+ fence, w, err := os.Pipe()
92+ if err != nil {
93+ t.Fatal(err)
94+ }
95+ defer fence.Close()
96+ defer w.Close()
97+ o.holdRead(fence)
98+ defer o.dropRead()
99+ overlayFrames(t, o, r, s, c, 1)
100+}
101+
102+// A frame whose buffer KMS took before is not probed again.
103+func TestOverlayProbeCached(t *testing.T) {
104+ o, k, commits := overlayOutput(t)
105+ o.cursor = nil
106+ k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once()
107+ o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
108+ s, c := overlayScene()
109+ ov, _ := o.overlayFrame(s, c)
110+ for range 3 {
111+ if !o.testOverlay(70, ov) {
112+ t.Fatal("refused")
113+ }
114+ }
115+ if len(*commits) != 1 {
116+ t.Fatalf("%d probes, want 1", len(*commits))
117+ }
118+ // Another place, primary image or cursor is probed.
119+ moved := ov
120+ moved.rect.X++
121+ o.testOverlay(70, moved)
122+ o.testOverlay(71, ov)
123+ if len(*commits) != 3 {
124+ t.Fatalf("%d probes, want 3", len(*commits))
125+ }
126+ // A modeset forgets them.
127+ o.overlayPassed = [2]overlayProbe{}
128+ o.testOverlay(70, ov)
129+ if len(*commits) != 4 {
130+ t.Fatalf("%d probes after forgetting", len(*commits))
131+ }
132+}
133+
134+// Anything but the overlay window changing composes: another window's
135+// content, the scene, or an image drawn by another path.
136+func TestSubmitFrameOverlayOnlyNeedsSameImage(t *testing.T) {
137+ for name, edit := range map[string]func(o *Output, s *ports.Scene, c map[ports.WindowID]ports.SurfaceContent){
138+ "other window content": func(_ *Output, _ *ports.Scene, c map[ports.WindowID]ports.SurfaceContent) {
139+ w := c[1]
140+ w.Seq++
141+ c[1] = w
142+ },
143+ "scene": func(_ *Output, s *ports.Scene, _ map[ports.WindowID]ports.SurfaceContent) {
144+ s.Background = "#ffffff"
145+ },
146+ "image forgotten": func(o *Output, _ *ports.Scene, _ map[ports.WindowID]ports.SurfaceContent) {
147+ o.front.forget()
148+ },
149+ } {
150+ t.Run(name, func(t *testing.T) {
151+ o, k, _ := overlayOutput(t)
152+ o.cursor, o.vrrProp = nil, 0
153+ k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Maybe()
154+ o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
155+ s, c := overlayScene()
156+ c[1] = ports.SurfaceContent{ID: 1, Seq: 1, Width: 100, Height: 100, Opaque: true, Solid: &ports.SolidColor{A: 1}}
157+ r := portsmocks.NewMockRenderer(t)
158+ r.EXPECT().UseTarget(mock.Anything).Return().Twice()
159+ var drawn []int
160+ r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(s ports.Scene, _ map[ports.WindowID]ports.SurfaceContent) (*os.File, error) {
161+ drawn = append(drawn, len(s.Windows))
162+ return nil, nil
163+ }).Twice()
164+ overlayFrames(t, o, r, s, c, 1)
165+ edit(o, &s, c)
166+ overlayFrames(t, o, r, s, c, 1)
167+ if len(drawn) != 2 || drawn[1] != 1 {
168+ t.Fatalf("rendered %v", drawn)
169+ }
170+ })
171+ }
172+}
M internal/adapters/drm/images.go +2 -0
@@ -237,6 +237,8 @@ func (o *Output) exportImages(r ports.Renderer, mods []uint64, singlePlane bool)
237237
238238 // freeImages removes the output images' framebuffers.
239239 func (o *Output) freeImages() {
240+ o.front.forget()
241+ o.overlayPassed = [2]overlayProbe{}
240242 for i, fb := range o.fbs {
241243 if fb != 0 {
242244 _ = o.k.rmFB(fb)
@@ -237,6 +237,8 @@ func (o *Output) exportImages(r ports.Renderer, mods []uint64, singlePlane bool)
237 237
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()
241+ o.overlayPassed = [2]overlayProbe{}
240 for i, fb := range o.fbs {242 for i, fb := range o.fbs {
241 if fb != 0 {243 if fb != 0 {
242 _ = o.k.rmFB(fb)244 _ = o.k.rmFB(fb)
M internal/adapters/drm/output.go +7 -0
@@ -147,6 +147,11 @@ type Output struct {
147147 frameReq, stateReq, withoutReq, probeReq atomicReq
148148 // restWindows is overlayFrame's scene windows, valid until its next call.
149149 restWindows []ports.SceneWindow
150+ // front is what the output image on screen holds (front.go).
151+ front frontImage
152+ // overlayPassed are the last overlay probes KMS took (testOverlay).
153+ overlayPassed [2]overlayProbe
154+ overlayNext int
150155 // lastFrame is when the last frame was committed. cursorHeld: a cursor
151156 // move waits for the next frame (see cursorWaits).
152157 lastFrame time.Time
@@ -437,6 +442,8 @@ func (o *Output) modesetImage(fb uint32, active bool) error {
437442 for _, fb := range o.clientFBs {
438443 fb.scaleTestedOK, fb.scaleRefused = false, false
439444 }
445+ o.front.forget()
446+ o.overlayPassed = [2]overlayProbe{}
440447 o.forgetColor()
441448 o.forgetColorVerdicts()
442449 o.shown, o.queued = 0, 0
@@ -147,6 +147,11 @@ type Output struct {
147 frameReq, stateReq, withoutReq, probeReq atomicReq147 frameReq, stateReq, withoutReq, probeReq atomicReq
148 // restWindows is overlayFrame's scene windows, valid until its next call.148 // restWindows is overlayFrame's scene windows, valid until its next call.
149 restWindows []ports.SceneWindow149 restWindows []ports.SceneWindow
150+ // front is what the output image on screen holds (front.go).
151+ front frontImage
152+ // overlayPassed are the last overlay probes KMS took (testOverlay).
153+ overlayPassed [2]overlayProbe
154+ overlayNext int
150 // lastFrame is when the last frame was committed. cursorHeld: a cursor155 // lastFrame is when the last frame was committed. cursorHeld: a cursor
151 // move waits for the next frame (see cursorWaits).156 // move waits for the next frame (see cursorWaits).
152 lastFrame time.Time157 lastFrame time.Time
@@ -437,6 +442,8 @@ func (o *Output) modesetImage(fb uint32, active bool) error {
437 for _, fb := range o.clientFBs {442 for _, fb := range o.clientFBs {
438 fb.scaleTestedOK, fb.scaleRefused = false, false443 fb.scaleTestedOK, fb.scaleRefused = false, false
439 }444 }
445+ o.front.forget()
446+ o.overlayPassed = [2]overlayProbe{}
440 o.forgetColor()447 o.forgetColor()
441 o.forgetColorVerdicts()448 o.forgetColorVerdicts()
442 o.shown, o.queued = 0, 0449 o.shown, o.queued = 0, 0
M internal/adapters/drm/overlay.go +30 -0
@@ -3,6 +3,7 @@ package drm
33 import (
44 "errors"
55 "os"
6+ "slices"
67
78 "github.com/bnema/neferwl/internal/ports"
89 "golang.org/x/sys/unix"
@@ -231,9 +232,19 @@ func (ov overlayWin) close() {
231232 // buffer; one that the plane's colour pipeline causes (the same frame passes
232233 // with Bypass) is cached on the plane for the buffer format and the cursor
233234 // state instead.
235+//
236+// A frame identical to one KMS took (same images, place, colour mode and
237+// cursor; the fence does not count) is not probed again: a refusal of the
238+// commit itself still drops the overlay (overlayConflict).
234239 func (o *Output) testOverlay(fb uint32, ov overlayWin) bool {
240+ key := o.overlayProbeOf(fb, ov)
241+ if slices.Contains(o.overlayPassed[:], key) {
242+ return true
243+ }
235244 err := o.probeOverlay(fb, ov, ov.color.mode)
236245 if err == nil {
246+ o.overlayPassed[o.overlayNext] = key
247+ o.overlayNext = 1 - o.overlayNext
237248 return true
238249 }
239250 if ov.color.mode != colorBypass && refused(err) && o.probeOverlay(fb, ov, colorBypass) == nil {
@@ -250,6 +261,25 @@ func (o *Output) testOverlay(fb uint32, ov overlayWin) bool {
250261 return false
251262 }
252263
264+// overlayProbe is what an overlay probe depends on, fences aside.
265+type overlayProbe struct {
266+ primary, overlay uint32
267+ rect ports.Rect
268+ w, h int
269+ color colorUse
270+ cursor cursorState
271+ mult uint64
272+ ctm uint32
273+}
274+
275+func (o *Output) overlayProbeOf(fb uint32, ov overlayWin) overlayProbe {
276+ 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}
277+ if o.cursor != nil {
278+ p.cursor = o.cursor.desired()
279+ }
280+ return p
281+}
282+
253283 // probeOverlay is a TEST_ONLY of the frame: the composed image on the
254284 // primary plane (Bypass), ov on the overlay with colour mode, the cursor.
255285 // Both planes' colour state is written whatever they applied: the test must
@@ -3,6 +3,7 @@ package drm
3 import (3 import (
4 "errors"4 "errors"
5 "os"5 "os"
6+ "slices"
6 7
7 "github.com/bnema/neferwl/internal/ports"8 "github.com/bnema/neferwl/internal/ports"
8 "golang.org/x/sys/unix"9 "golang.org/x/sys/unix"
@@ -231,9 +232,19 @@ func (ov overlayWin) close() {
231 // buffer; one that the plane's colour pipeline causes (the same frame passes232 // buffer; one that the plane's colour pipeline causes (the same frame passes
232 // with Bypass) is cached on the plane for the buffer format and the cursor233 // with Bypass) is cached on the plane for the buffer format and the cursor
233 // state instead.234 // state instead.
235+//
236+// A frame identical to one KMS took (same images, place, colour mode and
237+// cursor; the fence does not count) is not probed again: a refusal of the
238+// commit itself still drops the overlay (overlayConflict).
234 func (o *Output) testOverlay(fb uint32, ov overlayWin) bool {239 func (o *Output) testOverlay(fb uint32, ov overlayWin) bool {
240+ key := o.overlayProbeOf(fb, ov)
241+ if slices.Contains(o.overlayPassed[:], key) {
242+ return true
243+ }
235 err := o.probeOverlay(fb, ov, ov.color.mode)244 err := o.probeOverlay(fb, ov, ov.color.mode)
236 if err == nil {245 if err == nil {
246+ o.overlayPassed[o.overlayNext] = key
247+ o.overlayNext = 1 - o.overlayNext
237 return true248 return true
238 }249 }
239 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 {
@@ -250,6 +261,25 @@ func (o *Output) testOverlay(fb uint32, ov overlayWin) bool {
250 return false261 return false
251 }262 }
252 263
264+// overlayProbe is what an overlay probe depends on, fences aside.
265+type overlayProbe struct {
266+ primary, overlay uint32
267+ rect ports.Rect
268+ w, h int
269+ color colorUse
270+ cursor cursorState
271+ mult uint64
272+ ctm uint32
273+}
274+
275+func (o *Output) overlayProbeOf(fb uint32, ov overlayWin) overlayProbe {
276+ 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}
277+ if o.cursor != nil {
278+ p.cursor = o.cursor.desired()
279+ }
280+ return p
281+}
282+
253 // probeOverlay is a TEST_ONLY of the frame: the composed image on the283 // probeOverlay is a TEST_ONLY of the frame: the composed image on the
254 // primary plane (Bypass), ov on the overlay with colour mode, the cursor.284 // primary plane (Bypass), ov on the overlay with colour mode, the cursor.
255 // Both planes' colour state is written whatever they applied: the test must285 // Both planes' colour state is written whatever they applied: the test must
M internal/adapters/drm/protection.go +1 -0
@@ -203,6 +203,7 @@ func (o *Output) protectedModeset(ctx context.Context, r ports.Renderer) error {
203203 if err := o.modesetImage(o.fbs[o.back], true); err != nil {
204204 return err
205205 }
206+ o.front.forget()
206207 o.back = 1 - o.back
207208 o.protectSucceeded()
208209 return nil
@@ -203,6 +203,7 @@ func (o *Output) protectedModeset(ctx context.Context, r ports.Renderer) error {
203 if err := o.modesetImage(o.fbs[o.back], true); err != nil {203 if err := o.modesetImage(o.fbs[o.back], true); err != nil {
204 return err204 return err
205 }205 }
206+ o.front.forget()
206 o.back = 1 - o.back207 o.back = 1 - o.back
207 o.protectSucceeded()208 o.protectSucceeded()
208 return nil209 return nil
M internal/adapters/drm/scanout.go +2 -0
@@ -587,6 +587,8 @@ func (o *Output) dropClientFBs(now time.Time, all bool) {
587587 }
588588 if fb.fbID != 0 {
589589 _ = o.k.rmFB(fb.fbID)
590+ // KMS may give the id to another buffer.
591+ o.overlayPassed = [2]overlayProbe{}
590592 }
591593 delete(o.clientFBs, id)
592594 }
@@ -587,6 +587,8 @@ func (o *Output) dropClientFBs(now time.Time, all bool) {
587 }587 }
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.
591+ o.overlayPassed = [2]overlayProbe{}
590 }592 }
591 delete(o.clientFBs, id)593 delete(o.clientFBs, id)
592 }594 }
M internal/adapters/drm/security.go +1 -0
@@ -145,6 +145,7 @@ func (o *Output) submitProtectedFrame(ctx context.Context, r ports.Renderer, sce
145145 done.Close()
146146 }
147147 if err == nil {
148+ o.front.forget()
148149 o.back = 1 - o.back
149150 }
150151 return err
@@ -145,6 +145,7 @@ func (o *Output) submitProtectedFrame(ctx context.Context, r ports.Renderer, sce
145 done.Close()145 done.Close()
146 }146 }
147 if err == nil {147 if err == nil {
148+ o.front.forget()
148 o.back = 1 - o.back149 o.back = 1 - o.back
149 }150 }
150 return err151 return err

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