[WIP] js_webgpu backend prototype
Remaining todos:
- [ ] implement physical size for canvas context to make both imgui examples work again
- [ ] understand and fix the problem in creating a timestamp query set
- [ ] implement
jswriter.pyas aPatcherand add the comment injector - [ ] how do we package the wheel? (strip out unused code, pure python - but only in the browser?)
- [ ] try a graphic example without rendercanvas
- [x] docs gallery (copy from rendercanvas)
- [ ] can we avoid copying buffers
- [ ] run tests on CI?
- [ ] cleanup all the commented out debug prints
- [ ] Warn users about glsl not being supported (or try wasm compiled naga?)
new weekend, new project...
I think there is two options to get wgpu-py into the browser: compile wgpu-native for wasm and package that, or call the js backend directly. I run into compilation errors with the rust code, so gave up there... but:
basically autocompleted my way through errors to see what kind of patterns are needed... everything around moving data requires more effort. While pyodide provides some functions, they feel buggy and unpredictable. It is likely possible to codegen the vast majority of this and then fix up all the api diff - might get to that over the next few days. structs have potential to make this easier.
I changed some of the examples to auto layout since I couldn't get .create_bindgroup_layout() to work - and you don't need it with auto layout.
works with https://github.com/pygfx/rendercanvas/pull/115
couldn't get the cube example to work just yet, but triangle does - so the potential is there
more to come
there are were many headaches around the type conversion which aren't well documented... but I got to the cube in the end. Haven't looked at any code gen approach as there is quite a few specialties like when it's okay to use keywords when calling the js function. For exmaple:
self._internal.getMappedRange(offset=js_offset, size=data.nbytes)
vs
self._internal.getMappedRange(0, size)
And the error you get is about not of type unsigned long long because these function parameters are GPUSize64 which lead me down a rabbit hole of using BigInt - and now I am not sure if that is required anymore.
Or when your dict contains the key "type" it accesses dict on the js side, not the value...
For anyone else giving this a try or making their branch from here - the comments will be all over the place and likely contradict themselves.
https://github.com/user-attachments/assets/c32224da-398a-4a06-af1d-e5989499e1ea
I will hopefully find some more time this coming week to continue and maybe get some more interesting examples to run (pygfx?/fastplotlib?).
super exciting to get imgui working with a few tweaks
https://github.com/user-attachments/assets/0c582135-0263-4d43-ba05-90a5d610edf0
cc @pthom thanks a lot for your article I read a few weeks ago, that motivated me to give it a try here!
@Vipitis : many thanks for the info, that looks very promising. Please keep me in the loop!
I was expecting codegen to come a long way here. The codegen knows when the arguments of a function were actually wrapped in a dict in IDL, so we can generate the code to reconstruct the dict before passing it to the JS WebGPU API call.
I also think that codegen can do a lot, I just need to give it a try. The to_js method has a few more arguments to make use of, for example dict_converter which sounds like solves some problems. The custom accessor currently has two functions: access the ._internal object and replace dict/struct keys with camelCase. However it overwrites the default dict conversion.
It can likely also do the data conversion and more. So the whole API might look like the following which should be trivial to codegen.
def some_function(self, *args, **kwargs):
js_args = to_js(args, eager_converter=js_acccessor)
js_kwrags = to_js(kwargs, eager_converter=js_accessor, dict_converter=Object.from_entries)
self._internal.someFunction(*js_args, js_kwargs)
Whatever way this goes, what I care most about, is that when the IDL changes for a certain method, it will place some FIXME comment in the code for the JS backend, so that we won't forget to update that method there.
I feel like I have finally moved passt all the headaches and found a "general" approach to most functions. I switched to the pyodide dev branch as the upcoming 0.29 release makes changes to how dictionaries are converted... which has been a ton of pain and the upcoming version seems to work much better. I couldn't find any release timeline so it might still be month until there is a release... Structs were the key to get all the default values while renaming keys to camel case.
Also got started with a codegen prototype and I am feeling confident this is largely going to work, depends on how much time I find in the coming week.
There was also some weirdness with css scaled canvas for click events and resizing with the imgui example - so the rendercanvas PR likely needs some more fixes, I will see if I can find time for that too.
responding in the main thread
pyodide has
run_sync()as a function and that seems to do something really similar to.sync_wait()
Oh, that's huge! 🤯 I did not know about this yet ...
So, apparently, there is a new WASM spec, called JavaScript Promise Integration (JSPI). Basically it allows making async functions synchronous, solving a group of issues for pyodide, e.g. implementing urllib.request.urlopen() with JS's async fetch(). This is exactly what we need to implement .sync_wait().
It's a WASM feature; it means wasm code can use async JS functions synchronously, basically by pausing the WASM until the promise it is waiting for continues. JS code cannot use this.
What does this mean for wgpu-py:
- We can actually support the synchronous API in Pyodide!
- We could decide to make some of the API simply synchronous. Thinking mainly of
request_adapter()andrequest_device(). - For some API, like mapping a buffer, you still want to support/promote the async API for performance.
More info / support:
- Pyodide blog post: https://blog.pyodide.org/posts/jspi/
- Available in Chrome since spring 2025
- Firefox is positive about the feature, but has not implemented it yet, see https://github.com/mozilla/standards-positions/issues/944
- https://caniuse.com/?search=jsp
I checked and both the async and sync variants work for the cube example (chrome 141 on windows), not sure what happens on unsupported browsers.
https://github.com/user-attachments/assets/00776188-6a54-4ffc-af2e-8c8284fbae9b
I haven't looked into running tests on pyodide, as tests cover way more than the examples and I have seen a new test for async.
apart from the imgui_backend example this works again with all the changes of todays rendercanvas and wgpu releases. Still needs the physical size most likely.
I will only find time on Friday the earliest to look into the timestamp query problem and writing a proper patcher. I will try to add the remaining todos at the top when I find time tomorrow.
again, @almarklein feel free to take over the branch if you have the capacity. I hope the current state isn't too messy 😬
again, @almarklein feel free to take over the branch if you have the capacity. I hope the current state isn't too messy 😬
Thanks! For the time being I have other priorities. But this is nevertheless super-interesting so I definitely want to get this in at some point!
finally figured out the hiccups:
- the
"WGPU_PY_BUILD_NOARCH" = "1"env var still includes a lib if you had it in/resources/ flit.mainwould gather the include artefacts from the toml... but not run the custom hook.
this means there now is a ~178kb wheel without libs, running in the browser (still half of that is the wgpu-native backend and the /resource/ dir which I don't think are needed). See the hacked in gallery in the docs preview (currently loads all dependencies):
- https://wgpu-py--753.org.readthedocs.build/en/753/gallery/cube.html#interactive-example classic cube (although wrong gamma maybe?)
- https://wgpu-py--753.org.readthedocs.build/en/753/gallery/imgui_backend_sea.html#interactive-example (works, but sits width too wide)
- https://wgpu-py--753.org.readthedocs.build/en/753/gallery/imgui_renderer_sea.html#interactive-example (shows ~70-75 fps)
- https://wgpu-py--753.org.readthedocs.build/en/753/gallery/triangle.html#interactive-example (should work without numpy, candidate to do without rendercanvas even).
- https://wgpu-py--753.org.readthedocs.build/en/753/gallery/compute_noop.html#interactive-example (outputs to the console, it's possible to write into html, see serving example)
still half of that is the wgpu-native backend and the /resource/ dir which I don't think are needed
Ditch it!
Package size is so important on the web :)