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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
A Python implementation of WebGPU - the next generation GPU API. 🚀

<div>
<img width=320 src='https://raw.githubusercontent.com/pygfx/wgpu-py/main/examples/screenshots/triangle_auto.png' />
<img width=320 src='https://raw.githubusercontent.com/pygfx/wgpu-py/main/examples/screenshots/triangle.png' />
<img width=320 src='https://raw.githubusercontent.com/pygfx/wgpu-py/main/examples/screenshots/cube.png' />
</div>

Expand Down
14 changes: 10 additions & 4 deletions examples/cube.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@


def setup_drawing_sync(
context, power_preference="high-performance", limits=None
context, power_preference="high-performance", limits=None, format=None
) -> Callable:
"""Setup to draw a rotating cube on the given context.

Expand All @@ -38,7 +38,9 @@ def setup_drawing_sync(
)

pipeline_layout, uniform_buffer, bind_group = create_pipeline_layout(device)
pipeline_kwargs = get_render_pipeline_kwargs(context, device, pipeline_layout)
pipeline_kwargs = get_render_pipeline_kwargs(
context, device, pipeline_layout, render_texture_format=format
)

render_pipeline = device.create_render_pipeline(**pipeline_kwargs)

Expand Down Expand Up @@ -84,9 +86,13 @@ def get_drawing_func(context, device):


def get_render_pipeline_kwargs(
context, device: wgpu.GPUDevice, pipeline_layout: wgpu.GPUPipelineLayout
context,
device: wgpu.GPUDevice,
pipeline_layout: wgpu.GPUPipelineLayout,
render_texture_format: wgpu.TextureFormat | None = None,
) -> wgpu.RenderPipelineDescriptor:
render_texture_format = context.get_preferred_format(device.adapter)
if render_texture_format is None:
render_texture_format = context.get_preferred_format(device.adapter)
context.configure(device=device, format=render_texture_format)

shader = device.create_shader_module(
Expand Down
8 changes: 3 additions & 5 deletions examples/gui_auto.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""
Run the triangle example in an automatically selected GUI backend.
Run the cube example in an automatically selected GUI backend.

The rendercanvas automatically selects one of its available
GUI backends. E.g. running this in a notebook will use the
Expand All @@ -12,14 +12,12 @@

"""

# test_example = true
Comment thread
almarklein marked this conversation as resolved.

from rendercanvas.auto import RenderCanvas, loop

try:
from .triangle import setup_drawing_sync
from .cube import setup_drawing_sync
except ImportError:
from triangle import setup_drawing_sync
from cube import setup_drawing_sync

canvas = RenderCanvas(title="Cube example on $backend")
draw_frame = setup_drawing_sync(canvas.get_wgpu_context())
Expand Down
6 changes: 3 additions & 3 deletions examples/offscreen_hdr.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,17 @@
from rendercanvas.offscreen import RenderCanvas

try:
from .triangle import setup_drawing_sync
Comment thread
almarklein marked this conversation as resolved.
from .cube import setup_drawing_sync
except ImportError:
from triangle import setup_drawing_sync
from cube import setup_drawing_sync


canvas = RenderCanvas(size=(640, 480), pixel_ratio=2)
draw_frame = setup_drawing_sync(canvas.get_wgpu_context(), format="rgba16float")
canvas.request_draw(draw_frame)

# Draw, and download image
image = canvas.draw()
image = np.asarray(image)

# Convert to RGB
image = image[:, :, :3]
Expand Down
Binary file removed examples/screenshots/gui_auto.png
Binary file not shown.
Binary file added examples/screenshots/triangle.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed examples/screenshots/triangle_auto.png
Binary file not shown.
221 changes: 78 additions & 143 deletions examples/triangle.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,176 +5,111 @@
visualisation because it does not need buffers or textures. The same
example in other languages / API's:

