ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

three.js WebGPUTimestampQueryPool 详解:WebGPU 渲染耗时查询池的原理与实战

three.js WebGPUTimestampQueryPool 详解:WebGPU 渲染耗时查询池的原理与实战 three.js WebGPUTimestampQueryPool 详解WebGPU 渲染耗时查询池的原理与实战【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.jsWebGPUTimestampQueryPool 是 three.js 在 WebGPU 后端中负责性能计时的核心工具类它通过管理一组 WebGPU 时间戳查询timestamp query资源在 GPU 上精确测量每个渲染/计算通道render/compute pass的执行耗时。本文以其官方 API 文档 docs/pages/WebGPUTimestampQueryPool.html.md 为主线结合 WebGPUTimestampQueryPool.js 源码、基类与后端集成实现讲清它如何被懒加载创建、如何分配与回读时间戳、以及如何在自己的 WebGPU 渲染循环中读取毫秒级耗时数据。WebGPUTimestampQueryPool 在 three.js 架构中的位置从类的继承关系看WebGPUTimestampQueryPool 继承自抽象基类 TimestampQueryPool见 src/renderers/common/TimestampQueryPool.js后者是所有渲染后端共用的时间戳查询池基类它定义了池的基本状态与对外访问接口trackTimestamp是否开启时间戳跟踪默认truemaxQueries池可容纳的最大查询数基类默认256currentQueryIndex已分配查询的游标queryOffsetsMap记录每个渲染上下文 uid → 查询基础偏移timestampsMap保存每个 uid 解析后的耗时结果lastValue最近一次解析得到的整帧总耗时pendingResolve用于避免并发解析的标志/待解析 PromiseWebGL 后端用作布尔值WebGPU 后端存 PromisegetTimestampFrames()、getTimestamp(uid)、hasTimestampQuery(uid)供上层读取帧列表、单次查询耗时与可用性。同时基类声明了三个抽象方法allocateQueriesForContext、resolveQueriesAsync、dispose分别在 WebGPU 后端WebGPUTimestampQueryPool.js与 WebGL 后端src/renderers/webgl-fallback/utils/WebGLTimestampQueryPool.js中实现——这正是 three.js 用公共抽象基类 后端专用实现统一 WebGL/WebGPU 能力差异的典型设计。官方 API 文档将本类定位为Extends the base TimestampQueryPool to provide WebGPU-specific implementation与本仓库源码中的 JSDoc 注释完全一致。从调用方看WebGPUTimestampQueryPool 实例不会由用户手动new而是由 WebGPU 后端在需要计时的第一个渲染帧内懒加载创建。在 src/renderers/webgpu/WebGPUBackend.js 的initTimestampQuery()中可以看到initTimestampQuery( type, uid, descriptor ) { if ( ! this.trackTimestamp ) return; if ( ! this.timestampQueryPool[ type ] ) { // TODO: Variable maxQueries? this.timestampQueryPool[ type ] new WebGPUTimestampQueryPool( this.device, type, 2048 ); } const timestampQueryPool this.timestampQueryPool[ type ]; const baseOffset timestampQueryPool.allocateQueriesForContext( uid ); _renderPassTimestampWrites.querySet timestampQueryPool.querySet; _renderPassTimestampWrites.beginningOfPassWriteIndex baseOffset; _renderPassTimestampWrites.endOfPassWriteIndex baseOffset 1; descriptor.timestampWrites _renderPassTimestampWrites; }后端在基类 src/renderers/common/Backend.js 中为render与compute两种类型各预留了一个池槽位timestampQueryPool { [TimestampQuery.RENDER]: null, [TimestampQuery.COMPUTE]: null }。在 src/constants.js 中TimestampQuery.RENDER与TimestampQuery.COMPUTE的值分别是字符串render与compute即构造函数中type参数的取值来源。构造函数与 WebGPU 底层资源编排官方文档给出的构造签名如下new WebGPUTimestampQueryPool( device, type, maxQueries )device用于创建查询资源的 WebGPU 设备GPUDevicetype查询池类型标识符render或compute同时用于 GPU 资源的 label 命名maxQueries池可容纳的最大查询数量默认2048。构造函数的核心工作是围绕查询结果从 GPU 回到 CPU的整条数据通路一次性创建三块 GPU 资源。见 src/renderers/webgpu/utils/WebGPUTimestampQueryPool.jsquerySetGPUQuerySettype 为timestampGPU 端保存时间戳槽位的集合count 等于maxQueriesresolveBufferGPUBufferusage QUERY_RESOLVE | COPY_SRC时间戳解析结果的暂存缓冲大小maxQueries * 8字节WebGPU 时间戳为 64 位/8 字节resultBufferGPUBufferusage COPY_DST | MAP_READ用于把结果拷贝回 CPU 并可被mapAsync映射读取的最终缓冲大小同为maxQueries * 8字节。三块资源的 label 依次为queryset_global_timestamp_${type}、buffer_timestamp_resolve_${type}、buffer_timestamp_result_${type}便于在浏览器 WebGPU 调试工具中辨识。源码中使用了仓库自定义的资源描述符类GPUBufferDescriptor、GPUQuerySetDescriptor、GPUCommandEncoderDescriptor位于 src/renderers/webgpu/descriptors以便复用与内存池化每次创建后立即reset()归还描述符对象。值得留意的是当前构造函数创建的是单个 timestamp querySet这正是本类与 WebGL 版本实现的重要差异来源——WebGPU 通过渲染/计算通道描述符里的timestampWrites字段beginningOfPassWriteIndex / endOfPassWriteIndex把时间戳写入该 querySet 的指定槽位而不需要像 WebGL 那样依赖EXT_disjoint_timer_query_webgl2扩展与显式beginQuery/endQuery调用。核心方法逐一解析.allocateQueriesForContext( uid : string ) : number每次渲染一个场景或执行一次 compute 时后端都会调用该方法为当前渲染上下文分配一对相邻槽位起点索引与终点索引返回基础偏移量。完整实现见 src/renderers/webgpu/utils/WebGPUTimestampQueryPool.jsallocateQueriesForContext( uid ) { if ( ! this.trackTimestamp || this.isDisposed ) return null; if ( this.currentQueryIndex 2 this.maxQueries ) { this.resolveQueriesAsync(); this.currentQueryIndex 0; this.queryOffsets.clear(); } const baseOffset this.currentQueryIndex; this.currentQueryIndex 2; this.queryOffsets.set( uid, baseOffset ); return baseOffset; }其行为要点若trackTimestamp为false或池已销毁isDisposed直接返回null调用方据此跳过时间戳写入官方文档所说 Returns null if allocation failed每个上下文消耗 2 个槽位因此分配前检查currentQueryIndex 2 maxQueries当槽位耗尽时会先触发一次异步解析resolveQueriesAsync()然后把游标与 uid→偏移映射清空从而让单个池可以在长生命周期内无限复用uid是每个渲染/计算上下文的唯一标识。结合后端 src/renderers/common/Backend.js 的updateTimeStampUID()可以看到其格式为前缀:id:f帧号如render:12:f34前缀c:对应 compute、其余对应 render帧号由:f(\d)$捕获——这一格式正是_resolveQueries中解析耗时归属帧的依据。.resolveQueriesAsync() : Promise.将所有已分配但尚未解析的查询异步解析返回最后一帧的总耗时毫秒。该方法是整个池子数据回读的入口官方文档明确指出它具备去重特性若已存在一个未完成的解析操作直接返回该 PromiseIf theres already a pending resolve operation, returns that promise instead。见 resolveQueriesAsync 与其私有实现_resolveQueries同文件第 132-252 行完整流程如下async _resolveQueries() { if ( this.isDisposed ) return this.lastValue; if ( this.resultBuffer.mapState ! unmapped ) return this.lastValue; const currentOffsets new Map( this.queryOffsets ); const queryCount this.currentQueryIndex; const bytesUsed queryCount * 8; // Reset state before GPU work this.currentQueryIndex 0; this.queryOffsets.clear(); const commandEncoder this.device.createCommandEncoder( _commandEncoderDescriptor ); commandEncoder.resolveQuerySet( this.querySet, 0, queryCount, this.resolveBuffer, 0 ); commandEncoder.copyBufferToBuffer( this.resolveBuffer, 0, this.resultBuffer, 0, bytesUsed ); const commandBuffer commandEncoder.finish(); submit( this.device, commandBuffer ); await this.resultBuffer.mapAsync( GPUMapMode.READ, 0, bytesUsed ); const times new BigUint64Array( this.resultBuffer.getMappedRange( 0, bytesUsed ) ); const framesDuration {}; const frames []; this.timestamps.clear(); for ( const [ uid, baseOffset ] of currentOffsets ) { const match uid.match( /^(.*):f(\d)$/ ); const frame parseInt( match[ 2 ] ); // ...按帧累加、解析每对起止时间戳的耗时... const startTime times[ baseOffset ]; const endTime times[ baseOffset 1 ]; const duration Number( endTime - startTime ) / 1e6; // 纳秒 → 毫秒 this.timestamps.set( uid, duration ); framesDuration[ frame ] duration; } // Return the total duration of the last frame const totalDuration framesDuration[ frames[ frames.length - 1 ] ]; this.resultBuffer.unmap(); this.lastValue totalDuration; this.frames frames; return totalDuration; }内部机制可以拆解为异步串行保护进入时先检查resultBuffer.mapState ! unmapped——如果上一次映射尚未解除就提前返回上次的lastValue避免 GPUBuffer 在mapAsync期间被再次提交命令先重置、后做 GPU 工作在真正提交解析命令之前就把游标与偏移表清空这样解析过程中的新分配会进入下一轮批次不会污染本次数据快照GPU 侧两级缓冲resolveQuerySet()把 querySet 中0..queryCount的时间戳解析到resolveBufferQUERY_RESOLVE再用copyBufferToBuffer()拷入resultBufferMAP_READ最终以submit()提交命令缓冲区CPU 侧映射回读mapAsync( GPUMapMode.READ, 0, bytesUsed )后用BigUint64Array包装映射区64 位纳秒时间戳必须用 BigUint64Array 读取这正是缓冲区大小按maxQueries * 8计算的原因耗时换算与按帧归组对每对[baseOffset, baseOffset 1]读取起点/终点时间戳duration Number( end - start ) / 1e6将纳秒换算为毫秒写入timestamps随后按 uid 中解析出的帧号把同一帧内所有子耗时累加最终返回最后一帧的总耗时失败兜底整个流程包裹在 try/catch 中任何异常都会调用error()打印日志、尝试unmap并返回lastValue与文档 returns the last valid value if resolution fails 对应。.dispose() : Promise销毁查询池释放全部 GPU 资源并清空 CPU 侧状态。官方文档注明它重写了基类的TimestampQueryPool#dispose且为异步方法返回 Promise。实现位于 src/renderers/webgpu/utils/WebGPUTimestampQueryPool.js幂等保护若isDisposed为true直接返回等待未完成的解析操作若pendingResolve存在先await其结束确保不会在映射期间销毁缓冲解除映射若resultBuffer仍处于mapped状态先unmap()WebGPU 规范禁止销毁处于映射态的缓冲依次destroy()并置空querySet、resolveBuffer、resultBuffer清空queryOffsets、timestamps、frames并把pendingResolve置null。该方法的调用方是后端析构链路在 WebGPUBackend.js 的dispose()中遍历并 dispose 所有类型池同时在 src/renderers/common/Renderer.js 的dispose()里也会对backend.timestampQueryPool中的每个池执行 dispose保证渲染器销毁时不会泄漏 GPU 资源。在渲染循环中的实际用法与调用链WebGPUTimestampQueryPool 的全流程由三处协作完成写入阶段渲染/计算每个 pass 前WebGPUBackend在 src/renderers/webgpu/WebGPUBackend.jsrender 通道与同文件第 1866 行compute 通道调用initTimestampQuery(...)把timestampWritesbegin/end 写索引挂到 pass 描述符上回读阶段用户代码主动调用WebGPURenderer.resolveTimestampsAsync( type )。该公共方法定义在 src/renderers/common/Backend.js它从timestampQueryPool[ type ]取出池子await queryPool.resolveQueriesAsync()并把返回值写入renderer.info[ type ].timestamp展示阶段从 src/renderers/common/Info.js 可见info.render.timestamp与info.compute.timestamp均初始化为 0调用toFixed( 6 )即可显示毫秒耗时。官方示例 examples/webgpu_storage_buffer.html 演示了最典型的用法renderer.compute( compute ); renderer.render( scene, camera ); renderer.resolveTimestampsAsync( THREE.TimestampQuery.COMPUTE ); renderer.resolveTimestampsAsync( THREE.TimestampQuery.RENDER ); timestamps[ forceWebGL ? webgl : webgpu ].innerHTML Compute ${renderer.info.compute.frameCalls} pass in ${renderer.info.compute.timestamp.toFixed( 6 )}msbr Draw ${renderer.info.render.drawCalls} pass in ${renderer.info.render.timestamp.toFixed( 6 )}ms;examples/webgpu_compute_reduce.html 则是 compute-only 场景每次执行若干 compute pass 后调用一次resolveTimestampsAsync( THREE.TimestampQuery.COMPUTE )再从renderer.info.compute.timestamp.toFixed( 6 )读取结果。需要说明的适用前提与限制设备能力时间戳查询依赖 WebGPUtimestamp-query特性。在 WebGPUTimestampQueryPool 源码中查询集以type: timestamp创建若设备不支持createQuerySet会抛错真实可用的设备能力上限通常还受maxTimestampQueryCount限制因此 maxQueries 并非越大越好耗时定义返回值是最近一帧内本类型所有 render/compute pass 的总耗时毫秒且每对起止时间戳以整个 pass 的 begin/end 为界属于 GPU 端的绝对时间差不含 CPU 提交开销手动触发回读时间戳解析必须由业务代码在渲染循环内主动调用resolveTimestampsAsync()触发这与 WebGL 后端在帧末自动解析的机制不同可用类型type只接受 TimestampQuery 的render或compute后端还会依据 uid 前缀c:自动判断某次查询归属于哪个池子见 src/renderers/common/Backend.js。小结从本仓库源码可以归纳出 WebGPUTimestampQueryPool 的设计要点它把创建 GPU 时间戳资源 → 按上下文成对分配槽位 → 槽位耗尽自动轮转 → 两级缓冲回读 按帧归组 → 异常兜底与幂等销毁整条链路收敛到一个类中同时通过公共基类 TimestampQueryPool 与后端无关的 uid/querySet/偏移协议让 render/compute 两条管线共用一个 2048 容量的池子且支持无限帧复用。对需要做 WebGPU 性能分析的开发者而言只需理解initTimestampQuery写、resolveQueriesAsync读、info.timestamp展示这一调用链即可获得精确到毫秒的每帧 GPU 耗时数据。【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表