Skip to content
Draft
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
8abb9aa
sketch idea
Vipitis Sep 26, 2025
0d22166
some hangup with buffer alignment -.-
Vipitis Sep 26, 2025
ac858b8
new errors :!
Vipitis Sep 26, 2025
b94ab8a
context in progress
Vipitis Sep 27, 2025
a42e6f2
more headache and shortcuts
Vipitis Sep 27, 2025
0618db6
first success!
Vipitis Sep 27, 2025
9e16b0c
use _internal
Vipitis Sep 28, 2025
237e269
ensure the call the base init
Vipitis Sep 28, 2025
3b5723f
dicts with key "type" ruin everything !!
Vipitis Sep 28, 2025
431b701
move data with .assign
Vipitis Sep 29, 2025
0cc6381
enable imgui demos
Vipitis Sep 29, 2025
d2cd796
BigInt in not reuqired
Vipitis Oct 8, 2025
158237f
fromEntries doesn't solve it just yet
Vipitis Oct 10, 2025
84fa6ac
revert cube changes
Vipitis Oct 16, 2025
07465da
sorta repair again
Vipitis Oct 16, 2025
75e29cc
use descriptor structs!
Vipitis Oct 16, 2025
87a3049
maybe fix recursion caching issue?
Vipitis Oct 16, 2025
b51cce2
compute examples work!
Vipitis Oct 16, 2025
4b7256e
broken again :/
Vipitis Oct 17, 2025
a87b6fa
use pyodide 0.29 dev branch
Vipitis Oct 19, 2025
18cce35
more descriuptors - seems to work
Vipitis Oct 19, 2025
c6bc69f
start codegen for js api
Vipitis Oct 19, 2025
e571682
codegen more methods
Vipitis Oct 20, 2025
3878f98
merge upstream main into browser
Vipitis Oct 23, 2025
f190c3c
implement async promises
Vipitis Oct 24, 2025
078cdc3
update async usage
Vipitis Oct 24, 2025
b24f4b9
fix additional arg
Vipitis Oct 25, 2025
2b3f37c
use codegen infra
Vipitis Oct 26, 2025
be7e04e
tweak more codegen cases
Vipitis Oct 26, 2025
28442a1
flags and enums need no conversion
Vipitis Oct 26, 2025
2595c28
add default and optional positional args
Vipitis Oct 26, 2025
fa69c9b
move custom implementations
Vipitis Oct 26, 2025
c0e3f0e
move mixins to the front
Vipitis Oct 26, 2025
a3c0af8
fix forward references
Vipitis Oct 26, 2025
ecbbf79
patch in methods via codegen
Vipitis Oct 27, 2025
81700e7
include all custom methods
Vipitis Oct 27, 2025
bd8e022
add in props
Vipitis Oct 27, 2025
9342acd
fix mixins
Vipitis Oct 27, 2025
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