138 lines
5.3 KiB
Python
138 lines
5.3 KiB
Python
#!/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()
|