1package drm
2
3import (
4 "context"
5 "errors"
6 "time"
7
8 "github.com/bnema/neferwl/internal/adapters/outputkit/capture"
9 "github.com/bnema/neferwl/internal/ports"
10)
11
12// renderError is a frame the renderer failed to draw: the output stops.
13type renderError struct{ err error }
14
15func (e renderError) Error() string { return "render frame: " + e.err.Error() }
16func (e renderError) Unwrap() error { return e.err }
17
18// submitFrame puts one frame of the scene on screen: the fullscreen window
19// scanned out directly when the kernel takes it, else a composed frame with
20// an optional overlay plane, dropped when the kernel refuses it. Captures
21// force composition and are answered from the composed frame; on renderError
22// the caller still owns the requests and must fail them. direct reports scanout.
23//
24// On success the frame is in flight; errOverlayDropped asks for a new frame
25// without the overlay; a renderError stops the output; other errors are
26// commit errors for commitFailed.
27func (o *Output) submitFrame(ctx context.Context, r ports.Renderer, scene ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent, seen map[ports.WindowID]uint64, requests []ports.CaptureRequest, pipeline *capture.Pipeline) (direct bool, err error) {
28 presented := false // the frame was committed: what it gated is released
29 if !o.sceneCurrent(scene) {
30 return false, errSecurityScene
31 }
32 if o.protected {
33 return false, o.submitProtectedFrame(ctx, r, scene, surfaces, seen)
34 }
35 // shows is what the frame puts on screen: presentation feedback of
36 // windows it does not draw is discarded, not presented.
37 f := pendingFrame{security: scene.Security, animating: scene.Animating, shows: o.shownBy(scene, seen)}
38 o.wantContent(scene, surfaces)
39 // Requests that cannot be served (exclusion of another session, a
40 // workspace that moved) are failed here, never answered from the
41 // displayed frame.
42 // A request whose capture indicator the scene does not show yet is held
43 // (the caller keeps it, see capture.Waiting).
44 ready, _ := pipeline.Hold(scene, requests, time.Now())
45 if len(scene.CaptureIndicators) > 0 {
46 // The captures of this frame are delivered only once the frame that
47 // carries their indicator is committed, whatever the path out (see
48 // capture.Pipeline.EndGate): no failed or dropped frame serves one.
49 pipeline.BeginGate()
50 defer func() {
51 if err == nil && !presented {
52 err = capture.ErrFrameNotPresented // a panic on the way out
53 }
54 pipeline.EndGate(capture.GateVerdict(err))
55 }()
56 }
57 normal, excluded, hidden := pipeline.Split(scene, ready)
58 pipeline.Retire(scene)
59 if len(hidden) > 0 {
60 if !o.sceneCurrent(scene) || o.protected {
61 return false, errSecurityScene
62 }
63 // A hidden workspace: the child renderer draws it, whatever the display
64 // does (scanout included). Failures answer the requests and never stop
65 // the output.
66 pipeline.SubmitHidden(scene, surfaces, hidden)
67 // Publish child holds before queuing a display flip, which may stall
68 // past the stale-report timeout. Do not advance the display's Seen
69 // until its own GPU work finishes.
70 if o.capHidden != nil {
71 o.report(nil, o.reports.Seen())
72 }
73 clear(hidden)
74 }
75 // Displayed-frame captures, Exclude requests and the capture indicator
76 // force composition (scanout and the overlay plane would skip them); the
77 // hidden-workspace child does not.
78 decision := o.decideFrame(scene, surfaces, len(normal) > 0 || len(excluded) > 0 || len(scene.CaptureIndicators) > 0)
79 if !o.sceneCurrent(scene) || o.protected {
80 decision.overlay.close()
81 return false, errSecurityScene
82 }
83 if decision.fb != 0 {
84 c := decision.content
85 if direct, err = o.commitScanoutRect(decision.fb, c, pendingFrame{security: scene.Security, animating: scene.Animating, shows: o.directShownBy(c.ID, seen[c.ID])}, decision.rect, decision.color); direct {
86 presented = err == nil
87 return true, err
88 }
89 decision = o.composeFrame(scene, surfaces)
90 }
91 ov, composed := decision.overlay, decision.composed
92 // A render that was not committed may still read client buffers: only a
93 // new render's fence orders this flip after it, so the frame composes.
94 if ov.fb != 0 && len(normal) == 0 && len(excluded) == 0 && o.readDone() {
95 if front := o.fbs[1-o.back]; o.front.holds(front, composed, surfaces) {
96 // Only the overlay window changed: the image on screen already
97 // holds the rest.
98 if o.testOverlay(front, ov) {
99 err = o.commitOverlayOnly(front, scene, f, ov)
100 presented = err == nil
101 return false, err
102 }
103 // Refused: compose the window too (cached per buffer).
104 ov.close()
105 ov, composed = overlayWin{}, scene
106 }
107 }
108 r.UseTarget(o.back)
109 // Captures never show the capture indicator or the position hint, and show
110 // hint labels only to clients with hint.capture: their frame goes first.
111 took, pdone, perr := pipeline.SubmitPlain(r, composed, surfaces, normal)
112 if perr != nil {
113 o.holdRead(pdone)
114 if pdone != nil {
115 pdone.Close()
116 }
117 ov.close()
118 return false, renderError{perr}
119 }
120 // Not capture admission: an engage during the render retains its client
121 // reads; SubmitScoped already failed the requests of a stale epoch.
122 if !o.sceneCurrent(scene) || o.protected {
123 o.holdRead(pdone)
124 if pdone != nil {
125 pdone.Close()
126 }
127 ov.close()
128 return false, errSecurityScene
129 }
130 if pdone != nil {
131 pdone.Close()
132 }
133 if took {
134 normal = nil // answered from the frame without the indicator
135 }
136 if len(excluded) > 0 {
137 // The excluded frame goes first to the same target and is copied by
138 // the capture; the displayed frame is drawn over it in queue order.
139 // Cost: a second composition on frames with an excluded request.
140 // It has no indicator or position hint; it keeps hint labels with
141 // hint.capture (Split refuses internal requests there).
142 xdone, xerr := r.Render(pipeline.ExcludedScene(composed), surfaces)
143 if xerr != nil {
144 o.holdRead(xdone)
145 if xdone != nil {
146 xdone.Close()
147 }
148 ov.close()
149 return false, renderError{xerr}
150 }
151 // An excluded composition is not capture admission. Engage may have
152 // happened while Render submitted GPU work; retain its client reads
153 // and leave the request credit with the caller on rejection.
154 if !o.sceneCurrent(scene) || o.protected {
155 o.holdRead(xdone)
156 if xdone != nil {
157 xdone.Close()
158 }
159 ov.close()
160 return false, errSecurityScene
161 }
162 if xdone != nil {
163 xdone.Close()
164 }
165 pipeline.SubmitScoped(scene.Security, r, excluded)
166 // Ownership moved to the worker (or an immediate failure reply).
167 clear(excluded)
168 }
169 done, rerr := r.Render(composed, surfaces)
170 if rerr != nil {
171 ov.close()
172 return false, renderError{rerr}
173 }
174 if ov.fb != 0 && !o.testOverlay(o.fbs[o.back], ov) {
175 // Refused: compose the window too (cached per buffer).
176 if done != nil {
177 done.Close()
178 }
179 ov.close()
180 ov = overlayWin{}
181 if done, rerr = r.Render(scene, surfaces); rerr != nil {
182 return false, renderError{rerr}
183 }
184 }
185 if !o.sceneCurrent(scene) || o.protected {
186 o.holdRead(done)
187 if done != nil {
188 done.Close()
189 }
190 ov.close()
191 return false, errSecurityScene
192 }
193 if len(normal) > 0 {
194 // Scanout and the overlay plane were skipped for this frame.
195 o.log.Debug().Str("connector", o.conn.name).Int("captures", len(normal)).Msg("capture frame composed")
196 pipeline.SubmitScoped(scene.Security, r, normal)
197 // Ownership moved to the worker (or an immediate failure reply).
198 // A later commit panic must not fail the same requests again.
199 clear(normal)
200 }
201 if !o.sceneCurrent(scene) || o.protected {
202 o.holdRead(done)
203 if done != nil {
204 done.Close()
205 }
206 ov.close()
207 return false, errSecurityScene
208 }
209 // The overlay buffer is on screen like a scanned-out one: it is
210 // reported shown, so it is not released under the plane.
211 f.queued, f.zeroCopy, f.composed = ov.buf, ov.id, true
212 // A covering fullscreen window keeps VRR while composed (overlay
213 // layer, subsurfaces, size mismatch).
214 game := o.vrrProp != 0 && fullscreenShown(&scene)
215 err = o.commitWith(o.fbs[o.back], done, false, o.wantVRR(game), f, ov)
216 presented = err == nil
217 if err != nil {
218 // The GPU may still read client buffers for this frame.
219 o.holdRead(done)
220 }
221 if done != nil {
222 done.Close()
223 }
224 ov.close()
225 if err = o.finishOverlayCommit(err, ov.buf); err == nil {
226 // The image now holds what was drawn last: composed, or the whole
227 // scene after an overlay refusal.
228 if ov.fb != 0 {
229 o.front.hold(o.fbs[o.back], composed, surfaces)
230 } else {
231 o.front.hold(o.fbs[o.back], scene, surfaces)
232 }
233 o.back = 1 - o.back
234 }
235 return false, err
236}
237
238// commitOverlayOnly commits front, the image on screen, again with ov on
239// the overlay plane: no render, no GPU fence. It closes ov's fence.
240func (o *Output) commitOverlayOnly(front uint32, scene ports.Scene, f pendingFrame, ov overlayWin) error {
241 defer ov.close()
242 if !o.sceneCurrent(scene) || o.protected {
243 return errSecurityScene
244 }
245 f.queued, f.zeroCopy = ov.buf, ov.id
246 game := o.vrrProp != 0 && fullscreenShown(&scene)
247 return o.finishOverlayCommit(o.commitWith(front, nil, false, o.wantVRR(game), f, ov), ov.buf)
248}
249
250// finishOverlayCommit applies the result of a frame commit with buf on the
251// overlay (0: none): a refusal the overlay causes asks for a frame without
252// it (errOverlayDropped); a commit marks buf queued.
253func (o *Output) finishOverlayCommit(err error, buf uint64) error {
254 if errors.Is(err, errColorRefused) {
255 // Cached for the overlay's buffer format: the next frame composes it.
256 o.setOverlayReason("color_refused")
257 return errOverlayDropped
258 }
259 if err != nil && o.overlayConflict(err, buf) {
260 return errOverlayDropped
261 }
262 if err == nil {
263 o.queued = buf
264 }
265 return err
266}