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