Merge pull request 'perf(drm): flip only the overlay buffer when the composed image is unchanged' (#49) from perf/overlay-flip into main
Reviewed-on: https://git.bnema.dev/bnema/neferwl/pulls/49
9 files · +666 -14
M internal/adapters/drm/frame_submit.go +50 -6
| @@ -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) | |
| @@ -203,17 +219,45 @@ func (o *Output) submitFrame(ctx context.Context, r ports.Renderer, scene ports. | |||
| 203 | 219 | done.Close() | |
| 204 | 220 | } | |
| 205 | 221 | ov.close() | |
| 222 | + | if err = o.finishOverlayCommit(err, ov.buf); err == nil { | |
| 223 | + | // The image now holds what was drawn last: composed, or the whole | |
| 224 | + | // scene after an overlay refusal. | |
| 225 | + | if ov.fb != 0 { | |
| 226 | + | o.front.hold(o.fbs[o.back], composed, surfaces) | |
| 227 | + | } else { | |
| 228 | + | o.front.hold(o.fbs[o.back], scene, surfaces) | |
| 229 | + | } | |
| 230 | + | o.back = 1 - o.back | |
| 231 | + | } | |
| 232 | + | return false, err | |
| 233 | + | } | |
| 234 | + | ||
| 235 | + | // commitOverlayOnly commits front, the image on screen, again with ov on | |
| 236 | + | // the overlay plane: no render, no GPU fence. It closes ov's fence. | |
| 237 | + | func (o *Output) commitOverlayOnly(front uint32, scene ports.Scene, f pendingFrame, ov overlayWin) error { | |
| 238 | + | defer ov.close() | |
| 239 | + | if !o.sceneCurrent(scene) || o.protected { | |
| 240 | + | return errSecurityScene | |
| 241 | + | } | |
| 242 | + | f.queued, f.zeroCopy = ov.buf, ov.id | |
| 243 | + | game := o.vrrProp != 0 && fullscreenShown(&scene) | |
| 244 | + | return o.finishOverlayCommit(o.commitWith(front, nil, false, o.wantVRR(game), f, ov), ov.buf) | |
| 245 | + | } | |
| 246 | + | ||
| 247 | + | // finishOverlayCommit applies the result of a frame commit with buf on the | |
| 248 | + | // overlay (0: none): a refusal the overlay causes asks for a frame without | |
| 249 | + | // it (errOverlayDropped); a commit marks buf queued. | |
| 250 | + | func (o *Output) finishOverlayCommit(err error, buf uint64) error { | |
| 206 | 251 | if errors.Is(err, errColorRefused) { | |
| 207 | 252 | // Cached for the overlay's buffer format: the next frame composes it. | |
| 208 | 253 | o.setOverlayReason("color_refused") | |
| 209 | - | return false, errOverlayDropped | |
| 254 | + | return errOverlayDropped | |
| 210 | 255 | } | |
| 211 | - | if err != nil && o.overlayConflict(err, ov.buf) { | |
| 212 | - | return false, errOverlayDropped | |
| 256 | + | if err != nil && o.overlayConflict(err, buf) { | |
| 257 | + | return errOverlayDropped | |
| 213 | 258 | } | |
| 214 | 259 | if err == nil { | |
| 215 | - | o.queued = ov.buf | |
| 216 | - | o.back = 1 - o.back | |
| 260 | + | o.queued = buf | |
| 217 | 261 | } | |
| 218 | - | return false, err | |
| 262 | + | return err | |
| 219 | 263 | } | |
| @@ -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) | ||
| @@ -203,17 +219,45 @@ func (o *Output) submitFrame(ctx context.Context, r ports.Renderer, scene ports. | |||||
| 203 | done.Close() | 219 | done.Close() | ||
| 204 | } | 220 | } | ||
| 205 | ov.close() | 221 | ov.close() | ||
| 222 | + | if err = o.finishOverlayCommit(err, ov.buf); err == nil { | |||
| 223 | + | // The image now holds what was drawn last: composed, or the whole | |||
| 224 | + | // scene after an overlay refusal. | |||
| 225 | + | if ov.fb != 0 { | |||
| 226 | + | o.front.hold(o.fbs[o.back], composed, surfaces) | |||
| 227 | + | } else { | |||
| 228 | + | o.front.hold(o.fbs[o.back], scene, surfaces) | |||
| 229 | + | } | |||
| 230 | + | o.back = 1 - o.back | |||
| 231 | + | } | |||
| 232 | + | return false, err | |||
| 233 | + | } | |||
| 234 | + | ||||
| 235 | + | // commitOverlayOnly commits front, the image on screen, again with ov on | |||
| 236 | + | // the overlay plane: no render, no GPU fence. It closes ov's fence. | |||
| 237 | + | func (o *Output) commitOverlayOnly(front uint32, scene ports.Scene, f pendingFrame, ov overlayWin) error { | |||
| 238 | + | defer ov.close() | |||
| 239 | + | if !o.sceneCurrent(scene) || o.protected { | |||
| 240 | + | return errSecurityScene | |||
| 241 | + | } | |||
| 242 | + | f.queued, f.zeroCopy = ov.buf, ov.id | |||
| 243 | + | game := o.vrrProp != 0 && fullscreenShown(&scene) | |||
| 244 | + | return o.finishOverlayCommit(o.commitWith(front, nil, false, o.wantVRR(game), f, ov), ov.buf) | |||
| 245 | + | } | |||
| 246 | + | ||||
| 247 | + | // finishOverlayCommit applies the result of a frame commit with buf on the | |||
| 248 | + | // overlay (0: none): a refusal the overlay causes asks for a frame without | |||
| 249 | + | // it (errOverlayDropped); a commit marks buf queued. | |||
| 250 | + | func (o *Output) finishOverlayCommit(err error, buf uint64) error { | |||
| 206 | if errors.Is(err, errColorRefused) { | 251 | if errors.Is(err, errColorRefused) { | ||
| 207 | // Cached for the overlay's buffer format: the next frame composes it. | 252 | // Cached for the overlay's buffer format: the next frame composes it. | ||
| 208 | o.setOverlayReason("color_refused") | 253 | o.setOverlayReason("color_refused") | ||
| 209 | - | return false, errOverlayDropped | 254 | + | return errOverlayDropped |
| 210 | } | 255 | } | ||
| 211 | - | if err != nil && o.overlayConflict(err, ov.buf) { | 256 | + | if err != nil && o.overlayConflict(err, buf) { |
| 212 | - | return false, errOverlayDropped | 257 | + | return errOverlayDropped |
| 213 | } | 258 | } | ||
| 214 | if err == nil { | 259 | if err == nil { | ||
| 215 | - | o.queued = ov.buf | 260 | + | o.queued = buf |
| 216 | - | o.back = 1 - o.back | |||
| 217 | } | 261 | } | ||
| 218 | - | return false, err | 262 | + | return err |
| 219 | } | 263 | } | ||
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 +479 -0
| @@ -0,0 +1,479 @@ | |||
| 1 | + | package drm | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "errors" | |
| 6 | + | "os" | |
| 7 | + | "sync/atomic" | |
| 8 | + | "testing" | |
| 9 | + | "time" | |
| 10 | + | ||
| 11 | + | portsmocks "github.com/bnema/neferwl/internal/mocks/ports" | |
| 12 | + | "github.com/bnema/neferwl/internal/ports" | |
| 13 | + | "github.com/stretchr/testify/mock" | |
| 14 | + | "golang.org/x/sys/unix" | |
| 15 | + | ) | |
| 16 | + | ||
| 17 | + | // overlayFrames submits the overlay scene once (composed), then with the | |
| 18 | + | // overlay window's next buffers: the frames after the first only flip the | |
| 19 | + | // overlay buffer. | |
| 20 | + | func overlayFrames(t *testing.T, o *Output, r *portsmocks.MockRenderer, s ports.Scene, c map[ports.WindowID]ports.SurfaceContent, frames int) { | |
| 21 | + | t.Helper() | |
| 22 | + | for i := range frames { | |
| 23 | + | win := c[2] | |
| 24 | + | buf := *win.DMABuf | |
| 25 | + | buf.ID = uint64(9 + i) | |
| 26 | + | win.DMABuf, win.Seq = &buf, uint64(1+i) | |
| 27 | + | c[2] = win | |
| 28 | + | if _, err := o.submitFrame(context.Background(), r, s, c, map[ports.WindowID]uint64{}, nil, nil); err != nil { | |
| 29 | + | t.Fatalf("frame %d: %v", i, err) | |
| 30 | + | } | |
| 31 | + | o.frame.endPending() | |
| 32 | + | } | |
| 33 | + | } | |
| 34 | + | ||
| 35 | + | // When only the overlay window's buffer changes, the image on screen is | |
| 36 | + | // committed again with the new buffer: no render, no probe once KMS took | |
| 37 | + | // the same frame. | |
| 38 | + | func TestSubmitFrameOverlayOnlySkipsRender(t *testing.T) { | |
| 39 | + | o, k, commits := overlayOutput(t) | |
| 40 | + | o.cursor, o.vrrProp = nil, 0 | |
| 41 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).RunAndReturn(func(b *ports.DMABuf, _ uint32) (uint32, error) { | |
| 42 | + | return uint32(80 + b.ID), nil | |
| 43 | + | }).Times(4) | |
| 44 | + | o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}} | |
| 45 | + | s, c := overlayScene() | |
| 46 | + | r := portsmocks.NewMockRenderer(t) | |
| 47 | + | r.EXPECT().UseTarget(0).Return().Once() | |
| 48 | + | r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil).Once() | |
| 49 | + | overlayFrames(t, o, r, s, c, 4) | |
| 50 | + | var probes, frames int | |
| 51 | + | for _, cm := range *commits { | |
| 52 | + | if cm.flags&atomicTestOnly != 0 { | |
| 53 | + | probes++ | |
| 54 | + | continue | |
| 55 | + | } | |
| 56 | + | frames++ | |
| 57 | + | if v, _ := cm.req.value(tPrimary, pFB); v != 70 { | |
| 58 | + | t.Fatalf("frame %d primary fb %d, want the composed image 70", frames, v) | |
| 59 | + | } | |
| 60 | + | if _, ok := cm.req.value(tPrimary, pFence); ok { | |
| 61 | + | t.Fatalf("frame %d waits on a render fence", frames) | |
| 62 | + | } | |
| 63 | + | } | |
| 64 | + | if frames != 4 { | |
| 65 | + | t.Fatalf("%d frame commits", frames) | |
| 66 | + | } | |
| 67 | + | // Each new overlay buffer is probed once; nothing else is. | |
| 68 | + | if probes != 4 { | |
| 69 | + | t.Fatalf("%d probes", probes) | |
| 70 | + | } | |
| 71 | + | if last := (*commits)[len(*commits)-1]; o.queued != 12 { | |
| 72 | + | t.Fatalf("queued %d", o.queued) | |
| 73 | + | } else if v, _ := last.req.value(tOverlay, pFB); v != 92 { | |
| 74 | + | t.Fatalf("overlay fb %d", v) | |
| 75 | + | } | |
| 76 | + | if o.back != 1 { | |
| 77 | + | t.Fatalf("back %d: the skipped frames drew nothing", o.back) | |
| 78 | + | } | |
| 79 | + | } | |
| 80 | + | ||
| 81 | + | // An uncommitted render that may still read client buffers forces | |
| 82 | + | // composition: the overlay-only flip carries no fence to order it after | |
| 83 | + | // those reads, and its report would let wayland release them. | |
| 84 | + | func TestSubmitFrameOverlayOnlyWaitsForHeldReads(t *testing.T) { | |
| 85 | + | o, k, _ := overlayOutput(t) | |
| 86 | + | o.cursor, o.vrrProp = nil, 0 | |
| 87 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Maybe() | |
| 88 | + | o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}} | |
| 89 | + | s, c := overlayScene() | |
| 90 | + | r := portsmocks.NewMockRenderer(t) | |
| 91 | + | r.EXPECT().UseTarget(mock.Anything).Return().Twice() | |
| 92 | + | r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil).Twice() | |
| 93 | + | overlayFrames(t, o, r, s, c, 1) | |
| 94 | + | // An unsignalled fence of a render whose commit failed. | |
| 95 | + | fence, w, err := os.Pipe() | |
| 96 | + | if err != nil { | |
| 97 | + | t.Fatal(err) | |
| 98 | + | } | |
| 99 | + | defer fence.Close() | |
| 100 | + | defer w.Close() | |
| 101 | + | o.holdRead(fence) | |
| 102 | + | defer o.dropRead() | |
| 103 | + | overlayFrames(t, o, r, s, c, 1) | |
| 104 | + | } | |
| 105 | + | ||
| 106 | + | // The desktop case through Run: a video (a dmabuf window on the overlay) | |
| 107 | + | // next to a static window. Once the overlay is up, each new video buffer is | |
| 108 | + | // one frame commit of the same composed image with no render, and each flip | |
| 109 | + | // reports the video shown zero-copy at its latest content. | |
| 110 | + | func TestRunVideoOnOverlayDoesNotRender(t *testing.T) { | |
| 111 | + | o, k, commits, commitMu := testOutputMu(t) | |
| 112 | + | o.cursor, o.tearing, o.vrrProp = nil, false, 0 | |
| 113 | + | props := map[string]uint32{"zpos": pZpos} | |
| 114 | + | for n, v := range planeProps { | |
| 115 | + | props[n] = v | |
| 116 | + | } | |
| 117 | + | o.primary.props = props | |
| 118 | + | o.stray = []*plane{{id: tOverlay, typ: planeOverlay, props: props, zposValue: 2, formats: []ports.DMABufFormat{{Format: fourccXRGB}}}} | |
| 119 | + | o.primary.formats = nil | |
| 120 | + | o.pickOverlay() | |
| 121 | + | flips := make(chan flipEvent, 1) | |
| 122 | + | o.flipped = flips | |
| 123 | + | r := portsmocks.NewMockRenderer(t) | |
| 124 | + | pipeBuf := func() ports.DMABuf { | |
| 125 | + | f, w, err := os.Pipe() | |
| 126 | + | if err != nil { | |
| 127 | + | t.Fatal(err) | |
| 128 | + | } | |
| 129 | + | w.Close() | |
| 130 | + | return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}} | |
| 131 | + | } | |
| 132 | + | r.EXPECT().SetHDR(float64(0)).Return().Maybe() | |
| 133 | + | r.EXPECT().ExportTargets(2, mock.Anything, false).Return([]ports.DMABuf{pipeBuf(), pipeBuf()}, nil).Once() | |
| 134 | + | r.EXPECT().UseTarget(mock.Anything).Return() | |
| 135 | + | var renders atomic.Int32 | |
| 136 | + | r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(ports.Scene, map[ports.WindowID]ports.SurfaceContent) (*os.File, error) { | |
| 137 | + | renders.Add(1) | |
| 138 | + | return nil, nil | |
| 139 | + | }) | |
| 140 | + | r.EXPECT().Close().Return().Once() | |
| 141 | + | var images atomic.Uint32 | |
| 142 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).RunAndReturn(func(b *ports.DMABuf, _ uint32) (uint32, error) { | |
| 143 | + | if b.ID == 0 { | |
| 144 | + | return 70 + images.Add(1) - 1, nil // the output images | |
| 145 | + | } | |
| 146 | + | return uint32(100 + b.ID), nil | |
| 147 | + | }) | |
| 148 | + | k.EXPECT().createBlob(mock.Anything).Return(99, nil) | |
| 149 | + | k.EXPECT().destroyBlob(mock.Anything).Return(nil).Maybe() | |
| 150 | + | k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe() | |
| 151 | + | k.EXPECT().objProps(uint32(tOverlay), uint32(objPlane)).Return(map[string][2]uint64{}, nil).Maybe() | |
| 152 | + | frameCommits := func() []commitRec { | |
| 153 | + | commitMu.Lock() | |
| 154 | + | defer commitMu.Unlock() | |
| 155 | + | var out []commitRec | |
| 156 | + | for _, c := range *commits { | |
| 157 | + | if c.user&3 == userFrame { | |
| 158 | + | out = append(out, c) | |
| 159 | + | } | |
| 160 | + | } | |
| 161 | + | return out | |
| 162 | + | } | |
| 163 | + | scenes := make(chan ports.Scene, 1) | |
| 164 | + | contents := newContentFeed(t, 1) | |
| 165 | + | sink, presented := presentedSink(t, 64) | |
| 166 | + | ctx, cancel := context.WithCancel(context.Background()) | |
| 167 | + | done := make(chan error, 1) | |
| 168 | + | go func() { | |
| 169 | + | 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) | |
| 170 | + | }() | |
| 171 | + | // Stop Run on every path, before the mocks check their calls. | |
| 172 | + | t.Cleanup(func() { | |
| 173 | + | cancel() | |
| 174 | + | select { | |
| 175 | + | case err := <-done: | |
| 176 | + | if err != nil { | |
| 177 | + | t.Error(err) | |
| 178 | + | } | |
| 179 | + | case <-time.After(2 * time.Second): | |
| 180 | + | t.Error("Run did not stop") | |
| 181 | + | } | |
| 182 | + | }) | |
| 183 | + | static := ports.SurfaceContent{ID: 1, Seq: 1, Width: 100, Height: 100, Opaque: true, Solid: &ports.SolidColor{A: 1}} | |
| 184 | + | video := func(n int) ports.SurfaceContent { | |
| 185 | + | return ports.SurfaceContent{ID: 2, Seq: uint64(n), Width: 100, Height: 100, Opaque: true, | |
| 186 | + | DMABuf: &ports.DMABuf{ID: uint64(n%3 + 1), Width: 100, Height: 100, Format: fourccXRGB}} | |
| 187 | + | } | |
| 188 | + | contents.send(static, video(1)) | |
| 189 | + | scenes <- ports.Scene{Seq: 1, Scale: 1, OutputWidth: 200, OutputHeight: 100, Windows: []ports.SceneWindow{ | |
| 190 | + | {ID: 1, Rect: ports.Rect{W: 100, H: 100}}, | |
| 191 | + | {ID: 2, Rect: ports.Rect{X: 100, W: 100, H: 100}}, | |
| 192 | + | }} | |
| 193 | + | // play delivers video frame n and flips every frame commit until a flip | |
| 194 | + | // shows it; it returns that commit and its report. (Seen is what the | |
| 195 | + | // output took in, which may run ahead of the flip.) | |
| 196 | + | flipped := 0 | |
| 197 | + | vseq := uint32(0) | |
| 198 | + | play := func(n int) (commitRec, ports.OutputPresented) { | |
| 199 | + | t.Helper() | |
| 200 | + | contents.send(video(n)) | |
| 201 | + | var c commitRec | |
| 202 | + | var got ports.OutputPresented | |
| 203 | + | waitFor(t, func() bool { | |
| 204 | + | if cs := frameCommits(); len(cs) > flipped { | |
| 205 | + | c = cs[len(cs)-1] | |
| 206 | + | flipped = len(cs) | |
| 207 | + | vseq++ | |
| 208 | + | flips <- flipEvent{crtc: tCrtc, user: c.user, when: time.Duration(vseq) * time.Millisecond, seq: vseq} | |
| 209 | + | } | |
| 210 | + | for { | |
| 211 | + | select { | |
| 212 | + | case p := <-presented: | |
| 213 | + | if p.Flip != nil && p.Flip.Shows[2] == uint64(n) { | |
| 214 | + | got = p | |
| 215 | + | return true | |
| 216 | + | } | |
| 217 | + | default: | |
| 218 | + | return false | |
| 219 | + | } | |
| 220 | + | } | |
| 221 | + | }) | |
| 222 | + | return c, got | |
| 223 | + | } | |
| 224 | + | // Warm up until the video is on the overlay over a composed image. | |
| 225 | + | n := 1 | |
| 226 | + | for ; n < 10; n++ { | |
| 227 | + | c, _ := play(n) | |
| 228 | + | if v, _ := c.req.value(tOverlay, pFB); v != 0 { | |
| 229 | + | break | |
| 230 | + | } | |
| 231 | + | } | |
| 232 | + | base, commitsBefore := renders.Load(), len(frameCommits()) | |
| 233 | + | const frames = 60 | |
| 234 | + | for i := range frames { | |
| 235 | + | n++ | |
| 236 | + | c, p := play(n) | |
| 237 | + | if v, _ := c.req.value(tOverlay, pFB); v != uint64(100+n%3+1) { | |
| 238 | + | t.Fatalf("video frame %d: overlay fb %d", i, v) | |
| 239 | + | } | |
| 240 | + | if _, ok := c.req.value(tPrimary, pFence); ok { | |
| 241 | + | t.Fatalf("video frame %d waits on a render", i) | |
| 242 | + | } | |
| 243 | + | // The flip reports the video shown zero-copy at its content. | |
| 244 | + | if p.Flip.ZeroCopy != 2 || p.Flip.Shows[2] != uint64(n) || p.Seen[1] != 1 { | |
| 245 | + | t.Fatalf("video frame %d: report %+v flip %+v", i, p, *p.Flip) | |
| 246 | + | } | |
| 247 | + | } | |
| 248 | + | if got := len(frameCommits()) - commitsBefore; got != frames { | |
| 249 | + | t.Fatalf("%d frame commits for %d video frames", got, frames) | |
| 250 | + | } | |
| 251 | + | if got := renders.Load() - base; got != 0 { | |
| 252 | + | t.Fatalf("%d renders for %d video frames, want 0", got, frames) | |
| 253 | + | } | |
| 254 | + | // Three buffers in rotation: each probed once, then remembered. | |
| 255 | + | commitMu.Lock() | |
| 256 | + | probes := 0 | |
| 257 | + | for _, c := range *commits { | |
| 258 | + | if c.flags&atomicTestOnly != 0 { | |
| 259 | + | probes++ | |
| 260 | + | } | |
| 261 | + | } | |
| 262 | + | commitMu.Unlock() | |
| 263 | + | if probes > 4 { | |
| 264 | + | t.Fatalf("%d overlay probes for %d video frames of 3 buffers", probes, frames) | |
| 265 | + | } | |
| 266 | + | } | |
| 267 | + | ||
| 268 | + | // Refusals on the overlay-only path: a refused probe composes the whole | |
| 269 | + | // scene at once; a refused commit drops the overlay and asks for a new frame, | |
| 270 | + | // leaving the images and the queue as they were. | |
| 271 | + | func TestSubmitFrameOverlayOnlyRefused(t *testing.T) { | |
| 272 | + | for _, tc := range []struct { | |
| 273 | + | name string | |
| 274 | + | refuse func(r *atomicReq, flags uint32) bool | |
| 275 | + | wantErr error | |
| 276 | + | renders int | |
| 277 | + | }{ | |
| 278 | + | {"probe", func(_ *atomicReq, flags uint32) bool { return flags&atomicTestOnly != 0 }, nil, 1}, | |
| 279 | + | {"commit", func(_ *atomicReq, flags uint32) bool { return flags&atomicTestOnly == 0 }, errOverlayDropped, 0}, | |
| 280 | + | } { | |
| 281 | + | t.Run(tc.name, func(t *testing.T) { | |
| 282 | + | refusing := false | |
| 283 | + | o, k, _, _ := testOutputRules(t, func(r *atomicReq, flags uint32) error { | |
| 284 | + | if refusing && tc.refuse(r, flags) { | |
| 285 | + | return unix.EINVAL | |
| 286 | + | } | |
| 287 | + | return nil | |
| 288 | + | }) | |
| 289 | + | props := map[string]uint32{"zpos": pZpos} | |
| 290 | + | for n, v := range planeProps { | |
| 291 | + | props[n] = v | |
| 292 | + | } | |
| 293 | + | o.primary.props, o.primary.formats = props, nil | |
| 294 | + | o.stray = []*plane{{id: tOverlay, typ: planeOverlay, props: props, zposValue: 2, formats: []ports.DMABufFormat{{Format: fourccXRGB}}}} | |
| 295 | + | o.pickOverlay() | |
| 296 | + | o.cursor, o.vrrProp = nil, 0 | |
| 297 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).RunAndReturn(func(b *ports.DMABuf, _ uint32) (uint32, error) { | |
| 298 | + | return uint32(80 + b.ID), nil | |
| 299 | + | }) | |
| 300 | + | s, c := overlayScene() | |
| 301 | + | r := portsmocks.NewMockRenderer(t) | |
| 302 | + | r.EXPECT().UseTarget(mock.Anything).Return() | |
| 303 | + | var drawn []int | |
| 304 | + | r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(s ports.Scene, _ map[ports.WindowID]ports.SurfaceContent) (*os.File, error) { | |
| 305 | + | drawn = append(drawn, len(s.Windows)) | |
| 306 | + | return nil, nil | |
| 307 | + | }) | |
| 308 | + | overlayFrames(t, o, r, s, c, 1) // composed, front image held | |
| 309 | + | back, queued := o.back, o.queued | |
| 310 | + | refusing = true | |
| 311 | + | win := c[2] | |
| 312 | + | buf := *win.DMABuf | |
| 313 | + | buf.ID, win.DMABuf, win.Seq = 20, &buf, 2 | |
| 314 | + | c[2] = win | |
| 315 | + | _, err := o.submitFrame(context.Background(), r, s, c, map[ports.WindowID]uint64{}, nil, nil) | |
| 316 | + | if !errors.Is(err, tc.wantErr) && err != tc.wantErr { | |
| 317 | + | t.Fatalf("err %v, want %v", err, tc.wantErr) | |
| 318 | + | } | |
| 319 | + | if got := len(drawn) - 1; got != tc.renders { | |
| 320 | + | t.Fatalf("%d renders, want %d", got, tc.renders) | |
| 321 | + | } | |
| 322 | + | if tc.renders == 1 && drawn[1] != 2 { | |
| 323 | + | t.Fatalf("composed %d windows, want the whole scene", drawn[1]) | |
| 324 | + | } | |
| 325 | + | if tc.wantErr != nil { | |
| 326 | + | if o.back != back || o.queued != queued || o.clientFBs[20].overlayFailed != "cursor_conflict" { | |
| 327 | + | t.Fatalf("back %d queued %d reason %q", o.back, o.queued, o.clientFBs[20].overlayFailed) | |
| 328 | + | } | |
| 329 | + | // The retry composes the window. | |
| 330 | + | refusing = false | |
| 331 | + | if _, err := o.submitFrame(context.Background(), r, s, c, map[ports.WindowID]uint64{}, nil, nil); err != nil { | |
| 332 | + | t.Fatal(err) | |
| 333 | + | } | |
| 334 | + | if drawn[len(drawn)-1] != 2 { | |
| 335 | + | t.Fatalf("retry composed %d windows", drawn[len(drawn)-1]) | |
| 336 | + | } | |
| 337 | + | } | |
| 338 | + | }) | |
| 339 | + | } | |
| 340 | + | } | |
| 341 | + | ||
| 342 | + | // A commit result with the overlay on: refusals the overlay causes ask for a | |
| 343 | + | // frame without it and leave the queue; a commit queues the buffer. | |
| 344 | + | func TestFinishOverlayCommit(t *testing.T) { | |
| 345 | + | boom := errors.New("boom") | |
| 346 | + | for _, tc := range []struct { | |
| 347 | + | name string | |
| 348 | + | err error | |
| 349 | + | want error | |
| 350 | + | wantQueued uint64 | |
| 351 | + | reason string | |
| 352 | + | }{ | |
| 353 | + | {"committed", nil, nil, 9, ""}, | |
| 354 | + | {"colour refused", errColorRefused, errOverlayDropped, 1, "color_refused"}, | |
| 355 | + | {"overlay conflict", unix.EINVAL, errOverlayDropped, 1, "cursor_conflict"}, | |
| 356 | + | {"other", boom, boom, 1, ""}, | |
| 357 | + | } { | |
| 358 | + | t.Run(tc.name, func(t *testing.T) { | |
| 359 | + | o, _, _ := testOutput(t) | |
| 360 | + | o.queued = 1 | |
| 361 | + | o.clientFBs[9] = &clientFB{} | |
| 362 | + | if err := o.finishOverlayCommit(tc.err, 9); err != tc.want { | |
| 363 | + | t.Fatalf("err %v, want %v", err, tc.want) | |
| 364 | + | } | |
| 365 | + | if o.queued != tc.wantQueued || o.overlayReason != tc.reason { | |
| 366 | + | t.Fatalf("queued %d reason %q", o.queued, o.overlayReason) | |
| 367 | + | } | |
| 368 | + | }) | |
| 369 | + | } | |
| 370 | + | } | |
| 371 | + | ||
| 372 | + | // A frame whose buffer KMS took before is not probed again. | |
| 373 | + | func TestOverlayProbeCached(t *testing.T) { | |
| 374 | + | o, k, commits := overlayOutput(t) | |
| 375 | + | o.cursor.image = true | |
| 376 | + | o.cursor.Move(10, 10) | |
| 377 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once() | |
| 378 | + | o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}} | |
| 379 | + | s, c := overlayScene() | |
| 380 | + | ov, _ := o.overlayFrame(s, c) | |
| 381 | + | for range 3 { | |
| 382 | + | if !o.testOverlay(70, ov) { | |
| 383 | + | t.Fatal("refused") | |
| 384 | + | } | |
| 385 | + | } | |
| 386 | + | if len(*commits) != 1 { | |
| 387 | + | t.Fatalf("%d probes, want 1", len(*commits)) | |
| 388 | + | } | |
| 389 | + | // Another place, primary image or cursor is probed. | |
| 390 | + | moved := ov | |
| 391 | + | moved.rect.X++ | |
| 392 | + | o.testOverlay(70, moved) | |
| 393 | + | o.testOverlay(71, ov) | |
| 394 | + | o.cursor.Move(20, 10) | |
| 395 | + | o.testOverlay(70, ov) | |
| 396 | + | if len(*commits) != 4 { | |
| 397 | + | t.Fatalf("%d probes, want 4", len(*commits)) | |
| 398 | + | } | |
| 399 | + | // The probe tested the cursor where the cache remembers it. | |
| 400 | + | if v, _ := (*commits)[3].req.value(tCursor, 14); v != 20-uint64(o.cursor.hotX) { | |
| 401 | + | t.Fatalf("probe cursor x %d", v) | |
| 402 | + | } | |
| 403 | + | // New colour state (HDR brightness) is probed again. | |
| 404 | + | o.colorMult++ | |
| 405 | + | o.testOverlay(70, ov) | |
| 406 | + | if len(*commits) != 5 { | |
| 407 | + | t.Fatalf("%d probes after a colour change, want 5", len(*commits)) | |
| 408 | + | } | |
| 409 | + | // A removed client framebuffer may give its id to another buffer. | |
| 410 | + | k.EXPECT().rmFB(uint32(88)).Return(nil).Once() | |
| 411 | + | o.dropClientFBs(time.Now(), true) | |
| 412 | + | o.testOverlay(70, ov) | |
| 413 | + | if len(*commits) != 6 { | |
| 414 | + | t.Fatal("probe cached across a framebuffer removal") | |
| 415 | + | } | |
| 416 | + | // A modeset (VT resume) forgets them. | |
| 417 | + | k.EXPECT().createBlob(mock.Anything).Return(99, nil).Once() | |
| 418 | + | k.EXPECT().objProps(uint32(tOverlay), uint32(objPlane)).Return(map[string][2]uint64{}, nil).Maybe() | |
| 419 | + | if err := o.modeset(); err != nil { | |
| 420 | + | t.Fatal(err) | |
| 421 | + | } | |
| 422 | + | before := len(*commits) | |
| 423 | + | o.testOverlay(70, ov) | |
| 424 | + | if len(*commits) != before+1 { | |
| 425 | + | t.Fatal("probe cached across a modeset") | |
| 426 | + | } | |
| 427 | + | } | |
| 428 | + | ||
| 429 | + | // Anything but the overlay window changing composes: another window's | |
| 430 | + | // content, the scene, or an image drawn by another path. | |
| 431 | + | func TestSubmitFrameOverlayOnlyNeedsSameImage(t *testing.T) { | |
| 432 | + | for name, edit := range map[string]func(o *Output, s *ports.Scene, c map[ports.WindowID]ports.SurfaceContent){ | |
| 433 | + | "other window content": func(_ *Output, _ *ports.Scene, c map[ports.WindowID]ports.SurfaceContent) { | |
| 434 | + | w := c[1] | |
| 435 | + | w.Seq++ | |
| 436 | + | c[1] = w | |
| 437 | + | }, | |
| 438 | + | "scene": func(_ *Output, s *ports.Scene, _ map[ports.WindowID]ports.SurfaceContent) { | |
| 439 | + | s.Background = "#ffffff" | |
| 440 | + | }, | |
| 441 | + | "image forgotten": func(o *Output, _ *ports.Scene, _ map[ports.WindowID]ports.SurfaceContent) { | |
| 442 | + | o.front.forget() | |
| 443 | + | }, | |
| 444 | + | "layer content": func(_ *Output, _ *ports.Scene, c map[ports.WindowID]ports.SurfaceContent) { | |
| 445 | + | l := c[5] | |
| 446 | + | l.Seq++ | |
| 447 | + | c[5] = l | |
| 448 | + | }, | |
| 449 | + | // The scene's slices rewritten in place by their owner: what the | |
| 450 | + | // image holds is a copy. | |
| 451 | + | "scene slice rewritten": func(_ *Output, s *ports.Scene, _ map[ports.WindowID]ports.SurfaceContent) { | |
| 452 | + | s.Windows[0].Rect.W = 90 | |
| 453 | + | }, | |
| 454 | + | } { | |
| 455 | + | t.Run(name, func(t *testing.T) { | |
| 456 | + | o, k, _ := overlayOutput(t) | |
| 457 | + | o.cursor, o.vrrProp = nil, 0 | |
| 458 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Maybe() | |
| 459 | + | o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}} | |
| 460 | + | s, c := overlayScene() | |
| 461 | + | s.Layers = []ports.SceneLayer{{ID: 5, Layer: ports.LayerBottom, Rect: ports.Rect{W: 10, H: 10}}} | |
| 462 | + | c[1] = ports.SurfaceContent{ID: 1, Seq: 1, Width: 100, Height: 100, Opaque: true, Solid: &ports.SolidColor{A: 1}} | |
| 463 | + | c[5] = ports.SurfaceContent{ID: 5, Seq: 1, Width: 10, Height: 10, Opaque: true, Solid: &ports.SolidColor{A: 1}} | |
| 464 | + | r := portsmocks.NewMockRenderer(t) | |
| 465 | + | r.EXPECT().UseTarget(mock.Anything).Return().Twice() | |
| 466 | + | var drawn []int | |
| 467 | + | r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(s ports.Scene, _ map[ports.WindowID]ports.SurfaceContent) (*os.File, error) { | |
| 468 | + | drawn = append(drawn, len(s.Windows)) | |
| 469 | + | return nil, nil | |
| 470 | + | }).Twice() | |
| 471 | + | overlayFrames(t, o, r, s, c, 1) | |
| 472 | + | edit(o, &s, c) | |
| 473 | + | overlayFrames(t, o, r, s, c, 1) | |
| 474 | + | if len(drawn) != 2 || drawn[1] != 1 { | |
| 475 | + | t.Fatalf("rendered %v", drawn) | |
| 476 | + | } | |
| 477 | + | }) | |
| 478 | + | } | |
| 479 | + | } | |
| @@ -0,0 +1,479 @@ | |||||
| 1 | + | package drm | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "errors" | |||
| 6 | + | "os" | |||
| 7 | + | "sync/atomic" | |||
| 8 | + | "testing" | |||
| 9 | + | "time" | |||
| 10 | + | ||||
| 11 | + | portsmocks "github.com/bnema/neferwl/internal/mocks/ports" | |||
| 12 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 13 | + | "github.com/stretchr/testify/mock" | |||
| 14 | + | "golang.org/x/sys/unix" | |||
| 15 | + | ) | |||
| 16 | + | ||||
| 17 | + | // overlayFrames submits the overlay scene once (composed), then with the | |||
| 18 | + | // overlay window's next buffers: the frames after the first only flip the | |||
| 19 | + | // overlay buffer. | |||
| 20 | + | func overlayFrames(t *testing.T, o *Output, r *portsmocks.MockRenderer, s ports.Scene, c map[ports.WindowID]ports.SurfaceContent, frames int) { | |||
| 21 | + | t.Helper() | |||
| 22 | + | for i := range frames { | |||
| 23 | + | win := c[2] | |||
| 24 | + | buf := *win.DMABuf | |||
| 25 | + | buf.ID = uint64(9 + i) | |||
| 26 | + | win.DMABuf, win.Seq = &buf, uint64(1+i) | |||
| 27 | + | c[2] = win | |||
| 28 | + | if _, err := o.submitFrame(context.Background(), r, s, c, map[ports.WindowID]uint64{}, nil, nil); err != nil { | |||
| 29 | + | t.Fatalf("frame %d: %v", i, err) | |||
| 30 | + | } | |||
| 31 | + | o.frame.endPending() | |||
| 32 | + | } | |||
| 33 | + | } | |||
| 34 | + | ||||
| 35 | + | // When only the overlay window's buffer changes, the image on screen is | |||
| 36 | + | // committed again with the new buffer: no render, no probe once KMS took | |||
| 37 | + | // the same frame. | |||
| 38 | + | func TestSubmitFrameOverlayOnlySkipsRender(t *testing.T) { | |||
| 39 | + | o, k, commits := overlayOutput(t) | |||
| 40 | + | o.cursor, o.vrrProp = nil, 0 | |||
| 41 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).RunAndReturn(func(b *ports.DMABuf, _ uint32) (uint32, error) { | |||
| 42 | + | return uint32(80 + b.ID), nil | |||
| 43 | + | }).Times(4) | |||
| 44 | + | o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}} | |||
| 45 | + | s, c := overlayScene() | |||
| 46 | + | r := portsmocks.NewMockRenderer(t) | |||
| 47 | + | r.EXPECT().UseTarget(0).Return().Once() | |||
| 48 | + | r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil).Once() | |||
| 49 | + | overlayFrames(t, o, r, s, c, 4) | |||
| 50 | + | var probes, frames int | |||
| 51 | + | for _, cm := range *commits { | |||
| 52 | + | if cm.flags&atomicTestOnly != 0 { | |||
| 53 | + | probes++ | |||
| 54 | + | continue | |||
| 55 | + | } | |||
| 56 | + | frames++ | |||
| 57 | + | if v, _ := cm.req.value(tPrimary, pFB); v != 70 { | |||
| 58 | + | t.Fatalf("frame %d primary fb %d, want the composed image 70", frames, v) | |||
| 59 | + | } | |||
| 60 | + | if _, ok := cm.req.value(tPrimary, pFence); ok { | |||
| 61 | + | t.Fatalf("frame %d waits on a render fence", frames) | |||
| 62 | + | } | |||
| 63 | + | } | |||
| 64 | + | if frames != 4 { | |||
| 65 | + | t.Fatalf("%d frame commits", frames) | |||
| 66 | + | } | |||
| 67 | + | // Each new overlay buffer is probed once; nothing else is. | |||
| 68 | + | if probes != 4 { | |||
| 69 | + | t.Fatalf("%d probes", probes) | |||
| 70 | + | } | |||
| 71 | + | if last := (*commits)[len(*commits)-1]; o.queued != 12 { | |||
| 72 | + | t.Fatalf("queued %d", o.queued) | |||
| 73 | + | } else if v, _ := last.req.value(tOverlay, pFB); v != 92 { | |||
| 74 | + | t.Fatalf("overlay fb %d", v) | |||
| 75 | + | } | |||
| 76 | + | if o.back != 1 { | |||
| 77 | + | t.Fatalf("back %d: the skipped frames drew nothing", o.back) | |||
| 78 | + | } | |||
| 79 | + | } | |||
| 80 | + | ||||
| 81 | + | // An uncommitted render that may still read client buffers forces | |||
| 82 | + | // composition: the overlay-only flip carries no fence to order it after | |||
| 83 | + | // those reads, and its report would let wayland release them. | |||
| 84 | + | func TestSubmitFrameOverlayOnlyWaitsForHeldReads(t *testing.T) { | |||
| 85 | + | o, k, _ := overlayOutput(t) | |||
| 86 | + | o.cursor, o.vrrProp = nil, 0 | |||
| 87 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Maybe() | |||
| 88 | + | o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}} | |||
| 89 | + | s, c := overlayScene() | |||
| 90 | + | r := portsmocks.NewMockRenderer(t) | |||
| 91 | + | r.EXPECT().UseTarget(mock.Anything).Return().Twice() | |||
| 92 | + | r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil).Twice() | |||
| 93 | + | overlayFrames(t, o, r, s, c, 1) | |||
| 94 | + | // An unsignalled fence of a render whose commit failed. | |||
| 95 | + | fence, w, err := os.Pipe() | |||
| 96 | + | if err != nil { | |||
| 97 | + | t.Fatal(err) | |||
| 98 | + | } | |||
| 99 | + | defer fence.Close() | |||
| 100 | + | defer w.Close() | |||
| 101 | + | o.holdRead(fence) | |||
| 102 | + | defer o.dropRead() | |||
| 103 | + | overlayFrames(t, o, r, s, c, 1) | |||
| 104 | + | } | |||
| 105 | + | ||||
| 106 | + | // The desktop case through Run: a video (a dmabuf window on the overlay) | |||
| 107 | + | // next to a static window. Once the overlay is up, each new video buffer is | |||
| 108 | + | // one frame commit of the same composed image with no render, and each flip | |||
| 109 | + | // reports the video shown zero-copy at its latest content. | |||
| 110 | + | func TestRunVideoOnOverlayDoesNotRender(t *testing.T) { | |||
| 111 | + | o, k, commits, commitMu := testOutputMu(t) | |||
| 112 | + | o.cursor, o.tearing, o.vrrProp = nil, false, 0 | |||
| 113 | + | props := map[string]uint32{"zpos": pZpos} | |||
| 114 | + | for n, v := range planeProps { | |||
| 115 | + | props[n] = v | |||
| 116 | + | } | |||
| 117 | + | o.primary.props = props | |||
| 118 | + | o.stray = []*plane{{id: tOverlay, typ: planeOverlay, props: props, zposValue: 2, formats: []ports.DMABufFormat{{Format: fourccXRGB}}}} | |||
| 119 | + | o.primary.formats = nil | |||
| 120 | + | o.pickOverlay() | |||
| 121 | + | flips := make(chan flipEvent, 1) | |||
| 122 | + | o.flipped = flips | |||
| 123 | + | r := portsmocks.NewMockRenderer(t) | |||
| 124 | + | pipeBuf := func() ports.DMABuf { | |||
| 125 | + | f, w, err := os.Pipe() | |||
| 126 | + | if err != nil { | |||
| 127 | + | t.Fatal(err) | |||
| 128 | + | } | |||
| 129 | + | w.Close() | |||
| 130 | + | return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}} | |||
| 131 | + | } | |||
| 132 | + | r.EXPECT().SetHDR(float64(0)).Return().Maybe() | |||
| 133 | + | r.EXPECT().ExportTargets(2, mock.Anything, false).Return([]ports.DMABuf{pipeBuf(), pipeBuf()}, nil).Once() | |||
| 134 | + | r.EXPECT().UseTarget(mock.Anything).Return() | |||
| 135 | + | var renders atomic.Int32 | |||
| 136 | + | r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(ports.Scene, map[ports.WindowID]ports.SurfaceContent) (*os.File, error) { | |||
| 137 | + | renders.Add(1) | |||
| 138 | + | return nil, nil | |||
| 139 | + | }) | |||
| 140 | + | r.EXPECT().Close().Return().Once() | |||
| 141 | + | var images atomic.Uint32 | |||
| 142 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).RunAndReturn(func(b *ports.DMABuf, _ uint32) (uint32, error) { | |||
| 143 | + | if b.ID == 0 { | |||
| 144 | + | return 70 + images.Add(1) - 1, nil // the output images | |||
| 145 | + | } | |||
| 146 | + | return uint32(100 + b.ID), nil | |||
| 147 | + | }) | |||
| 148 | + | k.EXPECT().createBlob(mock.Anything).Return(99, nil) | |||
| 149 | + | k.EXPECT().destroyBlob(mock.Anything).Return(nil).Maybe() | |||
| 150 | + | k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe() | |||
| 151 | + | k.EXPECT().objProps(uint32(tOverlay), uint32(objPlane)).Return(map[string][2]uint64{}, nil).Maybe() | |||
| 152 | + | frameCommits := func() []commitRec { | |||
| 153 | + | commitMu.Lock() | |||
| 154 | + | defer commitMu.Unlock() | |||
| 155 | + | var out []commitRec | |||
| 156 | + | for _, c := range *commits { | |||
| 157 | + | if c.user&3 == userFrame { | |||
| 158 | + | out = append(out, c) | |||
| 159 | + | } | |||
| 160 | + | } | |||
| 161 | + | return out | |||
| 162 | + | } | |||
| 163 | + | scenes := make(chan ports.Scene, 1) | |||
| 164 | + | contents := newContentFeed(t, 1) | |||
| 165 | + | sink, presented := presentedSink(t, 64) | |||
| 166 | + | ctx, cancel := context.WithCancel(context.Background()) | |||
| 167 | + | done := make(chan error, 1) | |||
| 168 | + | go func() { | |||
| 169 | + | 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) | |||
| 170 | + | }() | |||
| 171 | + | // Stop Run on every path, before the mocks check their calls. | |||
| 172 | + | t.Cleanup(func() { | |||
| 173 | + | cancel() | |||
| 174 | + | select { | |||
| 175 | + | case err := <-done: | |||
| 176 | + | if err != nil { | |||
| 177 | + | t.Error(err) | |||
| 178 | + | } | |||
| 179 | + | case <-time.After(2 * time.Second): | |||
| 180 | + | t.Error("Run did not stop") | |||
| 181 | + | } | |||
| 182 | + | }) | |||
| 183 | + | static := ports.SurfaceContent{ID: 1, Seq: 1, Width: 100, Height: 100, Opaque: true, Solid: &ports.SolidColor{A: 1}} | |||
| 184 | + | video := func(n int) ports.SurfaceContent { | |||
| 185 | + | return ports.SurfaceContent{ID: 2, Seq: uint64(n), Width: 100, Height: 100, Opaque: true, | |||
| 186 | + | DMABuf: &ports.DMABuf{ID: uint64(n%3 + 1), Width: 100, Height: 100, Format: fourccXRGB}} | |||
| 187 | + | } | |||
| 188 | + | contents.send(static, video(1)) | |||
| 189 | + | scenes <- ports.Scene{Seq: 1, Scale: 1, OutputWidth: 200, OutputHeight: 100, Windows: []ports.SceneWindow{ | |||
| 190 | + | {ID: 1, Rect: ports.Rect{W: 100, H: 100}}, | |||
| 191 | + | {ID: 2, Rect: ports.Rect{X: 100, W: 100, H: 100}}, | |||
| 192 | + | }} | |||
| 193 | + | // play delivers video frame n and flips every frame commit until a flip | |||
| 194 | + | // shows it; it returns that commit and its report. (Seen is what the | |||
| 195 | + | // output took in, which may run ahead of the flip.) | |||
| 196 | + | flipped := 0 | |||
| 197 | + | vseq := uint32(0) | |||
| 198 | + | play := func(n int) (commitRec, ports.OutputPresented) { | |||
| 199 | + | t.Helper() | |||
| 200 | + | contents.send(video(n)) | |||
| 201 | + | var c commitRec | |||
| 202 | + | var got ports.OutputPresented | |||
| 203 | + | waitFor(t, func() bool { | |||
| 204 | + | if cs := frameCommits(); len(cs) > flipped { | |||
| 205 | + | c = cs[len(cs)-1] | |||
| 206 | + | flipped = len(cs) | |||
| 207 | + | vseq++ | |||
| 208 | + | flips <- flipEvent{crtc: tCrtc, user: c.user, when: time.Duration(vseq) * time.Millisecond, seq: vseq} | |||
| 209 | + | } | |||
| 210 | + | for { | |||
| 211 | + | select { | |||
| 212 | + | case p := <-presented: | |||
| 213 | + | if p.Flip != nil && p.Flip.Shows[2] == uint64(n) { | |||
| 214 | + | got = p | |||
| 215 | + | return true | |||
| 216 | + | } | |||
| 217 | + | default: | |||
| 218 | + | return false | |||
| 219 | + | } | |||
| 220 | + | } | |||
| 221 | + | }) | |||
| 222 | + | return c, got | |||
| 223 | + | } | |||
| 224 | + | // Warm up until the video is on the overlay over a composed image. | |||
| 225 | + | n := 1 | |||
| 226 | + | for ; n < 10; n++ { | |||
| 227 | + | c, _ := play(n) | |||
| 228 | + | if v, _ := c.req.value(tOverlay, pFB); v != 0 { | |||
| 229 | + | break | |||
| 230 | + | } | |||
| 231 | + | } | |||
| 232 | + | base, commitsBefore := renders.Load(), len(frameCommits()) | |||
| 233 | + | const frames = 60 | |||
| 234 | + | for i := range frames { | |||
| 235 | + | n++ | |||
| 236 | + | c, p := play(n) | |||
| 237 | + | if v, _ := c.req.value(tOverlay, pFB); v != uint64(100+n%3+1) { | |||
| 238 | + | t.Fatalf("video frame %d: overlay fb %d", i, v) | |||
| 239 | + | } | |||
| 240 | + | if _, ok := c.req.value(tPrimary, pFence); ok { | |||
| 241 | + | t.Fatalf("video frame %d waits on a render", i) | |||
| 242 | + | } | |||
| 243 | + | // The flip reports the video shown zero-copy at its content. | |||
| 244 | + | if p.Flip.ZeroCopy != 2 || p.Flip.Shows[2] != uint64(n) || p.Seen[1] != 1 { | |||
| 245 | + | t.Fatalf("video frame %d: report %+v flip %+v", i, p, *p.Flip) | |||
| 246 | + | } | |||
| 247 | + | } | |||
| 248 | + | if got := len(frameCommits()) - commitsBefore; got != frames { | |||
| 249 | + | t.Fatalf("%d frame commits for %d video frames", got, frames) | |||
| 250 | + | } | |||
| 251 | + | if got := renders.Load() - base; got != 0 { | |||
| 252 | + | t.Fatalf("%d renders for %d video frames, want 0", got, frames) | |||
| 253 | + | } | |||
| 254 | + | // Three buffers in rotation: each probed once, then remembered. | |||
| 255 | + | commitMu.Lock() | |||
| 256 | + | probes := 0 | |||
| 257 | + | for _, c := range *commits { | |||
| 258 | + | if c.flags&atomicTestOnly != 0 { | |||
| 259 | + | probes++ | |||
| 260 | + | } | |||
| 261 | + | } | |||
| 262 | + | commitMu.Unlock() | |||
| 263 | + | if probes > 4 { | |||
| 264 | + | t.Fatalf("%d overlay probes for %d video frames of 3 buffers", probes, frames) | |||
| 265 | + | } | |||
| 266 | + | } | |||
| 267 | + | ||||
| 268 | + | // Refusals on the overlay-only path: a refused probe composes the whole | |||
| 269 | + | // scene at once; a refused commit drops the overlay and asks for a new frame, | |||
| 270 | + | // leaving the images and the queue as they were. | |||
| 271 | + | func TestSubmitFrameOverlayOnlyRefused(t *testing.T) { | |||
| 272 | + | for _, tc := range []struct { | |||
| 273 | + | name string | |||
| 274 | + | refuse func(r *atomicReq, flags uint32) bool | |||
| 275 | + | wantErr error | |||
| 276 | + | renders int | |||
| 277 | + | }{ | |||
| 278 | + | {"probe", func(_ *atomicReq, flags uint32) bool { return flags&atomicTestOnly != 0 }, nil, 1}, | |||
| 279 | + | {"commit", func(_ *atomicReq, flags uint32) bool { return flags&atomicTestOnly == 0 }, errOverlayDropped, 0}, | |||
| 280 | + | } { | |||
| 281 | + | t.Run(tc.name, func(t *testing.T) { | |||
| 282 | + | refusing := false | |||
| 283 | + | o, k, _, _ := testOutputRules(t, func(r *atomicReq, flags uint32) error { | |||
| 284 | + | if refusing && tc.refuse(r, flags) { | |||
| 285 | + | return unix.EINVAL | |||
| 286 | + | } | |||
| 287 | + | return nil | |||
| 288 | + | }) | |||
| 289 | + | props := map[string]uint32{"zpos": pZpos} | |||
| 290 | + | for n, v := range planeProps { | |||
| 291 | + | props[n] = v | |||
| 292 | + | } | |||
| 293 | + | o.primary.props, o.primary.formats = props, nil | |||
| 294 | + | o.stray = []*plane{{id: tOverlay, typ: planeOverlay, props: props, zposValue: 2, formats: []ports.DMABufFormat{{Format: fourccXRGB}}}} | |||
| 295 | + | o.pickOverlay() | |||
| 296 | + | o.cursor, o.vrrProp = nil, 0 | |||
| 297 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).RunAndReturn(func(b *ports.DMABuf, _ uint32) (uint32, error) { | |||
| 298 | + | return uint32(80 + b.ID), nil | |||
| 299 | + | }) | |||
| 300 | + | s, c := overlayScene() | |||
| 301 | + | r := portsmocks.NewMockRenderer(t) | |||
| 302 | + | r.EXPECT().UseTarget(mock.Anything).Return() | |||
| 303 | + | var drawn []int | |||
| 304 | + | r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(s ports.Scene, _ map[ports.WindowID]ports.SurfaceContent) (*os.File, error) { | |||
| 305 | + | drawn = append(drawn, len(s.Windows)) | |||
| 306 | + | return nil, nil | |||
| 307 | + | }) | |||
| 308 | + | overlayFrames(t, o, r, s, c, 1) // composed, front image held | |||
| 309 | + | back, queued := o.back, o.queued | |||
| 310 | + | refusing = true | |||
| 311 | + | win := c[2] | |||
| 312 | + | buf := *win.DMABuf | |||
| 313 | + | buf.ID, win.DMABuf, win.Seq = 20, &buf, 2 | |||
| 314 | + | c[2] = win | |||
| 315 | + | _, err := o.submitFrame(context.Background(), r, s, c, map[ports.WindowID]uint64{}, nil, nil) | |||
| 316 | + | if !errors.Is(err, tc.wantErr) && err != tc.wantErr { | |||
| 317 | + | t.Fatalf("err %v, want %v", err, tc.wantErr) | |||
| 318 | + | } | |||
| 319 | + | if got := len(drawn) - 1; got != tc.renders { | |||
| 320 | + | t.Fatalf("%d renders, want %d", got, tc.renders) | |||
| 321 | + | } | |||
| 322 | + | if tc.renders == 1 && drawn[1] != 2 { | |||
| 323 | + | t.Fatalf("composed %d windows, want the whole scene", drawn[1]) | |||
| 324 | + | } | |||
| 325 | + | if tc.wantErr != nil { | |||
| 326 | + | if o.back != back || o.queued != queued || o.clientFBs[20].overlayFailed != "cursor_conflict" { | |||
| 327 | + | t.Fatalf("back %d queued %d reason %q", o.back, o.queued, o.clientFBs[20].overlayFailed) | |||
| 328 | + | } | |||
| 329 | + | // The retry composes the window. | |||
| 330 | + | refusing = false | |||
| 331 | + | if _, err := o.submitFrame(context.Background(), r, s, c, map[ports.WindowID]uint64{}, nil, nil); err != nil { | |||
| 332 | + | t.Fatal(err) | |||
| 333 | + | } | |||
| 334 | + | if drawn[len(drawn)-1] != 2 { | |||
| 335 | + | t.Fatalf("retry composed %d windows", drawn[len(drawn)-1]) | |||
| 336 | + | } | |||
| 337 | + | } | |||
| 338 | + | }) | |||
| 339 | + | } | |||
| 340 | + | } | |||
| 341 | + | ||||
| 342 | + | // A commit result with the overlay on: refusals the overlay causes ask for a | |||
| 343 | + | // frame without it and leave the queue; a commit queues the buffer. | |||
| 344 | + | func TestFinishOverlayCommit(t *testing.T) { | |||
| 345 | + | boom := errors.New("boom") | |||
| 346 | + | for _, tc := range []struct { | |||
| 347 | + | name string | |||
| 348 | + | err error | |||
| 349 | + | want error | |||
| 350 | + | wantQueued uint64 | |||
| 351 | + | reason string | |||
| 352 | + | }{ | |||
| 353 | + | {"committed", nil, nil, 9, ""}, | |||
| 354 | + | {"colour refused", errColorRefused, errOverlayDropped, 1, "color_refused"}, | |||
| 355 | + | {"overlay conflict", unix.EINVAL, errOverlayDropped, 1, "cursor_conflict"}, | |||
| 356 | + | {"other", boom, boom, 1, ""}, | |||
| 357 | + | } { | |||
| 358 | + | t.Run(tc.name, func(t *testing.T) { | |||
| 359 | + | o, _, _ := testOutput(t) | |||
| 360 | + | o.queued = 1 | |||
| 361 | + | o.clientFBs[9] = &clientFB{} | |||
| 362 | + | if err := o.finishOverlayCommit(tc.err, 9); err != tc.want { | |||
| 363 | + | t.Fatalf("err %v, want %v", err, tc.want) | |||
| 364 | + | } | |||
| 365 | + | if o.queued != tc.wantQueued || o.overlayReason != tc.reason { | |||
| 366 | + | t.Fatalf("queued %d reason %q", o.queued, o.overlayReason) | |||
| 367 | + | } | |||
| 368 | + | }) | |||
| 369 | + | } | |||
| 370 | + | } | |||
| 371 | + | ||||
| 372 | + | // A frame whose buffer KMS took before is not probed again. | |||
| 373 | + | func TestOverlayProbeCached(t *testing.T) { | |||
| 374 | + | o, k, commits := overlayOutput(t) | |||
| 375 | + | o.cursor.image = true | |||
| 376 | + | o.cursor.Move(10, 10) | |||
| 377 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once() | |||
| 378 | + | o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}} | |||
| 379 | + | s, c := overlayScene() | |||
| 380 | + | ov, _ := o.overlayFrame(s, c) | |||
| 381 | + | for range 3 { | |||
| 382 | + | if !o.testOverlay(70, ov) { | |||
| 383 | + | t.Fatal("refused") | |||
| 384 | + | } | |||
| 385 | + | } | |||
| 386 | + | if len(*commits) != 1 { | |||
| 387 | + | t.Fatalf("%d probes, want 1", len(*commits)) | |||
| 388 | + | } | |||
| 389 | + | // Another place, primary image or cursor is probed. | |||
| 390 | + | moved := ov | |||
| 391 | + | moved.rect.X++ | |||
| 392 | + | o.testOverlay(70, moved) | |||
| 393 | + | o.testOverlay(71, ov) | |||
| 394 | + | o.cursor.Move(20, 10) | |||
| 395 | + | o.testOverlay(70, ov) | |||
| 396 | + | if len(*commits) != 4 { | |||
| 397 | + | t.Fatalf("%d probes, want 4", len(*commits)) | |||
| 398 | + | } | |||
| 399 | + | // The probe tested the cursor where the cache remembers it. | |||
| 400 | + | if v, _ := (*commits)[3].req.value(tCursor, 14); v != 20-uint64(o.cursor.hotX) { | |||
| 401 | + | t.Fatalf("probe cursor x %d", v) | |||
| 402 | + | } | |||
| 403 | + | // New colour state (HDR brightness) is probed again. | |||
| 404 | + | o.colorMult++ | |||
| 405 | + | o.testOverlay(70, ov) | |||
| 406 | + | if len(*commits) != 5 { | |||
| 407 | + | t.Fatalf("%d probes after a colour change, want 5", len(*commits)) | |||
| 408 | + | } | |||
| 409 | + | // A removed client framebuffer may give its id to another buffer. | |||
| 410 | + | k.EXPECT().rmFB(uint32(88)).Return(nil).Once() | |||
| 411 | + | o.dropClientFBs(time.Now(), true) | |||
| 412 | + | o.testOverlay(70, ov) | |||
| 413 | + | if len(*commits) != 6 { | |||
| 414 | + | t.Fatal("probe cached across a framebuffer removal") | |||
| 415 | + | } | |||
| 416 | + | // A modeset (VT resume) forgets them. | |||
| 417 | + | k.EXPECT().createBlob(mock.Anything).Return(99, nil).Once() | |||
| 418 | + | k.EXPECT().objProps(uint32(tOverlay), uint32(objPlane)).Return(map[string][2]uint64{}, nil).Maybe() | |||
| 419 | + | if err := o.modeset(); err != nil { | |||
| 420 | + | t.Fatal(err) | |||
| 421 | + | } | |||
| 422 | + | before := len(*commits) | |||
| 423 | + | o.testOverlay(70, ov) | |||
| 424 | + | if len(*commits) != before+1 { | |||
| 425 | + | t.Fatal("probe cached across a modeset") | |||
| 426 | + | } | |||
| 427 | + | } | |||
| 428 | + | ||||
| 429 | + | // Anything but the overlay window changing composes: another window's | |||
| 430 | + | // content, the scene, or an image drawn by another path. | |||
| 431 | + | func TestSubmitFrameOverlayOnlyNeedsSameImage(t *testing.T) { | |||
| 432 | + | for name, edit := range map[string]func(o *Output, s *ports.Scene, c map[ports.WindowID]ports.SurfaceContent){ | |||
| 433 | + | "other window content": func(_ *Output, _ *ports.Scene, c map[ports.WindowID]ports.SurfaceContent) { | |||
| 434 | + | w := c[1] | |||
| 435 | + | w.Seq++ | |||
| 436 | + | c[1] = w | |||
| 437 | + | }, | |||
| 438 | + | "scene": func(_ *Output, s *ports.Scene, _ map[ports.WindowID]ports.SurfaceContent) { | |||
| 439 | + | s.Background = "#ffffff" | |||
| 440 | + | }, | |||
| 441 | + | "image forgotten": func(o *Output, _ *ports.Scene, _ map[ports.WindowID]ports.SurfaceContent) { | |||
| 442 | + | o.front.forget() | |||
| 443 | + | }, | |||
| 444 | + | "layer content": func(_ *Output, _ *ports.Scene, c map[ports.WindowID]ports.SurfaceContent) { | |||
| 445 | + | l := c[5] | |||
| 446 | + | l.Seq++ | |||
| 447 | + | c[5] = l | |||
| 448 | + | }, | |||
| 449 | + | // The scene's slices rewritten in place by their owner: what the | |||
| 450 | + | // image holds is a copy. | |||
| 451 | + | "scene slice rewritten": func(_ *Output, s *ports.Scene, _ map[ports.WindowID]ports.SurfaceContent) { | |||
| 452 | + | s.Windows[0].Rect.W = 90 | |||
| 453 | + | }, | |||
| 454 | + | } { | |||
| 455 | + | t.Run(name, func(t *testing.T) { | |||
| 456 | + | o, k, _ := overlayOutput(t) | |||
| 457 | + | o.cursor, o.vrrProp = nil, 0 | |||
| 458 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Maybe() | |||
| 459 | + | o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}} | |||
| 460 | + | s, c := overlayScene() | |||
| 461 | + | s.Layers = []ports.SceneLayer{{ID: 5, Layer: ports.LayerBottom, Rect: ports.Rect{W: 10, H: 10}}} | |||
| 462 | + | c[1] = ports.SurfaceContent{ID: 1, Seq: 1, Width: 100, Height: 100, Opaque: true, Solid: &ports.SolidColor{A: 1}} | |||
| 463 | + | c[5] = ports.SurfaceContent{ID: 5, Seq: 1, Width: 10, Height: 10, Opaque: true, Solid: &ports.SolidColor{A: 1}} | |||
| 464 | + | r := portsmocks.NewMockRenderer(t) | |||
| 465 | + | r.EXPECT().UseTarget(mock.Anything).Return().Twice() | |||
| 466 | + | var drawn []int | |||
| 467 | + | r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(s ports.Scene, _ map[ports.WindowID]ports.SurfaceContent) (*os.File, error) { | |||
| 468 | + | drawn = append(drawn, len(s.Windows)) | |||
| 469 | + | return nil, nil | |||
| 470 | + | }).Twice() | |||
| 471 | + | overlayFrames(t, o, r, s, c, 1) | |||
| 472 | + | edit(o, &s, c) | |||
| 473 | + | overlayFrames(t, o, r, s, c, 1) | |||
| 474 | + | if len(drawn) != 2 || drawn[1] != 1 { | |||
| 475 | + | t.Fatalf("rendered %v", drawn) | |||
| 476 | + | } | |||
| 477 | + | }) | |||
| 478 | + | } | |||
| 479 | + | } | |||
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.forgetOverlayProbes() | |
| 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.forgetOverlayProbes() | |||
| 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 +8 -0
| @@ -147,6 +147,12 @@ 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 | + | // enough for a client cycling through four buffers. | |
| 154 | + | overlayPassed [4]overlayProbe | |
| 155 | + | overlayNext int | |
| 150 | 156 | // lastFrame is when the last frame was committed. cursorHeld: a cursor | |
| 151 | 157 | // move waits for the next frame (see cursorWaits). | |
| 152 | 158 | lastFrame time.Time | |
| @@ -437,6 +443,8 @@ func (o *Output) modesetImage(fb uint32, active bool) error { | |||
| 437 | 443 | for _, fb := range o.clientFBs { | |
| 438 | 444 | fb.scaleTestedOK, fb.scaleRefused = false, false | |
| 439 | 445 | } | |
| 446 | + | o.front.forget() | |
| 447 | + | o.forgetOverlayProbes() | |
| 440 | 448 | o.forgetColor() | |
| 441 | 449 | o.forgetColorVerdicts() | |
| 442 | 450 | o.shown, o.queued = 0, 0 | |
| @@ -147,6 +147,12 @@ 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 | + | // enough for a client cycling through four buffers. | |||
| 154 | + | overlayPassed [4]overlayProbe | |||
| 155 | + | overlayNext int | |||
| 150 | // lastFrame is when the last frame was committed. cursorHeld: a cursor | 156 | // lastFrame is when the last frame was committed. cursorHeld: a cursor | ||
| 151 | // move waits for the next frame (see cursorWaits). | 157 | // move waits for the next frame (see cursorWaits). | ||
| 152 | lastFrame time.Time | 158 | lastFrame time.Time | ||
| @@ -437,6 +443,8 @@ func (o *Output) modesetImage(fb uint32, active bool) error { | |||||
| 437 | for _, fb := range o.clientFBs { | 443 | for _, fb := range o.clientFBs { | ||
| 438 | fb.scaleTestedOK, fb.scaleRefused = false, false | 444 | fb.scaleTestedOK, fb.scaleRefused = false, false | ||
| 439 | } | 445 | } | ||
| 446 | + | o.front.forget() | |||
| 447 | + | o.forgetOverlayProbes() | |||
| 440 | o.forgetColor() | 448 | o.forgetColor() | ||
| 441 | o.forgetColorVerdicts() | 449 | o.forgetColorVerdicts() | ||
| 442 | o.shown, o.queued = 0, 0 | 450 | o.shown, o.queued = 0, 0 | ||
M internal/adapters/drm/overlay.go +43 -8
| @@ -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,13 +232,25 @@ 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 { | |
| 235 | - | err := o.probeOverlay(fb, ov, ov.color.mode) | |
| 240 | + | // One cursor snapshot: the input goroutine may move it meanwhile, and | |
| 241 | + | // what is remembered must be what KMS was asked. | |
| 242 | + | key := o.overlayProbeOf(fb, ov) | |
| 243 | + | if slices.Contains(o.overlayPassed[:], key) { | |
| 244 | + | return true | |
| 245 | + | } | |
| 246 | + | err := o.probeOverlay(fb, ov, ov.color.mode, key.cursor) | |
| 236 | 247 | if err == nil { | |
| 248 | + | o.overlayPassed[o.overlayNext] = key | |
| 249 | + | o.overlayNext = (o.overlayNext + 1) % len(o.overlayPassed) | |
| 237 | 250 | return true | |
| 238 | 251 | } | |
| 239 | - | if ov.color.mode != colorBypass && refused(err) && o.probeOverlay(fb, ov, colorBypass) == nil { | |
| 240 | - | o.overlay.setVerdict(ov.color.format, o.cursorShown(), false) | |
| 252 | + | if ov.color.mode != colorBypass && refused(err) && o.probeOverlay(fb, ov, colorBypass, key.cursor) == nil { | |
| 253 | + | o.overlay.setVerdict(ov.color.format, key.cursor.on, false) | |
| 241 | 254 | o.setOverlayReason("color_refused") | |
| 242 | 255 | o.renderLog.Info().Err(err).Uint32("format", ov.color.format).Str("connector", o.conn.name).Msg("overlay colour pipeline refused") | |
| 243 | 256 | return false | |
| @@ -250,11 +263,33 @@ func (o *Output) testOverlay(fb uint32, ov overlayWin) bool { | |||
| 250 | 263 | return false | |
| 251 | 264 | } | |
| 252 | 265 | ||
| 266 | + | // forgetOverlayProbes drops the remembered probes: KMS is asked again. | |
| 267 | + | func (o *Output) forgetOverlayProbes() { clear(o.overlayPassed[:]) } | |
| 268 | + | ||
| 269 | + | // overlayProbe is what an overlay probe depends on, fences aside. | |
| 270 | + | type overlayProbe struct { | |
| 271 | + | primary, overlay uint32 | |
| 272 | + | rect ports.Rect | |
| 273 | + | w, h int | |
| 274 | + | color colorUse | |
| 275 | + | cursor cursorState | |
| 276 | + | mult uint64 | |
| 277 | + | ctm uint32 | |
| 278 | + | } | |
| 279 | + | ||
| 280 | + | func (o *Output) overlayProbeOf(fb uint32, ov overlayWin) overlayProbe { | |
| 281 | + | p := overlayProbe{primary: fb, overlay: ov.fb, rect: ov.rect, w: ov.w, h: ov.h, color: ov.color, mult: o.colorMult, ctm: o.ctmBlob} | |
| 282 | + | if o.cursor != nil && !o.protected { | |
| 283 | + | p.cursor = o.cursor.desired() | |
| 284 | + | } | |
| 285 | + | return p | |
| 286 | + | } | |
| 287 | + | ||
| 253 | 288 | // 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. | |
| 255 | - | // Both planes' colour state is written whatever they applied: the test must | |
| 256 | - | // not rely on the state of the last commit. | |
| 257 | - | func (o *Output) probeOverlay(fb uint32, ov overlayWin, mode colorMode) error { | |
| 289 | + | // primary plane (Bypass), ov on the overlay with colour mode, the cursor in | |
| 290 | + | // state cur. Both planes' colour state is written whatever they applied: | |
| 291 | + | // the test must not rely on the state of the last commit. | |
| 292 | + | func (o *Output) probeOverlay(fb uint32, ov overlayWin, mode colorMode, cur cursorState) error { | |
| 258 | 293 | req := &o.probeReq // no frame request is alive: the frame commit follows | |
| 259 | 294 | req.reset() | |
| 260 | 295 | o.primaryProps(req, fb) | |
| @@ -262,7 +297,7 @@ func (o *Output) probeOverlay(fb uint32, ov overlayWin, mode colorMode) error { | |||
| 262 | 297 | o.overlayProps(req, ov) | |
| 263 | 298 | o.overlay.colorProps(req, mode, o.colorMult, o.ctmBlob) | |
| 264 | 299 | if o.cursor != nil { | |
| 265 | - | o.cursor.props(req, o.crtc, o.cursor.desired()) | |
| 300 | + | o.cursor.props(req, o.crtc, cur) | |
| 266 | 301 | } | |
| 267 | 302 | return o.k.commit(req, atomicTestOnly, 0) | |
| 268 | 303 | } | |
| @@ -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,13 +232,25 @@ 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 { | ||
| 235 | - | err := o.probeOverlay(fb, ov, ov.color.mode) | 240 | + | // One cursor snapshot: the input goroutine may move it meanwhile, and |
| 241 | + | // what is remembered must be what KMS was asked. | |||
| 242 | + | key := o.overlayProbeOf(fb, ov) | |||
| 243 | + | if slices.Contains(o.overlayPassed[:], key) { | |||
| 244 | + | return true | |||
| 245 | + | } | |||
| 246 | + | err := o.probeOverlay(fb, ov, ov.color.mode, key.cursor) | |||
| 236 | if err == nil { | 247 | if err == nil { | ||
| 248 | + | o.overlayPassed[o.overlayNext] = key | |||
| 249 | + | o.overlayNext = (o.overlayNext + 1) % len(o.overlayPassed) | |||
| 237 | return true | 250 | return true | ||
| 238 | } | 251 | } | ||
| 239 | - | if ov.color.mode != colorBypass && refused(err) && o.probeOverlay(fb, ov, colorBypass) == nil { | 252 | + | if ov.color.mode != colorBypass && refused(err) && o.probeOverlay(fb, ov, colorBypass, key.cursor) == nil { |
| 240 | - | o.overlay.setVerdict(ov.color.format, o.cursorShown(), false) | 253 | + | o.overlay.setVerdict(ov.color.format, key.cursor.on, false) |
| 241 | o.setOverlayReason("color_refused") | 254 | o.setOverlayReason("color_refused") | ||
| 242 | o.renderLog.Info().Err(err).Uint32("format", ov.color.format).Str("connector", o.conn.name).Msg("overlay colour pipeline refused") | 255 | o.renderLog.Info().Err(err).Uint32("format", ov.color.format).Str("connector", o.conn.name).Msg("overlay colour pipeline refused") | ||
| 243 | return false | 256 | return false | ||
| @@ -250,11 +263,33 @@ func (o *Output) testOverlay(fb uint32, ov overlayWin) bool { | |||||
| 250 | return false | 263 | return false | ||
| 251 | } | 264 | } | ||
| 252 | 265 | ||||
| 266 | + | // forgetOverlayProbes drops the remembered probes: KMS is asked again. | |||
| 267 | + | func (o *Output) forgetOverlayProbes() { clear(o.overlayPassed[:]) } | |||
| 268 | + | ||||
| 269 | + | // overlayProbe is what an overlay probe depends on, fences aside. | |||
| 270 | + | type overlayProbe struct { | |||
| 271 | + | primary, overlay uint32 | |||
| 272 | + | rect ports.Rect | |||
| 273 | + | w, h int | |||
| 274 | + | color colorUse | |||
| 275 | + | cursor cursorState | |||
| 276 | + | mult uint64 | |||
| 277 | + | ctm uint32 | |||
| 278 | + | } | |||
| 279 | + | ||||
| 280 | + | func (o *Output) overlayProbeOf(fb uint32, ov overlayWin) overlayProbe { | |||
| 281 | + | p := overlayProbe{primary: fb, overlay: ov.fb, rect: ov.rect, w: ov.w, h: ov.h, color: ov.color, mult: o.colorMult, ctm: o.ctmBlob} | |||
| 282 | + | if o.cursor != nil && !o.protected { | |||
| 283 | + | p.cursor = o.cursor.desired() | |||
| 284 | + | } | |||
| 285 | + | return p | |||
| 286 | + | } | |||
| 287 | + | ||||
| 253 | // probeOverlay is a TEST_ONLY of the frame: the composed image on the | 288 | // 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. | 289 | + | // primary plane (Bypass), ov on the overlay with colour mode, the cursor in |
| 255 | - | // Both planes' colour state is written whatever they applied: the test must | 290 | + | // state cur. Both planes' colour state is written whatever they applied: |
| 256 | - | // not rely on the state of the last commit. | 291 | + | // the test must not rely on the state of the last commit. |
| 257 | - | func (o *Output) probeOverlay(fb uint32, ov overlayWin, mode colorMode) error { | 292 | + | func (o *Output) probeOverlay(fb uint32, ov overlayWin, mode colorMode, cur cursorState) error { |
| 258 | req := &o.probeReq // no frame request is alive: the frame commit follows | 293 | req := &o.probeReq // no frame request is alive: the frame commit follows | ||
| 259 | req.reset() | 294 | req.reset() | ||
| 260 | o.primaryProps(req, fb) | 295 | o.primaryProps(req, fb) | ||
| @@ -262,7 +297,7 @@ func (o *Output) probeOverlay(fb uint32, ov overlayWin, mode colorMode) error { | |||||
| 262 | o.overlayProps(req, ov) | 297 | o.overlayProps(req, ov) | ||
| 263 | o.overlay.colorProps(req, mode, o.colorMult, o.ctmBlob) | 298 | o.overlay.colorProps(req, mode, o.colorMult, o.ctmBlob) | ||
| 264 | if o.cursor != nil { | 299 | if o.cursor != nil { | ||
| 265 | - | o.cursor.props(req, o.crtc, o.cursor.desired()) | 300 | + | o.cursor.props(req, o.crtc, cur) |
| 266 | } | 301 | } | ||
| 267 | return o.k.commit(req, atomicTestOnly, 0) | 302 | return o.k.commit(req, atomicTestOnly, 0) | ||
| 268 | } | 303 | } | ||
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.forgetOverlayProbes() | |
| 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.forgetOverlayProbes() | |||
| 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 | ||