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