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.
88//
89// kept reports a window drawn from a content its client withdrew (it
90// closed and fades out): the client may already have its buffer back, so
91// it is never put on a plane (nil: none is).
92func scanoutCandidate(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent, w, h int, kept func(ports.WindowID) bool) (ports.SurfaceContent, string) {
93 // A hardware plane would bypass the compositor-owned flash border.
94 if s.PositionHint.Shown() {
95 return ports.SurfaceContent{}, "position_hint"
96 }
97 if s.Outline != nil {
98 return ports.SurfaceContent{}, "selection_outline"
99 }
100 if s.Flash != nil {
101 return ports.SurfaceContent{}, "focus_flash"
102 }
103 // Hint labels are composed over the window.
104 if len(s.HintLabels) > 0 {
105 return ports.SurfaceContent{}, "hint_labels"
106 }
107 if s.Transform != 0 {
108 // The scanout buffer would have to be pre-rotated: rotated outputs compose.
109 return ports.SurfaceContent{}, "output_transform"
110 }
111 if s.WorkspaceClip != (ports.Rect{}) && s.WorkspaceClip != (ports.Rect{W: s.OutputWidth, H: s.OutputHeight}) {
112 return ports.SurfaceContent{}, "workspace_clip"
113 }
114 var full *ports.SceneWindow
115 for i := range s.Windows {
116 win := &s.Windows[i]
117 if !s.Draws(*win) {
118 continue
119 }
120 if !win.Fullscreen || !covers(&s, win) || full != nil {
121 return ports.SurfaceContent{}, "other_windows"
122 }
123 full = win
124 }
125 if full == nil {
126 return ports.SurfaceContent{}, "no_fullscreen"
127 }
128 if kept != nil && kept(full.ID) {
129 return ports.SurfaceContent{}, "leaving"
130 }
131 if len(s.DropHints) > 0 {
132 // Drag hints are drawn over the window.
133 return ports.SurfaceContent{}, "drop_hint"
134 }
135 if full.Dim > 0 {
136 // Its veil is composed over it: the plane would show it bright.
137 return ports.SurfaceContent{}, "dim"
138 }
139 if full.Radius > 0 || full.Fade > 0 || full.Preview > 0 || full.Zoom > 0 {
140 // A fading or zoomed window is composed: the plane would show it
141 // whole and opaque.
142 return ports.SurfaceContent{}, "window_fade"
143 }
144 // Core leaves the layers a fullscreen window hides out of the scene:
145 // one above the windows here is drawn.
146 for _, l := range s.Layers {
147 if l.Layer >= ports.LayerTop && l.Rect.W > 0 && l.Rect.H > 0 {
148 return ports.SurfaceContent{}, "overlay_surface"
149 }
150 }
151 c := surfaces[full.ID]
152 if c.DMABuf == nil && len(c.Children) > 0 {
153 var reason string
154 if c, reason = clientSubsurface(c); reason != "" {
155 return c, reason
156 }
157 }
158 switch {
159 case c.DMABuf == nil:
160 return c, "not_dmabuf"
161 case c.Fade > 0:
162 // The plane would show it unfaded (wp_alpha_modifier_v1).
163 return c, "fade"
164 case len(c.Children) > 0:
165 return c, "subsurfaces"
166 case c.Transform != 0:
167 // Planes show buffers as they are: rotation is composed.
168 return c, "buffer_transform"
169 case c.LogicalW != full.Rect.W || c.LogicalH != full.Rect.H:
170 return c, "letterbox"
171 case c.Width <= 0 || c.Height <= 0 || c.Source[2] < 0 || c.Source[3] < 0 || !finiteSource(c.Source):
172 return c, "source_crop"
173 case c.Geometry != (ports.Rect{}) && (c.Geometry.X != 0 || c.Geometry.Y != 0 || c.Geometry.W != c.LogicalW || c.Geometry.H != c.LogicalH):
174 return c, "geometry_crop"
175 }
176 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)) {
177 return c, "source_crop"
178 }
179 return c, ""
180}
181
182func finiteSource(src [4]float32) bool {
183 for _, n := range src {
184 if math.IsNaN(float64(n)) || math.IsInf(float64(n), 0) {
185 return false
186 }
187 }
188 return true
189}
190
191// cropped reports whether a viewport source shows less than the buffer.
192func cropped(c ports.SurfaceContent) bool {
193 return c.Source[2] != 0 && c.Source != [4]float32{0, 0, float32(c.Width), float32(c.Height)}
194}
195
196// clientSubsurface returns the content to scan out for a window whose
197// root has no GPU buffer but one opaque subsurface over its geometry: Wine
198// draws the frame in the root (wl_shm) and presents Vulkan in a subsurface
199// on the client area. That subsurface hides the root entirely, so it is
200// what the screen shows. The returned content keeps the window's ID and
201// Seq for release and presentation.
202func clientSubsurface(c ports.SurfaceContent) (ports.SurfaceContent, string) {
203 if len(c.Children) != 1 {
204 return c, "subsurfaces"
205 }
206 ch := c.Children[0]
207 switch {
208 case ch.DMABuf == nil:
209 return c, "not_dmabuf"
210 case !ch.Opaque:
211 return c, "subsurface_translucent"
212 case ch.Below:
213 // The root is drawn over it.
214 return c, "subsurface_below"
215 case ch.X != c.Geometry.X || ch.Y != c.Geometry.Y:
216 return c, "subsurface_offset"
217 }
218 out := ch.SurfaceContent
219 out.ID, out.Seq, out.Geometry = c.ID, c.Seq, ports.Rect{}
220 return out, ""
221}
222
223// scanoutFrame picks a fullscreen client buffer to flip directly. It
224// returns fb 0, with the reason logged on change, when the frame must be
225// composed.
226func (o *Output) scanoutFrame(scene ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent) (fb uint32, c ports.SurfaceContent, mode colorMode) {
227 reason := "disabled"
228 if o.scanout {
229 c, reason = scanoutCandidate(scene, surfaces, o.Width(), o.Height(), o.kept)
230 if reason == "" {
231 mode, reason = planeColor(c.Color, c.DMABuf.Format, c.Opaque, o.hdr.on, o.primary.pipeline)
232 }
233 }
234 if reason == "" && !o.clientFenceReady(c.Acquire) {
235 reason = "fence_pending"
236 }
237 if reason == "" {
238 fb, reason = o.scanoutFB(c.DMABuf, time.Now())
239 if reason == "" {
240 rect := scanoutRect(c, o.Width(), o.Height())
241 cfb := o.clientFBs[c.DMABuf.ID]
242 if cfb.scaleRefused && cfb.scaleFailed == rect {
243 reason = "scale_refused"
244 } else if rect != fullPlaneRect(o.Width(), o.Height()) && (!cfb.scaleTestedOK || cfb.scaleTested != rect) {
245 req := &o.probeReq
246 req.reset()
247 o.primaryRectProps(req, fb, rect)
248 if err := o.k.commit(req, atomicTestOnly, 0); err != nil {
249 cfb.scaleFailed = rect
250 cfb.scaleRefused = true
251 reason = "scale_refused"
252 o.renderLog.Info().Err(err).Str("reason", reason).Str("connector", o.conn.name).Msg("scanout scaling refused")
253 } else {
254 cfb.scaleTested = rect
255 cfb.scaleTestedOK = true
256 }
257 }
258 if reason == "" && mode != colorBypass && !o.colorAllowed(o.primary, c.DMABuf.Format, func(req *atomicReq) {
259 o.primaryRectProps(req, fb, rect)
260 if o.overlayOn != 0 {
261 o.overlayProps(req, overlayWin{})
262 }
263 }) {
264 reason = "color_refused"
265 }
266 }
267 }
268 o.setScanoutReason(reason)
269 if reason != "" {
270 return 0, c, colorBypass
271 }
272 return fb, c, mode
273}
274
275// overlayFrame decides the overlay of a frame: the window, and the scene
276// the renderer composes (the window left out). A zero overlayWin means
277// none; the reason is logged on change. The returned scene's Windows is
278// valid until the next call: it reuses o.restWindows (its consumers, Render,
279// ExcludedScene and the capture tracking, run synchronously before then).
280func (o *Output) overlayFrame(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent) (overlayWin, ports.Scene) {
281 reason := "no_plane"
282 var ov overlayWin
283 if o.overlay != nil && o.scanout {
284 var w ports.SceneWindow
285 var c ports.SurfaceContent
286 var mode colorMode
287 w, c, mode, reason = overlayCandidate(s, surfaces, o.hdr.on, o.overlay.pipeline, o.kept)
288 if reason == "" && mode != colorBypass {
289 if ok, known := o.overlay.verdict(c.DMABuf.Format, o.cursorShown()); known && !ok {
290 reason = "color_refused"
291 }
292 }
293 if reason == "" && !o.clientFenceReady(c.Acquire) {
294 reason = "fence_pending"
295 }
296 if reason == "" {
297 var fb uint32
298 if fb, reason = o.overlayFB(c.DMABuf, time.Now()); reason == "" {
299 scale := s.Scale
300 if scale <= 0 {
301 scale = 1
302 }
303 ov = overlayWin{id: w.ID, fb: fb, buf: c.DMABuf.ID, w: c.Width, h: c.Height,
304 rect: ports.Rect{X: int(float64(w.Rect.X) * scale), Y: int(float64(w.Rect.Y) * scale), W: c.Width, H: c.Height},
305 acquire: dupFence(c.Acquire), color: colorUse{mode: mode, format: c.DMABuf.Format}}
306 }
307 }
308 }
309 o.setOverlayReason(reason)
310 if ov.id == 0 {
311 return overlayWin{}, s
312 }
313 // The composed frame leaves the window out: the overlay shows it.
314 rest := s
315 o.restWindows = o.restWindows[:0]
316 for _, w := range s.Windows {
317 if w.ID != ov.id {
318 o.restWindows = append(o.restWindows, w)
319 }
320 }
321 rest.Windows = o.restWindows
322 return ov, rest
323}
324
325// frameDecision selects the zero-copy path or the scene to compose.
326type frameDecision struct {
327 fb uint32
328 content ports.SurfaceContent
329 rect planeRect
330 color colorMode // how the primary plane shows fb
331 overlay overlayWin
332 composed ports.Scene
333}
334
335// composeFrame chooses overlay or composition after direct scanout cannot commit.
336// The caller owns the returned overlay fence.
337func (o *Output) composeFrame(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent) frameDecision {
338 if time.Now().Before(o.scanoutPausedUntil) {
339 o.setScanoutReason("nomem")
340 o.setOverlayReason("nomem")
341 return frameDecision{composed: s}
342 }
343 ov, rest := o.overlayFrame(s, surfaces)
344 if ov.fb != 0 {
345 return frameDecision{overlay: ov, composed: rest}
346 }
347 return frameDecision{composed: s}
348}
349
350const nomemScanoutPause = 2 * time.Second
351
352// decideFrame tries direct scanout, then overlay, then composition.
353// Captures require composition; the caller owns the returned overlay fence.
354func (o *Output) decideFrame(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent, capture bool) frameDecision {
355 if capture {
356 o.setScanoutReason("capture")
357 o.setOverlayReason("capture")
358 return frameDecision{composed: s}
359 }
360 if time.Now().Before(o.scanoutPausedUntil) {
361 return o.composeFrame(s, surfaces)
362 }
363 fb, c, mode := o.scanoutFrame(s, surfaces)
364 if fb != 0 {
365 return frameDecision{fb: fb, content: c, rect: scanoutRect(c, o.Width(), o.Height()), color: mode}
366 }
367 return o.composeFrame(s, surfaces)
368}
369
370// setScanoutReason logs a direct-scanout transition once.
371func (o *Output) setScanoutReason(reason string) {
372 if reason != o.reason {
373 o.renderLog.Info().Bool("direct_scanout", reason == "").Str("reason", reason).Str("connector", o.conn.name).Msg("scanout")
374 o.reason = reason
375 }
376}
377
378// setOverlayReason logs an overlay transition once.
379func (o *Output) setOverlayReason(reason string) {
380 if reason != o.overlayReason {
381 o.renderLog.Info().Bool("overlay", reason == "").Str("reason", reason).Str("connector", o.conn.name).Msg("overlay")
382 o.overlayReason = reason
383 }
384}
385
386// clientFB is a client buffer imported as a KMS framebuffer.
387type clientFB struct {
388 fbID uint32
389 last time.Time // when last wanted on screen
390 // failed is the reason KMS refused the buffer; it is not retried.
391 failed string
392 // noAsync: KMS refused an async flip to it; it flips at vblank.
393 noAsync bool
394 scaleTested, scaleFailed planeRect
395 scaleRefused, scaleTestedOK bool
396 // overlayFailed is why the overlay plane refused it; kept apart from
397 // failed, so a refusal on one plane does not bar the other.
398 overlayFailed string
399 importErr bool
400}
401
402// scanoutFB returns the framebuffer of a client buffer for the primary
403// plane, importing it on first use. A buffer KMS refuses is remembered
404// with its reason.
405func (o *Output) scanoutFB(b *ports.DMABuf, now time.Time) (uint32, string) {
406 return o.planeFB(o.primary, b, now, func(fb *clientFB) *string { return &fb.failed })
407}
408
409// overlayFB is scanoutFB for the overlay plane, with its own formats and
410// refusal reason.
411func (o *Output) overlayFB(b *ports.DMABuf, now time.Time) (uint32, string) {
412 return o.planeFB(o.overlay, b, now, func(fb *clientFB) *string { return &fb.overlayFailed })
413}
414
415// planeFB imports b once as a framebuffer; reason is the plane's refusal
416// field. Both planes share the framebuffer: they show the same format.
417func (o *Output) planeFB(p *plane, b *ports.DMABuf, now time.Time, reason func(*clientFB) *string) (uint32, string) {
418 fb := o.clientFBs[b.ID]
419 if fb == nil {
420 fb = &clientFB{}
421 o.clientFBs[b.ID] = fb
422 }
423 fb.last = now
424 why := reason(fb)
425 if *why != "" {
426 return 0, *why
427 }
428 format, ok := planeFormat(p, b)
429 if !ok {
430 *why = "format"
431 return 0, *why
432 }
433 if fb.fbID == 0 {
434 if fb.importErr {
435 *why = "import_failed"
436 return 0, *why
437 }
438 id, err := o.k.addFB(b, format)
439 if err != nil {
440 o.renderLog.Info().Err(err).Uint32("format", b.Format).Uint64("modifier", b.Modifier).Msg("scanout import failed")
441 fb.importErr, *why = true, "import_failed"
442 return 0, *why
443 }
444 fb.fbID = id
445 }
446 return fb.fbID, ""
447}
448
449// scanoutFormat is the framebuffer format a client buffer is scanned out
450// as on the primary plane (see planeFormat).
451func (o *Output) scanoutFormat(b *ports.DMABuf) (uint32, bool) { return planeFormat(o.primary, b) }
452
453// planeFormat is the framebuffer format a client buffer shows as on p,
454// when p lists it with the buffer's modifier. Planes here show no alpha:
455// an alpha format goes as its X variant when the plane has that one.
456func planeFormat(p *plane, b *ports.DMABuf) (uint32, bool) {
457 has := func(f uint32) bool {
458 return slices.Contains(p.formats, ports.DMABufFormat{Format: f, Modifier: b.Modifier})
459 }
460 if x, ok := opaqueVariant[b.Format]; ok && has(x) {
461 return x, true
462 }
463 return b.Format, has(b.Format)
464}
465
466// opaqueVariant maps alpha formats to the same layout without alpha.
467var opaqueVariant = map[uint32]uint32{
468 fourccARGB: fourccXRGB,
469 fourccABGR: fourccXBGR,
470 fourccAR30: fourccXR30,
471 fourccAB30: fourccXB30,
472 fourccAR4H: fourccXR4H,
473 fourccAB4H: fourccXB4H,
474}
475
476// scanoutFormats are the formats a buffer can be scanned out with: in the
477// primary plane's IN_FORMATS (directly or as its opaque variant) and
478// composable by the renderer.
479func (o *Output) scanoutFormats(sampled []ports.DMABufFormat) []ports.DMABufFormat {
480 var out []ports.DMABufFormat
481 for _, f := range sampled {
482 b := ports.DMABuf{Format: f.Format, Modifier: f.Modifier}
483 if _, ok := o.scanoutFormat(&b); ok {
484 out = append(out, f)
485 }
486 }
487 return out
488}
489
490// isYUVFormat identifies video formats which are always composed, never scanned out.
491func isYUVFormat(format uint32) bool {
492 return format == fourccNV12 || format == fourccP010
493}
494
495// isTenBit identifies the packed 2101010 formats supported by the renderer.
496func isTenBit(format uint32) bool {
497 switch format {
498 case fourccXR30, fourccAR30, fourccXB30, fourccAB30:
499 return true
500 }
501 return false
502}
503
504const (
505 fourccNV12 = 'N' | 'V'<<8 | '1'<<16 | '2'<<24
506 fourccP010 = 'P' | '0'<<8 | '1'<<16 | '0'<<24
507 fourccXRGB = 'X' | 'R'<<8 | '2'<<16 | '4'<<24
508 fourccARGB = 'A' | 'R'<<8 | '2'<<16 | '4'<<24
509 fourccXBGR = 'X' | 'B'<<8 | '2'<<16 | '4'<<24
510 fourccABGR = 'A' | 'B'<<8 | '2'<<16 | '4'<<24
511 fourccXR30 = 'X' | 'R'<<8 | '3'<<16 | '0'<<24
512 fourccAR30 = 'A' | 'R'<<8 | '3'<<16 | '0'<<24
513 fourccXB30 = 'X' | 'B'<<8 | '3'<<16 | '0'<<24
514 fourccAB30 = 'A' | 'B'<<8 | '3'<<16 | '0'<<24
515 fourccXR4H = 'X' | 'R'<<8 | '4'<<16 | 'H'<<24
516 fourccAR4H = 'A' | 'R'<<8 | '4'<<16 | 'H'<<24
517 fourccXB4H = 'X' | 'B'<<8 | '4'<<16 | 'H'<<24
518 fourccAB4H = 'A' | 'B'<<8 | '4'<<16 | 'H'<<24
519)
520
521// addFB imports a dmabuf as a framebuffer of the given fourcc: client buffers, the renderer's
522// output images and cursor images. The GEM handle belongs to the card fd
523// and is shared by every import of the same buffer, so import, ADDFB2 and
524// close run under the lock and the handle is closed right away: the
525// framebuffer keeps its own reference to the buffer.
526func (k kmsDevice) addFB(b *ports.DMABuf, format uint32) (uint32, error) {
527 raw, err := b.Planes[0].File.SyscallConn()
528 if err != nil {
529 return 0, err
530 }
531 k.gemMu.Lock()
532 defer k.gemMu.Unlock()
533 var p primeHandle
534 var ioErr error
535 if err := raw.Control(func(fd uintptr) {
536 p.fd = int32(fd)
537 ioErr = ioctl(k.fd, ioctlPrimeFDToHandle, unsafe.Pointer(&p))
538 }); err != nil {
539 return 0, err
540 }
541 if ioErr != nil {
542 return 0, errors.Join(errors.New("prime import"), ioErr)
543 }
544 defer func() {
545 g := gemClose{handle: p.handle}
546 _ = ioctl(k.fd, ioctlGemClose, unsafe.Pointer(&g))
547 }()
548 cmd := fbCmd2{width: uint32(b.Width), height: uint32(b.Height), format: format}
549 useMod := b.Modifier != modInvalid && (b.Modifier != 0 || k.modifiers)
550 if useMod {
551 cmd.flags = fbModifiers
552 }
553 // Planes in the same dmabuf share its GEM handle: each distinct
554 // handle is closed once.
555 closing := map[uint32]bool{p.handle: true}
556 for i, pl := range b.Planes {
557 if i >= len(cmd.handles) {
558 break
559 }
560 h := p.handle
561 if i > 0 {
562 var err error
563 if h, err = k.primeImport(pl.File); err != nil {
564 return 0, err
565 }
566 if !closing[h] {
567 closing[h] = true
568 defer k.gemClose(h)
569 }
570 }
571 cmd.handles[i], cmd.pitches[i], cmd.offsets[i] = h, pl.Stride, pl.Offset
572 if useMod {
573 cmd.modifiers[i] = b.Modifier
574 }
575 }
576 if err := ioctl(k.fd, ioctlAddFB2, unsafe.Pointer(&cmd)); err != nil {
577 return 0, errors.Join(errors.New("add fb2"), err)
578 }
579 return cmd.fbID, nil
580}
581
582// primeImport returns the GEM handle of a dmabuf file. The caller holds
583// gemMu and closes the handle.
584func (k kmsDevice) primeImport(f *os.File) (uint32, error) {
585 raw, err := f.SyscallConn()
586 if err != nil {
587 return 0, err
588 }
589 var p primeHandle
590 var ioErr error
591 if err := raw.Control(func(fd uintptr) {
592 p.fd = int32(fd)
593 ioErr = ioctl(k.fd, ioctlPrimeFDToHandle, unsafe.Pointer(&p))
594 }); err != nil {
595 return 0, err
596 }
597 if ioErr != nil {
598 return 0, errors.Join(errors.New("prime import"), ioErr)
599 }
600 return p.handle, nil
601}
602
603func (k kmsDevice) gemClose(h uint32) {
604 g := gemClose{handle: h}
605 _ = ioctl(k.fd, ioctlGemClose, unsafe.Pointer(&g))
606}
607
608// dropClientFBs frees framebuffers not wanted for scanoutIdleTTL, or all
609// of them, except those on screen or queued.
610func (o *Output) dropClientFBs(now time.Time, all bool) {
611 for id, fb := range o.clientFBs {
612 if id == o.shown || id == o.queued {
613 continue
614 }
615 if !all && now.Sub(fb.last) < scanoutIdleTTL {
616 continue
617 }
618 if fb.fbID != 0 {
619 _ = o.k.rmFB(fb.fbID)
620 // KMS may give the id to another buffer.
621 o.forgetOverlayProbes()
622 }
623 delete(o.clientFBs, id)
624 }
625}
626
627// dupFence duplicates a sync file under its SyscallConn, so a concurrent
628// Close cannot hand us a reused fd; nil when f is nil or closed.
629func dupFence(f *os.File) *os.File {
630 if f == nil {
631 return nil
632 }
633 raw, err := f.SyscallConn()
634 if err != nil {
635 return nil
636 }
637 fd := -1
638 if raw.Control(func(v uintptr) { fd, _ = unix.FcntlInt(v, unix.F_DUPFD_CLOEXEC, 0) }) != nil || fd < 0 {
639 return nil
640 }
641 return os.NewFile(uintptr(fd), "acquire-fence")
642}