# 安装与运行指南

从零搭建环境、构建并在浏览器中运行本项目的完整步骤，以及国内网络 / 代理环境下的常见问题。

## 1. 环境要求

| 工具 | 用途 | 已验证版本 |
|---|---|---|
| Rust（rustup 安装） | 编译 Rust 源码 | stable 1.98.1 |
| `wasm32-unknown-unknown` target | Rust 编译到 WebAssembly 的目标平台 | 与 rustc 同版本 |
| wasm-pack | 一键编译并生成 JS 胶水代码到 `pkg/` | 0.15.0 |
| wasm-bindgen CLI | 由 wasm-pack 调用，生成 JS 绑定；**版本必须与 `Cargo.lock` 一致** | 0.2.126 |
| wasm-opt（binaryen） | 由 wasm-pack 调用，压缩 `.wasm` 体积（可选） | version_133 |
| Python 3 | 本地静态文件服务器（也可换成任意静态服务器） | 3.14 |
| nightly Rust + `rust-src`（可选） | 只用于多线程光线追踪 `pathtracer-mt`，见 [3.1](#31-多线程光线追踪可选) | nightly 1.100.0（2026-09-24） |
| 浏览器 | 运行 Demo | Chrome / Edge 113+（WebGPU Demo 需要） |

## 2. 安装步骤

### 2.1 安装 Rust

```bash
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
rustc -V
```

### 2.2 添加 wasm32 编译目标

```bash
rustup target add wasm32-unknown-unknown
rustup target list --installed    # 输出应包含 wasm32-unknown-unknown
```

> 失败请看 [问题 1](#问题-1rustup-target-add-报-404--tls-handshake-eof)。

### 2.3 安装 wasm-pack

```bash
cargo install wasm-pack
wasm-pack --version
```

### 2.4 预装 wasm-bindgen 与 wasm-opt（强烈建议）

wasm-pack 首次构建时会从 GitHub 自动下载这两个工具。**在需要代理的网络中，它的内置下载器不走 `HTTPS_PROXY`，会无报错地一直卡住**。提前手动装好，wasm-pack 就会直接使用 PATH 中的版本。

**wasm-bindgen**（版本必须与项目锁定的版本完全一致）：

```bash
# 查看项目锁定的版本
grep -A1 'name = "wasm-bindgen"$' Cargo.lock      # → version = "0.2.126"

# 方式 A：从源码安装（走 crates.io 镜像，耗时几分钟）
cargo install wasm-bindgen-cli --version 0.2.126

# 方式 B：下载预编译包（curl 会使用代理）
V=0.2.126
curl -fLO https://github.com/wasm-bindgen/wasm-bindgen/releases/download/$V/wasm-bindgen-$V-x86_64-unknown-linux-musl.tar.gz
tar xzf wasm-bindgen-$V-x86_64-unknown-linux-musl.tar.gz
cp wasm-bindgen-$V-x86_64-unknown-linux-musl/wasm-bindgen ${CARGO_HOME:-~/.cargo}/bin/

wasm-bindgen --version    # → wasm-bindgen 0.2.126
```

**wasm-opt**（任选其一）：

```bash
sudo dnf install binaryen        # Fedora
sudo apt install binaryen        # Debian / Ubuntu
brew install binaryen            # macOS

# 或下载预编译包（静态链接，可直接复制使用）
V=version_133
curl -fLO https://github.com/WebAssembly/binaryen/releases/download/$V/binaryen-$V-x86_64-linux.tar.gz
tar xzf binaryen-$V-x86_64-linux.tar.gz
cp binaryen-$V/bin/wasm-opt ${CARGO_HOME:-~/.cargo}/bin/

wasm-opt --version
```

## 3. 构建与运行

在项目根目录执行：

```bash
python3 scripts/build.py          # 编译全部演示 → pkg/<演示>/
python3 scripts/serve.py 8080     # 本地服务器：Cache-Control: no-cache + COOP/COEP
```

浏览器打开 <http://localhost:8080/www/>，首页按类别列出全部演示。各 Demo 的操作说明见 [HELP.md](../HELP.md)。

每个演示是 `crates/<类别>/<演示>/` 下一个独立的 crate，`build.py` 对它们逐个调用 `wasm-pack`。成功时每个演示输出一行：

```
✓ mandelbrot       17.0 KB  (0.3s)
✓ mandelbulb       25.4 KB  (0.3s)
…
– pathtracer-mt  skipped: rust-src is missing for nightly (rustup component add rust-src --toolchain nightly)
```

产物在 `pkg/<演示>/index.js` + `index_bg.wasm`，页面从 `../../pkg/<演示>/index.js` 导入。该目录已加入 `.gitignore`。只想重新编译某几个演示时，把名字写在后面：`python3 scripts/build.py mandelbrot fluid`。

两个特殊的构建由各自 `Cargo.toml` 里的 `[package.metadata.webtt]` 声明，`build.py` 会自动处理：

- **音频合成器 `synth`**：`raw = true`，不经过 wasm-bindgen，直接 `cargo build` 后复制为 `pkg/synth/index.wasm`（AudioWorklet 里无法加载 JS 胶水代码），装了 wasm-opt 时同样会压缩。
- **多线程光线追踪 `pathtracer-mt`**：同一个 crate 的第二个构建，需要 nightly 工具链。缺少条件时构建全部演示会跳过它并给出提示（上面最后一行），页面自动使用单线程版；单独指定 `build.py pathtracer-mt` 时则直接报错。

`serve.py` 还会返回 `Cross-Origin-Opener-Policy: same-origin` 和 `Cross-Origin-Embedder-Policy: require-corp`，让页面处于「跨源隔离」状态，这是使用 `SharedArrayBuffer`（WASM 多线程）的前提。所有资源都是同源的，这两个响应头不影响其他演示。

### 修改代码后

- 改了 `crates/**/*.rs`：重新运行 `python3 scripts/build.py <演示>`，然后刷新浏览器
- 只改了 `www/` 下的 HTML/JS/CSS：直接刷新浏览器（必要时 Ctrl+Shift+R 强制刷新）

### 3.1 多线程光线追踪（可选）

WASM 多线程需要用 nightly 工具链带 atomics 特性重新编译标准库（`-Z build-std`），因此要装 nightly 和它的标准库源码：

```bash
rustup toolchain install nightly
rustup component add rust-src --toolchain nightly
python3 scripts/build.py pathtracer-mt       # → pkg/pathtracer-mt/
```

之后刷新光线追踪页面，「线程」下拉框里的多线程选项就可以选了。多线程版单独使用 `target/pathtracer-mt/` 作为编译目录，不会让其他演示的编译缓存失效。部署到不能设置响应头的静态托管（如 Gitee Pages）时，页面不是跨源隔离的，会自动退回单线程。

### 3.2 MCP 调试台（可选，需要本地服务）

MCP 调试台要通过本地桥接服务连接 MCP 服务端和命令行工具：

```bash
cp mcp.config.example.json mcp.config.json      # 本地配置，已加入 .gitignore
python3 scripts/serve.py 8080 --mcp mcp.config.json
```

然后在**本机**浏览器打开 `http://localhost:8080/www/mcp/`。桥接服务只接受本机通过 localhost 发来、带有令牌 Cookie 的请求，手机或其他电脑访问 `/www/mcp/` 会看到「拒绝了请求」的提示；其余演示照常可以在局域网访问。

配置示例见 `mcp.config.example.json`（其中 `_examples` 字段演示了接入 Go / Rust / Zig 程序和远程 HTTP 服务的写法，不会被加载）。自带四个功能相同的 stdio 示例服务端（Python / Rust / Go / Zig），都只用各自语言的标准库、不依赖 MCP SDK，可以用来试用，也可以作为用这几种语言编写服务端的参考：

- `scripts/mcp_demo_server.py`：纯 Python，配置里的 `demo`，直接可用；
- `crates/tools/mcp/examples/demo_server.rs`：纯 Rust（只用标准库和本仓库的 JSON 模块，不依赖 MCP SDK），配置里的 `demo-rust`。先编译一次：`cargo build --release -p mcp --example demo_server`，生成 `target/release/examples/demo_server`；单元测试：`cargo test -p mcp --example demo_server`；
- `examples/mcp-servers/go/`：Go（1.22 及以上），配置里的 `demo-go`。编译：`go -C examples/mcp-servers/go build -o ../../../target/mcp-demo/demo-go .`；单元测试：`go -C examples/mcp-servers/go test .`；
- `examples/mcp-servers/zig/demo_server.zig`：Zig（按 0.17 的 `std.Io` 接口编写，更早的版本编译不过），配置里的 `demo-zig`。编译：`zig build-exe -O ReleaseSafe examples/mcp-servers/zig/demo_server.zig -femit-bin=target/mcp-demo/demo-zig`；单元测试：`zig test examples/mcp-servers/zig/demo_server.zig`。

四个服务端都由桥接服务在第一次请求时自动启动。没编译的服务端连接时会报「启动失败」，不影响其他服务端。`scripts/test_mcp_bridge.py` 会把同一串请求分别发给 Python 版和已编译的其他版本，逐条比对回复。桥接服务的测试：`python3 -m unittest scripts/test_mcp_bridge.py`。

页面只用一条事件流接收所有服务端的推送（每条事件由桥接服务标明来自哪个服务端），所以同时连接多少个服务端都可以。浏览器对同一网址最多只开 6 个连接，每个打开的调试台标签页各占一个，所以不要同时开 6 个以上的调试台标签页。

**受限 shell**：想在页面上直接输入命令时，可以在配置里加 `shell` 服务端。`mcp.config.example.json` 的 `_examples` 里有两个示例，挪到 `servers` 里就能用：免确认、只放只读命令的 `shell`，和只放 `git`、每条都要确认的 `shell-git`。格式：

```json
{"id": "shell", "name": "受限 shell", "shell": {"allow": ["ls", "cat", "git"], "cwd": ".", "timeout_s": 30}}
```

- 页面上只有一个「运行命令」工具，输入一行命令（可选填标准输入）；
- 命令按 shell 的规则拆成参数，但**不经过 shell**：管道、重定向、通配符、变量和 `$(...)` 都作为普通文字传给程序；
- 程序必须是 `allow` 里某一项在 PATH 上对应的**那个文件**（写全路径也可以），同名的其他文件（比如 `./ls`）不算；
- 每条命令都会在**运行 serve.py 的终端**里显示目录、完整命令和标准输入，输入 `y` 才执行；回车、其他输入或 120 秒没有回答都算拒绝。所以 serve.py 要在前台终端运行，放到后台或没有终端时一律拒绝；
- 同一时间只等一条确认：前一条还没回答时，新的命令会直接被拒绝；超过 2000 个字符的命令也直接拒绝（整条命令必须能在终端里看全）；标准输入只显示前 300 个字符，更长时会提示还有多少没显示；
- 确认期间如果程序文件被替换或移走，即使已经输入 `y` 也不会运行；
- **不要把解释器放进白名单**（`python3`、`bash`、`node` 等）：它们能执行 `-c` 参数或标准输入里的任意代码，等于放开了全部命令；
- 白名单只管「哪个程序」，不管参数：`git` 仍会执行仓库里的钩子，`find` 仍有 `-exec`，所以终端确认才是真正的防线。可以设 `"confirm": false` 关掉确认，但那样页面就能直接用白名单里程序的全部能力，请只在清楚后果时使用。

测试：`python3 -m unittest scripts/test_mcp_shell.py`。

### 3.3 重新训练手写识别模型（可选）

`crates/games/digits/src/weights.bin` 是训练好的 int8 权重，已提交到仓库，平时不需要重新训练。想自己训练时：

```bash
mkdir mnist && cd mnist
for f in train-images-idx3-ubyte train-labels-idx1-ubyte t10k-images-idx3-ubyte t10k-labels-idx1-ubyte; do
    curl -fLO https://storage.googleapis.com/cvdf-datasets/mnist/$f.gz
done
gunzip *.gz && cd ..
cargo run --release -p digits --example train -- mnist 30    # 30 轮约 70 秒，覆盖 weights.bin
MNIST_DIR=mnist cargo test -p digits --release -- --ignored   # 用 1 万张测试图检查准确率（≥ 97%）
python3 scripts/build.py digits
```

训练是确定性的：同样的数据和轮数得到完全相同的权重文件。数据集不要提交到仓库。

### 3.4 用 pm2 在本机常驻运行（可选）

想让开发服务器（连同 MCP 桥接）一直在后台运行、崩溃自动重启、开机自动启动，可以交给 pm2。仓库根目录的 `ecosystem.config.cjs` 已经配好（需要本机已安装 pm2）：

```bash
python3 scripts/build.py                   # 先构建 wasm；MCP 示例服务端按 §3.2 编译
pm2 start ecosystem.config.cjs             # 启动，应用名 webtt，端口 8289，带 --mcp mcp.config.json --compute
pm2 logs webtt                             # 看日志（启动时会打印加载的配置和每个受限 shell 是否需要确认）
pm2 save && pm2 startup                    # 开机自启：pm2 startup 会打印一条要用 sudo 执行的命令
```

- 先停掉手动运行在同一端口上的 `serve.py`，否则端口被占用。换端口或指定 Python：`WEBTT_PORT=9000 WEBTT_PYTHON=/path/to/python3 pm2 start ecosystem.config.cjs`；
- Python 路径在执行 `pm2 start` 时解析成绝对路径并由 `pm2 save` 保存，开机时不依赖 PATH（pyenv 的 shim 在开机时可能不在 PATH 里）。换了 Python 版本后重新 `pm2 delete webtt && pm2 start ecosystem.config.cjs && pm2 save`；
- **什么时候要重启**：重新构建 wasm、改页面，刷新浏览器即可；改 `mcp.config.json`、重新编译 MCP 示例服务端、改桥接服务代码，执行 `pm2 restart webtt`；单个 MCP 服务端出问题，在调试台上点「重启」即可；
- pm2 下没有终端，需要终端确认的受限 shell（`"confirm": true`）会拒绝所有命令，常驻运行时只配置 `"confirm": false` 的 shell；
- 崩溃会自动重启（连续崩溃时逐渐拉长间隔，最多 10 次），停止时 pm2 发 SIGINT，serve.py 会先关掉所有 MCP 子进程；
- 日志长期运行会变大，可以装 pm2 自带的轮转模块：`pm2 install pm2-logrotate`；
- 停止 / 移除：`pm2 stop webtt`、`pm2 delete webtt`（再 `pm2 save` 取消开机自启）。

测试：`python3 -m unittest scripts/test_ecosystem.py`。

### 3.5 通过域名远程访问 MCP 调试台（可选）

默认只有这台电脑上的浏览器能用调试台。要通过域名（比如 frp 转发到本机端口）从别处访问，在 `mcp.config.json` 顶层加一节 `remote`，并用环境变量设置密码：

```json
"remote": {"hosts": ["mcp.example.com"], "session_hours": 12, "trust_localhost": false}
```

```bash
export WEBTT_MCP_PASSWORD='至少 16 个字符的随机密码'   # 不要写进配置文件或 ecosystem.config.cjs
pm2 start ecosystem.config.cjs --update-env          # 或 python3 scripts/serve.py 8289 --mcp mcp.config.json
```

- `hosts` 是浏览器地址栏里的域名，带端口的写成 `mcp.example.com:8443`。配了 `remote` 却没设密码，或密码不足 16 个字符，serve.py 拒绝启动；
- 通过这些域名打开调试台会先显示登录框，登录后才能使用。会话 Cookie 是 `HttpOnly`、`SameSite=Strict`、`Secure`，会话只保存在内存里，重启 serve.py 后要重新登录；
- **必须走 HTTPS**：`Secure` Cookie 在 http 下不会被浏览器发送。frp 可以用 `https` 类型的代理，或在前面加一层 TLS。只能用 http 时设 `"insecure_http": true`，这时密码明文传输，启动时会打印警告；
- 连续输错 5 次锁定 1 分钟，之后每错一次锁定时间翻倍，最长 1 小时。经过 frp 转发后所有请求都来自本机，只能全局锁定：别人乱试密码时你也要等，在这台电脑上用 `localhost` 打开不受影响；
- 登录成功、失败、锁定都会写进日志（`pm2 logs webtt`），带 frp 转发的客户端地址；
- **受限 shell 默认不对远程开放**，要在那个服务端上写 `"remote": true`。其他服务端默认开放，不想开放的写 `"remote": false`。启动日志会列出远程可用的服务端；
- **配了 `remote` 之后，本机 localhost 也要登录**，并且和远程一样只能用远程可用的服务端。原因：经过 frp 转发的请求都来自本机，如果代理把 Host 改写成 localhost，程序无法区分“你在本机”和“外网请求”，所以不能再凭 Host 免登录。确认前面的代理不会改写 Host 时，可以设 `"trust_localhost": true` 恢复本机免登录、可用全部服务端（带 `X-Forwarded-For` 等转发头的请求仍不算本机）。没有 `remote` 一节时，本机访问和以前完全一样；
- 所有页面都带 `X-Frame-Options: DENY`，别的网站不能把调试台嵌进框架里诱导点击；连接 60 秒没有数据就断开。

frp 配置示例（frpc.toml，frp 0.52 及以上）：

```toml
[[proxies]]
name = "webtt"
type = "https"
customDomains = ["mcp.example.com"]
[proxies.plugin]
type = "https2http"
localAddr = "127.0.0.1:8289"
crtPath = "./mcp.example.com.crt"
keyPath = "./mcp.example.com.key"
hostHeaderRewrite = "mcp.example.com"   # Host 必须保持为 remote.hosts 里的域名，不要改写成 localhost
```

（以上按 frp 新版 toml 格式写，字段名请以你所用 frp 版本的文档为准。）

安全清单：
- 密码用随机生成的长串（例如 `python3 -c "import secrets; print(secrets.token_urlsafe(24))"`），不要和别的账号共用；
- pm2 会把启动时的环境变量（包括密码）保存在 `~/.pm2`，执行 `chmod 700 ~/.pm2`；
- 只把确实需要远程用的服务端开放给远程，受限 shell 的白名单保持最小；
- 定期看 `pm2 logs webtt` 里的 `[mcp remote]` 行。

测试：`python3 -m unittest scripts/test_mcp_remote.py`。

### 3.6 浏览器 vs 服务端（可选，需要本地服务）

「浏览器 vs 服务端」页面要调用服务端程序 `compute`（示例 crate 编译成的原生程序）：

```bash
cargo build --release -p compute                                # → target/release/compute
python3 scripts/serve.py 8080 --mcp mcp.config.json --compute   # --compute 需要和 --mcp 一起用
```

- `--compute` 沿用 MCP 桥接的访问控制（本机令牌、通过域名访问要登录），所以必须同时带 `--mcp`；配置里可以一个服务端都不写：`{"servers": []}`；
- 最多同时运行 2 个计算，超出返回 429；基准最长 60 秒、渲染最长 120 秒；数据最大 256 MB，渲染最大 1280×720、每像素 256 次采样，线程数不超过 CPU 核心数；
- 修改了 `sha256`、`compress`、`sudoku`、`pathtracer` 这几个 crate 后，要同时重新构建 wasm（`python3 scripts/build.py …`）和服务端程序（`cargo build --release -p compute`），两边才是同一份代码，页面的「结果一致」检查会发现不一致。

测试：`cargo test -p compute`、`python3 -m unittest scripts/test_compute_bridge.py`。

## 4. 测试

```bash
cargo test --workspace -- --test-threads=1    # 所有 crate 的单元测试
cargo test -p pathfind -- --test-threads=1       # 只测一个演示
cargo test -p pathtracer --features threads -- --test-threads=1   # 光线追踪的多线程代码路径（本机 rayon 线程）
```

SIMD 函数只在 wasm32 上编译，本机测试只覆盖标量版本；两者结果是否一致由页面在浏览器里逐项核对。

必须加 `--test-threads=1`：数据表格、生命游戏、粒子物理、迷宫寻路、流体模拟、文件哈希、落沙、五子棋、光栅化、光线追踪、SIMD、合成器、CHIP-8、智能缩放、波函数坍缩、布料、三角剖分这些 crate 使用全局 `static Mutex` 状态，并行运行时测试会互相干扰。

页面脚手架脚本的测试：`python3 -m unittest scripts/test_new_demo.py`。

## 5. 用手机访问

`scripts/serve.py` 监听所有网卡，手机和电脑连同一个 Wi-Fi 后，在手机浏览器里打开 `http://<电脑的局域网 IP>:8080/www/` 即可。

- **最低版本**：iOS 15+ Safari、Android Chrome 89+（包括微信内置浏览器）。页面使用了 ES 模块的顶层 `await`，更老的浏览器无法加载。
- **触屏操作**：曼德博集合、曼德博球支持双指缩放；流体、地形可以直接拖动；手机上会自动使用更轻的默认设置（流体 64×64 网格、隐藏 256 MB 哈希测试数据），曼德博球和地形在拖动时先用半分辨率预览。
- **安全上下文**：摄像头、麦克风和 AudioWorklet 只在安全上下文中可用。通过局域网 IP 访问是 `http://`，不算安全上下文，所以「实时视频滤镜」「音频频谱」的麦克风模式和「音频合成器」需要配置 HTTPS（例如用反向代理或自签证书）；同理，局域网 `http://` 访问时页面也不是跨源隔离的，光线追踪只能用单线程。其余示例不受影响。
- **SIMD 加速对比**：需要 iOS 16.4+ / Android Chrome 91+，更老的系统会显示不支持。

## 6. 常见问题

### 问题 1：`rustup target add` 报 404 / tls handshake eof

**现象**：报错中的下载地址是某个镜像（如 `mirrors.tuna.tsinghua.edu.cn`），即使临时设置 `RUSTUP_DIST_SERVER` 为其他镜像，地址也不变。

**原因**：已安装 toolchain 的清单文件 `$RUSTUP_HOME/toolchains/<toolchain>/lib/rustlib/multirust-channel-manifest.toml` 中写死了安装时所用镜像的完整 URL，`target add` 直接使用这些地址。镜像不可达，或已删除旧日期的包（404）时，就会失败。

**解决**：换一个可用的源，重新拉取清单后再添加 target：

```bash
export RUSTUP_DIST_SERVER=https://static.rust-lang.org   # 或其他可用镜像
rustup update stable
rustup target add wasm32-unknown-unknown
```

如果 shell 配置文件（`~/.bashrc` 等）中长期设置了一个已失效的 `RUSTUP_DIST_SERVER`，建议一并修改。

### 问题 2：`wasm-pack build` 卡住不动，没有任何报错

**现象**：输出停在以下某一行：

```
[INFO]: ⬇️  Installing wasm-bindgen...
[INFO]: Optimizing wasm binaries with `wasm-opt`...    ← 或停在这一行之前
```

**原因**：wasm-pack 正在从 GitHub 下载 wasm-bindgen 或 wasm-opt，但其内置下载器不走代理。

**解决**：按 [2.4](#24-预装-wasm-bindgen-与-wasm-opt强烈建议) 手动安装这两个工具。临时绕过时，也可以在 `Cargo.toml` 末尾关闭 wasm-opt（生成的 `.wasm` 会大一些）：

```toml
[package.metadata.wasm-pack.profile.release]
wasm-opt = false
```

排查时可以加 `--verbose` 查看卡在哪一步：`wasm-pack --verbose build --target web`。

### 问题 3：升级依赖后又卡住了

`wasm-bindgen` 依赖升级后（`Cargo.lock` 中的版本变化），PATH 中旧版本的 CLI 不再匹配，wasm-pack 会重新尝试下载。按 2.4 安装对应的新版本即可。

### 问题 4：`cargo run` 报 `a bin target must be available`

这是正常现象。本项目是 `cdylib` 库，只能编译为 `.wasm` 供浏览器加载，没有可执行入口。请使用第 3 节的构建命令。

### 问题 5：页面空白，控制台报 404 或模块加载失败

- 确认已执行 `python3 scripts/build.py`，且 `pkg/<演示>/` 目录存在
- 确认 HTTP 服务器是在**仓库根目录**启动的，而不是在 `www/` 下
- 不能直接双击打开 HTML 文件（`file://` 协议无法加载 ES module 和 WASM）

### 问题 6：`does not provide an export named 'xxx'`

**现象**：控制台报 `Uncaught SyntaxError: The requested module '../../pkg/benchmark/index.js' does not provide an export named 'bench_fib'`（或其他函数名）。

**原因**：浏览器用了缓存里旧版本的 `pkg/<演示>/index.js`。重新构建新增了导出函数，但 `python3 -m http.server` 不发送 `Cache-Control` 头，浏览器会按启发式规则继续使用旧文件。

**解决**：按 `Ctrl+Shift+R`（macOS 上是 `Cmd+Shift+R`）强制刷新。以后改用 `python3 scripts/serve.py 8080` 启动服务器，它会给每个响应加上 `Cache-Control: no-cache`，每次重新构建后普通刷新就能生效。如果刷新后仍然报错，确认已经运行过 `python3 scripts/build.py`。

### 问题 7：WebGPU / 摄像头 Demo 无法使用

- **WebGPU N-body**：需要 Chrome / Edge 113+；Linux 上可能要在 `chrome://flags` 中开启 WebGPU
- **Video Filter**：摄像头需要安全上下文，`http://localhost` 满足条件；用局域网 IP 访问时需要 HTTPS

### 问题 8：光线追踪的多线程选项是灰色的

选项后面的括号会写明原因：

- **需要跨源隔离**：服务器没有返回 COOP/COEP 响应头。用 `python3 scripts/serve.py` 启动，或在你的服务器上加这两个头（见第 3 节）。
- **未构建 pathtracer-mt**：按 [3.1](#31-多线程光线追踪可选) 安装 nightly 和 `rust-src` 后运行 `python3 scripts/build.py pathtracer-mt`。

注意：对一个不存在的工具链执行 `rustup component list --toolchain nightly`、`cargo +nightly …` 等命令时，rustup 会自动把它装上。只想查看有哪些工具链时用 `rustup toolchain list`。
