@gnsx/three 0.185.9 → 0.185.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/three.cjs CHANGED
@@ -59983,6 +59983,7 @@ class TextureUtils {
59983
59983
  }
59984
59984
 
59985
59985
  // WITH_GENESYS
59986
+
59986
59987
  /**
59987
59988
  * ProfilerService — per-label CPU timing with ring-buffer aggregation and DevTools integration.
59988
59989
  *
@@ -60003,12 +60004,16 @@ const RING_SIZE = 120;
60003
60004
  const FRAME_BUDGET_MS = 1000 / 60;
60004
60005
  const TRACE_TID_MAIN = 1;
60005
60006
  const TRACE_TID_ASYNC = 2;
60007
+ const TRACE_TID_GPU = 3;
60006
60008
  const NOOP = () => {};
60007
60009
 
60008
60010
  const DUMMY_SPAN = Object.freeze( { label: '', t0: 0, _seq: 0 } );
60011
+ const DUMMY_GPU_SPAN = Object.freeze( { label: '', renderer: null, t0: 0, _seq: 0, _generation: 0 } );
60009
60012
  const NOOP_BEGIN_SPAN = () => DUMMY_SPAN;
60010
60013
  const NOOP_END_SPAN = () => {};
60011
60014
 
60015
+ const NOOP_BEGIN_GPU_SPAN = () => DUMMY_GPU_SPAN;
60016
+
60012
60017
  class ProfilerServiceClass {
60013
60018
 
60014
60019
  constructor() {
@@ -60043,6 +60048,18 @@ class ProfilerServiceClass {
60043
60048
  /** @type {Map<string, number>} Total (uncapped) invocation count since last reset, for calls-per-frame. */
60044
60049
  this.invocations = new Map();
60045
60050
 
