diff --git a/CHANGELOG.md b/CHANGELOG.md index cbd6a61b..97be2d55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ Changed: * `GPUCommandEncoder.begin_render_pass()` binds the lifetime of passed texture views to the returned render pass object to prevent premature destruction when no reference to a texture view is kept. +* Renamed ``wgpu.gui.WgpuOffscreenCanvas` to `WgpuOffscreenCanvasBase`. Fixed: diff --git a/docs/gui.rst b/docs/gui.rst index 2f6191de..c052205a 100644 --- a/docs/gui.rst +++ b/docs/gui.rst @@ -21,7 +21,7 @@ The Canvas base classes ~WgpuCanvasInterface ~WgpuCanvasBase ~WgpuAutoGui - ~WgpuOffscreenCanvas + ~WgpuOffscreenCanvasBase For each supported GUI toolkit there is a module that implements a ``WgpuCanvas`` class, diff --git a/tests/test_gui_base.py b/tests/test_gui_base.py index e3060bc9..247790e0 100644 --- a/tests/test_gui_base.py +++ b/tests/test_gui_base.py @@ -78,7 +78,7 @@ def test_canvas_logging(caplog): assert text.count("division by zero") == 4 -class MyOffscreenCanvas(wgpu.gui.WgpuOffscreenCanvas): +class MyOffscreenCanvas(wgpu.gui.WgpuOffscreenCanvasBase): def __init__(self): super().__init__() self.textures = [] diff --git a/wgpu/gui/__init__.py b/wgpu/gui/__init__.py index fe5fd51a..c9591689 100644 --- a/wgpu/gui/__init__.py +++ b/wgpu/gui/__init__.py @@ -3,11 +3,11 @@ """ from .base import WgpuCanvasInterface, WgpuCanvasBase, WgpuAutoGui # noqa: F401 -from ._offscreen import WgpuOffscreenCanvas # noqa: F401 +from .offscreen import WgpuOffscreenCanvasBase # noqa: F401 __all__ = [ "WgpuCanvasInterface", "WgpuCanvasBase", "WgpuAutoGui", - "WgpuOffscreenCanvas", + "WgpuOffscreenCanvasBase", ] diff --git a/wgpu/gui/_offscreen.py b/wgpu/gui/_offscreen.py deleted file mode 100644 index b240236f..00000000 --- a/wgpu/gui/_offscreen.py +++ /dev/null @@ -1,133 +0,0 @@ -from .. import classes, flags -from ..gui.base import WgpuCanvasBase - - -class WgpuOffscreenCanvas(WgpuCanvasBase): - """Base class for off-screen canvases. - - It provides a custom context that renders to a texture instead of - a surface/screen. The resulting texture is passed to the `present()` - method. - """ - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - def get_window_id(self): - """This canvas does not correspond to an on-screen window.""" - return None - - def get_context(self, kind="webgpu"): - """Get the GPUCanvasContext object to obtain a texture to render to.""" - # Normally this creates a GPUCanvasContext object provided by - # the backend (e.g. wgpu-native), but here we use our own context. - assert kind == "webgpu" - if self._canvas_context is None: - self._canvas_context = GPUCanvasContext(self) - return self._canvas_context - - def present(self, texture): - """Method that gets called at the end of each draw event. - - The rendered image is represented by the texture argument. - Subclasses should overload this method and use the texture to - process the rendered image. The texture is a new object at each - draw, but is not explicitly destroyed, so it can be used e.g. - as a texture binding (subject to set TextureUsage). - """ - # Notes: Creating a new texture object for each draw is - # consistent with how real canvas contexts work, plus it avoids - # confusion of re-using the same texture except when the canvas - # changes size. For use-cases where you do want to render to the - # same texture one does not need the canvas API. E.g. in pygfx - # the renderer can also work with a target that is a (fixed - # size) texture. - pass - - def get_preferred_format(self): - """Get the preferred format for this canvas. This method can - be overloaded to control the used texture format. The default - is "rgba8unorm-srgb". - """ - # Use rgba because that order is more common for processing and storage. - # Use srgb because that's what how colors are usually expected to be. - # Use 8unorm because 8bit is enough (when using srgb). - return "rgba8unorm-srgb" - - -class GPUCanvasContext(classes.GPUCanvasContext): - """Helper class for canvases that render to a texture.""" - - # In this context implementation, we keep a ref to the texture, to keep - # it alive until at least until present() is called, and to be able to - # pass it to the canvas' present() method. Thereafter, the texture - # reference is removed. If there are no more references to it, it will - # be cleaned up. But if the offscreen canvas uses it for something, - # it'll simply stay alive longer. - - def __init__(self, canvas): - super().__init__(canvas) - self._config = None - self._texture = None - - def configure( - self, - *, - device, - format, - usage=flags.TextureUsage.RENDER_ATTACHMENT | flags.TextureUsage.COPY_SRC, - view_formats=[], - color_space="srgb", - alpha_mode="opaque" - ): - if format is None: - format = self.get_preferred_format(device.adapter) - self._config = { - "device": device, - "format": format, - "usage": usage, - "width": 0, - "height": 0, - # "view_formats": xx, - # "color_space": xx, - # "alpha_mode": xx, - } - - def unconfigure(self): - self._texture = None - self._config = None - - def get_current_texture(self): - if not self._config: - raise RuntimeError( - "Canvas context must be configured before calling get_current_texture()." - ) - - width, height = self._get_canvas().get_physical_size() - width, height = max(width, 1), max(height, 1) - - self._texture = self._config["device"].create_texture( - label="presentation-context", - size=(width, height, 1), - format=self._config["format"], - usage=self._config["usage"], - ) - return self._texture - - def present(self): - if not self._texture: - msg = "present() is called without a preceeding call to " - msg += "get_current_texture(). Note that present() is usually " - msg += "called automatically after the draw function returns." - raise RuntimeError(msg) - else: - texture = self._texture - self._texture = None - return self._get_canvas().present(texture) - - def get_preferred_format(self, adapter): - canvas = self._get_canvas() - if canvas: - return canvas.get_preferred_format() - else: - return "rgba8unorm-srgb" diff --git a/wgpu/gui/jupyter.py b/wgpu/gui/jupyter.py index 15286909..905146fd 100644 --- a/wgpu/gui/jupyter.py +++ b/wgpu/gui/jupyter.py @@ -7,7 +7,7 @@ import weakref import asyncio -from ._offscreen import WgpuOffscreenCanvas +from .offscreen import WgpuOffscreenCanvasBase from .base import WgpuAutoGui import numpy as np @@ -18,7 +18,7 @@ pending_jupyter_canvases = [] -class JupyterWgpuCanvas(WgpuAutoGui, WgpuOffscreenCanvas, RemoteFrameBuffer): +class JupyterWgpuCanvas(WgpuAutoGui, WgpuOffscreenCanvasBase, RemoteFrameBuffer): """An ipywidgets widget providing a wgpu canvas. Needs the jupyter_rfb library.""" def __init__(self, *, size=None, title=None, **kwargs): @@ -88,10 +88,10 @@ def _request_draw(self): self._request_draw_timer_running = True call_later(self._get_draw_wait_time(), RemoteFrameBuffer.request_draw, self) - # Implementation needed for WgpuOffscreenCanvas + # Implementation needed for WgpuOffscreenCanvasBase def present(self, texture): - # This gets called at the end of a draw pass via _offscreen.GPUCanvasContext + # This gets called at the end of a draw pass via offscreen.GPUCanvasContext device = texture._device size = texture.size bytes_per_pixel = 4 diff --git a/wgpu/gui/offscreen.py b/wgpu/gui/offscreen.py index 8cda3c34..7ea5b06d 100644 --- a/wgpu/gui/offscreen.py +++ b/wgpu/gui/offscreen.py @@ -1,10 +1,145 @@ import time -from ._offscreen import WgpuOffscreenCanvas -from .base import WgpuAutoGui +from .. import classes, flags +from .base import WgpuCanvasBase, WgpuAutoGui + + +class GPUCanvasContext(classes.GPUCanvasContext): + """GPUCanvasContext subclass for rendering to an offscreen texture.""" + + # In this context implementation, we keep a ref to the texture, to keep + # it alive until at least until present() is called, and to be able to + # pass it to the canvas' present() method. Thereafter, the texture + # reference is removed. If there are no more references to it, it will + # be cleaned up. But if the offscreen canvas uses it for something, + # it'll simply stay alive longer. + + def __init__(self, canvas): + super().__init__(canvas) + self._config = None + self._texture = None + + def configure( + self, + *, + device, + format, + usage=flags.TextureUsage.RENDER_ATTACHMENT | flags.TextureUsage.COPY_SRC, + view_formats=[], + color_space="srgb", + alpha_mode="opaque" + ): + if format is None: + format = self.get_preferred_format(device.adapter) + self._config = { + "device": device, + "format": format, + "usage": usage, + "width": 0, + "height": 0, + # "view_formats": xx, + # "color_space": xx, + # "alpha_mode": xx, + } + + def unconfigure(self): + self._texture = None + self._config = None + + def get_current_texture(self): + if not self._config: + raise RuntimeError( + "Canvas context must be configured before calling get_current_texture()." + ) + + width, height = self._get_canvas().get_physical_size() + width, height = max(width, 1), max(height, 1) + + self._texture = self._config["device"].create_texture( + label="presentation-context", + size=(width, height, 1), + format=self._config["format"], + usage=self._config["usage"], + ) + return self._texture + + def present(self): + if not self._texture: + msg = "present() is called without a preceeding call to " + msg += "get_current_texture(). Note that present() is usually " + msg += "called automatically after the draw function returns." + raise RuntimeError(msg) + else: + texture = self._texture + self._texture = None + return self._get_canvas().present(texture) + + def get_preferred_format(self, adapter): + canvas = self._get_canvas() + if canvas: + return canvas.get_preferred_format() + else: + return "rgba8unorm-srgb" + + +class WgpuOffscreenCanvasBase(WgpuCanvasBase): + """Base class for off-screen canvases. + + It provides a custom context that renders to a texture instead of + a surface/screen. On each draw the resulting image is passes as a + texture to the ``present()`` method. Subclasses should (at least) + implement ``present()`` + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def get_window_id(self): + """This canvas does not correspond to an on-screen window.""" + return None + + def get_context(self, kind="webgpu"): + """Get the GPUCanvasContext object to obtain a texture to render to.""" + # Normally this creates a GPUCanvasContext object provided by + # the backend (e.g. wgpu-native), but here we use our own context. + assert kind == "webgpu" + if self._canvas_context is None: + self._canvas_context = GPUCanvasContext(self) + return self._canvas_context + + def present(self, texture): + """Method that gets called at the end of each draw event. + + The rendered image is represented by the texture argument. + Subclasses should overload this method and use the texture to + process the rendered image. + + The texture is a new object at each draw, but is not explicitly + destroyed, so it can be used e.g. as a texture binding (subject + to set TextureUsage). + """ + # Notes: Creating a new texture object for each draw is + # consistent with how real canvas contexts work, plus it avoids + # confusion of re-using the same texture except when the canvas + # changes size. For use-cases where you do want to render to the + # same texture one does not need the canvas API. E.g. in pygfx + # the renderer can also work with a target that is a (fixed + # size) texture. + pass + + def get_preferred_format(self): + """Get the preferred format for this canvas. + + This method can be overloaded to control the used texture + format. The default is "rgba8unorm-srgb". + """ + # Use rgba because that order is more common for processing and storage. + # Use srgb because that's what how colors are usually expected to be. + # Use 8unorm because 8bit is enough (when using srgb). + return "rgba8unorm-srgb" -class WgpuManualOffscreenCanvas(WgpuAutoGui, WgpuOffscreenCanvas): +class WgpuManualOffscreenCanvas(WgpuAutoGui, WgpuOffscreenCanvasBase): """An offscreen canvas intended for manual use. Call the ``.draw()`` method to perform a draw and get the result. @@ -42,7 +177,7 @@ def _request_draw(self): pass def present(self, texture): - # This gets called at the end of a draw pass via _offscreen.GPUCanvasContext + # This gets called at the end of a draw pass via GPUCanvasContext device = texture._device size = texture.size bytes_per_pixel = 4