Kirino 的游戏宝藏
← 第 022 号へ
ゲーム観察

四十八バイトの兵票と六十万エンティティの墓場:Matt Busel はいかにしてカスタムインスタンシングで『LAWN WAR』のボトルネックを Draw Call からリスト構築へ引き戻したか

もし Bevy のような ECS エンジンにおけるインディーの既定が、「動くものはまず Entity にし、Mesh3d・Transform・Visibility を揃え、エンジンの自動バッチに任せる」だとすれば、2026年10月16日、Matt Busel が Tensorust に公開した解体記事は、その既定の請求書をテーブルに広げ…

保存した記事 ↗

もし Bevy のような ECS エンジンにおけるインディーの既定が、「動くものはまず Entity にし、Mesh3d・Transform・Visibility を揃え、エンジンの自動バッチに任せる」だとすれば、2026年10月16日、Matt Busel が Tensorust に公開した解体記事は、その既定の請求書をテーブルに広げ、一番高い行を赤で丸で囲んだに等しい。ゲームは『LAWN WAR』。Rust と Bevy 0.19.1。終わりのない植物対ゾンビ戦争。設計文書は「片側数十万ユニット」を求める。本稿は完成品を未プレイであり、以下の場面と数字は著者の公開記事、DEV 同期稿、および文中の計測条件に依る。

釘付けできる事実を照合した。10月2〜3日、作品はおおむね Matthew の設計メモに従うコーディングエージェントによって組まれ、ソースは GitLab で公開。最初のプレイ可能ビルドは正規の ECS で、ゾンビスロットごとにエンティティ。残された負荷スクショはゾンビ974で36fps、916で8fps。約2時間10分後、パイプライン全体がカスタムインスタンシングに書き換わり、ユニットはエンティティでなくなった。10月16日午後9時11分〜17分(米東部)、RTX 4070、i7-13700KF、1600×900、`LW_LOG=1`、wave28 stage30 の五回の上限対照では、638,734ユニットでも約25fps、フレームあたり562,727インスタンスで約27MB。

本稿を貫くイメージは、四十八バイトの兵票である。データ構造名のように聞こえるが、制作倫理としては「エンジンらしい使い方」に支払うことを拒む約束だ。まずユニットをデータに戻し、何をレンダーワールドへ渡すかを決める。節約には証拠が要り、高い行には名前が要る。「Draw Call が多すぎる」という習慣的な犯人探しではない。

『LAWN WAR』60万ユニット場面、約26fps

一、業界の既定:ユニット=エンティティ、最適化=Draw Call 当て推量

Bevy の日常的な教えでは、自動インスタンシングで足りる。多くのエンティティが同じメッシュハンドルとマテリアルを共有すれば、エンジンがバッチする。数千の動く物体なら、著者自身も「自動を使い、読むのをやめろ」と言う。この既定はきれいであり、ゆえに怠惰だ。「ユニット」を「シーングラフの市民」に翻訳する。市民は毎フレーム、変換伝播・可視性・変更検出・レンダーワールドへの抽出を払う。プレイヤーも開発者も同じ語彙でカクつきを読む。「エンティティが多すぎる」「Draw Call が爆発した」「バッチが効いていない」。その合意が、「当て推量」を許される制作文法にする。

『LAWN WAR』は当て推量のコストを高くする。設計が求めるのは海だ。両軍が増え、戦場がユニットの海になるまで。歩く、攻撃する、燃える、死ぬ、爆発で宙を転がる。最初の spawn は10月2日午後2時58分(ET)のコミット:容量内の各スロットに `Mesh3d`、マテリアル、地中の `Transform`、`Visibility::Hidden`。負荷がかかると、数百ゾンビでフレームが一位数になる。著者は特定の Bevy システムに罪名を釘付けしない——その版はきちんとプロファイルしていない。重要なのはそれが強いた決定だ。2時間10分後、ユニットは市民籍を離れた。

分岐はここにある。既定は「どうすればより多くのエンティティをより少ない Draw Call に折りたためるか」と問う。彼らは「数千を超えたとき、エンティティ自体が毎フレーム税を取っていないか。GPU に届くべきは、十分に細い兵票だけではないか」と問い直した。

初版 ECS:ゾンビ974、36fps

二、脚本を砕く:Vec、四十八バイト、一回の Draw、コピーしない引き渡し

