"""DBus backed display management for Mutter""" from collections import namedtuple import dbus from lutris.settings import DEFAULT_RESOLUTION_HEIGHT, DEFAULT_RESOLUTION_WIDTH from lutris.util.log import logger DisplayConfig = namedtuple("DisplayConfig", ("monitors", "name", "position", "transform", "primary", "scale")) class Output: """Representation of a physical display output""" def __init__(self, output_info): self._output = output_info def __repr__(self): return "" % (self.vendor, self.product, self.display_name) @property def output_id(self): """ID of the output""" return self._output[0] @property def winsys_id(self): """The low-level ID of this output (XID or KMS handle)""" return self._output[1] @property def current_crtc(self): """The CRTC that is currently driving this output, or -1 if the output is disabled """ return self._output[2] @property def crtcs(self): """All CRTCs that can control this output""" return self._output[3] @property def name(self): """The name of the connector to which the output is attached (like VGA1 or HDMI)""" return self._output[4] @property def modes(self): """Valid modes for this output""" return [int(mode_id) for mode_id in self._output[5]] @property def clones(self): """Valid clones for this output, ie other outputs that can be assigned the same CRTC as this one; if you want to mirror two outputs that don't have each other in the clone list, you must configure two different CRTCs for the same geometry. """ return self._output[6] @property def properties(self): """Other high-level properties that affect this output; they are not necessarily reflected in the hardware. """ return self._output[7] @property def vendor(self): """Vendor name of the output""" return str(self._output[7]["vendor"]) @property def product(self): """Product name of the output""" return str(self._output[7]["product"]) @property def display_name(self): """A human readable name of this output, to be shown in the UI""" return str(self._output[7]["display-name"]) @property def is_primary(self): """True if the output is the primary one""" return bool(self._output[7]["primary"]) class DisplayMode: """Representation of a screen mode (resolution, refresh rate)""" def __init__(self, mode_info): self.mode_info = mode_info def __str__(self): return "%sx%s@%s" % (self.width, self.height, self.frequency) def __repr__(self): return "" % (self.width, self.height, self.frequency) @property def id(self): # pylint: disable=invalid-name """ID of the mode""" return str(self.mode_info[0]) @property def winsys_id(self): """the low-level ID of this mode""" return str(self.mode_info[1]) @property def width(self): """width in physical pixels""" return int(self.mode_info[2]) @property def height(self): """height in physical pixels""" return int(self.mode_info[3]) @property def frequency(self): """refresh rate""" return str(self.mode_info[4]) @property def flags(self): """mode flags as defined in xf86drmMode.h and randr.h""" return self.mode_info[5] class CRTC: """A CRTC (CRT controller) is a logical monitor, ie a portion of the compositor coordinate space. It might correspond to multiple monitors, when in clone mode, but not that it is possible to implement clone mode also by setting different CRTCs to the same coordinates. """ def __init__(self, crtc_info): self.crtc_info = crtc_info def __repr__(self): return "%s %s %s" % (self.id, self.geometry_str, self.current_mode) @property def id(self): # pylint: disable=invalid-name """The ID in the API of this CRTC""" return str(self.crtc_info[0]) @property def winsys_id(self): """the low-level ID of this CRTC (which might be a XID, a KMS handle or something entirely different)""" return self.crtc_info[1] @property def geometry_str(self): """Return a human readable representation of the geometry""" return "%dx%d%s%d%s%d" % ( self.geometry[0], self.geometry[1], "" if self.geometry[2] < 0 else "+", self.geometry[2], "" if self.geometry[3] < 0 else "+", self.geometry[3], ) @property def geometry(self): """The geometry of this CRTC (might be invalid if the CRTC is not in use) """ return (int(self.crtc_info[2]), int(self.crtc_info[3]), int(self.crtc_info[4]), int(self.crtc_info[5])) @property def current_mode(self): """The current mode of the CRTC, or -1 if this CRTC is not used Note: the size of the mode will always correspond to the width and height of the CRTC""" return int(self.crtc_info[6]) @property def current_transform(self): """The current transform (espressed according to the wayland protocol)""" return str(self.crtc_info[7]) @property def transforms(self): """All possible transforms""" return str(self.crtc_info[8]) @property def properties(self): """Other high-level properties that affect this CRTC; they are not necessarily reflected in the hardware. No property is specified in this version of the API. """ return str(self.crtc_info[9]) class MonitorMode(DisplayMode): """Represents a mode given by a Monitor instance In addition to DisplayMode objects, this gives acces to the current scaling used and some additional properties like is_current. """ @property def width(self): """width in physical pixels""" return int(self.mode_info[1]) @property def height(self): """height in physical pixels""" return int(self.mode_info[2]) @property def frequency(self): """refresh rate""" return str(self.mode_info[3]) @property def scale(self): """scale preferred as per calculations""" return float(self.mode_info[4]) @property def supported_scale(self): """scales supported by this mode""" return self.mode_info[5] @property def properties(self): """Additional properties""" return self.mode_info[6] @property def is_current(self): """Return True if the mode is the current one""" return "is-current" in self.properties class Monitor: """A physical monitor""" def __init__(self, monitor): self._monitor = monitor def get_current_mode(self): """Return the current mode""" for mode in self.get_modes(): if mode.is_current: return mode return def get_modes(self): """Return available modes""" return [MonitorMode(mode) for mode in self._monitor[1]] def get_mode_for_resolution(self, resolution): """Return an appropriate mode for a given resolution""" width, height = [int(i) for i in resolution.split("x")] for mode in self.get_modes(): if mode.width == width and mode.height == height: return mode return @property def name(self): """Name of the connector""" return str(self._monitor[0][0]) @property def vendor(self): """Manufacturer of the monitor""" return str(self._monitor[0][1]) @property def model(self): """Model name of the monitor""" return str(self._monitor[0][2]) @property def serial_number(self): """Serial number""" return str(self._monitor[0][3]) @property def is_underscanning(self): """Return true if the monitor is underscanning""" return bool(self._monitor[2]["is-underscanning"]) @property def is_builtin(self): """Return true if the display is builtin the machine (a laptop or a tablet)""" return bool(self._monitor[2]["is-builtin"]) @property def display_name(self): """Human readable name of the display""" return str(self._monitor[2]["display-name"]) class LogicalMonitor: """A logical monitor. Similar to CRTCs but logical monitors also contain scaling information. """ def __init__(self, lm_info, monitors): self._lm = lm_info self._monitors = monitors @property def position(self): """Return the position of the monitor""" return int(self._lm[0]), int(self._lm[1]) @property def scale(self): """Scale""" return self._lm[2] @property def transform(self): """Transforms Possible transform values: 0: normal 1: 90° 2: 180° 3: 270° 4: flipped 5: 90° flipped 6: 180° flipped 7: 270° flipped """ return self._lm[3] @property def primary(self): """True if this is the primary logical monitor""" return bool(self._lm[4]) def _get_monitor_for_connector(self, connector): """Return a Monitor instance from its connector name""" for monitor in self._monitors: if monitor.name == str(connector): return monitor return @property def monitors(self): """Monitors displaying that logical monitor""" return [self._get_monitor_for_connector(m[0]) for m in self._lm[5]] @property def properties(self): """Possibly other properties""" return self._lm[6] def get_config(self): """Export the current configuration so it can be stored then reapplied later""" monitors = [(monitor.name, monitor.get_current_mode().id) for monitor in self.monitors] return DisplayConfig(monitors, self.monitors[0].name, self.position, self.transform, self.primary, self.scale) class DisplayState: """Snapshot of a display configuration at a given time""" def __init__(self, interface): self.interface = interface self._state = self.load_state() def load_state(self): """Return current state from dbus interface""" return self.interface.GetCurrentState() @property def serial(self): """Configuration serial""" return self._state[0] @property def monitors(self): """Available monitors""" return [Monitor(monitor) for monitor in self._state[1]] @property def logical_monitors(self): """Current logical monitor configuration""" return [LogicalMonitor(l_m, self.monitors) for l_m in self._state[2]] @property def properties(self): """Display configuration properties""" return self._state[3] def get_primary_logical_monitor(self): """Return the primary logical monitor""" for lm in self.logical_monitors: if lm.primary: return lm return None def get_primary_monitor(self): """Returns the first physical monitor on the primary logical monitor.""" lm = self.get_primary_logical_monitor() if lm: return lm.monitors[0] return self.monitors[0] def get_current_mode(self): """Return the current mode""" return self.get_primary_monitor().get_current_mode() class MutterDisplayConfig: """Class to interact with the Mutter.DisplayConfig service""" namespace = "org.gnome.Mutter.DisplayConfig" dbus_path = "/org/gnome/Mutter/DisplayConfig" # Methods used in ApplyMonitorConfig VERIFY_METHOD = 0 TEMPORARY_METHOD = 1 PERSISTENT_METHOD = 2 def __init__(self): session_bus = dbus.SessionBus() proxy_obj = session_bus.get_object(self.namespace, self.dbus_path) self.interface = dbus.Interface(proxy_obj, dbus_interface=self.namespace) self.resources = self.interface.GetResources() self.current_state = DisplayState(self.interface) @property def serial(self): """ @serial is an unique identifier representing the current state of the screen. It must be passed back to ApplyConfiguration() and will be increased for every configuration change (so that mutter can detect that the new configuration is based on old state) """ return self.resources[0] @property def crtcs(self): """ A CRTC (CRT controller) is a logical monitor, ie a portion of the compositor coordinate space. It might correspond to multiple monitors, when in clone mode, but not that it is possible to implement clone mode also by setting different CRTCs to the same coordinates. The number of CRTCs represent the maximum number of monitors that can be set to expand and it is a HW constraint; if more monitors are connected, then necessarily some will clone. This is complementary to the concept of the encoder (not exposed in the API), which groups outputs that necessarily will show the same image (again a HW constraint). A CRTC is represented by a DBus structure with the following layout: * u ID: the ID in the API of this CRTC * x winsys_id: the low-level ID of this CRTC (which might be a XID, a KMS handle or something entirely different) * i x, y, width, height: the geometry of this CRTC (might be invalid if the CRTC is not in use) * i current_mode: the current mode of the CRTC, or -1 if this CRTC is not used Note: the size of the mode will always correspond to the width and height of the CRTC * u current_transform: the current transform (espressed according to the wayland protocol) * au transforms: all possible transforms * a{sv} properties: other high-level properties that affect this CRTC; they are not necessarily reflected in the hardware. No property is specified in this version of the API. Note: all geometry information refers to the untransformed display. """ return [CRTC(crtc) for crtc in self.resources[1]] @property def outputs(self): """ An output represents a physical screen, connected somewhere to the computer. Floating connectors are not exposed in the API. An output is a DBus struct with the following fields: * u ID: the ID in the API * x winsys_id: the low-level ID of this output (XID or KMS handle) * i current_crtc: the CRTC that is currently driving this output, or -1 if the output is disabled * au possible_crtcs: all CRTCs that can control this output * s name: the name of the connector to which the output is attached (like VGA1 or HDMI) * au modes: valid modes for this output * au clones: valid clones for this output, ie other outputs that can be assigned the same CRTC as this one; if you want to mirror two outputs that don't have each other in the clone list, you must configure two different CRTCs for the same geometry * a{sv} properties: other high-level properties that affect this output; they are not necessarily reflected in the hardware. Known properties: - "vendor" (s): (readonly) the human readable name of the manufacturer - "product" (s): (readonly) the human readable name of the display model - "serial" (s): (readonly) the serial number of this particular hardware part - "display-name" (s): (readonly) a human readable name of this output, to be shown in the UI - "backlight" (i): (readonly, use the specific interface) the backlight value as a percentage (-1 if not supported) - "primary" (b): whether this output is primary or not - "presentation" (b): whether this output is for presentation only Note: properties might be ignored if not consistenly applied to all outputs in the same clone group. In general, it's expected that presentation or primary outputs will not be cloned. """ return [Output(output) for output in self.resources[2]] @property def modes(self): """ A mode represents a set of parameters that are applied to each output, such as resolution and refresh rate. It is a separate object so that it can be referenced by CRTCs and outputs. Multiple outputs in the same CRTCs must all have the same mode. A mode is exposed as: * u ID: the ID in the API * x winsys_id: the low-level ID of this mode * u width, height: the resolution * d frequency: refresh rate * u flags: mode flags as defined in xf86drmMode.h and randr.h Output and modes are read-only objects (except for output properties), they can change only in accordance to HW changes (such as hotplugging a monitor), while CRTCs can be changed with ApplyConfiguration(). XXX: actually, if you insist enough, you can add new modes through xrandr command line or the KMS API, overriding what the kernel driver and the EDID say. Usually, it only matters with old cards with broken drivers, or old monitors with broken EDIDs, but it happens more often with projectors (if for example the kernel driver doesn't add the 640x480 - 800x600 - 1024x768 default modes). Probably something that we need to handle in mutter anyway. """ return [DisplayMode(mode) for mode in self.resources[3]] @property def max_screen_width(self): """Maximum width supported""" return self.resources[4] @property def max_screen_height(self): """Maximum height supported""" return self.resources[5] def get_mode_for_resolution(self, resolution): """Return an appropriate mode for a given resolution""" width, height = [int(i) for i in resolution.split("x")] for mode in self.modes: if mode.width == width and mode.height == height: return mode return def get_primary_logical_monitor(self): """Return the primary logical monitor""" return self.current_state.get_primary_logical_monitor() def get_primary_monitor(self): """Returns the first physical monitor on the primary logical monitor.""" return self.current_state.get_primary_monitor() def apply_monitors_config(self, display_configs): """Set the selected display to the desired resolution""" # Reload resources if not display_configs: logger.error("No display config given, not applying anything") return self.resources = self.interface.GetResources() self.current_state = DisplayState(self.interface) monitors_config = [ [ config.position[0], config.position[1], dbus.Double(config.scale), dbus.UInt32(config.transform), config.primary, [ [dbus.String(str(display_name)), dbus.String(str(mode)), {}] for display_name, mode in config.monitors ], ] for config in display_configs ] self.interface.ApplyMonitorsConfig(self.current_state.serial, self.TEMPORARY_METHOD, monitors_config, {}) class MutterDisplayManager: """Manage displays using the DBus Mutter interface""" def __init__(self): self.display_config = MutterDisplayConfig() def get_config(self): """Return the current configuration for each logical monitor""" return [logical_monitor.get_config() for logical_monitor in self.display_config.current_state.logical_monitors] def get_display_names(self): """Return display names of connected displays""" return [output.display_name for output in self.display_config.outputs] def get_resolutions(self): """Return available resolutions""" resolutions = ["%sx%s" % (mode.width, mode.height) for mode in self.display_config.modes] if not resolutions: logger.error("Could not generate resolution list") return ["%sx%s" % (DEFAULT_RESOLUTION_WIDTH, DEFAULT_RESOLUTION_HEIGHT)] return sorted(set(resolutions), key=lambda x: int(x.split("x")[0]), reverse=True) def get_current_resolution(self): """Return the current resolution for the primary display""" logger.debug("Retrieving current resolution") current_mode = self.display_config.current_state.get_current_mode() if not current_mode: logger.error("Could not retrieve the current display mode") return str(DEFAULT_RESOLUTION_WIDTH), str(DEFAULT_RESOLUTION_HEIGHT) return str(current_mode.width), str(current_mode.height) def set_resolution(self, resolution): """Change the current resolution""" if isinstance(resolution, str): monitor = self.display_config.get_primary_monitor() mode = monitor.get_mode_for_resolution(resolution) if not mode: logger.error("Could not find valid mode for %s", resolution) return config = [DisplayConfig([(monitor.name, mode.id)], monitor.name, (0, 0), 0, True, 1.0)] self.display_config.apply_monitors_config(config) elif resolution: self.display_config.apply_monitors_config(resolution) else: return # Load a fresh config since the current one has changed self.display_config = MutterDisplayConfig()