Whether this machine can draw the 3-D view, asked before the first
one is built, and the one place a 3-D view is built.
The 3-D view is VTK, and VTK draws with OpenGL 3.2 or later. On Windows
it asks the system's opengl32.dll for a pixel format. A machine with no
graphics driver (a Remote Desktop session, a virtual machine without
one, a server) has only Windows' own OpenGL 1.1, and VTK, refused, looks
for Mesa's off-screen library and dies with an access violation before
anything is drawn. So the same opengl32.dll is asked first, in the
same way, and a machine that cannot draw is told what is missing.
It is asked when the first 3-D view is about to be built, not at
launch: a window that never shows a 3-D view never pays for the check
or for importing VTK. Every 3-D view is built by three_d_view, which
asks ready first; ready runs the check once per process and keeps
its answer, so a machine that cannot draw is told once and every 3-D
view after is refused without building anything.
The question is put to Windows directly, not through Qt: Qt can fall
back to its own software renderer where VTK cannot, and would answer
yes for a machine that VTK then crashes on. The macOS and Linux builds
are not asked. macOS always has OpenGL, and the Linux AppImage carries
Mesa.
Functions:
| Name |
Description |
judge |
Why OpenGL version from renderer cannot draw the 3-D view, or
|
windows_opengl |
(GL_VERSION, GL_RENDERER) from the OpenGL Windows gives a plain
|
missing |
Why this machine cannot draw the 3-D view, or None when it can,
|
ready |
Whether a 3-D view may be built here, asked once per process.
|
refusal |
Why 3-D views are refused here, or None: either the check found
|
require |
Raise RuntimeError, with the reason, when this machine cannot draw
|
three_d_view |
Build a 3-D view, or None when this machine cannot draw one.
|
check |
visualdynamics --check-gl: 0 and what OpenGL is here when the 3-D
|
view_format |
The OpenGL format the 3-D views draw with: desktop OpenGL 3.2,
|
one_graphics_system |
Have Qt Quick draw with OpenGL, as the 3-D views do, and every
|
say |
Show reason to the person about to see a 3-D view: on Windows
|
Functions:
judge
judge(version: str | None, renderer: str | None) -> str | None
Why OpenGL version from renderer cannot draw the 3-D view, or
None when it can.
version and renderer are the strings OpenGL reports
(GL_VERSION, GL_RENDERER), such as '4.6.0 NVIDIA 551.23' and
'1.1.0' with 'GDI Generic'; None for both when Windows offered no
OpenGL pixel format at all.
Source code in src/visualdynamics/gui/opengl.py
| def judge(version: str | None, renderer: str | None) -> str | None:
"""Why OpenGL `version` from `renderer` cannot draw the 3-D view, or
None when it can.
`version` and `renderer` are the strings OpenGL reports
(`GL_VERSION`, `GL_RENDERER`), such as '4.6.0 NVIDIA 551.23' and
'1.1.0' with 'GDI Generic'; None for both when Windows offered no
OpenGL pixel format at all.
"""
remedy = ('Install or update the graphics driver. Over Remote Desktop, '
'or in a virtual machine without a graphics driver, Windows '
'offers only its own OpenGL 1.1; placing Mesa\'s '
'opengl32.dll (llvmpipe) beside visualdynamics.exe gives '
'the 3-D view a software renderer instead.')
need = f'OpenGL {NEEDED[0]}.{NEEDED[1]}'
if version is None:
return (f'Visual Dynamics draws its 3-D view with {need} or later, '
f'and Windows offered no OpenGL here. {remedy}')
found = re.match(r'\s*(\d+)\.(\d+)', version)
if found and (int(found[1]), int(found[2])) >= NEEDED:
return None
what = version.split()[0] if version.split() else version
by = f' ({renderer})' if renderer else ''
return (f'Visual Dynamics draws its 3-D view with {need} or later, and '
f'this machine offers OpenGL {what}{by}. {remedy}')
|
windows_opengl
windows_opengl() -> tuple[str | None, str | None]
(GL_VERSION, GL_RENDERER) from the OpenGL Windows gives a plain
double-buffered window, asked through opengl32.dll as VTK asks;
(None, None) when no OpenGL pixel format is offered. Windows only.
Source code in src/visualdynamics/gui/opengl.py
| def windows_opengl() -> tuple[str | None, str | None]:
"""(GL_VERSION, GL_RENDERER) from the OpenGL Windows gives a plain
double-buffered window, asked through `opengl32.dll` as VTK asks;
(None, None) when no OpenGL pixel format is offered. Windows only."""
user32 = ctypes.WinDLL('user32')
gdi32 = ctypes.WinDLL('gdi32')
# the same search VTK's load goes through, so a Mesa opengl32.dll
# beside the exe is the one asked
opengl32 = ctypes.WinDLL('opengl32')
PixelFormat = _PixelFormat
# handles are pointer-sized; left to ctypes' default int they are cut
# to 32 bits on a 64-bit process
user32.CreateWindowExW.restype = wintypes.HWND
user32.CreateWindowExW.argtypes = [
wintypes.DWORD, wintypes.LPCWSTR, wintypes.LPCWSTR, wintypes.DWORD,
ctypes.c_int, ctypes.c_int, ctypes.c_int, ctypes.c_int,
wintypes.HWND, wintypes.HMENU, wintypes.HINSTANCE, wintypes.LPVOID]
user32.GetDC.restype = wintypes.HDC
user32.GetDC.argtypes = [wintypes.HWND]
user32.ReleaseDC.argtypes = [wintypes.HWND, wintypes.HDC]
user32.DestroyWindow.argtypes = [wintypes.HWND]
gdi32.ChoosePixelFormat.argtypes = [wintypes.HDC,
ctypes.POINTER(PixelFormat)]
gdi32.SetPixelFormat.argtypes = [wintypes.HDC, ctypes.c_int,
ctypes.POINTER(PixelFormat)]
opengl32.wglCreateContext.restype = _HGLRC
opengl32.wglCreateContext.argtypes = [wintypes.HDC]
opengl32.wglMakeCurrent.argtypes = [wintypes.HDC, _HGLRC]
opengl32.wglDeleteContext.argtypes = [_HGLRC]
opengl32.glGetString.restype = ctypes.c_char_p
opengl32.glGetString.argtypes = [ctypes.c_uint]
window = user32.CreateWindowExW(0, 'STATIC', 'OpenGL check',
0x80000000, # WS_POPUP, never shown
0, 0, 1, 1, None, None, None, None)
if not window:
return None, None
dc = user32.GetDC(window)
context = None
try:
wanted = PixelFormat()
wanted.nSize = ctypes.sizeof(PixelFormat)
wanted.nVersion = 1
# PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER
wanted.dwFlags = 0x4 | 0x20 | 0x1
wanted.cColorBits, wanted.cDepthBits, wanted.cStencilBits = 32, 24, 8
index = gdi32.ChoosePixelFormat(dc, ctypes.byref(wanted))
if not index or not gdi32.SetPixelFormat(dc, index,
ctypes.byref(wanted)):
return None, None
context = opengl32.wglCreateContext(dc)
if not context or not opengl32.wglMakeCurrent(dc, context):
return None, None
version = opengl32.glGetString(0x1F02) # GL_VERSION
renderer = opengl32.glGetString(0x1F01) # GL_RENDERER
return ((version or b'').decode(errors='replace') or None,
(renderer or b'').decode(errors='replace') or None)
finally:
if context:
opengl32.wglMakeCurrent(None, None)
opengl32.wglDeleteContext(context)
user32.ReleaseDC(window, dc)
user32.DestroyWindow(window)
|
missing
Why this machine cannot draw the 3-D view, or None when it can,
when the platform is not one that is asked (judge on Windows), or
when the probe itself failed: a check that cannot run is no reason
to refuse a 3-D view, and the failure goes to stderr.
Source code in src/visualdynamics/gui/opengl.py
| def missing() -> str | None:
"""Why this machine cannot draw the 3-D view, or None when it can,
when the platform is not one that is asked (`judge` on Windows), or
when the probe itself failed: a check that cannot run is no reason
to refuse a 3-D view, and the failure goes to stderr."""
# Asked since 2026-10-18: a release run on a driverless Windows
# runner (2026-09-01) died with 0xC0000005 before a window, and a
# user over Remote Desktop got the same crash rather than a sentence
if sys.platform != 'win32':
return None
try:
return judge(*windows_opengl())
except Exception as failure: # noqa: BLE001 — the probe, not the app
sys.stderr.write(f'the OpenGL check could not run ({failure}); '
'starting anyway\n')
return None
|
ready
Whether a 3-D view may be built here, asked once per process.
The first call runs the check (missing), and a machine that fails
it is told why (say). Every later call returns the same answer
without asking again or saying it again, so the check runs once,
before the first 3-D view, however many views are built after it.
Source code in src/visualdynamics/gui/opengl.py
| def ready() -> bool:
"""Whether a 3-D view may be built here, asked once per process.
The first call runs the check (`missing`), and a machine that fails
it is told why (`say`). Every later call returns the same answer
without asking again or saying it again, so the check runs once,
before the first 3-D view, however many views are built after it.
"""
if not _found:
# recorded before it is said: on Windows the message box runs
# an event loop of its own, which can deliver an event that asks
# for a 3-D view again, and that ask must find the answer, not
# run the check a second time
_found.append(missing())
if _found[0] is not None:
say(_found[0])
return _found[0] is None
|
refusal
Why 3-D views are refused here, or None: either the check found
nothing missing, or it has not run yet. Never runs the check.
Source code in src/visualdynamics/gui/opengl.py
| def refusal() -> str | None:
"""Why 3-D views are refused here, or None: either the check found
nothing missing, or it has not run yet. Never runs the check."""
return _found[0] if _found else None
|
require
Raise RuntimeError, with the reason, when this machine cannot draw
a 3-D view (ready). For a script that opens a 3-D window, which has
no window of its own to fall back on.
Source code in src/visualdynamics/gui/opengl.py
| def require() -> None:
"""Raise RuntimeError, with the reason, when this machine cannot draw
a 3-D view (`ready`). For a script that opens a 3-D window, which has
no window of its own to fall back on."""
if not ready():
raise RuntimeError(refusal())
|
three_d_view
three_d_view(parent: QWidget | None = None, *, offscreen: bool = False, **options: Any) -> Any
Build a 3-D view, or None when this machine cannot draw one.
Every 3-D view the application builds is made here, so the check
(ready) runs before the first of them, and VTK is first imported
here, not at launch. On screen it is a pyvistaqt QtInteractor in
parent, rendering synchronously (viz.undeferred), refusing drops
and smoothed with FXAA; with offscreen, a PyVista Plotter that
renders off screen, built with options.
Source code in src/visualdynamics/gui/opengl.py
| def three_d_view(parent: QWidget | None = None, *,
offscreen: bool = False, **options: Any) -> Any:
"""Build a 3-D view, or None when this machine cannot draw one.
Every 3-D view the application builds is made here, so the check
(`ready`) runs before the first of them, and VTK is first imported
here, not at launch. On screen it is a pyvistaqt `QtInteractor` in
`parent`, rendering synchronously (`viz.undeferred`), refusing drops
and smoothed with FXAA; with `offscreen`, a PyVista `Plotter` that
renders off screen, built with `options`.
"""
if not ready():
return None
if offscreen:
import pyvista as pv
return pv.Plotter(off_screen=True, **options)
from pyvistaqt import QtInteractor
from ..viz import undeferred
view = undeferred(QtInteractor(parent, **options))
# pyvistaqt takes drops and answers them with `pyvista.read`, so a
# .vdyn dropped on a 3-D view, aimed at the window and landing
# here, came back as "not able to be automatically read by pyvista"
# and was never imported. Qt routes drags only to widgets that say
# they take them, so refusing here sends the drop up to the window,
# which knows what a .vdyn is.
view.setAcceptDrops(False)
# cheap post-process pass: smooths the model's edges without the
# supersampling alternative, which shrinks points and lines because
# their sizes are in pixels
view.enable_anti_aliasing('fxaa')
return view
|
check
check() -> tuple[int, str]
visualdynamics --check-gl: 0 and what OpenGL is here when the 3-D
view can be drawn, 2 and the reason when it cannot, 1 and the error
when the probe itself failed. The Windows release smoke test runs
it with and without Mesa beside the exe.
Source code in src/visualdynamics/gui/opengl.py
| def check() -> tuple[int, str]:
"""`visualdynamics --check-gl`: 0 and what OpenGL is here when the 3-D
view can be drawn, 2 and the reason when it cannot, 1 and the error
when the probe itself failed. The Windows release smoke test runs
it with and without Mesa beside the exe."""
if sys.platform != 'win32':
return 0, 'not asked on this platform'
try:
version, renderer = windows_opengl()
except Exception as failure: # noqa: BLE001 — reported, not raised
return 1, f'the OpenGL check could not run: {failure}'
reason = judge(version, renderer)
if reason is not None:
return 2, reason
return 0, f'OpenGL {version} ({renderer})'
|
view_format() -> QSurfaceFormat
The OpenGL format the 3-D views draw with: desktop OpenGL 3.2,
core profile, double buffered, eight bits a color channel and for
depth, no stencil and no multisampling (VTK multisamples in its own
buffers).
It is VTK's own default for a Qt widget, the one pyvistaqt makes the
process's default when it builds a 3-D view. Each 3-D view sets the
default again, so this must agree with it in every field, or
building a view would change the format under the window.
Source code in src/visualdynamics/gui/opengl.py
| def view_format() -> QSurfaceFormat:
"""The OpenGL format the 3-D views draw with: desktop OpenGL 3.2,
core profile, double buffered, eight bits a color channel and for
depth, no stencil and no multisampling (VTK multisamples in its own
buffers).
It is VTK's own default for a Qt widget, the one pyvistaqt makes the
process's default when it builds a 3-D view. Each 3-D view sets the
default again, so this must agree with it in every field, or
building a view would change the format under the window.
"""
# Written here, not imported: pyvistaqt's is a private function
# (`pyvistaqt.rwi._default_format`) that 0.12 does not have, and
# importing it crashed the launch on a Mac with 0.12 installed
# (Brandon, 2026-10-27). tests/test_opengl.py checks this against
# pyvistaqt's whenever pyvistaqt has one.
from PySide6.QtGui import QSurfaceFormat
fmt = QSurfaceFormat()
fmt.setRenderableType(QSurfaceFormat.RenderableType.OpenGL)
fmt.setProfile(QSurfaceFormat.OpenGLContextProfile.CoreProfile)
fmt.setVersion(3, 2)
fmt.setSwapBehavior(QSurfaceFormat.SwapBehavior.DoubleBuffer)
fmt.setRedBufferSize(8)
fmt.setGreenBufferSize(8)
fmt.setBlueBufferSize(8)
fmt.setDepthBufferSize(8)
fmt.setAlphaBufferSize(8)
fmt.setStencilBufferSize(0)
fmt.setSamples(0)
return fmt
|
one_graphics_system
one_graphics_system() -> None
Have Qt Quick draw with OpenGL, as the 3-D views do, and every
OpenGL context be the one format the 3-D views need.
Qt composes each window with one graphics system. The 3-D views are
OpenGL widgets, and the report's web view is drawn by Qt Quick, which
on macOS defaults to Metal. With both in one window, the 3-D views
composed blank: every 3-D view on a Mac, with the report's web view
drawing normally. Set before the first Qt Quick window exists, which
is the first web view; a value already in the environment is left
as it is.
The format is the desktop OpenGL 3.2 core profile VTK draws with,
made the default before the first window. A 3-D view sets it as the
default when it is built, and a 3-D view is built when it is first
needed, after the window exists; Qt had by then chosen how to
compose the window for the format before, and its compositor's
shaders failed to compile against the new one.
Source code in src/visualdynamics/gui/opengl.py
| def one_graphics_system() -> None:
"""Have Qt Quick draw with OpenGL, as the 3-D views do, and every
OpenGL context be the one format the 3-D views need.
Qt composes each window with one graphics system. The 3-D views are
OpenGL widgets, and the report's web view is drawn by Qt Quick, which
on macOS defaults to Metal. With both in one window, the 3-D views
composed blank: every 3-D view on a Mac, with the report's web view
drawing normally. Set before the first Qt Quick window exists, which
is the first web view; a value already in the environment is left
as it is.
The format is the desktop OpenGL 3.2 core profile VTK draws with,
made the default before the first window. A 3-D view sets it as the
default when it is built, and a 3-D view is built when it is first
needed, after the window exists; Qt had by then chosen how to
compose the window for the format before, and its compositor's
shaders failed to compile against the new one.
"""
# A Mac running 0.1.0a74 (Brandon, 2026-10-27): importing an nc4
# printed QBackingStoreDefaultCompositor's '#version 120' shader
# refused ("version '120' is not supported", the core profile takes
# 150 and up) and the application crashed; the same import worked
# on the next launch.
from PySide6.QtGui import QSurfaceFormat
QSurfaceFormat.setDefaultFormat(view_format())
# pyvistaqt 0.13 made the 3-D view a QOpenGLWidget composed by Qt;
# 0.12's was a native window of its own, which never met Qt Quick.
# Measured on screen 2026-10-21 (Brandon: no 3-D view drew in the
# packaged app): 0.3 % of the view drawn, 21 % with this set, the
# report unchanged. Setting the widgets' backend as well
# (QT_WIDGETS_RHI_BACKEND=opengl) blanked it again.
import os
os.environ.setdefault(QUICK_BACKEND, 'opengl')
|
say
Show reason to the person about to see a 3-D view: on Windows
through its own message box, which needs nothing of Qt; elsewhere
through Qt, which this never reaches, since only Windows is asked.
Source code in src/visualdynamics/gui/opengl.py
| def say(reason: str) -> None:
"""Show `reason` to the person about to see a 3-D view: on Windows
through its own message box, which needs nothing of Qt; elsewhere
through Qt, which this never reaches, since only Windows is asked."""
# Win32's box rather than Qt's: when the check ran before any Qt, a
# QApplication built on a machine with no OpenGL crashed on the way
# out, and its crash dialog held a release runner for forty minutes
# (2026-10-18)
if sys.platform == 'win32':
ctypes.windll.user32.MessageBoxW(None, reason, 'Visual Dynamics',
0x10) # MB_ICONERROR
return
from PySide6.QtWidgets import QApplication, QMessageBox
QApplication.instance() or QApplication(['visualdynamics'])
QMessageBox.critical(None, 'Visual Dynamics', reason)
|