diff --git a/src/mol-canvas3d/canvas3d.ts b/src/mol-canvas3d/canvas3d.ts index b04242f6f634ca315874c280748c0a2b3c2e1f89..0b342296361ca794eb5fbc8dec65e3b5ebf3d2e7 100644 --- a/src/mol-canvas3d/canvas3d.ts +++ b/src/mol-canvas3d/canvas3d.ts @@ -85,6 +85,105 @@ export type PartialCanvas3DProps = { [K in keyof Canvas3DProps]?: Canvas3DProps[K] extends { name: string, params: any } ? Canvas3DProps[K] : Partial<Canvas3DProps[K]> } +export { Canvas3DContext }; + +/** Can be used to create multiple Canvas3D objects */ +interface Canvas3DContext { + readonly webgl: WebGLContext + readonly input: InputObserver + readonly passes: Passes + readonly attribs: Readonly<Canvas3DContext.Attribs> + readonly contextLost: BehaviorSubject<now.Timestamp> + readonly contextRestored: BehaviorSubject<now.Timestamp> + dispose: (options?: Partial<{ doNotForceWebGLContextLoss: boolean }>) => void +} + +namespace Canvas3DContext { + const DefaultAttribs = { + /** true by default to avoid issues with Safari (Jan 2021) */ + antialias: true, + /** true to support multiple Canvas3D objects with a single context */ + preserveDrawingBuffer: true, + pixelScale: 1, + pickScale: 0.25, + enableWboit: true + }; + export type Attribs = typeof DefaultAttribs + + export function fromCanvas(canvas: HTMLCanvasElement, attribs: Partial<Attribs> = {}): Canvas3DContext { + const a = { ...DefaultAttribs, ...attribs }; + const { antialias, preserveDrawingBuffer, pixelScale } = a; + const gl = getGLContext(canvas, { + antialias, + preserveDrawingBuffer, + alpha: true, // the renderer requires an alpha channel + depth: true, // the renderer requires a depth buffer + premultipliedAlpha: true, // the renderer outputs PMA + }); + if (gl === null) throw new Error('Could not create a WebGL rendering context'); + + const input = InputObserver.fromElement(canvas, { pixelScale }); + const webgl = createContext(gl, { pixelScale }); + const passes = new Passes(webgl, attribs); + + if (isDebugMode) { + const loseContextExt = gl.getExtension('WEBGL_lose_context'); + if (loseContextExt) { + /** Hold down shift+ctrl+alt and press any mouse button to trigger lose context */ + canvas.addEventListener('mousedown', e => { + if (webgl.isContextLost) return; + if (!e.shiftKey || !e.ctrlKey || !e.altKey) return; + + if (isDebugMode) console.log('lose context'); + loseContextExt.loseContext(); + + setTimeout(() => { + if (!webgl.isContextLost) return; + if (isDebugMode) console.log('restore context'); + loseContextExt.restoreContext(); + }, 1000); + }, false); + } + } + + // https://www.khronos.org/webgl/wiki/HandlingContextLost + + const contextLost = new BehaviorSubject<now.Timestamp>(0 as now.Timestamp); + + const handleWebglContextLost = (e: Event) => { + webgl.setContextLost(); + e.preventDefault(); + if (isDebugMode) console.log('context lost'); + contextLost.next(now()); + }; + + const handlewWebglContextRestored = () => { + if (!webgl.isContextLost) return; + webgl.handleContextRestored(); + if (isDebugMode) console.log('context restored'); + }; + + canvas.addEventListener('webglcontextlost', handleWebglContextLost, false); + canvas.addEventListener('webglcontextrestored', handlewWebglContextRestored, false); + + return { + webgl, + input, + passes, + attribs: a, + contextLost, + contextRestored: webgl.contextRestored, + dispose: (options?: Partial<{ doNotForceWebGLContextLoss: boolean }>) => { + input.dispose(); + + canvas.removeEventListener('webglcontextlost', handleWebglContextLost, false); + canvas.removeEventListener('webglcontextrestored', handlewWebglContextRestored, false); + webgl.destroy(options); + } + }; + } +} + export { Canvas3D }; interface Canvas3D { @@ -135,7 +234,7 @@ interface Canvas3D { readonly stats: RendererStats readonly interaction: Canvas3dInteractionHelper['events'] - dispose(options?: { doNotForceWebGLContextLoss?: boolean }): void + dispose(): void } const requestAnimationFrame = typeof window !== 'undefined' @@ -150,69 +249,7 @@ namespace Canvas3D { export interface DragEvent { current: Representation.Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys, pageStart: Vec2, pageEnd: Vec2 } export interface ClickEvent { current: Representation.Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys, page?: Vec2, position?: Vec3 } - - type Attribs = { - /** true by default to avoid issues with Safari (Jan 2021) */ - antialias: boolean, - /** true to support multiple viewports with a single context */ - preserveDrawingBuffer: boolean, - pixelScale: number, - pickScale: number, - enableWboit: boolean - } - - export function fromCanvas(canvas: HTMLCanvasElement, props: Partial<Canvas3DProps> = {}, attribs: Partial<Attribs> = {}) { - const gl = getGLContext(canvas, { - antialias: attribs.antialias ?? true, - preserveDrawingBuffer: attribs.preserveDrawingBuffer ?? true, - alpha: true, // the renderer requires an alpha channel - depth: true, // the renderer requires a depth buffer - premultipliedAlpha: true, // the renderer outputs PMA - }); - if (gl === null) throw new Error('Could not create a WebGL rendering context'); - - const { pixelScale } = attribs; - const input = InputObserver.fromElement(canvas, { pixelScale }); - const webgl = createContext(gl, { pixelScale }); - const passes = new Passes(webgl, attribs); - - if (isDebugMode) { - const loseContextExt = gl.getExtension('WEBGL_lose_context'); - if (loseContextExt) { - canvas.addEventListener('mousedown', e => { - if (webgl.isContextLost) return; - if (!e.shiftKey || !e.ctrlKey || !e.altKey) return; - - if (isDebugMode) console.log('lose context'); - loseContextExt.loseContext(); - - setTimeout(() => { - if (!webgl.isContextLost) return; - if (isDebugMode) console.log('restore context'); - loseContextExt.restoreContext(); - }, 1000); - }, false); - } - } - - // https://www.khronos.org/webgl/wiki/HandlingContextLost - - canvas.addEventListener('webglcontextlost', e => { - webgl.setContextLost(); - e.preventDefault(); - if (isDebugMode) console.log('context lost'); - }, false); - - canvas.addEventListener('webglcontextrestored', () => { - if (!webgl.isContextLost) return; - webgl.handleContextRestored(); - if (isDebugMode) console.log('context restored'); - }, false); - - return create(webgl, input, passes, props, { pixelScale }); - } - - export function create(webgl: WebGLContext, input: InputObserver, passes: Passes, props: Partial<Canvas3DProps> = {}, attribs: Partial<{ pixelScale: number }>): Canvas3D { + export function create({ webgl, input, passes, attribs }: Canvas3DContext, props: Partial<Canvas3DProps> = {}): Canvas3D { const p: Canvas3DProps = { ...DefaultCanvas3DParams, ...props }; const reprRenderObjects = new Map<Representation.Any, Set<GraphicsRenderObject>>(); @@ -705,17 +742,14 @@ namespace Canvas3D { get interaction() { return interactionHelper.events; }, - dispose: (options?: { doNotForceWebGLContextLoss?: boolean }) => { + dispose: () => { contextRestoredSub.unsubscribe(); scene.clear(); helper.debug.clear(); - input.dispose(); controls.dispose(); renderer.dispose(); interactionHelper.dispose(); - - if (!options?.doNotForceWebGLContextLoss) gl.getExtension('WEBGL_lose_context')?.loseContext(); } }; diff --git a/src/mol-gl/webgl/context.ts b/src/mol-gl/webgl/context.ts index fa6616a87501b7428308ee5f31aaf92309c61297..ce523b7dadcb8418fa677f30f0b6cdd42659f1f6 100644 --- a/src/mol-gl/webgl/context.ts +++ b/src/mol-gl/webgl/context.ts @@ -210,7 +210,7 @@ export interface WebGLContext { waitForGpuCommandsCompleteSync: () => void getDrawingBufferPixelData: () => PixelData clear: (red: number, green: number, blue: number, alpha: number) => void - destroy: () => void + destroy: (options?: Partial<{ doNotForceWebGLContextLoss: boolean }>) => void } export function createContext(gl: GLRenderingContext, props: Partial<{ pixelScale: number }> = {}): WebGLContext { @@ -232,7 +232,7 @@ export function createContext(gl: GLRenderingContext, props: Partial<{ pixelScal } let isContextLost = false; - let contextRestored = new BehaviorSubject<now.Timestamp>(0 as now.Timestamp); + const contextRestored = new BehaviorSubject<now.Timestamp>(0 as now.Timestamp); let readPixelsAsync: (x: number, y: number, width: number, height: number, buffer: Uint8Array) => Promise<void>; if (isWebGL2(gl)) { @@ -347,9 +347,12 @@ export function createContext(gl: GLRenderingContext, props: Partial<{ pixelScal gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); }, - destroy: () => { + destroy: (options?: Partial<{ doNotForceWebGLContextLoss: boolean }>) => { resources.destroy(); unbindResources(gl); + + // to aid GC + if (!options?.doNotForceWebGLContextLoss) gl.getExtension('WEBGL_lose_context')?.loseContext(); } }; } \ No newline at end of file diff --git a/src/mol-plugin/context.ts b/src/mol-plugin/context.ts index 1d785a6c95632383174f58d642ab4cb631e8c982..bb55468c39a72d2d6239669e46ceea83da5fbcdd 100644 --- a/src/mol-plugin/context.ts +++ b/src/mol-plugin/context.ts @@ -8,7 +8,7 @@ import produce, { setAutoFreeze } from 'immer'; import { List } from 'immutable'; import { merge } from 'rxjs'; -import { Canvas3D, DefaultCanvas3DParams } from '../mol-canvas3d/canvas3d'; +import { Canvas3D, Canvas3DContext, DefaultCanvas3DParams } from '../mol-canvas3d/canvas3d'; import { CustomProperty } from '../mol-model-props/common/custom-property'; import { Model, Structure } from '../mol-model/structure'; import { DataBuilder } from '../mol-plugin-state/builder/data'; @@ -104,6 +104,7 @@ export class PluginContext { } } as const; + readonly canvas3dContext: Canvas3DContext | undefined; readonly canvas3d: Canvas3D | undefined; readonly animationLoop = new PluginAnimationLoop(this); readonly layout = new PluginLayout(this); @@ -193,7 +194,8 @@ export class PluginContext { const pixelScale = this.config.get(PluginConfig.General.PixelScale) || 1; const pickScale = this.config.get(PluginConfig.General.PickScale) || 0.25; const enableWboit = this.config.get(PluginConfig.General.EnableWboit) || false; - (this.canvas3d as Canvas3D) = Canvas3D.fromCanvas(canvas, {}, { antialias, preserveDrawingBuffer, pixelScale, enableWboit, pickScale }); + (this.canvas3dContext as Canvas3DContext) = Canvas3DContext.fromCanvas(canvas, { antialias, preserveDrawingBuffer, pixelScale, pickScale, enableWboit }); + (this.canvas3d as Canvas3D) = Canvas3D.create(this.canvas3dContext!); this.canvas3dInit.next(true); let props = this.spec.components?.viewport?.canvas3d; @@ -259,7 +261,8 @@ export class PluginContext { dispose(options?: { doNotForceWebGLContextLoss?: boolean }) { if (this.disposed) return; this.commands.dispose(); - this.canvas3d?.dispose(options); + this.canvas3d?.dispose(); + this.canvas3dContext?.dispose(options); this.ev.dispose(); this.state.dispose(); this.managers.task.dispose(); diff --git a/src/tests/browser/marching-cubes.ts b/src/tests/browser/marching-cubes.ts index d442598e204ecd19bed41b1e9bd3c59f6ac79eda..741ad9fb494f578339f72bf7c2798a90e1c6345c 100644 --- a/src/tests/browser/marching-cubes.ts +++ b/src/tests/browser/marching-cubes.ts @@ -6,7 +6,7 @@ import './index.html'; import { resizeCanvas } from '../../mol-canvas3d/util'; -import { Canvas3DParams, Canvas3D } from '../../mol-canvas3d/canvas3d'; +import { Canvas3DParams, Canvas3D, Canvas3DContext } from '../../mol-canvas3d/canvas3d'; import { ColorNames } from '../../mol-util/color/names'; import { PositionData, Box3D, Sphere3D } from '../../mol-math/geometry'; import { OrderedSet } from '../../mol-data/int'; @@ -31,7 +31,7 @@ const canvas = document.createElement('canvas'); parent.appendChild(canvas); resizeCanvas(canvas, parent); -const canvas3d = Canvas3D.fromCanvas(canvas, PD.merge(Canvas3DParams, PD.getDefaultValues(Canvas3DParams), { +const canvas3d = Canvas3D.create(Canvas3DContext.fromCanvas(canvas), PD.merge(Canvas3DParams, PD.getDefaultValues(Canvas3DParams), { renderer: { backgroundColor: ColorNames.white }, camera: { mode: 'orthographic' } })); diff --git a/src/tests/browser/render-lines.ts b/src/tests/browser/render-lines.ts index 514a8b923d00cfb33c67defabbc4b1ef6645b48b..1c53f2cbf22c56b210fd9cd947183507920ae5d2 100644 --- a/src/tests/browser/render-lines.ts +++ b/src/tests/browser/render-lines.ts @@ -6,7 +6,7 @@ import './index.html'; import { resizeCanvas } from '../../mol-canvas3d/util'; -import { Canvas3D } from '../../mol-canvas3d/canvas3d'; +import { Canvas3D, Canvas3DContext } from '../../mol-canvas3d/canvas3d'; import { LinesBuilder } from '../../mol-geo/geometry/lines/lines-builder'; import { Mat4 } from '../../mol-math/linear-algebra'; import { DodecahedronCage } from '../../mol-geo/primitive/dodecahedron'; @@ -23,7 +23,7 @@ const canvas = document.createElement('canvas'); parent.appendChild(canvas); resizeCanvas(canvas, parent); -const canvas3d = Canvas3D.fromCanvas(canvas); +const canvas3d = Canvas3D.create(Canvas3DContext.fromCanvas(canvas)); canvas3d.animate(); function linesRepr() { diff --git a/src/tests/browser/render-mesh.ts b/src/tests/browser/render-mesh.ts index f7aa6bc296baf0cb485e0ab332e56bf450ce0fe3..55efcb0234b745cd3b291ef421b5389476fc597f 100644 --- a/src/tests/browser/render-mesh.ts +++ b/src/tests/browser/render-mesh.ts @@ -6,7 +6,7 @@ import './index.html'; import { resizeCanvas } from '../../mol-canvas3d/util'; -import { Canvas3D } from '../../mol-canvas3d/canvas3d'; +import { Canvas3D, Canvas3DContext } from '../../mol-canvas3d/canvas3d'; import { MeshBuilder } from '../../mol-geo/geometry/mesh/mesh-builder'; import { Mat4 } from '../../mol-math/linear-algebra'; import { HexagonalPrismCage } from '../../mol-geo/primitive/prism'; @@ -24,7 +24,7 @@ const canvas = document.createElement('canvas'); parent.appendChild(canvas); resizeCanvas(canvas, parent); -const canvas3d = Canvas3D.fromCanvas(canvas); +const canvas3d = Canvas3D.create(Canvas3DContext.fromCanvas(canvas)); canvas3d.animate(); function meshRepr() { diff --git a/src/tests/browser/render-shape.ts b/src/tests/browser/render-shape.ts index d342744560c3dfdf65567ce4f73703a175241866..80954b2dfb9fe3266d22105e69f523d0f2904299 100644 --- a/src/tests/browser/render-shape.ts +++ b/src/tests/browser/render-shape.ts @@ -7,7 +7,7 @@ import './index.html'; import { resizeCanvas } from '../../mol-canvas3d/util'; import { Representation } from '../../mol-repr/representation'; -import { Canvas3D } from '../../mol-canvas3d/canvas3d'; +import { Canvas3D, Canvas3DContext } from '../../mol-canvas3d/canvas3d'; import { lociLabel } from '../../mol-theme/label'; import { MarkerAction } from '../../mol-util/marker-action'; import { EveryLoci } from '../../mol-model/loci'; @@ -38,7 +38,7 @@ info.style.color = 'white'; parent.appendChild(info); let prevReprLoci = Representation.Loci.Empty; -const canvas3d = Canvas3D.fromCanvas(canvas); +const canvas3d = Canvas3D.create(Canvas3DContext.fromCanvas(canvas)); canvas3d.animate(); canvas3d.input.move.subscribe(({x, y}) => { const pickingId = canvas3d.identify(x, y)?.id; diff --git a/src/tests/browser/render-spheres.ts b/src/tests/browser/render-spheres.ts index 2862abfb23597b5ef9d61bcb83574e90db5bea53..fee81e444ef29c105390b638f56730abe1c69fdf 100644 --- a/src/tests/browser/render-spheres.ts +++ b/src/tests/browser/render-spheres.ts @@ -6,7 +6,7 @@ import './index.html'; import { resizeCanvas } from '../../mol-canvas3d/util'; -import { Canvas3D } from '../../mol-canvas3d/canvas3d'; +import { Canvas3D, Canvas3DContext } from '../../mol-canvas3d/canvas3d'; import { SpheresBuilder } from '../../mol-geo/geometry/spheres/spheres-builder'; import { Spheres } from '../../mol-geo/geometry/spheres/spheres'; import { Color } from '../../mol-util/color'; @@ -21,7 +21,7 @@ const canvas = document.createElement('canvas'); parent.appendChild(canvas); resizeCanvas(canvas, parent); -const canvas3d = Canvas3D.fromCanvas(canvas); +const canvas3d = Canvas3D.create(Canvas3DContext.fromCanvas(canvas)); canvas3d.animate(); function spheresRepr() { diff --git a/src/tests/browser/render-structure.ts b/src/tests/browser/render-structure.ts index 9bd232b080728411041cdce7f66fa96d0d31d224..1ec4c705b17f1a1ae95ce7d71706298094107250 100644 --- a/src/tests/browser/render-structure.ts +++ b/src/tests/browser/render-structure.ts @@ -5,7 +5,7 @@ */ import './index.html'; -import { Canvas3D } from '../../mol-canvas3d/canvas3d'; +import { Canvas3D, Canvas3DContext } from '../../mol-canvas3d/canvas3d'; import { CIF, CifFrame } from '../../mol-io/reader/cif'; import { Model, Structure } from '../../mol-model/structure'; import { ColorTheme } from '../../mol-theme/color'; @@ -37,7 +37,7 @@ const canvas = document.createElement('canvas'); parent.appendChild(canvas); resizeCanvas(canvas, parent); -const canvas3d = Canvas3D.fromCanvas(canvas); +const canvas3d = Canvas3D.create(Canvas3DContext.fromCanvas(canvas)); canvas3d.animate(); const info = document.createElement('div'); diff --git a/src/tests/browser/render-text.ts b/src/tests/browser/render-text.ts index d1c20a791ec8413779820897a9f9ebe3b5011285..36de4d2b5920471ab57f8c36b8f3a8ac53a859d0 100644 --- a/src/tests/browser/render-text.ts +++ b/src/tests/browser/render-text.ts @@ -5,7 +5,7 @@ */ import './index.html'; -import { Canvas3D } from '../../mol-canvas3d/canvas3d'; +import { Canvas3D, Canvas3DContext } from '../../mol-canvas3d/canvas3d'; import { TextBuilder } from '../../mol-geo/geometry/text/text-builder'; import { Text } from '../../mol-geo/geometry/text/text'; import { ParamDefinition as PD } from '../../mol-util/param-definition'; @@ -24,7 +24,7 @@ const canvas = document.createElement('canvas'); parent.appendChild(canvas); resizeCanvas(canvas, parent); -const canvas3d = Canvas3D.fromCanvas(canvas); +const canvas3d = Canvas3D.create(Canvas3DContext.fromCanvas(canvas)); canvas3d.animate(); function textRepr() {