Skip to content
Open
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
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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I disabled this and removed the screenshot. Since we get one for cube and one for triangle anyway. Perhaps not the best idea as we could miss problems


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
4 changes: 2 additions & 2 deletions examples/offscreen_hdr.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@
from rendercanvas.offscreen import RenderCanvas

try:
from .triangle import setup_drawing_sync

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... and problems we missed. I ran this locally and got a messed up image. As I was on main for rendercanvas I had to go back to v2.5.0 to get it to work again. Didn't bisect it fully but between v2.5.0 and v2.6.0 is what I could find on the quick.

I suspect something about the bitorder might be wrong:

Image

I will check the rendercanvas code when I find time. Maybe a numpy kwargs is all we need

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe width/height reversed?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wasn't it. I tried around with different shapes and all. But the individual pixel values don't make sense. I think it's somewhere in the new bitmapcontext in rendercanvas. I had a quick glance and it looked like it always writes bytes as uint8... so it could also be a platform specific problem with order across bytes.

I will try some variations and file this for rendercanvas hopefully in a few days.

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)
Expand Down
Binary file removed examples/screenshots/gui_auto.png
Binary file not shown.
220 changes: 76 additions & 144 deletions examples/triangle.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,176 +5,108 @@
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

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)

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


# %% Functions to create wgpu objects


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)

shader = device.create_shader_module(code=shader_source)
pipeline_layout = device.create_pipeline_layout(bind_group_layouts=[])

return wgpu.RenderPipelineDescriptor(
layout=pipeline_layout,
vertex=wgpu.VertexState(
module=shader,
entry_point="vs_main",
),
depth_stencil=None,
multisample=None,
fragment=wgpu.FragmentState(
module=shader,
entry_point="fs_main",
targets=[
wgpu.ColorTargetState(
format=render_texture_format,
blend={"color": {}, "alpha": {}},
)
],
),
)


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,
};
# the shader code is provided as a string literal for protability
wgsl_shader_source = """
struct VertexOutput {
@location(0) color : vec4<f32>,
@builtin(position) pos: vec4<f32>,
@location(0) color : vec4f,
@builtin(position) pos: vec4f,
};

@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),
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),
);
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),
// 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),
);
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);
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) vec4<f32> {
let physical_color = pow(in.color.rgb, vec3<f32>(2.2)); // gamma correct
return vec4<f32>(physical_color, in.color.a);
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);
}
"""


if __name__ == "__main__":
from rendercanvas.auto import RenderCanvas, loop
# adapter provides allows us to create a single device, which is the general entrypoint 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()

# 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)

canvas = RenderCanvas(size=(640, 480), title="wgpu triangle example")
context = canvas.get_wgpu_context()
# creating the shader module compiles the shader code for your GPU.
shader = device.create_shader_module(code=wgsl_shader_source)

draw_frame = setup_drawing_sync(context)
canvas.request_draw(draw_frame)
# 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,
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, 1, 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()])


if __name__ == "__main__":
canvas.request_draw(drawing_function)
loop.run()
Loading