@gnsx/three 0.185.9 → 0.185.11

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,687 @@ 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
+ // Queue labelled passes with the active beginGpu span(s). Flushing
60393
+ // immediately would commit them before their parent envelope exists and
60394
+ // can anchor gpuTraceOrigin mid-frame, which breaks Speedscope nesting.
60395
+ const passSpan = {
60396
+ label: `GPU pass: ${label}`,
60397
+ renderer,
60398
+ t0: cpuTime,
60399
+ _seq: ++ this._markId,
60400
+ _generation: this._gpuGeneration,
60401
+ _queries: {
60402
+ [ TimestampQuery.RENDER ]: new Set(),
60403
+ [ TimestampQuery.COMPUTE ]: new Set(),
60404
+ },
60405
+ };
60406
+ passSpan._queries[ type ].add( uid );
60407
+ state.pendingSpans.push( passSpan );
60408
+ // Only auto-flush when no beginGpu parent is open. Nested parents flush on endGpu
60409
+ // so pass slices share one origin and nest inside the parent envelope.
60410
+ if ( state.activeSpans.length === 0 ) {
60411
+
60412
+ this._scheduleGpuFlush( state );
60413
+
60414
+ }
60415
+
60416
+ }
60417
+
60418
+ }
60419
+
60420
+ /**
60421
+ * @param {import('./ProfilerService.js').GpuRendererState} state
60422
+ */
60423
+ _scheduleGpuFlush( state ) {
60424
+
60425
+ if ( state.flushScheduled ) return;
60426
+ state.flushScheduled = true;
60427
+
60428
+ const callback = () => {
60429
+
60430
+ state.flushScheduled = false;
60431
+ if ( this.gpuRendererStates.get( state.renderer ) === state ) {
60432
+
60433
+ this.flushGpu( state.renderer ).catch( error => {
60434
+
60435
+ warnOnce( `ProfilerService: Unable to resolve GPU timestamps (${error.message}).` );
60436
+
60437
+ } );
60438
+
60439
+ }
60440
+
60441
+ };
60442
+
60443
+ if ( typeof requestAnimationFrame === 'function' ) {
60444
+
60445
+ requestAnimationFrame( callback );
60446
+
60447
+ } else {
60448
+
60449
+ setTimeout( callback, 0 );
60450
+
60451
+ }
60452
+
60453
+ }
60454
+
60455
+ /**
60456
+ * @param {import('./ProfilerService.js').CommonGpuRendererState} state
60457
+ */
60458
+ async _flushCommonGpuState( state ) {
60459
+
60460
+ const spans = state.pendingSpans.splice( 0 );
60461
+ if ( spans.length === 0 ) return;
60462
+
60463
+ const hasRenderQueries = spans.some( span => span._queries.render.size > 0 );
60464
+ const hasComputeQueries = spans.some( span => span._queries.compute.size > 0 );
60465
+ const resolutions = [];
60466
+
60467
+ if ( hasRenderQueries ) resolutions.push( state.renderer.resolveTimestampsAsync( TimestampQuery.RENDER ) );
60468
+ if ( hasComputeQueries ) resolutions.push( state.renderer.resolveTimestampsAsync( TimestampQuery.COMPUTE ) );
60469
+ await Promise.all( resolutions );
60470
+
60471
+ if ( this._enabled === false ) return;
60472
+
60473
+ if ( state.gpuTimestampOrigin === null ) {
60474
+
60475
+ const origin = this._findEarliestGpuTimestampOrigin( state, spans );
60476
+ if ( origin !== null ) {
60477
+
60478
+ state.gpuTimestampOrigin = origin.gpuTimestampOrigin;
60479
+ state.gpuTraceOrigin = origin.gpuTraceOrigin;
60480
+
60481
+ }
60482
+
60483
+ }
60484
+
60485
+ const resolvedUids = new Set();
60486
+ /** @type {Array<{ label: string, startTime: number, durationMs: number, isPass: boolean }>} */
60487
+ const samples = [];
60488
+ /** @type {Set<string>} */
60489
+ const labelledQueryUids = new Set();
60490
+
60491
+ for ( const span of spans ) {
60492
+
60493
+ if ( span._generation !== this._gpuGeneration ) continue;
60494
+
60495
+ let duration = 0;
60496
+ let resolvedQueries = 0;
60497
+ let rangedQueries = 0;
60498
+ let gpuStart = null;
60499
+ let gpuEnd = null;
60500
+ for ( const type of [ TimestampQuery.RENDER, TimestampQuery.COMPUTE ] ) {
60501
+
60502
+ for ( const uid of span._queries[ type ] ) {
60503
+
60504
+ if ( state.renderer.backend.hasTimestampQuery( uid ) ) {
60505
+
60506
+ duration += state.renderer.backend.getTimestamp( uid );
60507
+ resolvedQueries ++;
60508
+ resolvedUids.add( uid );
60509
+
60510
+ const range = state.renderer.backend.getTimestampRange( uid );
60511
+ if ( range !== null ) {
60512
+
60513
+ rangedQueries ++;
60514
+ if ( gpuStart === null || range.start < gpuStart ) gpuStart = range.start;
60515
+ if ( gpuEnd === null || range.end > gpuEnd ) gpuEnd = range.end;
60516
+
60517
+ }
60518
+
60519
+ }
60520
+
60521
+ }
60522
+
60523
+ }
60524
+
60525
+ if ( resolvedQueries > 0 ) {
60526
+
60527
+ let startTime = span.t0;
60528
+ // Prefer the device envelope whenever any query has a range. Requiring
60529
+ // *all* queries to be ranged forced a CPU-t0 + summed-duration fallback
60530
+ // that often ended before later GPU pass slices, breaking flame nesting.
60531
+ if ( rangedQueries > 0 && gpuStart !== null && gpuEnd !== null && state.gpuTimestampOrigin !== null ) {
60532
+
60533
+ duration = Number( gpuEnd - gpuStart ) / 1e6;
60534
+ startTime = state.gpuTraceOrigin + Number( gpuStart - state.gpuTimestampOrigin ) / 1e6;
60535
+
60536
+ }
60537
+
60538
+ const isPass = span.label.startsWith( 'GPU pass:' );
60539
+ if ( isPass ) {
60540
+
60541
+ for ( const type of [ TimestampQuery.RENDER, TimestampQuery.COMPUTE ] ) {
60542
+
60543
+ for ( const uid of span._queries[ type ] ) labelledQueryUids.add( uid );
60544
+
60545
+ }
60546
+
60547
+ }
60548
+
60549
+ samples.push( {
60550
+ label: span.label,
60551
+ startTime,
60552
+ durationMs: duration,
60553
+ isPass,
60554
+ } );
60555
+
60556
+ }
60557
+
60558
+ }
60559
+
60560
+ // Parent beginGpu spans collect every timestamp query, including renders that
60561
+ // had no gpuProfilerLabel. Surface those as pass slices so they are not empty
60562
+ // holes under the parent envelope.
60563
+ for ( const span of spans ) {
60564
+
60565
+ if ( span._generation !== this._gpuGeneration ) continue;
60566
+ if ( span.label.startsWith( 'GPU pass:' ) ) continue;
60567
+ if ( state.gpuTimestampOrigin === null ) continue;
60568
+
60569
+ for ( const type of [ TimestampQuery.RENDER, TimestampQuery.COMPUTE ] ) {
60570
+
60571
+ for ( const uid of span._queries[ type ] ) {
60572
+
60573
+ if ( labelledQueryUids.has( uid ) ) continue;
60574
+ if ( state.renderer.backend.hasTimestampQuery( uid ) === false ) continue;
60575
+
60576
+ const range = state.renderer.backend.getTimestampRange( uid );
60577
+ if ( range === null ) continue;
60578
+
60579
+ const durationMs = Number( range.end - range.start ) / 1e6;
60580
+ if ( durationMs <= 0 ) continue;
60581
+
60582
+ samples.push( {
60583
+ label: 'GPU pass: (unlabeled)',
60584
+ startTime: state.gpuTraceOrigin + Number( range.start - state.gpuTimestampOrigin ) / 1e6,
60585
+ durationMs,
60586
+ isPass: true,
60587
+ } );
60588
+ labelledQueryUids.add( uid );
60589
+
60590
+ }
60591
+
60592
+ }
60593
+
60594
+ }
60595
+
60596
+ const passSamples = samples.filter( sample => sample.isPass );
60597
+ const otherSamples = samples.filter( sample => sample.isPass === false );
60598
+ this._snapHalfOpenGpuPassTraceIntervals( passSamples );
60599
+ this._expandGpuParentTraceToContainPasses( otherSamples, passSamples );
60600
+
60601
+ for ( const sample of otherSamples ) {
60602
+
60603
+ this._commitGpuDurationSample( sample.label, sample.startTime, sample.durationMs, {
60604
+ ts: sample.traceTs,
60605
+ dur: sample.traceDur,
60606
+ } );
60607
+
60608
+ }
60609
+
60610
+ for ( const sample of passSamples ) {
60611
+
60612
+ this._commitGpuDurationSample( sample.label, sample.startTime, sample.durationMs, {
60613
+ ts: sample.traceTs,
60614
+ dur: sample.traceDur,
60615
+ } );
60616
+
60617
+ }
60618
+
60619
+ // Keep CPU times for queries still referenced by active parent spans.
60620
+ for ( const uid of resolvedUids ) {
60621
+
60622
+ if ( this._isGpuQueryReferencedByActiveSpans( state, uid ) ) continue;
60623
+ state.queryCpuTimes.delete( uid );
60624
+
60625
+ }
60626
+
60627
+ }
60628
+
60629
+ /**
60630
+ * Convert labelled GPU pass samples to non-overlapping half-open µs intervals for
60631
+ * Chrome/Speedscope traces. Stats keep the raw measured durations.
60632
+ *
60633
+ * @param {Array<{ startTime: number, durationMs: number, traceTs?: number, traceDur?: number }>} samples
60634
+ */
60635
+ _snapHalfOpenGpuPassTraceIntervals( samples ) {
60636
+
60637
+ if ( samples.length === 0 ) return;
60638
+
60639
+ for ( const sample of samples ) {
60640
+
60641
+ sample.traceTs = Math.round( this._getTraceTimestamp( sample.startTime ) );
60642
+ sample.traceDur = Math.max( 1, Math.round( sample.durationMs * 1000 ) );
60643
+
60644
+ }
60645
+
60646
+ // Longer first at the same timestamp so the substantial pass keeps its start.
60647
+ samples.sort( ( a, b ) => a.traceTs - b.traceTs || b.traceDur - a.traceDur );
60648
+
60649
+ let cursor = Number.NEGATIVE_INFINITY;
60650
+ for ( const sample of samples ) {
60651
+
60652
+ if ( sample.traceTs < cursor ) {
60653
+
60654
+ const end = Math.max( sample.traceTs + sample.traceDur, cursor + 1 );
60655
+ sample.traceTs = cursor;
60656
+ sample.traceDur = Math.max( 1, end - sample.traceTs );
60657
+
60658
+ }
60659
+
60660
+ cursor = sample.traceTs + sample.traceDur;
60661
+
60662
+ }
60663
+
60664
+ }
60665
+
60666
+ /**
60667
+ * Speedscope nests by time containment on a thread. If a snapped GPU pass
60668
+ * sticks out past its beginGpu parent (rounding / half-open adjust), the
60669
+ * parent fails containment and is pushed onto a lower lane under renderFrame.
60670
+ * Expand overlapping parents so they strictly cover their pass children.
60671
+ *
60672
+ * @param {Array<{ startTime: number, durationMs: number, traceTs?: number, traceDur?: number }>} parents
60673
+ * @param {Array<{ traceTs: number, traceDur: number }>} passes
60674
+ */
60675
+ _expandGpuParentTraceToContainPasses( parents, passes ) {
60676
+
60677
+ if ( parents.length === 0 || passes.length === 0 ) return;
60678
+
60679
+ for ( const parent of parents ) {
60680
+
60681
+ parent.traceTs = Math.round( this._getTraceTimestamp( parent.startTime ) );
60682
+ parent.traceDur = Math.max( 1, Math.round( parent.durationMs * 1000 ) );
60683
+
60684
+ const parentEnd = parent.traceTs + parent.traceDur;
60685
+ let minTs = parent.traceTs;
60686
+ let maxEnd = parentEnd;
60687
+ let touched = false;
60688
+
60689
+ for ( const pass of passes ) {
60690
+
60691
+ const passEnd = pass.traceTs + pass.traceDur;
60692
+ if ( pass.traceTs >= parentEnd || passEnd <= parent.traceTs ) continue;
60693
+
60694
+ touched = true;
60695
+ if ( pass.traceTs < minTs ) minTs = pass.traceTs;
60696
+ if ( passEnd > maxEnd ) maxEnd = passEnd;
60697
+
60698
+ }
60699
+
60700
+ if ( touched === false ) continue;
60701
+
60702
+ // Start 1µs before the first child so equal-start pairs still nest
60703
+ // (Speedscope sorts same-ts by longer-first; a child that begins
60704
+ // strictly after the parent is unambiguous).
60705
+ parent.traceTs = minTs > 0 ? minTs - 1 : minTs;
60706
+ parent.traceDur = Math.max( 1, maxEnd - parent.traceTs );
60707
+
60708
+ }
60709
+
60710
+ }
60711
+
60712
+ /**
60713
+ * @param {import('./ProfilerService.js').CommonGpuRendererState} state
60714
+ * @param {import('./ProfilerService.js').GpuSpanHandle[]} spans
60715
+ * @return {?{ gpuTimestampOrigin: bigint, gpuTraceOrigin: number }}
60716
+ */
60717
+ _findEarliestGpuTimestampOrigin( state, spans ) {
60718
+
60719
+ let earliestRange = null;
60720
+ let earliestUid = null;
60721
+ const candidates = spans.concat( state.activeSpans );
60722
+
60723
+ for ( const span of candidates ) {
60724
+
60725
+ for ( const type of [ TimestampQuery.RENDER, TimestampQuery.COMPUTE ] ) {
60726
+
60727
+ const queries = span._queries?.[ type ];
60728
+ if ( queries === undefined ) continue;
60729
+
60730
+ for ( const uid of queries ) {
60731
+
60732
+ const range = state.renderer.backend.getTimestampRange( uid );
60733
+ if ( range !== null && ( earliestRange === null || range.start < earliestRange.start ) ) {
60734
+
60735
+ earliestRange = range;
60736
+ earliestUid = uid;
60737
+
60738
+ }
60739
+
60740
+ }
60741
+
60742
+ }
60743
+
60744
+ }
60745
+
60746
+ if ( earliestRange === null ) return null;
60747
+
60748
+ return {
60749
+ gpuTimestampOrigin: earliestRange.start,
60750
+ gpuTraceOrigin: state.queryCpuTimes.get( earliestUid ) ?? spans[ 0 ].t0,
60751
+ };
60752
+
60753
+ }
60754
+
60755
+ /**
60756
+ * @param {import('./ProfilerService.js').CommonGpuRendererState} state
60757
+ * @param {string} uid
60758
+ * @return {boolean}
60759
+ */
60760
+ _isGpuQueryReferencedByActiveSpans( state, uid ) {
60761
+
60762
+ for ( const span of state.activeSpans ) {
60763
+
60764
+ if ( span._queries.render.has( uid ) || span._queries.compute.has( uid ) ) return true;
60765
+
60766
+ }
60767
+
60768
+ return false;
60769
+
60770
+ }
60771
+
60772
+ /**
60773
+ * @param {import('./ProfilerService.js').LegacyWebGLGpuRendererState} state
60774
+ */
60775
+ async _flushLegacyWebGLState( state ) {
60776
+
60777
+ const spans = state.pendingSpans.splice( 0 );
60778
+
60779
+ for ( const span of spans ) {
60780
+
60781
+ const duration = await this._resolveLegacyWebGLQuery( state, span._query );
60782
+ if ( duration !== null && span._generation === this._gpuGeneration && this._enabled ) {
60783
+
60784
+ this._commitGpuDurationSample( span.label, span.t0, duration );
60785
+
60786
+ }
60787
+
60788
+ }
60789
+
60790
+ }
60791
+
60792
+ /**
60793
+ * @param {import('./ProfilerService.js').LegacyWebGLGpuRendererState} state
60794
+ * @param {WebGLQuery} query
60795
+ * @return {Promise<?number>}
60796
+ */
60797
+ _resolveLegacyWebGLQuery( state, query ) {
60798
+
60799
+ return new Promise( resolve => {
60800
+
60801
+ const poll = () => {
60802
+
60803
+ if ( state.gl.isContextLost() ) {
60804
+
60805
+ state.gl.deleteQuery( query );
60806
+ resolve( null );
60807
+ return;
60808
+
60809
+ }
60810
+
60811
+ const disjoint = state.gl.getParameter( state.extension.GPU_DISJOINT_EXT );
60812
+ const available = state.gl.getQueryParameter( query, state.gl.QUERY_RESULT_AVAILABLE );
60813
+
60814
+ if ( available === false ) {
60815
+
60816
+ setTimeout( poll, 1 );
60817
+ return;
60818
+
60819
+ }
60820
+
60821
+ const result = disjoint
60822
+ ? null
60823
+ : Number( state.gl.getQueryParameter( query, state.gl.QUERY_RESULT ) ) / 1e6;
60824
+ state.gl.deleteQuery( query );
60825
+ resolve( result );
60826
+
60827
+ };
60828
+
60829
+ poll();
60830
+
60831
+ } );
60832
+
60833
+ }
60834
+
60128
60835
  /**
60129
60836
  * @param {string} label
60130
60837
  */
