Skip to content

Add pygfx/WGPU backend for 2D/3D plotting#4520

Open
physwkim wants to merge 20 commits intosilx-kit:mainfrom
physwkim:pygfx-backend-pr
Open

Add pygfx/WGPU backend for 2D/3D plotting#4520
physwkim wants to merge 20 commits intosilx-kit:mainfrom
physwkim:pygfx-backend-pr

Conversation

@physwkim
Copy link
Copy Markdown
Contributor

@physwkim physwkim commented Mar 12, 2026

OpenGL has been deprecated on macOS. This PR adds a new rendering backend based on pygfx/WGPU, enabling native Metal (macOS), Vulkan (Linux), and DirectX (Windows) support.

Drop-in alternative to the existing OpenGL backend for both 2D and 3D plotting.

2D (silx.gui.plot)

  • Curves, images, scatter, markers, shapes, error bars, color bar
  • GPU colormap pipeline (scalar texture + shader)
  • GPU compute shaders for min/max reduction and histogram
  • Async compute for non-blocking streaming updates
  • Efficient image streaming update path

3D (silx.gui.plot3d)

  • SceneWindow with backend="pygfx" support
  • Mesh, scatter, volume (isosurface via marching cubes, cut planes), image, heightmap
  • Object parameters tree (SceneModel reuse via GroupItem)
  • 3D axes (pygfx Ruler) and bounding box wireframe
  • ClipPlane support for GroupItem

Key changes

  • BackendPygfx.py — 2D backend implementation
  • Plot3DWidgetPygfx.py, SceneWidgetPygfx.py — 3D backend implementation
  • _PlotFrameCore.py — shared plot frame logic between OpenGL and pygfx
  • _pygfx_sync.py — Item3D to pygfx object sync layer
  • Parametrized tests for both backends

Benchmarks

Two benchmarks were added to evaluate the backend performance.


Benchmarks

Benchmarks were run on a MacBook M4 Pro.

  • compareBackendsFPS.py for live line update performance
  • imageStreamingBenchmark.py for image streaming throughput

Image streaming benchmark

Example result for 4096 × 4096 streaming images:

Size   Norm    FPS     plot_ms   other   total
1024   linear  507.3   1.56      0.02    1.58
2048   linear  308.1   2.64      0.03    2.67
4096   linear  125.7   6.77      0.04    6.81

The backend maintains >120 FPS for 4K images during continuous streaming updates.

image

Curve update benchmark

Live curve updates comparison between backends:

image

matplotlib : ~136 FPS
OpenGL : ~120 FPS
pygfx/WGPU : ~244 FPS

The pygfx backend shows ~2× higher performance than the OpenGL backend for dynamic curve updates.


Platform Status

Platform Status
macOS (Metal) Fully supported
Linux (X11 + Vulkan) Works well
Linux (Wayland) Not working
Windows Expected to work via DirectX

Motivation

  • OpenGL is deprecated on macOS
  • Modern GPU APIs (Metal / Vulkan / DX12) are now the standard
  • WebGPU/WGPU provides a cross-platform abstraction layer

Using pygfx allows silx to benefit from:

  • modern GPU rendering
  • better macOS compatibility
  • improved performance for streaming visualization

AI Disclosure

Claude Code was used to generate a significant portion of the code in this PR, including the pygfx backend, synchronization layer, and associated tests.

The author guided the implementation by providing high-level design direction and examples, and performed iterative validation through manual testing, debugging, and benchmarking.

Add a GPU-accelerated 2D plot backend using pygfx (WebGPU/Vulkan/Metal).

New files:
- BackendPygfx.py: Full 2D plot backend implementation supporting
  curves, images, scatter, markers, shapes, error bars, and color bar.
  Includes GPU-accelerated colormap pipeline (scalar texture + shader),
  WGSL compute shaders for min/max reduction and histogram, and async
  compute for non-blocking streaming updates.
- _PlotFrameCore.py: Rendering-independent plot frame math extracted
  from GLPlotFrame. Provides coordinate transforms, tick generation,
  and layout calculation shared between OpenGL and pygfx backends.

Modified files:
- PlotWidget.py: Register "pygfx"/"wgpu" backend aliases and add
  updateImageData() method for efficient streaming data updates.
- image.py: Add updateData() fast path on ImageDataBase that bypasses
  the item dirty/remove/add cycle. Supports autoscale recomputation.
