1package wayland
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "slices"
8
9 extworkspace "github.com/bnema/go-wayland-bindings/client/extworkspace"
10
11 "github.com/bnema/nefercap/internal/ports"
12)
13
14// Freeze captures each output in ids once and keeps a private copy. Until
15// Thaw or Close, Capture serves those copies instead of the live screen:
16// whole outputs, regions and workspaces are cut from what the screen showed at
17// Freeze. An output that cannot be captured as is (a rotated one) is left out
18// and stays live. The returned frames, by output ID, own their storage and
19// stay valid until Thaw or Close; the caller must not modify them.
20func (s *Source) Freeze(ctx context.Context, ids []uint32) (map[uint32]ports.Frame, error) {
21 frozen := make(map[uint32]ports.Frame, len(ids))
22 for _, id := range ids {
23 f, err := s.Capture(ctx, ports.Target{OutputID: id})
24 if errors.Is(err, ports.ErrUnsupportedTransform) {
25 s.log.Info().Uint32("output", id).Msg("rotated output stays live")
26 continue
27 }
28 if err != nil {
29 return nil, fmt.Errorf("freeze output %d: %w", id, err)
30 }
31 frozen[id] = ownedCopy(f)
32 }
33 s.opMu.Lock()
34 defer s.opMu.Unlock()
35 if s.dead.Load() {
36 return nil, ports.ErrClosed
37 }
38 // Nothing is captured live until Thaw, unless an output stayed live: let
39 // the compositor drop the session and free the shared buffers, which a
40 // later live capture creates again.
41 s.dropSession()
42 if err := errors.Join(s.releaseBuffer(0, true), s.releaseBuffer(1, true)); err != nil {
43 s.terminate()
44 return nil, err
45 }
46 s.frozen = frozen
47 return frozen, nil
48}
49
50// Thaw makes Capture read the live screen again and drops the frozen copies,
51// except the one of each output in keep.
52func (s *Source) Thaw(keep ...uint32) {
53 s.opMu.Lock()
54 defer s.opMu.Unlock()
55 for id := range s.frozen {
56 if !slices.Contains(keep, id) {
57 delete(s.frozen, id)
58 }
59 }
60 if len(s.frozen) == 0 {
61 s.frozen = nil
62 }
63}
64
65// ownedCopy packs f into new storage, without stride padding.
66func ownedCopy(f ports.Frame) ports.Frame {
67 row := f.Width * ports.BytesPerPixel
68 pixels := make([]byte, row*f.Height)
69 for y := range f.Height {
70 copy(pixels[y*row:], f.Row(y))
71 }
72 return ports.Frame{Pixels: pixels, Width: f.Width, Height: f.Height, Stride: row, Format: f.Format}
73}
74
75// frozenTarget cuts t from the frozen frame of its output; ok is false when
76// that output is not frozen and is captured live. The caller holds opMu.
77func (s *Source) frozenTarget(t ports.Target) (_ ports.Frame, ok bool, _ error) {
78 f, ok := s.frozen[t.OutputID]
79 out := s.outputs[t.OutputID]
80 if !ok || out == nil {
81 return ports.Frame{}, false, nil
82 }
83 region := t.Region
84 if t.WorkspaceID != 0 {
85 if t.Region != (ports.Region{}) {
86 return ports.Frame{}, true, ports.ErrInvalidRegion
87 }
88 // As live without NeferWL's extension: only the workspace shown on
89 // this output at Freeze is what the copy shows.
90 w := s.ws.byID[t.WorkspaceID]
91 if w == nil || w.removed || w.state&extworkspace.STATE_ACTIVE == 0 || w.group == nil || !w.group.outputs[out.proxy.ID()] {
92 return ports.Frame{}, true, ports.ErrWorkspaceUnavailable
93 }
94 region = workspaceRegion(w, out)
95 if lw, lh := out.logicalSize(); region == (ports.Region{Width: lw, Height: lh}) {
96 region = ports.Region{}
97 }
98 }
99 if region == (ports.Region{}) {
100 return f, true, nil
101 }
102 f, err := cropFrame(f, region, out)
103 return f, true, err
104}