1package drm
2
3import (
4 "os"
5 "slices"
6 "testing"
7 "unsafe"
8
9 portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
10 "github.com/bnema/neferwl/internal/ports"
11 "github.com/stretchr/testify/mock"
12 "golang.org/x/sys/unix"
13)
14
15func TestHDRMetadataLayoutAndContent(t *testing.T) {
16 var layout hdrOutputMetadata
17 if unsafe.Sizeof(layout) != 32 || unsafe.Offsetof(layout.HDMI) != 4 || unsafe.Offsetof(layout.HDMI.DisplayPrimaries) != 2 || unsafe.Offsetof(layout.HDMI.WhitePoint) != 14 || unsafe.Offsetof(layout.HDMI.MaxDisplayMasteringLuminance) != 18 || unsafe.Offsetof(layout.HDMI.MinDisplayMasteringLuminance) != 20 || unsafe.Offsetof(layout.HDMI.MaxCLL) != 22 || unsafe.Offsetof(layout.HDMI.MaxFALL) != 24 {
18 t.Fatalf("unexpected HDR metadata layout: size=%d HDMI=%d", unsafe.Sizeof(layout), unsafe.Offsetof(layout.HDMI))
19 }
20 m := Monitor{Chromaticity: Chromaticity{RedX: 0.64, RedY: 0.33, GreenX: 0.3, GreenY: 0.6, BlueX: 0.15, BlueY: 0.06, WhiteX: 0.3127, WhiteY: 0.329}, HDR: HDRMetadata{MaxLuminance: 1000, MinLuminance: 0.005}}
21 data := hdrMetadata(m)
22 if len(data.bytes()) != 32 || data.MetadataType != 0 || data.HDMI.EOTF != 2 || data.HDMI.MetadataType != 0 || data.HDMI.DisplayPrimaries != [3][2]uint16{{32000, 16500}, {15000, 30000}, {7500, 3000}} || data.HDMI.WhitePoint != [2]uint16{15635, 16450} || data.HDMI.MaxDisplayMasteringLuminance != 1000 || data.HDMI.MinDisplayMasteringLuminance != 50 || data.HDMI.MaxCLL != 1000 || data.HDMI.MaxFALL != 1000 {
23 t.Fatalf("metadata: %+v", data)
24 }
25 m.HDR.MaxFrameAverage = 400
26 if withFALL := hdrMetadata(m); withFALL.HDMI.MaxFALL != 400 {
27 t.Fatalf("frame-average luminance: %+v", withFALL)
28 }
29 if zero := hdrMetadata(Monitor{}); zero.HDMI.MaxCLL != 0 || zero.HDMI.MaxFALL != 0 {
30 t.Fatalf("unknown luminance: %+v", zero)
31 }
32}
33
34func TestHDRModesetPropertiesAndPlaneRefusal(t *testing.T) {
35 o, _, _ := testOutput(t)
36 o.hdr.props = connectorHDRProps{Metadata: 100, Colorspace: 101, MaxBPC: 102, BT2020Value: 7, DefaultValue: 0, HasDefault: true, MaxBPCValue: 8}
37 o.hdr.blob, o.hdr.on = 321, true
38 for _, tc := range []struct {
39 on bool
40 bpc, colorspace, metadata uint64
41 }{
42 {true, 10, 7, 321}, {false, 8, 0, 0},
43 } {
44 o.hdr.on = tc.on
45 req := o.modesetReq(11, true)
46 for _, prop := range []struct {
47 id uint32
48 want uint64
49 }{{102, tc.bpc}, {101, tc.colorspace}, {100, tc.metadata}} {
50 if got, ok := req.value(o.conn.id, prop.id); !ok || got != prop.want {
51 t.Fatalf("HDR %t prop %d: got %d (%t), want %d", tc.on, prop.id, got, ok, prop.want)
52 }
53 }
54 }
55 o.hdr.on = true
56 if fb, _, _ := o.scanoutFrame(ports.Scene{}, nil); fb != 0 || o.reason != "no_fullscreen" {
57 t.Fatalf("scanout: fb %d reason %q", fb, o.reason)
58 }
59 if ov, _ := o.overlayFrame(ports.Scene{}, nil); ov.fb != 0 || o.overlayReason != "no_plane" {
60 t.Fatalf("overlay: fb %d reason %q", ov.fb, o.overlayReason)
61 }
62 // Colorspace property without a Default enum must not receive invented 0.
63 o.hdr.on = false
64 o.hdr.props.HasDefault = false
65 if _, ok := o.modesetReq(11, true).value(o.conn.id, o.hdr.props.Colorspace); ok {
66 t.Fatal("SDR set Colorspace with no Default enum")
67 }
68 restore := &atomicReq{}
69 o.hdrConnectorProps(restore, false)
70 if _, ok := restore.value(o.conn.id, o.hdr.props.Colorspace); ok {
71 t.Fatal("restore set Colorspace with no Default enum")
72 }
73}
74
75func TestHDRTestCommitFallbackReexportsSDR(t *testing.T) {
76 o, k, commits := testOutput(t, unix.EINVAL)
77 o.cursor = nil
78 o.hdr.cap = hdrCapability{Capable: true}
79 o.hdr.settings = HDRSettings{Enabled: true, SDRBrightness: 203}
80 o.hdr.props = connectorHDRProps{Metadata: 100, Colorspace: 101, MaxBPC: 102, BT2020Value: 7, HasDefault: true, MaxBPCValue: 8}
81 o.primary.formats = []ports.DMABufFormat{{Format: fourccXR30, Modifier: 0}}
82 r := portsmocks.NewMockRenderer(t)
83 buf := func() ports.DMABuf {
84 f, w, _ := os.Pipe()
85 w.Close()
86 return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
87 }
88 var calls []string
89 r.EXPECT().SetHDR(float64(203)).Run(func(float64) { calls = append(calls, "hdr") }).Return().Once()
90 r.EXPECT().SetHDR(float64(0)).Run(func(float64) { calls = append(calls, "sdr") }).Return().Once()
91 r.EXPECT().ExportTargets(2, []uint64{0}, false).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
92 r.EXPECT().ExportTargets(0, []uint64(nil), false).Run(func(int, []uint64, bool) { calls = append(calls, "drop") }).Return(nil, nil).Once()
93 r.EXPECT().ExportTargets(2, []uint64(nil), false).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
94 r.EXPECT().UseTarget(mock.Anything).Return()
95 r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)
96 k.EXPECT().addFB(mock.Anything, uint32(fourccXR30)).Return(70, nil)
97 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(71, nil)
98 k.EXPECT().rmFB(mock.Anything).Return(nil)
99 k.EXPECT().createBlob(mock.Anything).Return(321, nil).Once()
100 k.EXPECT().createBlob(mock.Anything).Return(11, nil)
101 k.EXPECT().destroyBlob(mock.Anything).Return(nil)
102 if err := o.showImages(r, imagesDriver, nil); err != nil {
103 t.Fatal(err)
104 }
105 if !slices.Equal(calls, []string{"hdr", "sdr", "drop"}) {
106 t.Fatalf("fallback order: %v", calls)
107 }
108 if o.hdr.on || !o.hdr.failed {
109 t.Fatalf("HDR on %t failed %t", o.hdr.on, o.hdr.failed)
110 }
111 // The HDR images were already linear: refusing them ends HDR at once,
112 // there is no second identical linear export.
113 if len(*commits) != 3 {
114 t.Fatalf("commits: %d", len(*commits))
115 }
116 if (*commits)[0].flags != atomicTestOnly|atomicAllowModes {
117 t.Fatalf("HDR test flags: %#x", (*commits)[0].flags)
118 }
119 if (*commits)[1].flags != atomicTestOnly|atomicAllowModes || (*commits)[2].flags != atomicAllowModes {
120 t.Fatalf("SDR commits: %#x %#x", (*commits)[1].flags, (*commits)[2].flags)
121 }
122 if v, _ := (*commits)[0].req.value(o.conn.id, 100); v != 321 {
123 t.Fatalf("HDR test metadata: %d", v)
124 }
125 if v, _ := (*commits)[len(*commits)-1].req.value(o.conn.id, 100); v != 0 {
126 t.Fatalf("SDR metadata: %d", v)
127 }
128}
129
130func TestHDRSettingsRestartComparison(t *testing.T) {
131 current := HDRSettings{Enabled: true, SDRBrightness: ports.DefaultSDRBrightness}
132 for _, tc := range []struct {
133 settings HDRSettings
134 restart bool
135 }{
136 {HDRSettings{Enabled: true}, false},
137 {HDRSettings{Enabled: true, SDRBrightness: 350}, true},
138 {HDRSettings{SDRBrightness: ports.DefaultSDRBrightness}, true},
139 } {
140 if restart := outputNeedsRestart(&Output{hdr: hdrState{settings: current}}, modeInfo{}, tc.settings); restart != tc.restart {
141 t.Fatalf("settings %+v: restart %t, want %t", tc.settings, restart, tc.restart)
142 }
143 }
144 if !outputNeedsRestart(&Output{hdr: hdrState{settings: current}}, modeInfo{VRefresh: 60}, current) {
145 t.Fatal("mode change did not restart")
146 }
147}
148
149func TestHDRSuccessfulModeset(t *testing.T) {
150 o, k, commits := testOutput(t)
151 o.fbs = [2]uint32{} // the first setup of a run: no image yet
152 o.cursor = nil
153 o.hdr.cap = hdrCapability{Capable: true}
154 o.hdr.settings = HDRSettings{Enabled: true, SDRBrightness: ports.DefaultSDRBrightness}
155 o.hdr.props = connectorHDRProps{Metadata: 100, Colorspace: 101, MaxBPC: 102, BT2020Value: 7, HasDefault: true, MaxBPCValue: 8}
156 o.primary.formats = []ports.DMABufFormat{{Format: fourccXR30, Modifier: 19}}
157 r := portsmocks.NewMockRenderer(t)
158 buf := func() ports.DMABuf {
159 f, w, _ := os.Pipe()
160 w.Close()
161 return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
162 }
163 r.EXPECT().SetHDR(float64(ports.DefaultSDRBrightness)).Return().Once()
164 r.EXPECT().ExportTargets(2, []uint64{19}, false).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
165 r.EXPECT().UseTarget(mock.Anything).Return()
166 r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)
167 k.EXPECT().addFB(mock.Anything, uint32(fourccXR30)).Return(70, nil).Twice()
168 k.EXPECT().createBlob(mock.Anything).Return(321, nil).Once()
169 k.EXPECT().createBlob(mock.Anything).Return(11, nil).Twice()
170 k.EXPECT().destroyBlob(mock.Anything).Return(nil)
171 if err := o.showImages(r, imagesDriver, nil); err != nil {
172 t.Fatal(err)
173 }
174 if !o.hdr.on || o.hdr.failed || len(*commits) != 2 {
175 t.Fatalf("on=%t failed=%t commits=%d", o.hdr.on, o.hdr.failed, len(*commits))
176 }
177 for _, c := range *commits {
178 for _, tc := range []struct {
179 prop uint32
180 want uint64
181 }{{100, 321}, {101, 7}, {102, 10}} {
182 if got, ok := c.req.value(o.conn.id, tc.prop); !ok || got != tc.want {
183 t.Fatalf("commit prop %d: %d (%t), want %d", tc.prop, got, ok, tc.want)
184 }
185 }
186 }
187}
188
189// The metadata blob is retained across repeated image setup (e.g. VT resume)
190// and destroyed only once when Close restores SDR.
191func TestHDRBlobReusedAcrossImageSetup(t *testing.T) {
192 o, k, commits := testOutput(t)
193 o.cursor = nil
194 o.hdr.cap = hdrCapability{Capable: true}
195 o.hdr.settings = HDRSettings{Enabled: true, SDRBrightness: ports.DefaultSDRBrightness}
196 o.hdr.props = connectorHDRProps{Metadata: 100, Colorspace: 101, MaxBPC: 102, BT2020Value: 7, HasDefault: true, MaxBPCValue: 8}
197 o.primary.formats = []ports.DMABufFormat{{Format: fourccXR30, Modifier: 19}}
198 r := portsmocks.NewMockRenderer(t)
199 buf := func() ports.DMABuf {
200 f, w, _ := os.Pipe()
201 w.Close()
202 return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
203 }
204 r.EXPECT().SetHDR(float64(ports.DefaultSDRBrightness)).Return().Twice()
205 r.EXPECT().ExportTargets(2, []uint64{19}, false).RunAndReturn(func(int, []uint64, bool) ([]ports.DMABuf, error) { return []ports.DMABuf{buf(), buf()}, nil }).Twice()
206 r.EXPECT().UseTarget(mock.Anything).Return()
207 r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)
208 k.EXPECT().addFB(mock.Anything, uint32(fourccXR30)).Return(70, nil)
209 k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
210 created, destroyed := 0, 0
211 k.EXPECT().createBlob(mock.Anything).RunAndReturn(func(data []byte) (uint32, error) {
212 if len(data) == 32 {
213 created++
214 return 321, nil
215 }
216 return 11, nil
217 })
218 k.EXPECT().destroyBlob(mock.Anything).RunAndReturn(func(id uint32) error {
219 if id == 321 {
220 destroyed++
221 }
222 return nil
223 })
224 for i := 0; i < 2; i++ {
225 if err := o.showImages(r, imagesDriver, nil); err != nil {
226 t.Fatal(err)
227 }
228 if o.hdr.blob != 321 || created != 1 || destroyed != 0 {
229 t.Fatalf("pass %d blob %d created %d destroyed %d", i, o.hdr.blob, created, destroyed)
230 }
231 }
232 if len(*commits) != 4 {
233 t.Fatalf("commits: %d", len(*commits))
234 }
235 o.Close()
236 if created != 1 || destroyed != 1 {
237 t.Fatalf("created %d destroyed %d", created, destroyed)
238 }
239}
240
241func TestHDRBlobReplacementWaitsForCommit(t *testing.T) {
242 o, k, _ := testOutput(t)
243 o.cursor = nil
244 o.hdr.cap = hdrCapability{Capable: true}
245 o.hdr.settings = HDRSettings{Enabled: true, SDRBrightness: ports.DefaultSDRBrightness}
246 o.hdr.props = connectorHDRProps{Metadata: 100, Colorspace: 101, MaxBPC: 102, BT2020Value: 7, HasDefault: true, MaxBPCValue: 8}
247 o.primary.formats = []ports.DMABufFormat{{Format: fourccXR30, Modifier: 19}}
248 r := portsmocks.NewMockRenderer(t)
249 buf := func() ports.DMABuf {
250 f, w, _ := os.Pipe()
251 w.Close()
252 return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
253 }
254 r.EXPECT().SetHDR(float64(ports.DefaultSDRBrightness)).Return().Twice()
255 r.EXPECT().ExportTargets(2, []uint64{19}, false).RunAndReturn(func(int, []uint64, bool) ([]ports.DMABuf, error) { return []ports.DMABuf{buf(), buf()}, nil }).Twice()
256 r.EXPECT().UseTarget(mock.Anything).Return()
257 r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)
258 k.EXPECT().addFB(mock.Anything, uint32(fourccXR30)).Return(70, nil)
259 k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
260 metadata := uint32(320)
261 k.EXPECT().createBlob(mock.Anything).RunAndReturn(func(data []byte) (uint32, error) {
262 if len(data) == 32 {
263 metadata++
264 return metadata, nil
265 }
266 return 11, nil
267 })
268 var destroyed []uint32
269 k.EXPECT().destroyBlob(mock.Anything).RunAndReturn(func(id uint32) error {
270 if id >= 321 {
271 destroyed = append(destroyed, id)
272 }
273 return nil
274 })
275 if err := o.showImages(r, imagesDriver, nil); err != nil {
276 t.Fatal(err)
277 }
278 o.monitor.HDR.MaxLuminance = 1000
279 if err := o.showImages(r, imagesDriver, nil); err != nil {
280 t.Fatal(err)
281 }
282 if o.hdr.blob != 322 || !slices.Equal(destroyed, []uint32{321}) {
283 t.Fatalf("blob %d destroyed %v", o.hdr.blob, destroyed)
284 }
285 o.Close()
286 if !slices.Equal(destroyed, []uint32{321, 322}) {
287 t.Fatalf("destroyed %v", destroyed)
288 }
289}
290
291// The metadata blob is reused when unchanged, and a replacement keeps the
292// old blob alive until its modeset is committed or rolled back.
293func TestHDRBlobSwap(t *testing.T) {
294 k := newMockkms(t)
295 meta := hdrMetadata(Monitor{HDR: HDRMetadata{MaxLuminance: 1000}})
296 other := hdrMetadata(Monitor{HDR: HDRMetadata{MaxLuminance: 600}})
297 k.EXPECT().createBlob(mock.Anything).Return(7, nil).Once()
298 var h hdrState
299 sw, err := h.prepareBlob(k, meta)
300 if err != nil || !sw.created || h.blob != 7 {
301 t.Fatalf("first blob: %v %+v %d", err, sw, h.blob)
302 }
303 h.commitBlob(k, sw)
304
305 // Unchanged metadata (VT resume): no new blob.
306 if sw, err = h.prepareBlob(k, meta); err != nil || sw.created || h.blob != 7 {
307 t.Fatalf("reuse: %v %+v %d", err, sw, h.blob)
308 }
309
310 // A refused replacement restores the old blob and frees the new one.
311 k.EXPECT().createBlob(mock.Anything).Return(8, nil).Once()
312 k.EXPECT().destroyBlob(uint32(8)).Return(nil).Once()
313 sw, _ = h.prepareBlob(k, other)
314 h.rollbackBlob(k, sw)
315 if h.blob != 7 || h.blobData != meta {
316 t.Fatalf("rollback: blob %d", h.blob)
317 }
318
319 // An accepted replacement frees the old blob.
320 k.EXPECT().createBlob(mock.Anything).Return(9, nil).Once()
321 k.EXPECT().destroyBlob(uint32(7)).Return(nil).Once()
322 sw, _ = h.prepareBlob(k, other)
323 h.commitBlob(k, sw)
324 if h.blob != 9 || h.blobData != other {
325 t.Fatalf("commit: blob %d", h.blob)
326 }
327
328 k.EXPECT().destroyBlob(uint32(9)).Return(nil).Once()
329 h.releaseBlob(k)
330 if h.blob != 0 || h.shown() {
331 t.Fatalf("release: blob %d", h.blob)
332 }
333}