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.Flash != nil {
71 return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "focus_flash"
72 }
73 if len(s.HintLabels) > 0 {
74 return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "hint_labels"
75 }
76 if s.Transform != 0 {
77 return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "output_transform"
78 }
79 scale := s.Scale
80 if scale <= 0 {
81 scale = 1
82 }
83 var pick *ports.SceneWindow
84 var pickAt int
85 var mode colorMode
86 for i := range s.Windows {
87 w := &s.Windows[i]
88 if !s.Draws(*w) {
89 // Not drawn (a tile wholly under a panel included): neither
90 // a candidate nor over one.
91 continue
92 }
93 if w.Popup {
94 // Popups draw after every window, wherever they are listed:
95 // checked once the candidate is known.
96 continue
97 }
98 c := surfaces[w.ID]
99 // A viewport crop is composed: the plane would show the whole
100 // buffer, and so is a colour the plane cannot show (raw PQ values
101 // on an SDR output, SDR on HDR without a pipeline). A dimmed
102 // window (a peeking stashed one) needs the veil drawn over it, a
103 // fading one its translucency; an overview preview is drawn smaller
104 // than its buffer.
105 if 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)) {
106 if m, why := planeColor(c.Color, c.DMABuf.Format, c.Opaque, hdrOn, pipeline); why == "" {
107 pick, pickAt, mode = w, i, m
108 continue
109 }
110 }
111 if pick != nil && w.Rect.Overlaps(pick.Rect) {
112 // Something is drawn over the candidate.
113 return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "window_above"
114 }
115 }
116 if pick != nil {
117 for i := range s.Windows {
118 if w := &s.Windows[i]; w.Popup && s.Draws(*w) && w.Rect.Overlaps(pick.Rect) {
119 return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "window_above"
120 }
121 }
122 }
123 if pick == nil {
124 if kept != nil && len(s.Windows) > 0 {
125 // The one window that could have gone on the plane fades out
126 // after its close: composed from its kept content.
127 for i := range s.Windows {
128 if w := &s.Windows[i]; s.Draws(*w) && kept(w.ID) {
129 return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "leaving"
130 }
131 }
132 }
133 return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "no_candidate"
134 }
135 if s.Dim > 0 {
136 // The renderer composes the veil (under the first float, or under
137 // every window with DimBehind): a window on the plane would skip it.
138 return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "dim"
139 }
140 if len(s.DropHints) > 0 {
141 // Drag hints are drawn over the windows.
142 return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "drop_hint"
143 }
144 for _, l := range s.Layers {
145 if l.Layer >= ports.LayerTop && l.Rect.Overlaps(pick.Rect) {
146 return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "layer_above"
147 }
148 }
149 if !pick.Fullscreen {
150 if pick.Inset != 0 && s.Border.Width > 0 {
151 // The content sits inside the lines, not at the window's rect.
152 return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "border"
153 }
154 for _, sep := range s.Separators {
155 if sep.Rect.Overlaps(pick.Rect) && !linesUnder(s, sep, pick, pickAt) {
156 // The overlay shows the buffer alone; the lines would go
157 // under it.
158 return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "border"
159 }
160 }
161 }
162 if clip := s.WorkspaceClip; clip != (ports.Rect{}) && !clip.Contains(pick.Rect) {
163 // The plane cannot apply the workspace's logical viewport crop.
164 return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "workspace_clip"
165 }
166 if a, ok := s.TileArea(); ok && pick.Tile() && !a.Contains(pick.Rect) {
167 // Nor the cut of a tile at a panel.
168 return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "tile_clip"
169 }
170 c := surfaces[pick.ID]
171 pw, ph := float64(pick.Rect.W)*scale, float64(pick.Rect.H)*scale
172 if float64(c.Width) != pw || float64(c.Height) != ph || c.Geometry != (ports.Rect{}) && (c.Geometry.X != 0 || c.Geometry.Y != 0) {
173 return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "scaled"
174 }
175 x, y := float64(pick.Rect.X)*scale, float64(pick.Rect.Y)*scale
176 if x != float64(int(x)) || y != float64(int(y)) {
177 return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "fractional_position"
178 }
179 return *pick, c, mode, ""
180}
181
182// linesUnder reports whether sep is painted before the candidate at index
183// pickAt, so stays composed under the plane: the tile lines and the lines
184// of windows listed before it, when the candidate is a float drawn above
185// the tiles.
186func linesUnder(s ports.Scene, sep ports.Separator, pick *ports.SceneWindow, pickAt int) bool {
187 if !pick.Floating || pick.Below {
188 return false
189 }
190 if sep.Window == 0 {
191 return true
192 }
193 for _, w := range s.Windows[:pickAt] {
194 if w.ID == sep.Window {
195 return true
196 }
197 }
198 return false
199}
200
201// overlayProps puts ov on the overlay plane, or turns it off.
202func (o *Output) overlayProps(req *atomicReq, ov overlayWin) {
203 p := o.overlay
204 if p == nil {
205 return
206 }
207 if ov.fb == 0 {
208 req.set(p.id, p.prop("FB_ID"), 0)
209 req.set(p.id, p.prop("CRTC_ID"), 0)
210 o.overlayColorProps(req, colorBypass)
211 return
212 }
213 o.overlayColorProps(req, ov.color.mode)
214 req.set(p.id, p.prop("FB_ID"), uint64(ov.fb))
215 req.set(p.id, p.prop("CRTC_ID"), uint64(o.crtc))
216 req.set(p.id, p.prop("SRC_X"), 0)
217 req.set(p.id, p.prop("SRC_Y"), 0)
218 req.set(p.id, p.prop("SRC_W"), uint64(ov.w)<<16)
219 req.set(p.id, p.prop("SRC_H"), uint64(ov.h)<<16)
220 req.set(p.id, p.prop("CRTC_X"), uint64(int64(ov.rect.X)))
221 req.set(p.id, p.prop("CRTC_Y"), uint64(int64(ov.rect.Y)))
222 req.set(p.id, p.prop("CRTC_W"), uint64(ov.rect.W))
223 req.set(p.id, p.prop("CRTC_H"), uint64(ov.rect.H))
224 if ov.acquire != nil {
225 // KMS waits for the client's GPU work before scanning it out.
226 req.set(p.id, p.prop("IN_FENCE_FD"), uint64(ov.acquire.Fd()))
227 }
228}
229
230// close releases the frame's duplicate of the acquire fence.
231func (ov overlayWin) close() {
232 if ov.acquire != nil {
233 ov.acquire.Close()
234 }
235}
236
237// testOverlay asks KMS whether it takes the frame with ov on the overlay,
238// the cursor at its current place included. A refusal is cached on the
239// buffer; one that the plane's colour pipeline causes (the same frame passes
240// with Bypass) is cached on the plane for the buffer format and the cursor
241// state instead.
242//
243// A frame identical to one KMS took (same images, place, colour mode and
244// cursor; the fence does not count) is not probed again: a refusal of the
245// commit itself still drops the overlay (overlayConflict).
246func (o *Output) testOverlay(fb uint32, ov overlayWin) bool {
247 // One cursor snapshot: the input goroutine may move it meanwhile, and
248 // what is remembered must be what KMS was asked.
249 key := o.overlayProbeOf(fb, ov)
250 if slices.Contains(o.overlayPassed[:], key) {
251 return true
252 }
253 err := o.probeOverlay(fb, ov, ov.color.mode, key.cursor)
254 if err == nil {
255 o.overlayPassed[o.overlayNext] = key
256 o.overlayNext = (o.overlayNext + 1) % len(o.overlayPassed)
257 return true
258 }
259 if ov.color.mode != colorBypass && refused(err) && o.probeOverlay(fb, ov, colorBypass, key.cursor) == nil {
260 o.overlay.setVerdict(ov.color.format, key.cursor.on, false)
261 o.setOverlayReason("color_refused")
262 o.renderLog.Info().Err(err).Uint32("format", ov.color.format).Str("connector", o.conn.name).Msg("overlay colour pipeline refused")
263 return false
264 }
265 if cfb := o.clientFBs[ov.buf]; cfb != nil && refused(err) {
266 cfb.overlayFailed = "overlay_refused"
267 }
268 o.setOverlayReason("overlay_refused")
269 o.renderLog.Info().Err(err).Str("connector", o.conn.name).Msg("overlay refused")
270 return false
271}
272
273// forgetOverlayProbes drops the remembered probes: KMS is asked again.
274func (o *Output) forgetOverlayProbes() { clear(o.overlayPassed[:]) }
275
276// overlayProbe is what an overlay probe depends on, fences aside.
277type overlayProbe struct {
278 primary, overlay uint32
279 rect ports.Rect
280 w, h int
281 color colorUse
282 cursor cursorState
283 mult uint64
284 ctm uint32
285}
286
287func (o *Output) overlayProbeOf(fb uint32, ov overlayWin) overlayProbe {
288 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}
289 if o.cursor != nil && !o.protected {
290 p.cursor = o.cursor.desired()
291 }
292 return p
293}
294
295// probeOverlay is a TEST_ONLY of the frame: the composed image on the
296// primary plane (Bypass), ov on the overlay with colour mode, the cursor in
297// state cur. Both planes' colour state is written whatever they applied:
298// the test must not rely on the state of the last commit.
299func (o *Output) probeOverlay(fb uint32, ov overlayWin, mode colorMode, cur cursorState) error {
300 req := &o.probeReq // no frame request is alive: the frame commit follows
301 req.reset()
302 o.primaryProps(req, fb)
303 o.primary.colorProps(req, colorBypass, 0, 0)
304 o.overlayProps(req, ov)
305 o.overlay.colorProps(req, mode, o.colorMult, o.ctmBlob)
306 if o.cursor != nil {
307 o.cursor.props(req, o.crtc, cur)
308 }
309 return o.k.commit(req, atomicTestOnly, 0)
310}
311
312// errOverlayDropped: a commit was refused with the overlay on; the frame is
313// composed again without it.
314var errOverlayDropped = errors.New("overlay dropped")
315
316// overlayConflict handles a commit refused with buffer buf on the overlay:
317// the buffer leaves the overlay for good (e.g. the cursor moved where the
318// plane forbids it) and the frame is composed. It reports whether it did.
319func (o *Output) overlayConflict(err error, buf uint64) bool {
320 if buf == 0 || !errors.Is(err, unix.EINVAL) {
321 return false
322 }
323 if cfb := o.clientFBs[buf]; cfb != nil {
324 cfb.overlayFailed = "cursor_conflict"
325 }
326 o.setOverlayReason("cursor_conflict")
327 return true
328}