• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

visgl / luma.gl / 23123400261

16 Mar 2026 12:51AM UTC coverage: 76.879% (-0.4%) from 77.251%
23123400261

push

github

web-flow
fix(engine): Re-enable GPU timing metrics in AnimationLoop (#2505)

* fix(engine): Re-enable GPU timing metrics in AnimationLoop

The GPU timing code in AnimationLoop was commented out during the v9
migration when the old Query class was removed. This commit re-enables
GPU time measurement using the new QuerySet API:

- Initialize GPU time query when 'timer-query-webgl' feature is available
- Use WEBGLQuerySet's beginTimestampQuery/endTimestampQuery methods
- Record GPU time results in _beginFrameTimers() when available
- Gracefully handle unsupported environments (WebGPU, missing extension)
- Clean up query resources in destroy()

This fixes deck.gl's gpuTimePerFrame metric always reporting 0.

Related: https://github.com/visgl/deck.gl/issues/9958

https://claude.ai/code/session_01H614UvusgZQfwVssKtezh9

* test(engine): Add test for GPU timing graceful fallback

Verifies that AnimationLoop:
- Creates gpuTime and cpuTime stats regardless of timer support
- Initializes _gpuTimeQuery only when timer-query-webgl is available
- Cleans up query resources on destroy without errors

https://claude.ai/code/session_01H614UvusgZQfwVssKtezh9

* comments

* wip

* cleanup

* format

* wip

* wip

* fix

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Xiaoji Chen <cxiaoji@gmail.com>
Co-authored-by: Ib Green <ib@unfolded.ai>

2622 of 3380 branches covered (77.57%)

Branch coverage included in aggregate %.

218 of 503 new or added lines in 10 files covered. (43.34%)

12 existing lines in 2 files now uncovered.

30413 of 39590 relevant lines covered (76.82%)

96.09 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

76.38
/modules/engine/src/animation-loop/animation-loop.ts
1
// luma.gl
1✔
2
// SPDX-License-Identifier: MIT
1✔
3
// Copyright (c) vis.gl contributors
1✔
4

1✔
5
import {luma, Device, QuerySet} from '@luma.gl/core';
1✔
6
import {
1✔
7
  requestAnimationFramePolyfill,
1✔
8
  cancelAnimationFramePolyfill
1✔
9
} from './request-animation-frame';
1✔
10
import {Timeline} from '../animation/timeline';
1✔
11
import {AnimationProps} from './animation-props';
1✔
12
import {Stats, Stat} from '@probe.gl/stats';
1✔
13

1✔
14
let statIdCounter = 0;
1✔
15

1✔
16
/** AnimationLoop properties */
1✔
17
export type AnimationLoopProps = {
1✔
18
  device: Device | Promise<Device>;
1✔
19

1✔
20
  onAddHTML?: (div: HTMLDivElement) => string; // innerHTML
1✔
21
  onInitialize?: (animationProps: AnimationProps) => Promise<unknown>;
1✔
22
  onRender?: (animationProps: AnimationProps) => unknown;
1✔
23
  onFinalize?: (animationProps: AnimationProps) => void;
1✔
24
  onError?: (reason: Error) => void;
1✔
25

1✔
26
  stats?: Stats;
1✔
27

1✔
28
  // view parameters - TODO move to CanvasContext?
1✔
29
  autoResizeViewport?: boolean;
1✔
30
};
1✔
31

1✔
32
export type MutableAnimationLoopProps = {
1✔
33
  // view parameters
1✔
34
  autoResizeViewport?: boolean;
1✔
35
};
1✔
36

1✔
37
/** Convenient animation loop */
1✔
38
export class AnimationLoop {
1✔
39
  static defaultAnimationLoopProps = {
1✔
40
    device: null!,
1✔
41

1✔
42
    onAddHTML: () => '',
1✔
43
    onInitialize: async () => null,
1✔
44
    onRender: () => {},
1✔
45
    onFinalize: () => {},
1✔
46
    onError: error => console.error(error), // eslint-disable-line no-console
1✔
47

1✔
48
    stats: luma.stats.get(`animation-loop-${statIdCounter++}`),
1✔
49

1✔
50
    // view parameters
1✔
51
    autoResizeViewport: false
1✔
52
  } as const satisfies Readonly<Required<AnimationLoopProps>>;
1✔
53

1✔
54
  device: Device | null = null;
1✔
55
  canvas: HTMLCanvasElement | OffscreenCanvas | null = null;
8✔
56

8✔
57
  props: Required<AnimationLoopProps>;
8✔
58
  animationProps: AnimationProps | null = null;
8✔
59
  timeline: Timeline | null = null;
8✔
60
  stats: Stats;
8✔
61
  cpuTime: Stat;
8✔
62
  gpuTime: Stat;
8✔
63
  frameRate: Stat;
8✔
64

8✔
65
  display: any;
8✔
66

8✔
67
  private _needsRedraw: string | false = 'initialized';
8✔
68

8✔
69
  _initialized: boolean = false;
8✔
70
  _running: boolean = false;
8✔
71
  _animationFrameId: any = null;
8✔
72
  _nextFramePromise: Promise<AnimationLoop> | null = null;
8✔
73
  _resolveNextFrame: ((animationLoop: AnimationLoop) => void) | null = null;
8✔
74
  _cpuStartTime: number = 0;
8✔
75
  _error: Error | null = null;
8✔
76
  _lastFrameTime: number = 0;
8✔
77

8✔
78
  /** GPU time query for measuring GPU execution time */
8✔
79
  _gpuTimeQuery: QuerySet | null = null;
8✔
80

1✔
81
  /*
1✔
82
   * @param {HTMLCanvasElement} canvas - if provided, width and height will be passed to context
1✔
83
   */
1✔
84
  constructor(props: AnimationLoopProps) {
1✔
85
    this.props = {...AnimationLoop.defaultAnimationLoopProps, ...props};
8✔
86
    props = this.props;
8✔
87

8✔
88
    if (!props.device) {
8!
89
      throw new Error('No device provided');
×
90
    }
×
91

8✔
92
    // state
8✔
93
    this.stats = props.stats || new Stats({id: 'animation-loop-stats'});
8!
94
    this.frameRate = this.stats.get('Frame Rate');
8✔
95
    this.frameRate.setSampleSize(1);
8✔
96
    this.cpuTime = this.stats.get('CPU Time');
8✔
97
    this.gpuTime = this.stats.get('GPU Time');
8✔
98

8✔
99
    this.setProps({autoResizeViewport: props.autoResizeViewport});
8✔
100

8✔
101
    // Bind methods
8✔
102
    this.start = this.start.bind(this);
8✔
103
    this.stop = this.stop.bind(this);
8✔
104

8✔
105
    this._onMousemove = this._onMousemove.bind(this);
8✔
106
    this._onMouseleave = this._onMouseleave.bind(this);
8✔
107
  }
8✔
108

1✔
109
  destroy(): void {
1✔
110
    this.stop();
2✔
111
    this._setDisplay(null);
2✔
112
    if (this._gpuTimeQuery) {
2!
NEW
113
      this._gpuTimeQuery.destroy();
×
NEW
114
      this._gpuTimeQuery = null;
×
NEW
115
    }
×
116
  }
2✔
117

1✔
118
  /** @deprecated Use .destroy() */
1✔
119
  delete(): void {
1✔
120
    this.destroy();
×
121
  }
×
122

1✔
123
  reportError(error: Error): void {
1✔
124
    this.props.onError(error);
×
125
    this._error = error;
×
126
  }
×
127

1✔
128
  /** Flags this animation loop as needing redraw */
1✔
129
  setNeedsRedraw(reason: string): this {
1✔
130
    this._needsRedraw = this._needsRedraw || reason;
7✔
131
    return this;
7✔
132
  }
7✔
133

1✔
134
  /** Query redraw status. Clears the flag. */
1✔
135
  needsRedraw(): false | string {
1✔
136
    const reason = this._needsRedraw;
×
137
    this._needsRedraw = false;
×
138
    return reason;
×
139
  }
×
140

1✔
141
  setProps(props: MutableAnimationLoopProps): this {
1✔
142
    if ('autoResizeViewport' in props) {
8✔
143
      this.props.autoResizeViewport = props.autoResizeViewport || false;
8✔
144
    }
8✔
145
    return this;
8✔
146
  }
8✔
147

1✔
148
  /** Starts a render loop if not already running */
1✔
149
  async start() {
1✔
150
    if (this._running) {
10✔
151
      return this;
2✔
152
    }
2✔
153
    this._running = true;
8✔
154

8✔
155
    try {
8✔
156
      let appContext;
8✔
157
      if (!this._initialized) {
10✔
158
        this._initialized = true;
7✔
159
        // Create the WebGL context
7✔
160
        await this._initDevice();
7✔
161
        this._initialize();
7✔
162

7✔
163
        // Note: onIntialize can return a promise (e.g. in case app needs to load resources)
7✔
164
        await this.props.onInitialize(this._getAnimationProps());
7✔
165
      }
7✔
166

8✔
167
      // check that we haven't been stopped
8✔
168
      if (!this._running) {
10✔
169
        return null;
1✔
170
      }
1✔
171

7✔
172
      // Start the loop
7✔
173
      if (appContext !== false) {
7✔
174
        // cancel any pending renders to ensure only one loop can ever run
7✔
175
        this._cancelAnimationFrame();
7✔
176
        this._requestAnimationFrame();
7✔
177
      }
7✔
178

7✔
179
      return this;
7✔
180
    } catch (err: unknown) {
10!
181
      const error = err instanceof Error ? err : new Error('Unknown error');
×
182
      this.props.onError(error);
×
183
      // this._running = false; // TODO
×
184
      throw error;
×
185
    }
×
186
  }
10✔
187

1✔
188
  /** Stops a render loop if already running, finalizing */
1✔
189
  stop() {
1✔
190
    // console.debug(`Stopping ${this.constructor.name}`);
10✔
191
    if (this._running) {
10✔
192
      // call callback
8✔
193
      // If stop is called immediately, we can end up in a state where props haven't been initialized...
8✔
194
      if (this.animationProps && !this._error) {
8✔
195
        this.props.onFinalize(this.animationProps);
8✔
196
      }
8✔
197

8✔
198
      this._cancelAnimationFrame();
8✔
199
      this._nextFramePromise = null;
8✔
200
      this._resolveNextFrame = null;
8✔
201
      this._running = false;
8✔
202
      this._lastFrameTime = 0;
8✔
203
    }
8✔
204
    return this;
10✔
205
  }
10✔
206

1✔
207
  /** Explicitly draw a frame */
1✔
208
  redraw(time?: number): this {
1✔
209
    if (this.device?.isLost || this._error) {
16!
210
      return this;
×
211
    }
×
212

16✔
213
    this._beginFrameTimers(time);
16✔
214

16✔
215
    this._setupFrame();
16✔
216
    this._updateAnimationProps();
16✔
217

16✔
218
    this._renderFrame(this._getAnimationProps());
16✔
219

16✔
220
    // clear needsRedraw flag
16✔
221
    this._clearNeedsRedraw();
16✔
222

16✔
223
    if (this._resolveNextFrame) {
16✔
224
      this._resolveNextFrame(this);
7✔
225
      this._nextFramePromise = null;
7✔
226
      this._resolveNextFrame = null;
7✔
227
    }
7✔
228

16✔
229
    this._endFrameTimers();
16✔
230

16✔
231
    return this;
16✔
232
  }
16✔
233

1✔
234
  /** Add a timeline, it will be automatically updated by the animation loop. */
1✔
235
  attachTimeline(timeline: Timeline): Timeline {
1✔
236
    this.timeline = timeline;
×
237
    return this.timeline;
×
238
  }
×
239

1✔
240
  /** Remove a timeline */
1✔
241
  detachTimeline(): void {
1✔
242
    this.timeline = null;
×
243
  }
×
244

1✔
245
  /** Wait until a render completes */
1✔
246
  waitForRender(): Promise<AnimationLoop> {
1✔
247
    this.setNeedsRedraw('waitForRender');
7✔
248

7✔
249
    if (!this._nextFramePromise) {
7✔
250
      this._nextFramePromise = new Promise(resolve => {
7✔
251
        this._resolveNextFrame = resolve;
7✔
252
      });
7✔
253
    }
7✔
254
    return this._nextFramePromise;
7✔
255
  }
7✔
256

1✔
257
  /** TODO - should use device.deviceContext */
1✔
258
  async toDataURL(): Promise<string> {
1✔
259
    this.setNeedsRedraw('toDataURL');
×
260
    await this.waitForRender();
×
261
    if (this.canvas instanceof HTMLCanvasElement) {
×
262
      return this.canvas.toDataURL();
×
263
    }
×
264
    throw new Error('OffscreenCanvas');
×
265
  }
×
266

1✔
267
  // PRIVATE METHODS
1✔
268

1✔
269
  _initialize(): void {
1✔
270
    this._startEventHandling();
7✔
271

7✔
272
    // Initialize the callback data
7✔
273
    this._initializeAnimationProps();
7✔
274
    this._updateAnimationProps();
7✔
275

7✔
276
    // Default viewport setup, in case onInitialize wants to render
7✔
277
    this._resizeViewport();
7✔
278

7✔
279
    // Initialize GPU time query if supported by the active adapter.
7✔
280
    if (this._hasTimestampQuerySupport()) {
7!
NEW
281
      const device = this.device;
×
NEW
282
      try {
×
NEW
283
        if (!device) {
×
NEW
284
          return;
×
NEW
285
        }
×
NEW
286

×
NEW
287
        this._gpuTimeQuery = device.createQuerySet({type: 'timestamp', count: 256});
×
NEW
288
        device.commandEncoder = device.createCommandEncoder({
×
NEW
289
          id: device.commandEncoder.props.id,
×
NEW
290
          timeProfilingQuerySet: this._gpuTimeQuery
×
NEW
291
        });
×
NEW
292
      } catch {
×
NEW
293
        // GPU timing not available - ignore
×
NEW
294
        this._gpuTimeQuery = null;
×
NEW
295
      }
×
NEW
296
    }
×
297
  }
7✔
298

1✔
299
  _setDisplay(display: any): void {
1✔
300
    if (this.display) {
2!
301
      this.display.destroy();
×
302
      this.display.animationLoop = null;
×
303
    }
×
304

2✔
305
    // store animation loop on the display
2✔
306
    if (display) {
2!
307
      display.animationLoop = this;
×
308
    }
×
309

2✔
310
    this.display = display;
2✔
311
  }
2✔
312

1✔
313
  _requestAnimationFrame(): void {
1✔
314
    if (!this._running) {
22✔
315
      return;
1✔
316
    }
1✔
317

21✔
318
    // VR display has a separate animation frame to sync with headset
21✔
319
    // TODO WebVR API discontinued, replaced by WebXR: https://immersive-web.github.io/webxr/
21✔
320
    // See https://developer.mozilla.org/en-US/docs/Web/API/VRDisplay/requestAnimationFrame
21✔
321
    // if (this.display && this.display.requestAnimationFrame) {
21✔
322
    //   this._animationFrameId = this.display.requestAnimationFrame(this._animationFrame.bind(this));
21✔
323
    // }
21✔
324
    this._animationFrameId = requestAnimationFramePolyfill(this._animationFrame.bind(this));
21✔
325
  }
22✔
326

1✔
327
  _cancelAnimationFrame(): void {
1✔
328
    if (this._animationFrameId === null) {
15✔
329
      return;
8✔
330
    }
8✔
331

7✔
332
    // VR display has a separate animation frame to sync with headset
7✔
333
    // TODO WebVR API discontinued, replaced by WebXR: https://immersive-web.github.io/webxr/
7✔
334
    // See https://developer.mozilla.org/en-US/docs/Web/API/VRDisplay/requestAnimationFrame
7✔
335
    // if (this.display && this.display.cancelAnimationFramePolyfill) {
7✔
336
    //   this.display.cancelAnimationFrame(this._animationFrameId);
7✔
337
    // }
7✔
338
    cancelAnimationFramePolyfill(this._animationFrameId);
7✔
339
    this._animationFrameId = null;
7✔
340
  }
15✔
341

1✔
342
  _animationFrame(time: number): void {
1✔
343
    if (!this._running) {
15!
344
      return;
×
345
    }
×
346
    this.redraw(time);
15✔
347
    this._requestAnimationFrame();
15✔
348
  }
15✔
349

1✔
350
  // Called on each frame, can be overridden to call onRender multiple times
1✔
351
  // to support e.g. stereoscopic rendering
1✔
352
  _renderFrame(animationProps: AnimationProps): void {
1✔
353
    // Allow e.g. VR display to render multiple frames.
16✔
354
    if (this.display) {
16!
355
      this.display._renderFrame(animationProps);
×
356
      return;
×
357
    }
×
358

16✔
359
    // call callback
16✔
360
    this.props.onRender(this._getAnimationProps());
16✔
361
    // end callback
16✔
362

16✔
363
    // Submit commands (necessary on WebGPU)
16✔
364
    this.device?.submit();
16✔
365
  }
16✔
366

1✔
367
  _clearNeedsRedraw(): void {
1✔
368
    this._needsRedraw = false;
16✔
369
  }
16✔
370

1✔
371
  _setupFrame(): void {
1✔
372
    this._resizeViewport();
16✔
373
  }
16✔
374

1✔
375
  // Initialize the  object that will be passed to app callbacks
1✔
376
  _initializeAnimationProps(): void {
1✔
377
    const canvasContext = this.device?.getDefaultCanvasContext();
7✔
378
    if (!this.device || !canvasContext) {
7!
379
      throw new Error('loop');
×
380
    }
×
381

7✔
382
    const canvas = canvasContext?.canvas;
7✔
383
    const useDevicePixels = canvasContext.props.useDevicePixels;
7✔
384

7✔
385
    this.animationProps = {
7✔
386
      animationLoop: this,
7✔
387

7✔
388
      device: this.device,
7✔
389
      canvasContext,
7✔
390
      canvas,
7✔
391
      // @ts-expect-error Deprecated
7✔
392
      useDevicePixels,
7✔
393

7✔
394
      timeline: this.timeline,
7✔
395

7✔
396
      needsRedraw: false,
7✔
397

7✔
398
      // Placeholders
7✔
399
      width: 1,
7✔
400
      height: 1,
7✔
401
      aspect: 1,
7✔
402

7✔
403
      // Animation props
7✔
404
      time: 0,
7✔
405
      startTime: Date.now(),
7✔
406
      engineTime: 0,
7✔
407
      tick: 0,
7✔
408
      tock: 0,
7✔
409

7✔
410
      // Experimental
7✔
411
      _mousePosition: null // Event props
7✔
412
    };
7✔
413
  }
7✔
414

1✔
415
  _getAnimationProps(): AnimationProps {
1✔
416
    if (!this.animationProps) {
39!
417
      throw new Error('animationProps');
×
418
    }
×
419
    return this.animationProps;
39✔
420
  }
39✔
421

1✔
422
  // Update the context object that will be passed to app callbacks
1✔
423
  _updateAnimationProps(): void {
1✔
424
    if (!this.animationProps) {
23!
425
      return;
×
426
    }
×
427

23✔
428
    // Can this be replaced with canvas context?
23✔
429
    const {width, height, aspect} = this._getSizeAndAspect();
23✔
430
    if (width !== this.animationProps.width || height !== this.animationProps.height) {
23!
431
      this.setNeedsRedraw('drawing buffer resized');
×
432
    }
×
433
    if (aspect !== this.animationProps.aspect) {
23!
434
      this.setNeedsRedraw('drawing buffer aspect changed');
×
435
    }
×
436

23✔
437
    this.animationProps.width = width;
23✔
438
    this.animationProps.height = height;
23✔
439
    this.animationProps.aspect = aspect;
23✔
440

23✔
441
    this.animationProps.needsRedraw = this._needsRedraw;
23✔
442

23✔
443
    // Update time properties
23✔
444
    this.animationProps.engineTime = Date.now() - this.animationProps.startTime;
23✔
445

23✔
446
    if (this.timeline) {
23!
447
      this.timeline.update(this.animationProps.engineTime);
×
448
    }
×
449

23✔
450
    this.animationProps.tick = Math.floor((this.animationProps.time / 1000) * 60);
23✔
451
    this.animationProps.tock++;
23✔
452

23✔
453
    // For back compatibility
23✔
454
    this.animationProps.time = this.timeline
23!
455
      ? this.timeline.getTime()
×
456
      : this.animationProps.engineTime;
23✔
457
  }
23✔
458

1✔
459
  /** Wait for supplied device */
1✔
460
  async _initDevice() {
1✔
461
    this.device = await this.props.device;
7✔
462
    if (!this.device) {
7!
463
      throw new Error('No device provided');
×
464
    }
×
465
    this.canvas = this.device.getDefaultCanvasContext().canvas || null;
7!
466
    // this._createInfoDiv();
7✔
467
  }
7✔
468

1✔
469
  _createInfoDiv(): void {
1✔
470
    if (this.canvas && this.props.onAddHTML) {
×
471
      const wrapperDiv = document.createElement('div');
×
472
      document.body.appendChild(wrapperDiv);
×
473
      wrapperDiv.style.position = 'relative';
×
474
      const div = document.createElement('div');
×
475
      div.style.position = 'absolute';
×
476
      div.style.left = '10px';
×
477
      div.style.bottom = '10px';
×
478
      div.style.width = '300px';
×
479
      div.style.background = 'white';
×
480
      if (this.canvas instanceof HTMLCanvasElement) {
×
481
        wrapperDiv.appendChild(this.canvas);
×
482
      }
×
483
      wrapperDiv.appendChild(div);
×
484
      const html = this.props.onAddHTML(div);
×
485
      if (html) {
×
486
        div.innerHTML = html;
×
487
      }
×
488
    }
×
489
  }
×
490

1✔
491
  _getSizeAndAspect(): {width: number; height: number; aspect: number} {
1✔
492
    if (!this.device) {
23!
493
      return {width: 1, height: 1, aspect: 1};
×
494
    }
×
495
    // https://webglfundamentals.org/webgl/lessons/webgl-resizing-the-canvas.html
23✔
496
    const [width, height] = this.device?.getDefaultCanvasContext().getDevicePixelSize() || [1, 1];
23!
497

23✔
498
    // https://webglfundamentals.org/webgl/lessons/webgl-anti-patterns.html
23✔
499
    let aspect = 1;
23✔
500
    const canvas = this.device?.getDefaultCanvasContext().canvas;
23✔
501

23✔
502
    // @ts-expect-error
23✔
503
    if (canvas && canvas.clientHeight) {
23✔
504
      // @ts-expect-error
23✔
505
      aspect = canvas.clientWidth / canvas.clientHeight;
23✔
506
    } else if (width > 0 && height > 0) {
23!
507
      aspect = width / height;
×
508
    }
×
509

23✔
510
    return {width, height, aspect};
23✔
511
  }
23✔
512

1✔
513
  /** @deprecated Default viewport setup */
1✔
514
  _resizeViewport(): void {
1✔
515
    // TODO can we use canvas context to code this in a portable way?
23✔
516
    // @ts-expect-error Expose on canvasContext
23✔
517
    if (this.props.autoResizeViewport && this.device.gl) {
23!
518
      // @ts-expect-error Expose canvasContext
×
519
      this.device.gl.viewport(
×
520
        0,
×
521
        0,
×
522
        // @ts-expect-error Expose canvasContext
×
523
        this.device.gl.drawingBufferWidth,
×
524
        // @ts-expect-error Expose canvasContext
×
525
        this.device.gl.drawingBufferHeight
×
526
      );
×
527
    }
×
528
  }
23✔
529

1✔
530
  _beginFrameTimers(time?: number) {
1✔
531
    const now = time ?? (typeof performance !== 'undefined' ? performance.now() : Date.now());
16!
532
    if (this._lastFrameTime) {
16✔
533
      const frameTime = now - this._lastFrameTime;
8✔
534
      if (frameTime > 0) {
8✔
535
        this.frameRate.addTime(frameTime);
8✔
536
      }
8✔
537
    }
8✔
538
    this._lastFrameTime = now;
16✔
539

16✔
540
    if (this.device && this._gpuTimeQuery) {
16!
NEW
541
      this._consumeEncodedGpuTime();
×
NEW
542
    }
×
543

16✔
544
    this.cpuTime.timeStart();
16✔
545
  }
16✔
546

1✔
547
  _endFrameTimers() {
1✔
548
    if (this.device && this._gpuTimeQuery) {
16!
NEW
549
      this._consumeEncodedGpuTime();
×
NEW
550
    }
×
551

16✔
552
    this.cpuTime.timeEnd();
16✔
553
  }
16✔
554

1✔
555
  _consumeEncodedGpuTime(): void {
1✔
NEW
556
    if (!this.device) {
×
NEW
557
      return;
×
NEW
558
    }
×
NEW
559

×
NEW
560
    const gpuTimeMs = this.device.commandEncoder._gpuTimeMs;
×
NEW
561
    if (gpuTimeMs !== undefined) {
×
NEW
562
      this.gpuTime.addTime(gpuTimeMs);
×
NEW
563
      this.device.commandEncoder._gpuTimeMs = undefined;
×
NEW
564
    }
×
NEW
565
  }
×
566

1✔
567
  _hasTimestampQuerySupport(): boolean {
1✔
568
    if (!this.device) {
7!
NEW
569
      return false;
×
NEW
570
    }
×
571

7✔
572
    return this.device.features.has('timestamp-query');
7✔
573
  }
7✔
574

1✔
575
  // Event handling
1✔
576

1✔
577
  _startEventHandling() {
1✔
578
    if (this.canvas) {
7✔
579
      this.canvas.addEventListener('mousemove', this._onMousemove.bind(this));
7✔
580
      this.canvas.addEventListener('mouseleave', this._onMouseleave.bind(this));
7✔
581
    }
7✔
582
  }
7✔
583

1✔
584
  _onMousemove(event: Event) {
1✔
585
    if (event instanceof MouseEvent) {
×
586
      this._getAnimationProps()._mousePosition = [event.offsetX, event.offsetY];
×
587
    }
×
588
  }
×
589

1✔
590
  _onMouseleave(event: Event) {
1✔
591
    this._getAnimationProps()._mousePosition = null;
×
592
  }
×
593
}
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc