Polygonをゼロから書き直した理由
Rewriting a Commercial Engine in Jai (polygonjs.com)
要約
Polygon.jsの開発者は、JavaScriptのメモリ管理とガベージコレクタの挙動に長年悩まされてきました。これらの問題が原因で、特にネイティブプラットフォームへのデプロイやパフォーマンスチューニングにおいて、開発の自由度や信頼性が制限されていました。そこで、Jonathan Blow氏が開発するプログラミング言語Jaiへの移行を決断し、メモリとパフォーマンスの完全な制御を目指してPolygonをゼロから書き直すプロジェクトに着手しました。
全文翻訳
Polygonをゼロから書き直した理由
2026年8月7日
Polygonjsを始めたのは8年前のことでした。それはthree.jsをベースに、TypeScriptとVue.jsを使用し、ブラウザでプロシージャルジオメトリを提供するものでした。それは非常によく機能し、数多くの商用プロジェクトのリリースに役立ちました。また、GitHubにもまだあります。
しかし、私をずっと悩ませる側面がありました。それはすべてJavaScriptに起因していました。
メモリ使用量の検査と制御
Polygonの重要な機能の一つは、プロシージャルジオメトリを生成できることです。これは、典型的なアプリが大量のジオメトリを生成する可能性があることを意味します。それらは大量のメモリを消費する可能性があります。ブラウザでは、メモリはかなり制約されています。使いすぎると、タブがクラッシュします。
この生成の背後には、どのジオメトリがどのように使用されているかを追跡するシステムがありました。レンダラーはそれらを使用していたか?ノード間のキャッシュとして機能し、下流のノードがより速く再計算できるようにしていたか?両方の質問に対する答えが「いいえ」であれば、システムはジオメトリを削除しました。ジオメトリを削除すると、それが使用していたメモリが解放されます。
あるいは、解放されるのでしょうか?
それがJavaScriptに対する私の主な不満です。メモリが何によって使用されているかを直接確認する方法がありません。もちろんブラウザの開発者ツールやブラウザフラグがありますが、その答えは常に部分的であるか、テストスイートで使用不可能であるように感じられました。
そして、もし他のオブジェクトがそのジオメトリを参照し続けていた場合、ガベージコレクタはそれを「使用中」と見なし、決して解放されませんでした。
私のコードが本当にそのメモリを解放したかどうかは、私が答えを知る必要のある質問でした。私は確かにできませんでした。
ガベージコレクタ
ガベージコレクタは、私が最も扱いにくいと感じる部分です。それは、参照されなくなったオブジェクトを見つけたときにメモリをクリアするシステムです。
それがクリアするとき、フレーム予算のかなりの部分を消費する可能性があり、フレームがドロップする可能性があります。これは常に不快であり、特に多数のアニメーションオブジェクトを持つ体験を設計している場合や、カメラがアニメーション化されているかユーザー制御の場合には顕著です。
これについてできる最善のことは、ガベージコレクタがそもそも実行されないようにすることです。それはメモリを非常にうまく制御することを意味しますが、前述のように、それは非常に困難です。
もちろん、オブジェクトプーリングのようなメモリを再利用するための多くのテクニックがあり、私はそれらを使用しました。しかし、システムが大きくなるほど、プーリングが必要な場所を見落としやすくなります。なぜなら、システムはあなたが一つを忘れた場合に教えてくれないからです。テストスイートがメモリリークの場所、あるいはメモリリークがあるかどうかを教えてくれない場合、あなたは盲目的に進んでいることになります。
デプロイメント
JavaScriptは、アプリをブラウザにデプロイするのに優れています。ネイティブプラットフォーム、例えばSteam上のゲームにアプリをデプロイしたい場合、ElectronやCapacitorを使用できます。私はそれらをまあまあだと感じました。Windows、Mac、Linuxのビルドを作成させてくれたからです。
しかし、マルチプレイヤーを提供するためにSteam APIの統合を開始したとき、JavaScriptからネイティブライブラリを呼び出す必要があり、Electronサンドボックスの内外で多くの手間をかける必要がありました。そしてそれはすぐに非常に混乱しました。コードが競合状態やメモリリークから解放されているという確信が持てませんでした。これは私がリリースしたくないコードだったので、ゲームをリリースしませんでした。
回り道の始まり
2年前、私は選択肢を探し始めました。Polygonの完全な書き直しはまだ考えていませんでしたが、一部のコア機能の書き直しが必要でした。
それから、Jonathan Blow氏が作成したプログラミング言語Jaiについて聞きました。彼が新しい言語がどのように役立つかについての理由は私に響きました。私はベータ版に申し込みました(この言語は私がこれを書いている時点ではまだ公開されていません)、そしてJaiコンパイラにアクセスしてテストすることができました。
私はそれに非常に快適に感じ、私が直面していた多くの問題を解決できることは明らかでした。メモリを検査し、どの情報がどこに割り当てられているかを確認し、どのように割り当てるか、そしていつ解放するかを決定できました。これは典型的なC/C++や低レベル言語のように聞こえますが、よりエレガントで実用的です。そして、プールで割り当てたり、一時割り当てを使用したりすると、すべてを解放するのは非常に高速です。これは一石二鳥でした。メモリの蓄積と、ガベージコレクタによるフレームドロップです。
また、すべてのオペレーティングシステム(Windows、Mac、Linux)、WebAssembly、Android、iOSにデプロイすることもできました。私はすぐにElectronのことを考えるのをやめ、一瞬たりともそれを惜しみませんでした。
この時点で、PolygonにJaiをどのように組み込むかではなく、全体を書き直すことが選択肢になるかどうかを考えていました。私は長所と短所のリストを作成し始めましたが、それは圧倒的でした。主な短所は次のとおりです。
- コアプロシージャルエンジン全体と、ノードのほとんど(約300個、多少の増減あり)を書き直す必要がありました。
- three.jsに頼ることができなくなります。Polygon v1の柱であったそのライブラリがあったのに、シーングラフとレンダラーを自分で書く必要がありました。
コアエンジンは一度やったことがあるので、二度目はできると思いました。しかし、レンダラーは、three.jsが提供するすべてのマテリアルとともに、別の問題でした。それをやり遂げられるかどうかは全く分かりませんでした。
長所を見ると、2つの項目が際立っていました。
- メモリの制御。
- パフォーマンスの制御。
私はそれに抵抗できませんでした。それが種を植え、その後すぐに書き直しを開始しました。最初のステップは、未知の未知を明らかにすることでした。
1年目
1年目は学習曲線の最も難しい部分でした。低レベル言語に慣れ直す必要がありました。私はC/C++を使用していましたが、非常に錆びついていました。オブジェクト指向言語で作成されたノードシステムを、構造体と関数のみをサポートする言語に再構築することは、謙虚な経験でした。しかし、新しいシステムはよりクリーンで、より純粋になり、データのレイアウトが正確に分かるようになりました。それほどまでに、オブジェクト指向デザインへの興味を完全に失いました。
しばらくの間、three.jsはレンダリングレイヤーのままでした。そこでWebAssemblyでジオメトリを生成し、JavaScriptにポインタとサイズを渡していました。JavaScriptはそれから型付き配列を作成し、それを使ってジオメトリ属性を構築していました。それは機能し、商用プロジェクトのリリースを助け、開発を維持しました。
しかし、それはネイティブの解決策にはなりませんでした。そこで、ゆっくりと着実に、生のWebGPU/wgpuに慣れていきました。バインディング、レイアウト、パイプラインの作成方法、オブジェクトやカメラのマトリックスの供給方法、GPUにデータをアップロードする方法など、レンダラーが必要とするすべての基本的なコンポーネントを学びました。そして、JaiからJavaScriptへの薄い変換レイヤーを追加して、wgpu呼び出しをWebGPU呼び出しに変換し、一部の構造体変換でWebAssemblyメモリバッファを正しいアドレスで読み取れるようにしました。
それが基盤を構築した年でした。
2年目
さまざまなジオメトリ生成方法を実験し、メモリが解放されずに再利用されるリサイクル可能なシステムにたどり着きました。そのため、ノードがジオメトリを生成するときは、中央の場所からメモリを要求します。そこは、新しく割り当てられた領域か、以前に解放された領域のいずれかを割り当てます。
多くの低レベルプログラマーはこれを読んで、最も基本的な機能だと考えるでしょう。それを適用することを決定するのは簡単な部分でした。Polygonのすべてのデータピースに対してそれを機能させることが難しい部分でした。今では、作成されるすべてのものがリサイクル可能な配列内にあるか、フレームの終わりにクリアされる一時割り当て内にあります。すべてのノード、すべての式、すべてのジオメトリ属性、すべてのオブジェクト属性、すべての接続、その他多くのものがこの方法で保存されています。これにより、システム全体が非常に、非常に効率的になります。
全体として、開発は容易になり、バグは減り、発生した場合の診断ははるかに簡単になりました。
これにより、長年意図していたが時間が見つからなかったセクションをリファクタリングできるようになりました。例えば、glとjsノードを単一のコードコンテキストにマージすることです。そこでは、GPU(GLSLまたはWGSL)用とCPU(JavaScriptまたはJai)用のコードを生成するノードの区別がありません。コードノードは現在、簡単なトグルで、どちらかのコードを生成できます。
そしてレンダラーはかなりまともになりました。その年の初めは機能することだけでしたが、その時点で複数のプロシージャルに生成されたオブジェクトを非常に基本的なランバートシェーダーで表示できました。少しずつ、新しい機能やテストスイートの新しいチェックを追加していきました。現在ではカスタムマテリアルや、後で詳しく説明する他の多くのものをレンダリングできます。
現在の状況
Polygon v2がリリースされました。バージョン2.0.54です。プロシージャルジオメトリ、カスタムマテリアル、コンピュートシェーダー、およびレンダーグラフを備えています。さらに多くの機能が開発中です。シーンファイルを単に開いて保存する必要があるアーティスト向けのシンプルな実行可能ファイルとして、またはカスタムノードを書きたい開発者向けのSDKとして提供されます。