From bc94caff6d6969f42b424f5d41c0af2312c77763 Mon Sep 17 00:00:00 2001 From: wyzula-jan Date: Mon, 10 Aug 2026 16:39:29 +0200 Subject: [PATCH] fix(plots): let use_opengl override the software-renderer default --- bec_widgets/utils/gpu_acceleration.py | 24 ++++++++++++---- bec_widgets/widgets/plots/plot_base.py | 25 +++++++++++++---- tests/unit_tests/test_gpu_acceleration.py | 30 ++++++++++++++++++++ tests/unit_tests/test_plot_base_next_gen.py | 31 ++++++++++++++++++++- 4 files changed, 98 insertions(+), 12 deletions(-) diff --git a/bec_widgets/utils/gpu_acceleration.py b/bec_widgets/utils/gpu_acceleration.py index 460a457e..bfd8d144 100644 --- a/bec_widgets/utils/gpu_acceleration.py +++ b/bec_widgets/utils/gpu_acceleration.py @@ -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 diff --git a/bec_widgets/widgets/plots/plot_base.py b/bec_widgets/widgets/plots/plot_base.py index 6186aa8a..78efbb44 100644 --- a/bec_widgets/widgets/plots/plot_base.py +++ b/bec_widgets/widgets/plots/plot_base.py @@ -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) diff --git a/tests/unit_tests/test_gpu_acceleration.py b/tests/unit_tests/test_gpu_acceleration.py index fbc3a429..3c59a20a 100644 --- a/tests/unit_tests/test_gpu_acceleration.py +++ b/tests/unit_tests/test_gpu_acceleration.py @@ -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") diff --git a/tests/unit_tests/test_plot_base_next_gen.py b/tests/unit_tests/test_plot_base_next_gen.py index dc1eef12..ad2be19b 100644 --- a/tests/unit_tests/test_plot_base_next_gen.py +++ b/tests/unit_tests/test_plot_base_next_gen.py @@ -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