From 6aeb3d81859a53b3ac779b881d6ab0e8035939fa Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 29 May 2020 20:53:21 +0200 Subject: Refactor how we get OpenGL info This allows us to get the version string in addition to the vendor. We also show that version string in the version info output. See #5313 --- qutebrowser/misc/backendproblem.py | 16 ++------ qutebrowser/utils/utils.py | 15 +++++++ qutebrowser/utils/version.py | 70 ++++++++++++++++++++++++++++---- tests/conftest.py | 6 +-- tests/unit/utils/test_version.py | 82 +++++++++++++++++++++++++++++++++++--- 5 files changed, 159 insertions(+), 30 deletions(-) diff --git a/qutebrowser/misc/backendproblem.py b/qutebrowser/misc/backendproblem.py index 2ed273547..73044cd99 100644 --- a/qutebrowser/misc/backendproblem.py +++ b/qutebrowser/misc/backendproblem.py @@ -23,8 +23,6 @@ import os import sys import functools import html -import ctypes -import ctypes.util import enum import shutil import typing @@ -201,19 +199,10 @@ class _BackendProblemChecker: def _nvidia_shader_workaround(self) -> None: """Work around QOpenGLShaderProgram issues. - NOTE: This needs to be called before _handle_nouveau_graphics, or some - setups will segfault in version.opengl_vendor(). - See https://bugs.launchpad.net/ubuntu/+source/python-qt4/+bug/941826 """ self._assert_backend(usertypes.Backend.QtWebEngine) - - if os.environ.get('QUTE_SKIP_LIBGL_WORKAROUND'): - return - - libgl = ctypes.util.find_library("GL") - if libgl is not None: - ctypes.CDLL(libgl, mode=ctypes.RTLD_GLOBAL) + utils.libgl_workaround() def _handle_nouveau_graphics(self) -> None: """Force software rendering when using the Nouveau driver. @@ -231,7 +220,8 @@ class _BackendProblemChecker: if qtutils.version_check('5.10', compiled=False): return - if version.opengl_vendor() != 'nouveau': + opengl_info = version.opengl_info() + if opengl_info is None or opengl_info.vendor != 'nouveau': return if (os.environ.get('LIBGL_ALWAYS_SOFTWARE') == '1' or diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py index 368cb0ab6..39d46add8 100644 --- a/qutebrowser/utils/utils.py +++ b/qutebrowser/utils/utils.py @@ -36,6 +36,8 @@ import shlex import glob import mimetypes import typing +import ctypes +import ctypes.util from PyQt5.QtCore import QUrl from PyQt5.QtGui import QColor, QClipboard, QDesktopServices @@ -776,3 +778,16 @@ def ceil_log(number: int, base: int) -> int: result += 1 accum *= base return result + + +def libgl_workaround(): + """Work around QOpenGLShaderProgram issues, especially for Nvidia. + + See https://bugs.launchpad.net/ubuntu/+source/python-qt4/+bug/941826 + """ + if os.environ.get('QUTE_SKIP_LIBGL_WORKAROUND'): + return + + libgl = ctypes.util.find_library("GL") + if libgl is not None: + ctypes.CDLL(libgl, mode=ctypes.RTLD_GLOBAL) diff --git a/qutebrowser/utils/version.py b/qutebrowser/utils/version.py index 1ad8b22cf..8dc4ec593 100644 --- a/qutebrowser/utils/version.py +++ b/qutebrowser/utils/version.py @@ -31,6 +31,7 @@ import enum import datetime import getpass import typing +import functools import attr import pkg_resources @@ -442,8 +443,8 @@ def version() -> str: if qapp: style = qapp.style() lines.append('Style: {}'.format(style.metaObject().className())) - platform_name = qapp.platformName() - lines.append('Platform plugin: {}'.format(platform_name)) + lines.append('Platform plugin: {}'.format(qapp.platformName())) + lines.append('OpenGL: {}'.format(opengl_info())) importpath = os.path.dirname(os.path.abspath(qutebrowser.__file__)) @@ -487,7 +488,55 @@ def version() -> str: return '\n'.join(lines) -def opengl_vendor() -> typing.Optional[str]: # pragma: no cover +@attr.s +class OpenGLInfo: + + """Information about the OpenGL setup in use.""" + + # If we're using OpenGL ES. If so, no further information is available. + gles = attr.ib(False) # type: bool + + # The name of the vendor. Examples: + # - nouveau + # - "Intel Open Source Technology Center", "Intel", "Intel Inc." + vendor = attr.ib(None) # type: typing.Optional[str] + + # The OpenGL version as a string. See tests for examples. + version_str = attr.ib(None) # type: typing.Optional[str] + + # The parsed version as a (major, minor) tuple of ints + version = attr.ib(None) # type: typing.Optional[typing.Tuple[int, ...]] + + # The vendor specific information following the version number + vendor_specific = attr.ib(None) # type: typing.Optional[str] + + def __str__(self) -> str: + if self.gles: + return 'OpenGL ES' + return '{}, {}'.format(self.vendor, self.version_str) + + @classmethod + def parse(cls, *, vendor: str, version: str) -> 'OpenGLInfo': + if ' ' not in version: + log.misc.warning("Failed to parse OpenGL version (missing space): " + "{}".format(version)) + return cls(vendor=vendor, version_str=version) + + num_str, vendor_specific = version.split(' ', maxsplit=1) + + try: + parsed_version = tuple(int(i) for i in num_str.split('.')) + except ValueError: + log.misc.warning("Failed to parse OpenGL version (parsing int): " + "{}".format(version)) + return cls(vendor=vendor, version_str=version) + + return cls(vendor=vendor, version_str=version, + version=parsed_version, vendor_specific=vendor_specific) + + +@functools.lru_cache(maxsize=1) +def opengl_info() -> typing.Optional[OpenGLInfo]: # pragma: no cover """Get the OpenGL vendor used. This returns a string such as 'nouveau' or @@ -496,10 +545,14 @@ def opengl_vendor() -> typing.Optional[str]: # pragma: no cover """ assert QApplication.instance() - override = os.environ.get('QUTE_FAKE_OPENGL_VENDOR') + # Some setups can segfault in here if we don't do this. + utils.libgl_workaround() + + override = os.environ.get('QUTE_FAKE_OPENGL') if override is not None: log.init.debug("Using override {}".format(override)) - return override + vendor, version = override.split(', ', maxsplit=1) + return OpenGLInfo.parse(vendor=vendor, version=version) old_context = typing.cast(typing.Optional[QOpenGLContext], QOpenGLContext.currentContext()) @@ -522,7 +575,7 @@ def opengl_vendor() -> typing.Optional[str]: # pragma: no cover try: if ctx.isOpenGLES(): # Can't use versionFunctions there - return None + return OpenGLInfo(gles=True) vp = QOpenGLVersionProfile() vp.setVersion(2, 0) @@ -537,7 +590,10 @@ def opengl_vendor() -> typing.Optional[str]: # pragma: no cover log.init.debug("Getting version functions failed!") return None - return vf.glGetString(vf.GL_VENDOR) + vendor = vf.glGetString(vf.GL_VENDOR) + version = vf.glGetString(vf.GL_VERSION) + + return OpenGLInfo.parse(vendor=vendor, version=version) finally: ctx.doneCurrent() if old_context and old_surface: diff --git a/tests/conftest.py b/tests/conftest.py index c6b6c2efc..e698bde74 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -25,8 +25,6 @@ import os import sys import warnings import pathlib -import ctypes -import ctypes.util import pytest import hypothesis @@ -258,9 +256,7 @@ def set_backend(monkeypatch, request): @pytest.fixture(autouse=True, scope='session') def apply_libgl_workaround(): """Make sure we load libGL early so QtWebEngine tests run properly.""" - libgl = ctypes.util.find_library("GL") - if libgl is not None: - ctypes.CDLL(libgl, mode=ctypes.RTLD_GLOBAL) + utils.libgl_workaround() @pytest.fixture(autouse=True) diff --git a/tests/unit/utils/test_version.py b/tests/unit/utils/test_version.py index 0a3c5e4aa..903585b62 100644 --- a/tests/unit/utils/test_version.py +++ b/tests/unit/utils/test_version.py @@ -35,6 +35,8 @@ import datetime import attr import pkg_resources import pytest +import hypothesis +import hypothesis.strategies import qutebrowser from qutebrowser.config import config @@ -956,11 +958,15 @@ def test_version_output(params, stubs, monkeypatch, config_stub): 'config.instance.yaml_loaded': params.autoconfig_loaded, } + version.opengl_info.cache_clear() + monkeypatch.setenv('QUTE_FAKE_OPENGL', 'VENDOR, 1.0 VERSION') + substitutions = { 'git_commit': '\nGit commit: GIT COMMIT' if params.git_commit else '', 'style': '\nStyle: STYLE' if params.qapp else '', 'platform_plugin': ('\nPlatform plugin: PLATFORM' if params.qapp else ''), + 'opengl': '\nOpenGL: VENDOR, 1.0 VERSION' if params.qapp else '', 'qt': 'QT VERSION', 'frozen': str(params.frozen), 'import_path': import_path, @@ -1026,7 +1032,7 @@ def test_version_output(params, stubs, monkeypatch, config_stub): pdf.js: PDFJS VERSION sqlite: SQLITE VERSION QtNetwork SSL: {ssl} - {style}{platform_plugin} + {style}{platform_plugin}{opengl} Platform: PLATFORM, ARCHITECTURE{linuxdist} Frozen: {frozen} Imported from {import_path} @@ -1045,10 +1051,76 @@ def test_version_output(params, stubs, monkeypatch, config_stub): assert version.version() == expected -def test_opengl_vendor(qapp): - """Simply call version.opengl_vendor() and see if it doesn't crash.""" - pytest.importorskip("PyQt5.QtOpenGL") - return version.opengl_vendor() +class TestOpenGLInfo: + + @pytest.fixture(autouse=True) + def cache_clear(self): + """Clear the lru_cache between tests.""" + version.opengl_info.cache_clear() + + def test_func(self, qapp): + """Simply call version.opengl_info() and see if it doesn't crash.""" + pytest.importorskip("PyQt5.QtOpenGL") + version.opengl_info() + + def test_func_fake(self, qapp, monkeypatch): + monkeypatch.setenv('QUTE_FAKE_OPENGL', 'Outtel Inc., 3.0 Messiah 20.0') + info = version.opengl_info() + assert info.vendor == 'Outtel Inc.' + assert info.version_str == '3.0 Messiah 20.0' + assert info.version == (3, 0) + assert info.vendor_specific == 'Messiah 20.0' + + @pytest.mark.parametrize('version_str, reason', [ + ('blah', 'missing space'), + ('2,x blah', 'parsing int'), + ]) + def test_parse_invalid(self, caplog, version_str, reason): + with caplog.at_level(logging.WARNING): + info = version.OpenGLInfo.parse(vendor="vendor", + version=version_str) + + assert info.version is None + assert info.vendor_specific is None + assert info.vendor == 'vendor' + assert info.version_str == version_str + + msg = "Failed to parse OpenGL version ({}): {}".format( + reason, version_str) + assert caplog.messages == [msg] + + @hypothesis.given(vendor=hypothesis.strategies.text(), + version_str=hypothesis.strategies.text()) + def test_parse_hypothesis(self, caplog, vendor, version_str): + with caplog.at_level(logging.WARNING): + info = version.OpenGLInfo.parse(vendor=vendor, version=version_str) + + assert info.vendor == vendor + assert info.version_str == version_str + assert vendor in str(info) + assert version_str in str(info) + + if info.version is not None: + reconstructed = ' '.join(['.'.join(str(part) + for part in info.version), + info.vendor_specific]) + assert reconstructed == info.version_str + + @pytest.mark.parametrize('version_str, expected', [ + ("2.1 INTEL-10.36.26", (2, 1)), + ("4.6 (Compatibility Profile) Mesa 20.0.7", (4, 6)), + ("3.0 Mesa 20.0.7", (3, 0)), + ("3.0 Mesa 20.0.6", (3, 0)), + # Not from the wild, but can happen according to standards + ("3.0.2 Mesa 20.0.6", (3, 0, 2)), + ]) + def test_version(self, version_str, expected): + info = version.OpenGLInfo.parse(vendor='vendor', version=version_str) + assert info.version == expected + + def test_str_gles(self): + info = version.OpenGLInfo(gles=True) + assert str(info) == 'OpenGL ES' @pytest.fixture -- cgit v1.2.3-54-g00ecf