From 991d2e3a57d4dce6facfbaa444f968fc46340881 Mon Sep 17 00:00:00 2001 From: Alexander Rose <alexander.rose@weirdbyte.de> Date: Fri, 19 Jul 2019 09:46:27 -0700 Subject: [PATCH] async gl repr object handling --- src/mol-canvas3d/canvas3d.ts | 43 +++++++-- src/mol-gl/scene.ts | 95 +++++++++++++++---- .../behavior/static/representation.ts | 11 ++- src/mol-plugin/context.ts | 5 +- src/mol-util/async-queue.ts | 2 + 5 files changed, 120 insertions(+), 36 deletions(-) diff --git a/src/mol-canvas3d/canvas3d.ts b/src/mol-canvas3d/canvas3d.ts index f674f3c49..8805de61b 100644 --- a/src/mol-canvas3d/canvas3d.ts +++ b/src/mol-canvas3d/canvas3d.ts @@ -31,6 +31,7 @@ import { PixelData } from '../mol-util/image'; import { readTexture } from '../mol-gl/compute/util'; import { DrawPass } from './passes/draw'; import { PickPass } from './passes/pick'; +import { Task } from '../mol-task'; export const Canvas3DParams = { // TODO: FPS cap? @@ -66,6 +67,7 @@ interface Canvas3D { getLoci: (pickingId: PickingId) => Representation.Loci readonly didDraw: BehaviorSubject<now.Timestamp> + readonly reprCount: BehaviorSubject<number> handleResize: () => void /** Focuses camera on scene's bounding sphere, centered and zoomed. */ @@ -85,12 +87,13 @@ interface Canvas3D { } const requestAnimationFrame = typeof window !== 'undefined' ? window.requestAnimationFrame : (f: (time: number) => void) => setImmediate(()=>f(Date.now())) +const DefaultRunTask = (task: Task<unknown>) => task.run() namespace Canvas3D { export interface HighlightEvent { current: Representation.Loci, modifiers?: ModifiersKeys } export interface ClickEvent { current: Representation.Loci, buttons: ButtonsType, modifiers: ModifiersKeys } - export function fromCanvas(canvas: HTMLCanvasElement, props: Partial<Canvas3DProps> = {}) { + export function fromCanvas(canvas: HTMLCanvasElement, props: Partial<Canvas3DProps> = {}, runTask = DefaultRunTask) { const gl = getGLContext(canvas, { alpha: false, antialias: true, @@ -99,10 +102,10 @@ namespace Canvas3D { }) if (gl === null) throw new Error('Could not create a WebGL rendering context') const input = InputObserver.fromElement(canvas) - return Canvas3D.create(gl, input, props) + return Canvas3D.create(gl, input, props, runTask) } - export function create(gl: GLRenderingContext, input: InputObserver, props: Partial<Canvas3DProps> = {}): Canvas3D { + export function create(gl: GLRenderingContext, input: InputObserver, props: Partial<Canvas3DProps> = {}, runTask = DefaultRunTask): Canvas3D { const p = { ...PD.getDefaultValues(Canvas3DParams), ...props } const reprRenderObjects = new Map<Representation.Any, Set<GraphicsRenderObject>>() @@ -137,6 +140,7 @@ namespace Canvas3D { let isUpdating = false let drawPending = false + let cameraResetRequested = false function getLoci(pickingId: PickingId) { let loci: Loci = EmptyLoci @@ -201,7 +205,7 @@ namespace Canvas3D { } function render(variant: 'pick' | 'draw', force: boolean) { - if (isUpdating) return false + if (isUpdating || scene.isCommiting) return false let didRender = false controls.update(currentTime); @@ -279,8 +283,15 @@ namespace Canvas3D { scene.update(repr.renderObjects, false) if (debugHelper.isEnabled) debugHelper.update() isUpdating = false - requestDraw(true) - reprCount.next(reprRenderObjects.size) + + runTask(scene.commit()).then(() => { + if (cameraResetRequested && !scene.isCommiting) { + camera.focus(scene.boundingSphere.center, scene.boundingSphere.radius) + cameraResetRequested = false + } + requestDraw(true) + reprCount.next(reprRenderObjects.size) + }) } handleResize() @@ -307,8 +318,15 @@ namespace Canvas3D { scene.update(void 0, false) if (debugHelper.isEnabled) debugHelper.update() isUpdating = false - requestDraw(true) - reprCount.next(reprRenderObjects.size) + + runTask(scene.commit()).then(() => { + if (cameraResetRequested && !scene.isCommiting) { + camera.focus(scene.boundingSphere.center, scene.boundingSphere.radius) + cameraResetRequested = false + } + requestDraw(true) + reprCount.next(reprRenderObjects.size) + }) } }, update: (repr, keepSphere) => { @@ -334,8 +352,12 @@ namespace Canvas3D { handleResize, resetCamera: () => { - camera.focus(scene.boundingSphere.center, scene.boundingSphere.radius) - requestDraw(true); + if (scene.isCommiting) { + cameraResetRequested = true + } else { + camera.focus(scene.boundingSphere.center, scene.boundingSphere.radius) + requestDraw(true); + } }, camera, downloadScreenshot: () => { @@ -351,6 +373,7 @@ namespace Canvas3D { } }, didDraw, + reprCount, setProps: (props: Partial<Canvas3DProps>) => { if (props.cameraMode !== undefined && props.cameraMode !== camera.state.mode) { camera.setState({ mode: props.cameraMode }) diff --git a/src/mol-gl/scene.ts b/src/mol-gl/scene.ts index 347cdb5d1..69db1673e 100644 --- a/src/mol-gl/scene.ts +++ b/src/mol-gl/scene.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Alexander Rose <alexander.rose@weirdbyte.de> */ @@ -12,6 +12,8 @@ import { Object3D } from './object3d'; import { Sphere3D } from '../mol-math/geometry'; import { Vec3 } from '../mol-math/linear-algebra'; import { BoundaryHelper } from '../mol-math/geometry/boundary-helper'; +import { RuntimeContext, Task } from '../mol-task'; +import { AsyncQueue } from '../mol-util/async-queue'; const boundaryHelper = new BoundaryHelper(); function calculateBoundingSphere(renderables: Renderable<RenderableValues & BaseValues>[], boundingSphere: Sphere3D): Sphere3D { @@ -54,10 +56,12 @@ interface Scene extends Object3D { readonly count: number readonly renderables: ReadonlyArray<Renderable<RenderableValues & BaseValues>> readonly boundingSphere: Sphere3D + readonly isCommiting: boolean update: (objects: ArrayLike<GraphicsRenderObject> | undefined, keepBoundingSphere: boolean) => void - add: (o: GraphicsRenderObject) => Renderable<any> + add: (o: GraphicsRenderObject) => void // Renderable<any> remove: (o: GraphicsRenderObject) => void + commit: () => Task<void> has: (o: GraphicsRenderObject) => boolean clear: () => void forEach: (callbackFn: (value: Renderable<RenderableValues & BaseValues>, key: GraphicsRenderObject) => void) => void @@ -68,15 +72,63 @@ namespace Scene { const renderableMap = new Map<GraphicsRenderObject, Renderable<RenderableValues & BaseValues>>() const renderables: Renderable<RenderableValues & BaseValues>[] = [] const boundingSphere = Sphere3D.zero() + let boundingSphereDirty = true const object3d = Object3D.create() + const add = (o: GraphicsRenderObject) => { + if (!renderableMap.has(o)) { + const renderable = createRenderable(ctx, o) + renderables.push(renderable) + renderableMap.set(o, renderable) + boundingSphereDirty = true + return renderable; + } else { + console.warn(`RenderObject with id '${o.id}' already present`) + return renderableMap.get(o)! + } + } + + const remove = (o: GraphicsRenderObject) => { + const renderable = renderableMap.get(o) + if (renderable) { + renderable.dispose() + renderables.splice(renderables.indexOf(renderable), 1) + renderableMap.delete(o) + boundingSphereDirty = true + } + } + + const commitQueue = new AsyncQueue<any>(); + const toAdd: GraphicsRenderObject[] = [] + const toRemove: GraphicsRenderObject[] = [] + + type CommitParams = { toAdd: GraphicsRenderObject[], toRemove: GraphicsRenderObject[] } + + const step = 100 + const handle = async (ctx: RuntimeContext, arr: GraphicsRenderObject[], fn: (o: GraphicsRenderObject) => void, message: string) => { + for (let i = 0, il = arr.length; i < il; i += step) { + if (ctx.shouldUpdate) await ctx.update({ message, current: i, max: il }) + for (let j = i, jl = Math.min(i + step, il); j < jl; ++j) { + fn(arr[j]) + } + } + } + + const commit = async (ctx: RuntimeContext, p: CommitParams) => { + await handle(ctx, p.toRemove, remove, 'Removing GraphicsRenderObjects') + await handle(ctx, p.toAdd, add, 'Adding GraphicsRenderObjects') + if (ctx.shouldUpdate) await ctx.update({ message: 'Sorting GraphicsRenderObjects' }) + renderables.sort(renderableSort) + } + return { get view () { return object3d.view }, get position () { return object3d.position }, get direction () { return object3d.direction }, get up () { return object3d.up }, + get isCommiting () { return commitQueue.length > 0 }, update(objects, keepBoundingSphere) { Object3D.update(object3d) @@ -94,27 +146,28 @@ namespace Scene { if (!keepBoundingSphere) boundingSphereDirty = true }, add: (o: GraphicsRenderObject) => { - if (!renderableMap.has(o)) { - const renderable = createRenderable(ctx, o) - renderables.push(renderable) - renderables.sort(renderableSort) - renderableMap.set(o, renderable) - boundingSphereDirty = true - return renderable; - } else { - console.warn(`RenderObject with id '${o.id}' already present`) - return renderableMap.get(o)! - } + toAdd.push(o) }, remove: (o: GraphicsRenderObject) => { - const renderable = renderableMap.get(o) - if (renderable) { - renderable.dispose() - renderables.splice(renderables.indexOf(renderable), 1) - renderables.sort(renderableSort) - renderableMap.delete(o) - boundingSphereDirty = true - } + toRemove.push(o) + }, + commit: () => { + const params = { toAdd: [ ...toAdd ], toRemove: [ ...toRemove ] } + toAdd.length = 0 + toRemove.length = 0 + + return Task.create('Commiting GraphicsRenderObjects', async ctx => { + const removed = await commitQueue.enqueue(params); + if (!removed) return; + + try { + await commit(ctx, params); + } finally { + commitQueue.handled(params); + } + }, () => { + commitQueue.remove(params); + }) }, has: (o: GraphicsRenderObject) => { return renderableMap.has(o) diff --git a/src/mol-plugin/behavior/static/representation.ts b/src/mol-plugin/behavior/static/representation.ts index 0a608a768..2deaca9ae 100644 --- a/src/mol-plugin/behavior/static/representation.ts +++ b/src/mol-plugin/behavior/static/representation.ts @@ -18,15 +18,19 @@ export function registerDefault(ctx: PluginContext) { export function SyncRepresentationToCanvas(ctx: PluginContext) { let reprCount = 0; + ctx.events.canvas3d.initialized.subscribe(() => { + ctx.canvas3d.reprCount.subscribe(v => { + if (reprCount === 0) ctx.canvas3d.resetCamera(); + reprCount = v; + }); + }) + const events = ctx.state.dataState.events; events.object.created.subscribe(e => { if (!SO.isRepresentation3D(e.obj)) return; updateVisibility(e.state.cells.get(e.ref)!, e.obj.data.repr); e.obj.data.repr.setState({ syncManually: true }); ctx.canvas3d.add(e.obj.data.repr); - - if (reprCount === 0) ctx.canvas3d.resetCamera(); - reprCount++; }); events.object.updated.subscribe(e => { if (e.oldObj && SO.isRepresentation3D(e.oldObj)) { @@ -50,7 +54,6 @@ export function SyncRepresentationToCanvas(ctx: PluginContext) { ctx.canvas3d.remove(e.obj.data.repr); ctx.canvas3d.requestDraw(true); e.obj.data.repr.destroy(); - reprCount--; }); } diff --git a/src/mol-plugin/context.ts b/src/mol-plugin/context.ts index a11fe2ba1..54cc723a5 100644 --- a/src/mol-plugin/context.ts +++ b/src/mol-plugin/context.ts @@ -73,6 +73,7 @@ export class PluginContext { log: this.ev<LogEntry>(), task: this.tasks.events, canvas3d: { + initialized: this.ev(), settingsUpdated: this.ev() }, interactivity: { @@ -127,7 +128,9 @@ export class PluginContext { try { this.layout.setRoot(container); if (this.spec.layout && this.spec.layout.initial) this.layout.setProps(this.spec.layout.initial); - (this.canvas3d as Canvas3D) = Canvas3D.fromCanvas(canvas); + (this.canvas3d as Canvas3D) = Canvas3D.fromCanvas(canvas, {}, t => this.runTask(t)); + this.events.canvas3d.initialized.next() + this.events.canvas3d.initialized.isStopped = true // TODO is this a good way? const renderer = this.canvas3d.props.renderer; PluginCommands.Canvas3D.SetSettings.dispatch(this, { settings: { renderer: { ...renderer, backgroundColor: Color(0xFCFBF9) } } }); this.canvas3d.animate(); diff --git a/src/mol-util/async-queue.ts b/src/mol-util/async-queue.ts index 5e01ff69f..b11407251 100644 --- a/src/mol-util/async-queue.ts +++ b/src/mol-util/async-queue.ts @@ -11,6 +11,8 @@ export class AsyncQueue<T> { private queue: T[] = []; private signal = new Subject<{ v: T, stillPresent: boolean }>(); + get length() { return this.queue.length } + enqueue(v: T) { this.queue.push(v); if (this.queue.length === 1) return true; -- GitLab