1package wayland
2
3import (
4 "errors"
5 "image"
6 "math"
7 "time"
8
9 "github.com/bnema/go-wayland-bindings/server/extforeigntoplevellist"
10 source "github.com/bnema/go-wayland-bindings/server/extimagecapturesource"
11 ext "github.com/bnema/go-wayland-bindings/server/extimagecopycapture"
12 extworkspace "github.com/bnema/go-wayland-bindings/server/extworkspace"
13 "github.com/bnema/go-wayland-bindings/server/wayland"
14 "github.com/bnema/neferwl/internal/adapters/wayland/imagecapture"
15 "github.com/bnema/neferwl/internal/ports"
16 "github.com/bnema/purego-libwayland/server"
17)
18
19// ext-image-copy-capture with three kinds of source: an output (the standard
20// ext-image-capture-source), and the workspace and output-region sources of
21// neferwl_image_capture_source_manager_v1. Every session is registered with
22// core (ports.CaptureSessionOpen), which decides its target, tells when it
23// ends and draws the capture indicator while it lives; see ports/capture.go.
24
25type sourceKind int
26
27const (
28 srcDead sourceKind = iota // output or workspace gone: sessions stop at once
29 srcOutput // a whole output
30 srcRegion // a logical rectangle of an output
31 srcWorkspace // a workspace frame
32 srcWindow // a toplevel, at its own size
33)
34
35// captureSource is the state behind one ext_image_capture_source_v1.
36type captureSource struct {
37 kind sourceKind
38 output *output
39 region image.Rectangle // output-local logical, srcRegion
40 workspace uint64
41 window ports.WindowID // srcWindow
42 gen uint32 // srcWindow: its mapping; a remap kills the source
43}
44
45func registerImageCopy(d *server.Display, s *Server) error {
46 if err := source.NewExtOutputImageCaptureSourceManagerV1Global(d, 1, func(c server.Client, v, id uint32) {
47 _, _ = source.NewExtOutputImageCaptureSourceManagerV1(c, int32(v), id, outputCaptureSource{s})
48 }); err != nil {
49 return err
50 }
51 if err := imagecapture.NewNeferwlImageCaptureSourceManagerV1Global(d, 1, func(c server.Client, v, id uint32) {
52 _, _ = imagecapture.NewNeferwlImageCaptureSourceManagerV1(c, int32(v), id, extraSources{s})
53 }); err != nil {
54 return err
55 }
56 if err := source.NewExtForeignToplevelImageCaptureSourceManagerV1Global(d, 1, func(c server.Client, v, id uint32) {
57 _, _ = source.NewExtForeignToplevelImageCaptureSourceManagerV1(c, int32(v), id, toplevelCaptureSource{s})
58 }); err != nil {
59 return err
60 }
61 return ext.NewExtImageCopyCaptureManagerV1Global(d, 1, func(c server.Client, v, id uint32) {
62 _, _ = ext.NewExtImageCopyCaptureManagerV1(c, int32(v), id, copyCaptureManager{s})
63 })
64}
65
66type captureSourceHandler struct{}
67
68func (captureSourceHandler) Destroy(*source.ExtImageCaptureSourceV1) {}
69
70// newSource makes the ext_image_capture_source_v1 of a source.
71func (s *Server) newSource(c server.Client, id uint32, src *captureSource) {
72 res, err := source.NewExtImageCaptureSourceV1(c, 1, id, captureSourceHandler{})
73 if err != nil {
74 return
75 }
76 s.captureSources[res.Resource] = src
77 res.OnDestroy = func() { delete(s.captureSources, res.Resource) }
78}
79
80type outputCaptureSource struct{ s *Server }
81
82func (outputCaptureSource) Destroy(*source.ExtOutputImageCaptureSourceManagerV1) {}
83func (m outputCaptureSource) CreateSource(r *source.ExtOutputImageCaptureSourceManagerV1, id uint32, out *wayland.Output) {
84 src := &captureSource{kind: srcDead}
85 if o := m.s.outputOf(out); o != nil {
86 src = &captureSource{kind: srcOutput, output: o}
87 }
88 m.s.newSource(r.Client(), id, src)
89}
90
91// toplevelCaptureSource makes the source of an ext-foreign-toplevel-list
92// handle: that window. A closed or unknown handle gives a dead source.
93type toplevelCaptureSource struct{ s *Server }
94
95func (toplevelCaptureSource) Destroy(*source.ExtForeignToplevelImageCaptureSourceManagerV1) {}
96func (m toplevelCaptureSource) CreateSource(r *source.ExtForeignToplevelImageCaptureSourceManagerV1, id uint32, h *extforeigntoplevellist.ExtForeignToplevelHandleV1) {
97 src := &captureSource{kind: srcDead}
98 if h != nil {
99 if wid, gen, ok := m.s.extHandleWindow(h.Resource); ok {
100 src = &captureSource{kind: srcWindow, window: wid, gen: gen}
101 }
102 }
103 m.s.newSource(r.Client(), id, src)
104}
105
106// windowCaptureGone stops the sessions of a window that unmapped or closed.
107func (s *Server) windowCaptureGone(id ports.WindowID) {
108 for c := range s.captureSessions {
109 if c.src != nil && c.src.kind == srcWindow && c.src.window == id {
110 if c.id != 0 {
111 s.log.Info().Uint64("session", c.id).Str("reason", string(ports.CaptureReasonWindowGone)).Msg("capture session end")
112 }
113 c.end(true, true)
114 }
115 }
116}
117
118type extraSources struct{ s *Server }
119
120func (extraSources) Destroy(*imagecapture.NeferwlImageCaptureSourceManagerV1) {}
121func (m extraSources) CreateWorkspaceSource(r *imagecapture.NeferwlImageCaptureSourceManagerV1, id uint32, ws *extworkspace.ExtWorkspaceHandleV1) {
122 src := &captureSource{kind: srcDead}
123 if ws != nil {
124 if wid, ok := m.s.workspaceIDOf(ws.Resource); ok {
125 src = &captureSource{kind: srcWorkspace, workspace: wid}
126 }
127 }
128 m.s.newSource(r.Client(), id, src)
129}
130func (m extraSources) CreateOutputRegionSource(r *imagecapture.NeferwlImageCaptureSourceManagerV1, id uint32, out *wayland.Output, x, y, w, h int32) {
131 if w <= 0 || h <= 0 {
132 r.PostError(uint32(imagecapture.NeferwlImageCaptureSourceManagerV1ErrorInvalidRegion), "width or height is not positive")
133 return
134 }
135 src := &captureSource{kind: srcDead}
136 if o := m.s.outputOf(out); o != nil {
137 src = &captureSource{kind: srcRegion, output: o, region: image.Rect(int(x), int(y), int64Clamp(int64(x)+int64(w)), int64Clamp(int64(y)+int64(h)))}
138 }
139 m.s.newSource(r.Client(), id, src)
140}
141
142func (m extraSources) GetWorkspaceFrame(r *imagecapture.NeferwlImageCaptureSourceManagerV1, id uint32, ws *extworkspace.ExtWorkspaceHandleV1) {
143 m.s.newWorkspaceFrame(r.Client(), id, ws)
144}
145
146// workspaceIDOf is the workspace behind a handle of any ext-workspace manager.
147func (s *Server) workspaceIDOf(res *server.Resource) (uint64, bool) {
148 for _, m := range s.workspaceManagers {
149 for id, h := range m.handles {
150 if h.res.Resource == res {
151 return id, true
152 }
153 }
154 }
155 return 0, false
156}
157
158// workspaceInfo finds a workspace of the last snapshot core sent.
159func (s *Server) workspaceInfo(id uint64) (output string, w ports.WorkspaceInfo, ok bool) {
160 for _, out := range s.workspaceSnapshot.Outputs {
161 for _, w := range out.Workspaces {
162 if w.ID == id {
163 return out.Name, w, true
164 }
165 }
166 }
167 return "", ports.WorkspaceInfo{}, false
168}
169
170type copyCaptureManager struct{ s *Server }
171
172func (copyCaptureManager) Destroy(*ext.ExtImageCopyCaptureManagerV1) {}
173func (m copyCaptureManager) CreateSession(r *ext.ExtImageCopyCaptureManagerV1, id uint32, src *source.ExtImageCaptureSourceV1, options uint32) {
174 s := m.s
175 if options&^1 != 0 {
176 r.PostError(uint32(ext.ExtImageCopyCaptureManagerV1ErrorInvalidOption), "invalid capture option")
177 return
178 }
179 var cs *captureSource
180 if src != nil {
181 cs = s.captureSources[src.Resource]
182 }
183 state := &captureSession{s: s, src: cs, cursor: options&1 != 0}
184 res, err := ext.NewExtImageCopyCaptureSessionV1(r.Client(), 1, id, state)
185 if err != nil {
186 return
187 }
188 state.res = res
189 s.captureSessions[state] = struct{}{}
190 // Frames outlive their session (the spec): each frame drops its own reply.
191 res.OnDestroy = func() { state.end(false, true) }
192 if cs == nil || cs.kind == srcDead || !s.mayCapture(r.Client()) {
193 state.end(true, false)
194 return
195 }
196 if _, _, _, ok := state.geometry(); !ok {
197 state.end(true, false)
198 return
199 }
200 s.nextSession++
201 state.id = s.nextSession
202 s.sessionsByID[state.id] = state
203 open := ports.CaptureSessionOpen{ID: state.id}
204 switch cs.kind {
205 case srcOutput:
206 open.Output = cs.output.name()
207 case srcRegion:
208 open.Output, open.Region = cs.output.name(), ports.Rect{X: cs.region.Min.X, Y: cs.region.Min.Y, W: cs.region.Dx(), H: cs.region.Dy()}
209 case srcWorkspace:
210 open.Workspace = cs.workspace
211 case srcWindow:
212 open.Window = cs.window
213 }
214 s.emit(open)
215 state.sendConstraints()
216}
217
218func (m copyCaptureManager) CreatePointerCursorSession(r *ext.ExtImageCopyCaptureManagerV1, id uint32, _ *source.ExtImageCaptureSourceV1, _ *wayland.Pointer) {
219 state := &cursorCaptureSession{}
220 res, err := ext.NewExtImageCopyCaptureCursorSessionV1(r.Client(), 1, id, state)
221 if err == nil {
222 state.res = res
223 }
224}
225
226type cursorCaptureSession struct {
227 res *ext.ExtImageCopyCaptureCursorSessionV1
228 used bool
229}
230
231func (*cursorCaptureSession) Destroy(*ext.ExtImageCopyCaptureCursorSessionV1) {}
232func (c *cursorCaptureSession) GetCaptureSession(_ *ext.ExtImageCopyCaptureCursorSessionV1, id uint32) {
233 if c.used {
234 c.res.PostError(uint32(ext.ExtImageCopyCaptureCursorSessionV1ErrorDuplicateSession), "session already created")
235 return
236 }
237 c.used = true
238 state := &captureSession{stopped: true}
239 res, err := ext.NewExtImageCopyCaptureSessionV1(c.res.Client(), 1, id, state)
240 if err == nil {
241 state.res = res
242 res.SendStopped()
243 }
244}
245
246// captureSession is one ext_image_copy_capture_session_v1.
247type captureSession struct {
248 s *Server
249 res *ext.ExtImageCopyCaptureSessionV1
250 src *captureSource
251 id uint64 // registered with core; 0 when never
252 cursor, stopped bool
253 frame *captureExtFrame
254 sent image.Point // buffer size of the last constraints
255 // st is the last decision of core; haveState tells there is one.
256 st ports.CaptureSessionState
257 haveState bool
258 excl *exclusion // the live exclusion object of this session
259 // exclSeen: the last state of core knew an exclusion. Frames of an
260 // excluded session leave the HUD out while it is (see tag).
261 exclSeen bool
262}
263
264func (*captureSession) Destroy(*ext.ExtImageCopyCaptureSessionV1) {}
265
266// end stops the session once. send tells the client (stopped), notify core
267// (which has to forget it unless it decided the end itself).
268func (c *captureSession) end(send, notify bool) {
269 if c.stopped {
270 return
271 }
272 c.stopped = true
273 if send && c.res.Alive() {
274 c.res.SendStopped()
275 }
276 if c.excl != nil {
277 c.excl.end(false)
278 }
279 if c.id != 0 {
280 delete(c.s.sessionsByID, c.id)
281 if notify {
282 c.s.emit(ports.CaptureSessionClose{ID: c.id})
283 }
284 }
285 delete(c.s.captureSessions, c)
286}
287
288// geometry locates the session's frames now: the output, the physical region
289// of the output they cover (for a workspace that is not on screen, the whole
290// image of its off-screen render, from the origin) and whether the workspace
291// is off screen. Core's last state decides where a workspace is; before it,
292// the last workspace snapshot does.
293func (c *captureSession) geometry() (o *output, rect image.Rectangle, hidden, ok bool) {
294 src := c.src
295 if src == nil {
296 return nil, image.Rectangle{}, false, false
297 }
298 switch src.kind {
299 case srcOutput:
300 o = src.output
301 if o == nil || c.s.outputByNameExact(o.name()) != o {
302 return nil, image.Rectangle{}, false, false
303 }
304 return o, image.Rect(0, 0, o.place.Info.Width, o.place.Info.Height), false, true
305 case srcRegion:
306 o = src.output
307 if o == nil || c.s.outputByNameExact(o.name()) != o {
308 return nil, image.Rectangle{}, false, false
309 }
310 rect = captureRegion(o, src.region)
311 return o, rect, false, !rect.Empty()
312 case srcWorkspace:
313 var name string
314 var frame ports.Rect
315 if c.haveState && c.st.Reason == ports.CaptureReasonNone {
316 name, frame, hidden = c.st.Output, c.st.Rect, c.st.Hidden
317 } else {
318 out, w, found := c.s.workspaceInfo(src.workspace)
319 if !found {
320 return nil, image.Rectangle{}, false, false
321 }
322 name, frame, hidden = out, w.Frame, !w.Active
323 }
324 o = c.s.outputByNameExact(name)
325 if o == nil || frame.W <= 0 || frame.H <= 0 {
326 return nil, image.Rectangle{}, false, false
327 }
328 if hidden {
329 scale := o.place.Scale
330 if scale <= 0 {
331 scale = 1
332 }
333 // The size of the off-screen child image (capture.childSize).
334 return o, image.Rect(0, 0, int(math.Ceil(float64(frame.W)*scale)), int(math.Ceil(float64(frame.H)*scale))), true, true
335 }
336 rect = captureRegion(o, image.Rect(frame.X, frame.Y, frame.X+frame.W, frame.Y+frame.H))
337 return o, rect, false, !rect.Empty()
338 case srcWindow:
339 // Core's state gives the size; before it, the last configure.
340 w := c.s.windows[src.window]
341 if w == nil || !w.mapped || w.toplevel == nil || w.gen != src.gen {
342 return nil, image.Rectangle{}, false, false
343 }
344 name, size := w.last.Output, ports.Rect{W: w.last.Width, H: w.last.Height}
345 if c.haveState && c.st.Reason == ports.CaptureReasonNone {
346 name, size = c.st.Output, c.st.Rect
347 }
348 if size.W <= 0 || size.H <= 0 {
349 if gw, gh := w.size(); gw > 0 && gh > 0 {
350 size = ports.Rect{W: gw, H: gh}
351 }
352 }
353 o = c.s.outputByNameExact(name)
354 if o == nil || size.W <= 0 || size.H <= 0 {
355 return nil, image.Rectangle{}, false, false
356 }
357 scale := o.place.Scale
358 if scale <= 0 {
359 scale = 1
360 }
361 // The size of the off-screen child image (capture.childSize).
362 return o, image.Rect(0, 0, int(math.Ceil(float64(size.W)*scale)), int(math.Ceil(float64(size.H)*scale))), true, true
363 }
364 return nil, image.Rectangle{}, false, false
365}
366
367// sendConstraints sends one complete constraint batch: size, formats, done.
368func (c *captureSession) sendConstraints() {
369 _, rect, _, ok := c.geometry()
370 if !ok || !c.res.Alive() || c.s.protected() {
371 return
372 }
373 c.sent = image.Pt(rect.Dx(), rect.Dy())
374 c.res.SendBufferSize(uint32(rect.Dx()), uint32(rect.Dy()))
375 c.res.SendShmFormat(uint32(wayland.ShmFormatXrgb8888))
376 c.res.SendShmFormat(uint32(wayland.ShmFormatArgb8888))
377 c.res.SendDone()
378}
379
380// refresh follows the target: new constraints when the buffer size changed,
381// the end when nothing is left of it.
382func (c *captureSession) refresh() {
383 if c.stopped || c.id == 0 {
384 return
385 }
386 if c.src.kind == srcWorkspace {
387 // The workspace left the snapshot: its handle is removed, stop at once
388 // without waiting for core's state.
389 if _, _, found := c.s.workspaceInfo(c.src.workspace); !found {
390 c.s.log.Info().Uint64("session", c.id).Str("reason", string(ports.CaptureReasonWorkspaceGone)).Msg("capture session end")
391 c.end(true, true)
392 return
393 }
394 }
395 _, rect, _, ok := c.geometry()
396 switch {
397 case !ok && c.src.kind != srcWorkspace && c.src.kind != srcWindow:
398 c.s.log.Info().Uint64("session", c.id).Msg("capture session end: target empty")
399 c.end(true, true)
400 case ok && image.Pt(rect.Dx(), rect.Dy()) != c.sent:
401 c.sendConstraints()
402 }
403}
404
405// refreshSessions runs when outputs, scales or workspaces changed.
406func (s *Server) refreshSessions() {
407 for c := range s.captureSessions {
408 c.refresh()
409 }
410}
411
412// endCaptureSessions stops every session, and its exclusion with it, when
413// the session gets protected: a capture session never outlives the lock, and
414// its client must ask for a new one after the unlock.
415func (s *Server) endCaptureSessions() {
416 for c := range s.captureSessions {
417 if c.id != 0 {
418 s.log.Info().Uint64("session", c.id).Str("reason", "session-protected").Msg("capture session end")
419 }
420 c.end(true, true)
421 }
422}
423
424// outputGone ends the sessions of an unplugged output without waiting for
425// core. Workspace sessions follow their workspace to its new output.
426func (s *Server) outputGone(o *output) {
427 for c := range s.captureSessions {
428 if c.src != nil && c.src.output == o && (c.src.kind == srcOutput || c.src.kind == srcRegion) {
429 s.log.Info().Uint64("session", c.id).Str("reason", string(ports.CaptureReasonOutputGone)).Msg("capture session end")
430 c.end(true, true)
431 }
432 }
433}
434
435// captureState applies what core decided about a session.
436func (s *Server) captureState(st ports.CaptureSessionState) {
437 c := s.sessionsByID[st.ID]
438 if c == nil {
439 return
440 }
441 if st.Reason.Terminal() {
442 s.log.Info().Uint64("session", c.id).Str("reason", string(st.Reason)).Msg("capture session end")
443 c.end(true, false)
444 return
445 }
446 c.st, c.haveState, c.exclSeen = st, true, st.Exclusion
447 s.log.Debug().Uint64("session", c.id).Bool("active", st.Active).Bool("hidden", st.Hidden).Uint64("revision", st.Revision).Msg("capture session state")
448 if ex := c.excl; ex != nil {
449 ex.confirm(st.Layers)
450 }
451 c.refresh()
452}
453
454// tag says how a frame of this session is served. A session with an
455// exclusion gets frames without its HUD, fenced by the revision core last
456// confirmed; until core knows the exclusion, and while a finished one is
457// still in the last state, a frame is not served rather than shown with the
458// HUD in it (ok false).
459func (c *captureSession) tag(hidden bool) (t captureTag, ok bool) {
460 t = captureTag{session: c.id}
461 switch c.src.kind {
462 case srcWorkspace:
463 t.workspace, t.offscreen = c.src.workspace, hidden
464 case srcWindow:
465 t.window, t.offscreen = c.src.window, true
466 }
467 switch c.src.kind {
468 case srcOutput:
469 t.taken = ports.CaptureFrameTaken{Output: c.src.output.name()}
470 case srcRegion:
471 r := c.src.region
472 t.taken = ports.CaptureFrameTaken{Output: c.src.output.name(), Region: ports.Rect{X: r.Min.X, Y: r.Min.Y, W: r.Dx(), H: r.Dy()}}
473 case srcWorkspace:
474 t.taken = ports.CaptureFrameTaken{Workspace: c.src.workspace}
475 case srcWindow:
476 t.taken = ports.CaptureFrameTaken{Window: c.src.window}
477 }
478 if c.excl != nil && !c.exclSeen {
479 return t, false
480 }
481 if c.exclSeen {
482 t.exclude, t.revision = true, c.st.Revision
483 }
484 return t, true
485}
486
487func (c *captureSession) CreateFrame(r *ext.ExtImageCopyCaptureSessionV1, id uint32) {
488 if c.frame != nil {
489 r.PostError(uint32(ext.ExtImageCopyCaptureSessionV1ErrorDuplicateFrame), "frame still alive")
490 return
491 }
492 state := &captureExtFrame{session: c}
493 res, err := ext.NewExtImageCopyCaptureFrameV1(r.Client(), 1, id, state)
494 if err == nil {
495 state.res = res
496 c.frame = state
497 res.OnDestroy = func() {
498 if c.frame == state {
499 c.frame = nil
500 }
501 if c.s != nil {
502 delete(c.s.captureReplies, state.replyID)
503 }
504 }
505 }
506}
507
508type captureExtFrame struct {
509 session *captureSession
510 res *ext.ExtImageCopyCaptureFrameV1
511 buf *wayland.Buffer
512 used bool
513 replyID uint64
514 // deadline ends the retries of a frame whose target is not ready yet.
515 deadline time.Time
516}
517
518// A frame whose target is briefly unavailable (not active yet, rendered off
519// screen by another session, its indicator or exclusion not on screen yet,
520// a renderer slot busy) is tried again rather than failed: failed(unknown)
521// ends the whole screencast in clients such as xdg-desktop-portal-wlr. Only
522// a stopped session, a refused client or a wrong buffer fail at once.
523const (
524 captureRetryEvery = 16 * time.Millisecond
525 captureRetryFor = time.Second
526)
527
528func (*captureExtFrame) Destroy(*ext.ExtImageCopyCaptureFrameV1) {}
529func (f *captureExtFrame) AttachBuffer(_ *ext.ExtImageCopyCaptureFrameV1, b *wayland.Buffer) {
530 if f.used {
531 f.res.PostError(uint32(ext.ExtImageCopyCaptureFrameV1ErrorAlreadyCaptured), "already captured")
532 return
533 }
534 f.buf = b
535}
536func (f *captureExtFrame) DamageBuffer(_ *ext.ExtImageCopyCaptureFrameV1, x, y, w, h int32) {
537 if f.used {
538 f.res.PostError(uint32(ext.ExtImageCopyCaptureFrameV1ErrorAlreadyCaptured), "already captured")
539 return
540 }
541 if x < 0 || y < 0 || w <= 0 || h <= 0 {
542 f.res.PostError(uint32(ext.ExtImageCopyCaptureFrameV1ErrorInvalidBufferDamage), "invalid damage")
543 }
544}
545
546// constraintsFailed fails the frame with buffer_constraints. The buffer was
547// checked against the geometry computed now; when that differs from the last
548// constraints the client got (the target moved or rescaled and the refresh has
549// not run yet), a new batch goes first so the client can retry with the size
550// the compositor expects.
551func (f *captureExtFrame) constraintsFailed(region image.Rectangle) {
552 if c := f.session; image.Pt(region.Dx(), region.Dy()) != c.sent {
553 c.sendConstraints()
554 }
555 f.res.SendFailed(uint32(ext.ExtImageCopyCaptureFrameV1FailureReasonBufferConstraints))
556}
557
558func (f *captureExtFrame) Capture(*ext.ExtImageCopyCaptureFrameV1) {
559 if f.used {
560 f.res.PostError(uint32(ext.ExtImageCopyCaptureFrameV1ErrorAlreadyCaptured), "already captured")
561 return
562 }
563 f.used = true
564 c := f.session
565 if c.stopped || c.src == nil || !c.s.mayCaptureFrame(f.res.Client()) {
566 f.res.SendFailed(uint32(ext.ExtImageCopyCaptureFrameV1FailureReasonStopped))
567 return
568 }
569 if f.buf == nil {
570 f.res.PostError(uint32(ext.ExtImageCopyCaptureFrameV1ErrorNoBuffer), "no buffer attached")
571 return
572 }
573 f.deadline = c.s.clock.Now().Add(captureRetryFor)
574 f.attempt()
575}
576
577// retry tries the frame again shortly, or fails it past its deadline.
578func (f *captureExtFrame) retry(why string) {
579 s := f.session.s
580 if !s.clock.Now().Before(f.deadline) {
581 s.log.Info().Uint64("session", f.session.id).Str("reason", why).Msg("capture frame failed after retries")
582 f.res.SendFailed(uint32(ext.ExtImageCopyCaptureFrameV1FailureReasonUnknown))
583 return
584 }
585 s.clock.AfterFunc(captureRetryEvery, func() { s.display.Do(f.attempt) })
586}
587
588// attempt serves the frame from the current target. Display goroutine only.
589func (f *captureExtFrame) attempt() {
590 if !f.res.Alive() {
591 return
592 }
593 c := f.session
594 if c.stopped || !c.s.mayCaptureFrame(f.res.Client()) {
595 f.res.SendFailed(uint32(ext.ExtImageCopyCaptureFrameV1FailureReasonStopped))
596 return
597 }
598 o, region, hidden, ok := c.geometry()
599 if !ok || c.haveState && !c.st.Active {
600 f.retry("target not ready")
601 return
602 }
603 tag, ok := c.tag(hidden)
604 // On-screen frames are target pixels, which the output holds under its
605 // transform; off-screen child images are unrotated.
606 transform := uint32(0)
607 if !hidden {
608 transform = uint32(o.place.Transform)
609 }
610 buf, isBuf := c.s.buffers[f.buf.GetResource()].(*buffer)
611 switch {
612 case !isBuf:
613 f.constraintsFailed(region)
614 return
615 case !ok:
616 // The exclusion is not confirmed by core yet: nothing is served.
617 f.retry("exclusion not confirmed")
618 return
619 }
620 f.replyID, ok = c.s.requestCapture(o, region, c.cursor, f.buf, buf.format, func(done ports.CaptureDone) {
621 switch {
622 case errors.Is(done.Err, ports.ErrCaptureTransient):
623 f.retry(done.Err.Error())
624 return
625 case done.Err != nil:
626 // Retrying cannot clear it (a bad buffer, an unsupported size).
627 f.res.SendFailed(uint32(ext.ExtImageCopyCaptureFrameV1FailureReasonUnknown))
628 return
629 }
630 f.res.SendTransform(transform)
631 f.res.SendDamage(0, 0, int32(region.Dx()), int32(region.Dy()))
632 hi, lo, ns := captureTime(done.Time)
633 f.res.SendPresentationTime(hi, lo, ns)
634 f.res.SendReady()
635 }, f.res.Resource, tag)
636 if !ok {
637 switch {
638 case !c.s.validCaptureBuffer(f.buf, region, buf.format):
639 f.constraintsFailed(region)
640 case !c.s.mayCaptureFrame(f.res.Client()):
641 f.res.SendFailed(uint32(ext.ExtImageCopyCaptureFrameV1FailureReasonStopped))
642 default:
643 // The renderer queue or the in-flight bound is full.
644 f.retry("capture queue full")
645 }
646 }
647}