Skip to content

Commit

Permalink
Add feature to match display's native resolution
Browse files Browse the repository at this point in the history
Accepts new setup definitions like

DP-1 native_resolution=3840x2160
eDP-1 00ffffff...

To match any monitors attached as DP-1 with native resolution of 3840x2160
  • Loading branch information
Ackerley Tng committed Apr 22, 2023
1 parent 06b341a commit ac9ccfa
Showing 1 changed file with 137 additions and 4 deletions.
141 changes: 137 additions & 4 deletions autorandr.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,119 @@ def is_closed_lid(output):
return False


def _read_resolution_from_18_byte_data_block(edid, offset):
"""Read resolution from edid bytearray beginning at offset. Return a tuple
of (horizontal, vertical) pixels if a valid resolution is defined, else
return None"""

horz = edid[offset + 2] + ((edid[offset + 4] & 0xf0) << 4)
if horz == 0:
return 18, None

vert = edid[offset + 5] + ((edid[offset + 7] & 0xf0) << 4)

return 18, (horz, vert)


def _read_resolution_from_displayid_block(edid, offset):
payload_length = edid[offset + 2]

def do_read(expected_payload_length, resolution_offset):
if payload_length != expected_payload_length:
return None

horz = ((edid[offset + resolution_offset + 1] << 8) +
edid[offset + resolution_offset])
if horz == 0:
return None

vert = ((edid[offset + resolution_offset + 3] << 8) +
edid[offset + resolution_offset + 2])

return horz, vert

tag = edid[offset]
if tag == 0x01:
res = do_read(12, 7)
elif tag == 0x0c:
res = do_read(13, 5)
elif tag == 0x21:
res = do_read(29, 7)
else:
res = None

# Add header length of 3
return payload_length + 3, res


def _read_resolutions_from_block(edid, offset):
def do_read(start, end, read_block):
resolutions = []

index = start
while index < end:
length, resolution = read_block(edid, offset + index)
if resolution:
resolutions.append(resolution)

index += length

return resolutions

block_type = edid[offset]
if block_type == 0x00:
return do_read(0x36, 0x7e, _read_resolution_from_18_byte_data_block)

if block_type == 0x02:
version = edid[offset + 1]
if version < 1:
return []

detail = edid[offset + 2]
if detail < 4:
return []

return do_read(detail, 127, _read_resolution_from_18_byte_data_block)

if block_type == 0x70:
# DisplayID length has a maximum of 121
length = min(edid[offset + 2], 121)
base = offset + 5

return do_read(base, length, _read_resolution_from_displayid_block)

return []


def read_native_resolutions(edid):
"""Given an edid hex string, return a set of all the native
resolutions (horizontal, vertical) defined in the EDID"""

edid_page_size = 128

edid_bytearray = bytearray.fromhex(edid)

native_resolutions = set()
for offset in range(0, len(edid_bytearray), edid_page_size):
native_resolutions.update(
_read_resolutions_from_block(edid_bytearray, offset))

return native_resolutions


def _parse_edid_pattern(pattern):
"""Parse (horizontal, vertical) pixels from a pattern defined like
`native_resolution=1920x1080`"""

# Only supports patterns like "native_resolution=3840x2160" for now
if "native_resolution=" not in pattern:
return None

m = re.match(r"native_resolution=(\d+)x(\d+)", pattern)
if m:
return int(m.group(1)), int(m.group(2))


class AutorandrException(Exception):
def __init__(self, message, original_exception=None, report_bug=False):
self.message = message
Expand Down Expand Up @@ -361,6 +474,8 @@ def parse_serial_from_edid(self):
return
if "*" in self.edid:
return
if _parse_edid_pattern(self.edid):
return
# Thx to pyedid project, the following code was
# copied (and modified) from pyedid/__init__py:21 [parse_edid()]
raw = bytes.fromhex(self.edid)
Expand Down Expand Up @@ -551,6 +666,10 @@ def edid_equals(self, other):
return match_asterisk(self.edid, other.edid) > 0
elif "*" in other.edid:
return match_asterisk(other.edid, self.edid) > 0
if _parse_edid_pattern(self.edid):
return match_native_resolution(self.edid, other.edid)
elif _parse_edid_pattern(other.edid):
return match_native_resolution(other.edid, self.edid)
return self.edid == other.edid

def __ne__(self, other):
Expand Down Expand Up @@ -721,6 +840,14 @@ def match_asterisk(pattern, data):
return matched * 1. / total


def match_native_resolution(edid_pattern, edid_string):
horz_x_vert = _parse_edid_pattern(edid_pattern)
if not horz_x_vert:
return False

return horz_x_vert in read_native_resolutions(edid_string)


def update_profiles_edid(profiles, config):
fp_map = {}
for c in config:
Expand Down Expand Up @@ -766,8 +893,14 @@ def find_profiles(current_config, profiles):
if not matches or any((name not in config.keys() for name in current_config.keys() if current_config[name].fingerprint)):
continue
if matches:
closeness = max(match_asterisk(output.edid, current_config[name].edid), match_asterisk(
current_config[name].edid, output.edid))
config_edid = current_config[name].edid
parsed = _parse_edid_pattern(output.edid) or _parse_edid_pattern(config_edid)
if parsed:
closeness = int(match_native_resolution(config_edid, output.edid) or
match_native_resolution(output.edid, config_edid))
else:
closeness = max(match_asterisk(output.edid, config_edid),
match_asterisk(config_edid, output.edid))
detected_profiles.append((closeness, profile_name))
detected_profiles = [o[1] for o in sorted(detected_profiles, key=lambda x: -x[0])]
return detected_profiles
Expand Down Expand Up @@ -1094,9 +1227,9 @@ def generate_virtual_profile(configuration, modes, profile_name):
else:
shift_index = "height"
pos_specifier = "0x%s"

config_iter = reversed(configuration) if "reverse" in profile_name else iter(configuration)

for output in config_iter:
configuration[output].options = {}
if output in modes and configuration[output].edid:
Expand Down

0 comments on commit ac9ccfa

Please sign in to comment.