- core.py: Optional GPU stats hook in ColormapMixIn._setColormappedData()
  to pre-fill autoscale cache with GPU-computed min/max.
- ColormapDialog.py: Optional GPU histogram hook for accelerated
  histogram computation when pygfx backend is active.
- pyproject.toml: Add [pygfx] optional dependency group.
- _config.py: Document pygfx backend option.

Examples:
- compareBackendsFPS.py: Live curve update FPS benchmark across
  matplotlib, opengl, and pygfx backends.
- imageStreamingBenchmark.py: Image streaming throughput benchmark
  for the pygfx backend with per-frame timing breakdown.
- plot3dVolume.py: 3D scalar field volume with isosurfaces and
  interactive cut plane.
@loichuder loichuder requested a review from t20100 March 12, 2026 08:23
@loichuder
Copy link
Copy Markdown
Member

loichuder commented Mar 12, 2026

Wow, thanks for the big contribution, that looks very promising!

We'll get back to you but know that the review may take a bit of time 😉

@t20100
Copy link
Copy Markdown
Member

t20100 commented Mar 12, 2026

Thanks for the contribution!

We'll come back to you and iterate over this PR.

A first quick comment, we aim at keeping plot backends playing an equal role, so adding a plot backend should only modify PlotWidget and not other part of the code like plot items or ColormapDialog.
If optimizations of other parts of the code are needed, let's to do it separately.

Remove optimizations outside of PlotWidget to keep plot backends
playing an equal role:
- image.py: Remove updateData() fast path
- core.py: Remove GPU stats hook in ColormapMixIn
- ColormapDialog.py: Remove GPU histogram hook
- PlotWidget.py: Remove updateImageData() method
- BackendPygfx updateData: compute clim from data when None
  (nanmin/nanmax) instead of keeping stale values
- Remove benchmark and example scripts not part of the backend PR
@physwkim
Copy link
Copy Markdown
Contributor Author

Unlike the previous PR, this one has been cleaned up to contain only the backend-related changes. A separate PR for the addImage fast path optimization has been created at #4524.

@t20100
Copy link
Copy Markdown
Member

t20100 commented Mar 17, 2026

Thanks for the split!

I had a quick look at the code and a first try, here are some more early comments:

  • Could you add the "pygfx" backend to the tests?
    To do so, search for "gl" in src/silx/gui/plot/test, there's so ("mpl", "gl") (or ("gl", "mpl")) test parameters where you can add "pygfx".
    Also in src/silx/gui/plot/test/test_plotwidget.py, there is some classes of this kind:
@pytest.mark.usefixtures("use_opengl")
class TestPlotWidget_Gl(TestPlotWidget):
    backend = "gl"

Add similar ones for pygfx.
This will enable the tests with the pygfx backend.

  • I tested in on Linux (ubuntu24.04) with python3.14 and up-to-date dependencies.
    I managed to run the backend with PyQt5 (with some warnings) but it panics with both PyQt6 and PySide6:
from silx import sx
w = sx.PlotWidget(backend="pygfx")

Error:

WARNING:wgpu:Unable to find extension: VK_EXT_physical_device_drm
WARNING:wgpu:Detected skylake derivative running on mesa i915. Clears to srgb textures will use manual shader clears.

thread '<unnamed>' (595070) panicked at /root/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/wgpu-hal-27.0.4/src/vulkan/instance.rs:452:18:
XlibSurface::create_xlib_surface() failed: ERROR_OUT_OF_HOST_MEMORY
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

thread '<unnamed>' (595070) panicked at library/core/src/panicking.rs:230:5:
panic in a function that cannot unwind

Do you know what could go wrong?

@t20100
Copy link
Copy Markdown
Member

t20100 commented Mar 17, 2026

To complete previous comment, it also panics when setting QT_QPA_PLATFORM=wayland with PyQt5 so wayland support is probably the root cause.

Add --no-pygfx pytest option, use_pygfx fixture, and _TestOptions
support for conditionally skipping pygfx tests. Add pygfx backend
parametrization to existing plot widget, active item, line style,
and plot window tests. Add Linux display/Wayland guard in PlotWidget
for pygfx backend initialization.
Implement _pickImage and _pickTriangles methods that were left as
TODO stubs. Store origin/scale/dataShape on _PygfxImageItem and
x/y/triangles on _PygfxTrianglesItem for use during picking.
Add PRESENT_METHOD class variable and emit axis limits changed
signals on resize.
rendercanvas's request_draw uses a scheduler that may delay the
actual draw, so processEvents() does not always flush pending item
updates. Add QWidget.update() call to postRedisplay/replot to
ensure Qt paint events are processed synchronously, matching the
OpenGL backend's behavior.
@physwkim
Copy link
Copy Markdown
Contributor Author

