Skip to content

visualdynamics.gui.opengl

opengl

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

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.

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

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.

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

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.

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

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.

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

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

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.

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)