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

visgl / luma.gl / 28177465729

25 Jun 2026 02:28PM UTC coverage: 70.408% (-0.3%) from 70.66%
28177465729

push

github

web-flow
website: Improve example browsing and InfoBox panels (#2692)

9602 of 15462 branches covered (62.1%)

Branch coverage included in aggregate %.

19228 of 25485 relevant lines covered (75.45%)

4245.37 hits per line

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

69.92
/modules/webgpu/src/adapter/resources/webgpu-query-set.ts
1
// luma.gl
2
// SPDX-License-Identifier: MIT
3
// Copyright (c) vis.gl contributors
4

5
import {Buffer, QuerySet, QuerySetProps} from '@luma.gl/core';
6
import {WebGPUDevice} from '../webgpu-device';
7
import {WebGPUBuffer} from './webgpu-buffer';
8
import {
9
  getCpuHotspotSubmitReason,
10
  setCpuHotspotSubmitReason
11
} from '../helpers/cpu-hotspot-profiler';
12

13
/**
14
 * Immutable
15
 */
16
export class WebGPUQuerySet extends QuerySet {
17
  readonly device: WebGPUDevice;
18
  readonly handle: GPUQuerySet;
19

20
  protected _resolveBuffer: WebGPUBuffer | null = null;
6✔
21
  protected _readBuffer: WebGPUBuffer | null = null;
6✔
22
  protected _cachedResults: bigint[] | null = null;
6✔
23
  protected _readResultsPromise: Promise<bigint[]> | null = null;
6✔
24
  protected _resultsPendingResolution: boolean = false;
6✔
25

26
  constructor(device: WebGPUDevice, props: QuerySetProps) {
27
    super(device, props);
6✔
28
    this.device = device;
6✔
29
    const suppliedHandle = this.props.handle as GPUQuerySet | undefined;
6✔
30
    this.handle =
6✔
31
      suppliedHandle ||
12✔
32
      this.device.handle.createQuerySet({
33
        type: this.props.type,
34
        count: this.props.count
35
      });
36
    this.handle.label = this.props.id;
6✔
37
  }
38

39
  override destroy(): void {
40
    if (!this.destroyed) {
6!
41
      this.handle?.destroy();
6✔
42
      this.destroyResource();
6✔
43
      // @ts-expect-error readonly
44
      this.handle = null;
6✔
45
    }
46
  }
47

48
  isResultAvailable(queryIndex?: number): boolean {
49
    if (!this._cachedResults) {
1!
50
      return false;
1✔
51
    }
52

53
    return queryIndex === undefined
×
54
      ? true
55
      : queryIndex >= 0 && queryIndex < this._cachedResults.length;
×
56
  }
57

58
  async readResults(options?: {firstQuery?: number; queryCount?: number}): Promise<bigint[]> {
59
    const firstQuery = options?.firstQuery || 0;
2✔
60
    const queryCount = options?.queryCount || this.props.count - firstQuery;
2!
61

62
    if (firstQuery < 0 || queryCount < 0 || firstQuery + queryCount > this.props.count) {
2!
63
      throw new Error('Query read range is out of bounds');
×
64
    }
65

66
    let needsFreshResults = true;
2✔
67
    while (needsFreshResults) {
2✔
68
      if (!this._readResultsPromise) {
2!
69
        this._readResultsPromise = this._readAllResults();
2✔
70
      }
71

72
      const readResultsPromise = this._readResultsPromise;
2✔
73
      const results = await readResultsPromise;
2✔
74

75
      // A later submit may have invalidated the query set while this read was in flight.
76
      // Retry so each caller observes the freshest resolved results instead of stale data.
77
      needsFreshResults = this._resultsPendingResolution;
2✔
78
      if (!needsFreshResults) {
2!
79
        return results.slice(firstQuery, firstQuery + queryCount);
2✔
80
      }
81
    }
82

83
    throw new Error('Query read unexpectedly failed to resolve');
×
84
  }
85

86
  async readTimestampDuration(beginIndex: number, endIndex: number): Promise<number> {
87
    if (this.props.type !== 'timestamp') {
2!
88
      throw new Error('Timestamp durations require a timestamp QuerySet');
×
89
    }
90
    if (beginIndex < 0 || endIndex <= beginIndex || endIndex >= this.props.count) {
2!
91
      throw new Error('Timestamp duration range is out of bounds');
×
92
    }
93

94
    const results = await this.readResults({
2✔
95
      firstQuery: beginIndex,
96
      queryCount: endIndex - beginIndex + 1
97
    });
98
    return Number(results[results.length - 1] - results[0]) / 1e6;
2✔
99
  }
100

101
  /** Marks any cached query results as stale after new writes have been encoded. */
102
  _invalidateResults(): void {
103
    this._cachedResults = null;
4✔
104
    this._resultsPendingResolution = true;
4✔
105
  }
106

107
  protected async _readAllResults(): Promise<bigint[]> {
108
    this._ensureBuffers();
2✔
109

110
    try {
2✔
111
      // Use a dedicated encoder so async profiling reads cannot flush or replace the
112
      // device's active frame encoder while application rendering is in flight.
113
      if (this._resultsPendingResolution) {
2!
114
        const commandEncoder = this.device.createCommandEncoder({
2✔
115
          id: `${this.id}-read-results`
116
        });
117
        commandEncoder.resolveQuerySet(this, this._resolveBuffer!);
2✔
118
        commandEncoder.copyBufferToBuffer({
2✔
119
          sourceBuffer: this._resolveBuffer!,
120
          destinationBuffer: this._readBuffer!,
121
          size: this._resolveBuffer!.byteLength
122
        });
123
        const commandBuffer = commandEncoder.finish();
2✔
124
        const previousSubmitReason = getCpuHotspotSubmitReason(this.device) || undefined;
2✔
125
        setCpuHotspotSubmitReason(this.device, 'query-readback');
2✔
126
        try {
2✔
127
          this.device.submit(commandBuffer);
2✔
128
        } finally {
129
          setCpuHotspotSubmitReason(this.device, previousSubmitReason);
2✔
130
        }
131
      }
132

133
      const data = await this._readBuffer!.readAsync(0, this._readBuffer!.byteLength);
2✔
134
      const resultView = new BigUint64Array(data.buffer, data.byteOffset, this.props.count);
2✔
135
      this._cachedResults = Array.from(resultView, value => value);
4✔
136
      this._resultsPendingResolution = false;
2✔
137
      return this._cachedResults;
2✔
138
    } finally {
139
      this._readResultsPromise = null;
2✔
140
    }
141
  }
142

143
  protected _ensureBuffers(): void {
144
    if (this._resolveBuffer && this._readBuffer) {
2!
145
      return;
×
146
    }
147

148
    const byteLength = this.props.count * 8;
2✔
149
    this._resolveBuffer = this.device.createBuffer({
2✔
150
      id: `${this.id}-resolve-buffer`,
151
      usage: Buffer.QUERY_RESOLVE | Buffer.COPY_SRC,
152
      byteLength
153
    });
154
    this.attachResource(this._resolveBuffer);
2✔
155

156
    this._readBuffer = this.device.createBuffer({
2✔
157
      id: `${this.id}-read-buffer`,
158
      usage: Buffer.COPY_DST | Buffer.MAP_READ,
159
      byteLength
160
    });
161
    this.attachResource(this._readBuffer);
2✔
162
  }
163

164
  _encodeResolveToReadBuffer(
165
    commandEncoder: {
166
      resolveQuerySet: (
167
        querySet: WebGPUQuerySet,
168
        destination: WebGPUBuffer,
169
        options?: {firstQuery?: number; queryCount?: number; destinationOffset?: number}
170
      ) => void;
171
      copyBufferToBuffer: (options: {
172
        sourceBuffer: WebGPUBuffer;
173
        destinationBuffer: WebGPUBuffer;
174
        sourceOffset?: number;
175
        destinationOffset?: number;
176
        size?: number;
177
      }) => void;
178
    },
179
    options?: {firstQuery?: number; queryCount?: number}
180
  ): boolean {
181
    if (!this._resultsPendingResolution) {
1!
182
      return false;
×
183
    }
184

185
    // If a readback is already mapping the shared read buffer, defer to the fallback read path.
186
    // That path will submit resolve/copy commands once the current read has completed.
187
    if (this._readResultsPromise) {
1!
188
      return false;
1✔
189
    }
190

191
    this._ensureBuffers();
×
192
    const firstQuery = options?.firstQuery || 0;
×
193
    const queryCount = options?.queryCount || this.props.count - firstQuery;
×
194
    const byteLength = queryCount * BigUint64Array.BYTES_PER_ELEMENT;
×
195
    const byteOffset = firstQuery * BigUint64Array.BYTES_PER_ELEMENT;
×
196

197
    commandEncoder.resolveQuerySet(this, this._resolveBuffer!, {
×
198
      firstQuery,
199
      queryCount,
200
      destinationOffset: byteOffset
201
    });
202
    commandEncoder.copyBufferToBuffer({
×
203
      sourceBuffer: this._resolveBuffer!,
204
      sourceOffset: byteOffset,
205
      destinationBuffer: this._readBuffer!,
206
      destinationOffset: byteOffset,
207
      size: byteLength
208
    });
209
    this._resultsPendingResolution = false;
×
210
    return true;
×
211
  }
212
}
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