> Documentation build: development; package version 2.0.0-alpha.8; source 03c7dc1127befb7a1c674804972a1977d36d3c96. Match installed declarations before using an API.

# Hilo3d 现代前端工程改造记录

> **Archived design snapshot — 2026-09-20, source commit `6433334c`.** Historical proposals and
> validation records below are not current instructions or release evidence. Superseded by
> [ENGINEERING](../../documentation/ENGINEERING.md) and the [current roadmap](../../documentation/ROADMAP.md). Do not restore retired
> APIs, legacy renderers, or execute proposed commands from this snapshot.

状态：已完成 · 目标版本：2.0.0 · 最后核验：2026-07-26

范围：源码、示例、测试、构建、类型声明、API 文档、站点、npm 包、CI 与发布流程

> RHI 与 renderer 架构部分已由 [`RENDERING_ARCHITECTURE.md`](../../documentation/RENDERING_ARCHITECTURE.md)
> 取代。旧 feature driver、旧 RHI
> wrapper 与同提交 A/B 基准已经删除；后续性能基线以当前共享 Renderer/RHI 实现的冻结快照为准。

## 结论

本仓库已经从“JavaScript 文件改名为 TypeScript、但主体仍按旧工程运行”的过渡状态，改造成一套单一、严格、可复现的现代前端库工程。

现在的工程事实是：

- 所有一方维护的引擎源码、示例、测试和 Node 工具均为受严格检查的 TypeScript；`src/` 与 `examples/`
  中不再存在一方 JavaScript 实现，`npm run check:modernity`
  会阻止旧 JavaScript 或旧构建配置重新进入受维护目录。
- 源码与发布物统一使用原生 ESM、原生 `class`、显式领域类型和标准浏览器 API；旧动态类/mixin
  API 与 CommonJS/UMD 输出已完全删除。
- 渲染器提供显式选择的 WebGL 2 与 WebGPU 双后端。可移植 raster shader 以 GLSL ES
  3.00 为唯一源码，并严格执行“引擎预处理 → Vulkan GLSL 4.50 → Naga WASM → WGSL”。WebGPU-only
  compute 通过 `ComputeShader` 使用受控 Direct WGSL；storage-aware raster 使用受控 GLSL ES 3.10
  readonly-std430 → Naga。不存在手写 graphics WGSL 镜像、WebGL 1、GLSL 1.00 或逐项 numeric
  uniform 路径。
- 渲染代码统一位于 `src/render`，公开入口由
  `Renderer`/`RenderTarget`、SRP 与受控 compute/storage 对象组成，后端实现收口于
  `src/render/rhi/backends/webgl2` 与 `src/render/rhi/backends/webgpu`；`UniformBuffer` 与 frame
  planning 保持后端中立，WebGPU 通过同步 `renderFrame()`
  合并资源已就绪的多 pass，并以 submission 内 UBO
  revision 快照保证阴影与多相机数据不会互相覆盖。Shader variant 使用有界结构化 hash 与精确碰撞校验。
- Vite 负责库与多页面示例构建，Vitest Browser
  Mode 在真实 Chromium 环境中运行单元测试；Playwright 从示例目录自动收集 96 个 HTML：81 个执行 WebGL
  2 + WebGPU，WebXR 明确 WebGL
  2-only，14 个 compute、Clustered、GI、时序与粒子页面（包括 Bloom）明确 WebGPU-only，并对适用后端执行确定性视觉、交互、后处理与拾取门禁；真实 WebGPU
  adapter/device/pipeline fixture 作为额外的深度验收，而不是 WebGPU 唯一覆盖。
- 类型声明、TypeDoc API 页面和 API Extractor 签名报告全部从同一份已检查源码生成。
- npm 发布物按真实 tarball 校验，而不是只检查仓库内文件；功能提交 push 后由 CI 执行完整门禁，候选版本也可按需使用
  `npm run validate`（或等价别名 `npm run release:check`）执行完整双后端浏览器矩阵。npm
  tag 发布 workflow 和 npm 生命周期只执行快速、确定性的发布检查，再由 `prepack`
  构建 tarball，避免重复运行功能 CI。默认 hosted
  CI 把等价的 portable 门禁拆成预检、coverage、RHI、包/API/文档和六个隔离的 WebGL 2
  presentation/UI/视觉工作组；WebGPU native/offscreen RHI 保持独立进程，non-evidence portable
  benchmark smoke 则由按性能路径、定时或手动触发的独立工作流执行。
- 旧 Gulp、Webpack、Babel、Mocha、JSDoc、手写声明、旧 `build/`、已提交的旧
  `docs/`、旧测试页、运行时 vendor 脚本和远程 CDN 依赖已经退出活跃工程。

本次改造没有使用 `@ts-nocheck`、`@ts-ignore`、`@ts-expect-error`、显式
`any`、目录级 lint 逃逸、空视觉断言、覆盖率排除核心目录或捕获错误后继续成功等临时手段。

## 改造结果

| 领域     | 改造前                                                  | 当前实现                                                                |
| -------- | ------------------------------------------------------- | ----------------------------------------------------------------------- |
| 语言     | 大量 `.ts` 仅透传旧 JavaScript，主体使用 `Class.create` | 全部一方代码严格 TypeScript；对象模型统一为原生 class 与 ESM            |
| 类型检查 | 单一配置混合浏览器、测试与 Node 环境                    | `base/lib/test/examples/node` project references，严格规则全覆盖        |
| 静态质量 | lint 文件白名单，无统一 formatter                       | typed ESLint flat config 覆盖整仓，Prettier 与 EditorConfig 固化格式    |
| 库构建   | Gulp、Webpack、Babel 和历史 polyfill                    | Vite 8 + Rolldown，ES2022 ESM、Naga/WASM 按需分包与完整 source map      |
| 类型发布 | 手工维护的 namespace/CommonJS 声明                      | `tsc` 从源码 emit，声明 rollup 后由 Bundler/NodeNext 消费配置校验       |
| API 契约 | JSDoc 静态产物，与源码和包入口脱节                      | TypeDoc 零警告文档 + API Extractor 签名基线                             |
| 单元测试 | 旧断言、旧 mock、浏览器错误不一定失败                   | Vitest 原生 `expect`/`vi`，Chromium Browser Mode，错误门禁与 V8 覆盖率  |
| UI 测试  | 少量代表页面 smoke test                                 | 96 个 HTML 自动清单；81 个双后端，WebXR、14 个 WebGPU 专属页明确单后端  |
| 视觉测试 | 截图比较为空实现                                        | 两个后端共用确定场景、readback 断言、截图基线与像素差异阈值             |
| 示例     | 旧全局变量、vendor 脚本、远程运行时资源                 | 严格 TS 多页面应用，本地 npm 依赖与本地静态资产                         |
| 渲染 ABI | WebGL 1/2 分支、GLSL 1.00 转译与逐项 uniform            | portable raster GLSL→Naga→WGSL；受控 Direct WGSL compute 与 storage ABI |
| npm 包   | 仓库内入口能运行即视为通过                              | publint、Are the Types Wrong、Bundler/NodeNext 与真实 ESM 运行时消费    |
| CI/发布  | 老版本 Actions、Node 与零散命令                         | 固定 Node 20.19.0、npm 10、Chromium 与单一完整发布门禁                  |
| 文档站点 | 跟踪旧生成物                                            | CI 现场生成 TypeDoc 与 Vite 示例站点并部署 Pages                        |

### 多包与 workspace 决策

仓库采用一个 Git 仓库、一个 lockfile 和一组根级质量门禁管理多个独立发布包，也就是 npm workspaces
monorepo；它不是把所有能力重新合并为一个 npm 包。当前发布边界是根目录的 `hilo3d` 核心包，以及
`addon-particle/`、`addon-physics/` 两个 workspace：

- 粒子和物理，尤其 Rapier WASM，保持独立包和显式导入，未使用的能力不会进入核心依赖图；
- addon 用 peer dependency 声明支持的核心版本，用本地 `file:..` dev
  dependency 解析同仓核心，避免开发与 CI 意外加载 npm registry 中的旧版 `hilo3d`；
- 每个公开 export 都有 API Extractor 报告；JavaScript source map 内嵌 source
  content，addon 不发布会引用缺失 `../src` 的 declaration map；
- 发布门禁把核心与 addon 的实际 tarball 安装进空 consumer，并运行所有根入口和 Rapier
  subpath 的真实 ESM import。仅做 `publint`、声明检查或 dry-run 不足以证明 peer 版本可运行。

保持独立发布包比“单 npm 包 + 可选导出”更符合按需安装、WASM 隔离和依赖所有权。当前三个包尚不足以证明把根核心整体搬到
`packages/hilo3d/`
的大规模路径迁移有收益；如果以后出现独立版本、独立负责人或更多共享构建包，再统一迁入
`packages/*`，不改变上述发布边界。

## 语言与架构

### 严格 TypeScript 全覆盖

共享配置启用了 `strict`、`noUncheckedIndexedAccess`、`exactOptionalPropertyTypes`、
`noImplicitOverride`、`noImplicitReturns`、`verbatimModuleSyntax`、`isolatedModules`、
`noFallthroughCasesInSwitch` 等规则。根 `tsconfig.json` 只维护 project references：

- `tsconfig.lib.json`：浏览器引擎源码与 declaration emit；
- `tsconfig.test.json`：Vitest 浏览器测试；
- `tsconfig.examples.json`：全部示例应用；
- `tsconfig.node.json`：Vite、Vitest、ESLint、Playwright 和工程脚本。

生产源码不会被测试全局类型或 Node 类型污染。lint 同时使用 TypeScript 类型信息检查
`src/`、`examples/`、`test/`、`scripts/` 与工程配置，生成物是唯一的目录级忽略对象。

### 原生对象模型

引擎内部的 scene
graph、renderer、geometry、material、texture、loader、animation、math、light、camera 与 helper 均已迁移到原生类。构造参数、事件、WebGL 资源、uniform、shader
semantic、glTF、动画状态、纹理来源等动态结构均有明确的 interface、union 或 generic。

旧 `Class.create`、`Class.mix` 与 `EventMixin`
已从源码、根 barrel、类型声明和测试删除。所有可监听对象直接继承类型化的
`EventDispatcher`，资源生命周期统一使用 backend-neutral
`releaseGPUResources()`；不保留 backend 名称泄漏到公共 API 的旧别名。

相机输入也遵循单一公共实现：`src/controls/OrbitControls.ts`
提供透视相机的 orbit、dolly、pan、触摸手势与受约束的程序化
`setView()`。一方维护的浏览器示例复用该入口，不在示例目录内重复实现 pointer、wheel 或 touch 相机控制器；自动巡游在适用时同样通过 controls 更新视图。

### 运行时与资源

- `Stage` 与 `Renderer` 只提供异步 `create(...)` 创建入口。`Stage.create(...)` 接受
  `backend: 'auto' | 'webgl2' | 'webgpu'`；省略字段等同 `auto`。auto 通过
  `Renderer.isBackendSupported('webgpu', options)` 只执行 `requestAdapter` 与 adapter
  policy/feature/limit 校验，兼容时正式初始化一次 WebGPU，不兼容时直接创建 WebGL
  2。显式请求 WebGPU 后绝不会静默回退。
- WebGL 2 上下文创建失败会直接报告不支持，不再尝试 WebGL
  1 或扩展模拟核心能力。WebGPU 会显式申请 adapter、校验 required
  feature/limit、创建设备并初始化 Naga WASM；任一步失败都会 reject
  `ready`。Naga 首次加载或 WASM 初始化失败不会污染进程级 Promise，后续新 renderer 可以重试。运行中 device
  lost 会按 generation/device identity 过滤旧回调，发出
  `webgpuDeviceLost`，释放旧 managers/context/GPU 资源，按冻结的 adapter
  options 重新获取等价 adapter，复核 fallback policy、required
  features/limits，再用相同的有效 device
  descriptor 自动重建。恢复期间 render 安全跳帧；成功时保留公共 `RenderTarget` identity 并发出
  `webgpuDeviceRestored`，失败时发出
  `webgpuDeviceRecoveryFailed`，之后的 render 显式抛出原始恢复错误。renderer
  destroy 会取消正在进行的恢复。
- VAO、实例化、MRT/draw buffers、三维/数组纹理和 UBO 均使用 WebGL 2 core
  API；扩展注册表只保留各向异性过滤、context loss、浮点颜色附件和纹理压缩格式等真正可选能力。
- 删除 `KHR_techniques_webgl` loader、类型和 SampleTechniques 资产。该历史扩展以任意 GLSL
  1.00 与 classic numeric uniform 为接口，无法满足固定 UBO
  ABI；仅改内置样例而继续宣称支持会使外部资产在 program link 时失败，因此不保留伪兼容入口。
- 删除 AMC 专有扩展；相关演示资产转换为标准 glTF。
- WebXR 使用标准 WebXR 类型与浏览器 API；当前呈现层是 `XRWebGLLayer`，因此 `webxr.html`
  被明确声明为 WebGL 2-only。它暂不计入 WebGPU 上线门槛，也不会先请求 WebGPU、失败后再回退 WebGL
  2；双后端与 WebGPU-only 页面的完整范围由下文自动清单与合同验证。
- 资源观测统一为 `renderer.resourceManager.getDiagnostics(rootNode?)`
  返回的后端中立快照，包括已跟踪 mesh/resource、当前使用、待销毁数量和 frame 状态。会读取 WebGL 私有 cache 并产生日志副作用的
  `logGLResource()` 已从公共入口和源码删除。
- 物理能力位于独立的 `@hilo/addon-physics` ESM workspace；主 `hilo3d`
  入口不引用物理代码或 WASM。当前 `/rapier2d` 与 `/rapier3d` 入口分别使用 Rapier 的 2D/3D
  compat 包，并通过公共 Stage System
  ABI 接入；原 Cannon 依赖与示例已删除。原 Draco 示例适配器及演示资产已退出：上游只提供 UMD/CommonJS 风格的浏览器 wrapper，而可用的纯 TypeScript 候选无法通过本仓库的严格 TypeScript
  6 声明检查；不通过构建期字符串改写、假声明或 `skipLibCheck` 伪装成现代模块。
- 粒子能力位于独立的 `@hilo/addon-particle` ESM workspace；主入口仅保留通用的 `RenderNodeExtension`
  和 Stage System ABI，不导入粒子系统。粒子包通过 typed Stage
  service 托管系统、帧级预算与 renderer-before-teardown 销毁，也保留显式生命周期的独立构造方式。
- 示例画廊只保留仍能说明当前引擎能力的加载路径。OSG、SMD、TGA 等历史格式示例及其大体积专用资产已经移除；glTF、CanvasTexture、HDR 与压缩纹理示例继续使用严格 TypeScript 和受支持的公共 API。
- 示例运行时资源来自仓库或 npm 依赖，不依赖第三方 CDN 才能通过测试。
- Vite 配置不读取、截断或改写 `node_modules`
  中的 UMD/CommonJS 源码；依赖必须提供可直接消费的 ESM 与严格类型契约，否则删除该集成，等待合格实现后再按正常模块边界接入。

## 双后端渲染与 shader ABI

### 后端选择与轻量 auto 探测

Renderer 的实际后端仍只有 `webgl2` 与 `webgpu`；异步工厂额外接受 `auto`
选择策略。所有后端都通过异步工厂创建，让调用点明确表达初始化边界：

```ts
const webglRenderer = await Renderer.create({ backend: 'webgl2', domElement: canvas });
const autoRenderer = await Renderer.create({ backend: 'auto', domElement: canvas });
const webgpuRenderer = await Renderer.create({ backend: 'webgpu', domElement: canvas });

const autoStage = await Stage.create({ camera });
const webglStage = await Stage.create({ backend: 'webgl2', camera });
const webgpuStage = await Stage.create({ backend: 'webgpu', camera });
const webgpuSupported = await Renderer.isBackendSupported('webgpu');
```

省略 `backend` 等同于 `auto`。auto 先调用公开的
`Renderer.isBackendSupported('webgpu', options)`：它只读取 `navigator.gpu`、调用
`requestAdapter()`，再校验 fallback-adapter policy、required feature/limit 与引擎最低 adapter
limits；不得调用
`requestDevice()`、`canvas.getContext('webgpu')`，不得初始化 Naga、pipeline、manager 或任何 GPU 资源。探测返回
`false` 时直接构造 WebGL 2；传入 `preserveDrawingBuffer` 时也直接选择 WebGL 2；请求
`alpha: true, premultipliedAlpha: false` 的 straight-alpha canvas 合成时同样直接选择 WebGL
2，不发起 WebGPU 探测。显式 WebGL 2 与自动选择使用同一个异步初始化边界。

探测返回 `true` 后只正式创建一次 WebGPU Stage。此后的 device/context 请求、shader
compiler、pipeline 或资源初始化错误都直接 reject，不得被 catch 后切换 WebGL
2。显式选择 WebGPU 会跳过 auto 探测；浏览器不支持、adapter 不存在、feature/limit 不满足、恢复失败或 shader 编译失败均为可观察错误。引擎内部禁止吞掉错误后关闭 feature 或换一条低能力渲染路径继续运行。

WebGL 2 backend 直接使用 core API 和原生 GLSL ES 3.00；项目不创建 WebGL 1 context，不保留 WebGL 1
extension adapter，也不接受 GLSL 1.00 自定义 shader。

### WebGPU-shaped RHI 边界

生产渲染底层以 `src/render/rhi/core`
的 RHI 契约为唯一硬件边界：device 创建 buffer、texture、sampler、prepared shader、bind-group
layout、pipeline layout、bind group 与 graphics pipeline；frame-scoped command context、render
pass、queue 和 submission 表达执行、提交与完成。RHI 产品代码不引用 Mesh、Material、Stage、Light、RenderList 或 shader
variant，也不负责 GLSL→WGSL；这些引擎语义属于 shared Renderer、`src/shader` variant 层和
`src/render/shader` 编译层。具体设备只允许位于 `src/render/rhi/backends/webgl2` 与
`src/render/rhi/backends/webgpu`，RHI core 和 Render Graph 都不得反向 import
renderer 或 scene 类型。

`src/render/rhi/RHIFactory.ts` 是唯一硬件组合根，负责 support probe、构造 RHI
device/surface 并等待 ready；它不产生逐 command 转发对象。`src/render/internal/RendererFactory.ts`
无论后端都只构造 `SharedRendererDriver`，backend 选择仅决定其持有的 concrete RHI device。scene
traversal、resource preparation、Render
Graph、pass、present、readback 与恢复流程不再分叉到两个 feature driver。

WebGPU RHI resource/context/pass/queue 直接持有并调用对应 native object，不使用 Proxy、二次 command
list 或 GL 风格全局状态机。WebGL2 RHI 在同一可移植契约下实现 pipeline、bind group 与 render
pass，但 command context 是明确的 immediate context：GL 命令在调用时经 device-owned state-diff
cache 即时执行，`endFrame()` 只封闭 frame
scope 并产生 submission，不回放软件命令列表。两后端的 descriptor snapshot、resource
generation、in-flight
lifetime 与 validation 共享同一契约；VAO、framebuffer、pipeline、sampler 和 immutable layout
cache 有界且只归 device 所有。

旧 RHI wrapper 与 WebGL2/WebGPU feature
driver 已删除。性能回归使用当前 RHI 的跨提交冻结快照，生产树不再为同提交 A/B 保留第二套 renderer、manager 或 native
fast path。RHI 的 draw/context 稳态热段不得创建临时 descriptor、array、iterator 或 typed-array；pass
setup、shader/pipeline prepare、graph compile 与 transient allocation 必须在 execute 前完成。

公开 Renderer 不携带 `gl` 或 `gpuDevice` 字段。需要 XR、验证或第三方 native 互操作时，调用方通过
`renderer.getExtension('webgl2-native')` 或 `renderer.getExtension('webgpu-native')`
显式请求能力并检查返回值；常规 scene、material、render
target 与 readback 不得依赖 extension。等待提交完成统一使用
`await renderer.waitForIdle()`，调用方不直接访问 native queue fence。

可移植契约不伪造完整 WebGPU。RHI 已包含 WebGPU-only compute pipeline/pass、storage
buffer/texture、buffer clear、direct/indirect dispatch 与 indirect draw，但它们在 WebGL
2 上通过缺失的 `features`、limit 和创建/执行时错误明确暴露，不使用 texture、transform
feedback、fragment pass 或 CPU 隐藏降级来模拟。1D texture、异步通用 buffer
mapping、base-vertex 等未公开能力同样 fail-closed。每个 texture
format 的 sampled/filterable/renderable/storage 与 sample-count 子集在 RHI 内部按 WebGPU feature
tier 或 WebGL extension 做保守 gate，不会误报硬件实际无法创建的组合。

### WebGPU device loss 自动恢复

统一 `Renderer` 公开 `initializing`/`ready`/`recovering`/`failed`/`destroyed` 恢复状态与当前
`recoveryPromise`。活跃 WebGPU device 丢失时，renderer 立即发出
`webgpuDeviceLost`，暂停 target 的 native
resource，销毁旧设备绑定的 manager/context。初始化成功时保存并冻结 adapter options、fallback adapter
policy 与实际生效的 device descriptor；恢复时不复用可能已经失效的旧 adapter，而是通过
`navigator.gpu.requestAdapter()`
按相同 options 重新获取等价 adapter，重新验证 fallback 策略、全部 required
feature、bind-group/UBO/texture/vertex 等引擎 limits，再按相同的有效 descriptor 请求替代 device。恢复期间的正常帧不会访问已丢失资源、创建 command
encoder 或提交 queue，而是安全返回。

成功后重建 buffer/texture/uniform/pipeline/bind-group manager 和 canvas context，并在原有
`RenderTarget`/attachment `Texture`
对象上重建 native 资源；当前选中 target、所有权与对象 identity 都不变，然后发出
`webgpuDeviceRestored`。adapter/device 请求或资源恢复失败会保存原始 cause、发出
`webgpuDeviceRecoveryFailed` 并进入 `failed`；后续 render 明确抛错，不吞错、不跳过功能、不切换 WebGL
2。generation 与 device
identity 保证过期 loss/recovery 回调不能覆盖新设备；显式 destroy 使恢复结果失效并确定性释放已创建资源。

`isImageCanRelease` 不以丢失恢复能力换取公开 CPU image 的释放。公开 image 清空前，引擎按逻辑
`Texture` identity 建立 backend-neutral recovery backing：TypedArray/DataView、ImageData、cube
face、显式 mipmap 与 sub-texture update 保存不可变副本，DOM/canvas/ImageBitmap/video 等 external
source 保留私有引用。WebGL 2 新 context allocation 与 WebGPU 新 device
manager 都从它建立各自的上传快照并重放同一 descriptor、base
level、mipmap 与增量更新；公开 image 始终不可访问。替换公开 source 会使旧 backing 失效；各 renderer 的 device-local 副本在资源释放时确定性清除，identity 不再可达后 engine
backing 由 WeakMap 回收。

公共 `releaseGPUResources()` 与完整 device teardown 分离：它释放 owned `RenderTarget`、device
allocation、UBO 和 pipeline/bind-group
cache，但保留可继续使用的 device/context/manager 与纹理恢复数据，并立即重建主画布 depth/MSAA
attachment，因此调用后的下一次 render 仍会创建 render pass 并提交。device
loss 的内部 suspend 路径则保留公共 target/attachment
identity 与 ownership，待新 device 激活后原位恢复。

### Raster GLSL 预处理与 Direct WGSL Compute

全部可移植 raster shader 都以 `src/shader/` 中的 GLSL ES
3.00 为唯一源码。两个后端复用材质、阴影与 fullscreen present shader；仅 WebGPU 需要的 mipmap utility
pass 也继续写成 GLSL，而不是另建 WGSL 特例。WebGPU 不维护第二套手写 WGSL，对每个已解析的 shader
variant 和内部 utility pass 执行同一确定流水线：

1. 先由引擎解析 object-like/function-like
   `#define`、嵌套条件分支与 include，得到当前材质、灯光、geometry 和 render-state 对应的有效源码；预处理表达式完整处理位运算、移位、一元
   `~`、三元表达式、整数后缀、指数与八进制，宏参数按 GLSL
   token边界展开，递归或非法表达式直接报告源位置，不使用“解析失败视为 1”的宽松行为；
2. 将有效 GLSL ES 3.00 准备为 Naga 接受的 Vulkan GLSL 4.50：注入
   `HILO_WEBGPU`、分配稳定且紧凑的 vertex/varying/fragment location、拆分 matrix IO、把 std140
   block 映射到固定 set/binding；
3. 把 GLSL combined sampler 拆成 WebGPU 独立 texture/sampler binding；sampler
   array 的常量元素直接展开为 binding pair，dynamically-uniform 索引则生成有类型的 dispatch
   function，覆盖 texture builtin、sampler 函数参数和多数组组合，不依赖可选 binding-array feature；
4. 把用户 `main` 改为内部函数，再生成稳定 entry wrapper；wrapper 在用户函数返回后完成 matrix
   varying 写回以及 WebGL `[-w, w]` 到 WebGPU `[0, w]` 的 clip-space
   depth 转换，因此用户代码中的提前 `return` 也不能绕过后处理；
5. 比较顶点与片元阶段同名 UBO 的规范字段签名；字段类型、顺序、数组长度或宏展开结果不一致时，在进入 Naga 前直接失败；
6. 最后调用 `web-naga` 的 WASM GLSL frontend 生成 WGSL，把公共 std140
   ABI 确定性表达为 WGSL 默认 uniform 布局可接受的 `@align`/`@size` wrapper，再建立 shader
   module 与 pipeline。

