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.cursor = nil
152 o.hdr.cap = hdrCapability{Capable: true}
153 o.hdr.settings = HDRSettings{Enabled: true, SDRBrightness: ports.DefaultSDRBrightness}
154 o.hdr.props = connectorHDRProps{Metadata: 100, Colorspace: 101, MaxBPC: 102, BT2020Value: 7, HasDefault: true, MaxBPCValue: 8}
155 o.primary.formats = []ports.DMABufFormat{{Format: fourccXR30, Modifier: 19}}
156 r := portsmocks.NewMockRenderer(t)
157 buf := func() ports.DMABuf {
158 f, w, _ := os.Pipe()
159 w.Close()
160 return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
161 }
162 r.EXPECT().SetHDR(float64(ports.DefaultSDRBrightness)).Return().Once()
163 r.EXPECT().ExportTargets(2, []uint64{19}, false).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
164 r.EXPECT().UseTarget(mock.Anything).Return()
165 r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)
166 k.EXPECT().addFB(mock.Anything, uint32(fourccXR30)).Return(70, nil).Twice()
167 k.EXPECT().createBlob(mock.Anything).Return(321, nil).Once()
168 k.EXPECT().createBlob(mock.Anything).Return(11, nil).Twice()
169 k.EXPECT().destroyBlob(mock.Anything).Return(nil)
170 if err := o.showImages(r, imagesDriver, nil); err != nil {
171 t.Fatal(err)
172 }
173 if !o.hdr.on || o.hdr.failed || len(*commits) != 2 {
174 t.Fatalf("on=%t failed=%t commits=%d", o.hdr.on, o.hdr.failed, len(*commits))
175 }
176 for _, c := range *commits {
177 for _, tc := range []struct {
178 prop uint32
179 want uint64
180 }{{100, 321}, {101, 7}, {102, 10}} {
181 if got, ok := c.req.value(o.conn.id, tc.prop); !ok || got != tc.want {
182 t.Fatalf("commit prop %d: %d (%t), want %d", tc.prop, got, ok, tc.want)
183 }
184 }
185 }
186}
187
188// The metadata blob is retained across repeated image setup (e.g. VT resume)
189// and destroyed only once when Close restores SDR.
190func TestHDRBlobReusedAcrossImageSetup(t *testing.T) {
191 o, k, commits := testOutput(t)
192 o.cursor = nil
193 o.hdr.cap = hdrCapability{Capable: true}
194 o.hdr.settings = HDRSettings{Enabled: true, SDRBrightness: ports.DefaultSDRBrightness}
195 o.hdr.props = connectorHDRProps{Metadata: 100, Colorspace: 101, MaxBPC: 102, BT2020Value: 7, HasDefault: true, MaxBPCValue: 8}
196 o.primary.formats = []ports.DMABufFormat{{Format: fourccXR30, Modifier: 19}}
197 r := portsmocks.NewMockRenderer(t)
198 buf := func() ports.DMABuf {
199 f, w, _ := os.Pipe()
200 w.Close()
201 return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
202 }
203 r.EXPECT().SetHDR(float64(ports.DefaultSDRBrightness)).Return().Twice()
204 r.EXPECT().ExportTargets(2, []uint64{19}, false).RunAndReturn(func(int, []uint64, bool) ([]ports.DMABuf, error) { return []ports.DMABuf{buf(), buf()}, nil }).Twice()
205 r.EXPECT().UseTarget(mock.Anything).Return()
206 r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)
207 k.EXPECT().addFB(mock.Anything, uint32(fourccXR30)).Return(70, nil)
208 k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
209 created, destroyed := 0, 0
210 k.EXPECT().createBlob(mock.Anything).RunAndReturn(func(data []byte) (uint32, error) {
211 if len(data) == 32 {
212 created++
213 return 321, nil
214 }
215 return 11, nil
216 })
217 k.EXPECT().destroyBlob(mock.Anything).RunAndReturn(func(id uint32) error {
218 if id == 321 {
219 destroyed++
220 }
221 return nil
222 })
223 for i := 0; i < 2; i++ {
224 if err := o.showImages(r, imagesDriver, nil); err != nil {
225 t.Fatal(err)
226 }
227 if o.hdr.blob != 321 || created != 1 || destroyed != 0 {
228 t.Fatalf("pass %d blob %d created %d destroyed %d", i, o.hdr.blob, created, destroyed)
229 }
230 }
231 if len(*commits) != 4 {
232 t.Fatalf("commits: %d", len(*commits))
233 }
234 o.Close()
235 if created != 1 || destroyed != 1 {
236 t.Fatalf("created %d destroyed %d", created, destroyed)
237 }
238}
239
240func TestHDRBlobReplacementWaitsForCommit(t *testing.T) {
241 o, k, _ := testOutput(t)
242 o.cursor = nil
243 o.hdr.cap = hdrCapability{Capable: true}
244 o.hdr.settings = HDRSettings{Enabled: true, SDRBrightness: ports.DefaultSDRBrightness}
245 o.hdr.props = connectorHDRProps{Metadata: 100, Colorspace: 101, MaxBPC: 102, BT2020Value: 7, HasDefault: true, MaxBPCValue: 8}
246 o.primary.formats = []ports.DMABufFormat{{Format: fourccXR30, Modifier: 19}}
247 r := portsmocks.NewMockRenderer(t)
248 buf := func() ports.DMABuf {
249 f, w, _ := os.Pipe()
250 w.Close()
251 return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
252 }
253 r.EXPECT().SetHDR(float64(ports.DefaultSDRBrightness)).Return().Twice()
254 r.EXPECT().ExportTargets(2, []uint64{19}, false).RunAndReturn(func(int, []uint64, bool) ([]ports.DMABuf, error) { return []ports.DMABuf{buf(), buf()}, nil }).Twice()
255 r.EXPECT().UseTarget(mock.Anything).Return()
256 r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)
257 k.EXPECT().addFB(mock.Anything, uint32(fourccXR30)).Return(70, nil)
258 k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
259 metadata := uint32(320)
260 k.EXPECT().createBlob(mock.Anything).RunAndReturn(func(data []byte) (uint32, error) {
261 if len(data) == 32 {
262 metadata++
263 return metadata, nil
264 }
265 return 11, nil
266 })
267 var destroyed []uint32
268 k.EXPECT().destroyBlob(mock.Anything).RunAndReturn(func(id uint32) error {
269 if id >= 321 {
270 destroyed = append(destroyed, id)
271 }
272 return nil
273 })
274 if err := o.showImages(r, imagesDriver, nil); err != nil {
275 t.Fatal(err)
276 }
277 o.monitor.HDR.MaxLuminance = 1000
278 if err := o.showImages(r, imagesDriver, nil); err != nil {
279 t.Fatal(err)
280 }
281 if o.hdr.blob != 322 || !slices.Equal(destroyed, []uint32{321}) {
282 t.Fatalf("blob %d destroyed %v", o.hdr.blob, destroyed)
283 }
284 o.Close()
285 if !slices.Equal(destroyed, []uint32{321, 322}) {
286 t.Fatalf("destroyed %v", destroyed)
287 }
288}
289
290// The metadata blob is reused when unchanged, and a replacement keeps the
291// old blob alive until its modeset is committed or rolled back.
292func TestHDRBlobSwap(t *testing.T) {
293 k := newMockkms(t)
294 meta := hdrMetadata(Monitor{HDR: HDRMetadata{MaxLuminance: 1000}})
295 other := hdrMetadata(Monitor{HDR: HDRMetadata{MaxLuminance: 600}})
296 k.EXPECT().createBlob(mock.Anything).Return(7, nil).Once()
297 var h hdrState
298 sw, err := h.prepareBlob(k, meta)
299 if err != nil || !sw.created || h.blob != 7 {
300 t.Fatalf("first blob: %v %+v %d", err, sw, h.blob)
301 }
302 h.commitBlob(k, sw)
303
304 // Unchanged metadata (VT resume): no new blob.
305 if sw, err = h.prepareBlob(k, meta); err != nil || sw.created || h.blob != 7 {
306 t.Fatalf("reuse: %v %+v %d", err, sw, h.blob)
307 }
308
309 // A refused replacement restores the old blob and frees the new one.
310 k.EXPECT().createBlob(mock.Anything).Return(8, nil).Once()
311 k.EXPECT().destroyBlob(uint32(8)).Return(nil).Once()
312 sw, _ = h.prepareBlob(k, other)
313 h.rollbackBlob(k, sw)
314 if h.blob != 7 || h.blobData != meta {
315 t.Fatalf("rollback: blob %d", h.blob)
316 }
317
318 // An accepted replacement frees the old blob.
319 k.EXPECT().createBlob(mock.Anything).Return(9, nil).Once()
320 k.EXPECT().destroyBlob(uint32(7)).Return(nil).Once()
321 sw, _ = h.prepareBlob(k, other)
322 h.commitBlob(k, sw)
323 if h.blob != 9 || h.blobData != other {
324 t.Fatalf("commit: blob %d", h.blob)
325 }
326
327 k.EXPECT().destroyBlob(uint32(9)).Return(nil).Once()
328 h.releaseBlob(k)
329 if h.blob != 0 || h.shown() {
330 t.Fatalf("release: blob %d", h.blob)
331 }
332}