1package wayland
2
3import (
4 "errors"
5 "fmt"
6 "time"
7
8 "github.com/bnema/wlturbo/protocol/imagecapturesource"
9 "github.com/bnema/wlturbo/protocol/imagecopycapture"
10
11 "github.com/bnema/nefercap/internal/ports"
12)
13
14// wl_shm format values for the two supported native little-endian layouts.
15const (
16 shmARGB8888 = uint32(ports.ARGB8888)
17 shmXRGB8888 = uint32(ports.XRGB8888)
18)
19
20// maxFrameAttempts bounds the retries of one frame after failures that the
21// protocol calls recoverable.
22const maxFrameAttempts = 4
23
24var errCompositorFailedCapture = errors.New("wayland: compositor failed the capture")
25
26// errCaptureRefused is ErrCaptureStopped for a session that stopped before
27// serving any frame.
28var errCaptureRefused = fmt.Errorf("%w: compositor refused capture (on NeferWL, list this executable in /etc/neferwl/capture-allow)", ports.ErrCaptureStopped)
29
30// constraints is one batch of buffer constraints, valid once done arrived.
31type constraints struct {
32 width, height uint32
33 haveSize bool
34 format uint32 // first supported shm format of the batch
35 haveFormat bool
36 sawShm bool
37 unsupported uint32 // last rejected shm format
38}
39
40// session is one ext_image_copy_capture_session_v1. Its events are dispatched
41// on the goroutine that owns the operation.
42type session struct {
43 proxy *imagecopycapture.ExtImageCopyCaptureSession
44 key sessionKey
45
46 pending constraints
47 current constraints
48 gen uint64 // number of constraint batches completed
49 stopped bool
50 dead bool // destroy sent
51
52 exclusion *exclusion // nil until requested
53
54 // Frame pacing. At most one frame is in flight. The last completed frame
55 // lives in buffer cur and stays untouched while the next one is written to
56 // the other buffer, so a Capture that finds the compositor idle (it waits
57 // for damage after the first frame) can repeat the last frame at once.
58 frame captureFrame // outcome of the in-flight frame, reset per frame
59 inflight *captureFrame // &frame while a frame is in flight, else nil
60 inflightBuf int
61 inflightGeom bufGeom // buffer layout the in-flight frame was made for
62 haveLast bool
63 cur int
64 lastGeom bufGeom
65 served int // frames handed out by this session
66 fails int // consecutive failed frames
67
68 // Frame event handlers, built once and shared by every frame proxy.
69 onTransform func(uint32)
70 onReady func()
71 onFailed func(uint32)
72}
73
74// newSession wraps a created session proxy and subscribes to its events.
75func newSession(proxy *imagecopycapture.ExtImageCopyCaptureSession, key sessionKey) *session {
76 c := &session{proxy: proxy, key: key}
77 proxy.OnBufferSize(func(width, height uint32) {
78 c.pending.width, c.pending.height, c.pending.haveSize = width, height, true
79 })
80 proxy.OnShmFormat(func(format uint32) {
81 c.pending.sawShm = true
82 switch {
83 case format != shmARGB8888 && format != shmXRGB8888:
84 c.pending.unsupported = format
85 case !c.pending.haveFormat:
86 c.pending.format, c.pending.haveFormat = format, true
87 }
88 })
89 proxy.OnDone(func() {
90 c.current, c.pending = c.pending, constraints{}
91 c.gen++
92 })
93 proxy.OnStopped(func() { c.stopped = true })
94 c.onTransform = func(transform uint32) { c.frame.transform = transform }
95 c.onReady = func() { c.frame.ready = true }
96 c.onFailed = func(reason uint32) { c.frame.failed, c.frame.reason = true, reason }
97 return c
98}
99
100// captureFrame is one ext_image_copy_capture_frame_v1 and its outcome. The
101// session owns a single one, reset for each frame.
102type captureFrame struct {
103 proxy *imagecopycapture.ExtImageCopyCaptureFrame
104 transform uint32
105 ready bool
106 failed bool
107 reason uint32
108}
109
110// newCaptureFrame creates the next frame of c, resetting its outcome, and
111// subscribes the session's shared handlers to its events.
112func newCaptureFrame(c *session) (*captureFrame, error) {
113 proxy, err := c.proxy.CreateFrame()
114 if err != nil {
115 return nil, err
116 }
117 c.frame = captureFrame{proxy: proxy}
118 proxy.OnTransform(c.onTransform)
119 proxy.OnReady(c.onReady)
120 proxy.OnFailed(c.onFailed)
121 return &c.frame, nil
122}
123
124// sessionKey identifies what a session captures. Crop sessions have a zero
125// region: the region is cut from each frame.
126type sessionKey struct {
127 output uint32
128 workspace uint64
129 region ports.Region
130}
131
132func validRegion(r ports.Region) error {
133 if r == (ports.Region{}) {
134 return nil
135 }
136 if r.X < 0 || r.Y < 0 || r.Width < 1 || r.Height < 1 ||
137 r.X > ports.MaxDimension || r.Y > ports.MaxDimension ||
138 r.Width > ports.MaxDimension || r.Height > ports.MaxDimension {
139 return fmt.Errorf("%w: %+v", ports.ErrInvalidRegion, r)
140 }
141 return nil
142}
143
144// captureTarget resolves t to a session, creating or replacing it, and copies
145// one frame. The caller holds opMu.
146func (s *Source) captureTarget(t ports.Target) (ports.Frame, error) {
147 out := s.outputs[t.OutputID]
148 if out == nil {
149 return ports.Frame{}, ports.ErrOutputNotFound
150 }
151 key, crop, err := s.resolve(t, out)
152 if err != nil {
153 return ports.Frame{}, err
154 }
155 sess, err := s.sessionFor(key, out)
156 if err != nil {
157 return ports.Frame{}, err
158 }
159 frame, err := s.nextFrame(sess, out)
160 if err != nil {
161 return ports.Frame{}, err
162 }
163 if crop == (ports.Region{}) {
164 return frame, nil
165 }
166 return cropFrame(frame, crop, out)
167}
168
169// resolve maps a target to its session key and the crop to apply per frame.
170func (s *Source) resolve(t ports.Target, out *output) (sessionKey, ports.Region, error) {
171 if t.WorkspaceID != 0 {
172 return s.workspaceKey(t, out)
173 }
174 if t.Region != (ports.Region{}) && s.ext.source != nil {
175 return sessionKey{output: out.id, region: t.Region}, ports.Region{}, nil
176 }
177 return sessionKey{output: out.id}, t.Region, nil
178}
179
180// sessionFor returns the live session for key. A different key replaces the
181// current session, which ends its exclusion.
182func (s *Source) sessionFor(key sessionKey, out *output) (*session, error) {
183 if c := s.sess; c != nil {
184 if c.key == key && (!c.stopped || c.exclusion != nil) {
185 // A stopped session with an exclusion stays stopped: replacing it
186 // would quietly lose the exclusion in the middle of a recording.
187 return c, nil
188 }
189 s.dropSession()
190 }
191 var src *imagecapturesource.ExtImageCaptureSource
192 var err error
193 switch {
194 case key.workspace != 0:
195 src, err = s.createWorkspaceSource(key.workspace)
196 case key.region != (ports.Region{}):
197 src, err = s.createRegionSource(out, key.region)
198 default:
199 src, err = s.outSource.CreateSource(out.proxy)
200 }
201 if err != nil {
202 s.terminate()
203 return nil, fmt.Errorf("wayland: create capture source: %w", err)
204 }
205 proxy, err := s.copyManager.CreateSession(src, 0)
206 _ = src.Destroy()
207 if err != nil {
208 s.terminate()
209 return nil, fmt.Errorf("wayland: create capture session: %w", err)
210 }
211 c := newSession(proxy, key)
212 s.sess = c
213 return c, nil
214}
215
216// dropSession destroys the current session, if any.
217func (s *Source) dropSession() {
218 c := s.sess
219 s.sess = nil
220 if c == nil || c.dead {
221 return
222 }
223 c.dead = true
224 s.destroyFrame(c)
225 if c.exclusion != nil {
226 c.exclusion.destroy(s)
227 }
228 _ = c.proxy.Destroy()
229}
230
231// nextFrame returns the session's current frame.
232//
233// The first frame of a session is waited for: the protocol guarantees it does
234// not wait for damage. After that the compositor may hold a capture back until
235// the screen changes, so a capture stays pending across calls (still one frame
236// in flight) and a call that finds it unfinished after pollWait returns the
237// last completed frame again. The caller's schedule is then never stretched by
238// a static screen, and a recording repeats that frame.
239//
240// A failed frame is retried after a roundtrip, at most maxFrameAttempts
241// failures in a row (the count survives calls and restarts at each completed
242// frame): stopped ends the capture, buffer_constraints is retried with the
243// latest constraints, which are the current ones when the compositor sent no
244// new batch. An unknown failure is retried the same way, unless a last frame
245// exists: that one is repeated, like when a capture is still pending.
246func (s *Source) nextFrame(c *session, out *output) (ports.Frame, error) {
247 for {
248 for c.gen == 0 && !c.stopped {
249 if err := s.dispatch(); err != nil {
250 return ports.Frame{}, err
251 }
252 }
253 if c.stopped {
254 return ports.Frame{}, s.stoppedError(c, out)
255 }
256 g, err := s.geometry(c)
257 if err != nil {
258 return ports.Frame{}, err
259 }
260 // A new constraint batch with the same layout changes nothing: the last
261 // frame stays valid and the pending capture stays in flight.
262 if c.haveLast && c.lastGeom != g {
263 c.haveLast = false
264 }
265 if c.inflight != nil && c.inflightGeom != g {
266 s.destroyFrame(c)
267 }
268 if c.inflight == nil {
269 if err := s.startFrame(c, g); err != nil {
270 return ports.Frame{}, err
271 }
272 }
273 if err := s.awaitFrame(c, c.haveLast); err != nil {
274 return ports.Frame{}, err
275 }
276 f := c.inflight
277 switch {
278 case c.stopped:
279 return ports.Frame{}, s.stoppedError(c, out)
280 case f.ready:
281 return s.complete(c, g, f)
282 case f.failed:
283 if err := s.retryAfterFailure(c, f); err != nil {
284 if c.stopped {
285 return ports.Frame{}, s.stoppedError(c, out)
286 }
287 c.fails = 0 // the next call starts afresh
288 return ports.Frame{}, err
289 }
290 if c.stopped {
291 return ports.Frame{}, s.stoppedError(c, out)
292 }
293 if c.haveLast && f.reason == imagecopycapture.FAILURE_REASON_UNKNOWN {
294 return s.lastFrame(c)
295 }
296 default: // still pending after the poll: repeat the last frame
297 return s.lastFrame(c)
298 }
299 }
300}
301
302// awaitFrame dispatches until the in-flight frame ended or the session
303// stopped. With poll set it gives up after pollWait.
304func (s *Source) awaitFrame(c *session, poll bool) error {
305 f := c.inflight
306 var deadline time.Time
307 if poll {
308 deadline = time.Now().Add(s.pollWait)
309 }
310 for !f.ready && !f.failed && !c.stopped {
311 timedOut, err := s.dispatchUntil(deadline)
312 if err != nil {
313 return err
314 }
315 if timedOut {
316 return nil
317 }
318 }
319 return nil
320}
321
322// retryAfterFailure handles a failed frame: one roundtrip, so that a stop or a
323// new constraint batch that follows the failure is seen, then a retry.
324func (s *Source) retryAfterFailure(c *session, f *captureFrame) error {
325 s.destroyFrame(c)
326 if f.reason == imagecopycapture.FAILURE_REASON_STOPPED {
327 c.stopped = true
328 return nil
329 }
330 if c.fails++; c.fails >= maxFrameAttempts {
331 return errCompositorFailedCapture
332 }
333 switch f.reason {
334 case imagecopycapture.FAILURE_REASON_BUFFER_CONSTRAINTS, imagecopycapture.FAILURE_REASON_UNKNOWN:
335 return s.roundtrip()
336 }
337 return fmt.Errorf("%w: failure reason %d", errCompositorFailedCapture, f.reason)
338}
339
340// complete hands out a frame the compositor finished.
341func (s *Source) complete(c *session, g bufGeom, f *captureFrame) (ports.Frame, error) {
342 transform := f.transform
343 idx := c.inflightBuf
344 s.destroyFrame(c)
345 if transform != 0 {
346 return ports.Frame{}, fmt.Errorf("%w: output transform %d is not supported", ports.ErrUnsupportedTransform, transform)
347 }
348 c.haveLast, c.cur, c.lastGeom = true, idx, g
349 c.served++
350 c.fails = 0
351 frame, err := s.frameOf(idx, g)
352 // From the second frame on the next capture starts right away, so the
353 // compositor can answer during the wait for the next slot. A one-shot
354 // screenshot never reaches it and allocates a single buffer.
355 if err == nil && c.served > 1 {
356 err = s.startFrame(c, g)
357 }
358 return frame, err
359}
360
361// lastFrame repeats the last completed frame, still held in its own buffer.
362func (s *Source) lastFrame(c *session) (ports.Frame, error) {
363 return s.frameOf(c.cur, c.lastGeom)
364}
365
366// startFrame sends one capture into the buffer the last frame is not in.
367func (s *Source) startFrame(c *session, g bufGeom) error {
368 idx := 0
369 if c.haveLast {
370 idx = 1 - c.cur
371 }
372 if err := s.ensureBuffer(idx, g); err != nil {
373 return err
374 }
375 f, err := newCaptureFrame(c)
376 if err != nil {
377 s.terminate()
378 return fmt.Errorf("wayland: create frame: %w", err)
379 }
380 c.inflight, c.inflightBuf, c.inflightGeom = f, idx, g
381 if err := f.proxy.AttachBuffer(s.bufs[idx].buffer); err != nil {
382 s.terminate()
383 return fmt.Errorf("wayland: attach buffer: %w", err)
384 }
385 if err := f.proxy.DamageBuffer(0, 0, int32(g.width), int32(g.height)); err != nil {
386 s.terminate()
387 return fmt.Errorf("wayland: damage buffer: %w", err)
388 }
389 if err := f.proxy.Capture(); err != nil {
390 s.terminate()
391 return fmt.Errorf("wayland: capture request: %w", err)
392 }
393 return nil
394}
395
396// destroyFrame destroys the in-flight frame object once it ended.
397func (s *Source) destroyFrame(c *session) {
398 if f := c.inflight; f != nil {
399 c.inflight = nil
400 _ = f.proxy.Destroy()
401 }
402}
403
404// stoppedError classifies a stopped session. An output that went away is the
405// usual cause: read its removal first. A workspace session stops when its
406// workspace is removed: that is a plain stop, whatever the session served. Any
407// other session that stopped before serving a frame was refused by the
408// compositor, which the error says.
409func (s *Source) stoppedError(c *session, out *output) error {
410 if err := s.roundtrip(); err != nil {
411 return err
412 }
413 switch {
414 case out.removed:
415 return ports.ErrOutputNotFound
416 case c.key.workspace != 0 && s.ws.byID[c.key.workspace] == nil:
417 return fmt.Errorf("%w: %w", ports.ErrWorkspaceUnavailable, ports.ErrCaptureStopped)
418 case c.served == 0:
419 return errCaptureRefused
420 }
421 return ports.ErrCaptureStopped
422}
423
424// geometry validates the session's current constraints and returns the buffer
425// layout to allocate.
426func (s *Source) geometry(c *session) (bufGeom, error) {
427 k := c.current
428 switch {
429 case !k.sawShm:
430 return bufGeom{}, fmt.Errorf("%w: compositor offers no wl_shm buffer", ports.ErrUnsupportedFormat)
431 case !k.haveFormat:
432 return bufGeom{}, fmt.Errorf("%w: compositor offers shm format %#x", ports.ErrUnsupportedFormat, k.unsupported)
433 case !k.haveSize:
434 return bufGeom{}, errors.New("wayland: compositor sent no buffer size")
435 }
436 g := bufGeom{format: k.format, width: k.width, height: k.height, stride: k.width * ports.BytesPerPixel}
437 if err := g.validate(); err != nil {
438 return bufGeom{}, err
439 }
440 return g, nil
441}
442
443func (s *Source) frameOf(idx int, g bufGeom) (ports.Frame, error) {
444 frame := ports.Frame{
445 Pixels: s.bufs[idx].data[:g.size:g.size],
446 Width: int(g.width), Height: int(g.height), Stride: int(g.stride),
447 Format: ports.PixelFormat(g.format),
448 }
449 if err := frame.Validate(); err != nil {
450 return ports.Frame{}, err
451 }
452 return frame, nil
453}
454
455// logicalSize is the output's size in logical pixels: xdg-output when the
456// compositor offers it, else the current mode divided by the integer scale.
457func (o *output) logicalSize() (w, h int) {
458 if o.lw > 0 && o.lh > 0 {
459 return int(o.lw), int(o.lh)
460 }
461 return int(o.width) / int(max(o.scale, 1)), int(o.height) / int(max(o.scale, 1))
462}
463
464// cropFrame returns the window of f that covers region r, given in logical
465// pixels of out. The result borrows f's storage: no pixel is copied.
466func cropFrame(f ports.Frame, r ports.Region, out *output) (ports.Frame, error) {
467 lw, lh := out.logicalSize()
468 if lw < 1 || lh < 1 {
469 return ports.Frame{}, fmt.Errorf("%w: output %s has no known logical size", ports.ErrInvalidRegion, outputName(out))
470 }
471 edge := func(v, buf, logical int) int { // round to the nearest buffer pixel
472 return int((int64(v)*int64(buf) + int64(logical)/2) / int64(logical))
473 }
474 x0, x1 := edge(r.X, f.Width, lw), min(edge(r.X+r.Width, f.Width, lw), f.Width)
475 y0, y1 := edge(r.Y, f.Height, lh), min(edge(r.Y+r.Height, f.Height, lh), f.Height)
476 if x0 >= x1 || y0 >= y1 {
477 return ports.Frame{}, fmt.Errorf("%w: %+v is outside output %s", ports.ErrInvalidRegion, r, outputName(out))
478 }
479 w, h := x1-x0, y1-y0
480 start := y0*f.Stride + x0*ports.BytesPerPixel
481 end := start + (h-1)*f.Stride + w*ports.BytesPerPixel
482 cropped := f
483 cropped.Pixels = f.Pixels[start:end:end]
484 cropped.Width, cropped.Height = w, h
485 if err := cropped.Validate(); err != nil {
486 return ports.Frame{}, err
487 }
488 return cropped, nil
489}