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