* JavaScript WebGPU:
https://webgpu.github.io/webgpu-samples/?sample=helloTriangle
* Rust wgpu:
https://github.com/gfx-rs/wgpu-rs/blob/master/examples/hello-triangle/main.rs
https://github.com/gfx-rs/wgpu/blob/trunk/examples/features/src/hello_triangle/mod.rs
* C wgpu:
https://github.com/gfx-rs/wgpu/blob/master/examples/triangle/main.c
https://github.com/gfx-rs/wgpu-native/blob/trunk/examples/triangle/main.c
* Python Vulkan:
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 rendercanvas with the auto-backend.

This example is meant as a standalone starting point. And is therefore as minimal as possible.
"""

from typing import Callable
# test_example = true

import wgpu

# %% Entrypoints (sync and async)


def setup_drawing_sync(
context, power_preference="high-performance", limits=None, format=None
) -> Callable:
"""Setup to draw a triangle on the given context.

Returns the draw function.
"""

adapter = wgpu.gpu.request_adapter_sync(power_preference=power_preference)
device = adapter.request_device_sync(required_limits=limits)

pipeline_kwargs = get_render_pipeline_kwargs(context, device, format)

render_pipeline = device.create_render_pipeline(**pipeline_kwargs)

return get_draw_function(context, device, render_pipeline, asynchronous=False)


async def setup_drawing_async(context, limits=None, format=None) -> Callable:
"""Setup to async-draw a triangle on the given context.
from rendercanvas.auto import RenderCanvas, loop

Returns the draw function.
"""

adapter = await wgpu.gpu.request_adapter_async(power_preference="high-performance")
device = await adapter.request_device_async(required_limits=limits)

pipeline_kwargs = get_render_pipeline_kwargs(context, device, format)

render_pipeline = await device.create_render_pipeline_async(**pipeline_kwargs)
# the shader code is provided as a string literal for portability
wgsl_shader_source = """
struct VertexOutput {
@location(0) color : vec4f,
@builtin(position) pos: vec4f,
};

return get_draw_function(context, device, render_pipeline, asynchronous=True)
@vertex
fn vs_main(@builtin(vertex_index) index: u32) -> VertexOutput {
var positions = array<vec2f, 3>(
vec2(0.0, -0.5),
vec2(0.5, 0.5),
vec2(-0.5, 0.75),
);
// vertex attributes are interpolated in the fragment shader.
var colors = array<vec3f, 3>( // srgb colors
vec3(1.0, 1.0, 0.0),
vec3(1.0, 0.0, 1.0),
vec3(0.0, 1.0, 1.0),
);
var out: VertexOutput;
out.pos = vec4(positions[index], 0.0, 1.0);
out.color = vec4(colors[index], 1.0);
return out;
}

