capture_test.go RawHistoryFolder

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

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