v0.1.0 d9218d6

commit_test.go RawHistoryFolder

1package drm
2
3import (
4	"context"
5	"os"
6	"testing"
7	"time"
8
9	portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
10	"github.com/bnema/neferwl/internal/ports"
11	"github.com/stretchr/testify/mock"
12	"golang.org/x/sys/unix"
13)
14
15// An event queued before a modeset (or for an earlier output on the CRTC)
16// must not complete the commit that follows.
17func TestStaleEventDoesNotCompleteNewCommit(t *testing.T) {
18	o, _, commits := testOutput(t)
19	if err := o.commitFrame(70, nil, false, false, pendingFrame{}); err != nil {
20		t.Fatal(err)
21	}
22	old := eventOf((*commits)[0])
23	o.pending = false // a modeset dropped the commit; its event is queued
24	if err := o.commitFrame(71, nil, false, false, pendingFrame{}); err != nil {
25		t.Fatal(err)
26	}
27	if o.completed(old, nil) || !o.pending {
28		t.Fatal("stale event completed the new commit")
29	}
30	if !o.completed(eventOf((*commits)[1]), nil) || o.pending {
31		t.Fatal("own event did not complete the commit")
32	}
33}
34
35// After EBUSY no commit of ours is known: the next event of the CRTC ends
36// the wait, then the frame commits.
37func TestBusyFrameCommitsAfterEvent(t *testing.T) {
38	o, _, commits := testOutput(t, unix.EBUSY)
39	enabled := true
40	if err := o.commitFrame(70, nil, false, false, pendingFrame{}); !o.commitFailed(err, &enabled) {
41		t.Fatalf("EBUSY not handled: %v", err)
42	}
43	if !o.completed(flipEvent{crtc: tCrtc, user: 12345 << userKindBits}, nil) || o.pending {
44		t.Fatal("EBUSY wait not ended by the CRTC's event")
45	}
46	if err := o.commitFrame(70, nil, false, false, pendingFrame{}); err != nil || len(*commits) != 2 || !o.pending {
47		t.Fatalf("frame after EBUSY: %v, %d commits", err, len(*commits))
48	}
49}
50
51// A cursor image change writes only an image the plane neither shows nor
52// is about to show; with both busy it waits for the event.
53func TestCursorImageNeverOverwritesScreen(t *testing.T) {
54	o, _, _ := testOutput(t)
55	c := o.cursor
56	r := portsmocks.NewMockRenderer(t)
57	var written []int
58	r.EXPECT().WriteCursor(mock.Anything, mock.Anything, 1, 1).RunAndReturn(func(i int, _ []byte, _, _ int) error {
59		written = append(written, i)
60		return nil
61	})
62	px := []byte{1, 2, 3, 4}
63	// Image 0 on screen: two changes before a commit both go to image 1.
64	c.screen, c.cur, c.image = 90, 0, true
65	for range 2 {
66		if err := c.setImage(r, px, 1, 1, 0, 0); err != nil {
67			t.Fatal(err)
68		}
69	}
70	if len(written) != 2 || written[0] != 1 || written[1] != 1 {
71		t.Fatalf("written %v", written)
72	}
73	// Image 1 in flight, image 0 on screen: nothing is free.
74	c.committed(cursorState{on: true, fb: 91})
75	if err := c.setImage(r, px, 1, 1, 0, 0); err != nil || len(written) != 2 || c.later == nil {
76		t.Fatalf("wrote a busy image: %v %v", err, written)
77	}
78	// The flip frees image 0.
79	c.landed()
80	if changed, err := c.flushLater(r); !changed || err != nil || len(written) != 3 || written[2] != 0 || c.cur != 0 {
81		t.Fatalf("after flip: %v %v %v cur %d", changed, err, written, c.cur)
82	}
83}
84
85// KMS refusing the cursor plane turns the hardware cursor off and the
86// frame commits without it; the output keeps running.
87func TestCursorRefusedFrameCommitsWithout(t *testing.T) {
88	o, _, commits := testOutput(t, unix.EINVAL, unix.EINVAL)
89	o.cursor.image = true
90	o.cursor.Move(5, 5)
91	if err := o.commitFrame(70, nil, false, false, pendingFrame{}); err != nil {
92		t.Fatal(err)
93	}
94	if !o.cursor.off || len(*commits) != 3 || (*commits)[1].flags != atomicTestOnly {
95		t.Fatalf("off=%v commits %d", o.cursor.off, len(*commits))
96	}
97	if v, _ := (*commits)[2].req.value(tCursor, pFB); v != 0 {
98		t.Fatalf("retry shows cursor fb %d", v)
99	}
100}
101
102// A refused cursor is not blamed on the client buffer.
103func TestCursorRefusedDoesNotBlameScanoutBuffer(t *testing.T) {
104	o, _, _ := testOutput(t, unix.EINVAL, unix.EINVAL)
105	o.cursor.image = true
106	o.cursor.Move(5, 5)
107	o.clientFBs[9] = &clientFB{fbID: 80}
108	direct, err := o.commitScanout(80, ports.SurfaceContent{DMABuf: &ports.DMABuf{ID: 9}}, pendingFrame{})
109	if err != nil || !direct || o.clientFBs[9].failed != "" {
110		t.Fatalf("direct=%v err=%v failed=%q", direct, err, o.clientFBs[9].failed)
111	}
112}
113
114// showImages validates the images with a TEST_ONLY modeset and falls back
115// to linear images when KMS refuses the driver's.
116func TestShowImagesTestsBeforeModeset(t *testing.T) {
117	o, k, commits := testOutput(t, unix.EINVAL)
118	o.cursor = nil
119	r := portsmocks.NewMockRenderer(t)
120	buf := func() ports.DMABuf {
121		f, w, _ := os.Pipe()
122		w.Close()
123		return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
124	}
125	r.EXPECT().ExportTargets(2, []uint64(nil)).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
126	r.EXPECT().ExportTargets(2, []uint64{0}).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
127	r.EXPECT().UseTarget(mock.Anything).Return()
128	r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)
129	k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil)
130	k.EXPECT().rmFB(mock.Anything).Return(nil)
131	k.EXPECT().createBlob(mock.Anything).Return(99, nil)
132	k.EXPECT().destroyBlob(mock.Anything).Return(nil)
133	if err := o.showImages(r, imagesDriver, nil); err != nil {
134		t.Fatal(err)
135	}
136	want := []uint32{atomicTestOnly | atomicAllowModes, atomicTestOnly | atomicAllowModes, atomicAllowModes}
137	if len(*commits) != len(want) {
138		t.Fatalf("%d commits", len(*commits))
139	}
140	for i, f := range want {
141		if (*commits)[i].flags != f {
142			t.Fatalf("commit %d flags %#x want %#x", i, (*commits)[i].flags, f)
143		}
144	}
145	if o.kind != imagesLinear {
146		t.Fatalf("kind %d", o.kind)
147	}
148}
149
150// Enabling an output whose images were not made yet (started switched
151// away) tests them and modesets once.
152func TestEnableAfterStartAwayModesetsOnce(t *testing.T) {
153	o, k, commits, commitMu := testOutputMu(t)
154	o.cursor, o.tearing, o.fbs = nil, false, [2]uint32{}
155	r := portsmocks.NewMockRenderer(t)
156	buf := func() ports.DMABuf {
157		f, w, _ := os.Pipe()
158		w.Close()
159		return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
160	}
161	r.EXPECT().ExportTargets(2, mock.Anything).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
162	r.EXPECT().UseTarget(mock.Anything).Return()
163	r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)
164	r.EXPECT().Close().Return().Once()
165	k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice()
166	k.EXPECT().createBlob(mock.Anything).Return(99, nil)
167	k.EXPECT().destroyBlob(mock.Anything).Return(nil)
168	k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
169	active := make(chan bool, 1)
170	active <- false
171	ctx, cancel := context.WithCancel(context.Background())
172	done := make(chan error, 1)
173	go func() {
174		done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, active, nil, nil, nil, make(chan ports.OutputPresented, 1))
175	}()
176	count := func(flags uint32) int {
177		commitMu.Lock()
178		defer commitMu.Unlock()
179		n := 0
180		for _, c := range *commits {
181			if c.flags == flags {
182				n++
183			}
184		}
185		return n
186	}
187	time.Sleep(20 * time.Millisecond)
188	if n := count(atomicAllowModes); n != 0 {
189		t.Fatalf("%d modesets while away", n)
190	}
191	active <- true
192	waitFor(t, func() bool { return count(atomicAllowModes) == 1 })
193	cancel()
194	if err := <-done; err != nil {
195		t.Fatal(err)
196	}
197	if n := count(atomicTestOnly | atomicAllowModes); n != 1 {
198		t.Fatalf("%d test modesets", n)
199	}
200}
201
202// Close without a mode to restore turns the CRTC off.
203func TestCloseWithoutSavedModeDisablesCrtc(t *testing.T) {
204	o, k, commits := testOutput(t)
205	k.EXPECT().rmFB(mock.Anything).Return(nil)
206	o.Close()
207	c := (*commits)[0]
208	for _, p := range []struct{ obj, prop uint32 }{{tCrtc, pActive}, {tCrtc, pMode}, {tPrimary, pFB}, {tConn, pConnCrtc}} {
209		if v, ok := c.req.value(p.obj, p.prop); !ok || v != 0 {
210			t.Fatalf("object %d prop %d = %d %v", p.obj, p.prop, v, ok)
211		}
212	}
213}
214
215// Close restores the saved mode and gives the connector its CRTC back.
216func TestCloseRestoresSavedMode(t *testing.T) {
217	o, k, commits := testOutput(t)
218	o.saved = modeCrtc{crtcID: tCrtc, fbID: 5, modeValid: 1, mode: modeInfo{HDisplay: 200, VDisplay: 100}}
219	k.EXPECT().createBlob(mock.Anything).Return(98, nil).Once()
220	k.EXPECT().destroyBlob(uint32(98)).Return(nil).Once()
221	k.EXPECT().rmFB(mock.Anything).Return(nil)
222	o.Close()
223	c := (*commits)[0]
224	if v, _ := c.req.value(tConn, pConnCrtc); v != tCrtc {
225		t.Fatalf("connector crtc %d", v)
226	}
227	if v, _ := c.req.value(tPrimary, pFB); v != 5 {
228		t.Fatalf("primary fb %d", v)
229	}
230}
231
232// With the cursor off no image is loaded (no warning per change).
233func TestCursorOffSkipsLoads(t *testing.T) {
234	o, _, _ := testOutput(t)
235	o.cursor.off = true
236	loaded := false
237	o.setCursor(nil, func(ports.CursorChange, float64, int) (ports.CursorImage, error) {
238		loaded = true
239		return ports.CursorImage{}, nil
240	}, ports.CursorChange{}, 1)
241	if loaded || o.cursor.desired().on {
242		t.Fatal("cursor used while off")
243	}
244}
245
246// Content read while only a cursor commit is in flight is reported when
247// that commit's event arrives, not held until a later frame.
248func TestRunReportsSeenAfterCursorCommit(t *testing.T) {
249	o, k, commits, commitMu := testOutputMu(t)
250	o.tearing, o.cursor.fbs = false, [2]uint32{}
251	flips := make(chan flipEvent, 1)
252	o.flipped = flips
253	r := portsmocks.NewMockRenderer(t)
254	buf := func() ports.DMABuf {
255		f, w, _ := os.Pipe()
256		w.Close()
257		return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
258	}
259	r.EXPECT().CursorBuffers(64).Return([2]ports.DMABuf{buf(), buf()}, nil).Once()
260	r.EXPECT().WriteCursor(mock.Anything, mock.Anything, 1, 1).Return(nil)
261	r.EXPECT().ExportTargets(2, mock.Anything).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
262	r.EXPECT().UseTarget(mock.Anything).Return()
263	r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)
264	r.EXPECT().Close().Return().Once()
265	k.EXPECT().addFB(mock.Anything, uint32(fourccARGB)).Return(90, nil).Once()
266	k.EXPECT().addFB(mock.Anything, uint32(fourccARGB)).Return(91, nil).Once()
267	k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice()
268	k.EXPECT().createBlob(mock.Anything).Return(99, nil)
269	k.EXPECT().destroyBlob(mock.Anything).Return(nil).Maybe()
270	k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
271	load := func(ports.CursorChange, float64, int) (ports.CursorImage, error) {
272		return ports.CursorImage{Pixels: []byte{1, 2, 3, 4}, W: 1, H: 1}, nil
273	}
274	last := func(kind uint64) (commitRec, bool) {
275		commitMu.Lock()
276		defer commitMu.Unlock()
277		for i := len(*commits) - 1; i >= 0; i-- {
278			if c := (*commits)[i]; c.user&3 == kind {
279				return c, true
280			}
281		}
282		return commitRec{}, false
283	}
284	scenes := make(chan ports.Scene, 1)
285	contents := make(chan ports.SurfaceContent, 1)
286	presented := make(chan ports.OutputPresented, 8)
287	ctx, cancel := context.WithCancel(context.Background())
288	done := make(chan error, 1)
289	go func() {
290		done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, load, make(chan bool), scenes, contents, nil, presented)
291	}()
292	scenes <- ports.Scene{OutputWidth: 200, OutputHeight: 100, Scale: 1}
293	var frame commitRec
294	waitFor(t, func() bool { var ok bool; frame, ok = last(userFrame); return ok })
295	flips <- eventOf(frame)
296	<-presented // the frame's flip
297	o.cursor.Move(3, 3)
298	var cur commitRec
299	waitFor(t, func() bool { var ok bool; cur, ok = last(userState); return ok })
300	// A window this output does not show commits while the cursor flies.
301	contents <- ports.SurfaceContent{ID: 7, Seq: 2, SHM: &ports.SHMBuffer{}}
302	time.Sleep(20 * time.Millisecond)
303	flips <- eventOf(cur)
304	select {
305	case p := <-presented:
306		if p.Seen[7] != 2 {
307			t.Fatalf("report %+v", p)
308		}
309	case <-time.After(time.Second):
310		t.Fatal("Seen not reported after the cursor commit")
311	}
312	cancel()
313	if err := <-done; err != nil {
314		t.Fatal(err)
315	}
316}
317
318// Frame fences are closed on every path: flipped, EBUSY (re-rendered
319// after the event) and lost master (re-rendered after the modeset). The
320// fd count is unchanged after many frames.
321func TestRunClosesFrameFences(t *testing.T) {
322	fds := func() int {
323		ents, err := os.ReadDir("/proc/self/fd")
324		if err != nil {
325			t.Skip(err)
326		}
327		return len(ents)
328	}
329	const rounds = 30
330	// Commit results: modeset test and modeset, then per round
331	// EBUSY + flip, or lost master + modeset + flip, or a plain flip.
332	errs := []error{nil, nil}
333	for i := range rounds {
334		switch i % 3 {
335		case 0:
336			errs = append(errs, unix.EBUSY, nil)
337		case 1:
338			errs = append(errs, unix.EACCES, nil, nil)
339		default:
340			errs = append(errs, nil)
341		}
342	}
343	o, k, commits, commitMu := testOutputMu(t, errs...)
344	o.cursor, o.tearing, o.fbs = nil, false, [2]uint32{}
345	flips := make(chan flipEvent, 1)
346	o.flipped = flips
347	r := portsmocks.NewMockRenderer(t)
348	buf := func() ports.DMABuf {
349		f, w, _ := os.Pipe()
350		w.Close()
351		return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
352	}
353	r.EXPECT().ExportTargets(2, mock.Anything).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
354	r.EXPECT().UseTarget(mock.Anything).Return()
355	r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(ports.Scene, map[ports.WindowID]ports.SurfaceContent) (*os.File, error) {
356		f, w, _ := os.Pipe()
357		_, _ = w.Write([]byte{1})
358		w.Close()
359		return f, nil
360	})
361	r.EXPECT().Close().Return().Once()
362	k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice()
363	k.EXPECT().createBlob(mock.Anything).Return(99, nil)
364	k.EXPECT().destroyBlob(mock.Anything).Return(nil)
365	k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
366	before := fds()
367	active := make(chan bool, 1)
368	scenes := make(chan ports.Scene, 1)
369	presented := make(chan ports.OutputPresented, 4*rounds)
370	ctx, cancel := context.WithCancel(context.Background())
371	done := make(chan error, 1)
372	go func() {
373		done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, active, scenes, nil, nil, presented)
374	}()
375	count := func() int {
376		commitMu.Lock()
377		defer commitMu.Unlock()
378		return len(*commits)
379	}
380	last := func() commitRec {
381		commitMu.Lock()
382		defer commitMu.Unlock()
383		return (*commits)[len(*commits)-1]
384	}
385	waitFor(t, func() bool { return count() == 2 })
386	for i := range rounds {
387		n := count()
388		scenes <- ports.Scene{Seq: uint64(i)}
389		switch i % 3 {
390		case 0:
391			waitFor(t, func() bool { return count() == n+1 })
392			flips <- flipEvent{crtc: tCrtc} // ends the EBUSY wait
393			waitFor(t, func() bool { return count() == n+2 })
394		case 1:
395			waitFor(t, func() bool { return count() == n+1 })
396			active <- true
397			waitFor(t, func() bool { return count() == n+3 })
398		default:
399			waitFor(t, func() bool { return count() == n+1 })
400		}
401		flips <- eventOf(last())
402	}
403	cancel()
404	if err := <-done; err != nil {
405		t.Fatal(err)
406	}
407	if after := fds(); after > before {
408		t.Fatalf("fds %d -> %d", before, after)
409	}
410}

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