context.ts 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505
  1. /**
  2. * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  3. *
  4. * @author Alexander Rose <alexander.rose@weirdbyte.de>
  5. */
  6. import { createProgramCache, ProgramCache } from './program'
  7. import { createShaderCache, ShaderCache } from './shader'
  8. import { GLRenderingContext, COMPAT_instanced_arrays, COMPAT_standard_derivatives, COMPAT_vertex_array_object, getInstancedArrays, getStandardDerivatives, getVertexArrayObject, isWebGL2, COMPAT_element_index_uint, getElementIndexUint, COMPAT_texture_float, getTextureFloat, COMPAT_texture_float_linear, getTextureFloatLinear, COMPAT_blend_minmax, getBlendMinMax, getFragDepth, COMPAT_frag_depth, COMPAT_color_buffer_float, getColorBufferFloat, COMPAT_draw_buffers, getDrawBuffers, getShaderTextureLod, COMPAT_shader_texture_lod, getDepthTexture, COMPAT_depth_texture } from './compat';
  9. import { createFramebufferCache, FramebufferCache, checkFramebufferStatus } from './framebuffer';
  10. import { Scheduler } from 'mol-task';
  11. import { isDebugMode } from 'mol-util/debug';
  12. export function getGLContext(canvas: HTMLCanvasElement, contextAttributes?: WebGLContextAttributes): GLRenderingContext | null {
  13. function getContext(contextId: 'webgl' | 'experimental-webgl' | 'webgl2') {
  14. try {
  15. return canvas.getContext(contextId, contextAttributes) as GLRenderingContext | null
  16. } catch (e) {
  17. return null
  18. }
  19. }
  20. return getContext('webgl2') || getContext('webgl') || getContext('experimental-webgl')
  21. }
  22. function getPixelRatio() {
  23. return (typeof window !== 'undefined') ? window.devicePixelRatio : 1
  24. }
  25. function getErrorDescription(gl: GLRenderingContext, error: number) {
  26. switch (error) {
  27. case gl.NO_ERROR: return 'no error'
  28. case gl.INVALID_ENUM: return 'invalid enum'
  29. case gl.INVALID_VALUE: return 'invalid value'
  30. case gl.INVALID_OPERATION: return 'invalid operation'
  31. case gl.INVALID_FRAMEBUFFER_OPERATION: return 'invalid framebuffer operation'
  32. case gl.OUT_OF_MEMORY: return 'out of memory'
  33. case gl.CONTEXT_LOST_WEBGL: return 'context lost'
  34. }
  35. return 'unknown error'
  36. }
  37. export function checkError(gl: GLRenderingContext) {
  38. const error = gl.getError()
  39. if (error) throw new Error(`WebGL error: '${getErrorDescription(gl, error)}'`)
  40. }
  41. function unbindResources (gl: GLRenderingContext) {
  42. // bind null to all texture units
  43. const maxTextureImageUnits = gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS)
  44. for (let i = 0; i < maxTextureImageUnits; ++i) {
  45. gl.activeTexture(gl.TEXTURE0 + i)
  46. gl.bindTexture(gl.TEXTURE_2D, null)
  47. gl.bindTexture(gl.TEXTURE_CUBE_MAP, null)
  48. if (isWebGL2(gl)) {
  49. gl.bindTexture(gl.TEXTURE_2D_ARRAY, null)
  50. gl.bindTexture(gl.TEXTURE_3D, null)
  51. }
  52. }
  53. // assign the smallest possible buffer to all attributes
  54. const buf = gl.createBuffer();
  55. gl.bindBuffer(gl.ARRAY_BUFFER, buf);
  56. const maxVertexAttribs = gl.getParameter(gl.MAX_VERTEX_ATTRIBS);
  57. for (let i = 0; i < maxVertexAttribs; ++i) {
  58. gl.vertexAttribPointer(i, 1, gl.FLOAT, false, 0, 0);
  59. }
  60. // bind null to all buffers
  61. gl.bindBuffer(gl.ARRAY_BUFFER, null)
  62. gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null)
  63. gl.bindRenderbuffer(gl.RENDERBUFFER, null)
  64. unbindFramebuffer(gl)
  65. }
  66. function unbindFramebuffer(gl: GLRenderingContext) {
  67. gl.bindFramebuffer(gl.FRAMEBUFFER, null)
  68. }
  69. const tmpPixel = new Uint8Array(1 * 4);
  70. function checkSync(gl: WebGL2RenderingContext, sync: WebGLSync, resolve: () => void) {
  71. if (gl.getSyncParameter(sync, gl.SYNC_STATUS) === gl.SIGNALED) {
  72. gl.deleteSync(sync)
  73. resolve()
  74. } else {
  75. Scheduler.setImmediate(checkSync, gl, sync, resolve)
  76. }
  77. }
  78. function fence(gl: WebGL2RenderingContext, resolve: () => void) {
  79. const sync = gl.fenceSync(gl.SYNC_GPU_COMMANDS_COMPLETE, 0)
  80. if (!sync) {
  81. console.warn('Could not create a WebGLSync object')
  82. gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, tmpPixel)
  83. resolve()
  84. } else {
  85. Scheduler.setImmediate(checkSync, gl, sync, resolve)
  86. }
  87. }
  88. let SentWebglSyncObjectNotSupportedInWebglMessage = false
  89. function waitForGpuCommandsComplete(gl: GLRenderingContext): Promise<void> {
  90. return new Promise(resolve => {
  91. if (isWebGL2(gl)) {
  92. // TODO seems quite slow
  93. fence(gl, resolve)
  94. } else {
  95. if (!SentWebglSyncObjectNotSupportedInWebglMessage) {
  96. console.info('Sync object not supported in WebGL')
  97. SentWebglSyncObjectNotSupportedInWebglMessage = true
  98. }
  99. waitForGpuCommandsCompleteSync(gl)
  100. resolve()
  101. }
  102. })
  103. }
  104. function waitForGpuCommandsCompleteSync(gl: GLRenderingContext): void {
  105. gl.bindFramebuffer(gl.FRAMEBUFFER, null)
  106. gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, tmpPixel)
  107. }
  108. function readPixels(gl: GLRenderingContext, x: number, y: number, width: number, height: number, buffer: Uint8Array | Float32Array) {
  109. if (isDebugMode) checkFramebufferStatus(gl)
  110. if (buffer instanceof Uint8Array) {
  111. gl.readPixels(x, y, width, height, gl.RGBA, gl.UNSIGNED_BYTE, buffer)
  112. } else if (buffer instanceof Float32Array) {
  113. gl.readPixels(x, y, width, height, gl.RGBA, gl.FLOAT, buffer)
  114. } else {
  115. throw new Error('unsupported readPixels buffer type')
  116. }
  117. if (isDebugMode) checkError(gl)
  118. }
  119. export function createImageData(buffer: ArrayLike<number>, width: number, height: number) {
  120. const w = width * 4
  121. const h = height
  122. const data = new Uint8ClampedArray(width * height * 4)
  123. for (let i = 0, maxI = h / 2; i < maxI; ++i) {
  124. for (let j = 0, maxJ = w; j < maxJ; ++j) {
  125. const index1 = i * w + j;
  126. const index2 = (h-i-1) * w + j;
  127. data[index1] = buffer[index2];
  128. data[index2] = buffer[index1];
  129. }
  130. }
  131. return new ImageData(data, width, height);
  132. }
  133. //
  134. export type WebGLExtensions = {
  135. instancedArrays: COMPAT_instanced_arrays
  136. standardDerivatives: COMPAT_standard_derivatives
  137. blendMinMax: COMPAT_blend_minmax
  138. textureFloat: COMPAT_texture_float
  139. textureFloatLinear: COMPAT_texture_float_linear
  140. elementIndexUint: COMPAT_element_index_uint
  141. depthTexture: COMPAT_depth_texture
  142. vertexArrayObject: COMPAT_vertex_array_object | null
  143. fragDepth: COMPAT_frag_depth | null
  144. colorBufferFloat: COMPAT_color_buffer_float | null
  145. drawBuffers: COMPAT_draw_buffers | null
  146. shaderTextureLod: COMPAT_shader_texture_lod | null
  147. }
  148. function createExtensions(gl: GLRenderingContext): WebGLExtensions {
  149. const instancedArrays = getInstancedArrays(gl)
  150. if (instancedArrays === null) {
  151. throw new Error('Could not find support for "instanced_arrays"')
  152. }
  153. const standardDerivatives = getStandardDerivatives(gl)
  154. if (standardDerivatives === null) {
  155. throw new Error('Could not find support for "standard_derivatives"')
  156. }
  157. const blendMinMax = getBlendMinMax(gl)
  158. if (blendMinMax === null) {
  159. throw new Error('Could not find support for "blend_minmax"')
  160. }
  161. const textureFloat = getTextureFloat(gl)
  162. if (textureFloat === null) {
  163. throw new Error('Could not find support for "texture_float"')
  164. }
  165. const textureFloatLinear = getTextureFloatLinear(gl)
  166. if (textureFloatLinear === null) {
  167. throw new Error('Could not find support for "texture_float_linear"')
  168. }
  169. const elementIndexUint = getElementIndexUint(gl)
  170. if (elementIndexUint === null) {
  171. throw new Error('Could not find support for "element_index_uint"')
  172. }
  173. const depthTexture = getDepthTexture(gl)
  174. if (depthTexture === null) {
  175. throw new Error('Could not find support for "depth_texture"')
  176. }
  177. const vertexArrayObject = getVertexArrayObject(gl)
  178. if (vertexArrayObject === null) {
  179. console.log('Could not find support for "vertex_array_object"')
  180. }
  181. const fragDepth = getFragDepth(gl)
  182. if (fragDepth === null) {
  183. console.log('Could not find support for "frag_depth"')
  184. }
  185. const colorBufferFloat = getColorBufferFloat(gl)
  186. if (colorBufferFloat === null) {
  187. console.log('Could not find support for "color_buffer_float"')
  188. }
  189. const drawBuffers = getDrawBuffers(gl)
  190. if (drawBuffers === null) {
  191. console.log('Could not find support for "draw_buffers"')
  192. }
  193. const shaderTextureLod = getShaderTextureLod(gl)
  194. if (shaderTextureLod === null) {
  195. console.log('Could not find support for "shader_texture_lod"')
  196. }
  197. return {
  198. instancedArrays,
  199. standardDerivatives,
  200. blendMinMax,
  201. textureFloat,
  202. textureFloatLinear,
  203. elementIndexUint,
  204. depthTexture,
  205. vertexArrayObject,
  206. fragDepth,
  207. colorBufferFloat,
  208. drawBuffers,
  209. shaderTextureLod,
  210. }
  211. }
  212. export type WebGLStats = {
  213. bufferCount: number
  214. framebufferCount: number
  215. renderbufferCount: number
  216. textureCount: number
  217. vaoCount: number
  218. drawCount: number
  219. instanceCount: number
  220. instancedDrawCount: number
  221. }
  222. function createStats(): WebGLStats {
  223. return {
  224. bufferCount: 0,
  225. framebufferCount: 0,
  226. renderbufferCount: 0,
  227. textureCount: 0,
  228. vaoCount: 0,
  229. drawCount: 0,
  230. instanceCount: 0,
  231. instancedDrawCount: 0,
  232. }
  233. }
  234. export type WebGLState = {
  235. currentProgramId: number
  236. currentMaterialId: number
  237. currentRenderItemId: number
  238. enable: (cap: number) => void
  239. disable: (cap: number) => void
  240. frontFace: (mode: number) => void
  241. cullFace: (mode: number) => void
  242. depthMask: (flag: boolean) => void
  243. colorMask: (red: boolean, green: boolean, blue: boolean, alpha: boolean) => void
  244. clearColor: (red: number, green: number, blue: number, alpha: number) => void
  245. blendFunc: (src: number, dst: number) => void
  246. blendFuncSeparate: (srcRGB: number, dstRGB: number, srcAlpha: number, dstAlpha: number) => void
  247. blendEquation: (mode: number) => void
  248. blendEquationSeparate: (modeRGB: number, modeAlpha: number) => void
  249. }
  250. function createState(gl: GLRenderingContext): WebGLState {
  251. const enabledCapabilities: { [k: number]: boolean } = {}
  252. let currentFrontFace = gl.getParameter(gl.FRONT_FACE)
  253. let currentCullFace = gl.getParameter(gl.CULL_FACE_MODE)
  254. let currentDepthMask = gl.getParameter(gl.DEPTH_WRITEMASK)
  255. let currentColorMask = gl.getParameter(gl.COLOR_WRITEMASK)
  256. let currentClearColor = gl.getParameter(gl.COLOR_CLEAR_VALUE)
  257. let currentBlendSrcRGB = gl.getParameter(gl.BLEND_SRC_RGB)
  258. let currentBlendDstRGB = gl.getParameter(gl.BLEND_DST_RGB)
  259. let currentBlendSrcAlpha = gl.getParameter(gl.BLEND_SRC_ALPHA)
  260. let currentBlendDstAlpha = gl.getParameter(gl.BLEND_DST_ALPHA)
  261. let currentBlendEqRGB = gl.getParameter(gl.BLEND_EQUATION_RGB)
  262. let currentBlendEqAlpha = gl.getParameter(gl.BLEND_EQUATION_ALPHA)
  263. return {
  264. currentProgramId: -1,
  265. currentMaterialId: -1,
  266. currentRenderItemId: -1,
  267. enable: (cap: number) => {
  268. if (enabledCapabilities[cap] !== true ) {
  269. gl.enable(cap)
  270. enabledCapabilities[cap] = true
  271. }
  272. },
  273. disable: (cap: number) => {
  274. if (enabledCapabilities[cap] !== false) {
  275. gl.disable(cap)
  276. enabledCapabilities[cap] = false
  277. }
  278. },
  279. frontFace: (mode: number) => {
  280. if (mode !== currentFrontFace) {
  281. gl.frontFace(mode)
  282. currentFrontFace = mode
  283. }
  284. },
  285. cullFace: (mode: number) => {
  286. if (mode !== currentCullFace) {
  287. gl.cullFace(mode)
  288. currentCullFace = mode
  289. }
  290. },
  291. depthMask: (flag: boolean) => {
  292. if (flag !== currentDepthMask) {
  293. gl.depthMask(flag)
  294. currentDepthMask = flag
  295. }
  296. },
  297. colorMask: (red: boolean, green: boolean, blue: boolean, alpha: boolean) => {
  298. if (red !== currentColorMask[0] || green !== currentColorMask[1] || blue !== currentColorMask[2] || alpha !== currentColorMask[3])
  299. gl.colorMask(red, green, blue, alpha)
  300. currentColorMask[0] = red
  301. currentColorMask[1] = green
  302. currentColorMask[2] = blue
  303. currentColorMask[3] = alpha
  304. },
  305. clearColor: (red: number, green: number, blue: number, alpha: number) => {
  306. if (red !== currentClearColor[0] || green !== currentClearColor[1] || blue !== currentClearColor[2] || alpha !== currentClearColor[3])
  307. gl.clearColor(red, green, blue, alpha)
  308. currentClearColor[0] = red
  309. currentClearColor[1] = green
  310. currentClearColor[2] = blue
  311. currentClearColor[3] = alpha
  312. },
  313. blendFunc: (src: number, dst: number) => {
  314. if (src !== currentBlendSrcRGB || dst !== currentBlendDstRGB || src !== currentBlendSrcAlpha || dst !== currentBlendDstAlpha) {
  315. gl.blendFunc(src, dst)
  316. currentBlendSrcRGB = src
  317. currentBlendDstRGB = dst
  318. currentBlendSrcAlpha = src
  319. currentBlendDstAlpha = dst
  320. }
  321. },
  322. blendFuncSeparate: (srcRGB: number, dstRGB: number, srcAlpha: number, dstAlpha: number) => {
  323. if (srcRGB !== currentBlendSrcRGB || dstRGB !== currentBlendDstRGB || srcAlpha !== currentBlendSrcAlpha || dstAlpha !== currentBlendDstAlpha) {
  324. gl.blendFuncSeparate(srcRGB, dstRGB, srcAlpha, dstAlpha)
  325. currentBlendSrcRGB = srcRGB
  326. currentBlendDstRGB = dstRGB
  327. currentBlendSrcAlpha = srcAlpha
  328. currentBlendDstAlpha = dstAlpha
  329. }
  330. },
  331. blendEquation: (mode: number) => {
  332. if (mode !== currentBlendEqRGB || mode !== currentBlendEqAlpha) {
  333. gl.blendEquation(mode)
  334. currentBlendEqRGB = mode
  335. currentBlendEqAlpha = mode
  336. }
  337. },
  338. blendEquationSeparate: (modeRGB: number, modeAlpha: number) => {
  339. if (modeRGB !== currentBlendEqRGB || modeAlpha !== currentBlendEqAlpha) {
  340. gl.blendEquationSeparate(modeRGB, modeAlpha)
  341. currentBlendEqRGB = modeRGB
  342. currentBlendEqAlpha = modeAlpha
  343. }
  344. }
  345. }
  346. }
  347. /** A WebGL context object, including the rendering context, resource caches and counts */
  348. export interface WebGLContext {
  349. readonly gl: GLRenderingContext
  350. readonly isWebGL2: boolean
  351. readonly pixelRatio: number
  352. readonly extensions: WebGLExtensions
  353. readonly state: WebGLState
  354. readonly stats: WebGLStats
  355. readonly shaderCache: ShaderCache
  356. readonly programCache: ProgramCache
  357. readonly framebufferCache: FramebufferCache
  358. readonly maxTextureSize: number
  359. readonly maxDrawBuffers: number
  360. unbindFramebuffer: () => void
  361. readPixels: (x: number, y: number, width: number, height: number, buffer: Uint8Array | Float32Array) => void
  362. readPixelsAsync: (x: number, y: number, width: number, height: number, buffer: Uint8Array) => Promise<void>
  363. waitForGpuCommandsComplete: () => Promise<void>
  364. waitForGpuCommandsCompleteSync: () => void
  365. destroy: () => void
  366. }
  367. export function createContext(gl: GLRenderingContext): WebGLContext {
  368. const extensions = createExtensions(gl)
  369. const state = createState(gl)
  370. const stats = createStats()
  371. const shaderCache: ShaderCache = createShaderCache(gl)
  372. const programCache: ProgramCache = createProgramCache(gl, state, extensions, shaderCache)
  373. const framebufferCache: FramebufferCache = createFramebufferCache(gl, stats)
  374. const parameters = {
  375. maxTextureSize: gl.getParameter(gl.MAX_TEXTURE_SIZE) as number,
  376. maxDrawBuffers: isWebGL2(gl) ? gl.getParameter(gl.MAX_DRAW_BUFFERS) as number : 0,
  377. maxVertexTextureImageUnits: gl.getParameter(gl.MAX_VERTEX_TEXTURE_IMAGE_UNITS) as number,
  378. }
  379. if (parameters.maxVertexTextureImageUnits < 8) {
  380. throw new Error('Need "MAX_VERTEX_TEXTURE_IMAGE_UNITS" >= 8')
  381. }
  382. let readPixelsAsync: (x: number, y: number, width: number, height: number, buffer: Uint8Array) => Promise<void>
  383. if (isWebGL2(gl)) {
  384. const pbo = gl.createBuffer()
  385. let _buffer: Uint8Array | undefined = void 0
  386. let _resolve: (() => void) | undefined = void 0
  387. let _reading = false
  388. const bindPBO = () => {
  389. gl.bindBuffer(gl.PIXEL_PACK_BUFFER, pbo)
  390. gl.getBufferSubData(gl.PIXEL_PACK_BUFFER, 0, _buffer!)
  391. gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null)
  392. _reading = false
  393. _resolve!()
  394. _resolve = void 0
  395. _buffer = void 0
  396. }
  397. readPixelsAsync = (x: number, y: number, width: number, height: number, buffer: Uint8Array): Promise<void> => new Promise<void>((resolve, reject) => {
  398. if (_reading) {
  399. reject('Can not call multiple readPixelsAsync at the same time')
  400. return
  401. }
  402. _reading = true;
  403. gl.bindBuffer(gl.PIXEL_PACK_BUFFER, pbo)
  404. gl.bufferData(gl.PIXEL_PACK_BUFFER, width * height * 4, gl.STREAM_READ)
  405. gl.readPixels(x, y, width, height, gl.RGBA, gl.UNSIGNED_BYTE, 0)
  406. gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null)
  407. // need to unbind/bind PBO before/after async awaiting the fence
  408. _resolve = resolve
  409. _buffer = buffer
  410. fence(gl, bindPBO)
  411. })
  412. } else {
  413. readPixelsAsync = async (x: number, y: number, width: number, height: number, buffer: Uint8Array) => {
  414. readPixels(gl, x, y, width, height, buffer)
  415. }
  416. }
  417. return {
  418. gl,
  419. isWebGL2: isWebGL2(gl),
  420. get pixelRatio () {
  421. // this can change during the lifetime of a rendering context, so need to re-obtain on access
  422. return getPixelRatio()
  423. },
  424. extensions,
  425. state,
  426. stats,
  427. shaderCache,
  428. programCache,
  429. framebufferCache,
  430. get maxTextureSize () { return parameters.maxTextureSize },
  431. get maxDrawBuffers () { return parameters.maxDrawBuffers },
  432. unbindFramebuffer: () => unbindFramebuffer(gl),
  433. readPixels: (x: number, y: number, width: number, height: number, buffer: Uint8Array | Float32Array) => {
  434. readPixels(gl, x, y, width, height, buffer)
  435. },
  436. readPixelsAsync,
  437. waitForGpuCommandsComplete: () => waitForGpuCommandsComplete(gl),
  438. waitForGpuCommandsCompleteSync: () => waitForGpuCommandsCompleteSync(gl),
  439. destroy: () => {
  440. unbindResources(gl)
  441. programCache.dispose()
  442. shaderCache.dispose()
  443. framebufferCache.dispose()
  444. // TODO destroy buffers and textures
  445. }
  446. }
  447. }