预处理结果同时输出 vertex inputs、fragment outputs、uniform blocks 与 sampler
bindings 元数据；pipeline、vertex layout 与 bind
group 都消费同一份结果，不再各自用正则猜测接口。Naga 错误保留 stage 和送入 Naga 的完整 GLSL
4.50，避免只暴露下游 WGSL 错误。共享 present 与 WebGPU mipmap
pass 直接消费转译元数据中的默认 sampler ABI（group 1、texture binding 1、sampler binding
2，并显式提供空 group
0），不存在硬编码 WGSL、备用 module 或失败后 fallback。内置 Basic/PBR、灯光、阴影、skinning、morph、instancing、quantized、geometry、present 和 mipmap
shader corpus 都必须在单元测试中真实通过 Naga。

GLSL frontend 同时解析全局多声明、固定长度数组以及带 instance name、数组成员和 interpolation
qualifier 的 stage interface block，并把它们展平为稳定 location；UBO 的 `layout(...)`
qualifier 可以合法重排或包含显式 set/binding，但必须声明
`std140`，最终 binding 仍由固定 ABI 规范化。项目只接受 WebGL 2 对应的 GLSL ES 3.00
builtin 集：`textureProj*` 与 constant-offset 形式会进入 sampler-array 类型化 lowering，而
`textureGather*`、`textureQueryLevels`、`textureQueryLod`
等更高版本接口在两个后端之前统一失败，不能制造只在 WebGPU 可运行的方言。depth-only
pass 使用同一片元源码和 Naga 流水线，但把 color output 降为 private sink，只保留
`discard`、`gl_FragDepth` 与其他片元副作用，因此阴影不会依赖虚假的 color
attachment，也没有第二套 shadow shader。

这条路径不申请、探测或依赖可选的 `uniform_buffer_standard_layout` language feature。标量/`vec2`
数组、非方阵以及 `bool`/`bvec`
都会在 Naga 前后按同一 std140 字节契约做结构化降级与语义恢复；WebGPU 直接上传 WebGL
2 使用的同一份字节，不维护第二套压缩布局。内置 vertex/fragment corpus 除 Naga 转译外还必须通过真实
`GPUShaderModule.getCompilationInfo()`。

shader variant key 使用当前 geometry 产生的完整结构宏签名，包括四类 decode、color
size 与 morph 结构，而不是缓存某个 geometry 首次渲染时的粗略布尔值。公共 precision
header 按 renderer 实例参与 shader cache key，因此不同 WebGL 2/WebGPU
renderer 或运行时 precision 变化不会交叉复用错误源码。`commonOptions`
每次生成不可变快照并把规范化 signature 纳入 header/variant key；不存在可变的全局
`commonHeader`，也不要求调用方手动清 cache。

WebGPU-only compute 是独立且受控的例外：`ComputeShader` 接受 Direct
WGSL，不走 GLSL 转换，但必须经过 Naga WGSL
frontend/validator、selected-entry/workgroup/workgroup-storage/override metadata 检查、显式 binding
ABI 比对和真实 WebGPU pipeline validation。它只能声明 `@compute`，不能借此提交 graphics WGSL。`f16`
原始源码先通过 Naga frontend；当前 `web-naga`
writer 使用 token-safe 等价 f32 特化完成 validator/backend，原始 artifact 再经 `shader-f16`
capability 和真实 WebGPU pipeline 验证。buffer `minBindingSize` 从 WGSL store type 推导，runtime
array 按一个元素计算，低报在 native 调用前失败。

WebGPU-only storage-aware raster 仍不接受 graphics WGSL。`StorageGraphicsShader` 使用受控 GLSL ES
3.10 readonly-std430 subset，经引擎 preprocessing、Vulkan GLSL
4.50 与 Naga 生成 WGSL；普通 Shader/Material 继续严格使用 GLSL ES 3.00/std140/sampler ABI。

### 按更新频率固定的 UBO 与 bind group

所有内置数值 shader 数据按所有权和更新频率进入固定 std140 block。WebGL 2 使用稳定的 flat
binding；WebGPU 将同一逻辑 ABI 分到四个 bind group，避免高频对象数据导致全局或材质资源重新绑定：

| Block                  | 数据所有者与典型内容                                                                           | 更新时机                      | WebGL 2 | WebGPU |
| ---------------------- | ---------------------------------------------------------------------------------------------- | ----------------------------- | ------: | -----: |
| `FrameBlock`           | render size、时间与帧序号                                                                      | 每个 renderer frame 一次      |       0 |    0/0 |
| `CameraBlock`          | current/previous view/projection、逆矩阵、render origin、history/depth 标志、物理像素 viewport | 每个 camera/render pass 一次  |       1 |    0/1 |
| `SceneBlock`           | fog 等 scene-owned 状态                                                                        | scene revision 变化时         |       2 |    0/2 |
| `LightBlock`           | view-space 灯光、衰减、阴影 atlas 参数                                                         | 每个 camera/render pass 一次  |       3 |    0/3 |
| `MaterialBlock`        | Basic/PBR scalar、color、IBL 与材质标志                                                        | 最终 std140 字段变化时        |       4 |    1/0 |
| `MaterialTextureBlock` | 24 个独立 texture slot 的 UV/transform/encoding/channel metadata                               | texture-slot 数据变化时       |       9 |    1/1 |
| `ModelBlock`           | current/previous model 与 world-normal 变换                                                    | 非实例对象 transform 变化时   |       5 |    2/0 |
| `GeometryBlock`        | position/normal/UV decode 变换                                                                 | geometry revision 变化时      |       6 |    2/1 |
| `SkinningBlock`        | current/previous joint palette                                                                 | skeleton pose 变化时          |       7 |    2/2 |
| `MorphBlock`           | current/previous morph weights 与 target 状态                                                  | morph pose 变化时             |       8 |    2/3 |
| `InstanceBlock`        | 每批 current/previous model 与 normal matrix array                                             | WebGPU instanced batch 变化时 |       — |    2/4 |

WebGPU group 0 是 frame/pass/scene 全局域，group 1 是 material 与纹理域，group
2 是 object/geometry/pose 域，group
3 专供应用自定义 block。自定义 block 仍先通过共享 registry 注册：WebGL 2 flat binding
10 映射到 WebGPU group 3 binding 2，group 3 binding 0/1 保留给 pass-global scene
texture，后续依次递增。内置名称和位置不允许按 shader variant 临时分配。 `CameraBlock.u_viewport`
的语义固定为当前 attachment 的物理像素
`(x, y, width, height)`；canvas、RenderTarget、平面阴影与点光六面阴影切换 camera/pass 时都更新同一 std140 字段，不保留 classic
`uniform` 兼容路径。

前后帧状态属于 submission transaction，而不是“执行过一次 CPU
update”就推进：成功提交后才把 pending 变成 previous，record/prepare/execute 失败统一 rollback；device
recovery 使旧 generation 无效。Camera cut、teleport、world-origin shift、pose
reset 等不连续变化通过公开 `Node.invalidateTransformHistory()`
把下一次 previous 设为 current。spawn 首帧同样 previous=current；despawn 不产生 motion 输入。

公开 `Std140Schema`
是刻意固定的扁平 scalar/vector/matrix（含定长数组）模型；嵌套 struct 不在这套跨 WebGL
2/WebGPU 可移植 ABI 中，构造时会明确拒绝，不能把嵌套对象强转后按错误 offset 打包。`UniformBuffer`
必须由 `Std140Layout` 创建，不再接受无布局的裸 TypedArray。

引擎在创建 backend 时校验 uniform buffer 数量、单 block 大小、alignment、bind
group 数量、纹理/vertex 资源上限和显式 required feature/limit。std140 schema 由 offset、matrix
stride、array stride 与固定 ABI 测试锁定；CPU revision 与 dirty byte
range 驱动上传，禁止为了“统一”而在每次 draw 重建或整块重传所有 block。

GeometryData、Geometry、MaterialInstance 与 Texture 都发布单调 revision；每个 renderer/device 分别记录自己已经上传的版本，不清除会影响另一个 backend 的全局脏标记。`MaterialBlock`
与 `MaterialTextureBlock` 分别把最终 std140 byte
image 写入复用的候选区并直接逐字节比较，因此直接修改
`Color`/`Matrix`、纹理派生的 mip 数量或其他嵌套数值也会自动刷新；字节未变时不推进
`UniformBuffer.revision`，两后端都不会重复上传，也不要求应用手工设置 `isDirty`。WebGL
2 还为每个 VAO 单独记录 attribute/index 的 revision、shape 与 count，不能让阴影 VAO 的首次上传吞掉主渲染 VAO 的绑定更新。partial
buffer/texture/UBO 更新保留带 revision 的有界快照，上传成功后只推进当前 backend 的游标；慢消费者越过 64 条 UBO
history 后执行一次完整上传，再恢复 partial。资源以
`mesh → pass owner → material/shader/instancing variant`
保存；阴影、主画布和每个 RenderTarget 各自更新命中的 variant，不会用一次 pass 的快照覆盖其他 pass。材质/几何 identity 替换、target 销毁和 mesh 销毁会确定性释放对应状态，每个 mesh 的 variant 使用 32 项 LRU 上限；完整帧成功后才原子提交，异常帧只回收未提交的新资源。共享资源按 renderer 全局最终引用判断，不能被另一个 scene/pass 提前销毁。owner 释放会同时删除 CPU
UBO cache 和 native wrapper。缓存归属只保留一层：RHI device 缓存 immutable sampler、bind-group
layout、pipeline layout 与 render pipeline，Renderer 缓存 material、Mesh、shader variant、binding
set 与 upload revision。RHI 不按 descriptor 去重 buffer、texture、shader module 或 bind
group，label 不参与 device key；device/context 丢失、恢复和 destroy 会清除 device
cache。vertex/instance/index buffer variant 使用 per-owner LRU，每纹理 resolved snapshot 与每个 base
shader 的 numeric-depth 专化同样使用有界 access-order
LRU；pipeline 把不可淘汰的 in-flight 去重层与已完成的有界 LRU 分开。UBO/texture
identity 变化只失效 bind group，不重建稳定的 pipeline
layout，资源 churn 不会让历史 variant 永久常驻、重复并发编译或触发无关 pipeline 重编译。

WebGPU high-end storage shading 还使用 renderer-local `SharedMaterialRecordDatabase`。它按公共
`MaterialInstance.materialId` 去重为 family/layout dense handle，每个 renderer 独立保存 committed
revision；只重打包 revision 变化的 record，并合并相邻 upload range。数据库不会依赖全局 texture-slot
dirty set 决定是否上传，因此某个 renderer 在成功 submission 后提交 slot
dirtiness 不会让另一个 renderer 漏传。失败帧保留旧 committed revision，下一帧重试；`cpu-shadow`
recipe 在 device recovery 后恢复同一 record/handle identity。storage record 遵守
`MaterialInstance.invalidateData()` 合同；默认 Forward 的 std140
byte-image 比较仍保留上文所述的嵌套值自动检测。

WebGPU geometry 上传同样消费有界 dirty history：interleaved
attribute 只重打包受影响的 AoS 顶点区间，支持 matrix
column、normalized、stride/offset、多 source 与离散 range；只命中 padding 时不产生
`writeBuffer`。Uint8/Uint16/Uint32 index update 会按 WebGPU 的 4-byte copy
alignment 合并写入并保持 primitive-restart 规范化。历史游标落后或 shape/byteLength 变化时才执行一次完整重建，旧 native
buffer 在新资源成功后确定性销毁。

当前灯光着色使用 view-space 数据，因此 `LightBlock` 与 `CameraBlock`
同属 render-pass 频率。环境贴图、球谐与 IBL 强度允许逐材质变化，因此属于
`MaterialBlock`。WebGPU 阴影资源使用单一 depth-comparison atlas；atlas 尺寸、slice
rect、bias 与 light-space matrix 都进入 `LightBlock`，不会退回逐灯散装 uniform 或占用一组 sampler
array。WebGL 2 点光源 cube shadow 会为六个面分别显式开始 camera pass，即使复用同一个 shadow
camera 对象也必须刷新六组 `CameraBlock` view/view-projection bytes；不能用 camera
identity 代替 pass 边界。只有 Directional/Spot/Point light 接受
`ShadowCastingLightParameters`；Area/Ambient/base light 在构造或运行时设置 `shadow`
时就于后端中立层失败，不允许 WebGL 报错而 WebGPU 静默忽略。renderer-owned shadow camera 与 debug
helper 按 renderer/light 使用明确的 owner map；debug 关闭、shadow 置空、light
disabled、light 离开 stage、GPU resource release、device suspend/recovery 与 renderer
destroy 都会 prune 对应节点并解除 parent/listener。多个 renderer 不共享这些节点，重复恢复也不会让 light
children 增长。

