1package drm
2
3import (
4 "sync/atomic"
5 "testing"
6 "testing/synctest"
7 "time"
8
9 "github.com/bnema/neferwl/internal/ports"
10 "github.com/stretchr/testify/mock"
11 "golang.org/x/sys/unix"
12)
13
14func TestNoMemRetriesWithoutModeset(t *testing.T) {
15 synctest.Test(t, func(t *testing.T) {
16 h := startStuck(t, nil, unix.ENOMEM, unix.ENOMEM, nil)
17 h.scenes <- ports.Scene{Seq: 1}
18 synctest.Wait()
19 for _, delay := range []time.Duration{nomemRetryMin, 2 * nomemRetryMin} {
20 select {
21 case p := <-h.presented:
22 t.Fatalf("refused frame reported: %+v", p)
23 default:
24 }
25 time.Sleep(delay)
26 synctest.Wait()
27 }
28 c := h.snapshot()
29 if len(c) != 5 {
30 t.Fatalf("commits %d, want 5", len(c))
31 }
32 for _, r := range c[2:] {
33 if r.flags&atomicAllowModes != 0 || r.user&3 != userFrame {
34 t.Fatalf("not a frame retry: %+v", r)
35 }
36 }
37 select {
38 case p := <-h.presented:
39 t.Fatalf("reported before successful flip: %+v", p)
40 default:
41 }
42 h.flipped <- eventOf(c[4])
43 synctest.Wait()
44 select {
45 case <-h.presented:
46 default:
47 t.Fatal("successful flip not reported")
48 }
49 select {
50 case p := <-h.presented:
51 t.Fatalf("extra report: %+v", p)
52 default:
53 }
54 h.stop()
55 })
56}
57
58func TestLastingNoMemKeepsOutput(t *testing.T) {
59 synctest.Test(t, func(t *testing.T) {
60 errs := make([]error, 21)
61 for i := range errs {
62 errs[i] = unix.ENOMEM
63 }
64 h := startStuck(t, nil, errs...)
65 h.scenes <- ports.Scene{Seq: 1}
66 synctest.Wait()
67 for i := range 19 {
68 time.Sleep(min(nomemRetryMax, nomemRetryMin*time.Duration(1<<min(i, 7))))
69 synctest.Wait()
70 }
71 select {
72 case err := <-h.done:
73 t.Fatalf("Run stopped: %v", err)
74 default:
75 }
76 c := h.stop()
77 if len(c) != 22 {
78 t.Fatalf("commits %d want 22", len(c))
79 }
80 for _, r := range c[2:] {
81 if r.flags&atomicAllowModes != 0 {
82 t.Fatal("ENOMEM triggered modeset")
83 }
84 }
85 })
86}
87
88func TestNoMemModesetRetries(t *testing.T) {
89 synctest.Test(t, func(t *testing.T) {
90 h := startStuck(t, nil, nil, unix.ENOMEM, nil)
91 h.scenes <- ports.Scene{Seq: 1, Off: true}
92 synctest.Wait()
93 if h.count() != 3 {
94 t.Fatal("power off did not commit")
95 }
96 h.scenes <- ports.Scene{Seq: 2}
97 synctest.Wait()
98 time.Sleep(nomemRetryMin)
99 synctest.Wait()
100 select {
101 case err := <-h.done:
102 t.Fatalf("Run stopped: %v", err)
103 default:
104 }
105 c := h.stop()
106 if len(c) != 6 {
107 t.Fatalf("commits %d want 6", len(c))
108 }
109 for _, r := range c[3:5] {
110 if r.flags != atomicAllowModes {
111 t.Fatalf("not a retried modeset: %#x", r.flags)
112 }
113 }
114 })
115}
116
117func TestScanoutNoMemComposes(t *testing.T) {
118 synctest.Test(t, func(t *testing.T) {
119 o, _, _ := testOutput(t, unix.ENOMEM)
120 s, surfaces := fullscreenScene()
121 c := surfaces[1]
122 o.clientFBs[9] = &clientFB{fbID: 80}
123 o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
124 direct, err := o.commitScanoutRect(80, c, pendingFrame{}, fullPlaneRect(200, 100), colorBypass)
125 if direct || err != nil || o.reason != "nomem" || o.clientFBs[9].failed != "" {
126 t.Fatalf("direct=%v err=%v reason=%q failed=%q", direct, err, o.reason, o.clientFBs[9].failed)
127 }
128 if d := o.composeFrame(s, surfaces); d.overlay.fb != 0 || d.composed.Seq != s.Seq {
129 t.Fatal("immediate fallback did not compose")
130 }
131 for _, delay := range []time.Duration{0, nomemScanoutPause - time.Nanosecond} {
132 time.Sleep(delay)
133 d := o.decideFrame(s, surfaces, false)
134 if d.fb != 0 || d.overlay.fb != 0 || d.composed.Seq != s.Seq || o.reason != "nomem" {
135 t.Fatalf("not composed: %+v reason=%q", d, o.reason)
136 }
137 }
138 time.Sleep(time.Nanosecond)
139 if d := o.decideFrame(s, surfaces, false); d.fb != 80 {
140 t.Fatalf("scanout not resumed: %+v reason=%q", d, o.reason)
141 }
142 })
143}
144
145func TestProtectedNoMemKeepsProtectionRetry(t *testing.T) {
146 for _, stage := range []string{"modeset", "commit"} {
147 t.Run(stage, func(t *testing.T) {
148 o, k, _ := testOutput(t, unix.ENOMEM)
149 var state atomic.Uint64
150 state.Store(3)
151 securityGate(t, o, &state)
152 o.observeSecurity()
153 events := make(chan ports.SecurityBackendEvent, 4)
154 o.SecurityEvents = events
155 var err error
156 if stage == "modeset" {
157 k.EXPECT().createBlob(mock.Anything).Return(99, nil)
158 k.EXPECT().destroyBlob(mock.Anything).Return(nil)
159 err = o.modesetImage(70, true)
160 } else {
161 o.securityPrepared = true // an admitted lock-screen frame
162 err = o.commitFrame(70, nil, false, false, pendingFrame{security: o.securityState})
163 }
164 enabled := true
165 if !o.commitFailed(err, &enabled) || !enabled || o.securityPrepared || !o.protectBackoffActive() || !o.frame.nomemSince.IsZero() {
166 t.Fatalf("protection retry bypassed: err=%v prepared=%v backoff=%v nomem=%v", err, o.securityPrepared, o.protectBackoff, o.frame.nomemSince)
167 }
168 select {
169 case ev := <-events:
170 t.Fatalf("proof on refusal: %T", ev)
171 default:
172 }
173 })
174 }
175}
176
177func TestNoMemSuccessClearsRun(t *testing.T) {
178 o, _, _ := testOutput(t, unix.ENOMEM, nil)
179 enabled := true
180 if !o.commitFailed(o.commitFrame(70, nil, false, false, pendingFrame{}), &enabled) {
181 t.Fatal("ENOMEM unhandled")
182 }
183 o.frame.timeout(o.frame.stuckAt, true)
184 if err := o.commitFrame(70, nil, false, false, pendingFrame{}); err != nil {
185 t.Fatal(err)
186 }
187 if !o.frame.nomemSince.IsZero() || o.frame.nomemAttempts != 0 {
188 t.Fatal("successful commit did not clear run")
189 }
190}
191
192func TestNoMemRetryRechecksSecurity(t *testing.T) {
193 o, _, commits := testOutput(t, unix.ENOMEM)
194 enabled := true
195 if !o.commitFailed(o.commitFrame(70, nil, false, false, pendingFrame{}), &enabled) {
196 t.Fatal("ENOMEM unhandled")
197 }
198 o.frame.timeout(o.frame.stuckAt, true)
199 var state atomic.Uint64
200 state.Store(3)
201 securityGate(t, o, &state)
202 if err := o.commitFrame(70, nil, false, false, pendingFrame{}); err != errSecurityScene {
203 t.Fatalf("stale retry: %v", err)
204 }
205 if len(*commits) != 1 {
206 t.Fatal("stale retry reached KMS")
207 }
208}