Thanks for testing!

I've added the "pygfx" backend to the test parametrizations and added the _Pygfx test classes — that's already included in the latest push.

About the Wayland crash — yeah, I was able to reproduce it too. This is a known upstream issue in wgpu. The wgpu-py docs mention that Wayland support is currently broken, so it'll probably take some time on their side to fix it. For now I've added a guard in PlotWidget that raises a clear error for Wayland sessions instead of letting it panic.

While adding the tests, I also found and fixed a couple of issues — image/triangle picking was stubbed out (return None # TODO), and processEvents() wasn't reliably flushing pending item updates due to rendercanvas's deferred draw scheduling. Both are fixed in the latest commits.

You mentioned recently that AI-generated code should be disclosed. Could you clarify where you'd like it noted — in the commit messages, PR description, or somewhere in the source code?

Override paintEvent to flush dirty plot items via _paintContext()
inside the paint event handler, where GPU operations are safe.
This mirrors the OpenGL backend's paintGL pattern and ensures
_backendRenderer is up-to-date before pick() is called.

Without this, processEvents() on Linux does not reliably trigger
rendercanvas's async scheduler, leaving _backendRenderer as None.
Replace Python for-loops with numpy vectorized operations for building
error bar line segments, improving performance for large datasets.
Also fix handling of scalar (ndim=0) and column-vector (N,1) shaped
error values that were previously unhandled.
@physwkim physwkim force-pushed the pygfx-backend-pr branch 2 times, most recently from 8bd519f to a497784 Compare March 19, 2026 03:49
2D and 3D example scripts demonstrating pygfx backend usage:
curves, images, scatter, line styles, markers, error bars, log axes,
backend comparison, benchmarks, dual Y-axis, ImageView, large data,
3D scatter/volume/surface/mesh/heightmap, and clipping groups.
- Fix Box/Cylinder/Hexagon setColor -> color property
- Remove unused import and variable (flake8)
- Remove debug_overlay.py
- Add GPU colormap streaming benchmark (19_gpu_colormap_benchmark.py)
@physwkim
Copy link
Copy Markdown
Contributor Author

The Linux CI doesn't have a GPU, so wgpu device creation fails. OpenGL works fine there because Mesa provides a software renderer (llvmpipe), but wgpu doesn't have that kind of fallback. I added a check that tries to create a wgpu device before running pygfx tests — if it fails, the tests are skipped.

To run pygfx tests on Linux CI, one option would be using a GPU-equipped runner, or installing mesa-vulkan-drivers (lavapipe) for software Vulkan. I'm not sure how to set that up in the CI pipeline though.

@loichuder
Copy link
Copy Markdown
Member

You mentioned recently that AI-generated code should be disclosed. Could you clarify where you'd like it noted — in the commit messages, PR description, or somewhere in the source code?

Just a quick sentence in the PR description is enough. Something like:

I used ChatGPT to generate the docstrings.

or

This part of the code was written with Copilot autocompletion.

@physwkim physwkim changed the title Add pygfx/WGPU backend for 2D plotting Add pygfx/WGPU backend for 2D/3D plotting Mar 19, 2026
Implemented with Claude Code
@physwkim
Copy link
Copy Markdown
Contributor Author

3D plotting seems to be coming along nicely as well.

  • examples/pygfx_backend/13_3d_scatter.py
image
  • examples/pygfx_backend/14_3d_volume.py
image

@t20100
Copy link
Copy Markdown
Member

t20100 commented Mar 30, 2026

Hi,

Sorry for the delayed reply.

We are currently preparing a major release after a very long time and we want to have this available as soon as possible, so we will not integrate your PR before then.
Don't worry, we will come back to you once this is done and we aim at resuming a shorter and regular release cycle. In the meantime, you can already use your backend in your application by passing the class to the plots: plot = PlotWidget(backend=MyBackendClass).

For the review process, it would be best to focus on one feature at a time: Reviews are still done by humans in this project. It would be much simpler to go step by step and have a first PR for the 2d plot backend only (you can keep this one opened to demo 2d/3d plot and open another one providing only the 2d plot backend).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants