three.js TimestampQueryPool 深度解析:GPU 时间戳查询池的抽象基类、调用链与实战用法
【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js
时间戳查询(Timestamp Query)是 three.js 渲染器中用于测量 GPU 渲染 / 计算通道真实耗时的底层机制。本文以官方 API 参考文档 docs/pages/TimestampQueryPool.html.md 为骨架,结合 src/renderers/common/TimestampQueryPool.js 及各后端实现源码,系统讲解这一抽象基类的构造参数、全部属性与方法语义、它在 WebGPU / WebGL 两套后端中的落地实现,以及如何在 WebGPURenderer 中开启并读取每一帧渲染耗时。读完本文,你将能完全理解renderer.info.render.timestamp这类耗时数据从 GPU 查询到 CPU 回读的完整链路。
一、TimestampQueryPool 的定位:渲染耗时数据从哪来
当你在示例中看到renderer.info.render.timestamp、renderer.info.compute.timestamp这样的毫秒级数据时,背后就是时间戳查询池在工作。它的角色可以概括为几点:
- 抽象基类:
TimestampQueryPool本身不直接创建 GPU 查询对象,而是定义所有具体时间戳查询池(pool)共有的数据结构、接口和生命周期约定,是“一类可容纳 N 个查询的资源池”的模板; - 按通道独立管理:从 src/renderers/common/Backend.js 可以看到,每个后端在初始化时都会预留
render与compute两个池位,分别对应渲染通道与计算通道(类型常量定义在 src/constants.js); - GPU 查询资源的簿记者:它负责把一组 GPU 时间戳查询“打包”在一个上限(
maxQueries)内复用,跟踪已分配数量、帧号、上下文偏移,并最终把纳秒级原始读数换算成毫秒级时长交还给渲染器。
从源码结构看,three.js 当前共有两个具体子类,各自服务一种后端:
| 子类 | 服务后端 | 源码路径 | 池默认容量 |
|---|---|---|---|
WebGPUTimestampQueryPool | WebGPU | src/renderers/webgpu/utils/WebGPUTimestampQueryPool.js | 2048 |
WebGLTimestampQueryPool | WebGL(fallback) | src/renderers/webgl-fallback/utils/WebGLTimestampQueryPool.js | 2048 |
基类文档说明的构造默认值是256,而两个子类实际按2048初始化——这一点在使用与阅读源码时需要区分:文档与基类描述的是约定契约,具体后端拥有自己的容量决策(见 WebGLBackend.js 中的懒创建)。
二、构造函数:new TimestampQueryPool( maxQueries )
new TimestampQueryPool( maxQueries = 256 )- 创建一个新的时间戳查询池,属于抽象构造函数,实际应由具体子类通过
super( maxQueries )调用; maxQueries:该池最多能容纳的查询数量,默认256。超出上限后,子类实现会在下次分配时先触发一次自动解析(详见后文)。
基类构造时初始化的一组实例字段(src/renderers/common/TimestampQueryPool.js)如下:
| 属性 | 类型 | 默认值 | 含义 |
|---|---|---|---|
.trackTimestamp | boolean | true | 是否开启时间戳跟踪 |
.maxQueries | number | 256 | 池可容纳的最大查询数 |
.currentQueryIndex | number | 0 | 到目前为止已分配的查询数 |
.queryOffsets | Map<string, number> | 空 Map | 为不同渲染上下文(以 uid 标识)记录偏移 |
.isDisposed | boolean | false | 池是否已被释放 |
.lastValue | number | 0 | 直到下一次解析为止的总帧时长 |
.frames | Array<number> | 空数组 | 存放所有时间戳帧号 |
.pendingResolve | boolean | Promise<number> | false | 防止并发解析的同步标记 |
.timestamps | Map<string, number> | 空 Map | 缓存每个渲染上下文的最新时间戳 |
三、属性语义逐个拆解
.currentQueryIndex : number
“到目前为止已分配了多少查询”。分配是自增的:子类每次为上下文取一段偏移后currentQueryIndex += 2(每个上下文占用一对查询,代表一次“开始/结束”写入)。当它逼近maxQueries时,意味着池容量即将耗尽。
.maxQueries : number
池的容量上限。默认256,但两个实际后端都使用2048。超出后,子类实现会自动调用一次解析、把currentQueryIndex归零并清空queryOffsets,从而在同一个池内环形复用 GPU 查询资源,这正是“池(pool)”的本质。
.queryOffsets : Map<string, number>
“不同上下文的查询偏移表”。键是渲染上下文/计算节点的 uid,值是它在查询序列中的起始偏移。例如 WebGPU 实现中queryOffsets.set( uid, baseOffset )(见 WebGPUTimestampQueryPool.js)。uid 与偏移的对应关系是之后回读数据时“把读数归位到具体上下文”的唯一凭证。
.timestamps : Map<string, number>
“每个渲染上下文的最新时间戳”。解析完成后,实现会清空并重建该 Map,把每个 uid 的毫秒耗时存进去(注意:不是纳秒原始值)。对外查询接口getTimestamp( uid )/hasTimestampQuery( uid )都读取这张表。
.frames : Array<number>
“所有时间戳帧”。解析时会从 uid 字符串解析出帧号并去重收集。例如 uid 形如r:3:42:f7,正则/^(.*):f(\d+)$/提取出帧7。该数组主要用于把耗时按帧归类、并定位“最后一帧”计算lastValue。
.lastValue : number
“直到下一次更新为止的总帧时长”。具体子类解析完成后,会把最后一次解析的最后一帧总时长写入该字段,单位毫秒;如果 GPU 查询因故失败(特性缺失、disjoint、已 dispose 等),也会回退返回该值,保证接口总是有合理结果而不是抛错。
.pendingResolve : boolean | Promise<number>
并发保护的同步状态。文档与基类注释明确指出其双形态:
- 在WebGL 后端中仅作布尔标志使用,解析期间置
true,finally中复位(见 WebGLTimestampQueryPool.js); - 在WebGPU 后端中保存当前解析操作的 Promise,若已有解析在进行则直接返回同一 Promise,避免重复 map / resolve(见 WebGPUTimestampQueryPool.js)。
.trackTimestamp : boolean
是否跟踪时间戳的总开关,默认true。但当运行环境不支持相关能力时,后端会将其置false:例如 WebGL 后端找不到EXT_disjoint_timer_query扩展时调用warn并关闭跟踪(WebGLTimestampQueryPool.js);WebGPU 后端则要求设备支持timestamp-query特性(WebGPUBackend.js 中this.trackTimestamp = this.trackTimestamp && this.hasFeature( GPUFeatureName.TimestampQuery ))。
.isDisposed : boolean
池是否已被释放。释放后所有分配与解析入口都会提前返回(如allocateQueriesForContext返回null),防止对已销毁的 GPU 资源继续操作。
四、方法详解
.allocateQueriesForContext( uid, frameId ) : number —— 抽象
为指定 uid 分配查询,返回分配到的起始偏移(分配失败返回null/undefined)。基类为空实现(src/renderers/common/TimestampQueryPool.js),两个参数含义:
| 参数 | 类型 | 说明 |
|---|---|---|
uid | string | 渲染上下文的唯一标识符 |
frameId | number | 当前帧标识符 |
从调用侧看,uid 由Backend.updateTimeStampUID()统一生成,格式为[r|c]:<frameCalls>:<上下文id>:f<帧号>(见 src/renderers/common/Backend.js)。WebGPU 子类实现中,分配前会检查容量并可能自动触发resolveQueriesAsync()来腾出空间。
.dispose() —— 抽象
释放查询池。基类为空实现;WebGPU 子类会先等待未完成的解析、把已映射 bufferunmap,再销毁querySet、resolveBuffer、resultBuffer并清空所有 Map(WebGPUTimestampQueryPool.js);WebGL 子类则删除全部WebGLQuery对象(WebGLTimestampQueryPool.js)。渲染器销毁时会遍历后端持有的两个池统一释放(src/renderers/common/Renderer.js 的 dispose 路径中迭代backend.timestampQueryPool)。
.resolveQueriesAsync() : Promise<number> | number —— 异步、抽象
解析全部时间戳并把数据读回(或就地处理),返回解析出的时间戳值(毫秒时长)。基类为空实现;子类语义是“解析当前已分配、尚未解析的查询,返回最后一帧总时长,并缓存到lastValue”。该方法是后端起止点,最终经Backend.resolveTimestampsAsync()写入renderer.info[type].timestamp。
.getTimestamp( uid ) : number
返回指定渲染上下文的耗时时间戳:
getTimestamp( uid ) { let timestamp = this.timestamps.get( uid ); if ( timestamp === undefined ) { warn( `TimestampQueryPool: No timestamp available for uid ${ uid }.` ); timestamp = 0; } return timestamp; }注意基类实现的实际行为:查询不到时并非返回undefined,而是输出一条warn日志并返回0(src/renderers/common/TimestampQueryPool.js)。这属于“由源码结构看”的实现事实,文档中所写的“不存在时返回 undefined”应理解为接口约定层面——文档与实现存在细微出入,实战中以源码行为为准。
uid:渲染上下文的唯一标识符;- 返回:对应上下文的时间戳(毫秒),不可用时为
0并告警。
.getTimestampFrames() : Array<number>
返回全部时间戳帧号数组,直接透传内部frames(src/renderers/common/TimestampQueryPool.js)。在Backend层由getTimestampFrames( type )代理(src/renderers/common/Backend.js),可用于判断最近解析覆盖了哪些帧。
.hasTimestampQuery( uid ) : boolean
判断某 uid 是否已有可用时间戳,即this.timestamps.has( uid )(src/renderers/common/TimestampQueryPool.js)。它比getTimestamp更安全:不会产生告警,适合“先探测后读取”的写法。由Backend.hasTimestampQuery( uid )对外暴露(src/renderers/common/Backend.js)。
五、调用链:从 uid 生成到 info.timestamp 落盘
把四个抽象/实例方法串起来,可以看到完整的数据流(源码依据:src/renderers/common/Backend.js):
- 生成 uid:每帧
updateTimeStampUID( context )依据通道类型生成r:...或c:...前缀的 uid(Backend.js); - 池内分配:按类型取池,
_getQueryPool( uid )依据uid.startsWith('c:')区分计算/渲染(Backend.js); - GPU 写入起止点:渲染通道起止、计算通道起止处分别插入查询写入(WebGL 后端为
initTimestampQuery/prepareTimestampBuffer,见 WebGLBackend.js;WebGPU 后端则设置timestampWrites); - 回读解析:
resolveTimestampsAsync( type = 'render' )先检查trackTimestamp(关闭时warnOnce提示并直接返回),再调用对应池的resolveQueriesAsync(),最终把时长写入this.renderer.info[ type ].timestamp并返回(Backend.js); - 用户读取:应用层调用
renderer.resolveTimestampsAsync( THREE.TimestampQuery.COMPUTE / .RENDER ),再读取renderer.info.compute.timestamp/renderer.info.render.timestamp。
六、源码级深挖:两个具体子类如何把抽象落地
WebGPUTimestampQueryPool:querySet + 双 Buffer + BigUint64Array
WebGPU 子类是“真·时间戳(绝对时间点)”,核心资源与流程(WebGPUTimestampQueryPool.js):
- 构造资源(L27-L59):
device.createQuerySet()创建type: 'timestamp'、容量为maxQueries的查询集;随后创建两块 GPUBuffer——resolveBuffer(QUERY_RESOLVE | COPY_SRC)与resultBuffer(COPY_DST | MAP_READ),大小均为maxQueries * 8字节(每个时间戳占 64 位 / 8 字节); - 容量自愈(L67-L87):
allocateQueriesForContext在currentQueryIndex + 2 > maxQueries时先同步触发一次解析并清空状态,实现循环利用; - 解析管线(L132-L252):用
commandEncoder.resolveQuerySet()把查询集拷到resolveBuffer,再copyBufferToBuffer搬到resultBuffer,submit后mapAsync( GPUMapMode.READ ),通过BigUint64Array视图读取纳秒级时间戳,对每个 uid 计算( endTime - startTime ) / 1e6得到毫秒时长,同时按帧累加framesDuration,并把“最后一帧总时长”写入lastValue; - 容错:mapState 非 unmapped、catch 到异常、池已 dispose 等场景都会安全回退到
lastValue,不会抛出未捕获错误。
WebGLTimestampQueryPool:扩展探测 + 轮询解析
WebGL 子类依赖EXT_disjoint_timer_query_webgl2(或旧版EXT_disjoint_timer_query)扩展(WebGLTimestampQueryPool.js):
- 按扩展有无决定开关:没有扩展则
warn并置trackTimestamp = false,此后一切入口空转; - beginQuery / endQuery(L88-L180):为每次测量维护
queryStates(inactive → started → ended),用gl.beginQuery( TIME_ELAPSED_EXT, query )与gl.endQuery( TIME_ELAPSED_EXT )包裹被测量区间; - 逐查询异步解析(L286-L367):通过
getQueryParameter( QUERY_RESULT_AVAILABLE )以 1ms 间隔轮询,同时检查GPU_DISJOINT_EXT(GPU 状态切换导致计时不可信时放弃本次结果),就绪后以getQueryParameter( QUERY_RESULT )取出纳秒值并除以1e6转为毫秒。
二者计量维度不同:WebGPU 端量的是时间点之差(开始/结束写入两次时间戳),WebGL 端由扩展的
TIME_ELAPSED_EXT语义天然给出区间耗时,但换算后对外都统一为毫秒时长,上层无需感知差异。
七、实战用法:开启跟踪并读取每帧耗时
时间戳跟踪是需要显式开启的能力。最直接的验证用例是仓库自带的 WebGPU 示例,例如 examples/webgpu_storage_buffer.html(L188 以trackTimestamp: true构造渲染器,L227-L233 做解析与展示)以及 examples/webgpu_compute_reduce.html(L1001、L1293-L1345)。标准用法如下:
// 1) 构造渲染器时开启时间戳跟踪 const renderer = new THREE.WebGPURenderer( { antialias: false, trackTimestamp: true // 同时要求设备支持 'timestamp-query' 特性 } ); // 2) 渲染循环内异步解析,并读取结果(单位:毫秒) await renderer.resolveTimestampsAsync( THREE.TimestampQuery.RENDER ); await renderer.resolveTimestampsAsync( THREE.TimestampQuery.COMPUTE ); const renderMs = renderer.info.render.timestamp; const computeMs = renderer.info.compute.timestamp;更贴近真实 UI 的做法可参考 examples/jsm/inspector/RendererInspector.js(L271-L272 对 COMPUTE 与 RENDER 分别解析)与 examples/jsm/inspector/Inspector.js(L338 动态打开renderer.backend.trackTimestamp = true),调试场景甚至可以渲染器构造后再开启跟踪。
需要注意的能力与边界:
- WebGPU 前提:
timestamp-query是设备可选特性,需要trackTimestamp: true且设备特性通过双重判断,否则自动退回禁用状态; - WebGL 前提:必须有 disjoint timer query 扩展,否则发出
warn并禁用;数值来自计时扩展,可能受GPU_DISJOINT_EXT影响而作废; - 接口时序:
resolveTimestampsAsync是异步回读,读取info.*.timestamp需在await之后; - 解析防护:
pendingResolve保证不会并发发起重复解析;频繁调用不会把解析任务无限堆积(WebGPU 端未完成 map 时直接返回lastValue)。
八、相关仓库资源
若需深入研读,可在当前仓库中按以下相对路径定位:
- API 参考: docs/pages/TimestampQueryPool.html.md
- 抽象基类实现: src/renderers/common/TimestampQueryPool.js
- 通道类型常量: src/constants.js
- 后端公共封装(uid 生成、池管理、
resolveTimestampsAsync): src/renderers/common/Backend.js - WebGPU 子类: src/renderers/webgpu/utils/WebGPUTimestampQueryPool.js
- WebGL 子类: src/renderers/webgl-fallback/utils/WebGLTimestampQueryPool.js
- WebGPU / WebGL 后端的接入点: src/renderers/webgpu/WebGPUBackend.js、 src/renderers/webgl-fallback/WebGLBackend.js
- 可运行示例: examples/webgpu_storage_buffer.html、 examples/webgpu_compute_reduce.html、 examples/webgpu_lines_fat_raycasting.html
- 官方调试面板中对时间戳的使用: examples/jsm/inspector/RendererInspector.js、 examples/jsm/inspector/Inspector.js
【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考