Skip to content
Draft
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
3 changes: 2 additions & 1 deletion codegen/idlparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
identify and remove code paths that are no longer used.
"""

from typing import Dict
from codegen.utils import print
from codegen.files import read_file

Expand Down Expand Up @@ -128,7 +129,7 @@ def peek_line(self):

def parse(self, verbose=True):
self._interfaces = {}
self.classes = {}
self.classes:Dict[str, Interface] = {}
self.structs = {}
self.flags = {}
self.enums = {}
Expand Down
122 changes: 122 additions & 0 deletions codegen/jswriter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
"""
Codegen the JS webgpu backend, based on the parsed idl.

write to the backends/js_webgpu/_api.py file.
"""

from codegen.idlparser import Attribute, get_idl_parser
from codegen.apipatcher import IdlPatcherMixin, BaseApiPatcher

Check failure on line 8 in codegen/jswriter.py

View workflow job for this annotation

GitHub Actions / Test Linting

Ruff (F401)

codegen/jswriter.py:8:32: F401 `codegen.apipatcher.IdlPatcherMixin` imported but unused
from textwrap import indent

Check failure on line 9 in codegen/jswriter.py

View workflow job for this annotation

GitHub Actions / Test Linting

Ruff (F401)

codegen/jswriter.py:9:22: F401 `textwrap.indent` imported but unused

create_template = """
def {py_method_name}(self, **kwargs):
descriptor = struct.{py_descriptor_name}(**kwargs)
js_descriptor = to_js(descriptor, eager_converter=simple_js_accessor)
js_obj = self._internal.{js_method_name}(js_descriptor)

