# スレッドを使う

ブラウザのゲームは、処理を移さない限り1つのスレッドで動きます。物理、大群、経路探索、パーティクル流体のような重いロジックは、[Web Worker](https://developer.mozilla.org/ja/docs/Web/API/Web_Workers_API) に任せられます。ワーカーがシミュレーションしている間も、ページは描画と入力の読み取りを全速で続けます。複数のワーカーは `SharedArrayBuffer` で1つのメモリを共有できるので、データをやり取りせずに処理を分けられます。

`SharedArrayBuffer` は、*クロスオリジン分離*されたページでしか使えません。そのためにはサーバーから2つのレスポンスヘッダーを送る必要があります。debuff は開発中にこのヘッダーを送り、ヘッダーファイルを読むホスト向けにファイルも書き出します。@debuffjp/cli 0.23.0 から使えます。

[spryte のフローデモ](/spryte/demos/flow/){target="_self"}は、川の流体をこの方法で8つのワーカーで動かしています。

## 分離を有効にする {#turn-on-isolation}

```ts
// debuff.config.ts
import { defineConfig } from "@debuffjp/cli";

export default defineConfig({
  isolation: true,
});
```

- `debuff dev` は、すべてのレスポンスで3つのヘッダーを送ります。`Cross-Origin-Opener-Policy: same-origin`、`Cross-Origin-Embedder-Policy`、`Cross-Origin-Resource-Policy: same-origin` です。
- `debuff build` は、これらを `dist/_headers` に書き出します。`public/` に自分の `_headers` がある場合は、そこに追記します。ただし、そのファイルがすでに `Cross-Origin-Embedder-Policy` を設定しているなら変更しません。
- `debuff doctor` は、`index.html` がほかのサイトから読み込むスクリプト、フレーム、スタイル、画像について警告します。
- `debuff ship itch` は、itch.io のオプションにチェックを入れるよう知らせます（後述）。

ゲームの中では次のように確認します。

```ts
const threads = globalThis.crossOriginIsolated ? Math.min(8, navigator.hardwareConcurrency - 1) : 1;
```

シングルスレッドで動く経路は必ず残してください。ページを分離しないブラウザやホストもあります。そこでも、余裕が少ないだけでゲームは動くべきです。

### 分離の代償 {#what-isolation-costs}

ページを分離すると、ほかのサイトのファイルは `Cross-Origin-Resource-Policy` か CORS のヘッダーで許可していない限り、ブラウザに拒否されます。CDN のフォントやスクリプト、アナリティクス、広告、埋め込み動画がよく影響を受けます。できるだけ自分でホストしてください。ページとやり取りする必要があるサインインのポップアップ（一部の OAuth）も動かなくなります。

`isolation: true` は `embed: "require-corp"` を使い、どのブラウザでも動きます。`embed: "credentialless"` は、ほかのサイトのファイルを拒否する代わりに Cookie なしで読み込むので、サードパーティのファイルには優しい方法です。ただし Safari はこれに対応していないため、Safari ではページが分離されません。

```ts
export default defineConfig({ isolation: { embed: "credentialless" } });
```

## ホストごとに必要なこと {#what-each-host-needs}

| ホスト | すること |
| --- | --- |
| Wavedash | 何もしなくて大丈夫です。[Wavedash はすべてのゲームを分離して動かします](https://docs.wavedash.com/platform/cross-origin-isolation)。ゲームが通信するすべてのサーバーもヘッダーを送る必要があります。 |
| Cloudflare Pages、Netlify | 何もしなくて大丈夫です。`debuff build` が書き出す `dist/_headers` を読みます。 |
| itch.io | **Embed options** の **SharedArrayBuffer support** にチェックを入れます。itch.io では実験的な機能です。ゲームが新しいドメインに移るのでプレイヤーのセーブはやり直しになり、Safari のプレイヤーにはポップアップウィンドウで表示されます。 |
| Vercel | プロジェクトの `vercel.json` に3つのヘッダーを追加します（`"headers": [{ "source": "/(.*)", "headers": [...] }]`）。 |
| GitHub Pages | 独自のヘッダーを送れません。[coi-serviceworker](https://github.com/gzuidhof/coi-serviceworker) がサービスワーカーからヘッダーを追加しますが、初回訪問時に1回リロードが入ります。 |
| 自分のサーバー | ゲーム以下のすべてのレスポンスで3つのヘッダーを送ります。 |

## ワーカーでシミュレーションする {#simulate-in-a-worker}

フローデモで使っている形を、4つに分けて説明します。

**ワーカーは固定ステップで動かします。** debuff のゲームループと同じく、シミュレーションを1秒に60回の固定間隔で進めます。1ステップが割り当て時間を超えたら、追いつくために余分なステップを回すのではなく、遅れた時間を捨てます。余分なステップを回すと次のフレームがさらに遅くなり、どんどん遅れていきます。時間を捨てれば、シミュレーションはスローモーションになっても、ページは滑らかなままです。

**ページは直近2つのステップの間を描きます。** 各パーティクルについて、最新ステップの開始時と終了時の位置を持っておき、現在のフレームが次のステップにどれだけ進んだかに応じて、その間の位置に描きます。1ステップ遅れて描くので数ミリ秒の遅延が出ますが、シミュレーションの速さにかかわらず動きが滑らかになります。シミュレーションが毎ステップでパーティクルを並べ替える場合（たとえば高速化のためにグリッドのセル順に並べる場合）は、パーティクルごとに両方の位置を送ってください。次のステップでは、インデックス `i` が同じパーティクルを指さないからです。

**受け渡しの方法を選びます。** 分離していない場合は、各ステップの配列を `postMessage` で転送します（`postMessage(data, [data.buffer])`）。こうすればコピーは発生しません。分離している場合は、ワーカーが `SharedArrayBuffer` に書き込み、ページがそれを直接読めます。ダブルバッファにして、各ステップの後に `Atomics` のカウンターを切り替えれば、書き込み途中のステップをページが読むことはありません。

**ワーカーの間隔は期限で管理します。** タイムアウト付きの `Atomics.wait` は、OS のタイマーと同じ精度しかありません。Windows では 14 ms の待機が約 30 ms になり、ステップの速さが半分になることがあります。時間に十分な余裕があるうちは短い待機を繰り返し、最後の数ミリ秒はスピンで待ちます。

```ts
deadline += 1000 / 60;
while (deadline - performance.now() > 3) Atomics.wait(pace, 0, 0, 1);
while (performance.now() < deadline) {}
```

ページのメインスレッドでは `Atomics.wait` を呼ばないでください。ブラウザがそこでの呼び出しを許可していません。

### 1つのシミュレーションを複数のワーカーに分ける {#splitting-one-simulation-across-workers}

複数のワーカーが同じパーティクルを足並みをそろえて進めることもできます。各ワーカーが一部を受け持ち、フェーズの間にバリア（共有カウンターに対する `Atomics.add`、`Atomics.wait`、`Atomics.notify`）で待ち合わせます。近くのパーティクルを押しのける処理のように、あるパーティクルがほかのパーティクルへ書き込むフェーズは、先に形を変える必要があります。各パーティクルは受け取った力を合計し、自分自身にだけ書き込むようにします。この形は行き過ぎることがあるので、力を弱め、1ステップあたり2〜3回に分けて落ち着かせます。完全な例はフローデモの[川のソース](https://github.com/debuffjp/debuff/tree/main/packages/spryte/examples/flow)にあります。30,000 個のパーティクルで、1ステップが2ワーカーで 64 ms、8ワーカーで 20 ms です。