Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/wx_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def __init__(self):
super().__init__(None, title="wgpu triangle embedded in a wx app")
self.SetSize(640, 480)

# Using present_method 'image' because it reports "The surface texture is suboptimal"
# Using present_method 'bitmap' because it reports "The surface texture is suboptimal"
self.canvas = RenderWidget(
self, update_mode="continuous", present_method="bitmap"
)
Expand Down
103 changes: 57 additions & 46 deletions rendercanvas/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ class BaseRenderCanvas:
against screen tearing, but limits the fps. Default True.
present_method (str | None): Override the method to present the rendered result.
Can be set to 'screen' or 'bitmap'. Default None, which means that the method is selected
based on what the canvas supports and what the context prefers.
based on what the canvas and context support and prefer.

"""

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

# Handle custom present method
if not (present_method is None or isinstance(present_method, str)):
raise TypeError(
f"The canvas present_method should be None or str, not {present_method!r}."
)
self._present_method = present_method

# Variables and flags used internally
self.__is_drawing = False
self.__title_info = {
Expand Down Expand Up @@ -294,36 +301,34 @@ def get_context(self, context_type: str | type) -> contexts.BaseContext:
f"Cannot get context for '{context_name}': a context of type '{ref_context_name}' is already set."
)

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

# Select present_method
for present_method in context_class.present_methods:
assert present_method in ("bitmap", "screen")
if present_method in present_methods:
break
else:
# Let the canvas select the method and provide the corresponding info object.
# Take care not to hold onto this dict, it may contain objects that we don't want to unnecessarily reference.
info = self._rc_get_present_info(present_methods)
if info is None:
method_message = f"Methods {set(present_methods)!r} are not supported."
if len(present_methods) == 1:
method_message = f"Method {present_methods[0]!r} is not supported."
raise TypeError(
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}."
f"Could not create {context_name!r} for {self.__class__.__name__!r}: {method_message}"
)
if info.get("method") not in present_methods:
raise RuntimeError(
f"Present info method field ({info.get('method')!r}) is not part of the available methods {set(present_methods)}."
)
self._present_method = info["method"]

# Select present_info, and shape it into what the contexts need.
present_info = present_methods[present_method]
assert "method" not in present_info, (
"the field 'method' is reserved in present_methods dicts"
)
present_info = {
"method": present_method,
"source": self.__class__.__name__,
**present_info,
"vsync": self._vsync,
}
# Add some info
present_info = {**info, "source": self.__class__.__name__, "vsync": self._vsync}

# Create the context
self._canvas_context = context_class(present_info)
Expand Down Expand Up @@ -641,32 +646,38 @@ def _rc_gui_poll(self):
"""Process native events."""
pass

def _rc_get_present_methods(self):
"""Get info on the present methods supported by this canvas.
def _rc_get_present_info(self, present_methods: list[str]) -> dict | None:
"""Select a present method and return corresponding info dict.

This method is only called once, when the context is created. The
subclass can use this moment to setup the internal state for the
selected presentation method.

Must return a small dict, used by the canvas-context to determine
how the rendered result will be presented to the canvas.
This method is only called once, when the context is created.
The ``present_methods`` represents the supported methods of the
canvas-context, possibly filtered by a user-specified method. A canvas
backend must implement at least the "screen" or "bitmap" method.

Each supported method is represented by a field in the dict. The value
is another dict with information specific to that present method.
A canvas backend must implement at least either "screen" or "bitmap".
The returned dict must contain at least the key 'method', which must
match one of the ``present_methods``. The remaining values represent
information required by the canvas-context to perform the presentation,
and optionally some (debug) meta data. The backend may optionally return
None to indicate that none of the ``present_methods`` is supported.

With method "screen", the context will render directly to a surface
representing the region on the screen. The sub-dict should have a ``window``
field containing the window id. On Linux there should also be ``platform``
field to distinguish between "wayland" and "x11", and a ``display`` field
for the display id. This information is used by wgpu to obtain the required
surface id. For Pyodide the required info is different.
With method "screen", the context will render directly to a (virtual)
surface. The dict should have a ``window`` field containing the window
id. On Linux there should also be ``platform`` field to distinguish
between "wayland" and "x11", and a ``display`` field for the display id.
This information is used by wgpu to obtain the required surface id. For
Pyodide the 'window' field should be the ``<canvas>`` object.

With method "bitmap", the context will present the result as an image
bitmap. For the `WgpuContext`, the result will first be rendered to texture,
and then downloaded to RAM. The sub-dict must have a
field 'formats': a list of supported image formats. Examples are "rgba-u8"
and "i-u8". A canvas must support at least "rgba-u8". Note that srgb mapping
bitmap. For the ``WgpuContext``, the result will first be rendered to a
texture, and then downloaded to RAM. The dict must have a field
'formats': a list of supported image formats. Examples are "rgba-u8" and
"i-u8". A canvas must support at least "rgba-u8". Note that srgb mapping
is assumed to be handled by the canvas.
"""
raise NotImplementedError()
return None

