frame_submit.go RawHistoryFolder

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}

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