package xorg import ( "cmp" "context" "errors" "slices" "strings" "github.com/go-logr/logr" "github.com/jezek/xgb/randr" corev1 "k8s.io/api/core/v1" kerrors "k8s.io/apimachinery/pkg/api/errors" "sigs.k8s.io/controller-runtime/pkg/client" "edge-infra.dev/pkg/lib/kernel/drm" "edge-infra.dev/pkg/sds/display/constants" "edge-infra.dev/pkg/sds/display/displaymanager/reader" v2 "edge-infra.dev/pkg/sds/display/k8s/apis/v2" "edge-infra.dev/pkg/sds/lib/xorg" "edge-infra.dev/pkg/sds/lib/xorg/dpms" "edge-infra.dev/pkg/sds/lib/xorg/xinput" "edge-infra.dev/pkg/sds/lib/xorg/xrandr" ) var displayPortOverrideObjectKey = client.ObjectKey{ Namespace: constants.Namespace, Name: constants.DisplayPortOverride, } var ( errUnableToMatchByEDID = errors.New("unable to find connector with matching EDID") errUnableToMatchByName = errors.New("unable to find connector with matching name") ) // Returns a new DisplayReader which can query the display configuration on the // node with Xrandr, represented as a DisplayConfig and returns the input devices // on the node using Xinput. // // Must be instantiated with xorg.OutputIDs and xorg.InputIDs to keep track // of device IDs supplied by Xorg at runtime. 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 { return &displayReader{ outputIDs: outputIDs, inputIDs: inputIDs, xrandr: r, xinput: i, dpms: d, client: c, log: log.WithName("reader"), } } type displayReader struct { opts reader.Options // outputIDs maps display-ports to output IDs, e.g. // {"card0-HDMI-A-1":"HDMI1","card0-DP-1":"DP1"} outputIDs map[v2.DisplayPort]xorg.OutputID // inputIDs maps input device names to each of their xinput IDs, e.g. // {"elo_touch_2077":["13","14"],"elo_touch_2088":["16"]} inputIDs map[v2.InputDeviceName][]xorg.InputDeviceID xrandr xrandr.Xrandr xinput xinput.Xinput dpms dpms.DPMS client client.Client log logr.Logger } func (x *displayReader) Read(ctx context.Context, options ...reader.Option) (*v2.DisplayConfig, []v2.InputDeviceName, error) { x.opts = reader.CreateOptions(options...) displayPortOverride, err := x.getDisplayPortOverride(ctx) if err != nil { return nil, nil, err } displayConfig, err := x.queryDisplayConfig(displayPortOverride) if err != nil { return nil, nil, err } inputDevices, err := x.queryInputDevices() if err != nil { return nil, nil, err } return displayConfig, inputDevices, nil } // Returns the display-port-override map from the ConfigMap, returning empty if not present. func (x *displayReader) getDisplayPortOverride(ctx context.Context) (map[xorg.OutputID]v2.DisplayPort, error) { displayPortOverride := map[xorg.OutputID]v2.DisplayPort{} // get ConfigMap and return empty if not found configMap := &corev1.ConfigMap{} if err := x.client.Get(ctx, displayPortOverrideObjectKey, configMap); kerrors.IsNotFound(err) { return displayPortOverride, nil } else if err != nil { return nil, err } // populate override values from config data for outputID, dp := range configMap.Data { displayPortOverride[xorg.OutputID(outputID)] = v2.DisplayPort(dp) } return displayPortOverride, nil } func (x *displayReader) queryDisplayConfig(displayPortOverride map[xorg.OutputID]v2.DisplayPort) (*v2.DisplayConfig, error) { displays, layout, err := x.queryDisplaysAndLayout(displayPortOverride) if err != nil { return nil, err } dpms, err := x.queryDPMS() if err != nil { return nil, err } return &v2.DisplayConfig{ Displays: displays, Layout: layout, DPMS: dpms, }, nil } func (x *displayReader) queryDisplaysAndLayout(displayPortOverride map[xorg.OutputID]v2.DisplayPort) (v2.Displays, v2.Layout, error) { outputs, err := x.queryOutputs() if err != nil { return nil, nil, err } // get all connectors from /sys/class/drm connectors, err := drm.Connectors() if err != nil { return nil, nil, err } displays := v2.Displays{} layout := v2.Layout{} for _, output := range outputs { dp := x.findDisplayPortForOutput(output, connectors, displayPortOverride) x.outputIDs[dp] = output.OutputID() // create display from output display := x.displayFromOutput(output, dp) displays.UpdateDisplay(display) // add to layout layout = append(layout, dp) } return displays, layout, nil } // queryOutputs queries the displays from X, returning them in order left-to-right. func (x *displayReader) queryOutputs() (xrandr.Outputs, error) { outputs, err := x.xrandr.GetOutputs() if err != nil { return nil, err } // return outputs in reverse order as they are found // (this is the default behavior for X) if x.opts.DefaultLayout { slices.Reverse(outputs) return outputs, nil } // otherwise return outputs in their current layout slices.SortFunc(outputs, func(a, b xrandr.Output) int { return cmp.Compare(a.Crtc.X, b.Crtc.X) }) return outputs, nil } func (x *displayReader) findDisplayPortForOutput(output xrandr.Output, connectors []drm.Connector, displayPortOverride map[xorg.OutputID]v2.DisplayPort) v2.DisplayPort { outputID := output.OutputID() log := x.log.WithValues("output", outputID) // if an override entry exists, we are done if dp, ok := displayPortOverride[outputID]; ok { return dp } // attempt to match output to connector by EDID connector, edidErr := findConnectorForOutputByEDID(output, connectors) if edidErr == nil { return v2.DisplayPort(connector.String()) } // attempt to match output ID to connector port connector, nameErr := findConnectorForOutputByName(output, connectors) if nameErr == nil { dp := v2.DisplayPort(connector.String()) log.Error(edidErr, "matched output to connector by name, which may be incorrect", "dp", dp) return dp } dp := v2.NewDisplayPort(v2.UnknownCard, outputID.String()) err := errors.Join(edidErr, nameErr) log.Error(err, "unable to match output to connector - marked card as \"unknown\"", "dp", dp) return dp } func findConnectorForOutputByEDID(output xrandr.Output, connectors []drm.Connector) (*drm.Connector, error) { outputEDID, err := output.EDID() if err != nil { return nil, err } for _, connector := range connectors { if outputEDID.Matches(connector.EDID()) { return &connector, nil } } return nil, errUnableToMatchByEDID } func findConnectorForOutputByName(output xrandr.Output, connectors []drm.Connector) (*drm.Connector, error) { outputID := output.OutputID() // find connector with port that matches the output ID for _, connector := range connectors { if connector.Port() == outputID.String() { return &connector, nil } } // try again with sanitised name for _, connector := range connectors { if outputMatchesSanitisedPort(outputID, connector) { return &connector, nil } } // final attempt for HDMI special port syntax for _, connector := range connectors { if outputMatchesSanitisedHDMIPort(outputID, connector) { return &connector, nil } } return nil, errUnableToMatchByName } // Compares output ID to connector port with "-" removed. func outputMatchesSanitisedPort(outputID xorg.OutputID, connector drm.Connector) bool { port := strings.ReplaceAll(connector.Port(), "-", "") return strings.EqualFold(port, outputID.String()) } // Compares HDMI output ID to connector port by removing syntax, // e.g. HDMI1 should match card0-HDMI-A-1. func outputMatchesSanitisedHDMIPort(outputID xorg.OutputID, connector drm.Connector) bool { if !strings.HasPrefix(outputID.String(), "HDMI") { return false } strs := strings.Split(connector.Port(), "-") if len(strs) != 3 { return false } port := strs[0] + strs[2] return strings.EqualFold(port, outputID.String()) } func (x *displayReader) displayFromOutput(output xrandr.Output, dp v2.DisplayPort) v2.Display { return v2.Display{ DisplayPort: dp, MPID: x.mpidFromOutput(output), Primary: x.primaryFromOutput(output), Resolution: x.resolutionFromOutput(output), SupportedResolutions: x.supportedResolutionsFromOutput(output), Orientation: x.orientationFromOutput(output), } } // Creates an MPID for the display based on its EDID. // Returns nil if MPID cannot be created. func (x *displayReader) mpidFromOutput(output xrandr.Output) *v2.MPID { edid, err := output.EDID() if err != nil { return nil } mpid := v2.NewMPID(edid.ManufacturerId, uint(edid.ProductCode)) return &mpid } func (x *displayReader) primaryFromOutput(output xrandr.Output) *v2.Primary { primary := v2.Primary(output.Primary) return &primary } func (x *displayReader) resolutionFromOutput(output xrandr.Output) *v2.Resolution { hasPreferred := output.PreferredMode != nil && output.PreferredMode.Info != nil if x.opts.PreferredResolution && hasPreferred { return &v2.Resolution{ Width: int(output.PreferredMode.Info.Width), Height: int(output.PreferredMode.Info.Height), } } hasCurrent := output.CurrentMode != nil && output.CurrentMode.Info != nil if hasCurrent { return &v2.Resolution{ Width: int(output.CurrentMode.Info.Width), Height: int(output.CurrentMode.Info.Height), } } return nil } func (x *displayReader) supportedResolutionsFromOutput(output xrandr.Output) []v2.Resolution { if x.opts.IgnoreSupportedResolutions { return nil } var resolutions []v2.Resolution for _, mode := range output.Modes { resolutions = append(resolutions, v2.Resolution{ Width: int(mode.Info.Width), Height: int(mode.Info.Height), }) } return resolutions } func (x *displayReader) orientationFromOutput(output xrandr.Output) *v2.Orientation { if output.Crtc != nil { switch output.Crtc.Rotation { case randr.RotationRotate0: return &v2.NormalOrientation case randr.RotationRotate90: return &v2.RightOrientation case randr.RotationRotate180: return &v2.InvertedOrientation case randr.RotationRotate270: return &v2.LeftOrientation } } return &v2.NormalOrientation } func (x *displayReader) queryDPMS() (*v2.DPMS, error) { dpmsInfo, err := x.dpms.GetDPMSInfo() if err != nil { return nil, err } dpms := &v2.DPMS{} if dpmsInfo.ScreenSaver != nil { blankTime := int(dpmsInfo.ScreenSaver.Timeout) dpms.BlankTime = &blankTime } if dpmsInfo.Info != nil { enabled := dpmsInfo.Info.State dpms.Enabled = &enabled } if dpmsInfo.Timeouts != nil { offTime := int(dpmsInfo.Timeouts.OffTimeout) dpms.OffTime = &offTime standbyTime := int(dpmsInfo.Timeouts.StandbyTimeout) dpms.StandbyTime = &standbyTime suspendTime := int(dpmsInfo.Timeouts.SuspendTimeout) dpms.SuspendTime = &suspendTime } return dpms, nil } func (x *displayReader) queryInputDevices() ([]v2.InputDeviceName, error) { inputDeviceInfos, err := x.xinput.GetInputDeviceInfos() if err != nil { return nil, err } if x.opts.IgnoreRelativeInputDevices { inputDeviceInfos = inputDeviceInfos.AbsoluteDevices() } if len(inputDeviceInfos) == 0 { return nil, nil } inputDevices := []v2.InputDeviceName{} for _, inputDeviceInfo := range inputDeviceInfos { name := v2.InputDeviceName(inputDeviceInfo.Name) inputDevices = append(inputDevices, name) x.inputIDs[name] = append(x.inputIDs[name], inputDeviceInfo.ID) } return inputDevices, nil }