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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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. | |||
| 89 | 89 | decision = o.composeFrame(scene, surfaces) | |
| 90 | 90 | } | |
| 91 | 91 | 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 | 108 | r.UseTarget(o.back) | |
| 93 | 109 | // Captures never show the capture indicator: their frame goes first. | |
| 94 | 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 | 230 | if err == nil { | |
| 215 | 231 | 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 | 239 | o.back = 1 - o.back | |
| 217 | 240 | } | |
| 218 | 241 | 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 | + | } | |
| @@ -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.composed | 91 | 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.buf | 231 | 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.back | 239 | o.back = 1 - o.back | ||
| 217 | } | 240 | } | ||
| 218 | return false, err | 241 | 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) | |||
| 237 | 237 | ||
| 238 | 238 | // freeImages removes the output images' framebuffers. | |
| 239 | 239 | func (o *Output) freeImages() { | |
| 240 | + | o.front.forget() | |
| 241 | + | o.overlayPassed = [2]overlayProbe{} | |
| 240 | 242 | for i, fb := range o.fbs { | |
| 241 | 243 | if fb != 0 { | |
| 242 | 244 | _ = 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 { | |||
| 147 | 147 | frameReq, stateReq, withoutReq, probeReq atomicReq | |
| 148 | 148 | // restWindows is overlayFrame's scene windows, valid until its next call. | |
| 149 | 149 | 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 | 155 | // lastFrame is when the last frame was committed. cursorHeld: a cursor | |
| 151 | 156 | // move waits for the next frame (see cursorWaits). | |
| 152 | 157 | lastFrame time.Time | |
| @@ -437,6 +442,8 @@ func (o *Output) modesetImage(fb uint32, active bool) error { | |||
| 437 | 442 | for _, fb := range o.clientFBs { | |
| 438 | 443 | fb.scaleTestedOK, fb.scaleRefused = false, false | |
| 439 | 444 | } | |
| 445 | + | o.front.forget() | |
| 446 | + | o.overlayPassed = [2]overlayProbe{} | |
| 440 | 447 | o.forgetColor() | |
| 441 | 448 | o.forgetColorVerdicts() | |
| 442 | 449 | o.shown, o.queued = 0, 0 | |
| @@ -147,6 +147,11 @@ type Output struct { | |||||
| 147 | frameReq, stateReq, withoutReq, probeReq atomicReq | 147 | 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.SceneWindow | 149 | 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 cursor | 155 | // 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.Time | 157 | 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, false | 443 | 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, 0 | 449 | o.shown, o.queued = 0, 0 | ||
M internal/adapters/drm/overlay.go +30 -0
| @@ -3,6 +3,7 @@ package drm | |||
| 3 | 3 | import ( | |
| 4 | 4 | "errors" | |
| 5 | 5 | "os" | |
| 6 | + | "slices" | |
| 6 | 7 | ||
| 7 | 8 | "github.com/bnema/neferwl/internal/ports" | |
| 8 | 9 | "golang.org/x/sys/unix" | |
| @@ -231,9 +232,19 @@ func (ov overlayWin) close() { | |||
| 231 | 232 | // buffer; one that the plane's colour pipeline causes (the same frame passes | |
| 232 | 233 | // with Bypass) is cached on the plane for the buffer format and the cursor | |
| 233 | 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 | 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 | 244 | err := o.probeOverlay(fb, ov, ov.color.mode) | |
| 236 | 245 | if err == nil { | |
| 246 | + | o.overlayPassed[o.overlayNext] = key | |
| 247 | + | o.overlayNext = 1 - o.overlayNext | |
| 237 | 248 | return true | |
| 238 | 249 | } | |
| 239 | 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 | 261 | 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 | 283 | // probeOverlay is a TEST_ONLY of the frame: the composed image on the | |
| 254 | 284 | // primary plane (Bypass), ov on the overlay with colour mode, the cursor. | |
| 255 | 285 | // 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 passes | 232 | // 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 cursor | 233 | // 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 true | 248 | 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 false | 261 | 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 the | 283 | // 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 must | 285 | // 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 { | |||
| 203 | 203 | if err := o.modesetImage(o.fbs[o.back], true); err != nil { | |
| 204 | 204 | return err | |
| 205 | 205 | } | |
| 206 | + | o.front.forget() | |
| 206 | 207 | o.back = 1 - o.back | |
| 207 | 208 | o.protectSucceeded() | |
| 208 | 209 | 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 err | 204 | return err | ||
| 205 | } | 205 | } | ||
| 206 | + | o.front.forget() | |||
| 206 | o.back = 1 - o.back | 207 | o.back = 1 - o.back | ||
| 207 | o.protectSucceeded() | 208 | o.protectSucceeded() | ||
| 208 | return nil | 209 | return nil | ||
M internal/adapters/drm/scanout.go +2 -0
| @@ -587,6 +587,8 @@ func (o *Output) dropClientFBs(now time.Time, all bool) { | |||
| 587 | 587 | } | |
| 588 | 588 | if fb.fbID != 0 { | |
| 589 | 589 | _ = o.k.rmFB(fb.fbID) | |
| 590 | + | // KMS may give the id to another buffer. | |
| 591 | + | o.overlayPassed = [2]overlayProbe{} | |
| 590 | 592 | } | |
| 591 | 593 | delete(o.clientFBs, id) | |
| 592 | 594 | } | |
| @@ -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 | |||
| 145 | 145 | done.Close() | |
| 146 | 146 | } | |
| 147 | 147 | if err == nil { | |
| 148 | + | o.front.forget() | |
| 148 | 149 | o.back = 1 - o.back | |
| 149 | 150 | } | |
| 150 | 151 | 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.back | 149 | o.back = 1 - o.back | ||
| 149 | } | 150 | } | ||
| 150 | return err | 151 | return err | ||