main f2d166b

imagecopy.go RawHistoryFolder

1package wayland
2
3import (
4	"errors"
5	"image"
6	"math"
7	"time"
8
9	"github.com/bnema/go-wayland-bindings/server/extforeigntoplevellist"
10	source "github.com/bnema/go-wayland-bindings/server/extimagecapturesource"
11	ext "github.com/bnema/go-wayland-bindings/server/extimagecopycapture"
12	extworkspace "github.com/bnema/go-wayland-bindings/server/extworkspace"
13	"github.com/bnema/go-wayland-bindings/server/wayland"
14	"github.com/bnema/neferwl/internal/adapters/wayland/imagecapture"
15	"github.com/bnema/neferwl/internal/ports"
16	"github.com/bnema/purego-libwayland/server"
17)
18
19// ext-image-copy-capture with three kinds of source: an output (the standard
20// ext-image-capture-source), and the workspace and output-region sources of
21// neferwl_image_capture_source_manager_v1. Every session is registered with
22// core (ports.CaptureSessionOpen), which decides its target, tells when it
23// ends and draws the capture indicator while it lives; see ports/capture.go.
24
25type sourceKind int
26
27const (
28	srcDead      sourceKind = iota // output or workspace gone: sessions stop at once
29	srcOutput                      // a whole output
30	srcRegion                      // a logical rectangle of an output
31	srcWorkspace                   // a workspace frame
32	srcWindow                      // a toplevel, at its own size
33)
34
35// captureSource is the state behind one ext_image_capture_source_v1.
36type captureSource struct {
37	kind      sourceKind
38	output    *output
39	region    image.Rectangle // output-local logical, srcRegion
40	workspace uint64
41	window    ports.WindowID // srcWindow
42	gen       uint32         // srcWindow: its mapping; a remap kills the source
43}
44
45func registerImageCopy(d *server.Display, s *Server) error {
46	if err := source.NewExtOutputImageCaptureSourceManagerV1Global(d, 1, func(c server.Client, v, id uint32) {
47		_, _ = source.NewExtOutputImageCaptureSourceManagerV1(c, int32(v), id, outputCaptureSource{s})
48	}); err != nil {
49		return err
50	}
51	if err := imagecapture.NewNeferwlImageCaptureSourceManagerV1Global(d, 1, func(c server.Client, v, id uint32) {
52		_, _ = imagecapture.NewNeferwlImageCaptureSourceManagerV1(c, int32(v), id, extraSources{s})
53	}); err != nil {
54		return err
55	}
56	if err := source.NewExtForeignToplevelImageCaptureSourceManagerV1Global(d, 1, func(c server.Client, v, id uint32) {
57		_, _ = source.NewExtForeignToplevelImageCaptureSourceManagerV1(c, int32(v), id, toplevelCaptureSource{s})
58	}); err != nil {
59		return err
60	}
61	return ext.NewExtImageCopyCaptureManagerV1Global(d, 1, func(c server.Client, v, id uint32) {
62		_, _ = ext.NewExtImageCopyCaptureManagerV1(c, int32(v), id, copyCaptureManager{s})
63	})
64}
65
66type captureSourceHandler struct{}
67
68func (captureSourceHandler) Destroy(*source.ExtImageCaptureSourceV1) {}
69
70// newSource makes the ext_image_capture_source_v1 of a source.
71func (s *Server) newSource(c server.Client, id uint32, src *captureSource) {
72	res, err := source.NewExtImageCaptureSourceV1(c, 1, id, captureSourceHandler{})
73	if err != nil {
74		return
75	}
76	s.captureSources[res.Resource] = src
77	res.OnDestroy = func() { delete(s.captureSources, res.Resource) }
78}
79
80type outputCaptureSource struct{ s *Server }
81
82func (outputCaptureSource) Destroy(*source.ExtOutputImageCaptureSourceManagerV1) {}
83func (m outputCaptureSource) CreateSource(r *source.ExtOutputImageCaptureSourceManagerV1, id uint32, out *wayland.Output) {
84	src := &captureSource{kind: srcDead}
85	if o := m.s.outputOf(out); o != nil {
86		src = &captureSource{kind: srcOutput, output: o}
87	}
88	m.s.newSource(r.Client(), id, src)
89}
90
91// toplevelCaptureSource makes the source of an ext-foreign-toplevel-list
92// handle: that window. A closed or unknown handle gives a dead source.
93type toplevelCaptureSource struct{ s *Server }
94
95func (toplevelCaptureSource) Destroy(*source.ExtForeignToplevelImageCaptureSourceManagerV1) {}
96func (m toplevelCaptureSource) CreateSource(r *source.ExtForeignToplevelImageCaptureSourceManagerV1, id uint32, h *extforeigntoplevellist.ExtForeignToplevelHandleV1) {
97	src := &captureSource{kind: srcDead}
98	if h != nil {
99		if wid, gen, ok := m.s.extHandleWindow(h.Resource); ok {
100			src = &captureSource{kind: srcWindow, window: wid, gen: gen}
101		}
102	}
103	m.s.newSource(r.Client(), id, src)
104}
105
106// windowCaptureGone stops the sessions of a window that unmapped or closed.
107func (s *Server) windowCaptureGone(id ports.WindowID) {
108	for c := range s.captureSessions {
109		if c.src != nil && c.src.kind == srcWindow && c.src.window == id {
110			if c.id != 0 {
111				s.log.Info().Uint64("session", c.id).Str("reason", string(ports.CaptureReasonWindowGone)).Msg("capture session end")
112			}
113			c.end(true, true)
114		}
115	}
116	for _, c := range s.castSessions {
117		if c.src.kind == srcWindow && c.src.window == id {
118			s.log.Info().Uint64("cast", c.cast).Str("reason", string(ports.CaptureReasonWindowGone)).Msg("cast end")
119			c.end(ports.CaptureReasonWindowGone, nil, true, true)
120		}
121	}
122}
123
124type extraSources struct{ s *Server }
125
126func (extraSources) Destroy(*imagecapture.NeferwlImageCaptureSourceManagerV1) {}
127func (m extraSources) CreateWorkspaceSource(r *imagecapture.NeferwlImageCaptureSourceManagerV1, id uint32, ws *extworkspace.ExtWorkspaceHandleV1) {
128	src := &captureSource{kind: srcDead}
129	if ws != nil {
130		if wid, ok := m.s.workspaceIDOf(ws.Resource); ok {
131			src = &captureSource{kind: srcWorkspace, workspace: wid}
132		}
133	}
134	m.s.newSource(r.Client(), id, src)
135}
136func (m extraSources) CreateOutputRegionSource(r *imagecapture.NeferwlImageCaptureSourceManagerV1, id uint32, out *wayland.Output, x, y, w, h int32) {
137	if w <= 0 || h <= 0 {
138		r.PostError(uint32(imagecapture.NeferwlImageCaptureSourceManagerV1ErrorInvalidRegion), "width or height is not positive")
139		return
140	}
141	src := &captureSource{kind: srcDead}
142	if o := m.s.outputOf(out); o != nil {
143		src = &captureSource{kind: srcRegion, output: o, region: image.Rect(int(x), int(y), int64Clamp(int64(x)+int64(w)), int64Clamp(int64(y)+int64(h)))}
144	}
145	m.s.newSource(r.Client(), id, src)
146}
147
148func (m extraSources) GetWorkspaceFrame(r *imagecapture.NeferwlImageCaptureSourceManagerV1, id uint32, ws *extworkspace.ExtWorkspaceHandleV1) {
149	m.s.newWorkspaceFrame(r.Client(), id, ws)
150}
151
152// workspaceIDOf is the workspace behind a handle of any ext-workspace manager.
153func (s *Server) workspaceIDOf(res *server.Resource) (uint64, bool) {
154	for _, m := range s.workspaceManagers {
155		for id, h := range m.handles {
156			if h.res.Resource == res {
157				return id, true
158			}
159		}
160	}
161	return 0, false
162}
163
164// workspaceInfo finds a workspace of the last snapshot core sent.
165func (s *Server) workspaceInfo(id uint64) (output string, w ports.WorkspaceInfo, ok bool) {
166	for _, out := range s.workspaceSnapshot.Outputs {
167		for _, w := range out.Workspaces {
168			if w.ID == id {
169				return out.Name, w, true
170			}
171		}
172	}
173	return "", ports.WorkspaceInfo{}, false
174}
175
176type copyCaptureManager struct{ s *Server }
177
178func (copyCaptureManager) Destroy(*ext.ExtImageCopyCaptureManagerV1) {}
179func (m copyCaptureManager) CreateSession(r *ext.ExtImageCopyCaptureManagerV1, id uint32, src *source.ExtImageCaptureSourceV1, options uint32) {
180	s := m.s
181	if options&^1 != 0 {
182		r.PostError(uint32(ext.ExtImageCopyCaptureManagerV1ErrorInvalidOption), "invalid capture option")
183		return
184	}
185	var cs *captureSource
186	if src != nil {
187		cs = s.captureSources[src.Resource]
188	}
189	state := &captureSession{captureTarget: captureTarget{s: s, src: cs}, cursor: options&1 != 0}
190	res, err := ext.NewExtImageCopyCaptureSessionV1(r.Client(), 1, id, state)
191	if err != nil {
192		return
193	}
194	state.res = res
195	s.captureSessions[state] = struct{}{}
196	// Frames outlive their session (the spec): each frame drops its own reply.
197	res.OnDestroy = func() { state.end(false, true) }
198	if cs == nil || cs.kind == srcDead || !s.mayCapture(r.Client()) {
199		state.end(true, false)
200		return
201	}
202	if _, _, _, ok := state.geometry(); !ok {
203		state.end(true, false)
204		return
205	}
206	s.nextSession++
207	state.id = s.nextSession
208	s.sessionsByID[state.id] = state
209	open := ports.CaptureSessionOpen{ID: state.id}
210	switch cs.kind {
211	case srcOutput:
212		open.Output = cs.output.name()
213	case srcRegion:
214		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()}
215	case srcWorkspace:
216		open.Workspace = cs.workspace
217	case srcWindow:
218		open.Window = cs.window
219	}
220	s.emit(open)
221	state.sendConstraints()
222}
223
224func (m copyCaptureManager) CreatePointerCursorSession(r *ext.ExtImageCopyCaptureManagerV1, id uint32, _ *source.ExtImageCaptureSourceV1, _ *wayland.Pointer) {
225	state := &cursorCaptureSession{}
226	res, err := ext.NewExtImageCopyCaptureCursorSessionV1(r.Client(), 1, id, state)
227	if err == nil {
228		state.res = res
229	}
230}
231
232type cursorCaptureSession struct {
233	res  *ext.ExtImageCopyCaptureCursorSessionV1
234	used bool
235}
236
237func (*cursorCaptureSession) Destroy(*ext.ExtImageCopyCaptureCursorSessionV1) {}
238func (c *cursorCaptureSession) GetCaptureSession(_ *ext.ExtImageCopyCaptureCursorSessionV1, id uint32) {
239	if c.used {
240		c.res.PostError(uint32(ext.ExtImageCopyCaptureCursorSessionV1ErrorDuplicateSession), "session already created")
241		return
242	}
243	c.used = true
244	state := &captureSession{stopped: true}
245	res, err := ext.NewExtImageCopyCaptureSessionV1(c.res.Client(), 1, id, state)
246	if err == nil {
247		state.res = res
248		res.SendStopped()
249	}
250}
251
252// captureTarget is what every registered capture follows, whatever consumes
253// its frames (an ext_image_copy_capture_session_v1 or a screencast cast): the
254// source, the id core knows it by and core's last decision about it.
255type captureTarget struct {
256	s   *Server
257	src *captureSource
258	id  uint64 // registered with core; 0 when never
259	// st is the last decision of core; haveState tells there is one.
260	st        ports.CaptureSessionState
261	haveState bool
262}
263
264// captureSession is one ext_image_copy_capture_session_v1.
265type captureSession struct {
266	captureTarget
267	res             *ext.ExtImageCopyCaptureSessionV1
268	cursor, stopped bool
269	frame           *captureExtFrame
270	sent            image.Point // buffer size of the last constraints
271	excl            *exclusion  // the live exclusion object of this session
272	// exclSeen: the last state of core knew an exclusion. Frames of an
273	// excluded session leave the HUD out while it is (see tag).
274	exclSeen bool
275}
276
277func (*captureSession) Destroy(*ext.ExtImageCopyCaptureSessionV1) {}
278
279// end stops the session once. send tells the client (stopped), notify core
280// (which has to forget it unless it decided the end itself).
281func (c *captureSession) end(send, notify bool) {
282	if c.stopped {
283		return
284	}
285	c.stopped = true
286	if send && c.res.Alive() {
287		c.res.SendStopped()
288	}
289	if c.excl != nil {
290		c.excl.end(false)
291	}
292	if c.id != 0 {
293		delete(c.s.sessionsByID, c.id)
294		if notify {
295			c.s.emit(ports.CaptureSessionClose{ID: c.id})
296		}
297	}
298	delete(c.s.captureSessions, c)
299}
300
301// geometry locates the session's frames now: the output, the physical region
302// of the output they cover (for a workspace that is not on screen, the whole
303// image of its off-screen render, from the origin) and whether the workspace
304// is off screen. Core's last state decides where a workspace is; before it,
305// the last workspace snapshot does.
306func (c *captureTarget) geometry() (o *output, rect image.Rectangle, hidden, ok bool) {
307	src := c.src
308	if src == nil {
309		return nil, image.Rectangle{}, false, false
310	}
311	switch src.kind {
312	case srcOutput:
313		o = src.output
314		if o == nil || c.s.outputByNameExact(o.name()) != o {
315			return nil, image.Rectangle{}, false, false
316		}
317		return o, image.Rect(0, 0, o.place.Info.Width, o.place.Info.Height), false, true
318	case srcRegion:
319		o = src.output
320		if o == nil || c.s.outputByNameExact(o.name()) != o {
321			return nil, image.Rectangle{}, false, false
322		}
323		rect = captureRegion(o, src.region)
324		return o, rect, false, !rect.Empty()
325	case srcWorkspace:
326		var name string
327		var frame ports.Rect
328		if c.haveState && c.st.Reason == ports.CaptureReasonNone {
329			name, frame, hidden = c.st.Output, c.st.Rect, c.st.Hidden
330		} else {
331			out, w, found := c.s.workspaceInfo(src.workspace)
332			if !found {
333				return nil, image.Rectangle{}, false, false
334			}
335			name, frame, hidden = out, w.Frame, !w.Active
336		}
337		o = c.s.outputByNameExact(name)
338		if o == nil || frame.W <= 0 || frame.H <= 0 {
339			return nil, image.Rectangle{}, false, false
340		}
341		if hidden {
342			scale := o.place.Scale
343			if scale <= 0 {
344				scale = 1
345			}
346			// The size of the off-screen child image (capture.childSize).
347			return o, image.Rect(0, 0, int(math.Ceil(float64(frame.W)*scale)), int(math.Ceil(float64(frame.H)*scale))), true, true
348		}
349		rect = captureRegion(o, image.Rect(frame.X, frame.Y, frame.X+frame.W, frame.Y+frame.H))
350		return o, rect, false, !rect.Empty()
351	case srcWindow:
352		// Core's state gives the size; before it, the last configure.
353		w := c.s.windows[src.window]
354		if w == nil || !w.mapped || w.toplevel == nil || w.gen != src.gen {
355			return nil, image.Rectangle{}, false, false
356		}
357		name, size := w.last.Output, ports.Rect{W: w.last.Width, H: w.last.Height}
358		if c.haveState && c.st.Reason == ports.CaptureReasonNone {
359			name, size = c.st.Output, c.st.Rect
360		}
361		if size.W <= 0 || size.H <= 0 {
362			if gw, gh := w.size(); gw > 0 && gh > 0 {
363				size = ports.Rect{W: gw, H: gh}
364			}
365		}
366		o = c.s.outputByNameExact(name)
367		if o == nil || size.W <= 0 || size.H <= 0 {
368			return nil, image.Rectangle{}, false, false
369		}
370		scale := o.place.Scale
371		if scale <= 0 {
372			scale = 1
373		}
374		// The size of the off-screen child image (capture.childSize).
375		return o, image.Rect(0, 0, int(math.Ceil(float64(size.W)*scale)), int(math.Ceil(float64(size.H)*scale))), true, true
376	}
377	return nil, image.Rectangle{}, false, false
378}
379
380// sendConstraints sends one complete constraint batch: size, formats, done.
381func (c *captureSession) sendConstraints() {
382	_, rect, _, ok := c.geometry()
383	if !ok || !c.res.Alive() || c.s.protected() {
384		return
385	}
386	c.sent = image.Pt(rect.Dx(), rect.Dy())
387	c.res.SendBufferSize(uint32(rect.Dx()), uint32(rect.Dy()))
388	c.res.SendShmFormat(uint32(wayland.ShmFormatXrgb8888))
389	c.res.SendShmFormat(uint32(wayland.ShmFormatArgb8888))
390	c.res.SendDone()
391}
392
393// targetFate says what became of a followed target.
394type targetFate uint8
395
396const (
397	targetLive          targetFate = iota
398	targetWorkspaceGone            // the workspace left the snapshot
399	targetEmpty                    // an output or region with nothing left
400)
401
402// follow looks at the target now: its fate and, while it lives, its geometry
403// (rect, ok as geometry gives them). A window or workspace that is not
404// locatable for the moment stays live: core's next state decides.
405func (c *captureTarget) follow() (fate targetFate, rect image.Rectangle, ok bool) {
406	if c.src.kind == srcWorkspace {
407		// The workspace left the snapshot: its handle is removed, stop at once
408		// without waiting for core's state.
409		if _, _, found := c.s.workspaceInfo(c.src.workspace); !found {
410			return targetWorkspaceGone, image.Rectangle{}, false
411		}
412	}
413	_, rect, _, ok = c.geometry()
414	if !ok && c.src.kind != srcWorkspace && c.src.kind != srcWindow {
415		return targetEmpty, rect, false
416	}
417	return targetLive, rect, ok
418}
419
420// applyState takes what core decided; it reports a decision that ends the
421// target (a terminal reason).
422func (c *captureTarget) applyState(st ports.CaptureSessionState) (ended bool) {
423	if st.Reason.Terminal() {
424		c.s.log.Info().Uint64("session", c.id).Str("reason", string(st.Reason)).Msg("capture session end")
425		return true
426	}
427	c.st, c.haveState = st, true
428	c.s.log.Debug().Uint64("session", c.id).Bool("active", st.Active).Bool("hidden", st.Hidden).Uint64("revision", st.Revision).Msg("capture session state")
429	return false
430}
431
432// refresh follows the target: new constraints when the buffer size changed,
433// the end when nothing is left of it.
434func (c *captureSession) refresh() {
435	if c.stopped || c.id == 0 {
436		return
437	}
438	fate, rect, ok := c.follow()
439	switch {
440	case fate == targetWorkspaceGone:
441		c.s.log.Info().Uint64("session", c.id).Str("reason", string(ports.CaptureReasonWorkspaceGone)).Msg("capture session end")
442		c.end(true, true)
443	case fate == targetEmpty:
444		c.s.log.Info().Uint64("session", c.id).Msg("capture session end: target empty")
445		c.end(true, true)
446	case ok && image.Pt(rect.Dx(), rect.Dy()) != c.sent:
447		c.sendConstraints()
448	}
449}
450
451// refreshSessions runs when outputs, scales or workspaces changed.
452func (s *Server) refreshSessions() {
453	for c := range s.captureSessions {
454		c.refresh()
455	}
456	for _, c := range s.castSessions {
457		c.refresh()
458	}
459}
460
461// endCaptureSessions stops every session, and its exclusion with it, when
462// the session gets protected: a capture session never outlives the lock, and
463// its client must ask for a new one after the unlock.
464func (s *Server) endCaptureSessions() {
465	for c := range s.captureSessions {
466		if c.id != 0 {
467			s.log.Info().Uint64("session", c.id).Str("reason", "session-protected").Msg("capture session end")
468		}
469		c.end(true, true)
470	}
471	for _, c := range s.castSessions {
472		s.log.Info().Uint64("cast", c.cast).Str("reason", "session-protected").Msg("cast end")
473		c.end(ports.CaptureReasonNone, errCastProtected, true, true)
474	}
475	s.castInventoryChanged()
476}
477
478// outputGone ends the sessions of an unplugged output without waiting for
479// core. Workspace sessions follow their workspace to its new output.
480func (s *Server) outputGone(o *output) {
481	for c := range s.captureSessions {
482		if c.src != nil && c.src.output == o && (c.src.kind == srcOutput || c.src.kind == srcRegion) {
483			s.log.Info().Uint64("session", c.id).Str("reason", string(ports.CaptureReasonOutputGone)).Msg("capture session end")
484			c.end(true, true)
485		}
486	}
487	for _, c := range s.castSessions {
488		if c.src.kind == srcOutput && c.src.output == o {
489			s.log.Info().Uint64("cast", c.cast).Str("reason", string(ports.CaptureReasonOutputGone)).Msg("cast end")
490			c.end(ports.CaptureReasonOutputGone, nil, true, true)
491		}
492	}
493}
494
495// captureState applies what core decided about a session.
496func (s *Server) captureState(st ports.CaptureSessionState) {
497	if c := s.castByCore[st.ID]; c != nil {
498		c.state(st)
499		return
500	}
501	c := s.sessionsByID[st.ID]
502	if c == nil {
503		return
504	}
505	if c.applyState(st) {
506		c.end(true, false)
507		return
508	}
509	c.exclSeen = st.Exclusion
510	if ex := c.excl; ex != nil {
511		ex.confirm(st.Layers)
512	}
513	c.refresh()
514}
515
516// tag says how a frame of this session is served. A session with an
517// exclusion gets frames without its HUD, fenced by the revision core last
518// confirmed; until core knows the exclusion, and while a finished one is
519// still in the last state, a frame is not served rather than shown with the
520// HUD in it (ok false).
521func (c *captureSession) tag(hidden bool) (t captureTag, ok bool) {
522	t = c.targetTag(hidden)
523	if c.excl != nil && !c.exclSeen {
524		return t, false
525	}
526	if c.exclSeen {
527		t.exclude, t.revision = true, c.st.Revision
528	}
529	return t, true
530}
531
532// targetTag is the part of a frame's tag the target decides: the session,
533// what is captured and what core is told of it.
534func (c *captureTarget) targetTag(hidden bool) (t captureTag) {
535	t = captureTag{session: c.id}
536	switch c.src.kind {
537	case srcWorkspace:
538		t.workspace, t.offscreen = c.src.workspace, hidden
539	case srcWindow:
540		t.window, t.offscreen = c.src.window, true
541	}
542	switch c.src.kind {
543	case srcOutput:
544		t.taken = ports.CaptureFrameTaken{Output: c.src.output.name()}
545	case srcRegion:
546		r := c.src.region
547		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()}}
548	case srcWorkspace:
549		t.taken = ports.CaptureFrameTaken{Workspace: c.src.workspace}
550	case srcWindow:
551		t.taken = ports.CaptureFrameTaken{Window: c.src.window}
552	}
553	return t
554}
555
556func (c *captureSession) CreateFrame(r *ext.ExtImageCopyCaptureSessionV1, id uint32) {
557	if c.frame != nil {
558		r.PostError(uint32(ext.ExtImageCopyCaptureSessionV1ErrorDuplicateFrame), "frame still alive")
559		return
560	}
561	state := &captureExtFrame{session: c}
562	res, err := ext.NewExtImageCopyCaptureFrameV1(r.Client(), 1, id, state)
563	if err == nil {
564		state.res = res
565		c.frame = state
566		res.OnDestroy = func() {
567			if c.frame == state {
568				c.frame = nil
569			}
570			if c.s != nil {
571				delete(c.s.captureReplies, state.replyID)
572			}
573		}
574	}
575}
576
577type captureExtFrame struct {
578	session *captureSession
579	res     *ext.ExtImageCopyCaptureFrameV1
580	buf     *wayland.Buffer
581	used    bool
582	replyID uint64
583	// deadline ends the retries of a frame whose target is not ready yet.
584	deadline time.Time
585}
586
587// A frame whose target is briefly unavailable (not active yet, rendered off
588// screen by another session, its indicator or exclusion not on screen yet,
589// a renderer slot busy) is tried again rather than failed: failed(unknown)
590// ends the whole screencast in screen-sharing clients. Only
591// a stopped session, a refused client or a wrong buffer fail at once.
592const (
593	captureRetryEvery = 16 * time.Millisecond
594	captureRetryFor   = time.Second
595)
596
597func (*captureExtFrame) Destroy(*ext.ExtImageCopyCaptureFrameV1) {}
598func (f *captureExtFrame) AttachBuffer(_ *ext.ExtImageCopyCaptureFrameV1, b *wayland.Buffer) {
599	if f.used {
600		f.res.PostError(uint32(ext.ExtImageCopyCaptureFrameV1ErrorAlreadyCaptured), "already captured")
601		return
602	}
603	f.buf = b
604}
605func (f *captureExtFrame) DamageBuffer(_ *ext.ExtImageCopyCaptureFrameV1, x, y, w, h int32) {
606	if f.used {
607		f.res.PostError(uint32(ext.ExtImageCopyCaptureFrameV1ErrorAlreadyCaptured), "already captured")
608		return
609	}
610	if x < 0 || y < 0 || w <= 0 || h <= 0 {
611		f.res.PostError(uint32(ext.ExtImageCopyCaptureFrameV1ErrorInvalidBufferDamage), "invalid damage")
612	}
613}
614
615// constraintsFailed fails the frame with buffer_constraints. The buffer was
616// checked against the geometry computed now; when that differs from the last
617// constraints the client got (the target moved or rescaled and the refresh has
618// not run yet), a new batch goes first so the client can retry with the size
619// the compositor expects.
620func (f *captureExtFrame) constraintsFailed(region image.Rectangle) {
621	if c := f.session; image.Pt(region.Dx(), region.Dy()) != c.sent {
622		c.sendConstraints()
623	}
624	f.res.SendFailed(uint32(ext.ExtImageCopyCaptureFrameV1FailureReasonBufferConstraints))
625}
626
627func (f *captureExtFrame) Capture(*ext.ExtImageCopyCaptureFrameV1) {
628	if f.used {
629		f.res.PostError(uint32(ext.ExtImageCopyCaptureFrameV1ErrorAlreadyCaptured), "already captured")
630		return
631	}
632	f.used = true
633	c := f.session
634	if c.stopped || c.src == nil || !c.s.mayCaptureFrame(f.res.Client()) {
635		f.res.SendFailed(uint32(ext.ExtImageCopyCaptureFrameV1FailureReasonStopped))
636		return
637	}
638	if f.buf == nil {
639		f.res.PostError(uint32(ext.ExtImageCopyCaptureFrameV1ErrorNoBuffer), "no buffer attached")
640		return
641	}
642	f.deadline = c.s.clock.Now().Add(captureRetryFor)
643	f.attempt()
644}
645
646// retry tries the frame again shortly, or fails it past its deadline.
647func (f *captureExtFrame) retry(why string) {
648	s := f.session.s
649	if !s.clock.Now().Before(f.deadline) {
650		s.log.Info().Uint64("session", f.session.id).Str("reason", why).Msg("capture frame failed after retries")
651		f.res.SendFailed(uint32(ext.ExtImageCopyCaptureFrameV1FailureReasonUnknown))
652		return
653	}
654	s.clock.AfterFunc(captureRetryEvery, func() { s.display.Do(f.attempt) })
655}
656
657// attempt serves the frame from the current target. Display goroutine only.
658func (f *captureExtFrame) attempt() {
659	if !f.res.Alive() {
660		return
661	}
662	c := f.session
663	if c.stopped || !c.s.mayCaptureFrame(f.res.Client()) {
664		f.res.SendFailed(uint32(ext.ExtImageCopyCaptureFrameV1FailureReasonStopped))
665		return
666	}
667	o, region, hidden, ok := c.geometry()
668	if !ok || c.haveState && !c.st.Active {
669		f.retry("target not ready")
670		return
671	}
672	tag, ok := c.tag(hidden)
673	// On-screen frames are target pixels, which the output holds under its
674	// transform; off-screen child images are unrotated.
675	transform := uint32(0)
676	if !hidden {
677		transform = uint32(o.place.Transform)
678	}
679	buf, isBuf := c.s.buffers[f.buf.GetResource()].(*buffer)
680	switch {
681	case !isBuf:
682		f.constraintsFailed(region)
683		return
684	case !ok:
685		// The exclusion is not confirmed by core yet: nothing is served.
686		f.retry("exclusion not confirmed")
687		return
688	}
689	f.replyID, ok = c.s.requestCapture(o, region, c.cursor, f.buf, buf.format, func(done ports.CaptureDone) {
690		switch {
691		case errors.Is(done.Err, ports.ErrCaptureTransient):
692			f.retry(done.Err.Error())
693			return
694		case done.Err != nil:
695			// Retrying cannot clear it (a bad buffer, an unsupported size).
696			f.res.SendFailed(uint32(ext.ExtImageCopyCaptureFrameV1FailureReasonUnknown))
697			return
698		}
699		f.res.SendTransform(transform)
700		f.res.SendDamage(0, 0, int32(region.Dx()), int32(region.Dy()))
701		hi, lo, ns := captureTime(done.Time)
702		f.res.SendPresentationTime(hi, lo, ns)
703		f.res.SendReady()
704	}, f.res.Resource, tag)
705	if !ok {
706		switch {
707		case !c.s.validCaptureBuffer(f.buf, region, buf.format):
708			f.constraintsFailed(region)
709		case !c.s.mayCaptureFrame(f.res.Client()):
710			f.res.SendFailed(uint32(ext.ExtImageCopyCaptureFrameV1FailureReasonStopped))
711		default:
712			// The renderer queue or the in-flight bound is full.
713			f.retry("capture queue full")
714		}
715	}
716}

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 25

Tags 10

↑↓ moveenter openesc close