60051
+ /** @type {Map<string, Float64Array>} GPU time ring buffers (one per label). */
60052
+ this.gpuBuffers = new Map();
60053
+ /** @type {Map<string, number>} */
60054
+ this.gpuCursors = new Map();
60055
+ /** @type {Map<string, number>} */
60056
+ this.gpuCounts = new Map();
60057
+ /** @type {Map<string, number>} */
60058
+ this.gpuInvocations = new Map();
60059
+ /** @type {Map<Object, import('./ProfilerService.js').GpuRendererState>} */
60060
+ this.gpuRendererStates = new Map();
60061
+ this._gpuGeneration = 1;
60062
+
60046
60063
  if ( this._enabled ) {
60047
60064
 
60048
60065
  this.traceStartTime = performance.now();
@@ -60050,6 +60067,8 @@ class ProfilerServiceClass {
60050
60067
  this.end = this._endImpl.bind( this );
60051
60068
  this.beginSpan = this._beginSpanImpl.bind( this );
60052
60069
  this.endSpan = this._endSpanImpl.bind( this );
60070
+ this.beginGpu = this._beginGpuImpl.bind( this );
60071
+ this.endGpu = this._endGpuImpl.bind( this );
60053
60072
  this._exposeGlobal();
60054
60073
  console.log( `[ProfilerService] auto-started from env (profile: ${this._profile})` );
60055
60074
 
@@ -60059,6 +60078,8 @@ class ProfilerServiceClass {
60059
60078
  this.end = NOOP;
60060
60079
  this.beginSpan = NOOP_BEGIN_SPAN;
60061
60080
  this.endSpan = NOOP_END_SPAN;
60081
+ this.beginGpu = NOOP_BEGIN_GPU_SPAN;
60082
+ this.endGpu = NOOP_END_SPAN;
60062
60083
 
60063
60084
  }
60064
60085
 
@@ -60092,6 +60113,8 @@ class ProfilerServiceClass {
60092
60113
  this.end = this._endImpl.bind( this );
60093
60114
  this.beginSpan = this._beginSpanImpl.bind( this );
60094
60115
  this.endSpan = this._endSpanImpl.bind( this );
60116
+ this.beginGpu = this._beginGpuImpl.bind( this );
60117
+ this.endGpu = this._endGpuImpl.bind( this );
60095
60118
  this._exposeGlobal();
60096
60119
  console.log( `[ProfilerService] enabled (profile: ${this._profile}) — call __gnsx_profiler.report() or downloadTrace() from the console` );
60097
60120
 
@@ -60105,6 +60128,9 @@ class ProfilerServiceClass {
60105
60128
  this.end = NOOP;
60106
60129
  this.beginSpan = NOOP_BEGIN_SPAN;
60107
60130
  this.endSpan = NOOP_END_SPAN;
60131
+ this.beginGpu = NOOP_BEGIN_GPU_SPAN;
60132
+ this.endGpu = NOOP_END_SPAN;
60133
+ this._detachGpuRenderers();
60108
60134
  console.log( '[ProfilerService] disabled' );
60109
60135
 
60110
60136
  }
@@ -60125,6 +60151,467 @@ class ProfilerServiceClass {
60125
60151
 
60126
60152
  }
60127
60153
 
60154
+ /**
60155
+ * Enables GPU timestamp collection for a renderer.
60156
+ *
60157
+ * @param {Object} renderer
60158
+ * @return {Promise<boolean>} Whether GPU timestamps are available.
60159
+ */
60160
+ async attachGpuRenderer( renderer ) {
60161
+
60162
+ if ( this._enabled === false || renderer === null || renderer === undefined ) return false;
60163
+
60164
+ const existingState = this.gpuRendererStates.get( renderer );
60165
+ if ( existingState !== undefined ) return existingState.available;
60166
+
60167
+ if ( renderer.isWebGLRenderer === true ) {
60168
+
60169
+ const gl = renderer.getContext();
60170
+ const extension = gl.getExtension( 'EXT_disjoint_timer_query_webgl2' );
60171
+ const state = {
60172
+ kind: 'legacy-webgl',
60173
+ available: extension !== null,
60174
+ renderer,
60175
+ gl,
60176
+ extension,
60177
+ activeSpan: null,
60178
+ pendingSpans: [],
60179
+ flushPromise: null,
60180
+ flushScheduled: false,
60181
+ };
60182
+ this.gpuRendererStates.set( renderer, state );
60183
+
60184
+ if ( extension === null ) {
60185
+
60186
+ warnOnce( 'ProfilerService: EXT_disjoint_timer_query_webgl2 is unavailable; GPU profiling is disabled for this WebGLRenderer.' );
60187
+
60188
+ }
60189
+
60190
+ return state.available;
60191
+
60192
+ }
60193
+
60194
+ if ( renderer.isRenderer === true && renderer.backend !== undefined ) {
60195
+
60196
+ await renderer.init();
60197
+ const previousTrackTimestamp = renderer.backend.trackTimestamp;
60198
+ renderer.backend.trackTimestamp = true;
60199
+
60200
+ const available = renderer.backend.hasTimestamp === true && renderer.hasFeature( 'timestamp-query' ) === true;
60201
+ const listener = ( type, uid, label ) => this._captureGpuTimestampQuery( renderer, type, uid, label );
60202
+ const state = {
60203
+ kind: 'common',
60204
+ available,
60205
+ renderer,
60206
+ listener,
60207
+ previousTrackTimestamp,
60208
+ activeSpans: [],
60209
+ pendingSpans: [],
60210
+ queryCpuTimes: new Map(),
60211
+ gpuTimestampOrigin: null,
60212
+ gpuTraceOrigin: null,
60213
+ flushPromise: null,
60214
+ flushScheduled: false,
60215
+ };
60216
+ this.gpuRendererStates.set( renderer, state );
60217
+
60218
+ if ( available ) {
60219
+
60220
+ renderer.backend.addTimestampQueryListener( listener );
60221
+
60222
+ } else {
60223
+
60224
+ warnOnce( 'ProfilerService: Timestamp queries are unavailable; GPU profiling is disabled for this renderer.' );
60225
+
60226
+ }
60227
+
60228
+ return available;
60229
+
60230
+ }
60231
+
60232
+ warnOnce( 'ProfilerService: Unsupported renderer; expected Renderer or WebGLRenderer.' );
60233
+ return false;
60234
+
60235
+ }
60236
+
60237
+ /**
60238
+ * @param {string} label
60239
+ * @param {Object} renderer
60240
+ * @return {import('./ProfilerService.js').GpuSpanHandle}
60241
+ */
60242
+ _beginGpuImpl( label, renderer ) {
60243
+
60244
+ const state = this.gpuRendererStates.get( renderer );
60245
+ if ( state === undefined || state.available === false ) return DUMMY_GPU_SPAN;
60246
+
60247
+ const handle = {
60248
+ label,
60249
+ renderer,
60250
+ t0: performance.now(),
60251
+ _seq: ++ this._markId,
60252
+ _generation: this._gpuGeneration,
60253
+ };
60254
+
60255
+ if ( state.kind === 'common' ) {
60256
+
60257
+ handle._queries = {
60258
+ [ TimestampQuery.RENDER ]: new Set(),
60259
+ [ TimestampQuery.COMPUTE ]: new Set(),
60260
+ };
60261
+ state.activeSpans.push( handle );
60262
+ return handle;
60263
+
60264
+ }
60265
+
60266
+ if ( state.activeSpan !== null ) {
60267
+
60268
+ warnOnce( 'ProfilerService: Nested GPU spans are unsupported by legacy WebGLRenderer.' );
60269
+ return DUMMY_GPU_SPAN;
60270
+
60271
+ }
60272
+
60273
+ const query = state.gl.createQuery();
60274
+ if ( query === null ) return DUMMY_GPU_SPAN;
60275
+
60276
+ try {
60277
+
60278
+ state.gl.beginQuery( state.extension.TIME_ELAPSED_EXT, query );
60279
+ handle._query = query;
60280
+ state.activeSpan = handle;
60281
+ return handle;
60282
+
60283
+ } catch ( error ) {
60284
+
60285
+ state.gl.deleteQuery( query );
60286
+ warnOnce( `ProfilerService: Unable to begin a WebGL GPU span (${error.message}).` );
60287
+ return DUMMY_GPU_SPAN;
60288
+
60289
+ }
60290
+
60291
+ }
60292
+
60293
+ /**
60294
+ * @param {import('./ProfilerService.js').GpuSpanHandle} handle
60295
+ */
60296
+ _endGpuImpl( handle ) {
60297
+
60298
+ if ( handle._seq === 0 || handle._generation !== this._gpuGeneration ) return;
60299
+
60300
+ const state = this.gpuRendererStates.get( handle.renderer );
60301
+ if ( state === undefined || state.available === false ) return;
60302
+
60303
+ handle._endTime = performance.now();
60304
+
60305
+ if ( state.kind === 'common' ) {
60306
+
60307
+ const index = state.activeSpans.indexOf( handle );
60308
+ if ( index === -1 ) return;
60309
+
60310
+ state.activeSpans.splice( index, 1 );
60311
+ if ( handle._queries.render.size > 0 || handle._queries.compute.size > 0 ) {
60312
+
60313
+ state.pendingSpans.push( handle );
60314
+ this._scheduleGpuFlush( state );
60315
+
60316
+ }
60317
+
60318
+ return;
60319
+
60320
+ }
60321
+
60322
+ if ( state.activeSpan !== handle ) return;
60323
+
60324
+ try {
60325
+
60326
+ state.gl.endQuery( state.extension.TIME_ELAPSED_EXT );
60327
+ state.activeSpan = null;
60328
+ state.pendingSpans.push( handle );
60329
+ this._scheduleGpuFlush( state );
60330
+
60331
+ } catch ( error ) {
60332
+
60333
+ state.activeSpan = null;
60334
+ state.gl.deleteQuery( handle._query );
60335
+ warnOnce( `ProfilerService: Unable to end a WebGL GPU span (${error.message}).` );
60336
+
60337
+ }
60338
+
60339
+ }
60340
+
60341
+ /**
60342
+ * Resolves pending GPU timestamp queries for a renderer.
60343
+ *
60344
+ * @param {Object} renderer
60345
+ * @return {Promise<void>}
60346
+ */
60347
+ async flushGpu( renderer ) {
60348
+
60349
+ const state = this.gpuRendererStates.get( renderer );
60350
+ if ( state === undefined || state.available === false ) return;
60351
+ if ( state.flushPromise !== null ) return state.flushPromise;
60352
+
60353
+ state.flushPromise = state.kind === 'common'
60354
+ ? this._flushCommonGpuState( state )
60355
+ : this._flushLegacyWebGLState( state );
60356
+
60357
+ try {
60358
+
60359
+ await state.flushPromise;
60360
+
60361
+ } finally {
60362
+
60363
+ state.flushPromise = null;
60364
+ if ( state.pendingSpans.length > 0 ) this._scheduleGpuFlush( state );
60365
+
60366
+ }
60367
+
60368
+ }
60369
+
60370
+ /**
60371
+ * @param {Object} renderer
60372
+ * @param {'render'|'compute'} type
60373
+ * @param {string} uid
60374
+ * @param {?string} [label=null]
60375
+ */
60376
+ _captureGpuTimestampQuery( renderer, type, uid, label = null ) {
60377
+
60378
+ const state = this.gpuRendererStates.get( renderer );
60379
+ if ( state === undefined || state.kind !== 'common' ) return;
60380
+
60381
+ const cpuTime = performance.now();
60382
+ state.queryCpuTimes.set( uid, cpuTime );
60383
+
60384
+ for ( const span of state.activeSpans ) {
60385
+
60386
+ span._queries[ type ].add( uid );
60387
+
60388
+ }
60389
+
60390
+ if ( label !== null && label !== undefined && label !== '' ) {
60391
+
60392
+ const passSpan = {
60393
+ label: `GPU pass: ${label}`,
60394
+ renderer,
60395
+ t0: cpuTime,
60396
+ _seq: ++ this._markId,
60397
+ _generation: this._gpuGeneration,
60398
+ _queries: {
60399
+ [ TimestampQuery.RENDER ]: new Set(),
60400
+ [ TimestampQuery.COMPUTE ]: new Set(),
60401
+ },
60402
+ };
60403
+ passSpan._queries[ type ].add( uid );
60404
+ state.pendingSpans.push( passSpan );
60405
+ this._scheduleGpuFlush( state );
60406
+
60407
+ }
60408
+
60409
+ }
60410
+
60411
+ /**
60412
+ * @param {import('./ProfilerService.js').GpuRendererState} state
60413
+ */
60414
+ _scheduleGpuFlush( state ) {
60415
+
60416
+ if ( state.flushScheduled ) return;
60417
+ state.flushScheduled = true;
60418
+
60419
+ const callback = () => {
60420
+
60421
+ state.flushScheduled = false;
60422
+ if ( this.gpuRendererStates.get( state.renderer ) === state ) {
60423
+
60424
+ this.flushGpu( state.renderer ).catch( error => {
60425
+
60426
+ warnOnce( `ProfilerService: Unable to resolve GPU timestamps (${error.message}).` );
60427
+
60428
+ } );
60429
+
60430
+ }
60431
+
60432
+ };
60433
+
60434
+ if ( typeof requestAnimationFrame === 'function' ) {
60435
+
60436
+ requestAnimationFrame( callback );
60437
+
60438
+ } else {
60439
+
60440
+ setTimeout( callback, 0 );
60441
+
60442
+ }
60443
+
60444
+ }
60445
+
60446
+ /**
60447
+ * @param {import('./ProfilerService.js').CommonGpuRendererState} state
60448
+ */
60449
+ async _flushCommonGpuState( state ) {
60450
+
60451
+ const spans = state.pendingSpans.splice( 0 );
60452
+ if ( spans.length === 0 ) return;
60453
+
60454
+ const hasRenderQueries = spans.some( span => span._queries.render.size > 0 );
60455
+ const hasComputeQueries = spans.some( span => span._queries.compute.size > 0 );
60456
+ const resolutions = [];
60457
+
60458
+ if ( hasRenderQueries ) resolutions.push( state.renderer.resolveTimestampsAsync( TimestampQuery.RENDER ) );
60459
+ if ( hasComputeQueries ) resolutions.push( state.renderer.resolveTimestampsAsync( TimestampQuery.COMPUTE ) );
60460
+ await Promise.all( resolutions );
60461
+
60462
+ if ( this._enabled === false ) return;
60463
+
60464
+ if ( state.gpuTimestampOrigin === null ) {
60465
+
60466
+ let earliestRange = null;
60467
+ let earliestUid = null;
60468
+ for ( const span of spans ) {
60469
+
60470
+ for ( const type of [ TimestampQuery.RENDER, TimestampQuery.COMPUTE ] ) {
60471
+
60472
+ for ( const uid of span._queries[ type ] ) {
60473
+
60474
+ const range = state.renderer.backend.getTimestampRange( uid );
60475
+ if ( range !== null && ( earliestRange === null || range.start < earliestRange.start ) ) {
60476
+
60477
+ earliestRange = range;
60478
+ earliestUid = uid;
60479
+
60480
+ }
60481
+
60482
+ }
60483
+
60484
+ }
60485
+
60486
+ }
60487
+
60488
+ if ( earliestRange !== null ) {
60489
+
60490
+ state.gpuTimestampOrigin = earliestRange.start;
60491
+ state.gpuTraceOrigin = state.queryCpuTimes.get( earliestUid ) ?? spans[ 0 ].t0;
60492
+
60493
+ }
60494
+
60495
+ }
60496
+
60497
+ const resolvedUids = new Set();
60498
+ for ( const span of spans ) {
60499
+
60500
+ if ( span._generation !== this._gpuGeneration ) continue;
60501
+
60502
+ let duration = 0;
60503
+ let resolvedQueries = 0;
60504
+ let rangedQueries = 0;
60505
+ let gpuStart = null;
60506
+ let gpuEnd = null;
60507
+ for ( const type of [ TimestampQuery.RENDER, TimestampQuery.COMPUTE ] ) {
60508
+
60509
+ for ( const uid of span._queries[ type ] ) {
60510
+
60511
+ if ( state.renderer.backend.hasTimestampQuery( uid ) ) {
60512
+
60513
+ duration += state.renderer.backend.getTimestamp( uid );
60514
+ resolvedQueries ++;
60515
+ resolvedUids.add( uid );
60516
+
60517
+ const range = state.renderer.backend.getTimestampRange( uid );
60518
+ if ( range !== null ) {
60519
+
60520
+ rangedQueries ++;
60521
+ if ( gpuStart === null || range.start < gpuStart ) gpuStart = range.start;
60522
+ if ( gpuEnd === null || range.end > gpuEnd ) gpuEnd = range.end;
60523
+
60524
+ }
60525
+
60526
+ }
60527
+
60528
+ }
60529
+
60530
+ }
60531
+
60532
+ if ( resolvedQueries > 0 ) {
60533
+
60534
+ let startTime = span.t0;
60535
+ if ( rangedQueries === resolvedQueries && gpuStart !== null && gpuEnd !== null && state.gpuTimestampOrigin !== null ) {
60536
+
60537
+ duration = Number( gpuEnd - gpuStart ) / 1e6;
60538
+ startTime = state.gpuTraceOrigin + Number( gpuStart - state.gpuTimestampOrigin ) / 1e6;
60539
+
60540
+ }
60541
+
60542
+ this._commitGpuDurationSample( span.label, startTime, duration );
60543
+
60544
+ }
60545
+
60546
+ }
60547
+
60548
+ for ( const uid of resolvedUids ) state.queryCpuTimes.delete( uid );
60549
+
60550
+ }
60551
+
60552
+ /**
60553
+ * @param {import('./ProfilerService.js').LegacyWebGLGpuRendererState} state
60554
+ */
60555
+ async _flushLegacyWebGLState( state ) {
60556
+
60557
+ const spans = state.pendingSpans.splice( 0 );
60558
+
60559
+ for ( const span of spans ) {
60560
+
60561
+ const duration = await this._resolveLegacyWebGLQuery( state, span._query );
60562
+ if ( duration !== null && span._generation === this._gpuGeneration && this._enabled ) {
60563
+
60564
+ this._commitGpuDurationSample( span.label, span.t0, duration );
60565
+
60566
+ }
60567
+
60568
+ }
60569
+
60570
+ }
60571
+
60572
+ /**
60573
+ * @param {import('./ProfilerService.js').LegacyWebGLGpuRendererState} state
60574
+ * @param {WebGLQuery} query
60575
+ * @return {Promise<?number>}
60576
+ */
60577
+ _resolveLegacyWebGLQuery( state, query ) {
60578
+
60579
+ return new Promise( resolve => {
60580
+
60581
+ const poll = () => {
60582
+
60583
+ if ( state.gl.isContextLost() ) {
60584
+
60585
+ state.gl.deleteQuery( query );
60586
+ resolve( null );
60587
+ return;
60588
+
60589
+ }
60590
+
60591
+ const disjoint = state.gl.getParameter( state.extension.GPU_DISJOINT_EXT );
60592
+ const available = state.gl.getQueryParameter( query, state.gl.QUERY_RESULT_AVAILABLE );
60593
+
60594
+ if ( available === false ) {
60595
+
60596
+ setTimeout( poll, 1 );
60597
+ return;
60598
+
60599
+ }
60600
+
60601
+ const result = disjoint
60602
+ ? null
60603
+ : Number( state.gl.getQueryParameter( query, state.gl.QUERY_RESULT ) ) / 1e6;
60604
+ state.gl.deleteQuery( query );
60605
+ resolve( result );
60606
+
60607
+ };
60608
+
60609
+ poll();
60610
+
60611
+ } );
60612
+
60613
+ }
60614
+
60128
60615
  /**
60129
60616
  * @param {string} label
60130
60617
  */
@@ -60282,6 +60769,45 @@ class ProfilerServiceClass {
60282
60769
 
60283
60770
  }
60284
60771
 
60772
+ /**
60773
+ * @param {string} label
60774
+ * @param {number} startTime
60775
+ * @param {number} durationMs
60776
+ */
60777
+ _commitGpuDurationSample( label, startTime, durationMs ) {
60778
+
60779
+ let buffer = this.gpuBuffers.get( label );
60780
+ if ( buffer === undefined ) {
60781
+
60782
+ buffer = new Float64Array( RING_SIZE );
60783
+ this.gpuBuffers.set( label, buffer );
60784
+ this.gpuCursors.set( label, 0 );
60785
+ this.gpuCounts.set( label, 0 );
60786
+
60787
+ }
60788
+
60789
+ const cursor = this.gpuCursors.get( label );
60790
+ buffer[ cursor ] = durationMs;
60791
+ this.gpuCursors.set( label, ( cursor + 1 ) % RING_SIZE );
60792
+ this.gpuCounts.set( label, Math.min( this.gpuCounts.get( label ) + 1, RING_SIZE ) );
60793
+ this.gpuInvocations.set( label, ( this.gpuInvocations.get( label ) ?? 0 ) + 1 );
60794
+
60795
+ if ( this._profile === 'full' ) {
60796
+
60797
+ this.traceEvents.push( {
60798
+ name: label,
60799
+ ph: 'X',
60800
+ ts: Math.round( this._getTraceTimestamp( startTime ) ),
60801
+ dur: Math.max( 1, Math.round( durationMs * 1000 ) ),
60802
+ pid: 1,
60803
+ tid: TRACE_TID_GPU,
60804
+ cat: 'gnsx-gpu',
60805
+ } );
60806
+
60807
+ }
60808
+
60809
+ }
60810
+
60285
60811
  /**
60286
60812
  * @param {string} label
60287
60813
  * @return {number[]}
@@ -60297,6 +60823,21 @@ class ProfilerServiceClass {
60297
60823
 
60298
60824
  }
60299
60825
 
60826
+ /**
60827
+ * @param {string} label
60828
+ * @return {number[]}
60829
+ */
60830
+ getValidGpuSamples( label ) {
60831
+
60832
+ const buffer = this.gpuBuffers.get( label );
60833
+ const count = this.gpuCounts.get( label ) ?? 0;
60834
+ if ( buffer === undefined || count === 0 ) return [];
60835
+ return count < RING_SIZE
60836
+ ? Array.from( buffer.subarray( 0, count ) )
60837
+ : Array.from( buffer );
60838
+
60839
+ }
60840
+
60300
60841
  /**
60301
60842
  * @param {string} label
60302
60843
  * @return {import('./ProfilerService.js').ProfilerStats|null}
@@ -60344,6 +60885,31 @@ class ProfilerServiceClass {
60344
60885
 
60345
60886
  }
60346
60887
 
60888
+ /**
60889
+ * @param {string} label
60890
+ * @return {import('./ProfilerService.js').GpuProfilerStats|null}
60891
+ */
60892
+ getGpuStats( label ) {
60893
+
60894
+ const samples = this.getValidGpuSamples( label );
60895
+ if ( samples.length === 0 ) return null;
60896
+
60897
+ const sorted = [ ...samples ].sort( ( a, b ) => a - b );
60898
+ const avg = sorted.reduce( ( a, b ) => a + b, 0 ) / sorted.length;
60899
+
60900
+ return {
60901
+ label,
60902
+ samples: samples.length,
60903
+ avg,
60904
+ min: sorted[ 0 ],
60905
+ max: sorted[ sorted.length - 1 ],
60906
+ p95: sorted[ Math.floor( sorted.length * 0.95 ) ],
60907
+ frameBudget: ( avg / FRAME_BUDGET_MS ) * 100,
60908
+ totalInvocations: this.gpuInvocations.get( label ) ?? 0,
60909
+ };
60910
+
60911
+ }
60912
+
60347
60913
  /**
60348
60914
  * @return {import('./ProfilerService.js').ProfilerStats[]}
60349
60915
  */
@@ -60355,10 +60921,22 @@ class ProfilerServiceClass {
60355
60921
 
60356
60922
  }
60357
60923
 
60924
+ /**
60925
+ * @return {import('./ProfilerService.js').GpuProfilerStats[]}
60926
+ */
60927
+ getAllGpuStats() {
60928
+
60929
+ return [ ...this.gpuBuffers.keys() ]
60930
+ .map( label => this.getGpuStats( label ) )
60931
+ .filter( stats => stats !== null );
60932
+
60933
+ }
60934
+
60358
60935
  report() {
60359
60936
 
60360
60937
  const stats = this.getAllStats();
60361
- if ( stats.length === 0 ) {
60938
+ const gpuStats = this.getAllGpuStats();
60939
+ if ( stats.length === 0 && gpuStats.length === 0 ) {
60362
60940
 
60363
60941
  console.log( '[ProfilerService] No data. Enable profiling first and wait a few frames.' );
60364
60942
  return;
@@ -60378,6 +60956,24 @@ class ProfilerServiceClass {
60378
60956
  } ) )
60379
60957
  );
60380
60958
 
60959
+ if ( gpuStats.length > 0 ) {
60960
+
60961
+ gpuStats.sort( ( a, b ) => b.avg - a.avg );
60962
+ console.log( '[ProfilerService] GPU timings' );
60963
+ console.table(
60964
+ gpuStats.map( stats => ( {
60965
+ label: stats.label,
60966
+ 'avg ms': stats.avg.toFixed( 3 ),
60967
+ 'min ms': stats.min.toFixed( 3 ),
60968
+ 'max ms': stats.max.toFixed( 3 ),
60969
+ 'p95 ms': stats.p95.toFixed( 3 ),
60970
+ 'budget %': stats.frameBudget.toFixed( 1 ) + '%',
60971
+ samples: stats.samples,
60972
+ } ) )
60973
+ );
60974
+
60975
+ }
60976
+
60381
60977
  }
60382
60978
 
60383
60979
  /**
@@ -60394,6 +60990,7 @@ class ProfilerServiceClass {
60394
60990
 
60395
60991
  } );
60396
60992
  const hasAsync = slices.some( e => e.tid === TRACE_TID_ASYNC );
60993
+ const hasGpu = slices.some( e => e.tid === TRACE_TID_GPU );
60397
60994
  /** @type {import('./ProfilerService.js').ChromeTraceMetadataEvent[]} */
60398
60995
  const prefix = [
60399
60996
  {
@@ -60429,6 +61026,20 @@ class ProfilerServiceClass {
60429
61026
 
60430
61027
  }
60431
61028
 
61029
+ if ( hasGpu ) {
61030
+
61031
+ prefix.push( {
61032
+ cat: '__metadata',
61033
+ name: 'thread_name',
61034
+ ph: 'M',
61035
+ pid: 1,
61036
+ tid: TRACE_TID_GPU,
61037
+ ts: 0,
61038
+ args: { name: 'GPU (device timestamps)' },
61039
+ } );
61040
+
61041
+ }
61042
+
60432
61043
  return {
60433
61044
  displayTimeUnit: 'ms',
60434
61045
  traceEvents: [ ...prefix, ...slices ],
@@ -60493,8 +61104,24 @@ class ProfilerServiceClass {
60493
61104
 
60494
61105
  }
60495
61106
 
61107
+ /**
61108
+ * @return {import('./ProfilerService.js').GpuProfilerStats[]}
61109
+ */
61110
+ exportGpuJSON() {
61111
+
61112
+ return this.getAllGpuStats();
61113
+
61114
+ }
61115
+
60496
61116
  _clearState() {
60497
61117
 
61118
+ this._gpuGeneration ++;
61119
+ for ( const state of this.gpuRendererStates.values() ) {
61120
+
61121
+ this._clearGpuRendererState( state );
61122
+
61123
+ }
61124
+
60498
61125
  this.buffers.clear();
60499
61126
  this.cursors.clear();
60500
61127
  this.counts.clear();
@@ -60507,6 +61134,79 @@ class ProfilerServiceClass {
60507
61134
  this.selfCounts.clear();
60508
61135
  this.callStack.length = 0;
60509
61136
  this.invocations.clear();
61137
+ this.gpuBuffers.clear();
61138
+ this.gpuCursors.clear();
61139
+ this.gpuCounts.clear();
61140
+ this.gpuInvocations.clear();
61141
+
61142
+ }
61143
+
61144
+ /**
61145
+ * @param {import('./ProfilerService.js').GpuRendererState} state
61146
+ */
61147
+ _clearGpuRendererState( state ) {
61148
+
61149
+ if ( state.kind === 'common' ) {
61150
+
61151
+ state.activeSpans.length = 0;
61152
+ state.pendingSpans.length = 0;
61153
+ state.queryCpuTimes.clear();
61154
+ state.gpuTimestampOrigin = null;
61155
+ state.gpuTraceOrigin = null;
61156
+ return;
61157
+
61158
+ }
61159
+
61160
+ if ( state.activeSpan !== null ) {
61161
+
61162
+ try {
61163
+
61164
+ state.gl.endQuery( state.extension.TIME_ELAPSED_EXT );
61165
+
61166
+ } catch {
61167
+
61168
+ // The context may have been lost while the query was active.
61169
+
61170
+ }
61171
+
61172
+ state.gl.deleteQuery( state.activeSpan._query );
61173
+ state.activeSpan = null;
61174
+
61175
+ }
61176
+
61177
+ if ( state.flushPromise === null ) {
61178
+
61179
+ for ( const span of state.pendingSpans ) {
61180
+
61181
+ state.gl.deleteQuery( span._query );
61182
+
61183
+ }
61184
+
61185
+ }
61186
+
61187
+ state.pendingSpans.length = 0;
61188
+
61189
+ }
61190
+
61191
+ _detachGpuRenderers() {
61192
+
61193
+ for ( const state of this.gpuRendererStates.values() ) {
61194
+
61195
+ if ( state.kind === 'common' && state.available ) {
61196
+
61197
+ state.renderer.backend.removeTimestampQueryListener( state.listener );
61198
+
61199
+ }
61200
+
61201
+ if ( state.kind === 'common' ) {
61202
+
61203
+ state.renderer.backend.trackTimestamp = state.previousTrackTimestamp;
61204
+
61205
+ }
61206
+
61207
+ }
61208
+
61209
+ this.gpuRendererStates.clear();
60510
61210
 
60511
61211
  }
60512
61212
 
@@ -60536,6 +61236,18 @@ class ProfilerServiceClass {
60536
61236
  * @property {number|undefined} selfFrameBudget Exclusive time as percentage of a 60 fps frame budget.
60537
61237
  */
60538
61238
 
61239
+ /**
61240
+ * @typedef {Object} GpuProfilerStats
61241
+ * @property {string} label
61242
+ * @property {number} samples
61243
+ * @property {number} avg
61244
+ * @property {number} min
61245
+ * @property {number} max
61246
+ * @property {number} p95
61247
+ * @property {number} frameBudget Percentage of a 60 fps frame budget (16.67 ms)
61248
+ * @property {number} totalInvocations
61249
+ */
61250
+
60539
61251
  /**
60540
61252
  * @typedef {Object} SpanHandle
60541
61253
  * @property {string} label
@@ -60544,6 +61256,51 @@ class ProfilerServiceClass {
60544
61256
  * @property {string} _startMark
60545
61257
  */
60546
61258
 
61259
+ /**
61260
+ * @typedef {Object} GpuSpanHandle
61261
+ * @property {string} label
61262
+ * @property {?Object} renderer
61263
+ * @property {number} t0
61264
+ * @property {number} _seq
61265
+ * @property {number} _generation
61266
+ * @property {{render: Set<string>, compute: Set<string>}} [_queries]
61267
+ * @property {WebGLQuery} [_query]
61268
+ * @property {number} [_endTime]
61269
+ */
61270
+
61271
+ /**
61272
+ * @typedef {Object} CommonGpuRendererState
61273
+ * @property {'common'} kind
61274
+ * @property {boolean} available
61275
+ * @property {Object} renderer
61276
+ * @property {function('render'|'compute', string, ?string): void} listener
61277
+ * @property {boolean} previousTrackTimestamp
61278
+ * @property {GpuSpanHandle[]} activeSpans
61279
+ * @property {GpuSpanHandle[]} pendingSpans
61280
+ * @property {Map<string, number>} queryCpuTimes
61281
+ * @property {?bigint} gpuTimestampOrigin
61282
+ * @property {?number} gpuTraceOrigin
61283
+ * @property {?Promise<void>} flushPromise
61284
+ * @property {boolean} flushScheduled
61285
+ */
61286
+
61287
+ /**
61288
+ * @typedef {Object} LegacyWebGLGpuRendererState
61289
+ * @property {'legacy-webgl'} kind
61290
+ * @property {boolean} available
61291
+ * @property {Object} renderer
61292
+ * @property {WebGL2RenderingContext} gl
61293
+ * @property {?EXT_disjoint_timer_query_webgl2} extension
61294
+ * @property {?GpuSpanHandle} activeSpan
61295
+ * @property {GpuSpanHandle[]} pendingSpans
61296
+ * @property {?Promise<void>} flushPromise
61297
+ * @property {boolean} flushScheduled
61298
+ */
61299
+
61300
+ /**
61301
+ * @typedef {CommonGpuRendererState|LegacyWebGLGpuRendererState} GpuRendererState
61302
+ */
61303
+
60547
61304
  /**
60548
61305
  * @typedef {Object} EndSpanOptions
60549
61306
  * @property {boolean} [asyncTimeline] When true, trace slice uses `tid=2` (virtual async row).
@@ -60557,7 +61314,7 @@ class ProfilerServiceClass {
60557
61314
  * @property {number} dur
60558
61315
  * @property {1} pid
60559
61316
  * @property {number} tid
60560
- * @property {'gnsx'} cat
61317
+ * @property {'gnsx'|'gnsx-gpu'} cat
60561
61318
  */
60562
61319
 
60563
61320
  /**