main f2d166b

overlay.go RawHistoryFolder

1package drm
2
3import (
4	"errors"
5	"os"
6	"slices"
7
8	"github.com/bnema/neferwl/internal/ports"
9	"golang.org/x/sys/unix"
10)
11
12// Overlay plane: a window drawn from one opaque dmabuf, with nothing but
13// the cursor above it, goes on an overlay plane at its place on screen;
14// the rest of the scene is composed into the primary plane without it.
15// The client buffer is shown with no GPU pass, like direct scanout, but
16// the window need not cover the output. A refusal (TEST_ONLY) keeps the
17// window composed and is cached per buffer.
18
19// pickOverlay takes an overlay plane above the primary one (zpos), when
20// the output has one.
21func (o *Output) pickOverlay() {
22	pz, ok := o.primary.zpos()
23	if !ok {
24		return
25	}
26	for _, p := range o.stray {
27		if p.typ != planeOverlay {
28			continue
29		}
30		if z, ok := p.zpos(); ok && z > pz {
31			o.overlay = p
32			return
33		}
34	}
35}
36
37// zpos is the plane's zpos value when known.
38func (p *plane) zpos() (uint64, bool) {
39	if p.props["zpos"] == 0 {
40		return 0, false
41	}
42	return p.zposValue, true
43}
44
45// overlayWin is the window on the overlay plane: its buffer and where it
46// shows, in physical pixels.
47type overlayWin struct {
48	id   ports.WindowID
49	fb   uint32
50	buf  uint64
51	rect ports.Rect // CRTC rect
52	w, h int        // buffer size
53	// color is how the plane's colour pipeline shows the buffer.
54	color colorUse
55	// acquire is the client's explicit-sync fence, a duplicate owned by
56	// the frame loop (closed after the commit); nil without one.
57	acquire *os.File
58}
59
60// overlayCandidate finds the one window the overlay can show: an opaque
61// dmabuf at integer physical coordinates, drawn 1:1, with no other window,
62// popup, border line or layer drawn over it, and the plane colour mode it
63// needs. What is above it elsewhere on screen stays on the primary plane,
64// which the overlay covers only over the window. reason is why none.
65//
66// kept is scanoutCandidate's: a window drawn from a withdrawn content is
67// composed, never put on the plane.
68func overlayCandidate(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent, hdrOn bool, pipeline *colorPipeline, kept func(ports.WindowID) bool) (ports.SceneWindow, ports.SurfaceContent, colorMode, string) {
69	// A hardware plane would bypass the compositor-owned flash border.
70	if s.PositionHint.Shown() {
71		return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "position_hint"
72	}
73	if s.Outline != nil {
74		return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "selection_outline"
75	}
76	if s.Flash != nil {
77		return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "focus_flash"
78	}
79	if len(s.HintLabels) > 0 {
80		return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "hint_labels"
81	}
82	if s.Transform != 0 {
83		return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "output_transform"
84	}
85	scale := s.Scale
86	if scale <= 0 {
87		scale = 1
88	}
89	var pick *ports.SceneWindow
90	var pickAt int
91	var mode colorMode
92	for i := range s.Windows {
93		w := &s.Windows[i]
94		if !s.Draws(*w) {
95			// Not drawn (a tile wholly under a panel included): neither
96			// a candidate nor over one.
97			continue
98		}
99		if w.Popup {
100			// Popups draw after every window, wherever they are listed:
101			// checked once the candidate is known.
102			continue
103		}
104		c := surfaces[w.ID]
105		// A viewport crop is composed: the plane would show the whole
106		// buffer, and so is a colour the plane cannot show (raw PQ values
107		// on an SDR output, SDR on HDR without a pipeline). A dimmed
108		// window (a peeking stashed one) needs the veil drawn over it, a
109		// fading one its translucency; an overview preview is drawn smaller
110		// than its buffer.
111		if w.Radius == 0 && w.Dim <= 0 && w.Fade <= 0 && w.Zoom <= 0 && w.Preview <= 0 && c.DMABuf != nil && !isYUVFormat(c.DMABuf.Format) && c.Opaque && len(c.Children) == 0 && c.Transform == 0 && !cropped(c) && (kept == nil || !kept(w.ID)) {
112			if m, why := planeColor(c.Color, c.DMABuf.Format, c.Opaque, hdrOn, pipeline); why == "" {
113				pick, pickAt, mode = w, i, m
114				continue
115			}
116		}
117		if pick != nil && w.Rect.Overlaps(pick.Rect) {
118			// Something is drawn over the candidate.
119			return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "window_above"
120		}
121	}
122	if pick != nil {
123		for i := range s.Windows {
124			if w := &s.Windows[i]; w.Popup && s.Draws(*w) && w.Rect.Overlaps(pick.Rect) {
125				return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "window_above"
126			}
127		}
128	}
129	if pick == nil {
130		if kept != nil && len(s.Windows) > 0 {
131			// The one window that could have gone on the plane fades out
132			// after its close: composed from its kept content.
133			for i := range s.Windows {
134				if w := &s.Windows[i]; s.Draws(*w) && kept(w.ID) {
135					return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "leaving"
136				}
137			}
138		}
139		return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "no_candidate"
140	}
141	if s.Dim > 0 {
142		// The renderer composes the veil (under the first float, or under
143		// every window with DimBehind): a window on the plane would skip it.
144		return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "dim"
145	}
146	if len(s.DropHints) > 0 {
147		// Drag hints are drawn over the windows.
148		return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "drop_hint"
149	}
150	for _, l := range s.Layers {
151		if l.Layer >= ports.LayerTop && l.Rect.Overlaps(pick.Rect) {
152			return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "layer_above"
153		}
154	}
155	if !pick.Fullscreen {
156		if pick.Inset != 0 && s.Border.Width > 0 {
157			// The content sits inside the lines, not at the window's rect.
158			return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "border"
159		}
160		for _, sep := range s.Separators {
161			if sep.Rect.Overlaps(pick.Rect) && !linesUnder(s, sep, pick, pickAt) {
162				// The overlay shows the buffer alone; the lines would go
163				// under it.
164				return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "border"
165			}
166		}
167	}
168	if clip := s.WorkspaceClip; clip != (ports.Rect{}) && !clip.Contains(pick.Rect) {
169		// The plane cannot apply the workspace's logical viewport crop.
170		return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "workspace_clip"
171	}
172	if a, ok := s.TileArea(); ok && pick.Tile() && !a.Contains(pick.Rect) {
173		// Nor the cut of a tile at a panel.
174		return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "tile_clip"
175	}
176	c := surfaces[pick.ID]
177	pw, ph := float64(pick.Rect.W)*scale, float64(pick.Rect.H)*scale
178	if float64(c.Width) != pw || float64(c.Height) != ph || c.Geometry != (ports.Rect{}) && (c.Geometry.X != 0 || c.Geometry.Y != 0) {
179		return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "scaled"
180	}
181	x, y := float64(pick.Rect.X)*scale, float64(pick.Rect.Y)*scale
182	if x != float64(int(x)) || y != float64(int(y)) {
183		return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "fractional_position"
184	}
185	return *pick, c, mode, ""
186}
187
188// linesUnder reports whether sep is painted before the candidate at index
189// pickAt, so stays composed under the plane: the tile lines and the lines
190// of windows listed before it, when the candidate is a float drawn above
191// the tiles.
192func linesUnder(s ports.Scene, sep ports.Separator, pick *ports.SceneWindow, pickAt int) bool {
193	if !pick.Floating || pick.Below {
194		return false
195	}
196	if sep.Window == 0 {
197		return true
198	}
199	for _, w := range s.Windows[:pickAt] {
200		if w.ID == sep.Window {
201			return true
202		}
203	}
204	return false
205}
206
207// overlayProps puts ov on the overlay plane, or turns it off.
208func (o *Output) overlayProps(req *atomicReq, ov overlayWin) {
209	p := o.overlay
210	if p == nil {
211		return
212	}
213	if ov.fb == 0 {
214		req.set(p.id, p.prop("FB_ID"), 0)
215		req.set(p.id, p.prop("CRTC_ID"), 0)
216		o.overlayColorProps(req, colorBypass)
217		return
218	}
219	o.overlayColorProps(req, ov.color.mode)
220	req.set(p.id, p.prop("FB_ID"), uint64(ov.fb))
221	req.set(p.id, p.prop("CRTC_ID"), uint64(o.crtc))
222	req.set(p.id, p.prop("SRC_X"), 0)
223	req.set(p.id, p.prop("SRC_Y"), 0)
224	req.set(p.id, p.prop("SRC_W"), uint64(ov.w)<<16)
225	req.set(p.id, p.prop("SRC_H"), uint64(ov.h)<<16)
226	req.set(p.id, p.prop("CRTC_X"), uint64(int64(ov.rect.X)))
227	req.set(p.id, p.prop("CRTC_Y"), uint64(int64(ov.rect.Y)))
228	req.set(p.id, p.prop("CRTC_W"), uint64(ov.rect.W))
229	req.set(p.id, p.prop("CRTC_H"), uint64(ov.rect.H))
230	// KMS waits for the client's GPU work before scanning it out.
231	o.fenceProp(req, p, ov.acquire)
232}
233
234// close releases the frame's duplicate of the acquire fence.
235func (ov overlayWin) close() {
236	if ov.acquire != nil {
237		ov.acquire.Close()
238	}
239}
240
241// testOverlay asks KMS whether it takes the frame with ov on the overlay,
242// the cursor at its current place included. A refusal is cached on the
243// buffer; one that the plane's colour pipeline causes (the same frame passes
244// with Bypass) is cached on the plane for the buffer format and the cursor
245// state instead.
246//
247// A frame identical to one KMS took (same images, place, colour mode and
248// cursor; the fence does not count) is not probed again: a refusal of the
249// commit itself still drops the overlay (overlayConflict).
250func (o *Output) testOverlay(fb uint32, ov overlayWin) bool {
251	// One cursor snapshot: the input goroutine may move it meanwhile, and
252	// what is remembered must be what KMS was asked.
253	key := o.overlayProbeOf(fb, ov)
254	if slices.Contains(o.overlayPassed[:], key) {
255		return true
256	}
257	err := o.probeOverlay(fb, ov, ov.color.mode, key.cursor)
258	if err == nil {
259		o.overlayPassed[o.overlayNext] = key
260		o.overlayNext = (o.overlayNext + 1) % len(o.overlayPassed)
261		return true
262	}
263	if ov.color.mode != colorBypass && refused(err) && o.probeOverlay(fb, ov, colorBypass, key.cursor) == nil {
264		o.overlay.setVerdict(ov.color.format, key.cursor.on, false)
265		o.setOverlayReason("color_refused")
266		o.renderLog.Info().Err(err).Uint32("format", ov.color.format).Str("connector", o.conn.name).Msg("overlay colour pipeline refused")
267		return false
268	}
269	if cfb := o.clientFBs[ov.buf]; cfb != nil && refused(err) {
270		cfb.overlayFailed = "overlay_refused"
271	}
272	o.setOverlayReason("overlay_refused")
273	o.renderLog.Info().Err(err).Str("connector", o.conn.name).Msg("overlay refused")
274	return false
275}
276
277// forgetOverlayProbes drops the remembered probes: KMS is asked again.
278func (o *Output) forgetOverlayProbes() { clear(o.overlayPassed[:]) }
279
280// overlayProbe is what an overlay probe depends on, fences aside.
281type overlayProbe struct {
282	primary, overlay uint32
283	rect             ports.Rect
284	w, h             int
285	color            colorUse
286	cursor           cursorState
287	mult             uint64
288	ctm              uint32
289}
290
291func (o *Output) overlayProbeOf(fb uint32, ov overlayWin) overlayProbe {
292	p := overlayProbe{primary: fb, overlay: ov.fb, rect: ov.rect, w: ov.w, h: ov.h, color: ov.color, mult: o.colorMult, ctm: o.ctmBlob}
293	if o.cursor != nil && !o.protected {
294		p.cursor = o.cursor.desired()
295	}
296	return p
297}
298
299// probeOverlay is a TEST_ONLY of the frame: the composed image on the
300// primary plane (Bypass), ov on the overlay with colour mode, the cursor in
301// state cur. Both planes' colour state is written whatever they applied:
302// the test must not rely on the state of the last commit.
303func (o *Output) probeOverlay(fb uint32, ov overlayWin, mode colorMode, cur cursorState) error {
304	req := &o.probeReq // no frame request is alive: the frame commit follows
305	req.reset()
306	o.primaryProps(req, fb)
307	o.primary.colorProps(req, colorBypass, 0, 0)
308	o.overlayProps(req, ov)
309	o.overlay.colorProps(req, mode, o.colorMult, o.ctmBlob)
310	if o.cursor != nil {
311		o.cursor.props(req, o.crtc, cur)
312	}
313	return o.k.commit(req, atomicTestOnly, 0)
314}
315
316// errOverlayDropped: a commit was refused with the overlay on; the frame is
317// composed again without it.
318var errOverlayDropped = errors.New("overlay dropped")
319
320// overlayConflict handles a commit refused with buffer buf on the overlay:
321// the buffer leaves the overlay for good (e.g. the cursor moved where the
322// plane forbids it) and the frame is composed. It reports whether it did.
323func (o *Output) overlayConflict(err error, buf uint64) bool {
324	if buf == 0 || !errors.Is(err, unix.EINVAL) {
325		return false
326	}
327	if cfb := o.clientFBs[buf]; cfb != nil {
328		cfb.overlayFailed = "cursor_conflict"
329	}
330	o.setOverlayReason("cursor_conflict")
331	return true
332}

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 25

Tags 10

↑↓ moveenter openesc close