v0.3.0 cf370a0

hdr_test.go RawHistoryFolder

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, 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}).Return([]ports.DMABuf{buf(), buf()}, nil).Twice()
92	r.EXPECT().ExportTargets(0, []uint64(nil)).Run(func(int, []uint64) { calls = append(calls, "drop") }).Return(nil, nil).Once()
93	r.EXPECT().ExportTargets(2, []uint64(nil)).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	if len(*commits) != 4 {
112		t.Fatalf("commits: %d", len(*commits))
113	}
114	for i := range 2 {
115		if (*commits)[i].flags != atomicTestOnly|atomicAllowModes {
116			t.Fatalf("HDR test %d flags: %#x", i, (*commits)[i].flags)
117		}
118	}
119	if (*commits)[2].flags != atomicTestOnly|atomicAllowModes || (*commits)[3].flags != atomicAllowModes {
120		t.Fatalf("SDR commits: %#x %#x", (*commits)[2].flags, (*commits)[3].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}).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}).RunAndReturn(func(int, []uint64) ([]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}).RunAndReturn(func(int, []uint64) ([]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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 25

Tags 10

↑↓ moveenter openesc close