1391 lines
48 KiB
Python
1391 lines
48 KiB
Python
# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
|
|
# Use of this source code is governed by a BSD-style license that can be
|
|
# found in the LICENSE file.
|
|
|
|
"""
|
|
Provides graphics related utils, like capturing screenshots or checking on
|
|
the state of the graphics driver.
|
|
"""
|
|
|
|
import collections
|
|
import contextlib
|
|
import fcntl
|
|
import glob
|
|
import logging
|
|
import os
|
|
import re
|
|
import struct
|
|
import sys
|
|
import time
|
|
|
|
from autotest_lib.client.bin import test
|
|
from autotest_lib.client.bin import utils
|
|
from autotest_lib.client.common_lib import error
|
|
from autotest_lib.client.cros.input_playback import input_playback
|
|
from autotest_lib.client.cros.power import power_utils
|
|
from functools import wraps
|
|
|
|
# The uinput module might not be available at SDK test time.
|
|
try:
|
|
from autotest_lib.client.cros.graphics import graphics_uinput
|
|
except ImportError:
|
|
graphics_uinput = None
|
|
|
|
|
|
class GraphicsTest(test.test):
|
|
"""Base class for graphics test.
|
|
|
|
GraphicsTest is the base class for graphics tests.
|
|
Every subclass of GraphicsTest should call GraphicsTests initialize/cleanup
|
|
method as they will do GraphicsStateChecker as well as report states to
|
|
Chrome Perf dashboard.
|
|
|
|
Attributes:
|
|
_test_failure_description(str): Failure name reported to chrome perf
|
|
dashboard. (Default: Failures)
|
|
_test_failure_report_enable(bool): Enable/Disable reporting
|
|
failures to chrome perf dashboard
|
|
automatically. (Default: True)
|
|
_test_failure_report_subtest(bool): Enable/Disable reporting
|
|
subtests failure to chrome perf
|
|
dashboard automatically.
|
|
(Default: False)
|
|
"""
|
|
version = 1
|
|
_GSC = None
|
|
|
|
_test_failure_description = "Failures"
|
|
_test_failure_report_enable = True
|
|
_test_failure_report_subtest = False
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
"""Initialize flag setting."""
|
|
super(GraphicsTest, self).__init__(*args, **kwargs)
|
|
self._failures_by_description = {}
|
|
self._player = None
|
|
|
|
def initialize(self, raise_error_on_hang=False, *args, **kwargs):
|
|
"""Initial state checker and report initial value to perf dashboard."""
|
|
self._GSC = GraphicsStateChecker(
|
|
raise_error_on_hang=raise_error_on_hang,
|
|
run_on_sw_rasterizer=utils.is_virtual_machine())
|
|
|
|
self.output_perf_value(
|
|
description='Timeout_Reboot',
|
|
value=1,
|
|
units='count',
|
|
higher_is_better=False,
|
|
replace_existing_values=True
|
|
)
|
|
|
|
if hasattr(super(GraphicsTest, self), "initialize"):
|
|
utils.cherry_pick_call(super(GraphicsTest, self).initialize,
|
|
*args, **kwargs)
|
|
|
|
def input_check(self):
|
|
"""Check if it exists and initialize input player."""
|
|
if self._player is None:
|
|
self._player = input_playback.InputPlayback()
|
|
self._player.emulate(input_type='keyboard')
|
|
self._player.find_connected_inputs()
|
|
|
|
def cleanup(self, *args, **kwargs):
|
|
"""Finalize state checker and report values to perf dashboard."""
|
|
if self._GSC:
|
|
self._GSC.finalize()
|
|
|
|
self._output_perf()
|
|
if self._player:
|
|
self._player.close()
|
|
|
|
if hasattr(super(GraphicsTest, self), "cleanup"):
|
|
utils.cherry_pick_call(super(GraphicsTest, self).cleanup,
|
|
*args, **kwargs)
|
|
|
|
@contextlib.contextmanager
|
|
def failure_report(self, name, subtest=None):
|
|
"""Record the failure of an operation to self._failures_by_description.
|
|
|
|
Records if the operation taken inside executed normally or not.
|
|
If the operation taken inside raise unexpected failure, failure named
|
|
|name|, will be added to the self._failures_by_description dictionary
|
|
and reported to the chrome perf dashboard in the cleanup stage.
|
|
|
|
Usage:
|
|
# Record failure of doSomething
|
|
with failure_report('doSomething'):
|
|
doSomething()
|
|
"""
|
|
# Assume failed at the beginning
|
|
self.add_failures(name, subtest=subtest)
|
|
try:
|
|
yield {}
|
|
self.remove_failures(name, subtest=subtest)
|
|
except (error.TestWarn, error.TestNAError) as e:
|
|
self.remove_failures(name, subtest=subtest)
|
|
raise e
|
|
|
|
@classmethod
|
|
def failure_report_decorator(cls, name, subtest=None):
|
|
"""Record the failure if the function failed to finish.
|
|
This method should only decorate to functions of GraphicsTest.
|
|
In addition, functions with this decorator should be called with no
|
|
unnamed arguments.
|
|
Usage:
|
|
@GraphicsTest.test_run_decorator('graphics_test')
|
|
def Foo(self, bar='test'):
|
|
return doStuff()
|
|
|
|
is equivalent to
|
|
|
|
def Foo(self, bar):
|
|
with failure_reporter('graphics_test'):
|
|
return doStuff()
|
|
|
|
# Incorrect usage.
|
|
@GraphicsTest.test_run_decorator('graphics_test')
|
|
def Foo(self, bar='test'):
|
|
pass
|
|
self.Foo('test_name', bar='test_name') # call Foo with named args
|
|
|
|
# Incorrect usage.
|
|
@GraphicsTest.test_run_decorator('graphics_test')
|
|
def Foo(self, bar='test'):
|
|
pass
|
|
self.Foo('test_name') # call Foo with unnamed args
|
|
"""
|
|
def decorator(fn):
|
|
@wraps(fn)
|
|
def wrapper(*args, **kwargs):
|
|
if len(args) > 1:
|
|
raise error.TestError('Unnamed arguments is not accepted. '
|
|
'Please apply this decorator to '
|
|
'function without unnamed args.')
|
|
# A member function of GraphicsTest is decorated. The first
|
|
# argument is the instance itself.
|
|
instance = args[0]
|
|
with instance.failure_report(name, subtest):
|
|
# Cherry pick the arguments for the wrapped function.
|
|
d_args, d_kwargs = utils.cherry_pick_args(fn, args, kwargs)
|
|
return fn(instance, *d_args, **d_kwargs)
|
|
return wrapper
|
|
return decorator
|
|
|
|
def add_failures(self, name, subtest=None):
|
|
"""
|
|
Add a record to failures list which will report back to chrome perf
|
|
dashboard at cleanup stage.
|
|
Args:
|
|
name: failure name.
|
|
subtest: subtest which will appears in cros-perf. If None is
|
|
specified, use name instead.
|
|
"""
|
|
target = self._get_failure(name, subtest=subtest)
|
|
if target:
|
|
target['names'].append(name)
|
|
else:
|
|
target = {
|
|
'description': self._get_failure_description(name, subtest),
|
|
'unit': 'count',
|
|
'higher_is_better': False,
|
|
'graph': self._get_failure_graph_name(),
|
|
'names': [name],
|
|
}
|
|
self._failures_by_description[target['description']] = target
|
|
return target
|
|
|
|
def remove_failures(self, name, subtest=None):
|
|
"""
|
|
Remove a record from failures list which will report back to chrome perf
|
|
dashboard at cleanup stage.
|
|
Args:
|
|
name: failure name.
|
|
subtest: subtest which will appears in cros-perf. If None is
|
|
specified, use name instead.
|
|
"""
|
|
target = self._get_failure(name, subtest=subtest)
|
|
if name in target['names']:
|
|
target['names'].remove(name)
|
|
|
|
|
|
def _output_perf(self):
|
|
"""Report recorded failures back to chrome perf."""
|
|
self.output_perf_value(
|
|
description='Timeout_Reboot',
|
|
value=0,
|
|
units='count',
|
|
higher_is_better=False,
|
|
replace_existing_values=True
|
|
)
|
|
|
|
if not self._test_failure_report_enable:
|
|
return
|
|
|
|
total_failures = 0
|
|
# Report subtests failures
|
|
for failure in self._failures_by_description.values():
|
|
if len(failure['names']) > 0:
|
|
logging.debug('GraphicsTest failure: %s' % failure['names'])
|
|
total_failures += len(failure['names'])
|
|
|
|
if not self._test_failure_report_subtest:
|
|
continue
|
|
|
|
self.output_perf_value(
|
|
description=failure['description'],
|
|
value=len(failure['names']),
|
|
units=failure['unit'],
|
|
higher_is_better=failure['higher_is_better'],
|
|
graph=failure['graph']
|
|
)
|
|
|
|
# Report the count of all failures
|
|
self.output_perf_value(
|
|
description=self._get_failure_graph_name(),
|
|
value=total_failures,
|
|
units='count',
|
|
higher_is_better=False,
|
|
)
|
|
|
|
def _get_failure_graph_name(self):
|
|
return self._test_failure_description
|
|
|
|
def _get_failure_description(self, name, subtest):
|
|
return subtest or name
|
|
|
|
def _get_failure(self, name, subtest):
|
|
"""Get specific failures."""
|
|
description = self._get_failure_description(name, subtest=subtest)
|
|
return self._failures_by_description.get(description, None)
|
|
|
|
def get_failures(self):
|
|
"""
|
|
Get currently recorded failures list.
|
|
"""
|
|
return [name for failure in self._failures_by_description.values()
|
|
for name in failure['names']]
|
|
|
|
def open_vt1(self):
|
|
"""Switch to VT1 with keyboard."""
|
|
self.input_check()
|
|
self._player.blocking_playback_of_default_file(
|
|
input_type='keyboard', filename='keyboard_ctrl+alt+f1')
|
|
time.sleep(5)
|
|
|
|
def open_vt2(self):
|
|
"""Switch to VT2 with keyboard."""
|
|
self.input_check()
|
|
self._player.blocking_playback_of_default_file(
|
|
input_type='keyboard', filename='keyboard_ctrl+alt+f2')
|
|
time.sleep(5)
|
|
|
|
def wake_screen_with_keyboard(self):
|
|
"""Use the vt1 keyboard shortcut to bring the devices screen back on.
|
|
|
|
This is useful if you want to take screenshots of the UI. If you try
|
|
to take them while the screen is off, it will fail.
|
|
"""
|
|
self.open_vt1()
|
|
|
|
|
|
def screen_disable_blanking():
|
|
""" Called from power_Backlight to disable screen blanking. """
|
|
# We don't have to worry about unexpected screensavers or DPMS here.
|
|
return
|
|
|
|
|
|
def screen_disable_energy_saving():
|
|
""" Called from power_Consumption to immediately disable energy saving. """
|
|
# All we need to do here is enable displays via Chrome.
|
|
power_utils.set_display_power(power_utils.DISPLAY_POWER_ALL_ON)
|
|
return
|
|
|
|
|
|
def screen_toggle_fullscreen():
|
|
"""Toggles fullscreen mode."""
|
|
press_keys(['KEY_F11'])
|
|
|
|
|
|
def screen_toggle_mirrored():
|
|
"""Toggles the mirrored screen."""
|
|
press_keys(['KEY_LEFTCTRL', 'KEY_F4'])
|
|
|
|
|
|
def hide_cursor():
|
|
"""Hides mouse cursor."""
|
|
# Send a keystroke to hide the cursor.
|
|
press_keys(['KEY_UP'])
|
|
|
|
|
|
def hide_typing_cursor():
|
|
"""Hides typing cursor."""
|
|
# Press the tab key to move outside the typing bar.
|
|
press_keys(['KEY_TAB'])
|
|
|
|
|
|
def screen_wakeup():
|
|
"""Wake up the screen if it is dark."""
|
|
# Move the mouse a little bit to wake up the screen.
|
|
device = graphics_uinput.get_device_mouse_rel()
|
|
graphics_uinput.emit(device, 'REL_X', 1)
|
|
graphics_uinput.emit(device, 'REL_X', -1)
|
|
|
|
|
|
def switch_screen_on(on):
|
|
"""
|
|
Turn the touch screen on/off.
|
|
|
|
@param on: On or off.
|
|
"""
|
|
raise error.TestFail('switch_screen_on is not implemented.')
|
|
|
|
|
|
def press_keys(key_list):
|
|
"""Presses the given keys as one combination.
|
|
|
|
Please do not leak uinput dependencies outside of the file.
|
|
|
|
@param key: A list of key strings, e.g. ['LEFTCTRL', 'F4']
|
|
"""
|
|
graphics_uinput.emit_combo(graphics_uinput.get_device_keyboard(), key_list)
|
|
|
|
|
|
def click_mouse():
|
|
"""Just click the mouse.
|
|
Presumably only hacky tests use this function.
|
|
"""
|
|
logging.info('click_mouse()')
|
|
# Move a little to make the cursor appear.
|
|
device = graphics_uinput.get_device_mouse_rel()
|
|
graphics_uinput.emit(device, 'REL_X', 1)
|
|
# Some sleeping is needed otherwise events disappear.
|
|
time.sleep(0.1)
|
|
# Move cursor back to not drift.
|
|
graphics_uinput.emit(device, 'REL_X', -1)
|
|
time.sleep(0.1)
|
|
# Click down.
|
|
graphics_uinput.emit(device, 'BTN_LEFT', 1)
|
|
time.sleep(0.2)
|
|
# Release click.
|
|
graphics_uinput.emit(device, 'BTN_LEFT', 0)
|
|
|
|
|
|
# TODO(ihf): this function is broken. Make it work.
|
|
def activate_focus_at(rel_x, rel_y):
|
|
"""Clicks with the mouse at screen position (x, y).
|
|
|
|
This is a pretty hacky method. Using this will probably lead to
|
|
flaky tests as page layout changes over time.
|
|
@param rel_x: relative horizontal position between 0 and 1.
|
|
@param rel_y: relattive vertical position between 0 and 1.
|
|
"""
|
|
width, height = get_internal_resolution()
|
|
device = graphics_uinput.get_device_touch()
|
|
graphics_uinput.emit(device, 'ABS_MT_SLOT', 0, syn=False)
|
|
graphics_uinput.emit(device, 'ABS_MT_TRACKING_ID', 1, syn=False)
|
|
graphics_uinput.emit(device, 'ABS_MT_POSITION_X', int(rel_x * width),
|
|
syn=False)
|
|
graphics_uinput.emit(device, 'ABS_MT_POSITION_Y', int(rel_y * height),
|
|
syn=False)
|
|
graphics_uinput.emit(device, 'BTN_TOUCH', 1, syn=True)
|
|
time.sleep(0.2)
|
|
graphics_uinput.emit(device, 'BTN_TOUCH', 0, syn=True)
|
|
|
|
|
|
def take_screenshot(resultsdir, fname_prefix):
|
|
"""Take screenshot and save to a new file in the results dir.
|
|
Args:
|
|
@param resultsdir: Directory to store the output in.
|
|
@param fname_prefix: Prefix for the output fname.
|
|
Returns:
|
|
the path of the saved screenshot file
|
|
"""
|
|
|
|
old_exc_type = sys.exc_info()[0]
|
|
|
|
next_index = len(glob.glob(
|
|
os.path.join(resultsdir, '%s-*.png' % fname_prefix)))
|
|
screenshot_file = os.path.join(
|
|
resultsdir, '%s-%d.png' % (fname_prefix, next_index))
|
|
logging.info('Saving screenshot to %s.', screenshot_file)
|
|
|
|
try:
|
|
utils.run('screenshot "%s"' % screenshot_file)
|
|
except Exception as err:
|
|
# Do not raise an exception if the screenshot fails while processing
|
|
# another exception.
|
|
if old_exc_type is None:
|
|
raise
|
|
logging.error(err)
|
|
|
|
return screenshot_file
|
|
|
|
|
|
def take_screenshot_crop(fullpath, box=None, crtc_id=None):
|
|
"""
|
|
Take a screenshot using import tool, crop according to dim given by the box.
|
|
@param fullpath: path, full path to save the image to.
|
|
@param box: 4-tuple giving the upper left and lower right pixel coordinates.
|
|
@param crtc_id: if set, take a screen shot of the specified CRTC.
|
|
"""
|
|
cmd = 'screenshot'
|
|
if crtc_id is not None:
|
|
cmd += ' --crtc-id=%d' % crtc_id
|
|
else:
|
|
cmd += ' --internal'
|
|
if box:
|
|
x, y, r, b = box
|
|
w = r - x
|
|
h = b - y
|
|
cmd += ' --crop=%dx%d+%d+%d' % (w, h, x, y)
|
|
cmd += ' "%s"' % fullpath
|
|
utils.run(cmd)
|
|
return fullpath
|
|
|
|
|
|
# id encoder status name size (mm) modes encoders
|
|
# 39 0 connected eDP-1 256x144 1 38
|
|
_MODETEST_CONNECTOR_PATTERN = re.compile(
|
|
r'^(\d+)\s+(\d+)\s+(connected|disconnected)\s+(\S+)\s+\d+x\d+\s+\d+\s+\d+')
|
|
|
|
# id crtc type possible crtcs possible clones
|
|
# 38 0 TMDS 0x00000002 0x00000000
|
|
_MODETEST_ENCODER_PATTERN = re.compile(
|
|
r'^(\d+)\s+(\d+)\s+\S+\s+0x[0-9a-fA-F]+\s+0x[0-9a-fA-F]+')
|
|
|
|
# Group names match the drmModeModeInfo struct
|
|
_MODETEST_MODE_PATTERN = re.compile(
|
|
r'\s+(?P<name>.+)'
|
|
r'\s+(?P<vrefresh>\d+)'
|
|
r'\s+(?P<hdisplay>\d+)'
|
|
r'\s+(?P<hsync_start>\d+)'
|
|
r'\s+(?P<hsync_end>\d+)'
|
|
r'\s+(?P<htotal>\d+)'
|
|
r'\s+(?P<vdisplay>\d+)'
|
|
r'\s+(?P<vsync_start>\d+)'
|
|
r'\s+(?P<vsync_end>\d+)'
|
|
r'\s+(?P<vtotal>\d+)'
|
|
r'\s+(?P<clock>\d+)'
|
|
r'\s+flags:.+type:'
|
|
r' preferred')
|
|
|
|
_MODETEST_CRTCS_START_PATTERN = re.compile(r'^id\s+fb\s+pos\s+size')
|
|
|
|
_MODETEST_CRTC_PATTERN = re.compile(
|
|
r'^(\d+)\s+(\d+)\s+\((\d+),(\d+)\)\s+\((\d+)x(\d+)\)')
|
|
|
|
_MODETEST_PLANES_START_PATTERN = re.compile(
|
|
r'^id\s+crtc\s+fb\s+CRTC\s+x,y\s+x,y\s+gamma\s+size\s+possible\s+crtcs')
|
|
|
|
_MODETEST_PLANE_PATTERN = re.compile(
|
|
r'^(\d+)\s+(\d+)\s+(\d+)\s+(\d+),(\d+)\s+(\d+),(\d+)\s+(\d+)\s+(0x)(\d+)')
|
|
|
|
Connector = collections.namedtuple(
|
|
'Connector', [
|
|
'cid', # connector id (integer)
|
|
'eid', # encoder id (integer)
|
|
'ctype', # connector type, e.g. 'eDP', 'HDMI-A', 'DP'
|
|
'connected', # boolean
|
|
'size', # current screen size in mm, e.g. (256, 144)
|
|
'encoder', # encoder id (integer)
|
|
# list of resolution tuples, e.g. [(1920,1080), (1600,900), ...]
|
|
'modes',
|
|
])
|
|
|
|
Encoder = collections.namedtuple(
|
|
'Encoder', [
|
|
'eid', # encoder id (integer)
|
|
'crtc_id', # CRTC id (integer)
|
|
])
|
|
|
|
CRTC = collections.namedtuple(
|
|
'CRTC', [
|
|
'id', # crtc id
|
|
'fb', # fb id
|
|
'pos', # position, e.g. (0,0)
|
|
'size', # size, e.g. (1366,768)
|
|
'is_internal', # True if for the internal display
|
|
])
|
|
|
|
Plane = collections.namedtuple(
|
|
'Plane', [
|
|
'id', # plane id
|
|
'possible_crtcs', # possible associated CRTC indexes.
|
|
])
|
|
|
|
def get_display_resolution():
|
|
"""
|
|
Parses output of modetest to determine the display resolution of the dut.
|
|
@return: tuple, (w,h) resolution of device under test.
|
|
"""
|
|
connectors = get_modetest_connectors()
|
|
for connector in connectors:
|
|
if connector.connected:
|
|
return connector.size
|
|
return None
|
|
|
|
|
|
def _get_num_outputs_connected():
|
|
"""
|
|
Parses output of modetest to determine the number of connected displays
|
|
@return: The number of connected displays
|
|
"""
|
|
connected = 0
|
|
connectors = get_modetest_connectors()
|
|
for connector in connectors:
|
|
if connector.connected:
|
|
connected = connected + 1
|
|
|
|
return connected
|
|
|
|
|
|
def get_num_outputs_on():
|
|
"""
|
|
Retrieves the number of connected outputs that are on.
|
|
|
|
Return value: integer value of number of connected outputs that are on.
|
|
"""
|
|
|
|
return _get_num_outputs_connected()
|
|
|
|
|
|
def get_modetest_connectors():
|
|
"""
|
|
Retrieves a list of Connectors using modetest.
|
|
|
|
Return value: List of Connectors.
|
|
"""
|
|
connectors = []
|
|
modetest_output = utils.system_output('modetest -c')
|
|
for line in modetest_output.splitlines():
|
|
# First search for a new connector.
|
|
connector_match = re.match(_MODETEST_CONNECTOR_PATTERN, line)
|
|
if connector_match is not None:
|
|
cid = int(connector_match.group(1))
|
|
eid = int(connector_match.group(2))
|
|
connected = False
|
|
if connector_match.group(3) == 'connected':
|
|
connected = True
|
|
ctype = connector_match.group(4)
|
|
size = (-1, -1)
|
|
encoder = -1
|
|
modes = None
|
|
connectors.append(
|
|
Connector(cid, eid, ctype, connected, size, encoder, modes))
|
|
else:
|
|
# See if we find corresponding line with modes, sizes etc.
|
|
mode_match = re.match(_MODETEST_MODE_PATTERN, line)
|
|
if mode_match is not None:
|
|
size = (int(mode_match.group('hdisplay')),
|
|
int(mode_match.group('vdisplay')))
|
|
# Update display size of last connector in list.
|
|
c = connectors.pop()
|
|
connectors.append(
|
|
Connector(
|
|
c.cid, c.eid, c.ctype, c.connected, size, c.encoder,
|
|
c.modes))
|
|
return connectors
|
|
|
|
|
|
def get_modetest_encoders():
|
|
"""
|
|
Retrieves a list of Encoders using modetest.
|
|
|
|
Return value: List of Encoders.
|
|
"""
|
|
encoders = []
|
|
modetest_output = utils.system_output('modetest -e')
|
|
for line in modetest_output.splitlines():
|
|
encoder_match = re.match(_MODETEST_ENCODER_PATTERN, line)
|
|
if encoder_match is None:
|
|
continue
|
|
|
|
eid = int(encoder_match.group(1))
|
|
crtc_id = int(encoder_match.group(2))
|
|
encoders.append(Encoder(eid, crtc_id))
|
|
return encoders
|
|
|
|
|
|
def find_eid_from_crtc_id(crtc_id):
|
|
"""
|
|
Finds the integer Encoder ID matching a CRTC ID.
|
|
|
|
@param crtc_id: The integer CRTC ID.
|
|
|
|
@return: The integer Encoder ID or None.
|
|
"""
|
|
encoders = get_modetest_encoders()
|
|
for encoder in encoders:
|
|
if encoder.crtc_id == crtc_id:
|
|
return encoder.eid
|
|
return None
|
|
|
|
|
|
def find_connector_from_eid(eid):
|
|
"""
|
|
Finds the Connector object matching an Encoder ID.
|
|
|
|
@param eid: The integer Encoder ID.
|
|
|
|
@return: The Connector object or None.
|
|
"""
|
|
connectors = get_modetest_connectors()
|
|
for connector in connectors:
|
|
if connector.eid == eid:
|
|
return connector
|
|
return None
|
|
|
|
|
|
def get_modetest_crtcs():
|
|
"""
|
|
Returns a list of CRTC data.
|
|
|
|
Sample:
|
|
[CRTC(id=19, fb=50, pos=(0, 0), size=(1366, 768)),
|
|
CRTC(id=22, fb=54, pos=(0, 0), size=(1920, 1080))]
|
|
"""
|
|
crtcs = []
|
|
modetest_output = utils.system_output('modetest -p')
|
|
found = False
|
|
for line in modetest_output.splitlines():
|
|
if found:
|
|
crtc_match = re.match(_MODETEST_CRTC_PATTERN, line)
|
|
if crtc_match is not None:
|
|
crtc_id = int(crtc_match.group(1))
|
|
fb = int(crtc_match.group(2))
|
|
x = int(crtc_match.group(3))
|
|
y = int(crtc_match.group(4))
|
|
width = int(crtc_match.group(5))
|
|
height = int(crtc_match.group(6))
|
|
# CRTCs with fb=0 are disabled, but lets skip anything with
|
|
# trivial width/height just in case.
|
|
if not (fb == 0 or width == 0 or height == 0):
|
|
eid = find_eid_from_crtc_id(crtc_id)
|
|
connector = find_connector_from_eid(eid)
|
|
if connector is None:
|
|
is_internal = False
|
|
else:
|
|
is_internal = (connector.ctype ==
|
|
get_internal_connector_name())
|
|
crtcs.append(CRTC(crtc_id, fb, (x, y), (width, height),
|
|
is_internal))
|
|
elif line and not line[0].isspace():
|
|
return crtcs
|
|
if re.match(_MODETEST_CRTCS_START_PATTERN, line) is not None:
|
|
found = True
|
|
return crtcs
|
|
|
|
|
|
def get_modetest_planes():
|
|
"""
|
|
Returns a list of planes information.
|
|
|
|
Sample:
|
|
[Plane(id=26, possible_crtcs=1),
|
|
Plane(id=29, possible_crtcs=1)]
|
|
"""
|
|
planes = []
|
|
modetest_output = utils.system_output('modetest -p')
|
|
found = False
|
|
for line in modetest_output.splitlines():
|
|
if found:
|
|
plane_match = re.match(_MODETEST_PLANE_PATTERN, line)
|
|
if plane_match is not None:
|
|
plane_id = int(plane_match.group(1))
|
|
possible_crtcs = int(plane_match.group(10))
|
|
if not (plane_id == 0 or possible_crtcs == 0):
|
|
planes.append(Plane(plane_id, possible_crtcs))
|
|
elif line and not line[0].isspace():
|
|
return planes
|
|
if re.match(_MODETEST_PLANES_START_PATTERN, line) is not None:
|
|
found = True
|
|
return planes
|
|
|
|
|
|
def is_nv12_supported_by_drm_planes():
|
|
"""
|
|
Returns if the planes information mention NV12 format or not.
|
|
|
|
This is a crude way to figure out if the device will not be able to promote
|
|
video frames to overlays at all, which happens for example on Broadwell.
|
|
"""
|
|
modetest_output = utils.system_output('modetest -p')
|
|
return "nv12" in modetest_output.lower()
|
|
|
|
def get_modetest_output_state():
|
|
"""
|
|
Reduce the output of get_modetest_connectors to a dictionary of connector/active states.
|
|
"""
|
|
connectors = get_modetest_connectors()
|
|
outputs = {}
|
|
for connector in connectors:
|
|
# TODO(ihf): Figure out why modetest output needs filtering.
|
|
if connector.connected:
|
|
outputs[connector.ctype] = connector.connected
|
|
return outputs
|
|
|
|
|
|
def get_output_rect(output):
|
|
"""Gets the size and position of the given output on the screen buffer.
|
|
|
|
@param output: The output name as a string.
|
|
|
|
@return A tuple of the rectangle (width, height, fb_offset_x,
|
|
fb_offset_y) of ints.
|
|
"""
|
|
connectors = get_modetest_connectors()
|
|
for connector in connectors:
|
|
if connector.ctype == output:
|
|
# Concatenate two 2-tuples to 4-tuple.
|
|
return connector.size + (0, 0) # TODO(ihf): Should we use CRTC.pos?
|
|
return (0, 0, 0, 0)
|
|
|
|
|
|
def get_internal_crtc():
|
|
for crtc in get_modetest_crtcs():
|
|
if crtc.is_internal:
|
|
return crtc
|
|
return None
|
|
|
|
|
|
def get_external_crtc(index=0):
|
|
for crtc in get_modetest_crtcs():
|
|
if not crtc.is_internal:
|
|
if index == 0:
|
|
return crtc
|
|
index -= 1
|
|
return None
|
|
|
|
|
|
def get_internal_resolution():
|
|
crtc = get_internal_crtc()
|
|
if crtc:
|
|
return crtc.size
|
|
return (-1, -1)
|
|
|
|
|
|
def has_internal_display():
|
|
"""Checks whether the DUT is equipped with an internal display.
|
|
|
|
@return True if internal display is present; False otherwise.
|
|
"""
|
|
return bool(get_internal_connector_name())
|
|
|
|
|
|
def get_external_resolution():
|
|
"""Gets the resolution of the external display.
|
|
|
|
@return A tuple of (width, height) or None if no external display is
|
|
connected.
|
|
"""
|
|
crtc = get_external_crtc()
|
|
if crtc:
|
|
return crtc.size
|
|
return None
|
|
|
|
|
|
def get_display_output_state():
|
|
"""
|
|
Retrieves output status of connected display(s).
|
|
|
|
Return value: dictionary of connected display states.
|
|
"""
|
|
return get_modetest_output_state()
|
|
|
|
|
|
def set_modetest_output(output_name, enable):
|
|
# TODO(ihf): figure out what to do here. Don't think this is the right command.
|
|
# modetest -s <connector_id>[,<connector_id>][@<crtc_id>]:<mode>[-<vrefresh>][@<format>] set a mode
|
|
pass
|
|
|
|
|
|
def set_display_output(output_name, enable):
|
|
"""
|
|
Sets the output given by |output_name| on or off.
|
|
"""
|
|
set_modetest_output(output_name, enable)
|
|
|
|
|
|
# TODO(ihf): Fix this for multiple external connectors.
|
|
def get_external_crtc_id(index=0):
|
|
crtc = get_external_crtc(index)
|
|
if crtc is not None:
|
|
return crtc.id
|
|
return -1
|
|
|
|
|
|
def get_internal_crtc_id():
|
|
crtc = get_internal_crtc()
|
|
if crtc is not None:
|
|
return crtc.id
|
|
return -1
|
|
|
|
|
|
# TODO(ihf): Fix this for multiple external connectors.
|
|
def get_external_connector_name():
|
|
"""Gets the name of the external output connector.
|
|
|
|
@return The external output connector name as a string, if any.
|
|
Otherwise, return False.
|
|
"""
|
|
outputs = get_display_output_state()
|
|
for output in outputs.iterkeys():
|
|
if outputs[output] and (output.startswith('HDMI')
|
|
or output.startswith('DP')
|
|
or output.startswith('DVI')
|
|
or output.startswith('VGA')):
|
|
return output
|
|
return False
|
|
|
|
|
|
def get_internal_connector_name():
|
|
"""Gets the name of the internal output connector.
|
|
|
|
@return The internal output connector name as a string, if any.
|
|
Otherwise, return False.
|
|
"""
|
|
outputs = get_display_output_state()
|
|
for output in outputs.iterkeys():
|
|
# reference: chromium_org/chromeos/display/output_util.cc
|
|
if (output.startswith('eDP')
|
|
or output.startswith('LVDS')
|
|
or output.startswith('DSI')):
|
|
return output
|
|
return False
|
|
|
|
|
|
def wait_output_connected(output):
|
|
"""Wait for output to connect.
|
|
|
|
@param output: The output name as a string.
|
|
|
|
@return: True if output is connected; False otherwise.
|
|
"""
|
|
def _is_connected(output):
|
|
"""Helper function."""
|
|
outputs = get_display_output_state()
|
|
if output not in outputs:
|
|
return False
|
|
return outputs[output]
|
|
|
|
return utils.wait_for_value(lambda: _is_connected(output),
|
|
expected_value=True)
|
|
|
|
|
|
def set_content_protection(output_name, state):
|
|
"""
|
|
Sets the content protection to the given state.
|
|
|
|
@param output_name: The output name as a string.
|
|
@param state: One of the states 'Undesired', 'Desired', or 'Enabled'
|
|
|
|
"""
|
|
raise error.TestFail('freon: set_content_protection not implemented')
|
|
|
|
|
|
def get_content_protection(output_name):
|
|
"""
|
|
Gets the state of the content protection.
|
|
|
|
@param output_name: The output name as a string.
|
|
@return: A string of the state, like 'Undesired', 'Desired', or 'Enabled'.
|
|
False if not supported.
|
|
|
|
"""
|
|
raise error.TestFail('freon: get_content_protection not implemented')
|
|
|
|
|
|
def is_sw_rasterizer():
|
|
"""Return true if OpenGL is using a software rendering."""
|
|
cmd = utils.wflinfo_cmd() + ' | grep "OpenGL renderer string"'
|
|
output = utils.run(cmd)
|
|
result = output.stdout.splitlines()[0]
|
|
logging.info('wflinfo: %s', result)
|
|
# TODO(ihf): Find exhaustive error conditions (especially ARM).
|
|
return 'llvmpipe' in result.lower() or 'soft' in result.lower()
|
|
|
|
|
|
def get_gles_version():
|
|
cmd = utils.wflinfo_cmd()
|
|
wflinfo = utils.system_output(cmd, retain_output=False, ignore_status=False)
|
|
# OpenGL version string: OpenGL ES 3.0 Mesa 10.5.0-devel
|
|
version = re.findall(r'OpenGL version string: '
|
|
r'OpenGL ES ([0-9]+).([0-9]+)', wflinfo)
|
|
if version:
|
|
version_major = int(version[0][0])
|
|
version_minor = int(version[0][1])
|
|
return (version_major, version_minor)
|
|
return (None, None)
|
|
|
|
|
|
def get_egl_version():
|
|
cmd = 'eglinfo'
|
|
eglinfo = utils.system_output(cmd, retain_output=False, ignore_status=False)
|
|
# EGL version string: 1.4 (DRI2)
|
|
version = re.findall(r'EGL version string: ([0-9]+).([0-9]+)', eglinfo)
|
|
if version:
|
|
version_major = int(version[0][0])
|
|
version_minor = int(version[0][1])
|
|
return (version_major, version_minor)
|
|
return (None, None)
|
|
|
|
|
|
class GraphicsKernelMemory(object):
|
|
"""
|
|
Reads from sysfs to determine kernel gem objects and memory info.
|
|
"""
|
|
# These are sysfs fields that will be read by this test. For different
|
|
# architectures, the sysfs field paths are different. The "paths" are given
|
|
# as lists of strings because the actual path may vary depending on the
|
|
# system. This test will read from the first sysfs path in the list that is
|
|
# present.
|
|
# e.g. ".../memory" vs ".../gpu_memory" -- if the system has either one of
|
|
# these, the test will read from that path.
|
|
amdgpu_fields = {
|
|
'gem_objects': ['/sys/kernel/debug/dri/0/amdgpu_gem_info'],
|
|
'memory': ['/sys/kernel/debug/dri/0/amdgpu_gtt_mm'],
|
|
}
|
|
arm_fields = {}
|
|
exynos_fields = {
|
|
'gem_objects': ['/sys/kernel/debug/dri/?/exynos_gem_objects'],
|
|
'memory': ['/sys/class/misc/mali0/device/memory',
|
|
'/sys/class/misc/mali0/device/gpu_memory'],
|
|
}
|
|
mediatek_fields = {}
|
|
# TODO(crosbug.com/p/58189) Add mediatek GPU memory nodes
|
|
qualcomm_fields = {}
|
|
# TODO(b/119269602) Add qualcomm GPU memory nodes once GPU patches land
|
|
rockchip_fields = {}
|
|
tegra_fields = {
|
|
'memory': ['/sys/kernel/debug/memblock/memory'],
|
|
}
|
|
i915_fields = {
|
|
'gem_objects': ['/sys/kernel/debug/dri/0/i915_gem_objects'],
|
|
'memory': ['/sys/kernel/debug/dri/0/i915_gem_gtt'],
|
|
}
|
|
# In Linux Kernel 5, i915_gem_gtt merged into i915_gem_objects
|
|
i915_fields_kernel_5 = {
|
|
'gem_objects': ['/sys/kernel/debug/dri/0/i915_gem_objects'],
|
|
}
|
|
cirrus_fields = {}
|
|
virtio_fields = {}
|
|
|
|
arch_fields = {
|
|
'amdgpu': amdgpu_fields,
|
|
'arm': arm_fields,
|
|
'cirrus': cirrus_fields,
|
|
'exynos5': exynos_fields,
|
|
'i915': i915_fields,
|
|
'i915_kernel_5': i915_fields_kernel_5,
|
|
'mediatek': mediatek_fields,
|
|
'qualcomm': qualcomm_fields,
|
|
'rockchip': rockchip_fields,
|
|
'tegra': tegra_fields,
|
|
'virtio': virtio_fields,
|
|
}
|
|
|
|
|
|
num_errors = 0
|
|
|
|
def __init__(self):
|
|
self._initial_memory = self.get_memory_keyvals()
|
|
|
|
def get_memory_difference_keyvals(self):
|
|
"""
|
|
Reads the graphics memory values and return the difference between now
|
|
and the memory usage at initialization stage as keyvals.
|
|
"""
|
|
current_memory = self.get_memory_keyvals()
|
|
return {key: self._initial_memory[key] - current_memory[key]
|
|
for key in self._initial_memory}
|
|
|
|
def get_memory_keyvals(self):
|
|
"""
|
|
Reads the graphics memory values and returns them as keyvals.
|
|
"""
|
|
keyvals = {}
|
|
|
|
# Get architecture type and list of sysfs fields to read.
|
|
soc = utils.get_cpu_soc_family()
|
|
|
|
arch = utils.get_cpu_arch()
|
|
kernel_version = utils.get_kernel_version()[0:4].rstrip(".")
|
|
if arch == 'x86_64' or arch == 'i386':
|
|
pci_vga_device = utils.run("lspci | grep VGA").stdout.rstrip('\n')
|
|
if "Advanced Micro Devices" in pci_vga_device:
|
|
soc = 'amdgpu'
|
|
elif "Intel Corporation" in pci_vga_device:
|
|
soc = 'i915'
|
|
if utils.compare_versions(kernel_version, "4.19") > 0:
|
|
soc = 'i915_kernel_5'
|
|
elif "Cirrus Logic" in pci_vga_device:
|
|
# Used on qemu with kernels 3.18 and lower. Limited to 800x600
|
|
# resolution.
|
|
soc = 'cirrus'
|
|
else:
|
|
pci_vga_device = utils.run('lshw -c video').stdout.rstrip()
|
|
groups = re.search('configuration:.*driver=(\S*)',
|
|
pci_vga_device)
|
|
if groups and 'virtio' in groups.group(1):
|
|
soc = 'virtio'
|
|
|
|
if not soc in self.arch_fields:
|
|
raise error.TestFail('Error: Architecture "%s" not yet supported.' % soc)
|
|
fields = self.arch_fields[soc]
|
|
|
|
for field_name in fields:
|
|
possible_field_paths = fields[field_name]
|
|
field_value = None
|
|
for path in possible_field_paths:
|
|
if utils.system('ls %s' % path, ignore_status=True):
|
|
continue
|
|
field_value = utils.system_output('cat %s' % path)
|
|
break
|
|
|
|
if not field_value:
|
|
logging.error('Unable to find any sysfs paths for field "%s"',
|
|
field_name)
|
|
self.num_errors += 1
|
|
continue
|
|
|
|
parsed_results = GraphicsKernelMemory._parse_sysfs(field_value)
|
|
|
|
for key in parsed_results:
|
|
keyvals['%s_%s' % (field_name, key)] = parsed_results[key]
|
|
|
|
if 'bytes' in parsed_results and parsed_results['bytes'] == 0:
|
|
logging.error('%s reported 0 bytes', field_name)
|
|
self.num_errors += 1
|
|
|
|
keyvals['meminfo_MemUsed'] = (utils.read_from_meminfo('MemTotal') -
|
|
utils.read_from_meminfo('MemFree'))
|
|
keyvals['meminfo_SwapUsed'] = (utils.read_from_meminfo('SwapTotal') -
|
|
utils.read_from_meminfo('SwapFree'))
|
|
return keyvals
|
|
|
|
@staticmethod
|
|
def _parse_sysfs(output):
|
|
"""
|
|
Parses output of graphics memory sysfs to determine the number of
|
|
buffer objects and bytes.
|
|
|
|
Arguments:
|
|
output Unprocessed sysfs output
|
|
Return value:
|
|
Dictionary containing integer values of number bytes and objects.
|
|
They may have the keys 'bytes' and 'objects', respectively. However
|
|
the result may not contain both of these values.
|
|
"""
|
|
results = {}
|
|
labels = ['bytes', 'objects']
|
|
|
|
# First handle i915_gem_objects in 5.x kernels. Example:
|
|
# 296 shrinkable [0 free] objects, 274833408 bytes
|
|
# frecon: 3 objects, 72192000 bytes (0 active, 0 inactive, 0 unbound, 0 closed)
|
|
# chrome: 6 objects, 74629120 bytes (0 active, 0 inactive, 376832 unbound, 0 closed)
|
|
# <snip>
|
|
i915_gem_objects_pattern = re.compile(
|
|
r'(?P<objects>\d*) shrinkable.*objects, (?P<bytes>\d*) bytes')
|
|
i915_gem_objects_match = i915_gem_objects_pattern.match(output)
|
|
if i915_gem_objects_match is not None:
|
|
results['bytes'] = int(i915_gem_objects_match.group('bytes'))
|
|
results['objects'] = int(i915_gem_objects_match.group('objects'))
|
|
return results
|
|
|
|
for line in output.split('\n'):
|
|
# Strip any commas to make parsing easier.
|
|
line_words = line.replace(',', '').split()
|
|
|
|
prev_word = None
|
|
for word in line_words:
|
|
# When a label has been found, the previous word should be the
|
|
# value. e.g. "3200 bytes"
|
|
if word in labels and word not in results and prev_word:
|
|
logging.info(prev_word)
|
|
results[word] = int(prev_word)
|
|
|
|
prev_word = word
|
|
|
|
# Once all values has been parsed, return.
|
|
if len(results) == len(labels):
|
|
return results
|
|
|
|
return results
|
|
|
|
|
|
class GraphicsStateChecker(object):
|
|
"""
|
|
Analyzes the state of the GPU and log history. Should be instantiated at the
|
|
beginning of each graphics_* test.
|
|
"""
|
|
dirty_writeback_centisecs = 0
|
|
existing_hangs = {}
|
|
|
|
_BROWSER_VERSION_COMMAND = '/opt/google/chrome/chrome --version'
|
|
_HANGCHECK = ['drm:i915_hangcheck_elapsed', 'drm:i915_hangcheck_hung',
|
|
'Hangcheck timer elapsed...',
|
|
'drm/i915: Resetting chip after gpu hang']
|
|
_HANGCHECK_WARNING = ['render ring idle']
|
|
_MESSAGES_FILE = '/var/log/messages'
|
|
|
|
def __init__(self, raise_error_on_hang=True, run_on_sw_rasterizer=False):
|
|
"""
|
|
Analyzes the initial state of the GPU and log history.
|
|
"""
|
|
# Attempt flushing system logs every second instead of every 10 minutes.
|
|
self.dirty_writeback_centisecs = utils.get_dirty_writeback_centisecs()
|
|
utils.set_dirty_writeback_centisecs(100)
|
|
self._raise_error_on_hang = raise_error_on_hang
|
|
logging.info(utils.get_board_with_frequency_and_memory())
|
|
self.graphics_kernel_memory = GraphicsKernelMemory()
|
|
self._run_on_sw_rasterizer = run_on_sw_rasterizer
|
|
|
|
if utils.get_cpu_arch() != 'arm':
|
|
if not self._run_on_sw_rasterizer and is_sw_rasterizer():
|
|
raise error.TestFail('Refusing to run on SW rasterizer.')
|
|
logging.info('Initialize: Checking for old GPU hangs...')
|
|
messages = open(self._MESSAGES_FILE, 'r')
|
|
for line in messages:
|
|
for hang in self._HANGCHECK:
|
|
if hang in line:
|
|
logging.info(line)
|
|
self.existing_hangs[line] = line
|
|
messages.close()
|
|
|
|
def finalize(self):
|
|
"""
|
|
Analyzes the state of the GPU, log history and emits warnings or errors
|
|
if the state changed since initialize. Also makes a note of the Chrome
|
|
version for later usage in the perf-dashboard.
|
|
"""
|
|
utils.set_dirty_writeback_centisecs(self.dirty_writeback_centisecs)
|
|
new_gpu_hang = False
|
|
new_gpu_warning = False
|
|
if utils.get_cpu_arch() != 'arm':
|
|
logging.info('Cleanup: Checking for new GPU hangs...')
|
|
messages = open(self._MESSAGES_FILE, 'r')
|
|
for line in messages:
|
|
for hang in self._HANGCHECK:
|
|
if hang in line:
|
|
if not line in self.existing_hangs.keys():
|
|
logging.info(line)
|
|
for warn in self._HANGCHECK_WARNING:
|
|
if warn in line:
|
|
new_gpu_warning = True
|
|
logging.warning(
|
|
'Saw GPU hang warning during test.')
|
|
else:
|
|
logging.warning('Saw GPU hang during test.')
|
|
new_gpu_hang = True
|
|
messages.close()
|
|
|
|
if not self._run_on_sw_rasterizer and is_sw_rasterizer():
|
|
logging.warning('Finished test on SW rasterizer.')
|
|
raise error.TestFail('Finished test on SW rasterizer.')
|
|
if self._raise_error_on_hang and new_gpu_hang:
|
|
raise error.TestError('Detected GPU hang during test.')
|
|
if new_gpu_hang:
|
|
raise error.TestWarn('Detected GPU hang during test.')
|
|
if new_gpu_warning:
|
|
raise error.TestWarn('Detected GPU warning during test.')
|
|
|
|
def get_memory_access_errors(self):
|
|
""" Returns the number of errors while reading memory stats. """
|
|
return self.graphics_kernel_memory.num_errors
|
|
|
|
def get_memory_difference_keyvals(self):
|
|
return self.graphics_kernel_memory.get_memory_difference_keyvals()
|
|
|
|
def get_memory_keyvals(self):
|
|
""" Returns memory stats. """
|
|
return self.graphics_kernel_memory.get_memory_keyvals()
|
|
|
|
class GraphicsApiHelper(object):
|
|
"""
|
|
Report on the available graphics APIs.
|
|
Ex. gles2, gles3, gles31, and vk
|
|
"""
|
|
_supported_apis = []
|
|
|
|
DEQP_BASEDIR = os.path.join('/usr', 'local', 'deqp')
|
|
DEQP_EXECUTABLE = {
|
|
'gles2': os.path.join('modules', 'gles2', 'deqp-gles2'),
|
|
'gles3': os.path.join('modules', 'gles3', 'deqp-gles3'),
|
|
'gles31': os.path.join('modules', 'gles31', 'deqp-gles31'),
|
|
'vk': os.path.join('external', 'vulkancts', 'modules',
|
|
'vulkan', 'deqp-vk')
|
|
}
|
|
|
|
def __init__(self):
|
|
# Determine which executable should be run. Right now never egl.
|
|
major, minor = get_gles_version()
|
|
logging.info('Found gles%d.%d.', major, minor)
|
|
if major is None or minor is None:
|
|
raise error.TestFail(
|
|
'Failed: Could not get gles version information (%d, %d).' %
|
|
(major, minor)
|
|
)
|
|
if major >= 2:
|
|
self._supported_apis.append('gles2')
|
|
if major >= 3:
|
|
self._supported_apis.append('gles3')
|
|
if major > 3 or minor >= 1:
|
|
self._supported_apis.append('gles31')
|
|
|
|
# If libvulkan is installed, then assume the board supports vulkan.
|
|
has_libvulkan = False
|
|
for libdir in ('/usr/lib', '/usr/lib64',
|
|
'/usr/local/lib', '/usr/local/lib64'):
|
|
if os.path.exists(os.path.join(libdir, 'libvulkan.so')):
|
|
has_libvulkan = True
|
|
|
|
if has_libvulkan:
|
|
executable_path = os.path.join(
|
|
self.DEQP_BASEDIR,
|
|
self.DEQP_EXECUTABLE['vk']
|
|
)
|
|
if os.path.exists(executable_path):
|
|
self._supported_apis.append('vk')
|
|
else:
|
|
logging.warning('Found libvulkan.so but did not find deqp-vk '
|
|
'binary for testing.')
|
|
|
|
def get_supported_apis(self):
|
|
"""Return the list of supported apis. eg. gles2, gles3, vk etc.
|
|
@returns: a copy of the supported api list will be returned
|
|
"""
|
|
return list(self._supported_apis)
|
|
|
|
def get_deqp_executable(self, api):
|
|
"""Return the path to the api executable."""
|
|
if api not in self.DEQP_EXECUTABLE:
|
|
raise KeyError(
|
|
"%s is not a supported api for GraphicsApiHelper." % api
|
|
)
|
|
|
|
executable = os.path.join(
|
|
self.DEQP_BASEDIR,
|
|
self.DEQP_EXECUTABLE[api]
|
|
)
|
|
return executable
|
|
|
|
# Possible paths of the kernel DRI debug text file.
|
|
_DRI_DEBUG_FILE_PATH_0 = "/sys/kernel/debug/dri/0/state"
|
|
_DRI_DEBUG_FILE_PATH_1 = "/sys/kernel/debug/dri/1/state"
|
|
_DRI_DEBUG_FILE_PATH_2 = "/sys/kernel/debug/dri/2/state"
|
|
|
|
# The DRI debug file will have a lot of information, including the position and
|
|
# sizes of each plane. Some planes might be disabled but have some lingering
|
|
# crtc-pos information, those are skipped.
|
|
_CRTC_PLANE_START_PATTERN = re.compile(r'plane\[')
|
|
_CRTC_DISABLED_PLANE = re.compile(r'crtc=\(null\)')
|
|
_CRTC_POS_AND_SIZE_PATTERN = re.compile(r'crtc-pos=(?!0x0\+0\+0)')
|
|
|
|
def get_num_hardware_overlays():
|
|
"""
|
|
Counts the amount of hardware overlay planes in use. There's always at
|
|
least 2 overlays active: the whole screen and the cursor -- unless the
|
|
cursor has never moved (e.g. in autotests), and it's not present.
|
|
|
|
Raises: RuntimeError if the DRI debug file is not present.
|
|
OSError/IOError if the file cannot be open()ed or read().
|
|
"""
|
|
file_path = _DRI_DEBUG_FILE_PATH_0;
|
|
if os.path.exists(_DRI_DEBUG_FILE_PATH_0):
|
|
file_path = _DRI_DEBUG_FILE_PATH_0;
|
|
elif os.path.exists(_DRI_DEBUG_FILE_PATH_1):
|
|
file_path = _DRI_DEBUG_FILE_PATH_1;
|
|
elif os.path.exists(_DRI_DEBUG_FILE_PATH_2):
|
|
file_path = _DRI_DEBUG_FILE_PATH_2;
|
|
else:
|
|
raise RuntimeError('No DRI debug file exists (%s, %s)' %
|
|
(_DRI_DEBUG_FILE_PATH_0, _DRI_DEBUG_FILE_PATH_1))
|
|
|
|
filetext = open(file_path).read()
|
|
logging.debug(filetext)
|
|
|
|
matches = []
|
|
# Split the debug output by planes, skip the disabled ones and extract those
|
|
# with correct position and size information.
|
|
planes = re.split(_CRTC_PLANE_START_PATTERN, filetext)
|
|
for plane in planes:
|
|
if len(plane) == 0:
|
|
continue;
|
|
if len(re.findall(_CRTC_DISABLED_PLANE, plane)) > 0:
|
|
continue;
|
|
|
|
matches.append(re.findall(_CRTC_POS_AND_SIZE_PATTERN, plane))
|
|
|
|
# TODO(crbug.com/865112): return also the sizes/locations.
|
|
return len(matches)
|
|
|
|
def is_drm_debug_supported():
|
|
"""
|
|
@returns true if either of the DRI debug files are present.
|
|
"""
|
|
return (os.path.exists(_DRI_DEBUG_FILE_PATH_0) or
|
|
os.path.exists(_DRI_DEBUG_FILE_PATH_1) or
|
|
os.path.exists(_DRI_DEBUG_FILE_PATH_2))
|
|
|
|
# Path and file name regex defining the filesystem location for DRI devices.
|
|
_DEV_DRI_FOLDER_PATH = '/dev/dri'
|
|
_DEV_DRI_CARD_PATH = '/dev/dri/card?'
|
|
|
|
# IOCTL code and associated parameter to set the atomic cap. Defined originally
|
|
# in the kernel's include/uapi/drm/drm.h file.
|
|
_DRM_IOCTL_SET_CLIENT_CAP = 0x4010640d
|
|
_DRM_CLIENT_CAP_ATOMIC = 3
|
|
|
|
def is_drm_atomic_supported():
|
|
"""
|
|
@returns true if there is at least a /dev/dri/card? file that seems to
|
|
support drm_atomic mode (accepts a _DRM_IOCTL_SET_CLIENT_CAP ioctl).
|
|
"""
|
|
if not os.path.isdir(_DEV_DRI_FOLDER_PATH):
|
|
# This should never ever happen.
|
|
raise error.TestError('path %s inexistent', _DEV_DRI_FOLDER_PATH);
|
|
|
|
for dev_path in glob.glob(_DEV_DRI_CARD_PATH):
|
|
try:
|
|
logging.debug('trying device %s', dev_path);
|
|
with open(dev_path, 'rw') as dev:
|
|
# Pack a struct drm_set_client_cap: two u64.
|
|
drm_pack = struct.pack("QQ", _DRM_CLIENT_CAP_ATOMIC, 1)
|
|
result = fcntl.ioctl(dev, _DRM_IOCTL_SET_CLIENT_CAP, drm_pack)
|
|
|
|
if result is None or len(result) != len(drm_pack):
|
|
# This should never ever happen.
|
|
raise error.TestError('ioctl failure')
|
|
|
|
logging.debug('%s supports atomic', dev_path);
|
|
|
|
if not is_drm_debug_supported():
|
|
raise error.TestError('platform supports DRM but there '
|
|
' are no debug files for it')
|
|
return True
|
|
except IOError as err:
|
|
logging.warning('ioctl failed on %s: %s', dev_path, str(err));
|
|
|
|
logging.debug('No dev files seems to support atomic');
|
|
return False
|
|
|
|
def get_max_num_available_drm_planes():
|
|
"""
|
|
@returns The maximum number of DRM planes available in the system
|
|
(associated to the same CRTC), or 0 if something went wrong (e.g. modetest
|
|
failed, etc).
|
|
"""
|
|
|
|
planes = get_modetest_planes()
|
|
if len(planes) == 0:
|
|
return 0;
|
|
packed_possible_crtcs = [plane.possible_crtcs for plane in planes]
|
|
# |packed_possible_crtcs| is actually a bit field of possible CRTCs, e.g.
|
|
# 0x6 (b1001) means the plane can be associated with CRTCs index 0 and 3 but
|
|
# not with index 1 nor 2. Unpack those into |possible_crtcs|, an array of
|
|
# binary arrays.
|
|
possible_crtcs = [[int(bit) for bit in bin(crtc)[2:].zfill(16)]
|
|
for crtc in packed_possible_crtcs]
|
|
# Accumulate the CRTCs indexes and return the maximum number of 'votes'.
|
|
return max(map(sum, zip(*possible_crtcs)))
|