v0.8.0 446d1e3

overlay.go RawHistoryFolder

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

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