[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:
Kevin Lubick 2021-10-11 14:56:49 -04:00
parent e4ac6eabe8
commit 262dfbafb4
6 changed files with 304 additions and 0 deletions

5
experimental/webgpu-bazel/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
bazel-bazel-webgpu
bazel-bin
bazel-out
bazel-testlogs
build/

View 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

View 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")

View 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>

View 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",
)

View 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");
}