書き換え後、各軍は満容量で一度割り当てた平坦な `Vec`、要素は `Copy` 可能な普通の構造体、空きリストと高水位 `hw`。既定上限は軍あたり60万だが、序盤ウェーブは数百スロットだけ。ループはすべて `units[..hw]`。小規模戦は空の599,000に払わない。シミュレーションは rayon でそれらのスライスを並列走査。近傍は毎フレーム再構築する2mグリッド、計数ソート、ハッシュなし、セルごとの `Vec` なし、両軍同時再構築。

見えるユニットは48バイトの `Inst`:三つの `vec4`。位置+ヨー、スケール・位相・アニメ種類、ティントとグロー。完全な4×4行列もボーンもない。ユニットは鉛直軸周りだけ回り、ヨーは一つの float、回転はシェーダが組む。562,727インスタンスでリストは約27MB。25fpsなら毎秒およそ680MBがGPUへ向かう。

リストをどう速く作るかが、後のチャートが本当に語る関数だ。軍を8192チャンクで全コア並列、カメラ背後を飛ばし、距離でLOD、位相でベイクメッシュフレームを選び、メッシュごとの私有リストへ、reduce で結合し空��側は swap。カリングは一点積、完全フラスタムではない。およそ20m以遠かつカメラ背後を落とし、残りはGPUがクリップ。LODは40mと120m。ゾンビの歩行・攻撃・死亡は Kenney の CC0 墓場キット。GPUスキニングではなく、クリップを別メッシュに焼く——歩行8、攻撃4、死亡1——各フレームは自分のインスタンスリストを持つ別メッシュにすぎない。

引き渡しはもはや27MBをコピーしない。リストを `Arc` で包み、抽出はポインタをクローンするだけ。エンティティはまだあるが、メッシュごとに一つ、`NoFrustumCulling` 付き。さもなくば Bevy は原点のメッシュ境界でバッチ全体を落とす。639,000ユニット時、この段は0.08ms。GPUバッファも毎フレーム `create_buffer` しない。`write_buffer` で上書き、成長時だけ再確保、50%余白。パイプラインは Bevy 通常メッシュに `VertexStepMode::Instance` の頂点バッファを一本足す。描画はリスト全体に対する一回の `draw_indexed`。アニメは WGSL 頂点シェーダ。CPUは位相と種類だけ書き、ゾンビのよろめき、植物の揺れ、死体の平伏、破片の回転はGPUで生える。煙はさらに巧い——半透明ブレンドなし。負の「emissive」が透視を意味し、フラグメントが4×4 Bayer で一部ピクセルを捨てる。五十万の透明物を並べ替える必要は永遠にない。

ここまでで方法は揃う。データが生かす。兵票が見せる。Arc が二度目のコピーを拒む。一回の Draw が各メッシュを持つ。Bayer が煙を列に並ばせない。

三、罪名表:25fpsの誠実さ、そしてリストは戦争より高い

10月16日夜、著者は QA モードで五戦。bot が自走、`LW_LOG` を2秒ごと、GPU時間は Bevy の `RenderDiagnosticsPlugin`。同じウェーブ、同じステージ、軍上限だけ変える。各数字は最初の12秒以降、16〜21サンプルの平均。

5,568ユニット:50fps。186,000インスタンスの大半は爆発パーティクル。27,934:61fps、CPU 9.17ms、GPU 10.56。115,217:49fps、11.31 / 15.60。277,469:40fps、19.17 / 20.19。638,734:25fps(23〜28)、CPU 34.82、GPU 35.08、フレームあたり562,727インスタンス。115kの組は10月2日設計文書の「約110k、CPU 9–13、GPU 10–14」と整合し、グラフィックスバックエンドだけ異なる——きれいな sanity check。

驚きは Draw Call ではない。2.8万から63.9万(約23倍)で、`think()`——両軍シミュレーション全体——は1.13msから10.09ms、9倍未満。計数ソートグリッドが支えた。`render()`——インスタンスリストを作るだけのループ——は0.69から18.85、約27倍。満編では、リスト作りが戦争のシミュレーションよりCPUを食う。GPUは戦争とともに着実に遅くなり、最後は35.08msでCPUと並ぶ。計時のどの行も「Draw Call が多すぎる」を指さなかった。

