Browse Source

reduce number of webgl state changes

- add viewport and scissor to state object
- add hasOpaque to scene object
Alexander Rose 2 years ago
parent
commit
ae9e04b8d4

+ 3 - 0
CHANGELOG.md

@@ -18,6 +18,9 @@ Note that since we don't clearly distinguish between a public and private interf
 - Don't filter IndexPairBonds by element-based rules in MOL/SDF and MOL2 (without symmetry) models
 - Fix Glycam Saccharide Names used by default
 - Prefer WebGL1 for more Safari versions to avoid broken GPU surfaces rendering
+- Reduce number of webgl state changes
+    - Add ``viewport`` and ``scissor`` to state object
+    - Add ``hasOpaque`` to scene object
 - Handle edge cases where some renderables would not get (correctly) rendered
     - Fix text background rendering for opaque text
     - Fix helper scenes not shown when rendering directly to draw target

+ 9 - 7
src/mol-canvas3d/passes/draw.ts

@@ -120,14 +120,13 @@ export class DrawPass {
     private _renderWboit(renderer: Renderer, camera: ICamera, scene: Scene, transparentBackground: boolean, postprocessingProps: PostprocessingProps) {
         if (!this.wboit?.supported) throw new Error('expected wboit to be supported');
 
-        this.colorTarget.bind();
+        this.depthTextureOpaque.attachFramebuffer(this.colorTarget.framebuffer, 'depth');
         renderer.clear(true);
 
         // render opaque primitives
-        this.depthTextureOpaque.attachFramebuffer(this.colorTarget.framebuffer, 'depth');
-        this.colorTarget.bind();
-        renderer.clearDepth();
-        renderer.renderWboitOpaque(scene.primitives, camera, null);
+        if (scene.hasOpaque) {
+            renderer.renderWboitOpaque(scene.primitives, camera, null);
+        }
 
         if (PostprocessingPass.isEnabled(postprocessingProps)) {
             if (PostprocessingPass.isOutlineEnabled(postprocessingProps)) {
@@ -165,14 +164,17 @@ export class DrawPass {
         if (toDrawingBuffer) {
             this.drawTarget.bind();
         } else {
-            this.colorTarget.bind();
             if (!this.packedDepth) {
                 this.depthTextureOpaque.attachFramebuffer(this.colorTarget.framebuffer, 'depth');
+            } else {
+                this.colorTarget.bind();
             }
         }
 
         renderer.clear(true);
-        renderer.renderBlendedOpaque(scene.primitives, camera, null);
+        if (scene.hasOpaque) {
+            renderer.renderBlendedOpaque(scene.primitives, camera, null);
+        }
 
         if (!toDrawingBuffer) {
             // do a depth pass if not rendering to drawing buffer and

+ 2 - 2
src/mol-canvas3d/passes/fxaa.ts

@@ -44,8 +44,8 @@ export class FxaaPass {
         state.depthMask(false);
 
         const { x, y, width, height } = viewport;
-        gl.viewport(x, y, width, height);
-        gl.scissor(x, y, width, height);
+        state.viewport(x, y, width, height);
+        state.scissor(x, y, width, height);
 
         state.clearColor(0, 0, 0, 1);
         gl.clear(gl.COLOR_BUFFER_BIT);

+ 4 - 4
src/mol-canvas3d/passes/marking.ts

@@ -64,8 +64,8 @@ export class MarkingPass {
         state.depthMask(false);
 
         const { x, y, width, height } = viewport;
-        gl.viewport(x, y, width, height);
-        gl.scissor(x, y, width, height);
+        state.viewport(x, y, width, height);
+        state.scissor(x, y, width, height);
 
         state.clearColor(0, 0, 0, 0);
         gl.clear(gl.COLOR_BUFFER_BIT);
@@ -82,8 +82,8 @@ export class MarkingPass {
         state.depthMask(false);
 
         const { x, y, width, height } = viewport;
-        gl.viewport(x, y, width, height);
-        gl.scissor(x, y, width, height);
+        state.viewport(x, y, width, height);
+        state.scissor(x, y, width, height);
     }
 
     setSize(width: number, height: number) {

+ 10 - 10
src/mol-canvas3d/passes/multi-sample.ts

@@ -176,8 +176,8 @@ export class MultiSamplePass {
             state.blendFuncSeparate(gl.ONE, gl.ONE, gl.ONE, gl.ONE);
             state.disable(gl.DEPTH_TEST);
             state.depthMask(false);
-            gl.viewport(x, y, width, height);
-            gl.scissor(x, y, width, height);
+            state.viewport(x, y, width, height);
+            state.scissor(x, y, width, height);
             if (i === 0) {
                 state.clearColor(0, 0, 0, 0);
                 gl.clear(gl.COLOR_BUFFER_BIT);
@@ -192,8 +192,8 @@ export class MultiSamplePass {
         compose.update();
 
         this.bindOutputTarget(toDrawingBuffer);
-        gl.viewport(x, y, width, height);
-        gl.scissor(x, y, width, height);
+        state.viewport(x, y, width, height);
+        state.scissor(x, y, width, height);
 
         state.disable(gl.BLEND);
         compose.render();
@@ -231,8 +231,8 @@ export class MultiSamplePass {
             state.disable(gl.BLEND);
             state.disable(gl.DEPTH_TEST);
             state.depthMask(false);
-            gl.viewport(x, y, width, height);
-            gl.scissor(x, y, width, height);
+            state.viewport(x, y, width, height);
+            state.scissor(x, y, width, height);
             compose.render();
             sampleIndex += 1;
         } else {
@@ -267,8 +267,8 @@ export class MultiSamplePass {
                 state.blendFuncSeparate(gl.ONE, gl.ONE, gl.ONE, gl.ONE);
                 state.disable(gl.DEPTH_TEST);
                 state.depthMask(false);
-                gl.viewport(x, y, width, height);
-                gl.scissor(x, y, width, height);
+                state.viewport(x, y, width, height);
+                state.scissor(x, y, width, height);
                 if (sampleIndex === 0) {
                     state.clearColor(0, 0, 0, 0);
                     gl.clear(gl.COLOR_BUFFER_BIT);
@@ -283,8 +283,8 @@ export class MultiSamplePass {
         drawPass.postprocessing.setOcclusionOffset(0, 0);
 
         this.bindOutputTarget(toDrawingBuffer);
-        gl.viewport(x, y, width, height);
-        gl.scissor(x, y, width, height);
+        state.viewport(x, y, width, height);
+        state.scissor(x, y, width, height);
 
         const accumulationWeight = sampleIndex * sampleWeight;
         if (accumulationWeight > 0) {

+ 2 - 2
src/mol-canvas3d/passes/postprocessing.ts

@@ -538,8 +538,8 @@ export class PostprocessingPass {
         state.depthMask(false);
 
         const { x, y, width, height } = camera.viewport;
-        gl.viewport(x, y, width, height);
-        gl.scissor(x, y, width, height);
+        state.viewport(x, y, width, height);
+        state.scissor(x, y, width, height);
     }
 
     private occlusionOffset: [x: number, y: number] = [0, 0];

+ 2 - 2
src/mol-canvas3d/passes/smaa.ts

@@ -71,8 +71,8 @@ export class SmaaPass {
         state.depthMask(false);
 
         const { x, y, width, height } = viewport;
-        gl.viewport(x, y, width, height);
-        gl.scissor(x, y, width, height);
+        state.viewport(x, y, width, height);
+        state.scissor(x, y, width, height);
 
         state.clearColor(0, 0, 0, 1);
         gl.clear(gl.COLOR_BUFFER_BIT);

+ 6 - 6
src/mol-geo/geometry/texture-mesh/color-smoothing.ts

@@ -319,8 +319,8 @@ export function calcTextureMeshColorSmoothing(input: ColorSmoothingInput, resolu
 
     if (isTimingMode) webgl.timer.mark('ColorAccumulate.render');
     setAccumulateDefaults(webgl);
-    gl.viewport(0, 0, width, height);
-    gl.scissor(0, 0, width, height);
+    state.viewport(0, 0, width, height);
+    state.scissor(0, 0, width, height);
     gl.clear(gl.COLOR_BUFFER_BIT);
     ValueCell.update(uCurrentY, 0);
     let currCol = 0;
@@ -336,8 +336,8 @@ export function calcTextureMeshColorSmoothing(input: ColorSmoothingInput, resolu
         // console.log({ i, currX, currY });
         ValueCell.update(uCurrentX, currX);
         ValueCell.update(uCurrentSlice, i);
-        gl.viewport(currX, currY, dx, dy);
-        gl.scissor(currX, currY, dx, dy);
+        state.viewport(currX, currY, dx, dy);
+        state.scissor(currX, currY, dx, dy);
         accumulateRenderable.render();
         ++currCol;
         currX += dx;
@@ -371,8 +371,8 @@ export function calcTextureMeshColorSmoothing(input: ColorSmoothingInput, resolu
 
     setNormalizeDefaults(webgl);
     texture.attachFramebuffer(framebuffer, 0);
-    gl.viewport(0, 0, width, height);
-    gl.scissor(0, 0, width, height);
+    state.viewport(0, 0, width, height);
+    state.scissor(0, 0, width, height);
     gl.clear(gl.COLOR_BUFFER_BIT);
     normalizeRenderable.render();
     if (isTimingMode) webgl.timer.markEnd('ColorNormalize.render');

+ 2 - 2
src/mol-gl/compute/grid3d.ts

@@ -225,8 +225,8 @@ export function createGrid3dComputeRenderable<S extends RenderableSchema, P, CS>
 
 function resetGl(webgl: WebGLContext, w: number) {
     const { gl, state } = webgl;
-    gl.viewport(0, 0, w, w);
-    gl.scissor(0, 0, w, w);
+    state.viewport(0, 0, w, w);
+    state.scissor(0, 0, w, w);
     state.disable(gl.SCISSOR_TEST);
     state.disable(gl.BLEND);
     state.disable(gl.DEPTH_TEST);

+ 7 - 7
src/mol-gl/compute/histogram-pyramid/reduction.ts

@@ -122,7 +122,7 @@ export interface HistogramPyramid {
 
 export function createHistogramPyramid(ctx: WebGLContext, inputTexture: Texture, scale: Vec2, gridTexDim: Vec3): HistogramPyramid {
     if (isTimingMode) ctx.timer.mark('createHistogramPyramid');
-    const { gl } = ctx;
+    const { gl, state } = ctx;
     const w = inputTexture.getWidth();
     const h = inputTexture.getHeight();
 
@@ -146,7 +146,7 @@ export function createHistogramPyramid(ctx: WebGLContext, inputTexture: Texture,
     const framebuffer = getFramebuffer('pyramid', ctx);
     pyramidTex.attachFramebuffer(framebuffer, 0);
 
-    gl.viewport(0, 0, maxSizeX, maxSizeY);
+    state.viewport(0, 0, maxSizeX, maxSizeY);
     if (isWebGL2(gl)) {
         gl.clearBufferiv(gl.COLOR, 0, [0, 0, 0, 0]);
     } else {
@@ -157,7 +157,7 @@ export function createHistogramPyramid(ctx: WebGLContext, inputTexture: Texture,
     for (let i = 0; i < levels; ++i) levelTexturesFramebuffers.push(getLevelTextureFramebuffer(ctx, i));
 
     const renderable = getHistopyramidReductionRenderable(ctx, inputTexture, levelTexturesFramebuffers[0].texture);
-    ctx.state.currentRenderItemId = -1;
+    state.currentRenderItemId = -1;
     setRenderingDefaults(ctx);
 
     let offset = 0;
@@ -176,15 +176,15 @@ export function createHistogramPyramid(ctx: WebGLContext, inputTexture: Texture,
             ValueCell.update(renderable.values.tPreviousLevel, levelTexturesFramebuffers[levels - i].texture);
             renderable.update();
         }
-        ctx.state.currentRenderItemId = -1;
-        gl.viewport(0, 0, size, size);
-        gl.scissor(0, 0, size, size);
+        state.currentRenderItemId = -1;
+        state.viewport(0, 0, size, size);
+        state.scissor(0, 0, size, size);
         if (isWebGL2(gl)) {
             gl.clearBufferiv(gl.COLOR, 0, [0, 0, 0, 0]);
         } else {
             gl.clear(gl.COLOR_BUFFER_BIT);
         }
-        gl.scissor(0, 0, gridTexDim[0], gridTexDim[1]);
+        state.scissor(0, 0, gridTexDim[0], gridTexDim[1]);
         renderable.render();
 
         pyramidTex.bind(0);

+ 2 - 2
src/mol-gl/compute/histogram-pyramid/sum.ts

@@ -68,7 +68,7 @@ const sumInts = new Int32Array(4);
 
 export function getHistopyramidSum(ctx: WebGLContext, pyramidTopTexture: Texture) {
     if (isTimingMode) ctx.timer.mark('getHistopyramidSum');
-    const { gl, resources } = ctx;
+    const { gl, state, resources } = ctx;
 
     const renderable = getHistopyramidSumRenderable(ctx, pyramidTopTexture);
     ctx.state.currentRenderItemId = -1;
@@ -89,7 +89,7 @@ export function getHistopyramidSum(ctx: WebGLContext, pyramidTopTexture: Texture
 
     setRenderingDefaults(ctx);
 
-    gl.viewport(0, 0, 1, 1);
+    state.viewport(0, 0, 1, 1);
     renderable.render();
     gl.finish();
 

+ 4 - 4
src/mol-gl/compute/marching-cubes/active-voxels.ts

@@ -85,7 +85,7 @@ function setRenderingDefaults(ctx: WebGLContext) {
 
 export function calcActiveVoxels(ctx: WebGLContext, volumeData: Texture, gridDim: Vec3, gridTexDim: Vec3, isoValue: number, gridScale: Vec2) {
     if (isTimingMode) ctx.timer.mark('calcActiveVoxels');
-    const { gl, resources } = ctx;
+    const { gl, state, resources } = ctx;
     const width = volumeData.getWidth();
     const height = volumeData.getHeight();
 
@@ -106,10 +106,10 @@ export function calcActiveVoxels(ctx: WebGLContext, volumeData: Texture, gridDim
 
     activeVoxelsTex.attachFramebuffer(framebuffer, 0);
     setRenderingDefaults(ctx);
-    gl.viewport(0, 0, width, height);
-    gl.scissor(0, 0, width, height);
+    state.viewport(0, 0, width, height);
+    state.scissor(0, 0, width, height);
     gl.clear(gl.COLOR_BUFFER_BIT);
-    gl.scissor(0, 0, gridTexDim[0], gridTexDim[1]);
+    state.scissor(0, 0, gridTexDim[0], gridTexDim[1]);
     renderable.render();
 
     // console.log('gridScale', gridScale, 'gridTexDim', gridTexDim, 'gridDim', gridDim);

+ 2 - 2
src/mol-gl/compute/marching-cubes/isosurface.ts

@@ -127,7 +127,7 @@ export function createIsosurfaceBuffers(ctx: WebGLContext, activeVoxelsBase: Tex
     if (!drawBuffers) throw new Error('need WebGL draw buffers');
 
     if (isTimingMode) ctx.timer.mark('createIsosurfaceBuffers');
-    const { gl, resources, extensions } = ctx;
+    const { gl, state, resources, extensions } = ctx;
     const { pyramidTex, height, levels, scale, count } = histogramPyramid;
     const width = pyramidTex.getWidth();
 
@@ -192,7 +192,7 @@ export function createIsosurfaceBuffers(ctx: WebGLContext, activeVoxelsBase: Tex
     ]);
 
     setRenderingDefaults(ctx);
-    gl.viewport(0, 0, width, height);
+    state.viewport(0, 0, width, height);
     gl.clear(gl.COLOR_BUFFER_BIT);
     renderable.render();
 

+ 2 - 2
src/mol-gl/compute/util.ts

@@ -125,8 +125,8 @@ export function readAlphaTexture(ctx: WebGLContext, texture: Texture) {
     state.clearColor(0, 0, 0, 0);
     state.blendFunc(gl.ONE, gl.ONE);
     state.blendEquation(gl.FUNC_ADD);
-    gl.viewport(0, 0, width, height);
-    gl.scissor(0, 0, width, height);
+    state.viewport(0, 0, width, height);
+    state.scissor(0, 0, width, height);
     gl.clear(gl.COLOR_BUFFER_BIT);
     copy.render();
 

+ 12 - 8
src/mol-gl/renderer.ts

@@ -64,7 +64,7 @@ interface Renderer {
     renderDepthTransparent: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
     renderMarkingDepth: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
     renderMarkingMask: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
-    renderBlended: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
+    renderBlended: (scene: Scene, camera: ICamera, depthTexture: Texture | null) => void
     renderBlendedOpaque: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
     renderBlendedTransparent: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
     renderBlendedVolume: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
@@ -359,8 +359,8 @@ namespace Renderer {
             state.colorMask(true, true, true, true);
 
             const { x, y, width, height } = viewport;
-            gl.viewport(x, y, width, height);
-            gl.scissor(x, y, width, height);
+            state.viewport(x, y, width, height);
+            state.scissor(x, y, width, height);
 
             globalUniformsNeedUpdate = true;
             state.currentRenderItemId = -1;
@@ -475,9 +475,13 @@ namespace Renderer {
             if (isTimingMode) ctx.timer.markEnd('Renderer.renderMarkingMask');
         };
 
-        const renderBlended = (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => {
-            renderBlendedOpaque(group, camera, depthTexture);
-            renderBlendedTransparent(group, camera, depthTexture);
+        const renderBlended = (scene: Scene, camera: ICamera, depthTexture: Texture | null) => {
+            if (scene.hasOpaque) {
+                renderBlendedOpaque(scene, camera, depthTexture);
+            }
+            if (scene.opacityAverage < 1) {
+                renderBlendedTransparent(scene, camera, depthTexture);
+            }
         };
 
         const renderBlendedOpaque = (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => {
@@ -714,8 +718,8 @@ namespace Renderer {
                 }
             },
             setViewport: (x: number, y: number, width: number, height: number) => {
-                gl.viewport(x, y, width, height);
-                gl.scissor(x, y, width, height);
+                state.viewport(x, y, width, height);
+                state.scissor(x, y, width, height);
                 if (x !== viewport.x || y !== viewport.y || width !== viewport.width || height !== viewport.height) {
                     Viewport.set(viewport, x, y, width, height);
                     ValueCell.update(globalUniforms.uViewport, Vec4.set(globalUniforms.uViewport.ref.value, x, y, width, height));

+ 27 - 1
src/mol-gl/scene.ts

@@ -80,8 +80,12 @@ interface Scene extends Object3D {
     has: (o: GraphicsRenderObject) => boolean
     clear: () => void
     forEach: (callbackFn: (value: GraphicsRenderable, key: GraphicsRenderObject) => void) => void
+    /** Marker average of primitive renderables */
     readonly markerAverage: number
+    /** Opacity average of primitive renderables */
     readonly opacityAverage: number
+    /** Is `true` if any primitive renderable (possibly) has any opaque part */
+    readonly hasOpaque: boolean
 }
 
 namespace Scene {
@@ -103,6 +107,7 @@ namespace Scene {
 
         let markerAverage = 0;
         let opacityAverage = 0;
+        let hasOpaque = false;
 
         const object3d = Object3D.create();
         const { view, position, direction, up } = object3d;
@@ -160,7 +165,9 @@ namespace Scene {
             }
 
             renderables.sort(renderableSort);
+            markerAverage = calculateMarkerAverage();
             opacityAverage = calculateOpacityAverage();
+            hasOpaque = calculateHasOpaque();
             return true;
         }
 
@@ -182,7 +189,10 @@ namespace Scene {
             const newVisibleHash = computeVisibleHash();
             if (newVisibleHash !== visibleHash) {
                 boundingSphereVisibleDirty = true;
+                markerAverage = calculateMarkerAverage();
                 opacityAverage = calculateOpacityAverage();
+                hasOpaque = calculateHasOpaque();
+                visibleHash = newVisibleHash;
                 return true;
             } else {
                 return false;
@@ -220,6 +230,19 @@ namespace Scene {
             return count > 0 ? opacityAverage / count : 0;
         }
 
+        function calculateHasOpaque() {
+            if (primitives.length === 0) return false;
+            for (let i = 0, il = primitives.length; i < il; ++i) {
+                const p = primitives[i];
+                if (!p.state.visible) continue;
+
+                if (p.state.opaque) return true;
+                if (p.state.alphaFactor === 1 && p.values.alpha.ref.value === 1 && p.values.transparencyAverage.ref.value !== 1) return true;
+                if (p.values.dTransparentBackfaces?.ref.value === 'opaque') return true;
+            }
+            return false;
+        }
+
         return {
             view, position, direction, up,
 
@@ -247,6 +270,7 @@ namespace Scene {
                 }
                 markerAverage = calculateMarkerAverage();
                 opacityAverage = calculateOpacityAverage();
+                hasOpaque = calculateHasOpaque();
             },
             add: (o: GraphicsRenderObject) => commitQueue.add(o),
             remove: (o: GraphicsRenderObject) => commitQueue.remove(o),
@@ -283,7 +307,6 @@ namespace Scene {
                 if (boundingSphereVisibleDirty) {
                     calculateBoundingSphere(renderables, boundingSphereVisible, true);
                     boundingSphereVisibleDirty = false;
-                    visibleHash = computeVisibleHash();
                 }
                 return boundingSphereVisible;
             },
@@ -293,6 +316,9 @@ namespace Scene {
             get opacityAverage() {
                 return opacityAverage;
             },
+            get hasOpaque() {
+                return hasOpaque;
+            },
         };
     }
 }

+ 5 - 5
src/mol-gl/webgl/context.ts

@@ -142,12 +142,12 @@ export function readPixels(gl: GLRenderingContext, x: number, y: number, width:
     if (isDebugMode) checkError(gl);
 }
 
-function getDrawingBufferPixelData(gl: GLRenderingContext) {
+function getDrawingBufferPixelData(gl: GLRenderingContext, state: WebGLState) {
     const w = gl.drawingBufferWidth;
     const h = gl.drawingBufferHeight;
     const buffer = new Uint8Array(w * h * 4);
     unbindFramebuffer(gl);
-    gl.viewport(0, 0, w, h);
+    state.viewport(0, 0, w, h);
     readPixels(gl, 0, 0, w, h, buffer);
     return PixelData.flipY(PixelData.create(buffer, w, h));
 }
@@ -345,15 +345,15 @@ export function createContext(gl: GLRenderingContext, props: Partial<{ pixelScal
         readPixelsAsync,
         waitForGpuCommandsComplete: () => waitForGpuCommandsComplete(gl),
         waitForGpuCommandsCompleteSync: () => waitForGpuCommandsCompleteSync(gl),
-        getDrawingBufferPixelData: () => getDrawingBufferPixelData(gl),
+        getDrawingBufferPixelData: () => getDrawingBufferPixelData(gl, state),
         clear: (red: number, green: number, blue: number, alpha: number) => {
             unbindFramebuffer(gl);
             state.enable(gl.SCISSOR_TEST);
             state.depthMask(true);
             state.colorMask(true, true, true, true);
             state.clearColor(red, green, blue, alpha);
-            gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
-            gl.scissor(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
+            state.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
+            state.scissor(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
             gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
         },
 

+ 1 - 1
src/mol-gl/webgl/render-item.ts

@@ -168,7 +168,7 @@ export function createRenderItem<T extends string>(ctx: WebGLContext, drawMode:
         getProgram: (variant: T) => programs[variant],
 
         render: (variant: T, sharedTexturesCount: number) => {
-            if (drawCount === 0 || instanceCount === 0 || ctx.isContextLost) return;
+            if (drawCount === 0 || instanceCount === 0) return;
             const program = programs[variant];
             if (program.id === currentProgramId && state.currentRenderItemId === id) {
                 program.setUniforms(uniformValueEntries);

+ 29 - 0
src/mol-gl/webgl/state.ts

@@ -69,6 +69,9 @@ export type WebGLState = {
     clearVertexAttribsState: () => void
     disableUnusedVertexAttribs: () => void
 
+    viewport: (x: number, y: number, width: number, height: number) => void
+    scissor: (x: number, y: number, width: number, height: number) => void
+
     reset: () => void
 }
 
@@ -95,6 +98,9 @@ export function createState(gl: GLRenderingContext): WebGLState {
     let maxVertexAttribs = gl.getParameter(gl.MAX_VERTEX_ATTRIBS);
     const vertexAttribsState: number[] = [];
 
+    let currentViewport: [number, number, number, number] = gl.getParameter(gl.VIEWPORT);
+    let currentScissor: [number, number, number, number] = gl.getParameter(gl.SCISSOR_BOX);
+
     const clearVertexAttribsState = () => {
         for (let i = 0; i < maxVertexAttribs; ++i) {
             vertexAttribsState[i] = 0;
@@ -222,6 +228,26 @@ export function createState(gl: GLRenderingContext): WebGLState {
             }
         },
 
+        viewport: (x: number, y: number, width: number, height: number) => {
+            if (x !== currentViewport[0] || y !== currentViewport[1] || width !== currentViewport[2] || height !== currentViewport[3]) {
+                gl.viewport(x, y, width, height);
+                currentViewport[0] = x;
+                currentViewport[1] = y;
+                currentViewport[2] = width;
+                currentViewport[3] = height;
+            }
+        },
+
+        scissor: (x: number, y: number, width: number, height: number) => {
+            if (x !== currentScissor[0] || y !== currentScissor[1] || width !== currentScissor[2] || height !== currentScissor[3]) {
+                gl.scissor(x, y, width, height);
+                currentScissor[0] = x;
+                currentScissor[1] = y;
+                currentScissor[2] = width;
+                currentScissor[3] = height;
+            }
+        },
+
         reset: () => {
             enabledCapabilities = {};
 
@@ -247,6 +273,9 @@ export function createState(gl: GLRenderingContext): WebGLState {
             for (let i = 0; i < maxVertexAttribs; ++i) {
                 vertexAttribsState[i] = 0;
             }
+
+            currentViewport = gl.getParameter(gl.VIEWPORT);
+            currentScissor = gl.getParameter(gl.SCISSOR_BOX);
         }
     };
 }

+ 6 - 6
src/mol-math/geometry/gaussian-density/gpu.ts

@@ -166,8 +166,8 @@ function calcGaussianDensityTexture2d(webgl: WebGLContext, position: PositionDat
         state.currentRenderItemId = -1;
         fbTex.attachFramebuffer(framebuffer, 0);
         if (clear) {
-            gl.viewport(0, 0, width, height);
-            gl.scissor(0, 0, width, height);
+            state.viewport(0, 0, width, height);
+            state.scissor(0, 0, width, height);
             gl.clear(gl.COLOR_BUFFER_BIT);
         }
         ValueCell.update(uCurrentY, 0);
@@ -184,8 +184,8 @@ function calcGaussianDensityTexture2d(webgl: WebGLContext, position: PositionDat
             // console.log({ i, currX, currY });
             ValueCell.update(uCurrentX, currX);
             ValueCell.update(uCurrentSlice, i);
-            gl.viewport(currX, currY, dx, dy);
-            gl.scissor(currX, currY, dx, dy);
+            state.viewport(currX, currY, dx, dy);
+            state.scissor(currX, currY, dx, dy);
             renderable.render();
             ++currCol;
             currX += dx;
@@ -232,8 +232,8 @@ function calcGaussianDensityTexture3d(webgl: WebGLContext, position: PositionDat
     const framebuffer = getFramebuffer(webgl);
     framebuffer.bind();
     setRenderingDefaults(webgl);
-    gl.viewport(0, 0, dx, dy);
-    gl.scissor(0, 0, dx, dy);
+    state.viewport(0, 0, dx, dy);
+    state.scissor(0, 0, dx, dy);
 
     if (!texture) texture = colorBufferHalfFloat && textureHalfFloat
         ? resources.texture('volume-float16', 'rgba', 'fp16', 'linear')