1package capture
2
3import (
4 "context"
5 "image"
6 "os"
7 "testing"
8
9 portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
10 "github.com/bnema/neferwl/internal/ports"
11 "github.com/stretchr/testify/mock"
12 "github.com/stretchr/testify/require"
13 "golang.org/x/sys/unix"
14)
15
16// fencePipe is a sync file stand-in from the kernel: readable once written.
17// fencePipe comes from pipeline_fence_test.go.
18
19func signalFence(t *testing.T, w *os.File) {
20 t.Helper()
21 _, err := w.Write([]byte{1})
22 require.NoError(t, err)
23}
24
25func fenceClosed(f *os.File) bool {
26 _, err := f.Stat()
27 return err != nil
28}
29
30// hiddenPipeline returns a pipeline whose child renders return the given
31// fences in order, and the scene/surfaces to submit.
32func hiddenPipeline(t *testing.T, fences ...*os.File) (*Pipeline, chan ports.CaptureDone, *portsmocks.MockRenderer) {
33 t.Helper()
34 replies := make(chan ports.CaptureDone, 8)
35 p := NewPipeline(context.Background(), replies)
36 child := portsmocks.NewMockRenderer(t)
37 frame := portsmocks.NewMockCaptureFrame(t)
38 frame.EXPECT().Done().Return(nil).Maybe()
39 frame.EXPECT().Read(mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe()
40 child.EXPECT().EndCapture(frame).Return().Maybe()
41 child.EXPECT().BeginCapture().Return(frame, nil).Maybe()
42 child.EXPECT().Close().Return().Maybe()
43 for _, f := range fences {
44 child.EXPECT().Render(mock.Anything, mock.Anything).Return(f, nil).Once()
45 }
46 p.EnableOffscreen(func(int, int) (ports.Renderer, error) { return child, nil })
47 return p, replies, child
48}
49
50func hiddenSceneWindows(ids ...ports.WindowID) ports.Scene {
51 s := hiddenScene()
52 s.CaptureScene.Windows, s.CaptureScene.Layers = nil, nil
53 for _, id := range ids {
54 s.CaptureScene.Windows = append(s.CaptureScene.Windows, ports.SceneWindow{ID: id})
55 }
56 return s
57}
58
59func submitHiddenOne(t *testing.T, p *Pipeline, s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent, id uint64) {
60 t.Helper()
61 p.SubmitHidden(s, surfaces, []ports.CaptureRequest{hiddenRequest(t, id, image.Rect(0, 0, 200, 100))})
62}
63
64// GO001: a failing or cancelled wait still closes every fence, the ones not
65// waited for included.
66func TestWaitHiddenClosesEveryFenceOnCancel(t *testing.T) {
67 f1, _ := fencePipe(t)
68 f2, _ := fencePipe(t)
69 p, replies, _ := hiddenPipeline(t, f1, f2)
70 defer p.Close(nil)
71 s := hiddenSceneWindows(5)
72 submitHiddenOne(t, p, s, nil, 1)
73 submitHiddenOne(t, p, s, nil, 2)
74 require.Equal(t, 2, p.off.nf)
75 ctx, cancel := context.WithCancel(context.Background())
76 cancel()
77 require.Error(t, p.WaitHidden(ctx))
78 require.Zero(t, p.off.nf)
79 require.True(t, fenceClosed(f1), "first fence closed")
80 require.True(t, fenceClosed(f2), "second fence, never waited for, closed too")
81 for range 2 {
82 awaitCapture(t, replies)
83 }
84}
85
86// A terminal fence error must cancel and join readback before closing the
87// device. Holds remain in place until Close has made GPU reads impossible.
88func TestTerminalChildFenceClosesPipelineBeforeClearingHolds(t *testing.T) {
89 read, failed, err := os.Pipe()
90 require.NoError(t, err)
91 defer failed.Close()
92 pending, _ := fencePipe(t)
93 p := NewPipeline(context.Background(), make(chan ports.CaptureDone, 2))
94 defer p.Close(nil)
95 child := portsmocks.NewMockRenderer(t)
96 frame := portsmocks.NewMockCaptureFrame(t)
97 frame.EXPECT().Done().Return(pending).Maybe()
98 child.EXPECT().Render(mock.Anything, mock.Anything).Return(failed, nil).Once()
99 child.EXPECT().BeginCapture().Return(frame, nil).Once()
100 ended := false
101 child.EXPECT().EndCapture(frame).Run(func(ports.CaptureFrame) { ended = true }).Once()
102 child.EXPECT().Close().Run(func() {
103 require.True(t, ended, "worker joined and lease returned before device closes")
104 require.Equal(t, 1, p.off.nf, "hold retained until device closes")
105 }).Once()
106 p.EnableOffscreen(func(int, int) (ports.Renderer, error) { return child, nil })
107 submitHiddenOne(t, p, hiddenSceneWindows(5), map[ports.WindowID]ports.SurfaceContent{5: {Seq: 2}}, 1)
108 _, reads, busy := p.CapHiddenSeen(map[ports.WindowID]uint64{5: 7})
109 require.True(t, busy)
110 require.Equal(t, map[ports.WindowID]uint64{5: 2}, reads)
111 require.NoError(t, read.Close()) // write end now polls POLLERR
112 p.Retire(ports.Scene{}) // session end must not wait forever on the error
113 require.Nil(t, p.off.r, "failed device closed")
114 require.Nil(t, p.HiddenCompleted(), "failed worker joined")
115 seen := map[ports.WindowID]uint64{5: 7}
116 got, reads, busy := p.CapHiddenSeen(seen)
117 require.False(t, busy)
118 require.Empty(t, reads, "closed device lifts non-expiring holds")
119 require.Equal(t, seen, got)
120 require.True(t, fenceClosed(failed))
121}
122
123func TestWaitHiddenWaitsForSignalledFences(t *testing.T) {
124 f1, w1 := fencePipe(t)
125 p, replies, _ := hiddenPipeline(t, f1)
126 defer p.Close(nil)
127 submitHiddenOne(t, p, hiddenSceneWindows(5), nil, 1)
128 signalFence(t, w1)
129 require.NoError(t, p.WaitHidden(context.Background()))
130 require.True(t, fenceClosed(f1))
131 awaitCapture(t, replies)
132}
133
134// GO002: the ceiling of a fence covers only the windows its render drew, per
135// fence, and a signalled fence lifts it.
136func TestCapHiddenSeenPerFenceCeilings(t *testing.T) {
137 f1, w1 := fencePipe(t)
138 f2, w2 := fencePipe(t)
139 p, replies, _ := hiddenPipeline(t, f1, f2)
140 defer p.Close(nil)
141
142 // Render 1 drew window 5 (content Seq 2); render 2 drew 5 (Seq 6) and 6 (Seq 3).
143 submitHiddenOne(t, p, hiddenSceneWindows(5), map[ports.WindowID]ports.SurfaceContent{5: {ID: 5, Seq: 2}}, 1)
144 submitHiddenOne(t, p, hiddenSceneWindows(5, 6), map[ports.WindowID]ports.SurfaceContent{5: {ID: 5, Seq: 6}, 6: {ID: 6, Seq: 3}}, 2)
145
146 // 5 is capped by the lowest ceiling among the unfinished renders that drew
147 // it; 6 by the second; the display's window 1 and an unrelated 9 are free.
148 seen := map[ports.WindowID]uint64{1: 40, 5: 8, 6: 8, 9: 50}
149 got, reads, capped := p.CapHiddenSeen(seen)
150 require.Equal(t, map[ports.WindowID]uint64{5: 2, 6: 3}, reads)
151 require.True(t, capped)
152 require.Equal(t, map[ports.WindowID]uint64{1: 40, 5: 2, 6: 3, 9: 50}, got)
153 require.Equal(t, uint64(8), seen[5], "input untouched")
154
155 // Render 1 finishes: window 5 is now limited by render 2 only.
156 signalFence(t, w1)
157 got, reads, capped = p.CapHiddenSeen(seen)
158 require.Equal(t, map[ports.WindowID]uint64{5: 6, 6: 3}, reads)
159 require.True(t, capped)
160 require.Equal(t, map[ports.WindowID]uint64{1: 40, 5: 6, 6: 3, 9: 50}, got)
161
162 // A window the earlier render never saw is not capped by it: a display
163 // window that is new to the child releases at once.
164 signalFence(t, w2)
165 got, reads, capped = p.CapHiddenSeen(seen)
166 require.Empty(t, reads)
167 require.False(t, capped)
168 require.Equal(t, seen, got)
169 for range 2 {
170 awaitCapture(t, replies)
171 }
172}
173
174// A window the child drew with no content yet is capped at 0; one it never
175// drew is not capped at all.
176func TestCapHiddenSeenMissingContentCapsToZero(t *testing.T) {
177 f1, _ := fencePipe(t)
178 p, replies, _ := hiddenPipeline(t, f1)
179 defer p.Close(nil)
180 submitHiddenOne(t, p, hiddenSceneWindows(5), map[ports.WindowID]ports.SurfaceContent{}, 1)
181 got, reads, capped := p.CapHiddenSeen(map[ports.WindowID]uint64{5: 4, 7: 4})
182 require.Equal(t, map[ports.WindowID]uint64{5: 0}, reads)
183 require.True(t, capped)
184 require.Equal(t, map[ports.WindowID]uint64{5: 0, 7: 4}, got)
185 awaitCapture(t, replies)
186}
187
188func TestCapHiddenSeenNoAllocationInSteadyState(t *testing.T) {
189 f1, _ := fencePipe(t)
190 p, replies, _ := hiddenPipeline(t, f1)
191 defer p.Close(nil)
192 submitHiddenOne(t, p, hiddenSceneWindows(5), map[ports.WindowID]ports.SurfaceContent{5: {ID: 5, Seq: 2}}, 1)
193 seen := map[ports.WindowID]uint64{1: 40, 5: 8}
194 p.CapHiddenSeen(seen) // warm the scratch map
195 allocs := testing.AllocsPerRun(100, func() { p.CapHiddenSeen(seen) })
196 require.LessOrEqual(t, allocs, float64(1), "only the poll descriptor may allocate")
197 t.Logf("outstanding child report: %.1f allocs", allocs)
198 awaitCapture(t, replies)
199 var none *Pipeline
200 got, reads, capped := none.CapHiddenSeen(seen)
201 require.Nil(t, reads)
202 require.False(t, capped)
203 require.Equal(t, seen, got)
204}
205
206// GO003: a third render is refused before the renderer is asked to draw it.
207func TestSubmitHiddenThirdRenderRefusedBeforeRender(t *testing.T) {
208 f1, _ := fencePipe(t)
209 f2, _ := fencePipe(t)
210 p, replies, _ := hiddenPipeline(t, f1, f2) // only two Render expectations
211 defer p.Close(nil)
212 s := hiddenSceneWindows(5)
213 submitHiddenOne(t, p, s, nil, 1)
214 submitHiddenOne(t, p, s, nil, 2)
215 submitHiddenOne(t, p, s, nil, 3)
216 byID := map[uint64]error{}
217 for range 3 {
218 d := awaitCapture(t, replies)
219 byID[d.ID] = d.Err
220 }
221 require.NoError(t, byID[1])
222 require.NoError(t, byID[2])
223 require.ErrorIs(t, byID[3], ErrOffscreenBusy)
224}
225
226// GO005: an idle child of an ended session is closed once, whether its lease
227// or its fence came back last.
228func TestRetireClosesIdleChildOnce(t *testing.T) {
229 f1, w1 := fencePipe(t)
230 replies := make(chan ports.CaptureDone, 4)
231 p := NewPipeline(context.Background(), replies)
232 child := portsmocks.NewMockRenderer(t)
233 frame := portsmocks.NewMockCaptureFrame(t)
234 frame.EXPECT().Done().Return(nil).Maybe()
235 frame.EXPECT().Read(mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe()
236 child.EXPECT().Render(mock.Anything, mock.Anything).Return(f1, nil).Once()
237 child.EXPECT().BeginCapture().Return(frame, nil).Once()
238 child.EXPECT().EndCapture(frame).Return().Once()
239 child.EXPECT().Close().Return().Once() // exactly once
240 p.EnableOffscreen(func(int, int) (ports.Renderer, error) { return child, nil })
241
242 submitHiddenOne(t, p, hiddenSceneWindows(5), nil, 1)
243 awaitCapture(t, replies)
244 p.RecycleHidden(<-p.HiddenCompleted())
245
246 // The fence is still pending: the child stays.
247 p.Retire(ports.Scene{})
248 require.NotNil(t, p.off.r)
249 signalFence(t, w1)
250 p.Retire(ports.Scene{})
251 require.Nil(t, p.off.r, "closed once idle")
252 require.Nil(t, p.HiddenCompleted(), "no child, no channel")
253 p.Retire(ports.Scene{}) // nothing left to close
254 p.Close(nil)
255}
256
257// GO006: a readable fence (or an invalid descriptor) is done; POLLERR and poll
258// failures are terminal; timeouts and interrupts stay pending.
259func TestPollResult(t *testing.T) {
260 for _, tc := range []struct {
261 name string
262 n int
263 err error
264 revents int16
265 want fenceState
266 }{
267 {"readable", 1, nil, unix.POLLIN, fenceDone},
268 {"timeout", 0, nil, 0, fencePending},
269 {"interrupted", -1, unix.EINTR, 0, fencePending},
270 {"other poll error", -1, unix.EIO, 0, fenceFailed},
271 {"invalid poll descriptor", -1, unix.EBADF, 0, fenceDone},
272 {"invalid descriptor event", 1, nil, unix.POLLNVAL, fenceDone},
273 {"gpu error status", 1, nil, unix.POLLERR, fenceFailed},
274 {"error with data", 1, nil, unix.POLLIN | unix.POLLERR, fenceFailed},
275 {"hangup only", 1, nil, unix.POLLHUP, fencePending},
276 } {
277 t.Run(tc.name, func(t *testing.T) {
278 require.Equal(t, tc.want, pollResult(tc.n, tc.err, tc.revents))
279 })
280 }
281}
282
283func TestPollFenceOnRealDescriptors(t *testing.T) {
284 r, w := fencePipe(t)
285 require.Equal(t, fencePending, pollFence(r), "nothing written yet")
286 signalFence(t, w)
287 require.Equal(t, fenceDone, pollFence(r))
288 closed, _ := fencePipe(t)
289 require.NoError(t, closed.Close())
290 require.Equal(t, fenceDone, pollFence(closed), "a closed descriptor has nothing to wait for")
291 allocs := testing.AllocsPerRun(100, func() { pollFence(r) })
292 require.Zero(t, allocs, "polling a fence does not allocate")
293 t.Logf("pollFence: %.1f allocs", allocs)
294}