context.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  1. /**
  2. * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  3. *
  4. * @author Alexander Rose <alexander.rose@weirdbyte.de>
  5. */
  6. import { GLRenderingContext, isWebGL2 } from './compat';
  7. import { checkFramebufferStatus, Framebuffer } from './framebuffer';
  8. import { Scheduler } from '../../mol-task';
  9. import { isDebugMode } from '../../mol-util/debug';
  10. import { createExtensions, WebGLExtensions } from './extensions';
  11. import { WebGLState, createState } from './state';
  12. import { PixelData } from '../../mol-util/image';
  13. import { WebGLResources, createResources } from './resources';
  14. import { RenderTarget, createRenderTarget } from './render-target';
  15. import { BehaviorSubject } from 'rxjs';
  16. import { now } from '../../mol-util/now';
  17. import { Texture, TextureFilter } from './texture';
  18. import { ComputeRenderable } from '../renderable';
  19. import { createTimer, WebGLTimer } from './timer';
  20. export function getGLContext(canvas: HTMLCanvasElement, attribs?: WebGLContextAttributes & { preferWebGl1?: boolean }): GLRenderingContext | null {
  21. function get(id: 'webgl' | 'experimental-webgl' | 'webgl2') {
  22. try {
  23. return canvas.getContext(id, attribs) as GLRenderingContext | null;
  24. } catch (e) {
  25. return null;
  26. }
  27. }
  28. const gl = (attribs?.preferWebGl1 ? null : get('webgl2')) || get('webgl') || get('experimental-webgl');
  29. if (isDebugMode) console.log(`isWebgl2: ${isWebGL2(gl)}`);
  30. return gl;
  31. }
  32. export function getErrorDescription(gl: GLRenderingContext, error: number) {
  33. switch (error) {
  34. case gl.NO_ERROR: return 'no error';
  35. case gl.INVALID_ENUM: return 'invalid enum';
  36. case gl.INVALID_VALUE: return 'invalid value';
  37. case gl.INVALID_OPERATION: return 'invalid operation';
  38. case gl.INVALID_FRAMEBUFFER_OPERATION: return 'invalid framebuffer operation';
  39. case gl.OUT_OF_MEMORY: return 'out of memory';
  40. case gl.CONTEXT_LOST_WEBGL: return 'context lost';
  41. }
  42. return 'unknown error';
  43. }
  44. export function checkError(gl: GLRenderingContext) {
  45. const error = gl.getError();
  46. if (error !== gl.NO_ERROR) {
  47. throw new Error(`WebGL error: '${getErrorDescription(gl, error)}'`);
  48. }
  49. }
  50. function unbindResources(gl: GLRenderingContext) {
  51. // bind null to all texture units
  52. const maxTextureImageUnits = gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS);
  53. for (let i = 0; i < maxTextureImageUnits; ++i) {
  54. gl.activeTexture(gl.TEXTURE0 + i);
  55. gl.bindTexture(gl.TEXTURE_2D, null);
  56. gl.bindTexture(gl.TEXTURE_CUBE_MAP, null);
  57. if (isWebGL2(gl)) {
  58. gl.bindTexture(gl.TEXTURE_2D_ARRAY, null);
  59. gl.bindTexture(gl.TEXTURE_3D, null);
  60. }
  61. }
  62. // assign the smallest possible buffer to all attributes
  63. const buf = gl.createBuffer();
  64. gl.bindBuffer(gl.ARRAY_BUFFER, buf);
  65. const maxVertexAttribs = gl.getParameter(gl.MAX_VERTEX_ATTRIBS);
  66. for (let i = 0; i < maxVertexAttribs; ++i) {
  67. gl.vertexAttribPointer(i, 1, gl.FLOAT, false, 0, 0);
  68. }
  69. // bind null to all buffers
  70. gl.bindBuffer(gl.ARRAY_BUFFER, null);
  71. gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
  72. gl.bindRenderbuffer(gl.RENDERBUFFER, null);
  73. unbindFramebuffer(gl);
  74. }
  75. function unbindFramebuffer(gl: GLRenderingContext) {
  76. gl.bindFramebuffer(gl.FRAMEBUFFER, null);
  77. }
  78. const tmpPixel = new Uint8Array(1 * 4);
  79. function checkSync(gl: WebGL2RenderingContext, sync: WebGLSync, resolve: () => void) {
  80. if (gl.getSyncParameter(sync, gl.SYNC_STATUS) === gl.SIGNALED) {
  81. gl.deleteSync(sync);
  82. resolve();
  83. } else {
  84. Scheduler.setImmediate(checkSync, gl, sync, resolve);
  85. }
  86. }
  87. function fence(gl: WebGL2RenderingContext, resolve: () => void) {
  88. const sync = gl.fenceSync(gl.SYNC_GPU_COMMANDS_COMPLETE, 0);
  89. if (!sync) {
  90. console.warn('Could not create a WebGLSync object');
  91. gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, tmpPixel);
  92. resolve();
  93. } else {
  94. Scheduler.setImmediate(checkSync, gl, sync, resolve);
  95. }
  96. }
  97. let SentWebglSyncObjectNotSupportedInWebglMessage = false;
  98. function waitForGpuCommandsComplete(gl: GLRenderingContext): Promise<void> {
  99. return new Promise(resolve => {
  100. if (isWebGL2(gl)) {
  101. // TODO seems quite slow
  102. fence(gl, resolve);
  103. } else {
  104. if (!SentWebglSyncObjectNotSupportedInWebglMessage) {
  105. console.info('Sync object not supported in WebGL');
  106. SentWebglSyncObjectNotSupportedInWebglMessage = true;
  107. }
  108. waitForGpuCommandsCompleteSync(gl);
  109. resolve();
  110. }
  111. });
  112. }
  113. function waitForGpuCommandsCompleteSync(gl: GLRenderingContext): void {
  114. gl.bindFramebuffer(gl.FRAMEBUFFER, null);
  115. gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, tmpPixel);
  116. }
  117. export function readPixels(gl: GLRenderingContext, x: number, y: number, width: number, height: number, buffer: Uint8Array | Float32Array | Int32Array) {
  118. if (isDebugMode) checkFramebufferStatus(gl);
  119. if (buffer instanceof Uint8Array) {
  120. gl.readPixels(x, y, width, height, gl.RGBA, gl.UNSIGNED_BYTE, buffer);
  121. } else if (buffer instanceof Float32Array) {
  122. gl.readPixels(x, y, width, height, gl.RGBA, gl.FLOAT, buffer);
  123. } else if (buffer instanceof Int32Array && isWebGL2(gl)) {
  124. gl.readPixels(x, y, width, height, gl.RGBA_INTEGER, gl.INT, buffer);
  125. } else {
  126. throw new Error('unsupported readPixels buffer type');
  127. }
  128. if (isDebugMode) checkError(gl);
  129. }
  130. function getDrawingBufferPixelData(gl: GLRenderingContext) {
  131. const w = gl.drawingBufferWidth;
  132. const h = gl.drawingBufferHeight;
  133. const buffer = new Uint8Array(w * h * 4);
  134. unbindFramebuffer(gl);
  135. gl.viewport(0, 0, w, h);
  136. readPixels(gl, 0, 0, w, h, buffer);
  137. return PixelData.flipY(PixelData.create(buffer, w, h));
  138. }
  139. //
  140. function createStats() {
  141. return {
  142. resourceCounts: {
  143. attribute: 0,
  144. elements: 0,
  145. framebuffer: 0,
  146. program: 0,
  147. renderbuffer: 0,
  148. shader: 0,
  149. texture: 0,
  150. cubeTexture: 0,
  151. vertexArray: 0,
  152. },
  153. drawCount: 0,
  154. instanceCount: 0,
  155. instancedDrawCount: 0,
  156. };
  157. }
  158. export type WebGLStats = ReturnType<typeof createStats>
  159. //
  160. /** A WebGL context object, including the rendering context, resource caches and counts */
  161. export interface WebGLContext {
  162. readonly gl: GLRenderingContext
  163. readonly isWebGL2: boolean
  164. readonly pixelRatio: number
  165. readonly extensions: WebGLExtensions
  166. readonly state: WebGLState
  167. readonly stats: WebGLStats
  168. readonly resources: WebGLResources
  169. readonly timer: WebGLTimer
  170. readonly maxTextureSize: number
  171. readonly max3dTextureSize: number
  172. readonly maxRenderbufferSize: number
  173. readonly maxDrawBuffers: number
  174. readonly maxTextureImageUnits: number
  175. readonly isContextLost: boolean
  176. readonly contextRestored: BehaviorSubject<now.Timestamp>
  177. setContextLost: () => void
  178. handleContextRestored: (extraResets?: () => void) => void
  179. /** Cache for compute renderables, managed by consumers */
  180. readonly namedComputeRenderables: { [name: string]: ComputeRenderable<any> }
  181. /** Cache for frambuffers, managed by consumers */
  182. readonly namedFramebuffers: { [name: string]: Framebuffer }
  183. /** Cache for textures, managed by consumers */
  184. readonly namedTextures: { [name: string]: Texture }
  185. createRenderTarget: (width: number, height: number, depth?: boolean, type?: 'uint8' | 'float32' | 'fp16', filter?: TextureFilter) => RenderTarget
  186. unbindFramebuffer: () => void
  187. readPixels: (x: number, y: number, width: number, height: number, buffer: Uint8Array | Float32Array | Int32Array) => void
  188. readPixelsAsync: (x: number, y: number, width: number, height: number, buffer: Uint8Array) => Promise<void>
  189. waitForGpuCommandsComplete: () => Promise<void>
  190. waitForGpuCommandsCompleteSync: () => void
  191. getDrawingBufferPixelData: () => PixelData
  192. clear: (red: number, green: number, blue: number, alpha: number) => void
  193. destroy: (options?: Partial<{ doNotForceWebGLContextLoss: boolean }>) => void
  194. }
  195. export function createContext(gl: GLRenderingContext, props: Partial<{ pixelScale: number }> = {}): WebGLContext {
  196. const extensions = createExtensions(gl);
  197. const state = createState(gl);
  198. const stats = createStats();
  199. const resources = createResources(gl, state, stats, extensions);
  200. const timer = createTimer(gl, extensions);
  201. const parameters = {
  202. maxTextureSize: gl.getParameter(gl.MAX_TEXTURE_SIZE) as number,
  203. max3dTextureSize: isWebGL2(gl) ? gl.getParameter(gl.MAX_3D_TEXTURE_SIZE) as number : 0,
  204. maxRenderbufferSize: gl.getParameter(gl.MAX_RENDERBUFFER_SIZE) as number,
  205. maxDrawBuffers: extensions.drawBuffers ? gl.getParameter(extensions.drawBuffers.MAX_DRAW_BUFFERS) as number : 0,
  206. maxTextureImageUnits: gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS) as number,
  207. maxVertexTextureImageUnits: gl.getParameter(gl.MAX_VERTEX_TEXTURE_IMAGE_UNITS) as number,
  208. };
  209. if (parameters.maxVertexTextureImageUnits < 8) {
  210. throw new Error('Need "MAX_VERTEX_TEXTURE_IMAGE_UNITS" >= 8');
  211. }
  212. let isContextLost = false;
  213. const contextRestored = new BehaviorSubject<now.Timestamp>(0 as now.Timestamp);
  214. let readPixelsAsync: (x: number, y: number, width: number, height: number, buffer: Uint8Array) => Promise<void>;
  215. if (isWebGL2(gl)) {
  216. const pbo = gl.createBuffer();
  217. let _buffer: Uint8Array | undefined = void 0;
  218. let _resolve: (() => void) | undefined = void 0;
  219. let _reading = false;
  220. const bindPBO = () => {
  221. gl.bindBuffer(gl.PIXEL_PACK_BUFFER, pbo);
  222. gl.getBufferSubData(gl.PIXEL_PACK_BUFFER, 0, _buffer!);
  223. gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null);
  224. _reading = false;
  225. _resolve!();
  226. _resolve = void 0;
  227. _buffer = void 0;
  228. };
  229. readPixelsAsync = (x: number, y: number, width: number, height: number, buffer: Uint8Array): Promise<void> => new Promise<void>((resolve, reject) => {
  230. if (_reading) {
  231. reject('Can not call multiple readPixelsAsync at the same time');
  232. return;
  233. }
  234. _reading = true;
  235. gl.bindBuffer(gl.PIXEL_PACK_BUFFER, pbo);
  236. gl.bufferData(gl.PIXEL_PACK_BUFFER, width * height * 4, gl.STREAM_READ);
  237. gl.readPixels(x, y, width, height, gl.RGBA, gl.UNSIGNED_BYTE, 0);
  238. gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null);
  239. // need to unbind/bind PBO before/after async awaiting the fence
  240. _resolve = resolve;
  241. _buffer = buffer;
  242. fence(gl, bindPBO);
  243. });
  244. } else {
  245. readPixelsAsync = async (x: number, y: number, width: number, height: number, buffer: Uint8Array) => {
  246. readPixels(gl, x, y, width, height, buffer);
  247. };
  248. }
  249. const renderTargets = new Set<RenderTarget>();
  250. return {
  251. gl,
  252. isWebGL2: isWebGL2(gl),
  253. get pixelRatio() {
  254. const dpr = (typeof window !== 'undefined') ? (window.devicePixelRatio || 1) : 1;
  255. return dpr * (props.pixelScale || 1);
  256. },
  257. extensions,
  258. state,
  259. stats,
  260. resources,
  261. timer,
  262. get maxTextureSize() { return parameters.maxTextureSize; },
  263. get max3dTextureSize() { return parameters.max3dTextureSize; },
  264. get maxRenderbufferSize() { return parameters.maxRenderbufferSize; },
  265. get maxDrawBuffers() { return parameters.maxDrawBuffers; },
  266. get maxTextureImageUnits() { return parameters.maxTextureImageUnits; },
  267. namedComputeRenderables: Object.create(null),
  268. namedFramebuffers: Object.create(null),
  269. namedTextures: Object.create(null),
  270. get isContextLost() {
  271. return isContextLost || gl.isContextLost();
  272. },
  273. contextRestored,
  274. setContextLost: () => {
  275. isContextLost = true;
  276. },
  277. handleContextRestored: (extraResets?: () => void) => {
  278. Object.assign(extensions, createExtensions(gl));
  279. state.reset();
  280. state.currentMaterialId = -1;
  281. state.currentProgramId = -1;
  282. state.currentRenderItemId = -1;
  283. resources.reset();
  284. renderTargets.forEach(rt => rt.reset());
  285. extraResets?.();
  286. isContextLost = false;
  287. contextRestored.next(now());
  288. },
  289. createRenderTarget: (width: number, height: number, depth?: boolean, type?: 'uint8' | 'float32' | 'fp16', filter?: TextureFilter) => {
  290. const renderTarget = createRenderTarget(gl, resources, width, height, depth, type, filter);
  291. renderTargets.add(renderTarget);
  292. return {
  293. ...renderTarget,
  294. destroy: () => {
  295. renderTarget.destroy();
  296. renderTargets.delete(renderTarget);
  297. }
  298. };
  299. },
  300. unbindFramebuffer: () => unbindFramebuffer(gl),
  301. readPixels: (x: number, y: number, width: number, height: number, buffer: Uint8Array | Float32Array | Int32Array) => {
  302. readPixels(gl, x, y, width, height, buffer);
  303. },
  304. readPixelsAsync,
  305. waitForGpuCommandsComplete: () => waitForGpuCommandsComplete(gl),
  306. waitForGpuCommandsCompleteSync: () => waitForGpuCommandsCompleteSync(gl),
  307. getDrawingBufferPixelData: () => getDrawingBufferPixelData(gl),
  308. clear: (red: number, green: number, blue: number, alpha: number) => {
  309. unbindFramebuffer(gl);
  310. state.enable(gl.SCISSOR_TEST);
  311. state.depthMask(true);
  312. state.colorMask(true, true, true, true);
  313. state.clearColor(red, green, blue, alpha);
  314. gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
  315. gl.scissor(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
  316. gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
  317. },
  318. destroy: (options?: Partial<{ doNotForceWebGLContextLoss: boolean }>) => {
  319. resources.destroy();
  320. unbindResources(gl);
  321. // to aid GC
  322. if (!options?.doNotForceWebGLContextLoss) {
  323. gl.getExtension('WEBGL_lose_context')?.loseContext();
  324. gl.getExtension('STACKGL_destroy_context')?.destroy();
  325. }
  326. }
  327. };
  328. }