Skip to content

Commit 148eebb

Browse files
Korijnalmarklein
andauthored
Canvas/context inversion of control (#764)
* invert context wrapper control * go one step further * clean up a bit * attempt to clear out get_physical_size and vsync, remove dummy canvas * cleanup * Work with new rendercanvas, and clean up some of the bitmap presenting * Provide public method to create CanvasContext * Remove references to CanvasInterface * Prefer canvas.get_wgpu_context() over canvas.get_context('wgpu') * clean/docs * codegen * fix * Adjust triangle example too * forgot to update triangle.glsl * pin rendercanvas to future release * add prop to get physical size * fix typos * Adjust docstring for pyodide * codegen * fix memtests * ruff * update examples * make a prop * codgen --------- Co-authored-by: Almar Klein <almar@almarklein.org>
1 parent 44a8943 commit 148eebb

27 files changed

+253
-455
lines changed

docs/guide.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ Next, we can setup the render context, which we will need later on.
2828

2929
.. code-block:: py
3030
31-
present_context = canvas.get_context("wgpu")
31+
present_context = canvas.get_wgpu_context()
3232
render_texture_format = present_context.get_preferred_format(device.adapter)
3333
present_context.configure(device=device, format=render_texture_format)
3434

docs/wgpu.rst

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -72,16 +72,14 @@ The async methods return a :class:`GPUPromise`, which resolves to the actual res
7272
* In sync code, you can use ``promise.sync_wait()``. This is similar to the ``_sync()`` flavour mentioned above (it makes your code less portable).
7373

7474

75-
Canvas API
76-
----------
77-
78-
In order for wgpu to render to a canvas (which can be on screen, inside a GUI, offscreen, etc.),
79-
a canvas object is needed. We recommend using the `rendercanvas <https://github.com/pygfx/rendercanvas>`_ library to get a wide variety of canvases.
80-
81-
That said, the canvas object can be any object, as long as it adheres to the
82-
``WgpuCanvasInterface``, see https://github.com/pygfx/wgpu-py/blob/main/wgpu/_canvas.py for details.
75+
Rendering to a canvas
76+
---------------------
8377

78+
In order for wgpu to render to a canvas (which can be on screen, inside a GUI,
79+
offscreen, etc.), we highly recommend using the `rendercanvas <https://github.com/pygfx/rendercanvas>`_ library.
80+
One can then use ``canvas.get_wgpu_context()`` to get a `WgpuContext <https://rendercanvas.readthedocs.io/stable/contexts.html#rendercanvas.contexts.WgpuContext>`_.
8481

82+
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.
8583

8684

8785
Overview

examples/cube.py

Lines changed: 20 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
because it adds buffers and textures.
66
77
This example is set up so it can be run with any canvas. Running this file
8-
as a script will use the auto-backend.
8+
as a script will use rendercanvas with the auto-backend.
99
"""
1010

1111
# test_example = true
@@ -24,11 +24,10 @@
2424

2525

2626
def setup_drawing_sync(
27-
canvas, power_preference="high-performance", limits=None
27+
context, power_preference="high-performance", limits=None
2828
) -> Callable:
29-
"""Setup to draw a rotating cube on the given canvas.
29+
"""Setup to draw a rotating cube on the given context.
3030
31-
The given canvas must implement WgpuCanvasInterface, but nothing more.
3231
Returns the draw function.
3332
"""
3433

@@ -39,19 +38,18 @@ def setup_drawing_sync(
3938
)
4039

4140
pipeline_layout, uniform_buffer, bind_group = create_pipeline_layout(device)
42-
pipeline_kwargs = get_render_pipeline_kwargs(canvas, device, pipeline_layout)
41+
pipeline_kwargs = get_render_pipeline_kwargs(context, device, pipeline_layout)
4342

4443
render_pipeline = device.create_render_pipeline(**pipeline_kwargs)
4544

4645
return get_draw_function(
47-
canvas, device, render_pipeline, uniform_buffer, bind_group
46+
context, device, render_pipeline, uniform_buffer, bind_group
4847
)
4948

5049

51-
async def setup_drawing_async(canvas, limits=None):
52-
"""Setup to async-draw a rotating cube on the given canvas.
50+
async def setup_drawing_async(context, limits=None):
51+
"""Setup to async-draw a rotating cube on the given context.
5352
54-
The given canvas must implement WgpuCanvasInterface, but nothing more.
5553
Returns the draw function.
5654
"""
5755
adapter = await wgpu.gpu.request_adapter_async(power_preference="high-performance")
@@ -61,34 +59,33 @@ async def setup_drawing_async(canvas, limits=None):
6159
)
6260

6361
pipeline_layout, uniform_buffer, bind_group = create_pipeline_layout(device)
64-
pipeline_kwargs = get_render_pipeline_kwargs(canvas, device, pipeline_layout)
62+
pipeline_kwargs = get_render_pipeline_kwargs(context, device, pipeline_layout)
6563

6664
render_pipeline = await device.create_render_pipeline_async(**pipeline_kwargs)
6765

6866
return get_draw_function(
69-
canvas, device, render_pipeline, uniform_buffer, bind_group
67+
context, device, render_pipeline, uniform_buffer, bind_group
7068
)
7169

7270

73-
def get_drawing_func(canvas, device):
71+
def get_drawing_func(context, device):
7472
pipeline_layout, uniform_buffer, bind_group = create_pipeline_layout(device)
75-
pipeline_kwargs = get_render_pipeline_kwargs(canvas, device, pipeline_layout)
73+
pipeline_kwargs = get_render_pipeline_kwargs(context, device, pipeline_layout)
7674

7775
render_pipeline = device.create_render_pipeline(**pipeline_kwargs)
7876
# render_pipeline = device.create_render_pipeline(**pipeline_kwargs)
7977

8078
return get_draw_function(
81-
canvas, device, render_pipeline, uniform_buffer, bind_group
79+
context, device, render_pipeline, uniform_buffer, bind_group
8280
)
8381

8482

8583
# %% Functions to create wgpu objects
8684

8785

8886
def get_render_pipeline_kwargs(
89-
canvas, device: wgpu.GPUDevice, pipeline_layout: wgpu.GPUPipelineLayout
87+
context, device: wgpu.GPUDevice, pipeline_layout: wgpu.GPUPipelineLayout
9088
) -> wgpu.RenderPipelineDescriptor:
91-
context = canvas.get_context("wgpu")
9289
render_texture_format = context.get_preferred_format(device.adapter)
9390
context.configure(device=device, format=render_texture_format)
9491

@@ -250,7 +247,7 @@ def create_pipeline_layout(device: wgpu.GPUDevice):
250247

251248

252249
def get_draw_function(
253-
canvas,
250+
context,
254251
device: wgpu.GPUDevice,
255252
render_pipeline: wgpu.GPURenderPipeline,
256253
uniform_buffer: wgpu.GPUBuffer,
@@ -304,9 +301,9 @@ def upload_uniform_buffer():
304301

305302
def draw_frame():
306303
current_texture_view: wgpu.GPUTextureView = (
307-
canvas.get_context("wgpu")
308-
.get_current_texture()
309-
.create_view(label="Cube Example current surface texture view")
304+
context.get_current_texture().create_view(
305+
label="Cube Example current surface texture view"
306+
)
310307
)
311308
command_encoder = device.create_command_encoder(
312309
label="Cube Example render pass command encoder"
@@ -481,18 +478,19 @@ def draw_func():
481478
max_fps=60,
482479
vsync=True,
483480
)
481+
context = canvas.get_wgpu_context()
484482

485483
# Pick one
486484

487485
if True:
488486
# Async
489487
@loop.add_task
490488
async def init():
491-
draw_frame = await setup_drawing_async(canvas)
489+
draw_frame = await setup_drawing_async(context)
492490
canvas.request_draw(draw_frame)
493491
else:
494492
# Sync
495-
draw_frame = setup_drawing_sync(canvas)
493+
draw_frame = setup_drawing_sync(context)
496494
canvas.request_draw(draw_frame)
497495

498496
# loop.add_task(poller)

examples/extras_debug.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ def setup_demo():
113113
from cube import setup_drawing_sync
114114

115115
canvas = RenderCanvas(title="Cube example with debug symbols")
116-
draw_frame = setup_drawing_sync(canvas)
116+
draw_frame = setup_drawing_sync(canvas.get_wgpu_context())
117117

118118
# we set the auto capture on frame 50, so after 100 frames gui should exit
119119
# this will lead to RenderDoc automatically opening the capture!

examples/extras_dxc.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232

3333

3434
canvas = RenderCanvas(title="Cube example on DX12 using Dxc")
35-
draw_frame = setup_drawing_sync(canvas)
35+
draw_frame = setup_drawing_sync(canvas.get_wgpu_context())
3636

3737

3838
@canvas.request_draw

examples/gui_auto.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from triangle import setup_drawing_sync
2323

2424
canvas = RenderCanvas(title="Cube example on $backend")
25-
draw_frame = setup_drawing_sync(canvas)
25+
draw_frame = setup_drawing_sync(canvas.get_wgpu_context())
2626

2727

2828
@canvas.request_draw

examples/gui_direct.py

Lines changed: 34 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import atexit
1414

1515
import glfw
16-
from wgpu.backends.wgpu_native import GPUCanvasContext
16+
import wgpu
1717

1818
# from triangle import setup_drawing_sync
1919
from cube import setup_drawing_sync
@@ -26,37 +26,33 @@
2626
api_is_wayland = True
2727

2828

29-
def get_glfw_present_methods(window):
29+
def get_glfw_present_info(window):
3030
if sys.platform.startswith("win"):
3131
return {
32-
"screen": {
33-
"platform": "windows",
34-
"window": int(glfw.get_win32_window(window)),
35-
}
32+
"platform": "windows",
33+
"window": int(glfw.get_win32_window(window)),
34+
"vsync": True,
3635
}
3736
elif sys.platform.startswith("darwin"):
3837
return {
39-
"screen": {
40-
"platform": "cocoa",
41-
"window": int(glfw.get_cocoa_window(window)),
42-
}
38+
"platform": "cocoa",
39+
"window": int(glfw.get_cocoa_window(window)),
40+
"vsync": True,
4341
}
4442
elif sys.platform.startswith("linux"):
4543
if api_is_wayland:
4644
return {
47-
"screen": {
48-
"platform": "wayland",
49-
"window": int(glfw.get_wayland_window(window)),
50-
"display": int(glfw.get_wayland_display()),
51-
}
45+
"platform": "wayland",
46+
"window": int(glfw.get_wayland_window(window)),
47+
"display": int(glfw.get_wayland_display()),
48+
"vsync": True,
5249
}
5350
else:
5451
return {
55-
"screen": {
56-
"platform": "x11",
57-
"window": int(glfw.get_x11_window(window)),
58-
"display": int(glfw.get_x11_display()),
59-
}
52+
"platform": "x11",
53+
"window": int(glfw.get_x11_window(window)),
54+
"display": int(glfw.get_x11_display()),
55+
"vsync": True,
6056
}
6157
else:
6258
raise RuntimeError(f"Cannot get GLFW surface info on {sys.platform}.")
@@ -66,43 +62,39 @@ def get_glfw_present_methods(window):
6662
glfw.init()
6763
atexit.register(glfw.terminate)
6864

65+
# disable automatic API selection, we are not using opengl
66+
glfw.window_hint(glfw.CLIENT_API, glfw.NO_API)
67+
glfw.window_hint(glfw.RESIZABLE, True)
6968

70-
class MinimalGlfwCanvas: # implements WgpuCanvasInterface
71-
"""Minimal canvas interface required by wgpu."""
72-
73-
def __init__(self, title):
74-
# disable automatic API selection, we are not using opengl
75-
glfw.window_hint(glfw.CLIENT_API, glfw.NO_API)
76-
glfw.window_hint(glfw.RESIZABLE, True)
7769

78-
self.window = glfw.create_window(640, 480, title, None, None)
79-
self.context = GPUCanvasContext(self, get_glfw_present_methods(self.window))
70+
title = "wgpu glfw direct"
71+
window = glfw.create_window(640, 480, title, None, None)
72+
present_info = get_glfw_present_info(window)
8073

81-
def get_physical_size(self):
82-
"""get framebuffer size in integer pixels"""
83-
psize = glfw.get_framebuffer_size(self.window)
84-
return int(psize[0]), int(psize[1])
74+
context = wgpu.gpu.get_canvas_context(present_info)
8575

86-
def get_context(self, kind="wgpu"):
87-
return self.context
76+
# Initialize physical size once. For robust apps update this on resize events.
77+
context.set_physical_size(*glfw.get_framebuffer_size(window))
8878

8979

9080
def main():
91-
# create canvas
92-
canvas = MinimalGlfwCanvas("wgpu gui direct")
93-
draw_frame = setup_drawing_sync(canvas)
81+
draw_frame = setup_drawing_sync(context)
9482

9583
last_frame_time = time.perf_counter()
9684
frame_count = 0
9785

9886
# render loop
99-
while not glfw.window_should_close(canvas.window):
87+
while not glfw.window_should_close(window):
10088
# process inputs
10189
glfw.poll_events()
90+
91+
# resize handling
92+
context.set_physical_size(*glfw.get_framebuffer_size(window))
93+
10294
# draw a frame
10395
draw_frame()
10496
# present the frame to the screen
105-
canvas.context.present()
97+
context.present()
10698
# stats
10799
frame_count += 1
108100
etime = time.perf_counter() - last_frame_time
@@ -111,7 +103,8 @@ def main():
111103
last_frame_time, frame_count = time.perf_counter(), 0
112104

113105
# dispose resources
114-
glfw.destroy_window(canvas.window)
106+
context.unconfigure()
107+
glfw.destroy_window(window)
115108

116109
# allow proper cleanup (workaround for glfw bug)
117110
end_time = time.perf_counter() + 0.1

examples/gui_qt_embed.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ def whenButton2Clicked(self):
8686
app = QtWidgets.QApplication([])
8787
example = ExampleWidget()
8888

89-
draw_frame = setup_drawing_sync(example.canvas)
89+
draw_frame = setup_drawing_sync(example.canvas.get_wgpu_context())
9090
example.canvas.request_draw(draw_frame)
9191

9292
# Enter Qt event loop (compatible with qt5/qt6)

examples/imgui_backend_sea.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
device = adapter.request_device_sync()
2323

2424
# Prepare present context
25-
present_context = canvas.get_context("wgpu")
25+
present_context = canvas.get_wgpu_context()
2626
render_texture_format = wgpu.TextureFormat.bgra8unorm
2727
present_context.configure(device=device, format=render_texture_format)
2828

examples/imgui_renderer_sea.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
device = adapter.request_device_sync()
2323

2424
# Prepare present context
25-
present_context = canvas.get_context("wgpu")
25+
present_context = canvas.get_wgpu_context()
2626
render_texture_format = wgpu.TextureFormat.bgra8unorm
2727
present_context.configure(device=device, format=render_texture_format)
2828

0 commit comments

Comments
 (0)