output_test.go RawHistoryFolder

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

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 23

Tags 10

↑↓ moveenter openesc close