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