# Copyright 2019 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import os
import re
from typing import Dict, FrozenSet, List, Match, Optional, Tuple, Union
import unittest.mock as mock
from gpu_tests import constants
from gpu_tests.util import host_information
from telemetry.internal.platform import gpu_info as tgi
# This set must be the union of the driver tags used in WebGL and WebGL2
# expectations files.
# Examples:
# intel_lt_25.20.100.6577
# mesa_ge_20.1
EXPECTATIONS_DRIVER_TAGS = frozenset([
'mesa_lt_19.1',
'mesa_ge_21.0',
'mesa_ge_23.2',
'nvidia_ge_31.0.15.4601',
'nvidia_lt_31.0.15.4601',
'nvidia_ge_535.183.01',
'nvidia_lt_535.183.01',
])
# Driver tag format: VENDOR_OPERATION_VERSION
DRIVER_TAG_MATCHER = re.compile(
r'^([a-z\d]+)_(eq|ne|ge|gt|le|lt)_([a-z\d\.]+)$')
REMOTE_BROWSER_TYPES = [
'android-chromium',
'android-webview-instrumentation',
'cros-chrome',
'fuchsia-chrome',
'web-engine-shell',
'cast-streaming-shell',
]
TAG_SUBSTRING_REPLACEMENTS = {
# nvidia on desktop, nvidia-coproration on Android.
'nvidia-corporation': 'nvidia',
}
ENTIRE_TAG_REPLACEMENTS = {
# Includes a Vulkan and LLVM version.
re.compile('google-vulkan.*swiftshader-device.*', re.IGNORECASE):
'google-vulkan',
}
INTEL_DEVICE_ID_MASK = 0xFF00
INTEL_GEN_9 = {0x1900, 0x3100, 0x3E00, 0x5900, 0x5A00, 0x9B00}
INTEL_GEN_12 = {0x4C00, 0x9A00, 0x4900, 0x4600, 0x4F00, 0x5600, 0xA700, 0x7D00}
def _ParseANGLEGpuVendorString(device_string: str) -> Optional[str]:
if not device_string:
return None
# ANGLE's device (renderer) string is of the form:
# "ANGLE (vendor_string, renderer_string, gl_version profile)"
# This function will be used to get the first value in the tuple
match = re.search(r'ANGLE \((.*), .*, .*\)', device_string)
if match:
return match.group(1)
return None
def GetANGLEGpuDeviceId(device_string: str) -> Optional[str]:
if not device_string:
return None
# ANGLE's device (renderer) string is of the form:
# "ANGLE (vendor_string, renderer_string, gl_version profile)"
# This function will be used to get the second value in the tuple
match = re.search(r'ANGLE \(.*, (.*), .*\)', device_string)
if match:
return match.group(1)
return None
def GetGpuVendorString(gpu_info: Optional[tgi.GPUInfo], index: int) -> str:
if gpu_info:
primary_gpu = gpu_info.devices[index]
if primary_gpu:
vendor_string = primary_gpu.vendor_string
angle_vendor_string = _ParseANGLEGpuVendorString(
primary_gpu.device_string)
vendor_id = primary_gpu.vendor_id
try:
vendor_id = constants.GpuVendor(vendor_id)
return vendor_id.name.lower()
except ValueError:
# Hit if vendor_id is not a known vendor.
pass
if angle_vendor_string:
return angle_vendor_string.lower()
if vendor_string:
return vendor_string.split(' ')[0].lower()
return 'unknown_gpu'
def GetGpuDeviceId(gpu_info: Optional[tgi.GPUInfo],
index: int) -> Union[int, str]:
if gpu_info:
primary_gpu = gpu_info.devices[index]
if primary_gpu:
return (primary_gpu.device_id
or GetANGLEGpuDeviceId(primary_gpu.device_string)
or primary_gpu.device_string)
return 0
def IsIntel(vendor_id: int) -> bool:
return vendor_id == constants.GpuVendor.INTEL
# Intel GPU architectures
def IsIntelGen9(gpu_device_id: int) -> bool:
return gpu_device_id & INTEL_DEVICE_ID_MASK in INTEL_GEN_9
def IsIntelGen12(gpu_device_id: int) -> bool:
return gpu_device_id & INTEL_DEVICE_ID_MASK in INTEL_GEN_12
def GetGpuDriverVendor(gpu_info: Optional[tgi.GPUInfo]) -> Optional[str]:
if gpu_info:
primary_gpu = gpu_info.devices[0]
if primary_gpu:
return primary_gpu.driver_vendor
return None
def GetGpuDriverVersion(gpu_info: Optional[tgi.GPUInfo]) -> Optional[str]:
if gpu_info:
primary_gpu = gpu_info.devices[0]
if primary_gpu:
return primary_gpu.driver_version
return None
def GetANGLERenderer(gpu_info: Optional[tgi.GPUInfo]) -> str:
retval = 'angle-disabled'
if gpu_info and gpu_info.aux_attributes:
gl_renderer = gpu_info.aux_attributes.get('gl_renderer')
if gl_renderer and 'ANGLE' in gl_renderer:
if 'Direct3D11' in gl_renderer:
retval = 'angle-d3d11'
elif 'Direct3D9' in gl_renderer:
retval = 'angle-d3d9'
elif 'OpenGL ES' in gl_renderer:
retval = 'angle-opengles'
elif 'OpenGL' in gl_renderer:
retval = 'angle-opengl'
elif 'Metal' in gl_renderer:
retval = 'angle-metal'
# SwiftShader first because it also contains Vulkan
elif 'SwiftShader' in gl_renderer:
retval = 'angle-swiftshader'
elif 'Vulkan' in gl_renderer:
retval = 'angle-vulkan'
return retval
def GetCommandDecoder(gpu_info: Optional[tgi.GPUInfo]) -> str:
if gpu_info and gpu_info.aux_attributes and \
gpu_info.aux_attributes.get('passthrough_cmd_decoder', False):
return 'passthrough'
return 'no_passthrough'
def GetSkiaGraphiteStatus(gpu_info: Optional[tgi.GPUInfo]) -> str:
if gpu_info and gpu_info.feature_status and gpu_info.feature_status.get(
'skia_graphite') == 'enabled_on':
return 'graphite-enabled'
return 'graphite-disabled'
def GetSkiaRenderer(gpu_info: Optional[tgi.GPUInfo]) -> str:
retval = 'renderer-software'
if gpu_info:
gpu_feature_status = gpu_info.feature_status
skia_renderer_enabled = (
gpu_feature_status
and gpu_feature_status.get('gpu_compositing') == 'enabled')
if skia_renderer_enabled:
if HasVulkanSkiaRenderer(gpu_feature_status):
retval = 'renderer-skia-vulkan'
# The check for GL must come after Vulkan since the 'opengl' feature can
# be enabled for WebGL and interop even if SkiaRenderer is using Vulkan.
elif HasGlSkiaRenderer(gpu_feature_status):
retval = 'renderer-skia-gl'
return retval
def GetDisplayServer(browser_type: str) -> Optional[str]:
# Browser types run on a remote device aren't Linux, but the host running
# this code uses Linux, so return early to avoid erroneously reporting a
# display server.
if browser_type in REMOTE_BROWSER_TYPES:
return None
if host_information.IsLinux():
if 'WAYLAND_DISPLAY' in os.environ:
return 'display-server-wayland'
return 'display-server-x'
return None
def GetOOPCanvasStatus(gpu_info: Optional[tgi.GPUInfo]) -> str:
if gpu_info and gpu_info.feature_status and gpu_info.feature_status.get(
'canvas_oop_rasterization') == 'enabled_on':
return 'oop-c'
return 'no-oop-c'
def GetAsanStatus(gpu_info: Optional[tgi.GPUInfo]) -> str:
if gpu_info and gpu_info.aux_attributes.get('is_asan', False):
return 'asan'
return 'no-asan'
def GetTargetCpuStatus(gpu_info: Optional[tgi.GPUInfo]) -> str:
suffix = 'unknown'
if gpu_info:
suffix = gpu_info.aux_attributes.get('target_cpu_bits', 'unknown')
return 'target-cpu-%s' % suffix
def GetClangCoverage(gpu_info: Optional[tgi.GPUInfo]) -> str:
if gpu_info and gpu_info.aux_attributes.get('is_clang_coverage', False):
return 'clang-coverage'
return 'no-clang-coverage'
def HasGlSkiaRenderer(gpu_feature_status: Dict[str, str]) -> bool:
return (bool(gpu_feature_status)
and gpu_feature_status.get('opengl') == 'enabled_on')
def HasVulkanSkiaRenderer(gpu_feature_status: Dict[str, str]) -> bool:
return (bool(gpu_feature_status)
and gpu_feature_status.get('vulkan') == 'enabled_on')
def ReplaceTags(tags: List[str]) -> List[str]:
"""Replaces certain strings in tags to make them consistent across platforms.
Args:
tags: A list of strings containing expectation tags.
Returns:
|tags| but potentially with some elements replaced.
"""
replaced_tags = []
for t in tags:
continue_to_next_tag = False
for regex, replacement in ENTIRE_TAG_REPLACEMENTS.items():
if regex.match(t):
replaced_tags.append(replacement)
continue_to_next_tag = True
break
if continue_to_next_tag:
continue
for original, replacement in TAG_SUBSTRING_REPLACEMENTS.items():
if original in t:
replaced_tags.append(t.replace(original, replacement))
continue_to_next_tag = True
break
if continue_to_next_tag:
continue
replaced_tags.append(t)
return replaced_tags
# used by unittests to create a mock arguments object
def GetMockArgs(webgl_version: str = '1.0.0') -> mock.MagicMock:
args = mock.MagicMock()
args.webgl_conformance_version = webgl_version
args.webgl2_only = False
# for power_measurement_integration_test.py, .url has to be None to
# generate the correct test lists for bots.
args.url = None
args.duration = 10
args.delay = 10
args.resolution = 100
args.fullscreen = False
args.underlay = False
args.logdir = '/tmp'
args.repeat = 1
args.outliers = 0
args.bypass_ipg = False
args.expected_vendor_id = 0
args.expected_device_id = 0
args.browser_options = []
args.use_worker = 'none'
return args
def MatchDriverTag(tag: str) -> Optional[Match[str]]:
return DRIVER_TAG_MATCHER.match(tag.lower())
# No good way to reduce the number of local variables, particularly since each
# argument is also considered a local. Also no good way to reduce the number of
# branches without harming readability.
# pylint: disable=too-many-locals,too-many-branches
def EvaluateVersionComparison(version: str,
operation: str,
ref_version: str,
os_name: Optional[str] = None,
driver_vendor: Optional[str] = None) -> bool:
def parse_version(ver: str) -> Union[Tuple[int, str], Tuple[None, None]]:
if ver.isdigit():
return int(ver), ''
for i, digit in enumerate(ver):
if not digit.isdigit():
return int(ver[:i]) if i > 0 else 0, ver[i:]
return None, None
def versions_can_be_compared(ver_list1, ver_list2):
# If either of the two versions doesn't match the Intel driver version
# schema, they should not be compared.
if len(ver_list1) != 4 or len(ver_list2) != 4:
return False
return True
ver_list1 = version.split('.')
ver_list2 = ref_version.split('.')
# On Windows, if the driver vendor is Intel, the driver version should be
# compared based on the Intel graphics driver version schema.
# https://www.intel.com/content/www/us/en/support/articles/000005654/graphics-drivers.html
if os_name == 'win' and driver_vendor == 'intel':
if not versions_can_be_compared(ver_list1, ver_list2):
return operation == 'ne'
ver_list1 = ver_list1[2:]
ver_list2 = ver_list2[2:]
for i in range(0, max(len(ver_list1), len(ver_list2))):
ver1 = ver_list1[i] if i < len(ver_list1) else '0'
ver2 = ver_list2[i] if i < len(ver_list2) else '0'
num1, suffix1 = parse_version(ver1)
num2, suffix2 = parse_version(ver2)
if num1 is None:
continue
# This comes from EXPECTATIONS_DRIVER_TAGS, so we should never fail to
# parse a version.
assert num2 is not None
if not num1 == num2:
diff = num1 - num2
elif suffix1 == suffix2:
continue
elif suffix1 > suffix2:
diff = 1
else:
diff = -1
if operation == 'eq':
return False
if operation == 'ne':
return True
if operation in ('ge', 'gt'):
return diff > 0
if operation in ('le', 'lt'):
return diff < 0
raise Exception('Invalid operation: ' + operation)
return operation in ('eq', 'ge', 'le')
# pylint: enable=too-many-locals,too-many-branches
# No good way to reduce the number of return statements to the required level
# without harming readability.
# pylint: disable=too-many-return-statements,too-many-branches
def IsDriverTagDuplicated(driver_tag1: str, driver_tag2: str) -> bool:
if driver_tag1 == driver_tag2:
return True
match = MatchDriverTag(driver_tag1)
assert match is not None
vendor1 = match.group(1)
operation1 = match.group(2)
version1 = match.group(3)
match = MatchDriverTag(driver_tag2)
assert match is not None
vendor2 = match.group(1)
operation2 = match.group(2)
version2 = match.group(3)
if vendor1 != vendor2:
return False
if operation1 == 'ne':
return not (operation2 == 'eq' and version1 == version2)
if operation2 == 'ne':
return not (operation1 == 'eq' and version1 == version2)
if operation1 == 'eq':
return EvaluateVersionComparison(version1, operation2, version2)
if operation2 == 'eq':
return EvaluateVersionComparison(version2, operation1, version1)
if operation1 in ('ge', 'gt') and operation2 in ('ge', 'gt'):
return True
if operation1 in ('le', 'lt') and operation2 in ('le', 'lt'):
return True
if operation1 == 'ge':
if operation2 == 'le':
return not EvaluateVersionComparison(version1, 'gt', version2)
if operation2 == 'lt':
return not EvaluateVersionComparison(version1, 'ge', version2)
if operation1 == 'gt':
return not EvaluateVersionComparison(version1, 'ge', version2)
if operation1 == 'le':
if operation2 == 'ge':
return not EvaluateVersionComparison(version1, 'lt', version2)
if operation2 == 'gt':
return not EvaluateVersionComparison(version1, 'le', version2)
if operation1 == 'lt':
return not EvaluateVersionComparison(version1, 'le', version2)
assert False
return False
# pylint: enable=too-many-return-statements,too-many-branches
def ExpectationsDriverTags() -> FrozenSet[str]:
return EXPECTATIONS_DRIVER_TAGS