@@ -60282,6 +60989,46 @@ class ProfilerServiceClass {
60282
60989
 
60283
60990
  }
60284
60991
 
60992
+ /**
60993
+ * @param {string} label
60994
+ * @param {number} startTime
60995
+ * @param {number} durationMs
60996
+ * @param {{ ts: number, dur: number }} [traceOverride]
60997
+ */
60998
+ _commitGpuDurationSample( label, startTime, durationMs, traceOverride = null ) {
60999
+
61000
+ let buffer = this.gpuBuffers.get( label );
61001
+ if ( buffer === undefined ) {
61002
+
61003
+ buffer = new Float64Array( RING_SIZE );
61004
+ this.gpuBuffers.set( label, buffer );
61005
+ this.gpuCursors.set( label, 0 );
61006
+ this.gpuCounts.set( label, 0 );
61007
+
61008
+ }
61009
+
61010
+ const cursor = this.gpuCursors.get( label );
61011
+ buffer[ cursor ] = durationMs;
61012
+ this.gpuCursors.set( label, ( cursor + 1 ) % RING_SIZE );
61013
+ this.gpuCounts.set( label, Math.min( this.gpuCounts.get( label ) + 1, RING_SIZE ) );
61014
+ this.gpuInvocations.set( label, ( this.gpuInvocations.get( label ) ?? 0 ) + 1 );
61015
+
61016
+ if ( this._profile === 'full' ) {
61017
+
61018
+ this.traceEvents.push( {
61019
+ name: label,
61020
+ ph: 'X',
61021
+ ts: traceOverride?.ts ?? Math.round( this._getTraceTimestamp( startTime ) ),
61022
+ dur: traceOverride?.dur ?? Math.max( 1, Math.round( durationMs * 1000 ) ),
61023
+ pid: 1,
61024
+ tid: TRACE_TID_GPU,
61025
+ cat: 'gnsx-gpu',
61026
+ } );
61027
+
61028
+ }
61029
+
61030
+ }
61031
+
60285
61032
  /**
60286
61033
  * @param {string} label
60287
61034
  * @return {number[]}
@@ -60297,6 +61044,21 @@ class ProfilerServiceClass {
60297
61044
 
60298
61045
  }
60299
61046
 
61047
+ /**
61048
+ * @param {string} label
61049
+ * @return {number[]}
61050
+ */
61051
+ getValidGpuSamples( label ) {
61052
+
61053
+ const buffer = this.gpuBuffers.get( label );
61054
+ const count = this.gpuCounts.get( label ) ?? 0;
61055
+ if ( buffer === undefined || count === 0 ) return [];
61056
+ return count < RING_SIZE
61057
+ ? Array.from( buffer.subarray( 0, count ) )
61058
+ : Array.from( buffer );
61059
+
61060
+ }
61061
+
60300
61062
  /**
60301
61063
  * @param {string} label
60302
61064
  * @return {import('./ProfilerService.js').ProfilerStats|null}
@@ -60344,6 +61106,31 @@ class ProfilerServiceClass {
60344
61106
 
60345
61107
  }
60346
61108
 
61109
+ /**
61110
+ * @param {string} label
61111
+ * @return {import('./ProfilerService.js').GpuProfilerStats|null}
61112
+ */
61113
+ getGpuStats( label ) {
61114
+
61115
+ const samples = this.getValidGpuSamples( label );
61116
+ if ( samples.length === 0 ) return null;
61117
+
61118
+ const sorted = [ ...samples ].sort( ( a, b ) => a - b );
61119
+ const avg = sorted.reduce( ( a, b ) => a + b, 0 ) / sorted.length;
61120
+
61121
+ return {
61122
+ label,
61123
+ samples: samples.length,
61124
+ avg,
61125
+ min: sorted[ 0 ],
61126
+ max: sorted[ sorted.length - 1 ],
61127
+ p95: sorted[ Math.floor( sorted.length * 0.95 ) ],
61128
+ frameBudget: ( avg / FRAME_BUDGET_MS ) * 100,
61129
+ totalInvocations: this.gpuInvocations.get( label ) ?? 0,
61130
+ };
61131
+
61132
+ }
61133
+
60347
61134
  /**
60348
61135
  * @return {import('./ProfilerService.js').ProfilerStats[]}
60349
61136
  */
