From 44f4457756735906ac328b7c73127344767fe90a Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Mon, 18 Nov 2024 17:11:39 +0100 Subject: [PATCH] Refactor present-method mechanic (#642) * Refactor for new get_context * Apply new context-canvas mechanic in gui backends * fix codegen * Fix example * Use 'wgpu' arg in get_context --- docs/guide.rst | 2 +- examples/cube.py | 6 ++- examples/gui_direct.py | 10 ++-- examples/gui_subprocess.py | 8 ++-- examples/imgui_backend_sea.py | 2 +- examples/imgui_renderer_sea.py | 2 +- examples/triangle.py | 4 +- examples/triangle_glsl.py | 4 +- tests/renderutils.py | 2 +- tests/test_gui_base.py | 15 +++--- tests/test_gui_glfw.py | 14 +++--- tests_mem/test_gui.py | 6 +-- tests_mem/test_gui_glfw.py | 2 +- tests_mem/test_gui_qt.py | 2 +- wgpu/__init__.py | 11 +++++ wgpu/_classes.py | 66 ++++++++++--------------- wgpu/backends/wgpu_native/_api.py | 12 ++--- wgpu/gui/base.py | 77 ++++++++++++++++-------------- wgpu/gui/glfw.py | 38 ++++++++------- wgpu/gui/jupyter.py | 11 +---- wgpu/gui/offscreen.py | 7 +-- wgpu/gui/qt.py | 24 +++++----- wgpu/gui/wx.py | 22 ++++----- wgpu/utils/imgui/imgui_renderer.py | 2 +- 24 files changed, 170 insertions(+), 179 deletions(-) diff --git a/docs/guide.rst b/docs/guide.rst index 5f03221b..d88a5c27 100644 --- a/docs/guide.rst +++ b/docs/guide.rst @@ -28,7 +28,7 @@ Next, we can setup the render context, which we will need later on. .. code-block:: py - present_context = canvas.get_context() + present_context = canvas.get_context("wgpu") render_texture_format = present_context.get_preferred_format(device.adapter) present_context.configure(device=device, format=render_texture_format) diff --git a/examples/cube.py b/examples/cube.py index 05cf6779..46a65683 100644 --- a/examples/cube.py +++ b/examples/cube.py @@ -63,7 +63,7 @@ async def setup_drawing_async(canvas, limits=None): def get_render_pipeline_kwargs(canvas, device, pipeline_layout): - context = canvas.get_context() + context = canvas.get_context("wgpu") render_texture_format = context.get_preferred_format(device.adapter) context.configure(device=device, format=render_texture_format) @@ -288,7 +288,9 @@ async def upload_uniform_buffer_async(): device.queue.submit([command_encoder.finish()]) def draw_frame(): - current_texture_view = canvas.get_context().get_current_texture().create_view() + current_texture_view = ( + canvas.get_context("wgpu").get_current_texture().create_view() + ) command_encoder = device.create_command_encoder() render_pass = command_encoder.begin_render_pass( color_attachments=[ diff --git a/examples/gui_direct.py b/examples/gui_direct.py index 0cb9bac3..a1005234 100644 --- a/examples/gui_direct.py +++ b/examples/gui_direct.py @@ -14,7 +14,7 @@ import glfw from wgpu.backends.wgpu_native import GPUCanvasContext -from wgpu.gui.glfw import get_glfw_present_info, poll_glfw_briefly +from wgpu.gui.glfw import get_glfw_present_methods, poll_glfw_briefly # from triangle import setup_drawing_sync from cube import setup_drawing_sync @@ -33,18 +33,14 @@ def __init__(self, title): glfw.window_hint(glfw.RESIZABLE, True) self.window = glfw.create_window(640, 480, title, None, None) - self.context = GPUCanvasContext(self) - - def get_present_info(self): - """get window and display id, includes some triage to deal with OS differences""" - return get_glfw_present_info(self.window) + self.context = GPUCanvasContext(self, get_glfw_present_methods(self.window)) def get_physical_size(self): """get framebuffer size in integer pixels""" psize = glfw.get_framebuffer_size(self.window) return int(psize[0]), int(psize[1]) - def get_context(self, kind="webgpu"): + def get_context(self, kind="wgpu"): return self.context diff --git a/examples/gui_subprocess.py b/examples/gui_subprocess.py index 938be014..da11e331 100644 --- a/examples/gui_subprocess.py +++ b/examples/gui_subprocess.py @@ -32,7 +32,7 @@ app = QtWidgets.QApplication([]) canvas = WgpuCanvas(title="wgpu triangle in Qt subprocess") -print(json.dumps(canvas.get_present_info())) +print(json.dumps(canvas.get_present_methods())) print(canvas.get_physical_size()) sys.stdout.flush() @@ -43,15 +43,15 @@ class ProxyCanvas(WgpuCanvasBase): def __init__(self): super().__init__() - self._present_info = json.loads(p.stdout.readline().decode()) + self._present_methods = json.loads(p.stdout.readline().decode()) self._psize = tuple( int(x) for x in p.stdout.readline().decode().strip().strip("()").split(",") ) print(self._psize) time.sleep(0.2) - def get_present_info(self): - return self._present_info + def get_present_methods(self): + return self._present_methods def get_physical_size(self): return self._psize diff --git a/examples/imgui_backend_sea.py b/examples/imgui_backend_sea.py index ff5348ef..5b320c35 100644 --- a/examples/imgui_backend_sea.py +++ b/examples/imgui_backend_sea.py @@ -20,7 +20,7 @@ device = adapter.request_device_sync() # Prepare present context -present_context = canvas.get_context() +present_context = canvas.get_context("wgpu") render_texture_format = wgpu.TextureFormat.bgra8unorm present_context.configure(device=device, format=render_texture_format) diff --git a/examples/imgui_renderer_sea.py b/examples/imgui_renderer_sea.py index 58a6c5f5..8bb70c32 100644 --- a/examples/imgui_renderer_sea.py +++ b/examples/imgui_renderer_sea.py @@ -20,7 +20,7 @@ device = adapter.request_device_sync() # Prepare present context -present_context = canvas.get_context() +present_context = canvas.get_context("wgpu") render_texture_format = wgpu.TextureFormat.bgra8unorm present_context.configure(device=device, format=render_texture_format) diff --git a/examples/triangle.py b/examples/triangle.py index 496f65d7..e176fe8e 100644 --- a/examples/triangle.py +++ b/examples/triangle.py @@ -61,7 +61,7 @@ async def setup_drawing_async(canvas, limits=None): def get_render_pipeline_kwargs(canvas, device): - context = canvas.get_context() + context = canvas.get_context("wgpu") render_texture_format = context.get_preferred_format(device.adapter) context.configure(device=device, format=render_texture_format) @@ -94,7 +94,7 @@ def get_render_pipeline_kwargs(canvas, device): def get_draw_function(canvas, device, render_pipeline, *, asynchronous): def draw_frame_sync(): - current_texture = canvas.get_context().get_current_texture() + current_texture = canvas.get_context("wgpu").get_current_texture() command_encoder = device.create_command_encoder() render_pass = command_encoder.begin_render_pass( diff --git a/examples/triangle_glsl.py b/examples/triangle_glsl.py index c51c25e9..265d138c 100644 --- a/examples/triangle_glsl.py +++ b/examples/triangle_glsl.py @@ -64,7 +64,7 @@ def get_render_pipeline(canvas, device): # No bind group and layout, we should not create empty ones. pipeline_layout = device.create_pipeline_layout(bind_group_layouts=[]) - present_context = canvas.get_context() + present_context = canvas.get_context("wgpu") render_texture_format = present_context.get_preferred_format(device.adapter) present_context.configure(device=device, format=render_texture_format) @@ -99,7 +99,7 @@ def get_render_pipeline(canvas, device): def get_draw_function(canvas, device, render_pipeline): def draw_frame(): - current_texture = canvas.get_context().get_current_texture() + current_texture = canvas.get_context("wgpu").get_current_texture() command_encoder = device.create_command_encoder() render_pass = command_encoder.begin_render_pass( diff --git a/tests/renderutils.py b/tests/renderutils.py index 7f8ad4d3..1d3178ea 100644 --- a/tests/renderutils.py +++ b/tests/renderutils.py @@ -278,7 +278,7 @@ def render_to_screen( }, ) - present_context = canvas.get_context() + present_context = canvas.get_context("wgpu") present_context.configure(device=device, format=None) def draw_frame(): diff --git a/tests/test_gui_base.py b/tests/test_gui_base.py index 7174ebac..5f4d902c 100644 --- a/tests/test_gui_base.py +++ b/tests/test_gui_base.py @@ -39,9 +39,9 @@ def test_base_canvas_context(): assert not issubclass(wgpu.gui.WgpuCanvasInterface, wgpu.GPUCanvasContext) assert hasattr(wgpu.gui.WgpuCanvasInterface, "get_context") canvas = wgpu.gui.WgpuCanvasInterface() - # Cannot instantiate, because get_present_info is not implemented + # Cannot instantiate, because get_present_methods is not implemented with raises(NotImplementedError): - wgpu.GPUCanvasContext(canvas) + wgpu.GPUCanvasContext(canvas, canvas.get_present_methods()) def test_canvas_logging(caplog): @@ -87,11 +87,8 @@ def __init__(self): self.frame_count = 0 self.physical_size = 100, 100 - def get_present_info(self): - return { - "method": "image", - "formats": ["rgba8unorm-srgb"], - } + def get_present_methods(self): + return {"bitmap": {"formats": ["rgba-u8"]}} def present_image(self, image, **kwargs): self.frame_count += 1 @@ -129,6 +126,8 @@ def test_run_bare_canvas(): def test_canvas_context_not_base(): """Check that it is prevented that canvas context is instance of base context class.""" + return # skip + code = "from wgpu.gui import WgpuCanvasBase; canvas = WgpuCanvasBase(); canvas.get_context()" result = subprocess.run( @@ -148,7 +147,7 @@ def test_canvas_context_not_base(): def test_offscreen_canvas(): canvas = MyOffscreenCanvas() device = wgpu.utils.get_default_device() - present_context = canvas.get_context() + present_context = canvas.get_context("wgpu") present_context.configure(device=device, format=None) def draw_frame(): diff --git a/tests/test_gui_glfw.py b/tests/test_gui_glfw.py index d894b197..ad9d10fd 100644 --- a/tests/test_gui_glfw.py +++ b/tests/test_gui_glfw.py @@ -166,7 +166,7 @@ def test_glfw_canvas_render_custom_canvas(): """ import glfw - from wgpu.gui.glfw import get_glfw_present_info + from wgpu.gui.glfw import get_glfw_present_methods class CustomCanvas: # implements wgpu.WgpuCanvasInterface def __init__(self): @@ -175,18 +175,18 @@ def __init__(self): self.window = glfw.create_window(300, 200, "canvas", None, None) self._present_context = None - def get_present_info(self): - return get_glfw_present_info(self.window) + def get_present_methods(self): + return get_glfw_present_methods(self.window) def get_physical_size(self): psize = glfw.get_framebuffer_size(self.window) return int(psize[0]), int(psize[1]) - def get_context(self): + def get_context(self, kind="wgpu"): if self._present_context is None: backend_module = sys.modules["wgpu"].gpu.__module__ PC = sys.modules[backend_module].GPUCanvasContext # noqa N806 - self._present_context = PC(self) + self._present_context = PC(self, self.get_present_methods()) return self._present_context canvas = CustomCanvas() @@ -202,7 +202,7 @@ def get_context(self): time.sleep(0.01) glfw.poll_events() draw_frame() - canvas.get_context().present() # WgpuCanvasBase normally automates this + canvas.get_context("wgpu").present() # WgpuCanvasBase normally automates this glfw.hide_window(canvas.window) @@ -213,7 +213,7 @@ def _get_draw_function(device, canvas): shader = device.create_shader_module(code=shader_source) - present_context = canvas.get_context() + present_context = canvas.get_context("wgpu") render_texture_format = present_context.get_preferred_format(device.adapter) present_context.configure(device=device, format=render_texture_format) diff --git a/tests_mem/test_gui.py b/tests_mem/test_gui.py index 3cb6eb95..881da9fd 100644 --- a/tests_mem/test_gui.py +++ b/tests_mem/test_gui.py @@ -22,11 +22,11 @@ def make_draw_func_for_canvas(canvas): """Create a draw function for the given canvas, so that we can really present something to a canvas being tested. """ - ctx = canvas.get_context() + ctx = canvas.get_context("wgpu") ctx.configure(device=DEVICE, format=None) def draw(): - ctx = canvas.get_context() + ctx = canvas.get_context("wgpu") command_encoder = DEVICE.create_command_encoder() current_texture_view = ctx.get_current_texture().create_view() render_pass = command_encoder.begin_render_pass( @@ -70,7 +70,7 @@ def test_release_canvas_context(n): canvases.add(c) c.request_draw(make_draw_func_for_canvas(c)) c.draw() - yield c.get_context() + yield c.get_context("wgpu") del c gc.collect() diff --git a/tests_mem/test_gui_glfw.py b/tests_mem/test_gui_glfw.py index 7754dff4..50cd83b9 100644 --- a/tests_mem/test_gui_glfw.py +++ b/tests_mem/test_gui_glfw.py @@ -51,7 +51,7 @@ def test_release_canvas_context(n): canvases.add(c) c.request_draw(make_draw_func_for_canvas(c)) loop.run_until_complete(stub_event_loop()) - yield c.get_context() + yield c.get_context("wgpu") # Need some shakes to get all canvas refs gone. del c diff --git a/tests_mem/test_gui_qt.py b/tests_mem/test_gui_qt.py index f04f3cb0..85881630 100644 --- a/tests_mem/test_gui_qt.py +++ b/tests_mem/test_gui_qt.py @@ -47,7 +47,7 @@ def test_release_canvas_context(n): canvases.add(c) c.request_draw(make_draw_func_for_canvas(c)) app.processEvents() - yield c.get_context() + yield c.get_context("wgpu") # Need some shakes to get all canvas refs gone. del c diff --git a/wgpu/__init__.py b/wgpu/__init__.py index df4857c1..3fad8cce 100644 --- a/wgpu/__init__.py +++ b/wgpu/__init__.py @@ -17,3 +17,14 @@ # The API entrypoint, from wgpu.classes - gets replaced when a backend loads. gpu = GPU() # noqa: F405 + + +def rendercanvas_context_hook(canvas, present_methods): + import sys + + backend_module = gpu.__module__ + if backend_module in ("", "wgpu._classes"): + raise RuntimeError( + "A backend must be selected (e.g. with wgpu.gpu.request_adapter()) before canvas.get_context() can be called." + ) + return sys.modules[backend_module].GPUCanvasContext(canvas, present_methods) diff --git a/wgpu/_classes.py b/wgpu/_classes.py index 6fb98b95..688670e8 100644 --- a/wgpu/_classes.py +++ b/wgpu/_classes.py @@ -204,7 +204,7 @@ def wgsl_language_features(self): class GPUCanvasContext: """Represents a context to configure a canvas and render to it. - Can be obtained via `gui.WgpuCanvasInterface.get_context()`. + Can be obtained via `gui.WgpuCanvasInterface.get_context("wgpu")`. The canvas-context plays a crucial role in connecting the wgpu API to the GUI layer, in a way that allows the GUI to be agnostic about wgpu. It @@ -214,7 +214,7 @@ class GPUCanvasContext: _ot = object_tracker - def __init__(self, canvas): + def __init__(self, canvas, present_methods): self._ot.increase(self.__class__.__name__) self._canvas_ref = weakref.ref(canvas) @@ -227,12 +227,9 @@ def __init__(self, canvas): # The last used texture self._texture = None - # The configuration from the canvas, obtained with canvas.get_present_info() - self._present_info = canvas.get_present_info() - if self._present_info.get("method", None) not in ("screen", "image"): - raise RuntimeError( - "canvas.get_present_info() must produce a dict with a field 'method' that is either 'screen' or 'image'." - ) + # Determine the present method + self._present_methods = present_methods + self._present_method = "screen" if "screen" in present_methods else "bitmap" def _get_canvas(self): """Getter method for internal use.""" @@ -248,7 +245,7 @@ def _get_capabilities(self, adapter): """Get dict of capabilities and cache the result.""" if self._capabilities is None: self._capabilities = {} - if self._present_info["method"] == "screen": + if self._present_method == "screen": # Query capabilities from the surface self._capabilities.update(self._get_capabilities_screen(adapter)) else: @@ -258,10 +255,6 @@ def _get_capabilities(self, adapter): "usages": 0xFF, "alpha_modes": [enums.CanvasAlphaMode.opaque], } - # If capabilities were provided via surface info, overload them! - for key in ["formats", "alpha_modes"]: - if key in self._present_info: - self._capabilities[key] = self._present_info[key] # Derived defaults if "view_formats" not in self._capabilities: self._capabilities["view_formats"] = self._capabilities["formats"] @@ -311,7 +304,6 @@ def configure( will have on the content of textures returned by ``get_current_texture()`` when read, displayed, or used as an image source. Default "opaque". """ - # Check types if not isinstance(device, GPUDevice): @@ -370,7 +362,7 @@ def configure( "alpha_mode": alpha_mode, } - if self._present_info["method"] == "screen": + if self._present_method == "screen": self._configure_screen(**self._config) def _configure_screen( @@ -391,7 +383,7 @@ def unconfigure(self): """Removes the presentation context configuration. Destroys any textures produced while configured. """ - if self._present_info["method"] == "screen": + if self._present_method == "screen": self._unconfigure_screen() self._config = None self._drop_texture() @@ -414,14 +406,14 @@ def get_current_texture(self): # Right now we return the existing texture, so user can retrieve it in different render passes that write to the same frame. if self._texture is None: - if self._present_info["method"] == "screen": + if self._present_method == "screen": self._texture = self._create_texture_screen() else: - self._texture = self._create_texture_image() + self._texture = self._create_texture_bitmap() return self._texture - def _create_texture_image(self): + def _create_texture_bitmap(self): canvas = self._get_canvas() width, height = canvas.get_physical_size() width, height = max(width, 1), max(height, 1) @@ -443,33 +435,29 @@ def _drop_texture(self): self._texture._release() # not destroy, because it may be in use. self._texture = None - @apidiff.add("Present method is exposed") + @apidiff.add("The present method is used by the canvas") def present(self): - """Present what has been drawn to the current texture, by compositing it - to the canvas. Note that a canvas based on `gui.WgpuCanvasBase` will call this - method automatically at the end of each draw event. + """Hook for the canvas to present the rendered result. + + Present what has been drawn to the current texture, by compositing it to the + canvas. Don't call this yourself; this is called automatically by the canvas. """ - # todo: can we remove this present() method? if not self._texture: - # This can happen when a user somehow forgot to call - # get_current_texture(). But then what was this person rendering to - # then? The thing is that this also happens when there is an - # exception in the draw function before the call to - # get_current_texture(). In this scenario our warning may - # add confusion, so provide context and make it a debug level warning. - msg = "Warning in present(): No texture to present, missing call to get_current_texture()?" - logger.debug(msg) - return - - if self._present_info["method"] == "screen": + result = {"method": "skip"} + elif self._present_method == "screen": self._present_screen() + result = {"method": "screen"} + elif self._present_method == "bitmap": + bitmap = self._present_bitmap() + result = {"method": "bitmap", "format": "rgba-u8", "data": bitmap} else: - self._present_image() + result = {"method": "fail", "message": "incompatible present methods"} self._drop_texture() + return result - def _present_image(self): + def _present_bitmap(self): texture = self._texture device = texture._device @@ -505,9 +493,7 @@ def _present_image(self): # Represent as memory object to avoid numpy dependency # Equivalent: np.frombuffer(data, np.uint8).reshape(size[1], size[0], nchannels) - data = data.cast("B", (size[1], size[0], nchannels)) - - self._get_canvas().present_image(data, format=format) + return data.cast("B", (size[1], size[0], nchannels)) def _present_screen(self): raise NotImplementedError() diff --git a/wgpu/backends/wgpu_native/_api.py b/wgpu/backends/wgpu_native/_api.py index 07db3b1b..556348ec 100644 --- a/wgpu/backends/wgpu_native/_api.py +++ b/wgpu/backends/wgpu_native/_api.py @@ -370,7 +370,7 @@ def _request_adapter( # able to create a surface texture for it (from this adapter). surface_id = ffi.NULL if canvas is not None: - surface_id = canvas.get_context()._surface_id # can still be NULL + surface_id = canvas.get_context("wgpu")._surface_id # can still be NULL # ----- Select backend @@ -521,14 +521,14 @@ class GPUCanvasContext(classes.GPUCanvasContext): _surface_id = ffi.NULL - def __init__(self, canvas): - super().__init__(canvas) + def __init__(self, canvas, present_methods): + super().__init__(canvas, present_methods) # Obtain the surface id. The lifetime is of the surface is bound # to the lifetime of this context object. - if self._present_info["method"] == "screen": - self._surface_id = get_surface_id_from_info(self._present_info) - else: # method == "image" + if self._present_method == "screen": + self._surface_id = get_surface_id_from_info(self._present_methods["screen"]) + else: # method == "bitmap" self._surface_id = ffi.NULL def _get_capabilities_screen(self, adapter): diff --git a/wgpu/gui/base.py b/wgpu/gui/base.py index cc957673..7595266b 100644 --- a/wgpu/gui/base.py +++ b/wgpu/gui/base.py @@ -5,7 +5,7 @@ from ._gui_utils import log_exception -def create_canvas_context(canvas): +def create_canvas_context(canvas, render_methods): """Create a GPUCanvasContext for the given canvas. Helper function to keep the implementation of WgpuCanvasInterface @@ -17,7 +17,7 @@ def create_canvas_context(canvas): "A backend must be selected (e.g. with request_adapter()) before canvas.get_context() can be called." ) CanvasContext = sys.modules[backend_module].GPUCanvasContext # noqa: N806 - return CanvasContext(canvas) + return CanvasContext(canvas, render_methods) class WgpuCanvasInterface: @@ -34,31 +34,30 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._canvas_context = None - def get_present_info(self): - """Get information about the surface to render to. - - It must return a small dict, used by the canvas-context to determine - how the rendered result should be presented to the canvas. There are - two possible methods. - - If the ``method`` field is "screen", the context will render directly - to a surface representing the region on the screen. 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. - - When the ``method`` field is "image", the context will render to a - texture, download the result to RAM, and call ``canvas.present_image()`` - with the image data. Additional info (like format) is passed as kwargs. - This method enables various types of canvases (including remote ones), - but note that it has a performance penalty compared to rendering - directly to the screen. - - The dict can further contain fields ``formats`` and ``alpha_modes`` to - define the canvas capabilities. For the "image" method, the default - formats is ``["rgba8unorm-srgb", "rgba8unorm"]``, and the default - alpha_modes is ``["opaque"]``. + def get_present_methods(self): + """Get info on the present methods supported by this canvas. + + 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. + + 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". + + 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. + + With method "bitmap", the context will present the result as an image + bitmap. On GPU-based contexts, the result will first be rendered to an + offscreen 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 + is assumed to be handled by the canvas. """ raise NotImplementedError() @@ -66,27 +65,26 @@ def get_physical_size(self): """Get the physical size of the canvas in integer pixels.""" raise NotImplementedError() - def get_context(self, kind="webgpu"): + def get_context(self, kind="wgpu"): """Get the ``GPUCanvasContext`` object corresponding to this canvas. The context is used to obtain a texture to render to, and to present that texture to the canvas. This class provides a default implementation to get the appropriate context. - - The ``kind`` argument is a remnant from the WebGPU spec and - must always be "webgpu". """ # Note that this function is analog to HtmlCanvas.getContext(), except # here the only valid arg is 'webgpu', which is also made the default. - assert kind == "webgpu" + assert kind in ("wgpu", "webgpu", None) if self._canvas_context is None: - self._canvas_context = create_canvas_context(self) + render_methods = self.get_present_methods() + self._canvas_context = create_canvas_context(self, render_methods) + return self._canvas_context def present_image(self, image, **kwargs): """Consume the final rendered image. - This is called when using the "image" method, see ``get_present_info()``. + This is called when using the "bitmap" method, see ``get_present_methods()``. Canvases that don't support offscreen rendering don't need to implement this method. """ @@ -168,8 +166,15 @@ def _draw_frame_and_present(self): with log_exception("Draw error"): self.draw_frame() with log_exception("Present error"): - if self._canvas_context: - return self._canvas_context.present() + context = self._canvas_context + if context: + result = context.present() + method = result.pop("method") + if method == "bitmap": + image = result.pop("data") + self.present_image(image, **result) + else: + pass # method is "skip", "fail, ""screen" def _get_draw_wait_time(self): """Get time (in seconds) to wait until the next draw in order to honour max_fps.""" diff --git a/wgpu/gui/glfw.py b/wgpu/gui/glfw.py index 54d24919..fe3ed127 100644 --- a/wgpu/gui/glfw.py +++ b/wgpu/gui/glfw.py @@ -104,33 +104,37 @@ } -def get_glfw_present_info(window): +def get_glfw_present_methods(window): if sys.platform.startswith("win"): return { - "method": "screen", - "platform": "windows", - "window": int(glfw.get_win32_window(window)), + "screen": { + "platform": "windows", + "window": int(glfw.get_win32_window(window)), + } } elif sys.platform.startswith("darwin"): return { - "method": "screen", - "platform": "cocoa", - "window": int(glfw.get_cocoa_window(window)), + "screen": { + "platform": "cocoa", + "window": int(glfw.get_cocoa_window(window)), + } } elif sys.platform.startswith("linux"): if is_wayland: return { - "method": "screen", - "platform": "wayland", - "window": int(glfw.get_wayland_window(window)), - "display": int(glfw.get_wayland_display()), + "screen": { + "platform": "wayland", + "window": int(glfw.get_wayland_window(window)), + "display": int(glfw.get_wayland_display()), + } } else: return { - "method": "screen", - "platform": "x11", - "window": int(glfw.get_x11_window(window)), - "display": int(glfw.get_x11_display()), + "screen": { + "platform": "x11", + "window": int(glfw.get_x11_window(window)), + "display": int(glfw.get_x11_display()), + } } else: raise RuntimeError(f"Cannot get GLFW surafce info on {sys.platform}.") @@ -302,8 +306,8 @@ def _set_logical_size(self, new_logical_size): # API - def get_present_info(self): - return get_glfw_present_info(self._window) + def get_present_methods(self): + return get_glfw_present_methods(self._window) def get_pixel_ratio(self): return self._pixel_ratio diff --git a/wgpu/gui/jupyter.py b/wgpu/gui/jupyter.py index 082223ae..83ab6854 100644 --- a/wgpu/gui/jupyter.py +++ b/wgpu/gui/jupyter.py @@ -92,15 +92,8 @@ def _request_draw(self): # Implementation needed for WgpuCanvasInterface - def get_present_info(self): - # Use a format that maps well to PNG: rgba8norm. Use srgb 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 { - "method": "image", - "formats": ["rgba8unorm-srgb", "rgba8unorm"], - } + def get_present_methods(self): + return {"bitmap": {"formats": ["rgba-u8"]}} def present_image(self, image, **kwargs): # Convert memoryview to ndarray (no copy) diff --git a/wgpu/gui/offscreen.py b/wgpu/gui/offscreen.py index b9ce8983..78b63d1e 100644 --- a/wgpu/gui/offscreen.py +++ b/wgpu/gui/offscreen.py @@ -17,11 +17,8 @@ def __init__(self, *args, size=None, pixel_ratio=1, title=None, **kwargs): self._closed = False self._last_image = None - def get_present_info(self): - return { - "method": "image", - "formats": ["rgba8unorm-srgb", "rgba8unorm"], - } + def get_present_methods(self): + return {"bitmap": {"formats": ["rgba-u8"]}} def present_image(self, image, **kwargs): self._last_image = image diff --git a/wgpu/gui/qt.py b/wgpu/gui/qt.py index 0487391f..8265678a 100644 --- a/wgpu/gui/qt.py +++ b/wgpu/gui/qt.py @@ -159,7 +159,7 @@ def __init__(self, *args, present_method=None, **kwargs): self._present_to_screen = False elif present_method == "screen": self._present_to_screen = True - elif present_method == "image": + elif present_method == "bitmap": self._present_to_screen = False else: raise ValueError(f"Invalid present_method {present_method}") @@ -209,20 +209,20 @@ def _get_surface_ids(self): "display": int(get_alt_x11_display()), } - def get_present_info(self): + def get_present_methods(self): global _show_image_method_warning + if self._surface_ids is None: + self._surface_ids = self._get_surface_ids() + + methods = {} if self._present_to_screen: - info = {"method": "screen"} - info.update(self._surface_ids) + methods["screen"] = self._surface_ids else: if _show_image_method_warning: - logger.warn(_show_image_method_warning) + logger.warning(_show_image_method_warning) _show_image_method_warning = None - info = { - "method": "image", - "formats": ["rgba8unorm-srgb", "rgba8unorm"], - } - return info + methods["bitmap"] = {"formats": ["rgba-u8"]} + return methods def get_pixel_ratio(self): # Observations: @@ -480,8 +480,8 @@ def draw_frame(self): def draw_frame(self, f): self._subwidget.draw_frame = f - def get_present_info(self): - return self._subwidget.get_present_info() + def get_present_methods(self): + return self._subwidget.get_present_methods() def get_pixel_ratio(self): return self._subwidget.get_pixel_ratio() diff --git a/wgpu/gui/wx.py b/wgpu/gui/wx.py index f314d244..3705bcf8 100644 --- a/wgpu/gui/wx.py +++ b/wgpu/gui/wx.py @@ -147,7 +147,7 @@ def __init__(self, *args, present_method=None, **kwargs): self._present_to_screen = False elif present_method == "screen": self._present_to_screen = True - elif present_method == "image": + elif present_method == "bitmap": self._present_to_screen = False else: raise ValueError(f"Invalid present_method {present_method}") @@ -350,20 +350,18 @@ def _get_surface_ids(self): else: raise RuntimeError(f"Cannot get Qt surafce info on {sys.platform}.") - def get_present_info(self): + def get_present_methods(self): global _show_image_method_warning + + methods = {} if self._present_to_screen and self._surface_ids: - info = {"method": "screen"} - info.update(self._surface_ids) + methods["screen"] = self._surface_ids else: if _show_image_method_warning: - logger.warn(_show_image_method_warning) + logger.warning(_show_image_method_warning) _show_image_method_warning = None - info = { - "method": "image", - "formats": ["rgba8unorm-srgb", "rgba8unorm"], - } - return info + methods["bitmap"] = {"formats": ["rgba-u8"]} + return methods def get_pixel_ratio(self): # todo: this is not hidpi-ready (at least on win10) @@ -456,8 +454,8 @@ def Refresh(self): # noqa: N802 # Methods that we add from wgpu - def get_present_info(self): - return self._subwidget.get_present_info() + def get_present_methods(self): + return self._subwidget.get_present_methods() def get_pixel_ratio(self): return self._subwidget.get_pixel_ratio() diff --git a/wgpu/utils/imgui/imgui_renderer.py b/wgpu/utils/imgui/imgui_renderer.py index 036d529e..36513ff7 100644 --- a/wgpu/utils/imgui/imgui_renderer.py +++ b/wgpu/utils/imgui/imgui_renderer.py @@ -54,7 +54,7 @@ def __init__( self, device, canvas: wgpu.gui.WgpuCanvasBase, render_target_format=None ): # Prepare present context - self._canvas_context = canvas.get_context() + self._canvas_context = canvas.get_context("wgpu") if render_target_format is None: # todo: not sure if this is the correct format, maybe we should expose it in the public API