@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4f {
let physical_color = pow(in.color.rgb, vec3(2.2)); // gamma correct
return vec4(physical_color, in.color.a);
}
"""

# %% Functions to create wgpu objects

# adapter is required to create the device object, which is the general entry point to create most wgpu objects.
# for convenience and interoperability `wgpu.utils.get_default_device()` and associated configuration are provided.
adapter = wgpu.gpu.request_adapter_sync()
device = adapter.request_device_sync()

def get_render_pipeline_kwargs(
context, device, render_texture_format
) -> wgpu.RenderPipelineDescriptor:
if render_texture_format is None:
render_texture_format = context.get_preferred_format(device.adapter)
context.configure(device=device, format=render_texture_format)
# setting up a canvas, so we can see what we draw
canvas = RenderCanvas(size=(640, 480), title="wgpu triangle example")
context = canvas.get_wgpu_context()
render_texture_format = context.get_preferred_format(device.adapter)
context.configure(device=device, format=render_texture_format)

shader = device.create_shader_module(code=shader_source)
pipeline_layout = device.create_pipeline_layout(bind_group_layouts=[])
# creating the shader module compiles the shader code for your GPU.
shader = device.create_shader_module(code=wgsl_shader_source)

return wgpu.RenderPipelineDescriptor(
layout=pipeline_layout,
vertex=wgpu.VertexState(
module=shader,
entry_point="vs_main",
),
# in wgpu-py, methods that take descriptors will take the keyword arguments instead.
# descriptors and other structs can still be accessed via wgpu.structs or top level wgpu.
render_pipeline = device.create_render_pipeline(
**wgpu.RenderPipelineDescriptor(
layout=wgpu.AutoLayoutMode.auto,
vertex=wgpu.VertexState(module=shader),
depth_stencil=None,
multisample=None,
fragment=wgpu.FragmentState(
module=shader,
entry_point="fs_main",
targets=[
wgpu.ColorTargetState(
format=render_texture_format,
blend={"color": {}, "alpha": {}},
)
],
targets=[wgpu.ColorTargetState(format=render_texture_format)],
),
)
)


# this function gets called for every frame. It ends with submitting a buffer of work onto the GPU queue.
def drawing_function():
command_encoder = device.create_command_encoder()
current_texture_view = context.get_current_texture().create_view()

render_pass = command_encoder.begin_render_pass(
color_attachments=[
wgpu.RenderPassColorAttachment(
view=current_texture_view,
clear_value=(0, 0.2, 0, 1), # a green background
load_op=wgpu.LoadOp.clear,
store_op=wgpu.StoreOp.store,
)
],
)
render_pass.set_pipeline(render_pipeline)
render_pass.draw(3)
render_pass.end()
device.queue.submit([command_encoder.finish()])


def get_draw_function(
context,
device: wgpu.GPUDevice,
render_pipeline: wgpu.GPURenderPipeline,
*,
asynchronous: bool,
) -> Callable:
def draw_frame_sync():
current_texture = context.get_current_texture()
command_encoder = device.create_command_encoder()

render_pass = command_encoder.begin_render_pass(
color_attachments=[
wgpu.RenderPassColorAttachment(
view=current_texture.create_view(),
resolve_target=None,
clear_value=(0, 0, 0, 1),
load_op="clear",
store_op="store",
)
],
)

render_pass.set_pipeline(render_pipeline)
# render_pass.set_bind_group(0, no_bind_group)
render_pass.draw(3, 1, 0, 0)
render_pass.end()
device.queue.submit([command_encoder.finish()])

async def draw_frame_async():
draw_frame_sync() # nothing async here

if asynchronous:
return draw_frame_async
else:
return draw_frame_sync


# %% WGSL


shader_source = """
struct VertexInput {
@builtin(vertex_index) vertex_index : u32,
};
struct VertexOutput {
@location(0) color : vec4<f32>,
@builtin(position) pos: vec4<f32>,
};

@vertex
fn vs_main(in: VertexInput) -> VertexOutput {
var positions = array<vec2<f32>, 3>(
vec2<f32>(0.0, -0.5),
vec2<f32>(0.5, 0.5),
vec2<f32>(-0.5, 0.75),
);
var colors = array<vec3<f32>, 3>( // srgb colors
vec3<f32>(1.0, 1.0, 0.0),
vec3<f32>(1.0, 0.0, 1.0),
vec3<f32>(0.0, 1.0, 1.0),
);
let index = i32(in.vertex_index);
var out: VertexOutput;
out.pos = vec4<f32>(positions[index], 0.0, 1.0);
out.color = vec4<f32>(colors[index], 1.0);
return out;
}

@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
let physical_color = pow(in.color.rgb, vec3<f32>(2.2)); // gamma correct
return vec4<f32>(physical_color, in.color.a);
}
"""
canvas.request_draw(drawing_function)


if __name__ == "__main__":
from rendercanvas.auto import RenderCanvas, loop

canvas = RenderCanvas(size=(640, 480), title="wgpu triangle example")
context = canvas.get_wgpu_context()

draw_frame = setup_drawing_sync(context)
canvas.request_draw(draw_frame)
loop.run()
Loading
Loading