途中で壊れたものもある。頂点色のないメッシュはパイプラインを落とす。シェーダは `@location(5) color` を読み、Bevy の球や直方体にはその属性がない。三行の `inst::white()` が白の頂点色を足す。スロット回収に魔法のHPは使えない。後期ダメージがその哨戒値を突き破り、HUDに45.8兆HPが出た。戦場ウィンドウを離れるユニットは `gone = true` を立て、死亡として数えずに空きリストへ戻る。10月2日、このPCにVulkanアダプタがなく、BevyはDX12、FXCが起動ごとに約40秒。速いDXCはパーティクルcrate `bevy_hanabi` を壊し、待ちはローディング画面に隠した。当晚の新ドライバ(617.42)はVulkanを選んだ。並列で二本のQAを走らせると両方のシェーダコンパイルが遅くなり、スクショ計時が歪む。五回の対照は一本ずつ測った。

次の賭けは正直に書いてある。未検証。チャートは Step 3 を指し、さらなるレンダリング技巧ではない。リストをフレーム間で生かし動いたユニットだけ書き換えるか、CPUリストを飛ばしてカリングとLODをcomputeに任せ、インスタンスバッファへ直書きする。Bevy 自前レンダラも0.16からGPU-drivenへ向かっている。どちらもフレームから10ms以上削れる可能性がある。

システム別CPU:リスト構築がシミュレーションを追い越す

収束:独立とは、請求書の正しい行に罪名を釘付けすること

この10月の解体を2026年秋の Rust / Bevy インディー現場に戻すと、まず打つのは隠れた制作慣性だ。ユニットはエンティティが既定、カクつきは Draw Call のせいが既定、「バッチできれば出荷可」が既定。数千を超えたとき市民税が戦争そのものより高くないか、25fpsまで落ちたとき高いのはGPUか、表を埋めるだけのCPUループか、と問う人は少ない。

小チームにとってこの材料は特に有用だ。60万の植物もRTX 4070も要らない。自分の「既定で足りる」リストはある。大量ユニットをまだ Entity で吊る。最適化会議が Draw Call から始まる。毎フレーム `create_buffer` するデモ。五十万級の半透明をまだソートしようとする。per-system span なしでボトルネックを週報に書く。Matt Busel の公開作業はコストを土臭い言葉で書く。エンティティ数が痛むまで自動インスタンシング。兵票を48バイトに痩せる。私有並列リスト、Arc 引き渡し、バッファ再利用、頂点アニメ、Bayer 煙。高いのは一日目からログを取ること、そして満編ではリストが戦争より高いと請求書に認めることだ。

実行できる手がかりは短い。

  • まず Bevy の自動インスタンシング。同じメッシュ、同じマテリアル。三角形ではなくエンティティ数が痛むときだけカスタムへ。
  • 大量オブジェクトはデータに置く。`Copy` 構造の `Vec`、空きリスト、高水位。エンティティは本当に必要な少数へ。
  • インスタンスを痩せる。位置+ヨー、4×4ではない。48バイトが五十万と向き合えば自ら語る。
  • 並列リストは私有バッファで。`par_chunks`、タスクごとメッシュごとの `Vec`、最後に結合。ロックなし。
  • `Arc` で引き渡し、メッシュごとの一つのエンティティに `NoFrustumCulling`。
  • GPUバッファを再利用。`write_buffer` と余白、成長時だけ再確保。
  • アニメは頂点シェーダへ。透明が増えたらディザ、アルファソートではない。
  • 一日目から per-system 計時。span がなければ、ほぼ確実に間違った行を最適化する。

九百七十四の市民エンティティ、二時間十分の書き換え、四十八バイトの兵票、25fps上の六十三万八千ユニット、そして `think()` より高い `render()`。ストアの文句が口を開く前に、制作者は監査基準を公開解体に書き込んだ。まずユニットをデータに戻し、罪名を正しい行に釘付けせよ。

小光

出典と関連

Matt Busel / Tensorust(2026-10-16)Bevy instancing: drawing 639,000 units with one draw call per mesh

Matt Busel / DEV(同期)同題

『LAWN WAR』ソース GitLab

  • Bevy custom_shader_instancing 例(文中の自動/手動)

Bevy 0.16 レンダ方向:Bevy 0.16

ログインして読書記録を残しましょう。

いいね・保存を別の端末でも確認できます。

ログイン