diff --git a/src/apps/canvas/component/viewport.tsx b/src/apps/canvas/component/viewport.tsx
index 7814b26479102e741894b4662e75bc8134029770..3a5d2072cc396ede866ba8720111aaca9b0a5960 100644
--- a/src/apps/canvas/component/viewport.tsx
+++ b/src/apps/canvas/component/viewport.tsx
@@ -45,9 +45,9 @@ export class Viewport extends React.Component<ViewportProps, ViewportState> {
         viewer.input.resize.subscribe(() => this.handleResize())
 
         let prevLoci: Loci = EmptyLoci
-        viewer.input.move.subscribe(({x, y, inside, buttons}) => {
+        viewer.input.move.subscribe(async ({x, y, inside, buttons}) => {
             if (!inside || buttons) return
-            const p = viewer.identify(x, y)
+            const p = await viewer.identify(x, y)
             if (p) {
                 const loci = viewer.getLoci(p)
 
diff --git a/src/mol-canvas3d/viewer.ts b/src/mol-canvas3d/viewer.ts
index aab76b56f79e2f5444ee06aea0bbf388e141ecb8..788a2a0bd5dfc403fc80060621f36c848666984b 100644
--- a/src/mol-canvas3d/viewer.ts
+++ b/src/mol-canvas3d/viewer.ts
@@ -43,7 +43,7 @@ interface Viewer {
     requestDraw: (force?: boolean) => void
     animate: () => void
     pick: () => void
-    identify: (x: number, y: number) => PickingId | undefined
+    identify: (x: number, y: number) => Promise<PickingId | undefined>
     mark: (loci: Loci, action: MarkerAction) => void
     getLoci: (pickingId: PickingId) => Loci
 
@@ -216,7 +216,7 @@ namespace Viewer {
             pickDirty = false
         }
 
-        function identify(x: number, y: number): PickingId | undefined {
+        async function identify(x: number, y: number): Promise<PickingId | undefined> {
             if (pickDirty) return undefined
 
             isPicking = true
@@ -230,15 +230,15 @@ namespace Viewer {
             const yp = Math.round(y * pickScale)
 
             objectPickTarget.bind()
-            ctx.readPixels(xp, yp, 1, 1, buffer)
+            await ctx.readPixelsAsync(xp, yp, 1, 1, buffer)
             const objectId = decodeIdRGB(buffer[0], buffer[1], buffer[2])
 
             instancePickTarget.bind()
-            ctx.readPixels(xp, yp, 1, 1, buffer)
+            await ctx.readPixels(xp, yp, 1, 1, buffer)
             const instanceId = decodeIdRGB(buffer[0], buffer[1], buffer[2])
 
             groupPickTarget.bind()
-            ctx.readPixels(xp, yp, 1, 1, buffer)
+            await ctx.readPixels(xp, yp, 1, 1, buffer)
             const groupId = decodeIdRGB(buffer[0], buffer[1], buffer[2])
 
             isPicking = false
diff --git a/src/mol-gl/webgl/context.ts b/src/mol-gl/webgl/context.ts
index f811766691b590d518df73ac4c2a494bdd0c2e14..a94f6e5831bf9811c94a8e7c3731329bcab053ec 100644
--- a/src/mol-gl/webgl/context.ts
+++ b/src/mol-gl/webgl/context.ts
@@ -7,6 +7,7 @@
 import { createProgramCache, ProgramCache } from './program'
 import { createShaderCache, ShaderCache } from './shader'
 import { GLRenderingContext, COMPAT_instanced_arrays, COMPAT_standard_derivatives, COMPAT_vertex_array_object, getInstancedArrays, getStandardDerivatives, getVertexArrayObject, isWebGL2, COMPAT_element_index_uint, getElementIndexUint, COMPAT_texture_float, getTextureFloat, COMPAT_texture_float_linear, getTextureFloatLinear, COMPAT_blend_minmax, getBlendMinMax, getFragDepth, COMPAT_frag_depth } from './compat';
+import { createFramebufferCache, FramebufferCache } from './framebuffer';
 
 export function getGLContext(canvas: HTMLCanvasElement, contextAttributes?: WebGLContextAttributes): GLRenderingContext | null {
     function getContext(contextId: 'webgl' | 'experimental-webgl' | 'webgl2') {
@@ -122,6 +123,7 @@ export interface Context {
 
     readonly shaderCache: ShaderCache
     readonly programCache: ProgramCache
+    readonly framebufferCache: FramebufferCache
 
     bufferCount: number
     framebufferCount: number
@@ -179,6 +181,7 @@ export function createContext(gl: GLRenderingContext): Context {
 
     const shaderCache = createShaderCache()
     const programCache = createProgramCache()
+    const framebufferCache = createFramebufferCache()
 
     const parameters = {
         maxTextureSize: gl.getParameter(gl.MAX_TEXTURE_SIZE),
@@ -197,8 +200,12 @@ export function createContext(gl: GLRenderingContext): Context {
             gl.bindBuffer(gl.PIXEL_PACK_BUFFER, pbo)
             gl.bufferData(gl.PIXEL_PACK_BUFFER, width * height * 4, gl.STATIC_COPY)
             gl.readPixels(x, y, width, height, gl.RGBA, gl.UNSIGNED_BYTE, 0)
+            gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null)
+            // need to unbind/bind PBO before/after async awaiting the fence
             await fence(gl)
-            gl.getBufferSubData(gl.PIXEL_PACK_BUFFER, 0, buffer);
+            gl.bindBuffer(gl.PIXEL_PACK_BUFFER, pbo)
+            gl.getBufferSubData(gl.PIXEL_PACK_BUFFER, 0, buffer)
+            gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null)
         }
     } else {
         readPixelsAsync = async (x: number, y: number, width: number, height: number, buffer: Uint8Array) => {
@@ -223,6 +230,7 @@ export function createContext(gl: GLRenderingContext): Context {
 
         shaderCache,
         programCache,
+        framebufferCache,
 
         bufferCount: 0,
         framebufferCount: 0,
@@ -254,6 +262,7 @@ export function createContext(gl: GLRenderingContext): Context {
             unbindResources(gl)
             programCache.dispose()
             shaderCache.dispose()
+            framebufferCache.dispose()
             // TODO destroy buffers and textures
         }
     }
diff --git a/src/mol-gl/webgl/framebuffer.ts b/src/mol-gl/webgl/framebuffer.ts
index f359c1ed64d15b238e3da589ad2ca6131c8cbe0e..77eeee74a07a52e16f8723109c259b6efbd84b46 100644
--- a/src/mol-gl/webgl/framebuffer.ts
+++ b/src/mol-gl/webgl/framebuffer.ts
@@ -6,6 +6,7 @@
 
 import { Context } from './context'
 import { idFactory } from 'mol-util/id-factory';
+import { ReferenceCache, createReferenceCache } from 'mol-util/reference-cache';
 
 const getNextFramebufferId = idFactory()
 
@@ -37,4 +38,14 @@ export function createFramebuffer (ctx: Context): Framebuffer {
             ctx.framebufferCount -= 1
         }
     }
+}
+
+export type FramebufferCache = ReferenceCache<Framebuffer, string, Context>
+
+export function createFramebufferCache(): FramebufferCache {
+    return createReferenceCache(
+        (name: string) => name,
+        (ctx: Context) => createFramebuffer(ctx),
+        (framebuffer: Framebuffer) => { framebuffer.destroy() }
+    )
 }
\ No newline at end of file
diff --git a/src/mol-math/geometry/gaussian-density/gpu.ts b/src/mol-math/geometry/gaussian-density/gpu.ts
index 34a1f966166a542f91ab944c0d2b49f4340112b9..dffd04cad16e296ee742095aab2e298c08eed766 100644
--- a/src/mol-math/geometry/gaussian-density/gpu.ts
+++ b/src/mol-math/geometry/gaussian-density/gpu.ts
@@ -16,11 +16,13 @@ import { ValueCell, defaults } from 'mol-util'
 import { RenderableState, Renderable } from 'mol-gl/renderable'
 import { createRenderable, createGaussianDensityRenderObject } from 'mol-gl/render-object'
 import { Context, createContext, getGLContext } from 'mol-gl/webgl/context';
-import { createFramebuffer } from 'mol-gl/webgl/framebuffer';
 import { createTexture, Texture } from 'mol-gl/webgl/texture';
 import { GLRenderingContext, isWebGL2 } from 'mol-gl/webgl/compat';
 import { decodeIdRGB } from 'mol-geo/geometry/picking';
 
+/** name for shared framebuffer used for gpu gaussian surface operations */
+const FramebufferName = 'gaussian-density-gpu'
+
 export async function GaussianDensityGPU(ctx: RuntimeContext, position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityProps): Promise<DensityData> {
     const webgl = defaults(props.webgl, getWebGLContext())
     // always use texture2d when the gaussian density needs to be downloaded from the GPU,
@@ -68,10 +70,10 @@ async function GaussianDensityTexture2d(ctx: RuntimeContext, webgl: Context, pos
 
     //
 
-    const { gl } = webgl
+    const { gl, framebufferCache } = webgl
     const { uCurrentSlice, uCurrentX, uCurrentY } = renderObject.values
 
-    const framebuffer = createFramebuffer(webgl)
+    const framebuffer = framebufferCache.get(webgl, FramebufferName).value
     framebuffer.bind()
     setRenderingDefaults(gl)
 
@@ -108,8 +110,6 @@ async function GaussianDensityTexture2d(ctx: RuntimeContext, webgl: Context, pos
     setupGroupIdRendering(webgl, renderable)
     render(texture)
 
-    framebuffer.destroy() // clean up
-
     await ctx.update({ message: 'gpu gaussian density calculation' });
     await webgl.waitForGpuCommandsComplete()
 
@@ -129,10 +129,10 @@ async function GaussianDensityTexture3d(ctx: RuntimeContext, webgl: Context, pos
 
     //
 
-    const { gl } = webgl
+    const { gl, framebufferCache } = webgl
     const { uCurrentSlice } = renderObject.values
 
-    const framebuffer = createFramebuffer(webgl)
+    const framebuffer = framebufferCache.get(webgl, FramebufferName).value
     framebuffer.bind()
     setRenderingDefaults(gl)
     gl.viewport(0, 0, dx, dy)
@@ -157,8 +157,6 @@ async function GaussianDensityTexture3d(ctx: RuntimeContext, webgl: Context, pos
     setupGroupIdRendering(webgl, renderable)
     render(texture)
 
-    framebuffer.destroy() // clean up
-
     await ctx.update({ message: 'gpu gaussian density calculation' });
     await webgl.waitForGpuCommandsComplete()
 
@@ -310,27 +308,10 @@ function getTexture2dSize(maxTexSize: number, gridDim: Vec3) {
     return { texDimX, texDimY, texRows, texCols }
 }
 
-  
-//   function pick_nonblocking_getBufferSubData() {
-//     gl.readPixels(mouse.x, pickingTexture.height - mouse.y, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, 0);
-  
-//     fence().then(function() {
-//       stats1.begin();
-//       gl.getBufferSubData(gl.PIXEL_PACK_BUFFER, 0, readbackBuffer);
-//       stats1.end();
-//       gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null);
-  
-//       var id = (readbackBuffer[0] << 16) | (readbackBuffer[1] << 8) | (readbackBuffer[2]);
-//       render(id);
-//       gl.finish();
-//       stats2.end();
-//     });
-//   }
-
 async function fieldFromTexture2d(ctx: Context, texture: Texture, dim: Vec3) {
     console.log('isWebGL2', isWebGL2(ctx.gl))
     console.time('fieldFromTexture2d')
-    const { gl } = ctx
+    const { framebufferCache } = ctx
     const [ dx, dy, dz ] = dim
     const { width, height } = texture
     const fboTexCols = Math.floor(width / dx)
@@ -343,25 +324,10 @@ async function fieldFromTexture2d(ctx: Context, texture: Texture, dim: Vec3) {
 
     const image = new Uint8Array(width * height * 4)
 
-    const framebuffer = createFramebuffer(ctx)
+    const framebuffer = framebufferCache.get(ctx, FramebufferName).value
     framebuffer.bind()
-    texture.attachFramebuffer(framebuffer, 0)
-    
-    if (isWebGL2(gl)) {
-        const pbo = gl.createBuffer()
-        gl.bindBuffer(gl.PIXEL_PACK_BUFFER, pbo)
-        gl.bufferData(gl.PIXEL_PACK_BUFFER, width * height * 4, gl.STATIC_COPY)
-        gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, 0)
-        await ctx.waitForGpuCommandsComplete()
-        gl.getBufferSubData(gl.PIXEL_PACK_BUFFER, 0, image);
-        gl.deleteBuffer(pbo)
-    } else {
-        gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, image)
-    }
-    // gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, image)
-
-    framebuffer.destroy()
-    gl.finish()
+    texture.attachFramebuffer(framebuffer, 0)   
+    await ctx.readPixelsAsync(0, 0, width, height, image)
 
     let j = 0
     let tmpCol = 0
@@ -385,40 +351,4 @@ async function fieldFromTexture2d(ctx: Context, texture: Texture, dim: Vec3) {
     console.timeEnd('fieldFromTexture2d')
 
     return { field, idField }
-}
-
-// function fieldFromTexture3d(ctx: Context, texture: Texture, dim: Vec3) {
-//     console.time('fieldFromTexture3d')
-//     const { gl } = ctx
-//     const { width, height, depth } = texture
-
-//     const space = Tensor.Space(dim, [2, 1, 0], Float32Array)
-//     const data = space.create()
-//     const field = Tensor.create(space, data)
-//     const idData = space.create()
-//     const idField = Tensor.create(space, idData)
-
-//     const slice = new Uint8Array(width * height * 4)
-
-//     const framebuffer = createFramebuffer(ctx)
-//     framebuffer.bind()
-
-//     let j = 0
-//     for (let i = 0; i < depth; ++i) {
-//         texture.attachFramebuffer(framebuffer, 0, i)
-//         gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, slice)
-//         for (let iy = 0; iy < height; ++iy) {
-//             for (let ix = 0; ix < width; ++ix) {
-//                 const idx = 4 * (iy * width + ix)
-//                 data[j] = slice[idx + 3] / 255
-//                 idData[j] = decodeIdRGB(slice[idx], slice[idx + 1], slice[idx + 2])
-//                 ++j
-//             }
-//         }
-//     }
-
-//     framebuffer.destroy()
-//     console.timeEnd('fieldFromTexture3d')
-
-//     return { field, idField }
-// }
\ No newline at end of file
+}
\ No newline at end of file