From 07a5ddbcc73c42cdcb97abd08c50d41e782b95f6 Mon Sep 17 00:00:00 2001 From: Ib Green Date: Wed, 28 Feb 2024 06:44:24 -0500 Subject: [PATCH 1/7] feat: Add QuerySet resource --- modules/constants/src/webgl-constants.ts | 6 +- modules/core/src/adapter/device.ts | 3 + .../src/adapter/resources/command-encoder.ts | 37 +++- .../src/adapter/resources/compute-pass.ts | 18 +- .../core/src/adapter/resources/query-set.ts | 41 +++++ .../core/src/adapter/resources/render-pass.ts | 67 +++---- modules/core/src/index.ts | 3 + .../src/adapter/resources/webgl-query-set.ts | 171 ++++++++++++++++++ .../adapter/resources/webgl-render-pass.ts | 11 ++ modules/webgl/src/adapter/webgl-device.ts | 8 +- .../resources/webgpu-command-encoder.ts | 21 +++ .../adapter/resources/webgpu-compute-pass.ts | 17 +- .../src/adapter/resources/webgpu-query-set.ts | 33 ++++ .../adapter/resources/webgpu-render-pass.ts | 15 ++ modules/webgpu/src/adapter/webgpu-device.ts | 9 +- 15 files changed, 410 insertions(+), 50 deletions(-) create mode 100644 modules/core/src/adapter/resources/query-set.ts create mode 100644 modules/webgl/src/adapter/resources/webgl-query-set.ts create mode 100644 modules/webgpu/src/adapter/resources/webgpu-query-set.ts diff --git a/modules/constants/src/webgl-constants.ts b/modules/constants/src/webgl-constants.ts index 9225205693..78a6947baf 100644 --- a/modules/constants/src/webgl-constants.ts +++ b/modules/constants/src/webgl-constants.ts @@ -620,7 +620,7 @@ enum GLEnum { INT_2_10_10_10_REV = 0x8d9f, // Queries - + CURRENT_QUERY = 0x8865, /** Returns a GLuint containing the query result. */ QUERY_RESULT = 0x8866, @@ -969,8 +969,8 @@ enum GLEnum { /** The current time. */ TIMESTAMP_EXT = 0x8e28, /** A Boolean indicating whether or not the GPU performed any disjoint operation (lost context) */ - GPU_DISJOINT_EXT = 0x8fbb, - + GPU_DISJOINT_EXT = 0x8fbb, + // KHR_parallel_shader_compile https://registry.khronos.org/webgl/extensions/KHR_parallel_shader_compile /** a non-blocking poll operation, so that compile/link status availability can be queried without potentially incurring stalls */ diff --git a/modules/core/src/adapter/device.ts b/modules/core/src/adapter/device.ts index 86680d81d1..491619bcf2 100644 --- a/modules/core/src/adapter/device.ts +++ b/modules/core/src/adapter/device.ts @@ -22,6 +22,7 @@ import type {ComputePass, ComputePassProps} from './resources/compute-pass'; import type {CommandEncoder, CommandEncoderProps} from './resources/command-encoder'; import type {VertexArray, VertexArrayProps} from './resources/vertex-array'; import type {TransformFeedback, TransformFeedbackProps} from './resources/transform-feedback'; +import type {QuerySet, QuerySetProps} from './resources/query-set'; import {isTextureFormatCompressed} from './type-utils/decode-texture-format'; @@ -395,6 +396,8 @@ export abstract class Device { /** Create a transform feedback (immutable set of output buffer bindings). WebGL only. */ abstract createTransformFeedback(props: TransformFeedbackProps): TransformFeedback; + abstract createQuerySet(props: QuerySetProps): QuerySet; + createCommandEncoder(props: CommandEncoderProps = {}): CommandEncoder { throw new Error('not implemented'); } diff --git a/modules/core/src/adapter/resources/command-encoder.ts b/modules/core/src/adapter/resources/command-encoder.ts index 9b221f29c5..b70d1e374c 100644 --- a/modules/core/src/adapter/resources/command-encoder.ts +++ b/modules/core/src/adapter/resources/command-encoder.ts @@ -6,6 +6,7 @@ import {Device} from '../device'; import {Resource, ResourceProps} from './resource'; import {Buffer} from './buffer'; import {Texture} from './texture'; +import {QuerySet} from './query-set'; export type WriteBufferOptions = { buffer: Buffer; @@ -143,23 +144,39 @@ export abstract class CommandEncoder extends Resource { super(device, props, CommandEncoder.defaultProps); } + /** Completes recording of the commands sequence */ abstract finish(): void; // TODO - return the CommandBuffer? - // beginRenderPass(GPURenderPassDescriptor descriptor): GPURenderPassEncoder; - // beginComputePass(optional GPUComputePassDescriptor descriptor = {}): GPUComputePassEncoder; - // finish(options?: {id?: string}): GPUCommandBuffer; - + /** Add a command that that copies data from a sub-region of a Buffer to a sub-region of another Buffer. */ abstract copyBufferToBuffer(options: CopyBufferToBufferOptions): void; + /** Add a command that copies data from a sub-region of a GPUBuffer to a sub-region of one or multiple continuous texture subresources. */ abstract copyBufferToTexture(options: CopyBufferToTextureOptions): void; + /** Add a command that copies data from a sub-region of one or multiple continuous texture subresources to a sub-region of a Buffer. */ abstract copyTextureToBuffer(options: CopyTextureToBufferOptions): void; + /** Add a command that copies data from a sub-region of one or multiple contiguous texture subresources to another sub-region of one or multiple continuous texture subresources. */ abstract copyTextureToTexture(options: CopyTextureToTextureOptions): void; - pushDebugGroup(groupLabel: string): void {} - - popDebugGroup() {} - - insertDebugMarker(markerLabel: string): void {} -} + /** Reads results from a query set into a GPU buffer. Values are 64 bits so byteLength must be querySet.props.count * 8 */ + abstract resolveQuerySet( + querySet: QuerySet, + destination: Buffer, + options?: { + firstQuery: number; + queryCount: number; + } + ): void; + + /** Begins a labeled debug group containing subsequent commands */ + abstract pushDebugGroup(groupLabel: string): void; + /** Ends the labeled debug group most recently started by pushDebugGroup() */ + abstract popDebugGroup(): void; + /** Marks a point in a stream of commands with a label */ + abstract insertDebugMarker(markerLabel: string): void; + + // TODO - luma.gl has these on the device, should we align with WebGPU API? + // beginRenderPass(GPURenderPassDescriptor descriptor): GPURenderPassEncoder; + // beginComputePass(optional GPUComputePassDescriptor descriptor = {}): GPUComputePassEncoder; + } diff --git a/modules/core/src/adapter/resources/compute-pass.ts b/modules/core/src/adapter/resources/compute-pass.ts index ecfc028062..0b4e399f80 100644 --- a/modules/core/src/adapter/resources/compute-pass.ts +++ b/modules/core/src/adapter/resources/compute-pass.ts @@ -6,12 +6,23 @@ import {Resource, ResourceProps} from './resource'; import {ComputePipeline} from './compute-pipeline'; import type {Device} from '../device'; import {Buffer} from './buffer'; +import {QuerySet} from './query-set'; -export type ComputePassProps = ResourceProps & {}; +export type ComputePassProps = ResourceProps & { + /** QuerySet to write beging/end timestamps to */ + timestampQuerySet?: QuerySet; + /** QuerySet index to write begin timestamp to. No timestamp is written if not provided. */ + beginTimestampIndex?: number; + /** QuerySet index to write end timestamp to. No timestamp is written if not provided. */ + endTimestampIndex?: number; +}; export abstract class ComputePass extends Resource { static override defaultProps: Required = { - ...Resource.defaultProps + ...Resource.defaultProps, + timestampQuerySet: undefined, + beginTimestampIndex: undefined, + endTimestampIndex: undefined }; override get [Symbol.toStringTag](): string { @@ -46,7 +57,10 @@ export abstract class ComputePass extends Resource { */ abstract dispatchIndirect(indirectBuffer: Buffer, indirectOffset?: number): void; + /** Begins a labeled debug group containing subsequent commands */ abstract pushDebugGroup(groupLabel: string): void; + /** Ends the labeled debug group most recently started by pushDebugGroup() */ abstract popDebugGroup(): void; + /** Marks a point in a stream of commands with a label */ abstract insertDebugMarker(markerLabel: string): void; } diff --git a/modules/core/src/adapter/resources/query-set.ts b/modules/core/src/adapter/resources/query-set.ts new file mode 100644 index 0000000000..d349425d66 --- /dev/null +++ b/modules/core/src/adapter/resources/query-set.ts @@ -0,0 +1,41 @@ +// luma.gl +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +import type {Device} from '../device'; +import {Resource, ResourceProps} from './resource'; + +/** + * Properties for creating a QuerySet + * - 'timestamp' - query the GPU timestamp counter at the start and end of render passes + * timestamp queries are available if the 'timestamp-query' feature is present. + * - 'occlusion' - query the number of fragment samples that pass all per-fragment tests for a set of drawing commands + * including scissor, sample mask, alpha to coverage, stencil, and depth tests + */ +export type QuerySetProps = ResourceProps & { + /** + * The type of query set + * occlusion - query the number of fragment samples that pass all the per-fragment tests for a set of drawing commands, including scissor, sample mask, alpha to coverage, stencil, and depth tests + * timestamp - query the GPU timestamp counter at the start and end of render passes + */ + type: 'occlusion' | 'timestamp'; + /** The number of queries managed by the query set */ + count: number; +}; + +/** Immutable QuerySet object */ +export abstract class QuerySet extends Resource { + static override defaultProps: Required = { + ...Resource.defaultProps, + type: undefined, + count: undefined + }; + + get [Symbol.toStringTag](): string { + return 'QuerySet'; + } + + constructor(device: Device, props: QuerySetProps) { + super(device, props, QuerySet.defaultProps); + } +} diff --git a/modules/core/src/adapter/resources/render-pass.ts b/modules/core/src/adapter/resources/render-pass.ts index b6d3874843..ef3384738d 100644 --- a/modules/core/src/adapter/resources/render-pass.ts +++ b/modules/core/src/adapter/resources/render-pass.ts @@ -8,6 +8,7 @@ import type {RenderPassParameters} from '../types/parameters'; import {Resource, ResourceProps} from './resource'; import {Framebuffer} from './framebuffer'; import {NumberArray} from '../..'; +import {QuerySet} from './query-set'; /** * Properties for a RenderPass instance is a required parameter to all draw calls. @@ -29,6 +30,15 @@ export type RenderPassProps = ResourceProps & { stencilReadOnly?: boolean; /** Whether to disable / discard the output of the rasterizer */ discard?: boolean; + + /** QuerySet to write begin/end timestamps to */ + occlusionQuerySet?: QuerySet; + /** QuerySet to write begin/end timestamps to */ + timestampQuerySet?: QuerySet; + /** QuerySet index to write begin timestamp to. No timestamp is written if not provided. */ + beginTimestampIndex?: number; + /** QuerySet index to write end timestamp to. No timestamp is written if not provided. */ + endTimestampIndex?: number; }; /** @@ -50,7 +60,12 @@ export abstract class RenderPass extends Resource { clearStencil: 0, depthReadOnly: false, stencilReadOnly: false, - discard: false + discard: false, + + occlusionQuerySet: undefined, + timestampQuerySet: undefined, + beginTimestampIndex: undefined, + endTimestampIndex: undefined }; override get [Symbol.toStringTag](): string { @@ -64,45 +79,33 @@ export abstract class RenderPass extends Resource { /** Call when rendering is done in this pass. */ abstract end(): void; - /** - * A small set of parameters can be changed between every draw call - * (viewport, scissorRect, blendColor, stencilReference) - */ + /** A few parameters can be changed at any time (viewport, scissorRect, blendColor, stencilReference) */ abstract setParameters(parameters: RenderPassParameters): void; + // executeBundles(bundles: Iterable): void; + + /** Being an occlusion query. Value will be stored in the occlusionQuerySet at the index. Occlusion queries cannot be nested. */ + abstract beginOcclusionQuery(queryIndex: number): void; + /** End an occlusion query. Stores result in the index specified in beginOcclusionQuery. */ + abstract endOcclusionQuery(): void; + + /** Begins a labeled debug group containing subsequent commands */ abstract pushDebugGroup(groupLabel: string): void; + /** Ends the labeled debug group most recently started by pushDebugGroup() */ abstract popDebugGroup(): void; + /** Marks a point in a stream of commands with a label */ abstract insertDebugMarker(markerLabel: string): void; - // executeBundles(bundles: Iterable): void; - - // TODO - In WebGPU the following methods are on the renderpass. - // luma.gl keeps them on the pipeline for now - // setPipeline(pipeline: RenderPipeline): void {} - - // setIndexBuffer( - // buffer: Buffer, - // indexFormat: 'uint16' | 'uint32', - // offset?: number, - // size?: number - // ): void {} + // In WebGPU the following methods are on the renderpass instead of the renderpipeline + // luma.gl keeps them on the pipeline for now. + // TODO - Can we align WebGL implementation with WebGPU API? + // abstract setPipeline(pipeline: RenderPipeline): void {} + // abstract setIndexBuffer() // abstract setVertexBuffer(slot: number, buffer: Buffer, offset: number): void; - // abstract setBindings(bindings: Record): void; - // abstract setParameters(parameters: RenderPassParameters); - - // draw(options: { - // vertexCount?: number; // Either vertexCount or indexCount must be provided - // indexCount?: number; // Activates indexed drawing (call setIndexBuffer()) - // instanceCount?: number; // - // firstVertex?: number; - // firstIndex?: number; // requires device.features.has('indirect-first-instance')? - // firstInstance?: number; - // baseVertex?: number; - // }): void {} - - // drawIndirect(indirectBuffer: GPUBuffer, indirectOffset: number): void; - // drawIndexedIndirect(indirectBuffer: GPUBuffer, indirectOffset: number): void; + // abstract draw(options: { + // abstract drawIndirect(indirectBuffer: GPUBuffer, indirectOffset: number): void; + // abstract drawIndexedIndirect(indirectBuffer: GPUBuffer, indirectOffset: number): void; } diff --git a/modules/core/src/index.ts b/modules/core/src/index.ts index cb1d5f8542..fefd6ae07c 100644 --- a/modules/core/src/index.ts +++ b/modules/core/src/index.ts @@ -69,6 +69,9 @@ export {VertexArray} from './adapter/resources/vertex-array'; export type {TransformFeedbackProps, BufferRange} from './adapter/resources/transform-feedback'; export {TransformFeedback} from './adapter/resources/transform-feedback'; +export type {QuerySetProps} from './adapter/resources/query-set'; +export {QuerySet} from './adapter/resources/query-set'; + // API TYPES export type {AccessorObject} from './adapter/types/accessor'; export type { diff --git a/modules/webgl/src/adapter/resources/webgl-query-set.ts b/modules/webgl/src/adapter/resources/webgl-query-set.ts new file mode 100644 index 0000000000..1a4aebbecc --- /dev/null +++ b/modules/webgl/src/adapter/resources/webgl-query-set.ts @@ -0,0 +1,171 @@ +// WebGL2 Query (also handles disjoint timer extensions) +import {QuerySet, QuerySetProps} from '@luma.gl/core'; +import {GL} from '@luma.gl/constants'; +import {WebGLDevice} from '../webgl-device'; + +/** + * Asynchronous queries for different kinds of information + */ +export class WEBGLQuerySet extends QuerySet { + device: WebGLDevice; + handle: WebGLQuery; + + target: number | null = null; + _queryPending = false; + _pollingPromise: Promise | null = null; + + override get [Symbol.toStringTag](): string { + return 'Query'; + } + + // Create a query class + constructor(device: WebGLDevice, props: QuerySetProps) { + super(device, props); + this.device = device; + + if (props.count > 1) { + throw new Error('WebGL QuerySet can only have one value'); + } + + this.handle = this.device.gl.createQuery(); + Object.seal(this); + } + + override destroy() { + this.device.gl.deleteQuery(this.handle); + } + + // FOR RENDER PASS AND COMMAND ENCODER + + /** + * Shortcut for timer query (dependent on extension in both WebGL1 and 2) + * Measures GPU time delta between this call and a matching `end` call in the + * GPU instruction stream. + */ + beginTimestampQuery(): void { + return this._begin(GL.TIME_ELAPSED_EXT); + } + + endTimestampQuery(): void { + this._end(); + } + + // Shortcut for occlusion queries + beginOcclusionQuery(options?: {conservative?: boolean}): void { + return this._begin( + options?.conservative ? GL.ANY_SAMPLES_PASSED_CONSERVATIVE : GL.ANY_SAMPLES_PASSED + ); + } + + endOcclusionQuery() { + this._end(); + } + + // Shortcut for transformFeedbackQuery + beginTransformFeedbackQuery(): void { + return this._begin(GL.TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN); + } + + endTransformFeedbackQuery(): void { + this._end(); + } + + async resolveQuery(): Promise { + const value = await this.pollQuery(); + return [value]; + } + + // PRIVATE METHODS + + /** + * Due to OpenGL API limitations, after calling `begin()` on one Query + * instance, `end()` must be called on that same instance before + * calling `begin()` on another query. While there can be multiple + * outstanding queries representing disjoint `begin()`/`end()` intervals. + * It is not possible to interleave or overlap `begin` and `end` calls. + */ + protected _begin(target: number): void { + // Don't start a new query if one is already active. + if (this._queryPending) { + return; + } + + this.target = target; + this.device.gl.beginQuery(this.target, this.handle); + + return; + } + + // ends the current query + protected _end(): void { + // Can't end a new query if the last one hasn't been resolved. + if (this._queryPending) { + return; + } + + if (this.target) { + this.device.gl.endQuery(this.target); + this.target = null; + this._queryPending = true; + } + return; + } + + // Returns true if the query result is available + isResultAvailable(): boolean { + if (!this._queryPending) { + return false; + } + + const resultAvailable = this.device.gl.getQueryParameter( + this.handle, + GL.QUERY_RESULT_AVAILABLE + ); + if (resultAvailable) { + this._queryPending = false; + } + return resultAvailable; + } + + // Timing query is disjoint, i.e. results are invalid + isTimerDisjoint(): boolean { + return this.device.gl.getParameter(GL.GPU_DISJOINT_EXT); + } + + // Returns query result. + getResult(): any { + return this.device.gl.getQueryParameter(this.handle, GL.QUERY_RESULT); + } + + // Returns the query result, converted to milliseconds to match JavaScript conventions. + getTimerMilliseconds() { + return this.getResult() / 1e6; + } + + // Polls the query + pollQuery(limit: number = Number.POSITIVE_INFINITY): Promise { + if (this._pollingPromise) { + return this._pollingPromise; + } + + let counter = 0; + + this._pollingPromise = new Promise((resolve, reject) => { + const poll = () => { + if (this.isResultAvailable()) { + resolve(this.getResult()); + this._pollingPromise = null; + } else if (counter++ > limit) { + reject('Timed out'); + this._pollingPromise = null; + } else { + requestAnimationFrame(poll); + } + }; + + requestAnimationFrame(poll); + }); + + return this._pollingPromise; + } +} diff --git a/modules/webgl/src/adapter/resources/webgl-render-pass.ts b/modules/webgl/src/adapter/resources/webgl-render-pass.ts index 224a2dae06..2b8b3779d2 100644 --- a/modules/webgl/src/adapter/resources/webgl-render-pass.ts +++ b/modules/webgl/src/adapter/resources/webgl-render-pass.ts @@ -8,6 +8,7 @@ import {GL, GLParameters} from '@luma.gl/constants'; import {withGLParameters} from '../../context/state-tracker/with-parameters'; import {setGLParameters} from '../../context/parameters/unified-parameter-api'; import {pushContextState, popContextState} from '../../context/state-tracker/track-context-state'; +import {WEBGLQuerySet} from './webgl-query-set'; // Should collapse during minification const GL_DEPTH_BUFFER_BIT = 0x00000100; @@ -101,6 +102,16 @@ export class WEBGLRenderPass extends RenderPass { setGLParameters(this.device, glParameters); } + beginOcclusionQuery(queryIndex: number): void { + const webglQuerySet = this.props.occlusionQuerySet as WEBGLQuerySet; + webglQuerySet?.beginOcclusionQuery(); + } + + override endOcclusionQuery(): void { + const webglQuerySet = this.props.occlusionQuerySet as WEBGLQuerySet; + webglQuerySet?.endOcclusionQuery(); + } + // PRIVATE /** diff --git a/modules/webgl/src/adapter/webgl-device.ts b/modules/webgl/src/adapter/webgl-device.ts index ee961deec9..73f76c0919 100644 --- a/modules/webgl/src/adapter/webgl-device.ts +++ b/modules/webgl/src/adapter/webgl-device.ts @@ -54,7 +54,8 @@ import type { ComputePassProps, // CommandEncoder, CommandEncoderProps, - TransformFeedbackProps + TransformFeedbackProps, + QuerySetProps } from '@luma.gl/core'; import {WEBGLBuffer} from './resources/webgl-buffer'; @@ -67,6 +68,7 @@ import {WEBGLRenderPipeline} from './resources/webgl-render-pipeline'; import {WEBGLCommandEncoder} from './resources/webgl-command-encoder'; import {WEBGLVertexArray} from './resources/webgl-vertex-array'; import {WEBGLTransformFeedback} from './resources/webgl-transform-feedback'; +import {WEBGLQuerySet} from './resources/webgl-query-set'; import {readPixelsToArray, readPixelsToBuffer} from '../classic/copy-and-blit'; import {setGLParameters, getGLParameters} from '../context/parameters/unified-parameter-api'; @@ -306,6 +308,10 @@ ${device.info.vendor}, ${device.info.renderer} for canvas: ${device.canvasContex return new WEBGLTransformFeedback(this, props); } + createQuerySet(props: QuerySetProps): WEBGLQuerySet { + return new WEBGLQuerySet(this, props); + } + createRenderPipeline(props: RenderPipelineProps): WEBGLRenderPipeline { return new WEBGLRenderPipeline(this, props); } diff --git a/modules/webgpu/src/adapter/resources/webgpu-command-encoder.ts b/modules/webgpu/src/adapter/resources/webgpu-command-encoder.ts index 425868b2a6..308530983c 100644 --- a/modules/webgpu/src/adapter/resources/webgpu-command-encoder.ts +++ b/modules/webgpu/src/adapter/resources/webgpu-command-encoder.ts @@ -7,6 +7,7 @@ import type {CopyTextureToTextureOptions, CopyTextureToBufferOptions} from '@lum import {WebGPUDevice} from '../webgpu-device'; import {WebGPUBuffer} from './webgpu-buffer'; import {WebGPUTexture} from './webgpu-texture'; +import {WebGPUQuerySet} from './webgpu-query-set'; export class WebGPUCommandEncoder extends CommandEncoder { readonly device: WebGPUDevice; @@ -123,4 +124,24 @@ export class WebGPUCommandEncoder extends CommandEncoder { override insertDebugMarker(markerLabel: string): void { this.handle.insertDebugMarker(markerLabel); } + + override resolveQuerySet( + querySet: WebGPUQuerySet, + destination: Buffer, + options?: { + firstQuery?: number; + queryCount?: number; + destinationOffset?: number; + } + ): void { + const webgpuQuerySet = querySet as WebGPUQuerySet; + const webgpuBuffer = destination as WebGPUBuffer; + this.handle.resolveQuerySet( + webgpuQuerySet.handle, + options.firstQuery || 0, + options.queryCount || querySet.props.count - (options.firstQuery || 0), + webgpuBuffer.handle, + options.destinationOffset || 0 + ); + } } diff --git a/modules/webgpu/src/adapter/resources/webgpu-compute-pass.ts b/modules/webgpu/src/adapter/resources/webgpu-compute-pass.ts index 7d66971212..620c95364d 100644 --- a/modules/webgpu/src/adapter/resources/webgpu-compute-pass.ts +++ b/modules/webgpu/src/adapter/resources/webgpu-compute-pass.ts @@ -7,6 +7,7 @@ import {WebGPUDevice} from '../webgpu-device'; import {WebGPUBuffer} from './webgpu-buffer'; // import {WebGPUCommandEncoder} from './webgpu-command-encoder'; import {WebGPUComputePipeline} from './webgpu-compute-pipeline'; +import {WebGPUQuerySet} from './webgpu-query-set'; export class WebGPUComputePass extends ComputePass { readonly device: WebGPUDevice; @@ -17,10 +18,24 @@ export class WebGPUComputePass extends ComputePass { super(device, props); this.device = device; + // Set up queries + let timestampWrites: GPUComputePassTimestampWrites | undefined; + if (device.features.has('timestamp-query')) { + const webgpuQuerySet = props.timestampQuerySet as WebGPUQuerySet; + if (webgpuQuerySet) { + timestampWrites = { + querySet: webgpuQuerySet.handle, + beginningOfPassWriteIndex: props.beginTimestampIndex, + endOfPassWriteIndex: props.endTimestampIndex + }; + } + } + this.handle = this.props.handle || device.commandEncoder?.beginComputePass({ - label: this.props.id + label: this.props.id, + timestampWrites }); } diff --git a/modules/webgpu/src/adapter/resources/webgpu-query-set.ts b/modules/webgpu/src/adapter/resources/webgpu-query-set.ts new file mode 100644 index 0000000000..9ce5134d4c --- /dev/null +++ b/modules/webgpu/src/adapter/resources/webgpu-query-set.ts @@ -0,0 +1,33 @@ +// luma.gl +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +import {QuerySet, QuerySetProps} from '@luma.gl/core'; +import {WebGPUDevice} from '../webgpu-device'; + +export type QuerySetProps2 = { + type: 'occlusion' | 'timestamp'; + count: number; +}; + +/** + * Immutable + */ +export class WebGPUQuerySet extends QuerySet { + readonly device: WebGPUDevice; + readonly handle: GPUQuerySet; + + constructor(device: WebGPUDevice, props: QuerySetProps) { + super(device, props); + this.device = device; + this.handle = this.props.handle || this.device.handle.createQuerySet({ + type: this.props.type, + count: this.props.count, + }); + this.handle.label = this.props.id; + } + + override destroy(): void { + this.handle.destroy(); + } +} diff --git a/modules/webgpu/src/adapter/resources/webgpu-render-pass.ts b/modules/webgpu/src/adapter/resources/webgpu-render-pass.ts index 5ed60e744d..13076098a0 100644 --- a/modules/webgpu/src/adapter/resources/webgpu-render-pass.ts +++ b/modules/webgpu/src/adapter/resources/webgpu-render-pass.ts @@ -9,6 +9,7 @@ import {WebGPUBuffer} from './webgpu-buffer'; import {WebGPUTextureView} from './webgpu-texture-view'; // import {WebGPUCommandEncoder} from './webgpu-command-encoder'; import {WebGPURenderPipeline} from './webgpu-render-pipeline'; +import {WebGPUQuerySet} from './webgpu-query-set'; export class WebGPURenderPass extends RenderPass { readonly device: WebGPUDevice; @@ -23,6 +24,20 @@ export class WebGPURenderPass extends RenderPass { const framebuffer = props.framebuffer || device.canvasContext.getCurrentFramebuffer(); const renderPassDescriptor = this.getRenderPassDescriptor(framebuffer); + const webgpuQuerySet = props.timestampQuerySet as WebGPUQuerySet; + if (webgpuQuerySet) { + renderPassDescriptor.occlusionQuerySet = webgpuQuerySet.handle; + } + + if (device.features.has('timestamp-query')) { + const webgpuQuerySet = props.timestampQuerySet as WebGPUQuerySet; + renderPassDescriptor.timestampWrites = webgpuQuerySet ? { + querySet: webgpuQuerySet.handle, + beginningOfPassWriteIndex: props.beginTimestampIndex, + endOfPassWriteIndex: props.endTimestampIndex + } as GPUComputePassTimestampWrites : undefined; + } + this.handle = this.props.handle || device.commandEncoder.beginRenderPass(renderPassDescriptor); this.handle.label = this.props.id; log.groupCollapsed(3, `new WebGPURenderPass(${this.id})`)(); diff --git a/modules/webgpu/src/adapter/webgpu-device.ts b/modules/webgpu/src/adapter/webgpu-device.ts index 3782f33ad1..fa7dcd30a4 100644 --- a/modules/webgpu/src/adapter/webgpu-device.ts +++ b/modules/webgpu/src/adapter/webgpu-device.ts @@ -26,7 +26,9 @@ import type { // CommandEncoderProps, VertexArrayProps, TransformFeedback, - TransformFeedbackProps + TransformFeedbackProps, + QuerySet, + QuerySetProps } from '@luma.gl/core'; import {Device, DeviceFeatures, CanvasContext, log, uid} from '@luma.gl/core'; import {WebGPUBuffer} from './resources/webgpu-buffer'; @@ -43,6 +45,7 @@ import {WebGPUComputePass} from './resources/webgpu-compute-pass'; import {WebGPUVertexArray} from './resources/webgpu-vertex-array'; import {WebGPUCanvasContext} from './webgpu-canvas-context'; +import {WebGPUQuerySet} from './resources/webgpu-query-set'; /** WebGPU Device implementation */ export class WebGPUDevice extends Device { @@ -245,6 +248,10 @@ export class WebGPUDevice extends Device { throw new Error('Transform feedback not supported in WebGPU'); } + override createQuerySet(props: QuerySetProps): QuerySet { + return new WebGPUQuerySet(this, props); + } + createCanvasContext(props: CanvasContextProps): WebGPUCanvasContext { return new WebGPUCanvasContext(this, this.adapter, props); } From cf03fdf234d199ba4a0073518cf82429bce20d00 Mon Sep 17 00:00:00 2001 From: Ib Green Date: Wed, 28 Feb 2024 07:34:26 -0500 Subject: [PATCH 2/7] wip --- .eslintrc.cjs | 3 ++- modules/constants/src/webgl-constants.ts | 6 +++--- .../core/src/adapter/resources/command-encoder.ts | 7 ++++--- .../src/adapter/resources/webgl-command-encoder.ts | 14 +++++++++++++- .../src/adapter/resources/webgl-render-pass.ts | 2 +- .../adapter/resources/webgpu-command-encoder.ts | 2 +- .../src/adapter/resources/webgpu-query-set.ts | 12 +++++++----- .../src/adapter/resources/webgpu-render-pass.ts | 12 +++++++----- modules/webgpu/src/adapter/webgpu-device.ts | 2 +- webpack.config.js => webpack.config.js.disabled | 0 10 files changed, 39 insertions(+), 21 deletions(-) rename webpack.config.js => webpack.config.js.disabled (100%) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index d188569c44..710a954ba9 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -163,7 +163,8 @@ module.exports = getESLintConfig({ "GPUTextureFormat": true, "GPUBufferUsage": true, "GPUVertexFormat": true, - "GPURenderPassDescriptor": true + "GPURenderPassDescriptor": true, + "GPUComputePassTimestampWrites": true } } ], diff --git a/modules/constants/src/webgl-constants.ts b/modules/constants/src/webgl-constants.ts index 78a6947baf..9225205693 100644 --- a/modules/constants/src/webgl-constants.ts +++ b/modules/constants/src/webgl-constants.ts @@ -620,7 +620,7 @@ enum GLEnum { INT_2_10_10_10_REV = 0x8d9f, // Queries - + CURRENT_QUERY = 0x8865, /** Returns a GLuint containing the query result. */ QUERY_RESULT = 0x8866, @@ -969,8 +969,8 @@ enum GLEnum { /** The current time. */ TIMESTAMP_EXT = 0x8e28, /** A Boolean indicating whether or not the GPU performed any disjoint operation (lost context) */ - GPU_DISJOINT_EXT = 0x8fbb, - + GPU_DISJOINT_EXT = 0x8fbb, + // KHR_parallel_shader_compile https://registry.khronos.org/webgl/extensions/KHR_parallel_shader_compile /** a non-blocking poll operation, so that compile/link status availability can be queried without potentially incurring stalls */ diff --git a/modules/core/src/adapter/resources/command-encoder.ts b/modules/core/src/adapter/resources/command-encoder.ts index b70d1e374c..59d6999324 100644 --- a/modules/core/src/adapter/resources/command-encoder.ts +++ b/modules/core/src/adapter/resources/command-encoder.ts @@ -164,8 +164,9 @@ export abstract class CommandEncoder extends Resource { querySet: QuerySet, destination: Buffer, options?: { - firstQuery: number; - queryCount: number; + firstQuery?: number; + queryCount?: number; + destinationOffset?: number; } ): void; @@ -179,4 +180,4 @@ export abstract class CommandEncoder extends Resource { // TODO - luma.gl has these on the device, should we align with WebGPU API? // beginRenderPass(GPURenderPassDescriptor descriptor): GPURenderPassEncoder; // beginComputePass(optional GPUComputePassDescriptor descriptor = {}): GPUComputePassEncoder; - } +} diff --git a/modules/webgl/src/adapter/resources/webgl-command-encoder.ts b/modules/webgl/src/adapter/resources/webgl-command-encoder.ts index 0b349263c5..82bfac3802 100644 --- a/modules/webgl/src/adapter/resources/webgl-command-encoder.ts +++ b/modules/webgl/src/adapter/resources/webgl-command-encoder.ts @@ -7,7 +7,9 @@ import type { CopyBufferToBufferOptions, CopyBufferToTextureOptions, CopyTextureToBufferOptions, - CopyTextureToTextureOptions + CopyTextureToTextureOptions, + QuerySet, + Buffer } from '@luma.gl/core'; import {WEBGLCommandBuffer} from './webgl-command-buffer'; @@ -54,4 +56,14 @@ export class WEBGLCommandEncoder extends CommandEncoder { override popDebugGroup() {} override insertDebugMarker(markerLabel: string): void {} + + override resolveQuerySet( + querySet: QuerySet, + destination: Buffer, + options?: { + firstQuery?: number; + queryCount?: number; + destinationOffset?: number; + } + ): void {} } diff --git a/modules/webgl/src/adapter/resources/webgl-render-pass.ts b/modules/webgl/src/adapter/resources/webgl-render-pass.ts index 2b8b3779d2..ba8159c16e 100644 --- a/modules/webgl/src/adapter/resources/webgl-render-pass.ts +++ b/modules/webgl/src/adapter/resources/webgl-render-pass.ts @@ -109,7 +109,7 @@ export class WEBGLRenderPass extends RenderPass { override endOcclusionQuery(): void { const webglQuerySet = this.props.occlusionQuerySet as WEBGLQuerySet; - webglQuerySet?.endOcclusionQuery(); + webglQuerySet?.endOcclusionQuery(); } // PRIVATE diff --git a/modules/webgpu/src/adapter/resources/webgpu-command-encoder.ts b/modules/webgpu/src/adapter/resources/webgpu-command-encoder.ts index 308530983c..0325195e51 100644 --- a/modules/webgpu/src/adapter/resources/webgpu-command-encoder.ts +++ b/modules/webgpu/src/adapter/resources/webgpu-command-encoder.ts @@ -134,7 +134,7 @@ export class WebGPUCommandEncoder extends CommandEncoder { destinationOffset?: number; } ): void { - const webgpuQuerySet = querySet as WebGPUQuerySet; + const webgpuQuerySet = querySet; const webgpuBuffer = destination as WebGPUBuffer; this.handle.resolveQuerySet( webgpuQuerySet.handle, diff --git a/modules/webgpu/src/adapter/resources/webgpu-query-set.ts b/modules/webgpu/src/adapter/resources/webgpu-query-set.ts index 9ce5134d4c..93277aae2c 100644 --- a/modules/webgpu/src/adapter/resources/webgpu-query-set.ts +++ b/modules/webgpu/src/adapter/resources/webgpu-query-set.ts @@ -5,7 +5,7 @@ import {QuerySet, QuerySetProps} from '@luma.gl/core'; import {WebGPUDevice} from '../webgpu-device'; -export type QuerySetProps2 = { +export type QuerySetProps2 = { type: 'occlusion' | 'timestamp'; count: number; }; @@ -20,10 +20,12 @@ export class WebGPUQuerySet extends QuerySet { constructor(device: WebGPUDevice, props: QuerySetProps) { super(device, props); this.device = device; - this.handle = this.props.handle || this.device.handle.createQuerySet({ - type: this.props.type, - count: this.props.count, - }); + this.handle = + this.props.handle || + this.device.handle.createQuerySet({ + type: this.props.type, + count: this.props.count + }); this.handle.label = this.props.id; } diff --git a/modules/webgpu/src/adapter/resources/webgpu-render-pass.ts b/modules/webgpu/src/adapter/resources/webgpu-render-pass.ts index 13076098a0..d41fccc20b 100644 --- a/modules/webgpu/src/adapter/resources/webgpu-render-pass.ts +++ b/modules/webgpu/src/adapter/resources/webgpu-render-pass.ts @@ -31,11 +31,13 @@ export class WebGPURenderPass extends RenderPass { if (device.features.has('timestamp-query')) { const webgpuQuerySet = props.timestampQuerySet as WebGPUQuerySet; - renderPassDescriptor.timestampWrites = webgpuQuerySet ? { - querySet: webgpuQuerySet.handle, - beginningOfPassWriteIndex: props.beginTimestampIndex, - endOfPassWriteIndex: props.endTimestampIndex - } as GPUComputePassTimestampWrites : undefined; + renderPassDescriptor.timestampWrites = webgpuQuerySet + ? ({ + querySet: webgpuQuerySet.handle, + beginningOfPassWriteIndex: props.beginTimestampIndex, + endOfPassWriteIndex: props.endTimestampIndex + } as GPUComputePassTimestampWrites) + : undefined; } this.handle = this.props.handle || device.commandEncoder.beginRenderPass(renderPassDescriptor); diff --git a/modules/webgpu/src/adapter/webgpu-device.ts b/modules/webgpu/src/adapter/webgpu-device.ts index fa7dcd30a4..a2a0671473 100644 --- a/modules/webgpu/src/adapter/webgpu-device.ts +++ b/modules/webgpu/src/adapter/webgpu-device.ts @@ -249,7 +249,7 @@ export class WebGPUDevice extends Device { } override createQuerySet(props: QuerySetProps): QuerySet { - return new WebGPUQuerySet(this, props); + return new WebGPUQuerySet(this, props); } createCanvasContext(props: CanvasContextProps): WebGPUCanvasContext { diff --git a/webpack.config.js b/webpack.config.js.disabled similarity index 100% rename from webpack.config.js rename to webpack.config.js.disabled From a3186fd027dc1d363b72adc56ab083850f2fa50d Mon Sep 17 00:00:00 2001 From: Ib Green Date: Wed, 28 Feb 2024 07:35:10 -0500 Subject: [PATCH 3/7] wip --- Dockerfile | 19 ---------- docker-compose.yml | 7 ---- webpack.config.js.disabled | 77 -------------------------------------- 3 files changed, 103 deletions(-) delete mode 100644 Dockerfile delete mode 100644 docker-compose.yml delete mode 100644 webpack.config.js.disabled diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 3fb68c3ebf..0000000000 --- a/Dockerfile +++ /dev/null @@ -1,19 +0,0 @@ -# Docker Image for BuildKite CI -# ----------------------------- - -FROM node:8.9.0 - -WORKDIR /luma-gl -ENV PATH /luma-gl/node_modules/.bin:$PATH - -ENV DISPLAY :99 - -RUN apt-get update -RUN apt-get -y install libxi-dev libgl1-mesa-dev xvfb - -ADD .buildkite/xvfb /etc/init.d/xvfb -RUN chmod a+x /etc/init.d/xvfb - -COPY package.json yarn.lock /luma-gl/ - -RUN yarn diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index d238c13f46..0000000000 --- a/docker-compose.yml +++ /dev/null @@ -1,7 +0,0 @@ -version: '2' -services: - luma-gl: - build: . - volumes: - - .:/luma-gl - - /luma-gl/node_modules/ diff --git a/webpack.config.js.disabled b/webpack.config.js.disabled deleted file mode 100644 index 0b38b74f52..0000000000 --- a/webpack.config.js.disabled +++ /dev/null @@ -1,77 +0,0 @@ -/* eslint-disable import/no-extraneous-dependencies */ -const {getWebpackConfig} = require('ocular-dev-tools'); - -module.exports = (env = {}) => { - let config = getWebpackConfig(env); - - config.devtool = 'source-map'; - - config = addBabelSettings(config); - - switch (env.mode) { - case 'perf': - config.entry = { - perf: './test/perf/index.js' - }; - break; - - default: - } - - // Log regex - // eslint-disable-next-line - Object.defineProperty(RegExp.prototype, 'toJSON', { - value: RegExp.prototype.toString - }); - - // Uncomment to debug config - // console.warn(JSON.stringify(config, null, 2)); - - return [config]; -}; - -function makeBabelRule() { - return { - // Compile source using babel - test: /(\.js|\.ts|\.tsx)$/, - loader: 'babel-loader', - include: [/modules\/.*\/src/, /modules\/.*\/test/, /examples/, /test/], - exclude: [/node_modules/], - options: { - presets: [ - '@babel/preset-react', - '@babel/preset-typescript', - [ - '@babel/preset-env', - { - exclude: [/transform-async-to-generator/, /transform-regenerator/] - } - ] - ], - plugins: ['@babel/plugin-proposal-class-properties'] - } - }; -} - -function addBabelSettings(config) { - return { - ...config, - module: { - ...config.module, - rules: [ - ...config.module.rules.filter(r => r.loader !== 'babel-loader'), - makeBabelRule(), - // See https://github.com/apollographql/apollo-link-state/issues/302 - { - test: /\.mjs$/, - include: /node_modules/, - type: 'javascript/auto' - } - ] - }, - resolve: { - ...config.resolve, - extensions: ['.ts', '.tsx', '.js', '.json'] - } - }; -} From f421582c019c148c9416f390e50f04a60adedf00 Mon Sep 17 00:00:00 2001 From: Ib Green Date: Wed, 28 Feb 2024 07:45:08 -0500 Subject: [PATCH 4/7] wip --- .../adapter/resources/framebuffer.spec.ts | 4 + .../test/adapter/resources/query-set.spec.ts | 101 ++++++------------ modules/core-tests/test/index.ts | 7 +- 3 files changed, 41 insertions(+), 71 deletions(-) rename wip/modules-wip/webgl-legacy/test/classic/query.spec.ts => modules/core-tests/test/adapter/resources/query-set.spec.ts (59%) diff --git a/modules/core-tests/test/adapter/resources/framebuffer.spec.ts b/modules/core-tests/test/adapter/resources/framebuffer.spec.ts index 5c231c63b5..c9fda83feb 100644 --- a/modules/core-tests/test/adapter/resources/framebuffer.spec.ts +++ b/modules/core-tests/test/adapter/resources/framebuffer.spec.ts @@ -1,3 +1,7 @@ +// luma.gl +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + /* eslint-disable max-len */ import test from 'tape-promise/tape'; import {Framebuffer} from '@luma.gl/core'; diff --git a/wip/modules-wip/webgl-legacy/test/classic/query.spec.ts b/modules/core-tests/test/adapter/resources/query-set.spec.ts similarity index 59% rename from wip/modules-wip/webgl-legacy/test/classic/query.spec.ts rename to modules/core-tests/test/adapter/resources/query-set.spec.ts index 21902bc9c1..6b1e96a7fd 100644 --- a/wip/modules-wip/webgl-legacy/test/classic/query.spec.ts +++ b/modules/core-tests/test/adapter/resources/query-set.spec.ts @@ -1,18 +1,24 @@ -/* eslint-disable max-len, max-statements */ +// luma.gl +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + import test from 'tape-promise/tape'; -import {Query} from '@luma.gl/webgl-legacy'; -// import util from 'util'; -import {GL} from '@luma.gl/constants'; -import {fixture} from 'test/setup'; +import {getTestDevices} from '@luma.gl/test-utils'; +import {QuerySet} from '@luma.gl/core'; -function pollQuery(query, t) { - return query - .createPoll(10) - .then((result) => t.pass(`Timer query: ${result}ms`)) - .catch((error) => t.fail(`Timer query: ${error}`)); -} -function testQueryConstructDelete(gl, t) { +test('QuerySet construct/delete', async (t) => { + for (const device of await getTestDevices()) { + const querySet = device.createQuerySet({type: 'occlusion', count: 1}); + t.ok(querySet instanceof QuerySet, 'QuerySet construction successful'); + querySet.destroy(); + t.pass('QuerySet delete successful'); + } + t.end(); +}); + +/* +test('Query construct/delete', (t) => { const ext = gl.getExtension('EXT_disjoint_timer_query'); t.comment(`EXT_disjoint_timer_query is ${Boolean(ext)} ${ext}`, ext); // util.inspect(ext, {showHidden: true}); @@ -24,8 +30,6 @@ function testQueryConstructDelete(gl, t) { t.comment('Query is not supported, testing graceful fallback'); } - // @ts-expect-error - t.throws(() => new Query(), /.*WebGLRenderingContext.*/, 'Query throws on missing gl context'); const timerQuery = new Query(gl); t.ok(timerQuery, 'Query construction successful'); @@ -39,23 +43,10 @@ function testQueryConstructDelete(gl, t) { t.end(); } -test('WebGL#Query construct/delete', (t) => { - const {gl} = fixture; - - testQueryConstructDelete(gl, t); -}); - -test('WebGL2#Query construct/delete', (t) => { - const {gl2} = fixture; - if (!gl2) { - t.comment('WebGL2 not available, skipping tests'); - t.end(); - return; - } - testQueryConstructDelete(gl2, t); -}); - function testQueryCompleteFail(gl, t) { +} + +test('Query completed/failed queries', (t) => { if (!Query.isSupported(gl, ['timers'])) { t.comment('Query Timer API not supported, skipping tests'); return null; @@ -66,26 +57,17 @@ function testQueryCompleteFail(gl, t) { timerQuery.beginTimeElapsedQuery().end(); return pollQuery(timerQuery, t); -} - -test('WebGL#Query completed/failed queries', (t) => { - const {gl} = fixture; - testQueryCompleteFail(gl, t); t.end(); }); -test('WebGL2#Query completed/failed queries', (t) => { +test('TimeElapsedQuery', (t) => { const {gl2} = fixture; if (!gl2) { t.comment('WebGL2 not available, skipping tests'); t.end(); return; } - testQueryCompleteFail(gl2, t); - t.end(); -}); - -function testQuery(gl, opts, target, t) { + const opts = ['timers']; if (!Query.isSupported(gl, opts)) { t.comment('Query API not supported, skipping tests'); return null; @@ -94,35 +76,10 @@ function testQuery(gl, opts, target, t) { query.begin(target).end(); return pollQuery(query, t); -} - -test('WebGL#TimeElapsedQuery', (t) => { - const {gl} = fixture; - const opts = ['timers']; - testQuery(gl, opts, GL.TIME_ELAPSED_EXT, t); t.end(); }); -test('WebGL2#TimeElapsedQuery', (t) => { - const {gl2} = fixture; - if (!gl2) { - t.comment('WebGL2 not available, skipping tests'); - t.end(); - return; - } - const opts = ['timers']; - testQuery(gl2, opts, GL.TIME_ELAPSED_EXT, t); - t.end(); -}); - -test('WebGL#OcclusionQuery', (t) => { - const {gl} = fixture; - const opts = ['queries']; - testQuery(gl, opts, GL.ANY_SAMPLES_PASSED_CONSERVATIVE, t); - t.end(); -}); - -test('WebGL2#OcclusionQuery', (t) => { +test('OcclusionQuery', (t) => { const {gl2} = fixture; if (!gl2) { t.comment('WebGL2 not available, skipping tests'); @@ -141,7 +98,7 @@ test('WebGL#TransformFeedbackQuery', (t) => { t.end(); }); -test('WebGL2#TransformFeedbackQuery', (t) => { +test('TransformFeedbackQuery', (t) => { const {gl2} = fixture; if (!gl2) { t.comment('WebGL2 not available, skipping tests'); @@ -152,3 +109,11 @@ test('WebGL2#TransformFeedbackQuery', (t) => { testQuery(gl2, opts, GL.TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN, t); t.end(); }); + +function pollQuery(query, t) { + return query + .createPoll(10) + .then((result) => t.pass(`Timer query: ${result}ms`)) + .catch((error) => t.fail(`Timer query: ${error}`)); +} +*/ diff --git a/modules/core-tests/test/index.ts b/modules/core-tests/test/index.ts index ea07de49f0..aae9467b0f 100644 --- a/modules/core-tests/test/index.ts +++ b/modules/core-tests/test/index.ts @@ -16,8 +16,8 @@ import './adapter/device-helpers/device-features.spec'; import './adapter/device-helpers/device-limits.spec'; import './adapter/device-helpers/set-device-parameters.spec'; -// import './adapter/webgl-device.spec'; -// import './adapter/webgl-canvas-context.spec'; +// import './adapter/device.spec'; +// import './adapter/canvas-context.spec'; // Resources import './adapter/texture-formats.spec'; @@ -30,4 +30,5 @@ import './adapter/resources/render-pipeline.spec'; import './adapter/resources/shader.spec'; import './adapter/resources/sampler.spec'; import './adapter/resources/texture.spec'; -// import './adapter/resources/vertex-array.spec'; +import './adapter/resources/vertex-array.spec'; +import './adapter/resources/query.spec'; From d9311c2eecd67feb042bfde0e0f259421ff99be0 Mon Sep 17 00:00:00 2001 From: Ib Green Date: Wed, 28 Feb 2024 07:47:43 -0500 Subject: [PATCH 5/7] wip --- modules/core-tests/test/index.ts | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/modules/core-tests/test/index.ts b/modules/core-tests/test/index.ts index aae9467b0f..5d3cc7d925 100644 --- a/modules/core-tests/test/index.ts +++ b/modules/core-tests/test/index.ts @@ -2,12 +2,6 @@ // SPDX-License-Identifier: MIT // Copyright (c) vis.gl contributors -// Note that we do two test runs on luma.gl, with and without headless-gl -// This file imports tests that should run *with* headless-gl included - -// import './adapter/helpers/parse-shader-compiler-log.spec'; -// import './adapter/helpers/get-shader-layout.spec'; - // ADAPTER // WebGLDevice, features & limits @@ -16,8 +10,11 @@ import './adapter/device-helpers/device-features.spec'; import './adapter/device-helpers/device-limits.spec'; import './adapter/device-helpers/set-device-parameters.spec'; -// import './adapter/device.spec'; -// import './adapter/canvas-context.spec'; +import './adapter/helpers/parse-shader-compiler-log.spec'; +// import './adapter/helpers/get-shader-layout.spec'; + +import './adapter/device.spec'; +import './adapter/canvas-context.spec'; // Resources import './adapter/texture-formats.spec'; @@ -31,4 +28,4 @@ import './adapter/resources/shader.spec'; import './adapter/resources/sampler.spec'; import './adapter/resources/texture.spec'; import './adapter/resources/vertex-array.spec'; -import './adapter/resources/query.spec'; +import './adapter/resources/query-set.spec'; From 94f5f933dc9c80295836213a598d3c8535699e14 Mon Sep 17 00:00:00 2001 From: Ib Green Date: Wed, 28 Feb 2024 08:43:45 -0500 Subject: [PATCH 6/7] wip --- .../api-reference/core/resources/query-set.md | 81 +++++++++++++++++++ docs/developer-guide/profiling.md | 12 ++- docs/sidebar.json | 3 +- .../test/adapter/resources/query-set.spec.ts | 3 +- 4 files changed, 94 insertions(+), 5 deletions(-) create mode 100644 docs/api-reference/core/resources/query-set.md diff --git a/docs/api-reference/core/resources/query-set.md b/docs/api-reference/core/resources/query-set.md new file mode 100644 index 0000000000..67b6a3cb42 --- /dev/null +++ b/docs/api-reference/core/resources/query-set.md @@ -0,0 +1,81 @@ +# QuerySet + +:::caution +This page is incomplete. +::: + +A `QuerySet` object provides an API for using asynchronous GPU queries of the following types +- timer queries. +- 'Occlusion' +- 'Transform Feedback' + +A `QuerySet` holds a number of 64 bit values. + +Timer queries are available if the `timestamp-query` extension is available. (On WebGL 2 this is equivalent to the +[`EXT_disjoint_timer_query_webgl2`](https://www.khronos.org/registry/webgl/extensions/EXT_disjoint_timer_query_webgl2/) +being supported on the current browser. + +Note that even when supported, timer queries can fail whenever a change in the GPU occurs that will make the values returned by this extension unusable for performance metrics, for example if the GPU is throttled mid-frame. + +## Usage + +Create a timestamp query set: + +```typescript +import {QuerySet} from '@luma.gl/core'; +... +const timestampQuery = device.createQuerySet({type: 'timestamp'}}); +``` + +## Types + +### `QueryProps` + +- `type`: `occlusion` | `timestamp` - type of timer +- `count`: `number` - number of query results held by this `QuerySet` + +| Query Type | Usage | Description | +| ---------------------------------------- | -------------------------------------------- | -------------------------------------------------------------------------- | +| `timestamp` (`RenderPass begin/end`) | `beginRenderPass({timestampQuery: ...})` | Time taken by GPU to execute RenderPass commands | +| `timestamp` (`ComputePass begin/end`) | `beginComputePass({timestampQuery: ...})` | Time taken by GPU to execute ComputePass commands | +| `occlusion` | `beginOcclusionQuery({conservative: false})` | Occlusion query how many fragment samples pass tests (depth, stencil, ...) | +| `occlusion` | `beginOcclusionQuery({conservative: true})` | Same as above above, but less accurate and faster | +| `transform-feedback` (Not yet supported) | `beginTransformFeedbackQuery()` | Number of primitives that are written to transform feedback buffers. | + +In addition to above queries, Query object also provides `getTimeStamp` which returns GPU time stamp at the time this query is executed by GPU. Two sets of these methods can be used to calculate time taken by GPU for a set of GL commands. + +## DeviceFeatures + +`timestamp-query`: Whether `QuerySet` can be created with type `timestamp`. + +## Methods + + + +### `constructor(device: Device, props: Object)` + +`new Query(gl, {})` +- options.timers=false Object - If true, checks if 'TIME_ELAPSED_EXT' queries are supported + +### `destroy()` + +Destroys the WebGL object. Rejects any pending query. + +- return Query - returns itself, to enable chaining of calls. + + +### beginTimeElapsedQuery() + +Shortcut for timer query (dependent on extension in both WebGL 1 and 2) + +## RenderPass + + +### `RenderPass.beginOcclusionQuery({conservative = false})` + +Shortcut for occlusion query (dependent on WebGL 2) + +### `RenderPass.beginTransformFeedbackQuery()` + +WebGL 2 only. not yet implemented. + diff --git a/docs/developer-guide/profiling.md b/docs/developer-guide/profiling.md index 3f72996749..3b911175c0 100644 --- a/docs/developer-guide/profiling.md +++ b/docs/developer-guide/profiling.md @@ -1,7 +1,7 @@ # Profiling GPU programming is all about performance, so having tools to systematically -measure the performance impact of code changes are critical. luma.gl offers +measure the performance impact of code changes is critical. luma.gl offers several built-in facilities. ## probe.gl Stats @@ -31,4 +31,12 @@ however tracking allocations can help spot resource leaks or unnecessary work be ## Performance Profiling -Queries are supported if available. +`device.createQuerySet()` can be used to create GPU queries that + +- Occlusion Queries always supported. +- Timestamp Queries are supported if the `timestamp-query` feature is available, check with `device.features.has('timestamp-query')`. + +`QuerySet` instances can be supplied when creating `RenderPass` and `ComputePass` instances. + +Results are available through +`commandEncoder.resolveQuerySet()` diff --git a/docs/sidebar.json b/docs/sidebar.json index 8627be713e..4fcc73b3bc 100644 --- a/docs/sidebar.json +++ b/docs/sidebar.json @@ -94,7 +94,8 @@ "api-reference/core/resources/shader", "api-reference/core/shader-logs", "api-reference/core/resources/texture", - "api-reference/core/resources/vertex-array" + "api-reference/core/resources/vertex-array", + "api-reference/core/resources/query-set" ] }, { diff --git a/modules/core-tests/test/adapter/resources/query-set.spec.ts b/modules/core-tests/test/adapter/resources/query-set.spec.ts index 6b1e96a7fd..3012597f66 100644 --- a/modules/core-tests/test/adapter/resources/query-set.spec.ts +++ b/modules/core-tests/test/adapter/resources/query-set.spec.ts @@ -6,8 +6,7 @@ import test from 'tape-promise/tape'; import {getTestDevices} from '@luma.gl/test-utils'; import {QuerySet} from '@luma.gl/core'; - -test('QuerySet construct/delete', async (t) => { +test('QuerySet construct/delete', async t => { for (const device of await getTestDevices()) { const querySet = device.createQuerySet({type: 'occlusion', count: 1}); t.ok(querySet instanceof QuerySet, 'QuerySet construction successful'); From a41e010a4ad242c4cb01d4c6313cca3464fa4147 Mon Sep 17 00:00:00 2001 From: Ib Green Date: Wed, 28 Feb 2024 08:47:32 -0500 Subject: [PATCH 7/7] lint --- .eslintrc.cjs | 3 +++ .../test/adapter/helpers/parse-shader-compiler-log.spec.ts | 2 ++ 2 files changed, 5 insertions(+) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 710a954ba9..19a1dfe3b2 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -38,6 +38,9 @@ module.exports = getESLintConfig({ { files: ['**/*.ts', '**/*.tsx', '**/*.d.ts'], rules: { + 'quotes': 0, // handled by prettier + 'indent': 0, // handled by prettier + // typescript-eslint 6.0 '@typescript-eslint/no-unsafe-argument': 0, '@typescript-eslint/no-redundant-type-constituents': 0, diff --git a/modules/core-tests/test/adapter/helpers/parse-shader-compiler-log.spec.ts b/modules/core-tests/test/adapter/helpers/parse-shader-compiler-log.spec.ts index 2a6a7160ce..07e6922b74 100644 --- a/modules/core-tests/test/adapter/helpers/parse-shader-compiler-log.spec.ts +++ b/modules/core-tests/test/adapter/helpers/parse-shader-compiler-log.spec.ts @@ -2,6 +2,8 @@ // SPDX-License-Identifier: MIT // Copyright (c) vis.gl contributors +/* eslint-disable quotes */ + import test from 'tape-promise/tape'; import {parseShaderCompilerLog} from '@luma.gl/webgl/adapter/helpers/parse-shader-compiler-log';