1package drm
2
3import (
4 "context"
5 "errors"
6 "os"
7 "path/filepath"
8 "testing"
9 "time"
10
11 "github.com/bnema/neferwl/internal/ports"
12 "github.com/stretchr/testify/mock"
13 "golang.org/x/sys/unix"
14)
15
16// pipeFence returns a pipe standing in for a sync file: its read end is
17// readable (signalled) once the write end got a byte. Both ends close with
18// the test.
19func pipeFence(t *testing.T) (fence, signal *os.File) {
20 t.Helper()
21 r, w, err := os.Pipe()
22 if err != nil {
23 t.Fatal(err)
24 }
25 t.Cleanup(func() { r.Close(); w.Close() })
26 return r, w
27}
28
29func TestParseVersion(t *testing.T) {
30 for _, tc := range []struct {
31 in string
32 want [3]int
33 ok bool
34 }{
35 {"560.35.03\n", [3]int{560, 35, 3}, true},
36 {"615.78.08", [3]int{615, 78, 8}, true},
37 {"550.120", [3]int{550, 120, 0}, true},
38 {"570", [3]int{570, 0, 0}, true},
39 {"", [3]int{}, false},
40 {"abc", [3]int{}, false},
41 {"560.x.3", [3]int{}, false},
42 {"1.2.3.4", [3]int{}, false},
43 {"-1.2", [3]int{}, false},
44 {"+560.35.3", [3]int{}, false},
45 {"560.+35.3", [3]int{}, false},
46 {"560.-5.3", [3]int{}, false},
47 {"560..3", [3]int{}, false},
48 {"99999999999.1.1", [3]int{}, false},
49 } {
50 got, err := parseVersion(tc.in)
51 if (err == nil) != tc.ok || tc.ok && got != tc.want {
52 t.Errorf("parseVersion(%q) = %v, %v", tc.in, got, err)
53 }
54 }
55}
56
57// The gate opens for nvidia-drm before 560.35.3 or of unknown version, and
58// for no other driver.
59func TestBrokenInFence(t *testing.T) {
60 for _, tc := range []struct {
61 name, driver, version string
62 err error
63 want bool
64 }{
65 {"amd", "amdgpu", "", nil, false},
66 {"intel", "i915", "garbage", nil, false},
67 {"old nvidia", driverNVIDIA, "555.58.02", nil, true},
68 {"one patch before", driverNVIDIA, "560.35.2", nil, true},
69 {"minor before", driverNVIDIA, "560.34.99", nil, true},
70 {"fixed", driverNVIDIA, "560.35.03\n", nil, false},
71 {"patch after", driverNVIDIA, "560.35.5", nil, false},
72 {"minor after", driverNVIDIA, "560.36.0", nil, false},
73 {"major after", driverNVIDIA, "561.0.0", nil, false},
74 {"newer", driverNVIDIA, "615.78.08", nil, false},
75 {"unreadable", driverNVIDIA, "", errors.New("no such file"), true},
76 {"unparsable", driverNVIDIA, "n/a", nil, true},
77 } {
78 if got := brokenInFence(tc.driver, tc.version, tc.err); got != tc.want {
79 t.Errorf("%s: brokenInFence = %v, want %v", tc.name, got, tc.want)
80 }
81 }
82}
83
84// The driver of a DRM device is read with DRM_IOCTL_VERSION. A render node
85// answers it too: skips without one.
86func TestDriverName(t *testing.T) {
87 nodes, _ := filepath.Glob("/dev/dri/renderD*")
88 if len(nodes) == 0 {
89 t.Skip("no /dev/dri/renderD* node")
90 }
91 for _, n := range nodes {
92 f, err := os.OpenFile(n, os.O_RDWR|unix.O_CLOEXEC, 0)
93 if err != nil {
94 continue
95 }
96 defer f.Close()
97 name, err := driverName(int(f.Fd()))
98 if err != nil || name == "" {
99 t.Fatalf("driverName(%s) = %q, %v", n, name, err)
100 }
101 t.Logf("%s: %s", n, name)
102 return
103 }
104 t.Skip("no render node can be opened")
105}
106
107// With CPU waits no commit carries IN_FENCE_FD; otherwise the plane does.
108func TestFenceProp(t *testing.T) {
109 f, _ := pipeFence(t)
110 for _, cpu := range []bool{false, true} {
111 o, _, _ := testOutput(t)
112 o.cpuFences = cpu
113 req := &atomicReq{}
114 o.fenceProp(req, o.primary, f)
115 o.fenceProp(req, o.primary, nil)
116 v, ok := req.value(o.primary.id, o.primary.prop("IN_FENCE_FD"))
117 if ok == cpu || ok && v != uint64(f.Fd()) {
118 t.Errorf("cpuFences=%v: IN_FENCE_FD %d, set %v", cpu, v, ok)
119 }
120 }
121}
122
123// waitFences returns for a fence that signalled, for one that never will
124// once the output stops, and at once where fences go to the kernel.
125func TestWaitFences(t *testing.T) {
126 returns := func(t *testing.T, o *Output, fences ...*os.File) {
127 t.Helper()
128 done := make(chan struct{})
129 go func() { o.waitFences(fences...); close(done) }()
130 select {
131 case <-done:
132 case <-time.After(30 * time.Second): // failure guard only
133 t.Fatal("waitFences did not return")
134 }
135 }
136 t.Run("signalled", func(t *testing.T) {
137 o, _, _ := testOutput(t)
138 o.cpuFences = true
139 f, w := pipeFence(t)
140 _, _ = w.Write([]byte{1})
141 returns(t, o, nil, f)
142 })
143 t.Run("never signalled, output stopped", func(t *testing.T) {
144 o, _, _ := testOutput(t)
145 o.cpuFences = true
146 ctx, cancel := context.WithCancel(t.Context())
147 cancel()
148 o.runContext = ctx
149 f, _ := pipeFence(t)
150 returns(t, o, f)
151 if o.flipStats.fenceTimeouts != 0 {
152 t.Fatal("a stopped output counted a fence timeout")
153 }
154 })
155 t.Run("kernel waits", func(t *testing.T) {
156 o, _, _ := testOutput(t)
157 f, _ := pipeFence(t)
158 returns(t, o, f)
159 })
160}
161
162// A fence wait that hit its limit counts in the stats, and warns at most
163// once per fenceWarnEvery.
164func TestWaitFencesTimeoutIsCountedAndRateLimited(t *testing.T) {
165 o, _, _ := testOutput(t)
166 o.cpuFences = true
167 o.fenceLimit = time.Millisecond
168 f, _ := pipeFence(t)
169 o.waitFences(f)
170 if o.flipStats.fenceTimeouts != 1 || o.fenceWarnAt.IsZero() {
171 t.Fatalf("timeouts %d, warned at %v", o.flipStats.fenceTimeouts, o.fenceWarnAt)
172 }
173 first := o.fenceWarnAt
174 o.waitFences(f)
175 if o.flipStats.fenceTimeouts != 2 || !o.fenceWarnAt.Equal(first) {
176 t.Fatalf("timeouts %d; warning not rate-limited (%v then %v)", o.flipStats.fenceTimeouts, first, o.fenceWarnAt)
177 }
178}
179
180// Without CPU fence waits every fence is ready; with them only a signalled
181// client fence is, and one that cannot be polled is not.
182func TestClientFenceReady(t *testing.T) {
183 f, w := pipeFence(t)
184 o, _, _ := testOutput(t)
185 if !o.clientFenceReady(f) || !o.clientFenceReady(nil) {
186 t.Fatal("fence not ready without CPU waits")
187 }
188 o.cpuFences = true
189 if !o.clientFenceReady(nil) {
190 t.Fatal("no fence must be ready")
191 }
192 if o.clientFenceReady(f) {
193 t.Fatal("pending client fence ready")
194 }
195 _, _ = w.Write([]byte{1})
196 if !o.clientFenceReady(f) {
197 t.Fatal("signalled client fence not ready")
198 }
199 closed, cw := pipeFence(t)
200 closed.Close()
201 cw.Close()
202 if o.clientFenceReady(closed) {
203 t.Fatal("closed fence ready")
204 }
205}
206
207// Polling a fence allocates nothing.
208func TestClientFenceReadyAllocatesNothing(t *testing.T) {
209 f, _ := pipeFence(t)
210 o, _, _ := testOutput(t)
211 o.cpuFences = true
212 o.clientFenceReady(f) // builds the poll closure
213 if n := testing.AllocsPerRun(100, func() { o.clientFenceReady(f) }); n != 0 {
214 t.Fatalf("%v allocations per poll", n)
215 }
216}
217
218// With CPU waits a client buffer whose fence is pending is composed, on the
219// primary plane and on the overlay alike: neither plane waits for it. Once
220// the fence signalled the same frame uses the plane.
221func TestPendingClientFenceComposes(t *testing.T) {
222 for _, tc := range []struct {
223 name string
224 reason func(o *Output) string
225 // decide returns whether the plane took the buffer.
226 decide func(t *testing.T, o *Output, k *mockkms, fence *os.File) bool
227 }{
228 {"primary scanout", func(o *Output) string { return o.reason }, func(t *testing.T, o *Output, k *mockkms, fence *os.File) bool {
229 o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
230 s, c := fullscreenScene()
231 content := c[1]
232 content.Acquire = fence
233 fb, _, _ := o.scanoutFrame(s, map[ports.WindowID]ports.SurfaceContent{1: content})
234 return fb != 0
235 }},
236 {"overlay", func(o *Output) string { return o.overlayReason }, func(t *testing.T, o *Output, k *mockkms, fence *os.File) bool {
237 o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
238 s, c := overlayScene()
239 content := c[2]
240 content.Acquire = fence
241 ov, _ := o.overlayFrame(s, map[ports.WindowID]ports.SurfaceContent{2: content})
242 ov.close()
243 return ov.fb != 0
244 }},
245 } {
246 t.Run(tc.name, func(t *testing.T) {
247 o, k, _ := overlayOutput(t)
248 k.EXPECT().addFB(mock.Anything, mock.Anything).Return(60, nil).Maybe()
249 o.cpuFences = true
250 fence, w := pipeFence(t)
251 if tc.decide(t, o, k, fence) || tc.reason(o) != "fence_pending" {
252 t.Fatalf("pending fence: plane took the buffer or reason %q", tc.reason(o))
253 }
254 _, _ = w.Write([]byte{1})
255 if !tc.decide(t, o, k, fence) {
256 t.Fatalf("signalled fence: reason %q", tc.reason(o))
257 }
258 })
259 }
260}
261
262// A commit that is retried (here: VRR refused, then the same frame without
263// it) waits for its fence once, not once per attempt.
264func TestCommitWaitsForItsFenceOnce(t *testing.T) {
265 o, _, commits := testOutput(t, unix.EINVAL)
266 o.cpuFences = true
267 o.fenceLimit = time.Millisecond
268 fence, _ := pipeFence(t)
269 if err := o.commitFrame(70, fence, false, true, pendingFrame{}); err != nil {
270 t.Fatal(err)
271 }
272 if len(*commits) < 2 {
273 t.Fatalf("%d commits: no retry happened", len(*commits))
274 }
275 if o.flipStats.fenceTimeouts != 1 {
276 t.Fatalf("%d fence waits for %d commits, want one", o.flipStats.fenceTimeouts, len(*commits))
277 }
278 for i, c := range *commits {
279 if _, ok := c.req.value(tPrimary, pFence); ok {
280 t.Fatalf("commit %d carries IN_FENCE_FD with CPU waits", i)
281 }
282 }
283}