screencopy.go RawHistoryFolder

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

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 23

Tags 10

↑↓ moveenter openesc close