プログラミング
S式の宣言的なWebGPU
Declarative WebGPU with S-Expressions (hugodaniel.com)
要約
pngineは、WebGPUのパイプライン、バッファ、シェーダーコードなどをS式(S-expressions)で宣言的に記述し、管理するためのフォーマットとランタイムです。これにより、クロスプラットフォームでのWebGPUの利用、設定の検証、そして単一のファイル(PNG形式も含む)へのエクスポートが可能になります。このツールは、WebGPUの複雑な設定を簡素化し、開発者がより高レベルな抽象化に集中できるようにすることを目指しています。
全文翻訳
これはpngine、私が過去2.5年間取り組んできたWebGPUのための宣言的なフォーマットとランタイムです。
(shader-module :name code :code """
@vertex fn vertexMain( @builtin(vertex_index) VertexIndex : u32 ) -> @builtin(position) vec4f {
var pos = array<vec2f, 3>(
vec2(0.0, 0.5),
vec2(-0.5, -0.5),
vec2(0.5, -0.5)
);
return vec4f(pos[VertexIndex], 0.0, 1.0);
}
@fragment fn fragMain() -> @location(0) vec4f {
return vec4(1.0, 0.0, 0.0, 1.0);
}
""")
(render-pipeline :name pipeline :layout auto
(vertex :module code :entry vertexMain)
(fragment :module code :entry fragMain (target :format preferred-canvas-format))
)
(render-pass :name trianglePass
(color-attachment :view context-current-texture :clear-value [0 0 0 0] :load-op clear :store-op store)
:pipeline pipeline
(draw :vertex-count 3)
)
(frame :name main :perform [trianglePass])
上記のコードは、WebGPU仕様と1対1で対応するS式を使用した配管処理による、単純な赤い三角形です。
1-2-3 pngine
Pngineには3つの主な機能があります。
クロスプラットフォームの方法(ブラウザに限定されず、Rustのwgpuでも動作し、AndroidおよびiOS用のプレイヤーも存在します)で、WebGPUの配管(シェーダーとCPU/WASM初期化を含む)を宣言および出荷できます。
ライブWebGPUコンテキストを必要とせずに、WGSLリフレクションと仕様チェックを使用して宣言されたWebGPU構成を検証し、エラーと警告を生成します。
また、LSPを備えているため、入力中にリアルタイムで確認できます。
すべてを単一の.html、.zip、または.pngファイルにエクスポートできます。これにより、実行可能なPNGを相手に渡すことができます(.pngは自動実行されませんが、バイトコードが含まれているだけで、それを読み込んで要求に応じて再生するには小さなプレイヤーが必要です)。
最後の部分がクールな部分です。pngineの「png」の由来は、S式がバイナリ表現にコンパイルされ、それが小さなランタイムによって解釈されるためです。そのランタイムとペイロードは、PNGの追加チャンクに入れることができ、PNG自体は出荷するもののプレビュー画像として残ります。これらすべてについて、こちらにある専用ページで詳しく読むことができます。コードはCC0であり、問題や議論のためにGitHubで入手可能で、リリースは私のセルフホストリポジトリからカットされます。
しかし、なぜ?
カスタムWebGPU配線を出荷および共有することは、私にとって未解決の問題でした。すべてのWebGPUパイプライン/バッファ/パスなどを記述する単一のファイルと、シェーダーモジュールおよびWGSLコードが必要です。現在、WebGPUをサポートする任意の汎用言語でこれを行うことができますが、私はより高い抽象化を、気を散らすものなしで求めています。他のツールの基盤として使用でき、より深い洞察(人間または機械による)を提供するものです。SJSONは、DSLを自動的に検証でき、S式を通じて一貫した composable な方法で配信できる方法を提供してくれました。これはWebGPUの配線部分のみであり、シェーダーコード用ではありません。WGSLはそれ自体をDSLと見なしており、そのまま、 pristine な状態で保持され、拡張されていません。
プログラム表現自体が読み取り可能なデータであるというこのアイデアは、pngineの中心にあります。このプロパティは私にとって非常に役立ちます。なぜなら、プログラム表現自体が通常の構造化データである場合、検証、再生、検査、最小化、シリアル化、および機械生成がすべて事前に可能になり、各機能が同じ表現で動作するからです。このトピックについては、SJSONの記事とページで広範囲に記述しているので、これ以上深くは踏み込みません。
配線をパッケージ化する
今日、あなたが私にパイプラインを共有したい場合、まず一連の規約に同意する必要があります。どの言語で記述しますか?TypeScript、JavaScript、Rust?ハンドルはどのように表現され、渡されますか?このパイプラインは、小さなアニメーション、ゲーム、またはまったく別のもののの一部ですか?これらの選択は、同じWebGPU配線がどのようにパッケージ化され、共有されるかに影響します。Pngineはこれらを削除しませんが、表面積を非常に小さくしようとします。これにより、非常に異なる目的とターゲットのために、同じ方法で複数のWebGPU配線をバンドルできます。
ほとんどのWebGPU配線は静的です。
WebGPU仕様は、グラフィックスの世界がどうなったかという、泥だらけで複雑なトピックに適用された一貫性とエレガンスの驚くべき作品です。WebGPUは、暗黙的な状態を取り除き、それを慎重に設計されたAPI呼び出しや構成オブジェクトに移行するために多大な努力を払っています。この点を少し強調したいのですが、最初に示した単純な三角形の同等のJavaScriptコードを見てみましょう。
// (shader-module :name code ...)
const code = device.createShaderModule({
code: `
@vertex fn vertexMain( @builtin(vertex_index) VertexIndex : u32 ) -> @builtin(position) vec4f {
var pos = array<vec2f, 3>(
vec2(0.0, 0.5),
vec2(-0.5, -0.5),
vec2(0.5, -0.5)
);
return vec4f(pos[VertexIndex], 0.0, 1.0);
}
@fragment fn fragMain() -> @location(0) vec4f {
return vec4(1.0, 0.0, 0.0, 1.0);
}
`,
});
// (render-pipeline :name pipeline ...)
const pipeline = device.createRenderPipeline({
layout: "auto",
vertex: {
module: code,
entryPoint: "vertexMain",
},
fragment: {
module: code,
entryPoint: "fragMain",
targets: [{ format }],
},
primitive: {
topology: "triangle-list",
}, // default
});
// (frame :name main :perform [trianglePass])
const encoder = device.createCommandEncoder();
// (render-pass :name trianglePass ...)
const pass = encoder.beginRenderPass({
colorAttachments: [
{
view: context.getCurrentTexture().createView(),
clearValue: [0, 0, 0, 0],
loadOp: "clear",
storeOp: "store",
},
],
});
pass.setPipeline(pipeline);
pass.draw(3); // (draw :vertex-count 3)
pass.end();
device.queue.submit([encoder.finish()]);
このコードは、もの(createShaderModule、createRenderPipeline、createCommandEncoder)を作成し、それらを配線し、最後にパスでシーケンスします。(ちなみに、JavaScriptコードはpngineで生成されたもので、S式から可能な出力の1つです)。
作成、配線、シーケンスはWebGPU関連の処理であり、通常は好みに応じて抽象化できる手続き的な環境で行われます。WebGPUの配線を宣言的な空間に持ち込むのは簡単で、ほぼ即座に行えます。これにより、概念やブロックを、順序に関係なく記述できる方法で操作できます。それらは宣言されたまま存在し、必要に応じて移動できます。WebGPUのシーケンス部分もそれほど難しくありません。何が起こるべきかの記述を保持し、適切なタイミングで実行します。pngineでは、フレームは順序が重要な数少ない場所の1つであり、:performの各名前はファイル内の別の場所で宣言されたパスを参照します。
(frame :name main :perform [ update-uniforms update-textures sdf-pass post-processing ])
パイプラインに沿って
シェーダーコードの共有は素晴らしいです。Shadertoyは素晴らしいコミュニティと、学ぶべき例やコードがたくさんある素晴らしいツールです。それを使用してください。
もう少し深く掘り下げたい場合は、compute.toysがあります。これは、複数のコンピュートシェーダーが互いにフィードし、画面にピクセルとして送信される最終バッファに供給できるものです(まあ、それ以上のものですが、例として)。これは、いくつかのパイプライン構成などを指定するために使用できる#macrosでWGSLを拡張します。素晴らしいツールで、私の作業の多くをそこで行いました。WGSLを学び、いくつかのアイデアを試したい場合に最適です。
今日では、このようなツールがたくさんあります。「より良いバージョンを作って、自分のシェーダーの好みに合わせることができる」という考えから、それは本当にクールで、シェーダーツールはアーティストによって進化しました。しかし、これはpngineではありません。残念ながら、プレイグラウンドはありません(LSPはありますが)、それがここでは意図ではありません。私の考えは、pngineは基盤であり、非常に汎用性の高い基盤であり、あなたを制限するのではなく、あなたが(またはあなたの好きなマシンが)高レベルの抽象化では簡単に得られないクレイジーなもの(シーングラフ、マテリアルノードなど)を構築するのを助けるということです。
三角形、そして…
三角形はすべて楽しいですが、その単純な例はpngineがあなたをどこに置くかをあまりよく示していません。CPUに触れないパーティクルシステムが欲しいですか?プレイするPNGをダウンロードする2048 GPU