1package drm
2
3import (
4 "encoding/binary"
5 "slices"
6 "testing"
7 "time"
8 "unsafe"
9
10 "github.com/bnema/neferwl/internal/ports"
11)
12
13func TestStructSizesMatchIoctls(t *testing.T) {
14 for _, tc := range []struct {
15 name string
16 got uintptr
17 req uintptr
18 }{
19 {"card_res", unsafe.Sizeof(cardRes{}), ioctlGetResources},
20 {"get_encoder", unsafe.Sizeof(getEncoder{}), ioctlGetEncoder},
21 {"get_connector", unsafe.Sizeof(getConnector{}), ioctlGetConnector},
22 {"fb_cmd2", unsafe.Sizeof(fbCmd2{}), ioctlAddFB2},
23 {"prime_handle", unsafe.Sizeof(primeHandle{}), ioctlPrimeFDToHandle},
24 {"gem_close", unsafe.Sizeof(gemClose{}), ioctlGemClose},
25 {"obj_get_props", unsafe.Sizeof(objGetProps{}), ioctlObjGetProps},
26 {"get_prop", unsafe.Sizeof(getProp{}), ioctlGetProp},
27 {"mode_crtc", unsafe.Sizeof(modeCrtc{}), ioctlGetCrtc},
28 {"set_client_cap", unsafe.Sizeof(setClientCap{}), ioctlSetClientCap},
29 {"get_plane_res", unsafe.Sizeof(getPlaneRes{}), ioctlGetPlaneRes},
30 {"get_plane", unsafe.Sizeof(getPlane{}), ioctlGetPlane},
31 {"get_blob", unsafe.Sizeof(getBlob{}), ioctlGetPropBlob},
32 {"atomic", unsafe.Sizeof(modeAtomic{}), ioctlAtomic},
33 {"create_blob", unsafe.Sizeof(createBlob{}), ioctlCreateBlob},
34 {"get_cap", unsafe.Sizeof(getCap{}), ioctlGetCap},
35 {"destroy_blob", unsafe.Sizeof(uint32(0)), ioctlDestroyBlob},
36 } {
37 if want := (tc.req >> 16) & 0x3fff; tc.got != want {
38 t.Errorf("%s: size %d, ioctl encodes %d", tc.name, tc.got, want)
39 }
40 }
41 if unsafe.Sizeof(modeInfo{}) != 68 {
42 t.Errorf("modeInfo size %d, want 68", unsafe.Sizeof(modeInfo{}))
43 }
44}
45
46func TestWantPicksMode(t *testing.T) {
47 mode := func(w, h uint16, hz uint32, pref bool) modeInfo {
48 // htotal*vtotal = w*h keeps refreshMilli = clock*1000/(w*h).
49 m := modeInfo{HDisplay: w, VDisplay: h, HTotal: w, VTotal: h, Clock: uint32(uint64(w) * uint64(h) * uint64(hz) / 1000), VRefresh: hz}
50 if pref {
51 m.Type = modeTypePrefered
52 }
53 return m
54 }
55 lg := connector{name: "DP-2", connected: true, modes: []modeInfo{mode(5120, 2160, 165, true), mode(3440, 1440, 100, true), mode(5120, 2160, 100, false), mode(5120, 2160, 60, false)}}
56 off := connector{name: "DP-1", modes: []modeInfo{mode(1920, 1080, 60, true)}}
57 for _, tc := range []struct {
58 name string
59 want Want
60 mode string
61 }{
62 {"first preferred", Want{}, "5120x2160@165.000"},
63 {"closest refresh", Want{Modes: map[string][3]float64{"DP-2": {5120, 2160, 99}}}, "5120x2160@100.000"},
64 {"highest refresh", Want{Modes: map[string][3]float64{"DP-2": {5120, 2160, 0}}}, "5120x2160@165.000"},
65 {"unknown mode falls back to preferred", Want{Modes: map[string][3]float64{"DP-2": {800, 600, 0}}}, "5120x2160@165.000"},
66 } {
67 if m := tc.want.pickMode(lg); m.String() != tc.mode {
68 t.Errorf("%s: got %s", tc.name, m)
69 }
70 }
71 vr := lg
72 vr.nonDesktop = true
73 if (Want{}).usable(off) || !(Want{}).usable(lg) || (Want{Disabled: map[string]bool{"DP-2": true}}).usable(lg) || (Want{}).usable(vr) {
74 t.Fatal("usable: non-desktop connectors must not be modeset")
75 }
76}
77
78func TestRefreshMilliLG(t *testing.T) {
79 // Real 5120x2160@165.058 CVT-RB timings.
80 m := modeInfo{Clock: 1_989_530, HDisplay: 5120, HTotal: 5200, VDisplay: 2160, VTotal: 2318}
81 if got := m.refreshMilli(); got < 165_000 || got > 165_100 {
82 t.Fatalf("refresh %d mHz", got)
83 }
84}
85
86func TestParseEDID(t *testing.T) {
87 d := make([]byte, 128)
88 copy(d, "\x00\xff\xff\xff\xff\xff\xff\x00")
89 d[8], d[9] = 0x1e, 0x6d // GSM
90 copy(d[54:], []byte{0, 0, 0, 0xfc, 0})
91 copy(d[59:], "LG ULTRAGEAR+\n")
92 copy(d[72:], []byte{0, 0, 0, 0xff, 0})
93 copy(d[77:], "SN123\n ")
94 m := parseEDID(d)
95 if m.Model != "LG ULTRAGEAR+" || m.Serial != "SN123" || (m.Make != "GSM" && m.Make != "LG Electronics") {
96 t.Fatalf("%+v", m)
97 }
98}
99
100func TestParseFlips(t *testing.T) {
101 ev := func(typ, crtc uint32, user uint64, sec, usec, seq uint32) []byte {
102 b := make([]byte, 32)
103 le := binary.LittleEndian
104 le.PutUint32(b, typ)
105 le.PutUint32(b[4:], 32)
106 le.PutUint64(b[8:], user)
107 le.PutUint32(b[16:], sec)
108 le.PutUint32(b[20:], usec)
109 le.PutUint32(b[24:], seq)
110 le.PutUint32(b[28:], crtc)
111 return b
112 }
113 buf := append(append(ev(eventFlipDone, 41, userFrame, 3, 250, 7), ev(1, 7, 0, 0, 0, 0)...), ev(eventFlipDone, 42, userState, 0, 1, 8)...)
114 got := parseFlips(nil, buf)
115 want := []flipEvent{{crtc: 41, user: userFrame, when: 3*time.Second + 250*time.Microsecond, seq: 7}, {crtc: 42, user: userState, when: time.Microsecond, seq: 8}}
116 if !slices.Equal(got, want) {
117 t.Fatalf("got %+v, want %+v", got, want)
118 }
119 if got := parseFlips(nil, buf[:20]); len(got) != 0 {
120 t.Fatalf("truncated: got %v", got)
121 }
122 // ReadEvents reuses one slice across reads.
123 reused := parseFlips(nil, buf)
124 if n := testing.AllocsPerRun(100, func() { reused = parseFlips(reused[:0], buf) }); n != 0 || !slices.Equal(reused, want) {
125 t.Fatalf("reused parse: %.1f allocs, got %+v", n, reused)
126 }
127}
128
129func TestParseInFormats(t *testing.T) {
130 le := binary.LittleEndian
131 // 3 formats at 24, 2 modifiers at 40.
132 b := make([]byte, 40+2*24)
133 le.PutUint32(b, 1)
134 le.PutUint32(b[8:], 3)
135 le.PutUint32(b[12:], 24)
136 le.PutUint32(b[16:], 2)
137 le.PutUint32(b[20:], 40)
138 for i, f := range []uint32{fourccXRGB, fourccARGB, 0x3432564e} {
139 le.PutUint32(b[24+i*4:], f)
140 }
141 // linear on formats 0 and 2, a tiled modifier on format 1.
142 le.PutUint64(b[40:], 0b101)
143 le.PutUint64(b[56:], 0)
144 le.PutUint64(b[64:], 0b010)
145 le.PutUint64(b[80:], 0x0200000000000001)
146 got := parseInFormats(b)
147 want := []ports.DMABufFormat{{Format: fourccXRGB}, {Format: 0x3432564e}, {Format: fourccARGB, Modifier: 0x0200000000000001}}
148 if !slices.Equal(got, want) {
149 t.Fatalf("got %+v, want %+v", got, want)
150 }
151 if parseInFormats(b[:30]) != nil {
152 t.Fatal("truncated blob parsed")
153 }
154}