WebGL 2 的 native resource 也不使用进程级单例所有权。Program、Buffer 与 VertexArrayObject 按
`WebGL2RenderingContext` 分区；Texture 与 `UniformBuffer` allocation、upload
cursor 和 cache 则由每个 `WebGLState` 的 backend manager 持有；不可变 `WebGLSampler`
variant 由有界 sampler manager 按 context 和 texture unit 管理。共享 `Texture`
只保存 CPU 内容、revision 与恢复 snapshot，WebGL uploader 不再位于
`src/texture`，公开 API 也不暴露 native texture/cache。VAO current binding、Framebuffer
reset/destroy、capability snapshot 与 extension object 同样是 context-local。

同一 shader/geometry/texture identity 可在多个 renderer 中各自建立 native
allocation，不会把 A 的 program/state/gl 返回给 B。A 的 context
loss、restore、`releaseGPUResources()`
或 destroy 只清理 A 的 namespace，不删除 B 的 handle、不覆盖 B 的 extension/capability 视图。Texture
content descriptor snapshot 会检测 size/format/mipmap 变化，并在 framebuffer
resize/reset 时稳定复用 native object；一个 texture attachment 不能同时归属两个 framebuffer
graph。不可取消的内部 destroy observer 会先释放所有 backend
allocation，再触发公开事件；`Texture.needDestroy` 也会同步失效该逻辑纹理在所有 WebGL
context 与 WebGPU device 上的旧 allocation，不能由第一个消费者吞掉全局状态。

这一所有权边界由两个真实 WebGL 2
context 的首次分配、reset/release 和继续渲染回归锁定，不使用单 context mock 代替。旧的根导出
`capabilities`/`extensions`
单例已删除，因为它们无法表示“当前”多个 context；需要 native互操作的调用方从所属 Renderer 的
`getExtension('webgl2-native')` 读取 context-local capability
snapshot。Program、Buffer、VAO 与 Texture native cache 都由 backend
manager 私有持有，不再从根 barrel 暴露。

固定 ABI 同时固定容量：最多 8 个 directional light、8 个 spot light、16 个 point light、8 个 area
light、128 个 skin joint、8 个 morph weight 与每个 WebGPU instanced batch
128 个实例。超过容量必须分批或抛出带领域名称的错误；禁止截断数组、复用最后一项或退回 classic
uniform。

### texture 与 sampler 是 opaque resource 例外

GLSL opaque sampler 不能成为 uniform block 成员，因此 sampler 是 block 外唯一合法的 `uniform`
声明。WebGL 2 在 program link 后为 active sampler 分配整数 texture
unit；WebGPU 预处理器把同一声明降级为独立的 texture/sampler binding pair。材质纹理、环境贴图、BRDF
LUT、LTC、shadow atlas 和 post-process input 都遵守这一规则。

共享 shader/texture 契约完整支持 GLSL ES 3.00 的 `sampler3D`、`sampler2DArray`、
`sampler2DArrayShadow`，以及 `isampler*`/`usampler*`
在 2D、3D、cube、2D-array 上的全部组合。预处理器在保留 sampler
array 与函数参数语义的同时生成准确的 WGSL dimension、sample type 和 comparison binding；bind-group
layout 根据 float/depth/sint/uint 选择 filtering、comparison 或 non-filtering
sampler，不依赖不可用的占位资源。

managed `Texture` 的公共 target 契约覆盖 2D、cube、3D 与 2D-array，raw upload、显式 mip、recovery
backing、`flipY` 和 device/context limit 验证都保留 depth/layer 维度；signed/unsigned integer
internal format 使用严格匹配的 typed storage 与 WGSL sint/uint sample type。integer texture 必须使用
`NEAREST` magnification、`NEAREST` 或 `NEAREST_MIPMAP_NEAREST`
minification、`anisotropic: 1`；一旦使用 mipmap filter 就必须提供显式完整 chain。3D
texture 的 mip 深度逐层收缩，也必须提供显式完整 chain。WebGPU 规范没有 compressed 3D
texture，因此 WebGL 2 与 WebGPU 都在原生分配前拒绝该组合；compressed 2D-array 则保留，并在 format
feature 可用时上传完整 layer/mip 数据。

`Texture.updateSubTexture()` 只接受完整 descriptor，不保留旧的 `(xOffset, yOffset, image)`
重载。所有 target 都必须提供
`mipLevel/x/y/width/height/image`：2D 不接受子资源选择字段；cube 必须提供
`face: 0..5`；2D-array 必须提供 `layer/depth`；3D 必须提供
`z/depth`。这些字段互斥，并在后端选择前完成范围、source 尺寸和 raw 数据长度校验。非 base
level 只能更新显式 mip chain，自动生成的 mip 始终由 level 0 派生。

cube 显式 mip chain 包含 level 0，每个 level 连续提供六项，顺序固定为
`+X, -X, +Y, -Y, +Z, -Z`，总项数为
`mipmapCount * 6`；每项的 face、width 与 height 必须与所在 level 完全一致。managed raw depth 支持
`DEPTH_COMPONENT16/DEPTH_COMPONENT/UNSIGNED_SHORT`、
`DEPTH_COMPONENT32F/DEPTH_COMPONENT/FLOAT`，以及 device 提供 `depth32float-stencil8` feature 时的
`DEPTH32F_STENCIL8/DEPTH_STENCIL/FLOAT_32_UNSIGNED_INT_24_8_REV`。DEPTH24/DEPTH24_STENCIL8 仍可作为无初始数据的 attachment
allocation，但 raw base/sub-update 因没有可移植 WebGPU 字节表示而在后端中立层拒绝；depth external
image 与 depth mipmap filter 同样拒绝。

compressed sub-update 只接受 raw
block 数据；origin 必须按 block 对齐，width/height 只有到达逻辑 mip 边缘时才允许不是 block 整数倍，payload 必须与 block 数精确相符。WebGPU 对 2×2、1×1 等 mip
tail 使用底层格式要求的物理 4×4 copy extent，因此完整 8→4→2→1 chain 可正常上传。WebGL2 的 compressed
buffer upload 只保留完整 mip slice；partial region 无法在不解析、重编码 BC/ETC2/ASTC opaque block
payload 的前提下满足 portable top-left row 契约，因此必须在 native command 前以
`unsupported-feature` 失败。array/cube 的单 layer/face 完整 mip
slice 仍然支持，WebGPU 的 block-aligned partial
update 契约不变。Texture 每次局部更新都会同步维护准确的完整 CPU
checkpoint，同时只保留最多 64 条增量历史。每个 WebGL context/WebGPU
device 独立确认 revision；消费者落后于 checkpoint 边界时，
`TextureUpdateSnapshot.requiresFullUpload` 为
`true`，从完整 checkpoint 重放成功后再继续增量，不清除其他 backend 的游标。

depth texture 同时支持 comparison 与 numeric 两条正式 ABI：shadow sampler 直接使用 Naga 生成的
`texture_depth_*`/comparison sampler；普通 sampler 在运行时解析出真实 depth resource 后，将对应 Naga
WGSL binding 专门化为 `texture_depth_*`，并把标量 sample/load 结果恢复成 GLSL 的 vec4 `.r`
语义。WebGPU numeric depth 必须是 nearest-only、`anisotropic: 1` 的 non-filtering
sampler，非法 filter 在创建 bind group 前失败；WebGL 2 按 Program 反射出的 shadow/ordinary
sampler 类型选择按 texture unit 绑定的不可变 `WebGLSampler` variant，comparison
function 来自同一 render-target 契约；WebGPU bind group 使用相同的默认 compare，因此同一个 depth
attachment 可安全用于两种读取方式。sampler 仍按完整 descriptor 缓存，并在 resolved
binding 中保存不可变快照。

纹理色彩管理也只有一条跨后端语义：DOM/external
image 一律进入浏览器标准的 sRGB-managed 上传路径；旧的 `colorSpaceConversion`
布尔开关已删除，因为 WebGPU 没有与 WebGL `NONE`
等价且同步的 copy 语义。TypedArray/DataView 是未标记的原始数值，不发生隐式色彩转换，并统一采用紧密行布局；`flipY`
在两个后端上传前都形成相同的确定性行顺序，非对齐 DataView 会先安全复制到其 `type`
声明的分量视图，绝不静默忽略或依赖驱动默认 unpack 对齐。

`HTMLVideoElement` 也不使用特例回退。WebGPU 通过 `requestVideoFrameCallback`
只观察已解码帧，将完整帧写入该 texture 私有的 staging canvas，再用 `copyExternalImageToTexture`
上传。queue copy 在途时不允许改写 canvas；等待 `onSubmittedWorkDone()`
后才接受下一帧，允许视频正常播放时丢弃中间帧而不阻塞 decoder。首帧未到达时保持新建 GPU
texture 的合法零初始内容，不推进 uploaded revision、不生成 mipmap、不释放 source。视频 source
identity 变更、texture destroy、manager suspend/device
recovery 都会取消旧 callback 并重建观察。捕获、copy 或 queue 失败会保留 cause 并在下次资源同步时显式抛出；不直传缺少 decoder
backing 的 video、不停止播放伪造静态纹理、不回退 WebGL。

任何 block 外的 float、vector、matrix、integer、boolean 或其数组都会被拒绝，不能借
`ShaderMaterial`、示例代码或 backend 分支绕过。纹理 UV
channel、强度、尺寸、阈值和 kernel 等数值元数据必须进入 Scene、Light、Material 或注册过的自定义 block；opaque
resource 例外不是保留通用逐项 uniform 系统的理由。

### Render target、MRT 与 readback

公共离屏契约只有后端中立的 `Renderer` 与 `RenderTarget`：同一个 Renderer 在两个后端都提供
`createRenderTarget()`、`setRenderTarget()` 和 `renderToTarget()`，调用方不接触原生
`WebGLFramebuffer`、`GPUTexture` 或 backend-specific handle。统一 descriptor 支持多 color
attachment、1×/4× MSAA resolve、可选 depth/stencil、合法情况下可采样的 color/depth
texture、resize、所有权、显式 present 选择与异步区域 readback；跨 backend
target、非法 attachment、MSAA depth sampling、无附件 target、销毁后复用和 depth-only
present 均直接报错。

RenderTarget attachment format 与其当前内容的颜色编码是两个独立合同。`present()` 或
`setRenderTarget(..., { present: true })` 默认把 attachment
zero 解释为线性颜色，并只在写入浏览器 surface 时执行一次 linear-to-sRGB transfer。已经自行完成 tone
mapping/display transform 的自定义 pipeline 必须通过 `colorEncoding: 'srgb'`
显式声明；该路径只做原样采样呈现，不会再次提亮。

WebGPU MRT pipeline target 与 attachment location 一一对应，支持中间为 `null` 的 sparse color
slot；depth-only target 不声明颜色输出。resize 在新 color/depth/MSAA
allocation 全部成功后才提交，失败会销毁新资源并保留旧 target
identity、尺寸和可用 allocation，不存在部分替换。

shared RenderTarget cache 明确区分 persistent resolve/readable 纹理与 graph-transient MSAA render
source。仅当 4× MSAA 的全部相关 color/depth/stencil attachment 都是首 pass clear、末 pass
discard 时，cache 才不创建持久 multisample source/depth handle，由 Render
Graph 在 execution 前分配并在 submission 完成后回收；任一 load/store 需要跨 graph 保留内容就原子切回 persistent
policy。opaque 与 transparent pass 在同一 graph 中共用同一个 source，resize、recovery 与 partial
allocation failure 保持该 lifetime
policy 的事务语义，不允许同时保留 persistent 和 transient 两份 MSAA backing。

材质采样公共 RenderTarget texture 时必须声明显式 graph
read。若本 graph 没有 writer，它读取跨帧保留的 persistent 内容；若存在 opaque/transparent 等一个或多个 writer，compiler 保留完整 writer 链，并让 pure
consumer 依赖最后 writer，即使 consumer 先被插入也一样。同 pass feedback 继续作为 duplicate
access 拒绝，普通 imported texture 不改变原有插入顺序语义。

