context.ts 14 KB

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