WebGPU 概念
ZenFG 是面向 WebGPU 和 wgpu 的横向 GPU 调度基础设施。本指南介绍 @zenfg/webgpu。Rust crate 自行维护其使用和生命周期约定。确切的 API 签名请以随包发布的源代码文档为准。
系统边界
ZenFG 负责声明、编译、诊断和执行图可见的 GPU 工作:
- 逻辑资源、视图、访问和依赖;
- 保留根以及无用工作的裁剪;
- 保留后的执行顺序和不透明的提交边界;
- 瞬态生命周期分析、别名计划和运行时资源池;
- 校验和可选的诊断报告。
应用或渲染器继续负责:
- 场景、材质、摄像机、管线、绑定组、采样器和着色器;
- 设备、队列、表面、当前纹理的获取和呈现;
- 长生命周期资源策略、资源内容、尺寸调整和设备丢失;
- 具体的绘制、分派、复制操作以及第三方渲染器行为。
renderer features / compute systems / third-party engines
|
graph declarations
|
@zenfg/webgpu or zenfg runtime
|
WebGPU / wgpu device + queueTypeScript 和 Rust 运行时共享语义和 Snapshot 投影,但不保证源代码层面的 API 一一对应。在两种语言间迁移工作流时,调用方应转换其意图,而不是逐个照搬方法名。
Snapshot 是独立的协议边界:
@zenfg/snapshot负责协议的规范性定义、JSON Schema、测试夹具和一致性测试集。zenfg-snapshot是与 wgpu 无关、与之匹配的 Rust 线协议模型和编解码器。@zenfg/inspector消费 Snapshot 数据,不依赖任一 GPU 运行时。- 运行时报告对象属于实现 API;Snapshot 才是可移植的交换格式。
录制与执行
长生命周期的运行时会为每个逻辑帧创建一个只能使用一次的录制对象:
TypeScript: FrameGraph(device) -> beginFrame() -> recorder -> compile() -> execute()无论编译成功还是失败,编译都会消耗这次录制。逻辑句柄、视图和类型化访问令牌都属于该次录制,不能跨录制边界使用。
编译会保留录制顺序的稳定子序列。ZenFG 可以移除不可观察的工作,但不会重排或合并节点。系统不会自动分析不同帧之间的依赖;各个编译后帧之间的关系取决于应用策略和队列提交顺序。
只要所有捕获的回调和导入的原生对象仍然有效,TypeScript 运行时就可以有条件地重新执行编译后的帧。包含当前表面纹理的编译帧通常只属于单个帧周期,两个运行时都是如此:每个新的呈现帧都要重新获取、导入、编译、执行并呈现一张新的当前纹理。
原生工作会同步编码并提交。可选的 GPU 计时可能异步完成,但这不会让节点回调或外部提交回调变成异步操作。
资源与内容
ZenFG 区分三类所有权:
- 瞬态资源是逻辑纹理或缓冲区,其原生存储只在一次编译后执行期间存在。运行时负责分配并将其纳入资源池。
- 导入资源借用调用方拥有的原生存储。ZenFG 绝不会销毁该存储或将其纳入资源池。
- 表面资源代表刚获取的呈现目标。它们会被导入以便在图中使用,但获取和呈现仍由应用负责。
在帧组合边界导入一次共享原生存储,再把同一个逻辑句柄传给所有参与的子系统。没有图可见数据流的状态,例如摄像机对象或管线缓存,通常应留在图外。
选择资源声明粒度
声明粒度由调用方根据工作负载的组织方式、调试需求和图的复杂度决定。对于复杂的图,如果公开某些资源并不会带来有用的图依赖、资源管理能力或可观察结果,可以考虑让工作负载在内部管理它们。这样主数据流会更易读。简单示例、教学内容或细粒度资源诊断仍适合使用更显式的声明;这是一项建议,不是新规则,也不是新的校验模式。
| 情况 | 建议的选择及原因 |
|---|---|
| 固定的推理权重和算子参数 | 可考虑在内部绑定,以免只读输入淹没主数据流。 |
| 由 CPU 更新并被多个通道读取的摄像机或帧 uniform | 内部绑定是合理的;如果需要检查其使用范围,也可以显式导入。 |
| 在单个工作负载内复用的临时缓冲区 | 可以在内部管理;需要检查图分配、内容校验或资源诊断时再公开。 |
| 简单示例或教学示例中的 uniform | 显式声明有助于展示导入、访问令牌和所有权。 |
| 调查哪些通道使用某项资源 | 添加相关声明,以取得访问和资源信息。 |
| 一个通道写入、另一个通道读取的纹理 | 保留声明来表达实际依赖。 |
| 历史状态、回读和呈现结果 | 保留相应的声明和根,说明为何必须执行相关工作并使其免于裁剪。 |
选择声明粒度不会放宽现有的正确性要求。不要隐藏图为了排序或管理所需的依赖。一旦资源属于图约定,就要完整、准确地声明其访问,包括写入的内容语义。该约定之外的资源,其生命周期、初始化和内部访问安全仍由调用方负责。工作负载内部的分派顺序也仍由工作负载自己负责。
例如,Monocular 在推理期间公开历史数据、稳定范围、重建表面和输出目标;权重和内部临时工作区仍由工作负载管理。稳定图像帧只需公开表面和输出。Slime Mold 会公开 agents 和 trail 纹理,但在内部绑定其三个 uniform。“Imported Resource”教学示例则有意公开仅供单个通道使用的 uniform,用于演示借用和显式输入校验。
显式声明并不等于完整的 GPU 跟踪。Inspector 统计的是已声明的图资源及其访问,不会自动扫描着色器绑定,也不会测量 GPU 内存总量。
内容有效性
内容有效性按规范化后的纹理子资源或缓冲区字节范围跟踪:
- 瞬态资源和表面资源的内容初始未定义;
- TypeScript 导入资源的内容初始默认为已定义,除非调用方选择未定义;
- 读取和保留式写入要求其完整声明范围内的内容均已定义;
- 覆盖式写入会完整定义所声明的范围,且不读取其原有值;
- 部分、条件、稀疏或原子写入必须保留原内容;
- 附件清除属于覆盖,附件加载属于保留;
- 丢弃会使受影响的内容变为未定义。
对瞬态资源某个范围的首次写入必须覆盖该范围。声明覆盖表示调用方保证每条执行路径都会完整写入所选范围;如果只用于部分写入,图模型就会不正确。
纹理依赖使用规范化的 mip、层、深度切片和 aspect 区域。缓冲区依赖使用字节范围;省略范围时覆盖整个缓冲区。TypeScript 视图的默认值取决于访问角色和视图维度。请明确指定维度和数量。
依赖与保留
逻辑句柄标识录制本地的存储。节点实际看到的值由资源范围和该节点在录制顺序中的位置决定。
- 写后读(RAW)依赖会把已生成的值传给消费者。
- 读后写(WAR)和写后写(WAW)依赖用于维持顺序。
- 值依赖可以保留生产节点;仅有排序冲突不会让原本不可达的工作变得可观察。
保留根说明最终值为何重要。呈现、应用输出、回读、调试捕获、导入的持久状态和实际副作用都可以保留工作。编译会从这些根反向追溯依赖,并裁剪所有不贡献可观察结果的节点。
资源根会保留其所选范围内所有最终生产节点,并记录已定义的初始内容是否也参与生成结果。范围内存在未定义或已丢弃的部分时,编译会以 FG1004 失败,无论是否启用报告。根的身份由资源、原因和规范化范围共同确定;完全相同的声明会合并,彼此重叠但不同的范围仍独立处理。
在 TypeScript 中,markOutput、markPersistentState 和 markDebugCapture 接受整个资源、纹理视图或 (buffer, range)。markReadback(buffer, range?) 支持选择暂存缓冲区的部分范围。markPresent 始终选择整个表面纹理。空根范围会被拒绝;普通的空缓冲区访问仍然允许。
根不会转移所有权,不会让瞬态分配在执行结束后继续存在,也不会让瞬态原生对象可以安全地缓存。执行结束后仍需要的结果,必须存放在调用方拥有的导入存储中。
生命周期与分配
保留访问中的首次和末次访问决定每个瞬态逻辑资源的生命周期。生命周期不重叠且兼容的瞬态资源可以共用同一个物理分配。运行时会为每个保留的物理分配创建一个原生对象,并在执行后将其放回运行时资源池。
导入资源从不参与瞬态资源池。解析出的瞬态对象只在接收它的同步回调期间有效;不得让它逃逸回调,也不得跨帧保留。
资源池字节数和 Snapshot 分配大小,是根据 ZenFG 描述符和资源池桶估算得出的。它们用于比较和别名分析,不代表驱动分配、驻留、对齐、元数据或物理显存使用量的测量值。
资源池没有自动内存预算或驱逐策略。运行时创建的 ResourcePoolScope 用于划定帧内分配别名共享和跨次执行复用的范围;省略作用域时使用默认共享区域。作用域不拥有资源,不需要释放,清空后仍可继续使用。导入资源和表面资源永远不会进入资源池。
应用可以在尺寸调整后清空某个作用域,也可以使用同步谓词,按作用域、物理描述符、估算字节数或空闲执行次数驱逐保留的物理实例。选择会在资源池变更前完成;谓词失败时资源池保持不变,但谓词产生的外部副作用不会回滚。每个实例会访问一次,顺序未指定。TypeScript 会向同步布尔谓词提供深度冻结的快照。这些快照不公开 GPU 对象、内部键或稳定的资源池实例 ID。
执行通过参数和预检校验后,资源池年龄递增一次;空执行和随后执行失败也会计入。获取实例时会将它的年龄重置为零。编译、资源池维护、应用帧索引和 GPU 完成状态都不会增加年龄。
资源池管理发生在执行之外:在谓词维护期间,TypeScript 会拒绝同一运行时上的执行、清空、驱逐或销毁操作。清空不会使已编译的计划失效。尺寸调整后进行清理前,应停止执行使用旧尺寸的 TypeScript 计划,因为这些计划可能重新填充缓存。省略参数或显式传入 undefined 会清空所有区域。若只想清空默认区域,应使用能选择 undefined 的谓词。
执行会同步提交所有图可见工作,并在退出时归还瞬态资源,包括执行失败时。资源池驱逐会销毁原生资源,但不会等待 GPU 完成;对于已经提交的工作,WebGPU 会延后底层回收。移除字节数的估算不代表显存会立即释放。销毁与设备绑定的渲染器栈时,应释放运行时拥有的资源池。
集成层级
组合工作负载有两种常见方式:连接现有引擎,或构建把自身工作录制到图中的模块。这些是使用场景,不是单独的插件协议。原生集成到图中的模块可以使用原生 GPU API 或兼容的第三方库;场景、着色器、管线和绘制策略仍由模块负责。
会自行管理提交的现有引擎应使用外部节点。录制图自有工作的模块,则根据编码需求使用结构化原生节点或命令集成。一个帧中可以混用这三种集成深度。模块把越多工作声明为结构化节点,图校验和检查就能看到越多执行流程。仅仅共享设备,并不能保证格式、深度约定或引擎资源模型彼此兼容。
原生渲染、计算与复制
结构化节点声明附件、类型化资源访问,以及声明式复制或清除操作。它们提供最完整的校验、内容跟踪和诊断,同时仍由调用方负责管线和具体命令。
命令集成
命令节点让子系统可以在 FrameGraph 拥有的编码器中录制自定义命令。子系统必须声明所有图可见访问,也不得结束或提交该编码器。对于不适合结构化节点、但能参与图管理的提交的工作,可使用命令节点。
不透明的外部提交
外部节点适用于自行管理命令编码器、并通过共享设备队列提交工作的渲染器。它会声明图可见的资源访问,但 ZenFG 无法检查其内部命令。
跨渲染器组合仍要求使用共享的设备和队列、为每个共享原生资源只导入一个逻辑资源,并准确声明所有图可见访问。ZenFG 不会自动让彼此无关的引擎资源模型互通。
执行片段
原生渲染、计算、复制、清除和命令节点会编码到 FrameGraph 拥有的命令片段中。保留的外部提交会关闭并提交前一个图片段,调用由调用方拥有的提交操作,然后为后续原生工作开始新的图片段。
这一边界保证队列提交顺序。它不是 GPU 完成栅栏,不会暴露外部渲染器进行的原生提交数量或内容,也无法回滚已经提交的工作。
所有图可见的外部工作都必须在同步外部回调返回前加入共享队列。之后加入队列的工作或提交到其他队列的工作,都不属于声明的排序模型。当 CPU 必须等待 GPU 完成时,仍需使用图形 API 提供的完成机制。
诊断边界
编译报告、可选的 CPU/GPU 计时和资源池统计是彼此独立的观测数据。请求报告不会更改执行计划。普通执行不会收集计时。显式计时执行可选择 CPU、GPU 或两者;CPU 报告会同步返回,GPU 回读则独立进行。
CPU 计时涵盖所有保留的节点类型。它测量同步操作的实际经过时间,包括节点内部的准备和清理,不是线程 CPU 使用时间。外部节点会将同步提交时间计入其回调。执行总耗时也包括共享准备、提交和瞬态资源释放;不包括录制、编译、等待 GPU、投影报告和 Inspector 更新。
GPU 计时为每个保留的渲染或计算通道使用两个查询。当这类通道超过 2048 个时,运行时会在分配查询资源前报告计时不可用,但仍会执行图;选择 both 时仍会提供 CPU 结果。
只有成功执行才会生成 CPU 报告。GPU 计时从第一个保留的渲染或计算通道开始,到最后一个此类通道结束为止,不测量其他节点类型。如果没有保留这类通道,GPU 计时会以 no-timed-nodes 为原因标记为不可用。不要把 CPU 和 GPU 耗时相加来推算帧时间。时钟精度和抢占都会影响读数。资源或视图的准备方式因运行时而异,所以各节点的 CPU 读数并非严格的跨语言基准。
运行时报告类型可能包含面向实现的细节,并会随该运行时演进。
Snapshot 适配器会将兼容的报告数据显式投影到可移植的 Snapshot 1.2 模型中。Snapshot 包含图结构、诊断、分配信息,以及可选的计时或资源池信息;它不包含 GPU 命令或资源内容,也无法重放一帧。
Snapshot 规范和 Schema 定义可移植结构与语义。Inspector 是与渲染器无关的消费者,会基于符合规范的 Snapshot 数据构建自己的索引、视图和 UI 状态。文件系统存储、捕获命名、传输和保留策略仍由调用方负责。