[experimental] Add WebGPU demo (built with Bazel)
This uses the Bazel rule wasm_cc_binary, which is defined in @emsdk [1] Note that wasm_cc_binary does not have a linkopts argument defined, so we instead put any emcc options in the cc_binary target. This works around a few bugs in the emsdk Bazel rules: - https://github.com/emscripten-core/emsdk/issues/907 - https://github.com/emscripten-core/emsdk/issues/807 Prior to PS 5, this CL tried a different way to bring in the toolchain, a more manual way outlined in [2]. A similar approach (modifying the .bazelrc and specifying the toolchain directly) might be necessary at some point, but can probably still be done using the @emsdk Bazel rules and --config=wasm. To update the version of emscripten used, we just need to update the parameter in the WORKSPACE call to emsdk_emscripten_deps(). The example/index.html file in this CL does exactly the same as [3], except the WebGPU calls are made from C++ via WASM. I made heavy use of these examples [4], [5] while exploring APIs. What was also useful was looking at the emscripten source headers [6], [7], [8], [9]. I also learned a lot about WebGPU from [10]. [1]3891e7b04b/bazel/emscripten_toolchain/wasm_cc_binary.bzl
[2] https://hackernoon.com/c-to-webassembly-using-bazel-and-emscripten-4him3ymc [3]206c1f3f7e/demos.skia.org/demos/webgpu/index.html
[4] https://github.com/kainino0x/webgpu-cross-platform-demo [5] https://github.com/Twinklebear/wgpu-cpp-starter [6]5e6c74153b/system/include/emscripten/html5_webgpu.h
[7]5e6c74153b/system/include/webgpu/webgpu.h
[8]5e6c74153b/system/include/webgpu/webgpu_cpp.h
[9]5e6c74153b/src/library_html5_webgpu.js (L24)
[10] https://alain.xyz/blog/raw-webgpu Change-Id: Iff33b72e7265200b2caacbc03e5fcc06a650b56b Reviewed-on: https://skia-review.googlesource.com/c/skia/+/457396 Reviewed-by: Leandro Lovisolo <lovisolo@google.com> Reviewed-by: Brian Salomon <bsalomon@google.com>
This commit is contained in:
parent
e4ac6eabe8
commit
262dfbafb4
5
experimental/webgpu-bazel/.gitignore
vendored
Normal file
5
experimental/webgpu-bazel/.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
bazel-bazel-webgpu
|
||||
bazel-bin
|
||||
bazel-out
|
||||
bazel-testlogs
|
||||
build/
|
16
experimental/webgpu-bazel/Makefile
Normal file
16
experimental/webgpu-bazel/Makefile
Normal file
@ -0,0 +1,16 @@
|
||||
release:
|
||||
bazel build //src:hello-world-wasm --compilation_mode opt
|
||||
- rm -rf build/
|
||||
mkdir build
|
||||
cp bazel-bin/src/hello-world-wasm/hello-world.js build/hello-world.js
|
||||
cp bazel-bin/src/hello-world-wasm/hello-world.wasm build/hello-world.wasm
|
||||
|
||||
debug:
|
||||
bazel build //src:hello-world-wasm --compilation_mode dbg
|
||||
- rm -rf build/
|
||||
mkdir build
|
||||
cp bazel-bin/src/hello-world-wasm/hello-world.js build/hello-world.js
|
||||
cp bazel-bin/src/hello-world-wasm/hello-world.wasm build/hello-world.wasm
|
||||
|
||||
serve:
|
||||
python3 ../../tools/serve_wasm.py
|
27
experimental/webgpu-bazel/WORKSPACE
Normal file
27
experimental/webgpu-bazel/WORKSPACE
Normal file
@ -0,0 +1,27 @@
|
||||
workspace(name = "bazel_webgpu_example")
|
||||
|
||||
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
|
||||
|
||||
# Loading in the emscripten toolchain is documented at https://github.com/emscripten-core/emsdk/tree/3891e7b04bf8cbb3bc62758e9c575ae096a9a518/bazel
|
||||
# The hash in the http_archive URL corresponds to https://github.com/emscripten-core/emsdk/commit/3891e7b04bf8cbb3bc62758e9c575ae096a9a518
|
||||
# AKA the 2.0.31 release. The sha256 sum came from a manual inspection of that archive.
|
||||
http_archive(
|
||||
name = "emsdk",
|
||||
sha256 = "d55e3c73fc4f8d1fecb7aabe548de86bdb55080fe6b12ce593d63b8bade54567",
|
||||
strip_prefix = "emsdk-3891e7b04bf8cbb3bc62758e9c575ae096a9a518/bazel",
|
||||
url = "https://github.com/emscripten-core/emsdk/archive/3891e7b04bf8cbb3bc62758e9c575ae096a9a518.tar.gz",
|
||||
)
|
||||
|
||||
# Working around https://github.com/emscripten-core/emsdk/issues/907
|
||||
http_archive(
|
||||
name = "build_bazel_rules_nodejs",
|
||||
sha256 = "3635797a96c7bfcd0d265dacd722a07335e64d6ded9834af8d3f1b7ba5a25bba",
|
||||
urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/4.3.0/rules_nodejs-4.3.0.tar.gz"],
|
||||
)
|
||||
# Once the workaround is no longer needed, we should be able to uncomment below
|
||||
# load("@emsdk//:deps.bzl", emsdk_deps = "deps")
|
||||
# emsdk_deps()
|
||||
|
||||
load("@emsdk//:emscripten_deps.bzl", emsdk_emscripten_deps = "emscripten_deps")
|
||||
|
||||
emsdk_emscripten_deps(emscripten_version = "2.0.31")
|
70
experimental/webgpu-bazel/example/index.html
Normal file
70
experimental/webgpu-bazel/example/index.html
Normal file
@ -0,0 +1,70 @@
|
||||
<!DOCTYPE html>
|
||||
<title>Testing WebGPU compiled with Bazel</title>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<script type="text/javascript" src="/build/hello-world.js"></script>
|
||||
|
||||
<p id="log"></p>
|
||||
|
||||
<canvas id="webgpu-demo-canvas" width=500 height=500></canvas>
|
||||
|
||||
<script type="text/javascript" charset="utf-8">
|
||||
if ("gpu" in navigator) {
|
||||
log("WebGPU detected")
|
||||
WebGPUDemo();
|
||||
} else {
|
||||
log("No WebGPU support.")
|
||||
}
|
||||
|
||||
function log(s) {
|
||||
document.getElementById("log").innerText = s;
|
||||
}
|
||||
|
||||
async function WebGPUDemo() {
|
||||
const adapter = await navigator.gpu.requestAdapter();
|
||||
if (!adapter) {
|
||||
log("Could not load an adapter. For Chrome, try running with --enable-features=Vulkan --enable-unsafe-webgpu");
|
||||
return;
|
||||
}
|
||||
const device = await adapter.requestDevice();
|
||||
console.log(adapter, device);
|
||||
|
||||
const wk = await WebGPUKitInit({locateFile: (file) => '/build/'+file});
|
||||
// https://github.com/emscripten-core/emscripten/issues/12750#issuecomment-725001907
|
||||
wk.preinitializedWebGPUDevice = device;
|
||||
|
||||
const surface = new wk.WebGPUSurface("#webgpu-demo-canvas", 500, 500);
|
||||
|
||||
const triangleVertexShader = surface.MakeShader(`[[stage(vertex)]]
|
||||
fn main([[builtin(vertex_index)]] VertexIndex : u32)
|
||||
-> [[builtin(position)]] vec4<f32> {
|
||||
var pos = array<vec2<f32>, 3>(
|
||||
vec2<f32>(0.0, 0.5),
|
||||
vec2<f32>(-0.5, -0.5),
|
||||
vec2<f32>(0.5, -0.5));
|
||||
|
||||
return vec4<f32>(pos[VertexIndex], 0.0, 1.0);
|
||||
}`);
|
||||
|
||||
const redFragmentShader = surface.MakeShader(`[[stage(fragment)]]
|
||||
fn main() -> [[location(0)]] vec4<f32> {
|
||||
return vec4<f32>(1.0, 0.0, 0.0, 1.0);
|
||||
}`);
|
||||
|
||||
const pipeline = surface.MakeRenderPipeline(triangleVertexShader, redFragmentShader);
|
||||
|
||||
const startTime = Date.now();
|
||||
function frame() {
|
||||
const now = Date.now();
|
||||
surface.drawPipeline(pipeline,
|
||||
Math.abs(Math.sin((startTime - now) / 500)), // red
|
||||
Math.abs(Math.sin((startTime - now) / 600)), // green
|
||||
Math.abs(Math.sin((startTime - now) / 700)), // blue
|
||||
1.0);
|
||||
requestAnimationFrame(frame);
|
||||
}
|
||||
requestAnimationFrame(frame);
|
||||
}
|
||||
</script>
|
52
experimental/webgpu-bazel/src/BUILD
Normal file
52
experimental/webgpu-bazel/src/BUILD
Normal file
@ -0,0 +1,52 @@
|
||||
load("@rules_cc//cc:defs.bzl", "cc_binary")
|
||||
load("@emsdk//emscripten_toolchain:wasm_rules.bzl", "wasm_cc_binary")
|
||||
|
||||
BASE_LINKOPTS = [
|
||||
#"-flto", # https://github.com/emscripten-core/emsdk/issues/807
|
||||
"--bind", # Compiles the source code using the Embind bindings to connect C/C++ and JavaScript
|
||||
"-s ALLOW_MEMORY_GROWTH=1",
|
||||
"-s USE_PTHREADS=0", # Disable pthreads
|
||||
"-s ASSERTIONS=0", # Turn off assertions
|
||||
"-s MODULARIZE=1",
|
||||
"-s EXPORT_NAME=WebGPUKitInit",
|
||||
"-s DISABLE_EXCEPTION_CATCHING=1", # Disable all exception catching
|
||||
"-s NODEJS_CATCH_EXIT=0", # We don't have a 'main' so disable exit() catching
|
||||
"-s WASM=1",
|
||||
"-s USE_WEBGPU=1",
|
||||
]
|
||||
|
||||
RELEASE_OPTS = [
|
||||
"--closure 1", # Run the closure compiler
|
||||
]
|
||||
|
||||
DEBUG_OPTS = [
|
||||
"--closure 0", # Do not use closure
|
||||
]
|
||||
|
||||
config_setting(
|
||||
name = "release_opts",
|
||||
values = {"compilation_mode": "opt"},
|
||||
)
|
||||
|
||||
config_setting(
|
||||
name = "debug_opts",
|
||||
values = {"compilation_mode": "dbg"},
|
||||
)
|
||||
|
||||
cc_binary(
|
||||
name = "hello-world",
|
||||
srcs = ["bindings.cpp"],
|
||||
linkopts = select({
|
||||
":debug_opts": BASE_LINKOPTS + DEBUG_OPTS,
|
||||
":release_opts": BASE_LINKOPTS + RELEASE_OPTS,
|
||||
"//conditions:default": BASE_LINKOPTS + RELEASE_OPTS,
|
||||
}),
|
||||
# This target won't build successfully on its own because of missing emscripten
|
||||
# headers etc. Therefore, we hide it from wildcards.
|
||||
tags = ["manual"],
|
||||
)
|
||||
|
||||
wasm_cc_binary(
|
||||
name = "hello-world-wasm",
|
||||
cc_target = ":hello-world",
|
||||
)
|
134
experimental/webgpu-bazel/src/bindings.cpp
Normal file
134
experimental/webgpu-bazel/src/bindings.cpp
Normal file
@ -0,0 +1,134 @@
|
||||
/*
|
||||
* Copyright 2021 Google LLC
|
||||
*
|
||||
* Use of this source code is governed by a BSD-style license that can be
|
||||
* found in the LICENSE file.
|
||||
*/
|
||||
#include <emscripten/bind.h>
|
||||
#include <emscripten/emscripten.h>
|
||||
#include <emscripten/html5.h>
|
||||
// https://github.com/emscripten-core/emscripten/blob/main/system/include/emscripten/html5_webgpu.h
|
||||
// The import/export functions defined here should allow us to fetch a handle to a given JS
|
||||
// Texture/Sampler/Device etc if needed.
|
||||
#include <emscripten/html5_webgpu.h>
|
||||
// https://github.com/emscripten-core/emscripten/blob/main/system/include/webgpu/webgpu.h
|
||||
// This defines WebGPU constants and such. It also includes a lot of typedefs that make something
|
||||
// like WGPUDevice defined as a pointer to something external. These "pointers" are actually just
|
||||
// a small integer that refers to an array index of JS objects being held by a "manager"
|
||||
// https://github.com/emscripten-core/emscripten/blob/f47bef371f3464471c6d30b631cffcdd06ced004/src/library_webgpu.js#L192
|
||||
#include <webgpu/webgpu.h>
|
||||
// https://github.com/emscripten-core/emscripten/blob/main/system/include/webgpu/webgpu_cpp.h
|
||||
// This defines the C++ equivalents to the JS WebGPU API.
|
||||
#include <webgpu/webgpu_cpp.h>
|
||||
|
||||
using namespace emscripten;
|
||||
|
||||
wgpu::ShaderModule createShaderModule(wgpu::Device device, const char* source) {
|
||||
// https://github.com/emscripten-core/emscripten/blob/da842597941f425e92df0b902d3af53f1bcc2713/system/include/webgpu/webgpu_cpp.h#L1415
|
||||
wgpu::ShaderModuleWGSLDescriptor wDesc;
|
||||
wDesc.source = source;
|
||||
wgpu::ShaderModuleDescriptor desc = {.nextInChain = &wDesc};
|
||||
return device.CreateShaderModule(&desc);
|
||||
}
|
||||
|
||||
wgpu::RenderPipeline createRenderPipeline(wgpu::Device device, wgpu::ShaderModule vertexShader,
|
||||
wgpu::ShaderModule fragmentShader) {
|
||||
wgpu::ColorTargetState colorTargetState{};
|
||||
colorTargetState.format = wgpu::TextureFormat::BGRA8Unorm;
|
||||
|
||||
wgpu::FragmentState fragmentState{};
|
||||
fragmentState.module = fragmentShader;
|
||||
fragmentState.entryPoint = "main"; // assumes main() is defined in fragment shader code
|
||||
fragmentState.targetCount = 1;
|
||||
fragmentState.targets = &colorTargetState;
|
||||
|
||||
wgpu::PipelineLayoutDescriptor pl{};
|
||||
|
||||
// Inspired by https://github.com/kainino0x/webgpu-cross-platform-demo/blob/4061dd13096580eb5525619714145087b0d5acf6/main.cpp#L129
|
||||
wgpu::RenderPipelineDescriptor pipelineDescriptor{};
|
||||
pipelineDescriptor.layout = device.CreatePipelineLayout(&pl);
|
||||
pipelineDescriptor.vertex.module = vertexShader;
|
||||
pipelineDescriptor.vertex.entryPoint = "main"; // assumes main() is defined in vertex code
|
||||
pipelineDescriptor.fragment = &fragmentState;
|
||||
pipelineDescriptor.primitive.topology = wgpu::PrimitiveTopology::TriangleList;
|
||||
return device.CreateRenderPipeline(&pipelineDescriptor);
|
||||
}
|
||||
|
||||
wgpu::SwapChain getSwapChainForCanvas(wgpu::Device device, std::string canvasSelector, int width, int height) {
|
||||
wgpu::SurfaceDescriptorFromCanvasHTMLSelector surfaceSelector;
|
||||
surfaceSelector.selector = canvasSelector.c_str();
|
||||
|
||||
wgpu::SurfaceDescriptor surface_desc;
|
||||
surface_desc.nextInChain = &surfaceSelector;
|
||||
wgpu::Instance instance;
|
||||
wgpu::Surface surface = instance.CreateSurface(&surface_desc);
|
||||
|
||||
wgpu::SwapChainDescriptor swap_chain_desc;
|
||||
swap_chain_desc.format = wgpu::TextureFormat::BGRA8Unorm;
|
||||
swap_chain_desc.usage = wgpu::TextureUsage::RenderAttachment;
|
||||
swap_chain_desc.presentMode = wgpu::PresentMode::Fifo;
|
||||
swap_chain_desc.width = width;
|
||||
swap_chain_desc.height = height;
|
||||
return device.CreateSwapChain(surface, &swap_chain_desc);
|
||||
}
|
||||
|
||||
void drawPipeline(wgpu::Device device, wgpu::TextureView view, wgpu::RenderPipeline pipeline,
|
||||
wgpu::Color clearColor) {
|
||||
wgpu::RenderPassColorAttachment attachment{};
|
||||
attachment.view = view;
|
||||
attachment.loadOp = wgpu::LoadOp::Clear;
|
||||
attachment.storeOp = wgpu::StoreOp::Store;
|
||||
attachment.clearColor = clearColor;
|
||||
|
||||
wgpu::RenderPassDescriptor renderpass{};
|
||||
renderpass.colorAttachmentCount = 1;
|
||||
renderpass.colorAttachments = &attachment;
|
||||
wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
|
||||
wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&renderpass);
|
||||
pass.SetPipeline(pipeline);
|
||||
pass.Draw(3, // vertexCount
|
||||
1, // instanceCount
|
||||
0, // firstIndex
|
||||
0 // firstInstance
|
||||
);
|
||||
pass.EndPass();
|
||||
wgpu::CommandBuffer commands = encoder.Finish();
|
||||
device.GetQueue().Submit(1, &commands);
|
||||
}
|
||||
|
||||
class WebGPUSurface {
|
||||
public:
|
||||
WebGPUSurface(std::string canvasSelector, int width, int height) {
|
||||
fDevice = wgpu::Device::Acquire(emscripten_webgpu_get_device());
|
||||
fCanvasSwap = getSwapChainForCanvas(fDevice, canvasSelector, width, height);
|
||||
}
|
||||
|
||||
wgpu::ShaderModule makeShader(std::string source) {
|
||||
return createShaderModule(fDevice, source.c_str());
|
||||
}
|
||||
|
||||
wgpu::RenderPipeline makeRenderPipeline(wgpu::ShaderModule vertexShader,
|
||||
wgpu::ShaderModule fragmentShader) {
|
||||
return createRenderPipeline(fDevice, vertexShader, fragmentShader);
|
||||
}
|
||||
|
||||
void drawPipeline(wgpu::RenderPipeline pipeline, float r, float g, float b, float a) {
|
||||
// We cannot cache the TextureView because it will be destroyed after use.
|
||||
::drawPipeline(fDevice, fCanvasSwap.GetCurrentTextureView(), pipeline, {r, g, b, a});
|
||||
}
|
||||
|
||||
private:
|
||||
wgpu::Device fDevice;
|
||||
wgpu::SwapChain fCanvasSwap;
|
||||
};
|
||||
|
||||
EMSCRIPTEN_BINDINGS(Skia) {
|
||||
class_<WebGPUSurface>("WebGPUSurface")
|
||||
.constructor<std::string, int, int>()
|
||||
.function("MakeShader", &WebGPUSurface::makeShader)
|
||||
.function("MakeRenderPipeline", &WebGPUSurface::makeRenderPipeline)
|
||||
.function("drawPipeline", &WebGPUSurface::drawPipeline);
|
||||
|
||||
class_<wgpu::ShaderModule>("ShaderModule");
|
||||
class_<wgpu::RenderPipeline>("RenderPipeline");
|
||||
}
|
Loading…
Reference in New Issue
Block a user