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