1package wayland
2
3import (
4 "errors"
5 "image"
6 "math"
7 "os"
8 "time"
9
10 "github.com/bnema/go-wayland-bindings/server/wayland"
11 wlr "github.com/bnema/go-wayland-bindings/server/wlrscreencopy"
12 "github.com/bnema/neferwl/internal/ports"
13 "github.com/bnema/purego-libwayland/server"
14 "golang.org/x/sys/unix"
15)
16
17// maxCaptureInflight caps accepted captures that the backend has not completed
18// yet, across all clients. Destroying a client frame or resource does not
19// release its slot: only the backend completion does.
20const maxCaptureInflight = ports.MaxCaptureInflight
21
22// The renderer's scene and wl_output modes are already in physical orientation;
23// this compositor does not advertise rotated wl_output transforms.
24func captureRegion(o *output, logical image.Rectangle) image.Rectangle {
25 if o == nil {
26 return image.Rectangle{}
27 }
28 scale := o.place.Scale
29 if scale <= 0 {
30 scale = 1
31 }
32 p := image.Rect(int(math.Floor(float64(logical.Min.X)*scale)), int(math.Floor(float64(logical.Min.Y)*scale)), int(math.Ceil(float64(logical.Max.X)*scale)), int(math.Ceil(float64(logical.Max.Y)*scale)))
33 return p.Intersect(image.Rect(0, 0, o.place.Info.Width, o.place.Info.Height))
34}
35func captureTime(t time.Time) (uint32, uint32, uint32) {
36 sec := uint64(t.Unix())
37 return uint32(sec >> 32), uint32(sec), uint32(t.Nanosecond())
38}
39func registerCapture(d *server.Display, s *Server) error {
40 if err := wlr.NewZwlrScreencopyManagerV1Global(d, 3, func(c server.Client, v, id uint32) {
41 _, _ = wlr.NewZwlrScreencopyManagerV1(c, int32(v), id, screencopyManager{s})
42 }); err != nil {
43 return err
44 }
45 return registerImageCopy(d, s)
46}
47
48type screencopyManager struct{ s *Server }
49
50func (screencopyManager) Destroy(*wlr.ZwlrScreencopyManagerV1) {}
51func (m screencopyManager) CaptureOutput(r *wlr.ZwlrScreencopyManagerV1, id uint32, cursor int32, out *wayland.Output) {
52 o := m.s.outputOf(out)
53 if o == nil {
54 m.create(r, id, cursor, nil, image.Rectangle{})
55 return
56 }
57 m.create(r, id, cursor, o, image.Rect(0, 0, o.place.Info.Width, o.place.Info.Height))
58}
59func (m screencopyManager) CaptureOutputRegion(r *wlr.ZwlrScreencopyManagerV1, id uint32, cursor int32, out *wayland.Output, x, y, w, h int32) {
60 o := m.s.outputOf(out)
61 if w <= 0 || h <= 0 {
62 m.create(r, id, cursor, o, image.Rectangle{})
63 return
64 }
65 m.create(r, id, cursor, o, captureRegion(o, image.Rect(int(x), int(y), int64Clamp(int64(x)+int64(w)), int64Clamp(int64(y)+int64(h)))))
66}
67func int64Clamp(x int64) int {
68 if x > int64(^uint(0)>>1) {
69 return int(^uint(0) >> 1)
70 }
71 return int(x)
72}
73func (m screencopyManager) create(r *wlr.ZwlrScreencopyManagerV1, id uint32, cursor int32, o *output, rect image.Rectangle) {
74 state := &screencopyFrame{s: m.s, o: o, region: rect, cursor: cursor != 0}
75 f, err := wlr.NewZwlrScreencopyFrameV1(r.Client(), r.Version(), id, state)
76 if err != nil {
77 return
78 }
79 state.res = f
80 if rect.Empty() {
81 f.SendFailed()
82 state.used = true
83 return
84 }
85 f.SendBuffer(uint32(wayland.ShmFormatXrgb8888), uint32(rect.Dx()), uint32(rect.Dy()), uint32(rect.Dx()*4))
86 if f.Version() >= 3 {
87 f.SendBufferDone()
88 }
89}
90
91type screencopyFrame struct {
92 s *Server
93 res *wlr.ZwlrScreencopyFrameV1
94 o *output
95 region image.Rectangle
96 cursor, used bool
97 replyID uint64
98}
99
100func (f *screencopyFrame) Destroy(*wlr.ZwlrScreencopyFrameV1) { delete(f.s.captureReplies, f.replyID) }
101func (f *screencopyFrame) Copy(_ *wlr.ZwlrScreencopyFrameV1, b *wayland.Buffer) { f.copy(b, false) }
102func (f *screencopyFrame) CopyWithDamage(_ *wlr.ZwlrScreencopyFrameV1, b *wayland.Buffer) {
103 f.copy(b, true)
104}
105func (f *screencopyFrame) copy(b *wayland.Buffer, damage bool) {
106 if f.used {
107 f.res.PostError(uint32(wlr.ZwlrScreencopyFrameV1ErrorAlreadyUsed), "frame already used")
108 return
109 }
110 f.used = true
111 if f.o == nil || f.region.Empty() || f.s.outputByNameExact(f.o.name()) != f.o {
112 f.res.SendFailed()
113 return
114 }
115 if !f.s.validCaptureBuffer(b, f.region, uint32(wayland.ShmFormatXrgb8888)) {
116 f.res.PostError(uint32(wlr.ZwlrScreencopyFrameV1ErrorInvalidBuffer), "invalid shm buffer")
117 return
118 }
119 id, ok := f.s.requestCapture(f.o, f.region, f.cursor, b, uint32(wayland.ShmFormatXrgb8888), func(done ports.CaptureDone) {
120 if done.Err != nil {
121 f.res.SendFailed()
122 return
123 }
124 f.res.SendFlags(0)
125 if damage && f.res.Version() >= 2 {
126 f.res.SendDamage(0, 0, uint32(f.region.Dx()), uint32(f.region.Dy()))
127 }
128 hi, lo, ns := captureTime(done.Time)
129 f.res.SendReady(hi, lo, ns)
130 }, f.res.Resource, captureTag{taken: ports.CaptureFrameTaken{Output: f.o.name(), Region: logicalRegion(f.o, f.region)}})
131 f.replyID = id
132 if !ok {
133 f.res.SendFailed()
134 }
135}
136
137// validCaptureBuffer checks the advertised shm constraints before any request is queued.
138func (s *Server) validCaptureBuffer(b *wayland.Buffer, rect image.Rectangle, format uint32) bool {
139 if b == nil {
140 return false
141 }
142 buf, ok := s.buffers[b.Resource].(*buffer)
143 return ok && (format == uint32(wayland.ShmFormatArgb8888) || format == uint32(wayland.ShmFormatXrgb8888)) && buf.width == rect.Dx() && buf.height == rect.Dy() && buf.stride >= rect.Dx()*4 && buf.format == format && fileHolds(buf.pool.file, int64(buf.offset)+int64(buf.height-1)*int64(buf.stride)+int64(buf.width)*4)
144}
145
146// logicalRegion is a physical region of an output as the logical rectangle
147// core marks: zero for the whole output, else the region's bounds rounded
148// outwards to logical pixels.
149func logicalRegion(o *output, phys image.Rectangle) ports.Rect {
150 scale := o.place.Scale
151 if scale <= 0 {
152 scale = 1
153 }
154 if phys == image.Rect(0, 0, o.place.Info.Width, o.place.Info.Height) {
155 return ports.Rect{}
156 }
157 x0, y0 := int(math.Floor(float64(phys.Min.X)/scale)), int(math.Floor(float64(phys.Min.Y)/scale))
158 x1, y1 := int(math.Ceil(float64(phys.Max.X)/scale)), int(math.Ceil(float64(phys.Max.Y)/scale))
159 return ports.Rect{X: x0, Y: y0, W: x1 - x0, H: y1 - y0}
160}
161
162// captureTag says what a capture is for the renderer: the registered session
163// (0 for wlr-screencopy), the exclusion fence and the workspace. taken is
164// what core is told of the frame (ports.CaptureFrameTaken): the compositor
165// shows an indicator for every capture, whatever the client or protocol.
166type captureTag struct {
167 session uint64
168 exclude bool
169 revision uint64
170 workspace uint64
171 window ports.WindowID
172 offscreen bool
173 taken ports.CaptureFrameTaken
174}
175
176// requestCapture duplicates the pool descriptor, so destroying the wl_buffer
177// cannot invalidate a capture in flight.
178func (s *Server) requestCapture(o *output, rect image.Rectangle, cursor bool, b *wayland.Buffer, format uint32, reply func(ports.CaptureDone), life *server.Resource, tag captureTag) (uint64, bool) {
179 if o == nil || b == nil || s.channels.Captures == nil || s.channels.Captured == nil || s.outputByNameExact(o.name()) != o {
180 return 0, false
181 }
182 if !s.validCaptureBuffer(b, rect, format) {
183 s.log.Debug().Str("output", o.name()).Stringer("region", rect).Uint32("format", format).Msg("capture buffer rejected")
184 return 0, false
185 }
186 if len(s.captureInflight) >= maxCaptureInflight {
187 s.log.Debug().Str("output", o.name()).Int("inflight", len(s.captureInflight)).Msg("capture refused: too many captures in flight")
188 return 0, false
189 }
190 // An ext frame (tag.session set) rides on the session's resolution.
191 if life == nil || (tag.session != 0 && !s.mayCaptureFrame(life.Client())) || (tag.session == 0 && !s.mayCapture(life.Client())) {
192 s.log.Debug().Str("output", o.name()).Msg("capture refused: client may not capture")
193 return 0, false
194 }
195 buf := s.buffers[b.Resource].(*buffer)
196 fd, err := unix.Dup(int(buf.pool.file.Fd()))
197 if err != nil {
198 s.log.Warn().Err(err).Str("output", o.name()).Msg("capture: dup shm fd")
199 return 0, false
200 }
201 s.nextCapture++
202 id := s.nextCapture
203 s.captureInflight[id] = struct{}{}
204 start := time.Now()
205 taken := tag.taken
206 taken.Session = tag.session
207 s.captureReplies[id] = func(done ports.CaptureDone) {
208 if done.Err == nil {
209 // Whatever became of the client's frame object, its buffer holds
210 // an image now: the indicator lasts CaptureFlash from here.
211 s.emit(taken)
212 }
213 ev := s.log.Debug()
214 if done.Err != nil && !errors.Is(done.Err, ports.ErrCaptureTransient) {
215 ev = s.log.Info()
216 }
217 ev = ev.Err(done.Err)
218 ev.Uint64("id", id).Str("output", done.Output).Dur("took", time.Since(start)).Bool("delivered", life.Alive()).Msg("capture done")
219 if life.Alive() {
220 reply(done)
221 }
222 }
223 req := ports.CaptureRequest{ID: id, Output: o.name(), Region: rect, Cursor: cursor, Dst: ports.SHMBuffer{File: os.NewFile(uintptr(fd), "capture"), Offset: buf.offset}, Width: buf.width, Height: buf.height, Stride: buf.stride, Format: buf.format, Session: tag.session, Exclude: tag.exclude, CaptureRevision: tag.revision, Workspace: tag.workspace, Window: tag.window, OffScreen: tag.offscreen, Indicate: true}
224 select {
225 case s.channels.Captures <- req:
226 s.log.Debug().Uint64("id", id).Str("output", o.name()).Stringer("region", rect).Uint32("format", format).Bool("cursor", cursor).Msg("capture requested")
227 // The indicator is on screen while the frame is rendered.
228 s.emit(taken)
229 return id, true
230 default:
231 delete(s.captureReplies, id)
232 delete(s.captureInflight, id)
233 req.Dst.File.Close()
234 s.log.Debug().Str("output", o.name()).Msg("capture refused: request queue full")
235 return 0, false
236 }
237}
238func (s *Server) forwardCaptured(ctxDone <-chan struct{}) {
239 for {
240 select {
241 case <-ctxDone:
242 return
243 case <-s.display.Stopped():
244 return
245 case done, ok := <-s.channels.Captured:
246 if !ok {
247 return
248 }
249 if !s.display.Do(func() {
250 delete(s.captureInflight, done.ID)
251 if fn := s.captureReplies[done.ID]; fn != nil {
252 delete(s.captureReplies, done.ID)
253 fn(done)
254 }
255 s.maybeLocked()
256 }) {
257 return
258 }
259 }
260 }
261}