UNPKG

molstar

Version:

A comprehensive macromolecular library.

430 lines (429 loc) 17 kB
/** * Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Alexander Rose <alexander.rose@weirdbyte.de> */ import { isWebGL2 } from './compat.js'; import { checkFramebufferStatus, createNullFramebuffer } from './framebuffer.js'; import { Scheduler } from '../../mol-task/index.js'; import { isDebugMode } from '../../mol-util/debug.js'; import { createExtensions, resetExtensions } from './extensions.js'; import { createState } from './state.js'; import { createResources } from './resources.js'; import { createRenderTarget } from './render-target.js'; import { Subject } from 'rxjs'; import { now } from '../../mol-util/now.js'; import { createNullTexture } from './texture.js'; import { createTimer } from './timer.js'; export function getGLContext(canvas, attribs) { function get(id) { try { return canvas.getContext(id, attribs); } catch (e) { return null; } } const gl = ((attribs === null || attribs === void 0 ? void 0 : attribs.preferWebGl1) ? null : get('webgl2')) || get('webgl') || get('experimental-webgl'); if (isDebugMode) console.log(`isWebgl2: ${isWebGL2(gl)}`); return gl; } export function getErrorDescription(gl, error) { switch (error) { case gl.NO_ERROR: return 'no error'; case gl.INVALID_ENUM: return 'invalid enum'; case gl.INVALID_VALUE: return 'invalid value'; case gl.INVALID_OPERATION: return 'invalid operation'; case gl.INVALID_FRAMEBUFFER_OPERATION: return 'invalid framebuffer operation'; case gl.OUT_OF_MEMORY: return 'out of memory'; case gl.CONTEXT_LOST_WEBGL: return 'context lost'; } return 'unknown error'; } export function checkError(gl, message) { const error = gl.getError(); if (error !== gl.NO_ERROR) { console.log(`WebGL error: '${getErrorDescription(gl, error)}'${message ? ` (${message})` : ''}`); // throw new Error(`WebGL error: '${getErrorDescription(gl, error)}'${message ? ` (${message})` : ''}`); } } export function glEnumToString(gl, value) { const keys = []; for (const key in gl) { if (gl[key] === value) { keys.push(key); } } return keys.length ? keys.join(' | ') : `0x${value.toString(16)}`; } function unbindResources(gl) { // bind null to all texture units const maxTextureImageUnits = gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS); for (let i = 0; i < maxTextureImageUnits; ++i) { gl.activeTexture(gl.TEXTURE0 + i); gl.bindTexture(gl.TEXTURE_2D, null); gl.bindTexture(gl.TEXTURE_CUBE_MAP, null); if (isWebGL2(gl)) { gl.bindTexture(gl.TEXTURE_2D_ARRAY, null); gl.bindTexture(gl.TEXTURE_3D, null); } } // assign the smallest possible buffer to all attributes const buf = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, buf); const maxVertexAttribs = gl.getParameter(gl.MAX_VERTEX_ATTRIBS); for (let i = 0; i < maxVertexAttribs; ++i) { gl.vertexAttribPointer(i, 1, gl.FLOAT, false, 0, 0); } // bind null to all buffers gl.bindBuffer(gl.ARRAY_BUFFER, null); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null); gl.bindRenderbuffer(gl.RENDERBUFFER, null); gl.bindFramebuffer(gl.FRAMEBUFFER, null); if (isWebGL2(gl)) { gl.bindBuffer(gl.UNIFORM_BUFFER, null); gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null); } } const tmpPixel = new Uint8Array(1 * 4); function checkSync(gl, sync, resolve) { if (gl.getSyncParameter(sync, gl.SYNC_STATUS) === gl.SIGNALED) { gl.deleteSync(sync); resolve(); } else { Scheduler.setImmediate(checkSync, gl, sync, resolve); } } function fence(gl, resolve) { const sync = gl.fenceSync(gl.SYNC_GPU_COMMANDS_COMPLETE, 0); if (!sync) { console.warn('Could not create a WebGLSync object'); gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, tmpPixel); resolve(); } else { Scheduler.setImmediate(checkSync, gl, sync, resolve); } } let SentWebglSyncObjectNotSupportedInWebglMessage = false; function waitForGpuCommandsComplete(gl) { return new Promise(resolve => { if (isWebGL2(gl)) { fence(gl, resolve); } else { if (!SentWebglSyncObjectNotSupportedInWebglMessage) { console.info('Sync object not supported in WebGL'); SentWebglSyncObjectNotSupportedInWebglMessage = true; } waitForGpuCommandsCompleteSync(gl); resolve(); } }); } function waitForGpuCommandsCompleteSync(gl) { gl.bindFramebuffer(gl.FRAMEBUFFER, null); gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, tmpPixel); } export function readPixels(gl, x, y, width, height, buffer) { if (isDebugMode) checkFramebufferStatus(gl); if (buffer instanceof Uint8Array) { gl.readPixels(x, y, width, height, gl.RGBA, gl.UNSIGNED_BYTE, buffer); } else if (buffer instanceof Float32Array) { gl.readPixels(x, y, width, height, gl.RGBA, gl.FLOAT, buffer); } else if (buffer instanceof Int32Array && isWebGL2(gl)) { gl.readPixels(x, y, width, height, gl.RGBA_INTEGER, gl.INT, buffer); } else { throw new Error('unsupported readPixels buffer type'); } if (isDebugMode) checkError(gl); } function bindDrawingBuffer(gl, xrLayer) { if (xrLayer) { gl.bindFramebuffer(gl.FRAMEBUFFER, xrLayer.framebuffer); } else { gl.bindFramebuffer(gl.FRAMEBUFFER, null); } } function getDrawingBufferSize(gl, xrLayer, xrInteractionMode) { var _a, _b; let width = (_a = xrLayer === null || xrLayer === void 0 ? void 0 : xrLayer.framebufferWidth) !== null && _a !== void 0 ? _a : gl.drawingBufferWidth; if (xrInteractionMode === 'screen-space') { // workaround so XR with a single view behaves simlar to two views width *= 2; } const height = (_b = xrLayer === null || xrLayer === void 0 ? void 0 : xrLayer.framebufferHeight) !== null && _b !== void 0 ? _b : gl.drawingBufferHeight; return { width, height }; } function getShaderPrecisionFormat(gl, shader, precision, type) { const glShader = shader === 'vertex' ? gl.VERTEX_SHADER : gl.FRAGMENT_SHADER; const glPrecisionType = gl[`${precision.toUpperCase()}_${type.toUpperCase()}`]; return gl.getShaderPrecisionFormat(glShader, glPrecisionType); } function getShaderPrecisionFormats(gl, shader) { return { lowFloat: getShaderPrecisionFormat(gl, shader, 'low', 'float'), mediumFloat: getShaderPrecisionFormat(gl, shader, 'medium', 'float'), highFloat: getShaderPrecisionFormat(gl, shader, 'high', 'float'), lowInt: getShaderPrecisionFormat(gl, shader, 'low', 'int'), mediumInt: getShaderPrecisionFormat(gl, shader, 'medium', 'int'), highInt: getShaderPrecisionFormat(gl, shader, 'high', 'int'), }; } // function createStats() { const stats = { resourceCounts: { attribute: 0, elements: 0, pixelPack: 0, framebuffer: 0, program: 0, renderbuffer: 0, shader: 0, texture: 0, cubeTexture: 0, vertexArray: 0, }, drawCount: 0, instanceCount: 0, instancedDrawCount: 0, calls: { drawInstanced: 0, drawInstancedBase: 0, multiDrawInstancedBase: 0, counts: 0, }, culled: { lod: 0, frustum: 0, occlusion: 0, }, }; return stats; } // function createParameters(gl, extensions) { return { maxTextureSize: gl.getParameter(gl.MAX_TEXTURE_SIZE), max3dTextureSize: isWebGL2(gl) ? gl.getParameter(gl.MAX_3D_TEXTURE_SIZE) : 0, maxRenderbufferSize: gl.getParameter(gl.MAX_RENDERBUFFER_SIZE), maxDrawBuffers: extensions.drawBuffers ? gl.getParameter(extensions.drawBuffers.MAX_DRAW_BUFFERS) : 0, maxTextureImageUnits: gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), maxVertexTextureImageUnits: gl.getParameter(gl.MAX_VERTEX_TEXTURE_IMAGE_UNITS), }; } export function createContext(gl, props = {}) { const extensions = createExtensions(gl); const state = createState(gl, extensions); const stats = createStats(); const parameters = createParameters(gl, extensions); const resources = createResources(gl, state, stats, extensions, parameters); const timer = createTimer(gl, extensions, stats); if (parameters.maxVertexTextureImageUnits < 8) { throw new Error('Need "MAX_VERTEX_TEXTURE_IMAGE_UNITS" >= 8'); } const shaderPrecisionFormats = { vertex: getShaderPrecisionFormats(gl, 'vertex'), fragment: getShaderPrecisionFormats(gl, 'fragment'), }; if (isDebugMode) { console.log({ parameters, shaderPrecisionFormats }); } // optimize assuming flats first and last data are same or differences don't matter // extension is only available when `FIRST_VERTEX_CONVENTION` is more efficient const epv = extensions.provokingVertex; epv === null || epv === void 0 ? void 0 : epv.provokingVertex(epv.FIRST_VERTEX_CONVENTION); let isContextLost = false; const contextRestored = new Subject(); let pixelScale = props.pixelScale || 1; const xr = { session: undefined, layer: undefined, changed: new Subject(), clear: () => { xr.layer = undefined; xr.session = undefined; xr.changed.next(); } }; const renderTargets = new Set(); return { gl, isWebGL2: isWebGL2(gl), get pixelRatio() { const dpr = (typeof window !== 'undefined') ? (window.devicePixelRatio || 1) : 1; return dpr * (pixelScale || 1); }, extensions, state, stats, resources, timer, get maxTextureSize() { return parameters.maxTextureSize; }, get max3dTextureSize() { return parameters.max3dTextureSize; }, get maxRenderbufferSize() { return parameters.maxRenderbufferSize; }, get maxDrawBuffers() { return parameters.maxDrawBuffers; }, get maxTextureImageUnits() { return parameters.maxTextureImageUnits; }, get shaderPrecisionFormats() { return shaderPrecisionFormats; }, namedComputeRenderables: Object.create(null), namedFramebuffers: Object.create(null), namedTextures: Object.create(null), get isContextLost() { return isContextLost || gl.isContextLost(); }, contextRestored, setContextLost: () => { isContextLost = true; timer.clear(); }, handleContextRestored: (extraResets) => { resetExtensions(gl, extensions); state.reset(); state.currentMaterialId = -1; state.currentProgramId = -1; state.currentRenderItemId = -1; resources.reset(); renderTargets.forEach(rt => rt.reset()); extraResets === null || extraResets === void 0 ? void 0 : extraResets(); isContextLost = false; contextRestored.next(now()); }, xr: { get session() { return xr.session; }, changed: xr.changed, set: async (session, options) => { var _a, _b; if (xr.session === session) return; await ((_a = xr.session) === null || _a === void 0 ? void 0 : _a.end()); if (session === undefined) return; try { await gl.makeXRCompatible(); xr.session = session; xr.layer = new XRWebGLLayer(xr.session, gl, { antialias: true, alpha: true, depth: true, framebufferScaleFactor: pixelScale * ((_b = options === null || options === void 0 ? void 0 : options.resolutionScale) !== null && _b !== void 0 ? _b : 1), }); await xr.session.updateRenderState({ baseLayer: xr.layer }); xr.session.addEventListener('end', xr.clear); xr.changed.next(); } catch (err) { if (session) { await session.end(); } else { xr.layer = undefined; xr.session = undefined; } throw err; } }, end: async () => { var _a; return (_a = xr.session) === null || _a === void 0 ? void 0 : _a.end(); }, }, setPixelScale: (value) => { pixelScale = value; }, createRenderTarget: (width, height, depth, type, filter, format) => { const renderTarget = createRenderTarget(gl, resources, width, height, depth, type, filter, format); renderTargets.add(renderTarget); return { ...renderTarget, destroy: () => { renderTarget.destroy(); renderTargets.delete(renderTarget); } }; }, createDrawTarget: () => { return { id: -1, texture: createNullTexture(gl), framebuffer: createNullFramebuffer(), depthRenderbuffer: null, getByteCount: () => 0, getWidth: () => { var _a; return getDrawingBufferSize(gl, xr.layer, (_a = xr.session) === null || _a === void 0 ? void 0 : _a.interactionMode).width; }, getHeight: () => { var _a; return getDrawingBufferSize(gl, xr.layer, (_a = xr.session) === null || _a === void 0 ? void 0 : _a.interactionMode).height; }, bind: () => { bindDrawingBuffer(gl, xr.layer); }, setSize: () => { }, reset: () => { }, destroy: () => { } }; }, bindDrawingBuffer: () => bindDrawingBuffer(gl, xr.layer), getDrawingBufferSize: () => { var _a; return getDrawingBufferSize(gl, xr.layer, (_a = xr.session) === null || _a === void 0 ? void 0 : _a.interactionMode); }, readPixels: (x, y, width, height, buffer) => { readPixels(gl, x, y, width, height, buffer); }, waitForGpuCommandsComplete: () => waitForGpuCommandsComplete(gl), waitForGpuCommandsCompleteSync: () => waitForGpuCommandsCompleteSync(gl), getFenceSync: () => { return isWebGL2(gl) ? gl.fenceSync(gl.SYNC_GPU_COMMANDS_COMPLETE, 0) : null; }, checkSyncStatus: (sync) => { if (!isWebGL2(gl)) return true; if (gl.getSyncParameter(sync, gl.SYNC_STATUS) === gl.SIGNALED) { gl.deleteSync(sync); return true; } else { return false; } }, deleteSync: (sync) => { if (isWebGL2(gl)) gl.deleteSync(sync); }, clear: (red, green, blue, alpha) => { const drs = getDrawingBufferSize(gl, xr.layer); bindDrawingBuffer(gl, xr.layer); state.enable(gl.SCISSOR_TEST); state.depthMask(true); state.colorMask(true, true, true, true); state.clearColor(red, green, blue, alpha); state.viewport(0, 0, drs.width, drs.height); state.scissor(0, 0, drs.width, drs.height); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); }, checkError: (message) => { checkError(gl, message); }, checkFramebufferStatus: (message) => { checkFramebufferStatus(gl, message); }, destroy: (options) => { var _a, _b, _c, _d; resources.destroy(); unbindResources(gl); (_a = xr.session) === null || _a === void 0 ? void 0 : _a.removeEventListener('end', xr.clear); (_b = xr.session) === null || _b === void 0 ? void 0 : _b.end(); contextRestored.complete(); xr.changed.complete(); // to aid GC if (!(options === null || options === void 0 ? void 0 : options.doNotForceWebGLContextLoss)) { (_c = gl.getExtension('WEBGL_lose_context')) === null || _c === void 0 ? void 0 : _c.loseContext(); (_d = gl.getExtension('STACKGL_destroy_context')) === null || _d === void 0 ? void 0 : _d.destroy(); } } }; }