@@ -60355,10 +61142,22 @@ class ProfilerServiceClass {
60355
61142
 
60356
61143
  }
60357
61144
 
61145
+ /**
61146
+ * @return {import('./ProfilerService.js').GpuProfilerStats[]}
61147
+ */
61148
+ getAllGpuStats() {
61149
+
61150
+ return [ ...this.gpuBuffers.keys() ]
61151
+ .map( label => this.getGpuStats( label ) )
61152
+ .filter( stats => stats !== null );
61153
+
61154
+ }
61155
+
60358
61156
  report() {
60359
61157
 
60360
61158
  const stats = this.getAllStats();
60361
- if ( stats.length === 0 ) {
61159
+ const gpuStats = this.getAllGpuStats();
61160
+ if ( stats.length === 0 && gpuStats.length === 0 ) {
60362
61161
 
60363
61162
  console.log( '[ProfilerService] No data. Enable profiling first and wait a few frames.' );
60364
61163
  return;
@@ -60378,6 +61177,24 @@ class ProfilerServiceClass {
60378
61177
  } ) )
60379
61178
  );
60380
61179
 
61180
+ if ( gpuStats.length > 0 ) {
61181
+
61182
+ gpuStats.sort( ( a, b ) => b.avg - a.avg );
61183
+ console.log( '[ProfilerService] GPU timings' );
61184
+ console.table(
61185
+ gpuStats.map( stats => ( {
61186
+ label: stats.label,
61187
+ 'avg ms': stats.avg.toFixed( 3 ),
61188
+ 'min ms': stats.min.toFixed( 3 ),
61189
+ 'max ms': stats.max.toFixed( 3 ),
61190
+ 'p95 ms': stats.p95.toFixed( 3 ),
61191
+ 'budget %': stats.frameBudget.toFixed( 1 ) + '%',
61192
+ samples: stats.samples,
61193
+ } ) )
61194
+ );
61195
+
61196
+ }
61197
+
60381
61198
  }
