create-mekanism-modular/tools/guiplatform.py

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()