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.frame.endPending() // 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.frame.pendingCommit() {
28		t.Fatal("stale event completed the new commit")
29	}
30	if !o.completed(eventOf((*commits)[1]), nil) || o.frame.pendingCommit() {
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.frame.pendingCommit() {
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.frame.pendingCommit() {
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().SetHDR(float64(0)).Return().Maybe()
126	r.EXPECT().ExportTargets(2, []uint64(nil)).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
127	r.EXPECT().SetHDR(float64(0)).Return().Maybe()
128	r.EXPECT().ExportTargets(2, []uint64{0}).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
129	r.EXPECT().UseTarget(mock.Anything).Return()
130	r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)
131	k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil)
132	k.EXPECT().rmFB(mock.Anything).Return(nil)
133	k.EXPECT().createBlob(mock.Anything).Return(99, nil)
134	k.EXPECT().destroyBlob(mock.Anything).Return(nil)
135	if err := o.showImages(r, imagesDriver, nil); err != nil {
136		t.Fatal(err)
137	}
138	want := []uint32{atomicTestOnly | atomicAllowModes, atomicTestOnly | atomicAllowModes, atomicAllowModes}
139	if len(*commits) != len(want) {
140		t.Fatalf("%d commits", len(*commits))
141	}
142	for i, f := range want {
143		if (*commits)[i].flags != f {
144			t.Fatalf("commit %d flags %#x want %#x", i, (*commits)[i].flags, f)
145		}
146	}
147	if o.kind != imagesLinear {
148		t.Fatalf("kind %d", o.kind)
149	}
150}
151
152// Enabling an output whose images were not made yet (started switched
153// away) tests them and modesets once.
154func TestEnableAfterStartAwayModesetsOnce(t *testing.T) {
155	o, k, commits, commitMu := testOutputMu(t)
156	o.cursor, o.tearing, o.fbs = nil, false, [2]uint32{}
157	r := portsmocks.NewMockRenderer(t)
158	buf := func() ports.DMABuf {
159		f, w, _ := os.Pipe()
160		w.Close()
161		return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
162	}
163	r.EXPECT().SetHDR(float64(0)).Return().Maybe()
164	r.EXPECT().ExportTargets(2, mock.Anything).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
165	r.EXPECT().UseTarget(mock.Anything).Return()
166	r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)
167	r.EXPECT().Close().Return().Once()
168	k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice()
169	k.EXPECT().createBlob(mock.Anything).Return(99, nil)
170	k.EXPECT().destroyBlob(mock.Anything).Return(nil)
171	k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
172	active := make(chan bool, 1)
173	active <- false
174	ctx, cancel := context.WithCancel(context.Background())
175	done := make(chan error, 1)
176	go func() {
177		done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, active, nil, nil, nil, make(chan ports.OutputPresented, 1), nil, nil)
178	}()
179	count := func(flags uint32) int {
180		commitMu.Lock()
181		defer commitMu.Unlock()
182		n := 0
183		for _, c := range *commits {
184			if c.flags == flags {
185				n++
186			}
187		}
188		return n
189	}
190	time.Sleep(20 * time.Millisecond)
191	if n := count(atomicAllowModes); n != 0 {
192		t.Fatalf("%d modesets while away", n)
193	}
194	active <- true
195	waitFor(t, func() bool { return count(atomicAllowModes) == 1 })
196	cancel()
197	if err := <-done; err != nil {
198		t.Fatal(err)
199	}
200	if n := count(atomicTestOnly | atomicAllowModes); n != 1 {
201		t.Fatalf("%d test modesets", n)
202	}
203}
204
205// Close without a mode to restore turns the CRTC off.
206func TestCloseWithoutSavedModeDisablesCrtc(t *testing.T) {
207	o, k, commits := testOutput(t)
208	k.EXPECT().rmFB(mock.Anything).Return(nil)
209	o.Close()
210	c := (*commits)[0]
211	for _, p := range []struct{ obj, prop uint32 }{{tCrtc, pActive}, {tCrtc, pMode}, {tPrimary, pFB}, {tConn, pConnCrtc}} {
212		if v, ok := c.req.value(p.obj, p.prop); !ok || v != 0 {
213			t.Fatalf("object %d prop %d = %d %v", p.obj, p.prop, v, ok)
214		}
215	}
216}
217
218// Close restores the saved mode and gives the connector its CRTC back.
219func TestCloseRestoresSavedMode(t *testing.T) {
220	o, k, commits := testOutput(t)
221	o.saved = modeCrtc{crtcID: tCrtc, fbID: 5, modeValid: 1, mode: modeInfo{HDisplay: 200, VDisplay: 100}}
222	k.EXPECT().createBlob(mock.Anything).Return(98, nil).Once()
223	k.EXPECT().destroyBlob(uint32(98)).Return(nil).Once()
224	k.EXPECT().rmFB(mock.Anything).Return(nil)
225	o.Close()
226	c := (*commits)[0]
227	if v, _ := c.req.value(tConn, pConnCrtc); v != tCrtc {
228		t.Fatalf("connector crtc %d", v)
229	}
230	if v, _ := c.req.value(tPrimary, pFB); v != 5 {
231		t.Fatalf("primary fb %d", v)
232	}
233}
234
235// With the cursor off no image is loaded (no warning per change).
236func TestCursorOffSkipsLoads(t *testing.T) {
237	o, _, _ := testOutput(t)
238	o.cursor.off = true
239	loaded := false
240	o.setCursor(nil, func(ports.CursorChange, float64, ports.BufferTransform, int) (ports.CursorImage, error) {
241		loaded = true
242		return ports.CursorImage{}, nil
243	}, ports.CursorChange{}, 1, 0)
244	if loaded || o.cursor.desired().on {
245		t.Fatal("cursor used while off")
246	}
247}
248
249// Content read while only a cursor commit is in flight is reported when
250// that commit's event arrives, not held until a later frame.
251func TestRunReportsSeenAfterCursorCommit(t *testing.T) {
252	o, k, commits, commitMu := testOutputMu(t)
253	o.tearing, o.cursor.fbs = false, [2]uint32{}
254	flips := make(chan flipEvent, 1)
255	o.flipped = flips
256	r := portsmocks.NewMockRenderer(t)
257	buf := func() ports.DMABuf {
258		f, w, _ := os.Pipe()
259		w.Close()
260		return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
261	}
262	r.EXPECT().CursorBuffers(64).Return([2]ports.DMABuf{buf(), buf()}, nil).Once()
263	r.EXPECT().WriteCursor(mock.Anything, mock.Anything, 1, 1).Return(nil)
264	r.EXPECT().SetHDR(float64(0)).Return().Maybe()
265	r.EXPECT().ExportTargets(2, mock.Anything).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
266	r.EXPECT().UseTarget(mock.Anything).Return()
267	r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)
268	r.EXPECT().Close().Return().Once()
269	k.EXPECT().addFB(mock.Anything, uint32(fourccARGB)).Return(90, nil).Once()
270	k.EXPECT().addFB(mock.Anything, uint32(fourccARGB)).Return(91, nil).Once()
271	k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice()
272	k.EXPECT().createBlob(mock.Anything).Return(99, nil)
273	k.EXPECT().destroyBlob(mock.Anything).Return(nil).Maybe()
274	k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
275	load := func(ports.CursorChange, float64, ports.BufferTransform, int) (ports.CursorImage, error) {
276		return ports.CursorImage{Pixels: []byte{1, 2, 3, 4}, W: 1, H: 1}, nil
277	}
278	last := func(kind uint64) (commitRec, bool) {
279		commitMu.Lock()
280		defer commitMu.Unlock()
281		for i := len(*commits) - 1; i >= 0; i-- {
282			if c := (*commits)[i]; c.user&3 == kind {
283				return c, true
284			}
285		}
286		return commitRec{}, false
287	}
288	scenes := make(chan ports.Scene, 1)
289	contents := make(chan ports.SurfaceContent, 1)
290	presented := make(chan ports.OutputPresented, 8)
291	ctx, cancel := context.WithCancel(context.Background())
292	done := make(chan error, 1)
293	go func() {
294		done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, load, make(chan bool), scenes, contents, nil, presented, nil, nil)
295	}()
296	scenes <- ports.Scene{OutputWidth: 200, OutputHeight: 100, Scale: 1}
297	var frame commitRec
298	waitFor(t, func() bool { var ok bool; frame, ok = last(userFrame); return ok })
299	flips <- eventOf(frame)
300	<-presented // the frame's flip
301	o.cursor.Move(3, 3)
302	var cur commitRec
303	waitFor(t, func() bool { var ok bool; cur, ok = last(userState); return ok })
304	// A window this output does not show commits while the cursor flies.
305	contents <- ports.SurfaceContent{ID: 7, Seq: 2, SHM: &ports.SHMBuffer{}}
306	time.Sleep(20 * time.Millisecond)
307	flips <- eventOf(cur)
308	select {
309	case p := <-presented:
310		if p.Seen[7] != 2 {
311			t.Fatalf("report %+v", p)
312		}
313	case <-time.After(time.Second):
314		t.Fatal("Seen not reported after the cursor commit")
315	}
316	cancel()
317	if err := <-done; err != nil {
318		t.Fatal(err)
319	}
320}
321
322// Frame fences are closed on every path: flipped, EBUSY (re-rendered
323// after the event) and lost master (re-rendered after the modeset). The
324// fd count is unchanged after many frames.
325func TestRunClosesFrameFences(t *testing.T) {
326	fds := func() int {
327		ents, err := os.ReadDir("/proc/self/fd")
328		if err != nil {
329			t.Skip(err)
330		}
331		return len(ents)
332	}
333	const rounds = 30
334	// Commit results: modeset test and modeset, then per round
335	// EBUSY + flip, or lost master + modeset + flip, or a plain flip.
336	errs := []error{nil, nil}
337	for i := range rounds {
338		switch i % 3 {
339		case 0:
340			errs = append(errs, unix.EBUSY, nil)
341		case 1:
342			errs = append(errs, unix.EACCES, nil, nil)
343		default:
344			errs = append(errs, nil)
345		}
346	}
347	o, k, commits, commitMu := testOutputMu(t, errs...)
348	o.cursor, o.tearing, o.fbs = nil, false, [2]uint32{}
349	flips := make(chan flipEvent, 1)
350	o.flipped = flips
351	r := portsmocks.NewMockRenderer(t)
352	buf := func() ports.DMABuf {
353		f, w, _ := os.Pipe()
354		w.Close()
355		return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
356	}
357	r.EXPECT().SetHDR(float64(0)).Return().Maybe()
358	r.EXPECT().ExportTargets(2, mock.Anything).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
359	r.EXPECT().UseTarget(mock.Anything).Return()
360	r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(ports.Scene, map[ports.WindowID]ports.SurfaceContent) (*os.File, error) {
361		f, w, _ := os.Pipe()
362		_, _ = w.Write([]byte{1})
363		w.Close()
364		return f, nil
365	})
366	r.EXPECT().Close().Return().Once()
367	k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice()
368	k.EXPECT().createBlob(mock.Anything).Return(99, nil)
369	k.EXPECT().destroyBlob(mock.Anything).Return(nil)
370	k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
371	before := fds()
372	active := make(chan bool, 1)
373	scenes := make(chan ports.Scene, 1)
374	presented := make(chan ports.OutputPresented, 4*rounds)
375	ctx, cancel := context.WithCancel(context.Background())
376	done := make(chan error, 1)
377	go func() {
378		done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, active, scenes, nil, nil, presented, nil, nil)
379	}()
380	count := func() int {
381		commitMu.Lock()
382		defer commitMu.Unlock()
383		return len(*commits)
384	}
385	last := func() commitRec {
386		commitMu.Lock()
387		defer commitMu.Unlock()
388		return (*commits)[len(*commits)-1]
389	}
390	waitFor(t, func() bool { return count() == 2 })
391	for i := range rounds {
392		n := count()
393		scenes <- ports.Scene{Seq: uint64(i)}
394		switch i % 3 {
395		case 0:
396			waitFor(t, func() bool { return count() == n+1 })
397			flips <- flipEvent{crtc: tCrtc} // ends the EBUSY wait
398			waitFor(t, func() bool { return count() == n+2 })
399		case 1:
400			waitFor(t, func() bool { return count() == n+1 })
401			active <- true
402			waitFor(t, func() bool { return count() == n+3 })
403		default:
404			waitFor(t, func() bool { return count() == n+1 })
405		}
406		flips <- eventOf(last())
407	}
408	cancel()
409	if err := <-done; err != nil {
410		t.Fatal(err)
411	}
412	if after := fds(); after > before {
413		t.Fatalf("fds %d -> %d", before, after)
414	}
415}

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