两个实现返回相同的紧凑 top-to-bottom texel 结果。WebGPU 内部遵守每行 256-byte copy
alignment 后再压紧，WebGL 2 内部处理 framebuffer
resolve 和坐标方向；这些 native 差异不会泄漏到调用方。target-owned texture 继续使用引擎 `Texture`
identity，因此材质 sampler、bind-group/cache 与资源释放共享同一生命周期，不复制旁路纹理系统。attachment
identity 在 resize、WebGL context restore 与 WebGPU device
recovery 前后保持不变。两个 backend 的 target owner 都跟踪 attachment allocation generation：公开
`Texture.destroy()`、target 变化或上传失败会先清除旧 native
handle；WebGL 重新挂接 manager 的当前 allocation，WebGPU 则在同一逻辑 texture
identity 下原子重建，设备 suspend 期间禁止在失效 device 上分配。若这类失效发生在 WebGPU command
recording 期间，整帧会被 poison 且不提交，下一帧再恢复，旧 observer 或 material
binding 不能创建脱离 target 的旁路纹理。WebGL resize 会事务性更新全部 draw/resolve/depth
attachment，任一 allocation 失败就完整回滚，回滚本身失败则销毁 target 并抛出聚合错误，绝不留下半提交状态。显式 present 在两个 backend 都使用 fullscreen
texture-load pipeline；WebGL
2 不会把单采样 FBO 非法 blit 到浏览器可能启用 MSAA 的默认 framebuffer。WebGL
pass 的 clear 写掩码、begin/resolve/invalidate 异常恢复和嵌套 framebuffer 保存状态都有硬门禁，任何失败都在抛错前恢复 canvas
binding，下一帧会完整重绑 program、材质状态与 sampler。

示例 `PostProcess` 只使用公共 `RenderTarget`、UBO 和 fullscreen draw 组成 ping-pong
graph；`MeshPicker` 同样通过公共 target 执行 object-ID pass，再异步 readback。两者都在 WebGL
2 与 WebGPU 上运行，拾取不存在 CPU ray-cast 回退，后处理也不存在 WebGL-only
framebuffer 分支。旧的公共 `Framebuffer`、`LightShadow`、`CubeLightShadow`、灯光上的 `lightShadow`
字段、renderer-owned `useFramebuffer`/`framebufferOption`、隐式 present 以及 WebGPU 的无效 `clear*`
兼容桩已删除；`renderTarget.html` 只展示显式创建、resize 和 scoped render。shadow
allocation 通过内部 registry 持有；WebGL 原生 framebuffer
wrapper 仅是 RenderTarget/阴影的内部实现，不进入根出口或 API report。

WebGPU 不支持的 `LINE_LOOP` 与 `TRIANGLE_FAN` 不通过 backend 分支跳过：`Geometry`
在上传前把它们规范化为显式 `LINES`/`TRIANGLES` indices，glTF
sync/async/extension 解析路径也在暴露 geometry 前执行同一转换；索引类型、非索引 primitive、短输入和重复调用均有测试锁定。

### 原生压缩纹理能力

`Renderer.supportsTextureCompression()` 是两个后端共享的显式能力入口。WebGL
2 根据对应扩展报告 BC、ETC1、ETC2、ASTC 与 PVRTC；WebGPU 在创建设备时启用 adapter 实际暴露的
`texture-compression-bc`、 `texture-compression-etc2` 和 `texture-compression-astc`，ETC1
source 复用 ETC2 native feature；ETC2/EAC 映射覆盖 WebGL 2 定义的全部 10 个 core
format。WebGPU 对 PVRTC 明确返回不支持，loader/示例据此跳过该 source，不做 CPU 解码、格式替换或空纹理回退。缺少 required
feature 的压缩格式在 GPU 分配前报错。只要选择 mipmap
filter，两个后端都要求层数恰好覆盖到 1×1，并校验每层 width/height；base-only 或 partial
KTX 可以配合非 mipmap filter 使用，超过纹理理论最大层数的容器则在 loader 阶段失败。KTX1
header 与每级 `imageSize`
都服从容器端序标志；无效标志、运行时生成 mip、截断 payload/padding 立即失败。格式、尺寸、压缩状态、image 与 mipmaps 只由容器决定，外部请求只能提供明确白名单中的 sampler、名称、UV 和生命周期选项，不能覆盖 texel
layout。

### 实例化、骨骼与 morph 边界

- WebGL 2 使用 instanced vertex attributes 传递每实例 model/normal matrix；WebGPU 使用 group 2 的
  `InstanceBlock`，由 `gl_InstanceIndex`
  读取固定数组，并在超过 128 个实例时拆成多个 draw。两个路径都由 `HILO_WEBGPU` 的编译期 shader
  variant 明确表达，不运行时改写 uniform 声明。
- WebGPU `InstanceBlock` 保存真正的 current/previous model array；WebGL 2 的现有 attribute
  budget 保持 current model，shader 把 previous alias 到 current，因而 WebGL 2 instanced
  draw 的 motion 输入确定为零，而不虚构跨帧 attribute
  stream。普通 draw、skinning 与 morph 在两端均携带真实 previous。
- `ModelBlock` 只服务普通 draw 或整个 batch 的公共数据；model-view 与 MVP 在 shader 中由
  `CameraBlock × ModelBlock` 推导，不重复上传 camera-dependent 派生矩阵。
- vertex geometry/morph attribute 按兼容格式打包到尽量少的 interleaved GPU buffer，并同时校验
  `maxVertexAttributes` 与 `maxVertexBuffers`；不能假设开发机暴露超过 WebGPU 保证下限的 slot。
- WebGL 2 根据 program 反射严格区分 converted 与 integer attribute：`ivec*`/`uvec*` 只使用
  `vertexAttribIPointer` 并校验 signed/unsigned storage，float/vector/matrix 才使用
  `vertexAttribPointer`；不允许 normalized 或符号不匹配的整数旁路。
- geometry decode 变换属于低频 `GeometryBlock`，绝不能因为 vertex shader 使用它就退化为每 model
  draw 重写。
- joint index/weight 和 morph target 仍是 vertex attribute；joint palette 进入
  `SkinningBlock`，morph weight 进入
  `MorphBlock`。骨骼、形变与实例化组合必须使用各自 revision，不能共享错误 pose。
- `SkinningBlock` 仅保存 current/previous 各 128 个 mat4，精确占用 16,384
  bytes，兼容 WebGL2 最低 uniform-block capacity。skin history validity 使用
  `ModelBlock.u_modelHistoryParams.y`，model validity 仍使用
  `.x`；两者独立判断，且不依赖 ModelBlock/SkinningBlock 解析顺序。首次使用、motion
  participation 间断、显式失效与设备恢复均保持无效历史；失败 submission 不提交待定姿态。

### ShaderMaterial 迁移

旧的数值 semantic/uniform 写法：

```ts
new ShaderMaterial({
    uniforms: {
        u_color: {
            get: () => [1, 0, 0, 1]
        }
    }
});
```

必须迁为注册过的 std140 block：

```ts
import {
    ShaderMaterial,
    UniformBuffer,
    createStd140Layout,
    registerUniformBlockBinding
} from 'hilo3d';

registerUniformBlockBinding('EffectBlock');

const effectLayout = createStd140Layout({
    color: 'vec4',
    strength: 'float'
});
const effectBlock = UniformBuffer.fromSchema(effectLayout, {
    color: [1, 0, 0, 1],
    strength: 0.75
});

const material = new ShaderMaterial({
    attributes: { a_position: 'POSITION' },
    uniformBlocks: { EffectBlock: effectBlock },
    vs: `#version 300 es
        layout(std140) uniform EffectBlock {
            vec4 color;
            float strength;
        };
        in vec3 a_position;
        out vec4 v_color;
        void main() {
            v_color = vec4(color.rgb * strength, color.a);
            gl_Position = vec4(a_position, 1.0);
        }`,
    fs: `#version 300 es
        precision highp float;
        in vec4 v_color;
        layout(location = 0) out vec4 outColor;
        void main() {
            outColor = v_color;
        }`
});

effectBlock.set('strength', 1);
```

如果自定义 shader 使用相机、场景或模型数据，应声明对应的 canonical
block，而不是复制一套同义散装 uniform。如果使用纹理，sampler 可以保留在 block 外；与纹理相关的其他数值仍放在 block 内。

## 构建、包与发布契约

### 构建格式

- 根入口 `hilo3d`：唯一的 ES2022 ESM 入口；`gl-matrix` 保持 external，便于依赖去重。
- `web-naga` 通过动态 import 进入发布模块图；主引擎 chunk 约 1.06 MB，约 2.05 MB 的 Naga
  JavaScript/WASM 作为独立 chunk，只在 WebGPU 初始化时请求，不增加纯 WebGL 2 应用的初始下载。
- 不发布 `require` condition、CommonJS/UMD 文件、浏览器全局对象或 namespace 声明镜像。
- ESM 模块产物带许可证 banner 和完整 source map；单一公共入口不等于把可选编译器内联为单文件。
- `.d.ts` 与 `.d.ts.map` 由 `src/Hilo3d.ts` 的真实依赖图生成，不存在手写镜像声明。

`package.json` 用 `exports` 明确入口，`files` 只允许发布
`dist/`、README、CHANGELOG 与许可证。Node 最低版本、npm 版本和 ESM 包语义都有机器可读约束。

### 包消费验证

发布校验覆盖：

1. `moduleResolution: "Bundler"` 的 ESM 类型消费；
2. `moduleResolution: "NodeNext"` 的 ESM 类型消费；
3. 根 ESM 的 Node 运行时导入；
4. 安装真实 tarball 后初始化 Naga WASM 并完成一次 GLSL→WGSL 转译；
5. publint 包结构检查；
6. Are the Types Wrong 导出条件检查；
7. `npm pack --dry-run` 文件白名单检查。

校验对象是构建后的真实包内容，因而能够发现错误 export
condition、缺失声明、错误扩展名、未发布依赖或多打包文件。

## API 文档与站点

API 工程有两个互补产物：

- `npm run docs:build` 使用 TypeDoc 从 `src/Hilo3d.ts`
  生成可浏览的 API 站点，并把本文件、中文 README、CHANGELOG 与贡献规则作为 project
  documents 纳入同一个站点；无效链接、未导出类型和文档警告都会失败。
- `npm run api:check` 使用 API Extractor 将当前声明与 [`etc/hilo3d.api.md`](../../etc/hilo3d.api.md)
  比较；公共签名意外改变会失败，计划内变更必须运行 `npm run api:update` 并审查 diff。

API Extractor 通过 `--typescript-compiler-folder node_modules/typescript` 使用项目锁定的 TypeScript
system declarations。这样 WebGPU DOM 类型、声明 emit 与 API 分析使用同一版本，不会让工具内置的旧
`lib.dom.d.ts` 猜测或漏掉 `GPU*` 公共类型；这保留全部 API 检查，只消除编译器版本漂移。

API Extractor 的 release-tag 提示按项目级固定政策关闭：Hilo3d
2.x 的根 barrel 导出面全部视为 public，不设置 alpha/beta 分层。setter 文档提示也按固定政策关闭：访问器说明由 getter/TypeDoc 作为唯一正文来源。两项都不关闭 TypeScript 诊断、forgotten
export、API 差异或 TypeDoc 验证，也不是待删除的迁移豁免。

`npm run site:build`
是本地和 CI 部署 API 文档的单一入口。它会先为核心包、粒子 addon 和物理 addon 构建并检查声明，再生成 TypeDoc、示例和相互链接的站点；工作流不得在未生成这三个包时直接调用依赖预构建产物的 API 检查。

typed lint 同样遵循干净 checkout 规则：工作流调用
`npm run lint`，由该命令先构建核心和 addon 声明，再执行 ESLint；不得自行组合会遗漏 workspace 声明的
`build:types` 和 `lint:built`。`check:modernity` 会强制检查这两项工作流契约。

唯一的 `Renderer`、backend-neutral `RenderTarget`、`Stage<'webgpu'>`、异步
`MeshPicker`、`ShadowCastingLightParameters`、`KTXTextureOptions`、资源诊断与压缩纹理 capability 从根 barrel 导出，因而同时进入
`.d.ts`、TypeDoc 与 API report。Concrete backend driver、target、Naga translator、native
manager 和 binding API 保持内部；不存在文档手写声称为公共接口的影子 API。包消费类型测试直接通过
`Renderer`/`RenderTarget` 使用 MRT、MSAA、readback、压缩纹理查询和资源诊断，不把 backend-specific
target 当成跨后端主契约。

Renderer 在首次 WebGPU device 创建和 device recovery 前复用已经完成 `await initialize()` 的
`ShaderArtifactCompiler`，把 mipmap GLSL utility 准备为 vertex/fragment Shader
Artifact 后显式注入具体设备。内部 `RHIFactory` 把 mipmap Artifact 定义为 WebGPU device create
options 的必填依赖，并在运行时再次校验；它和 WebGPU backend 都不反向 import
renderer/GLSL、不隐式启动 Naga，也没有 WGSL fallback。shader
module/layout/sampler 在 device 创建时建立，按 format 复用的 pipeline 与逐 mip/layer view、bind
group 在 texture allocation 时准备，因此 command execute 只编码 mipmap render pass。

