v0.2.0 13c6a90

output_test.go RawHistoryFolder

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

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 25

Tags 10

↑↓ moveenter openesc close