From 0c370573510fdccca0667cc85473aeba2e3b12e6 Mon Sep 17 00:00:00 2001 From: Korijn van Golen Date: Fri, 31 Oct 2025 20:45:23 +0100 Subject: [PATCH 01/24] invert context wrapper control --- examples/cube.py | 41 ++++++++++++++-------------- examples/gui_direct.py | 41 ++++++++++++++-------------- wgpu/_classes.py | 62 ++++++++++++++++++++++++++++++++++++------ 3 files changed, 94 insertions(+), 50 deletions(-) diff --git a/examples/cube.py b/examples/cube.py index 13a49126..ca33d02f 100644 --- a/examples/cube.py +++ b/examples/cube.py @@ -24,11 +24,11 @@ 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. + The given context must implement ..., but nothing more. Returns the draw function. """ @@ -39,19 +39,19 @@ 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. + The given context must implement ..., but nothing more. Returns the draw function. """ adapter = await wgpu.gpu.request_adapter_async(power_preference="high-performance") @@ -61,24 +61,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 +86,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 +249,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 +303,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" @@ -488,11 +487,11 @@ def draw_func(): # Async @loop.add_task async def init(): - draw_frame = await setup_drawing_async(canvas) + draw_frame = await setup_drawing_async(canvas.get_context("wgpu")) canvas.request_draw(draw_frame) else: # Sync - draw_frame = setup_drawing_sync(canvas) + draw_frame = setup_drawing_sync(canvas.get_context("wgpu")) canvas.request_draw(draw_frame) # loop.add_task(poller) diff --git a/examples/gui_direct.py b/examples/gui_direct.py index f0b1af4f..89415c7c 100644 --- a/examples/gui_direct.py +++ b/examples/gui_direct.py @@ -66,43 +66,42 @@ 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) +title = "wgpu glfw direct" +window = glfw.create_window(640, 480, title, None, None) +present_methods = get_glfw_present_methods(window) - self.window = glfw.create_window(640, 480, title, None, None) - self.context = GPUCanvasContext(self, get_glfw_present_methods(self.window)) +# Create a GPUCanvasContext directly (no wrapper class needed). +context = GPUCanvasContext(None, present_methods) - 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="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) + # create canvas/context — setup_drawing_sync expects a "canvas" that can + # provide get_context/get_physical_size; GPUCanvasContext now implements those. + 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 +110,7 @@ def main(): last_frame_time, frame_count = time.perf_counter(), 0 # dispose resources - glfw.destroy_window(canvas.window) + glfw.destroy_window(window) # allow proper cleanup (workaround for glfw bug) end_time = time.perf_counter() + 0.1 diff --git a/wgpu/_classes.py b/wgpu/_classes.py index c7c9f400..a202aeb4 100644 --- a/wgpu/_classes.py +++ b/wgpu/_classes.py @@ -232,7 +232,38 @@ class GPUCanvasContext: def __init__(self, canvas, present_methods): self._ot.increase(self.__class__.__name__) - self._canvas_ref = weakref.ref(canvas) + # allow None: if canvas is None, create a small dummy that provides + # get_physical_size() and get_context() so existing backend code + # that calls self._get_canvas().get_physical_size() continues to work. + if canvas is None: + + class _DummyCanvas: + def __init__(self, ctx): + self._ctx = ctx + # keep an optional _vsync flag like real canvases may have + self._vsync = True + + def get_physical_size(self): + size = self._ctx._explicit_size + if size is None: + raise RuntimeError( + "Canvas physical size not set. Call GPUCanvasContext.set_physical_size((w,h))." + ) + return size + + def get_context(self, kind="wgpu"): + # mimic real canvas.get_context("wgpu") + if kind != "wgpu": + return None + return self._ctx + + self._dummy = _DummyCanvas(self) + self._canvas_ref = None + else: + self._dummy = None + self._canvas_ref = weakref.ref(canvas) + + self._explicit_size = None # Surface capabilities. Stored the first time it is obtained self._capabilities = None @@ -249,13 +280,28 @@ def __init__(self, canvas, present_methods): def _get_canvas(self): """Getter method for internal use.""" - return self._canvas_ref() + return self._dummy or self._canvas_ref() - # IDL: readonly attribute (HTMLCanvasElement or OffscreenCanvas) canvas; - @property - def canvas(self) -> CanvasLike: - """The associated canvas object.""" - return self._canvas_ref() + # Application-facing: let caller set current framebuffer physical size + def set_physical_size(self, size): + """Set the current framebuffer physical size (width, height). + + When the context was created without a real canvas, the application + must call this to keep the context informed about the window/framebuffer size. + """ + w, h = size + self._explicit_size = int(w), int(h) + + def get_physical_size(self): + """Return current framebuffer physical size. + + Delegates to the real canvas if present, otherwise returns the size + previously set with set_physical_size(). + """ + if self._dummy: + return self._explicit_size + c = self._canvas_ref() + return c.get_physical_size() def _get_capabilities(self, adapter): """Get dict of capabilities and cache the result.""" @@ -1064,7 +1110,7 @@ def create_bind_group( binding=2, resource=wgpu.BufferBinding( buffer=a_buffer, - offset=0. + offset=0., size=812, ) ) From dd9ea18d6703ffd31c76c181dbe097d6ae47755b Mon Sep 17 00:00:00 2001 From: Korijn van Golen Date: Fri, 31 Oct 2025 20:49:54 +0100 Subject: [PATCH 02/24] go one step further --- examples/gui_direct.py | 3 +- wgpu/_classes.py | 64 ++++++++++++++----------------- wgpu/backends/wgpu_native/_api.py | 4 +- 3 files changed, 31 insertions(+), 40 deletions(-) diff --git a/examples/gui_direct.py b/examples/gui_direct.py index 89415c7c..5bbb2d83 100644 --- a/examples/gui_direct.py +++ b/examples/gui_direct.py @@ -75,8 +75,7 @@ def get_glfw_present_methods(window): window = glfw.create_window(640, 480, title, None, None) present_methods = get_glfw_present_methods(window) -# Create a GPUCanvasContext directly (no wrapper class needed). -context = GPUCanvasContext(None, present_methods) +context = GPUCanvasContext(present_methods) # Initialize physical size once. For robust apps update this on resize events. context.set_physical_size(glfw.get_framebuffer_size(window)) diff --git a/wgpu/_classes.py b/wgpu/_classes.py index a202aeb4..d2f5d003 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 @@ -230,39 +229,35 @@ class GPUCanvasContext: _ot = object_tracker - def __init__(self, canvas, present_methods): + def __init__(self, present_methods): self._ot.increase(self.__class__.__name__) - # allow None: if canvas is None, create a small dummy that provides + + # create a small dummy that provides # get_physical_size() and get_context() so existing backend code # that calls self._get_canvas().get_physical_size() continues to work. - if canvas is None: - - class _DummyCanvas: - def __init__(self, ctx): - self._ctx = ctx - # keep an optional _vsync flag like real canvases may have - self._vsync = True - - def get_physical_size(self): - size = self._ctx._explicit_size - if size is None: - raise RuntimeError( - "Canvas physical size not set. Call GPUCanvasContext.set_physical_size((w,h))." - ) - return size - - def get_context(self, kind="wgpu"): - # mimic real canvas.get_context("wgpu") - if kind != "wgpu": - return None - return self._ctx - - self._dummy = _DummyCanvas(self) - self._canvas_ref = None - else: - self._dummy = None - self._canvas_ref = weakref.ref(canvas) - + class _DummyCanvas: + def __init__(self, ctx): + self._ctx = ctx + # keep an optional _vsync flag like real canvases may have + self._vsync = True + + def get_physical_size(self): + size = self._ctx._explicit_size + if size is None: + raise RuntimeError( + "Canvas physical size not set. Call GPUCanvasContext.set_physical_size((w,h))." + ) + return size + + def get_context(self, kind="wgpu"): + # mimic real canvas.get_context("wgpu") + if kind != "wgpu": + return None + return self._ctx + + self._dummy = _DummyCanvas(self) + + # Framebuffer size self._explicit_size = None # Surface capabilities. Stored the first time it is obtained @@ -280,7 +275,7 @@ def get_context(self, kind="wgpu"): def _get_canvas(self): """Getter method for internal use.""" - return self._dummy or self._canvas_ref() + return self._dummy # Application-facing: let caller set current framebuffer physical size def set_physical_size(self, size): @@ -298,10 +293,7 @@ def get_physical_size(self): Delegates to the real canvas if present, otherwise returns the size previously set with set_physical_size(). """ - if self._dummy: - return self._explicit_size - c = self._canvas_ref() - return c.get_physical_size() + return self._explicit_size def _get_capabilities(self, adapter): """Get dict of capabilities and cache the result.""" diff --git a/wgpu/backends/wgpu_native/_api.py b/wgpu/backends/wgpu_native/_api.py index 320d1220..8d2b321a 100644 --- a/wgpu/backends/wgpu_native/_api.py +++ b/wgpu/backends/wgpu_native/_api.py @@ -668,8 +668,8 @@ 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_methods): + super().__init__(present_methods) # Obtain the surface id. The lifetime is of the surface is bound # to the lifetime of this context object. From e9db27c31df3ad4d187dbf27aa3d04530fbd8745 Mon Sep 17 00:00:00 2001 From: Korijn van Golen Date: Fri, 31 Oct 2025 20:53:56 +0100 Subject: [PATCH 03/24] clean up a bit --- wgpu/_classes.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/wgpu/_classes.py b/wgpu/_classes.py index d2f5d003..7d9657ff 100644 --- a/wgpu/_classes.py +++ b/wgpu/_classes.py @@ -232,9 +232,10 @@ class GPUCanvasContext: def __init__(self, present_methods): self._ot.increase(self.__class__.__name__) - # create a small dummy that provides + # create a small dummy purely for backward compatibility that provides # get_physical_size() and get_context() so existing backend code # that calls self._get_canvas().get_physical_size() continues to work. + # ideally though, wgpu-py does not require a canvas reference at all class _DummyCanvas: def __init__(self, ctx): self._ctx = ctx @@ -277,22 +278,17 @@ def _get_canvas(self): """Getter method for internal use.""" return self._dummy - # Application-facing: let caller set current framebuffer physical size def set_physical_size(self, size): """Set the current framebuffer physical size (width, height). - When the context was created without a real canvas, the application - must call this to keep the context informed about the window/framebuffer size. + The application must call this to keep the context informed about + the window/framebuffer size. """ w, h = size self._explicit_size = int(w), int(h) def get_physical_size(self): - """Return current framebuffer physical size. - - Delegates to the real canvas if present, otherwise returns the size - previously set with set_physical_size(). - """ + """Return current framebuffer physical size.""" return self._explicit_size def _get_capabilities(self, adapter): From 8bd6c82aea6ee0775d620a8a423b8417dd896055 Mon Sep 17 00:00:00 2001 From: Korijn van Golen Date: Sun, 2 Nov 2025 11:23:02 +0100 Subject: [PATCH 04/24] attempt to clear out get_physical_size and vsync, remove dummy canvas --- examples/gui_direct.py | 4 +++ wgpu/_canvas.py | 4 --- wgpu/_classes.py | 49 +++++++++---------------------- wgpu/backends/wgpu_native/_api.py | 33 +++++++++++---------- 4 files changed, 36 insertions(+), 54 deletions(-) diff --git a/examples/gui_direct.py b/examples/gui_direct.py index 5bbb2d83..44c67246 100644 --- a/examples/gui_direct.py +++ b/examples/gui_direct.py @@ -32,6 +32,7 @@ def get_glfw_present_methods(window): "screen": { "platform": "windows", "window": int(glfw.get_win32_window(window)), + "vsync": True, } } elif sys.platform.startswith("darwin"): @@ -39,6 +40,7 @@ def get_glfw_present_methods(window): "screen": { "platform": "cocoa", "window": int(glfw.get_cocoa_window(window)), + "vsync": True, } } elif sys.platform.startswith("linux"): @@ -48,6 +50,7 @@ def get_glfw_present_methods(window): "platform": "wayland", "window": int(glfw.get_wayland_window(window)), "display": int(glfw.get_wayland_display()), + "vsync": True, } } else: @@ -56,6 +59,7 @@ def get_glfw_present_methods(window): "platform": "x11", "window": int(glfw.get_x11_window(window)), "display": int(glfw.get_x11_display()), + "vsync": True, } } else: diff --git a/wgpu/_canvas.py b/wgpu/_canvas.py index e260627d..4773f110 100644 --- a/wgpu/_canvas.py +++ b/wgpu/_canvas.py @@ -13,10 +13,6 @@ class WgpuCanvasInterface: _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. diff --git a/wgpu/_classes.py b/wgpu/_classes.py index 7d9657ff..a9cad131 100644 --- a/wgpu/_classes.py +++ b/wgpu/_classes.py @@ -232,34 +232,9 @@ class GPUCanvasContext: def __init__(self, present_methods): self._ot.increase(self.__class__.__name__) - # create a small dummy purely for backward compatibility that provides - # get_physical_size() and get_context() so existing backend code - # that calls self._get_canvas().get_physical_size() continues to work. - # ideally though, wgpu-py does not require a canvas reference at all - class _DummyCanvas: - def __init__(self, ctx): - self._ctx = ctx - # keep an optional _vsync flag like real canvases may have - self._vsync = True - - def get_physical_size(self): - size = self._ctx._explicit_size - if size is None: - raise RuntimeError( - "Canvas physical size not set. Call GPUCanvasContext.set_physical_size((w,h))." - ) - return size - - def get_context(self, kind="wgpu"): - # mimic real canvas.get_context("wgpu") - if kind != "wgpu": - return None - return self._ctx - - self._dummy = _DummyCanvas(self) - - # Framebuffer size - self._explicit_size = None + # Buffer to hold new physical size + # will be applied when the context reconfigures + self._new_physical_size = None # Surface capabilities. Stored the first time it is obtained self._capabilities = None @@ -285,11 +260,7 @@ def set_physical_size(self, size): the window/framebuffer size. """ w, h = size - self._explicit_size = int(w), int(h) - - def get_physical_size(self): - """Return current framebuffer physical size.""" - return self._explicit_size + self._new_physical_size = int(w), int(h) def _get_capabilities(self, adapter): """Get dict of capabilities and cache the result.""" @@ -356,6 +327,7 @@ def configure( color_space: str = "srgb", tone_mapping: structs.CanvasToneMappingStruct | None = None, alpha_mode: enums.CanvasAlphaModeEnum = "opaque", + size: tuple[int, int] = (320, 240), ) -> None: """Configures the presentation context for the associated canvas. Destroys any textures produced with a previous configuration. @@ -376,6 +348,7 @@ def configure( alpha_mode (structs.CanvasAlphaMode): Determines the effect that alpha values will have on the content of textures returned by ``get_current_texture()`` when read, displayed, or used as an image source. Default "opaque". + size (tuple[int, int]): The physical size of the canvas in pixels. """ # Check types tone_mapping = {} if tone_mapping is None else tone_mapping @@ -391,6 +364,11 @@ def configure( if not isinstance(usage, int): usage = str_flag_to_int(flags.TextureUsage, usage) + if not isinstance(size, tuple) or len(size) != 2: + raise TypeError("Configure: size must be a tuple (width, height).") + if not size[0] > 0 or not size[1] > 0: + raise ValueError("Configure: size values must be positive.") + color_space # noqa - not really supported, just assume srgb for now tone_mapping # noqa - not supported yet @@ -437,6 +415,7 @@ def configure( "color_space": color_space, "tone_mapping": tone_mapping, "alpha_mode": alpha_mode, + "size": size, } if self._present_method == "screen": @@ -452,6 +431,7 @@ def _configure_screen( color_space, tone_mapping, alpha_mode, + size, ): raise NotImplementedError() @@ -491,8 +471,7 @@ def get_current_texture(self) -> GPUTexture: return self._texture def _create_texture_bitmap(self): - canvas = self._get_canvas() - width, height = canvas.get_physical_size() + width, height = (self._wgpu_config.width, self._wgpu_config.height) width, height = max(width, 1), max(height, 1) # Note that the label 'present' is used by read_texture() to determine diff --git a/wgpu/backends/wgpu_native/_api.py b/wgpu/backends/wgpu_native/_api.py index 8d2b321a..2916c3a1 100644 --- a/wgpu/backends/wgpu_native/_api.py +++ b/wgpu/backends/wgpu_native/_api.py @@ -769,6 +769,7 @@ def _configure_screen( color_space, tone_mapping, alpha_mode, + size, ): capabilities = self._get_capabilities(device.adapter) @@ -799,7 +800,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 getattr(self._present_methods[self._present_method], "vsync", True): present_mode_pref = ["fifo", "mailbox"] else: present_mode_pref = ["immediate", "mailbox", "fifo"] @@ -811,7 +812,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( @@ -824,8 +824,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=size[0], # overriden elsewhere in this class + height=size[1], # overriden elsewhere in this class ) # Configure now (if possible) @@ -873,17 +873,20 @@ 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 (new_size := self._new_physical_size) is not None: + 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"] From ee03c1ff764b973e67fe03da9ef7e2d226e4cc84 Mon Sep 17 00:00:00 2001 From: Korijn van Golen Date: Sun, 2 Nov 2025 11:27:26 +0100 Subject: [PATCH 05/24] cleanup --- wgpu/_classes.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/wgpu/_classes.py b/wgpu/_classes.py index a9cad131..69b2126a 100644 --- a/wgpu/_classes.py +++ b/wgpu/_classes.py @@ -249,10 +249,6 @@ def __init__(self, present_methods): 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._dummy - def set_physical_size(self, size): """Set the current framebuffer physical size (width, height). @@ -260,6 +256,8 @@ def set_physical_size(self, size): the window/framebuffer size. """ w, h = size + if w <= 0 or h <= 0: + raise ValueError("Physical size values must be positive.") self._new_physical_size = int(w), int(h) def _get_capabilities(self, adapter): From 95e88b07eb712a4b2c51695abf2c538d812a4665 Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Thu, 6 Nov 2025 14:56:18 +0100 Subject: [PATCH 06/24] Work with new rendercanvas, and clean up some of the bitmap presenting --- examples/cube.py | 2 +- examples/gui_direct.py | 46 +++----- tests/test_canvas.py | 2 +- wgpu/__init__.py | 21 +--- wgpu/_canvas.py | 2 + wgpu/_classes.py | 184 ++++++------------------------ wgpu/backends/wgpu_native/_api.py | 22 ++-- 7 files changed, 70 insertions(+), 209 deletions(-) diff --git a/examples/cube.py b/examples/cube.py index ca33d02f..a0bdeabd 100644 --- a/examples/cube.py +++ b/examples/cube.py @@ -491,7 +491,7 @@ async def init(): canvas.request_draw(draw_frame) else: # Sync - draw_frame = setup_drawing_sync(canvas.get_context("wgpu")) + draw_frame = setup_drawing_sync(canvas.get_wgpu_context()) canvas.request_draw(draw_frame) # loop.add_task(poller) diff --git a/examples/gui_direct.py b/examples/gui_direct.py index 44c67246..a64e3906 100644 --- a/examples/gui_direct.py +++ b/examples/gui_direct.py @@ -26,41 +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)), - "vsync": True, - } + "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)), - "vsync": True, - } + "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()), - "vsync": True, - } + "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()), - "vsync": True, - } + "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}.") @@ -77,12 +69,12 @@ def get_glfw_present_methods(window): title = "wgpu glfw direct" window = glfw.create_window(640, 480, title, None, None) -present_methods = get_glfw_present_methods(window) +present_info = get_glfw_present_info(window) -context = GPUCanvasContext(present_methods) +context = GPUCanvasContext(present_info) # Initialize physical size once. For robust apps update this on resize events. -context.set_physical_size(glfw.get_framebuffer_size(window)) +context.set_physical_size(*glfw.get_framebuffer_size(window)) def main(): @@ -99,7 +91,7 @@ def main(): glfw.poll_events() # resize handling - context.set_physical_size(glfw.get_framebuffer_size(window)) + context.set_physical_size(*glfw.get_framebuffer_size(window)) # draw a frame draw_frame() diff --git a/tests/test_canvas.py b/tests/test_canvas.py index f6573892..06c2a3ac 100644 --- a/tests/test_canvas.py +++ b/tests/test_canvas.py @@ -94,7 +94,7 @@ def test_custom_canvas(): class CustomCanvas: # implements wgpu.WgpuCanvasInterface def __init__(self): self._canvas_context = None - self._present_methods = { + self._present_methods = { # TODO: remove, or move test to rendercanvas? "bitmap": { "formats": ["rgba-u8"], } diff --git a/wgpu/__init__.py b/wgpu/__init__.py index 603630f1..6d1a6d17 100644 --- a/wgpu/__init__.py +++ b/wgpu/__init__.py @@ -20,21 +20,6 @@ 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) + raise RuntimeError( + "The rendercanvas_context_hook is deprecated. If you're using rendercanvas, please update to the latest version. Otherwise, use TODO" + ) diff --git a/wgpu/_canvas.py b/wgpu/_canvas.py index 4773f110..a195e0c4 100644 --- a/wgpu/_canvas.py +++ b/wgpu/_canvas.py @@ -1,5 +1,7 @@ import wgpu +# TODO: remove + class WgpuCanvasInterface: """The minimal interface to be a valid canvas that wgpu can render to. diff --git a/wgpu/_classes.py b/wgpu/_classes.py index 69b2126a..0e07a3e9 100644 --- a/wgpu/_classes.py +++ b/wgpu/_classes.py @@ -219,22 +219,34 @@ class GPUPromise(BaseGPUPromise): class GPUCanvasContext: """Represents a context to configure a canvas and render to it. - Can be obtained via `canvas.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 - combines (and checks) the user's preferences with the capabilities and - preferences of the canvas. + Can be obtained via .... TODO + + When ``rendercanvas`` is used, it will automatically wrap a + ``GPUCanvasContext`` if necessary. From the p.o.v. of ``rendercanvas``, this + implements present_method 'screen'. + + 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. + + 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 dict must have the info so + the canvas or wgpu context can be retrieved. This dict is an interface between + ``rendercanvas`` and ``wgpu-py``. """ _ot = object_tracker - def __init__(self, present_methods): + def __init__(self, present_info: dict): self._ot.increase(self.__class__.__name__) + self._present_info = present_info - # Buffer to hold new physical size - # will be applied when the context reconfigures - self._new_physical_size = None + # 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 @@ -245,53 +257,23 @@ def __init__(self, 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 set_physical_size(self, size): - """Set the current framebuffer physical size (width, height). + 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. """ - w, h = size - if w <= 0 or h <= 0: + if width <= 0 or height <= 0: raise ValueError("Physical size values must be positive.") - self._new_physical_size = int(w), int(h) + self._physical_size = int(width), int(height) + self._has_new_size = True 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"] @@ -325,7 +307,6 @@ def configure( color_space: str = "srgb", tone_mapping: structs.CanvasToneMappingStruct | None = None, alpha_mode: enums.CanvasAlphaModeEnum = "opaque", - size: tuple[int, int] = (320, 240), ) -> None: """Configures the presentation context for the associated canvas. Destroys any textures produced with a previous configuration. @@ -346,7 +327,6 @@ def configure( alpha_mode (structs.CanvasAlphaMode): Determines the effect that alpha values will have on the content of textures returned by ``get_current_texture()`` when read, displayed, or used as an image source. Default "opaque". - size (tuple[int, int]): The physical size of the canvas in pixels. """ # Check types tone_mapping = {} if tone_mapping is None else tone_mapping @@ -362,11 +342,6 @@ def configure( if not isinstance(usage, int): usage = str_flag_to_int(flags.TextureUsage, usage) - if not isinstance(size, tuple) or len(size) != 2: - raise TypeError("Configure: size must be a tuple (width, height).") - if not size[0] > 0 or not size[1] > 0: - raise ValueError("Configure: size values must be positive.") - color_space # noqa - not really supported, just assume srgb for now tone_mapping # noqa - not supported yet @@ -413,11 +388,9 @@ def configure( "color_space": color_space, "tone_mapping": tone_mapping, "alpha_mode": alpha_mode, - "size": size, } - if self._present_method == "screen": - self._configure_screen(**self._config) + self._configure_screen(**self._config) def _configure_screen( self, @@ -438,8 +411,7 @@ def unconfigure(self) -> None: """Removes the presentation context configuration. Destroys any textures produced while configured. """ - if self._present_method == "screen": - self._unconfigure_screen() + self._unconfigure_screen() self._config = None self._drop_texture() @@ -453,35 +425,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): - width, height = (self._wgpu_config.width, self._wgpu_config.height) - 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() @@ -497,73 +445,9 @@ def present(self) -> None: 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": + 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() diff --git a/wgpu/backends/wgpu_native/_api.py b/wgpu/backends/wgpu_native/_api.py index 2916c3a1..2301ee76 100644 --- a/wgpu/backends/wgpu_native/_api.py +++ b/wgpu/backends/wgpu_native/_api.py @@ -668,15 +668,12 @@ class GPUCanvasContext(classes.GPUCanvasContext): _wgpu_config = None _skip_present_screen = False - def __init__(self, present_methods): - super().__init__(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 @@ -769,7 +766,6 @@ def _configure_screen( color_space, tone_mapping, alpha_mode, - size, ): capabilities = self._get_capabilities(device.adapter) @@ -800,7 +796,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._present_methods[self._present_method], "vsync", True): + if self._present_info.get("vsync", True): present_mode_pref = ["fifo", "mailbox"] else: present_mode_pref = ["immediate", "mailbox", "fifo"] @@ -824,8 +820,8 @@ def _configure_screen( viewFormats=c_view_formats, alphaMode=c_alpha_mode, presentMode=c_present_mode, - width=size[0], # overriden elsewhere in this class - height=size[1], # 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) @@ -873,7 +869,9 @@ def _create_texture_screen(self): # that by providing a dummy texture, and warn when this happens too often in succession. # Get size info - if (new_size := self._new_physical_size) is not None: + 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. @@ -884,7 +882,7 @@ def _create_texture_screen(self): 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 From 301249e42876673deccda780f9bd84f348e61de6 Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Thu, 6 Nov 2025 16:05:11 +0100 Subject: [PATCH 07/24] Provide public method to create CanvasContext --- wgpu/_classes.py | 7 +++++++ wgpu/backends/wgpu_native/_api.py | 4 ++++ 2 files changed, 11 insertions(+) diff --git a/wgpu/_classes.py b/wgpu/_classes.py index 0e07a3e9..3c539781 100644 --- a/wgpu/_classes.py +++ b/wgpu/_classes.py @@ -206,6 +206,13 @@ 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.""" + from .backends.auto import gpu + + return gpu.get_canvas_context(present_info) + # Instantiate API entrypoint gpu = GPU() diff --git a/wgpu/backends/wgpu_native/_api.py b/wgpu/backends/wgpu_native/_api.py index 2301ee76..851f2e73 100644 --- a/wgpu/backends/wgpu_native/_api.py +++ b/wgpu/backends/wgpu_native/_api.py @@ -647,6 +647,10 @@ 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.""" + return GPUCanvasContext(present_info) + # Instantiate API entrypoint gpu = GPU() From 19e3474e5d2936a171e9fcce5c2e791ff4e29b1c Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Fri, 7 Nov 2025 12:17:46 +0100 Subject: [PATCH 08/24] Remove references to CanvasInterface --- docs/wgpu.rst | 14 +++--- examples/triangle.py | 2 - tests/test_canvas.py | 71 +------------------------------ wgpu/_canvas.py | 64 ---------------------------- wgpu/_classes.py | 4 +- wgpu/backends/wgpu_native/_api.py | 26 +++++++++-- 6 files changed, 33 insertions(+), 148 deletions(-) delete mode 100644 wgpu/_canvas.py 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/triangle.py b/examples/triangle.py index b6a300e8..5a9ac97c 100644 --- a/examples/triangle.py +++ b/examples/triangle.py @@ -29,7 +29,6 @@ def setup_drawing_sync( ) -> Callable: """Setup to draw a triangle on the given canvas. - The given canvas must implement WgpuCanvasInterface, but nothing more. Returns the draw function. """ @@ -46,7 +45,6 @@ def setup_drawing_sync( async def setup_drawing_async(canvas, limits=None, format=None) -> Callable: """Setup to async-draw a triangle on the given canvas. - The given canvas must implement WgpuCanvasInterface, but nothing more. Returns the draw function. """ diff --git a/tests/test_canvas.py b/tests/test_canvas.py index 06c2a3ac..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 = { # TODO: remove, or move test to rendercanvas? - "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/wgpu/_canvas.py b/wgpu/_canvas.py deleted file mode 100644 index a195e0c4..00000000 --- a/wgpu/_canvas.py +++ /dev/null @@ -1,64 +0,0 @@ -import wgpu - -# TODO: remove - - -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_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 3c539781..d8e3f47f 100644 --- a/wgpu/_classes.py +++ b/wgpu/_classes.py @@ -130,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. """ diff --git a/wgpu/backends/wgpu_native/_api.py b/wgpu/backends/wgpu_native/_api.py index 851f2e73..e3793998 100644 --- a/wgpu/backends/wgpu_native/_api.py +++ b/wgpu/backends/wgpu_native/_api.py @@ -429,6 +429,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 @@ -451,8 +468,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 @@ -472,6 +489,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 @@ -479,7 +497,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 From 10dfd32973c8ea2718cbbd3b96c1f3ed3e4e5c4d Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Fri, 7 Nov 2025 12:24:19 +0100 Subject: [PATCH 09/24] Prefer canvas.get_wgpu_context() over canvas.get_context('wgpu') --- docs/guide.rst | 2 +- examples/cube.py | 2 +- examples/gui_direct.py | 2 -- 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 +- wgpu/backends/wgpu_native/_api.py | 2 +- wgpu/utils/imgui/imgui_renderer.py | 2 +- 10 files changed, 11 insertions(+), 13 deletions(-) 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/examples/cube.py b/examples/cube.py index a0bdeabd..e25e52d1 100644 --- a/examples/cube.py +++ b/examples/cube.py @@ -487,7 +487,7 @@ def draw_func(): # Async @loop.add_task async def init(): - draw_frame = await setup_drawing_async(canvas.get_context("wgpu")) + draw_frame = await setup_drawing_async(canvas.get_wgpu_context()) canvas.request_draw(draw_frame) else: # Sync diff --git a/examples/gui_direct.py b/examples/gui_direct.py index a64e3906..abfd3c2f 100644 --- a/examples/gui_direct.py +++ b/examples/gui_direct.py @@ -78,8 +78,6 @@ def get_glfw_present_info(window): def main(): - # create canvas/context — setup_drawing_sync expects a "canvas" that can - # provide get_context/get_physical_size; GPUCanvasContext now implements those. draw_frame = setup_drawing_sync(context) last_frame_time = time.perf_counter() 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/triangle.py b/examples/triangle.py index 5a9ac97c..4d657356 100644 --- a/examples/triangle.py +++ b/examples/triangle.py @@ -64,7 +64,7 @@ async def setup_drawing_async(canvas, limits=None, format=None) -> Callable: def get_render_pipeline_kwargs( canvas, device, render_texture_format ) -> wgpu.RenderPipelineDescriptor: - context = canvas.get_context("wgpu") + context = canvas.get_wgpu_context() if render_texture_format is None: render_texture_format = context.get_preferred_format(device.adapter) context.configure(device=device, format=render_texture_format) @@ -101,7 +101,7 @@ def get_draw_function( asynchronous: bool, ) -> Callable: def draw_frame_sync(): - current_texture = canvas.get_context("wgpu").get_current_texture() + current_texture = canvas.get_wgpu_context().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 1a89113c..9a2875b1 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("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) @@ -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("wgpu").get_current_texture() + current_texture = canvas.get_wgpu_context().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 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/wgpu/backends/wgpu_native/_api.py b/wgpu/backends/wgpu_native/_api.py index e3793998..2c058d14 100644 --- a/wgpu/backends/wgpu_native/_api.py +++ b/wgpu/backends/wgpu_native/_api.py @@ -435,7 +435,7 @@ def find_surface_id_from_canvas(canvas_or_context): surface_id = None # Try get context first, e.g. from rendercanvas if hasattr(ob, "get_context"): - ob = ob.get_context("wgpu") + ob = ob.get_wgpu_context() # 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): 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: From 00caadad591435e3ee2326f9cc1b2de9e986daf1 Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Fri, 7 Nov 2025 12:39:39 +0100 Subject: [PATCH 10/24] clean/docs --- examples/gui_direct.py | 4 ++-- wgpu/__init__.py | 4 ++-- wgpu/_classes.py | 27 +++++++++++++++------------ wgpu/backends/wgpu_native/_api.py | 14 ++++++++++++-- 4 files changed, 31 insertions(+), 18 deletions(-) diff --git a/examples/gui_direct.py b/examples/gui_direct.py index abfd3c2f..20d9ddd5 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 @@ -71,7 +71,7 @@ def get_glfw_present_info(window): window = glfw.create_window(640, 480, title, None, None) present_info = get_glfw_present_info(window) -context = GPUCanvasContext(present_info) +context = wgpu.gpu.get_canvas_context(present_info) # Initialize physical size once. For robust apps update this on resize events. context.set_physical_size(*glfw.get_framebuffer_size(window)) diff --git a/wgpu/__init__.py b/wgpu/__init__.py index 6d1a6d17..81ef938e 100644 --- a/wgpu/__init__.py +++ b/wgpu/__init__.py @@ -19,7 +19,7 @@ gpu = GPU() # noqa: F405 -def rendercanvas_context_hook(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 TODO" + "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/_classes.py b/wgpu/_classes.py index d8e3f47f..05bdf721 100644 --- a/wgpu/_classes.py +++ b/wgpu/_classes.py @@ -131,7 +131,7 @@ def request_adapter_async( force_fallback_adapter (bool): whether to use a (probably CPU-based) fallback adapter. 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. + 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. """ @@ -208,7 +208,17 @@ def wgsl_language_features(self) -> 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.""" + """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 dict must have the info so the + canvas or wgpu context can be retrieved. This dict is an interface + between ``rendercanvas`` and ``wgpu-py``. + """ from .backends.auto import gpu return gpu.get_canvas_context(present_info) @@ -226,23 +236,16 @@ class GPUPromise(BaseGPUPromise): class GPUCanvasContext: """Represents a context to configure a canvas and render to it. - Can be obtained via .... TODO + Can be obtained with ``wgpu.gpu.get_canvas_context()``. When ``rendercanvas`` is used, it will automatically wrap a - ``GPUCanvasContext`` if necessary. From the p.o.v. of ``rendercanvas``, this - implements present_method 'screen'. + ``GPUCanvasContext`` if necessary. From the perspective of ``rendercanvas``, this + implements the 'screen' present method. 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. - - 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 dict must have the info so - the canvas or wgpu context can be retrieved. This dict is an interface between - ``rendercanvas`` and ``wgpu-py``. """ _ot = object_tracker diff --git a/wgpu/backends/wgpu_native/_api.py b/wgpu/backends/wgpu_native/_api.py index 2c058d14..cc9fbf8c 100644 --- a/wgpu/backends/wgpu_native/_api.py +++ b/wgpu/backends/wgpu_native/_api.py @@ -435,7 +435,7 @@ def find_surface_id_from_canvas(canvas_or_context): surface_id = None # Try get context first, e.g. from rendercanvas if hasattr(ob, "get_context"): - ob = ob.get_wgpu_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): @@ -668,7 +668,17 @@ def to_py_str(key): 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.""" + """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 dict must have the info so the + canvas or wgpu context can be retrieved. This dict is an interface + between ``rendercanvas`` and ``wgpu-py``. + """ return GPUCanvasContext(present_info) From 63b4ef7d15da57e70de966882d7912acef9b0cee Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Fri, 7 Nov 2025 12:55:13 +0100 Subject: [PATCH 11/24] codegen --- examples/gui_direct.py | 1 + wgpu/_classes.py | 19 +++++++++++-------- wgpu/resources/codegen_report.md | 8 ++++---- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/examples/gui_direct.py b/examples/gui_direct.py index 20d9ddd5..f4137624 100644 --- a/examples/gui_direct.py +++ b/examples/gui_direct.py @@ -103,6 +103,7 @@ def main(): last_frame_time, frame_count = time.perf_counter(), 0 # dispose resources + context.unconfigure() glfw.destroy_window(window) # allow proper cleanup (workaround for glfw bug) diff --git a/wgpu/_classes.py b/wgpu/_classes.py index 05bdf721..da5ef93f 100644 --- a/wgpu/_classes.py +++ b/wgpu/_classes.py @@ -267,6 +267,13 @@ def __init__(self, present_info: dict): # The last used texture self._texture = None + # 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: + 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. @@ -319,6 +326,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. @@ -412,7 +420,6 @@ def _configure_screen( color_space, tone_mapping, alpha_mode, - size, ): raise NotImplementedError() @@ -421,9 +428,9 @@ def unconfigure(self) -> None: """Removes the presentation context configuration. Destroys any textures produced while configured. """ - self._unconfigure_screen() self._config = None self._drop_texture() + self._unconfigure_screen() def _unconfigure_screen(self): raise NotImplementedError() @@ -448,13 +455,9 @@ 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. - """ + """Present what has been drawn to the current texture, by compositing it to the canvas.""" if self._texture: self._present_screen() self._drop_texture() diff --git a/wgpu/resources/codegen_report.md b/wgpu/resources/codegen_report.md index fcfb995e..de6d81d7 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 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, 49 properties ### Patching API for backends/wgpu_native/_api.py -* Validated 38 classes, 113 methods, 0 properties +* Validated 38 classes, 114 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 From 6d6e0d89bc6f2d700eebb3d4bb81637a98b55bd1 Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Fri, 7 Nov 2025 13:04:14 +0100 Subject: [PATCH 12/24] fix --- wgpu/backends/wgpu_native/_api.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/wgpu/backends/wgpu_native/_api.py b/wgpu/backends/wgpu_native/_api.py index cc9fbf8c..6ff1bcf0 100644 --- a/wgpu/backends/wgpu_native/_api.py +++ b/wgpu/backends/wgpu_native/_api.py @@ -983,7 +983,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 @@ -1036,6 +1036,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 From a2746b6ea597d5cf967aa34ca0e16b88eb6938f2 Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Fri, 7 Nov 2025 13:09:09 +0100 Subject: [PATCH 13/24] Adjust triangle example too --- examples/cube.py | 9 ++++----- examples/triangle.py | 29 +++++++++++++++-------------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/examples/cube.py b/examples/cube.py index e25e52d1..439e2d9a 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 rendercanvas with the auto-backend. """ # test_example = true @@ -28,7 +28,6 @@ def setup_drawing_sync( ) -> Callable: """Setup to draw a rotating cube on the given context. - The given context must implement ..., but nothing more. Returns the draw function. """ @@ -51,7 +50,6 @@ def setup_drawing_sync( async def setup_drawing_async(context, limits=None): """Setup to async-draw a rotating cube on the given context. - The given context must implement ..., but nothing more. Returns the draw function. """ adapter = await wgpu.gpu.request_adapter_async(power_preference="high-performance") @@ -480,6 +478,7 @@ def draw_func(): max_fps=60, vsync=True, ) + context = canvas.get_wgpu_context() # Pick one @@ -487,11 +486,11 @@ def draw_func(): # Async @loop.add_task async def init(): - draw_frame = await setup_drawing_async(canvas.get_wgpu_context()) + draw_frame = await setup_drawing_async(context) canvas.request_draw(draw_frame) else: # Sync - draw_frame = setup_drawing_sync(canvas.get_wgpu_context()) + draw_frame = setup_drawing_sync(context) canvas.request_draw(draw_frame) # loop.add_task(poller) diff --git a/examples/triangle.py b/examples/triangle.py index 4d657356..4a3be785 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 rendercanvas with the auto-backend. """ @@ -25,9 +25,9 @@ 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. Returns the draw function. """ @@ -35,15 +35,15 @@ def setup_drawing_sync( 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. Returns the draw function. """ @@ -51,20 +51,19 @@ async def setup_drawing_async(canvas, limits=None, format=None) -> Callable: 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_wgpu_context() if render_texture_format is None: render_texture_format = context.get_preferred_format(device.adapter) context.configure(device=device, format=render_texture_format) @@ -94,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_wgpu_context().get_current_texture() + current_texture = context.get_current_texture() command_encoder = device.create_command_encoder() render_pass = command_encoder.begin_render_pass( @@ -174,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() From f1447e26e0c58b182a215e0d33fd1c0cc2ae2dfc Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Fri, 7 Nov 2025 13:34:46 +0100 Subject: [PATCH 14/24] forgot to update triangle.glsl --- examples/triangle_glsl.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/examples/triangle_glsl.py b/examples/triangle_glsl.py index 9a2875b1..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_wgpu_context() - 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_wgpu_context().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() From d495ab3122cbe679da4fc5e63bdc4a5e78c88d0b Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Fri, 7 Nov 2025 14:02:02 +0100 Subject: [PATCH 15/24] pin rendercanvas to future release --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) 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] From 7efdd37ea09cd7a9346526a35346ab0bb648a9e6 Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Mon, 10 Nov 2025 12:25:42 +0100 Subject: [PATCH 16/24] add prop to get physical size --- wgpu/_classes.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/wgpu/_classes.py b/wgpu/_classes.py index da5ef93f..10b2d642 100644 --- a/wgpu/_classes.py +++ b/wgpu/_classes.py @@ -285,6 +285,11 @@ def set_physical_size(self, width: int, height: int) -> None: 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") + 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: From 39bdeee3edf788f97befb839efadd61ecd0852de Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Mon, 10 Nov 2025 12:28:24 +0100 Subject: [PATCH 17/24] fix typos --- examples/cube.py | 2 +- examples/triangle.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/cube.py b/examples/cube.py index 439e2d9a..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 rendercanvas with the auto-backend. +as a script will use rendercanvas with the auto-backend. """ # test_example = true diff --git a/examples/triangle.py b/examples/triangle.py index 4a3be785..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 rendercanvas with the auto-backend. +as a script will use rendercanvas with the auto-backend. """ From 0a8c3170a3dc6b22c1844923819ad7a99c98ed79 Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Mon, 10 Nov 2025 12:31:47 +0100 Subject: [PATCH 18/24] Adjust docstring for pyodide --- wgpu/_classes.py | 5 ++--- wgpu/backends/wgpu_native/_api.py | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/wgpu/_classes.py b/wgpu/_classes.py index 10b2d642..31f939f5 100644 --- a/wgpu/_classes.py +++ b/wgpu/_classes.py @@ -215,9 +215,8 @@ def get_canvas_context(self, present_info: dict) -> GPUCanvasContext: 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 dict must have the info so the - canvas or wgpu context can be retrieved. This dict is an interface - between ``rendercanvas`` and ``wgpu-py``. + 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 diff --git a/wgpu/backends/wgpu_native/_api.py b/wgpu/backends/wgpu_native/_api.py index 17480392..ecd3b912 100644 --- a/wgpu/backends/wgpu_native/_api.py +++ b/wgpu/backends/wgpu_native/_api.py @@ -679,9 +679,8 @@ def get_canvas_context(self, present_info: dict) -> GPUCanvasContext: 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 dict must have the info so the - canvas or wgpu context can be retrieved. This dict is an interface - between ``rendercanvas`` and ``wgpu-py``. + 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) From 1f320f73c87680331b36070d734a708d5844416f Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Mon, 10 Nov 2025 12:33:30 +0100 Subject: [PATCH 19/24] codegen --- wgpu/resources/codegen_report.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/wgpu/resources/codegen_report.md b/wgpu/resources/codegen_report.md index de6d81d7..84889e4f 100644 --- a/wgpu/resources/codegen_report.md +++ b/wgpu/resources/codegen_report.md @@ -11,7 +11,7 @@ ### Patching API for _classes.py * 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, add set_physical_size, hide canvas +* 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, 120 methods, 49 properties +* Validated 38 classes, 121 methods, 49 properties ### Patching API for backends/wgpu_native/_api.py -* Validated 38 classes, 114 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 From 4f940d06b008d52b7d3ebbd9ee5e1952e7ba5b82 Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Mon, 10 Nov 2025 12:53:28 +0100 Subject: [PATCH 20/24] fix memtests --- tests_mem/test_gui.py | 1 - tests_mem/test_gui_glfw.py | 3 --- tests_mem/test_gui_qt.py | 3 --- tests_mem/testutils.py | 6 ++++-- 4 files changed, 4 insertions(+), 9 deletions(-) diff --git a/tests_mem/test_gui.py b/tests_mem/test_gui.py index 1825bde0..1b4968d8 100644 --- a/tests_mem/test_gui.py +++ b/tests_mem/test_gui.py @@ -59,7 +59,6 @@ def test_release_canvas_context(n): yield { "expected_counts_after_create": { - "CanvasContext": (n, 0), }, "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}" ) From 371fbf819e3adde56e279ffb69f954e102951fe6 Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Mon, 10 Nov 2025 13:05:20 +0100 Subject: [PATCH 21/24] ruff --- tests_mem/test_gui.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests_mem/test_gui.py b/tests_mem/test_gui.py index 1b4968d8..b96e6265 100644 --- a/tests_mem/test_gui.py +++ b/tests_mem/test_gui.py @@ -58,8 +58,7 @@ def test_release_canvas_context(n): from rendercanvas.offscreen import RenderCanvas yield { - "expected_counts_after_create": { - }, + "expected_counts_after_create": {}, "ignore": {"CommandBuffer", "Buffer"}, } From d91263bd385104a5756ddfeef253d77dde26c7ab Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Mon, 10 Nov 2025 13:08:01 +0100 Subject: [PATCH 22/24] update examples --- examples/extras_debug.py | 2 +- examples/extras_dxc.py | 2 +- examples/gui_auto.py | 2 +- examples/gui_qt_embed.py | 2 +- examples/offscreen_hdr.py | 2 +- examples/wgpu-examples.ipynb | 4 ++-- 6 files changed, 7 insertions(+), 7 deletions(-) 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_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/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/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" From f81ccb589dfb8901b793ed5e993ea5a695f68d55 Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Mon, 10 Nov 2025 13:37:21 +0100 Subject: [PATCH 23/24] make a prop --- wgpu/_classes.py | 1 + 1 file changed, 1 insertion(+) diff --git a/wgpu/_classes.py b/wgpu/_classes.py index 31f939f5..e1b41259 100644 --- a/wgpu/_classes.py +++ b/wgpu/_classes.py @@ -285,6 +285,7 @@ def set_physical_size(self, width: int, height: int) -> None: 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 From 0f38f9a5015e81d5fd077d2ade7fe9dd383af3e2 Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Mon, 10 Nov 2025 14:01:30 +0100 Subject: [PATCH 24/24] codgen --- wgpu/resources/codegen_report.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wgpu/resources/codegen_report.md b/wgpu/resources/codegen_report.md index 84889e4f..f87fb411 100644 --- a/wgpu/resources/codegen_report.md +++ b/wgpu/resources/codegen_report.md @@ -19,7 +19,7 @@ * 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, 115 methods, 0 properties ## Validating backends/wgpu_native/_api.py