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}