main f2d166b

fencegate.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 25

Tags 10

↑↓ moveenter openesc close