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