feat/data-dnd e05cb15

front_test.go RawHistoryFolder

1package drm
2
3import (
4	"context"
5	"errors"
6	"os"
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/stretchr/testify/mock"
14	"golang.org/x/sys/unix"
15)
16
17// overlayFrames submits the overlay scene once (composed), then with the
18// overlay window's next buffers: the frames after the first only flip the
19// overlay buffer.
20func overlayFrames(t *testing.T, o *Output, r *portsmocks.MockRenderer, s ports.Scene, c map[ports.WindowID]ports.SurfaceContent, frames int) {
21	t.Helper()
22	for i := range frames {
23		win := c[2]
24		buf := *win.DMABuf
25		buf.ID = uint64(9 + i)
26		win.DMABuf, win.Seq = &buf, uint64(1+i)
27		c[2] = win
28		if _, err := o.submitFrame(context.Background(), r, s, c, map[ports.WindowID]uint64{}, nil, nil); err != nil {
29			t.Fatalf("frame %d: %v", i, err)
30		}
31		o.frame.endPending()
32	}
33}
34
35// When only the overlay window's buffer changes, the image on screen is
36// committed again with the new buffer: no render, no probe once KMS took
37// the same frame.
38func TestSubmitFrameOverlayOnlySkipsRender(t *testing.T) {
39	o, k, commits := overlayOutput(t)
40	o.cursor, o.vrrProp = nil, 0
41	k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).RunAndReturn(func(b *ports.DMABuf, _ uint32) (uint32, error) {
42		return uint32(80 + b.ID), nil
43	}).Times(4)
44	o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
45	s, c := overlayScene()
46	r := portsmocks.NewMockRenderer(t)
47	r.EXPECT().UseTarget(0).Return().Once()
48	r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil).Once()
49	overlayFrames(t, o, r, s, c, 4)
50	var probes, frames int
51	for _, cm := range *commits {
52		if cm.flags&atomicTestOnly != 0 {
53			probes++
54			continue
55		}
56		frames++
57		if v, _ := cm.req.value(tPrimary, pFB); v != 70 {
58			t.Fatalf("frame %d primary fb %d, want the composed image 70", frames, v)
59		}
60		if _, ok := cm.req.value(tPrimary, pFence); ok {
61			t.Fatalf("frame %d waits on a render fence", frames)
62		}
63	}
64	if frames != 4 {
65		t.Fatalf("%d frame commits", frames)
66	}
67	// Each new overlay buffer is probed once; nothing else is.
68	if probes != 4 {
69		t.Fatalf("%d probes", probes)
70	}
71	if last := (*commits)[len(*commits)-1]; o.queued != 12 {
72		t.Fatalf("queued %d", o.queued)
73	} else if v, _ := last.req.value(tOverlay, pFB); v != 92 {
74		t.Fatalf("overlay fb %d", v)
75	}
76	if o.back != 1 {
77		t.Fatalf("back %d: the skipped frames drew nothing", o.back)
78	}
79}
80
81// An uncommitted render that may still read client buffers forces
82// composition: the overlay-only flip carries no fence to order it after
83// those reads, and its report would let wayland release them.
84func TestSubmitFrameOverlayOnlyWaitsForHeldReads(t *testing.T) {
85	o, k, _ := overlayOutput(t)
86	o.cursor, o.vrrProp = nil, 0
87	k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Maybe()
88	o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
89	s, c := overlayScene()
90	r := portsmocks.NewMockRenderer(t)
91	r.EXPECT().UseTarget(mock.Anything).Return().Twice()
92	r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil).Twice()
93	overlayFrames(t, o, r, s, c, 1)
94	// An unsignalled fence of a render whose commit failed.
95	fence, w, err := os.Pipe()
96	if err != nil {
97		t.Fatal(err)
98	}
99	defer fence.Close()
100	defer w.Close()
101	o.holdRead(fence)
102	defer o.dropRead()
103	overlayFrames(t, o, r, s, c, 1)
104}
105
106// The desktop case through Run: a video (a dmabuf window on the overlay)
107// next to a static window. Once the overlay is up, each new video buffer is
108// one frame commit of the same composed image with no render, and each flip
109// reports the video shown zero-copy at its latest content.
110func TestRunVideoOnOverlayDoesNotRender(t *testing.T) {
111	o, k, commits, commitMu := testOutputMu(t)
112	o.cursor, o.tearing, o.vrrProp = nil, false, 0
113	props := map[string]uint32{"zpos": pZpos}
114	for n, v := range planeProps {
115		props[n] = v
116	}
117	o.primary.props = props
118	o.stray = []*plane{{id: tOverlay, typ: planeOverlay, props: props, zposValue: 2, formats: []ports.DMABufFormat{{Format: fourccXRGB}}}}
119	o.primary.formats = nil
120	o.pickOverlay()
121	flips := make(chan flipEvent, 1)
122	o.flipped = flips
123	r := portsmocks.NewMockRenderer(t)
124	pipeBuf := func() ports.DMABuf {
125		f, w, err := os.Pipe()
126		if err != nil {
127			t.Fatal(err)
128		}
129		w.Close()
130		return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
131	}
132	r.EXPECT().SetHDR(float64(0)).Return().Maybe()
133	r.EXPECT().ExportTargets(2, mock.Anything, false).Return([]ports.DMABuf{pipeBuf(), pipeBuf()}, nil).Once()
134	r.EXPECT().UseTarget(mock.Anything).Return()
135	var renders atomic.Int32
136	r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(ports.Scene, map[ports.WindowID]ports.SurfaceContent) (*os.File, error) {
137		renders.Add(1)
138		return nil, nil
139	})
140	r.EXPECT().Close().Return().Once()
141	var images atomic.Uint32
142	k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).RunAndReturn(func(b *ports.DMABuf, _ uint32) (uint32, error) {
143		if b.ID == 0 {
144			return 70 + images.Add(1) - 1, nil // the output images
145		}
146		return uint32(100 + b.ID), nil
147	})
148	k.EXPECT().createBlob(mock.Anything).Return(99, nil)
149	k.EXPECT().destroyBlob(mock.Anything).Return(nil).Maybe()
150	k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
151	k.EXPECT().objProps(uint32(tOverlay), uint32(objPlane)).Return(map[string][2]uint64{}, nil).Maybe()
152	frameCommits := func() []commitRec {
153		commitMu.Lock()
154		defer commitMu.Unlock()
155		var out []commitRec
156		for _, c := range *commits {
157			if c.user&3 == userFrame {
158				out = append(out, c)
159			}
160		}
161		return out
162	}
163	scenes := make(chan ports.Scene, 1)
164	contents := newContentFeed(t, 1)
165	sink, presented := presentedSink(t, 64)
166	ctx, cancel := context.WithCancel(context.Background())
167	done := make(chan error, 1)
168	go func() {
169		done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), scenes, contents.in(), contents.ret, nil, sink, nil, nil)
170	}()
171	// Stop Run on every path, before the mocks check their calls.
172	t.Cleanup(func() {
173		cancel()
174		select {
175		case err := <-done:
176			if err != nil {
177				t.Error(err)
178			}
179		case <-time.After(2 * time.Second):
180			t.Error("Run did not stop")
181		}
182	})
183	static := ports.SurfaceContent{ID: 1, Seq: 1, Width: 100, Height: 100, Opaque: true, Solid: &ports.SolidColor{A: 1}}
184	video := func(n int) ports.SurfaceContent {
185		return ports.SurfaceContent{ID: 2, Seq: uint64(n), Width: 100, Height: 100, Opaque: true,
186			DMABuf: &ports.DMABuf{ID: uint64(n%3 + 1), Width: 100, Height: 100, Format: fourccXRGB}}
187	}
188	contents.send(static, video(1))
189	scenes <- ports.Scene{Seq: 1, Scale: 1, OutputWidth: 200, OutputHeight: 100, Windows: []ports.SceneWindow{
190		{ID: 1, Rect: ports.Rect{W: 100, H: 100}},
191		{ID: 2, Rect: ports.Rect{X: 100, W: 100, H: 100}},
192	}}
193	// play delivers video frame n and flips every frame commit until a flip
194	// shows it; it returns that commit and its report. (Seen is what the
195	// output took in, which may run ahead of the flip.)
196	flipped := 0
197	vseq := uint32(0)
198	play := func(n int) (commitRec, ports.OutputPresented) {
199		t.Helper()
200		contents.send(video(n))
201		var c commitRec
202		var got ports.OutputPresented
203		waitFor(t, func() bool {
204			if cs := frameCommits(); len(cs) > flipped {
205				c = cs[len(cs)-1]
206				flipped = len(cs)
207				vseq++
208				flips <- flipEvent{crtc: tCrtc, user: c.user, when: time.Duration(vseq) * time.Millisecond, seq: vseq}
209			}
210			for {
211				select {
212				case p := <-presented:
213					if p.Flip != nil && p.Flip.Shows[2] == uint64(n) {
214						got = p
215						return true
216					}
217				default:
218					return false
219				}
220			}
221		})
222		return c, got
223	}
224	// Warm up until the video is on the overlay over a composed image.
225	n := 1
226	for ; n < 10; n++ {
227		c, _ := play(n)
228		if v, _ := c.req.value(tOverlay, pFB); v != 0 {
229			break
230		}
231	}
232	base, commitsBefore := renders.Load(), len(frameCommits())
233	const frames = 60
234	for i := range frames {
235		n++
236		c, p := play(n)
237		if v, _ := c.req.value(tOverlay, pFB); v != uint64(100+n%3+1) {
238			t.Fatalf("video frame %d: overlay fb %d", i, v)
239		}
240		if _, ok := c.req.value(tPrimary, pFence); ok {
241			t.Fatalf("video frame %d waits on a render", i)
242		}
243		// The flip reports the video shown zero-copy at its content.
244		if p.Flip.ZeroCopy != 2 || p.Flip.Shows[2] != uint64(n) || p.Seen[1] != 1 {
245			t.Fatalf("video frame %d: report %+v flip %+v", i, p, *p.Flip)
246		}
247	}
248	if got := len(frameCommits()) - commitsBefore; got != frames {
249		t.Fatalf("%d frame commits for %d video frames", got, frames)
250	}
251	if got := renders.Load() - base; got != 0 {
252		t.Fatalf("%d renders for %d video frames, want 0", got, frames)
253	}
254	// Three buffers in rotation: each probed once, then remembered.
255	commitMu.Lock()
256	probes := 0
257	for _, c := range *commits {
258		if c.flags&atomicTestOnly != 0 {
259			probes++
260		}
261	}
262	commitMu.Unlock()
263	if probes > 4 {
264		t.Fatalf("%d overlay probes for %d video frames of 3 buffers", probes, frames)
265	}
266}
267
268// Refusals on the overlay-only path: a refused probe composes the whole
269// scene at once; a refused commit drops the overlay and asks for a new frame,
270// leaving the images and the queue as they were.
271func TestSubmitFrameOverlayOnlyRefused(t *testing.T) {
272	for _, tc := range []struct {
273		name    string
274		refuse  func(r *atomicReq, flags uint32) bool
275		wantErr error
276		renders int
277	}{
278		{"probe", func(_ *atomicReq, flags uint32) bool { return flags&atomicTestOnly != 0 }, nil, 1},
279		{"commit", func(_ *atomicReq, flags uint32) bool { return flags&atomicTestOnly == 0 }, errOverlayDropped, 0},
280	} {
281		t.Run(tc.name, func(t *testing.T) {
282			refusing := false
283			o, k, _, _ := testOutputRules(t, func(r *atomicReq, flags uint32) error {
284				if refusing && tc.refuse(r, flags) {
285					return unix.EINVAL
286				}
287				return nil
288			})
289			props := map[string]uint32{"zpos": pZpos}
290			for n, v := range planeProps {
291				props[n] = v
292			}
293			o.primary.props, o.primary.formats = props, nil
294			o.stray = []*plane{{id: tOverlay, typ: planeOverlay, props: props, zposValue: 2, formats: []ports.DMABufFormat{{Format: fourccXRGB}}}}
295			o.pickOverlay()
296			o.cursor, o.vrrProp = nil, 0
297			k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).RunAndReturn(func(b *ports.DMABuf, _ uint32) (uint32, error) {
298				return uint32(80 + b.ID), nil
299			})
300			s, c := overlayScene()
301			r := portsmocks.NewMockRenderer(t)
302			r.EXPECT().UseTarget(mock.Anything).Return()
303			var drawn []int
304			r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(s ports.Scene, _ map[ports.WindowID]ports.SurfaceContent) (*os.File, error) {
305				drawn = append(drawn, len(s.Windows))
306				return nil, nil
307			})
308			overlayFrames(t, o, r, s, c, 1) // composed, front image held
309			back, queued := o.back, o.queued
310			refusing = true
311			win := c[2]
312			buf := *win.DMABuf
313			buf.ID, win.DMABuf, win.Seq = 20, &buf, 2
314			c[2] = win
315			_, err := o.submitFrame(context.Background(), r, s, c, map[ports.WindowID]uint64{}, nil, nil)
316			if !errors.Is(err, tc.wantErr) && err != tc.wantErr {
317				t.Fatalf("err %v, want %v", err, tc.wantErr)
318			}
319			if got := len(drawn) - 1; got != tc.renders {
320				t.Fatalf("%d renders, want %d", got, tc.renders)
321			}
322			if tc.renders == 1 && drawn[1] != 2 {
323				t.Fatalf("composed %d windows, want the whole scene", drawn[1])
324			}
325			if tc.wantErr != nil {
326				if o.back != back || o.queued != queued || o.clientFBs[20].overlayFailed != "cursor_conflict" {
327					t.Fatalf("back %d queued %d reason %q", o.back, o.queued, o.clientFBs[20].overlayFailed)
328				}
329				// The retry composes the window.
330				refusing = false
331				if _, err := o.submitFrame(context.Background(), r, s, c, map[ports.WindowID]uint64{}, nil, nil); err != nil {
332					t.Fatal(err)
333				}
334				if drawn[len(drawn)-1] != 2 {
335					t.Fatalf("retry composed %d windows", drawn[len(drawn)-1])
336				}
337			}
338		})
339	}
340}
341
342// A commit result with the overlay on: refusals the overlay causes ask for a
343// frame without it and leave the queue; a commit queues the buffer.
344func TestFinishOverlayCommit(t *testing.T) {
345	boom := errors.New("boom")
346	for _, tc := range []struct {
347		name       string
348		err        error
349		want       error
350		wantQueued uint64
351		reason     string
352	}{
353		{"committed", nil, nil, 9, ""},
354		{"colour refused", errColorRefused, errOverlayDropped, 1, "color_refused"},
355		{"overlay conflict", unix.EINVAL, errOverlayDropped, 1, "cursor_conflict"},
356		{"other", boom, boom, 1, ""},
357	} {
358		t.Run(tc.name, func(t *testing.T) {
359			o, _, _ := testOutput(t)
360			o.queued = 1
361			o.clientFBs[9] = &clientFB{}
362			if err := o.finishOverlayCommit(tc.err, 9); err != tc.want {
363				t.Fatalf("err %v, want %v", err, tc.want)
364			}
365			if o.queued != tc.wantQueued || o.overlayReason != tc.reason {
366				t.Fatalf("queued %d reason %q", o.queued, o.overlayReason)
367			}
368		})
369	}
370}
371
372// A frame whose buffer KMS took before is not probed again.
373func TestOverlayProbeCached(t *testing.T) {
374	o, k, commits := overlayOutput(t)
375	o.cursor.image = true
376	o.cursor.Move(10, 10)
377	k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once()
378	o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
379	s, c := overlayScene()
380	ov, _ := o.overlayFrame(s, c)
381	for range 3 {
382		if !o.testOverlay(70, ov) {
383			t.Fatal("refused")
384		}
385	}
386	if len(*commits) != 1 {
387		t.Fatalf("%d probes, want 1", len(*commits))
388	}
389	// Another place, primary image or cursor is probed.
390	moved := ov
391	moved.rect.X++
392	o.testOverlay(70, moved)
393	o.testOverlay(71, ov)
394	o.cursor.Move(20, 10)
395	o.testOverlay(70, ov)
396	if len(*commits) != 4 {
397		t.Fatalf("%d probes, want 4", len(*commits))
398	}
399	// The probe tested the cursor where the cache remembers it.
400	if v, _ := (*commits)[3].req.value(tCursor, 14); v != 20-uint64(o.cursor.hotX) {
401		t.Fatalf("probe cursor x %d", v)
402	}
403	// New colour state (HDR brightness) is probed again.
404	o.colorMult++
405	o.testOverlay(70, ov)
406	if len(*commits) != 5 {
407		t.Fatalf("%d probes after a colour change, want 5", len(*commits))
408	}
409	// A removed client framebuffer may give its id to another buffer.
410	k.EXPECT().rmFB(uint32(88)).Return(nil).Once()
411	o.dropClientFBs(time.Now(), true)
412	o.testOverlay(70, ov)
413	if len(*commits) != 6 {
414		t.Fatal("probe cached across a framebuffer removal")
415	}
416	// A modeset (VT resume) forgets them.
417	k.EXPECT().createBlob(mock.Anything).Return(99, nil).Once()
418	k.EXPECT().objProps(uint32(tOverlay), uint32(objPlane)).Return(map[string][2]uint64{}, nil).Maybe()
419	if err := o.modeset(); err != nil {
420		t.Fatal(err)
421	}
422	before := len(*commits)
423	o.testOverlay(70, ov)
424	if len(*commits) != before+1 {
425		t.Fatal("probe cached across a modeset")
426	}
427}
428
429// Anything but the overlay window changing composes: another window's
430// content, the scene, or an image drawn by another path.
431func TestSubmitFrameOverlayOnlyNeedsSameImage(t *testing.T) {
432	for name, edit := range map[string]func(o *Output, s *ports.Scene, c map[ports.WindowID]ports.SurfaceContent){
433		"other window content": func(_ *Output, _ *ports.Scene, c map[ports.WindowID]ports.SurfaceContent) {
434			w := c[1]
435			w.Seq++
436			c[1] = w
437		},
438		"scene": func(_ *Output, s *ports.Scene, _ map[ports.WindowID]ports.SurfaceContent) {
439			s.Background = "#ffffff"
440		},
441		"image forgotten": func(o *Output, _ *ports.Scene, _ map[ports.WindowID]ports.SurfaceContent) {
442			o.front.forget()
443		},
444		"layer content": func(_ *Output, _ *ports.Scene, c map[ports.WindowID]ports.SurfaceContent) {
445			l := c[5]
446			l.Seq++
447			c[5] = l
448		},
449		// The scene's slices rewritten in place by their owner: what the
450		// image holds is a copy.
451		"scene slice rewritten": func(_ *Output, s *ports.Scene, _ map[ports.WindowID]ports.SurfaceContent) {
452			s.Windows[0].Rect.W = 90
453		},
454	} {
455		t.Run(name, func(t *testing.T) {
456			o, k, _ := overlayOutput(t)
457			o.cursor, o.vrrProp = nil, 0
458			k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Maybe()
459			o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
460			s, c := overlayScene()
461			s.Layers = []ports.SceneLayer{{ID: 5, Layer: ports.LayerBottom, Rect: ports.Rect{W: 10, H: 10}}}
462			c[1] = ports.SurfaceContent{ID: 1, Seq: 1, Width: 100, Height: 100, Opaque: true, Solid: &ports.SolidColor{A: 1}}
463			c[5] = ports.SurfaceContent{ID: 5, Seq: 1, Width: 10, Height: 10, Opaque: true, Solid: &ports.SolidColor{A: 1}}
464			r := portsmocks.NewMockRenderer(t)
465			r.EXPECT().UseTarget(mock.Anything).Return().Twice()
466			var drawn []int
467			r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(s ports.Scene, _ map[ports.WindowID]ports.SurfaceContent) (*os.File, error) {
468				drawn = append(drawn, len(s.Windows))
469				return nil, nil
470			}).Twice()
471			overlayFrames(t, o, r, s, c, 1)
472			edit(o, &s, c)
473			overlayFrames(t, o, r, s, c, 1)
474			if len(drawn) != 2 || drawn[1] != 1 {
475				t.Fatalf("rendered %v", drawn)
476			}
477		})
478	}
479}

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