diff --git a/src/mol-gl/webgl/compat.ts b/src/mol-gl/webgl/compat.ts index 242fe0e40abd661e60e0641159e3bf885bdac741..6c26919119431e5609cae34333ccb71c3b670264 100644 --- a/src/mol-gl/webgl/compat.ts +++ b/src/mol-gl/webgl/compat.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Alexander Rose <alexander.rose@weirdbyte.de> */ @@ -320,6 +320,87 @@ export function getSRGB(gl: GLRenderingContext): COMPAT_sRGB | null { } } +export interface COMPAT_disjoint_timer_query { + /** A GLint indicating the number of bits used to hold the query result for the given target. */ + QUERY_COUNTER_BITS: number + /** A WebGLQuery object, which is the currently active query for the given target. */ + CURRENT_QUERY: number + /** A GLuint64EXT containing the query result. */ + QUERY_RESULT: number + /** A GLboolean indicating whether or not a query result is available. */ + QUERY_RESULT_AVAILABLE: number + /** Elapsed time (in nanoseconds). */ + TIME_ELAPSED: number + /** The current time. */ + TIMESTAMP: number + /** A GLboolean indicating whether or not the GPU performed any disjoint operation. */ + GPU_DISJOINT: number + + /** Creates a new WebGLTimerQueryEXT. */ + createQuery: () => WebGLQuery + /** Deletes a given WebGLTimerQueryEXT. */ + deleteQuery: (query: WebGLQuery) => void + /** Returns true if a given object is a valid WebGLTimerQueryEXT. */ + isQuery: (query: WebGLQuery) => boolean + /** The timer starts when all commands prior to beginQueryEXT have been fully executed. */ + beginQuery: (target: number, query: WebGLQuery) => void + /** The timer stops when all commands prior to endQueryEXT have been fully executed. */ + endQuery: (target: number) => void + /** Records the current time into the corresponding query object. */ + queryCounter: (query: WebGLQuery, target: number) => void + /** Returns information about a query target. */ + getQuery: (target: number, pname: number) => WebGLQuery | number + /** Return the state of a query object. */ + getQueryParameter: (query: WebGLQuery, pname: number) => number | boolean +} + +export function getDisjointTimerQuery(gl: GLRenderingContext): COMPAT_disjoint_timer_query | null { + if (isWebGL2(gl)) { + // Firefox has EXT_disjoint_timer_query in webgl2 + const ext = gl.getExtension('EXT_disjoint_timer_query_webgl2') || gl.getExtension('EXT_disjoint_timer_query'); + if (ext === null) return null; + return { + QUERY_COUNTER_BITS: ext.QUERY_COUNTER_BITS_EXT, + CURRENT_QUERY: gl.CURRENT_QUERY, + QUERY_RESULT: gl.QUERY_RESULT, + QUERY_RESULT_AVAILABLE: gl.QUERY_RESULT_AVAILABLE, + TIME_ELAPSED: ext.TIME_ELAPSED_EXT, + TIMESTAMP: ext.TIMESTAMP_EXT, + GPU_DISJOINT: ext.GPU_DISJOINT_EXT, + + createQuery: gl.createQuery.bind(gl), + deleteQuery: gl.deleteQuery.bind(gl), + isQuery: gl.isQuery.bind(gl), + beginQuery: gl.beginQuery.bind(gl), + endQuery: gl.endQuery.bind(gl), + queryCounter: ext.queryCounterEXT.bind(ext), + getQuery: gl.getQuery.bind(gl), + getQueryParameter: gl.getQueryParameter.bind(gl), + }; + } else { + const ext = gl.getExtension('EXT_disjoint_timer_query'); + if (ext === null) return null; + return { + QUERY_COUNTER_BITS: ext.QUERY_COUNTER_BITS_EXT, + CURRENT_QUERY: ext.CURRENT_QUERY_EXT, + QUERY_RESULT: ext.QUERY_RESULT_EXT, + QUERY_RESULT_AVAILABLE: ext.QUERY_RESULT_AVAILABLE_EXT, + TIME_ELAPSED: ext.TIME_ELAPSED_EXT, + TIMESTAMP: ext.TIMESTAMP_EXT, + GPU_DISJOINT: ext.GPU_DISJOINT_EXT, + + createQuery: ext.createQueryEXT.bind(ext), + deleteQuery: ext.deleteQueryEXT.bind(ext), + isQuery: ext.isQueryEXT.bind(ext), + beginQuery: ext.beginQueryEXT.bind(ext), + endQuery: ext.endQueryEXT.bind(ext), + queryCounter: ext.queryCounterEXT.bind(ext), + getQuery: ext.getQueryEXT.bind(ext), + getQueryParameter: ext.getQueryObjectEXT.bind(ext), + }; + } +} + // const TextureTestVertShader = ` diff --git a/src/mol-gl/webgl/context.ts b/src/mol-gl/webgl/context.ts index 0c02300a24a14fa2ad463f240744146444989111..c3cd962a4f42716371db39688dcd4c66cd46ac22 100644 --- a/src/mol-gl/webgl/context.ts +++ b/src/mol-gl/webgl/context.ts @@ -17,6 +17,7 @@ import { BehaviorSubject } from 'rxjs'; import { now } from '../../mol-util/now'; import { Texture, TextureFilter } from './texture'; import { ComputeRenderable } from '../renderable'; +import { createTimer, WebGLTimer } from './timer'; export function getGLContext(canvas: HTMLCanvasElement, attribs?: WebGLContextAttributes & { preferWebGl1?: boolean }): GLRenderingContext | null { function get(id: 'webgl' | 'experimental-webgl' | 'webgl2') { @@ -186,6 +187,7 @@ export interface WebGLContext { readonly state: WebGLState readonly stats: WebGLStats readonly resources: WebGLResources + readonly timer: WebGLTimer readonly maxTextureSize: number readonly max3dTextureSize: number @@ -221,6 +223,7 @@ export function createContext(gl: GLRenderingContext, props: Partial<{ pixelScal const state = createState(gl); const stats = createStats(); const resources = createResources(gl, state, stats, extensions); + const timer = createTimer(gl, extensions); const parameters = { maxTextureSize: gl.getParameter(gl.MAX_TEXTURE_SIZE) as number, @@ -289,6 +292,7 @@ export function createContext(gl: GLRenderingContext, props: Partial<{ pixelScal state, stats, resources, + timer, get maxTextureSize() { return parameters.maxTextureSize; }, get max3dTextureSize() { return parameters.max3dTextureSize; }, diff --git a/src/mol-gl/webgl/extensions.ts b/src/mol-gl/webgl/extensions.ts index 8c0949fcb83ac3f0272f83029baf4a7ea0480591..99cc66b4882219a7fbde4be99e752e6fb44c0893 100644 --- a/src/mol-gl/webgl/extensions.ts +++ b/src/mol-gl/webgl/extensions.ts @@ -1,10 +1,10 @@ /** - * Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Alexander Rose <alexander.rose@weirdbyte.de> */ -import { GLRenderingContext, COMPAT_instanced_arrays, COMPAT_standard_derivatives, COMPAT_vertex_array_object, getInstancedArrays, getStandardDerivatives, COMPAT_element_index_uint, getElementIndexUint, COMPAT_texture_float, getTextureFloat, COMPAT_texture_float_linear, getTextureFloatLinear, COMPAT_blend_minmax, getBlendMinMax, getFragDepth, COMPAT_frag_depth, COMPAT_color_buffer_float, getColorBufferFloat, COMPAT_draw_buffers, getDrawBuffers, getShaderTextureLod, COMPAT_shader_texture_lod, getDepthTexture, COMPAT_depth_texture, COMPAT_sRGB, getSRGB, getTextureHalfFloat, getTextureHalfFloatLinear, COMPAT_texture_half_float, COMPAT_texture_half_float_linear, COMPAT_color_buffer_half_float, getColorBufferHalfFloat, getVertexArrayObject } from './compat'; +import { GLRenderingContext, COMPAT_instanced_arrays, COMPAT_standard_derivatives, COMPAT_vertex_array_object, getInstancedArrays, getStandardDerivatives, COMPAT_element_index_uint, getElementIndexUint, COMPAT_texture_float, getTextureFloat, COMPAT_texture_float_linear, getTextureFloatLinear, COMPAT_blend_minmax, getBlendMinMax, getFragDepth, COMPAT_frag_depth, COMPAT_color_buffer_float, getColorBufferFloat, COMPAT_draw_buffers, getDrawBuffers, getShaderTextureLod, COMPAT_shader_texture_lod, getDepthTexture, COMPAT_depth_texture, COMPAT_sRGB, getSRGB, getTextureHalfFloat, getTextureHalfFloatLinear, COMPAT_texture_half_float, COMPAT_texture_half_float_linear, COMPAT_color_buffer_half_float, getColorBufferHalfFloat, getVertexArrayObject, getDisjointTimerQuery, COMPAT_disjoint_timer_query } from './compat'; import { isDebugMode } from '../../mol-util/debug'; export type WebGLExtensions = { @@ -25,6 +25,7 @@ export type WebGLExtensions = { drawBuffers: COMPAT_draw_buffers | null shaderTextureLod: COMPAT_shader_texture_lod | null sRGB: COMPAT_sRGB | null + disjointTimerQuery: COMPAT_disjoint_timer_query | null } export function createExtensions(gl: GLRenderingContext): WebGLExtensions { @@ -99,6 +100,10 @@ export function createExtensions(gl: GLRenderingContext): WebGLExtensions { if (isDebugMode && sRGB === null) { console.log('Could not find support for "sRGB"'); } + const disjointTimerQuery = getDisjointTimerQuery(gl); + if (isDebugMode && disjointTimerQuery === null) { + console.log('Could not find support for "disjoint_timer_query"'); + } return { instancedArrays, @@ -118,5 +123,6 @@ export function createExtensions(gl: GLRenderingContext): WebGLExtensions { drawBuffers, shaderTextureLod, sRGB, + disjointTimerQuery, }; } \ No newline at end of file diff --git a/src/mol-gl/webgl/timer.ts b/src/mol-gl/webgl/timer.ts new file mode 100644 index 0000000000000000000000000000000000000000..2479455bf5277a4021f3a2489999fb6088bc5f2f --- /dev/null +++ b/src/mol-gl/webgl/timer.ts @@ -0,0 +1,194 @@ +/** + * Copyright (c) 2022 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { GLRenderingContext } from './compat'; +import { WebGLExtensions } from './extensions'; + +export type TimerResult = { + readonly label: string + readonly timeElapsed: number + readonly children: TimerResult[] +} + +function getQuery(extensions: WebGLExtensions) { + return extensions.disjointTimerQuery ? extensions.disjointTimerQuery.createQuery() : null; +} + +export type WebGLTimer = { + /** Check with GPU for finished timers. */ + resolve: () => TimerResult[] + mark: (label: string) => void + markEnd: (label: string) => void + clear: () => void + destroy: () => void +} + +type Measure = { label: string, queries: WebGLQuery[], children: Measure[], root: boolean, timeElapsed?: number }; +type QueryResult = { timeElapsed?: number, refCount: number }; + +export function createTimer(gl: GLRenderingContext, extensions: WebGLExtensions): WebGLTimer { + const dtq = extensions.disjointTimerQuery; + + const queries = new Map<WebGLQuery, QueryResult>(); + const pending = new Map<string, Measure>(); + const stack: Measure[] = []; + + let measures: Measure[] = []; + let current: WebGLQuery | null = null; + + const clear = () => { + if (!dtq) return; + + queries.forEach((_, query) => { + dtq.deleteQuery(query); + }); + pending.clear(); + measures = []; + current = null; + }; + + const add = () => { + if (!dtq) return; + + const query = getQuery(extensions); + if (!query) return; + + dtq.beginQuery(dtq.TIME_ELAPSED, query); + pending.forEach((measure, _) => { + measure.queries.push(query); + }); + queries.set(query, { refCount: pending.size }); + current = query; + }; + + return { + resolve: () => { + const results: TimerResult[] = []; + if (!dtq || !measures.length) return results; + // console.log('resolve'); + queries.forEach((result, query) => { + if (result.timeElapsed !== undefined) return; + + const available = dtq.getQueryParameter(query, dtq.QUERY_RESULT_AVAILABLE); + const disjoint = gl.getParameter(dtq.GPU_DISJOINT); + + if (available && !disjoint) { + const timeElapsed = dtq.getQueryParameter(query, dtq.QUERY_RESULT) as number; + result.timeElapsed = timeElapsed; + // console.log('timeElapsed', result.timeElapsed); + } + + if (available || disjoint) { + dtq.deleteQuery(query); + } + }); + + const unresolved: Measure[] = []; + for (const measure of measures) { + if (measure.queries.every(q => queries.get(q)?.timeElapsed !== undefined)) { + let timeElapsed = 0; + for (const query of measure.queries) { + const result = queries.get(query)!; + timeElapsed += result.timeElapsed!; + result.refCount -= 1; + } + measure.timeElapsed = timeElapsed; + if (measure.root) { + const children: TimerResult[] = []; + const add = (measures: Measure[], children: TimerResult[]) => { + for (const measure of measures) { + const result: TimerResult = { + label: measure.label, + timeElapsed: measure.timeElapsed!, + children: [] + }; + children.push(result); + add(measure.children, result.children); + } + }; + add(measure.children, children); + results.push({ label: measure.label, timeElapsed, children }); + } + } else { + unresolved.push(measure); + } + } + measures = unresolved; + + queries.forEach((result, query) => { + if (result.refCount === 0) { + queries.delete(query); + } + }); + + return results; + }, + mark: (label: string) => { + if (!dtq) return; + + if (pending.has(label)) { + throw new Error(`Timer mark for '${label}' already exists`); + } + + if (current !== null) { + dtq.endQuery(dtq.TIME_ELAPSED); + } + const measure: Measure = { label, queries: [], children: [], root: current === null }; + pending.set(label, measure); + + if (stack.length) { + stack[stack.length - 1].children.push(measure); + } + stack.push(measure); + + add(); + }, + markEnd: (label: string) => { + if (!dtq) return; + + const measure = pending.get(label); + if (!measure) { + throw new Error(`Timer mark for '${label}' does not exist`); + } + + if (stack.pop()?.label !== label) { + throw new Error(`Timer mark for '${label}' has pending nested mark`); + } + + dtq.endQuery(dtq.TIME_ELAPSED); + pending.delete(label); + measures.push(measure); + + if (pending.size > 0) { + add(); + } else { + current = null; + } + }, + clear, + destroy: () => { + clear(); + } + }; +} + +function formatTimerResult(result: TimerResult) { + const timeElapsed = result.timeElapsed / 1000 / 1000; + return `${result.label} ${timeElapsed.toFixed(2)}ms`; +} + +export function printTimerResults(results: TimerResult[]) { + return results.map(r => { + const f = formatTimerResult(r); + if (r.children.length) { + console.groupCollapsed(f); + printTimerResults(r.children); + console.groupEnd(); + } else { + console.log(f); + } + }); +}