diff --git a/docs/guide.rst b/docs/guide.rst index f8107234..980989aa 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("wgpu") + present_context = canvas.get_wgpu_context() render_texture_format = present_context.get_preferred_format(device.adapter) present_context.configure(device=device, format=render_texture_format) diff --git a/docs/wgpu.rst b/docs/wgpu.rst index 2b72ee55..375216e3 100644 --- a/docs/wgpu.rst +++ b/docs/wgpu.rst @@ -72,16 +72,14 @@ The async methods return a :class:`GPUPromise`, which resolves to the actual res * In sync code, you can use ``promise.sync_wait()``. This is similar to the ``_sync()`` flavour mentioned above (it makes your code less portable). -Canvas API ----------- - -In order for wgpu to render to a canvas (which can be on screen, inside a GUI, offscreen, etc.), -a canvas object is needed. We recommend using the `rendercanvas `_ library to get a wide variety of canvases. - -That said, the canvas object can be any object, as long as it adheres to the -``WgpuCanvasInterface``, see https://github.com/pygfx/wgpu-py/blob/main/wgpu/_canvas.py for details. +Rendering to a canvas +--------------------- +In order for wgpu to render to a canvas (which can be on screen, inside a GUI, +offscreen, etc.), we highly recommend using the `rendercanvas `_ library. +One can then use ``canvas.get_wgpu_context()`` to get a `WgpuContext `_. +For more low-level control, use ```wgpu.gpu.get_canvas_context()`` to get a :class:`GPUCanvasContext` object for drawing directly to a window on screen. Overview diff --git a/examples/cube.py b/examples/cube.py index 13a49126..5fdd4da6 100644 --- a/examples/cube.py +++ b/examples/cube.py @@ -5,7 +5,7 @@ because it adds buffers and textures. This example is set up so it can be run with any canvas. Running this file -as a script will use the auto-backend. +as a script will use rendercanvas with the auto-backend. """ # test_example = true @@ -24,11 +24,10 @@ def setup_drawing_sync( - canvas, power_preference="high-performance", limits=None + context, power_preference="high-performance", limits=None ) -> Callable: - """Setup to draw a rotating cube on the given canvas. + """Setup to draw a rotating cube on the given context. - The given canvas must implement WgpuCanvasInterface, but nothing more. Returns the draw function. """ @@ -39,19 +38,18 @@ def setup_drawing_sync( ) pipeline_layout, uniform_buffer, bind_group = create_pipeline_layout(device) - pipeline_kwargs = get_render_pipeline_kwargs(canvas, device, pipeline_layout) + pipeline_kwargs = get_render_pipeline_kwargs(context, device, pipeline_layout) render_pipeline = device.create_render_pipeline(**pipeline_kwargs) return get_draw_function( - canvas, device, render_pipeline, uniform_buffer, bind_group + context, device, render_pipeline, uniform_buffer, bind_group ) -async def setup_drawing_async(canvas, limits=None): - """Setup to async-draw a rotating cube on the given canvas. +async def setup_drawing_async(context, limits=None): + """Setup to async-draw a rotating cube on the given context. - The given canvas must implement WgpuCanvasInterface, but nothing more. Returns the draw function. """ adapter = await wgpu.gpu.request_adapter_async(power_preference="high-performance") @@ -61,24 +59,24 @@ async def setup_drawing_async(canvas, limits=None): ) pipeline_layout, uniform_buffer, bind_group = create_pipeline_layout(device) - pipeline_kwargs = get_render_pipeline_kwargs(canvas, device, pipeline_layout) + pipeline_kwargs = get_render_pipeline_kwargs(context, device, pipeline_layout) render_pipeline = await device.create_render_pipeline_async(**pipeline_kwargs) return get_draw_function( - canvas, device, render_pipeline, uniform_buffer, bind_group + context, device, render_pipeline, uniform_buffer, bind_group ) -def get_drawing_func(canvas, device): +def get_drawing_func(context, device): pipeline_layout, uniform_buffer, bind_group = create_pipeline_layout(device) - pipeline_kwargs = get_render_pipeline_kwargs(canvas, device, pipeline_layout) + pipeline_kwargs = get_render_pipeline_kwargs(context, device, pipeline_layout) render_pipeline = device.create_render_pipeline(**pipeline_kwargs) # render_pipeline = device.create_render_pipeline(**pipeline_kwargs) return get_draw_function( - canvas, device, render_pipeline, uniform_buffer, bind_group + context, device, render_pipeline, uniform_buffer, bind_group ) @@ -86,9 +84,8 @@ def get_drawing_func(canvas, device): def get_render_pipeline_kwargs( - canvas, device: wgpu.GPUDevice, pipeline_layout: wgpu.GPUPipelineLayout + context, device: wgpu.GPUDevice, pipeline_layout: wgpu.GPUPipelineLayout ) -> wgpu.RenderPipelineDescriptor: - context = canvas.get_context("wgpu") render_texture_format = context.get_preferred_format(device.adapter) context.configure(device=device, format=render_texture_format) @@ -250,7 +247,7 @@ def create_pipeline_layout(device: wgpu.GPUDevice): def get_draw_function( - canvas, + context, device: wgpu.GPUDevice, render_pipeline: wgpu.GPURenderPipeline, uniform_buffer: wgpu.GPUBuffer, @@ -304,9 +301,9 @@ def upload_uniform_buffer(): def draw_frame(): current_texture_view: wgpu.GPUTextureView = ( - canvas.get_context("wgpu") - .get_current_texture() - .create_view(label="Cube Example current surface texture view") + context.get_current_texture().create_view( + label="Cube Example current surface texture view" + ) ) command_encoder = device.create_command_encoder( label="Cube Example render pass command encoder" @@ -481,6 +478,7 @@ def draw_func(): max_fps=60, vsync=True, ) + context = canvas.get_wgpu_context() # Pick one @@ -488,11 +486,11 @@ def draw_func(): # Async @loop.add_task async def init(): - draw_frame = await setup_drawing_async(canvas) + draw_frame = await setup_drawing_async(context) canvas.request_draw(draw_frame) else: # Sync - draw_frame = setup_drawing_sync(canvas) + draw_frame = setup_drawing_sync(context) canvas.request_draw(draw_frame) # loop.add_task(poller) diff --git a/examples/extras_debug.py b/examples/extras_debug.py index 8b6e8bfa..31131915 100644 --- a/examples/extras_debug.py +++ b/examples/extras_debug.py @@ -113,7 +113,7 @@ def setup_demo(): from cube import setup_drawing_sync canvas = RenderCanvas(title="Cube example with debug symbols") - draw_frame = setup_drawing_sync(canvas) + draw_frame = setup_drawing_sync(canvas.get_wgpu_context()) # we set the auto capture on frame 50, so after 100 frames gui should exit # this will lead to RenderDoc automatically opening the capture! diff --git a/examples/extras_dxc.py b/examples/extras_dxc.py index 9771ae0a..1d6fc5ab 100644 --- a/examples/extras_dxc.py +++ b/examples/extras_dxc.py @@ -32,7 +32,7 @@ canvas = RenderCanvas(title="Cube example on DX12 using Dxc") -draw_frame = setup_drawing_sync(canvas) +draw_frame = setup_drawing_sync(canvas.get_wgpu_context()) @canvas.request_draw diff --git a/examples/gui_auto.py b/examples/gui_auto.py index e6b19380..f1f728bf 100644 --- a/examples/gui_auto.py +++ b/examples/gui_auto.py @@ -22,7 +22,7 @@ from triangle import setup_drawing_sync canvas = RenderCanvas(title="Cube example on $backend") -draw_frame = setup_drawing_sync(canvas) +draw_frame = setup_drawing_sync(canvas.get_wgpu_context()) @canvas.request_draw diff --git a/examples/gui_direct.py b/examples/gui_direct.py index f0b1af4f..f4137624 100644 --- a/examples/gui_direct.py +++ b/examples/gui_direct.py @@ -13,7 +13,7 @@ import atexit import glfw -from wgpu.backends.wgpu_native import GPUCanvasContext +import wgpu # from triangle import setup_drawing_sync from cube import setup_drawing_sync @@ -26,37 +26,33 @@ api_is_wayland = True -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)), - } + "platform": "windows", + "window": int(glfw.get_win32_window(window)), + "vsync": True, } elif sys.platform.startswith("darwin"): return { - "screen": { - "platform": "cocoa", - "window": int(glfw.get_cocoa_window(window)), - } + "platform": "cocoa", + "window": int(glfw.get_cocoa_window(window)), + "vsync": True, } 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()), - } + "platform": "wayland", + "window": int(glfw.get_wayland_window(window)), + "display": int(glfw.get_wayland_display()), + "vsync": True, } else: return { - "screen": { - "platform": "x11", - "window": int(glfw.get_x11_window(window)), - "display": int(glfw.get_x11_display()), - } + "platform": "x11", + "window": int(glfw.get_x11_window(window)), + "display": int(glfw.get_x11_display()), + "vsync": True, } else: raise RuntimeError(f"Cannot get GLFW surface info on {sys.platform}.") @@ -66,43 +62,39 @@ def get_glfw_present_methods(window): glfw.init() atexit.register(glfw.terminate) +# disable automatic API selection, we are not using opengl +glfw.window_hint(glfw.CLIENT_API, glfw.NO_API) +glfw.window_hint(glfw.RESIZABLE, True) -class MinimalGlfwCanvas: # implements WgpuCanvasInterface - """Minimal canvas interface required by wgpu.""" - - def __init__(self, title): - # disable automatic API selection, we are not using opengl - glfw.window_hint(glfw.CLIENT_API, glfw.NO_API) - glfw.window_hint(glfw.RESIZABLE, True) - self.window = glfw.create_window(640, 480, title, None, None) - self.context = GPUCanvasContext(self, get_glfw_present_methods(self.window)) +title = "wgpu glfw direct" +window = glfw.create_window(640, 480, title, None, None) +present_info = get_glfw_present_info(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]) +context = wgpu.gpu.get_canvas_context(present_info) - def get_context(self, kind="wgpu"): - return self.context +# Initialize physical size once. For robust apps update this on resize events. +context.set_physical_size(*glfw.get_framebuffer_size(window)) def main(): - # create canvas - canvas = MinimalGlfwCanvas("wgpu gui direct") - draw_frame = setup_drawing_sync(canvas) + draw_frame = setup_drawing_sync(context) last_frame_time = time.perf_counter() frame_count = 0 # render loop - while not glfw.window_should_close(canvas.window): + while not glfw.window_should_close(window): # process inputs glfw.poll_events() + + # resize handling + context.set_physical_size(*glfw.get_framebuffer_size(window)) + # draw a frame draw_frame() # present the frame to the screen - canvas.context.present() + context.present() # stats frame_count += 1 etime = time.perf_counter() - last_frame_time @@ -111,7 +103,8 @@ def main(): last_frame_time, frame_count = time.perf_counter(), 0 # dispose resources - glfw.destroy_window(canvas.window) + context.unconfigure() + glfw.destroy_window(window) # allow proper cleanup (workaround for glfw bug) end_time = time.perf_counter() + 0.1 diff --git a/examples/gui_qt_embed.py b/examples/gui_qt_embed.py index 66b38844..30585e80 100644 --- a/examples/gui_qt_embed.py +++ b/examples/gui_qt_embed.py @@ -86,7 +86,7 @@ def whenButton2Clicked(self): app = QtWidgets.QApplication([]) example = ExampleWidget() -draw_frame = setup_drawing_sync(example.canvas) +draw_frame = setup_drawing_sync(example.canvas.get_wgpu_context()) example.canvas.request_draw(draw_frame) # Enter Qt event loop (compatible with qt5/qt6) diff --git a/examples/imgui_backend_sea.py b/examples/imgui_backend_sea.py index e27e5cc5..d387459d 100644 --- a/examples/imgui_backend_sea.py +++ b/examples/imgui_backend_sea.py @@ -22,7 +22,7 @@ device = adapter.request_device_sync() # Prepare present context -present_context = canvas.get_context("wgpu") +present_context = canvas.get_wgpu_context() 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 8eb933dd..599f9fe2 100644 --- a/examples/imgui_renderer_sea.py +++ b/examples/imgui_renderer_sea.py @@ -22,7 +22,7 @@ device = adapter.request_device_sync() # Prepare present context -present_context = canvas.get_context("wgpu") +present_context = canvas.get_wgpu_context() render_texture_format = wgpu.TextureFormat.bgra8unorm present_context.configure(device=device, format=render_texture_format) diff --git a/examples/offscreen_hdr.py b/examples/offscreen_hdr.py index 2b8701e7..54d87bc5 100644 --- a/examples/offscreen_hdr.py +++ b/examples/offscreen_hdr.py @@ -22,7 +22,7 @@ canvas = RenderCanvas(size=(640, 480), pixel_ratio=2) -draw_frame = setup_drawing_sync(canvas, format="rgba16float") +draw_frame = setup_drawing_sync(canvas.get_wgpu_context(), format="rgba16float") canvas.request_draw(draw_frame) image = canvas.draw() diff --git a/examples/triangle.py b/examples/triangle.py index b6a300e8..8bb05d97 100644 --- a/examples/triangle.py +++ b/examples/triangle.py @@ -13,7 +13,7 @@ https://github.com/realitix/vulkan/blob/master/example/contribs/example_glfw.py This example is set up so it can be run with any canvas. Running this file -as a script will use the auto-backend. +as a script will use rendercanvas with the auto-backend. """ @@ -25,48 +25,45 @@ def setup_drawing_sync( - canvas, power_preference="high-performance", limits=None, format=None + context, power_preference="high-performance", limits=None, format=None ) -> Callable: - """Setup to draw a triangle on the given canvas. + """Setup to draw a triangle on the given context. - The given canvas must implement WgpuCanvasInterface, but nothing more. Returns the draw function. """ adapter = wgpu.gpu.request_adapter_sync(power_preference=power_preference) device = adapter.request_device_sync(required_limits=limits) - pipeline_kwargs = get_render_pipeline_kwargs(canvas, device, format) + pipeline_kwargs = get_render_pipeline_kwargs(context, device, format) render_pipeline = device.create_render_pipeline(**pipeline_kwargs) - return get_draw_function(canvas, device, render_pipeline, asynchronous=False) + return get_draw_function(context, device, render_pipeline, asynchronous=False) -async def setup_drawing_async(canvas, limits=None, format=None) -> Callable: - """Setup to async-draw a triangle on the given canvas. +async def setup_drawing_async(context, limits=None, format=None) -> Callable: + """Setup to async-draw a triangle on the given context. - The given canvas must implement WgpuCanvasInterface, but nothing more. Returns the draw function. """ adapter = await wgpu.gpu.request_adapter_async(power_preference="high-performance") device = await adapter.request_device_async(required_limits=limits) - pipeline_kwargs = get_render_pipeline_kwargs(canvas, device, format) + pipeline_kwargs = get_render_pipeline_kwargs(context, device, format) render_pipeline = await device.create_render_pipeline_async(**pipeline_kwargs) - return get_draw_function(canvas, device, render_pipeline, asynchronous=True) + return get_draw_function(context, device, render_pipeline, asynchronous=True) # %% Functions to create wgpu objects def get_render_pipeline_kwargs( - canvas, device, render_texture_format + context, device, render_texture_format ) -> wgpu.RenderPipelineDescriptor: - context = canvas.get_context("wgpu") if render_texture_format is None: render_texture_format = context.get_preferred_format(device.adapter) context.configure(device=device, format=render_texture_format) @@ -96,14 +93,14 @@ def get_render_pipeline_kwargs( def get_draw_function( - canvas, + context, device: wgpu.GPUDevice, render_pipeline: wgpu.GPURenderPipeline, *, asynchronous: bool, ) -> Callable: def draw_frame_sync(): - current_texture = canvas.get_context("wgpu").get_current_texture() + current_texture = context.get_current_texture() command_encoder = device.create_command_encoder() render_pass = command_encoder.begin_render_pass( @@ -176,6 +173,8 @@ async def draw_frame_async(): from rendercanvas.auto import RenderCanvas, loop canvas = RenderCanvas(size=(640, 480), title="wgpu triangle example") - draw_frame = setup_drawing_sync(canvas) + context = canvas.get_wgpu_context() + + draw_frame = setup_drawing_sync(context) canvas.request_draw(draw_frame) loop.run() diff --git a/examples/triangle_glsl.py b/examples/triangle_glsl.py index 1a89113c..386273cb 100644 --- a/examples/triangle_glsl.py +++ b/examples/triangle_glsl.py @@ -45,17 +45,17 @@ # %% The wgpu calls -def setup_drawing_sync(canvas, power_preference="high-performance", limits=None): - """Regular function to set up a viz on the given canvas.""" +def setup_drawing_sync(context, power_preference="high-performance", limits=None): + """Regular function to set up a viz on the given context.""" adapter = wgpu.gpu.request_adapter_sync(power_preference=power_preference) device = adapter.request_device_sync(required_limits=limits) - render_pipeline = get_render_pipeline(canvas, device) - return get_draw_function(canvas, device, render_pipeline) + render_pipeline = get_render_pipeline(context, device) + return get_draw_function(context, device, render_pipeline) -def get_render_pipeline(canvas, device): +def get_render_pipeline(context, device): vert_shader = device.create_shader_module(label="triangle_vert", code=vertex_shader) frag_shader = device.create_shader_module( label="triangle_frag", code=fragment_shader @@ -64,9 +64,8 @@ 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("wgpu") - render_texture_format = present_context.get_preferred_format(device.adapter) - present_context.configure(device=device, format=render_texture_format) + render_texture_format = context.get_preferred_format(device.adapter) + context.configure(device=device, format=render_texture_format) return device.create_render_pipeline( layout=pipeline_layout, @@ -97,9 +96,9 @@ def get_render_pipeline(canvas, device): ) -def get_draw_function(canvas, device, render_pipeline): +def get_draw_function(context, device, render_pipeline): def draw_frame(): - current_texture = canvas.get_context("wgpu").get_current_texture() + current_texture = context.get_current_texture() command_encoder = device.create_command_encoder() render_pass = command_encoder.begin_render_pass( @@ -127,6 +126,8 @@ def draw_frame(): from rendercanvas.auto import RenderCanvas, loop canvas = RenderCanvas(size=(640, 480), title="wgpu triangle glsl example") - draw_frame = setup_drawing_sync(canvas) + context = canvas.get_wgpu_context() + + draw_frame = setup_drawing_sync(context) canvas.request_draw(draw_frame) loop.run() diff --git a/examples/wgpu-examples.ipynb b/examples/wgpu-examples.ipynb index 04c12d49..3a732e36 100644 --- a/examples/wgpu-examples.ipynb +++ b/examples/wgpu-examples.ipynb @@ -64,7 +64,7 @@ "canvas = RenderCanvas(\n", " size=(640, 480), title=\"triangle example in a notebook\", update_mode=\"manual\"\n", ")\n", - "draw_frame = setup_drawing_sync(canvas)\n", + "draw_frame = setup_drawing_sync(canvas.get_wgpu_context())\n", "canvas.request_draw(draw_frame)\n", "\n", "canvas" @@ -136,7 +136,7 @@ "canvas = RenderCanvas(\n", " size=(640, 480), title=\"cube example in a notebook\", update_mode=\"continuous\"\n", ")\n", - "draw_frame = setup_drawing_sync(canvas)\n", + "draw_frame = setup_drawing_sync(canvas.get_wgpu_context())\n", "canvas.request_draw(draw_frame)\n", "\n", "canvas" diff --git a/pyproject.toml b/pyproject.toml index a147f5d0..618646dc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,7 @@ dependencies = [ "cffi>=1.15.0", "rubicon-objc>=0.4.1; sys_platform == 'darwin'", "sniffio", + "rendercanvas >=2.4", # Temporarily depend on rendercanvas because we re-aligned apis. Remove in a few months ] [project.optional-dependencies] diff --git a/tests/renderutils.py b/tests/renderutils.py index 468d5ed9..ea95da6d 100644 --- a/tests/renderutils.py +++ b/tests/renderutils.py @@ -276,7 +276,7 @@ def render_to_screen( }, ) - present_context = canvas.get_context("wgpu") + present_context = canvas.get_wgpu_context() present_context.configure(device=device, format=None) def draw_frame(): diff --git a/tests/test_canvas.py b/tests/test_canvas.py index f6573892..08b5dbac 100644 --- a/tests/test_canvas.py +++ b/tests/test_canvas.py @@ -1,12 +1,9 @@ """Test that wgpu works together with rendercanvas.""" -import sys - import wgpu # from rendercanvas import BaseRenderCanvas from rendercanvas.offscreen import RenderCanvas -from wgpu._canvas import WgpuCanvasInterface from pytest import skip from testutils import run_tests, can_use_wgpu_lib @@ -40,7 +37,7 @@ def test_rendercanvas(): canvas = RenderCanvas(size=(640, 480)) device = wgpu.utils.get_default_device() - draw_frame1 = _get_draw_function(device, canvas) + draw_frame1 = _get_draw_function(device, canvas.get_wgpu_context()) frame_counter = 0 @@ -67,76 +64,12 @@ def draw_frame2(): assert m.shape == (200, 300, 4) -def test_canvas_interface(): - """Render an orange square ... using the WgpuCanvasInterface.""" - canvas = WgpuCanvasInterface() - - device = wgpu.utils.get_default_device() - draw_frame = _get_draw_function(device, canvas) - - def draw(): - draw_frame() - info = canvas.get_context().present() - return info["data"] - - m = draw() - assert isinstance(m, memoryview) - assert m.shape == (480, 640, 4) - - -def test_custom_canvas(): - """Render an orange square ... in a custom offscreen canvas. - - This helps make sure that WgpuCanvasInterface is indeed - the minimal required canvas API. - """ - - class CustomCanvas: # implements wgpu.WgpuCanvasInterface - def __init__(self): - self._canvas_context = None - self._present_methods = { - "bitmap": { - "formats": ["rgba-u8"], - } - } - - def get_physical_size(self): - return 300, 200 - - def get_context(self, context_type="wgpu"): - assert context_type == "wgpu" - if self._canvas_context is None: - backend_module = sys.modules["wgpu"].gpu.__module__ - CC = sys.modules[backend_module].GPUCanvasContext # noqa N806 - self._canvas_context = CC(self, self._present_methods) - return self._canvas_context - - canvas = CustomCanvas() - - # Also pass canvas here, to touch that code somewhere - adapter = wgpu.gpu.request_adapter_sync( - canvas=canvas, power_preference="high-performance" - ) - device = adapter.request_device_sync() - draw_frame = _get_draw_function(device, canvas) - - def draw(): - draw_frame() - info = canvas.get_context().present() - return info["data"] - - m = draw() - assert isinstance(m, memoryview) - assert m.shape == (200, 300, 4) - - -def _get_draw_function(device, canvas): +def _get_draw_function(device, present_context): # Bindings and layout pipeline_layout = device.create_pipeline_layout(bind_group_layouts=[]) shader = device.create_shader_module(code=shader_source) - 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 1825bde0..b96e6265 100644 --- a/tests_mem/test_gui.py +++ b/tests_mem/test_gui.py @@ -58,9 +58,7 @@ def test_release_canvas_context(n): from rendercanvas.offscreen import RenderCanvas yield { - "expected_counts_after_create": { - "CanvasContext": (n, 0), - }, + "expected_counts_after_create": {}, "ignore": {"CommandBuffer", "Buffer"}, } diff --git a/tests_mem/test_gui_glfw.py b/tests_mem/test_gui_glfw.py index 035c724b..3acd2767 100644 --- a/tests_mem/test_gui_glfw.py +++ b/tests_mem/test_gui_glfw.py @@ -41,9 +41,6 @@ def test_release_canvas_context(n): from rendercanvas.glfw import RenderCanvas, poll_glfw_briefly yield { - "expected_counts_after_create": { - "CanvasContext": (n, 0), - }, "ignore": {"CommandBuffer"}, } diff --git a/tests_mem/test_gui_qt.py b/tests_mem/test_gui_qt.py index 59d9fcad..f54a27ec 100644 --- a/tests_mem/test_gui_qt.py +++ b/tests_mem/test_gui_qt.py @@ -37,9 +37,6 @@ def test_release_canvas_context(n): app = PySide6.QtWidgets.QApplication([""]) yield { - "expected_counts_after_create": { - "CanvasContext": (n, 0), - }, "ignore": {"CommandBuffer"}, } diff --git a/tests_mem/testutils.py b/tests_mem/testutils.py index 3f94431f..157ced27 100644 --- a/tests_mem/testutils.py +++ b/tests_mem/testutils.py @@ -224,7 +224,8 @@ def core_test_func(): ) # Test that class matches function name (should prevent a group of copy-paste errors) - assert ob_name == cls.__name__[3:] + if not ob_name.startswith("CanvasContext"): + assert ob_name == cls.__name__[3:] # Give wgpu some slack to clean up temporary resources clear_mem() @@ -236,7 +237,8 @@ def core_test_func(): print(" more after create:", more2) # Make sure the actual object has increased - assert more2 # not empty + if options["expected_counts_after_create"]: + assert more2 # not empty assert more2 == options["expected_counts_after_create"], ( f"Expected:\n{options['expected_counts_after_create']}\nGot:\n{more2}" ) diff --git a/wgpu/__init__.py b/wgpu/__init__.py index 603630f1..81ef938e 100644 --- a/wgpu/__init__.py +++ b/wgpu/__init__.py @@ -19,22 +19,7 @@ gpu = GPU() # noqa: F405 -def rendercanvas_context_hook(canvas, present_methods): - """Get a new GPUCanvasContext, given a canvas and present_methods dict. - - This is a hook for rendercanvas, so that it can support ``canvas.get_context("wgpu")``. - - See https://github.com/pygfx/wgpu-py/blob/main/wgpu/_canvas.py and https://rendercanvas.readthedocs.io/stable/contextapi.html. - """ - - import sys - - backend_module_name = gpu.__module__ - if backend_module_name in ("", "wgpu._classes"): - # Load backend now - from .backends import auto - - backend_module_name = gpu.__module__ - - backend_module = sys.modules[backend_module_name] - return backend_module.GPUCanvasContext(canvas, present_methods) +def rendercanvas_context_hook(canvas, _): + raise RuntimeError( + "The rendercanvas_context_hook is deprecated. If you're using rendercanvas, please update to the latest version. Otherwise, use wgpu.gpu.get_canvas_context()" + ) diff --git a/wgpu/_canvas.py b/wgpu/_canvas.py deleted file mode 100644 index e260627d..00000000 --- a/wgpu/_canvas.py +++ /dev/null @@ -1,66 +0,0 @@ -import wgpu - - -class WgpuCanvasInterface: - """The minimal interface to be a valid canvas that wgpu can render to. - - Any object that implements these methods is a canvas that wgpu can work with. - The object does not even have to derive from this class. - In practice, we recommend using the `rendercanvas `_ library. - """ - - # This implementation serves as documentation, but it actually works! - - _canvas_context = None - - def get_physical_size(self) -> tuple[int, int]: - """Get the physical size of the canvas in integer pixels.""" - return (640, 480) - - def get_context(self, context_type: str = "wgpu") -> wgpu.GPUCanvasContext: - """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. - - The canvas should get the context once, and then store it on ``self``. - Getting the context is best done using ``wgpu.rendercanvas_context_hook()``, - which accepts two arguments: the canvas object, and a dict with the present-methods - that this canvas supports. - - Each supported present-method is represented by a field in the dict. The value - is another dict with information specific to that present method. - A canvas must implement at least either the "screen" or "bitmap" method. - - 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. - - Also see https://rendercanvas.readthedocs.io/stable/contextapi.html - """ - - # Note that this function is analog to HtmlCanvas.getContext(), except - # here the only valid arg is 'webgpu', which is also made the default. - assert context_type in ("wgpu", "webgpu", None) - - # Support only bitmap-present, with rgba8unorm. - present_methods = { - "bitmap": { - "formats": ["rgba-u8"], - } - } - - if self._canvas_context is None: - self._canvas_context = wgpu.rendercanvas_context_hook(self, present_methods) - - return self._canvas_context diff --git a/wgpu/_classes.py b/wgpu/_classes.py index c7c9f400..e1b41259 100644 --- a/wgpu/_classes.py +++ b/wgpu/_classes.py @@ -11,7 +11,6 @@ # Allow using class names in type annotations, without Ruff triggering F821 from __future__ import annotations -import weakref import logging from typing import Sequence @@ -131,8 +130,8 @@ def request_adapter_async( power_preference (PowerPreference): "high-performance" or "low-power". force_fallback_adapter (bool): whether to use a (probably CPU-based) fallback adapter. - canvas : The canvas that the adapter should be able to render to. This can typically - be left to None. If given, the object must implement ``WgpuCanvasInterface``. + canvas : The canvas or context that the adapter should be able to render to. This can typically + be left to None. If given, it must be a ``GPUCanvasContext`` or ``RenderCanvas``. loop : the loop object for async support. Must have at least ``call_soon(f, *args)``. The loop object is required for asynchrouns use with ``promise.then()``. EXPERIMENTAL. """ @@ -207,6 +206,22 @@ def wgsl_language_features(self) -> set: # Looks like at the time of writing there are no definitions for extensions yet return set() + @apidiff.add("Provide a low-level way to let wgpu render to screen") + def get_canvas_context(self, present_info: dict) -> GPUCanvasContext: + """Get the GPUCanvasContext object for the appropriate backend. + + Note that the recommended way to get a context is to instead use the ``rendercanvas`` library. + + The ``present_info`` 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. For the Pyodide backend, the ``window`` is the ```` object. + This dict is an interface between ``rendercanvas`` and ``wgpu-py``. + """ + from .backends.auto import gpu + + return gpu.get_canvas_context(present_info) + # Instantiate API entrypoint gpu = GPU() @@ -220,19 +235,27 @@ class GPUPromise(BaseGPUPromise): class GPUCanvasContext: """Represents a context to configure a canvas and render to it. - Can be obtained via `canvas.get_context("wgpu")`. + Can be obtained with ``wgpu.gpu.get_canvas_context()``. + + When ``rendercanvas`` is used, it will automatically wrap a + ``GPUCanvasContext`` if necessary. From the perspective of ``rendercanvas``, this + implements the 'screen' present method. - 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 - combines (and checks) the user's preferences with the capabilities and - preferences of the canvas. + The purpose of the canvas-context is to connecting the wgpu API to the + GUI/window/canvas layer, in a way that allows the GUI to be agnostic about + wgpu, and wgpu to remain agnostic about a canvas. It combines (and checks) + the user's preferences with the capabilities and preferences of the canvas. """ _ot = object_tracker - def __init__(self, canvas, present_methods): + def __init__(self, present_info: dict): self._ot.increase(self.__class__.__name__) - self._canvas_ref = weakref.ref(canvas) + self._present_info = present_info + + # Buffer to hold new physical size, will be applied when the context reconfigures + self._physical_size = 0, 0 + self._has_new_size = False # Surface capabilities. Stored the first time it is obtained self._capabilities = None @@ -243,52 +266,36 @@ def __init__(self, canvas, present_methods): # The last used texture self._texture = None - # 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.""" - return self._canvas_ref() - # IDL: readonly attribute (HTMLCanvasElement or OffscreenCanvas) canvas; + @apidiff.hide("No unified concept of a canvas in Python, and avoid circular refs") @property def canvas(self) -> CanvasLike: - """The associated canvas object.""" - return self._canvas_ref() + return None + + @apidiff.add("External code needs to set the framebuffer size") + def set_physical_size(self, width: int, height: int) -> None: + """Set the current framebuffer physical size. + + The application must call this to keep the context informed about + the window/framebuffer size. + """ + if width <= 0 or height <= 0: + raise ValueError("Physical size values must be positive.") + self._physical_size = int(width), int(height) + self._has_new_size = True + + @apidiff.add("Allow code that uses a context to get the physical size") + @property + def physical_size(self) -> tuple[int, int]: + """The physical size of the underlying surface in integer pixels.""" + return self._physical_size def _get_capabilities(self, adapter): """Get dict of capabilities and cache the result.""" if self._capabilities is None: self._capabilities = {} - if self._present_method == "screen": - # Query capabilities from the surface - self._capabilities.update(self._get_capabilities_screen(adapter)) - else: - # Query format capabilities from the info provided by the canvas - formats = [] - for format in self._present_methods["bitmap"]["formats"]: - channels, _, fmt = format.partition("-") - channels = {"i": "r", "ia": "rg"}.get(channels, channels) - fmt = { - "u8": "8unorm", - "u16": "16uint", - "f16": "16float", - "f32": "32float", - }.get(fmt, fmt) - wgpu_format = channels + fmt - wgpu_format_srgb = wgpu_format + "-srgb" - if wgpu_format_srgb in enums.TextureFormat: - formats.append(wgpu_format_srgb) - formats.append(wgpu_format) - # Assume alpha modes for now - alpha_modes = [enums.CanvasAlphaMode.opaque] - # Build capabilitied dict - self._capabilities = { - "formats": formats, - "usages": 0xFF, - "alpha_modes": alpha_modes, - } + # Query capabilities from the surface + self._capabilities.update(self._get_capabilities_screen(adapter)) # Derived defaults if "view_formats" not in self._capabilities: self._capabilities["view_formats"] = self._capabilities["formats"] @@ -324,6 +331,7 @@ def configure( alpha_mode: enums.CanvasAlphaModeEnum = "opaque", ) -> None: """Configures the presentation context for the associated canvas. + Destroys any textures produced with a previous configuration. This clears the drawing buffer to transparent black. @@ -405,8 +413,7 @@ def configure( "alpha_mode": alpha_mode, } - if self._present_method == "screen": - self._configure_screen(**self._config) + self._configure_screen(**self._config) def _configure_screen( self, @@ -426,10 +433,9 @@ def unconfigure(self) -> None: """Removes the presentation context configuration. Destroys any textures produced while configured. """ - if self._present_method == "screen": - self._unconfigure_screen() self._config = None self._drop_texture() + self._unconfigure_screen() def _unconfigure_screen(self): raise NotImplementedError() @@ -441,36 +447,11 @@ def get_current_texture(self) -> GPUTexture: raise RuntimeError( "Canvas context must be configured before calling get_current_texture()." ) - - # When the texture is active right now, we could either: - # * return the existing texture - # * warn about it, and create a new one - # * raise an error - # 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_method == "screen": - self._texture = self._create_texture_screen() - else: - self._texture = self._create_texture_bitmap() + self._texture = self._create_texture_screen() return self._texture - def _create_texture_bitmap(self): - canvas = self._get_canvas() - width, height = canvas.get_physical_size() - width, height = max(width, 1), max(height, 1) - - # Note that the label 'present' is used by read_texture() to determine - # that it can use a shared copy buffer. - device = self._config["device"] - return device.create_texture( - label="present", - size=(width, height, 1), - format=self._config["format"], - usage=self._config["usage"] | flags.TextureUsage.COPY_SRC, - ) - def _create_texture_screen(self): raise NotImplementedError() @@ -479,80 +460,12 @@ def _drop_texture(self): self._texture._release() # not destroy, because it may be in use. self._texture = None - @apidiff.add("The present method is used by the canvas") + @apidiff.add("External code needs to invoke presenting at the appropriate time") def present(self) -> None: - """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. - """ - - if not self._texture: - result = {"method": "skip"} - elif self._present_method == "screen": + """Present what has been drawn to the current texture, by compositing it to the canvas.""" + if self._texture: self._present_screen() - result = {"method": "screen"} - elif self._present_method == "bitmap": - bitmap = self._present_bitmap() - result = {"method": "bitmap", "format": "rgba-u8", "data": bitmap} - else: - result = {"method": "fail", "message": "incompatible present methods"} - - self._drop_texture() - return result - - def _present_bitmap(self): - texture = self._texture - device = texture._device - - size = texture.size - format = texture.format - nchannels = 4 # we expect rgba or bgra - if not format.startswith(("rgba", "bgra")): - raise RuntimeError(f"Image present unsupported texture format {format}.") - if "8" in format: - bytes_per_pixel = nchannels - elif "16" in format: - bytes_per_pixel = nchannels * 2 - elif "32" in format: - bytes_per_pixel = nchannels * 4 - else: - raise RuntimeError( - f"Image present unsupported texture format bitdepth {format}." - ) - - data = device.queue.read_texture( - { - "texture": texture, - "mip_level": 0, - "origin": (0, 0, 0), - }, - { - "offset": 0, - "bytes_per_row": bytes_per_pixel * size[0], - "rows_per_image": size[1], - }, - size, - ) - - # Derive struct dtype from wgpu texture format - memoryview_type = "B" - if "float" in format: - memoryview_type = "e" if "16" in format else "f" - else: - if "32" in format: - memoryview_type = "I" - elif "16" in format: - memoryview_type = "H" - else: - memoryview_type = "B" - if "sint" in format: - memoryview_type = memoryview_type.lower() - - # Represent as memory object to avoid numpy dependency - # Equivalent: np.frombuffer(data, np.uint8).reshape(size[1], size[0], nchannels) - - return data.cast(memoryview_type, (size[1], size[0], nchannels)) + self._drop_texture() def _present_screen(self): raise NotImplementedError() @@ -1064,7 +977,7 @@ def create_bind_group( binding=2, resource=wgpu.BufferBinding( buffer=a_buffer, - offset=0. + offset=0., size=812, ) ) diff --git a/wgpu/backends/wgpu_native/_api.py b/wgpu/backends/wgpu_native/_api.py index 2578e831..ecd3b912 100644 --- a/wgpu/backends/wgpu_native/_api.py +++ b/wgpu/backends/wgpu_native/_api.py @@ -433,6 +433,23 @@ def _get_features(id: int, device: bool = False, adapter: bool = False): libf = SafeLibCalls(lib, error_handler) +def find_surface_id_from_canvas(canvas_or_context): + """Try to get the surface_id from a RenderCanvas, rendercanvas context, or GPUCanvasContect.""" + ob = canvas_or_context + surface_id = None + # Try get context first, e.g. from rendercanvas + if hasattr(ob, "get_context"): + ob = ob.get_context("wgpu") + # Now get native GPUCanvasContext, only assuming rendercanvas, but we're using knowledge of a private attr here :/ + for attr in ["_wgpu_context"]: + if hasattr(ob, attr): + ob = getattr(ob, attr) + # Finally, get the surface id + if hasattr(ob, "_surface_id"): + surface_id = ob._surface_id + return surface_id + + # %% The API @@ -455,8 +472,8 @@ def request_adapter_async( power_preference (PowerPreference): "high-performance" or "low-power". force_fallback_adapter (bool): whether to use a (probably CPU-based) fallback adapter. - canvas : The canvas that the adapter should be able to render to. This can typically - be left to None. If given, the object must implement ``WgpuCanvasInterface``. + canvas : The canvas or context that the adapter should be able to render to. This can typically + be left to None. If given, it must be a ``GPUCanvasContext`` or ``RenderCanvas``. """ # Similar to https://github.com/gfx-rs/wgpu?tab=readme-ov-file#environment-variables @@ -476,6 +493,7 @@ def request_adapter_async( promise._wgpu_set_input(adapters_llvm[0]) return promise + # ----- Surface ID # Get surface id that the adapter must be compatible with. If we @@ -483,7 +501,9 @@ def request_adapter_async( # 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("wgpu")._surface_id # can still be NULL + surface_id = find_surface_id_from_canvas(canvas) + if surface_id is None: + surface_id = ffi.NULL # ----- Select backend @@ -651,6 +671,19 @@ def to_py_str(key): # ----- Done return GPUAdapter(adapter_id, features, limits, adapter_info, loop) + def get_canvas_context(self, present_info: dict) -> GPUCanvasContext: + """Get the GPUCanvasContext object for the appropriate backend. + + Note that the recommended way to get a context is to instead use the ``rendercanvas`` library. + + The ``present_info`` 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. For the Pyodide backend, the ``window`` is the ```` object. + This dict is an interface between ``rendercanvas`` and ``wgpu-py``. + """ + return GPUCanvasContext(present_info) + # Instantiate API entrypoint gpu = GPU() @@ -672,15 +705,12 @@ class GPUCanvasContext(classes.GPUCanvasContext): _wgpu_config = None _skip_present_screen = False - def __init__(self, canvas, present_methods): - super().__init__(canvas, present_methods) + def __init__(self, present_info: dict): + super().__init__(present_info) # Obtain the surface id. The lifetime is of the surface is bound # to the lifetime of this context object. - 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 + self._surface_id = get_surface_id_from_info(present_info) # A stat for get_current_texture self._number_of_successive_unsuccesful_textures = 0 @@ -803,7 +833,7 @@ def _configure_screen( # benchmark something and get the highest FPS possible. Note # that we've observed rate limiting regardless of setting this # to Immediate, depending on OS or being on battery power. - if getattr(self._get_canvas(), "_vsync", True): + if self._present_info.get("vsync", True): present_mode_pref = ["fifo", "mailbox"] else: present_mode_pref = ["immediate", "mailbox", "fifo"] @@ -815,7 +845,6 @@ def _configure_screen( c_present_mode = getattr(lib, f"WGPUPresentMode_{present_mode.capitalize()}") # Prepare config object - width, height = self._get_canvas().get_physical_size() # H: nextInChain: WGPUChainedStruct *, device: WGPUDevice, format: WGPUTextureFormat, usage: WGPUTextureUsage/int, width: int, height: int, viewFormatCount: int, viewFormats: WGPUTextureFormat *, alphaMode: WGPUCompositeAlphaMode, presentMode: WGPUPresentMode self._wgpu_config = new_struct_p( @@ -828,8 +857,8 @@ def _configure_screen( viewFormats=c_view_formats, alphaMode=c_alpha_mode, presentMode=c_present_mode, - width=width, # overriden elsewhere in this class - height=height, # overriden elsewhere in this class + width=self._physical_size[0], # overriden elsewhere in this class + height=self._physical_size[1], # overriden elsewhere in this class ) # Configure now (if possible) @@ -877,17 +906,22 @@ def _create_texture_screen(self): # that by providing a dummy texture, and warn when this happens too often in succession. # Get size info - old_size = (self._wgpu_config.width, self._wgpu_config.height) - new_size = tuple(self._get_canvas().get_physical_size()) - if new_size[0] <= 0 or new_size[1] <= 0: - # It's the responsibility of the drawing /scheduling logic to prevent this case. - raise RuntimeError("Cannot get texture for a canvas with zero pixels.") - - # Re-configure when the size has changed. - if new_size != old_size: - self._wgpu_config.width = new_size[0] - self._wgpu_config.height = new_size[1] - self._configure_screen_real() + if self._has_new_size: + self._has_new_size = False + new_size = self._physical_size + old_size = (self._wgpu_config.width, self._wgpu_config.height) + if new_size[0] <= 0 or new_size[1] <= 0: + # It's the responsibility of the drawing /scheduling logic to prevent this case. + raise RuntimeError("Cannot get texture for a canvas with zero pixels.") + + # Re-configure when the size has changed. + if new_size != old_size: + self._wgpu_config.width = new_size[0] + self._wgpu_config.height = new_size[1] + self._configure_screen_real() + + # Clear buffer, so we only have to perform these checks when set_physical_size has been called. + self._new_physical_size = None # Prepare for obtaining a texture. status_str_map = enum_int2str["SurfaceGetCurrentTextureStatus"] @@ -952,7 +986,7 @@ def _create_texture_screen(self): libf.wgpuTextureRelease(texture_id) texture_id = 0 self._skip_present_screen = True - return self._create_texture_bitmap() + return self._create_plain_texture() else: # WGPUSurfaceGetCurrentTextureStatus_OutOfMemory # WGPUSurfaceGetCurrentTextureStatus_DeviceLost @@ -1005,6 +1039,21 @@ def _create_texture_screen(self): device = self._config["device"] return GPUTexture(label, texture_id, device, tex_info) + def _create_plain_texture(self): + # To have a dummy texture in case we have a size mismatch and must drop frames + width, height = self._physical_size + width, height = max(width, 1), max(height, 1) + + # Note that the label 'present' is used by read_texture() to determine + # that it can use a shared copy buffer. + device = self._config["device"] + return device.create_texture( + label="present", + size=(width, height, 1), + format=self._config["format"], + usage=self._config["usage"] | flags.TextureUsage.COPY_SRC, + ) + def _present_screen(self): if self._skip_present_screen: self._skip_present_screen = False diff --git a/wgpu/resources/codegen_report.md b/wgpu/resources/codegen_report.md index fcfb995e..f87fb411 100644 --- a/wgpu/resources/codegen_report.md +++ b/wgpu/resources/codegen_report.md @@ -9,9 +9,9 @@ * Wrote 34 enums to enums.py * Wrote 60 structs to structs.py ### Patching API for _classes.py -* Diffs for GPU: add enumerate_adapters_async, add enumerate_adapters_sync, change get_preferred_canvas_format, change request_adapter_async, change request_adapter_sync +* Diffs for GPU: add enumerate_adapters_async, add enumerate_adapters_sync, add get_canvas_context, change get_preferred_canvas_format, change request_adapter_async, change request_adapter_sync * Diffs for GPUPromise: add GPUPromise -* Diffs for GPUCanvasContext: add get_preferred_format, add present +* Diffs for GPUCanvasContext: add get_preferred_format, add physical_size, add present, add set_physical_size, hide canvas * Diffs for GPUAdapter: add summary * Diffs for GPUDevice: add adapter, add create_buffer_with_data, hide import_external_texture, hide lost_async, hide lost_sync, hide onuncapturederror, hide pop_error_scope_async, hide pop_error_scope_sync, hide push_error_scope * Diffs for GPUBuffer: add read_mapped, add write_mapped, hide get_mapped_range @@ -19,9 +19,9 @@ * Diffs for GPUTextureView: add size, add texture * Diffs for GPUBindingCommandsMixin: change set_bind_group * Diffs for GPUQueue: add read_buffer, add read_texture, hide copy_external_image_to_texture -* Validated 38 classes, 121 methods, 49 properties +* Validated 38 classes, 120 methods, 50 properties ### Patching API for backends/wgpu_native/_api.py -* Validated 38 classes, 113 methods, 0 properties +* Validated 38 classes, 115 methods, 0 properties ## Validating backends/wgpu_native/_api.py * Enum field FeatureName.core-features-and-limits missing in webgpu.h/wgpu.h * Enum field FeatureName.subgroups missing in webgpu.h/wgpu.h diff --git a/wgpu/utils/imgui/imgui_renderer.py b/wgpu/utils/imgui/imgui_renderer.py index 3f7bba9b..aae392df 100644 --- a/wgpu/utils/imgui/imgui_renderer.py +++ b/wgpu/utils/imgui/imgui_renderer.py @@ -66,7 +66,7 @@ class ImguiRenderer: def __init__(self, device, canvas, render_target_format=None): # Prepare present context - self._canvas_context = canvas.get_context("wgpu") + self._canvas_context = canvas.get_wgpu_context() # if the canvas is not configured, we configure it self. if self._canvas_context._config is None: