この記事ははてなエンジニア Advent Calendar 2019 - Qiitaの4日目の記事です。昨日はid:yigarashiさんのApollo ClientのInMemoryCacheとMutationに関する調査・考察 - yigarashi のブログでした。
こんにちは、はてなでWebアプリケーションエンジニアをしているid:maku693です。
普段はマンガチームの一員としてWebアプリケーションをつくる暮らしをしていますが、この記事ではブラウザで動作するグラフィックスAPIであるWebGPUについて紹介します。
WebGPUとは
WebGPUとは、W3Cが開発しているブラウザでGPUを使った処理を実行するためのAPIです。
既存の似たAPIとしてWebGLがありますが、WebGLはOpenGL ESを元にしたAPIである一方、WebGPUはDirectX 12, Metal, VulkanのようなOpenGLよりも低レベルなグラフィックスAPIをモデルにしているので、WebGLよりもドライバー負荷が低く、よりハードウェアの性能を生かしたアプリケーションを実装しやすくなります。
WebGPUの実装は各ブラウザで進められていますが、仕様はまだEditor’s Draft*1な上、ブラウザごとに実装状況はまちまち*2で、さらに言うと、Safariではシェーダ言語としてWSL(Windows Subsystem for LinuxではなくWeb Shading Language)を利用するのですが、現時点のChrome, FirefoxではSPIR-Vという別の言語を利用するAPIとなっているなど、仕様レベルの差もそれなりにあります。
最終的なAPIが確定し、各ブラウザで安定して動作するまでにはまだまだ時間がかかりそうな様子ではありますが、ブラウザで低レベルグラフィックスAPIを触れるようになると、C/C++やOSに依存した低レベルグラフィックスAPIに親しんでいない開発者でもGPUの高度な機能を使えるようになるので、夢のある技術だなと思っています。
WebGPUに触れてみる
さて、WebGPUの雰囲気を説明するために、素朴なパーティクルを実装してみました。
実際に動作するデモはこちらです…が、今回はWSLを使ってみたかったので、Safari Technology Previewでしか動作しません。動かしてみたい方は、Safari Technology Previewをインストールした上でDevelop > Experimental Features > WebGPUにチェックをつけてご覧ください。
ここからはいくつかプログラム的な見どころをご紹介します。
JavaScriptではなくコンピュートシェーダでパーティクルの位置を計算している
struct Particle { float2 position; float2 velocity; } [numthreads(1, 1, 1)] compute void ComputeMain( device Particle[] prevParticles : register(u0), device Particle[] nextParticles : register(u1), float3 threadID : SV_DispatchThreadID ) { uint index = uint(threadID.x); if (${particleCount} <= index) { return; } float2 position = prevParticles[index].position; float2 velocity = prevParticles[index].velocity; position += velocity; if (1.0 < position.x) { position.x = -1; } if (position.x < -1.0) { position.x = 1; } if (1.0 < position.y) { position.y = -1; } if (position.y < -1.0) { position.y = 1; } nextParticles[index].position = position; nextParticles[index].velocity = velocity; }
シェーダというのはGPU上で実行されるプログラムのことで、コンピュートシェーダというのはその中でも汎用計算向けのシェーダです。今回のプログラムではcompute void ComputeMain()
がそれで、WebGL 1.0で同じようなことをしたい場合、パーティクルのそれぞれの粒の位置データを一度テクスチャとして書き出すなどのワークアラウンドが必要だった*3のですが、WebGPUでは直接GPU上のメモリを読み書きできるようになったので、より素直にGPU上で実行したい処理を記述できるようになりました。
main 関数を async にしている
async function main() { …(中略)… }
DOMContentLoadedのコールバックに指定しているmain関数をasyncにしています。バッファ(GPUのメモリを表現するオブジェクト)を更新する処理など、WebGPUのいくつかのメソッドはPromiseを返すようになっており、いちいちPromiseをメソッドチェーンで処理しているとネストがかなり深くなってしまうので、awaitを使えるようにしています。
WebGPUがPromiseを使っているのは、GPUとの通信がキレイに現代的なJavaScriptのAPIとしてモデリングされているように感じて好きなポイントです。
レンダリングコマンドとコンピュートコマンドを同時にGPUに送信している
const commandEncoder = await device.createCommandEncoder(); const computePassEncoder = commandEncoder.beginComputePass(); computePassEncoder.setPipeline(computePipeline); computePassEncoder.setBindGroup(0, particleBindGroups[t % 2]); computePassEncoder.dispatch(particleCount, 1, 1); computePassEncoder.endPass(); const renderPassEncoder = commandEncoder.beginRenderPass({ colorAttachments: [ { attachment: swapChain.getCurrentTexture().createDefaultView(), loadOp: "clear", clearColor: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }, storeOp: "store" } ] }); renderPassEncoder.setPipeline(renderPipeline); renderPassEncoder.setVertexBuffers(0, [particleBuffers[(t + 1) % 2]], [0]); renderPassEncoder.draw(particleCount, 1, 0, 0); renderPassEncoder.endPass(); device.getQueue().submit([commandEncoder.finish()]);
WebGPUというより低レベルグラフィックスAPI全般に共通の特徴ですが、コマンドバッファ(GPUへの連続した命令を記録する仕組み)の管理がプログラマに任されており、かつ自由度も高いので、レンダリングコマンド(グラフィックス描画命令)とコンピュートコマンド(汎用計算命令)を同時にGPUに送信できます。
これの何が嬉しいかというと、この程度のデモではそこまでメリットがないのですが、例えばWebGLではコンピュートコマンドの実行終了を待ってからレンダリングコマンドを発行するしかなかったところ、WebGPUでは待つ必要がないので、CPU側の待ち時間を減らすことができ、より効果的にCPUを活用できます。
おわりに
この記事ではWebGPUについて紹介しました。少しでもグラフィックスプログラミングに興味を持ってもらえたら嬉しいです。明日はid:ma2sakaさんです!
*1:https://gpuweb.github.io/gpuweb/
*2:https://github.com/gpuweb/gpuweb/wiki/Implementation-Status
*3:実はWebGL2.0にはコンピュートパイプラインの仕様があるのですが、WindowsとLinuxのChromeとFirefoxでしか実装されていません。