Skip to content

Commit 8f2f998

Browse files
authored
Renamed WgpuOffscreenCanvas to WgpuOffscreenCanvasBase (#433)
1 parent f68d910 commit 8f2f998

File tree

7 files changed

+148
-145
lines changed

7 files changed

+148
-145
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ Changed:
2828
* `GPUCommandEncoder.begin_render_pass()` binds the lifetime of passed texture views to
2929
the returned render pass object to prevent premature destruction when no reference to
3030
a texture view is kept.
31+
* Renamed ``wgpu.gui.WgpuOffscreenCanvas` to `WgpuOffscreenCanvasBase`.
3132

3233
Fixed:
3334

docs/gui.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ The Canvas base classes
2121
~WgpuCanvasInterface
2222
~WgpuCanvasBase
2323
~WgpuAutoGui
24-
~WgpuOffscreenCanvas
24+
~WgpuOffscreenCanvasBase
2525

2626

2727
For each supported GUI toolkit there is a module that implements a ``WgpuCanvas`` class,

tests/test_gui_base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ def test_canvas_logging(caplog):
7878
assert text.count("division by zero") == 4
7979

8080

81-
class MyOffscreenCanvas(wgpu.gui.WgpuOffscreenCanvas):
81+
class MyOffscreenCanvas(wgpu.gui.WgpuOffscreenCanvasBase):
8282
def __init__(self):
8383
super().__init__()
8484
self.textures = []

wgpu/gui/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
"""
44

55
from .base import WgpuCanvasInterface, WgpuCanvasBase, WgpuAutoGui # noqa: F401
6-
from ._offscreen import WgpuOffscreenCanvas # noqa: F401
6+
from .offscreen import WgpuOffscreenCanvasBase # noqa: F401
77

88
__all__ = [
99
"WgpuCanvasInterface",
1010
"WgpuCanvasBase",
1111
"WgpuAutoGui",
12-
"WgpuOffscreenCanvas",
12+
"WgpuOffscreenCanvasBase",
1313
]

wgpu/gui/_offscreen.py

Lines changed: 0 additions & 133 deletions
This file was deleted.

wgpu/gui/jupyter.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import weakref
88
import asyncio
99

10-
from ._offscreen import WgpuOffscreenCanvas
10+
from .offscreen import WgpuOffscreenCanvasBase
1111
from .base import WgpuAutoGui
1212

1313
import numpy as np
@@ -18,7 +18,7 @@
1818
pending_jupyter_canvases = []
1919

2020

21-
class JupyterWgpuCanvas(WgpuAutoGui, WgpuOffscreenCanvas, RemoteFrameBuffer):
21+
class JupyterWgpuCanvas(WgpuAutoGui, WgpuOffscreenCanvasBase, RemoteFrameBuffer):
2222
"""An ipywidgets widget providing a wgpu canvas. Needs the jupyter_rfb library."""
2323

2424
def __init__(self, *, size=None, title=None, **kwargs):
@@ -88,10 +88,10 @@ def _request_draw(self):
8888
self._request_draw_timer_running = True
8989
call_later(self._get_draw_wait_time(), RemoteFrameBuffer.request_draw, self)
9090

91-
# Implementation needed for WgpuOffscreenCanvas
91+
# Implementation needed for WgpuOffscreenCanvasBase
9292

9393
def present(self, texture):
94-
# This gets called at the end of a draw pass via _offscreen.GPUCanvasContext
94+
# This gets called at the end of a draw pass via offscreen.GPUCanvasContext
9595
device = texture._device
9696
size = texture.size
9797
bytes_per_pixel = 4

wgpu/gui/offscreen.py

Lines changed: 139 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,145 @@
11
import time
22

3-
from ._offscreen import WgpuOffscreenCanvas
4-
from .base import WgpuAutoGui
3+
from .. import classes, flags
4+
from .base import WgpuCanvasBase, WgpuAutoGui
5+
6+
7+
class GPUCanvasContext(classes.GPUCanvasContext):
8+
"""GPUCanvasContext subclass for rendering to an offscreen texture."""
9+
10+
# In this context implementation, we keep a ref to the texture, to keep
11+
# it alive until at least until present() is called, and to be able to
12+
# pass it to the canvas' present() method. Thereafter, the texture
13+
# reference is removed. If there are no more references to it, it will
14+
# be cleaned up. But if the offscreen canvas uses it for something,
15+
# it'll simply stay alive longer.
16+
17+
def __init__(self, canvas):
18+
super().__init__(canvas)
19+
self._config = None
20+
self._texture = None
21+
22+
def configure(
23+
self,
24+
*,
25+
device,
26+
format,
27+
usage=flags.TextureUsage.RENDER_ATTACHMENT | flags.TextureUsage.COPY_SRC,
28+
view_formats=[],
29+
color_space="srgb",
30+
alpha_mode="opaque"
31+
):
32+
if format is None:
33+
format = self.get_preferred_format(device.adapter)
34+
self._config = {
35+
"device": device,
36+
"format": format,
37+
"usage": usage,
38+
"width": 0,
39+
"height": 0,
40+
# "view_formats": xx,
41+
# "color_space": xx,
42+
# "alpha_mode": xx,
43+
}
44+
45+
def unconfigure(self):
46+
self._texture = None
47+
self._config = None
48+
49+
def get_current_texture(self):
50+
if not self._config:
51+
raise RuntimeError(
52+
"Canvas context must be configured before calling get_current_texture()."
53+
)
54+
55+
width, height = self._get_canvas().get_physical_size()
56+
width, height = max(width, 1), max(height, 1)
57+
58+
self._texture = self._config["device"].create_texture(
59+
label="presentation-context",
60+
size=(width, height, 1),
61+
format=self._config["format"],
62+
usage=self._config["usage"],
63+
)
64+
return self._texture
65+
66+
def present(self):
67+
if not self._texture:
68+
msg = "present() is called without a preceeding call to "
69+
msg += "get_current_texture(). Note that present() is usually "
70+
msg += "called automatically after the draw function returns."
71+
raise RuntimeError(msg)
72+
else:
73+
texture = self._texture
74+
self._texture = None
75+
return self._get_canvas().present(texture)
76+
77+
def get_preferred_format(self, adapter):
78+
canvas = self._get_canvas()
79+
if canvas:
80+
return canvas.get_preferred_format()
81+
else:
82+
return "rgba8unorm-srgb"
83+
84+
85+
class WgpuOffscreenCanvasBase(WgpuCanvasBase):
86+
"""Base class for off-screen canvases.
87+
88+
It provides a custom context that renders to a texture instead of
89+
a surface/screen. On each draw the resulting image is passes as a
90+
texture to the ``present()`` method. Subclasses should (at least)
91+
implement ``present()``
92+
"""
93+
94+
def __init__(self, *args, **kwargs):
95+
super().__init__(*args, **kwargs)
96+
97+
def get_window_id(self):
98+
"""This canvas does not correspond to an on-screen window."""
99+
return None
100+
101+
def get_context(self, kind="webgpu"):
102+
"""Get the GPUCanvasContext object to obtain a texture to render to."""
103+
# Normally this creates a GPUCanvasContext object provided by
104+
# the backend (e.g. wgpu-native), but here we use our own context.
105+
assert kind == "webgpu"
106+
if self._canvas_context is None:
107+
self._canvas_context = GPUCanvasContext(self)
108+
return self._canvas_context
109+
110+
def present(self, texture):
111+
"""Method that gets called at the end of each draw event.
112+
113+
The rendered image is represented by the texture argument.
114+
Subclasses should overload this method and use the texture to
115+
process the rendered image.
116+
117+
The texture is a new object at each draw, but is not explicitly
118+
destroyed, so it can be used e.g. as a texture binding (subject
119+
to set TextureUsage).
120+
"""
121+
# Notes: Creating a new texture object for each draw is
122+
# consistent with how real canvas contexts work, plus it avoids
123+
# confusion of re-using the same texture except when the canvas
124+
# changes size. For use-cases where you do want to render to the
125+
# same texture one does not need the canvas API. E.g. in pygfx
126+
# the renderer can also work with a target that is a (fixed
127+
# size) texture.
128+
pass
129+
130+
def get_preferred_format(self):
131+
"""Get the preferred format for this canvas.
132+
133+
This method can be overloaded to control the used texture
134+
format. The default is "rgba8unorm-srgb".
135+
"""
136+
# Use rgba because that order is more common for processing and storage.
137+
# Use srgb because that's what how colors are usually expected to be.
138+
# Use 8unorm because 8bit is enough (when using srgb).
139+
return "rgba8unorm-srgb"
5140

6141

7-
class WgpuManualOffscreenCanvas(WgpuAutoGui, WgpuOffscreenCanvas):
142+
class WgpuManualOffscreenCanvas(WgpuAutoGui, WgpuOffscreenCanvasBase):
8143
"""An offscreen canvas intended for manual use.
9144
10145
Call the ``.draw()`` method to perform a draw and get the result.
@@ -42,7 +177,7 @@ def _request_draw(self):
42177
pass
43178

44179
def present(self, texture):
45-
# This gets called at the end of a draw pass via _offscreen.GPUCanvasContext
180+
# This gets called at the end of a draw pass via GPUCanvasContext
46181
device = texture._device
47182
size = texture.size
48183
bytes_per_pixel = 4

0 commit comments

Comments
 (0)