在使用 Plotly.js 绘制 WebGL 图表 (如 scattergl) 时,如果同时显示图表过多,就有可能观看到如下错误:
WARNING: Too many active WebGL contexts. Oldest context will be lost.
这是什么意思?如何解决?本文将进行全面分析,帮助您快速实现回退或跨越这个应用性限制。
问题说明
WebGL 是一种基于 GPU 的绘图 API,每一个 WebGL 图表就需要创建一个 WebGL 上下文 (WebGL context)。因为 GPU 资源有限,大部分浏览器会限制 WebGL context 的数量,通常在 8 ~ 16 个之间。
Plotly.js 里的 WebGL 图表,如 type: 'scattergl'
,同样会创建 WebGL context,而且当您使用 trellisBy n*n 分面布局显示多个图表时,很容易超过这个上限。
解决方案
1. 更换为 SVG 模式
如果每个图表点量不大,可以改用 SVG 模式(type: 'scatter'
):
{
type: 'scatter', // SVG 模式
mode: 'markers',
...
}
2. 分页/懒加载显示图表
分页显示 6~8 个图表,切换时重新渲染,当前页的图表没有显示时重置或释放上下文:
Plotly.purge(document.getElementById('chartId'));
3. 使用 Virtual WebGL (实际解决方案)
Plotly.js 官网提供一个性能量解决方案:使用 Virtual WebGL 实现多 WebGL 上下文虚拟在一个实际上下文上:
使用步骤
- 在页面中引入开源代码:(或作为
.js
静态资源文件放在项目assets
文件夹下手动引入)
<script src="https://unpkg.com/virtual-webgl@1.0.6/src/virtual-webgl.js"></script>
-
它会动态 patch 原本 HTMLCanvasElement.prototype.getContext,在您调用
getContext('webgl')
时代理为虚拟 WebGL 上下文,将多个 context 虚拟到同一 GPU context 上。 -
无需修改 Plotly 图表代码,如果您本来就是 scattergl,就会被自动虚拟化,保持结构不变。
注意事项
- Virtual WebGL 性能有所降低,适合图表量大但每图点量较小场景
- 如果您有大量点分布需求,还是要遵守 6~8 个图表上限
小结
Plotly.js 中 scattergl/热力图等 WebGL 模式图表有突出性能优势,但也带来了 WebGL context 限制的突点。通过分页、SVG fallback 或 virtual-webgl 虚拟技术,可以有效解决多图表同时渲染时的失效问题。
建议在 BI 项目中对 WebGL context 使用进行监控或 fallback 策略,以增强程序的应急性和稳定性。
virtual-webgl.js
/*
* Copyright 2018, Gregg Tavares.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
*
* * Neither the name of Gregg Tavares. nor the names of his
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
(function() {
const settings = {
disableWebGL2: false,
compositorCreator() {
},
};
const canvasToVirtualContextMap = new Map();
const extensionInfo = {
webgl_draw_buffers: {
wrapperFnMakerFn: makeWEBGL_draw_buffersWrapper,
saveRestoreMakerFn: makeWEBGL_drawBuffersSaveRestoreHelper,
},
oes_vertex_array_object: {
wrapperFnMakerFn: makeOES_vertex_array_objectWrapper,
},
angle_instanced_arrays: {
wrapperFnMakerFn: makeANGLE_instanced_arraysWrapper,
},
};
const extensionSaveRestoreHelpersArray = [];
const extensionSaveRestoreHelpers = {};
let currentVirtualContext = null;
let someContextsNeedRendering;
const sharedWebGLContext = document.createElement('canvas').getContext('webgl');
const sharedVAOExtension = sharedWebGLContext.getExtension("OES_vertex_array_object");
const sharedInstanceExtension = sharedWebGLContext.getExtension("ANGLE_instanced_arrays");
const numAttributes = sharedWebGLContext.getParameter(sharedWebGLContext.MAX_VERTEX_ATTRIBS);
const numTextureUnits = sharedWebGLContext.getParameter(sharedWebGLContext.MAX_COMBINED_TEXTURE_IMAGE_UNITS);
let numDrawBuffers;
const baseState = makeDefaultState(300, 150);
const vs = `
attribute vec4 position;
varying vec2 v_texcoord;
void main() {
gl_Position = position;
v_texcoord = position.xy * .5 + .5;
}
`;
const fs = `
precision mediump float;
varying vec2 v_texcoord;
uniform sampler2D u_tex;
void main() {
gl_FragColor = texture2D(u_tex, v_texcoord);
}
`;
const fs2 = `
precision mediump float;
varying vec2 v_texcoord;
uniform sampler2D u_tex;
void main() {
gl_FragColor = texture2D(u_tex, v_texcoord);
gl_FragColor.rgb *= gl_FragColor.a;
}
`;
const premultplyAlphaTrueProgram = createProgram(sharedWebGLContext, [vs, fs]);
const premultplyAlphaFalseProgram = createProgram(sharedWebGLContext, [vs, fs2]);
if (sharedVAOExtension) {
const vao = sharedVAOExtension.createVertexArrayOES();
sharedVAOExtension.bindVertexArrayOES(vao);
baseState.vertexArray = vao;
}
{
const gl = sharedWebGLContext;
const positionLoc = 0; // hard coded in createProgram
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
-1, -1,
1, -1,
-1, 1,
-1, 1,
1, -1,
1, 1,
]), gl.STATIC_DRAW);
gl.enableVertexAttribArray(positionLoc);
gl.vertexAttribPointer(positionLoc, 2, gl.FLOAT, false, 0, 0);
}
saveAllState(baseState);
HTMLCanvasElement.prototype.getContext = (function(origFn) {
return function(type, contextAttributes) {
if (type === 'webgl' || type === 'experimental-webgl') {
return createOrGetVirtualWebGLContext(this, type, contextAttributes);
} else if (type === 'webgl2' && settings.disableWebGL2) {
return null;
}
return origFn.call(this, type, contextAttributes);
};
}(HTMLCanvasElement.prototype.getContext));
function valueOrDefault(value, defaultValue) {
return value === undefined ? defaultValue : value;
}
function errorDisposedContext(fnName) {
return function() {
throw new Error(`tried to call ${fnName} on disposed context`);
};
}
class DefaultCompositor {
constructor(canvas) {
this._ctx = canvas.getContext('2d');
}
composite(gl, texture, canvas, contextAttributes) {
// note: not entirely sure what to do here. We need this canvas to be at least as large
// as the canvas we're drawing to. Resizing a canvas is slow so I think just making
// sure we never get smaller than the largest canvas. At the moment though I'm too lazy
// to make it smaller.
const ctx = this._ctx;
const width = canvas.width;
const height = canvas.height;
const maxWidth = Math.max(gl.canvas.width, width);
const maxHeight = Math.max(gl.canvas.height, height);
if (gl.canvas.width !== maxWidth || gl.canvas.height !== maxHeight) {
gl.canvas.width = maxWidth;
gl.canvas.height = maxHeight;
}
gl.viewport(0, 0, width, height);
gl.useProgram(contextAttributes.premultipliedAlpha ? premultplyAlphaTrueProgram : premultplyAlphaFalseProgram);
// draw the drawingbuffer's texture to the offscreen canvas
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.drawArrays(gl.TRIANGLES, 0, 6);
// copy it to target canvas
ctx.globalCompositeOperation = 'copy';
ctx.drawImage(
gl.canvas,
0, maxHeight - height, width, height, // src rect
0, 0, width, height); // dest rect
}
dispose() {
}
}
class VirtualWebGLContext {
constructor(canvas, contextAttributes = {}, compositor, disposeHelper) {
const gl = sharedWebGLContext;
this.canvas = canvas;
// Should use Symbols or someting to hide these variables from the outside.
this._compositor = compositor;
this._disposeHelper = disposeHelper;
this._extensions = {};
// based on context attributes and canvas.width, canvas.height
// create a texture and framebuffer
this._drawingbufferTexture = gl.createTexture();
this._drawingbufferFramebuffer = gl.createFramebuffer();
this._contextAttributes = {
alpha: valueOrDefault(contextAttributes.alpha, true),
antialias: false,
depth: valueOrDefault(contextAttributes.depth, true),
failIfMajorPerformanceCaveat: false,
premultipliedAlpha: valueOrDefault(contextAttributes.premultipliedAlpha, true),
stencil: valueOrDefault(contextAttributes.stencil, false),
};
this._preserveDrawingbuffer = valueOrDefault(contextAttributes.preserveDrawingBuffer, false);
const oldTexture = gl.getParameter(gl.TEXTURE_BINDING_2D);
const oldFramebuffer = gl.getParameter(gl.FRAMEBUFFER_BINDING);
gl.bindTexture(gl.TEXTURE_2D, this._drawingbufferTexture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
// this._drawingbufferTexture.id = canvas.id;
// this._drawingbufferFramebuffer.id = canvas.id;
gl.bindFramebuffer(gl.FRAMEBUFFER, this._drawingbufferFramebuffer);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this._drawingbufferTexture, 0);
if (this._contextAttributes.depth) {
const oldRenderbuffer = gl.getParameter(gl.RENDERBUFFER_BINDING);
this._depthRenderbuffer = gl.createRenderbuffer();
gl.bindRenderbuffer(gl.RENDERBUFFER, this._depthRenderbuffer);
const attachmentPoint = this._contextAttributes.stencil ? gl.DEPTH_STENCIL_ATTACHMENT : gl.DEPTH_ATTACHMENT;
gl.framebufferRenderbuffer(gl.FRAMEBUFFER, attachmentPoint, gl.RENDERBUFFER, this._depthRenderbuffer);
gl.bindRenderbuffer(gl.RENDERBUFFER, oldRenderbuffer);
}
gl.bindTexture(gl.TEXTURE_2D, oldTexture);
gl.bindFramebuffer(gl.FRAMEBUFFER, oldFramebuffer);
// remember all WebGL state (default bindings, default texture units,
// default attributes and/or vertex shade object, default program,
// default blend, stencil, zbuffer, culling, viewport etc... state
this._state = makeDefaultState(canvas.width, canvas.height);
this._state.framebuffer = this._drawingbufferFramebuffer;
if (sharedVAOExtension) {
this._state.vertexArray = sharedVAOExtension.createVertexArrayOES();
this._defaultVertexArray = this._state.vertexArray;
}
}
dispose() {
this._disposeHelper();
const gl = sharedWebGLContext;
gl.deleteFramebuffer(this._drawingbufferFramebuffer);
gl.deleteTexture(this._drawingbufferTexture);
if (this._depthRenderbuffer) {
gl.deleteRenderbuffer(this._depthRenderbuffer);
}
if (this._compositor.dispose) {
this._compositor.dispose();
}
for (const [key, value] of Object.entries(this)) {
if (typeof value === 'function') {
this[key] = errorDisposedContext(key);
}
}
for (const [key, value] of Object.entries(VirtualWebGLContext.prototype)) {
if (typeof value === 'function') {
this[key] = errorDisposedContext(key);
}
}
}
get drawingBufferWidth() {
return this.canvas.width;
}
get drawingBufferHeight() {
return this.canvas.height;
}
composite(gl) {
this._compositor.composite(gl, this._drawingbufferTexture, this.canvas, this._contextAttributes);
if (!this._preserveDrawingbuffer) {
this._needClear = true;
}
}
}
function makeDefaultState(width, height) {
const gl = WebGLRenderingContext;
const state ={
arrayBuffer: null,
renderbuffer: null,
framebuffer: null,
blend: false,
cullFace: false,
depthTest: false,
dither: false,
polygonOffsetFill: false,
sampleAlphaToCoverage: false,
sampleCoverage: false,
scissorTest: false,
stencilTest: false,
activeTexture: gl.TEXTURE0,
packAlignment: 4,
unpackAlignment: 4,
unpackColorspaceConversion: gl.BROWSER_DEFAULT_WEBGL,
unpackFlipY: 0,
unpackPremultiplyAlpha: 0,
currentProgram: null,
viewport: [0, 0, width, height],
scissor: [0, 0, 0, 0],
blendSrcRgb: gl.ONE,
blendDstRgb: gl.ZERO,
blendSrcAlpha: gl.ONE,
blendDstAlpha: gl.ZERO,
blendEquationRgb: gl.FUNC_ADD,
blendEquationAlpha: gl.FUNC_ADD,
blendColor: [0, 0, 0, 0],
colorClearValue: [0, 0, 0, 0],
colorMask: [true, true, true, true],
cullFaceMode: gl.BACK,
depthClearValue: 1,
depthFunc: gl.LESS,
depthRange: [0, 1],
depthMask: true,
frontFace: gl.CCW,
generateMipmapHint: gl.DONT_CARE,
lineWidth: 1,
polygonOffsetFactor: 0,
polygonOffsetUnits: 0,
sampleCoverageValue: 1,
sampleCoverageUnits: false,
stencilBackFail: gl.KEEP,
stencilBackFunc: gl.ALWAYS,
stencilBackPassDepthFail: gl.KEEP,
stencilBackPassDepthPass: gl.KEEP,
stencilBackRef: 0,
stencilBackValueMask: 0xFFFFFFFF,
stencilBackWriteMask: 0xFFFFFFFF,
stencilClearValue: 0,
stencilFail: gl.KEEP,
stencilFunc: gl.ALWAYS,
stencilPassDepthFail: gl.KEEP,
stencilPassDepthPass: gl.KEEP,
stencilRef: 0,
stencilValueMask: 0xFFFFFFFF,
stencilWriteMask: 0xFFFFFFFF,
textureUnits: [],
};
if (sharedVAOExtension) {
state.vertexArray = null;
} else {
state.elementArrayBuffer = null;
state.attributes = [];
for (let i = 0; i < numAttributes; ++i) {
const attrib = {
buffer: null,
enabled: false,
size: 4,
stride: 0,
type: gl.FLOAT,
normalized: false,
value: [0, 0, 0, 1],
};
if (sharedInstanceExtension) {
attrib.divisor = 0;
}
state.attributes.push(attrib);
}
}
for (let i = 0; i < numTextureUnits; ++i) {
state.textureUnits.push({
texture2D: null,
textureCubemap: null,
});
}
return state;
}
// copy all WebGL constants and functions to the prototype of
// VirtualWebGLContext
for (let key in WebGLRenderingContext.prototype) {
const propDesc = Object.getOwnPropertyDescriptor(WebGLRenderingContext.prototype, key);
if (propDesc.get) {
// it's a getter/setter ?
const virtualPropDesc = Object.getOwnPropertyDescriptor(VirtualWebGLContext.prototype, key);
if (!virtualPropDesc) {
console.warn(`WebGLRenderingContext.${key} is not supported`);
}
continue;
}
switch (key) {
default: {
const value = WebGLRenderingContext.prototype[key];
let newValue = value;
switch (key) {
case 'getContextAttributes':
newValue = virtualGetContextAttributes;
break;
case 'getExtension':
newValue = createGetExtensionWrapper(value);
break;
case 'bindFramebuffer':
newValue = virtualBindFramebuffer;
break;
case 'getParameter':
newValue = virtualGetParameter;
break;
case 'readPixels':
newValue = virtualReadPixels;
break;
case 'clear':
case 'drawArrays':
case 'drawElements':
newValue = createDrawWrapper(value);
break;
default:
if (typeof value === 'function') {
newValue = createWrapper(value);
}
break;
}
VirtualWebGLContext.prototype[key] = newValue;
break;
}
}
}
function createGetExtensionWrapper(origFn) {
return function(extensionName) {
extensionName = extensionName.toLowerCase();
// just like the real context each extension needs a virtual class because each use
// of the extension might be modified (as in people adding properties to it)
const existingExt = this._extensions[extensionName];
if (existingExt) {
return existingExt;
}
const ext = origFn.call(sharedWebGLContext, extensionName);
const wrapperInfo = extensionInfo[extensionName] || {};
const wrapperFnMakerFn = wrapperInfo.wrapperFnMakerFn || (() => { console.log("trying to get extension:", extensionName); });
const saveRestoreHelper = extensionSaveRestoreHelpers[extensionName];
if (!saveRestoreHelper) {
const saveRestoreMakerFn = wrapperInfo.saveRestoreMakerFn;
if (saveRestoreMakerFn) {
const saveRestore = saveRestoreMakerFn(ext);
extensionSaveRestoreHelpers[extensionName] = saveRestore;
extensionSaveRestoreHelpersArray.push(saveRestore);
}
}
const wrapper = {
_context: this,
};
for (let key in ext) {
let value = ext[key];
if (typeof value === 'function') {
value = wrapperFnMakerFn(ext, value, extensionName);
}
wrapper[key] = value;
}
this._extensions[extensionName] = wrapper;
return wrapper;
};
}
function isFramebufferBindingNull(vctx) {
return vctx._state.framebuffer === vctx._drawingbufferFramebuffer;
}
function makeOES_vertex_array_objectWrapper(ext, origFn, name) {
switch (name) {
case 'bindVertexArrayOES':
return virutalbindVertexArrayOES;
}
return function(...args) {
makeCurrentContext(this._context);
resizeCanvasIfChanged(this._context);
return origFn.call(ext, ...args);
}
}
function virutalbindVertexArrayOES(vao) {
makeCurrentContext(this._context);
resizeCanvasIfChanged(this._context);
if (!vao) {
vao = this._context._defaultVertexArray;
}
sharedVAOExtension.bindVertexArrayOES(vao);
}
function makeANGLE_instanced_arraysWrapper(ext, origFn, name) {
switch (name) {
case 'drawArraysInstancedANGLE':
case 'drawElementsInstancedANGLE':
return createExtDrawWrapper(ext, origFn);
break;
}
return function(...args) {
makeCurrentContext(this._context);
resizeCanvasIfChanged(this._context);
return origFn.call(ext, ...args);
}
}
function createExtDrawWrapper(ext, origFn) {
return function(...args) {
// a rendering function was called so we need to copy are drawingBuffer
// to the canvas for this context after the current event.
beforeDraw(this._context);
const result = origFn.call(exts, ...args);
afterDraw(this._context);
return result;
};
}
function makeWEBGL_draw_buffersWrapper(ext, origFn, name) {
const gl = sharedWebGLContext;
const backBuffer = [gl.COLOR_ATTACHMENT0];
return function(drawingBuffers) {
makeCurrentContext(this._context);
resizeCanvasIfChanged(this._context);
// if the virtual context is bound to canvas then fake it
if (isFramebufferBindingNull(this._context)) {
// this really isn't checking everything
// for example if the user passed in array.length != 1
// then we are supposed to generate an error
if (drawingBuffers[0] === gl.BACK) {
drawingBuffers = backBuffer;
}
}
origFn.call(ext, drawingBuffers);
}
}
function makeWEBGL_drawBuffersSaveRestoreHelper(ext) {
const gl = sharedWebGLContext;
const maxDrawBuffers = gl.getParameter(ext.MAX_DRAW_BUFFERS_WEBGL);
const defaultBackDrawBuffers = [gl.BACK];
const defaultFBDrawBuffers = [gl.COLOR_ATTACHMENT0];
for (let i = 1; i < maxDrawBuffers; ++i) {
defaultFBDrawBuffers.push(gl.NONE);
}
function initDrawBufferState(state) {
state.drawBuffers = state.framebuffer ? defaultFBDrawBuffers.slice() : defaultBackDrawBuffers.slice();
}
function save(state) {
if (!state.drawBuffers) {
initDrawBufferState(state);
}
// remember state.framebuffer is only null for the offscreen canvas
const drawBuffers = state.drawBuffers;
for (let i = 0; i < drawBuffers.length; ++i) {
drawBuffers[i] = gl.getParameter(ext.DRAW_BUFFER0_WEBGL + i);
}
}
function restore(state) {
if (!state.drawBuffers) {
initDrawBufferState(state);
}
ext.drawBuffersWEBGL(state.drawBuffers);
}
return {
save,
restore,
};
}
function virtualGetContextAttributes() {
return this._contextAttributes;
}
function virtualReadPixels(...args) {
makeCurrentContext(this);
resizeCanvasIfChanged(this);
clearIfNeeded(this);
const gl = sharedWebGLContext;
return gl.readPixels(...args);
}
function virtualGetParameter(pname) {
makeCurrentContext(this);
resizeCanvasIfChanged(this);
const gl = sharedWebGLContext;
const value = gl.getParameter(pname);
switch (pname) {
case gl.FRAMEBUFFER_BINDING:
if (value === this._drawingbufferFramebuffer) {
return null;
}
break;
case 0x8825: // DRAW_BUFFER0_WEBGL
if (isFramebufferBindingNull(this)) {
if (value === gl.COLOR_ATTACHMENT0) {
return gl.BACK;
}
}
break;
case 0x85B5: // VERTEX_ARRAY_BINDING_OES
if (value === this._defaultVertexArray) {
return null;
}
break;
}
return value;
}
function virtualBindFramebuffer(bindpoint, framebuffer) {
makeCurrentContext(this);
resizeCanvasIfChanged(this);
const gl = sharedWebGLContext;
if (bindpoint === WebGLRenderingContext.FRAMEBUFFER) {
if (framebuffer === null) {
// bind our drawingBuffer
framebuffer = this._drawingbufferFramebuffer;
}
}
gl.bindFramebuffer(bindpoint, framebuffer);
this._state.framebuffer = framebuffer;
}
function createWrapper(origFn) {
// lots of optimization could happen here depending on specific functions
return function(...args) {
makeCurrentContext(this);
resizeCanvasIfChanged(this);
return origFn.call(sharedWebGLContext, ...args);
};
}
function clearIfNeeded(vctx) {
if (vctx._needClear) {
vctx._needClear = false;
const gl = sharedWebGLContext;
gl.bindFramebuffer(gl.FRAMEBUFFER, vctx._drawingbufferFramebuffer);
gl.disable(gl.SCISSOR_TEST);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT);
enableDisable(gl, gl.SCISSOR_TEST, vctx._state.scissorTest);
gl.bindFramebuffer(gl.FRAMEBUFFER, vctx._state.framebuffer);
}
}
function beforeDraw(vctx) {
makeCurrentContext(vctx);
resizeCanvasIfChanged(vctx);
clearIfNeeded(vctx);
}
function afterDraw(vctx) {
if (isFramebufferBindingNull(vctx)) {
vctx._needComposite = true;
if (!someContextsNeedRendering) {
someContextsNeedRendering = true;
setTimeout(renderAllDirtyVirtualCanvases, 0);
}
}
}
function createDrawWrapper(origFn) {
return function(...args) {
// a rendering function was called so we need to copy are drawingBuffer
// to the canvas for this context after the current event.
beforeDraw(this);
const result = origFn.call(sharedWebGLContext, ...args);
afterDraw(this);
return result;
};
}
function makeCurrentContext(vctx) {
if (currentVirtualContext === vctx) {
return;
}
// save all current WebGL state on the previous current virtual context
if (currentVirtualContext) {
saveAllState(currentVirtualContext._state, currentVirtualContext);
}
// restore all state for the new context
restoreAllState(vctx._state, vctx);
// check if the current state is supposed to be rendering to the canvas.
// if so bind vctx._drawingbuffer
currentVirtualContext = vctx;
}
function resizeCanvasIfChanged(vctx) {
const width = vctx.canvas.width;
const height = vctx.canvas.height;
if (width !== vctx._width || height !== vctx._height) {
vctx._width = width;
vctx._height = height;
const gl = sharedWebGLContext;
const oldTexture = gl.getParameter(gl.TEXTURE_BINDING_2D);
const format = vctx._contextAttributes.alpha ? gl.RGBA : gl.RGB;
gl.bindTexture(gl.TEXTURE_2D, vctx._drawingbufferTexture);
gl.texImage2D(gl.TEXTURE_2D, 0, format, width, height, 0, format, gl.UNSIGNED_BYTE, null);
gl.bindTexture(gl.TEXTURE_2D, oldTexture);
if (vctx._depthRenderbuffer) {
const oldRenderbuffer = gl.getParameter(gl.RENDERBUFFER_BINDING);
const internalFormat = vctx._contextAttributes.stencil ? gl.DEPTH_STENCIL : gl.DEPTH_COMPONENT16;
gl.bindRenderbuffer(gl.RENDERBUFFER, vctx._depthRenderbuffer);
gl.renderbufferStorage(gl.RENDERBUFFER, internalFormat, width, height);
gl.bindRenderbuffer(gl.RENDERBUFFER, oldRenderbuffer);
}
}
}
function createOrGetVirtualWebGLContext(canvas, type, contextAttributes) {
// check if this canvas already has a context
const existingVirtualCtx = canvasToVirtualContextMap.get(canvas);
if (existingVirtualCtx) {
return existingVirtualCtx;
}
const compositor = settings.compositorCreator(canvas, type, contextAttributes) || new DefaultCompositor(canvas, type, contextAttributes);
const newVirtualCtx = new VirtualWebGLContext(canvas, contextAttributes, compositor, () => {
canvasToVirtualContextMap.delete(canvas);
});
canvasToVirtualContextMap.set(canvas, newVirtualCtx);
return newVirtualCtx;
}
function createProgram(gl, shaderSources) {
const program = gl.createProgram();
[gl.VERTEX_SHADER, gl.FRAGMENT_SHADER].forEach((type, ndx) => {
const shader = gl.createShader(type);
gl.shaderSource(shader, shaderSources[ndx]);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error(gl.getShaderInfoLog(shader)); // eslint-disable-line
}
gl.attachShader(program, shader);
});
gl.bindAttribLocation(program, 0, 'position');
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.error(gl.getProgramInfoLog(program)); // eslint-disable-line
}
return program;
}
function saveAllState(state, vctx) {
// save all WebGL state (current bindings, current texture units,
// current attributes and/or vertex shade object, current program,
// current blend, stencil, zbuffer, culling, viewport etc... state
const gl = sharedWebGLContext;
state.activeTexture = gl.getParameter(gl.ACTIVE_TEXTURE);
// save texture units
for (let i = 0; i < numTextureUnits; ++i) {
gl.activeTexture(gl.TEXTURE0 + i);
const unit = state.textureUnits[i];
unit.texture2D = gl.getParameter(gl.TEXTURE_BINDING_2D);
unit.textureCubemap = gl.getParameter(gl.TEXTURE_BINDING_CUBE_MAP);
}
// bindings
state.arrayBuffer = gl.getParameter(gl.ARRAY_BUFFER_BINDING);
state.renderbuffer = gl.getParameter(gl.RENDERBUFFER_BINDING);
state.framebuffer = gl.getParameter(gl.FRAMEBUFFER_BINDING);
// save attributes
if (sharedVAOExtension) {
state.vertexArray = gl.getParameter(sharedVAOExtension.VERTEX_ARRAY_BINDING_OES);
} else {
for (let i = 0; i < numAttributes; ++i) {
const attrib = state.attributes[i];
attrib.buffer = gl.getVertexAttrib(i, gl.VERTEX_ATTRIB_ARRAY_BUFFER_BINDING);
attrib.enabled = gl.getVertexAttrib(i, gl.VERTEX_ATTRIB_ARRAY_ENABLED);
attrib.size = gl.getVertexAttrib(i, gl.VERTEX_ATTRIB_ARRAY_SIZE);
attrib.stride = gl.getVertexAttrib(i, gl.VERTEX_ATTRIB_ARRAY_STRIDE);
attrib.type = gl.getVertexAttrib(i, gl.VERTEX_ATTRIB_ARRAY_TYPE);
attrib.normalized = gl.getVertexAttrib(i, gl.VERTEX_ATTRIB_ARRAY_NORMALIZED);
attrib.value = gl.getVertexAttrib(i, gl.CURRENT_VERTEX_ATTRIB);
attrib.offset = gl.getVertexAttribOffset(i, gl.VERTEX_ATTRIB_ARRAY_POINTER);
if (sharedInstanceExtension) {
attrib.divisor = gl.getVertexAttrib(i, sharedInstanceExtension.VERTEX_ATTRIB_ARRAY_DIVISOR_ANGLE);
}
}
state.elementArrayBuffer = gl.getParameter(gl.ELEMENT_ARRAY_BUFFER_BINDING);
}
state.blend = gl.getParameter(gl.BLEND);
state.cullFace = gl.getParameter(gl.CULL_FACE);
state.depthTest = gl.getParameter(gl.DEPTH_TEST);
state.dither = gl.getParameter(gl.DITHER);
state.polygonOffsetFill = gl.getParameter(gl.POLYGON_OFFSET_FILL);
state.sampleAlphaToCoverage = gl.getParameter(gl.SAMPLE_ALPHA_TO_COVERAGE);
state.sampleCoverage = gl.getParameter(gl.SAMPLE_COVERAGE);
state.scissorTest = gl.getParameter(gl.SCISSOR_TEST);
state.stencilTest = gl.getParameter(gl.STENCIL_TEST);
state.packAlignment = gl.getParameter(gl.PACK_ALIGNMENT);
state.unpackAlignment = gl.getParameter(gl.UNPACK_ALIGNMENT);
state.unpackColorspaceConversion = gl.getParameter(gl.UNPACK_COLORSPACE_CONVERSION_WEBGL);
state.unpackFlipY = gl.getParameter(gl.UNPACK_FLIP_Y_WEBGL);
state.unpackPremultiplyAlpha = gl.getParameter(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL);
state.currentProgram = gl.getParameter(gl.CURRENT_PROGRAM);
state.viewport = gl.getParameter(gl.VIEWPORT);
state.scissor = gl.getParameter(gl.SCISSOR_BOX);
state.blendSrcRgb = gl.getParameter(gl.BLEND_SRC_RGB);
state.blendDstRgb = gl.getParameter(gl.BLEND_DST_RGB);
state.blendSrcAlpha = gl.getParameter(gl.BLEND_SRC_ALPHA);
state.blendDstAlpha = gl.getParameter(gl.BLEND_DST_ALPHA);
state.blendEquationRgb = gl.getParameter(gl.BLEND_EQUATION_RGB);
state.blendEquationAlpha = gl.getParameter(gl.BLEND_EQUATION_ALPHA);
state.blendColor = gl.getParameter(gl.BLEND_COLOR);
state.colorClearValue = gl.getParameter(gl.COLOR_CLEAR_VALUE);
state.colorMask = gl.getParameter(gl.COLOR_WRITEMASK);
state.cullFaceMode = gl.getParameter(gl.CULL_FACE_MODE);
state.depthClearValue = gl.getParameter(gl.DEPTH_CLEAR_VALUE);
state.depthFunc = gl.getParameter(gl.DEPTH_FUNC);
state.depthRange = gl.getParameter(gl.DEPTH_RANGE);
state.depthMask = gl.getParameter(gl.DEPTH_WRITEMASK);
state.frontFace = gl.getParameter(gl.FRONT_FACE);
state.generateMipmapHint = gl.getParameter(gl.GENERATE_MIPMAP_HINT);
state.lineWidth = gl.getParameter(gl.LINE_WIDTH);
state.polygonOffsetFactor = gl.getParameter(gl.POLYGON_OFFSET_FACTOR);
state.polygonOffsetUnits = gl.getParameter(gl.POLYGON_OFFSET_UNITS);
state.sampleCoverageValue = gl.getParameter(gl.SAMPLE_COVERAGE_VALUE);
state.sampleCoverageUnits = gl.getParameter(gl.SAMPLE_COVERAGE_INVERT);
state.stencilBackFail = gl.getParameter(gl.STENCIL_BACK_FAIL);
state.stencilBackFunc = gl.getParameter(gl.STENCIL_BACK_FUNC);
state.stencilBackPassDepthFail = gl.getParameter(gl.STENCIL_BACK_PASS_DEPTH_FAIL);
state.stencilBackPassDepthPass = gl.getParameter(gl.STENCIL_BACK_PASS_DEPTH_PASS);
state.stencilBackRef = gl.getParameter(gl.STENCIL_BACK_REF);
state.stencilBackValueMask = gl.getParameter(gl.STENCIL_BACK_VALUE_MASK);
state.stencilBackWriteMask = gl.getParameter(gl.STENCIL_BACK_WRITEMASK);
state.stencilClearValue = gl.getParameter(gl.STENCIL_CLEAR_VALUE);
state.stencilFail = gl.getParameter(gl.STENCIL_FAIL);
state.stencilFunc = gl.getParameter(gl.STENCIL_FUNC);
state.stencilPassDepthFail = gl.getParameter(gl.STENCIL_PASS_DEPTH_FAIL);
state.stencilPassDepthPass = gl.getParameter(gl.STENCIL_PASS_DEPTH_PASS);
state.stencilRef = gl.getParameter(gl.STENCIL_REF);
state.stencilValueMask = gl.getParameter(gl.STENCIL_VALUE_MASK);
state.stencilWriteMask = gl.getParameter(gl.STENCIL_WRITEMASK);
for (const fns of extensionSaveRestoreHelpersArray) {
fns.save(state, vctx);
}
}
function restoreAllState(state, vctx) {
// restore all WebGL state (current bindings, current texture units,
// current attributes and/or vertex shade object, current program,
// current blend, stencil, zbuffer, culling, viewport etc... state
// save all WebGL state (current bindings, current texture units,
// current attributes and/or vertex shade object, current program,
// current blend, stencil, zbuffer, culling, viewport etc... state
const gl = sharedWebGLContext;
// restore texture units
for (let i = 0; i < numTextureUnits; ++i) {
gl.activeTexture(gl.TEXTURE0 + i);
const unit = state.textureUnits[i];
gl.bindTexture(gl.TEXTURE_2D, unit.texture2D);
gl.bindTexture(gl.TEXTURE_CUBE_MAP, unit.textureCubemap);
}
gl.activeTexture(state.activeTexture);
// restore attributes
if (sharedVAOExtension) {
sharedVAOExtension.bindVertexArrayOES(state.vertexArray);
} else {
for (let i = 0; i < numAttributes; ++i) {
const attrib = state.attributes[i];
if (attrib.enabled) {
gl.enableVertexAttribArray(i);
} else {
gl.disableVertexAttribArray(i);
}
gl.vertexAttrib4fv(i, attrib.value);
gl.bindBuffer(gl.ARRAY_BUFFER, attrib.buffer);
gl.vertexAttribPointer(i, attrib.size, attrib.type, attrib.normalized, attrib.stride, attrib.offset);
if (sharedInstanceExtension) {
sharedInstanceExtension.vertexAttribDivisorANGLE(i, attrib.divisor);
}
}
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, state.elementArrayBuffer);
}
// bindings'
gl.bindBuffer(gl.ARRAY_BUFFER, state.arrayBuffer);
gl.bindRenderbuffer(gl.RENDERBUFFER, state.renderbuffer);
gl.bindFramebuffer(gl.FRAMEBUFFER, state.framebuffer);
enableDisable(gl, gl.BLEND, state.blend);
enableDisable(gl, gl.CULL_FACE, state.cullFace);
enableDisable(gl, gl.DEPTH_TEST, state.depthTest);
enableDisable(gl, gl.DITHER, state.dither);
enableDisable(gl, gl.POLYGON_OFFSET_FILL, state.polygonOffsetFill);
enableDisable(gl, gl.SAMPLE_ALPHA_TO_COVERAGE, state.sampleAlphaToCoverage);
enableDisable(gl, gl.SAMPLE_COVERAGE, state.sampleCoverage);
enableDisable(gl, gl.SCISSOR_TEST, state.scissorTest);
enableDisable(gl, gl.STENCIL_TEST, state.stencilTest);
gl.pixelStorei(gl.PACK_ALIGNMENT, state.packAlignment);
gl.pixelStorei(gl.UNPACK_ALIGNMENT, state.unpackAlignment);
gl.pixelStorei(gl.UNPACK_COLORSPACE_CONVERSION_WEBGL, state.unpackColorspaceConversion);
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, state.unpackFlipY);
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, state.unpackPremultiplyAlpha);
gl.useProgram(state.currentProgram);
gl.viewport(...state.viewport);
gl.scissor(...state.scissor);
gl.blendFuncSeparate(state.blendSrcRgb, state.blendDstRgb, state.blendSrcAlpha, state.blendDstAlpha);
gl.blendEquationSeparate(state.blendEquationRgb, state.blendEquationAlpha);
gl.blendColor(...state.blendColor);
gl.clearColor(...state.colorClearValue);
gl.colorMask(...state.colorMask);
gl.cullFace(state.cullFaceMode);
gl.clearDepth(state.depthClearValue);
gl.depthFunc(state.depthFunc);
gl.depthRange(...state.depthRange);
gl.depthMask(state.depthMask);
gl.frontFace(state.frontFace);
gl.hint(gl.GENERATE_MIPMAP_HINT, state.generateMipmapHint);
gl.lineWidth(state.lineWidth);
gl.polygonOffset(state.polygonOffsetFactor, state.polygonOffsetUnits);
gl.sampleCoverage(state.sampleCoverageValue, state.sampleCoverageUnits);
gl.stencilFuncSeparate(gl.BACK, state.stencilBackFunc, state.stencilBackRef, state.stencilBackValueMask);
gl.stencilFuncSeparate(gl.FRONT, state.stencilFunc, state.stencilRef, state.stencilValueMask);
gl.stencilOpSeparate(gl.BACK, state.stencilBackFail, state.stencilBackPassDepthFail, state.stencilBackPassDepthPass);
gl.stencilOpSeparate(gl.FRONT, state.stencilFail, state.stencilPassDepthFail, state.stencilPassDepthPass);
gl.stencilMaskSeparate(gl.BACK, state.stencilBackWriteMask);
gl.stencilMaskSeparate(gl.FRONT, state.stencilWriteMask);
gl.clearStencil(state.stencilClearValue);
for (const fns of extensionSaveRestoreHelpersArray) {
fns.restore(state, vctx);
}
}
function enableDisable(gl, feature, enable) {
if (enable) {
gl.enable(feature);
} else {
gl.disable(feature);
}
}
function renderAllDirtyVirtualCanvases() {
if (!someContextsNeedRendering) {
return;
}
someContextsNeedRendering = false;
// save all current WebGL state on the previous current virtual context
if (currentVirtualContext) {
saveAllState(currentVirtualContext._state, currentVirtualContext);
currentVirtualContext = null;
}
// set the state back to the one for drawing the canvas
restoreAllState(baseState);
for (const vctx of canvasToVirtualContextMap.values()) {
if (!vctx._needComposite) {
continue;
}
vctx._needComposite = false;
vctx.composite(sharedWebGLContext);
}
}
window.requestAnimationFrame = (function(origFn) {
return function(callback) {
return origFn.call(window, (time) => {
const result = callback(time);
renderAllDirtyVirtualCanvases();
return result;
});
};
}(window.requestAnimationFrame))
function setup(options) {
Object.assign(settings, options);
}
window.virtualWebGL = {
setup,
};
}());
Comments 11 条评论
你好,求教一下次元口袋阿里云盘里面的资源怎么下载呀?我点击之后是浏览器自动下载速度很慢,也没法添加到阿里云盘里。
@aiden 很久没维护了,你想要哪个文件我给你分享网盘链接吧
@QiuYeDx 07.超级马里奥 RPG(Super Mario RPG )整合版v1.0.1 谢谢您!
@aiden 【快传】我给你发了 07….0.1, 快来看看 https://www.alipan.com/t/qmnIakLoEWAUCBxB9Qwy 点击链接即可保存。「阿里云盘」APP ,无需下载极速在线查看,视频原画倍速播放。
@aiden 这是一条私密评论
@aiden 这是一条私密评论
这是一条私密评论
这是一条私密评论
这是一条私密评论
这是一条私密评论
这是一条私密评论