Skip to content

Commit d4df1c9

Browse files
authored
Refactor how the present_method is selected (#169)
1 parent 6868f0a commit d4df1c9

File tree

11 files changed

+229
-181
lines changed

11 files changed

+229
-181
lines changed

examples/wx_app.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ def __init__(self):
1818
super().__init__(None, title="wgpu triangle embedded in a wx app")
1919
self.SetSize(640, 480)
2020

21-
# Using present_method 'image' because it reports "The surface texture is suboptimal"
21+
# Using present_method 'bitmap' because it reports "The surface texture is suboptimal"
2222
self.canvas = RenderWidget(
2323
self, update_mode="continuous", present_method="bitmap"
2424
)

rendercanvas/base.py

Lines changed: 57 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ class BaseRenderCanvas:
108108
against screen tearing, but limits the fps. Default True.
109109
present_method (str | None): Override the method to present the rendered result.
110110
Can be set to 'screen' or 'bitmap'. Default None, which means that the method is selected
111-
based on what the canvas supports and what the context prefers.
111+
based on what the canvas and context support and prefer.
112112
113113
"""
114114

@@ -151,6 +151,13 @@ def __init__(
151151
# The vsync is not-so-elegantly strored on the canvas, and picked up by wgou's canvas contex.
152152
self._vsync = bool(vsync)
153153

154+
# Handle custom present method
155+
if not (present_method is None or isinstance(present_method, str)):
156+
raise TypeError(
157+
f"The canvas present_method should be None or str, not {present_method!r}."
158+
)
159+
self._present_method = present_method
160+
154161
# Variables and flags used internally
155162
self.__is_drawing = False
156163
self.__title_info = {
@@ -294,36 +301,34 @@ def get_context(self, context_type: str | type) -> contexts.BaseContext:
294301
f"Cannot get context for '{context_name}': a context of type '{ref_context_name}' is already set."
295302
)
296303

297-
# Get available present methods.
298-
# Take care not to hold onto this dict, it may contain objects that we don't want to unnecessarily reference.
299-
present_methods = self._rc_get_present_methods()
300-
invalid_methods = set(present_methods.keys()) - {"screen", "bitmap"}
301-
if invalid_methods:
302-
logger.warning(
303-
f"{self.__class__.__name__} reports unknown present methods {invalid_methods!r}"
304-
)
304+
# Get available present methods that the canvas can chose from.
305+
present_methods = list(context_class.present_methods)
306+
assert all(m in ("bitmap", "screen") for m in present_methods) # sanity check
307+
if self._present_method is not None:
308+
if self._present_method not in present_methods:
309+
raise RuntimeError(
310+
f"Explicitly requested present_method {self._present_method!r} is not available for {context_name}."
311+
)
312+
present_methods = [self._present_method]
305313

306-
# Select present_method
307-
for present_method in context_class.present_methods:
308-
assert present_method in ("bitmap", "screen")
309-
if present_method in present_methods:
310-
break
311-
else:
314+
# Let the canvas select the method and provide the corresponding info object.
315+
# Take care not to hold onto this dict, it may contain objects that we don't want to unnecessarily reference.
316+
info = self._rc_get_present_info(present_methods)
317+
if info is None:
318+
method_message = f"Methods {set(present_methods)!r} are not supported."
319+
if len(present_methods) == 1:
320+
method_message = f"Method {present_methods[0]!r} is not supported."
312321
raise TypeError(
313-
f"Could not select present_method for context {context_name!r}: The methods {tuple(context_class.present_methods)!r} are not supported by the canvas backend {tuple(present_methods.keys())!r}."
322+
f"Could not create {context_name!r} for {self.__class__.__name__!r}: {method_message}"
314323
)
324+
if info.get("method") not in present_methods:
325+
raise RuntimeError(
326+
f"Present info method field ({info.get('method')!r}) is not part of the available methods {set(present_methods)}."
327+
)
328+
self._present_method = info["method"]
315329

316-
# Select present_info, and shape it into what the contexts need.
317-
present_info = present_methods[present_method]
318-
assert "method" not in present_info, (
319-
"the field 'method' is reserved in present_methods dicts"
320-
)
321-
present_info = {
322-
"method": present_method,
323-
"source": self.__class__.__name__,
324-
**present_info,
325-
"vsync": self._vsync,
326-
}
330+
# Add some info
331+
present_info = {**info, "source": self.__class__.__name__, "vsync": self._vsync}
327332

328333
# Create the context
329334
self._canvas_context = context_class(present_info)
@@ -641,32 +646,38 @@ def _rc_gui_poll(self):
641646
"""Process native events."""
642647
pass
643648

644-
def _rc_get_present_methods(self):
645-
"""Get info on the present methods supported by this canvas.
649+
def _rc_get_present_info(self, present_methods: list[str]) -> dict | None:
650+
"""Select a present method and return corresponding info dict.
651+
652+
This method is only called once, when the context is created. The
653+
subclass can use this moment to setup the internal state for the
654+
selected presentation method.
646655
647-
Must return a small dict, used by the canvas-context to determine
648-
how the rendered result will be presented to the canvas.
649-
This method is only called once, when the context is created.
656+
The ``present_methods`` represents the supported methods of the
657+
canvas-context, possibly filtered by a user-specified method. A canvas
658+
backend must implement at least the "screen" or "bitmap" method.
650659
651-
Each supported method is represented by a field in the dict. The value
652-
is another dict with information specific to that present method.
653-
A canvas backend must implement at least either "screen" or "bitmap".
660+
The returned dict must contain at least the key 'method', which must
661+
match one of the ``present_methods``. The remaining values represent
662+
information required by the canvas-context to perform the presentation,
663+
and optionally some (debug) meta data. The backend may optionally return
664+
None to indicate that none of the ``present_methods`` is supported.
654665
655-
With method "screen", the context will render directly to a surface
656-
representing the region on the screen. The sub-dict should have a ``window``
657-
field containing the window id. On Linux there should also be ``platform``
658-
field to distinguish between "wayland" and "x11", and a ``display`` field
659-
for the display id. This information is used by wgpu to obtain the required
660-
surface id. For Pyodide the required info is different.
666+
With method "screen", the context will render directly to a (virtual)
667+
surface. The dict should have a ``window`` field containing the window
668+
id. On Linux there should also be ``platform`` field to distinguish
669+
between "wayland" and "x11", and a ``display`` field for the display id.
670+
This information is used by wgpu to obtain the required surface id. For
671+
Pyodide the 'window' field should be the ``<canvas>`` object.
661672
662673
With method "bitmap", the context will present the result as an image
663-
bitmap. For the `WgpuContext`, the result will first be rendered to texture,
664-
and then downloaded to RAM. The sub-dict must have a
665-
field 'formats': a list of supported image formats. Examples are "rgba-u8"
666-
and "i-u8". A canvas must support at least "rgba-u8". Note that srgb mapping
674+
bitmap. For the ``WgpuContext``, the result will first be rendered to a
675+
texture, and then downloaded to RAM. The dict must have a field
676+
'formats': a list of supported image formats. Examples are "rgba-u8" and
677+
"i-u8". A canvas must support at least "rgba-u8". Note that srgb mapping
667678
is assumed to be handled by the canvas.
668679
"""
669-
raise NotImplementedError()
680+
return None
670681

671682
def _rc_request_draw(self):
672683
"""Request the GUI layer to perform a draw.

rendercanvas/glfw.py

Lines changed: 25 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -119,38 +119,38 @@
119119
}
120120

121121

122-
def get_glfw_present_methods(window):
122+
def get_glfw_present_info(window):
123123
if sys.platform.startswith("win"):
124124
return {
125-
"screen": {
126-
"platform": "windows",
127-
"window": int(glfw.get_win32_window(window)),
128-
}
125+
"method": "screen",
126+
"platform": "windows",
127+
"window": int(glfw.get_win32_window(window)),
129128
}
129+
130130
elif sys.platform.startswith("darwin"):
131131
return {
132-
"screen": {
133-
"platform": "cocoa",
134-
"window": int(glfw.get_cocoa_window(window)),
135-
}
132+
"method": "screen",
133+
"platform": "cocoa",
134+
"window": int(glfw.get_cocoa_window(window)),
136135
}
136+
137137
elif sys.platform.startswith("linux"):
138138
if api_is_wayland:
139139
return {
140-
"screen": {
141-
"platform": "wayland",
142-
"window": int(glfw.get_wayland_window(window)),
143-
"display": int(glfw.get_wayland_display()),
144-
}
140+
"method": "screen",
141+
"platform": "wayland",
142+
"window": int(glfw.get_wayland_window(window)),
143+
"display": int(glfw.get_wayland_display()),
145144
}
145+
146146
else:
147147
return {
148-
"screen": {
149-
"platform": "x11",
150-
"window": int(glfw.get_x11_window(window)),
151-
"display": int(glfw.get_x11_display()),
152-
}
148+
"method": "screen",
149+
"platform": "x11",
150+
"window": int(glfw.get_x11_window(window)),
151+
"display": int(glfw.get_x11_display()),
153152
}
153+
154154
else:
155155
raise RuntimeError(f"Cannot get GLFW surface info on {sys.platform}.")
156156

@@ -192,15 +192,10 @@ class GlfwRenderCanvas(BaseRenderCanvas):
192192

193193
_rc_canvas_group = GlfwCanvasGroup(loop)
194194

195-
def __init__(self, *args, present_method=None, **kwargs):
195+
def __init__(self, *args, **kwargs):
196196
enable_glfw()
197197
super().__init__(*args, **kwargs)
198198

199-
if present_method == "bitmap":
200-
logger.warning(
201-
"Ignoring present_method 'bitmap'; glfw can only render to screen"
202-
)
203-
204199
# Set window hints
205200
glfw.window_hint(glfw.CLIENT_API, glfw.NO_API)
206201
glfw.window_hint(glfw.RESIZABLE, True)
@@ -318,8 +313,11 @@ def _rc_gui_poll(self):
318313
self._is_in_poll_events = False
319314
self._maybe_close()
320315

321-
def _rc_get_present_methods(self):
322-
return get_glfw_present_methods(self._window)
316+
def _rc_get_present_info(self, present_methods):
317+
if "screen" in present_methods:
318+
return get_glfw_present_info(self._window)
319+
else:
320+
return None # raises error
323321

324322
def _rc_request_draw(self):
325323
if not self._is_minimized:

rendercanvas/jupyter.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,17 +49,19 @@ def get_frame(self):
4949
def _rc_gui_poll(self):
5050
pass
5151

52-
def _rc_get_present_methods(self):
53-
# We stick to the two common formats, because these can be easily converted to png
54-
# We assyme that srgb is used for perceptive color mapping. This is the
52+
def _rc_get_present_info(self, present_methods):
53+
# We stick to the a format, because these can be easily converted to png.
54+
# We assume that srgb is used for perceptive color mapping. This is the
5555
# common colorspace for e.g. png and jpg images. Most tools (browsers
5656
# included) will blit the png to screen as-is, and a screen wants colors
5757
# in srgb.
58-
return {
59-
"bitmap": {
58+
if "bitmap" in present_methods:
59+
return {
60+
"method": "bitmap",
6061
"formats": ["rgba-u8"],
6162
}
62-
}
63+
else:
64+
return None # raises error
6365

6466
def _rc_request_draw(self):
6567
self._draw_request_time = time.perf_counter()

rendercanvas/offscreen.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,14 @@ def __init__(self, *args, pixel_ratio=1.0, format="rgba-u8", **kwargs):
5252
def _rc_gui_poll(self):
5353
pass
5454

55-
def _rc_get_present_methods(self):
56-
return {
57-
"bitmap": {
55+
def _rc_get_present_info(self, present_methods):
56+
if "bitmap" in present_methods:
57+
return {
58+
"method": "bitmap",
5859
"formats": self._present_formats,
5960
}
60-
}
61+
else:
62+
return None # raises error
6163

6264
def _rc_request_draw(self):
6365
# Ok, cool, the scheduler want a draw. But we only draw when the user

rendercanvas/pyodide.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -427,18 +427,22 @@ def unregister_events():
427427
def _rc_gui_poll(self):
428428
pass # Nothing to be done; the JS loop is always running (and Pyodide wraps that in a global asyncio loop)
429429

430-
def _rc_get_present_methods(self):
431-
return {
432-
# Generic presentation
433-
"bitmap": {
434-
"formats": ["rgba-u8"],
435-
},
430+
def _rc_get_present_info(self, present_methods):
431+
if "screen" in present_methods:
436432
# wgpu-specific presentation. The wgpu.backends.pyodide.GPUCanvasContext must be able to consume this.
437-
"screen": {
433+
return {
434+
"method": "screen",
438435
"platform": "browser",
439436
"window": self._canvas_element, # Just provide the canvas object
440-
},
441-
}
437+
}
438+
elif "bitmap" in present_methods:
439+
# Generic presentation
440+
return {
441+
"method": "bitmap",
442+
"formats": ["rgba-u8"],
443+
}
444+
else:
445+
return None # raises error
442446

443447
def _rc_request_draw(self):
444448
window.requestAnimationFrame(

0 commit comments

Comments
 (0)