1package drm
2
3import (
4 "context"
5 "errors"
6 "sync"
7 "sync/atomic"
8 "testing"
9 "time"
10
11 portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
12 "github.com/bnema/neferwl/internal/ports"
13 "github.com/stretchr/testify/mock"
14 "golang.org/x/sys/unix"
15)
16
17// Connector HDR property IDs and values of the amdgpu rule tests.
18const (
19 pHDRMeta, pColorspace, pMaxBPC = 20, 21, 22
20 colorspaceBT2020 = 9
21)
22
23// Blob lifetime is part of the model: attach feeds createBlob/destroyBlob
24// mocks into the live set.
25type amdgpuModel struct {
26 t *testing.T
27 mu sync.Mutex
28 live map[uint32]bool
29 next uint32
30 rule func(*atomicReq, uint32) error
31}
32
33// newAmdgpuModel starts from a lit desktop whose mode blob 99 is live.
34func newAmdgpuModel(t *testing.T) *amdgpuModel {
35 t.Helper()
36 m := &amdgpuModel{t: t, live: map[uint32]bool{99: true}, next: 100}
37 m.rule = amdgpuRuleWith(t, m.isLive)
38 return m
39}
40
41func (m *amdgpuModel) isLive(blob uint32) bool {
42 m.mu.Lock()
43 defer m.mu.Unlock()
44 return m.live[blob]
45}
46
47// attach makes k create ids from 100 up and destroy only live blobs. With
48// strict ids, each is expected to be destroyed exactly once and no other
49// destroy is allowed; without, any live blob may be destroyed.
50func (m *amdgpuModel) attach(k *mockkms, strict ...uint32) {
51 k.EXPECT().createBlob(mock.Anything).RunAndReturn(func([]byte) (uint32, error) {
52 m.mu.Lock()
53 defer m.mu.Unlock()
54 id := m.next
55 m.next++
56 m.live[id] = true
57 return id, nil
58 }).Maybe()
59 destroy := func(id uint32) error {
60 m.mu.Lock()
61 defer m.mu.Unlock()
62 if !m.live[id] {
63 m.t.Errorf("amdgpu: destroyBlob(%d) of an unknown or destroyed blob", id)
64 return unix.ENOENT
65 }
66 delete(m.live, id)
67 return nil
68 }
69 if len(strict) == 0 {
70 k.EXPECT().destroyBlob(mock.Anything).RunAndReturn(destroy).Maybe()
71 }
72 for _, id := range strict {
73 k.EXPECT().destroyBlob(id).RunAndReturn(destroy).Once()
74 }
75}
76
77// amdgpuRule returns a commit rule encoding the atomic_check constraints of
78// amdgpu_dm that real hardware applied to the protected requests (EINVAL on
79// kernel 7.2, RX/DP+HDMI). The rule tracks the committed state, starting from
80// a lit desktop: CRTC enabled and active, primary/cursor/overlay/stray
81// planes attached, connector routed.
82//
83// - a CRTC with MODE_ID set (enabled) needs its primary plane attached,
84// even with ACTIVE=0 (drm_atomic_helper_check_crtc_primary_plane);
85// - MODE_ID, connector CRTC_ID and ACTIVE agree: an active or routed CRTC
86// is enabled, an enabled one is routed;
87// - a plane's FB_ID and CRTC_ID are both set or both clear, and only on an
88// enabled CRTC;
89// - the active primary plane covers the whole mode (no partial src/dst);
90// - HDR_OUTPUT_METADATA needs the BT.2020 Colorspace;
91// - MODE_ID must name a live blob, and a change of MODE_ID, ACTIVE or the
92// connector CRTC_ID is a modeset that needs the ALLOW_MODESET flag;
93// - a plane COLOR_PIPELINE cannot be selected together with the CRTC
94// DEGAMMA_LUT (amdgpu_dm_plane.c);
95// - an async commit changes nothing but a plane's FB_ID and IN_FENCE_FD
96// (drm_atomic_set_property: a changed CRTC, connector, colorop or other
97// plane property is EINVAL; writing the current value is allowed).
98//
99// amdgpuRule has no blob model: any MODE_ID is live.
100func amdgpuRule(t *testing.T) func(*atomicReq, uint32) error {
101 t.Helper()
102 return amdgpuRuleWith(t, func(uint32) bool { return true })
103}
104
105func amdgpuRuleWith(t *testing.T, live func(uint32) bool) func(*atomicReq, uint32) error {
106 t.Helper()
107 state := map[[2]uint32]uint64{
108 {tCrtc, pMode}: 99, {tCrtc, pActive}: 1, {tConn, pConnCrtc}: tCrtc,
109 }
110 for _, p := range []uint32{tPrimary, tCursor, tOverlay, 53} {
111 state[[2]uint32{p, pFB}], state[[2]uint32{p, pCrtcID}] = 70, tCrtc
112 }
113 // The lit desktop's primary plane covers the whole mode: a commit that
114 // leaves its geometry alone (an async flip) keeps it.
115 for _, c := range [...]struct {
116 prop uint32
117 val uint64
118 }{{12, 200 << 16}, {13, 100 << 16}, {16, 200}, {17, 100}} {
119 state[[2]uint32{tPrimary, c.prop}] = c.val
120 }
121 return func(req *atomicReq, flags uint32) error {
122 next := make(map[[2]uint32]uint64, len(state))
123 for k, v := range state {
124 next[k] = v
125 }
126 for i, obj := range req.objs {
127 for _, p := range req.props[i] {
128 next[[2]uint32{obj, p.prop}] = p.val
129 }
130 }
131 get := func(obj, prop uint32) uint64 { return next[[2]uint32{obj, prop}] }
132 if flags&flipAsyncFlag != 0 {
133 for i, obj := range req.objs {
134 for _, p := range req.props[i] {
135 isPlane := obj == tPrimary || obj == tCursor || obj == tOverlay || obj == 53
136 if p.val != state[[2]uint32{obj, p.prop}] && !(isPlane && (p.prop == pFB || p.prop == pFence)) {
137 t.Logf("amdgpu: async commit changes object %d property %d", obj, p.prop)
138 return unix.EINVAL
139 }
140 }
141 }
142 }
143 for _, p := range []uint32{tPrimary, tOverlay} {
144 if get(tCrtc, pDegamma) != 0 && (get(p, pColorPipe) != 0 || get(p, pColorPipeOverlay) != 0) {
145 t.Logf("amdgpu: COLOR_PIPELINE on plane %d with a CRTC DEGAMMA_LUT", p)
146 return unix.EINVAL
147 }
148 }
149 if mode := get(tCrtc, pMode); mode != 0 && !live(uint32(mode)) {
150 t.Logf("amdgpu: MODE_ID %d names an unknown or destroyed blob", mode)
151 return unix.EINVAL
152 }
153 for _, k := range [...][2]uint32{{tCrtc, pMode}, {tCrtc, pActive}, {tConn, pConnCrtc}} {
154 if next[k] != state[k] && flags&atomicAllowModes == 0 {
155 t.Logf("amdgpu: modeset change of %v without ALLOW_MODESET", k)
156 return unix.EINVAL
157 }
158 }
159 enabled, active := get(tCrtc, pMode) != 0, get(tCrtc, pActive) == 1
160 if active && !enabled || (get(tConn, pConnCrtc) != 0) != enabled {
161 t.Logf("amdgpu: ACTIVE/MODE_ID/connector routing disagree: %v", next)
162 return unix.EINVAL
163 }
164 for _, p := range []uint32{tPrimary, tCursor, tOverlay, 53} {
165 fb, crtc := get(p, pFB), get(p, pCrtcID)
166 if (fb != 0) != (crtc != 0) || crtc != 0 && (!enabled || crtc != tCrtc) {
167 t.Logf("amdgpu: plane %d fb=%d crtc=%d enabled=%v", p, fb, crtc, enabled)
168 return unix.EINVAL
169 }
170 }
171 if enabled && get(tPrimary, pFB) == 0 {
172 t.Log("amdgpu: enabled CRTC without primary plane")
173 return unix.EINVAL
174 }
175 if active {
176 for _, c := range [...]struct {
177 prop uint32
178 want uint64
179 }{{10, 0}, {11, 0}, {12, 200 << 16}, {13, 100 << 16}, {14, 0}, {15, 0}, {16, 200}, {17, 100}} {
180 if get(tPrimary, c.prop) != c.want {
181 t.Logf("amdgpu: primary prop %d = %d, want full-screen %d", c.prop, get(tPrimary, c.prop), c.want)
182 return unix.EINVAL
183 }
184 }
185 }
186 if get(tConn, pHDRMeta) != 0 && get(tConn, pColorspace) != colorspaceBT2020 {
187 t.Log("amdgpu: HDR metadata without BT.2020 colorspace")
188 return unix.EINVAL
189 }
190 if flags&atomicTestOnly == 0 {
191 state = next
192 }
193 return nil
194 }
195}
196
197func enableTestHDR(o *Output) {
198 o.hdr.on, o.hdr.blob = true, 55
199 o.hdr.props = connectorHDRProps{Metadata: pHDRMeta, Colorspace: pColorspace, MaxBPC: pMaxBPC, BT2020Value: colorspaceBT2020, MaxBPCValue: 8, HasDefault: true}
200}
201
202func TestAmdgpuRuleRejectsWhatHardwareRejected(t *testing.T) {
203 for name, build := range map[string]func(*atomicReq){
204 // The shape that failed on hardware: inactive, still enabled, no planes.
205 "enabled CRTC without primary": func(r *atomicReq) {
206 r.set(tCrtc, pActive, 0)
207 for _, p := range []uint32{tPrimary, tCursor, tOverlay, 53} {
208 r.set(p, pFB, 0)
209 r.set(p, pCrtcID, 0)
210 }
211 },
212 "plane on disabled CRTC": func(r *atomicReq) {
213 r.set(tCrtc, pActive, 0)
214 r.set(tCrtc, pMode, 0)
215 r.set(tConn, pConnCrtc, 0)
216 },
217 "connector routed to disabled CRTC": func(r *atomicReq) {
218 r.set(tCrtc, pActive, 0)
219 r.set(tCrtc, pMode, 0)
220 for _, p := range []uint32{tPrimary, tCursor, tOverlay, 53} {
221 r.set(p, pFB, 0)
222 r.set(p, pCrtcID, 0)
223 }
224 },
225 "cursor with CRTC but no FB": func(r *atomicReq) { r.set(tCursor, pFB, 0) },
226 "partial primary": func(r *atomicReq) { r.set(tPrimary, 16, 100) },
227 "HDR metadata without colorspace": func(r *atomicReq) {
228 r.set(tConn, pHDRMeta, 55)
229 },
230 "COLOR_PIPELINE with DEGAMMA_LUT": func(r *atomicReq) {
231 r.set(tCrtc, pDegamma, 77)
232 r.set(tPrimary, pColorPipe, opBase)
233 },
234 } {
235 t.Run(name, func(t *testing.T) {
236 req := &atomicReq{}
237 build(req)
238 if err := amdgpuRule(t)(req, atomicAllowModes); err == nil {
239 t.Fatal("rule accepted a request amdgpu refuses")
240 }
241 })
242 }
243}
244
245// Every protected disable/activation request must pass the amdgpu rules, with
246// HDR on and off, and leave the state dark then black-only.
247func TestProtectedRequestsAcceptedByAmdgpuRules(t *testing.T) {
248 for _, hdr := range []bool{false, true} {
249 name := map[bool]string{false: "sdr", true: "hdr"}[hdr]
250 t.Run(name, func(t *testing.T) {
251 m := newAmdgpuModel(t)
252 o, k, commits, _ := testOutputRules(t, m.rule)
253 m.attach(k, 99)
254 if hdr {
255 enableTestHDR(o)
256 }
257 o.modeBlob = 99
258 o.overlay = &plane{id: tOverlay, props: planeProps}
259 o.stray = []*plane{o.overlay, {id: 53, props: planeProps}}
260 k.EXPECT().objProps(uint32(53), uint32(objPlane)).Return(map[string][2]uint64{"CRTC_ID": {pCrtcID, tCrtc}}, nil)
261 r := portsmocks.NewMockRenderer(t)
262 r.EXPECT().UseTarget(0).Return().Once()
263 r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil).Once()
264 if err := o.protectedModeset(context.Background(), r); err != nil {
265 t.Fatalf("amdgpu refused the protected transaction: %v", err)
266 }
267 if len(*commits) != 2 {
268 t.Fatalf("commits %d", len(*commits))
269 }
270 act := (*commits)[1]
271 if fb, _ := act.req.value(tPrimary, pFB); fb != 71 && fb != 70 {
272 t.Fatalf("activated fb %d", fb)
273 }
274 if crtc, ok := act.req.value(tCursor, pCrtcID); !ok || crtc != 0 {
275 t.Fatalf("cursor not explicitly detached: %d %v", crtc, ok)
276 }
277 if fb, ok := act.req.value(tCursor, pFB); !ok || fb != 0 {
278 t.Fatalf("cursor fb not explicitly detached: %d %v", fb, ok)
279 }
280 if o.off || !o.protected {
281 t.Fatalf("off=%v protected=%v", o.off, o.protected)
282 }
283 // The desktop mode blob is destroyed once by the disable; the
284 // activation's blob is the only live one and o.modeBlob owns it.
285 if o.modeBlob != 100 || !m.isLive(100) || m.isLive(99) {
286 t.Fatalf("modeBlob %d live100=%v live99=%v", o.modeBlob, m.isLive(100), m.isLive(99))
287 }
288 })
289 }
290}
291
292func TestAmdgpuRuleRejectsModesetWithoutAllowModes(t *testing.T) {
293 for name, build := range map[string]func(*atomicReq){
294 "MODE_ID": func(r *atomicReq) { r.set(tCrtc, pMode, 99) },
295 "ACTIVE": func(r *atomicReq) { r.set(tCrtc, pActive, 0) },
296 "connector CRTC_ID": func(r *atomicReq) { r.set(tConn, pConnCrtc, 0) },
297 } {
298 t.Run(name, func(t *testing.T) {
299 m := newAmdgpuModel(t)
300 m.live[98] = true
301 req := &atomicReq{}
302 build(req)
303 if name == "MODE_ID" {
304 req.set(tCrtc, pMode, 98)
305 }
306 if err := m.rule(req, 0); err == nil {
307 t.Fatal("rule accepted a modeset change without ALLOW_MODESET")
308 }
309 })
310 }
311}
312
313// The state a lit desktop starts from is valid: the rule only refuses an
314// unknown MODE_ID once it models blobs.
315func TestAmdgpuRuleRejectsUnknownModeBlob(t *testing.T) {
316 req := &atomicReq{}
317 req.set(tCrtc, pMode, 123)
318 if err := newAmdgpuModel(t).rule(req, atomicAllowModes); err == nil {
319 t.Fatal("rule accepted an unknown MODE_ID")
320 }
321}
322
323func TestAmdgpuRuleRejectsDestroyedModeBlob(t *testing.T) {
324 m := newAmdgpuModel(t)
325 req := &atomicReq{}
326 req.set(tCrtc, pMode, 99)
327 for _, c := range [...]struct{ prop, val uint32 }{{12, 200 << 16}, {13, 100 << 16}, {16, 200}, {17, 100}} {
328 req.set(tPrimary, c.prop, uint64(c.val))
329 }
330 if err := m.rule(req, atomicAllowModes); err != nil {
331 t.Fatalf("live blob rejected: %v", err)
332 }
333 m.mu.Lock()
334 delete(m.live, 99)
335 m.mu.Unlock()
336 if err := m.rule(req, atomicAllowModes); err == nil {
337 t.Fatal("rule accepted a destroyed MODE_ID blob")
338 }
339}
340
341func TestProtectedResumeAcceptedByAmdgpuRules(t *testing.T) {
342 m := newAmdgpuModel(t)
343 o, k, commits, _ := testOutputRules(t, m.rule)
344 m.attach(k)
345 o.modeBlob = 99
346 o.protected = true
347 o.overlay = &plane{id: tOverlay, props: planeProps}
348 o.stray = []*plane{o.overlay, {id: 53, props: planeProps}}
349 k.EXPECT().objProps(uint32(53), uint32(objPlane)).Return(map[string][2]uint64{"CRTC_ID": {pCrtcID, tCrtc}}, nil)
350 if err := o.modeset(); err != nil { // resume while protected: dark, not stale
351 t.Fatalf("amdgpu refused protected inactive modeset: %v", err)
352 }
353 if !o.off || len(*commits) != 1 {
354 t.Fatalf("off=%v commits=%d", o.off, len(*commits))
355 }
356 if v, ok := (*commits)[0].req.value(tCrtc, pMode); !ok || v != 0 {
357 t.Fatal("protected disable left the CRTC enabled")
358 }
359}
360
361// The legacy request is refused by the rule; the output stays registered and
362// protected, retries with bounded backoff and never reports proof.
363func TestProtectedRefusalKeepsOutputProtectedAndBacksOff(t *testing.T) {
364 o, k, commits, _ := testOutputRules(t, func(r *atomicReq, flags uint32) error {
365 if v, ok := r.value(tCrtc, pActive); ok && v == 1 {
366 return unix.EINVAL
367 }
368 return nil
369 })
370 k.EXPECT().createBlob(mock.Anything).Return(100, nil)
371 k.EXPECT().destroyBlob(mock.Anything).Return(nil).Maybe()
372 r := portsmocks.NewMockRenderer(t)
373 r.EXPECT().UseTarget(0).Return().Once()
374 r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil).Once()
375 err := o.protectedModeset(context.Background(), r)
376 if err == nil || !retryableProtected(err) || !errorsIsEINVAL(err) {
377 t.Fatalf("refusal result %v", err)
378 }
379 if !o.protected || !o.off || o.back != 0 || len(*commits) != 2 {
380 t.Fatalf("protected=%v off=%v back=%d commits=%d", o.protected, o.off, o.back, len(*commits))
381 }
382 var enabled = true
383 if !o.commitFailed(err, &enabled) || !enabled || !o.protectBackoffActive() {
384 t.Fatal("refusal not retryable with backoff")
385 }
386 d := time.Duration(0)
387 for range 10 {
388 d = nextProtectBackoff(d)
389 }
390 if d != protectRetryMax || nextProtectBackoff(0) != protectRetryMin {
391 t.Fatalf("backoff not bounded: %v", d)
392 }
393}
394
395func errorsIsEINVAL(err error) bool { return err != nil && errors.Is(err, unix.EINVAL) }
396
397// Run: activation refused twice with EINVAL. The output keeps running, stays
398// dark, and only the later accepted black activation produces proof.
399func TestRunProtectedActivationRetriesWithoutStoppingOrEarlyProof(t *testing.T) {
400 var activations, refusals atomic.Int32
401 o, k, _, _ := testOutputRules(t, func(r *atomicReq, flags uint32) error {
402 if v, ok := r.value(tCrtc, pActive); ok && v == 1 && flags&atomicTestOnly == 0 {
403 activations.Add(1)
404 if refusals.Add(1) <= 2 {
405 return unix.EINVAL
406 }
407 }
408 return nil
409 })
410 o.cursor = nil
411 var state atomic.Uint64
412 state.Store(3)
413 securityGate(t, o, &state)
414 events := make(chan ports.SecurityBackendEvent, 32)
415 o.SecurityEvents = events
416 r := portsmocks.NewMockRenderer(t)
417 buf := func() ports.DMABuf {
418 f, w := protectionFence(t, false)
419 w.Close()
420 return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
421 }
422 r.EXPECT().SetHDR(float64(0)).Return().Maybe()
423 r.EXPECT().ExportTargets(2, mock.Anything).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
424 r.EXPECT().UseTarget(mock.Anything).Return()
425 r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)
426 r.EXPECT().Close().Return().Once()
427 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil)
428 k.EXPECT().createBlob(mock.Anything).Return(99, nil)
429 k.EXPECT().destroyBlob(mock.Anything).Return(nil).Maybe()
430 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
431 defer cancel()
432 done := make(chan error, 1)
433 go func() {
434 done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, nil, nil, nil, nil, make(chan ports.OutputPresented, 8), nil, nil)
435 }()
436 for {
437 select {
438 case ev := <-events:
439 if _, ok := ev.(ports.SecurityOutputProof); !ok {
440 continue
441 }
442 if n := activations.Load(); n < 3 {
443 t.Fatalf("proof after %d activation attempts; refused ones are no evidence", n)
444 }
445 cancel()
446 if err := <-done; err != nil {
447 t.Fatalf("output stopped: %v", err)
448 }
449 return
450 case err := <-done:
451 t.Fatalf("output stopped on refused protected commit: %v", err)
452 case <-ctx.Done():
453 t.Fatal("no proof after retries")
454 }
455 }
456}
457
458// Async commits may change FB_ID and IN_FENCE_FD only; writing a value a
459// property already has is allowed.
460func TestAmdgpuRuleAsyncChangesOnlyFB(t *testing.T) {
461 for _, tc := range []struct {
462 name string
463 build func(*atomicReq)
464 ok bool
465 }{
466 {"FB_ID and fence", func(r *atomicReq) { r.set(tPrimary, pFB, 71); r.set(tPrimary, pFence, 5) }, true},
467 {"the current value of another property", func(r *atomicReq) { r.set(tPrimary, pFB, 71); r.set(tPrimary, pCrtcID, tCrtc) }, true},
468 {"COLOR_PIPELINE changes", func(r *atomicReq) { r.set(tPrimary, pFB, 71); r.set(tPrimary, pColorPipe, opBase) }, false},
469 {"colorop BYPASS changes", func(r *atomicReq) { r.set(tPrimary, pFB, 71); r.set(opBase, bypassProp(opBase), 1) }, false},
470 {"VRR changes", func(r *atomicReq) { r.set(tPrimary, pFB, 71); r.set(tCrtc, pVRR, 1) }, false},
471 {"cursor moves", func(r *atomicReq) { r.set(tPrimary, pFB, 71); r.set(tCursor, 14, 9) }, false},
472 } {
473 t.Run(tc.name, func(t *testing.T) {
474 req := &atomicReq{}
475 tc.build(req)
476 err := amdgpuRule(t)(req, flipAsyncFlag)
477 if (err == nil) != tc.ok {
478 t.Fatalf("async rule error %v, want ok=%v", err, tc.ok)
479 }
480 })
481 }
482}
483
484// The colour pipeline sequence the output drives on an HDR output meets the
485// amdgpu rules: modeset on Bypass, SDR scanout selecting the pipeline in a
486// synchronous commit, async flips of it carrying FB_ID alone, a composed
487// frame back on Bypass, and Close.
488func TestColorPipelineSequenceAcceptedByAmdgpuRules(t *testing.T) {
489 m := newAmdgpuModel(t)
490 o, k, commits := colorOutput(t, true, m.rule)
491 m.attach(k)
492 k.EXPECT().objProps(uint32(tOverlay), uint32(objPlane)).Return(map[string][2]uint64{"CRTC_ID": {pCrtcID, tCrtc}}, nil).Maybe()
493 k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
494 enableTestHDR(o)
495 o.hdr.blob = 55
496 m.live[55], m.live[61] = true, true
497 o.modeBlob = 99
498 if err := o.modeset(); err != nil {
499 t.Fatalf("modeset: %v", err)
500 }
501 c := sdrContent(o)
502 c.Async = true
503 o.shown = 9
504 flip := func(mode colorMode) {
505 t.Helper()
506 o.frame.endPending()
507 o.vrrOn = true // keep VRR out of the way: it is its own commit field
508 if ok, err := o.commitScanoutRect(80, c, pendingFrame{}, fullPlaneRect(200, 100), mode); !ok || err != nil {
509 t.Fatalf("scanout %d: ok=%v err=%v", mode, ok, err)
510 }
511 }
512 flip(colorSDRToPQ)
513 flip(colorSDRToPQ)
514 flip(colorSDRToPQ)
515 if n := len(*commits); (*commits)[n-1].flags&flipAsyncFlag == 0 {
516 t.Fatal("steady-state flip with the pipeline was not async")
517 }
518 flip(colorBypass)
519 o.frame.endPending()
520 if err := o.commitFrame(70, nil, false, false, pendingFrame{}); err != nil {
521 t.Fatalf("composed: %v", err)
522 }
523 o.Close()
524}
525
526// The pipeline is never selected next to a CRTC DEGAMMA_LUT: the modeset
527// state this compositor commits has none.
528func TestColorPipelineNeverSetsDegamma(t *testing.T) {
529 m := newAmdgpuModel(t)
530 o, k, commits := colorOutput(t, false, m.rule)
531 m.attach(k)
532 k.EXPECT().objProps(mock.Anything, mock.Anything).Return(nil, nil).Maybe()
533 c := sdrContent(o)
534 if ok, err := o.commitScanoutRect(80, c, pendingFrame{}, fullPlaneRect(200, 100), colorSDRToPQ); !ok || err != nil {
535 t.Fatalf("ok=%v err=%v", ok, err)
536 }
537 for _, rec := range *commits {
538 if _, ok := rec.req.value(tCrtc, pDegamma); ok {
539 t.Fatal("commit touches DEGAMMA_LUT")
540 }
541 }
542}