1package drm
2
3import (
4 "bufio"
5 "math"
6 "os"
7 "path/filepath"
8 "strings"
9)
10
11// Monitor identifies the display attached to a connector.
12type Monitor struct {
13 Make, Model, Serial string
14 Chromaticity Chromaticity
15 BitsPerColor int // 0 when unspecified
16 HDR HDRMetadata
17}
18
19// Chromaticity is the EDID base block's 10-bit CIE xy coordinates.
20type Chromaticity struct {
21 RedX, RedY, GreenX, GreenY, BlueX, BlueY, WhiteX, WhiteY float64
22}
23
24// HDRMetadata is the CTA-861 HDR static metadata and BT.2020 colorimetry.
25type HDRMetadata struct {
26 SDR, PQ, HLG, StaticType1, BT2020RGB, BT2020YCC bool
27 MaxLuminance, MaxFrameAverage, MinLuminance float64 // cd/m²; zero when unspecified
28}
29
30// readMonitor parses the connector's EDID from sysfs. Missing data yields "Unknown".
31func readMonitor(card, connector string) Monitor {
32 data, err := os.ReadFile(filepath.Join("/sys/class/drm", filepath.Base(card)+"-"+connector, "edid"))
33 if err != nil {
34 return Monitor{Make: "Unknown", Model: "Unknown"}
35 }
36 return parseEDID(data)
37}
38
39func parseEDID(d []byte) Monitor {
40 m := Monitor{Make: "Unknown", Model: "Unknown"}
41 if len(d) < 128 || string(d[:8]) != "\x00\xff\xff\xff\xff\xff\xff\x00" {
42 return m
43 }
44 id := uint16(d[8])<<8 | uint16(d[9])
45 pnp := string([]byte{byte(id>>10&0x1f) + '@', byte(id>>5&0x1f) + '@', byte(id&0x1f) + '@'})
46 m.Make = pnpName(pnp)
47 if d[20]&0x80 != 0 {
48 m.BitsPerColor = []int{0, 6, 8, 10, 12, 14, 16, 0}[d[20]>>4&7]
49 }
50 xy := func(hi, lo byte) float64 { return float64(uint16(hi)<<2|uint16(lo)) / 1024 }
51 m.Chromaticity = Chromaticity{
52 RedX: xy(d[27], d[25]>>6), RedY: xy(d[28], d[25]>>4&3),
53 GreenX: xy(d[29], d[25]>>2&3), GreenY: xy(d[30], d[25]&3),
54 BlueX: xy(d[31], d[26]>>6), BlueY: xy(d[32], d[26]>>4&3),
55 WhiteX: xy(d[33], d[26]>>2&3), WhiteY: xy(d[34], d[26]&3),
56 }
57 for i := 0; i < int(d[126]) && (i+2)*128 <= len(d); i++ {
58 m.HDR.parseCTA(d[(i+1)*128 : (i+2)*128])
59 }
60 // Display descriptors: 0xFC name, 0xFF serial.
61 for off := 54; off+18 <= 126; off += 18 {
62 b := d[off : off+18]
63 if b[0] != 0 || b[1] != 0 {
64 continue
65 }
66 text := strings.TrimRight(strings.SplitN(string(b[5:18]), "\n", 2)[0], " ")
67 switch b[3] {
68 case 0xfc:
69 m.Model = text
70 case 0xff:
71 m.Serial = text
72 }
73 }
74 return m
75}
76
77// parseCTA skips malformed data blocks rather than reading into the DTD area.
78func (h *HDRMetadata) parseCTA(b []byte) {
79 if len(b) < 128 || b[0] != 0x02 || b[2] < 4 || b[2] > 127 {
80 return
81 }
82 for off := 4; off < int(b[2]); {
83 size := int(b[off] & 0x1f)
84 end := off + 1 + size
85 if end > int(b[2]) {
86 return
87 }
88 if b[off]>>5 == 7 && size >= 2 {
89 v := b[off+1 : end]
90 switch v[0] {
91 case 5: // colorimetry: BT2020RGB bit 7, BT2020YCC bit 6
92 h.BT2020RGB = h.BT2020RGB || v[1]&0x80 != 0
93 h.BT2020YCC = h.BT2020YCC || v[1]&0x40 != 0
94 case 6: // HDR static metadata: EOTF, descriptors, luminances
95 if len(v) < 3 {
96 break
97 }
98 h.SDR = h.SDR || v[1]&1 != 0
99 h.PQ = h.PQ || v[1]&4 != 0
100 h.HLG = h.HLG || v[1]&8 != 0
101 h.StaticType1 = h.StaticType1 || v[2]&1 != 0
102 if len(v) > 3 && v[3] != 0 {
103 h.MaxLuminance = 50 * math.Exp2(float64(v[3])/32)
104 }
105 if len(v) > 4 && v[4] != 0 {
106 h.MaxFrameAverage = 50 * math.Exp2(float64(v[4])/32)
107 }
108 if len(v) > 5 && h.MaxLuminance != 0 {
109 h.MinLuminance = h.MaxLuminance * math.Pow(float64(v[5])/255, 2) / 100
110 }
111 }
112 }
113 off = end
114 }
115}
116
117// pnpName maps a PNP id to a vendor name via hwdata, falling back to the id.
118func pnpName(id string) string {
119 f, err := os.Open("/usr/share/hwdata/pnp.ids")
120 if err != nil {
121 return id
122 }
123 defer f.Close()
124 s := bufio.NewScanner(f)
125 for s.Scan() {
126 if name, ok := strings.CutPrefix(s.Text(), id+"\t"); ok {
127 return name
128 }
129 }
130 return id
131}