news 2026/9/10 4:03:49

three.js TimestampQueryPool 深度解析:GPU 时间戳查询池的抽象基类、调用链与实战用法

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
three.js TimestampQueryPool 深度解析:GPU 时间戳查询池的抽象基类、调用链与实战用法

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.timestamprenderer.info.compute.timestamp这样的毫秒级数据时,背后就是时间戳查询池在工作。它的角色可以概括为几点:

  • 抽象基类TimestampQueryPool本身不直接创建 GPU 查询对象,而是定义所有具体时间戳查询池(pool)共有的数据结构、接口和生命周期约定,是“一类可容纳 N 个查询的资源池”的模板;
  • 按通道独立管理:从 src/renderers/common/Backend.js 可以看到,每个后端在初始化时都会预留rendercompute两个池位,分别对应渲染通道与计算通道(类型常量定义在 src/constants.js);
  • GPU 查询资源的簿记者:它负责把一组 GPU 时间戳查询“打包”在一个上限(maxQueries)内复用,跟踪已分配数量、帧号、上下文偏移,并最终把纳秒级原始读数换算成毫秒级时长交还给渲染器。

从源码结构看,three.js 当前共有两个具体子类,各自服务一种后端:

子类服务后端源码路径池默认容量
WebGPUTimestampQueryPoolWebGPUsrc/renderers/webgpu/utils/WebGPUTimestampQueryPool.js2048
WebGLTimestampQueryPoolWebGL(fallback)src/renderers/webgl-fallback/utils/WebGLTimestampQueryPool.js2048

基类文档说明的构造默认值是256,而两个子类实际按2048初始化——这一点在使用与阅读源码时需要区分:文档与基类描述的是约定契约,具体后端拥有自己的容量决策(见 WebGLBackend.js 中的懒创建)。

二、构造函数:new TimestampQueryPool( maxQueries )

new TimestampQueryPool( maxQueries = 256 )
  • 创建一个新的时间戳查询池,属于抽象构造函数,实际应由具体子类通过super( maxQueries )调用;
  • maxQueries:该池最多能容纳的查询数量,默认256。超出上限后,子类实现会在下次分配时先触发一次自动解析(详见后文)。

基类构造时初始化的一组实例字段(src/renderers/common/TimestampQueryPool.js)如下:

属性类型默认值含义
.trackTimestampbooleantrue是否开启时间戳跟踪
.maxQueriesnumber256池可容纳的最大查询数
.currentQueryIndexnumber0到目前为止已分配的查询数
.queryOffsetsMap<string, number>空 Map为不同渲染上下文(以 uid 标识)记录偏移
.isDisposedbooleanfalse池是否已被释放
.lastValuenumber0直到下一次解析为止的总帧时长
.framesArray<number>空数组存放所有时间戳帧号
.pendingResolveboolean | Promise<number>false防止并发解析的同步标记
.timestampsMap<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 后端中仅作布尔标志使用,解析期间置truefinally中复位(见 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),两个参数含义:

参数类型说明
uidstring渲染上下文的唯一标识符
frameIdnumber当前帧标识符

从调用侧看,uid 由Backend.updateTimeStampUID()统一生成,格式为[r|c]:<frameCalls>:<上下文id>:f<帧号>(见 src/renderers/common/Backend.js)。WebGPU 子类实现中,分配前会检查容量并可能自动触发resolveQueriesAsync()来腾出空间。

.dispose() —— 抽象

释放查询池。基类为空实现;WebGPU 子类会先等待未完成的解析、把已映射 bufferunmap,再销毁querySetresolveBufferresultBuffer并清空所有 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):

  1. 生成 uid:每帧updateTimeStampUID( context )依据通道类型生成r:...c:...前缀的 uid(Backend.js);
  2. 池内分配:按类型取池,_getQueryPool( uid )依据uid.startsWith('c:')区分计算/渲染(Backend.js);
  3. GPU 写入起止点:渲染通道起止、计算通道起止处分别插入查询写入(WebGL 后端为initTimestampQuery/prepareTimestampBuffer,见 WebGLBackend.js;WebGPU 后端则设置timestampWrites);
  4. 回读解析resolveTimestampsAsync( type = 'render' )先检查trackTimestamp(关闭时warnOnce提示并直接返回),再调用对应池的resolveQueriesAsync(),最终把时长写入this.renderer.info[ type ].timestamp并返回(Backend.js);
  5. 用户读取:应用层调用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——resolveBufferQUERY_RESOLVE | COPY_SRC)与resultBufferCOPY_DST | MAP_READ),大小均为maxQueries * 8字节(每个时间戳占 64 位 / 8 字节);
  • 容量自愈(L67-L87):allocateQueriesForContextcurrentQueryIndex + 2 > maxQueries时先同步触发一次解析并清空状态,实现循环利用;
  • 解析管线(L132-L252):用commandEncoder.resolveQuerySet()把查询集拷到resolveBuffer,再copyBufferToBuffer搬到resultBuffersubmitmapAsync( 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):为每次测量维护queryStatesinactive → 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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 4:03:46

TVBoxOSC 安装教程:3 步让闲置电视盒子变免费媒体中心

TVBoxOSC 安装教程&#xff1a;3 步让闲置电视盒子变免费媒体中心 【免费下载链接】TVBoxOSC TVBoxOSC - 一个基于第三方项目的代码库&#xff0c;用于电视盒子的控制和管理。 项目地址: https://gitcode.com/GitHub_Trending/tv/TVBoxOSC 抽屉里吃灰的旧电视盒子&#…

作者头像 李华
网站建设 2026/9/10 4:01:32

Fanuc FOCAS二次开发:C#调用DLL对接数控机床

简介&#xff1a;本资源是面向工业自动化领域开发者与数控系统集成工程师的Fanuc数控机床二次开发核心工具包&#xff0c;聚焦Focas协议通信与底层API调用&#xff0c;解决设备数据采集、远程监控及定制化HMI开发等实际工程问题。压缩包为ZIP格式&#xff0c;大小26.05MB&#…

作者头像 李华