> **本文回答**：`fx.setQuality / getQuality` + `getMetrics()` + 事件 `render / empty / overflow / compact`。

## 一、三档画质

```js
fx.setQuality('lite');        // 中低端机
fx.setQuality('standard');    // 默认
fx.setQuality('cinematic');   // 演示机 / 大屏

fx.getQuality();              // 当前档
```

| 档 | shader 宏 `QUALITY` | 含义 |
|---|---|---|
| `lite` | `1` | 简化迭代、降 octave、丢 bloom 抗锯齿 |
| `standard` | `2` | 默认平衡 |
| `cinematic` | `3` | 完整迭代、平滑边缘、最大噪声层数 |

所有 12 点 / 5 线 / Shadertoy 都在 shader 编译期读 `#if QUALITY >= 2` 这类宏分支（宏名是 **`QUALITY`**，不是 `FX_QUALITY`）→ 预设作者可根据档位写分支。`setQuality` 对非法值直接 `throw`（`lite|standard|cinematic` 之外报错）。

## 二、切换耗时

**首次切档**：用到的 program 重编译一次（< 30 ms）；
**切回旧档**：从 `programCache` 读缓存（≈ 0 ms）。

```js
console.time('lite');
fx.setQuality('lite');
console.timeEnd('lite');          // 8~30 ms 首次

console.time('lite-again');
fx.setQuality('lite');            // 无变化，直接返回
console.timeEnd('lite-again');    // < 1 ms
```

## 三、渲染分桶 bucket

```js
fx.addPoint('halo', { cad: [x, y], size: 50, bucket: 'topmost' });
```

| bucket | z-order |
|---|---|
| `below` | 底图 **下方**；适合地下管线烟雾 |
| `above`（默认） | 底图 + CAD 实体之上 |
| `topmost` | 所有图层之上；告警高亮 / 教学引导 |

默认只有 above 一个渲染层；第一次用 below / topmost 自动创建子层。批量数组里**每个元素的 bucket 独立生效**。

## 四、批量 API

| API | 用途 | 性能 |
|---|---|---|
| `addPoint(preset, opts)` | 单点 / 偶发 | 只写 CPU 影子缓冲，下一帧 draw 时合并成一次 `bufferSubData` |
| `addPoints(preset, list)` | 一次加多点 | 内部循环 `addPoint`，按 bucket 分组——与同步循环 `addPoint` 性能相当，胜在写法简洁 |
| `replaceAllPoints(preset, list)` | 周期整数据替换 | 清空该预设**默认 `above` 桶**后 `addPoints`；忽略各项 `bucket`、不处理 `triggerOnce`/`strikeEvery`，并使该预设旧句柄（含 `bindToEntity`）失效 |
| `setPointData(handle, patch)` | 改单点色 / 大小 / 位置 | 增量写 |

```js
// 1 万火焰
const handles = fx.addPoints('fire', list);

// 周期刷新
fx.replaceAllPoints('fire', newList);

// 单点改色
fx.setPointData(handles[0], { color: [1, 0, 0, 1] });
```

## 五、事件 `fx.on`

| 事件 | payload | 何时 fire |
|---|---|---|
| `render` | `(activeCount)` | 每一帧且 ≥ 1 个实例存活 |
| `empty` | `()` | active 从 N 降到 0 时**一次** |
| `overflow` | `({ kind, preset, capacity })` | GPU 容量自动扩容时 |
| `compact` | `({ preset, result })` | 闲置槽位整理完成 |

```js
fx.on('render', active => hud.active = active);
fx.on('empty', () => console.log('全部清空'));
fx.on('overflow', ({ kind, preset, capacity }) =>
  console.warn(`扩容 ${kind}/${preset} → ${capacity}`));
fx.on('compact', ({ preset, result }) =>
  console.log(`compacted ${preset}, moved=${result?.remap?.size ?? 0}`));
```

- `render` 不在 active=0 的帧 fire → 适合 FPS 采样
- `empty` 是**边沿**事件 → 适合 "清场后" 逻辑
- `overflow` / `compact` 都是**良性**提示

## 六、`getMetrics()` 实时指标

