1 package xorg
2
3 import (
4 "cmp"
5 "context"
6 "errors"
7 "slices"
8 "strings"
9
10 "github.com/go-logr/logr"
11 "github.com/jezek/xgb/randr"
12 corev1 "k8s.io/api/core/v1"
13 kerrors "k8s.io/apimachinery/pkg/api/errors"
14 "sigs.k8s.io/controller-runtime/pkg/client"
15
16 "edge-infra.dev/pkg/lib/kernel/drm"
17 "edge-infra.dev/pkg/sds/display/constants"
18 "edge-infra.dev/pkg/sds/display/displaymanager/reader"
19 v2 "edge-infra.dev/pkg/sds/display/k8s/apis/v2"
20 "edge-infra.dev/pkg/sds/lib/xorg"
21 "edge-infra.dev/pkg/sds/lib/xorg/dpms"
22 "edge-infra.dev/pkg/sds/lib/xorg/xinput"
23 "edge-infra.dev/pkg/sds/lib/xorg/xrandr"
24 )
25
26 var displayPortOverrideObjectKey = client.ObjectKey{
27 Namespace: constants.Namespace,
28 Name: constants.DisplayPortOverride,
29 }
30
31 var (
32 errUnableToMatchByEDID = errors.New("unable to find connector with matching EDID")
33 errUnableToMatchByName = errors.New("unable to find connector with matching name")
34 )
35
36
37
38
39
40
41
42 func NewXorgDisplayReader(outputIDs map[v2.DisplayPort]xorg.OutputID, inputIDs map[v2.InputDeviceName][]xorg.InputDeviceID, r xrandr.Xrandr, i xinput.Xinput, d dpms.DPMS, c client.Client, log logr.Logger) reader.DisplayReader {
43 return &displayReader{
44 outputIDs: outputIDs,
45 inputIDs: inputIDs,
46 xrandr: r,
47 xinput: i,
48 dpms: d,
49 client: c,
50 log: log.WithName("reader"),
51 }
52 }
53
54 type displayReader struct {
55 opts reader.Options
56
57
58
59 outputIDs map[v2.DisplayPort]xorg.OutputID
60
61
62 inputIDs map[v2.InputDeviceName][]xorg.InputDeviceID
63
64 xrandr xrandr.Xrandr
65 xinput xinput.Xinput
66 dpms dpms.DPMS
67
68 client client.Client
69
70 log logr.Logger
71 }
72
73 func (x *displayReader) Read(ctx context.Context, options ...reader.Option) (*v2.DisplayConfig, []v2.InputDeviceName, error) {
74 x.opts = reader.CreateOptions(options...)
75
76 displayPortOverride, err := x.getDisplayPortOverride(ctx)
77 if err != nil {
78 return nil, nil, err
79 }
80
81 displayConfig, err := x.queryDisplayConfig(displayPortOverride)
82 if err != nil {
83 return nil, nil, err
84 }
85
86 inputDevices, err := x.queryInputDevices()
87 if err != nil {
88 return nil, nil, err
89 }
90
91 return displayConfig, inputDevices, nil
92 }
93
94
95 func (x *displayReader) getDisplayPortOverride(ctx context.Context) (map[xorg.OutputID]v2.DisplayPort, error) {
96 displayPortOverride := map[xorg.OutputID]v2.DisplayPort{}
97
98
99 configMap := &corev1.ConfigMap{}
100 if err := x.client.Get(ctx, displayPortOverrideObjectKey, configMap); kerrors.IsNotFound(err) {
101 return displayPortOverride, nil
102 } else if err != nil {
103 return nil, err
104 }
105
106
107 for outputID, dp := range configMap.Data {
108 displayPortOverride[xorg.OutputID(outputID)] = v2.DisplayPort(dp)
109 }
110
111 return displayPortOverride, nil
112 }
113
114 func (x *displayReader) queryDisplayConfig(displayPortOverride map[xorg.OutputID]v2.DisplayPort) (*v2.DisplayConfig, error) {
115 displays, layout, err := x.queryDisplaysAndLayout(displayPortOverride)
116 if err != nil {
117 return nil, err
118 }
119
120 dpms, err := x.queryDPMS()
121 if err != nil {
122 return nil, err
123 }
124
125 return &v2.DisplayConfig{
126 Displays: displays,
127 Layout: layout,
128 DPMS: dpms,
129 }, nil
130 }
131
132 func (x *displayReader) queryDisplaysAndLayout(displayPortOverride map[xorg.OutputID]v2.DisplayPort) (v2.Displays, v2.Layout, error) {
133 outputs, err := x.queryOutputs()
134 if err != nil {
135 return nil, nil, err
136 }
137
138
139 connectors, err := drm.Connectors()
140 if err != nil {
141 return nil, nil, err
142 }
143
144 displays := v2.Displays{}
145 layout := v2.Layout{}
146
147 for _, output := range outputs {
148 dp := x.findDisplayPortForOutput(output, connectors, displayPortOverride)
149 x.outputIDs[dp] = output.OutputID()
150
151
152 display := x.displayFromOutput(output, dp)
153 displays.UpdateDisplay(display)
154
155
156 layout = append(layout, dp)
157 }
158
159 return displays, layout, nil
160 }
161
162
163 func (x *displayReader) queryOutputs() (xrandr.Outputs, error) {
164 outputs, err := x.xrandr.GetOutputs()
165 if err != nil {
166 return nil, err
167 }
168
169
170
171 if x.opts.DefaultLayout {
172 slices.Reverse(outputs)
173 return outputs, nil
174 }
175
176
177 slices.SortFunc(outputs, func(a, b xrandr.Output) int {
178 return cmp.Compare(a.Crtc.X, b.Crtc.X)
179 })
180
181 return outputs, nil
182 }
183
184 func (x *displayReader) findDisplayPortForOutput(output xrandr.Output, connectors []drm.Connector, displayPortOverride map[xorg.OutputID]v2.DisplayPort) v2.DisplayPort {
185 outputID := output.OutputID()
186 log := x.log.WithValues("output", outputID)
187
188
189 if dp, ok := displayPortOverride[outputID]; ok {
190 return dp
191 }
192
193
194 connector, edidErr := findConnectorForOutputByEDID(output, connectors)
195 if edidErr == nil {
196 return v2.DisplayPort(connector.String())
197 }
198
199
200 connector, nameErr := findConnectorForOutputByName(output, connectors)
201 if nameErr == nil {
202 dp := v2.DisplayPort(connector.String())
203 log.Error(edidErr, "matched output to connector by name, which may be incorrect", "dp", dp)
204 return dp
205 }
206
207 dp := v2.NewDisplayPort(v2.UnknownCard, outputID.String())
208
209 err := errors.Join(edidErr, nameErr)
210 log.Error(err, "unable to match output to connector - marked card as \"unknown\"", "dp", dp)
211
212 return dp
213 }
214
215 func findConnectorForOutputByEDID(output xrandr.Output, connectors []drm.Connector) (*drm.Connector, error) {
216 outputEDID, err := output.EDID()
217 if err != nil {
218 return nil, err
219 }
220
221 for _, connector := range connectors {
222 if outputEDID.Matches(connector.EDID()) {
223 return &connector, nil
224 }
225 }
226
227 return nil, errUnableToMatchByEDID
228 }
229
230 func findConnectorForOutputByName(output xrandr.Output, connectors []drm.Connector) (*drm.Connector, error) {
231 outputID := output.OutputID()
232
233
234 for _, connector := range connectors {
235 if connector.Port() == outputID.String() {
236 return &connector, nil
237 }
238 }
239
240
241 for _, connector := range connectors {
242 if outputMatchesSanitisedPort(outputID, connector) {
243 return &connector, nil
244 }
245 }
246
247
248 for _, connector := range connectors {
249 if outputMatchesSanitisedHDMIPort(outputID, connector) {
250 return &connector, nil
251 }
252 }
253
254 return nil, errUnableToMatchByName
255 }
256
257
258 func outputMatchesSanitisedPort(outputID xorg.OutputID, connector drm.Connector) bool {
259 port := strings.ReplaceAll(connector.Port(), "-", "")
260 return strings.EqualFold(port, outputID.String())
261 }
262
263
264
265 func outputMatchesSanitisedHDMIPort(outputID xorg.OutputID, connector drm.Connector) bool {
266 if !strings.HasPrefix(outputID.String(), "HDMI") {
267 return false
268 }
269
270 strs := strings.Split(connector.Port(), "-")
271 if len(strs) != 3 {
272 return false
273 }
274
275 port := strs[0] + strs[2]
276 return strings.EqualFold(port, outputID.String())
277 }
278
279 func (x *displayReader) displayFromOutput(output xrandr.Output, dp v2.DisplayPort) v2.Display {
280 return v2.Display{
281 DisplayPort: dp,
282 MPID: x.mpidFromOutput(output),
283 Primary: x.primaryFromOutput(output),
284 Resolution: x.resolutionFromOutput(output),
285 SupportedResolutions: x.supportedResolutionsFromOutput(output),
286 Orientation: x.orientationFromOutput(output),
287 }
288 }
289
290
291
292 func (x *displayReader) mpidFromOutput(output xrandr.Output) *v2.MPID {
293 edid, err := output.EDID()
294 if err != nil {
295 return nil
296 }
297 mpid := v2.NewMPID(edid.ManufacturerId, uint(edid.ProductCode))
298 return &mpid
299 }
300
301 func (x *displayReader) primaryFromOutput(output xrandr.Output) *v2.Primary {
302 primary := v2.Primary(output.Primary)
303 return &primary
304 }
305
306 func (x *displayReader) resolutionFromOutput(output xrandr.Output) *v2.Resolution {
307 hasPreferred := output.PreferredMode != nil && output.PreferredMode.Info != nil
308 if x.opts.PreferredResolution && hasPreferred {
309 return &v2.Resolution{
310 Width: int(output.PreferredMode.Info.Width),
311 Height: int(output.PreferredMode.Info.Height),
312 }
313 }
314
315 hasCurrent := output.CurrentMode != nil && output.CurrentMode.Info != nil
316 if hasCurrent {
317 return &v2.Resolution{
318 Width: int(output.CurrentMode.Info.Width),
319 Height: int(output.CurrentMode.Info.Height),
320 }
321 }
322
323 return nil
324 }
325
326 func (x *displayReader) supportedResolutionsFromOutput(output xrandr.Output) []v2.Resolution {
327 if x.opts.IgnoreSupportedResolutions {
328 return nil
329 }
330
331 var resolutions []v2.Resolution
332 for _, mode := range output.Modes {
333 resolutions = append(resolutions, v2.Resolution{
334 Width: int(mode.Info.Width),
335 Height: int(mode.Info.Height),
336 })
337 }
338
339 return resolutions
340 }
341
342 func (x *displayReader) orientationFromOutput(output xrandr.Output) *v2.Orientation {
343 if output.Crtc != nil {
344 switch output.Crtc.Rotation {
345 case randr.RotationRotate0:
346 return &v2.NormalOrientation
347 case randr.RotationRotate90:
348 return &v2.RightOrientation
349 case randr.RotationRotate180:
350 return &v2.InvertedOrientation
351 case randr.RotationRotate270:
352 return &v2.LeftOrientation
353 }
354 }
355 return &v2.NormalOrientation
356 }
357
358 func (x *displayReader) queryDPMS() (*v2.DPMS, error) {
359 dpmsInfo, err := x.dpms.GetDPMSInfo()
360 if err != nil {
361 return nil, err
362 }
363
364 dpms := &v2.DPMS{}
365
366 if dpmsInfo.ScreenSaver != nil {
367 blankTime := int(dpmsInfo.ScreenSaver.Timeout)
368 dpms.BlankTime = &blankTime
369 }
370
371 if dpmsInfo.Info != nil {
372 enabled := dpmsInfo.Info.State
373 dpms.Enabled = &enabled
374 }
375
376 if dpmsInfo.Timeouts != nil {
377 offTime := int(dpmsInfo.Timeouts.OffTimeout)
378 dpms.OffTime = &offTime
379 standbyTime := int(dpmsInfo.Timeouts.StandbyTimeout)
380 dpms.StandbyTime = &standbyTime
381 suspendTime := int(dpmsInfo.Timeouts.SuspendTimeout)
382 dpms.SuspendTime = &suspendTime
383 }
384
385 return dpms, nil
386 }
387
388 func (x *displayReader) queryInputDevices() ([]v2.InputDeviceName, error) {
389 inputDeviceInfos, err := x.xinput.GetInputDeviceInfos()
390 if err != nil {
391 return nil, err
392 }
393
394 if x.opts.IgnoreRelativeInputDevices {
395 inputDeviceInfos = inputDeviceInfos.AbsoluteDevices()
396 }
397
398 if len(inputDeviceInfos) == 0 {
399 return nil, nil
400 }
401
402 inputDevices := []v2.InputDeviceName{}
403 for _, inputDeviceInfo := range inputDeviceInfos {
404 name := v2.InputDeviceName(inputDeviceInfo.Name)
405 inputDevices = append(inputDevices, name)
406 x.inputIDs[name] = append(x.inputIDs[name], inputDeviceInfo.ID)
407 }
408
409 return inputDevices, nil
410 }
411
View as plain text