|
17 | 17 | # You should have received a copy of the GNU General Public License |
18 | 18 | # along with Checkbox. If not, see <http://www.gnu.org/licenses/>. |
19 | 19 |
|
20 | | -import subprocess |
21 | | -import re |
| 20 | +import shutil |
| 21 | +import subprocess as sp |
| 22 | +import typing as T |
22 | 23 | import os |
| 24 | +import platform |
| 25 | +import argparse |
| 26 | +from pathlib import Path |
23 | 27 |
|
24 | 28 |
|
25 | | -class GLSupport: |
26 | | - """ |
27 | | - This is a simple class to use unity_support_test to verify |
28 | | - OpenGL is supported or not |
29 | | - """ |
| 29 | +# Checkbox could run in a snap container, so we need to prepend this root path |
| 30 | +try: |
| 31 | + CHECKBOX_RUNTIME = Path(os.environ["CHECKBOX_RUNTIME"]) |
| 32 | +except KeyError: # from indexing os.environ |
| 33 | + CHECKBOX_RUNTIME = None |
| 34 | +GLMARK2_DATA_PATH = Path("/usr/share/glmark2") |
30 | 35 |
|
31 | | - def remove_color_code(self, string: str) -> str: |
| 36 | + |
| 37 | +class GLSupportTester: |
| 38 | + |
| 39 | + def pick_glmark2_executable( |
| 40 | + self, xdg_session_type: str, cpu_arch: str |
| 41 | + ) -> str: |
| 42 | + """ |
| 43 | + Pure function that picks a glmark2 executable based on xdg_session_type |
| 44 | + and cpu arch |
| 45 | +
|
| 46 | + :param xdg_session_type: the $XDG_SESSION_TYPE variable |
| 47 | + :param cpu_arch: the `uname -m` value like x86_64 |
| 48 | + :return: glmark2 command to use. Caller is responsible for checking if |
| 49 | + the command exists |
| 50 | + """ |
| 51 | + if cpu_arch in ("x86_64", "amd64"): |
| 52 | + # x86 DUTs should run the version that uses the full opengl api |
| 53 | + glmark2_executable = "glmark2" |
| 54 | + else: |
| 55 | + # default to es2 as the common denominator |
| 56 | + # TODO: explicitly check for aarch64? |
| 57 | + glmark2_executable = "glmark2-es2" |
| 58 | + |
| 59 | + if xdg_session_type == "wayland": |
| 60 | + glmark2_executable += "-wayland" |
| 61 | + # if x11, don't add anything |
| 62 | + return glmark2_executable |
| 63 | + |
| 64 | + def gl_renderer_str_is_hardware_renderer(self, gl_renderer: str) -> bool: |
| 65 | + """Checks if gl_renderer is produced by a hardware renderer. |
| 66 | +
|
| 67 | + This uses the same logic as unity_support_test. Details: |
| 68 | + https://github.com/canonical/checkbox/issues/1630#issuecomment-2540843110 |
| 69 | +
|
| 70 | + :param gl_renderer: the GL_RENDERER string. |
| 71 | + https://registry.khronos.org/OpenGL-Refpages/gl4/html/glGetString.xhtml |
| 72 | + :return: whether GL_RENDERER is produced by a hardware renderer |
| 73 | + """ |
| 74 | + # These 2 values are carried over from unity_support_test |
| 75 | + # never seen this before on devices after ubuntu 16 |
| 76 | + if gl_renderer in ("Software Rasterizer", "Mesa X11"): |
| 77 | + return False |
| 78 | + # https://docs.mesa3d.org/envvars.html#envvar-GALLIUM_DRIVER |
| 79 | + # it's almost always the 'llvmpipe' case if we find software rendering |
| 80 | + if "llvmpipe" in gl_renderer or "softpipe" in gl_renderer: |
| 81 | + return False |
| 82 | + |
| 83 | + return True |
| 84 | + |
| 85 | + def extract_gl_variable( |
| 86 | + self, |
| 87 | + glmark2_validate_output: str, |
| 88 | + gl_variable_name: "T.Literal['GL_VERSION', 'GL_RENDERER']", |
| 89 | + ) -> str: |
| 90 | + """Attempts to extract the specified gl variable from |
| 91 | + `glmark2 --validate`'s output |
| 92 | +
|
| 93 | + :param glmark2_validate_output: stdout of `glmark2 --validate` |
| 94 | + :param gl_variable_name: the variable to get |
| 95 | + :raises SystemExit: when the value of this variable doesn't appear in |
| 96 | + glmark2_validate_output |
| 97 | + :return: value of gl_variable_name, trimmed |
| 98 | + """ |
| 99 | + gl_renderer_line = None # type: str | None |
| 100 | + for line in glmark2_validate_output.splitlines(): |
| 101 | + if gl_variable_name in line: |
| 102 | + gl_renderer_line = line.strip() |
| 103 | + break |
| 104 | + |
| 105 | + if gl_renderer_line is None: |
| 106 | + raise SystemExit( |
| 107 | + "{} was not in glmark2's output".format(gl_variable_name) |
| 108 | + ) |
| 109 | + |
| 110 | + return gl_renderer_line.split(":")[-1].strip() |
| 111 | + |
| 112 | + def call_glmark2_validate( |
| 113 | + self, glmark2_executable_override: "str | None" = None |
| 114 | + ) -> str: |
32 | 115 | """ |
33 | | - Use to make the color code removing could be unit tested |
| 116 | + Calls 'glmark2 --validate --offscreen' with the symlink hack, |
| 117 | + but allow errors to be thrown unlike reboot_check_test.py |
34 | 118 |
|
35 | | - :param string: the string that you would like to remove color code |
| 119 | + :raises SystemExit: when XDG_SESSION_TYPE is not x11/wayland |
| 120 | + :return: stdout of `glmark2 --validate` |
36 | 121 | """ |
37 | 122 |
|
38 | | - return re.sub(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])", "", string) |
| 123 | + XDG_SESSION_TYPE = os.environ.get("XDG_SESSION_TYPE") |
| 124 | + if XDG_SESSION_TYPE not in ("x11", "wayland"): |
| 125 | + # usually it's tty if we get here, |
| 126 | + # happens when gnome failed to start or not using graphical session |
| 127 | + raise SystemExit( |
| 128 | + "Unsupported session type: '{}'. ".format(XDG_SESSION_TYPE) |
| 129 | + + "Expected either 'x11' or 'wayland'" |
| 130 | + ) |
| 131 | + |
| 132 | + print("XDG_SESSION type used by the desktop is:", XDG_SESSION_TYPE) |
| 133 | + |
| 134 | + if glmark2_executable_override is not None: |
| 135 | + if shutil.which(glmark2_executable_override) is None: |
| 136 | + raise SystemExit( |
| 137 | + "Override command '{}' doesn't exist".format( |
| 138 | + glmark2_executable_override |
| 139 | + ) |
| 140 | + ) |
| 141 | + glmark2_executable = glmark2_executable_override |
| 142 | + else: |
| 143 | + glmark2_executable = self.pick_glmark2_executable( |
| 144 | + XDG_SESSION_TYPE, platform.uname().machine |
| 145 | + ) |
39 | 146 |
|
40 | | - def is_support_opengl(self): |
41 | | - cr = os.getenv("CHECKBOX_RUNTIME", default="") |
42 | | - cmd = [ |
43 | | - "{}/usr/lib/nux/unity_support_test".format(cr), |
44 | | - "-p", |
45 | | - ] |
46 | 147 | try: |
47 | | - rv = subprocess.run( |
48 | | - cmd, |
| 148 | + if CHECKBOX_RUNTIME and not os.path.exists(GLMARK2_DATA_PATH): |
| 149 | + # the official way to specify the location of the data files |
| 150 | + # is "--data-path path/to/data/files" |
| 151 | + # but 16, 18, 20 doesn't have this option |
| 152 | + # and the /usr/share/glmark2 path is hard-coded inside glmark2 |
| 153 | + # by the GLMARK_DATA_PATH build macro |
| 154 | + src = CHECKBOX_RUNTIME / GLMARK2_DATA_PATH |
| 155 | + dst = GLMARK2_DATA_PATH |
| 156 | + print( |
| 157 | + "[ DEBUG ] Symlinking glmark2 data dir ({} -> {})".format( |
| 158 | + src, dst |
| 159 | + ) |
| 160 | + ) |
| 161 | + os.symlink(src, dst, target_is_directory=True) |
| 162 | + # override is needed for snaps on classic ubuntu |
| 163 | + # to allow the glmark2 command itself to be discovered |
| 164 | + # in debian version of checkbox this line does nothing |
| 165 | + glmark2_output = sp.check_output( |
| 166 | + # all glmark2 programs share the same args |
| 167 | + [glmark2_executable, "--off-screen", "--validate"], |
49 | 168 | universal_newlines=True, |
50 | | - stdout=subprocess.PIPE, |
51 | | - stderr=subprocess.STDOUT, |
| 169 | + # be more relaxed on this timeout in case |
| 170 | + # the device needs a lot of time to wake up the GPU |
| 171 | + timeout=120, |
52 | 172 | ) |
53 | | - except (subprocess.CalledProcessError, FileNotFoundError) as e: |
54 | | - raise SystemExit("running cmd:[{}] fail:{}".format(cmd, repr(e))) |
55 | | - print(self.remove_color_code(rv.stdout)) |
56 | | - if rv.returncode != 0: |
57 | | - raise SystemExit("Some OpenGL functions might not be supported") |
| 173 | + return glmark2_output |
| 174 | + finally: |
| 175 | + # immediately cleanup |
| 176 | + if CHECKBOX_RUNTIME and os.path.islink(GLMARK2_DATA_PATH): |
| 177 | + print("[ DEBUG ] Un-symlinking glmark2 data") |
| 178 | + os.unlink(GLMARK2_DATA_PATH) |
| 179 | + |
| 180 | + |
| 181 | +def remove_prefix(s: str, prefix: str) -> str: |
| 182 | + """3.8 and older doesn't have <str>.removeprefix()""" |
| 183 | + if s.startswith(prefix): |
| 184 | + return s[len(prefix) :] |
| 185 | + return s |
| 186 | + |
| 187 | + |
| 188 | +def parse_args(): |
| 189 | + parser = argparse.ArgumentParser() |
| 190 | + parser.add_argument( |
| 191 | + "--glmark2-override", |
| 192 | + help=( |
| 193 | + "Override the glmark2 executable to use, " |
| 194 | + "even if it might be unsupported on this platform" |
| 195 | + ), |
| 196 | + choices=( |
| 197 | + "glmark2", |
| 198 | + "glmark2-wayland", |
| 199 | + "glmark2-es2", |
| 200 | + "glmark2-es2-wayland", |
| 201 | + ), |
| 202 | + required=False, |
| 203 | + ) |
| 204 | + return parser.parse_args() |
| 205 | + |
| 206 | + |
| 207 | +def main() -> None: |
| 208 | + args = parse_args() |
| 209 | + tester = GLSupportTester() |
| 210 | + glmark2_output = tester.call_glmark2_validate(args.glmark2_override) |
| 211 | + |
| 212 | + gl_version_str = ( |
| 213 | + remove_prefix( |
| 214 | + tester.extract_gl_variable( |
| 215 | + glmark2_output, "GL_VERSION" |
| 216 | + ), # 4.6 (Compatibility Profile) Mesa 25.0.7-0ubuntu0.25.04.1 |
| 217 | + "OpenGL ES", # OpenGL ES 3.0 Mesa 18.0.5 |
| 218 | + ) |
| 219 | + .strip() # technically not needed but might as well be careful |
| 220 | + .split()[0] # 4.6 |
| 221 | + .strip() # final cleanup |
| 222 | + ) |
| 223 | + # Mesa Intel(R) Graphics (LNL) |
| 224 | + gl_renderer = tester.extract_gl_variable(glmark2_output, "GL_RENDERER") |
| 225 | + |
| 226 | + print("GL_VERSION:", gl_version_str) |
| 227 | + print("GL_RENDERER:", gl_renderer) |
| 228 | + |
| 229 | + # check if it's newer than 3.0 |
| 230 | + # we don't have to check the minor version |
| 231 | + # since it would be just comparing a positive int to 0 |
| 232 | + if int(gl_version_str.split(".")[0]) < 3: |
| 233 | + raise SystemExit( |
| 234 | + "The minimum required OpenGL version is 3.0, but got {}".format( |
| 235 | + gl_version_str |
| 236 | + ) |
| 237 | + ) |
| 238 | + |
| 239 | + if not tester.gl_renderer_str_is_hardware_renderer(gl_renderer): |
| 240 | + raise SystemExit( |
| 241 | + "This machine is not using a hardware renderer. " |
| 242 | + + "Got GL_RENDERER={}".format(gl_renderer) |
| 243 | + ) |
| 244 | + |
| 245 | + print( |
| 246 | + "OK! This machine meets the minimum OpenGL version requirement", |
| 247 | + "({} >= 3.0)".format(gl_version_str), |
| 248 | + "and is using a hardware renderer for {} apps".format( |
| 249 | + os.environ["XDG_SESSION_TYPE"] |
| 250 | + ), # wayland working doesn't necessarily imply Xwayland working |
| 251 | + ) |
58 | 252 |
|
59 | 253 |
|
60 | 254 | if __name__ == "__main__": |
61 | | - GLSupport().is_support_opengl() |
| 255 | + main() |
0 commit comments