1package drm
2
3import (
4 "bytes"
5 "context"
6 "errors"
7 "image"
8 "os"
9 "strings"
10 "sync"
11 "sync/atomic"
12 "testing"
13 "time"
14
15 "github.com/bnema/neferwl/internal/adapters/presented"
16 portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
17 "github.com/bnema/neferwl/internal/ports"
18 "github.com/bnema/zerowrap"
19 "github.com/stretchr/testify/mock"
20 "golang.org/x/sys/unix"
21)
22
23// Object and property IDs of the test output.
24const (
25 tCrtc, tConn, tPrimary, tCursor, tOverlay = 30, 40, 50, 51, 52
26 pFB, pCrtcID, pFence, pMode, pActive = 1, 2, 3, 4, 5
27 pVRR, pConnCrtc = 6, 7
28)
29
30var planeProps = map[string]uint32{"FB_ID": pFB, "CRTC_ID": pCrtcID, "IN_FENCE_FD": pFence, "SRC_X": 10, "SRC_Y": 11, "SRC_W": 12, "SRC_H": 13, "CRTC_X": 14, "CRTC_Y": 15, "CRTC_W": 16, "CRTC_H": 17}
31
32// commitRec is one recorded atomic commit.
33type commitRec struct {
34 req atomicReq
35 flags uint32
36 user uint64
37 at time.Time // when the commit was made
38}
39
40// testOutput returns an output on a mocked kms that records commits;
41// commit returns the next error of errs (nil when empty).
42func testOutput(t *testing.T, errs ...error) (*Output, *mockkms, *[]commitRec) {
43 o, k, commits, _ := testOutputMu(t, errs...)
44 return o, k, commits
45}
46
47// testOutputMu is testOutput with the mutex that guards the recorded
48// commits (Run commits on its goroutine).
49func testOutputMu(t *testing.T, errs ...error) (*Output, *mockkms, *[]commitRec, *sync.Mutex) {
50 return testOutputRules(t, nil, errs...)
51}
52
53// testOutputRules is testOutputMu with an optional driver rule: a non-nil
54// rule sees every commit (under the commit mutex) and may refuse it.
55func testOutputRules(t *testing.T, rule func(*atomicReq, uint32) error, errs ...error) (*Output, *mockkms, *[]commitRec, *sync.Mutex) {
56 k := newMockkms(t)
57 var commits []commitRec
58 commitMu := &sync.Mutex{}
59 k.EXPECT().commit(mock.Anything, mock.Anything, mock.Anything).RunAndReturn(func(r *atomicReq, flags uint32, user uint64) error {
60 commitMu.Lock()
61 defer commitMu.Unlock()
62 c := commitRec{flags: flags, user: user, at: time.Now()}
63 c.req.objs = append([]uint32(nil), r.objs...)
64 for _, ps := range r.props {
65 c.req.props = append(c.req.props, append([]propValue(nil), ps...))
66 }
67 commits = append(commits, c)
68 if rule != nil {
69 if err := rule(r, flags); err != nil {
70 return err
71 }
72 }
73 if len(errs) > 0 {
74 err := errs[0]
75 errs = errs[1:]
76 return err
77 }
78 return nil
79 }).Maybe()
80 primary := &plane{id: tPrimary, typ: planePrimary, props: planeProps}
81 cur := newCursor(&plane{id: tCursor, typ: planeCursor, props: planeProps}, 64)
82 cur.fbs = [2]uint32{90, 91}
83 o := &Output{k: k, frame: frameLifecycle{serials: &atomic.Uint64{}}, crtc: tCrtc, conn: connector{id: tConn, name: "DP-1"}, mode: modeInfo{HDisplay: 200, VDisplay: 100, VRefresh: 60}, log: zerowrap.Default(), renderLog: zerowrap.Default(),
84 crtcProps: map[string]uint32{"MODE_ID": pMode, "ACTIVE": pActive, "VRR_ENABLED": pVRR}, connCrtc: pConnCrtc, vrrProp: pVRR,
85 primary: primary, cursor: cur, fbs: [2]uint32{70, 71}, clientFBs: map[uint64]*clientFB{}, scanout: true, tearing: true, asyncFence: true, planeRect: fullPlaneRect(200, 100), seatDisable: make(chan chan struct{})}
86 return o, k, &commits, commitMu
87}
88
89// eventOf is the flip event of commit c.
90func eventOf(c commitRec) flipEvent { return flipEvent{crtc: tCrtc, user: c.user} }
91
92func objCount(c commitRec) int { return len(c.req.objs) }
93
94func TestFrameCommitCarriesPrimaryFenceCursorAndVRR(t *testing.T) {
95 o, _, commits := testOutput(t)
96 o.cursor.image = true
97 o.cursor.Move(12, 34)
98 fence, w, err := os.Pipe()
99 if err != nil {
100 t.Fatal(err)
101 }
102 defer fence.Close()
103 defer w.Close()
104 if err := o.commitFrame(70, fence, false, true, pendingFrame{}); err != nil {
105 t.Fatal(err)
106 }
107 c := (*commits)[0]
108 if c.flags != atomicNonblock|flipEventFlag || c.user&3 != userFrame || c.user>>userKindBits != o.frame.pendingSerial {
109 t.Fatalf("flags %#x user %d", c.flags, c.user)
110 }
111 if v, _ := c.req.value(tPrimary, pFB); v != 70 {
112 t.Fatalf("primary fb %d", v)
113 }
114 if v, ok := c.req.value(tPrimary, pFence); !ok || v != uint64(fence.Fd()) {
115 t.Fatalf("fence %d %v", v, ok)
116 }
117 if v, _ := c.req.value(tCursor, 14); v != 12 {
118 t.Fatalf("cursor x %d", v)
119 }
120 if v, _ := c.req.value(tCrtc, pVRR); v != 1 {
121 t.Fatalf("vrr %d", v)
122 }
123 if !o.frame.pendingCommit() || !o.vrrOn || o.cursor.applied.x != 12 {
124 t.Fatalf("state pending=%v vrr=%v cursor=%+v", o.frame.pendingCommit(), o.vrrOn, o.cursor.applied)
125 }
126}
127
128func TestAsyncCommitHasOnlyPrimaryFBAndFence(t *testing.T) {
129 o, _, commits := testOutput(t)
130 fence, w, _ := os.Pipe()
131 defer fence.Close()
132 defer w.Close()
133 if err := o.commitFrame(70, fence, true, false, pendingFrame{}); err != nil {
134 t.Fatal(err)
135 }
136 c := (*commits)[0]
137 if c.flags&flipAsyncFlag == 0 {
138 t.Fatal("not async")
139 }
140 if objCount(c) != 1 || c.req.objs[0] != tPrimary || len(c.req.props[0]) != 2 {
141 t.Fatalf("async commit %+v", c.req)
142 }
143}
144
145func TestAsyncFallsBackToSyncWhenCursorMoves(t *testing.T) {
146 o, _, commits := testOutput(t)
147 o.cursor.image = true
148 o.cursor.Move(5, 5)
149 if err := o.commitFrame(70, nil, true, false, pendingFrame{}); err != nil {
150 t.Fatal(err)
151 }
152 if (*commits)[0].flags&flipAsyncFlag != 0 {
153 t.Fatal("async commit moved the cursor")
154 }
155}
156
157func TestAsyncWithoutFenceSupportStaysSync(t *testing.T) {
158 o, _, commits := testOutput(t)
159 o.asyncFence = false
160 fence, w, _ := os.Pipe()
161 defer fence.Close()
162 defer w.Close()
163 if err := o.commitFrame(70, fence, true, false, pendingFrame{}); err != nil {
164 t.Fatal(err)
165 }
166 if (*commits)[0].flags&flipAsyncFlag != 0 {
167 t.Fatal("async commit with a fence the driver refuses")
168 }
169}
170
171func TestScanoutAsyncRefusedRetriesSync(t *testing.T) {
172 o, _, commits := testOutput(t, unix.EINVAL)
173 o.shown, o.vrrOn = 1, true
174 buf := &ports.DMABuf{ID: 9}
175 o.clientFBs[9] = &clientFB{fbID: 80}
176 ok, err := o.commitScanout(80, ports.SurfaceContent{DMABuf: buf, Async: true}, pendingFrame{})
177 if !ok || err != nil {
178 t.Fatalf("ok=%v err=%v", ok, err)
179 }
180 if len(*commits) != 2 || (*commits)[0].flags&flipAsyncFlag == 0 || (*commits)[1].flags&flipAsyncFlag != 0 {
181 t.Fatalf("commits %+v", *commits)
182 }
183 if !o.clientFBs[9].noAsync || o.queued != 9 {
184 t.Fatalf("noAsync=%v queued=%d", o.clientFBs[9].noAsync, o.queued)
185 }
186}
187
188func TestCursorCommitWaitsForEvent(t *testing.T) {
189 o, _, commits := testOutput(t)
190 o.cursor.image = true
191 o.cursor.Move(1, 1)
192 if err := o.commitState(false); err != nil {
193 t.Fatal(err)
194 }
195 c := (*commits)[0]
196 if c.flags != atomicNonblock|flipEventFlag || c.user&3 != userState || !o.frame.pendingCommit() {
197 t.Fatalf("cursor commit flags %#x user %d pending %v", c.flags, c.user, o.frame.pendingCommit())
198 }
199 // Its event is not a frame: nothing is reported.
200 seen := map[ports.WindowID]uint64{}
201 o.completed(eventOf(c), seen)
202 if o.frame.pendingCommit() || o.reports.Len() != 0 || o.flips != 0 {
203 t.Fatalf("pending=%v unsent=%d flips=%d", o.frame.pendingCommit(), o.reports.Len(), o.flips)
204 }
205 // Nothing changed: no commit.
206 if err := o.commitState(false); err != nil || len(*commits) != 1 {
207 t.Fatalf("idle commit: %v, %d commits", err, len(*commits))
208 }
209}
210
211// Under VRR with a game shown, a cursor move rides on the next frame: a
212// cursor-only commit would hold the panel for its slowest refresh. A game
213// that stops drawing still gets the cursor after cursorMinInterval.
214func TestVRRGameCursorWaitsForFrame(t *testing.T) {
215 o, _, commits := testOutput(t)
216 o.cursor.image = true
217 o.vrrOn, o.vrrGame = true, true
218 o.lastFrame = time.Now()
219 o.cursor.Move(5, 5)
220 if err := o.commitState(o.stateVRR()); err != nil {
221 t.Fatal(err)
222 }
223 if len(*commits) != 0 || !o.cursorHeld || o.frame.pendingCommit() {
224 t.Fatalf("cursor committed alone: %d commits held=%v", len(*commits), o.cursorHeld)
225 }
226 // The next frame carries the cursor.
227 if err := o.commitFrame(70, nil, false, true, pendingFrame{}); err != nil {
228 t.Fatal(err)
229 }
230 if v, _ := (*commits)[0].req.value(tCursor, 14); v != 5 || o.cursorHeld {
231 t.Fatalf("frame cursor x %d held=%v", v, o.cursorHeld)
232 }
233 o.completed(eventOf((*commits)[0]), map[ports.WindowID]uint64{})
234 // The game stalls: the cursor commits alone after the interval.
235 o.lastFrame = time.Now().Add(-cursorMinInterval)
236 o.cursor.Move(8, 8)
237 if err := o.commitState(o.stateVRR()); err != nil {
238 t.Fatal(err)
239 }
240 if len(*commits) != 2 || o.cursorHeld {
241 t.Fatalf("stalled game: %d commits held=%v", len(*commits), o.cursorHeld)
242 }
243 if v, _ := (*commits)[1].req.value(tCursor, 14); v != 8 {
244 t.Fatalf("cursor x %d", v)
245 }
246}
247
248// A held move reversed before the next frame holds nothing: the cursor
249// timer is not armed again.
250func TestHeldCursorMovedBackClearsHold(t *testing.T) {
251 o, _, commits := testOutput(t)
252 o.cursor.image = true
253 o.vrrOn, o.vrrGame, o.lastFrame = true, true, time.Now()
254 o.cursor.Move(0, 0)
255 o.cursor.applied = o.cursor.desired() // on screen at (0, 0)
256 o.cursor.Move(5, 5)
257 if err := o.commitState(o.stateVRR()); err != nil || !o.cursorHeld {
258 t.Fatalf("err %v held=%v", err, o.cursorHeld)
259 }
260 o.cursor.Move(0, 0)
261 if err := o.commitState(o.stateVRR()); err != nil || o.cursorHeld || len(*commits) != 0 {
262 t.Fatalf("err %v held=%v commits %d", err, o.cursorHeld, len(*commits))
263 }
264}
265
266// Without a game, the cursor keeps its own commits.
267func TestCursorCommitsAloneWithoutGame(t *testing.T) {
268 o, _, commits := testOutput(t)
269 o.cursor.image = true
270 o.vrrOn, o.lastFrame = true, time.Now()
271 o.composedSince = time.Now()
272 o.cursor.Move(5, 5)
273 if err := o.commitState(o.stateVRR()); err != nil || len(*commits) != 1 || o.cursorHeld {
274 t.Fatalf("err %v, %d commits held=%v", err, len(*commits), o.cursorHeld)
275 }
276}
277
278func TestBusyCommitStaysPending(t *testing.T) {
279 o, _, _ := testOutput(t, unix.EBUSY)
280 err := o.commitFrame(70, nil, false, false, pendingFrame{})
281 enabled := true
282 if !o.commitFailed(err, &enabled) || !o.frame.pendingCommit() || !enabled {
283 t.Fatalf("EBUSY: pending=%v enabled=%v", o.frame.pendingCommit(), enabled)
284 }
285}
286
287func TestFlipReportsTimestampAndShownFrame(t *testing.T) {
288 o, _, commits := testOutput(t)
289 o.vrrOn = false
290 o.mode.Clock, o.mode.HTotal, o.mode.VTotal = 0, 0, 0
291 if err := o.commitFrame(70, nil, false, false, pendingFrame{shows: map[ports.WindowID]uint64{1: 4}}); err != nil {
292 t.Fatal(err)
293 }
294 o.completed(flipEvent{user: (*commits)[0].user, when: 5 * time.Second, seq: 11}, map[ports.WindowID]uint64{1: 4})
295 if o.reports.Len() != 1 {
296 t.Fatalf("reports %d", o.reports.Len())
297 }
298 f := o.reports.Unsent()[0].Flip
299 if f == nil || f.When != 5*time.Second || f.Seq != 11 || f.Refresh != time.Second/60 || f.Shows[1] != 4 || !f.HardwareClock {
300 t.Fatalf("flip %+v", f)
301 }
302}
303
304// Unchanged reports share an immutable snapshot; a later content update
305// must not change an earlier report, even after it has been sent.
306func TestReportSeenAllocations(t *testing.T) {
307 o, _, _ := testOutput(t)
308 seen := map[ports.WindowID]uint64{1: 1, 2: 2}
309 o.report(nil, seen)
310 // Keep the queue bounded without measuring the queue's growth.
311 if allocs := testing.AllocsPerRun(100, func() { o.report(nil, seen) }); allocs != 0 {
312 t.Errorf("unchanged report: %.1f allocs, want 0", allocs)
313 } else {
314 t.Logf("unchanged report: %.1f allocs", allocs)
315 }
316 previous := o.reports.Unsent()[0].Seen
317 seen[1] = 3
318 o.report(nil, seen)
319 if previous[1] != 1 || o.reports.Unsent()[0].Seen[1] != 3 {
320 t.Fatalf("snapshot mutated: old %v new %v", previous, o.reports.Unsent()[0].Seen)
321 }
322}
323
324func TestShownBySnapshotAllocations(t *testing.T) {
325 o := &Output{}
326 s := ports.Scene{OutputWidth: 10, OutputHeight: 10, Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 10, H: 10}}}}
327 seen := map[ports.WindowID]uint64{1: 1, 2: 2}
328 previous := o.shownBy(s, seen)
329 if allocs := testing.AllocsPerRun(100, func() { o.shownBy(s, seen) }); allocs != 0 {
330 t.Errorf("unchanged shows: %.1f allocs, want 0", allocs)
331 }
332 seen[1] = 3
333 current := o.shownBy(s, seen)
334 if previous[1] != 1 || current[1] != 3 {
335 t.Fatalf("snapshot mutated: old %v new %v", previous, current)
336 }
337 direct := o.directShownBy(1, 3)
338 if allocs := testing.AllocsPerRun(100, func() { o.directShownBy(1, 3) }); allocs != 0 {
339 t.Errorf("unchanged direct shows: %.1f allocs, want 0", allocs)
340 }
341 o.directShownBy(1, 4)
342 if direct[1] != 3 {
343 t.Fatalf("direct snapshot mutated: %v", direct)
344 }
345}
346
347func TestReportsKeepEveryFlip(t *testing.T) {
348 o, _, _ := testOutput(t)
349 seen := map[ports.WindowID]uint64{1: 1}
350 o.report(&ports.FlipInfo{Seq: 1}, seen)
351 o.report(nil, seen)
352 o.report(&ports.FlipInfo{Seq: 2}, seen)
353 o.report(nil, map[ports.WindowID]uint64{1: 3})
354 if o.reports.Len() != 2 || o.reports.Unsent()[0].Flip.Seq != 1 || o.reports.Unsent()[1].Flip.Seq != 2 || o.reports.Unsent()[1].Seen[1] != 3 {
355 t.Fatalf("unsent %+v", o.reports.Unsent())
356 }
357 for i := range presented.MaxUnsent + 3 {
358 o.report(&ports.FlipInfo{Seq: uint64(10 + i), Shows: map[ports.WindowID]uint64{}}, seen)
359 }
360 if o.reports.Len() != presented.MaxUnsent || o.reports.Unsent()[0].Flip.Merged == 0 {
361 t.Fatalf("cap: %d reports, first merged %d", o.reports.Len(), o.reports.Unsent()[0].Flip.Merged)
362 }
363}
364
365func TestModesetDisablesCursorAndStrayPlanes(t *testing.T) {
366 o, k, commits := testOutput(t)
367 stray := &plane{id: tOverlay, typ: planeOverlay, props: planeProps}
368 o.stray = []*plane{stray}
369 k.EXPECT().createBlob(mock.Anything).Return(99, nil).Once()
370 k.EXPECT().objProps(uint32(tOverlay), uint32(objPlane)).Return(map[string][2]uint64{"CRTC_ID": {pCrtcID, tCrtc}}, nil).Once()
371 if err := o.modeset(); err != nil {
372 t.Fatal(err)
373 }
374 c := (*commits)[0]
375 if c.flags != atomicAllowModes {
376 t.Fatalf("flags %#x", c.flags)
377 }
378 if v, _ := c.req.value(tCrtc, pMode); v != 99 {
379 t.Fatalf("mode blob %d", v)
380 }
381 for _, p := range []uint32{tCursor, tOverlay} {
382 if v, ok := c.req.value(p, pFB); !ok || v != 0 {
383 t.Fatalf("plane %d fb %d %v", p, v, ok)
384 }
385 }
386 if v, _ := c.req.value(tConn, pConnCrtc); v != tCrtc {
387 t.Fatalf("connector crtc %d", v)
388 }
389 // The next modeset replaces the blob.
390 k.EXPECT().createBlob(mock.Anything).Return(100, nil).Once()
391 k.EXPECT().objProps(uint32(tOverlay), uint32(objPlane)).Return(map[string][2]uint64{}, nil).Once()
392 k.EXPECT().destroyBlob(uint32(99)).Return(nil).Once()
393 if err := o.modeset(); err != nil {
394 t.Fatal(err)
395 }
396 if _, ok := (*commits)[1].req.value(tOverlay, pFB); ok {
397 t.Fatal("plane on another CRTC turned off")
398 }
399}
400
401func TestAsyncProbeAfterModeset(t *testing.T) {
402 o, _, commits := testOutput(t, unix.EINVAL)
403 o.asyncFence = true
404 r := portsmocks.NewMockRenderer(t)
405 fence, w, _ := os.Pipe()
406 defer w.Close()
407 r.EXPECT().UseTarget(mock.Anything).Return().Once()
408 r.EXPECT().Render(mock.Anything, mock.Anything).Return(fence, nil).Once()
409 o.probeAsync(r)
410 c := (*commits)[0]
411 if c.flags != atomicTestOnly|flipAsyncFlag {
412 t.Fatalf("flags %#x", c.flags)
413 }
414 if _, ok := c.req.value(tPrimary, pFence); !ok {
415 t.Fatal("probe without a fence")
416 }
417 if o.asyncFence {
418 t.Fatal("refused probe kept asyncFence")
419 }
420 if _, err := fence.Stat(); err == nil {
421 t.Fatal("probe fence not closed")
422 }
423 o.probeAsync(r) // once only
424}
425
426func TestVRRToggleOnlyInsideFrames(t *testing.T) {
427 o, _, commits := testOutput(t)
428 o.vrrOn = true
429 o.composedSince = time.Now().Add(-time.Second)
430 if o.wantVRR(false) {
431 t.Fatal("VRR kept after vrrHold of composition")
432 }
433 o.composedSince = time.Now()
434 if !o.wantVRR(false) {
435 t.Fatal("VRR dropped before vrrHold")
436 }
437 if err := o.commitState(false); err != nil {
438 t.Fatal(err)
439 }
440 if v, ok := (*commits)[0].req.value(tCrtc, pVRR); !ok || v != 0 || (*commits)[0].flags&flipEventFlag == 0 {
441 t.Fatalf("vrr off commit %+v", (*commits)[0])
442 }
443}
444
445// A composed fullscreen frame keeps VRR and restarts the hold; state
446// commits (cursor, vrrOff timer) keep it too until the fullscreen ends.
447func TestVRRKeptWhileFullscreenComposed(t *testing.T) {
448 o, _, _ := testOutput(t)
449 o.vrrOn = true
450 o.composedSince = time.Now().Add(-time.Second)
451 if !o.wantVRR(true) || !o.composedSince.IsZero() {
452 t.Fatal("composed fullscreen dropped VRR")
453 }
454 // Cursor-only state commit between frames.
455 if !o.stateVRR() {
456 t.Fatal("state commit dropped VRR during fullscreen")
457 }
458 // Fullscreen ends, a tiled window stays on the overlay plane: the
459 // hold starts, then VRR goes off.
460 o.shown = 9
461 if !o.wantVRR(false) || o.composedSince.IsZero() {
462 t.Fatal("hold not started on exit")
463 }
464 o.composedSince = time.Now().Add(-time.Second)
465 if o.stateVRR() {
466 t.Fatal("VRR kept after the hold (overlay buffer taken for a game)")
467 }
468}
469
470// A fullscreen game on a workspace that is left keeps VRR only for the
471// hold: its hidden window no longer counts as a game frame, and its
472// commits do not draw frames.
473func TestVRRHiddenGameOnlyHolds(t *testing.T) {
474 o, _, _ := testOutput(t)
475 game := ports.SceneWindow{ID: 1, Rect: ports.Rect{W: 100, H: 50}, Fullscreen: true}
476 shown := ports.Scene{OutputWidth: 100, OutputHeight: 50, Windows: []ports.SceneWindow{game}}
477 o.vrrOn = true
478 if !o.wantVRR(fullscreenShown(&shown)) {
479 t.Fatal("VRR off while the game is shown")
480 }
481 game.Hidden, game.Rect = true, ports.Rect{}
482 left := ports.Scene{OutputWidth: 100, OutputHeight: 50, Windows: []ports.SceneWindow{game, {ID: 2, Rect: ports.Rect{W: 100, H: 50}}}}
483 if fullscreenShown(&left) || left.Shows(1) {
484 t.Fatal("hidden game still drives frames")
485 }
486 if !o.wantVRR(fullscreenShown(&left)) {
487 t.Fatal("VRR dropped before the hold")
488 }
489 o.composedSince = time.Now().Add(-vrrHold - time.Millisecond)
490 if o.stateVRR() {
491 t.Fatal("VRR kept after the hold for a hidden game")
492 }
493}
494
495// A VRR refusal on a composed frame with an overlay retries with the
496// overlay: the composed image left its window out.
497func TestVRRRefusedKeepsOverlay(t *testing.T) {
498 o, k, commits := overlayOutput(t, unix.EINVAL)
499 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once()
500 o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
501 s, c := overlayScene()
502 ov, _ := o.overlayFrame(s, c)
503 if ov.fb == 0 {
504 t.Fatal("no overlay")
505 }
506 if err := o.commitWith(70, nil, false, true, pendingFrame{queued: ov.buf}, ov); err != nil {
507 t.Fatal(err)
508 }
509 if len(*commits) != 2 || o.vrrProp != 0 {
510 t.Fatalf("%d commits, vrrProp %d", len(*commits), o.vrrProp)
511 }
512 if v, _ := (*commits)[1].req.value(tOverlay, pFB); v != 88 || o.overlayOn != ov.buf {
513 t.Fatalf("retry overlay fb %d, overlayOn %d", v, o.overlayOn)
514 }
515}
516
517func TestReadPlanes(t *testing.T) {
518 k := newMockkms(t)
519 k.EXPECT().planes().Return([]planeRes{{id: 1, possible: 1}, {id: 2, possible: 2}, {id: 3, possible: 3}}, nil)
520 k.EXPECT().objProps(uint32(1), uint32(objPlane)).Return(map[string][2]uint64{"type": {20, planePrimary}, "FB_ID": {21, 0}, "IN_FORMATS": {22, 5}}, nil)
521 k.EXPECT().objProps(uint32(3), uint32(objPlane)).Return(map[string][2]uint64{"type": {20, planeCursor}}, nil)
522 k.EXPECT().getBlob(uint32(5)).Return(nil, nil)
523 ps, err := readPlanes(k, 0)
524 if err != nil || len(ps) != 2 || ps[0].typ != planePrimary || ps[0].prop("FB_ID") != 21 || ps[1].typ != planeCursor {
525 t.Fatalf("planes %+v %v", ps, err)
526 }
527}
528
529func TestVRRRefusedIsDisabled(t *testing.T) {
530 o, _, commits := testOutput(t, unix.EINVAL)
531 if err := o.commitFrame(70, nil, false, true, pendingFrame{}); err != nil {
532 t.Fatal(err)
533 }
534 if len(*commits) != 2 || o.vrrProp != 0 || !o.frame.pendingCommit() {
535 t.Fatalf("%d commits, vrrProp %d", len(*commits), o.vrrProp)
536 }
537 if v, _ := (*commits)[1].req.value(tCrtc, pVRR); v != 0 {
538 t.Fatal("retry kept VRR on")
539 }
540}
541
542func TestBufferRefusedIsNotBlamedOnVRR(t *testing.T) {
543 o, _, _ := testOutput(t, unix.EINVAL, unix.EINVAL)
544 o.clientFBs[9] = &clientFB{fbID: 80}
545 ok, err := o.commitScanout(80, ports.SurfaceContent{DMABuf: &ports.DMABuf{ID: 9}}, pendingFrame{})
546 if ok || err != nil || o.vrrProp == 0 || o.clientFBs[9].failed != "flip_refused" {
547 t.Fatalf("ok=%v err=%v vrrProp=%d failed=%q", ok, err, o.vrrProp, o.clientFBs[9].failed)
548 }
549}
550
551// Run: while a frame is in flight its buffers may still be read by the
552// GPU, so a newer content is reported only after the flip event; every
553// frame fence is closed.
554func TestRunReportsSeenAfterFlip(t *testing.T) {
555 o, k, commits, commitMu := testOutputMu(t)
556 o.cursor, o.tearing = nil, false
557 flips := make(chan flipEvent, 1)
558 o.flipped = flips
559 r := portsmocks.NewMockRenderer(t)
560 var mu sync.Mutex
561 var fences []*os.File
562 pipeBuf := func() ports.DMABuf {
563 f, w, _ := os.Pipe()
564 w.Close()
565 return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
566 }
567 r.EXPECT().SetHDR(float64(0)).Return().Maybe()
568 r.EXPECT().ExportTargets(2, mock.Anything).Return([]ports.DMABuf{pipeBuf(), pipeBuf()}, nil).Once()
569 r.EXPECT().UseTarget(mock.Anything).Return()
570 r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(ports.Scene, map[ports.WindowID]ports.SurfaceContent) (*os.File, error) {
571 f, w, _ := os.Pipe()
572 _, _ = w.Write([]byte{1}) // readable: signalled
573 w.Close()
574 mu.Lock()
575 fences = append(fences, f)
576 mu.Unlock()
577 return f, nil
578 })
579 r.EXPECT().Close().Return().Once()
580 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice()
581 k.EXPECT().createBlob(mock.Anything).Return(99, nil)
582 k.EXPECT().destroyBlob(mock.Anything).Return(nil).Maybe()
583 k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
584 frameCommits := func() int {
585 commitMu.Lock()
586 defer commitMu.Unlock()
587 n := 0
588 for _, c := range *commits {
589 if c.user&3 == userFrame {
590 n++
591 }
592 }
593 return n
594 }
595 scenes := make(chan ports.Scene, 1)
596 contents := make(chan ports.SurfaceContent, 1)
597 presented := make(chan ports.OutputPresented, 8)
598 ctx, cancel := context.WithCancel(context.Background())
599 done := make(chan error, 1)
600 go func() {
601 done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), scenes, contents, nil, presented, nil, nil)
602 }()
603 scenes <- ports.Scene{OutputWidth: 200, OutputHeight: 100, Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 10, H: 10}}}}
604 waitFor(t, func() bool { return frameCommits() == 1 })
605 // Frame 1 is in flight: content 3 replaces what it may be reading.
606 contents <- ports.SurfaceContent{ID: 1, Seq: 3, SHM: &ports.SHMBuffer{}}
607 select {
608 case p := <-presented:
609 t.Fatalf("report while a frame is in flight: %+v", p)
610 case <-time.After(30 * time.Millisecond):
611 }
612 commitMu.Lock()
613 last := (*commits)[len(*commits)-1]
614 commitMu.Unlock()
615 flips <- flipEvent{crtc: tCrtc, user: last.user, when: time.Second, seq: 1}
616 select {
617 case p := <-presented:
618 if p.Flip == nil || p.Flip.When != time.Second || p.Seen[1] != 3 {
619 t.Fatalf("report %+v", p)
620 }
621 case <-time.After(2 * time.Second):
622 t.Fatal("no report after the flip")
623 }
624 cancel()
625 if err := <-done; err != nil {
626 t.Fatal(err)
627 }
628 mu.Lock()
629 defer mu.Unlock()
630 if len(fences) < 3 {
631 t.Fatalf("%d renders", len(fences))
632 }
633 for _, f := range fences {
634 if _, err := f.Stat(); err == nil {
635 t.Fatal("frame fence left open")
636 }
637 }
638}
639
640// Run: a fullscreen window composed under an overlay layer keeps VRR on in
641// the frame commit; a tiled scene turns it off only after vrrHold.
642func TestRunComposedFullscreenKeepsVRR(t *testing.T) {
643 o, k, commits, commitMu := testOutputMu(t)
644 o.cursor, o.tearing = nil, false
645 flips := make(chan flipEvent, 1)
646 o.flipped = flips
647 r := portsmocks.NewMockRenderer(t)
648 pipeBuf := func() ports.DMABuf {
649 f, w, _ := os.Pipe()
650 w.Close()
651 return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
652 }
653 r.EXPECT().SetHDR(float64(0)).Return().Maybe()
654 r.EXPECT().ExportTargets(2, mock.Anything).Return([]ports.DMABuf{pipeBuf(), pipeBuf()}, nil).Once()
655 r.EXPECT().UseTarget(mock.Anything).Return()
656 r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)
657 r.EXPECT().Close().Return().Once()
658 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice()
659 k.EXPECT().createBlob(mock.Anything).Return(99, nil)
660 k.EXPECT().destroyBlob(mock.Anything).Return(nil).Maybe()
661 k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
662 lastFrame := func() (commitRec, int) {
663 commitMu.Lock()
664 defer commitMu.Unlock()
665 n, last := 0, commitRec{}
666 for _, c := range *commits {
667 if c.user&3 == userFrame {
668 n, last = n+1, c
669 }
670 }
671 return last, n
672 }
673 scenes := make(chan ports.Scene, 1)
674 ctx, cancel := context.WithCancel(context.Background())
675 done := make(chan error, 1)
676 go func() {
677 done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), scenes, nil, nil, make(chan ports.OutputPresented, 8), nil, nil)
678 }()
679 scenes <- ports.Scene{OutputWidth: 200, OutputHeight: 100,
680 Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 200, H: 100}, Fullscreen: true}},
681 Layers: []ports.SceneLayer{{ID: 5, Layer: ports.LayerOverlay, Rect: ports.Rect{W: 10, H: 10}}}}
682 waitFor(t, func() bool { _, n := lastFrame(); return n == 1 })
683 last, _ := lastFrame()
684 if v, ok := last.req.value(tCrtc, pVRR); !ok || v != 1 {
685 t.Fatalf("composed fullscreen frame VRR %d %v", v, ok)
686 }
687 flips <- flipEvent{crtc: tCrtc, user: last.user, when: time.Second, seq: 1}
688 scenes <- ports.Scene{OutputWidth: 200, OutputHeight: 100, Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 100, H: 100}}}}
689 waitFor(t, func() bool { _, n := lastFrame(); return n == 2 })
690 last, _ = lastFrame()
691 if v, _ := last.req.value(tCrtc, pVRR); v != 1 {
692 t.Fatal("VRR dropped before vrrHold")
693 }
694 flips <- flipEvent{crtc: tCrtc, user: last.user, when: 2 * time.Second, seq: 2}
695 // The vrrOff timer turns VRR off with a state commit after the hold.
696 waitFor(t, func() bool {
697 commitMu.Lock()
698 defer commitMu.Unlock()
699 c := (*commits)[len(*commits)-1]
700 v, ok := c.req.value(tCrtc, pVRR)
701 return c.user&3 == userState && ok && v == 0
702 })
703 cancel()
704 if err := <-done; err != nil {
705 t.Fatal(err)
706 }
707}
708
709func waitFor(t *testing.T, cond func() bool) {
710 t.Helper()
711 deadline := time.Now().Add(2 * time.Second)
712 for !cond() {
713 if time.Now().After(deadline) {
714 t.Fatal("timed out")
715 }
716 time.Sleep(2 * time.Millisecond)
717 }
718}
719
720// The stats tick lets the renderer free what the output no longer draws,
721// even while nothing renders; a failed trim stops the output.
722func TestRunTrimsRendererOnStatsTick(t *testing.T) {
723 o, k, _ := testOutput(t)
724 o.cursor, o.tearing, o.fbs = nil, false, [2]uint32{}
725 ticks := make(chan time.Time)
726 clk := portsmocks.NewMockClock(t)
727 tk := portsmocks.NewMockTicker(t)
728 clk.EXPECT().NewTicker(10 * time.Second).Return(tk).Once()
729 tk.EXPECT().C().Return(ticks)
730 tk.EXPECT().Stop().Return().Once()
731 now := time.Unix(1000, 0)
732 clk.EXPECT().Now().Return(now)
733 o.clock = clk
734 r := portsmocks.NewMockRenderer(t)
735 r.EXPECT().SetHDR(float64(0)).Return().Maybe()
736 r.EXPECT().Close().Return().Once()
737 r.EXPECT().TakeRedrawn().Return(1234).Once()
738 var statsLog bytes.Buffer // written by Run's goroutine, read after it ends
739 o.log = zerowrap.New(zerowrap.Config{Level: "info", Format: "json", Output: &statsLog})
740 o.flipStats = flipStats{missedVblanks: 3, maxInterval: 50 * time.Millisecond, maxFlipToRead: 25 * time.Millisecond, lateFences: 2}
741 trimmed := make(chan struct{}, 2)
742 failure := os.ErrInvalid
743 calls := 0 // Run's goroutine only
744 r.EXPECT().Trim(now).RunAndReturn(func(time.Time) error {
745 trimmed <- struct{}{}
746 if calls++; calls == 2 {
747 return failure
748 }
749 return nil
750 }).Twice()
751 k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
752 k.EXPECT().destroyBlob(mock.Anything).Return(nil).Maybe()
753 // Switched away from the start: no target, no frame.
754 active := make(chan bool, 1)
755 active <- false
756 done := make(chan error, 1)
757 go func() {
758 done <- o.Run(context.Background(), func(int, int) (ports.Renderer, error) { return r, nil }, nil, active, nil, nil, nil, make(chan ports.OutputPresented, 1), nil, nil)
759 }()
760 ticks <- now
761 <-trimmed
762 ticks <- now
763 if err := <-done; !errors.Is(err, failure) {
764 t.Fatalf("run: %v", err)
765 }
766 // Reported once, then reset. late_fences needs flip tracing.
767 for _, want := range []string{`"message":"stats"`, `"missed_vblanks":3`, `"max_flip_interval_ms":50`, `"max_flip_to_read_ms":25`, `"redrawn_pixels":1234`} {
768 if !strings.Contains(statsLog.String(), want) {
769 t.Fatalf("stats entry lacks %s: %s", want, statsLog.String())
770 }
771 }
772 if strings.Contains(statsLog.String(), "late_fences") || o.flipStats != (flipStats{}) {
773 t.Fatalf("late fences untraced or stats not reset: %s %+v", statsLog.String(), o.flipStats)
774 }
775}
776
777// A render failure must complete captures queued before the first frame.
778func TestCaptureRenderFailureClosesPendingRequest(t *testing.T) {
779 o, k, _ := testOutput(t)
780 o.cursor, o.tearing = nil, false
781 r := portsmocks.NewMockRenderer(t)
782 boom := errors.New("render failed")
783 r.EXPECT().SetHDR(float64(0)).Return().Once()
784 r.EXPECT().UseTarget(mock.Anything).Return()
785 makeBuf := func() ports.DMABuf {
786 f, err := os.CreateTemp(t.TempDir(), "target")
787 if err != nil {
788 t.Fatal(err)
789 }
790 t.Cleanup(func() { _ = f.Close() })
791 return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
792 }
793 r.EXPECT().ExportTargets(2, mock.Anything).Return([]ports.DMABuf{makeBuf(), makeBuf()}, nil).Once()
794 r.EXPECT().Render(ports.Scene{Background: "#000000"}, mock.Anything).Return(nil, nil).Twice()
795 r.EXPECT().Render(mock.MatchedBy(func(s ports.Scene) bool { return s.Background != "#000000" }), mock.Anything).Return(nil, boom).Once()
796 r.EXPECT().Close().Return().Once()
797 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice()
798 k.EXPECT().createBlob(mock.Anything).Return(99, nil)
799 k.EXPECT().destroyBlob(mock.Anything).Return(nil).Maybe()
800 k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
801 ctx, cancel := context.WithCancel(context.Background())
802 defer cancel()
803 scenes := make(chan ports.Scene, 1)
804 requests := make(chan ports.CaptureRequest)
805 replies := make(chan ports.CaptureDone, 1)
806 f, err := os.CreateTemp(t.TempDir(), "capture")
807 if err != nil {
808 t.Fatal(err)
809 }
810 done := make(chan error, 1)
811 go func() {
812 done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, nil, scenes, nil, nil, nil, requests, replies)
813 }()
814 // The unbuffered handoff proves Run owns the request before any scene.
815 select {
816 case requests <- ports.CaptureRequest{ID: 9, Dst: ports.SHMBuffer{File: f}}:
817 case <-time.After(2 * time.Second):
818 t.Fatal("output did not accept capture")
819 }
820 scenes <- ports.Scene{Seq: 1, Scale: 1}
821 select {
822 case err := <-done:
823 if !errors.Is(err, boom) {
824 t.Fatalf("run error %v", err)
825 }
826 case <-time.After(2 * time.Second):
827 t.Fatal("output did not stop after render failure")
828 }
829 select {
830 case result := <-replies:
831 if result.ID != 9 || result.Err == nil {
832 t.Fatalf("capture result %+v", result)
833 }
834 default:
835 t.Fatal("render failure lost capture completion")
836 }
837 if _, err := f.Stat(); err == nil {
838 t.Fatal("render failure leaked capture descriptor")
839 }
840}
841
842// A request must render even when the output already has an unchanged scene.
843func TestCaptureForcesDRMComposition(t *testing.T) {
844 o, k, commits, commitMu := testOutputMu(t)
845 o.cursor, o.tearing = nil, false
846 flips := make(chan flipEvent, 2)
847 o.flipped = flips
848 r := &portsmocks.MockRenderer{}
849 makeBuf := func() ports.DMABuf {
850 f, w, _ := os.Pipe()
851 w.Close()
852 return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
853 }
854 r.EXPECT().SetHDR(float64(0)).Return().Maybe() // SDR output
855 r.EXPECT().ExportTargets(2, mock.Anything).Return([]ports.DMABuf{makeBuf(), makeBuf()}, nil).Once()
856 r.EXPECT().UseTarget(mock.Anything).Return()
857 rendered := make(chan ports.Scene, 3)
858 r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(s ports.Scene, _ map[ports.WindowID]ports.SurfaceContent) (*os.File, error) {
859 rendered <- s
860 return nil, nil
861 })
862 frame := portsmocks.NewMockCaptureFrame(t)
863 frame.EXPECT().Done().Return(nil)
864 frame.EXPECT().Read(image.Rect(0, 0, 2, 2), mock.MatchedBy(func(p []byte) bool { return len(p) >= 16 }), 8).RunAndReturn(func(_ image.Rectangle, dst []byte, _ int) error { copy(dst, []byte{3, 4, 5, 255}); return nil }).Once()
865 r.EXPECT().BeginCapture().Return(frame, nil).Once()
866 r.EXPECT().EndCapture(frame).Return().Once()
867 r.EXPECT().Close().Return().Once()
868 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice()
869 k.EXPECT().createBlob(mock.Anything).Return(99, nil)
870 k.EXPECT().destroyBlob(mock.Anything).Return(nil).Maybe()
871 k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
872 scenes := make(chan ports.Scene, 1)
873 requests := make(chan ports.CaptureRequest, 1)
874 replies := make(chan ports.CaptureDone, 1)
875 ctx, cancel := context.WithCancel(context.Background())
876 done := make(chan error, 1)
877 go func() {
878 done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, nil, scenes, nil, nil, make(chan ports.OutputPresented, 8), requests, replies)
879 }()
880 scenes <- ports.Scene{OutputWidth: 200, OutputHeight: 100, Background: "#000000"}
881 select {
882 case <-rendered:
883 case <-time.After(2 * time.Second):
884 t.Fatal("no initial render")
885 }
886 waitFor(t, func() bool { commitMu.Lock(); defer commitMu.Unlock(); return len(*commits) > 0 })
887 commitMu.Lock()
888 last := (*commits)[len(*commits)-1]
889 commitMu.Unlock()
890 flips <- flipEvent{crtc: tCrtc, user: last.user, when: time.Second, seq: 1}
891 f, err := os.CreateTemp(t.TempDir(), "capture")
892 if err != nil {
893 t.Fatal(err)
894 }
895 if err := f.Truncate(16); err != nil {
896 t.Fatal(err)
897 }
898 requests <- ports.CaptureRequest{ID: 4, Region: image.Rect(0, 0, 2, 2), Width: 2, Height: 2, Stride: 8, Dst: ports.SHMBuffer{File: f}}
899 select {
900 case result := <-replies:
901 if result.Err != nil {
902 t.Fatal(result.Err)
903 }
904 case <-time.After(2 * time.Second):
905 t.Fatal("capture not replied")
906 }
907 select {
908 case <-rendered:
909 case <-time.After(2 * time.Second):
910 t.Fatal("capture did not render")
911 }
912 frame.Calls = nil // Testify must not inspect unmapped slice during expectation cleanup.
913 if _, err := f.Stat(); err == nil {
914 t.Fatal("fd not closed")
915 }
916 cancel()
917 if err := <-done; err != nil {
918 t.Fatal(err)
919 }
920}
921
922func TestRunPanicReportsReadinessError(t *testing.T) {
923 o, _, _ := testOutput(t)
924 o.ready = make(chan error, 1)
925 defer func() {
926 if recover() == nil {
927 t.Fatal("panic not propagated")
928 }
929 select {
930 case err := <-o.Ready():
931 if err == nil {
932 t.Fatal("panic reported as ready")
933 }
934 default:
935 t.Fatal("missing readiness")
936 }
937 }()
938 o.Run(context.Background(), func(int, int) (ports.Renderer, error) { panic("renderer panic") }, nil, nil, nil, nil, nil, nil, nil, nil)
939}
940
941// stampRun starts Run on an output with unbuffered scene and content
942// channels: a send completes once Run took the message, and the next send
943// once it went round its loop again, which is the sync point for what the
944// loop did with the first.
945type stampRun struct {
946 o *Output
947 scenes chan ports.Scene
948 contents chan ports.SurfaceContent
949 flips chan flipEvent
950 wake chan ports.SecurityState
951 state *atomic.Uint64
952 frameCommit func() int
953 lastCommit func() commitRec
954 stop func()
955}
956
957func startStampRun(t *testing.T) *stampRun {
958 o, k, commits, commitMu := testOutputMu(t)
959 o.cursor, o.tearing = nil, false
960 sr := &stampRun{o: o, scenes: make(chan ports.Scene), contents: make(chan ports.SurfaceContent), flips: make(chan flipEvent, 1), wake: make(chan ports.SecurityState, 1), state: &atomic.Uint64{}}
961 o.flipped = sr.flips
962 securityGate(t, o, sr.state)
963 o.SecurityChanges = sr.wake
964 r := portsmocks.NewMockRenderer(t)
965 pipeBuf := func() ports.DMABuf {
966 f, w, _ := os.Pipe()
967 w.Close()
968 return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
969 }
970 r.EXPECT().SetHDR(float64(0)).Return().Maybe()
971 r.EXPECT().ExportTargets(2, mock.Anything).Return([]ports.DMABuf{pipeBuf(), pipeBuf()}, nil).Once()
972 r.EXPECT().UseTarget(mock.Anything).Return()
973 r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)
974 r.EXPECT().Close().Return().Once()
975 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice()
976 k.EXPECT().createBlob(mock.Anything).Return(99, nil)
977 k.EXPECT().destroyBlob(mock.Anything).Return(nil).Maybe()
978 k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
979 sr.frameCommit = func() int {
980 commitMu.Lock()
981 defer commitMu.Unlock()
982 n := 0
983 for _, c := range *commits {
984 if c.user&3 == userFrame {
985 n++
986 }
987 }
988 return n
989 }
990 sr.lastCommit = func() commitRec {
991 commitMu.Lock()
992 defer commitMu.Unlock()
993 return (*commits)[len(*commits)-1]
994 }
995 ctx, cancel := context.WithCancel(context.Background())
996 done := make(chan error, 1)
997 go func() {
998 done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), sr.scenes, sr.contents, nil, make(chan ports.OutputPresented, 8), nil, nil)
999 }()
1000 // stop ends Run: its state is ours to read afterwards.
1001 sr.stop = func() {
1002 cancel()
1003 if err := <-done; err != nil {
1004 t.Fatal(err)
1005 }
1006 }
1007 return sr
1008}
1009
1010// Run: a frame wanted while the previous one is in flight is stamped when
1011// it becomes dirty, and the commit that follows takes the stamp.
1012func TestRunStampsWhenFrameWasWanted(t *testing.T) {
1013 sr := startStampRun(t)
1014 scene := ports.Scene{OutputWidth: 200, OutputHeight: 100, Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 10, H: 10}}}}
1015 sr.scenes <- scene
1016 waitFor(t, func() bool { return sr.frameCommit() == 1 })
1017 // Frame 1 is in flight: a new scene arrives and waits.
1018 last := sr.lastCommit()
1019 scene.Seq = 2
1020 before := monotonic()
1021 sr.scenes <- scene
1022 sr.contents <- ports.SurfaceContent{ID: 1, Seq: 1, SHM: &ports.SHMBuffer{}} // Run went round its loop
1023 flipSent := monotonic()
1024 sr.flips <- flipEvent{crtc: tCrtc, user: last.user, when: time.Second, seq: 1}
1025 waitFor(t, func() bool { return sr.frameCommit() == 2 })
1026 sr.stop()
1027 if sr.o.wantedAt != 0 {
1028 t.Fatalf("stamp left after the commit: %s", sr.o.wantedAt)
1029 }
1030 f := sr.o.frame.pendingFrame
1031 if !f.frame || f.wantedAt < before || f.wantedAt >= flipSent {
1032 t.Fatalf("second frame (%v) wanted at %s, scene sent at %s, flip sent at %s", f.frame, f.wantedAt, before, flipSent)
1033 }
1034}
1035
1036// A change of security state drops the scene, and with it the stamp of
1037// the frame it wanted.
1038func TestRunSecurityResetClearsWantedAt(t *testing.T) {
1039 sr := startStampRun(t)
1040 scene := ports.Scene{OutputWidth: 200, OutputHeight: 100, Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 10, H: 10}}}}
1041 sr.scenes <- scene
1042 waitFor(t, func() bool { return sr.frameCommit() == 1 })
1043 scene.Seq = 2
1044 sr.scenes <- scene
1045 sr.contents <- ports.SurfaceContent{ID: 1, Seq: 1, SHM: &ports.SHMBuffer{}} // stamped by now
1046 // A new security generation, not protected.
1047 sr.state.Store(2)
1048 sr.wake <- ports.SecurityState{}
1049 sr.contents <- ports.SurfaceContent{ID: 1, Seq: 2, SHM: &ports.SHMBuffer{}} // Run went round and reset
1050 sr.stop()
1051 if sr.o.wantedAt != 0 {
1052 t.Fatalf("stamp kept across the security reset: %s", sr.o.wantedAt)
1053 }
1054}