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 for _, c := range s.castSessions {
117 if c.src.kind == srcWindow && c.src.window == id {
118 s.log.Info().Uint64("cast", c.cast).Str("reason", string(ports.CaptureReasonWindowGone)).Msg("cast end")
119 c.end(ports.CaptureReasonWindowGone, nil, true, true)
120 }
121 }
122}
123
124type extraSources struct{ s *Server }
125
126func (extraSources) Destroy(*imagecapture.NeferwlImageCaptureSourceManagerV1) {}
127func (m extraSources) CreateWorkspaceSource(r *imagecapture.NeferwlImageCaptureSourceManagerV1, id uint32, ws *extworkspace.ExtWorkspaceHandleV1) {
128 src := &captureSource{kind: srcDead}
129 if ws != nil {
130 if wid, ok := m.s.workspaceIDOf(ws.Resource); ok {
131 src = &captureSource{kind: srcWorkspace, workspace: wid}
132 }
133 }
134 m.s.newSource(r.Client(), id, src)
135}
136func (m extraSources) CreateOutputRegionSource(r *imagecapture.NeferwlImageCaptureSourceManagerV1, id uint32, out *wayland.Output, x, y, w, h int32) {
137 if w <= 0 || h <= 0 {
138 r.PostError(uint32(imagecapture.NeferwlImageCaptureSourceManagerV1ErrorInvalidRegion), "width or height is not positive")
139 return
140 }
141 src := &captureSource{kind: srcDead}
142 if o := m.s.outputOf(out); o != nil {
143 src = &captureSource{kind: srcRegion, output: o, region: image.Rect(int(x), int(y), int64Clamp(int64(x)+int64(w)), int64Clamp(int64(y)+int64(h)))}
144 }
145 m.s.newSource(r.Client(), id, src)
146}
147
148func (m extraSources) GetWorkspaceFrame(r *imagecapture.NeferwlImageCaptureSourceManagerV1, id uint32, ws *extworkspace.ExtWorkspaceHandleV1) {
149 m.s.newWorkspaceFrame(r.Client(), id, ws)
150}
151
152// workspaceIDOf is the workspace behind a handle of any ext-workspace manager.
153func (s *Server) workspaceIDOf(res *server.Resource) (uint64, bool) {
154 for _, m := range s.workspaceManagers {
155 for id, h := range m.handles {
156 if h.res.Resource == res {
157 return id, true
158 }
159 }
160 }
161 return 0, false
162}
163
164// workspaceInfo finds a workspace of the last snapshot core sent.
165func (s *Server) workspaceInfo(id uint64) (output string, w ports.WorkspaceInfo, ok bool) {
166 for _, out := range s.workspaceSnapshot.Outputs {
167 for _, w := range out.Workspaces {
168 if w.ID == id {
169 return out.Name, w, true
170 }
171 }
172 }
173 return "", ports.WorkspaceInfo{}, false
174}
175
176type copyCaptureManager struct{ s *Server }
177
178func (copyCaptureManager) Destroy(*ext.ExtImageCopyCaptureManagerV1) {}
179func (m copyCaptureManager) CreateSession(r *ext.ExtImageCopyCaptureManagerV1, id uint32, src *source.ExtImageCaptureSourceV1, options uint32) {
180 s := m.s
181 if options&^1 != 0 {
182 r.PostError(uint32(ext.ExtImageCopyCaptureManagerV1ErrorInvalidOption), "invalid capture option")
183 return
184 }
185 var cs *captureSource
186 if src != nil {
187 cs = s.captureSources[src.Resource]
188 }
189 state := &captureSession{captureTarget: captureTarget{s: s, src: cs}, cursor: options&1 != 0}
190 res, err := ext.NewExtImageCopyCaptureSessionV1(r.Client(), 1, id, state)
191 if err != nil {
192 return
193 }
194 state.res = res
195 s.captureSessions[state] = struct{}{}
196 // Frames outlive their session (the spec): each frame drops its own reply.
197 res.OnDestroy = func() { state.end(false, true) }
198 if cs == nil || cs.kind == srcDead || !s.mayCapture(r.Client()) {
199 state.end(true, false)
200 return
201 }
202 if _, _, _, ok := state.geometry(); !ok {
203 state.end(true, false)
204 return
205 }
206 s.nextSession++
207 state.id = s.nextSession
208 s.sessionsByID[state.id] = state
209 open := ports.CaptureSessionOpen{ID: state.id}
210 switch cs.kind {
211 case srcOutput:
212 open.Output = cs.output.name()
213 case srcRegion:
214 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()}
215 case srcWorkspace:
216 open.Workspace = cs.workspace
217 case srcWindow:
218 open.Window = cs.window
219 }
220 s.emit(open)
221 state.sendConstraints()
222}
223
224func (m copyCaptureManager) CreatePointerCursorSession(r *ext.ExtImageCopyCaptureManagerV1, id uint32, _ *source.ExtImageCaptureSourceV1, _ *wayland.Pointer) {
225 state := &cursorCaptureSession{}
226 res, err := ext.NewExtImageCopyCaptureCursorSessionV1(r.Client(), 1, id, state)
227 if err == nil {
228 state.res = res
229 }
230}
231
232type cursorCaptureSession struct {
233 res *ext.ExtImageCopyCaptureCursorSessionV1
234 used bool
235}
236
237func (*cursorCaptureSession) Destroy(*ext.ExtImageCopyCaptureCursorSessionV1) {}
238func (c *cursorCaptureSession) GetCaptureSession(_ *ext.ExtImageCopyCaptureCursorSessionV1, id uint32) {
239 if c.used {
240 c.res.PostError(uint32(ext.ExtImageCopyCaptureCursorSessionV1ErrorDuplicateSession), "session already created")
241 return
242 }
243 c.used = true
244 state := &captureSession{stopped: true}
245 res, err := ext.NewExtImageCopyCaptureSessionV1(c.res.Client(), 1, id, state)
246 if err == nil {
247 state.res = res
248 res.SendStopped()
249 }
250}
251
252// captureTarget is what every registered capture follows, whatever consumes
253// its frames (an ext_image_copy_capture_session_v1 or a screencast cast): the
254// source, the id core knows it by and core's last decision about it.
255type captureTarget struct {
256 s *Server
257 src *captureSource
258 id uint64 // registered with core; 0 when never
259 // st is the last decision of core; haveState tells there is one.
260 st ports.CaptureSessionState
261 haveState bool
262}
263
264// captureSession is one ext_image_copy_capture_session_v1.
265type captureSession struct {
266 captureTarget
267 res *ext.ExtImageCopyCaptureSessionV1
268 cursor, stopped bool
269 frame *captureExtFrame
270 sent image.Point // buffer size of the last constraints
271 excl *exclusion // the live exclusion object of this session
272 // exclSeen: the last state of core knew an exclusion. Frames of an
273 // excluded session leave the HUD out while it is (see tag).
274 exclSeen bool
275}
276
277func (*captureSession) Destroy(*ext.ExtImageCopyCaptureSessionV1) {}
278
279// end stops the session once. send tells the client (stopped), notify core
280// (which has to forget it unless it decided the end itself).
281func (c *captureSession) end(send, notify bool) {
282 if c.stopped {
283 return
284 }
285 c.stopped = true
286 if send && c.res.Alive() {
287 c.res.SendStopped()
288 }
289 if c.excl != nil {
290 c.excl.end(false)
291 }
292 if c.id != 0 {
293 delete(c.s.sessionsByID, c.id)
294 if notify {
295 c.s.emit(ports.CaptureSessionClose{ID: c.id})
296 }
297 }
298 delete(c.s.captureSessions, c)
299}
300
301// geometry locates the session's frames now: the output, the physical region
302// of the output they cover (for a workspace that is not on screen, the whole
303// image of its off-screen render, from the origin) and whether the workspace
304// is off screen. Core's last state decides where a workspace is; before it,
305// the last workspace snapshot does.
306func (c *captureTarget) geometry() (o *output, rect image.Rectangle, hidden, ok bool) {
307 src := c.src
308 if src == nil {
309 return nil, image.Rectangle{}, false, false
310 }
311 switch src.kind {
312 case srcOutput:
313 o = src.output
314 if o == nil || c.s.outputByNameExact(o.name()) != o {
315 return nil, image.Rectangle{}, false, false
316 }
317 return o, image.Rect(0, 0, o.place.Info.Width, o.place.Info.Height), false, true
318 case srcRegion:
319 o = src.output
320 if o == nil || c.s.outputByNameExact(o.name()) != o {
321 return nil, image.Rectangle{}, false, false
322 }
323 rect = captureRegion(o, src.region)
324 return o, rect, false, !rect.Empty()
325 case srcWorkspace:
326 var name string
327 var frame ports.Rect
328 if c.haveState && c.st.Reason == ports.CaptureReasonNone {
329 name, frame, hidden = c.st.Output, c.st.Rect, c.st.Hidden
330 } else {
331 out, w, found := c.s.workspaceInfo(src.workspace)
332 if !found {
333 return nil, image.Rectangle{}, false, false
334 }
335 name, frame, hidden = out, w.Frame, !w.Active
336 }
337 o = c.s.outputByNameExact(name)
338 if o == nil || frame.W <= 0 || frame.H <= 0 {
339 return nil, image.Rectangle{}, false, false
340 }
341 if hidden {
342 scale := o.place.Scale
343 if scale <= 0 {
344 scale = 1
345 }
346 // The size of the off-screen child image (capture.childSize).
347 return o, image.Rect(0, 0, int(math.Ceil(float64(frame.W)*scale)), int(math.Ceil(float64(frame.H)*scale))), true, true
348 }
349 rect = captureRegion(o, image.Rect(frame.X, frame.Y, frame.X+frame.W, frame.Y+frame.H))
350 return o, rect, false, !rect.Empty()
351 case srcWindow:
352 // Core's state gives the size; before it, the last configure.
353 w := c.s.windows[src.window]
354 if w == nil || !w.mapped || w.toplevel == nil || w.gen != src.gen {
355 return nil, image.Rectangle{}, false, false
356 }
357 name, size := w.last.Output, ports.Rect{W: w.last.Width, H: w.last.Height}
358 if c.haveState && c.st.Reason == ports.CaptureReasonNone {
359 name, size = c.st.Output, c.st.Rect
360 }
361 if size.W <= 0 || size.H <= 0 {
362 if gw, gh := w.size(); gw > 0 && gh > 0 {
363 size = ports.Rect{W: gw, H: gh}
364 }
365 }
366 o = c.s.outputByNameExact(name)
367 if o == nil || size.W <= 0 || size.H <= 0 {
368 return nil, image.Rectangle{}, false, false
369 }
370 scale := o.place.Scale
371 if scale <= 0 {
372 scale = 1
373 }
374 // The size of the off-screen child image (capture.childSize).
375 return o, image.Rect(0, 0, int(math.Ceil(float64(size.W)*scale)), int(math.Ceil(float64(size.H)*scale))), true, true
376 }
377 return nil, image.Rectangle{}, false, false
378}
379
380// sendConstraints sends one complete constraint batch: size, formats, done.
381func (c *captureSession) sendConstraints() {
382 _, rect, _, ok := c.geometry()
383 if !ok || !c.res.Alive() || c.s.protected() {
384 return
385 }
386 c.sent = image.Pt(rect.Dx(), rect.Dy())
387 c.res.SendBufferSize(uint32(rect.Dx()), uint32(rect.Dy()))
388 c.res.SendShmFormat(uint32(wayland.ShmFormatXrgb8888))
389 c.res.SendShmFormat(uint32(wayland.ShmFormatArgb8888))
390 c.res.SendDone()
391}
392
393// targetFate says what became of a followed target.
394type targetFate uint8
395
396const (
397 targetLive targetFate = iota
398 targetWorkspaceGone // the workspace left the snapshot
399 targetEmpty // an output or region with nothing left
400)
401
402// follow looks at the target now: its fate and, while it lives, its geometry
403// (rect, ok as geometry gives them). A window or workspace that is not
404// locatable for the moment stays live: core's next state decides.
405func (c *captureTarget) follow() (fate targetFate, rect image.Rectangle, ok bool) {
406 if c.src.kind == srcWorkspace {
407 // The workspace left the snapshot: its handle is removed, stop at once
408 // without waiting for core's state.
409 if _, _, found := c.s.workspaceInfo(c.src.workspace); !found {
410 return targetWorkspaceGone, image.Rectangle{}, false
411 }
412 }
413 _, rect, _, ok = c.geometry()
414 if !ok && c.src.kind != srcWorkspace && c.src.kind != srcWindow {
415 return targetEmpty, rect, false
416 }
417 return targetLive, rect, ok
418}
419
420// applyState takes what core decided; it reports a decision that ends the
421// target (a terminal reason).
422func (c *captureTarget) applyState(st ports.CaptureSessionState) (ended bool) {
423 if st.Reason.Terminal() {
424 c.s.log.Info().Uint64("session", c.id).Str("reason", string(st.Reason)).Msg("capture session end")
425 return true
426 }
427 c.st, c.haveState = st, true
428 c.s.log.Debug().Uint64("session", c.id).Bool("active", st.Active).Bool("hidden", st.Hidden).Uint64("revision", st.Revision).Msg("capture session state")
429 return false
430}
431
432// refresh follows the target: new constraints when the buffer size changed,
433// the end when nothing is left of it.
434func (c *captureSession) refresh() {
435 if c.stopped || c.id == 0 {
436 return
437 }
438 fate, rect, ok := c.follow()
439 switch {
440 case fate == targetWorkspaceGone:
441 c.s.log.Info().Uint64("session", c.id).Str("reason", string(ports.CaptureReasonWorkspaceGone)).Msg("capture session end")
442 c.end(true, true)
443 case fate == targetEmpty:
444 c.s.log.Info().Uint64("session", c.id).Msg("capture session end: target empty")
445 c.end(true, true)
446 case ok && image.Pt(rect.Dx(), rect.Dy()) != c.sent:
447 c.sendConstraints()
448 }
449}
450
451// refreshSessions runs when outputs, scales or workspaces changed.
452func (s *Server) refreshSessions() {
453 for c := range s.captureSessions {
454 c.refresh()
455 }
456 for _, c := range s.castSessions {
457 c.refresh()
458 }
459}
460
461// endCaptureSessions stops every session, and its exclusion with it, when
462// the session gets protected: a capture session never outlives the lock, and
463// its client must ask for a new one after the unlock.
464func (s *Server) endCaptureSessions() {
465 for c := range s.captureSessions {
466 if c.id != 0 {
467 s.log.Info().Uint64("session", c.id).Str("reason", "session-protected").Msg("capture session end")
468 }
469 c.end(true, true)
470 }
471 for _, c := range s.castSessions {
472 s.log.Info().Uint64("cast", c.cast).Str("reason", "session-protected").Msg("cast end")
473 c.end(ports.CaptureReasonNone, errCastProtected, true, true)
474 }
475 s.castInventoryChanged()
476}
477
478// outputGone ends the sessions of an unplugged output without waiting for
479// core. Workspace sessions follow their workspace to its new output.
480func (s *Server) outputGone(o *output) {
481 for c := range s.captureSessions {
482 if c.src != nil && c.src.output == o && (c.src.kind == srcOutput || c.src.kind == srcRegion) {
483 s.log.Info().Uint64("session", c.id).Str("reason", string(ports.CaptureReasonOutputGone)).Msg("capture session end")
484 c.end(true, true)
485 }
486 }
487 for _, c := range s.castSessions {
488 if c.src.kind == srcOutput && c.src.output == o {
489 s.log.Info().Uint64("cast", c.cast).Str("reason", string(ports.CaptureReasonOutputGone)).Msg("cast end")
490 c.end(ports.CaptureReasonOutputGone, nil, true, true)
491 }
492 }
493}
494
495// captureState applies what core decided about a session.
496func (s *Server) captureState(st ports.CaptureSessionState) {
497 if c := s.castByCore[st.ID]; c != nil {
498 c.state(st)
499 return
500 }
501 c := s.sessionsByID[st.ID]
502 if c == nil {
503 return
504 }
505 if c.applyState(st) {
506 c.end(true, false)
507 return
508 }
509 c.exclSeen = st.Exclusion
510 if ex := c.excl; ex != nil {
511 ex.confirm(st.Layers)
512 }
513 c.refresh()
514}
515
516// tag says how a frame of this session is served. A session with an
517// exclusion gets frames without its HUD, fenced by the revision core last
518// confirmed; until core knows the exclusion, and while a finished one is
519// still in the last state, a frame is not served rather than shown with the
520// HUD in it (ok false).
521func (c *captureSession) tag(hidden bool) (t captureTag, ok bool) {
522 t = c.targetTag(hidden)
523 if c.excl != nil && !c.exclSeen {
524 return t, false
525 }
526 if c.exclSeen {
527 t.exclude, t.revision = true, c.st.Revision
528 }
529 return t, true
530}
531
532// targetTag is the part of a frame's tag the target decides: the session,
533// what is captured and what core is told of it.
534func (c *captureTarget) targetTag(hidden bool) (t captureTag) {
535 t = captureTag{session: c.id}
536 switch c.src.kind {
537 case srcWorkspace:
538 t.workspace, t.offscreen = c.src.workspace, hidden
539 case srcWindow:
540 t.window, t.offscreen = c.src.window, true
541 }
542 switch c.src.kind {
543 case srcOutput:
544 t.taken = ports.CaptureFrameTaken{Output: c.src.output.name()}
545 case srcRegion:
546 r := c.src.region
547 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()}}
548 case srcWorkspace:
549 t.taken = ports.CaptureFrameTaken{Workspace: c.src.workspace}
550 case srcWindow:
551 t.taken = ports.CaptureFrameTaken{Window: c.src.window}
552 }
553 return t
554}
555
556func (c *captureSession) CreateFrame(r *ext.ExtImageCopyCaptureSessionV1, id uint32) {
557 if c.frame != nil {
558 r.PostError(uint32(ext.ExtImageCopyCaptureSessionV1ErrorDuplicateFrame), "frame still alive")
559 return
560 }
561 state := &captureExtFrame{session: c}
562 res, err := ext.NewExtImageCopyCaptureFrameV1(r.Client(), 1, id, state)
563 if err == nil {
564 state.res = res
565 c.frame = state
566 res.OnDestroy = func() {
567 if c.frame == state {
568 c.frame = nil
569 }
570 if c.s != nil {
571 delete(c.s.captureReplies, state.replyID)
572 }
573 }
574 }
575}
576
577type captureExtFrame struct {
578 session *captureSession
579 res *ext.ExtImageCopyCaptureFrameV1
580 buf *wayland.Buffer
581 used bool
582 replyID uint64
583 // deadline ends the retries of a frame whose target is not ready yet.
584 deadline time.Time
585}
586
587// A frame whose target is briefly unavailable (not active yet, rendered off
588// screen by another session, its indicator or exclusion not on screen yet,
589// a renderer slot busy) is tried again rather than failed: failed(unknown)
590// ends the whole screencast in screen-sharing clients. Only
591// a stopped session, a refused client or a wrong buffer fail at once.
592const (
593 captureRetryEvery = 16 * time.Millisecond
594 captureRetryFor = time.Second
595)
596
597func (*captureExtFrame) Destroy(*ext.ExtImageCopyCaptureFrameV1) {}
598func (f *captureExtFrame) AttachBuffer(_ *ext.ExtImageCopyCaptureFrameV1, b *wayland.Buffer) {
599 if f.used {
600 f.res.PostError(uint32(ext.ExtImageCopyCaptureFrameV1ErrorAlreadyCaptured), "already captured")
601 return
602 }
603 f.buf = b
604}
605func (f *captureExtFrame) DamageBuffer(_ *ext.ExtImageCopyCaptureFrameV1, x, y, w, h int32) {
606 if f.used {
607 f.res.PostError(uint32(ext.ExtImageCopyCaptureFrameV1ErrorAlreadyCaptured), "already captured")
608 return
609 }
610 if x < 0 || y < 0 || w <= 0 || h <= 0 {
611 f.res.PostError(uint32(ext.ExtImageCopyCaptureFrameV1ErrorInvalidBufferDamage), "invalid damage")
612 }
613}
614
615// constraintsFailed fails the frame with buffer_constraints. The buffer was
616// checked against the geometry computed now; when that differs from the last
617// constraints the client got (the target moved or rescaled and the refresh has
618// not run yet), a new batch goes first so the client can retry with the size
619// the compositor expects.
620func (f *captureExtFrame) constraintsFailed(region image.Rectangle) {
621 if c := f.session; image.Pt(region.Dx(), region.Dy()) != c.sent {
622 c.sendConstraints()
623 }
624 f.res.SendFailed(uint32(ext.ExtImageCopyCaptureFrameV1FailureReasonBufferConstraints))
625}
626
627func (f *captureExtFrame) Capture(*ext.ExtImageCopyCaptureFrameV1) {
628 if f.used {
629 f.res.PostError(uint32(ext.ExtImageCopyCaptureFrameV1ErrorAlreadyCaptured), "already captured")
630 return
631 }
632 f.used = true
633 c := f.session
634 if c.stopped || c.src == nil || !c.s.mayCaptureFrame(f.res.Client()) {
635 f.res.SendFailed(uint32(ext.ExtImageCopyCaptureFrameV1FailureReasonStopped))
636 return
637 }
638 if f.buf == nil {
639 f.res.PostError(uint32(ext.ExtImageCopyCaptureFrameV1ErrorNoBuffer), "no buffer attached")
640 return
641 }
642 f.deadline = c.s.clock.Now().Add(captureRetryFor)
643 f.attempt()
644}
645
646// retry tries the frame again shortly, or fails it past its deadline.
647func (f *captureExtFrame) retry(why string) {
648 s := f.session.s
649 if !s.clock.Now().Before(f.deadline) {
650 s.log.Info().Uint64("session", f.session.id).Str("reason", why).Msg("capture frame failed after retries")
651 f.res.SendFailed(uint32(ext.ExtImageCopyCaptureFrameV1FailureReasonUnknown))
652 return
653 }
654 s.clock.AfterFunc(captureRetryEvery, func() { s.display.Do(f.attempt) })
655}
656
657// attempt serves the frame from the current target. Display goroutine only.
658func (f *captureExtFrame) attempt() {
659 if !f.res.Alive() {
660 return
661 }
662 c := f.session
663 if c.stopped || !c.s.mayCaptureFrame(f.res.Client()) {
664 f.res.SendFailed(uint32(ext.ExtImageCopyCaptureFrameV1FailureReasonStopped))
665 return
666 }
667 o, region, hidden, ok := c.geometry()
668 if !ok || c.haveState && !c.st.Active {
669 f.retry("target not ready")
670 return
671 }
672 tag, ok := c.tag(hidden)
673 // On-screen frames are target pixels, which the output holds under its
674 // transform; off-screen child images are unrotated.
675 transform := uint32(0)
676 if !hidden {
677 transform = uint32(o.place.Transform)
678 }
679 buf, isBuf := c.s.buffers[f.buf.GetResource()].(*buffer)
680 switch {
681 case !isBuf:
682 f.constraintsFailed(region)
683 return
684 case !ok:
685 // The exclusion is not confirmed by core yet: nothing is served.
686 f.retry("exclusion not confirmed")
687 return
688 }
689 f.replyID, ok = c.s.requestCapture(o, region, c.cursor, f.buf, buf.format, func(done ports.CaptureDone) {
690 switch {
691 case errors.Is(done.Err, ports.ErrCaptureTransient):
692 f.retry(done.Err.Error())
693 return
694 case done.Err != nil:
695 // Retrying cannot clear it (a bad buffer, an unsupported size).
696 f.res.SendFailed(uint32(ext.ExtImageCopyCaptureFrameV1FailureReasonUnknown))
697 return
698 }
699 f.res.SendTransform(transform)
700 f.res.SendDamage(0, 0, int32(region.Dx()), int32(region.Dy()))
701 hi, lo, ns := captureTime(done.Time)
702 f.res.SendPresentationTime(hi, lo, ns)
703 f.res.SendReady()
704 }, f.res.Resource, tag)
705 if !ok {
706 switch {
707 case !c.s.validCaptureBuffer(f.buf, region, buf.format):
708 f.constraintsFailed(region)
709 case !c.s.mayCaptureFrame(f.res.Client()):
710 f.res.SendFailed(uint32(ext.ExtImageCopyCaptureFrameV1FailureReasonStopped))
711 default:
712 // The renderer queue or the in-flight bound is full.
713 f.retry("capture queue full")
714 }
715 }
716}