v0.2.0 13c6a90

imagecopy.go RawHistoryFolder

1package wayland
2
3import (
4	"image"
5	"math"
6
7	source "github.com/bnema/go-wayland-bindings/server/extimagecapturesource"
8	ext "github.com/bnema/go-wayland-bindings/server/extimagecopycapture"
9	extworkspace "github.com/bnema/go-wayland-bindings/server/extworkspace"
10	"github.com/bnema/go-wayland-bindings/server/wayland"
11	"github.com/bnema/neferwl/internal/adapters/wayland/imagecapture"
12	"github.com/bnema/neferwl/internal/ports"
13	"github.com/bnema/purego-libwayland/server"
14)
15
16// ext-image-copy-capture with three kinds of source: an output (the standard
17// ext-image-capture-source), and the workspace and output-region sources of
18// neferwl_image_capture_source_manager_v1. Every session is registered with
19// core (ports.CaptureSessionOpen), which decides its target, tells when it
20// ends and draws the capture indicator while it lives; see ports/capture.go.
21
22type sourceKind int
23
24const (
25	srcDead      sourceKind = iota // output or workspace gone: sessions stop at once
26	srcOutput                      // a whole output
27	srcRegion                      // a logical rectangle of an output
28	srcWorkspace                   // a workspace frame
29)
30
31// captureSource is the state behind one ext_image_capture_source_v1.
32type captureSource struct {
33	kind      sourceKind
34	output    *output
35	region    image.Rectangle // output-local logical, srcRegion
36	workspace uint64
37}
38
39func registerImageCopy(d *server.Display, s *Server) error {
40	if err := source.NewExtOutputImageCaptureSourceManagerV1Global(d, 1, func(c server.Client, v, id uint32) {
41		_, _ = source.NewExtOutputImageCaptureSourceManagerV1(c, int32(v), id, outputCaptureSource{s})
42	}); err != nil {
43		return err
44	}
45	if err := imagecapture.NewNeferwlImageCaptureSourceManagerV1Global(d, 1, func(c server.Client, v, id uint32) {
46		_, _ = imagecapture.NewNeferwlImageCaptureSourceManagerV1(c, int32(v), id, extraSources{s})
47	}); err != nil {
48		return err
49	}
50	return ext.NewExtImageCopyCaptureManagerV1Global(d, 1, func(c server.Client, v, id uint32) {
51		_, _ = ext.NewExtImageCopyCaptureManagerV1(c, int32(v), id, copyCaptureManager{s})
52	})
53}
54
55type captureSourceHandler struct{}
56
57func (captureSourceHandler) Destroy(*source.ExtImageCaptureSourceV1) {}
58
59// newSource makes the ext_image_capture_source_v1 of a source.
60func (s *Server) newSource(c server.Client, id uint32, src *captureSource) {
61	res, err := source.NewExtImageCaptureSourceV1(c, 1, id, captureSourceHandler{})
62	if err != nil {
63		return
64	}
65	s.captureSources[res.Resource] = src
66	res.OnDestroy = func() { delete(s.captureSources, res.Resource) }
67}
68
69type outputCaptureSource struct{ s *Server }
70
71func (outputCaptureSource) Destroy(*source.ExtOutputImageCaptureSourceManagerV1) {}
72func (m outputCaptureSource) CreateSource(r *source.ExtOutputImageCaptureSourceManagerV1, id uint32, out *wayland.Output) {
73	src := &captureSource{kind: srcDead}
74	if o := m.s.outputOf(out); o != nil {
75		src = &captureSource{kind: srcOutput, output: o}
76	}
77	m.s.newSource(r.Client(), id, src)
78}
79
80type extraSources struct{ s *Server }
81
82func (extraSources) Destroy(*imagecapture.NeferwlImageCaptureSourceManagerV1) {}
83func (m extraSources) CreateWorkspaceSource(r *imagecapture.NeferwlImageCaptureSourceManagerV1, id uint32, ws *extworkspace.ExtWorkspaceHandleV1) {
84	src := &captureSource{kind: srcDead}
85	if ws != nil {
86		if wid, ok := m.s.workspaceIDOf(ws.Resource); ok {
87			src = &captureSource{kind: srcWorkspace, workspace: wid}
88		}
89	}
90	m.s.newSource(r.Client(), id, src)
91}
92func (m extraSources) CreateOutputRegionSource(r *imagecapture.NeferwlImageCaptureSourceManagerV1, id uint32, out *wayland.Output, x, y, w, h int32) {
93	if w <= 0 || h <= 0 {
94		r.PostError(uint32(imagecapture.NeferwlImageCaptureSourceManagerV1ErrorInvalidRegion), "width or height is not positive")
95		return
96	}
97	src := &captureSource{kind: srcDead}
98	if o := m.s.outputOf(out); o != nil {
99		src = &captureSource{kind: srcRegion, output: o, region: image.Rect(int(x), int(y), int64Clamp(int64(x)+int64(w)), int64Clamp(int64(y)+int64(h)))}
100	}
101	m.s.newSource(r.Client(), id, src)
102}
103
104func (m extraSources) GetWorkspaceFrame(r *imagecapture.NeferwlImageCaptureSourceManagerV1, id uint32, ws *extworkspace.ExtWorkspaceHandleV1) {
105	m.s.newWorkspaceFrame(r.Client(), id, ws)
106}
107
108// workspaceIDOf is the workspace behind a handle of any ext-workspace manager.
109func (s *Server) workspaceIDOf(res *server.Resource) (uint64, bool) {
110	for _, m := range s.workspaceManagers {
111		for id, h := range m.handles {
112			if h.res.Resource == res {
113				return id, true
114			}
115		}
116	}
117	return 0, false
118}
119
120// workspaceInfo finds a workspace of the last snapshot core sent.
121func (s *Server) workspaceInfo(id uint64) (output string, w ports.WorkspaceInfo, ok bool) {
122	for _, out := range s.workspaceSnapshot.Outputs {
123		for _, w := range out.Workspaces {
124			if w.ID == id {
125				return out.Name, w, true
126			}
127		}
128	}
129	return "", ports.WorkspaceInfo{}, false
130}
131
132type copyCaptureManager struct{ s *Server }
133
134func (copyCaptureManager) Destroy(*ext.ExtImageCopyCaptureManagerV1) {}
135func (m copyCaptureManager) CreateSession(r *ext.ExtImageCopyCaptureManagerV1, id uint32, src *source.ExtImageCaptureSourceV1, options uint32) {
136	s := m.s
137	if options&^1 != 0 {
138		r.PostError(uint32(ext.ExtImageCopyCaptureManagerV1ErrorInvalidOption), "invalid capture option")
139		return
140	}
141	var cs *captureSource
142	if src != nil {
143		cs = s.captureSources[src.Resource]
144	}
145	state := &captureSession{s: s, src: cs, cursor: options&1 != 0}
146	res, err := ext.NewExtImageCopyCaptureSessionV1(r.Client(), 1, id, state)
147	if err != nil {
148		return
149	}
150	state.res = res
151	s.captureSessions[state] = struct{}{}
152	// Frames outlive their session (the spec): each frame drops its own reply.
153	res.OnDestroy = func() { state.end(false, true) }
154	if cs == nil || cs.kind == srcDead || !s.mayCapture(r.Client()) {
155		state.end(true, false)
156		return
157	}
158	if _, _, _, ok := state.geometry(); !ok {
159		state.end(true, false)
160		return
161	}
162	s.nextSession++
163	state.id = s.nextSession
164	s.sessionsByID[state.id] = state
165	open := ports.CaptureSessionOpen{ID: state.id}
166	switch cs.kind {
167	case srcOutput:
168		open.Output = cs.output.name()
169	case srcRegion:
170		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()}
171	case srcWorkspace:
172		open.Workspace = cs.workspace
173	}
174	s.emit(open)
175	state.sendConstraints()
176}
177
178func (m copyCaptureManager) CreatePointerCursorSession(r *ext.ExtImageCopyCaptureManagerV1, id uint32, _ *source.ExtImageCaptureSourceV1, _ *wayland.Pointer) {
179	state := &cursorCaptureSession{}
180	res, err := ext.NewExtImageCopyCaptureCursorSessionV1(r.Client(), 1, id, state)
181	if err == nil {
182		state.res = res
183	}
184}
185
186type cursorCaptureSession struct {
187	res  *ext.ExtImageCopyCaptureCursorSessionV1
188	used bool
189}
190
191func (*cursorCaptureSession) Destroy(*ext.ExtImageCopyCaptureCursorSessionV1) {}
192func (c *cursorCaptureSession) GetCaptureSession(_ *ext.ExtImageCopyCaptureCursorSessionV1, id uint32) {
193	if c.used {
194		c.res.PostError(uint32(ext.ExtImageCopyCaptureCursorSessionV1ErrorDuplicateSession), "session already created")
195		return
196	}
197	c.used = true
198	state := &captureSession{stopped: true}
199	res, err := ext.NewExtImageCopyCaptureSessionV1(c.res.Client(), 1, id, state)
200	if err == nil {
201		state.res = res
202		res.SendStopped()
203	}
204}
205
206// captureSession is one ext_image_copy_capture_session_v1.
207type captureSession struct {
208	s               *Server
209	res             *ext.ExtImageCopyCaptureSessionV1
210	src             *captureSource
211	id              uint64 // registered with core; 0 when never
212	cursor, stopped bool
213	frame           *captureExtFrame
214	sent            image.Point // buffer size of the last constraints
215	// st is the last decision of core; haveState tells there is one.
216	st        ports.CaptureSessionState
217	haveState bool
218	excl      *exclusion // the live exclusion object of this session
219	// exclSeen: the last state of core knew an exclusion. Frames of an
220	// excluded session leave the HUD out while it is (see tag).
221	exclSeen bool
222}
223
224func (*captureSession) Destroy(*ext.ExtImageCopyCaptureSessionV1) {}
225
226// end stops the session once. send tells the client (stopped), notify core
227// (which has to forget it unless it decided the end itself).
228func (c *captureSession) end(send, notify bool) {
229	if c.stopped {
230		return
231	}
232	c.stopped = true
233	if send && c.res.Alive() {
234		c.res.SendStopped()
235	}
236	if c.excl != nil {
237		c.excl.end(false)
238	}
239	if c.id != 0 {
240		delete(c.s.sessionsByID, c.id)
241		if notify {
242			c.s.emit(ports.CaptureSessionClose{ID: c.id})
243		}
244	}
245	delete(c.s.captureSessions, c)
246}
247
248// geometry locates the session's frames now: the output, the physical region
249// of the output they cover (for a workspace that is not on screen, the whole
250// image of its off-screen render, from the origin) and whether the workspace
251// is off screen. Core's last state decides where a workspace is; before it,
252// the last workspace snapshot does.
253func (c *captureSession) geometry() (o *output, rect image.Rectangle, hidden, ok bool) {
254	src := c.src
255	if src == nil {
256		return nil, image.Rectangle{}, false, false
257	}
258	switch src.kind {
259	case srcOutput:
260		o = src.output
261		if o == nil || c.s.outputByNameExact(o.name()) != o {
262			return nil, image.Rectangle{}, false, false
263		}
264		return o, image.Rect(0, 0, o.place.Info.Width, o.place.Info.Height), false, true
265	case srcRegion:
266		o = src.output
267		if o == nil || c.s.outputByNameExact(o.name()) != o {
268			return nil, image.Rectangle{}, false, false
269		}
270		rect = captureRegion(o, src.region)
271		return o, rect, false, !rect.Empty()
272	case srcWorkspace:
273		var name string
274		var frame ports.Rect
275		if c.haveState && c.st.Reason == ports.CaptureReasonNone {
276			name, frame, hidden = c.st.Output, c.st.Rect, c.st.Hidden
277		} else {
278			out, w, found := c.s.workspaceInfo(src.workspace)
279			if !found {
280				return nil, image.Rectangle{}, false, false
281			}
282			name, frame, hidden = out, w.Frame, !w.Active
283		}
284		o = c.s.outputByNameExact(name)
285		if o == nil || frame.W <= 0 || frame.H <= 0 {
286			return nil, image.Rectangle{}, false, false
287		}
288		if hidden {
289			scale := o.place.Scale
290			if scale <= 0 {
291				scale = 1
292			}
293			// The size of the off-screen child image (capture.childSize).
294			return o, image.Rect(0, 0, int(math.Ceil(float64(frame.W)*scale)), int(math.Ceil(float64(frame.H)*scale))), true, true
295		}
296		rect = captureRegion(o, image.Rect(frame.X, frame.Y, frame.X+frame.W, frame.Y+frame.H))
297		return o, rect, false, !rect.Empty()
298	}
299	return nil, image.Rectangle{}, false, false
300}
301
302// sendConstraints sends one complete constraint batch: size, formats, done.
303func (c *captureSession) sendConstraints() {
304	_, rect, _, ok := c.geometry()
305	if !ok || !c.res.Alive() || c.s.protected() {
306		return
307	}
308	c.sent = image.Pt(rect.Dx(), rect.Dy())
309	c.res.SendBufferSize(uint32(rect.Dx()), uint32(rect.Dy()))
310	c.res.SendShmFormat(uint32(wayland.ShmFormatXrgb8888))
311	c.res.SendShmFormat(uint32(wayland.ShmFormatArgb8888))
312	c.res.SendDone()
313}
314
315// refresh follows the target: new constraints when the buffer size changed,
316// the end when nothing is left of it.
317func (c *captureSession) refresh() {
318	if c.stopped || c.id == 0 {
319		return
320	}
321	if c.src.kind == srcWorkspace {
322		// The workspace left the snapshot: its handle is removed, stop at once
323		// without waiting for core's state.
324		if _, _, found := c.s.workspaceInfo(c.src.workspace); !found {
325			c.s.log.Info().Uint64("session", c.id).Str("reason", string(ports.CaptureReasonWorkspaceGone)).Msg("capture session end")
326			c.end(true, true)
327			return
328		}
329	}
330	_, rect, _, ok := c.geometry()
331	switch {
332	case !ok && c.src.kind != srcWorkspace:
333		c.s.log.Info().Uint64("session", c.id).Msg("capture session end: target empty")
334		c.end(true, true)
335	case ok && image.Pt(rect.Dx(), rect.Dy()) != c.sent:
336		c.sendConstraints()
337	}
338}
339
340// refreshSessions runs when outputs, scales or workspaces changed.
341func (s *Server) refreshSessions() {
342	for c := range s.captureSessions {
343		c.refresh()
344	}
345}
346
347// endCaptureSessions stops every session, and its exclusion with it, when
348// the session gets protected: a capture session never outlives the lock, and
349// its client must ask for a new one after the unlock.
350func (s *Server) endCaptureSessions() {
351	for c := range s.captureSessions {
352		if c.id != 0 {
353			s.log.Info().Uint64("session", c.id).Str("reason", "session-protected").Msg("capture session end")
354		}
355		c.end(true, true)
356	}
357}
358
359// outputGone ends the sessions of an unplugged output without waiting for
360// core. Workspace sessions follow their workspace to its new output.
361func (s *Server) outputGone(o *output) {
362	for c := range s.captureSessions {
363		if c.src != nil && c.src.output == o && (c.src.kind == srcOutput || c.src.kind == srcRegion) {
364			s.log.Info().Uint64("session", c.id).Str("reason", string(ports.CaptureReasonOutputGone)).Msg("capture session end")
365			c.end(true, true)
366		}
367	}
368}
369
370// captureState applies what core decided about a session.
371func (s *Server) captureState(st ports.CaptureSessionState) {
372	c := s.sessionsByID[st.ID]
373	if c == nil {
374		return
375	}
376	if st.Reason.Terminal() {
377		s.log.Info().Uint64("session", c.id).Str("reason", string(st.Reason)).Msg("capture session end")
378		c.end(true, false)
379		return
380	}
381	c.st, c.haveState, c.exclSeen = st, true, st.Exclusion
382	s.log.Debug().Uint64("session", c.id).Bool("active", st.Active).Bool("hidden", st.Hidden).Uint64("revision", st.Revision).Msg("capture session state")
383	if ex := c.excl; ex != nil {
384		ex.confirm(st.Layers)
385	}
386	c.refresh()
387}
388
389// tag says how a frame of this session is served. A session with an
390// exclusion gets frames without its HUD, fenced by the revision core last
391// confirmed; until core knows the exclusion, and while a finished one is
392// still in the last state, a frame is not served rather than shown with the
393// HUD in it (ok false).
394func (c *captureSession) tag(hidden bool) (t captureTag, ok bool) {
395	t = captureTag{session: c.id}
396	if c.src.kind == srcWorkspace {
397		t.workspace, t.offscreen = c.src.workspace, hidden
398	}
399	switch c.src.kind {
400	case srcOutput:
401		t.taken = ports.CaptureFrameTaken{Output: c.src.output.name()}
402	case srcRegion:
403		r := c.src.region
404		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()}}
405	case srcWorkspace:
406		t.taken = ports.CaptureFrameTaken{Workspace: c.src.workspace}
407	}
408	if c.excl != nil && !c.exclSeen {
409		return t, false
410	}
411	if c.exclSeen {
412		t.exclude, t.revision = true, c.st.Revision
413	}
414	return t, true
415}
416
417func (c *captureSession) CreateFrame(r *ext.ExtImageCopyCaptureSessionV1, id uint32) {
418	if c.frame != nil {
419		r.PostError(uint32(ext.ExtImageCopyCaptureSessionV1ErrorDuplicateFrame), "frame still alive")
420		return
421	}
422	state := &captureExtFrame{session: c}
423	res, err := ext.NewExtImageCopyCaptureFrameV1(r.Client(), 1, id, state)
424	if err == nil {
425		state.res = res
426		c.frame = state
427		res.OnDestroy = func() {
428			if c.frame == state {
429				c.frame = nil
430			}
431			if c.s != nil {
432				delete(c.s.captureReplies, state.replyID)
433			}
434		}
435	}
436}
437
438type captureExtFrame struct {
439	session *captureSession
440	res     *ext.ExtImageCopyCaptureFrameV1
441	buf     *wayland.Buffer
442	used    bool
443	replyID uint64
444}
445
446func (*captureExtFrame) Destroy(*ext.ExtImageCopyCaptureFrameV1) {}
447func (f *captureExtFrame) AttachBuffer(_ *ext.ExtImageCopyCaptureFrameV1, b *wayland.Buffer) {
448	if f.used {
449		f.res.PostError(uint32(ext.ExtImageCopyCaptureFrameV1ErrorAlreadyCaptured), "already captured")
450		return
451	}
452	f.buf = b
453}
454func (f *captureExtFrame) DamageBuffer(_ *ext.ExtImageCopyCaptureFrameV1, x, y, w, h int32) {
455	if f.used {
456		f.res.PostError(uint32(ext.ExtImageCopyCaptureFrameV1ErrorAlreadyCaptured), "already captured")
457		return
458	}
459	if x < 0 || y < 0 || w <= 0 || h <= 0 {
460		f.res.PostError(uint32(ext.ExtImageCopyCaptureFrameV1ErrorInvalidBufferDamage), "invalid damage")
461	}
462}
463
464// constraintsFailed fails the frame with buffer_constraints. The buffer was
465// checked against the geometry computed now; when that differs from the last
466// constraints the client got (the target moved or rescaled and the refresh has
467// not run yet), a new batch goes first so the client can retry with the size
468// the compositor expects.
469func (f *captureExtFrame) constraintsFailed(region image.Rectangle) {
470	if c := f.session; image.Pt(region.Dx(), region.Dy()) != c.sent {
471		c.sendConstraints()
472	}
473	f.res.SendFailed(uint32(ext.ExtImageCopyCaptureFrameV1FailureReasonBufferConstraints))
474}
475
476func (f *captureExtFrame) Capture(*ext.ExtImageCopyCaptureFrameV1) {
477	if f.used {
478		f.res.PostError(uint32(ext.ExtImageCopyCaptureFrameV1ErrorAlreadyCaptured), "already captured")
479		return
480	}
481	f.used = true
482	c := f.session
483	if c.stopped || c.src == nil || !c.s.mayCaptureFrame(f.res.Client()) {
484		f.res.SendFailed(uint32(ext.ExtImageCopyCaptureFrameV1FailureReasonStopped))
485		return
486	}
487	if f.buf == nil {
488		f.res.PostError(uint32(ext.ExtImageCopyCaptureFrameV1ErrorNoBuffer), "no buffer attached")
489		return
490	}
491	o, region, hidden, ok := c.geometry()
492	if !ok || c.haveState && !c.st.Active {
493		f.res.SendFailed(uint32(ext.ExtImageCopyCaptureFrameV1FailureReasonUnknown))
494		return
495	}
496	tag, ok := c.tag(hidden)
497	buf, isBuf := c.s.buffers[f.buf.GetResource()].(*buffer)
498	switch {
499	case !isBuf:
500		f.constraintsFailed(region)
501		return
502	case !ok:
503		// The exclusion is not confirmed by core yet: nothing is served.
504		f.res.SendFailed(uint32(ext.ExtImageCopyCaptureFrameV1FailureReasonUnknown))
505		return
506	}
507	f.replyID, ok = c.s.requestCapture(o, region, c.cursor, f.buf, buf.format, func(done ports.CaptureDone) {
508		if done.Err != nil {
509			f.res.SendFailed(uint32(ext.ExtImageCopyCaptureFrameV1FailureReasonUnknown))
510			return
511		}
512		f.res.SendTransform(0)
513		f.res.SendDamage(0, 0, int32(region.Dx()), int32(region.Dy()))
514		hi, lo, ns := captureTime(done.Time)
515		f.res.SendPresentationTime(hi, lo, ns)
516		f.res.SendReady()
517	}, f.res.Resource, tag)
518	if !ok {
519		if !c.s.validCaptureBuffer(f.buf, region, buf.format) {
520			f.constraintsFailed(region)
521		} else {
522			f.res.SendFailed(uint32(ext.ExtImageCopyCaptureFrameV1FailureReasonUnknown))
523		}
524	}
525}

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