@gnsx/three 0.185.8 → 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.
@@ -60003,6 +60003,7 @@ class TextureUtils {
60003
60003
  }
60004
60004
 
60005
60005
  // WITH_GENESYS
60006
+
60006
60007
  /**
60007
60008
  * ProfilerService — per-label CPU timing with ring-buffer aggregation and DevTools integration.
60008
60009
  *
@@ -60023,12 +60024,16 @@ const RING_SIZE = 120;
60023
60024
  const FRAME_BUDGET_MS = 1000 / 60;
60024
60025
  const TRACE_TID_MAIN = 1;
60025
60026
  const TRACE_TID_ASYNC = 2;
60027
+ const TRACE_TID_GPU = 3;
60026
60028
  const NOOP = () => {};
60027
60029
 
60028
60030
  const DUMMY_SPAN = Object.freeze( { label: '', t0: 0, _seq: 0 } );
60031
+ const DUMMY_GPU_SPAN = Object.freeze( { label: '', renderer: null, t0: 0, _seq: 0, _generation: 0 } );
60029
60032
  const NOOP_BEGIN_SPAN = () => DUMMY_SPAN;
60030
60033
  const NOOP_END_SPAN = () => {};
60031
60034
 
60035
+ const NOOP_BEGIN_GPU_SPAN = () => DUMMY_GPU_SPAN;
60036
+
60032
60037
  class ProfilerServiceClass {
60033
60038
 
60034
60039
  constructor() {
@@ -60063,6 +60068,18 @@ class ProfilerServiceClass {
60063
60068
  /** @type {Map<string, number>} Total (uncapped) invocation count since last reset, for calls-per-frame. */
60064
60069
  this.invocations = new Map();
60065
60070
 