def _rc_request_draw(self):
"""Request the GUI layer to perform a draw.
Expand Down
52 changes: 25 additions & 27 deletions rendercanvas/glfw.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,38 +119,38 @@
}


def get_glfw_present_methods(window):
def get_glfw_present_info(window):
if sys.platform.startswith("win"):
return {
"screen": {
"platform": "windows",
"window": int(glfw.get_win32_window(window)),
}
"method": "screen",
"platform": "windows",
"window": int(glfw.get_win32_window(window)),
}

elif sys.platform.startswith("darwin"):
return {
"screen": {
"platform": "cocoa",
"window": int(glfw.get_cocoa_window(window)),
}
"method": "screen",
"platform": "cocoa",
"window": int(glfw.get_cocoa_window(window)),
}

elif sys.platform.startswith("linux"):
if api_is_wayland:
return {
"screen": {
"platform": "wayland",
"window": int(glfw.get_wayland_window(window)),
"display": int(glfw.get_wayland_display()),
}
"method": "screen",
"platform": "wayland",
"window": int(glfw.get_wayland_window(window)),
"display": int(glfw.get_wayland_display()),
}

else:
return {
"screen": {
"platform": "x11",
"window": int(glfw.get_x11_window(window)),
"display": int(glfw.get_x11_display()),
}
"method": "screen",
"platform": "x11",
"window": int(glfw.get_x11_window(window)),
"display": int(glfw.get_x11_display()),
}

else:
raise RuntimeError(f"Cannot get GLFW surface info on {sys.platform}.")

Expand Down Expand Up @@ -192,15 +192,10 @@ class GlfwRenderCanvas(BaseRenderCanvas):

_rc_canvas_group = GlfwCanvasGroup(loop)

def __init__(self, *args, present_method=None, **kwargs):
def __init__(self, *args, **kwargs):
enable_glfw()
super().__init__(*args, **kwargs)

if present_method == "bitmap":
logger.warning(
"Ignoring present_method 'bitmap'; glfw can only render to screen"
)

# Set window hints
glfw.window_hint(glfw.CLIENT_API, glfw.NO_API)
glfw.window_hint(glfw.RESIZABLE, True)
Expand Down Expand Up @@ -318,8 +313,11 @@ def _rc_gui_poll(self):
self._is_in_poll_events = False
self._maybe_close()

def _rc_get_present_methods(self):
return get_glfw_present_methods(self._window)
def _rc_get_present_info(self, present_methods):
if "screen" in present_methods:
return get_glfw_present_info(self._window)
else:
return None # raises error

def _rc_request_draw(self):
if not self._is_minimized:
Expand Down
14 changes: 8 additions & 6 deletions rendercanvas/jupyter.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,17 +49,19 @@ def get_frame(self):
def _rc_gui_poll(self):
pass

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

def _rc_request_draw(self):
self._draw_request_time = time.perf_counter()
Expand Down
10 changes: 6 additions & 4 deletions rendercanvas/offscreen.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,14 @@ def __init__(self, *args, pixel_ratio=1.0, format="rgba-u8", **kwargs):
def _rc_gui_poll(self):
pass

def _rc_get_present_methods(self):
return {
"bitmap": {
def _rc_get_present_info(self, present_methods):
if "bitmap" in present_methods:
return {
"method": "bitmap",
"formats": self._present_formats,
}
}
else:
return None # raises error

def _rc_request_draw(self):
# Ok, cool, the scheduler want a draw. But we only draw when the user
Expand Down
22 changes: 13 additions & 9 deletions rendercanvas/pyodide.py
Original file line number Diff line number Diff line change
Expand Up @@ -427,18 +427,22 @@ def unregister_events():
def _rc_gui_poll(self):
pass # Nothing to be done; the JS loop is always running (and Pyodide wraps that in a global asyncio loop)

def _rc_get_present_methods(self):
return {
# Generic presentation
"bitmap": {
"formats": ["rgba-u8"],
},
def _rc_get_present_info(self, present_methods):
if "screen" in present_methods:
# wgpu-specific presentation. The wgpu.backends.pyodide.GPUCanvasContext must be able to consume this.
"screen": {
return {
"method": "screen",
"platform": "browser",
"window": self._canvas_element, # Just provide the canvas object
},
}
}
elif "bitmap" in present_methods:
# Generic presentation
return {
"method": "bitmap",
"formats": ["rgba-u8"],
}
else:
return None # raises error

def _rc_request_draw(self):
window.requestAnimationFrame(
Expand Down
Loading