1# Copyright (c) 2013 The Chromium OS Authors. All rights reserved. 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5""" 6Provides graphics related utils, like capturing screenshots or checking on 7the state of the graphics driver. 8""" 9 10import collections 11import contextlib 12import fcntl 13import glob 14import logging 15import os 16import re 17import struct 18import sys 19import time 20 21from autotest_lib.client.bin import test 22from autotest_lib.client.bin import utils 23from autotest_lib.client.common_lib import error 24from autotest_lib.client.common_lib import test as test_utils 25from autotest_lib.client.cros.input_playback import input_playback 26from autotest_lib.client.cros.power import power_utils 27from functools import wraps 28 29# The uinput module might not be available at SDK test time. 30try: 31 from autotest_lib.client.cros.graphics import graphics_uinput 32except ImportError: 33 graphics_uinput = None 34 35 36class GraphicsTest(test.test): 37 """Base class for graphics test. 38 39 GraphicsTest is the base class for graphics tests. 40 Every subclass of GraphicsTest should call GraphicsTests initialize/cleanup 41 method as they will do GraphicsStateChecker as well as report states to 42 Chrome Perf dashboard. 43 44 Attributes: 45 _test_failure_description(str): Failure name reported to chrome perf 46 dashboard. (Default: Failures) 47 _test_failure_report_enable(bool): Enable/Disable reporting 48 failures to chrome perf dashboard 49 automatically. (Default: True) 50 _test_failure_report_subtest(bool): Enable/Disable reporting 51 subtests failure to chrome perf 52 dashboard automatically. 53 (Default: False) 54 """ 55 version = 1 56 _GSC = None 57 58 _test_failure_description = "Failures" 59 _test_failure_report_enable = True 60 _test_failure_report_subtest = False 61 62 def __init__(self, *args, **kwargs): 63 """Initialize flag setting.""" 64 super(GraphicsTest, self).__init__(*args, **kwargs) 65 self._failures_by_description = {} 66 self._player = None 67 68 def initialize(self, raise_error_on_hang=False, *args, **kwargs): 69 """Initial state checker and report initial value to perf dashboard.""" 70 self._GSC = GraphicsStateChecker( 71 raise_error_on_hang=raise_error_on_hang, 72 run_on_sw_rasterizer=utils.is_virtual_machine()) 73 74 self.output_perf_value( 75 description='Timeout_Reboot', 76 value=1, 77 units='count', 78 higher_is_better=False, 79 replace_existing_values=True 80 ) 81 82 if hasattr(super(GraphicsTest, self), "initialize"): 83 test_utils._cherry_pick_call(super(GraphicsTest, self).initialize, 84 *args, **kwargs) 85 86 def input_check(self): 87 """Check if it exists and initialize input player.""" 88 if self._player is None: 89 self._player = input_playback.InputPlayback() 90 self._player.emulate(input_type='keyboard') 91 self._player.find_connected_inputs() 92 93 def cleanup(self, *args, **kwargs): 94 """Finalize state checker and report values to perf dashboard.""" 95 if self._GSC: 96 self._GSC.finalize() 97 98 self._output_perf() 99 if self._player: 100 self._player.close() 101 102 if hasattr(super(GraphicsTest, self), "cleanup"): 103 test_utils._cherry_pick_call(super(GraphicsTest, self).cleanup, 104 *args, **kwargs) 105 106 @contextlib.contextmanager 107 def failure_report(self, name, subtest=None): 108 """Record the failure of an operation to self._failures_by_description. 109 110 Records if the operation taken inside executed normally or not. 111 If the operation taken inside raise unexpected failure, failure named 112 |name|, will be added to the self._failures_by_description dictionary 113 and reported to the chrome perf dashboard in the cleanup stage. 114 115 Usage: 116 # Record failure of doSomething 117 with failure_report('doSomething'): 118 doSomething() 119 """ 120 # Assume failed at the beginning 121 self.add_failures(name, subtest=subtest) 122 try: 123 yield {} 124 self.remove_failures(name, subtest=subtest) 125 except (error.TestWarn, error.TestNAError) as e: 126 self.remove_failures(name, subtest=subtest) 127 raise e 128 129 @classmethod 130 def failure_report_decorator(cls, name, subtest=None): 131 """Record the failure if the function failed to finish. 132 This method should only decorate to functions of GraphicsTest. 133 In addition, functions with this decorator should be called with no 134 unnamed arguments. 135 Usage: 136 @GraphicsTest.test_run_decorator('graphics_test') 137 def Foo(self, bar='test'): 138 return doStuff() 139 140 is equivalent to 141 142 def Foo(self, bar): 143 with failure_reporter('graphics_test'): 144 return doStuff() 145 146 # Incorrect usage. 147 @GraphicsTest.test_run_decorator('graphics_test') 148 def Foo(self, bar='test'): 149 pass 150 self.Foo('test_name', bar='test_name') # call Foo with named args 151 152 # Incorrect usage. 153 @GraphicsTest.test_run_decorator('graphics_test') 154 def Foo(self, bar='test'): 155 pass 156 self.Foo('test_name') # call Foo with unnamed args 157 """ 158 def decorator(fn): 159 @wraps(fn) 160 def wrapper(*args, **kwargs): 161 if len(args) > 1: 162 raise error.TestError('Unnamed arguments is not accepted. ' 163 'Please apply this decorator to ' 164 'function without unnamed args.') 165 # A member function of GraphicsTest is decorated. The first 166 # argument is the instance itself. 167 instance = args[0] 168 with instance.failure_report(name, subtest): 169 # Cherry pick the arguments for the wrapped function. 170 d_args, d_kwargs = test_utils._cherry_pick_args(fn, args, 171 kwargs) 172 return fn(instance, *d_args, **d_kwargs) 173 return wrapper 174 return decorator 175 176 def add_failures(self, name, subtest=None): 177 """ 178 Add a record to failures list which will report back to chrome perf 179 dashboard at cleanup stage. 180 Args: 181 name: failure name. 182 subtest: subtest which will appears in cros-perf. If None is 183 specified, use name instead. 184 """ 185 target = self._get_failure(name, subtest=subtest) 186 if target: 187 target['names'].append(name) 188 else: 189 target = { 190 'description': self._get_failure_description(name, subtest), 191 'unit': 'count', 192 'higher_is_better': False, 193 'graph': self._get_failure_graph_name(), 194 'names': [name], 195 } 196 self._failures_by_description[target['description']] = target 197 return target 198 199 def remove_failures(self, name, subtest=None): 200 """ 201 Remove a record from failures list which will report back to chrome perf 202 dashboard at cleanup stage. 203 Args: 204 name: failure name. 205 subtest: subtest which will appears in cros-perf. If None is 206 specified, use name instead. 207 """ 208 target = self._get_failure(name, subtest=subtest) 209 if name in target['names']: 210 target['names'].remove(name) 211 212 213 def _output_perf(self): 214 """Report recorded failures back to chrome perf.""" 215 self.output_perf_value( 216 description='Timeout_Reboot', 217 value=0, 218 units='count', 219 higher_is_better=False, 220 replace_existing_values=True 221 ) 222 223 if not self._test_failure_report_enable: 224 return 225 226 total_failures = 0 227 # Report subtests failures 228 for failure in self._failures_by_description.values(): 229 if len(failure['names']) > 0: 230 logging.debug('GraphicsTest failure: %s' % failure['names']) 231 total_failures += len(failure['names']) 232 233 if not self._test_failure_report_subtest: 234 continue 235 236 self.output_perf_value( 237 description=failure['description'], 238 value=len(failure['names']), 239 units=failure['unit'], 240 higher_is_better=failure['higher_is_better'], 241 graph=failure['graph'] 242 ) 243 244 # Report the count of all failures 245 self.output_perf_value( 246 description=self._get_failure_graph_name(), 247 value=total_failures, 248 units='count', 249 higher_is_better=False, 250 ) 251 252 def _get_failure_graph_name(self): 253 return self._test_failure_description 254 255 def _get_failure_description(self, name, subtest): 256 return subtest or name 257 258 def _get_failure(self, name, subtest): 259 """Get specific failures.""" 260 description = self._get_failure_description(name, subtest=subtest) 261 return self._failures_by_description.get(description, None) 262 263 def get_failures(self): 264 """ 265 Get currently recorded failures list. 266 """ 267 return [name for failure in self._failures_by_description.values() 268 for name in failure['names']] 269 270 def open_vt1(self): 271 """Switch to VT1 with keyboard.""" 272 self.input_check() 273 self._player.blocking_playback_of_default_file( 274 input_type='keyboard', filename='keyboard_ctrl+alt+f1') 275 time.sleep(5) 276 277 def open_vt2(self): 278 """Switch to VT2 with keyboard.""" 279 self.input_check() 280 self._player.blocking_playback_of_default_file( 281 input_type='keyboard', filename='keyboard_ctrl+alt+f2') 282 time.sleep(5) 283 284 def wake_screen_with_keyboard(self): 285 """Use the vt1 keyboard shortcut to bring the devices screen back on. 286 287 This is useful if you want to take screenshots of the UI. If you try 288 to take them while the screen is off, it will fail. 289 """ 290 self.open_vt1() 291 292 293def screen_disable_blanking(): 294 """ Called from power_Backlight to disable screen blanking. """ 295 # We don't have to worry about unexpected screensavers or DPMS here. 296 return 297 298 299def screen_disable_energy_saving(): 300 """ Called from power_Consumption to immediately disable energy saving. """ 301 # All we need to do here is enable displays via Chrome. 302 power_utils.set_display_power(power_utils.DISPLAY_POWER_ALL_ON) 303 return 304 305 306def screen_toggle_fullscreen(): 307 """Toggles fullscreen mode.""" 308 press_keys(['KEY_F11']) 309 310 311def screen_toggle_mirrored(): 312 """Toggles the mirrored screen.""" 313 press_keys(['KEY_LEFTCTRL', 'KEY_F4']) 314 315 316def hide_cursor(): 317 """Hides mouse cursor.""" 318 # Send a keystroke to hide the cursor. 319 press_keys(['KEY_UP']) 320 321 322def hide_typing_cursor(): 323 """Hides typing cursor.""" 324 # Press the tab key to move outside the typing bar. 325 press_keys(['KEY_TAB']) 326 327 328def screen_wakeup(): 329 """Wake up the screen if it is dark.""" 330 # Move the mouse a little bit to wake up the screen. 331 device = graphics_uinput.get_device_mouse_rel() 332 graphics_uinput.emit(device, 'REL_X', 1) 333 graphics_uinput.emit(device, 'REL_X', -1) 334 335 336def switch_screen_on(on): 337 """ 338 Turn the touch screen on/off. 339 340 @param on: On or off. 341 """ 342 raise error.TestFail('switch_screen_on is not implemented.') 343 344 345def press_keys(key_list): 346 """Presses the given keys as one combination. 347 348 Please do not leak uinput dependencies outside of the file. 349 350 @param key: A list of key strings, e.g. ['LEFTCTRL', 'F4'] 351 """ 352 graphics_uinput.emit_combo(graphics_uinput.get_device_keyboard(), key_list) 353 354 355def click_mouse(): 356 """Just click the mouse. 357 Presumably only hacky tests use this function. 358 """ 359 logging.info('click_mouse()') 360 # Move a little to make the cursor appear. 361 device = graphics_uinput.get_device_mouse_rel() 362 graphics_uinput.emit(device, 'REL_X', 1) 363 # Some sleeping is needed otherwise events disappear. 364 time.sleep(0.1) 365 # Move cursor back to not drift. 366 graphics_uinput.emit(device, 'REL_X', -1) 367 time.sleep(0.1) 368 # Click down. 369 graphics_uinput.emit(device, 'BTN_LEFT', 1) 370 time.sleep(0.2) 371 # Release click. 372 graphics_uinput.emit(device, 'BTN_LEFT', 0) 373 374 375# TODO(ihf): this function is broken. Make it work. 376def activate_focus_at(rel_x, rel_y): 377 """Clicks with the mouse at screen position (x, y). 378 379 This is a pretty hacky method. Using this will probably lead to 380 flaky tests as page layout changes over time. 381 @param rel_x: relative horizontal position between 0 and 1. 382 @param rel_y: relattive vertical position between 0 and 1. 383 """ 384 width, height = get_internal_resolution() 385 device = graphics_uinput.get_device_touch() 386 graphics_uinput.emit(device, 'ABS_MT_SLOT', 0, syn=False) 387 graphics_uinput.emit(device, 'ABS_MT_TRACKING_ID', 1, syn=False) 388 graphics_uinput.emit(device, 'ABS_MT_POSITION_X', int(rel_x * width), 389 syn=False) 390 graphics_uinput.emit(device, 'ABS_MT_POSITION_Y', int(rel_y * height), 391 syn=False) 392 graphics_uinput.emit(device, 'BTN_TOUCH', 1, syn=True) 393 time.sleep(0.2) 394 graphics_uinput.emit(device, 'BTN_TOUCH', 0, syn=True) 395 396 397def take_screenshot(resultsdir, fname_prefix): 398 """Take screenshot and save to a new file in the results dir. 399 Args: 400 @param resultsdir: Directory to store the output in. 401 @param fname_prefix: Prefix for the output fname. 402 Returns: 403 the path of the saved screenshot file 404 """ 405 406 old_exc_type = sys.exc_info()[0] 407 408 next_index = len(glob.glob( 409 os.path.join(resultsdir, '%s-*.png' % fname_prefix))) 410 screenshot_file = os.path.join( 411 resultsdir, '%s-%d.png' % (fname_prefix, next_index)) 412 logging.info('Saving screenshot to %s.', screenshot_file) 413 414 try: 415 utils.run('screenshot "%s"' % screenshot_file) 416 except Exception as err: 417 # Do not raise an exception if the screenshot fails while processing 418 # another exception. 419 if old_exc_type is None: 420 raise 421 logging.error(err) 422 423 return screenshot_file 424 425 426def take_screenshot_crop(fullpath, box=None, crtc_id=None): 427 """ 428 Take a screenshot using import tool, crop according to dim given by the box. 429 @param fullpath: path, full path to save the image to. 430 @param box: 4-tuple giving the upper left and lower right pixel coordinates. 431 @param crtc_id: if set, take a screen shot of the specified CRTC. 432 """ 433 cmd = 'screenshot' 434 if crtc_id is not None: 435 cmd += ' --crtc-id=%d' % crtc_id 436 else: 437 cmd += ' --internal' 438 if box: 439 x, y, r, b = box 440 w = r - x 441 h = b - y 442 cmd += ' --crop=%dx%d+%d+%d' % (w, h, x, y) 443 cmd += ' "%s"' % fullpath 444 utils.run(cmd) 445 return fullpath 446 447 448# id encoder status name size (mm) modes encoders 449# 39 0 connected eDP-1 256x144 1 38 450_MODETEST_CONNECTOR_PATTERN = re.compile( 451 r'^(\d+)\s+(\d+)\s+(connected|disconnected)\s+(\S+)\s+\d+x\d+\s+\d+\s+\d+') 452 453# id crtc type possible crtcs possible clones 454# 38 0 TMDS 0x00000002 0x00000000 455_MODETEST_ENCODER_PATTERN = re.compile( 456 r'^(\d+)\s+(\d+)\s+\S+\s+0x[0-9a-fA-F]+\s+0x[0-9a-fA-F]+') 457 458# Group names match the drmModeModeInfo struct 459_MODETEST_MODE_PATTERN = re.compile( 460 r'\s+(?P<name>.+)' 461 r'\s+(?P<vrefresh>\d+)' 462 r'\s+(?P<hdisplay>\d+)' 463 r'\s+(?P<hsync_start>\d+)' 464 r'\s+(?P<hsync_end>\d+)' 465 r'\s+(?P<htotal>\d+)' 466 r'\s+(?P<vdisplay>\d+)' 467 r'\s+(?P<vsync_start>\d+)' 468 r'\s+(?P<vsync_end>\d+)' 469 r'\s+(?P<vtotal>\d+)' 470 r'\s+(?P<clock>\d+)' 471 r'\s+flags:.+type:' 472 r' preferred') 473 474_MODETEST_CRTCS_START_PATTERN = re.compile(r'^id\s+fb\s+pos\s+size') 475 476_MODETEST_CRTC_PATTERN = re.compile( 477 r'^(\d+)\s+(\d+)\s+\((\d+),(\d+)\)\s+\((\d+)x(\d+)\)') 478 479_MODETEST_PLANES_START_PATTERN = re.compile( 480 r'^id\s+crtc\s+fb\s+CRTC\s+x,y\s+x,y\s+gamma\s+size\s+possible\s+crtcs') 481 482_MODETEST_PLANE_PATTERN = re.compile( 483 r'^(\d+)\s+(\d+)\s+(\d+)\s+(\d+),(\d+)\s+(\d+),(\d+)\s+(\d+)\s+(0x)(\d+)') 484 485Connector = collections.namedtuple( 486 'Connector', [ 487 'cid', # connector id (integer) 488 'eid', # encoder id (integer) 489 'ctype', # connector type, e.g. 'eDP', 'HDMI-A', 'DP' 490 'connected', # boolean 491 'size', # current screen size in mm, e.g. (256, 144) 492 'encoder', # encoder id (integer) 493 # list of resolution tuples, e.g. [(1920,1080), (1600,900), ...] 494 'modes', 495 ]) 496 497Encoder = collections.namedtuple( 498 'Encoder', [ 499 'eid', # encoder id (integer) 500 'crtc_id', # CRTC id (integer) 501 ]) 502 503CRTC = collections.namedtuple( 504 'CRTC', [ 505 'id', # crtc id 506 'fb', # fb id 507 'pos', # position, e.g. (0,0) 508 'size', # size, e.g. (1366,768) 509 'is_internal', # True if for the internal display 510 ]) 511 512Plane = collections.namedtuple( 513 'Plane', [ 514 'id', # plane id 515 'possible_crtcs', # possible associated CRTC indexes. 516 ]) 517 518def get_display_resolution(): 519 """ 520 Parses output of modetest to determine the display resolution of the dut. 521 @return: tuple, (w,h) resolution of device under test. 522 """ 523 connectors = get_modetest_connectors() 524 for connector in connectors: 525 if connector.connected: 526 return connector.size 527 return None 528 529 530def _get_num_outputs_connected(): 531 """ 532 Parses output of modetest to determine the number of connected displays 533 @return: The number of connected displays 534 """ 535 connected = 0 536 connectors = get_modetest_connectors() 537 for connector in connectors: 538 if connector.connected: 539 connected = connected + 1 540 541 return connected 542 543 544def get_num_outputs_on(): 545 """ 546 Retrieves the number of connected outputs that are on. 547 548 Return value: integer value of number of connected outputs that are on. 549 """ 550 551 return _get_num_outputs_connected() 552 553 554def get_modetest_connectors(): 555 """ 556 Retrieves a list of Connectors using modetest. 557 558 Return value: List of Connectors. 559 """ 560 connectors = [] 561 modetest_output = utils.system_output('modetest -c') 562 for line in modetest_output.splitlines(): 563 # First search for a new connector. 564 connector_match = re.match(_MODETEST_CONNECTOR_PATTERN, line) 565 if connector_match is not None: 566 cid = int(connector_match.group(1)) 567 eid = int(connector_match.group(2)) 568 connected = False 569 if connector_match.group(3) == 'connected': 570 connected = True 571 ctype = connector_match.group(4) 572 size = (-1, -1) 573 encoder = -1 574 modes = None 575 connectors.append( 576 Connector(cid, eid, ctype, connected, size, encoder, modes)) 577 else: 578 # See if we find corresponding line with modes, sizes etc. 579 mode_match = re.match(_MODETEST_MODE_PATTERN, line) 580 if mode_match is not None: 581 size = (int(mode_match.group('hdisplay')), 582 int(mode_match.group('vdisplay'))) 583 # Update display size of last connector in list. 584 c = connectors.pop() 585 connectors.append( 586 Connector( 587 c.cid, c.eid, c.ctype, c.connected, size, c.encoder, 588 c.modes)) 589 return connectors 590 591 592def get_modetest_encoders(): 593 """ 594 Retrieves a list of Encoders using modetest. 595 596 Return value: List of Encoders. 597 """ 598 encoders = [] 599 modetest_output = utils.system_output('modetest -e') 600 for line in modetest_output.splitlines(): 601 encoder_match = re.match(_MODETEST_ENCODER_PATTERN, line) 602 if encoder_match is None: 603 continue 604 605 eid = int(encoder_match.group(1)) 606 crtc_id = int(encoder_match.group(2)) 607 encoders.append(Encoder(eid, crtc_id)) 608 return encoders 609 610 611def find_eid_from_crtc_id(crtc_id): 612 """ 613 Finds the integer Encoder ID matching a CRTC ID. 614 615 @param crtc_id: The integer CRTC ID. 616 617 @return: The integer Encoder ID or None. 618 """ 619 encoders = get_modetest_encoders() 620 for encoder in encoders: 621 if encoder.crtc_id == crtc_id: 622 return encoder.eid 623 return None 624 625 626def find_connector_from_eid(eid): 627 """ 628 Finds the Connector object matching an Encoder ID. 629 630 @param eid: The integer Encoder ID. 631 632 @return: The Connector object or None. 633 """ 634 connectors = get_modetest_connectors() 635 for connector in connectors: 636 if connector.eid == eid: 637 return connector 638 return None 639 640 641def get_modetest_crtcs(): 642 """ 643 Returns a list of CRTC data. 644 645 Sample: 646 [CRTC(id=19, fb=50, pos=(0, 0), size=(1366, 768)), 647 CRTC(id=22, fb=54, pos=(0, 0), size=(1920, 1080))] 648 """ 649 crtcs = [] 650 modetest_output = utils.system_output('modetest -p') 651 found = False 652 for line in modetest_output.splitlines(): 653 if found: 654 crtc_match = re.match(_MODETEST_CRTC_PATTERN, line) 655 if crtc_match is not None: 656 crtc_id = int(crtc_match.group(1)) 657 fb = int(crtc_match.group(2)) 658 x = int(crtc_match.group(3)) 659 y = int(crtc_match.group(4)) 660 width = int(crtc_match.group(5)) 661 height = int(crtc_match.group(6)) 662 # CRTCs with fb=0 are disabled, but lets skip anything with 663 # trivial width/height just in case. 664 if not (fb == 0 or width == 0 or height == 0): 665 eid = find_eid_from_crtc_id(crtc_id) 666 connector = find_connector_from_eid(eid) 667 if connector is None: 668 is_internal = False 669 else: 670 is_internal = (connector.ctype == 671 get_internal_connector_name()) 672 crtcs.append(CRTC(crtc_id, fb, (x, y), (width, height), 673 is_internal)) 674 elif line and not line[0].isspace(): 675 return crtcs 676 if re.match(_MODETEST_CRTCS_START_PATTERN, line) is not None: 677 found = True 678 return crtcs 679 680 681def get_modetest_planes(): 682 """ 683 Returns a list of planes information. 684 685 Sample: 686 [Plane(id=26, possible_crtcs=1), 687 Plane(id=29, possible_crtcs=1)] 688 """ 689 planes = [] 690 modetest_output = utils.system_output('modetest -p') 691 found = False 692 for line in modetest_output.splitlines(): 693 if found: 694 plane_match = re.match(_MODETEST_PLANE_PATTERN, line) 695 if plane_match is not None: 696 plane_id = int(plane_match.group(1)) 697 possible_crtcs = int(plane_match.group(10)) 698 if not (plane_id == 0 or possible_crtcs == 0): 699 planes.append(Plane(plane_id, possible_crtcs)) 700 elif line and not line[0].isspace(): 701 return planes 702 if re.match(_MODETEST_PLANES_START_PATTERN, line) is not None: 703 found = True 704 return planes 705 706 707def get_modetest_output_state(): 708 """ 709 Reduce the output of get_modetest_connectors to a dictionary of connector/active states. 710 """ 711 connectors = get_modetest_connectors() 712 outputs = {} 713 for connector in connectors: 714 # TODO(ihf): Figure out why modetest output needs filtering. 715 if connector.connected: 716 outputs[connector.ctype] = connector.connected 717 return outputs 718 719 720def get_output_rect(output): 721 """Gets the size and position of the given output on the screen buffer. 722 723 @param output: The output name as a string. 724 725 @return A tuple of the rectangle (width, height, fb_offset_x, 726 fb_offset_y) of ints. 727 """ 728 connectors = get_modetest_connectors() 729 for connector in connectors: 730 if connector.ctype == output: 731 # Concatenate two 2-tuples to 4-tuple. 732 return connector.size + (0, 0) # TODO(ihf): Should we use CRTC.pos? 733 return (0, 0, 0, 0) 734 735 736def get_internal_crtc(): 737 for crtc in get_modetest_crtcs(): 738 if crtc.is_internal: 739 return crtc 740 return None 741 742 743def get_external_crtc(index=0): 744 for crtc in get_modetest_crtcs(): 745 if not crtc.is_internal: 746 if index == 0: 747 return crtc 748 index -= 1 749 return None 750 751 752def get_internal_resolution(): 753 crtc = get_internal_crtc() 754 if crtc: 755 return crtc.size 756 return (-1, -1) 757 758 759def has_internal_display(): 760 """Checks whether the DUT is equipped with an internal display. 761 762 @return True if internal display is present; False otherwise. 763 """ 764 return bool(get_internal_connector_name()) 765 766 767def get_external_resolution(): 768 """Gets the resolution of the external display. 769 770 @return A tuple of (width, height) or None if no external display is 771 connected. 772 """ 773 crtc = get_external_crtc() 774 if crtc: 775 return crtc.size 776 return None 777 778 779def get_display_output_state(): 780 """ 781 Retrieves output status of connected display(s). 782 783 Return value: dictionary of connected display states. 784 """ 785 return get_modetest_output_state() 786 787 788def set_modetest_output(output_name, enable): 789 # TODO(ihf): figure out what to do here. Don't think this is the right command. 790 # modetest -s <connector_id>[,<connector_id>][@<crtc_id>]:<mode>[-<vrefresh>][@<format>] set a mode 791 pass 792 793 794def set_display_output(output_name, enable): 795 """ 796 Sets the output given by |output_name| on or off. 797 """ 798 set_modetest_output(output_name, enable) 799 800 801# TODO(ihf): Fix this for multiple external connectors. 802def get_external_crtc_id(index=0): 803 crtc = get_external_crtc(index) 804 if crtc is not None: 805 return crtc.id 806 return -1 807 808 809def get_internal_crtc_id(): 810 crtc = get_internal_crtc() 811 if crtc is not None: 812 return crtc.id 813 return -1 814 815 816# TODO(ihf): Fix this for multiple external connectors. 817def get_external_connector_name(): 818 """Gets the name of the external output connector. 819 820 @return The external output connector name as a string, if any. 821 Otherwise, return False. 822 """ 823 outputs = get_display_output_state() 824 for output in outputs.iterkeys(): 825 if outputs[output] and (output.startswith('HDMI') 826 or output.startswith('DP') 827 or output.startswith('DVI') 828 or output.startswith('VGA')): 829 return output 830 return False 831 832 833def get_internal_connector_name(): 834 """Gets the name of the internal output connector. 835 836 @return The internal output connector name as a string, if any. 837 Otherwise, return False. 838 """ 839 outputs = get_display_output_state() 840 for output in outputs.iterkeys(): 841 # reference: chromium_org/chromeos/display/output_util.cc 842 if (output.startswith('eDP') 843 or output.startswith('LVDS') 844 or output.startswith('DSI')): 845 return output 846 return False 847 848 849def wait_output_connected(output): 850 """Wait for output to connect. 851 852 @param output: The output name as a string. 853 854 @return: True if output is connected; False otherwise. 855 """ 856 def _is_connected(output): 857 """Helper function.""" 858 outputs = get_display_output_state() 859 if output not in outputs: 860 return False 861 return outputs[output] 862 863 return utils.wait_for_value(lambda: _is_connected(output), 864 expected_value=True) 865 866 867def set_content_protection(output_name, state): 868 """ 869 Sets the content protection to the given state. 870 871 @param output_name: The output name as a string. 872 @param state: One of the states 'Undesired', 'Desired', or 'Enabled' 873 874 """ 875 raise error.TestFail('freon: set_content_protection not implemented') 876 877 878def get_content_protection(output_name): 879 """ 880 Gets the state of the content protection. 881 882 @param output_name: The output name as a string. 883 @return: A string of the state, like 'Undesired', 'Desired', or 'Enabled'. 884 False if not supported. 885 886 """ 887 raise error.TestFail('freon: get_content_protection not implemented') 888 889 890def is_sw_rasterizer(): 891 """Return true if OpenGL is using a software rendering.""" 892 cmd = utils.wflinfo_cmd() + ' | grep "OpenGL renderer string"' 893 output = utils.run(cmd) 894 result = output.stdout.splitlines()[0] 895 logging.info('wflinfo: %s', result) 896 # TODO(ihf): Find exhaustive error conditions (especially ARM). 897 return 'llvmpipe' in result.lower() or 'soft' in result.lower() 898 899 900def get_gles_version(): 901 cmd = utils.wflinfo_cmd() 902 wflinfo = utils.system_output(cmd, retain_output=False, ignore_status=False) 903 # OpenGL version string: OpenGL ES 3.0 Mesa 10.5.0-devel 904 version = re.findall(r'OpenGL version string: ' 905 r'OpenGL ES ([0-9]+).([0-9]+)', wflinfo) 906 if version: 907 version_major = int(version[0][0]) 908 version_minor = int(version[0][1]) 909 return (version_major, version_minor) 910 return (None, None) 911 912 913def get_egl_version(): 914 cmd = 'eglinfo' 915 eglinfo = utils.system_output(cmd, retain_output=False, ignore_status=False) 916 # EGL version string: 1.4 (DRI2) 917 version = re.findall(r'EGL version string: ([0-9]+).([0-9]+)', eglinfo) 918 if version: 919 version_major = int(version[0][0]) 920 version_minor = int(version[0][1]) 921 return (version_major, version_minor) 922 return (None, None) 923 924 925class GraphicsKernelMemory(object): 926 """ 927 Reads from sysfs to determine kernel gem objects and memory info. 928 """ 929 # These are sysfs fields that will be read by this test. For different 930 # architectures, the sysfs field paths are different. The "paths" are given 931 # as lists of strings because the actual path may vary depending on the 932 # system. This test will read from the first sysfs path in the list that is 933 # present. 934 # e.g. ".../memory" vs ".../gpu_memory" -- if the system has either one of 935 # these, the test will read from that path. 936 amdgpu_fields = { 937 'gem_objects': ['/sys/kernel/debug/dri/0/amdgpu_gem_info'], 938 'memory': ['/sys/kernel/debug/dri/0/amdgpu_gtt_mm'], 939 } 940 arm_fields = {} 941 exynos_fields = { 942 'gem_objects': ['/sys/kernel/debug/dri/?/exynos_gem_objects'], 943 'memory': ['/sys/class/misc/mali0/device/memory', 944 '/sys/class/misc/mali0/device/gpu_memory'], 945 } 946 mediatek_fields = {} 947 # TODO(crosbug.com/p/58189) Add mediatek GPU memory nodes 948 qualcomm_fields = {} 949 # TODO(b/119269602) Add qualcomm GPU memory nodes once GPU patches land 950 rockchip_fields = {} 951 tegra_fields = { 952 'memory': ['/sys/kernel/debug/memblock/memory'], 953 } 954 i915_fields = { 955 'gem_objects': ['/sys/kernel/debug/dri/0/i915_gem_objects'], 956 'memory': ['/sys/kernel/debug/dri/0/i915_gem_gtt'], 957 } 958 # In Linux Kernel 5, i915_gem_gtt merged into i915_gem_objects 959 i915_fields_kernel_5 = { 960 'gem_objects': ['/sys/kernel/debug/dri/0/i915_gem_objects'], 961 } 962 cirrus_fields = {} 963 virtio_fields = {} 964 965 arch_fields = { 966 'amdgpu': amdgpu_fields, 967 'arm': arm_fields, 968 'cirrus': cirrus_fields, 969 'exynos5': exynos_fields, 970 'i915': i915_fields, 971 'i915_kernel_5': i915_fields_kernel_5, 972 'mediatek': mediatek_fields, 973 'qualcomm': qualcomm_fields, 974 'rockchip': rockchip_fields, 975 'tegra': tegra_fields, 976 'virtio': virtio_fields, 977 } 978 979 980 num_errors = 0 981 982 def __init__(self): 983 self._initial_memory = self.get_memory_keyvals() 984 985 def get_memory_difference_keyvals(self): 986 """ 987 Reads the graphics memory values and return the difference between now 988 and the memory usage at initialization stage as keyvals. 989 """ 990 current_memory = self.get_memory_keyvals() 991 return {key: self._initial_memory[key] - current_memory[key] 992 for key in self._initial_memory} 993 994 def get_memory_keyvals(self): 995 """ 996 Reads the graphics memory values and returns them as keyvals. 997 """ 998 keyvals = {} 999 1000 # Get architecture type and list of sysfs fields to read. 1001 soc = utils.get_cpu_soc_family() 1002 1003 arch = utils.get_cpu_arch() 1004 kernel_version = utils.get_kernel_version()[0:4].rstrip(".") 1005 if arch == 'x86_64' or arch == 'i386': 1006 pci_vga_device = utils.run("lspci | grep VGA").stdout.rstrip('\n') 1007 if "Advanced Micro Devices" in pci_vga_device: 1008 soc = 'amdgpu' 1009 elif "Intel Corporation" in pci_vga_device: 1010 soc = 'i915' 1011 if utils.compare_versions(kernel_version, "4.19") > 0: 1012 soc = 'i915_kernel_5' 1013 elif "Cirrus Logic" in pci_vga_device: 1014 # Used on qemu with kernels 3.18 and lower. Limited to 800x600 1015 # resolution. 1016 soc = 'cirrus' 1017 else: 1018 pci_vga_device = utils.run('lshw -c video').stdout.rstrip() 1019 groups = re.search('configuration:.*driver=(\S*)', 1020 pci_vga_device) 1021 if groups and 'virtio' in groups.group(1): 1022 soc = 'virtio' 1023 1024 if not soc in self.arch_fields: 1025 raise error.TestFail('Error: Architecture "%s" not yet supported.' % soc) 1026 fields = self.arch_fields[soc] 1027 1028 for field_name in fields: 1029 possible_field_paths = fields[field_name] 1030 field_value = None 1031 for path in possible_field_paths: 1032 if utils.system('ls %s' % path, ignore_status=True): 1033 continue 1034 field_value = utils.system_output('cat %s' % path) 1035 break 1036 1037 if not field_value: 1038 logging.error('Unable to find any sysfs paths for field "%s"', 1039 field_name) 1040 self.num_errors += 1 1041 continue 1042 1043 parsed_results = GraphicsKernelMemory._parse_sysfs(field_value) 1044 1045 for key in parsed_results: 1046 keyvals['%s_%s' % (field_name, key)] = parsed_results[key] 1047 1048 if 'bytes' in parsed_results and parsed_results['bytes'] == 0: 1049 logging.error('%s reported 0 bytes', field_name) 1050 self.num_errors += 1 1051 1052 keyvals['meminfo_MemUsed'] = (utils.read_from_meminfo('MemTotal') - 1053 utils.read_from_meminfo('MemFree')) 1054 keyvals['meminfo_SwapUsed'] = (utils.read_from_meminfo('SwapTotal') - 1055 utils.read_from_meminfo('SwapFree')) 1056 return keyvals 1057 1058 @staticmethod 1059 def _parse_sysfs(output): 1060 """ 1061 Parses output of graphics memory sysfs to determine the number of 1062 buffer objects and bytes. 1063 1064 Arguments: 1065 output Unprocessed sysfs output 1066 Return value: 1067 Dictionary containing integer values of number bytes and objects. 1068 They may have the keys 'bytes' and 'objects', respectively. However 1069 the result may not contain both of these values. 1070 """ 1071 results = {} 1072 labels = ['bytes', 'objects'] 1073 1074 # First handle i915_gem_objects in 5.x kernels. Example: 1075 # 296 shrinkable [0 free] objects, 274833408 bytes 1076 # frecon: 3 objects, 72192000 bytes (0 active, 0 inactive, 0 unbound, 0 closed) 1077 # chrome: 6 objects, 74629120 bytes (0 active, 0 inactive, 376832 unbound, 0 closed) 1078 # <snip> 1079 i915_gem_objects_pattern = re.compile( 1080 r'(?P<objects>\d*) shrinkable.*objects, (?P<bytes>\d*) bytes') 1081 i915_gem_objects_match = i915_gem_objects_pattern.match(output) 1082 if i915_gem_objects_match is not None: 1083 results['bytes'] = int(i915_gem_objects_match.group('bytes')) 1084 results['objects'] = int(i915_gem_objects_match.group('objects')) 1085 return results 1086 1087 for line in output.split('\n'): 1088 # Strip any commas to make parsing easier. 1089 line_words = line.replace(',', '').split() 1090 1091 prev_word = None 1092 for word in line_words: 1093 # When a label has been found, the previous word should be the 1094 # value. e.g. "3200 bytes" 1095 if word in labels and word not in results and prev_word: 1096 logging.info(prev_word) 1097 results[word] = int(prev_word) 1098 1099 prev_word = word 1100 1101 # Once all values has been parsed, return. 1102 if len(results) == len(labels): 1103 return results 1104 1105 return results 1106 1107 1108class GraphicsStateChecker(object): 1109 """ 1110 Analyzes the state of the GPU and log history. Should be instantiated at the 1111 beginning of each graphics_* test. 1112 """ 1113 crash_blacklist = [] 1114 dirty_writeback_centisecs = 0 1115 existing_hangs = {} 1116 1117 _BROWSER_VERSION_COMMAND = '/opt/google/chrome/chrome --version' 1118 _HANGCHECK = ['drm:i915_hangcheck_elapsed', 'drm:i915_hangcheck_hung', 1119 'Hangcheck timer elapsed...', 1120 'drm/i915: Resetting chip after gpu hang'] 1121 _HANGCHECK_WARNING = ['render ring idle'] 1122 _MESSAGES_FILE = '/var/log/messages' 1123 1124 def __init__(self, raise_error_on_hang=True, run_on_sw_rasterizer=False): 1125 """ 1126 Analyzes the initial state of the GPU and log history. 1127 """ 1128 # Attempt flushing system logs every second instead of every 10 minutes. 1129 self.dirty_writeback_centisecs = utils.get_dirty_writeback_centisecs() 1130 utils.set_dirty_writeback_centisecs(100) 1131 self._raise_error_on_hang = raise_error_on_hang 1132 logging.info(utils.get_board_with_frequency_and_memory()) 1133 self.graphics_kernel_memory = GraphicsKernelMemory() 1134 self._run_on_sw_rasterizer = run_on_sw_rasterizer 1135 1136 if utils.get_cpu_arch() != 'arm': 1137 if not self._run_on_sw_rasterizer and is_sw_rasterizer(): 1138 raise error.TestFail('Refusing to run on SW rasterizer.') 1139 logging.info('Initialize: Checking for old GPU hangs...') 1140 messages = open(self._MESSAGES_FILE, 'r') 1141 for line in messages: 1142 for hang in self._HANGCHECK: 1143 if hang in line: 1144 logging.info(line) 1145 self.existing_hangs[line] = line 1146 messages.close() 1147 1148 def finalize(self): 1149 """ 1150 Analyzes the state of the GPU, log history and emits warnings or errors 1151 if the state changed since initialize. Also makes a note of the Chrome 1152 version for later usage in the perf-dashboard. 1153 """ 1154 utils.set_dirty_writeback_centisecs(self.dirty_writeback_centisecs) 1155 new_gpu_hang = False 1156 new_gpu_warning = False 1157 if utils.get_cpu_arch() != 'arm': 1158 logging.info('Cleanup: Checking for new GPU hangs...') 1159 messages = open(self._MESSAGES_FILE, 'r') 1160 for line in messages: 1161 for hang in self._HANGCHECK: 1162 if hang in line: 1163 if not line in self.existing_hangs.keys(): 1164 logging.info(line) 1165 for warn in self._HANGCHECK_WARNING: 1166 if warn in line: 1167 new_gpu_warning = True 1168 logging.warning( 1169 'Saw GPU hang warning during test.') 1170 else: 1171 logging.warning('Saw GPU hang during test.') 1172 new_gpu_hang = True 1173 messages.close() 1174 1175 if not self._run_on_sw_rasterizer and is_sw_rasterizer(): 1176 logging.warning('Finished test on SW rasterizer.') 1177 raise error.TestFail('Finished test on SW rasterizer.') 1178 if self._raise_error_on_hang and new_gpu_hang: 1179 raise error.TestError('Detected GPU hang during test.') 1180 if new_gpu_hang: 1181 raise error.TestWarn('Detected GPU hang during test.') 1182 if new_gpu_warning: 1183 raise error.TestWarn('Detected GPU warning during test.') 1184 1185 def get_memory_access_errors(self): 1186 """ Returns the number of errors while reading memory stats. """ 1187 return self.graphics_kernel_memory.num_errors 1188 1189 def get_memory_difference_keyvals(self): 1190 return self.graphics_kernel_memory.get_memory_difference_keyvals() 1191 1192 def get_memory_keyvals(self): 1193 """ Returns memory stats. """ 1194 return self.graphics_kernel_memory.get_memory_keyvals() 1195 1196class GraphicsApiHelper(object): 1197 """ 1198 Report on the available graphics APIs. 1199 Ex. gles2, gles3, gles31, and vk 1200 """ 1201 _supported_apis = [] 1202 1203 DEQP_BASEDIR = os.path.join('/usr', 'local', 'deqp') 1204 DEQP_EXECUTABLE = { 1205 'gles2': os.path.join('modules', 'gles2', 'deqp-gles2'), 1206 'gles3': os.path.join('modules', 'gles3', 'deqp-gles3'), 1207 'gles31': os.path.join('modules', 'gles31', 'deqp-gles31'), 1208 'vk': os.path.join('external', 'vulkancts', 'modules', 1209 'vulkan', 'deqp-vk') 1210 } 1211 1212 def __init__(self): 1213 # Determine which executable should be run. Right now never egl. 1214 major, minor = get_gles_version() 1215 logging.info('Found gles%d.%d.', major, minor) 1216 if major is None or minor is None: 1217 raise error.TestFail( 1218 'Failed: Could not get gles version information (%d, %d).' % 1219 (major, minor) 1220 ) 1221 if major >= 2: 1222 self._supported_apis.append('gles2') 1223 if major >= 3: 1224 self._supported_apis.append('gles3') 1225 if major > 3 or minor >= 1: 1226 self._supported_apis.append('gles31') 1227 1228 # If libvulkan is installed, then assume the board supports vulkan. 1229 has_libvulkan = False 1230 for libdir in ('/usr/lib', '/usr/lib64', 1231 '/usr/local/lib', '/usr/local/lib64'): 1232 if os.path.exists(os.path.join(libdir, 'libvulkan.so')): 1233 has_libvulkan = True 1234 1235 if has_libvulkan: 1236 executable_path = os.path.join( 1237 self.DEQP_BASEDIR, 1238 self.DEQP_EXECUTABLE['vk'] 1239 ) 1240 if os.path.exists(executable_path): 1241 self._supported_apis.append('vk') 1242 else: 1243 logging.warning('Found libvulkan.so but did not find deqp-vk ' 1244 'binary for testing.') 1245 1246 def get_supported_apis(self): 1247 """Return the list of supported apis. eg. gles2, gles3, vk etc. 1248 @returns: a copy of the supported api list will be returned 1249 """ 1250 return list(self._supported_apis) 1251 1252 def get_deqp_executable(self, api): 1253 """Return the path to the api executable.""" 1254 if api not in self.DEQP_EXECUTABLE: 1255 raise KeyError( 1256 "%s is not a supported api for GraphicsApiHelper." % api 1257 ) 1258 1259 executable = os.path.join( 1260 self.DEQP_BASEDIR, 1261 self.DEQP_EXECUTABLE[api] 1262 ) 1263 return executable 1264 1265# Possible paths of the kernel DRI debug text file. 1266_DRI_DEBUG_FILE_PATH_0 = "/sys/kernel/debug/dri/0/state" 1267_DRI_DEBUG_FILE_PATH_1 = "/sys/kernel/debug/dri/1/state" 1268 1269# The DRI debug file will have a lot of information, including the position and 1270# sizes of each plane. Some planes might be disabled but have some lingering 1271# crtc-pos information, those are skipped. 1272_CRTC_PLANE_START_PATTERN = re.compile(r'plane\[') 1273_CRTC_DISABLED_PLANE = re.compile(r'crtc=\(null\)') 1274_CRTC_POS_AND_SIZE_PATTERN = re.compile(r'crtc-pos=(?!0x0\+0\+0)') 1275 1276def get_num_hardware_overlays(): 1277 """ 1278 Counts the amount of hardware overlay planes in use. There's always at 1279 least 2 overlays active: the whole screen and the cursor -- unless the 1280 cursor has never moved (e.g. in autotests), and it's not present. 1281 1282 Raises: RuntimeError if the DRI debug file is not present. 1283 OSError/IOError if the file cannot be open()ed or read(). 1284 """ 1285 file_path = _DRI_DEBUG_FILE_PATH_0; 1286 if os.path.exists(_DRI_DEBUG_FILE_PATH_0): 1287 file_path = _DRI_DEBUG_FILE_PATH_0; 1288 elif os.path.exists(_DRI_DEBUG_FILE_PATH_1): 1289 file_path = _DRI_DEBUG_FILE_PATH_1; 1290 else: 1291 raise RuntimeError('No DRI debug file exists (%s, %s)' % 1292 (_DRI_DEBUG_FILE_PATH_0, _DRI_DEBUG_FILE_PATH_1)) 1293 1294 filetext = open(file_path).read() 1295 logging.debug(filetext) 1296 1297 matches = [] 1298 # Split the debug output by planes, skip the disabled ones and extract those 1299 # with correct position and size information. 1300 planes = re.split(_CRTC_PLANE_START_PATTERN, filetext) 1301 for plane in planes: 1302 if len(plane) == 0: 1303 continue; 1304 if len(re.findall(_CRTC_DISABLED_PLANE, plane)) > 0: 1305 continue; 1306 1307 matches.append(re.findall(_CRTC_POS_AND_SIZE_PATTERN, plane)) 1308 1309 # TODO(crbug.com/865112): return also the sizes/locations. 1310 return len(matches) 1311 1312def is_drm_debug_supported(): 1313 """ 1314 @returns true if either of the DRI debug files are present. 1315 """ 1316 return (os.path.exists(_DRI_DEBUG_FILE_PATH_0) or 1317 os.path.exists(_DRI_DEBUG_FILE_PATH_1)) 1318 1319# Path and file name regex defining the filesystem location for DRI devices. 1320_DEV_DRI_FOLDER_PATH = '/dev/dri' 1321_DEV_DRI_CARD_PATH = '/dev/dri/card?' 1322 1323# IOCTL code and associated parameter to set the atomic cap. Defined originally 1324# in the kernel's include/uapi/drm/drm.h file. 1325_DRM_IOCTL_SET_CLIENT_CAP = 0x4010640d 1326_DRM_CLIENT_CAP_ATOMIC = 3 1327 1328def is_drm_atomic_supported(): 1329 """ 1330 @returns true if there is at least a /dev/dri/card? file that seems to 1331 support drm_atomic mode (accepts a _DRM_IOCTL_SET_CLIENT_CAP ioctl). 1332 """ 1333 if not os.path.isdir(_DEV_DRI_FOLDER_PATH): 1334 # This should never ever happen. 1335 raise error.TestError('path %s inexistent', _DEV_DRI_FOLDER_PATH); 1336 1337 for dev_path in glob.glob(_DEV_DRI_CARD_PATH): 1338 try: 1339 logging.debug('trying device %s', dev_path); 1340 with open(dev_path, 'rw') as dev: 1341 # Pack a struct drm_set_client_cap: two u64. 1342 drm_pack = struct.pack("QQ", _DRM_CLIENT_CAP_ATOMIC, 1) 1343 result = fcntl.ioctl(dev, _DRM_IOCTL_SET_CLIENT_CAP, drm_pack) 1344 1345 if result is None or len(result) != len(drm_pack): 1346 # This should never ever happen. 1347 raise error.TestError('ioctl failure') 1348 1349 logging.debug('%s supports atomic', dev_path); 1350 1351 if not is_drm_debug_supported(): 1352 raise error.TestError('platform supports DRM but there ' 1353 ' are no debug files for it') 1354 return True 1355 except IOError as err: 1356 logging.warning('ioctl failed on %s: %s', dev_path, str(err)); 1357 1358 logging.debug('No dev files seems to support atomic'); 1359 return False 1360 1361def get_max_num_available_drm_planes(): 1362 """ 1363 @returns The maximum number of DRM planes available in the system 1364 (associated to the same CRTC), or 0 if something went wrong (e.g. modetest 1365 failed, etc). 1366 """ 1367 1368 planes = get_modetest_planes() 1369 if len(planes) == 0: 1370 return 0; 1371 packed_possible_crtcs = [plane.possible_crtcs for plane in planes] 1372 # |packed_possible_crtcs| is actually a bit field of possible CRTCs, e.g. 1373 # 0x6 (b1001) means the plane can be associated with CRTCs index 0 and 3 but 1374 # not with index 1 nor 2. Unpack those into |possible_crtcs|, an array of 1375 # binary arrays. 1376 possible_crtcs = [[int(bit) for bit in bin(crtc)[2:].zfill(16)] 1377 for crtc in packed_possible_crtcs] 1378 # Accumulate the CRTCs indexes and return the maximum number of 'votes'. 1379 return max(map(sum, zip(*possible_crtcs))) 1380