1package drm
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "os"
8 "strconv"
9 "strings"
10 "time"
11 "unsafe"
12
13 "github.com/bnema/neferwl/internal/adapters/syncfile"
14 "github.com/bnema/zerowrap"
15 "golang.org/x/sys/unix"
16)
17
18// Old nvidia-drm drivers mishandle IN_FENCE_FD: the flip does not wait for
19// the fence and shows a frame the GPU still draws
20// (NVIDIA/open-gpu-kernel-modules#622, fixed in 560.35.3). On those the
21// output waits for each fence on the CPU and commits without it.
22
23const (
24 driverNVIDIA = "nvidia-drm"
25 // nvidiaDRMVersionFile holds the loaded module's version.
26 nvidiaDRMVersionFile = "/sys/module/nvidia_drm/version"
27 // ioctlVersion is DRM_IOWR('d', 0x00, struct drm_version).
28 ioctlVersion = 0xC0406400
29)
30
31// nvidiaFenceFixed is the first nvidia-drm version that honours IN_FENCE_FD.
32var nvidiaFenceFixed = [3]int{560, 35, 3}
33
34// drmVersion is struct drm_version.
35type drmVersion struct {
36 major, minor, patch int32
37 nameLen uint64
38 name unsafe.Pointer
39 dateLen uint64
40 date unsafe.Pointer
41 descLen uint64
42 desc unsafe.Pointer
43}
44
45// driverName is the kernel driver of the DRM device fd (DRM_IOCTL_VERSION).
46func driverName(fd int) (string, error) {
47 var buf [64]byte
48 v := drmVersion{nameLen: uint64(len(buf)), name: unsafe.Pointer(&buf[0])}
49 if err := ioctl(fd, ioctlVersion, unsafe.Pointer(&v)); err != nil {
50 return "", err
51 }
52 return string(buf[:min(int(v.nameLen), len(buf))]), nil
53}
54
55// parseVersion reads a dotted version such as "560.35.03" (missing parts
56// are 0). Parts are plain digits: no sign.
57func parseVersion(s string) ([3]int, error) {
58 var v [3]int
59 parts := strings.Split(strings.TrimSpace(s), ".")
60 if len(parts) > len(v) {
61 return v, fmt.Errorf("version %q", s)
62 }
63 for i, p := range parts {
64 n, err := strconv.ParseUint(p, 10, 31)
65 if err != nil {
66 return v, fmt.Errorf("version %q", s)
67 }
68 v[i] = int(n)
69 }
70 return v, nil
71}
72
73// versionLess reports whether a < b.
74func versionLess(a, b [3]int) bool {
75 for i := range a {
76 if a[i] != b[i] {
77 return a[i] < b[i]
78 }
79 }
80 return false
81}
82
83// brokenInFence reports whether the driver may mishandle IN_FENCE_FD:
84// nvidia-drm before 560.35.3, or whose version is unknown (version is the
85// module version file's content, err its read error).
86func brokenInFence(driver, version string, err error) bool {
87 if driver != driverNVIDIA {
88 return false
89 }
90 if err != nil {
91 return true
92 }
93 v, perr := parseVersion(version)
94 return perr != nil || versionLess(v, nvidiaFenceFixed)
95}
96
97// fenceGate decides, once per card, whether commits carry no IN_FENCE_FD.
98func fenceGate(fd int, log zerowrap.Logger) bool {
99 driver, err := driverName(fd)
100 if err != nil {
101 log.Debug().Err(err).Msg("drm driver name unavailable")
102 return false
103 }
104 var version string
105 var verr error
106 if driver == driverNVIDIA {
107 b, rerr := os.ReadFile(nvidiaDRMVersionFile)
108 version, verr = string(b), rerr
109 }
110 if !brokenInFence(driver, version, verr) {
111 return false
112 }
113 log.Warn().Err(verr).Str("driver", driver).Str("version", strings.TrimSpace(version)).Msg("nvidia-drm version unknown or before 560.35.3 mishandles IN_FENCE_FD: waiting for frame fences on the CPU")
114 return true
115}
116
117// fenceProp sets IN_FENCE_FD on plane p to the fence, unless there is none
118// or the fences are waited for on the CPU (waitFences).
119func (o *Output) fenceProp(req *atomicReq, p *plane, fence *os.File) {
120 if fence == nil || o.cpuFences {
121 return
122 }
123 req.set(p.id, p.prop("IN_FENCE_FD"), uint64(fence.Fd()))
124}
125
126// fenceWaitLimit bounds the CPU wait of one commit, whatever its number of
127// fences.
128const fenceWaitLimit = time.Second
129
130// fenceWarnEvery is the least time between two warnings about a fence wait
131// that hit fenceWaitLimit.
132const fenceWarnEvery = 10 * time.Second
133
134// fencePoll polls one fence without blocking. fn is built once, so polling
135// allocates nothing; the output goroutine owns it.
136type fencePoll struct {
137 fds [1]unix.PollFd
138 ready bool
139 fn func(fd uintptr)
140}
141
142func (p *fencePoll) init() {
143 p.fn = func(fd uintptr) {
144 p.fds[0] = unix.PollFd{Fd: int32(fd), Events: unix.POLLIN}
145 n, err := unix.Poll(p.fds[:], 0)
146 p.ready = err == nil && n > 0 && p.fds[0].Revents&unix.POLLIN != 0
147 }
148}
149
150// clientFenceReady reports whether a client's acquire fence lets its buffer
151// be shown by a plane this frame. Where fences are waited for on the CPU, a
152// client fence that has not signalled must not stall the output: the buffer
153// is composed instead (the renderer waits for it on the GPU). Without CPU
154// waits, and for no fence, it is ready; with them, a fence that cannot be
155// polled is not ready.
156func (o *Output) clientFenceReady(acquire *os.File) bool {
157 if !o.cpuFences || acquire == nil {
158 return true
159 }
160 if o.fencePoll.fn == nil {
161 o.fencePoll.init()
162 }
163 raw, err := acquire.SyscallConn()
164 if err != nil {
165 return false
166 }
167 o.fencePoll.ready = false
168 // Control keeps the descriptor open for the call: Wayland closing its
169 // file meanwhile cannot hand us a reused number.
170 if raw.Control(o.fencePoll.fn) != nil {
171 return false
172 }
173 return o.fencePoll.ready
174}
175
176// waitFences blocks until the fences signalled when the driver cannot wait
177// for them in the commit, for at most fenceWaitLimit in all, or until the
178// output stops. A client fence passed here (direct scanout) already
179// signalled: planes take only such buffers (clientFenceReady), so it
180// returns at once. A failed or timed out wait is not fatal: the frame may
181// then show early, as it would on any such driver. A timeout is counted in
182// the stats and warned about at most every fenceWarnEvery.
183func (o *Output) waitFences(fences ...*os.File) {
184 if !o.cpuFences {
185 return
186 }
187 ctx := o.runContext
188 if ctx == nil {
189 ctx = context.Background()
190 }
191 limit := o.fenceLimit
192 if limit == 0 {
193 limit = fenceWaitLimit
194 }
195 ctx, cancel := context.WithTimeout(ctx, limit)
196 defer cancel()
197 for _, f := range fences {
198 if f == nil {
199 continue
200 }
201 err := syncfile.Wait(ctx, f)
202 switch {
203 case err == nil || errors.Is(err, context.Canceled):
204 case errors.Is(err, context.DeadlineExceeded):
205 o.flipStats.fenceTimeouts++
206 if now := time.Now(); o.fenceWarnAt.IsZero() || now.Sub(o.fenceWarnAt) >= fenceWarnEvery {
207 o.fenceWarnAt = now
208 o.log.Warn().Str("connector", o.conn.name).Dur("limit", limit).Msg("frame fence did not signal in time; committing anyway")
209 }
210 default:
211 o.log.Debug().Err(err).Str("connector", o.conn.name).Msg("wait for frame fence")
212 }
213 }
214}