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}