1// Package capture transfers each request to an output: the output closes its
2// destination fd exactly once and attempts one reply, never blocking after
3// its context is done.
4package capture
5
6import (
7 "context"
8 "errors"
9 "fmt"
10 "runtime/debug"
11 "time"
12
13 "github.com/bnema/neferwl/internal/ports"
14 "golang.org/x/sys/unix"
15)
16
17const maxMappedBytes = 1 << 30
18
19// Wayland SHM format identifiers; all capture pixels are opaque BGRA.
20const (
21 shmARGB8888 = 0
22 shmXRGB8888 = 1
23)
24
25func reply(ctx context.Context, req ports.CaptureRequest, err error, replies chan<- ports.CaptureDone) {
26 replyAt(ctx, req, err, monotonicNow(), replies)
27}
28
29func replyAt(ctx context.Context, req ports.CaptureRequest, err error, at time.Time, replies chan<- ports.CaptureDone) {
30 if req.Dst.File != nil {
31 err = errors.Join(err, req.Dst.File.Close())
32 }
33 done := ports.CaptureDone{ID: req.ID, Output: req.Output, Err: err, Time: at}
34 // Prefer a ready reply even after output cancellation: Wayland needs the
35 // completion to release admission credit while the session is still alive.
36 select {
37 case replies <- done:
38 return
39 default:
40 }
41 // A nil channel is allowed: cancellation still releases the sender.
42 select {
43 case <-ctx.Done():
44 case replies <- done:
45 }
46}
47
48// monotonicNow represents CLOCK_MONOTONIC in the Time seconds/nanoseconds fields.
49func monotonicNow() time.Time {
50 var ts unix.Timespec
51 if err := unix.ClockGettime(unix.CLOCK_MONOTONIC, &ts); err != nil {
52 return time.Time{}
53 }
54 return time.Unix(ts.Sec, ts.Nsec)
55}
56
57func Fail(ctx context.Context, req ports.CaptureRequest, err error, replies chan<- ports.CaptureDone) {
58 reply(ctx, req, err, replies)
59}
60
61func writeFrame(ctx context.Context, req ports.CaptureRequest, frame ports.CaptureFrame, at time.Time, replies chan<- ports.CaptureDone) {
62 var err error
63 var data []byte
64 // A client can truncate its file after Stat while it is still mapped.
65 // Turn SIGBUS into an error on this worker, not a compositor crash.
66 defer debug.SetPanicOnFault(debug.SetPanicOnFault(true))
67 defer func() {
68 if p := recover(); p != nil {
69 err = fmt.Errorf("capture read panic: %v\n%s", p, debug.Stack())
70 }
71 if data != nil {
72 err = errors.Join(err, unix.Munmap(data))
73 }
74 replyAt(ctx, req, err, at, replies)
75 }()
76 if req.Dst.File == nil || req.Width <= 0 || req.Width > maxMappedBytes/4 || req.Height <= 0 || req.Stride < req.Width*4 || req.Region.Dx() != req.Width || req.Region.Dy() != req.Height || req.Dst.Offset < 0 || (req.Format != shmARGB8888 && req.Format != shmXRGB8888) {
77 err = fmt.Errorf("invalid capture destination")
78 return
79 }
80 rowBytes := int64(req.Width) * 4
81 // Validate the bound before multiplication: padding must not wrap size.
82 if int64(req.Dst.Offset) > maxMappedBytes-rowBytes || (req.Height > 1 && int64(req.Stride) > (maxMappedBytes-int64(req.Dst.Offset)-rowBytes)/int64(req.Height-1)) {
83 err = fmt.Errorf("capture too large")
84 return
85 }
86 size := int64(req.Dst.Offset) + int64(req.Height-1)*int64(req.Stride) + rowBytes
87 if size <= 0 || size > int64(^uint(0)>>1) || size > maxMappedBytes {
88 err = fmt.Errorf("capture too large")
89 return
90 }
91 var st unix.Stat_t
92 if e := unix.Fstat(int(req.Dst.File.Fd()), &st); e != nil {
93 err = fmt.Errorf("fstat capture destination: %w", e)
94 return
95 }
96 if st.Size < size {
97 err = fmt.Errorf("capture buffer truncated")
98 return
99 }
100 // Map only for the duration of this capture; the renderer copies BGRA rows
101 // directly from its readback buffer into the selected shm region.
102 data, err = unix.Mmap(int(req.Dst.File.Fd()), 0, int(size), unix.PROT_WRITE, unix.MAP_SHARED)
103 if err != nil {
104 return
105 }
106 err = frame.Read(req.Region, data[req.Dst.Offset:], req.Stride)
107}