v0.1.0 d9218d6

output_test.go RawHistoryFolder

1package drm
2
3import (
4	"context"
5	"os"
6	"sync"
7	"sync/atomic"
8	"testing"
9	"time"
10
11	portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
12	"github.com/bnema/neferwl/internal/ports"
13	"github.com/bnema/zerowrap"
14	"github.com/stretchr/testify/mock"
15	"golang.org/x/sys/unix"
16)
17
18// Object and property IDs of the test output.
19const (
20	tCrtc, tConn, tPrimary, tCursor, tOverlay = 30, 40, 50, 51, 52
21	pFB, pCrtcID, pFence, pMode, pActive      = 1, 2, 3, 4, 5
22	pVRR, pConnCrtc                           = 6, 7
23)
24
25var 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}
26
27// commitRec is one recorded atomic commit.
28type commitRec struct {
29	req   atomicReq
30	flags uint32
31	user  uint64
32}
33
34// testOutput returns an output on a mocked kms that records commits;
35// commit returns the next error of errs (nil when empty).
36func testOutput(t *testing.T, errs ...error) (*Output, *mockkms, *[]commitRec) {
37	o, k, commits, _ := testOutputMu(t, errs...)
38	return o, k, commits
39}
40
41// testOutputMu is testOutput with the mutex that guards the recorded
42// commits (Run commits on its goroutine).
43func testOutputMu(t *testing.T, errs ...error) (*Output, *mockkms, *[]commitRec, *sync.Mutex) {
44	k := newMockkms(t)
45	var commits []commitRec
46	commitMu := &sync.Mutex{}
47	k.EXPECT().commit(mock.Anything, mock.Anything, mock.Anything).RunAndReturn(func(r *atomicReq, flags uint32, user uint64) error {
48		commitMu.Lock()
49		defer commitMu.Unlock()
50		c := commitRec{flags: flags, user: user}
51		c.req.objs = append([]uint32(nil), r.objs...)
52		for _, ps := range r.props {
53			c.req.props = append(c.req.props, append([]propValue(nil), ps...))
54		}
55		commits = append(commits, c)
56		if len(errs) > 0 {
57			err := errs[0]
58			errs = errs[1:]
59			return err
60		}
61		return nil
62	}).Maybe()
63	primary := &plane{id: tPrimary, typ: planePrimary, props: planeProps}
64	cur := newCursor(&plane{id: tCursor, typ: planeCursor, props: planeProps}, 64)
65	cur.fbs = [2]uint32{90, 91}
66	o := &Output{k: k, serials: &atomic.Uint64{}, crtc: tCrtc, conn: connector{id: tConn, name: "DP-1"}, mode: modeInfo{HDisplay: 200, VDisplay: 100, VRefresh: 60}, log: zerowrap.Default(),
67		crtcProps: map[string]uint32{"MODE_ID": pMode, "ACTIVE": pActive, "VRR_ENABLED": pVRR}, connCrtc: pConnCrtc, vrrProp: pVRR,
68		primary: primary, cursor: cur, fbs: [2]uint32{70, 71}, clientFBs: map[uint64]*clientFB{}, scanout: true, tearing: true, asyncFence: true}
69	return o, k, &commits, commitMu
70}
71
72// eventOf is the flip event of commit c.
73func eventOf(c commitRec) flipEvent { return flipEvent{crtc: tCrtc, user: c.user} }
74
75func objCount(c commitRec) int { return len(c.req.objs) }
76
77func TestFrameCommitCarriesPrimaryFenceCursorAndVRR(t *testing.T) {
78	o, _, commits := testOutput(t)
79	o.cursor.image = true
80	o.cursor.Move(12, 34)
81	fence, w, err := os.Pipe()
82	if err != nil {
83		t.Fatal(err)
84	}
85	defer fence.Close()
86	defer w.Close()
87	if err := o.commitFrame(70, fence, false, true, pendingFrame{}); err != nil {
88		t.Fatal(err)
89	}
90	c := (*commits)[0]
91	if c.flags != atomicNonblock|flipEventFlag || c.user&3 != userFrame || c.user>>userKindBits != o.pendingSerial {
92		t.Fatalf("flags %#x user %d", c.flags, c.user)
93	}
94	if v, _ := c.req.value(tPrimary, pFB); v != 70 {
95		t.Fatalf("primary fb %d", v)
96	}
97	if v, ok := c.req.value(tPrimary, pFence); !ok || v != uint64(fence.Fd()) {
98		t.Fatalf("fence %d %v", v, ok)
99	}
100	if v, _ := c.req.value(tCursor, 14); v != 12 {
101		t.Fatalf("cursor x %d", v)
102	}
103	if v, _ := c.req.value(tCrtc, pVRR); v != 1 {
104		t.Fatalf("vrr %d", v)
105	}
106	if !o.pending || !o.vrrOn || o.cursor.applied.x != 12 {
107		t.Fatalf("state pending=%v vrr=%v cursor=%+v", o.pending, o.vrrOn, o.cursor.applied)
108	}
109}
110
111func TestAsyncCommitHasOnlyPrimaryFBAndFence(t *testing.T) {
112	o, _, commits := testOutput(t)
113	fence, w, _ := os.Pipe()
114	defer fence.Close()
115	defer w.Close()
116	if err := o.commitFrame(70, fence, true, false, pendingFrame{}); err != nil {
117		t.Fatal(err)
118	}
119	c := (*commits)[0]
120	if c.flags&flipAsyncFlag == 0 {
121		t.Fatal("not async")
122	}
123	if objCount(c) != 1 || c.req.objs[0] != tPrimary || len(c.req.props[0]) != 2 {
124		t.Fatalf("async commit %+v", c.req)
125	}
126}
127
128func TestAsyncFallsBackToSyncWhenCursorMoves(t *testing.T) {
129	o, _, commits := testOutput(t)
130	o.cursor.image = true
131	o.cursor.Move(5, 5)
132	if err := o.commitFrame(70, nil, true, false, pendingFrame{}); err != nil {
133		t.Fatal(err)
134	}
135	if (*commits)[0].flags&flipAsyncFlag != 0 {
136		t.Fatal("async commit moved the cursor")
137	}
138}
139
140func TestAsyncWithoutFenceSupportStaysSync(t *testing.T) {
141	o, _, commits := testOutput(t)
142	o.asyncFence = false
143	fence, w, _ := os.Pipe()
144	defer fence.Close()
145	defer w.Close()
146	if err := o.commitFrame(70, fence, true, false, pendingFrame{}); err != nil {
147		t.Fatal(err)
148	}
149	if (*commits)[0].flags&flipAsyncFlag != 0 {
150		t.Fatal("async commit with a fence the driver refuses")
151	}
152}
153
154func TestScanoutAsyncRefusedRetriesSync(t *testing.T) {
155	o, _, commits := testOutput(t, unix.EINVAL)
156	o.shown, o.vrrOn = 1, true
157	buf := &ports.DMABuf{ID: 9}
158	o.clientFBs[9] = &clientFB{fbID: 80}
159	ok, err := o.commitScanout(80, ports.SurfaceContent{DMABuf: buf, Async: true}, pendingFrame{})
160	if !ok || err != nil {
161		t.Fatalf("ok=%v err=%v", ok, err)
162	}
163	if len(*commits) != 2 || (*commits)[0].flags&flipAsyncFlag == 0 || (*commits)[1].flags&flipAsyncFlag != 0 {
164		t.Fatalf("commits %+v", *commits)
165	}
166	if !o.clientFBs[9].noAsync || o.queued != 9 {
167		t.Fatalf("noAsync=%v queued=%d", o.clientFBs[9].noAsync, o.queued)
168	}
169}
170
171func TestCursorCommitWaitsForEvent(t *testing.T) {
172	o, _, commits := testOutput(t)
173	o.cursor.image = true
174	o.cursor.Move(1, 1)
175	if err := o.commitState(false); err != nil {
176		t.Fatal(err)
177	}
178	c := (*commits)[0]
179	if c.flags != atomicNonblock|flipEventFlag || c.user&3 != userState || !o.pending {
180		t.Fatalf("cursor commit flags %#x user %d pending %v", c.flags, c.user, o.pending)
181	}
182	// Its event is not a frame: nothing is reported.
183	seen := map[ports.WindowID]uint64{}
184	o.completed(eventOf(c), seen)
185	if o.pending || len(o.unsent) != 0 || o.flips != 0 {
186		t.Fatalf("pending=%v unsent=%d flips=%d", o.pending, len(o.unsent), o.flips)
187	}
188	// Nothing changed: no commit.
189	if err := o.commitState(false); err != nil || len(*commits) != 1 {
190		t.Fatalf("idle commit: %v, %d commits", err, len(*commits))
191	}
192}
193
194func TestBusyCommitStaysPending(t *testing.T) {
195	o, _, _ := testOutput(t, unix.EBUSY)
196	err := o.commitFrame(70, nil, false, false, pendingFrame{})
197	enabled := true
198	if !o.commitFailed(err, &enabled) || !o.pending || !enabled {
199		t.Fatalf("EBUSY: pending=%v enabled=%v", o.pending, enabled)
200	}
201}
202
203func TestFlipReportsTimestampAndShownFrame(t *testing.T) {
204	o, _, commits := testOutput(t)
205	o.vrrOn = false
206	o.mode.Clock, o.mode.HTotal, o.mode.VTotal = 0, 0, 0
207	if err := o.commitFrame(70, nil, false, false, pendingFrame{shows: map[ports.WindowID]uint64{1: 4}}); err != nil {
208		t.Fatal(err)
209	}
210	o.completed(flipEvent{user: (*commits)[0].user, when: 5 * time.Second, seq: 11}, map[ports.WindowID]uint64{1: 4})
211	if len(o.unsent) != 1 {
212		t.Fatalf("reports %d", len(o.unsent))
213	}
214	f := o.unsent[0].Flip
215	if f == nil || f.When != 5*time.Second || f.Seq != 11 || f.Refresh != time.Second/60 || f.Shows[1] != 4 || !f.HardwareClock {
216		t.Fatalf("flip %+v", f)
217	}
218}
219
220func TestReportsKeepEveryFlip(t *testing.T) {
221	o, _, _ := testOutput(t)
222	seen := map[ports.WindowID]uint64{1: 1}
223	o.report(&ports.FlipInfo{Seq: 1}, seen)
224	o.report(nil, seen)
225	o.report(&ports.FlipInfo{Seq: 2}, seen)
226	o.report(nil, map[ports.WindowID]uint64{1: 3})
227	if len(o.unsent) != 2 || o.unsent[0].Flip.Seq != 1 || o.unsent[1].Flip.Seq != 2 || o.unsent[1].Seen[1] != 3 {
228		t.Fatalf("unsent %+v", o.unsent)
229	}
230	for i := range maxUnsent + 3 {
231		o.report(&ports.FlipInfo{Seq: uint64(10 + i), Shows: map[ports.WindowID]uint64{}}, seen)
232	}
233	if len(o.unsent) != maxUnsent || o.unsent[0].Flip.Merged == 0 {
234		t.Fatalf("cap: %d reports, first merged %d", len(o.unsent), o.unsent[0].Flip.Merged)
235	}
236}
237
238func TestModesetDisablesCursorAndStrayPlanes(t *testing.T) {
239	o, k, commits := testOutput(t)
240	stray := &plane{id: tOverlay, typ: planeOverlay, props: planeProps}
241	o.stray = []*plane{stray}
242	k.EXPECT().createBlob(mock.Anything).Return(99, nil).Once()
243	k.EXPECT().objProps(uint32(tOverlay), uint32(objPlane)).Return(map[string][2]uint64{"CRTC_ID": {pCrtcID, tCrtc}}, nil).Once()
244	if err := o.modeset(); err != nil {
245		t.Fatal(err)
246	}
247	c := (*commits)[0]
248	if c.flags != atomicAllowModes {
249		t.Fatalf("flags %#x", c.flags)
250	}
251	if v, _ := c.req.value(tCrtc, pMode); v != 99 {
252		t.Fatalf("mode blob %d", v)
253	}
254	for _, p := range []uint32{tCursor, tOverlay} {
255		if v, ok := c.req.value(p, pFB); !ok || v != 0 {
256			t.Fatalf("plane %d fb %d %v", p, v, ok)
257		}
258	}
259	if v, _ := c.req.value(tConn, pConnCrtc); v != tCrtc {
260		t.Fatalf("connector crtc %d", v)
261	}
262	// The next modeset replaces the blob.
263	k.EXPECT().createBlob(mock.Anything).Return(100, nil).Once()
264	k.EXPECT().objProps(uint32(tOverlay), uint32(objPlane)).Return(map[string][2]uint64{}, nil).Once()
265	k.EXPECT().destroyBlob(uint32(99)).Return(nil).Once()
266	if err := o.modeset(); err != nil {
267		t.Fatal(err)
268	}
269	if _, ok := (*commits)[1].req.value(tOverlay, pFB); ok {
270		t.Fatal("plane on another CRTC turned off")
271	}
272}
273
274func TestAsyncProbeAfterModeset(t *testing.T) {
275	o, _, commits := testOutput(t, unix.EINVAL)
276	o.asyncFence = true
277	r := portsmocks.NewMockRenderer(t)
278	fence, w, _ := os.Pipe()
279	defer w.Close()
280	r.EXPECT().UseTarget(mock.Anything).Return().Once()
281	r.EXPECT().Render(mock.Anything, mock.Anything).Return(fence, nil).Once()
282	o.probeAsync(r)
283	c := (*commits)[0]
284	if c.flags != atomicTestOnly|flipAsyncFlag {
285		t.Fatalf("flags %#x", c.flags)
286	}
287	if _, ok := c.req.value(tPrimary, pFence); !ok {
288		t.Fatal("probe without a fence")
289	}
290	if o.asyncFence {
291		t.Fatal("refused probe kept asyncFence")
292	}
293	if _, err := fence.Stat(); err == nil {
294		t.Fatal("probe fence not closed")
295	}
296	o.probeAsync(r) // once only
297}
298
299func TestVRRToggleOnlyInsideFrames(t *testing.T) {
300	o, _, commits := testOutput(t)
301	o.vrrOn = true
302	o.composedSince = time.Now().Add(-time.Second)
303	if o.wantVRR(false) {
304		t.Fatal("VRR kept after vrrHold of composition")
305	}
306	o.composedSince = time.Now()
307	if !o.wantVRR(false) {
308		t.Fatal("VRR dropped before vrrHold")
309	}
310	if err := o.commitState(false); err != nil {
311		t.Fatal(err)
312	}
313	if v, ok := (*commits)[0].req.value(tCrtc, pVRR); !ok || v != 0 || (*commits)[0].flags&flipEventFlag == 0 {
314		t.Fatalf("vrr off commit %+v", (*commits)[0])
315	}
316}
317
318func TestReadPlanes(t *testing.T) {
319	k := newMockkms(t)
320	k.EXPECT().planes().Return([]planeRes{{id: 1, possible: 1}, {id: 2, possible: 2}, {id: 3, possible: 3}}, nil)
321	k.EXPECT().objProps(uint32(1), uint32(objPlane)).Return(map[string][2]uint64{"type": {20, planePrimary}, "FB_ID": {21, 0}, "IN_FORMATS": {22, 5}}, nil)
322	k.EXPECT().objProps(uint32(3), uint32(objPlane)).Return(map[string][2]uint64{"type": {20, planeCursor}}, nil)
323	k.EXPECT().getBlob(uint32(5)).Return(nil, nil)
324	ps, err := readPlanes(k, 0)
325	if err != nil || len(ps) != 2 || ps[0].typ != planePrimary || ps[0].prop("FB_ID") != 21 || ps[1].typ != planeCursor {
326		t.Fatalf("planes %+v %v", ps, err)
327	}
328}
329
330func TestVRRRefusedIsDisabled(t *testing.T) {
331	o, _, commits := testOutput(t, unix.EINVAL)
332	if err := o.commitFrame(70, nil, false, true, pendingFrame{}); err != nil {
333		t.Fatal(err)
334	}
335	if len(*commits) != 2 || o.vrrProp != 0 || !o.pending {
336		t.Fatalf("%d commits, vrrProp %d", len(*commits), o.vrrProp)
337	}
338	if v, _ := (*commits)[1].req.value(tCrtc, pVRR); v != 0 {
339		t.Fatal("retry kept VRR on")
340	}
341}
342
343func TestBufferRefusedIsNotBlamedOnVRR(t *testing.T) {
344	o, _, _ := testOutput(t, unix.EINVAL, unix.EINVAL)
345	o.clientFBs[9] = &clientFB{fbID: 80}
346	ok, err := o.commitScanout(80, ports.SurfaceContent{DMABuf: &ports.DMABuf{ID: 9}}, pendingFrame{})
347	if ok || err != nil || o.vrrProp == 0 || o.clientFBs[9].failed != "flip_refused" {
348		t.Fatalf("ok=%v err=%v vrrProp=%d failed=%q", ok, err, o.vrrProp, o.clientFBs[9].failed)
349	}
350}
351
352// Run: while a frame is in flight its buffers may still be read by the
353// GPU, so a newer content is reported only after the flip event; every
354// frame fence is closed.
355func TestRunReportsSeenAfterFlip(t *testing.T) {
356	o, k, commits, commitMu := testOutputMu(t)
357	o.cursor, o.tearing = nil, false
358	flips := make(chan flipEvent, 1)
359	o.flipped = flips
360	r := portsmocks.NewMockRenderer(t)
361	var mu sync.Mutex
362	var fences []*os.File
363	pipeBuf := func() ports.DMABuf {
364		f, w, _ := os.Pipe()
365		w.Close()
366		return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
367	}
368	r.EXPECT().ExportTargets(2, mock.Anything).Return([]ports.DMABuf{pipeBuf(), pipeBuf()}, nil).Once()
369	r.EXPECT().UseTarget(mock.Anything).Return()
370	r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(ports.Scene, map[ports.WindowID]ports.SurfaceContent) (*os.File, error) {
371		f, w, _ := os.Pipe()
372		_, _ = w.Write([]byte{1}) // readable: signalled
373		w.Close()
374		mu.Lock()
375		fences = append(fences, f)
376		mu.Unlock()
377		return f, nil
378	})
379	r.EXPECT().Close().Return().Once()
380	k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice()
381	k.EXPECT().createBlob(mock.Anything).Return(99, nil)
382	k.EXPECT().destroyBlob(mock.Anything).Return(nil).Maybe()
383	k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
384	frameCommits := func() int {
385		commitMu.Lock()
386		defer commitMu.Unlock()
387		n := 0
388		for _, c := range *commits {
389			if c.user&3 == userFrame {
390				n++
391			}
392		}
393		return n
394	}
395	scenes := make(chan ports.Scene, 1)
396	contents := make(chan ports.SurfaceContent, 1)
397	presented := make(chan ports.OutputPresented, 8)
398	ctx, cancel := context.WithCancel(context.Background())
399	done := make(chan error, 1)
400	go func() {
401		done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), scenes, contents, nil, presented)
402	}()
403	scenes <- ports.Scene{OutputWidth: 200, OutputHeight: 100, Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 10, H: 10}}}}
404	waitFor(t, func() bool { return frameCommits() == 1 })
405	// Frame 1 is in flight: content 3 replaces what it may be reading.
406	contents <- ports.SurfaceContent{ID: 1, Seq: 3, SHM: &ports.SHMBuffer{}}
407	select {
408	case p := <-presented:
409		t.Fatalf("report while a frame is in flight: %+v", p)
410	case <-time.After(30 * time.Millisecond):
411	}
412	commitMu.Lock()
413	last := (*commits)[len(*commits)-1]
414	commitMu.Unlock()
415	flips <- flipEvent{crtc: tCrtc, user: last.user, when: time.Second, seq: 1}
416	select {
417	case p := <-presented:
418		if p.Flip == nil || p.Flip.When != time.Second || p.Seen[1] != 3 {
419			t.Fatalf("report %+v", p)
420		}
421	case <-time.After(2 * time.Second):
422		t.Fatal("no report after the flip")
423	}
424	cancel()
425	if err := <-done; err != nil {
426		t.Fatal(err)
427	}
428	mu.Lock()
429	defer mu.Unlock()
430	if len(fences) < 3 {
431		t.Fatalf("%d renders", len(fences))
432	}
433	for _, f := range fences {
434		if _, err := f.Stat(); err == nil {
435			t.Fatal("frame fence left open")
436		}
437	}
438}
439
440func waitFor(t *testing.T, cond func() bool) {
441	t.Helper()
442	deadline := time.Now().Add(2 * time.Second)
443	for !cond() {
444		if time.Now().After(deadline) {
445			t.Fatal("timed out")
446		}
447		time.Sleep(2 * time.Millisecond)
448	}
449}

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