1package drm
2
3import (
4 "errors"
5 "math"
6 "os"
7 "slices"
8 "time"
9 "unsafe"
10
11 "github.com/bnema/neferwl/internal/ports"
12 "golang.org/x/sys/unix"
13)
14
15// Direct scanout (ADR 006): when one fullscreen window covers the output
16// with a GPU buffer whose source can fill the plane, the output flips to that buffer
17// instead of compositing it. The buffer becomes a KMS framebuffer once
18// (PRIME import + ADDFB2) and is cached by DMABuf ID while the client
19// keeps using it. KMS waits on the buffer's implicit fences before showing
20// it. Anything else on screen or a buffer KMS refuses falls back to composition.
21
22const (
23 ioctlGemClose = 0x40086409
24 ioctlPrimeFDToHandle = 0xC00C642E
25 ioctlAddFB2 = 0xC06864B8
26
27 fbModifiers = 1 << 1
28 capAddFB2Modifiers = 0x10
29 modInvalid = 0x00ffffffffffffff
30 scanoutIdleTTL = 5 * time.Second // a cached framebuffer survives unused
31)
32
33type primeHandle struct {
34 handle, flags uint32
35 fd int32
36}
37
38type gemClose struct{ handle, pad uint32 }
39
40type fbCmd2 struct {
41 fbID, width, height, format, flags uint32
42 handles, pitches, offsets [4]uint32
43 modifiers [4]uint64
44}
45
46// planeRect contains source coordinates in 16.16 and destination pixels.
47type planeRect struct {
48 src [4]uint64
49 crtc [4]uint64
50}
51
52func fullPlaneRect(w, h int) planeRect {
53 return planeRect{src: [4]uint64{0, 0, uint64(w) << 16, uint64(h) << 16}, crtc: [4]uint64{0, 0, uint64(w), uint64(h)}}
54}
55
56func scanoutRect(c ports.SurfaceContent, w, h int) planeRect {
57 r := fullPlaneRect(w, h)
58 r.src = [4]uint64{0, 0, uint64(c.Width) << 16, uint64(c.Height) << 16}
59 if c.Source[2] != 0 {
60 for i, value := range c.Source {
61 r.src[i] = uint64(float64(value) * 65536)
62 }
63 }
64 return r
65}
66
67// covers reports whether win fills the output of scene s.
68func covers(s *ports.Scene, win *ports.SceneWindow) bool {
69 return (s.WorkspaceClip == (ports.Rect{}) || s.WorkspaceClip == (ports.Rect{W: s.OutputWidth, H: s.OutputHeight})) &&
70 win.Rect.X == 0 && win.Rect.Y == 0 && win.Rect.W == s.OutputWidth && win.Rect.H == s.OutputHeight
71}
72
73// fullscreenShown reports whether a visible fullscreen window covers the
74// output, whether its frame is scanned out or composed.
75func fullscreenShown(s *ports.Scene) bool {
76 for i := range s.Windows {
77 win := &s.Windows[i]
78 if win.Fullscreen && !win.Hidden && covers(s, win) {
79 return true
80 }
81 }
82 return false
83}
84
85// scanoutCandidate returns the window content the output can scan out
86// directly, or a reason why it cannot. w, h are the output's physical size.
87// The plane's formats are checked by scanoutFB.
88func scanoutCandidate(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent, w, h int) (ports.SurfaceContent, string) {
89 if s.WorkspaceClip != (ports.Rect{}) && s.WorkspaceClip != (ports.Rect{W: s.OutputWidth, H: s.OutputHeight}) {
90 return ports.SurfaceContent{}, "workspace_clip"
91 }
92 var full *ports.SceneWindow
93 for i := range s.Windows {
94 win := &s.Windows[i]
95 if win.Hidden || win.Rect.W <= 0 || win.Rect.H <= 0 {
96 continue
97 }
98 if !win.Fullscreen || !covers(&s, win) || full != nil {
99 return ports.SurfaceContent{}, "other_windows"
100 }
101 full = win
102 }
103 if full == nil {
104 return ports.SurfaceContent{}, "no_fullscreen"
105 }
106 if len(s.DropHints) > 0 {
107 // Drag hints are drawn over the window.
108 return ports.SurfaceContent{}, "drop_hint"
109 }
110 if full.Dim > 0 {
111 // Its veil is composed over it: the plane would show it bright.
112 return ports.SurfaceContent{}, "dim"
113 }
114 // Core leaves the layers a fullscreen window hides out of the scene:
115 // one above the windows here is drawn.
116 for _, l := range s.Layers {
117 if l.Layer >= ports.LayerTop && l.Rect.W > 0 && l.Rect.H > 0 {
118 return ports.SurfaceContent{}, "overlay_surface"
119 }
120 }
121 c := surfaces[full.ID]
122 if c.DMABuf == nil && len(c.Children) > 0 {
123 var reason string
124 if c, reason = clientSubsurface(c); reason != "" {
125 return c, reason
126 }
127 }
128 switch {
129 case c.DMABuf == nil:
130 return c, "not_dmabuf"
131 case c.Fade > 0:
132 // The plane would show it unfaded (wp_alpha_modifier_v1).
133 return c, "fade"
134 case len(c.Children) > 0:
135 return c, "subsurfaces"
136 case c.Transform != 0:
137 // Planes show buffers as they are: rotation is composed.
138 return c, "buffer_transform"
139 case c.LogicalW != full.Rect.W || c.LogicalH != full.Rect.H:
140 return c, "letterbox"
141 case c.Width <= 0 || c.Height <= 0 || c.Source[2] < 0 || c.Source[3] < 0 || !finiteSource(c.Source):
142 return c, "source_crop"
143 case c.Geometry != (ports.Rect{}) && (c.Geometry.X != 0 || c.Geometry.Y != 0 || c.Geometry.W != c.LogicalW || c.Geometry.H != c.LogicalH):
144 return c, "geometry_crop"
145 }
146 if c.Source[2] != 0 && (c.Source[0] < 0 || c.Source[1] < 0 || c.Source[2] <= 0 || c.Source[3] <= 0 || c.Source[0]+c.Source[2] > float32(c.Width) || c.Source[1]+c.Source[3] > float32(c.Height)) {
147 return c, "source_crop"
148 }
149 return c, ""
150}
151
152func finiteSource(src [4]float32) bool {
153 for _, n := range src {
154 if math.IsNaN(float64(n)) || math.IsInf(float64(n), 0) {
155 return false
156 }
157 }
158 return true
159}
160
161// cropped reports whether a viewport source shows less than the buffer.
162func cropped(c ports.SurfaceContent) bool {
163 return c.Source[2] != 0 && c.Source != [4]float32{0, 0, float32(c.Width), float32(c.Height)}
164}
165
166// clientSubsurface returns the content to scan out for a window whose
167// root has no GPU buffer but one opaque subsurface over its geometry: Wine
168// draws the frame in the root (wl_shm) and presents Vulkan in a subsurface
169// on the client area. That subsurface hides the root entirely, so it is
170// what the screen shows. The returned content keeps the window's ID and
171// Seq for release and presentation.
172func clientSubsurface(c ports.SurfaceContent) (ports.SurfaceContent, string) {
173 if len(c.Children) != 1 {
174 return c, "subsurfaces"
175 }
176 ch := c.Children[0]
177 switch {
178 case ch.DMABuf == nil:
179 return c, "not_dmabuf"
180 case !ch.Opaque:
181 return c, "subsurface_translucent"
182 case ch.Below:
183 // The root is drawn over it.
184 return c, "subsurface_below"
185 case ch.X != c.Geometry.X || ch.Y != c.Geometry.Y:
186 return c, "subsurface_offset"
187 }
188 out := ch.SurfaceContent
189 out.ID, out.Seq, out.Geometry = c.ID, c.Seq, ports.Rect{}
190 return out, ""
191}
192
193// scanoutFrame picks a fullscreen client buffer to flip directly. It
194// returns fb 0, with the reason logged on change, when the frame must be
195// composed.
196func (o *Output) scanoutFrame(scene ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent) (fb uint32, c ports.SurfaceContent) {
197 reason := "disabled"
198 if o.scanout {
199 c, reason = scanoutCandidate(scene, surfaces, o.Width(), o.Height())
200 if reason == "" && isYUVFormat(c.DMABuf.Format) {
201 reason = "yuv"
202 }
203 if o.hdr.on && reason == "" {
204 switch {
205 case !c.Color.IsPQ2020():
206 reason = "hdr_sdr_content"
207 case !isTenBit(c.DMABuf.Format):
208 reason = "hdr_format"
209 }
210 } else if !o.hdr.on && reason == "" && c.Color.IsPQ2020() {
211 // An SDR connector must never show raw PQ values.
212 reason = "sdr_pq_content"
213 }
214 }
215 if reason == "" {
216 fb, reason = o.scanoutFB(c.DMABuf, time.Now())
217 if reason == "" {
218 rect := scanoutRect(c, o.Width(), o.Height())
219 cfb := o.clientFBs[c.DMABuf.ID]
220 if cfb.scaleRefused && cfb.scaleFailed == rect {
221 reason = "scale_refused"
222 } else if rect != fullPlaneRect(o.Width(), o.Height()) && (!cfb.scaleTestedOK || cfb.scaleTested != rect) {
223 req := &atomicReq{}
224 o.primaryRectProps(req, fb, rect)
225 if err := o.k.commit(req, atomicTestOnly, 0); err != nil {
226 cfb.scaleFailed = rect
227 cfb.scaleRefused = true
228 reason = "scale_refused"
229 o.log.Info().Str("component", "render").Err(err).Str("reason", reason).Str("connector", o.conn.name).Msg("scanout scaling refused")
230 } else {
231 cfb.scaleTested = rect
232 cfb.scaleTestedOK = true
233 }
234 }
235 }
236 }
237 o.setScanoutReason(reason)
238 if reason != "" {
239 return 0, c
240 }
241 return fb, c
242}
243
244// overlayFrame decides the overlay of a frame: the window, and the scene
245// the renderer composes (the window left out). A zero overlayWin means
246// none; the reason is logged on change.
247func (o *Output) overlayFrame(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent) (overlayWin, ports.Scene) {
248 reason := "no_plane"
249 var ov overlayWin
250 if o.hdr.on {
251 reason = "hdr"
252 } else if o.overlay != nil && o.scanout {
253 var w ports.SceneWindow
254 var c ports.SurfaceContent
255 w, c, reason = overlayCandidate(s, surfaces)
256 if reason == "" {
257 var fb uint32
258 if fb, reason = o.overlayFB(c.DMABuf, time.Now()); reason == "" {
259 scale := s.Scale
260 if scale <= 0 {
261 scale = 1
262 }
263 ov = overlayWin{id: w.ID, fb: fb, buf: c.DMABuf.ID, w: c.Width, h: c.Height,
264 rect: ports.Rect{X: int(float64(w.Rect.X) * scale), Y: int(float64(w.Rect.Y) * scale), W: c.Width, H: c.Height},
265 acquire: dupFence(c.Acquire)}
266 }
267 }
268 }
269 o.setOverlayReason(reason)
270 if ov.fb == 0 {
271 return overlayWin{}, s
272 }
273 // The composed frame leaves the window out: the overlay shows it.
274 rest := s
275 rest.Windows = make([]ports.SceneWindow, 0, len(s.Windows))
276 for _, w := range s.Windows {
277 if w.ID != ov.id {
278 rest.Windows = append(rest.Windows, w)
279 }
280 }
281 return ov, rest
282}
283
284// frameDecision selects the zero-copy path or the scene to compose.
285type frameDecision struct {
286 fb uint32
287 content ports.SurfaceContent
288 rect planeRect
289 overlay overlayWin
290 composed ports.Scene
291}
292
293// composeFrame chooses overlay or composition after direct scanout cannot commit.
294// The caller owns the returned overlay fence.
295func (o *Output) composeFrame(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent) frameDecision {
296 ov, rest := o.overlayFrame(s, surfaces)
297 if ov.fb != 0 {
298 return frameDecision{overlay: ov, composed: rest}
299 }
300 return frameDecision{composed: s}
301}
302
303// decideFrame tries direct scanout, then overlay, then composition.
304// Captures require composition; the caller owns the returned overlay fence.
305func (o *Output) decideFrame(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent, capture bool) frameDecision {
306 if capture {
307 o.setScanoutReason("capture")
308 o.setOverlayReason("capture")
309 return frameDecision{composed: s}
310 }
311 fb, c := o.scanoutFrame(s, surfaces)
312 if fb != 0 {
313 return frameDecision{fb: fb, content: c, rect: scanoutRect(c, o.Width(), o.Height())}
314 }
315 return o.composeFrame(s, surfaces)
316}
317
318// setScanoutReason logs a direct-scanout transition once.
319func (o *Output) setScanoutReason(reason string) {
320 if reason != o.reason {
321 o.log.Info().Str("component", "render").Bool("direct_scanout", reason == "").Str("reason", reason).Str("connector", o.conn.name).Msg("scanout")
322 o.reason = reason
323 }
324}
325
326// setOverlayReason logs an overlay transition once.
327func (o *Output) setOverlayReason(reason string) {
328 if reason != o.overlayReason {
329 o.log.Info().Str("component", "render").Bool("overlay", reason == "").Str("reason", reason).Str("connector", o.conn.name).Msg("overlay")
330 o.overlayReason = reason
331 }
332}
333
334// clientFB is a client buffer imported as a KMS framebuffer.
335type clientFB struct {
336 fbID uint32
337 last time.Time // when last wanted on screen
338 // failed is the reason KMS refused the buffer; it is not retried.
339 failed string
340 // noAsync: KMS refused an async flip to it; it flips at vblank.
341 noAsync bool
342 scaleTested, scaleFailed planeRect
343 scaleRefused, scaleTestedOK bool
344 // overlayFailed is why the overlay plane refused it; kept apart from
345 // failed, so a refusal on one plane does not bar the other.
346 overlayFailed string
347 importErr bool
348}
349
350// scanoutFB returns the framebuffer of a client buffer for the primary
351// plane, importing it on first use. A buffer KMS refuses is remembered
352// with its reason.
353func (o *Output) scanoutFB(b *ports.DMABuf, now time.Time) (uint32, string) {
354 return o.planeFB(o.primary, b, now, func(fb *clientFB) *string { return &fb.failed })
355}
356
357// overlayFB is scanoutFB for the overlay plane, with its own formats and
358// refusal reason.
359func (o *Output) overlayFB(b *ports.DMABuf, now time.Time) (uint32, string) {
360 return o.planeFB(o.overlay, b, now, func(fb *clientFB) *string { return &fb.overlayFailed })
361}
362
363// planeFB imports b once as a framebuffer; reason is the plane's refusal
364// field. Both planes share the framebuffer: they show the same format.
365func (o *Output) planeFB(p *plane, b *ports.DMABuf, now time.Time, reason func(*clientFB) *string) (uint32, string) {
366 fb := o.clientFBs[b.ID]
367 if fb == nil {
368 fb = &clientFB{}
369 o.clientFBs[b.ID] = fb
370 }
371 fb.last = now
372 why := reason(fb)
373 if *why != "" {
374 return 0, *why
375 }
376 format, ok := planeFormat(p, b)
377 if !ok {
378 *why = "format"
379 return 0, *why
380 }
381 if fb.fbID == 0 {
382 if fb.importErr {
383 *why = "import_failed"
384 return 0, *why
385 }
386 id, err := o.k.addFB(b, format)
387 if err != nil {
388 o.log.Info().Str("component", "render").Err(err).Uint32("format", b.Format).Uint64("modifier", b.Modifier).Msg("scanout import failed")
389 fb.importErr, *why = true, "import_failed"
390 return 0, *why
391 }
392 fb.fbID = id
393 }
394 return fb.fbID, ""
395}
396
397// scanoutFormat is the framebuffer format a client buffer is scanned out
398// as on the primary plane (see planeFormat).
399func (o *Output) scanoutFormat(b *ports.DMABuf) (uint32, bool) { return planeFormat(o.primary, b) }
400
401// planeFormat is the framebuffer format a client buffer shows as on p,
402// when p lists it with the buffer's modifier. Planes here show no alpha:
403// an alpha format goes as its X variant when the plane has that one.
404func planeFormat(p *plane, b *ports.DMABuf) (uint32, bool) {
405 has := func(f uint32) bool {
406 return slices.Contains(p.formats, ports.DMABufFormat{Format: f, Modifier: b.Modifier})
407 }
408 if x, ok := opaqueVariant[b.Format]; ok && has(x) {
409 return x, true
410 }
411 return b.Format, has(b.Format)
412}
413
414// opaqueVariant maps alpha formats to the same layout without alpha.
415var opaqueVariant = map[uint32]uint32{
416 fourccARGB: fourccXRGB,
417 fourccABGR: fourccXBGR,
418 fourccAR30: fourccXR30,
419 fourccAB30: fourccXB30,
420 fourccAR4H: fourccXR4H,
421 fourccAB4H: fourccXB4H,
422}
423
424// scanoutFormats are the formats a buffer can be scanned out with: in the
425// primary plane's IN_FORMATS (directly or as its opaque variant) and
426// composable by the renderer.
427func (o *Output) scanoutFormats(sampled []ports.DMABufFormat) []ports.DMABufFormat {
428 var out []ports.DMABufFormat
429 for _, f := range sampled {
430 b := ports.DMABuf{Format: f.Format, Modifier: f.Modifier}
431 if _, ok := o.scanoutFormat(&b); ok {
432 out = append(out, f)
433 }
434 }
435 return out
436}
437
438// isYUVFormat identifies video formats which are always composed, never scanned out.
439func isYUVFormat(format uint32) bool {
440 return format == fourccNV12 || format == fourccP010
441}
442
443// isTenBit identifies the packed 2101010 formats supported by the renderer.
444func isTenBit(format uint32) bool {
445 switch format {
446 case fourccXR30, fourccAR30, fourccXB30, fourccAB30:
447 return true
448 }
449 return false
450}
451
452const (
453 fourccNV12 = 'N' | 'V'<<8 | '1'<<16 | '2'<<24
454 fourccP010 = 'P' | '0'<<8 | '1'<<16 | '0'<<24
455 fourccXRGB = 'X' | 'R'<<8 | '2'<<16 | '4'<<24
456 fourccARGB = 'A' | 'R'<<8 | '2'<<16 | '4'<<24
457 fourccXBGR = 'X' | 'B'<<8 | '2'<<16 | '4'<<24
458 fourccABGR = 'A' | 'B'<<8 | '2'<<16 | '4'<<24
459 fourccXR30 = 'X' | 'R'<<8 | '3'<<16 | '0'<<24
460 fourccAR30 = 'A' | 'R'<<8 | '3'<<16 | '0'<<24
461 fourccXB30 = 'X' | 'B'<<8 | '3'<<16 | '0'<<24
462 fourccAB30 = 'A' | 'B'<<8 | '3'<<16 | '0'<<24
463 fourccXR4H = 'X' | 'R'<<8 | '4'<<16 | 'H'<<24
464 fourccAR4H = 'A' | 'R'<<8 | '4'<<16 | 'H'<<24
465 fourccXB4H = 'X' | 'B'<<8 | '4'<<16 | 'H'<<24
466 fourccAB4H = 'A' | 'B'<<8 | '4'<<16 | 'H'<<24
467)
468
469// addFB imports a dmabuf as a framebuffer of the given fourcc: client buffers, the renderer's
470// output images and cursor images. The GEM handle belongs to the card fd
471// and is shared by every import of the same buffer, so import, ADDFB2 and
472// close run under the lock and the handle is closed right away: the
473// framebuffer keeps its own reference to the buffer.
474func (k kmsDevice) addFB(b *ports.DMABuf, format uint32) (uint32, error) {
475 raw, err := b.Planes[0].File.SyscallConn()
476 if err != nil {
477 return 0, err
478 }
479 k.gemMu.Lock()
480 defer k.gemMu.Unlock()
481 var p primeHandle
482 var ioErr error
483 if err := raw.Control(func(fd uintptr) {
484 p.fd = int32(fd)
485 ioErr = ioctl(k.fd, ioctlPrimeFDToHandle, unsafe.Pointer(&p))
486 }); err != nil {
487 return 0, err
488 }
489 if ioErr != nil {
490 return 0, errors.Join(errors.New("prime import"), ioErr)
491 }
492 defer func() {
493 g := gemClose{handle: p.handle}
494 _ = ioctl(k.fd, ioctlGemClose, unsafe.Pointer(&g))
495 }()
496 cmd := fbCmd2{width: uint32(b.Width), height: uint32(b.Height), format: format}
497 useMod := b.Modifier != modInvalid && (b.Modifier != 0 || k.modifiers)
498 if useMod {
499 cmd.flags = fbModifiers
500 }
501 // Planes in the same dmabuf share its GEM handle: each distinct
502 // handle is closed once.
503 closing := map[uint32]bool{p.handle: true}
504 for i, pl := range b.Planes {
505 if i >= len(cmd.handles) {
506 break
507 }
508 h := p.handle
509 if i > 0 {
510 var err error
511 if h, err = k.primeImport(pl.File); err != nil {
512 return 0, err
513 }
514 if !closing[h] {
515 closing[h] = true
516 defer k.gemClose(h)
517 }
518 }
519 cmd.handles[i], cmd.pitches[i], cmd.offsets[i] = h, pl.Stride, pl.Offset
520 if useMod {
521 cmd.modifiers[i] = b.Modifier
522 }
523 }
524 if err := ioctl(k.fd, ioctlAddFB2, unsafe.Pointer(&cmd)); err != nil {
525 return 0, errors.Join(errors.New("add fb2"), err)
526 }
527 return cmd.fbID, nil
528}
529
530// primeImport returns the GEM handle of a dmabuf file. The caller holds
531// gemMu and closes the handle.
532func (k kmsDevice) primeImport(f *os.File) (uint32, error) {
533 raw, err := f.SyscallConn()
534 if err != nil {
535 return 0, err
536 }
537 var p primeHandle
538 var ioErr error
539 if err := raw.Control(func(fd uintptr) {
540 p.fd = int32(fd)
541 ioErr = ioctl(k.fd, ioctlPrimeFDToHandle, unsafe.Pointer(&p))
542 }); err != nil {
543 return 0, err
544 }
545 if ioErr != nil {
546 return 0, errors.Join(errors.New("prime import"), ioErr)
547 }
548 return p.handle, nil
549}
550
551func (k kmsDevice) gemClose(h uint32) {
552 g := gemClose{handle: h}
553 _ = ioctl(k.fd, ioctlGemClose, unsafe.Pointer(&g))
554}
555
556// dropClientFBs frees framebuffers not wanted for scanoutIdleTTL, or all
557// of them, except those on screen or queued.
558func (o *Output) dropClientFBs(now time.Time, all bool) {
559 for id, fb := range o.clientFBs {
560 if id == o.shown || id == o.queued {
561 continue
562 }
563 if !all && now.Sub(fb.last) < scanoutIdleTTL {
564 continue
565 }
566 if fb.fbID != 0 {
567 _ = o.k.rmFB(fb.fbID)
568 }
569 delete(o.clientFBs, id)
570 }
571}
572
573// dupFence duplicates a sync file under its SyscallConn, so a concurrent
574// Close cannot hand us a reused fd; nil when f is nil or closed.
575func dupFence(f *os.File) *os.File {
576 if f == nil {
577 return nil
578 }
579 raw, err := f.SyscallConn()
580 if err != nil {
581 return nil
582 }
583 fd := -1
584 if raw.Control(func(v uintptr) { fd, _ = unix.FcntlInt(v, unix.F_DUPFD_CLOEXEC, 0) }) != nil || fd < 0 {
585 return nil
586 }
587 return os.NewFile(uintptr(fd), "acquire-fence")
588}