...

Source file src/edge-infra.dev/pkg/sds/display/displaymanager/reader/xorg/xorg_reader.go

Documentation: edge-infra.dev/pkg/sds/display/displaymanager/reader/xorg

     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  // Returns a new DisplayReader which can query the display configuration on the
    37  // node with Xrandr, represented as a DisplayConfig and returns the input devices
    38  // on the node using Xinput.
    39  //
    40  // Must be instantiated with xorg.OutputIDs and xorg.InputIDs to keep track
    41  // of device IDs supplied by Xorg at runtime.
    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  	// outputIDs maps display-ports to output IDs, e.g.
    58  	//   {"card0-HDMI-A-1":"HDMI1","card0-DP-1":"DP1"}
    59  	outputIDs map[v2.DisplayPort]xorg.OutputID
    60  	// inputIDs maps input device names to each of their xinput IDs, e.g.
    61  	//   {"elo_touch_2077":["13","14"],"elo_touch_2088":["16"]}
    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  // Returns the display-port-override map from the ConfigMap, returning empty if not present.
    95  func (x *displayReader) getDisplayPortOverride(ctx context.Context) (map[xorg.OutputID]v2.DisplayPort, error) {
    96  	displayPortOverride := map[xorg.OutputID]v2.DisplayPort{}
    97  
    98  	// get ConfigMap and return empty if not found
    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  	// populate override values from config data
   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  	// get all connectors from /sys/class/drm
   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  		// create display from output
   152  		display := x.displayFromOutput(output, dp)
   153  		displays.UpdateDisplay(display)
   154  
   155  		// add to layout
   156  		layout = append(layout, dp)
   157  	}
   158  
   159  	return displays, layout, nil
   160  }
   161  
   162  // queryOutputs queries the displays from X, returning them in order left-to-right.
   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  	// return outputs in reverse order as they are found
   170  	// (this is the default behavior for X)
   171  	if x.opts.DefaultLayout {
   172  		slices.Reverse(outputs)
   173  		return outputs, nil
   174  	}
   175  
   176  	// otherwise return outputs in their current layout
   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  	// if an override entry exists, we are done
   189  	if dp, ok := displayPortOverride[outputID]; ok {
   190  		return dp
   191  	}
   192  
   193  	// attempt to match output to connector by EDID
   194  	connector, edidErr := findConnectorForOutputByEDID(output, connectors)
   195  	if edidErr == nil {
   196  		return v2.DisplayPort(connector.String())
   197  	}
   198  
   199  	// attempt to match output ID to connector port
   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  	// find connector with port that matches the output ID
   234  	for _, connector := range connectors {
   235  		if connector.Port() == outputID.String() {
   236  			return &connector, nil
   237  		}
   238  	}
   239  
   240  	// try again with sanitised name
   241  	for _, connector := range connectors {
   242  		if outputMatchesSanitisedPort(outputID, connector) {
   243  			return &connector, nil
   244  		}
   245  	}
   246  
   247  	// final attempt for HDMI special port syntax
   248  	for _, connector := range connectors {
   249  		if outputMatchesSanitisedHDMIPort(outputID, connector) {
   250  			return &connector, nil
   251  		}
   252  	}
   253  
   254  	return nil, errUnableToMatchByName
   255  }
   256  
   257  // Compares output ID to connector port with "-" removed.
   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  // Compares HDMI output ID to connector port by removing syntax,
   264  // e.g. HDMI1 should match card0-HDMI-A-1.
   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  // Creates an MPID for the display based on its EDID.
   291  // Returns nil if MPID cannot be created.
   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