60382
61199
 
60383
61200
  /**
@@ -60394,6 +61211,7 @@ class ProfilerServiceClass {
60394
61211
 
60395
61212
  } );
60396
61213
  const hasAsync = slices.some( e => e.tid === TRACE_TID_ASYNC );
61214
+ const hasGpu = slices.some( e => e.tid === TRACE_TID_GPU );
60397
61215
  /** @type {import('./ProfilerService.js').ChromeTraceMetadataEvent[]} */
60398
61216
  const prefix = [
60399
61217
  {
@@ -60429,6 +61247,20 @@ class ProfilerServiceClass {
60429
61247
 
60430
61248
  }
60431
61249
 
61250
+ if ( hasGpu ) {
61251
+
61252
+ prefix.push( {
61253
+ cat: '__metadata',
61254
+ name: 'thread_name',
61255
+ ph: 'M',
61256
+ pid: 1,
61257
+ tid: TRACE_TID_GPU,
61258
+ ts: 0,
61259
+ args: { name: 'GPU (device timestamps)' },
61260
+ } );
61261
+
61262
+ }
61263
+
60432
61264
  return {
60433
61265
  displayTimeUnit: 'ms',
60434
61266
  traceEvents: [ ...prefix, ...slices ],
@@ -60493,8 +61325,24 @@ class ProfilerServiceClass {
60493
61325
 
60494
61326
  }
60495
61327
 
61328
+ /**
61329
+ * @return {import('./ProfilerService.js').GpuProfilerStats[]}
61330
+ */
61331
+ exportGpuJSON() {
61332
+
61333
+ return this.getAllGpuStats();
61334
+
61335
+ }
61336
+
60496
61337
  _clearState() {
60497
61338
 
61339
+ this._gpuGeneration ++;
61340
+ for ( const state of this.gpuRendererStates.values() ) {
61341
+
61342
+ this._clearGpuRendererState( state );
61343
+
61344
+ }
61345
+
60498
61346
  this.buffers.clear();
60499
61347
  this.cursors.clear();
60500
61348
  this.counts.clear();
@@ -60507,6 +61355,79 @@ class ProfilerServiceClass {
60507
61355
  this.selfCounts.clear();
60508
61356
  this.callStack.length = 0;
60509
61357
  this.invocations.clear();
61358
+ this.gpuBuffers.clear();
61359
+ this.gpuCursors.clear();
61360
+ this.gpuCounts.clear();
61361
+ this.gpuInvocations.clear();
61362
+
61363
+ }
61364
+
61365
+ /**
61366
+ * @param {import('./ProfilerService.js').GpuRendererState} state
61367
+ */
61368
+ _clearGpuRendererState( state ) {
61369
+
61370
+ if ( state.kind === 'common' ) {
61371
+
61372
+ state.activeSpans.length = 0;
61373
+ state.pendingSpans.length = 0;
61374
+ state.queryCpuTimes.clear();
61375
+ state.gpuTimestampOrigin = null;
61376
+ state.gpuTraceOrigin = null;
61377
+ return;
61378
+
61379
+ }
61380
+
61381
+ if ( state.activeSpan !== null ) {
61382
+
61383
+ try {
61384
+
61385
+ state.gl.endQuery( state.extension.TIME_ELAPSED_EXT );
61386
+
61387
+ } catch {
61388
+
61389
+ // The context may have been lost while the query was active.
61390
+
61391
+ }
61392
+
61393
+ state.gl.deleteQuery( state.activeSpan._query );
61394
+ state.activeSpan = null;
61395
+
61396
+ }
61397
+
61398
+ if ( state.flushPromise === null ) {
61399
+
61400
+ for ( const span of state.pendingSpans ) {
61401
+
61402
+ state.gl.deleteQuery( span._query );
61403
+
61404
+ }
61405
+
61406
+ }
61407
+
61408
+ state.pendingSpans.length = 0;
61409
+
61410
+ }
61411
+
61412
+ _detachGpuRenderers() {
61413
+
61414
+ for ( const state of this.gpuRendererStates.values() ) {
61415
+
61416
+ if ( state.kind === 'common' && state.available ) {
61417
+
61418
+ state.renderer.backend.removeTimestampQueryListener( state.listener );
61419
+
61420
+ }
61421
+
61422
+ if ( state.kind === 'common' ) {
61423
+
61424
+ state.renderer.backend.trackTimestamp = state.previousTrackTimestamp;
61425
+
61426
+ }
61427
+
61428
+ }
61429
+
61430
+ this.gpuRendererStates.clear();
60510
61431
 
60511
61432
  }
60512
61433
 
@@ -60536,6 +61457,18 @@ class ProfilerServiceClass {
60536
61457
  * @property {number|undefined} selfFrameBudget Exclusive time as percentage of a 60 fps frame budget.
60537
61458
  */
60538
61459
 
61460
+ /**
61461
+ * @typedef {Object} GpuProfilerStats
61462
+ * @property {string} label
61463
+ * @property {number} samples
61464
+ * @property {number} avg
61465
+ * @property {number} min
61466
+ * @property {number} max
61467
+ * @property {number} p95
61468
+ * @property {number} frameBudget Percentage of a 60 fps frame budget (16.67 ms)
61469
+ * @property {number} totalInvocations
61470
+ */
61471
+
60539
61472
  /**
60540
61473
  * @typedef {Object} SpanHandle
60541
61474
  * @property {string} label
@@ -60544,6 +61477,51 @@ class ProfilerServiceClass {
60544
61477
  * @property {string} _startMark
60545
61478
  */
60546
61479
 
61480
+ /**
61481
+ * @typedef {Object} GpuSpanHandle
61482
+ * @property {string} label
61483
+ * @property {?Object} renderer
61484
+ * @property {number} t0
61485
+ * @property {number} _seq
61486
+ * @property {number} _generation
61487
+ * @property {{render: Set<string>, compute: Set<string>}} [_queries]
61488
+ * @property {WebGLQuery} [_query]
61489
+ * @property {number} [_endTime]
61490
+ */
61491
+
61492
+ /**
61493
+ * @typedef {Object} CommonGpuRendererState
61494
+ * @property {'common'} kind
61495
+ * @property {boolean} available
61496
+ * @property {Object} renderer
61497
+ * @property {function('render'|'compute', string, ?string): void} listener
61498
+ * @property {boolean} previousTrackTimestamp
61499
+ * @property {GpuSpanHandle[]} activeSpans
61500
+ * @property {GpuSpanHandle[]} pendingSpans
61501
+ * @property {Map<string, number>} queryCpuTimes
61502
+ * @property {?bigint} gpuTimestampOrigin
61503
+ * @property {?number} gpuTraceOrigin
61504
+ * @property {?Promise<void>} flushPromise
61505
+ * @property {boolean} flushScheduled
61506
+ */
61507
+
61508
+ /**
61509
+ * @typedef {Object} LegacyWebGLGpuRendererState
61510
+ * @property {'legacy-webgl'} kind
61511
+ * @property {boolean} available
61512
+ * @property {Object} renderer
61513
+ * @property {WebGL2RenderingContext} gl
61514
+ * @property {?EXT_disjoint_timer_query_webgl2} extension
61515
+ * @property {?GpuSpanHandle} activeSpan
61516
+ * @property {GpuSpanHandle[]} pendingSpans
61517
+ * @property {?Promise<void>} flushPromise
61518
+ * @property {boolean} flushScheduled
61519
+ */
61520
+
61521
+ /**
61522
+ * @typedef {CommonGpuRendererState|LegacyWebGLGpuRendererState} GpuRendererState
61523
+ */
61524
+
60547
61525
  /**
60548
61526
  * @typedef {Object} EndSpanOptions
60549
61527
  * @property {boolean} [asyncTimeline] When true, trace slice uses `tid=2` (virtual async row).
@@ -60557,7 +61535,7 @@ class ProfilerServiceClass {
60557
61535
  * @property {number} dur
60558
61536
  * @property {1} pid
60559
61537
  * @property {number} tid
60560
- * @property {'gnsx'} cat
61538
+ * @property {'gnsx'|'gnsx-gpu'} cat
60561
61539
  */
60562
61540
 
60563
61541
  /**