`npm run site:build` 将维护的静态首页放在站点根路径，将 TypeDoc 输出放入
`/docs/`，将完整 Vite 示例构建放入 `/examples/`，并复制
`CNAME`。首页、文档和案例页互相提供导航入口。生成目录不提交到主工作树，由 Pages 工作流在 `dev`
分支更新后重新构建并通过 GitHub
Actions 部署。站点构建完成后还会扫描生成的 HTML/CSS，拒绝缺失的内部资源和依赖站点根路径的内部链接，确保自定义域名与 GitHub
Pages 子路径部署使用同一份产物。

## 测试体系

### 单元测试与覆盖率

单元测试运行于 Vitest Browser Mode + Playwright
Chromium，因此 DOM、Canvas 与 WebGL 行为来自真实浏览器上下文。WebGPU 资源管理器、std140/WGSL
layout、bind group、pipeline state、buffer packing 与 texture lifecycle 使用确定的 typed device
doubles 做边界测试；shader corpus 则加载真实 Naga WASM 完成 GLSL→WGSL 编译。测试初始化会把未预期的
`console.error`、shader compile/link error 与未捕获异常升级为失败。

纹理单元测试覆盖 2D/cube/3D/2D-array 的 descriptor 校验与 raw/external/compressed 上传分派、cube 显式 mip 顺序、managed
raw depth aspect、压缩边缘与 2×2/1×1 tail
extent，以及 200 次局部更新后快慢 backend 的 checkpoint 恢复；构造测试还锁定 CubeTexture/DataTexture/LazyTexture 均经过基类校验，并保留 LazyTexture 加载后的
`depth` 与 `wrapR`。

覆盖率统计显式包含
`src/**/*.ts`，仅排除声明文件；没有为了达到数字排除 loader、renderer、animation 等低覆盖模块。阈值是完整源码的无回退基线：`60/40/58/62`
分别约束statements/branches/functions/lines。后续改动不得降低基线；新增功能应同时提高相关模块覆盖率。

WebGPU device recovery 还有独立状态机测试，覆盖 target suspend/restore、同一 target
identity 与所有权恢复、重新获取 adapter、完整 capability 复核、released texture
backing 重放、恢复失败、过期 generation/device loss 回调、destroy 取消、`releaseGPUResources()`
后重新渲染，以及 shadow owner 在 debug/shadow/enabled/stage
membership 和 recovery 边界上的精确 prune，不以单纯修改状态字段冒充 GPU 资源重建。独立 Playwright
fixture 还在真实 Chromium/Dawn 设备上主动调用 `GPUDevice.destroy()`，要求 Lost → 新 adapter/device →
Restored 事件顺序正确、选中的 `RenderTarget`
identity 不变、已释放 texture 能重新上传，恢复后实际 draw/queue/readback 成功，且恢复前后 scene
pixel 逐字节完全相等并区别于 clear color。

### 96 个 HTML 的后端适用矩阵

Playwright 递归扫描 `examples/`
自动生成页面清单，不维护容易漏项的手工白名单。当前有 96 个 HTML，包含 94 个示例和两个画廊入口；81 个页面执行双后端，WebXR 执行 WebGL
2，14 个页面执行 WebGPU，共 177 个 page/backend 组合。WebGPU-only 范围包含 Bloom、两个 Clustered
Forward+ 灯光场景、动态 GI、体积光、大气天气、阴影驻留、SSR、TAA、四个 compute 场景和 GPU 粒子星云；准确路径由
`test/ui/example-paths.ts` 的 `WEBGPU_ONLY_EXAMPLE_PATHS`
与独立合同锁定。这些是创建前的显式能力边界，不是初始化失败后的 runtime fallback。

任何示例的后端适用范围发生变化时，必须同步更新
`examples/shared/catalog.ts`、`test/ui/example-paths.ts` 和 `test/ui/example-paths.contract.ts`
中的显式数量与例外集合，并在提交前运行
`npm run test:ui:contract`。画廊在任一首选后端下都展示完整 catalog，并为单后端条目标记 `WebGPU only`
或
`WebGL 2 only`；选择不兼容条目时，iframe 使用该条目唯一支持的后端。已有双后端页面改为单后端时，总 page/backend 测试组合数会减少一，但画廊条目总数保持不变。

画廊对 94 个示例逐项维护标题、用途、主题与后端要求；缺失元数据、重复路径、已删除页面的残留条目都会令合同失败。默认展示 25 个精选；主题数量、搜索与后端筛选始终限定在当前 Highlights 或 All
examples 集合内。主题数量同时反映搜索和后端条件，无结果的未选主题不显示；多词搜索支持顺序无关匹配及中文主题关键词。
`q`、`category`、`collection`、`compatible`
保存在画廊 URL 中，不传给示例；切换示例会清除上一个示例的专属参数。手机侧栏关闭后使用 `inert`
避免隐藏元素获得焦点，支持 `/`
搜索、Escape 关闭和 Tab 焦点循环。示例构建在场景脚本前注入轻量错误桥，将未捕获错误交给同源父画廊显示并提供重试；它不拦截或吞掉浏览器原始错误，也不替代真实 GPU 完成验证。静态发布不复制
`.blend`
创作源文件；它们继续保留在仓库；Markdown 授权与来源说明继续随静态资产发布。完整逐项用途与保留/清理决策见
[示例目录整理](../../documentation/EXAMPLE_CATALOG.md)。

每个组合都必须通过以下检查：

- 页面、ES module 与本地异步资源完成加载；
- 页面中存在 canvas 时，其尺寸有效且 `data-hilo3d-backend` 与请求后端一致；
- 至少一个真实 backend canvas 必须产生可见的非空帧；优先使用直接像素读取，若 WebGPU
  present 后浏览器禁止从 canvas 读取，则隔离该 canvas 并读取 Chromium compositor
  screenshot，不能用 DOM 完成标记或 draw counter 代替最终可见结果；
- 无 `pageerror`、非预期 `console.error`、失败 request 或 HTTP 4xx/5xx response；
- WebGL 2 组合必须观察到真实 draw；WebGPU 组合必须观察到 canvas texture acquisition、render-pass
  draw 与 queue submission，并且无 uncaptured error、device loss 或 DevTools rendering/validation
  error；GPU instrumentation 必须先等待两个稳定 animation frame，再对所有实际观察到的 `GPUQueue`
  执行 `onSubmittedWorkDone()`，成功后才允许终态采样；queue fence reject、uncaptured
  error 与非主动销毁的 device loss 都是硬失败，不允许延迟 validation event 越过门禁；
- loader 示例的模型与纹理请求真实完成。

额外交互门禁在两个后端分别点击 life-game canvas、拖动 ShaderToy、切换运行中 post-process
kernel、验证 GPU `MeshPicker` 返回同一对象，并让 glTF
Viewer 完成初次加载、GLB 替换和页面释放。这些用例不以 DOM
value 或“没有抛错”代替行为断言：操作前后必须出现新的 native draw，WebGPU 还必须出现新的 queue
submission；life-game 验证 render-target
attachment 的局部写入像素，ShaderToy 在冻结时间后比较 pointer 前后的离屏 readback，post-process 对同一 scene
texture 比较两套 kernel 输出，glTF
Viewer 则比较替换前后实际模型渲染的 readback。页面、request/response、console、DevTools
graphics 与 uncaptured GPU
error 门禁一直保持到交互和 pagehide 清理完成，最后再等待稳定帧做终态采样。video 示例在 WebGL
2/WebGPU 上都必须从本地媒体资源产生真实 draw，且无 GPU
validation 或 decoder-backing 上传错误。`renderTarget`、bloom、life-game 还会在各自适用后端以 DPR
1.25/1.5 下验证取整后的 backing size、真实 draw 和相同终态错误门禁，避免非整数尺寸进入 GPU
descriptor。CI 使用单 worker控制 SwiftShader GPU/内存峰值；没有重试或吞错来掩盖失败。

ShaderToy 的专用交互门禁会暂停 ticker，通过 320×180 offscreen
target 做两次显式 readback，断言彩色像素、pointer 坐标、输出 hash 变化和新的 native
draw/submit；普通示例门禁因此不再重复加载同一重型 ray-march 页面。这个唯一例外仍在两个后端保留 GPU
health、页面、网络、console、DevTools graphics 与终态稳定帧门禁，并由
`DEDICATED_RELEASE_TEST_EXAMPLE_PATHS`
契约锁定；通用门禁与专用门禁合计仍覆盖完整的 177 个 page/backend 组合。由于该 ray-march 交互在 GitHub
hosted runner 上成本过高，GitHub
Actions 明确跳过这一个专用用例；本地与发布前完整 Playwright 门禁仍会执行它。

### WebGPU 深度运行时测试

`npm run test:webgpu` 在 Chromium 中以最小 ANGLE/WebGPU 参数启用 SwiftShader
adapter。它属于本地/发布完整浏览器门禁；GitHub hosted Linux 的 SwiftShader 无法稳定完成 canvas
presentation，因此 hosted CI 不伪装执行这条通道。测试创建真实 `GPUAdapter`、`GPUDevice`、canvas
context、Naga shader module、bind group、render pipeline 与 command
encoder，在同一帧覆盖 Basic/PBR、`InstanceBlock` 批处理、带 primitive restart 和局部更新的 Uint8
indexed strip、mipmap 2D texture 替换、directional/spot/point 三类阴影、4× MSAA/stencil offscreen
target、双 attachment MRT、fullscreen present 与对齐 readback。测试同时断言 backend、draw
count、face count、attachment/sample count、texture revision、非零像素、GPU validation
error、页面异常和控制台错误；它不是只检查 `navigator.gpu`、只 mock device 或只测试 WGSL 字符串。

同一真实浏览器闭环还通过 managed `Texture` 创建 3D、2D-array、unsigned-integer
array、depth-array、numeric depth 与动态 sampler-array 资源，让 Naga 转译包含 `sampler3D`、
`sampler2DArray`、`sampler2DArrayShadow`、`usampler2DArray`、普通 depth `sampler2D`
和 UBO 动态索引的 GLSL ES 3.00，随后建立真实 bind-group layout、bind group 与 render
pipeline，执行 draw、queue submit 和 buffer map。门禁要求 shader compilation message 与 GPU
validation error 均为空，全部纹理解析出的 dimension 正确，并且 1×1 输出像素精确为
`[64, 128, 200, 255]`；这证明能力不是只在 translator 或 fake-device 单测中存在。

这项专用 fixture 是普通双后端示例之外的深度能力测试，不是唯一的 WebGPU UI 覆盖。同一套 `test:webgpu`
还让压缩纹理示例分别通过两个后端渲染各自声明支持的 source，确认 WebGPU 原生 BC/ETC2/ASTC 路径且明确跳过 PVRTC，并让异步 GPU
`MeshPicker` 在两个后端选择同一 mesh。

G0/L0 的 renderer browser fixture 直接构造注册的 opaque `Mesh`、GPU Scene
bucket 与动态局部光，验证 previous-frame Hi-Z、frustum/occlusion/LOD/compact、fixed indexed-indirect
arguments、3D cluster allocator、storage GGX PBR 与 diagnostics
readback。该测试不把 diagnostics 数据用于 draw 或 light
allocation，也不替代受控硬件上的 100k/10k 性能 baseline。

Compute/storage 另有 WebGPU-only showcase/acceptance fixture：它通过公共 SRP 记录 depth
prepass、sampled-depth tile culling、Scene group-3 readonly storage、Gaussian
reorder 与 1024 粒子 Hilo3D wordmark 的 fractal value/curl
noise、呼吸、涡旋、回归和 compaction，再用 GPU-generated indirect draw 产生 Gaussian 与 additive
particle glow。门禁检查真实 compute pass/dispatch/indirect draw、最终 readback 和 reload
determinism；只允许最后一次颜色验收 readback，不把 visible count、排序或 indirect
arguments 映射到 CPU。Forward+/Gaussian 算法仍是 acceptance-scale，页面也不是性能 baseline。

独立的 `compute_particles.html` 继续复用同一公共路径，把 65,536 个持久 GPU body 分成 4096 个 Hilo3D
word-lattice 粒子和 61,440 个分层星空、极光/星云与 cyber-dune deep-field body；三 octave value/curl
noise、回归力、轨道力场、低频流星头部碰撞和尾迹力场、边界反弹与鼠标磁吸/shockwave/vortex 都在一次 Direct
WGSL compute 中更新，再由三次 indirect draw 分别绘制 deep
field、additive 速度 halo 与 alpha-blended 发光 core。专用 Playwright 门禁发送真实 pointer 事件，检查上下坐标映射、字形采样覆盖和边缘背景亮度，冻结步进后比较确定性 readback
hash，并检查 compute dispatch、indirect draw 和终态 GPU validation；页面不把粒子状态映射回 CPU。

