リアクティ ブ GPU エンジン設計仕様書
概要
GLRE は、CPU 側の JavaScript コードと GPU 側のシェーダコードを結合するリアクティブエンジンである。 WebGL2 と WebGPU の両方をサポートし、直接 GLSL または WGSL を記述することも、 TypeScript ライクなノードシステムを通じてシェーダを生成することも可能である。
アーキテクチャ構成
CPU-GPU 結合の自動化システム
中核機能は、CPU 側のデータを GPU 側のシェーダプログラムに送信するための設定を自動的に構築することである。
この処理はsrc/index.tsの createGL 関数で管理され、WebGL2 と WebGPU の違いを抽象化する。
createGL 関数は、reev ライブラリの event 関数を使用してリアクティブな GL インスタンスを生成する。 このインスタンスは、uniform、attribute、texture のデータ設定を自動的にキューイングし、タイミングで GPU に転送する。
WebGL2 バックエンド実装
webgl.ts では、OpenGL ES 3.0 ベースのレンダリングパイプラインを実装している。 この実装はクラスではなく関数型アプローチを採用し、クロージャを活用してプライベートな状態を管理する。
WebGL コンテキストから shader program を初期化した後、uniform と attribute の location を nested 関数でキャッシュする。
render 関数、clean 関数、および設定関数(_uniform、_attribute、_texture)を戻り値として返し、
これらが reev の event 関数によって元の gl オブジェクトにマージされる。
WebGPU バックエンド実装
webgpu.ts では、WebGPU API を使用したレンダリングパイプラインを実装している。 WebGL バックエンドとは異なり、パイプラインの初期化は render 段階で実行される。
これは、attribute や uniform の設定が gl.queue に追加された後、 flush 処理を経てから buffer や bindGroup の layout を構築する必要があるためである。 cached 関数を使用して uniforms、textures、attribs のデータをキャッシュし、 これらの情報を基に createVertexBuffers、createBindGroup、createPipeline の関数で GPU リソースを構築する。
ノードシステム詳細
抽象構文木構築メカニズム
ノードシステムは、JavaScript コードから GLSL/WGSL シェーダコードを生成するための DSL(Domain Specific Language)である。 create.ts の create 関数は、React.createElement と同じ引数形式で抽象構文木を構築する。
NodeProxy オブジェクトは、Proxy パターンを使用して getter/setter による動的な抽象構文木を構築する。 type、props、children のプロパティを持ち、これらの関係性が code 関数によって解析されてシェーダコードに変換される。
抽象構文木の例:
operator(+)
/ \
uniform(a) uniform(b)
コード生成プロセス
code 関数は、抽象構文木を文字列のシェーダコードに変換する中核機能を提供する。 この関数は type による条件分岐を行い、WebGL(GLSL)と WebGPU(WGSL)の両方に対応した文字列を生成する。
生成プロセスは、variables、scopes、headers の三つのカテゴリに分類される。 variables は基本的な演算子のワンライナー生成、scopes は複数行にわたる処理、headers はシェーダ全体の冒頭に追加される定義文である。
NodeContext を通じて、uniform の struct としての統合や、 依存関係のトポロジカルソートなど、全ノードのビルド完了後に必要な後処理を管理する。
型推論システム
infer.ts は、ノードツリーから適切なシェーダ型を推論する機能を実装している。 これは TypeScript の型推論ではなく、JavaScript 実行時に GLSL/WGSL 生成のための型を決定するシステムである。
型推論は、演算子、関数、変数の種類に応じて適切な戻り値型を決定する。 例えば、comparison operators は常に bool を返し、vec 型同士の演算では適切な次元の vec 型を返す。
スコープ管理システム
scope.ts は、複数行のコードを生成するための機能を提供する。 let scope 変数と let define 変数を使用して、現在のスコープコンテキストを管理する。
addToScope 関数によって指定されたスコープに行を追加し、scoped 関数によって特定のスコープを一時的に切り替えて処理を実行する。 If、Loop、Switch などのコードはこのメカニズムでネストされたコードを生成する。
GPU リソース管理
パイプライン構築の自動化
pipeline.ts では、WebGPU 向けのリソース管理機能を提供する createBindings 関数は、 uniform、texture、attribute のリソースに対して自動的に group と binding の番号を割り当てる。
この番号割り当ては、シェーダコード生成時と実際の GPU リソース作成時で一致する必要があるため、 parse.ts の parseUniformHead、parseAttribHead 関数で参照される。
バッファ管理システム
createUniformBuffer、createAttribBuffer の関数は、 JavaScript の number 配列から GPU 用の Float32Array および GPUBuffer を生成する。 uniform バッファは 256 バイト境界に整列され、attribute バッファは頂点データのストライドに基づいてサイズで作成される。
レンダリングパイプライン統合
シェーダコンパイルフロー
webgl.ts および webgpu.ts では、fragment 関数と vertex 関数を呼び出して文字列形式のシェーダコードを生成する。 これらの関数は node/index.ts で定義され、NodeProxy から最終的な GLSL/WGSL コードへの変換を管理する。
生成されたシェーダコードは、WebGL では createProgram 関数、WebGPU では createPipeline 関数によって GPU プログラムとして初期化される。
リアクティブ更新メカニズム
reev ライブラリの durable 関数を使用して、uniform、attribute、texture の設定関数をキューイングする。 これにより、JavaScript コードからの値変更が自動的に GPU に反映される。
update サイクルは、gl.queue.flush()によってトリガーされ、累積された変更を一括して GPU に送信する。 このアプローチにより、レンダリングフレームごとの効率的な更新が実現される。
実装ガイドライン
ノードシステム使用時の注意点
ノードシステムを使用する場合、node.ts のファクトリ関数(uniform、attribute、constant など)から。 NodeProxy を生成し、これらを組み合わせて抽象構文木を構築する。
toVar()メソッドを使用して変数宣言を行い、assign()メソッドを使用して値の代入を実行する。 Fn()関数を使用してカスタム関数を定義し、setLayout()メソッドで引数と戻り値の型を明示的に指定する。
直接シェーダコード使用時の指針
ノードシステムを使用せず、直接 GLSL または WGSL を記述する場合は、createGL の vs および fs プロパティに文字列として渡す。 この場合でも、uniform、attribute、texture の設定は GL インスタンスのメソッドを通じて行う。
シェーダコード内で使用する uniform 名、attribute 名は、JavaScript 側の設定と一致させる必要がある。 WebGPU の場合は、group と binding の番号も適切に設定する。
エラー回避のための設計原則
NodeProxy の children 配列の設計パターンを理解することが重要である。 例えば、If ノードでは condition と scope が交互に配置され、最後の要素が Else の場合は条件なしで配置される。
struct 定義時は、依存関係のトポロジカルソートが実行されるため、循環参照を避ける設計が必要である。 uniform の group/binding 番号は自動割り当てされるため、手動での指定は推奨されない。
TypeScript コーディングスタイルガイド
基本設計思想
関数型プログラミング実装
class キーワードを使用せず、クロージャベースのコンストラクターパターンを採用する。 プライベート変数のアクセス制御と状態管理を実現する。
コンストラクター関数の最初で初期化処理を完了し、プライベート変数とメソッドを定義した後、 パブリック API として必要な要素のみをオブジェクトで返す。 カプセル化の原則を保ちつつ、JavaScript の特性を活用する。
TypeScript 型安全性の適用
TypeScript は型ヒントとして使用し、複雑な型定義によってコードが複雑化することを避ける 型推論の恩恵を得つつ、JavaScript がメインである状況を維持して、実装の柔軟性と開発効率を両立する
複雑な型が必要な場合は一旦 as キーワードを活用して型の橋渡しを行う。 型安全性よりも実装の簡潔性を重視して、開発者の認知負荷を軽減し、問題解決に集中する。
コード構造とファイル設計
ファイル分割の原則
各ファイルは 100 行前後を目標とし、この制限により責任範囲を明確化する。 ファイル名は 1 単語で決定し、複数単語が必要な場合はディレクトリ構造を見直す。
基本のファイル構成として、types.ts、const.ts、config.ts、index.ts のパターンを採用する。 この構成により、各ファイルの役割が自明となり、開発者間での認識を統一する。
命名規則と一貫性
変数と関数の命名パターン
小文字から始まる識別子には camelCase を使用し、boolean 変数は has 前置詞は使用せず、is 前置詞で始める。 関数名は動詞から始めて、その機能を示す。
大文字から始まる識別子には PascalCase を使用し、型とインターフェースの定義に適用する。 interface の使用を優先し、extends の互換性を重視する。 type キーワードは、typeof 演算子やユニオン型など、interface で定義できない場合のみ使用する。
ファイル名とディレクトリ構造
ファイル名に camelCase を使用せず、シンプルな単語で構成する パッケージのサブディレクトリは、import パスとして使用される際の利便性を考慮して決定する
書式設定の仕様
インデント設定による構造の強制
8 文字スペースインデントにより、コード構造の可視化を強化する。 この設定により、3 レベル以上のインデントは読みづらくなり、関数分割やリファクタリングを促進する。
120 文字の行幅制限により長さを確保し、セミコロン無しの設定でコードの簡潔性を実現する。 single quote の使用により、文字列表記を統一する。
条件分岐の記述
複雑な条件は事前に boolean 変数として定義し、条件の意図を明確にする。
const isValid = condition1 && condition2の形で条件をまとめ、
if 文ではif (isValid) returnの形で早期リターンを活用する。
実装パターンの統一
クロージャベースコンストラクターパターン
コンストラクター関数と してconst createInstance = (args) => {}の形を採用し、関数の最初で初期化処理を完了する。
プライベート変数はletやconstで定義し、パブリック API は最後にオブジェクトとして返す。
good: const createRenderer = (config) => { const context = initContext(config); const render = () => context.draw(); return { render } }
分岐処理の統一パターン
switch キーワードを使用せず、if-return パターンで分岐処理を実装する。 このパターンにより、条件の複雑化を防ぎ、各分岐の独立性を保つ。
good: const getFormat = (count) => { if (count === 2) return 'vec2'; if (count === 3) return 'vec3'; return 'float' }
対称性の重視
関連する関数やモジュール間で構造の対称性を保つ。 入力と出力、初期化と破棄、設定と取得など、対となる操作では一貫したパターンを適用して、 システム全体の理解しやすさを向上させる。