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// or layer above it, and the plane colour mode it needs. reason is why none.
62//
63// kept is scanoutCandidate's: a window drawn from a withdrawn content is
64// composed, never put on the plane.
65func 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) {
66 if s.Transform != 0 {
67 return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "output_transform"
68 }
69 scale := s.Scale
70 if scale <= 0 {
71 scale = 1
72 }
73 var pick *ports.SceneWindow
74 var mode colorMode
75 for i := range s.Windows {
76 w := &s.Windows[i]
77 if w.Hidden || w.Rect.W <= 0 || w.Rect.H <= 0 {
78 continue
79 }
80 if w.Popup {
81 // Popups draw after every window, wherever they are listed.
82 return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "window_above"
83 }
84 c := surfaces[w.ID]
85 // A viewport crop is composed: the plane would show the whole
86 // buffer, and so is a colour the plane cannot show (raw PQ values
87 // on an SDR output, SDR on HDR without a pipeline). A dimmed
88 // window (a peeking stashed one) needs the veil drawn over it, a
89 // fading one its translucency, and one with the focus effect its
90 // lift; an overview preview is drawn smaller than its buffer.
91 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)) {
92 if m, why := planeColor(c.Color, c.DMABuf.Format, c.Opaque, hdrOn, pipeline); why == "" {
93 pick, mode = w, m
94 continue
95 }
96 }
97 if pick != nil {
98 // Something is drawn above the candidate.
99 return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "window_above"
100 }
101 }
102 if pick == nil {
103 if kept != nil && len(s.Windows) > 0 {
104 // The one window that could have gone on the plane fades out
105 // after its close: composed from its kept content.
106 for i := range s.Windows {
107 if w := &s.Windows[i]; !w.Hidden && w.Rect.W > 0 && w.Rect.H > 0 && kept(w.ID) {
108 return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "leaving"
109 }
110 }
111 }
112 return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "no_candidate"
113 }
114 if s.Dim > 0 {
115 // The renderer composes the veil (under the first float, or under
116 // every window with DimBehind): a window on the plane would skip it.
117 return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "dim"
118 }
119 if len(s.DropHints) > 0 {
120 // Drag hints are drawn over the windows.
121 return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "drop_hint"
122 }
123 for _, l := range s.Layers {
124 if l.Layer >= ports.LayerTop && l.Rect.W > 0 && l.Rect.H > 0 {
125 return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "layer_above"
126 }
127 }
128 if !pick.Fullscreen && len(s.Separators) > 0 {
129 // The overlay shows the buffer alone; the lines would go with it.
130 return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "border"
131 }
132 if clip := s.WorkspaceClip; clip != (ports.Rect{}) &&
133 (pick.Rect.X < clip.X || pick.Rect.Y < clip.Y || pick.Rect.X+pick.Rect.W > clip.X+clip.W || pick.Rect.Y+pick.Rect.H > clip.Y+clip.H) {
134 // The plane cannot apply the workspace's logical viewport crop.
135 return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "workspace_clip"
136 }
137 c := surfaces[pick.ID]
138 pw, ph := float64(pick.Rect.W)*scale, float64(pick.Rect.H)*scale
139 if float64(c.Width) != pw || float64(c.Height) != ph || c.Geometry != (ports.Rect{}) && (c.Geometry.X != 0 || c.Geometry.Y != 0) {
140 return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "scaled"
141 }
142 x, y := float64(pick.Rect.X)*scale, float64(pick.Rect.Y)*scale
143 if x != float64(int(x)) || y != float64(int(y)) {
144 return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "fractional_position"
145 }
146 return *pick, c, mode, ""
147}
148
149// overlayProps puts ov on the overlay plane, or turns it off.
150func (o *Output) overlayProps(req *atomicReq, ov overlayWin) {
151 p := o.overlay
152 if p == nil {
153 return
154 }
155 if ov.fb == 0 {
156 req.set(p.id, p.prop("FB_ID"), 0)
157 req.set(p.id, p.prop("CRTC_ID"), 0)
158 o.overlayColorProps(req, colorBypass)
159 return
160 }
161 o.overlayColorProps(req, ov.color.mode)
162 req.set(p.id, p.prop("FB_ID"), uint64(ov.fb))
163 req.set(p.id, p.prop("CRTC_ID"), uint64(o.crtc))
164 req.set(p.id, p.prop("SRC_X"), 0)
165 req.set(p.id, p.prop("SRC_Y"), 0)
166 req.set(p.id, p.prop("SRC_W"), uint64(ov.w)<<16)
167 req.set(p.id, p.prop("SRC_H"), uint64(ov.h)<<16)
168 req.set(p.id, p.prop("CRTC_X"), uint64(int64(ov.rect.X)))
169 req.set(p.id, p.prop("CRTC_Y"), uint64(int64(ov.rect.Y)))
170 req.set(p.id, p.prop("CRTC_W"), uint64(ov.rect.W))
171 req.set(p.id, p.prop("CRTC_H"), uint64(ov.rect.H))
172 if ov.acquire != nil {
173 // KMS waits for the client's GPU work before scanning it out.
174 req.set(p.id, p.prop("IN_FENCE_FD"), uint64(ov.acquire.Fd()))
175 }
176}
177
178// close releases the frame's duplicate of the acquire fence.
179func (ov overlayWin) close() {
180 if ov.acquire != nil {
181 ov.acquire.Close()
182 }
183}
184
185// testOverlay asks KMS whether it takes the frame with ov on the overlay,
186// the cursor at its current place included. A refusal is cached on the
187// buffer; one that the plane's colour pipeline causes (the same frame passes
188// with Bypass) is cached on the plane for the buffer format and the cursor
189// state instead.
190func (o *Output) testOverlay(fb uint32, ov overlayWin) bool {
191 err := o.probeOverlay(fb, ov, ov.color.mode)
192 if err == nil {
193 return true
194 }
195 if ov.color.mode != colorBypass && refused(err) && o.probeOverlay(fb, ov, colorBypass) == nil {
196 o.overlay.setVerdict(ov.color.format, o.cursorShown(), false)
197 o.setOverlayReason("color_refused")
198 o.renderLog.Info().Err(err).Uint32("format", ov.color.format).Str("connector", o.conn.name).Msg("overlay colour pipeline refused")
199 return false
200 }
201 if cfb := o.clientFBs[ov.buf]; cfb != nil && refused(err) {
202 cfb.overlayFailed = "overlay_refused"
203 }
204 o.setOverlayReason("overlay_refused")
205 o.renderLog.Info().Err(err).Str("connector", o.conn.name).Msg("overlay refused")
206 return false
207}
208
209// probeOverlay is a TEST_ONLY of the frame: the composed image on the
210// primary plane (Bypass), ov on the overlay with colour mode, the cursor.
211// Both planes' colour state is written whatever they applied: the test must
212// not rely on the state of the last commit.
213func (o *Output) probeOverlay(fb uint32, ov overlayWin, mode colorMode) error {
214 req := &o.probeReq // no frame request is alive: the frame commit follows
215 req.reset()
216 o.primaryProps(req, fb)
217 o.primary.colorProps(req, colorBypass, 0, 0)
218 o.overlayProps(req, ov)
219 o.overlay.colorProps(req, mode, o.colorMult, o.ctmBlob)
220 if o.cursor != nil {
221 o.cursor.props(req, o.crtc, o.cursor.desired())
222 }
223 return o.k.commit(req, atomicTestOnly, 0)
224}
225
226// errOverlayDropped: a commit was refused with the overlay on; the frame is
227// composed again without it.
228var errOverlayDropped = errors.New("overlay dropped")
229
230// overlayConflict handles a commit refused with buffer buf on the overlay:
231// the buffer leaves the overlay for good (e.g. the cursor moved where the
232// plane forbids it) and the frame is composed. It reports whether it did.
233func (o *Output) overlayConflict(err error, buf uint64) bool {
234 if buf == 0 || !errors.Is(err, unix.EINVAL) {
235 return false
236 }
237 if cfb := o.clientFBs[buf]; cfb != nil {
238 cfb.overlayFailed = "cursor_conflict"
239 }
240 o.setOverlayReason("cursor_conflict")
241 return true
242}