main f2d166b

capture_test.go RawHistoryFolder

1package drm
2
3import (
4	"context"
5	"errors"
6	"image"
7	"os"
8	"sync"
9	"sync/atomic"
10	"testing"
11	"testing/synctest"
12	"time"
13
14	"github.com/bnema/neferwl/internal/adapters/outputkit/capture"
15	portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
16	"github.com/bnema/neferwl/internal/ports"
17	"github.com/stretchr/testify/mock"
18	"github.com/stretchr/testify/require"
19)
20
21// sessionRequest is a capture request; exclude sets the exclusion of session 7.
22func sessionRequest(t *testing.T, id uint64, exclude bool) ports.CaptureRequest {
23	t.Helper()
24	f, err := os.CreateTemp(t.TempDir(), "capture")
25	require.NoError(t, err)
26	t.Cleanup(func() { _ = f.Close() })
27	require.NoError(t, f.Truncate(16))
28	q := ports.CaptureRequest{ID: id, Dst: ports.SHMBuffer{File: f}, Region: image.Rect(0, 0, 2, 2), Width: 2, Height: 2, Stride: 8, Format: 1}
29	if exclude {
30		q.Exclude, q.Session, q.CaptureRevision = true, 7, 1
31	}
32	return q
33}
34
35func drmSessionScene() ports.Scene {
36	return ports.Scene{Seq: 4, Windows: []ports.SceneWindow{{ID: 1}}, Layers: []ports.SceneLayer{{ID: 10, Layer: ports.LayerTop}},
37		Capture: &ports.SceneCapture{Session: 7, Revision: 1, Excluded: []ports.WindowID{10}}}
38}
39
40// The excluded frame is drawn and copied before the displayed frame, on the
41// same target; the standard capture is taken after the displayed frame.
42func TestSubmitFrameExcludeBeforeDisplayedThenStandard(t *testing.T) {
43	o, _, _ := testOutput(t)
44	o.cursor = nil
45	var order []string
46	r := portsmocks.NewMockRenderer(t)
47	r.EXPECT().UseTarget(0).Return().Once()
48	r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(s ports.Scene, _ map[ports.WindowID]ports.SurfaceContent) (*os.File, error) {
49		if s.Capture == nil {
50			require.Zero(t, s.Seq)
51			require.Len(t, s.Layers, 0, "excluded HUD layer left out of the excluded frame")
52			order = append(order, "excluded")
53		} else {
54			require.Len(t, s.Layers, 1, "displayed frame keeps the HUD")
55			order = append(order, "shown")
56		}
57		return nil, nil
58	}).Twice()
59	frame := portsmocks.NewMockCaptureFrame(t)
60	frame.EXPECT().Done().Return(nil).Maybe()
61	frame.EXPECT().Read(mock.Anything, mock.Anything, 8).Return(nil).Maybe()
62	r.EXPECT().BeginCapture().RunAndReturn(func() (ports.CaptureFrame, error) {
63		order = append(order, "copy")
64		return frame, nil
65	}).Twice()
66	r.EXPECT().EndCapture(frame).Return().Twice()
67	replies := make(chan ports.CaptureDone, 4)
68	pipeline := capture.NewPipeline(context.Background(), replies)
69	requests := []ports.CaptureRequest{sessionRequest(t, 1, true), sessionRequest(t, 2, false)}
70	direct, err := o.submitFrame(context.Background(), r, drmSessionScene(), nil, nil, requests, pipeline)
71	require.NoError(t, err)
72	require.False(t, direct)
73	require.Equal(t, []string{"excluded", "copy", "shown", "copy"}, order)
74	for _, q := range requests {
75		require.True(t, capture.Handed(q), "requests handed to the worker")
76	}
77	pipeline.Close(r)
78	require.Len(t, replies, 2)
79}
80
81// A fullscreen window would be scanned out: a capture request of the
82// displayed frame forces composition.
83func TestCaptureRequestPreventsDirectScanout(t *testing.T) {
84	o, _, _ := testOutput(t)
85	o.cursor = nil
86	r := portsmocks.NewMockRenderer(t)
87	r.EXPECT().UseTarget(0).Return().Once()
88	r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil).Once()
89	frame := portsmocks.NewMockCaptureFrame(t)
90	frame.EXPECT().Done().Return(nil).Maybe()
91	frame.EXPECT().Read(mock.Anything, mock.Anything, 8).Return(nil).Maybe()
92	r.EXPECT().BeginCapture().Return(frame, nil).Once()
93	r.EXPECT().EndCapture(frame).Return().Once()
94	replies := make(chan ports.CaptureDone, 2)
95	pipeline := capture.NewPipeline(context.Background(), replies)
96	direct, err := o.submitFrame(context.Background(), r, drmSessionScene(), nil, nil, []ports.CaptureRequest{sessionRequest(t, 2, false)}, pipeline)
97	require.NoError(t, err)
98	require.False(t, direct)
99	require.Equal(t, "capture", o.reason)
100	pipeline.Close(r)
101}
102
103// A full render failing after the Exclude group was handed off: the caller
104// still owns only the standard request; the Exclude one is zeroed, so it is
105// never answered twice.
106func TestSubmitFrameRenderErrorKeepsOnlyUnhandedRequests(t *testing.T) {
107	o, _, _ := testOutput(t)
108	o.cursor = nil
109	boom := errors.New("boom")
110	r := portsmocks.NewMockRenderer(t)
111	r.EXPECT().UseTarget(0).Return().Once()
112	r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil).Once()
113	r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, boom).Once()
114	frame := portsmocks.NewMockCaptureFrame(t)
115	frame.EXPECT().Done().Return(nil).Maybe()
116	frame.EXPECT().Read(mock.Anything, mock.Anything, 8).Return(nil).Maybe()
117	r.EXPECT().BeginCapture().Return(frame, nil).Once()
118	r.EXPECT().EndCapture(frame).Return().Once()
119	replies := make(chan ports.CaptureDone, 4)
120	pipeline := capture.NewPipeline(context.Background(), replies)
121	requests := []ports.CaptureRequest{sessionRequest(t, 1, true), sessionRequest(t, 2, false)}
122	_, err := o.submitFrame(context.Background(), r, drmSessionScene(), nil, nil, requests, pipeline)
123	var fatal renderError
124	require.ErrorAs(t, err, &fatal)
125	owned := 0
126	for _, q := range requests {
127		if !capture.Handed(q) {
128			owned++
129			require.Equal(t, uint64(2), q.ID)
130		}
131	}
132	require.Equal(t, 1, owned)
133	pipeline.Close(r)
134	require.Len(t, replies, 1) // the Exclude capture, answered once by the worker
135}
136
137// An Exclude request of a session the scene no longer carries fails closed
138// and is never captured from the displayed frame.
139func TestSubmitFrameExcludeRequestWithoutSessionFails(t *testing.T) {
140	o, _, _ := testOutput(t)
141	o.cursor = nil
142	r := portsmocks.NewMockRenderer(t)
143	r.EXPECT().UseTarget(0).Return().Once()
144	r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil).Once()
145	replies := make(chan ports.CaptureDone, 2)
146	pipeline := capture.NewPipeline(context.Background(), replies)
147	defer pipeline.Close(r)
148	requests := []ports.CaptureRequest{sessionRequest(t, 1, true)}
149	scene := drmSessionScene()
150	scene.Capture = nil
151	_, err := o.submitFrame(context.Background(), r, scene, nil, nil, requests, pipeline)
152	require.NoError(t, err)
153	got := <-replies
154	require.ErrorIs(t, got.Err, capture.ErrSessionInactive)
155	require.True(t, capture.Handed(requests[0]))
156}
157
158// Capture state alone (a session, an exclusion, a hidden workspace) must
159// not cost a fullscreen window its direct scanout: only requests that read
160// the displayed frame do.
161func TestCaptureStateKeepsDirectScanout(t *testing.T) {
162	for _, tc := range []struct {
163		name    string
164		capture *ports.SceneCapture
165	}{
166		{"no session", nil},
167		{"session", &ports.SceneCapture{Shown: 3}},
168		{"exclusion", &ports.SceneCapture{Session: 7, Revision: 1, Excluded: []ports.WindowID{10}}},
169	} {
170		t.Run(tc.name, func(t *testing.T) {
171			o, k, _ := testOutput(t)
172			o.cursor, o.tearing, o.vrrProp = nil, false, 0
173			o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
174			r := portsmocks.NewMockRenderer(t)
175			s, c := fullscreenScene()
176			s.Capture = tc.capture
177			k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once()
178			pipeline := capture.NewPipeline(context.Background(), nil)
179			defer pipeline.Close(r)
180			direct, err := o.submitFrame(context.Background(), r, s, c, map[ports.WindowID]uint64{}, nil, pipeline)
181			require.NoError(t, err)
182			require.True(t, direct)
183		})
184	}
185}
186
187// A request for a hidden workspace with no child renderer factory
188// fails closed; it is neither served from the displayed frame nor does it
189// stop the output.
190func TestSubmitFrameHiddenWorkspaceWithoutFactoryFailsClosed(t *testing.T) {
191	o, _, _ := testOutput(t)
192	o.cursor = nil
193	r := portsmocks.NewMockRenderer(t)
194	r.EXPECT().UseTarget(0).Return().Once()
195	r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil).Once() // the display only
196	replies := make(chan ports.CaptureDone, 2)
197	pipeline := capture.NewPipeline(context.Background(), replies)
198	defer pipeline.Close(r)
199	scene := drmSessionScene()
200	scene.CaptureScene = &ports.Scene{OutputWidth: 200, OutputHeight: 100, Scale: 1}
201	scene.Capture.Workspace = 9
202	requests := []ports.CaptureRequest{sessionRequest(t, 1, false)}
203	requests[0].Workspace, requests[0].OffScreen = 9, true
204	_, err := o.submitFrame(context.Background(), r, scene, nil, nil, requests, pipeline)
205	require.NoError(t, err)
206	require.ErrorIs(t, (<-replies).Err, capture.ErrOffscreenUnavailable)
207}
208
209// A hidden request whose scene carries no CaptureScene still goes to the
210// hidden path, which fails it closed: it is neither dropped unanswered nor
211// served from the displayed frame.
212func TestSubmitFrameHiddenWorkspaceWithoutCaptureSceneFails(t *testing.T) {
213	o, _, _ := testOutput(t)
214	o.cursor = nil
215	r := portsmocks.NewMockRenderer(t)
216	r.EXPECT().UseTarget(0).Return().Once()
217	r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil).Once() // the display only
218	replies := make(chan ports.CaptureDone, 2)
219	pipeline := capture.NewPipeline(context.Background(), replies)
220	defer pipeline.Close(r)
221	scene := drmSessionScene()
222	scene.Capture.Workspace = 9
223	requests := []ports.CaptureRequest{sessionRequest(t, 1, false)}
224	requests[0].Workspace, requests[0].OffScreen = 9, true
225	_, err := o.submitFrame(context.Background(), r, scene, nil, nil, requests, pipeline)
226	require.NoError(t, err)
227	select {
228	case d := <-replies:
229		require.ErrorIs(t, d.Err, capture.ErrOffscreenUnavailable)
230	case <-time.After(3 * time.Second):
231		t.Fatal("hidden request without CaptureScene was dropped unanswered")
232	}
233}
234
235// A report caps only the windows an unfinished child render drew; the display's
236// own windows are reported at their full content Seq, and the report goes on
237// (nothing waits for the child).
238func TestReportCapsPerWindowWhileChildReads(t *testing.T) {
239	o, _, _ := testOutput(t)
240	readEnd, writeEnd, err := os.Pipe() // the child's sync file: readable once it signals
241	require.NoError(t, err)
242	t.Cleanup(func() { _ = writeEnd.Close() })
243	child := portsmocks.NewMockRenderer(t)
244	frame := portsmocks.NewMockCaptureFrame(t)
245	frame.EXPECT().Done().Return(nil).Maybe()
246	frame.EXPECT().Read(mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe()
247	child.EXPECT().Render(mock.Anything, mock.Anything).Return(readEnd, nil).Once()
248	child.EXPECT().BeginCapture().Return(frame, nil).Once()
249	child.EXPECT().EndCapture(frame).Return().Maybe()
250	child.EXPECT().Close().Return().Once()
251	replies := make(chan ports.CaptureDone, 2)
252	pipeline := capture.NewPipeline(context.Background(), replies)
253	pipeline.EnableOffscreen(func(int, int) (ports.Renderer, error) { return child, nil })
254	o.capHidden = pipeline.CapHiddenSeen
255	scene := ports.Scene{Scale: 1, OutputWidth: 2, OutputHeight: 2,
256		Capture:      &ports.SceneCapture{Workspace: 9},
257		CaptureScene: &ports.Scene{Scale: 1, OutputWidth: 2, OutputHeight: 2, Windows: []ports.SceneWindow{{ID: 5, Rect: ports.Rect{W: 2, H: 2}}}}}
258	surfaces := map[ports.WindowID]ports.SurfaceContent{5: {ID: 5, Seq: 2}}
259	req := sessionRequest(t, 1, false)
260	req.Workspace, req.OffScreen = 9, true
261	req.Region, req.Width, req.Height = image.Rect(0, 0, 2, 2), 2, 2
262	pipeline.SubmitHidden(scene, surfaces, []ports.CaptureRequest{req})
263
264	seen := map[ports.WindowID]uint64{1: 9, 5: 7} // window 1 belongs to the display
265	o.report(nil, seen)
266	require.True(t, o.capped, "an outstanding child read is reported repeatedly")
267	require.Equal(t, map[ports.WindowID]uint64{1: 9, 5: 2}, o.reports.Unsent()[0].Seen)
268	require.Equal(t, uint64(7), seen[5], "the caller's map is untouched")
269	require.Equal(t, map[ports.WindowID]uint64{5: 2}, o.reports.Unsent()[0].ChildReads)
270	previous := o.reports.Unsent()[0].ChildReads
271	allocs := testing.AllocsPerRun(100, func() { o.report(nil, seen) })
272	require.LessOrEqual(t, allocs, float64(1), "only the fence poll may allocate")
273	t.Logf("unchanged child report: %.1f allocs", allocs)
274
275	_, err = writeEnd.Write([]byte{1}) // the child's GPU work finished
276	require.NoError(t, err)
277	o.report(nil, seen)
278	require.False(t, o.capped)
279	require.Equal(t, map[ports.WindowID]uint64{1: 9, 5: 7}, o.reports.Unsent()[0].Seen, "limit lifted")
280	require.Empty(t, o.reports.Unsent()[0].ChildReads)
281	require.Equal(t, map[ports.WindowID]uint64{5: 2}, previous, "sent snapshot remains immutable")
282	pipeline.Close(nil)
283}
284
285// GO004: a hidden-workspace child is drawn on its own device: the display
286// keeps direct scanout of a fullscreen window when no capture reads the
287// displayed frame and the session draws no border on screen.
288func TestHiddenWorkspaceChildKeepsDirectScanout(t *testing.T) {
289	o, k, _ := testOutput(t)
290	o.cursor, o.tearing, o.vrrProp = nil, false, 0
291	o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
292	k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once()
293	readEnd, writeEnd, err := os.Pipe()
294	require.NoError(t, err)
295	t.Cleanup(func() { _ = writeEnd.Close() })
296	child := portsmocks.NewMockRenderer(t)
297	frame := portsmocks.NewMockCaptureFrame(t)
298	frame.EXPECT().Done().Return(nil).Maybe()
299	frame.EXPECT().Read(mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe()
300	child.EXPECT().Render(mock.Anything, mock.Anything).Return(readEnd, nil).Once()
301	child.EXPECT().BeginCapture().Return(frame, nil).Once()
302	child.EXPECT().EndCapture(frame).Return().Maybe()
303	child.EXPECT().Close().Return().Once()
304	display := portsmocks.NewMockRenderer(t) // no Render: the display is scanned out
305	replies := make(chan ports.CaptureDone, 2)
306	pipeline := capture.NewPipeline(context.Background(), replies)
307	pipeline.EnableOffscreen(func(int, int) (ports.Renderer, error) { return child, nil })
308	o.capHidden = pipeline.CapHiddenSeen
309	s, c := fullscreenScene()
310	s.Capture = &ports.SceneCapture{Workspace: 9}
311	s.CaptureScene = &ports.Scene{Scale: 1, OutputWidth: 200, OutputHeight: 100, Windows: []ports.SceneWindow{{ID: 5, Rect: ports.Rect{W: 200, H: 100}}}}
312	req := sessionRequest(t, 1, false)
313	req.Workspace, req.OffScreen = 9, true
314	req.Region, req.Width, req.Height, req.Stride = image.Rect(0, 0, 200, 100), 200, 100, 800
315	require.NoError(t, req.Dst.File.Truncate(800*100))
316	direct, err := o.submitFrame(context.Background(), display, s, c, map[ports.WindowID]uint64{}, []ports.CaptureRequest{req}, pipeline)
317	require.NoError(t, err)
318	require.True(t, direct, "the displayed fullscreen window stays scanned out")
319	require.True(t, o.frame.pendingCommit(), "display flip has not completed")
320	require.Equal(t, map[ports.WindowID]uint64{5: 0}, o.reports.Unsent()[0].ChildReads, "child hold published before the display flip")
321	require.Empty(t, o.reports.Unsent()[0].Seen, "display reads are not advanced early")
322	require.NoError(t, (<-replies).Err)
323	_, err = writeEnd.Write([]byte{1})
324	require.NoError(t, err)
325	pipeline.Close(display)
326}
327
328// Child holds do not expire, so a finished child read must lift its
329// hold even while the display's own commit never completes. The display's
330// Seen stays at the last safe value and the output is not stopped.
331func TestRunChildHoldLiftedWhileDisplayCommitStalls(t *testing.T) {
332	o, k, commits, commitMu := testOutputMu(t, nil, nil)
333	o.cursor, o.tearing, o.fbs = nil, false, [2]uint32{}
334	o.frame.stuckAfter = 30 * time.Second // the display commit stays in flight
335	o.flipped = make(chan flipEvent)      // no flip event ever
336	display := portsmocks.NewMockRenderer(t)
337	buf := func() ports.DMABuf {
338		f, w, _ := os.Pipe()
339		w.Close()
340		return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
341	}
342	display.EXPECT().SetHDR(float64(0)).Return().Maybe()
343	display.EXPECT().ExportTargets(2, mock.Anything, false).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
344	display.EXPECT().UseTarget(mock.Anything).Return()
345	display.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)
346	display.EXPECT().Close().Return().Once()
347	k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice()
348	k.EXPECT().createBlob(mock.Anything).Return(99, nil)
349	k.EXPECT().destroyBlob(mock.Anything).Return(nil)
350	k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
351
352	readEnd, writeEnd, err := os.Pipe()
353	require.NoError(t, err)
354	t.Cleanup(func() { _ = writeEnd.Close() })
355	child := portsmocks.NewMockRenderer(t)
356	frame := portsmocks.NewMockCaptureFrame(t)
357	frame.EXPECT().Done().Return(nil).Maybe()
358	frame.EXPECT().Read(mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe()
359	child.EXPECT().Render(mock.Anything, mock.Anything).Return(readEnd, nil).Once()
360	child.EXPECT().BeginCapture().Return(frame, nil).Once()
361	child.EXPECT().EndCapture(frame).Return().Maybe()
362	child.EXPECT().Close().Return().Once()
363	o.NewCaptureRenderer = func(int, int) (ports.Renderer, error) { return child, nil }
364
365	scenes := make(chan ports.Scene)
366	contents := newContentFeed(t, 0)            // unbuffered: taken before the scene
367	captures := make(chan ports.CaptureRequest) // unbuffered: a send returns once Run took it
368	captured := make(chan ports.CaptureDone, 4)
369	sink, presented := presentedSink(t, 64)
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 display, nil }, nil, make(chan bool), scenes, contents.in(), contents.ret, nil, sink, captures, captured)
374	}()
375	count := func() int {
376		commitMu.Lock()
377		defer commitMu.Unlock()
378		return len(*commits)
379	}
380	waitFor(t, func() bool { return count() == 2 })
381
382	contents.send(ports.SurfaceContent{ID: 5, Seq: 2, Width: 1, Height: 1, SHM: &ports.SHMBuffer{}})
383	// The request waits for the scene; once a display frame is pending no
384	// request is served, so it must be queued before the scene arrives.
385	req := sessionRequest(t, 1, false)
386	req.Workspace, req.OffScreen = 9, true
387	req.Region, req.Width, req.Height, req.Stride = image.Rect(0, 0, 200, 100), 200, 100, 800
388	require.NoError(t, req.Dst.File.Truncate(800*100))
389	captures <- req
390	scenes <- ports.Scene{Seq: 1, Scale: 1, OutputWidth: 200, OutputHeight: 100,
391		Capture:      &ports.SceneCapture{Workspace: 9},
392		CaptureScene: &ports.Scene{Scale: 1, OutputWidth: 200, OutputHeight: 100, Windows: []ports.SceneWindow{{ID: 5, Rect: ports.Rect{W: 200, H: 100}}}}}
393
394	next := func(match func(ports.OutputPresented) bool) ports.OutputPresented {
395		t.Helper()
396		timeout := time.After(2 * time.Second)
397		for {
398			select {
399			case r := <-presented:
400				if match(r) {
401					return r
402				}
403			case <-timeout:
404				t.Fatal("no matching report")
405			}
406		}
407	}
408	held := next(func(r ports.OutputPresented) bool { return len(r.ChildReads) > 0 })
409	require.Equal(t, map[ports.WindowID]uint64{5: 2}, held.ChildReads)
410	require.Equal(t, 3, count(), "display frame committed, never completes")
411
412	// A newer buffer of a display window arrives while the display commit is
413	// pending: the GPU may still read the older one, so it is not reported.
414	contents.send(ports.SurfaceContent{ID: 1, Seq: 9, Width: 1, Height: 1, SHM: &ports.SHMBuffer{}})
415	_, err = writeEnd.Write([]byte{1}) // the child's GPU work finished
416	require.NoError(t, err)
417	lifted := next(func(r ports.OutputPresented) bool { return len(r.ChildReads) == 0 })
418	require.Empty(t, lifted.ChildReads)
419	require.NotContains(t, lifted.Seen, ports.WindowID(1), "display Seen does not advance while its commit is pending: only the child hold is lifted")
420	require.Nil(t, lifted.Flip)
421	require.Equal(t, 3, count(), "no extra commit")
422	require.NoError(t, (<-captured).Err)
423	cancel()
424	require.NoError(t, <-done)
425}
426
427// A capture requested before the scene that shows its indicator is held: the
428// frame on screen is not redrawn for it and nothing is copied. The first scene
429// that shows the border draws the frame without it, copies that, then draws
430// and commits the frame with the border.
431func TestRunCaptureWaitsForItsIndicatorScene(t *testing.T) {
432	synctest.Test(t, runCaptureWaitsForItsIndicatorScene)
433}
434
435func runCaptureWaitsForItsIndicatorScene(t *testing.T) {
436	o, k, commits, commitMu := testOutputMu(t)
437	o.tearing, o.cursor, o.fbs = false, nil, [2]uint32{}
438	flips := make(chan flipEvent, 4)
439	o.flipped = flips
440	buf := func() ports.DMABuf {
441		f, w, _ := os.Pipe()
442		w.Close()
443		return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
444	}
445	// Only what the renderer draws for scenes after the modeset's black frame.
446	var mu sync.Mutex
447	var order []string
448	note := func(what string) {
449		mu.Lock()
450		defer mu.Unlock()
451		order = append(order, what)
452	}
453	r := portsmocks.NewMockRenderer(t)
454	r.EXPECT().SetHDR(float64(0)).Return().Maybe()
455	r.EXPECT().ExportTargets(2, mock.Anything, false).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
456	r.EXPECT().UseTarget(mock.Anything).Return()
457	r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(s ports.Scene, _ map[ports.WindowID]ports.SurfaceContent) (*os.File, error) {
458		switch {
459		case s.OutputWidth == 0:
460			// The modeset's black frame.
461		case s.Seq == 0:
462			note("plain")
463		case len(s.CaptureIndicators) > 0:
464			note("indicated")
465		default:
466			note("bare")
467		}
468		return nil, nil
469	})
470	frame := portsmocks.NewMockCaptureFrame(t)
471	frame.EXPECT().Done().Return(nil).Maybe()
472	frame.EXPECT().Read(mock.Anything, mock.Anything, 8).Return(nil).Maybe()
473	r.EXPECT().BeginCapture().RunAndReturn(func() (ports.CaptureFrame, error) {
474		note("copy")
475		return frame, nil
476	}).Once()
477	r.EXPECT().EndCapture(frame).Return().Maybe()
478	r.EXPECT().Close().Return().Once()
479	k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice()
480	k.EXPECT().createBlob(mock.Anything).Return(99, nil)
481	k.EXPECT().destroyBlob(mock.Anything).Return(nil).Maybe()
482	k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
483
484	scenes := make(chan ports.Scene, 2)
485	captures := make(chan ports.CaptureRequest)
486	captured := make(chan ports.CaptureDone, 4)
487	ctx, cancel := context.WithCancel(context.Background())
488	done := make(chan error, 1)
489	go func() {
490		done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), scenes, nil, nil, nil, discardSink(t), captures, captured)
491	}()
492	count := func() int {
493		commitMu.Lock()
494		defer commitMu.Unlock()
495		return len(*commits)
496	}
497	last := func() commitRec {
498		commitMu.Lock()
499		defer commitMu.Unlock()
500		return (*commits)[len(*commits)-1]
501	}
502	waitFor(t, func() bool { return count() == 2 }) // modeset
503	bare := ports.Scene{Seq: 1, Scale: 1, OutputWidth: 2, OutputHeight: 2, Windows: []ports.SceneWindow{{ID: 1}}}
504	scenes <- bare
505	waitFor(t, func() bool { return count() == 3 })
506	flips <- eventOf(last())
507
508	req := sessionRequest(t, 1, false)
509	req.Indicate = true
510	captures <- req
511	synctest.Wait()
512	select {
513	case d := <-captured:
514		t.Fatalf("served from a scene without the indicator: %+v", d)
515	default:
516	}
517	require.Equal(t, 3, count(), "no frame is drawn for a held request")
518
519	indicated := bare
520	indicated.Seq = 2
521	indicated.CaptureIndicators = []ports.CaptureIndicator{{Rect: ports.Rect{W: 2, H: 2}}}
522	scenes <- indicated
523	waitFor(t, func() bool { return count() == 4 })
524	select {
525	case d := <-captured:
526		require.NoError(t, d.Err)
527	case <-time.After(2 * time.Second):
528		t.Fatal("not served once the indicator scene exists")
529	}
530	flips <- eventOf(last())
531	cancel()
532	require.NoError(t, <-done)
533	mu.Lock()
534	defer mu.Unlock()
535	require.Equal(t, []string{"bare", "plain", "copy", "indicated"}, order, "copied from the frame without the indicator, committed together with the indicated frame")
536}
537
538// A request whose indicator never comes fails after the bounded wait, and no
539// frame is drawn or copied for it.
540func TestRunCaptureWithoutIndicatorFailsAfterTheBoundedWait(t *testing.T) {
541	o, k, commits, commitMu := testOutputMu(t)
542	o.tearing, o.cursor, o.fbs = false, nil, [2]uint32{}
543	flips := make(chan flipEvent, 2)
544	o.flipped = flips
545	buf := func() ports.DMABuf {
546		f, w, _ := os.Pipe()
547		w.Close()
548		return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
549	}
550	r := portsmocks.NewMockRenderer(t)
551	r.EXPECT().SetHDR(float64(0)).Return().Maybe()
552	r.EXPECT().ExportTargets(2, mock.Anything, false).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
553	r.EXPECT().UseTarget(mock.Anything).Return()
554	r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)
555	r.EXPECT().Close().Return().Once()
556	k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice()
557	k.EXPECT().createBlob(mock.Anything).Return(99, nil)
558	k.EXPECT().destroyBlob(mock.Anything).Return(nil).Maybe()
559	k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
560	scenes := make(chan ports.Scene, 1)
561	captures := make(chan ports.CaptureRequest)
562	captured := make(chan ports.CaptureDone, 4)
563	ctx, cancel := context.WithCancel(context.Background())
564	done := make(chan error, 1)
565	go func() {
566		done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), scenes, nil, nil, nil, discardSink(t), captures, captured)
567	}()
568	count := func() int {
569		commitMu.Lock()
570		defer commitMu.Unlock()
571		return len(*commits)
572	}
573	waitFor(t, func() bool { return count() == 2 })
574	scenes <- ports.Scene{Seq: 1, Scale: 1, OutputWidth: 2, OutputHeight: 2}
575	waitFor(t, func() bool { return count() == 3 })
576	commitMu.Lock()
577	flips <- eventOf((*commits)[2])
578	commitMu.Unlock()
579	start := time.Now()
580	req := sessionRequest(t, 1, false)
581	req.Indicate = true
582	captures <- req
583	select {
584	case d := <-captured:
585		require.ErrorIs(t, d.Err, capture.ErrIndicatorMissing)
586		require.GreaterOrEqual(t, time.Since(start), capture.HoldFor-20*time.Millisecond)
587	case <-time.After(3 * time.Second):
588		t.Fatal("a held request was never answered")
589	}
590	require.Equal(t, 3, count())
591	cancel()
592	require.NoError(t, <-done)
593}
594
595func indicatedScene() ports.Scene {
596	return ports.Scene{Seq: 4, OutputWidth: 200, OutputHeight: 100, Scale: 1, Windows: []ports.SceneWindow{{ID: 1}},
597		CaptureIndicators: []ports.CaptureIndicator{{Rect: ports.Rect{W: 200, H: 100}}}}
598}
599
600// A capture is delivered only when the frame that carries its indicator is
601// committed: the plain frame is rendered and copied, but the pixels stay
602// behind the gate and the capture fails when that frame fails to render or to
603// commit. The mock frame has no Read: a delivery would panic.
604func TestSubmitFrameFailedIndicatedFrameDeliversNothing(t *testing.T) {
605	boom := errors.New("boom")
606	for _, tc := range []struct {
607		name    string
608		renders []error // plain, then indicated
609		commit  []error
610	}{
611		{"render error", []error{nil, boom}, nil},
612		{"commit error", []error{nil, nil}, []error{boom}},
613	} {
614		t.Run(tc.name, func(t *testing.T) {
615			o, _, _ := testOutput(t, tc.commit...)
616			o.cursor = nil
617			r := portsmocks.NewMockRenderer(t)
618			r.EXPECT().UseTarget(0).Return().Once()
619			for _, err := range tc.renders {
620				r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, err).Once()
621			}
622			frame := portsmocks.NewMockCaptureFrame(t)
623			frame.EXPECT().Done().Return(nil).Maybe()
624			r.EXPECT().BeginCapture().Return(frame, nil).Once()
625			r.EXPECT().EndCapture(frame).Return().Once()
626			replies := make(chan ports.CaptureDone, 4)
627			pipeline := capture.NewPipeline(context.Background(), replies)
628			q := sessionRequest(t, 1, false)
629			_, err := o.submitFrame(context.Background(), r, indicatedScene(), nil, nil, []ports.CaptureRequest{q}, pipeline)
630			require.Error(t, err)
631			var done ports.CaptureDone
632			select {
633			case done = <-replies:
634			case <-time.After(5 * time.Second):
635				t.Fatal("no reply")
636			}
637			pipeline.Close(r)
638			require.ErrorIs(t, done.Err, capture.ErrIndicatorMissing)
639			require.ErrorIs(t, done.Err, boom)
640		})
641	}
642}
643
644// The same frame committed: the capture is delivered.
645func TestSubmitFrameIndicatedFrameCommittedDelivers(t *testing.T) {
646	o, _, _ := testOutput(t)
647	o.cursor = nil
648	r := portsmocks.NewMockRenderer(t)
649	r.EXPECT().UseTarget(0).Return().Once()
650	r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil).Twice()
651	frame := portsmocks.NewMockCaptureFrame(t)
652	frame.EXPECT().Done().Return(nil).Maybe()
653	var reads atomic.Int32
654	frame.EXPECT().Read(mock.Anything, mock.Anything, 8).RunAndReturn(func(image.Rectangle, []byte, int) error {
655		reads.Add(1)
656		return nil
657	}).Maybe()
658	r.EXPECT().BeginCapture().Return(frame, nil).Once()
659	r.EXPECT().EndCapture(frame).Return().Once()
660	replies := make(chan ports.CaptureDone, 4)
661	pipeline := capture.NewPipeline(context.Background(), replies)
662	_, err := o.submitFrame(context.Background(), r, indicatedScene(), nil, nil, []ports.CaptureRequest{sessionRequest(t, 1, false)}, pipeline)
663	require.NoError(t, err)
664	select {
665	case done := <-replies:
666		require.NoError(t, done.Err)
667	case <-time.After(5 * time.Second):
668		t.Fatal("no reply")
669	}
670	pipeline.Close(r)
671	require.Equal(t, int32(1), reads.Load())
672}

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