label = kwargs.pop("label", "")
return {return_type}(js_obj, label=label)
"""

unary_template = """
def {py_method_name}(self) -> None:
js_obj = self._internal.{js_method_name}()
"""

# TODO: this is a bit more complex but doable.
positional_args_template = """
def {py_method_name}(self, {args}):
{body}
js_obj = self._internal.{js_method_name}({js_args})
return {return_type}
"""



# most likely copy and modify the code in apipatcher.py... because we hopefully need code that looks really similar to _classes.py
idl = get_idl_parser()
helper_patcher = BaseApiPatcher() # to get access to name2py_names function

# todo import our to_js converter functions from elsewhere?
for class_name, interface in idl.classes.items():
# write idl line, header
# write the to_js block
# get label (where needed?)
# return the constructor call to the base class maybe?
mixins = [c for c in interface.bases if c.endswith("Mixin")]
print(f"class {class_name}(classes.{class_name}{', '.join(mixins)}):")

# TODO: can we property some of the webgpu attributes to replace the existing private mappings

for function_name, idl_line in interface.functions.items():
return_type = idl_line.split(" ")[0] # on some parts this doesn't exist
if "constructor" in idl_line:
return_type = None
continue # skip constructors?

# based on apipatcher.IDlCommentINjector.get_method_comment
args = idl_line.split("(")[1].rsplit(")")[0].split(", ")
args = [Attribute(arg) for arg in args if arg.strip()]
py_method_name = helper_patcher.name2py_names(class_name, function_name)
# case 1: single argument as a descriptor (TODO: could be optional - but that should just work)
if len(args) == 1 and args[0].typename.endswith(
("Option", "Descriptor", "Configuration")
):
method_string = create_template.format(
py_method_name=py_method_name[0], # TODO: async workaround?
py_descriptor_name=args[0].typename.removeprefix("GPU"),
js_method_name=function_name,
return_type=return_type if return_type else "None",
)
print(method_string)

# case 2: no arguments (and nothing to return?)
elif (len(args) == 0 and return_type == "undefined"):
method_string = unary_template.format(
py_method_name=py_method_name[0], # TODO: async workaround?
js_method_name=function_name,
)
print(method_string)
# TODO: return values, could be simple or complex... so might need a constructor or not at all?

# case 3: positional arguments, some of which might need ._internal lookup or struct->to_js conversion... but not all.
elif (len(args) > 0):
param_list = []
conversion_lines = []
js_arg_list = []
for idx, arg in enumerate(args):
py_name = helper_patcher.name2py_names(class_name, arg.name)[0]
param_list.append(py_name)
# if it's a GPUObject kinda thing we most likely need to call ._internal to get the correct js object
if arg.typename in idl.classes:
conversion_lines.append(f"js_{arg.name} = {py_name}._internal")
js_arg_list.append(f"js_{arg.name}")
# TODO: sequence of complex type?

# here we go via the struct (in idl.structs?)
elif arg.typename.endswith(("Info", "Descriptor")):
conversion_lines.append(f"{py_name}_desc = structs.{arg.typename.removeprefix('GPU')}(**{py_name})")
conversion_lines.append(f"js_{arg.name} = to_js({py_name}_desc, eager_converter=simple_js_accessor)")
js_arg_list.append(f"js_{arg.name}")
else:
# TODO: try idl.resolve_type(arg.typename) or in idl.enums?
conversion_lines.append(f"# TODO: argument {py_name} of type {arg.typename} might need conversion")
js_arg_list.append(py_name)

method_string = positional_args_template.format(
py_method_name=py_method_name[0], # TODO: async workaround?
args=", ".join(param_list), # TODO: default/optional args
body=("\n ".join(conversion_lines)),
js_method_name=function_name,
js_args=", ".join(js_arg_list),
return_type=return_type if return_type != "undefined" else "None",
)
print(method_string)

else:
print(f" # TODO: implement codegen for {function_name} with args {args} or return type {return_type}")

# else:
# print(" pass # TODO: maybe needs a constructor or properties?")

print()
99 changes: 99 additions & 0 deletions examples/browser.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<!-- adapted from: https://traineq.org/imgui_bundle_online/projects/min_bundle_pyodide_app/demo_heart.source.txt -->
<!doctype html>
<html>
<head>
wgpu-py on the HTML RenderCanvas canvas with Pyodide:<br>
<script src="https://cdn.jsdelivr.net/pyodide/v0.29.0/full/pyodide.js"></script>
<!-- <script src="https://cdn.jsdelivr.net/pyodide/dev/debug/pyodide.js"></script> -->
<style>
/* so the demo page can have interactive resizing */
#canvas {
position: absolute;
width: 90%;
height: 90%;
}
</style>
</head>
<body>
<!-- WIP: make it more of a "Playground"? -->
<select id="example-select">
<option value="cube.py">cube.py</option>
<option value="compute_noop.py">compute_noop.py</option>
<option value="compute_matmul.py">compute_matmul.py</option>
<option value="triangle.py">triangle.py</option>
<option value="imgui_backend_sea.py">imgui_backend_sea.py</option>
</select>
<!-- maybe something like add example (to test spawning more canvases and also closing them?) -->
<button id="reload-example">load example (NOT IMPLEMENTED)</button>
<button id="reload-page" style="background-color: orange;"onclick="location.reload();">Reload page</button>
<button id="reload-pyodide" style="background-color: yellow;">Reload pyodide (TODO)</button>
<br>
<!-- TODO: select the dependencies (pyodide doesn't find them automatically) -->
<label for="install-wgpu">wgpu:</label>
<input type="text" id="install-wgpu" name="wgpu" value="../dist/wgpu-0.26.0-py3-none-any.whl" size="30">
<label for="install-rendercanvas">rendercanvas:</label>
<input type="text" id="install-rendercanvas" name="rendercanvas" value="../../../../../../../projects/pygfx-repos/rendercanvas/dist/rendercanvas-2.2.1-py3-none-any.whl" size="50">
<!-- maybe <detaillist> for these two? -->
<label for="install-numpy">numpy:</label>
<input type="checkbox" id="install-numpy" name="numpy" checked>
<label for="install-imgui">imgui-bundle:</label>
<input type="checkbox" id="install-imgui" name="imgui-bundle">
<br>
<canvas id="canvas" width="640" height="480" style="background-color: lightgrey;"></canvas><br>
pixels got drawn!
<script type="text/javascript">
async function main(){

// fetch the file locally for easier scripting
// --allow-file-access-from-files or local webserver
// TODO: replace the actual code here (unless you have the module)
let example_select = document.getElementById("example-select");
let reload_example_button = document.getElementById("reload-example");

// Load Pyodide
console.log("Loading pyodide...");
let pyodide = await loadPyodide();
console.log("Pyodide awaiting");
pyodide.setDebug(true);
await pyodide.loadPackage("micropip");
const micropip = pyodide.pyimport("micropip");


// install wgpu and rendercanvas from local wheels
// set WGPU_PY_BUILLD_NOARCH=1
// python -m build -nw
let wgpu_location = document.getElementById("install-wgpu").value;
await micropip.install(wgpu_location);

// build locally from the PR branch: https://github.com/pygfx/rendercanvas/pull/115
let rendercanvas_location = document.getElementById("install-rendercanvas").value;
// await micropip.install('../../rendercanvas/dist/rendercanvas-2.2.1-py3-none-any.whl'); // local wheel for auto testing
await micropip.install(rendercanvas_location);

// additional dependencies, alreay existing for pyodide - so we can get them from micropip
if (document.getElementById("install-numpy").checked) {
// not needed by the triangle.py example but everything else I think
await micropip.install('numpy');
}
if (document.getElementById("install-imgui").checked) {
// works with the imgui examples
await micropip.install('imgui-bundle');
}



console.log("Pyodide ready");
// load this one late so you can change before pyodide is ready!
pythonCode = await (await fetch(example_select.value)).text();
pyodide.runPythonAsync(pythonCode);

reload_example_button.onclick = async function() {
console.log("we will need to implement pyodide as a webworker to interrupt running loops");
};

// TODO: automatically check imgui when an imgui item is selected?
}
main();
</script>
</body>
</html>
1 change: 0 additions & 1 deletion examples/cube.py
Original file line number Diff line number Diff line change
Expand Up @@ -472,7 +472,6 @@ def draw_func():
for a in wgpu.gpu.enumerate_adapters_sync():
print(a.summary)


if __name__ == "__main__":
canvas = RenderCanvas(
size=(640, 480),
Expand Down
9 changes: 5 additions & 4 deletions examples/triangle.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,19 +71,20 @@ def get_render_pipeline_kwargs(
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)
vert_shader = device.create_shader_module(code=shader_source)
frag_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,
module=vert_shader,
entry_point="vs_main",
),
depth_stencil=None,
multisample=None,
fragment=wgpu.FragmentState(
module=shader,
module=frag_shader,
entry_point="fs_main",
targets=[
wgpu.ColorTargetState(
Expand Down Expand Up @@ -175,7 +176,7 @@ async def draw_frame_async():
if __name__ == "__main__":
from rendercanvas.auto import RenderCanvas, loop

canvas = RenderCanvas(size=(640, 480), title="wgpu triangle example")
canvas = RenderCanvas(size=(640, 480), title="wgpu triangle example", update_mode="continuous")
draw_frame = setup_drawing_sync(canvas)
canvas.request_draw(draw_frame)
loop.run()
4 changes: 2 additions & 2 deletions wgpu/_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,7 @@ def configure(
usage = str_flag_to_int(flags.TextureUsage, usage)

color_space # noqa - not really supported, just assume srgb for now
tone_mapping # noqa - not supported yet
tone_mapping = {} if tone_mapping is None else tone_mapping

# Allow more than the IDL modes, see https://github.com/pygfx/wgpu-py/pull/719
extra_alpha_modes = ["auto", "unpremultiplied", "inherit"] # from webgpu.h
Expand Down Expand Up @@ -1899,7 +1899,7 @@ def set_index_buffer(
call to `GPUDevice.create_render_pipeline()`, it must match.
offset (int): The byte offset in the buffer. Default 0.
size (int): The number of bytes to use. If zero, the remaining size
(after offset) of the buffer is used. Default 0.
(after offset) of the buffer is used.
"""
raise NotImplementedError()

Expand Down
Loading
Loading