#!/usr/bin/env python3 """Pick a Qt platform that VTK can actually draw into, and say which and why. VTK's Python wheels ship no Wayland window backend - the on-screen class is `vtkXOpenGLRenderWindow`, and EGL and OSMesa are both offscreen. Left alone, Qt6 picks Wayland whenever `WAYLAND_DISPLAY` is set while VTK goes on creating an X window, and the two disagree about who owns the surface: the process dies with `BadWindow (X_ConfigureWindow)` before anything is drawn. There is a way out, and it is worth taking rather than forcing everyone onto XWayland. Asking VTK for a `vtkGenericOpenGLRenderWindow` and Qt's widget for its `QOpenGLWidget` base puts *Qt* in charge of the GL context; VTK then draws into a context it did not create and never touches X. That works natively on Wayland, and on X11 as well, so it is the path used either way. So the order of preference is Wayland, then X11, then offscreen - and the choice is announced once, because a tool that silently moves you to XWayland is a tool that will be blamed for the missing fractional scaling. """ from __future__ import annotations import os import subprocess import sys import tempfile def configure(prefer=None, quiet=False): """Set the Qt platform and VTK widget base. Call before importing Qt. Returns the platform name chosen. `prefer` forces one; `QT_QPA_PLATFORM` set in the environment wins over both, because someone who set it meant it. """ chosen = os.environ.get('QT_QPA_PLATFORM') if not chosen: chosen = prefer or _detect() os.environ['QT_QPA_PLATFORM'] = chosen origin = 'detected' else: origin = 'from QT_QPA_PLATFORM' # Must happen before QVTKRenderWindowInteractor is imported: the module # picks its base class at import time and caches it. import vtkmodules.qt vtkmodules.qt.QVTKRWIBase = 'QOpenGLWidget' if not quiet: note = '' if origin == 'detected' and chosen == 'xcb' and os.environ.get('WAYLAND_DISPLAY'): note = ' - Wayland is present but its GL context failed a test frame' print(f'display: {chosen} ({origin}){note}', file=sys.stderr) if chosen == 'offscreen': print('display: no Wayland or X11 session - rendering to files only', file=sys.stderr) return chosen def _detect(): wayland = os.environ.get('WAYLAND_DISPLAY') and _socket_exists() x11 = bool(os.environ.get('DISPLAY')) if wayland and _probe('wayland'): return 'wayland' if x11: return 'xcb' return 'wayland' if wayland else 'offscreen' # The failure this guards against is not an exception. A Wayland session whose # GL stack cannot make the context current - WSLg with a software Mesa is the # common one - takes the whole process down with an X `BadAccess` on the first # paint, after the window is already up. Nothing in-process can catch that, so # the question gets asked in a process we can afford to lose. _PROBE = """ import os, sys import vtkmodules.qt vtkmodules.qt.QVTKRWIBase = 'QOpenGLWidget' from vtkmodules.vtkRenderingOpenGL2 import vtkGenericOpenGLRenderWindow from PySide6 import QtCore, QtWidgets import pyvistaqt, pyvista as pv app = QtWidgets.QApplication(['probe']) win = QtWidgets.QMainWindow() view = pyvistaqt.QtInteractor(win, rw=vtkGenericOpenGLRenderWindow()) win.setCentralWidget(view) win.resize(64, 64) win.show() def go(): # Exercise what the tools actually ask for. A bare cube survives GL stacks # that fall over on a textured, depth-peeled scene, and a probe that passes # where the app crashes is worse than no probe at all. import numpy as np view.enable_depth_peeling(number_of_peels=8, occlusion_ratio=0.0) mesh = pv.Cube() mesh.active_texture_coordinates = np.random.rand(mesh.n_points, 2).astype(np.float32) tex = pv.Texture(np.random.randint(0, 255, (8, 8, 4), dtype=np.uint8)) tex.SetInterpolate(False) view.add_mesh(mesh, texture=tex) view.add_light(pv.Light(position=(1, 1, 1), light_type='scene light')) view.render() view.screenshot(sys.argv[1] + '.png') open(sys.argv[1], 'w').write('ok') app.quit() QtCore.QTimer.singleShot(0, go) app.exec() """ def _probe(platform, timeout=25): """Does a real VTK viewport survive its first frame on this platform?""" if os.environ.get('MIAPI_TOOLS_NO_PROBE'): return False with tempfile.TemporaryDirectory() as tmp: flag = os.path.join(tmp, 'flag') env = dict(os.environ, QT_QPA_PLATFORM=platform) env.pop('MIAPI_TOOLS_NO_PROBE', None) try: subprocess.run([sys.executable, '-c', _PROBE, flag], env=env, timeout=timeout, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) except (subprocess.TimeoutExpired, OSError): return False return os.path.exists(flag) def _socket_exists(): """A `WAYLAND_DISPLAY` with no socket behind it is a stale export.""" name = os.environ.get('WAYLAND_DISPLAY', '') if os.path.isabs(name): return os.path.exists(name) runtime = os.environ.get('XDG_RUNTIME_DIR') return bool(runtime) and os.path.exists(os.path.join(runtime, name)) def render_window(): """The render window that lets Qt own the context. Import Qt first.""" from vtkmodules.vtkRenderingOpenGL2 import vtkGenericOpenGLRenderWindow return vtkGenericOpenGLRenderWindow()