60071
+ /** @type {Map<string, Float64Array>} GPU time ring buffers (one per label). */
60072
+ this.gpuBuffers = new Map();
60073
+ /** @type {Map<string, number>} */
60074
+ this.gpuCursors = new Map();
60075
+ /** @type {Map<string, number>} */
60076
+ this.gpuCounts = new Map();
60077
+ /** @type {Map<string, number>} */
60078
+ this.gpuInvocations = new Map();
60079
+ /** @type {Map<Object, import('./ProfilerService.js').GpuRendererState>} */
60080
+ this.gpuRendererStates = new Map();
60081
+ this._gpuGeneration = 1;
60082
+
60066
60083
  if ( this._enabled ) {
60067
60084
 
60068
60085
  this.traceStartTime = performance.now();
@@ -60070,6 +60087,8 @@ class ProfilerServiceClass {
60070
60087
  this.end = this._endImpl.bind( this );
60071
60088
  this.beginSpan = this._beginSpanImpl.bind( this );
60072
60089
  this.endSpan = this._endSpanImpl.bind( this );
60090
+ this.beginGpu = this._beginGpuImpl.bind( this );
60091
+ this.endGpu = this._endGpuImpl.bind( this );
60073
60092
  this._exposeGlobal();
60074
60093
  console.log( `[ProfilerService] auto-started from env (profile: ${this._profile})` );
60075
60094
 
@@ -60079,6 +60098,8 @@ class ProfilerServiceClass {
60079
60098
  this.end = NOOP;
60080
60099
  this.beginSpan = NOOP_BEGIN_SPAN;
60081
60100
  this.endSpan = NOOP_END_SPAN;
60101
+ this.beginGpu = NOOP_BEGIN_GPU_SPAN;
60102
+ this.endGpu = NOOP_END_SPAN;
60082
60103
 
60083
60104
  }
60084
60105
 
@@ -60112,6 +60133,8 @@ class ProfilerServiceClass {
60112
60133
  this.end = this._endImpl.bind( this );
60113
60134
  this.beginSpan = this._beginSpanImpl.bind( this );
60114
60135
  this.endSpan = this._endSpanImpl.bind( this );
60136
+ this.beginGpu = this._beginGpuImpl.bind( this );
60137
+ this.endGpu = this._endGpuImpl.bind( this );
60115
60138
  this._exposeGlobal();
60116
60139
  console.log( `[ProfilerService] enabled (profile: ${this._profile}) — call __gnsx_profiler.report() or downloadTrace() from the console` );
60117
60140
 
@@ -60125,6 +60148,9 @@ class ProfilerServiceClass {
60125
60148
  this.end = NOOP;
60126
60149
  this.beginSpan = NOOP_BEGIN_SPAN;
60127
60150
  this.endSpan = NOOP_END_SPAN;
60151
+ this.beginGpu = NOOP_BEGIN_GPU_SPAN;
60152
+ this.endGpu = NOOP_END_SPAN;
60153
+ this._detachGpuRenderers();
60128
60154
  console.log( '[ProfilerService] disabled' );
60129
60155
 
60130
60156
  }
@@ -60145,6 +60171,467 @@ class ProfilerServiceClass {
60145
60171
 
60146
60172
  }
60147
60173
 
60174
+ /**
60175
+ * Enables GPU timestamp collection for a renderer.
60176
+ *
60177
+ * @param {Object} renderer
60178
+ * @return {Promise<boolean>} Whether GPU timestamps are available.
60179
+ */
60180
+ async attachGpuRenderer( renderer ) {
60181
+
60182
+ if ( this._enabled === false || renderer === null || renderer === undefined ) return false;
60183
+
60184
+ const existingState = this.gpuRendererStates.get( renderer );
60185
+ if ( existingState !== undefined ) return existingState.available;
60186
+
60187
+ if ( renderer.isWebGLRenderer === true ) {
60188
+
60189
+ const gl = renderer.getContext();
60190
+ const extension = gl.getExtension( 'EXT_disjoint_timer_query_webgl2' );
60191
+ const state = {
60192
+ kind: 'legacy-webgl',
60193
+ available: extension !== null,
60194
+ renderer,
60195
+ gl,
60196
+ extension,
60197
+ activeSpan: null,
60198
+ pendingSpans: [],
60199
+ flushPromise: null,
60200
+ flushScheduled: false,
60201
+ };
60202
+ this.gpuRendererStates.set( renderer, state );
60203
+
60204
+ if ( extension === null ) {
60205
+
60206
+ warnOnce( 'ProfilerService: EXT_disjoint_timer_query_webgl2 is unavailable; GPU profiling is disabled for this WebGLRenderer.' );
60207
+
60208
+ }
60209
+
60210
+ return state.available;
60211
+
60212
+ }
60213
+
60214
+ if ( renderer.isRenderer === true && renderer.backend !== undefined ) {
60215
+
60216
+ await renderer.init();
60217
+ const previousTrackTimestamp = renderer.backend.trackTimestamp;
60218
+ renderer.backend.trackTimestamp = true;
60219
+
60220
+ const available = renderer.backend.hasTimestamp === true && renderer.hasFeature( 'timestamp-query' ) === true;
60221
+ const listener = ( type, uid, label ) => this._captureGpuTimestampQuery( renderer, type, uid, label );
60222
+ const state = {
60223
+ kind: 'common',
60224
+ available,
60225
+ renderer,
60226
+ listener,
60227
+ previousTrackTimestamp,
60228
+ activeSpans: [],
60229
+ pendingSpans: [],
60230
+ queryCpuTimes: new Map(),
60231
+ gpuTimestampOrigin: null,
60232
+ gpuTraceOrigin: null,
60233
+ flushPromise: null,
60234
+ flushScheduled: false,
60235
+ };
60236
+ this.gpuRendererStates.set( renderer, state );
60237
+
60238
+ if ( available ) {
60239
+
60240
+ renderer.backend.addTimestampQueryListener( listener );
60241
+
60242
+ } else {
60243
+
60244
+ warnOnce( 'ProfilerService: Timestamp queries are unavailable; GPU profiling is disabled for this renderer.' );
60245
+
60246
+ }
60247
+
60248
+ return available;
60249
+
60250
+ }
60251
+
60252
+ warnOnce( 'ProfilerService: Unsupported renderer; expected Renderer or WebGLRenderer.' );
60253
+ return false;
60254
+
60255
+ }
60256
+
60257
+ /**
60258
+ * @param {string} label
60259
+ * @param {Object} renderer
60260
+ * @return {import('./ProfilerService.js').GpuSpanHandle}
60261
+ */
60262
+ _beginGpuImpl( label, renderer ) {
60263
+
60264
+ const state = this.gpuRendererStates.get( renderer );
60265
+ if ( state === undefined || state.available === false ) return DUMMY_GPU_SPAN;
60266
+
60267
+ const handle = {
60268
+ label,
60269
+ renderer,
60270
+ t0: performance.now(),
60271
+ _seq: ++ this._markId,
60272
+ _generation: this._gpuGeneration,
60273
+ };
60274
+
60275
+ if ( state.kind === 'common' ) {
60276
+
60277
+ handle._queries = {
60278
+ [ TimestampQuery.RENDER ]: new Set(),
60279
+ [ TimestampQuery.COMPUTE ]: new Set(),
60280
+ };
60281
+ state.activeSpans.push( handle );
60282
+ return handle;
60283
+
60284
+ }
60285
+
60286
+ if ( state.activeSpan !== null ) {
60287
+
60288
+ warnOnce( 'ProfilerService: Nested GPU spans are unsupported by legacy WebGLRenderer.' );
60289
+ return DUMMY_GPU_SPAN;
60290
+
60291
+ }
60292
+
60293
+ const query = state.gl.createQuery();
60294
+ if ( query === null ) return DUMMY_GPU_SPAN;
60295
+
60296
+ try {
60297
+
60298
+ state.gl.beginQuery( state.extension.TIME_ELAPSED_EXT, query );
60299
+ handle._query = query;
60300
+ state.activeSpan = handle;
60301
+ return handle;
60302
+
60303
+ } catch ( error ) {
60304
+
60305
+ state.gl.deleteQuery( query );
60306
+ warnOnce( `ProfilerService: Unable to begin a WebGL GPU span (${error.message}).` );
60307
+ return DUMMY_GPU_SPAN;
60308
+
60309
+ }
60310
+
60311
+ }
60312
+
60313
+ /**
60314
+ * @param {import('./ProfilerService.js').GpuSpanHandle} handle
60315
+ */
60316
+ _endGpuImpl( handle ) {
60317
+
60318
+ if ( handle._seq === 0 || handle._generation !== this._gpuGeneration ) return;
60319
+
60320
+ const state = this.gpuRendererStates.get( handle.renderer );
60321
+ if ( state === undefined || state.available === false ) return;
60322
+
60323
+ handle._endTime = performance.now();
60324
+
60325
+ if ( state.kind === 'common' ) {
60326
+
60327
+ const index = state.activeSpans.indexOf( handle );
60328
+ if ( index === -1 ) return;
60329
+
60330
+ state.activeSpans.splice( index, 1 );
60331
+ if ( handle._queries.render.size > 0 || handle._queries.compute.size > 0 ) {
60332
+
60333
+ state.pendingSpans.push( handle );
60334
+ this._scheduleGpuFlush( state );
60335
+
60336
+ }
60337
+
60338
+ return;
60339
+
60340
+ }
60341
+
60342
+ if ( state.activeSpan !== handle ) return;
60343
+
60344
+ try {
60345
+
60346
+ state.gl.endQuery( state.extension.TIME_ELAPSED_EXT );
60347
+ state.activeSpan = null;
60348
+ state.pendingSpans.push( handle );
60349
+ this._scheduleGpuFlush( state );
60350
+
60351
+ } catch ( error ) {
60352
+
60353
+ state.activeSpan = null;
60354
+ state.gl.deleteQuery( handle._query );
60355
+ warnOnce( `ProfilerService: Unable to end a WebGL GPU span (${error.message}).` );
60356
+
60357
+ }
60358
+
60359
+ }
60360
+
60361
+ /**
60362
+ * Resolves pending GPU timestamp queries for a renderer.
60363
+ *
60364
+ * @param {Object} renderer
60365
+ * @return {Promise<void>}
60366
+ */
60367
+ async flushGpu( renderer ) {
60368
+
60369
+ const state = this.gpuRendererStates.get( renderer );
60370
+ if ( state === undefined || state.available === false ) return;
60371
+ if ( state.flushPromise !== null ) return state.flushPromise;
60372
+
60373
+ state.flushPromise = state.kind === 'common'
60374
+ ? this._flushCommonGpuState( state )
60375
+ : this._flushLegacyWebGLState( state );
60376
+
60377
+ try {
60378
+
60379
+ await state.flushPromise;
60380
+
60381
+ } finally {
60382
+
60383
+ state.flushPromise = null;
60384
+ if ( state.pendingSpans.length > 0 ) this._scheduleGpuFlush( state );
60385
+
60386
+ }
60387
+
60388
+ }
60389
+
60390
+ /**
60391
+ * @param {Object} renderer
60392
+ * @param {'render'|'compute'} type
60393
+ * @param {string} uid
60394
+ * @param {?string} [label=null]
60395
+ */
60396
+ _captureGpuTimestampQuery( renderer, type, uid, label = null ) {
60397
+
60398
+ const state = this.gpuRendererStates.get( renderer );
60399
+ if ( state === undefined || state.kind !== 'common' ) return;
60400
+
60401
+ const cpuTime = performance.now();
60402
+ state.queryCpuTimes.set( uid, cpuTime );
60403
+
60404
+ for ( const span of state.activeSpans ) {
60405
+
60406
+ span._queries[ type ].add( uid );
60407
+
60408
+ }
60409
+
60410
+ if ( label !== null && label !== undefined && label !== '' ) {
60411
+
60412
+ const passSpan = {
60413
+ label: `GPU pass: ${label}`,
60414
+ renderer,
60415
+ t0: cpuTime,
60416
+ _seq: ++ this._markId,
60417
+ _generation: this._gpuGeneration,
60418
+ _queries: {
60419
+ [ TimestampQuery.RENDER ]: new Set(),
60420
+ [ TimestampQuery.COMPUTE ]: new Set(),
60421
+ },
60422
+ };
60423
+ passSpan._queries[ type ].add( uid );
60424
+ state.pendingSpans.push( passSpan );
60425
+ this._scheduleGpuFlush( state );
60426
+
60427
+ }
60428
+
60429
+ }
60430
+
60431
+ /**
60432
+ * @param {import('./ProfilerService.js').GpuRendererState} state
60433
+ */
60434
+ _scheduleGpuFlush( state ) {
60435
+
60436
+ if ( state.flushScheduled ) return;
60437
+ state.flushScheduled = true;
60438
+
60439
+ const callback = () => {
60440
+
60441
+ state.flushScheduled = false;
60442
+ if ( this.gpuRendererStates.get( state.renderer ) === state ) {
60443
+
60444
+ this.flushGpu( state.renderer ).catch( error => {
60445
+
60446
+ warnOnce( `ProfilerService: Unable to resolve GPU timestamps (${error.message}).` );
60447
+
60448
+ } );
60449
+
60450
+ }
60451
+
60452
+ };
60453
+
60454
+ if ( typeof requestAnimationFrame === 'function' ) {
60455
+
60456
+ requestAnimationFrame( callback );
60457
+
60458
+ } else {
60459
+
60460
+ setTimeout( callback, 0 );
60461
+
60462
+ }
60463
+
60464
+ }
60465
+
60466
+ /**
60467
+ * @param {import('./ProfilerService.js').CommonGpuRendererState} state
60468
+ */
60469
+ async _flushCommonGpuState( state ) {
60470
+
60471
+ const spans = state.pendingSpans.splice( 0 );
60472
+ if ( spans.length === 0 ) return;
60473
+
60474
+ const hasRenderQueries = spans.some( span => span._queries.render.size > 0 );
60475
+ const hasComputeQueries = spans.some( span => span._queries.compute.size > 0 );
60476
+ const resolutions = [];
60477
+
60478
+ if ( hasRenderQueries ) resolutions.push( state.renderer.resolveTimestampsAsync( TimestampQuery.RENDER ) );
60479
+ if ( hasComputeQueries ) resolutions.push( state.renderer.resolveTimestampsAsync( TimestampQuery.COMPUTE ) );
60480
+ await Promise.all( resolutions );
60481
+
60482
+ if ( this._enabled === false ) return;
60483
+
60484
+ if ( state.gpuTimestampOrigin === null ) {
60485
+
60486
+ let earliestRange = null;
60487
+ let earliestUid = null;
60488
+ for ( const span of spans ) {
60489
+
60490
+ for ( const type of [ TimestampQuery.RENDER, TimestampQuery.COMPUTE ] ) {
60491
+
60492
+ for ( const uid of span._queries[ type ] ) {
60493
+
60494
+ const range = state.renderer.backend.getTimestampRange( uid );
60495
+ if ( range !== null && ( earliestRange === null || range.start < earliestRange.start ) ) {
60496
+
60497
+ earliestRange = range;
60498
+ earliestUid = uid;
60499
+
60500
+ }
60501
+
60502
+ }
60503
+
60504
+ }
60505
+
60506
+ }
60507
+
60508
+ if ( earliestRange !== null ) {
60509
+
60510
+ state.gpuTimestampOrigin = earliestRange.start;
60511
+ state.gpuTraceOrigin = state.queryCpuTimes.get( earliestUid ) ?? spans[ 0 ].t0;
60512
+
60513
+ }
60514
+
60515
+ }
60516
+
60517
+ const resolvedUids = new Set();
60518
+ for ( const span of spans ) {
60519
+
60520
+ if ( span._generation !== this._gpuGeneration ) continue;
60521
+
60522
+ let duration = 0;
60523
+ let resolvedQueries = 0;
60524
+ let rangedQueries = 0;
60525
+ let gpuStart = null;
60526
+ let gpuEnd = null;
60527
+ for ( const type of [ TimestampQuery.RENDER, TimestampQuery.COMPUTE ] ) {
60528
+
60529
+ for ( const uid of span._queries[ type ] ) {
60530
+
60531
+ if ( state.renderer.backend.hasTimestampQuery( uid ) ) {
60532
+
60533
+ duration += state.renderer.backend.getTimestamp( uid );
60534
+ resolvedQueries ++;
60535
+ resolvedUids.add( uid );
60536
+
60537
+ const range = state.renderer.backend.getTimestampRange( uid );
60538
+ if ( range !== null ) {
60539
+
60540
+ rangedQueries ++;
60541
+ if ( gpuStart === null || range.start < gpuStart ) gpuStart = range.start;
60542
+ if ( gpuEnd === null || range.end > gpuEnd ) gpuEnd = range.end;
60543
+
60544
+ }
60545
+
60546
+ }
60547
+
60548
+ }
60549
+
60550
+ }
60551
+
60552
+ if ( resolvedQueries > 0 ) {
60553
+
60554
+ let startTime = span.t0;
60555
+ if ( rangedQueries === resolvedQueries && gpuStart !== null && gpuEnd !== null && state.gpuTimestampOrigin !== null ) {
60556
+
60557
+ duration = Number( gpuEnd - gpuStart ) / 1e6;
60558
+ startTime = state.gpuTraceOrigin + Number( gpuStart - state.gpuTimestampOrigin ) / 1e6;
60559
+
60560
+ }
60561
+
60562
+ this._commitGpuDurationSample( span.label, startTime, duration );
60563
+
60564
+ }
60565
+
60566
+ }
60567
+
60568
+ for ( const uid of resolvedUids ) state.queryCpuTimes.delete( uid );
60569
+
60570
+ }
60571
+
60572
+ /**
60573
+ * @param {import('./ProfilerService.js').LegacyWebGLGpuRendererState} state
60574
+ */
60575
+ async _flushLegacyWebGLState( state ) {
60576
+
60577
+ const spans = state.pendingSpans.splice( 0 );
60578
+
60579
+ for ( const span of spans ) {
60580
+
60581
+ const duration = await this._resolveLegacyWebGLQuery( state, span._query );
60582
+ if ( duration !== null && span._generation === this._gpuGeneration && this._enabled ) {
60583
+
60584
+ this._commitGpuDurationSample( span.label, span.t0, duration );
60585
+
60586
+ }
60587
+
60588
+ }
60589
+
60590
+ }
60591
+
60592
+ /**
60593
+ * @param {import('./ProfilerService.js').LegacyWebGLGpuRendererState} state
60594
+ * @param {WebGLQuery} query
60595
+ * @return {Promise<?number>}
60596
+ */
60597
+ _resolveLegacyWebGLQuery( state, query ) {
60598
+
60599
+ return new Promise( resolve => {
60600
+
60601
+ const poll = () => {
60602
+
60603
+ if ( state.gl.isContextLost() ) {
60604
+
60605
+ state.gl.deleteQuery( query );
60606
+ resolve( null );
60607
+ return;
60608
+
60609
+ }
60610
+
60611
+ const disjoint = state.gl.getParameter( state.extension.GPU_DISJOINT_EXT );
60612
+ const available = state.gl.getQueryParameter( query, state.gl.QUERY_RESULT_AVAILABLE );
60613
+
60614
+ if ( available === false ) {
60615
+
60616
+ setTimeout( poll, 1 );
60617
+ return;
60618
+
60619
+ }
60620
+
60621
+ const result = disjoint
60622
+ ? null
60623
+ : Number( state.gl.getQueryParameter( query, state.gl.QUERY_RESULT ) ) / 1e6;
60624
+ state.gl.deleteQuery( query );
60625
+ resolve( result );
60626
+
60627
+ };
60628
+
60629
+ poll();
60630
+
60631
+ } );
60632
+
60633
+ }
60634
+
60148
60635
  /**
60149
60636
  * @param {string} label
60150
60637
  */
@@ -60302,6 +60789,45 @@ class ProfilerServiceClass {
60302
60789
 
60303
60790
  }
60304
60791
 
60792
+ /**
60793
+ * @param {string} label
60794
+ * @param {number} startTime
60795
+ * @param {number} durationMs
60796
+ */
60797
+ _commitGpuDurationSample( label, startTime, durationMs ) {
60798
+
60799
+ let buffer = this.gpuBuffers.get( label );
60800
+ if ( buffer === undefined ) {
60801
+
60802
+ buffer = new Float64Array( RING_SIZE );
60803
+ this.gpuBuffers.set( label, buffer );
60804
+ this.gpuCursors.set( label, 0 );
60805
+ this.gpuCounts.set( label, 0 );
60806
+
60807
+ }
60808
+
60809
+ const cursor = this.gpuCursors.get( label );
60810
+ buffer[ cursor ] = durationMs;
60811
+ this.gpuCursors.set( label, ( cursor + 1 ) % RING_SIZE );
60812
+ this.gpuCounts.set( label, Math.min( this.gpuCounts.get( label ) + 1, RING_SIZE ) );
60813
+ this.gpuInvocations.set( label, ( this.gpuInvocations.get( label ) ?? 0 ) + 1 );
60814
+
60815
+ if ( this._profile === 'full' ) {
60816
+
60817
+ this.traceEvents.push( {
60818
+ name: label,
60819
+ ph: 'X',
60820
+ ts: Math.round( this._getTraceTimestamp( startTime ) ),
60821
+ dur: Math.max( 1, Math.round( durationMs * 1000 ) ),
60822
+ pid: 1,
60823
+ tid: TRACE_TID_GPU,
60824
+ cat: 'gnsx-gpu',
60825
+ } );
60826
+
60827
+ }
60828
+
60829
+ }
60830
+
60305
60831
  /**
60306
60832
  * @param {string} label
60307
60833
  * @return {number[]}
@@ -60317,6 +60843,21 @@ class ProfilerServiceClass {
60317
60843
 
60318
60844
  }
60319
60845
 
60846
+ /**
60847
+ * @param {string} label
60848
+ * @return {number[]}
60849
+ */
60850
+ getValidGpuSamples( label ) {
60851
+
60852
+ const buffer = this.gpuBuffers.get( label );
60853
+ const count = this.gpuCounts.get( label ) ?? 0;
60854
+ if ( buffer === undefined || count === 0 ) return [];
60855
+ return count < RING_SIZE
60856
+ ? Array.from( buffer.subarray( 0, count ) )
60857
+ : Array.from( buffer );
60858
+
60859
+ }
60860
+
60320
60861
  /**
60321
60862
  * @param {string} label
60322
60863
  * @return {import('./ProfilerService.js').ProfilerStats|null}
@@ -60364,6 +60905,31 @@ class ProfilerServiceClass {
60364
60905
 
60365
60906
  }
60366
60907
 
60908
+ /**
60909
+ * @param {string} label
60910
+ * @return {import('./ProfilerService.js').GpuProfilerStats|null}
60911
+ */
60912
+ getGpuStats( label ) {
60913
+
60914
+ const samples = this.getValidGpuSamples( label );
60915
+ if ( samples.length === 0 ) return null;
60916
+
60917
+ const sorted = [ ...samples ].sort( ( a, b ) => a - b );
60918
+ const avg = sorted.reduce( ( a, b ) => a + b, 0 ) / sorted.length;
60919
+
60920
+ return {
60921
+ label,
60922
+ samples: samples.length,
60923
+ avg,
60924
+ min: sorted[ 0 ],
60925
+ max: sorted[ sorted.length - 1 ],
60926
+ p95: sorted[ Math.floor( sorted.length * 0.95 ) ],
60927
+ frameBudget: ( avg / FRAME_BUDGET_MS ) * 100,
60928
+ totalInvocations: this.gpuInvocations.get( label ) ?? 0,
60929
+ };
60930
+
60931
+ }
60932
+
60367
60933
  /**
60368
60934
  * @return {import('./ProfilerService.js').ProfilerStats[]}
60369
60935
  */
@@ -60375,10 +60941,22 @@ class ProfilerServiceClass {
60375
60941
 
60376
60942
  }
60377
60943
 
60944
+ /**
60945
+ * @return {import('./ProfilerService.js').GpuProfilerStats[]}
60946
+ */
60947
+ getAllGpuStats() {
60948
+
60949
+ return [ ...this.gpuBuffers.keys() ]
60950
+ .map( label => this.getGpuStats( label ) )
60951
+ .filter( stats => stats !== null );
60952
+
60953
+ }
60954
+
60378
60955
  report() {
60379
60956
 
60380
60957
  const stats = this.getAllStats();
60381
- if ( stats.length === 0 ) {
60958
+ const gpuStats = this.getAllGpuStats();
60959
+ if ( stats.length === 0 && gpuStats.length === 0 ) {
60382
60960
 
60383
60961
  console.log( '[ProfilerService] No data. Enable profiling first and wait a few frames.' );
60384
60962
  return;
@@ -60398,6 +60976,24 @@ class ProfilerServiceClass {
60398
60976
  } ) )
60399
60977
  );
60400
60978
 
60979
+ if ( gpuStats.length > 0 ) {
60980
+
60981
+ gpuStats.sort( ( a, b ) => b.avg - a.avg );
60982
+ console.log( '[ProfilerService] GPU timings' );
60983
+ console.table(
60984
+ gpuStats.map( stats => ( {
60985
+ label: stats.label,
60986
+ 'avg ms': stats.avg.toFixed( 3 ),
60987
+ 'min ms': stats.min.toFixed( 3 ),
60988
+ 'max ms': stats.max.toFixed( 3 ),
60989
+ 'p95 ms': stats.p95.toFixed( 3 ),
60990
+ 'budget %': stats.frameBudget.toFixed( 1 ) + '%',
60991
+ samples: stats.samples,
60992
+ } ) )
60993
+ );
60994
+
60995
+ }
60996
+
60401
60997
  }
60402
60998
 
60403
60999
  /**
@@ -60414,6 +61010,7 @@ class ProfilerServiceClass {
60414
61010
 
60415
61011
  } );
60416
61012
  const hasAsync = slices.some( e => e.tid === TRACE_TID_ASYNC );
61013
+ const hasGpu = slices.some( e => e.tid === TRACE_TID_GPU );
60417
61014
  /** @type {import('./ProfilerService.js').ChromeTraceMetadataEvent[]} */
60418
61015
  const prefix = [
60419
61016
  {
@@ -60449,6 +61046,20 @@ class ProfilerServiceClass {
60449
61046
 
60450
61047
  }
60451
61048
 
61049
+ if ( hasGpu ) {
61050
+
61051
+ prefix.push( {
61052
+ cat: '__metadata',
61053
+ name: 'thread_name',
61054
+ ph: 'M',
61055
+ pid: 1,
61056
+ tid: TRACE_TID_GPU,
61057
+ ts: 0,
61058
+ args: { name: 'GPU (device timestamps)' },
61059
+ } );
61060
+
61061
+ }
61062
+
60452
61063
  return {
60453
61064
  displayTimeUnit: 'ms',
60454
61065
  traceEvents: [ ...prefix, ...slices ],
@@ -60513,8 +61124,24 @@ class ProfilerServiceClass {
60513
61124
 
60514
61125
  }
60515
61126
 
61127
+ /**
61128
+ * @return {import('./ProfilerService.js').GpuProfilerStats[]}
61129
+ */
61130
+ exportGpuJSON() {
61131
+
61132
+ return this.getAllGpuStats();
61133
+
61134
+ }
61135
+
60516
61136
  _clearState() {
60517
61137
 
61138
+ this._gpuGeneration ++;
61139
+ for ( const state of this.gpuRendererStates.values() ) {
61140
+
61141
+ this._clearGpuRendererState( state );
61142
+
61143
+ }
61144
+
60518
61145
  this.buffers.clear();
60519
61146
  this.cursors.clear();
60520
61147
  this.counts.clear();
@@ -60527,6 +61154,79 @@ class ProfilerServiceClass {
60527
61154
  this.selfCounts.clear();
60528
61155
  this.callStack.length = 0;
60529
61156
  this.invocations.clear();
61157
+ this.gpuBuffers.clear();
61158
+ this.gpuCursors.clear();
61159
+ this.gpuCounts.clear();
61160
+ this.gpuInvocations.clear();
61161
+
61162
+ }
61163
+
61164
+ /**
61165
+ * @param {import('./ProfilerService.js').GpuRendererState} state
61166
+ */
61167
+ _clearGpuRendererState( state ) {
61168
+
61169
+ if ( state.kind === 'common' ) {
61170
+
61171
+ state.activeSpans.length = 0;
61172
+ state.pendingSpans.length = 0;
61173
+ state.queryCpuTimes.clear();
61174
+ state.gpuTimestampOrigin = null;
61175
+ state.gpuTraceOrigin = null;
61176
+ return;
61177
+
61178
+ }
61179
+
61180
+ if ( state.activeSpan !== null ) {
61181
+
61182
+ try {
61183
+
61184
+ state.gl.endQuery( state.extension.TIME_ELAPSED_EXT );
61185
+
61186
+ } catch {
61187
+
61188
+ // The context may have been lost while the query was active.
61189
+
61190
+ }
61191
+
61192
+ state.gl.deleteQuery( state.activeSpan._query );
61193
+ state.activeSpan = null;
61194
+
61195
+ }
61196
+
61197
+ if ( state.flushPromise === null ) {
61198
+
61199
+ for ( const span of state.pendingSpans ) {
61200
+
61201
+ state.gl.deleteQuery( span._query );
61202
+
61203
+ }
61204
+
61205
+ }
61206
+
61207
+ state.pendingSpans.length = 0;
61208
+
61209
+ }
61210
+
61211
+ _detachGpuRenderers() {
61212
+
61213
+ for ( const state of this.gpuRendererStates.values() ) {
61214
+
61215
+ if ( state.kind === 'common' && state.available ) {
61216
+
61217
+ state.renderer.backend.removeTimestampQueryListener( state.listener );
61218
+
61219
+ }
61220
+
61221
+ if ( state.kind === 'common' ) {
61222
+
61223
+ state.renderer.backend.trackTimestamp = state.previousTrackTimestamp;
61224
+
61225
+ }
61226
+
61227
+ }
61228
+
61229
+ this.gpuRendererStates.clear();
60530
61230
 
60531
61231
  }
60532
61232
 
@@ -60556,6 +61256,18 @@ class ProfilerServiceClass {
60556
61256
  * @property {number|undefined} selfFrameBudget Exclusive time as percentage of a 60 fps frame budget.
60557
61257
  */
60558
61258
 
61259
+ /**
61260
+ * @typedef {Object} GpuProfilerStats
61261
+ * @property {string} label
61262
+ * @property {number} samples
61263
+ * @property {number} avg
61264
+ * @property {number} min
61265
+ * @property {number} max
61266
+ * @property {number} p95
61267
+ * @property {number} frameBudget Percentage of a 60 fps frame budget (16.67 ms)
61268
+ * @property {number} totalInvocations
61269
+ */
61270
+
60559
61271
  /**
60560
61272
  * @typedef {Object} SpanHandle
60561
61273
  * @property {string} label
@@ -60564,6 +61276,51 @@ class ProfilerServiceClass {
60564
61276
  * @property {string} _startMark
60565
61277
  */
60566
61278
 
61279
+ /**
61280
+ * @typedef {Object} GpuSpanHandle
61281
+ * @property {string} label
61282
+ * @property {?Object} renderer
61283
+ * @property {number} t0
61284
+ * @property {number} _seq
61285
+ * @property {number} _generation
61286
+ * @property {{render: Set<string>, compute: Set<string>}} [_queries]
61287
+ * @property {WebGLQuery} [_query]
61288
+ * @property {number} [_endTime]
61289
+ */
61290
+
61291
+ /**
61292
+ * @typedef {Object} CommonGpuRendererState
61293
+ * @property {'common'} kind
61294
+ * @property {boolean} available
61295
+ * @property {Object} renderer
61296
+ * @property {function('render'|'compute', string, ?string): void} listener
61297
+ * @property {boolean} previousTrackTimestamp
61298
+ * @property {GpuSpanHandle[]} activeSpans
61299
+ * @property {GpuSpanHandle[]} pendingSpans
61300
+ * @property {Map<string, number>} queryCpuTimes
61301
+ * @property {?bigint} gpuTimestampOrigin
61302
+ * @property {?number} gpuTraceOrigin
61303
+ * @property {?Promise<void>} flushPromise
61304
+ * @property {boolean} flushScheduled
61305
+ */
61306
+
61307
+ /**
61308
+ * @typedef {Object} LegacyWebGLGpuRendererState
61309
+ * @property {'legacy-webgl'} kind
61310
+ * @property {boolean} available
61311
+ * @property {Object} renderer
61312
+ * @property {WebGL2RenderingContext} gl
61313
+ * @property {?EXT_disjoint_timer_query_webgl2} extension
61314
+ * @property {?GpuSpanHandle} activeSpan
61315
+ * @property {GpuSpanHandle[]} pendingSpans
61316
+ * @property {?Promise<void>} flushPromise
61317
+ * @property {boolean} flushScheduled
61318
+ */
61319
+
61320
+ /**
61321
+ * @typedef {CommonGpuRendererState|LegacyWebGLGpuRendererState} GpuRendererState
61322
+ */
61323
+
60567
61324
  /**
60568
61325
  * @typedef {Object} EndSpanOptions
60569
61326
  * @property {boolean} [asyncTimeline] When true, trace slice uses `tid=2` (virtual async row).
@@ -60577,7 +61334,7 @@ class ProfilerServiceClass {
60577
61334
  * @property {number} dur
60578
61335
  * @property {1} pid
60579
61336
  * @property {number} tid
60580
- * @property {'gnsx'} cat
61337
+ * @property {'gnsx'|'gnsx-gpu'} cat
60581
61338
  */
60582
61339
 
60583
61340
  /**