fix(plots): let use_opengl override the software-renderer default

This commit is contained in:
2026-08-10 16:39:29 +02:00
parent 9c287d28e6
commit bc94caff6d
4 changed files with 98 additions and 12 deletions
+18 -6
View File
@@ -107,11 +107,26 @@ def _env_override() -> bool | None:
return None
def opengl_available(requested: bool = True) -> bool:
@lru_cache(maxsize=4)
def _log_software_renderer(renderer: str) -> None:
"""Report the software-renderer decision once per process, not per widget."""
logger.info(
f"OpenGL is software rendered ({renderer}); keeping the raster viewport by default. "
f"Set the plot's use_opengl property to True to use it anyway, or {ENV_VAR}=1 "
"to opt in for the whole session."
)
def opengl_available(requested: bool = True, explicit: bool = False) -> bool:
"""Decide whether a widget that asked for OpenGL should actually get it.
Args:
requested(bool): Whether the widget wants the OpenGL viewport at all.
explicit(bool): True when a caller deliberately asked -- a `use_opengl`
property set or an RPC call -- rather than this being the construction
default. A deliberate request overrides the software-renderer default,
since the caller has said what they want; it still cannot conjure a
context that does not exist, and `BEC_WIDGETS_OPENGL=0` still wins.
Returns:
bool: True if the OpenGL viewport should be installed.
@@ -123,13 +138,10 @@ def opengl_available(requested: bool = True) -> bool:
return False
if opengl_info() is None:
return False
if override is True:
if override is True or explicit:
return True
if is_software_renderer():
logger.info(
"OpenGL is available but software rendered; keeping the raster viewport. "
f"Set {ENV_VAR}=1 to override."
)
_log_software_renderer(opengl_info()["renderer"])
return False
return True
+20 -5
View File
@@ -166,7 +166,7 @@ class PlotBase(BECWidget, QWidget):
self.plot_widget = pg.GraphicsLayoutWidget(parent=self)
# GraphicsLayoutWidget forwards no viewport argument to GraphicsView, so the
# viewport is swapped after construction instead.
self.use_opengl = self.USE_OPENGL
self._set_use_opengl(self.USE_OPENGL, explicit=False)
self.plot_widget.ci.setContentsMargins(0, 0, 0, 0)
self.plot_item = pg.PlotItem(viewBox=BECViewBox(enableMenu=True))
self.plot_widget.addItem(self.plot_item)
@@ -322,8 +322,12 @@ class PlotBase(BECWidget, QWidget):
Whether the plot currently renders through an OpenGL viewport.
Reflects the live viewport rather than the requested value: setting this to
True is best effort, and stays False when no hardware-accelerated context is
available (see `bec_widgets.utils.gpu_acceleration.opengl_available`).
True is best effort and stays False when no OpenGL context can be created
at all (see `bec_widgets.utils.gpu_acceleration.opengl_available`).
Setting it is an explicit request, so it overrides the software-renderer
default that keeps freshly built plots on the raster viewport. That is what
makes it possible to compare the two on a remote console.
"""
return isinstance(self.plot_widget.viewport(), QOpenGLWidget)
@@ -335,8 +339,19 @@ class PlotBase(BECWidget, QWidget):
Args:
value(bool): Whether the OpenGL viewport is wanted.
"""
if value and not opengl_available(True):
if self.use_opengl:
self._set_use_opengl(value, explicit=True)
def _set_use_opengl(self, value: bool, explicit: bool) -> None:
"""
Apply an OpenGL viewport request.
Args:
value(bool): Whether the OpenGL viewport is wanted.
explicit(bool): Whether this came from a deliberate request rather than
the construction default. See `opengl_available`.
"""
if value and not opengl_available(True, explicit=explicit):
if isinstance(self.plot_widget.viewport(), QOpenGLWidget):
set_view_opengl(self.plot_widget, False)
return
set_view_opengl(self.plot_widget, value)
+30
View File
@@ -19,12 +19,15 @@ def _reset_opengl_probe(monkeypatch):
# hold on to the real cached function: monkeypatch may swap the module
# attribute for a stub, and it is only restored after this fixture resumes
probe = gpu_acceleration.opengl_info
notice = gpu_acceleration._log_software_renderer
probe.cache_clear()
notice.cache_clear()
monkeypatch.delenv(ENV_VAR, raising=False)
previous = pg.getConfigOption("useOpenGL")
yield
pg.setConfigOption("useOpenGL", previous)
probe.cache_clear()
notice.cache_clear()
def _fake_renderer(monkeypatch, renderer: str | None):
@@ -50,6 +53,33 @@ def test_opengl_refused_on_software_renderer(monkeypatch, renderer):
assert opengl_available(requested=True) is False
def test_explicit_request_overrides_software_renderer(monkeypatch):
"""The use_opengl property must be able to force OpenGL on a remote console."""
_fake_renderer(monkeypatch, "llvmpipe (LLVM 15.0.7, 256 bits)")
assert opengl_available(requested=True) is False
assert opengl_available(requested=True, explicit=True) is True
def test_explicit_request_cannot_invent_a_context(monkeypatch):
_fake_renderer(monkeypatch, None)
assert opengl_available(requested=True, explicit=True) is False
def test_env_var_off_beats_an_explicit_request(monkeypatch):
_fake_renderer(monkeypatch, "NVIDIA GeForce RTX 3090")
monkeypatch.setenv(ENV_VAR, "0")
assert opengl_available(requested=True, explicit=True) is False
def test_software_renderer_notice_is_logged_once(monkeypatch, caplog):
"""opengl_available runs per widget; the notice must not repeat per plot."""
_fake_renderer(monkeypatch, "llvmpipe (LLVM 15.0.7, 256 bits)")
with caplog.at_level("INFO"):
for _ in range(5):
opengl_available(requested=True)
assert sum("software rendered" in r.message for r in caplog.records) <= 1
def test_env_var_forces_opengl_on_software_renderer(monkeypatch):
_fake_renderer(monkeypatch, "llvmpipe (LLVM 15.0.7, 256 bits)")
monkeypatch.setenv(ENV_VAR, "1")
+30 -1
View File
@@ -569,8 +569,37 @@ def test_use_opengl_can_be_toggled_at_runtime(qtbot, mocked_client):
def test_use_opengl_declines_without_hardware_context(qtbot, mocked_client, monkeypatch):
"""Requesting OpenGL on a software renderer leaves the raster viewport in place."""
pb = create_widget(qtbot, PlotBase, client=mocked_client)
monkeypatch.setattr(plot_base_module, "opengl_available", lambda requested=True: False)
monkeypatch.setattr(
plot_base_module, "opengl_available", lambda requested=True, explicit=False: False
)
pb.use_opengl = True
assert pb.use_opengl is False
assert not isinstance(pb.plot_widget.viewport(), QOpenGLWidget)
def test_software_renderer_defaults_to_raster_but_property_can_force(qtbot, mocked_client):
"""On a remote console the default is raster, yet use_opengl must still work."""
from bec_widgets.utils import gpu_acceleration
real = gpu_acceleration.opengl_available
def fake(requested=True, explicit=False):
# pretend we are on llvmpipe: refused by default, honoured when explicit
return bool(requested and explicit)
monkeypatch = pytest.MonkeyPatch()
monkeypatch.setattr(plot_base_module, "opengl_available", fake)
try:
pb = create_widget(qtbot, PlotBase, client=mocked_client)
# construction takes the software-renderer default
assert pb.use_opengl is False
# an explicit request overrides it
pb.use_opengl = True
assert pb.use_opengl is True
assert isinstance(pb.plot_widget.viewport(), QOpenGLWidget)
pb.use_opengl = False
assert pb.use_opengl is False
finally:
monkeypatch.undo()
assert gpu_acceleration.opengl_available is real