`compute_raytracing.html` 则用一个 renderer-owned storage buffer 跨帧累积 HDR
radiance/normal/depth，由 Direct WGSL
compute 对解析球体、SDF 倒角方块、静态水面和 SDF 挤出/倒角的 Hilo3D 水晶字形求交，并完成五次 bounce、Fresnel 反射/折射、吸收、RGB 分波段色散、面积光软阴影、球透镜与文字投影光谱焦散、带可见性测试和 HG 相函数的单次体积散射及 Russian
roulette。公共 storage-aware GPU-driven pass 执行边缘感知降噪、anamorphic
bloom、ACES 与 present；拖拽环绕或滚轮 dolly 会重置累积，全程保持同一个 registry-stable bind
group。专用 Playwright 门禁验证真实 compute/dispatch/draw、渐进 readback、原生 bind-group 创建数稳定与终态 GPU
validation，且源码不访问 native WebGPU 对象。

WebGPU shader
corpus（包括 present/mipmap 内部 pass）与真实运行时测试是互补门禁：前者扩大 feature/variant 覆盖，后者证明浏览器端 Naga
WASM 加载、pipeline creation、draw 与 queue submission 端到端可用。

### 可选物理 GPU 门禁

`npm run test:webgpu:native` 使用独立的 `chromium-native-webgpu` Playwright
project：它不带任何 SwiftShader adapter/ANGLE 参数，禁用软件光栅器，给每次 `requestAdapter` 强制传入
`forceFallbackAdapter: false`，并拒绝 `adapter.info.isFallbackAdapter` 不是 `false`
或指纹明确属于 SwiftShader、llvmpipe、lavapipe 等软件实现的 adapter。通过 adapter 校验后，它复用正式 WebGPU 深度 fixture，要求 draw、submit、所有 queue 完成、device
recovery、纹理重放与 readback 全部成功。

物理 GPU 是否暴露给 Chromium 取决于 runner 驱动和宿主环境，因此这条命令不属于可移植的
`npm run validate`，也不会伪装成普通 GitHub-hosted CI 的必跑门禁。`.github/workflows/native_gpu.yml`
只支持手动触发，并要求带 `self-hosted`、`linux`、`gpu` 标签的 runner；默认 hosted
CI 通过 SwiftShader native/offscreen RHI 测试覆盖 WebGPU 核心，不声称具有 WebGPU
presentation 能力。WebXR 明确不属于该 WebGPU 通道。

### 视觉回归

视觉套件使用固定 viewport、UTC、英文 locale、固定 device
scale、禁用动画的 Chromium 和 SwiftShader。同一个确定性灯光 PBR 场景分别通过 WebGL
2 与 WebGPU 渲染；除截图外还断言 readback 背景像素、变换后像素数、方向光覆盖和前景颜色数量，并在运行时要求两个后端的首帧截图逐字节相同。两个后端仍各自保存
`test/ui/__screenshots__/`
基线以定位单后端回归；失败时保留 screenshot 与 trace，本地运行还会保留 video。GitHub hosted
CI 只运行稳定的 WebGL 2 visual baseline，完整双后端视觉与像素 parity 属于本地/发布或物理 GPU 验证。

## CI 与站点发布

CI 只保留项目最低版本 Node 20.19.0 这一个测试档位，使用当前维护的 GitHub
Actions、锁文件安装、固定 npm 10.9.4 和显式 Chromium 系统依赖，避免在 Node
20/22/24 上重复运行同一套高成本 GPU 矩阵。PR、`dev`、`master`
与版本 tag 先执行 modernity、格式、声明、lint、TypeScript project
references 和示例目录合同预检；预检成功后并行执行两个 Vitest
coverage 分片、RHI/架构、包/API/文档，以及六个 Playwright WebGL 2 页面/交互/视觉工作组。每个 GPU
job 内仍只使用一个 worker，不在同一 SwiftShader 进程并发争用设备；coverage 模式因此关闭 Vitest 文件并行，同时保留跨 runner 的两个分片。coverage
artifact 保留上传时的仓库相对目录，汇总 job 从其嵌套 `reports/vitest` 目录只读取 blob
report；跨 runner 分片完成后分别合并 coverage 和 Playwright 报告，并由稳定的 `Required CI`
聚合门禁统一给 branch
protection 使用。预检导致测试分片跳过时，报告汇总也跳过，避免缺失产物掩盖最初的失败。Playwright
presentation/UI 使用 `channel: chromium` 的完整 Chromium headless
compositor，保留 SwiftShader、单 worker、零重试和既有像素/物理/交互断言；不使用独立的 headless
shell。示例 HTML 由 Vite 统一声明空 favicon，避免浏览器自动请求不存在的 `/favicon.ico`。Linux hosted
runner 的 SwiftShader 会在 coverage instrumentation 下销毁 storage-aware
raster 的真实设备，因此只有对应的一项真实设备集成测试在 GitHub Actions
coverage 中跳过；本地 coverage 仍执行该测试，portable storage/RHI 合同继续由独立 RHI job 验证。

### 动态 GI 浏览器验收

`dynamic_global_illumination_atelier.html` 是 WebGPU-only 专项案例，进入 `test:webgpu` 和
`test:webgpu:native`，不重复进入普通 gallery 首帧矩阵。真实像素覆盖 GI 开关、独立灯具移动、门/墙色变化、截图后继续交互和销毁后的错误观察。该专项在本地也关闭视频，保留 trace、失败截图和显式 compositor 像素证据：同机 SwiftShader 首帧在视频录制下超过 120 秒，关闭视频的完整交互用例约 21 秒，诊断运行的 21 帧预热在 20 秒内完成。测试没有放宽像素阈值、增加 retries 或省略真实提交。原生 Metal 美术对照使用 production 分辨率；它与软件 GPU
CI 的正确性证据分开。

### 重型浏览器测试的维护约定

`HILO3D_UI_GROUP` 将同一 hosted UI 命令分成
`catalog`、`catalog-scenes`、`csm`、`physics`、`post-processing` 和
`chromatic`。默认不设该变量时仍运行完整矩阵。分组规则位于
`scripts/playwright-ui-groups.ts`；预检执行
`npx jiti scripts/check-ui-groups.ts`，用 Playwright 实际发现的测试 ID 验证六组的并集等于完整 WebGL2 清单，且没有重复或空组。新增测试必须进入恰好一组，新增工作组时同步修改规则、workflow
matrix 和清单检查。Chromatic 与 SSGI
chapel 已由双后端专项覆盖，不再重复运行通用首帧门禁；SSGI 专项继续验证启用/关闭后的像素差异、页面错误和 GPU 健康。

`catalog-scenes` 承担自动发现的 HTML 原生绘制/像素验收；`catalog`
承担画廊导航、2D 交互、运行时交互和基础视觉用例。拆分依据为同类 GitHub hosted
runner 的耗时产物：dev 的 run 34704509285 中，原 catalog
87 个用例累计 1424.9 秒，其中 72 个通用页面用例占 1146.5 秒；PR #125 的 run
35451054903 在新增画廊用例后超过 1500 秒整组期限。两个分组继续各使用一个 GPU
worker、独立 blob 文件和原有逐例/整组超时；清单检查保证每个用例恰好运行一次。画廊筛选/URL 状态测试使用轻量 iframe 文档，避免仅验证后端选择菜单时在 hosted
WebGL2 通道启动真实 WebGPU
presentation；原有双后端画廊集成和全部页面的原生绘制、像素、GPU 健康检查保留。

使用 `createExampleContext()` 的示例在显式 `?test=1` 时提供共享截图控制，正常页面不暴露该控制。
`test/ui/stable-capture.ts` 先确认真实 native draw，再暂停 ticker、等待 renderer
submission 完成、采集 compositor 像素，并在 `finally`
中恢复 ticker；queue 或截图失败也恢复。Physics、CSM、Chromatic 共用此路径。测试模式还通过
`examples/shared/test-frame-control.ts`
等待上一帧提交完成，再留出 50ms 输入处理窗口，避免点击、状态读取和断言期间持续提交 SwiftShader 帧。截图持有暂停状态时，后台 fence 完成不能提前恢复 ticker；页面销毁后也不能重新恢复。Physics 使用真实 elapsed
time 推进仿真，保持现有动作和物理断言。CSM 的虚拟时钟专项显式使用 `testClock=1`，由 Playwright
clock 控制 RAF/timer，不让真实 GPU fence 阻塞虚拟时间推进；其余 CSM 用例使用提交限流。Chromatic 的
`test=1` 模式通过专用 `advanceFrames()`
先跨过浏览器 resize/RAF 事件边界，再固定推进真实 Stage 帧并等待提交，测试断言帧数精确增长、原生 draw/pass 和像素变化，正常页面仍由 ticker 连续驱动。Physics 测试使用 512px 阴影图，CSM 保留其等预算阴影对比规格，Chromatic 保留实际后处理链。随机种子、动画相位和分辨率按示例已有合同控制，禁止全局替换随机数或时钟以掩盖时序错误。

CSM 和 Physics 截图前等待两个实际 ticker 帧，不能以 RAF 回调数代替已渲染帧数；限流期间 RAF 仍可执行，但场景可能尚未更新。等待者在页面销毁时会被清理并拒绝。

UI 默认关闭 trace 连续画面采集，保留 DOM、操作、源码和网络记录；CI 关闭视频，保留失败截图和显式像素断言。涉及像素和 presentation 的工作组继续使用完整 Chromium；无像素要求的合同由 Node 或独立 RHI
lane 检查。 `scripts/playwright-timing-reporter.ts` 将每例耗时、超时预算及结果写入
`reports/ui-timings`，上传保留 14 天，并在 Actions
summary 列出最慢 20 项。耗时达到预算 70% 时发出诊断提示；这些结果不是 GPU 性能基线。每组 Playwright 总期限为 25 分钟，早于 Actions 的 30 分钟 job 期限，给失败报告和产物上传预留时间。调整超时前应比较同 runner 的多次记录，确认耗时分布并检查 trace；禁止通过重试、跳过或更新视觉基线消除失败。

修改重型示例或截图工具后，至少运行受影响的完整工作组，例如：

```sh
CI=true HILO3D_UI_GROUP=physics npm run test:ui:webgl2:ci -- --reporter=line,./scripts/playwright-timing-reporter.ts
```

共享截图工具变更需补充 WebGPU 验证；CI 编排变更需通过清单检查。运行本地多个工作组时用
`HILO3D_PLAYWRIGHT_PORT` 分配不同端口，优先串行 GPU 检查，避免本机资源竞争污染耗时判断。

WebGPU 由独立 RHI job 中的 native/offscreen SwiftShader
lane 验证 adapter、device、pipeline、draw、submit、readback 与 backend contract。GitHub hosted
Linux 不稳定的 WebGPU canvas presentation 不作为虚假的合并门禁；完整双后端页面/视觉矩阵仍由本地
`npm run validate`、`npm run release:check` 与手动 physical-GPU workflow 负责。串行的
`npm run validate:ci` 保留为 hosted 门禁的本地复现入口，过期任务由 concurrency 自动取消。

Portable RHI benchmark smoke 只以单 draw 生产场景验证 fixture、allocation
profiler、readback 和当前 RHI 路径。它不属于普通合并门禁；独立 workflow 仅在 benchmark/performance 路径变化、每日定时或手动触发时运行 WebGL
2 production smoke。GitHub hosted Linux 的 SwiftShader WebGPU production
fixture 会在首帧前丢失 fallback device，因此 WebGPU 仍由独立 `test:rhi` native
lane、本地完整 UI/视觉矩阵和已登记 M3 Max macOS/Metal physical-GPU
benchmark 覆盖。正式采集固定 Node、Playwright/Chromium 二进制、系统/Metal 指纹，并在每次审计、采集与冻结前要求接入交流电、High
Power
Mode 和无热/性能告警。面向小型引擎的全量正式采集固定为 3 轮、每轮 120 帧预热和 500 帧 CPU/GPU 采样，不作为优化开发的逐次内循环；开发中使用 production
smoke 与受影响的定向测试，只有建立或替换跨提交不可变证据时才执行全量矩阵。分配证据保留逐字节采样，但在固定 profiler 预热与丢弃的稳定探针后让每个真实帧使用独立 CDP
profile，避免高 draw 场景产生无界响应。绝对分配门禁只由单 draw 哨兵承担；压力场景仍记录分配数据供跨提交分析，不把 V8
sampler tier
metadata 的波动误判为引擎回归。正式采集输出 scenario/backend/round 阶段进度，为每个浏览器阶段设置失败关闭超时，并在无响应时中止 owned
heap-profiler session。脚本直接运行时默认按 WebGPU、WebGL
2 顺序为每个 scenario/backend 启动独立 Chromium，且非证据 SwiftShader 用例不申请正式 rig 才需要的 WebGPU
`timestamp-query`。smoke 始终标记为 non-evidence，不替代物理 GPU 多场景冻结基线；需要扩展诊断时显式使用
`--all` 入口。定向 churn
smoke 默认覆盖一个完整普通 mesh 替换周期后结束，只有验证 10,000 帧完成行为时才传
`--full-churn`；正式登记采集始终完成全部 churn 帧。

