1package capture
2
3import (
4 "errors"
5 "os"
6 "slices"
7
8 "github.com/bnema/neferwl/internal/ports"
9)
10
11// A request with Exclude leaves out the exclusion's HUD layers and popups;
12// every other capture shows them. No capture shows the capture indicator
13// (Scene.CaptureIndicators): a scene that carries it is never the source of a
14// capture image. Requests of the displayed frame are then served from the
15// frame rendered without it first (SubmitPlain, ExcludedScene).
16
17// ErrSessionInactive fails an Exclude request whose session is not the one
18// the scene carries. Serving it as a normal capture would leak its HUD.
19var ErrSessionInactive = errors.New("capture exclusion is not active")
20
21// ErrSessionStale fails an Exclude request stamped with a revision newer than
22// the scene's: the scene predates a change of what an excluded frame must
23// hide (a new HUD layer, popup, target), so its exclusion list could leak.
24var ErrSessionStale = errors.New("capture exclusion scene is older than the request")
25
26// ErrWorkspaceMoved fails a workspace request whose workspace is neither on
27// screen nor the one rendered off screen in the scene: it moved, went away,
28// or the scene predates the session.
29var ErrWorkspaceMoved = errors.New("captured workspace is not where the request expected")
30
31// excluder derives the scene of Exclude captures: the frame without the
32// excluded layers and popups. Its scratch slices are reused,
33// so a warmed output allocates nothing.
34type excluder struct {
35 windows []ports.SceneWindow
36 layers []ports.SceneLayer
37}
38
39// Shows reports whether a content change of id needs a new frame: the
40// displayed scene draws it, or the hidden workspace of a capture does (its
41// child render and the reads it releases). Flips and Shown still count the
42// displayed scene only.
43func Shows(s ports.Scene, id ports.WindowID) bool {
44 return s.Shows(id) || s.CaptureScene != nil && s.CaptureScene.Shows(id)
45}
46
47// excludedShown reports whether the scene draws an excluded surface.
48func excludedShown(s ports.Scene) bool {
49 c := s.Capture
50 if c == nil || len(c.Excluded) == 0 {
51 return false
52 }
53 for _, w := range s.Windows {
54 if slices.Contains(c.Excluded, w.ID) {
55 return true
56 }
57 }
58 for _, l := range s.Layers {
59 if slices.Contains(c.Excluded, l.ID) {
60 return true
61 }
62 }
63 return false
64}
65
66// plain returns s without its capture state and indicators, Seq 0 so the
67// renderer redraws the whole target instead of reusing a displayed frame.
68func plain(s ports.Scene) ports.Scene {
69 s.Capture, s.CaptureIndicators, s.Seq = nil, nil, 0
70 return s
71}
72
73// SubmitPlain serves the displayed-frame requests of a scene that carries the
74// capture indicator: the frame is rendered without it, first (same renderer,
75// queue order, like ExcludedScene), and copied before the displayed one is
76// drawn. It reports whether it took the requests (and zeroed them); false
77// when the scene has no indicator or there is nothing to serve, the caller
78// submits them after the displayed frame as usual. On a render error the
79// caller still owns the requests.
80//
81// Admission is the session gate's: the requests are handed over with
82// SubmitScoped under the scene's epoch, so a locked or changed epoch fails
83// them before BeginCapture. The render fence is returned, never closed here:
84// the plain render reads client buffers even when the requests are rejected,
85// so the caller owns it (hold or wait it) whatever happened, error included.
86func (p *Pipeline) SubmitPlain(r ports.Renderer, s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent, normal []ports.CaptureRequest) (took bool, done *os.File, err error) {
87 if len(normal) == 0 || len(s.CaptureIndicators) == 0 {
88 return false, nil, nil
89 }
90 done, err = r.Render(plain(s), surfaces)
91 if err != nil {
92 return false, done, err
93 }
94 p.SubmitScoped(s.Security, r, normal)
95 clear(normal)
96 return true, done, nil
97}
98
99// scene returns s without the excluded windows
100// (popups included) and layers. The result is valid until the next call.
101func (c *excluder) scene(s ports.Scene) ports.Scene {
102 var ex []ports.WindowID
103 if s.Capture != nil {
104 ex = s.Capture.Excluded
105 }
106 s = plain(s)
107 if len(ex) == 0 {
108 return s
109 }
110 c.windows = c.windows[:0]
111 for _, w := range s.Windows {
112 if !slices.Contains(ex, w.ID) {
113 c.windows = append(c.windows, w)
114 }
115 }
116 c.layers = c.layers[:0]
117 for _, l := range s.Layers {
118 if !slices.Contains(ex, l.ID) {
119 c.layers = append(c.layers, l)
120 }
121 }
122 s.Windows, s.Layers = c.windows, c.layers
123 return s
124}
125
126// ExcludedScene is the scene of Exclude captures: no excluded surface; see Split. Valid until the next call.
127func (p *Pipeline) ExcludedScene(s ports.Scene) ports.Scene { return p.excl.scene(s) }
128
129// Split answers or orders the requests of one output frame, in place. No
130// allocation. Requests that cannot be served are failed and removed:
131// Exclude requests of a session other than the scene's, or with no exclusion
132// in it (ErrSessionInactive), and those newer than its Revision
133// (ErrSessionStale); workspace requests whose workspace is not where they
134// expected (ErrWorkspaceMoved). The rest is reordered:
135// - normal is the prefix, drawn from the displayed frame;
136// - excluded follows: Exclude requests, drawn from ExcludedScene rendered first,
137// or from the displayed frame (they stay in normal) when it shows no
138// excluded surface and no capture indicator;
139// - hidden: the requests of the workspace rendered off screen
140// (OffScreen), served from CaptureScene (see SubmitHidden); they come last.
141//
142// The entries past the groups are zeroed.
143func (p *Pipeline) Split(s ports.Scene, reqs []ports.CaptureRequest) (normal, excluded, hidden []ports.CaptureRequest) {
144 n := 0
145 for _, q := range reqs {
146 var err error
147 switch {
148 case q.Exclude && (s.Capture == nil || s.Capture.Session == 0 || q.Session != s.Capture.Session):
149 err = ErrSessionInactive
150 case q.Exclude && s.Capture.Revision < q.CaptureRevision:
151 err = ErrSessionStale
152 case q.Workspace != 0 && (s.Capture == nil || q.OffScreen && q.Workspace != s.Capture.Workspace || !q.OffScreen && q.Workspace != s.Capture.Shown):
153 err = ErrWorkspaceMoved
154 }
155 if err != nil {
156 Fail(p.ctx, q, err, p.replies)
157 continue
158 }
159 reqs[n] = q
160 n++
161 }
162 clear(reqs[n:])
163 reqs = reqs[:n]
164 excl := excludedShown(s) || len(s.CaptureIndicators) > 0
165 group := func(q ports.CaptureRequest) int {
166 switch {
167 case q.OffScreen:
168 return 2
169 case q.Exclude && excl:
170 return 1
171 }
172 return 0
173 }
174 var counts [3]int
175 for _, q := range reqs {
176 counts[group(q)]++
177 }
178 j := 0
179 for g := range 3 {
180 for i := j; i < n; i++ {
181 if group(reqs[i]) == g {
182 reqs[i], reqs[j] = reqs[j], reqs[i]
183 j++
184 }
185 }
186 }
187 a, b := counts[0], counts[0]+counts[1]
188 return reqs[:a], reqs[a:b], reqs[b:n]
189}
190
191// Handed reports a zeroed request: one Split failed, or one already given to
192// the worker. The owner never answers it again.
193func Handed(q ports.CaptureRequest) bool { return q.ID == 0 && q.Dst.File == nil }