```js
const arr = fx.getMetrics();
// [
//   { bucket: 'above',   active: 5234, points: [...], lines: [...], shadertoys: [...] },
//   { bucket: 'topmost', active:   12, ... }
// ]
```

| 字段 | 说明 |
|---|---|
| `bucket` | 'below' / 'above' / 'topmost' |
| `active` | 该桶活跃 FX 实例总数 |
| `points` | `{ preset, live, capacity }[]` |
| `lines` | `{ preset, live }[]` |
| `shadertoys` | `{ programKey, live, capacity }[]` |

调用成本 O(unique presets)；随便 setInterval 250ms 也无压力。

## 七、事件仪表盘工具

```js
function attachDashboard(fx) {
  const s = { fps: 0, frameMs: 0, peakMs: 0, renderFrames: 0, emptyFires: 0, overflowFires: 0, compactFires: 0 };
  let last = 0; const win = [];
  const onRender = () => {
    s.renderFrames++;
    const now = performance.now();
    if (last) {
      const dt = now - last;
      win.push(dt); if (win.length > 60) win.shift();
      s.frameMs = win.reduce((a, b) => a + b, 0) / win.length;
      s.peakMs = Math.max(s.peakMs, dt);
      s.fps = 1000 / s.frameMs;
    }
    last = now;
  };
  fx.on('render', onRender);
  fx.on('empty', () => { s.emptyFires++; last = 0; win.length = 0; });
  fx.on('overflow', () => s.overflowFires++);
  fx.on('compact', () => s.compactFires++);
  return s;
}

const stats = attachDashboard(fx);
setInterval(() => console.log(stats), 1000);
```

## 八、调优套餐

### 卡（< 30 FPS）

1. `fx.setQuality('lite')` → 常见 +20% FPS
2. 查 `getMetrics` 哪个 preset live 最大 → 瓶颈
3. 同区域重叠多个 cinematic Shadertoy → 换预设
4. Symbol 图标：`fx.symbolImage.addPreset` 时 `fps: 24~30`

### 海量点

1. `addPoints` 与同步循环 `addPoint` 都是每帧一次合并上传，差别不大；瓶颈通常在实例数与 shader 档位
2. 周期刷新走 `replaceAllPoints`（仅限 `above` 桶、且该预设没有 `bindToEntity` 绑定时）
3. 海量同形点 → `fx.symbolImage` 性能与点数解耦
4. 上万点还要 60 FPS → 聚合 (cluster) + symbolImage

### 偶发卡顿

1. `overflow` 频繁 → 反复 add/remove 改为 `setPointData({ visible: false })` 重用
2. `compact` 触发 → 自动整理，handle 自动 remap，无需手动处理
3. 切 quality 首次 30ms 编译 → 缓存后 0ms

## 九、压测参考（中端集成显卡）

| 预设 / 数量 | 1k | 5k | 10k | 20k | 50k |
|---|---|---|---|---|---|
| pulseRing | 60 | 60 | 58 | 50 | 35 |
| fire | 60 | 60 | 55 | 45 | 28 |
| halo | 60 | 60 | 60 | 55 | 40 |
| radarSweep | 60 | 60 | 55 | 42 | 25 |
| meteor | 60 | 60 | 55 | 40 | 22 |
| lightning | 60 | 60 | 50 | 35 | 18 |

数字仅供参考；实际看 GPU / 分辨率 / `size` 屏占比。

## 十、常见坑

1. **setQuality 每帧切**：没必要；程序生命周期里通常只切 1~2 次。
2. **cinematic 开全**：中低端机撑不住；给次要特效 lite。
3. **bucket topmost 滥用**：所有 FX 都 topmost → z-order 混乱，UI 遮挡。
4. **forgot to off fx events**：组件卸载时必须 `fx.off`。
5. **metrics 拿来做 UI 高频刷新**：250ms 够了；别 16ms。

## 相关

- FX 总览 → [fx-overview](fx-overview.md)
- 预设 → [fx-presets](fx-presets.md)
- 大量点 → [large-markers](../../../vjmap/references/perf/large-markers.md)