站点发布工作流同样从干净 checkout 执行 `npm ci`，再通过自包含的 `npm run site:build`
完成核心与 addon 声明构建、API 检查和站点生成后部署 artifact。仓库不再跟踪旧 API 生成物，也不会从开发者本机的残留目录发布。

## 唯一验收入口

```sh
npm ci
npx playwright install chromium
npm run validate
```

`validate` 按顺序执行：清理生成物、旧 JavaScript/旧工具配置门禁、格式检查、typed
lint、全部 TypeScript project
references、浏览器单测与覆盖率、库构建、两类 ESM 类型消费、96 个 HTML 后端适用矩阵（81 个双后端、WebXR 显式 WebGL
2-only、14 个 compute、Clustered、GI、时序与粒子页面（包括 Bloom）显式 WebGPU-only）、双后端交互、WebGPU 深度运行时、双后端视觉回归、全部示例构建、TypeDoc 验证、API 签名比较、npm 包契约验证和 pack 文件检查。任一步失败都会阻止 CI 与发布。

其中 shader 静态门禁会扫描 `src/shader/` 和示例中的 shader 源码：禁止 GLSL 1.00
`attribute`/`varying`、`texture2D`/`textureCube`、`gl_FragColor`/`gl_FragData`、WebGL 1 shader
extensions，以及 block 外的 non-sampler `uniform`。现代性门禁只允许与 `new ComputeShader(...)`
结构化关联并由 compiler/Naga 验证的 Direct WGSL `@compute`；`@vertex`/`@fragment` graphics
WGSL、普通 Shader 中的 GLSL ES 3.10 和未受控 raw source 继续禁止。`StorageGraphicsShader`
是唯一 GLSL ES 3.10 readonly-storage 入口。扫描范围同时覆盖生产 `.vert`/`.frag`/`.glsl`/`.wgsl`
源文件，不允许通过非 TypeScript 扩展绕过。运行时门禁会再次在 program link、Naga
translation 或 WebGPU pipeline creation 时拒绝漏网接口，静态规则、WebGL 2 link、Naga
corpus 与真实 WebGPU pipeline 互为补充。

## 验收清单

- [x] 一方源码、示例、测试与脚本没有类型检查绕过或显式 `any`。
- [x] `check:modernity` 阻止 `.js/.jsx/.mjs/.cjs` 实现、旧工具配置、CommonJS
      require/source 改写、WebGL 1 core-extension wrapper、类型/lint/coverage suppression 与显式
      `any` 回流。
- [x] 源码与发布物使用原生 ESM、原生 class 与 `EventDispatcher`；动态类/mixin API 已删除。
- [x] TypeScript project references 隔离 lib/test/examples/node 环境并严格检查全部一方代码。
- [x] typed ESLint、Prettier 与 EditorConfig 覆盖整仓。
- [x] 公共声明从源码生成，API 文档与 API report 同源。
- [x] 单一 ESM 入口、类型、source map、package exports 与真实 tarball 一致。
- [x] 浏览器单测执行完整源码覆盖率门禁，阈值面向 `src/**/*.ts`，不排除 renderer 或 WebGPU 核心目录。
- [x] 自动清单包含 96 个 HTML；81 个通过 WebGL 2 + WebGPU，WebXR 显式 WebGL
      2-only，14 个 compute、Clustered、GI、时序与粒子页面（包括 Bloom）显式 WebGPU-only；适用组合都经过页面、请求、控制台与 GPU 错误门禁。
- [x] 同一确定 PBR 场景和关键交互在两个后端都有 readback/截图或行为断言；WebGPU 不只依赖独立 fixture。
- [x] `Stage.create()` 默认用无 device/resource 分配的 adapter probe 实现 WebGPU-first `auto`；显式
      `webgl2`/`webgpu` 不切换，auto 选中 WebGPU 后的真实初始化错误也不会触发回退。
- [x] WebGPU device
      lost 会重新获取等价 adapter、复核 capability，并自动恢复 device/context/manager、released
      texture backing 与原 target identity；恢复期安全跳帧、失败后显式抛错、destroy 可取消。
- [x] 可移植 raster shader 只有 GLSL ES 3.00 source of truth；材质、阴影、present 与 mipmap utility
      pass 都在引擎预处理后由 Naga WASM 生成 WGSL。Direct WGSL 只存在于受控 WebGPU
      compute，storage-aware raster 只存在于受控 GLSL ES 3.10 readonly-storage contract。
- [x] 生成 WGSL 在默认 WebGPU language feature 上保留 std140 ABI，不依赖可选
      `uniform_buffer_standard_layout`。
- [x] Naga
      corpus 覆盖 Basic/PBR、灯光、阴影、skinning、morph、instancing、quantized、present 与 mipmap
      variant。
- [x] Playwright 在真实 WebGPU adapter/device/pipeline 上完成一帧 draw、submit 与 queue
      fence，不以 mock 冒充 UI 验收；另有不混入默认 CI 的可选物理 GPU 通道。
- [x] 内置数值 shader 数据进入固定 std140
      ABI；WebGPU 按 global/material/object/custom 四组和更新频率绑定。
- [x] WebGPU instancing 使用 `InstanceBlock` 分批，阴影使用 comparison depth atlas 与 `LightBlock`
      元数据。
- [x] ShaderMaterial、自定义 loader shader 与全部示例遵守 UBO/sampler 边界。
- [x] GLSL ES 3.00 的 3D、2D-array、array-shadow 与 signed/unsigned integer sampler
      family 全部经过 Naga、真实 bind group/pipeline、draw/submit 和精确像素 readback 闭环。
- [x] managed `Texture` 在双后端统一支持 2D/cube/3D/2D-array 与 integer format；integer/3D mip
      chain、nearest/anisotropy 约束、compressed 3D 拒绝和 compressed 2D-array 路径都有自动测试。
- [x] `updateSubTexture()` 只有 descriptor API，四类 target 的 raw/external/compressed update、cube
      mip 顺序、depth 原始上传、压缩尾级 block extent、有界历史与慢 backend
      checkpoint 恢复都有自动测试。
- [x] `Renderer`/`RenderTarget` 为双后端统一实现，覆盖 MRT、1×/4× MSAA、sampleable
      attachment、resize、render-to-target 与异步 readback；PostProcess 和 MeshPicker 不含 backend
      fallback。
- [x] 公共 `Framebuffer`/`LightShadow`/`CubeLightShadow`、灯光 `lightShadow` 与 renderer-owned
      framebuffer 配置已删除，没有临时兼容入口。
- [x] WebGPU 原生压缩纹理按 adapter
      feature 支持 BC/ETC2/ASTC，并明确拒绝 PVRTC，不替换空纹理或伪解码。
- [x] 后端中立 `getDiagnostics()` 替代并删除会探查 WebGL cache 的 `logGLResource()`。
- [x] std140 offset/stride、固定 block binding、dirty-range upload 和非法 classic
      uniform 有自动测试。
- [x] 旧构建、测试、文档生成和运行时 vendor 链路已删除。
- [x] CI 只验证固定的最低 Node 20.19.0 档位；功能提交 push 时运行完整门禁，npm tag 发布不重复执行。

## npm 发布生命周期

版本提交的功能 CI 通过后，创建并推送 npm 发布 tag：

```sh
npm run release:tag:push
```

`release:check` 保留为完整 `validate`
的显式别名，可用于本地候选版本验收，但不再是 tag 发布步骤。功能提交 push 触发的普通 CI 负责单元、覆盖率、RHI、浏览器、视觉、文档、API 和包消费门禁；发布操作应在该提交的 CI 通过后进行。

`release:tag:push` 要求工作区干净，确认根包与两个 addon 的版本完全一致、可作为 Git tag，并拒绝本地或
`origin` 上指向其他提交的同名 tag。命令创建带 `publish <version>` 注释的版本 tag，只推送
`refs/tags/<version>`，再从远端核验其目标提交；同一提交上的重试是幂等的。

`.github/workflows/publish.yml` 监听版本 tag。GitHub-hosted
runner 只用仓库固定的 Node/npm 工具链确认 tag、三个包版本与提交完全一致，不重复执行普通 push
CI 已经覆盖的功能门禁；随后切换到 npm Trusted Publishing 支持的 Node 24/npm
11，通过 OIDC 依次发布核心、粒子 addon 和物理 addon。两个 addon 的 `hilo3d` peer
dependency 必须等于同批版本；幂等重试跳过 registry 中已经存在的包版本。预发布版本自动使用
`next`，正式版本使用 `latest`，不得让 prerelease 覆盖 `latest`。npm package 的 Trusted
Publisher 必须为三个 npm 包分别绑定 `hiloteam/Hilo3d` 与 `publish.yml`，允许
`npm publish`；workflow 只授予 `contents: read` 与
`id-token: write`。如 registry 只接受了部分包，可通过 workflow dispatch 传入已有 `release_tag`
重试；workflow 会检出该 tag，并跳过 registry 中已经存在的同版本包。

`npm publish` 的 `prepublishOnly` 仍运行轻量 `publish:check`：现代性门禁和 tag/commit 复核。Hilo3D
Skill 由 `validate` 与 `validate:ci` 中的 `test:skill` 回归，不在上传阶段重复高成本矩阵； `prepack`
从 tagged source 重新构建 JS、source map 和声明。

## 后续维护规则

1. 禁止新增类型、lint、测试或 API 检查的临时豁免；无法表达的领域结构应先补类型模型。
2. 公共 API 变更必须同时更新源码 TSDoc、测试、API report、CHANGELOG 与消费测试。
3. 新增示例必须进入 Vite MPA 和自动 Playwright 页面清单，并默认通过 WebGL 2 +
   WebGPU；只有 WebXR 这类浏览器平台边界或 compute/storage 这类正式公开为 WebGPU-only 的 capability 才能显式列为单后端例外，例外不得实现失败后回退。不得以远程脚本或全局变量绕过依赖管理。
4. 渲染行为变化必须审查视觉 diff；只有确认是预期变化时才更新基线。
5. 覆盖率阈值只能保持或提高，不能通过扩大 exclude、空断言或无意义执行来提升。
6. 不提交
   `dist/`、`dist-examples/`、`docs/`、`site/`、coverage 或浏览器报告；发布只接受 CI 现场生成物。
7. 新工具必须替换旧工具的职责，不允许两套活跃构建、测试、文档或发布链路长期并存。
8. 不得恢复动态类/mixin
   API、backend-specific 生命周期别名、CommonJS/UMD 构建或浏览器全局入口。不得在 Vite/Node 工具中读取并改写第三方 UMD/CommonJS 源码来制造 ESM 入口。
9. 不得新增 WebGL 1/GLSL 1.00 兼容源码、手写 graphics WGSL 镜像或 block 外 numeric uniform；portable
   raster（包括 renderer-owned present、mipmap 与后续 utility
   pass）必须继续执行“当前 variant 预处理 → Vulkan GLSL 4.50 → Naga → WGSL”。Direct WGSL 只能通过
   `ComputeShader`，storage-aware raster 只能通过 `StorageGraphicsShader` 的受控 GLSL ES 3.10
   contract。
10. 不得让 WebGPU 失败后静默创建 WebGL 2
    renderer，也不得通过关闭 feature、跳过 pass、替换空纹理或吞掉 validation error 伪造成功；device
    recovery 只能在同一 WebGPU 后端内重建资源，恢复失败必须保留 cause 并进入显式失败态。可选能力必须成为显式 API、capability 或错误。
11. 新增 uniform block 必须先分配全局稳定 binding、定义 std140
    schema、补 offset/size 测试，并标明 owner、更新频率和 dirty
    revision；不能按 program 反射顺序临时占号。
12. 新增 portable raster shader feature 必须同时进入 WebGL 2 compile/link、Naga
    corpus 和适当的 WebGPU pipeline/UI 测试；WebGPU-only compute/storage feature 必须进入 Direct
    WGSL/GLSL ES 3.10 compiler、portable RHI、真实 WebGPU 与 WebGL 2
    negative 门禁。只让某个 backend 通过字符串快照不算完成。
13. 新离屏、后处理、拾取和 readback 功能必须建立在公共 `Renderer`/`RenderTarget`
    上；不得新增只服务单后端的业务层 framebuffer 包装、CPU fallback 或静默能力替换。资源工具必须消费
    `getDiagnostics()`，不得恢复 backend cache 日志探针。
14. 新增 WebGPU
    device-owned 资源必须接入 suspend/restore/destroy 生命周期，并用测试证明恢复后公共对象 identity、所有权与当前 target 不变；CPU
    source 允许公开释放时必须提供私有、可清理且真实恢复测试覆盖的 backing，不能依赖旧 adapter 或不可观察的空重建。
