// luma.gl // SPDX-License-Identifier: MIT // Copyright (c) vis.gl contributors import { RenderPipeline, cast, splitUniformsAndBindings, log } from '@luma.gl/core'; import { mergeShaderLayout } from '@luma.gl/core'; // import {mergeShaderLayout, getAttributeInfosFromLayouts} from '@luma.gl/core'; import { GL } from '@luma.gl/constants'; import { getShaderLayout } from "../helpers/get-shader-layout.js"; import { withDeviceAndGLParameters } from "../converters/device-parameters.js"; import { setUniform } from "../helpers/set-uniform.js"; import { WEBGLBuffer } from "./webgl-buffer.js"; import { WEBGLFramebuffer } from "./webgl-framebuffer.js"; import { WEBGLTexture } from "./webgl-texture.js"; import { WEBGLTextureView } from "./webgl-texture-view.js"; import { getGLDrawMode } from "../helpers/webgl-topology-utils.js"; const LOG_PROGRAM_PERF_PRIORITY = 4; /** Creates a new render pipeline */ export class WEBGLRenderPipeline extends RenderPipeline { /** The WebGL device that created this render pipeline */ device; /** Handle to underlying WebGL program */ handle; /** vertex shader */ vs; /** fragment shader */ fs; /** The layout extracted from shader by WebGL introspection APIs */ introspectedLayout; /** Uniforms set on this model */ uniforms = {}; /** Bindings set on this model */ bindings = {}; /** WebGL varyings */ varyings = null; _uniformCount = 0; _uniformSetters = {}; // TODO are these used? constructor(device, props) { super(device, props); this.device = device; this.handle = this.props.handle || this.device.gl.createProgram(); this.device.setSpectorMetadata(this.handle, { id: this.props.id }); // Create shaders if needed this.vs = cast(props.vs); this.fs = cast(props.fs); // assert(this.vs.stage === 'vertex'); // assert(this.fs.stage === 'fragment'); // Setup varyings if supplied // @ts-expect-error WebGL only const { varyings, bufferMode = 35981 } = props; if (varyings && varyings.length > 0) { this.varyings = varyings; this.device.gl.transformFeedbackVaryings(this.handle, varyings, bufferMode); } this._linkShaders(); log.time(1, `RenderPipeline ${this.id} - shaderLayout introspection`)(); this.introspectedLayout = getShaderLayout(this.device.gl, this.handle); log.timeEnd(1, `RenderPipeline ${this.id} - shaderLayout introspection`)(); // Merge provided layout with introspected layout this.shaderLayout = mergeShaderLayout(this.introspectedLayout, props.shaderLayout); // WebGPU has more restrictive topology support than WebGL switch (this.props.topology) { case 'triangle-fan-webgl': case 'line-loop-webgl': log.warn(`Primitive topology ${this.props.topology} is deprecated and will be removed in v9.1`); break; default: } } destroy() { if (this.handle) { this.device.gl.deleteProgram(this.handle); // this.handle = null; this.destroyed = true; } } /** * Bindings include: textures, samplers and uniform buffers * @todo needed for portable model */ setBindings(bindings, options) { // if (log.priority >= 2) { // checkUniformValues(uniforms, this.id, this._uniformSetters); // } for (const [name, value] of Object.entries(bindings)) { // Accept both `xyz` and `xyzUniforms` as valid names for `xyzUniforms` uniform block // This convention allows shaders to name uniform blocks as `uniform appUniforms {} app;` // and reference them as `app` from both GLSL and JS. // TODO - this is rather hacky - we could also remap the name directly in the shader layout. const binding = this.shaderLayout.bindings.find(binding => binding.name === name) || this.shaderLayout.bindings.find(binding => binding.name === `${name}Uniforms`); if (!binding) { const validBindings = this.shaderLayout.bindings .map(binding => `"${binding.name}"`) .join(', '); if (!options?.disableWarnings) { log.warn(`Unknown binding "${name}" in render pipeline "${this.id}", expected one of ${validBindings}`)(); } continue; // eslint-disable-line no-continue } if (!value) { log.warn(`Unsetting binding "${name}" in render pipeline "${this.id}"`)(); } switch (binding.type) { case 'uniform': // @ts-expect-error if (!(value instanceof WEBGLBuffer) && !(value.buffer instanceof WEBGLBuffer)) { throw new Error('buffer value'); } break; case 'texture': if (!(value instanceof WEBGLTextureView || value instanceof WEBGLTexture || value instanceof WEBGLFramebuffer)) { throw new Error('texture value'); } break; case 'sampler': log.warn(`Ignoring sampler ${name}`)(); break; default: throw new Error(binding.type); } this.bindings[name] = value; } } /** @todo needed for portable model * @note The WebGL API is offers many ways to draw things * This function unifies those ways into a single call using common parameters with sane defaults */ draw(options) { const { renderPass, parameters = this.props.parameters, topology = this.props.topology, vertexArray, vertexCount, // indexCount, instanceCount, isInstanced = false, firstVertex = 0, // firstIndex, // firstInstance, // baseVertex, transformFeedback } = options; const glDrawMode = getGLDrawMode(topology); const isIndexed = Boolean(vertexArray.indexBuffer); const glIndexType = vertexArray.indexBuffer?.glIndexType; // Note that we sometimes get called with 0 instances // If we are using async linking, we need to wait until linking completes if (this.linkStatus !== 'success') { log.info(2, `RenderPipeline:${this.id}.draw() aborted - waiting for shader linking`)(); return false; } // Avoid WebGL draw call when not rendering any data or values are incomplete // Note: async textures set as uniforms might still be loading. // Now that all uniforms have been updated, check if any texture // in the uniforms is not yet initialized, then we don't draw if (!this._areTexturesRenderable() || vertexCount === 0) { log.info(2, `RenderPipeline:${this.id}.draw() aborted - textures not yet loaded`)(); return false; } // (isInstanced && instanceCount === 0) if (vertexCount === 0) { log.info(2, `RenderPipeline:${this.id}.draw() aborted - no vertices to draw`)(); return true; } this.device.gl.useProgram(this.handle); // Note: Rebinds constant attributes before each draw call vertexArray.bindBeforeRender(renderPass); if (transformFeedback) { transformFeedback.begin(this.props.topology); } // We have to apply bindings before every draw call since other draw calls will overwrite this._applyBindings(); this._applyUniforms(); const webglRenderPass = renderPass; withDeviceAndGLParameters(this.device, parameters, webglRenderPass.glParameters, () => { if (isIndexed && isInstanced) { this.device.gl.drawElementsInstanced(glDrawMode, vertexCount || 0, // indexCount? glIndexType, firstVertex, instanceCount || 0); // } else if (isIndexed && this.device.isWebGL2 && !isNaN(start) && !isNaN(end)) { // this.device.gldrawRangeElements(glDrawMode, start, end, vertexCount, glIndexType, offset); } else if (isIndexed) { this.device.gl.drawElements(glDrawMode, vertexCount || 0, glIndexType, firstVertex); // indexCount? } else if (isInstanced) { this.device.gl.drawArraysInstanced(glDrawMode, firstVertex, vertexCount || 0, instanceCount || 0); } else { this.device.gl.drawArrays(glDrawMode, firstVertex, vertexCount || 0); } if (transformFeedback) { transformFeedback.end(); } }); vertexArray.unbindAfterRender(renderPass); return true; } // DEPRECATED METHODS setUniformsWebGL(uniforms) { const { bindings } = splitUniformsAndBindings(uniforms); Object.keys(bindings).forEach(name => { log.warn(`Unsupported value "${JSON.stringify(bindings[name])}" used in setUniforms() for key ${name}. Use setBindings() instead?`)(); }); // TODO - check against layout Object.assign(this.uniforms, uniforms); } // PRIVATE METHODS // setAttributes(attributes: Record): void {} // setBindings(bindings: Record): void {} async _linkShaders() { const { gl } = this.device; gl.attachShader(this.handle, this.vs.handle); gl.attachShader(this.handle, this.fs.handle); log.time(LOG_PROGRAM_PERF_PRIORITY, `linkProgram for ${this.id}`)(); gl.linkProgram(this.handle); log.timeEnd(LOG_PROGRAM_PERF_PRIORITY, `linkProgram for ${this.id}`)(); // TODO Avoid checking program linking error in production if (log.level === 0) { // return; } if (!this.device.features.has('compilation-status-async-webgl')) { const status = this._getLinkStatus(); this._reportLinkStatus(status); return; } // async case log.once(1, 'RenderPipeline linking is asynchronous')(); await this._waitForLinkComplete(); log.info(2, `RenderPipeline ${this.id} - async linking complete: ${this.linkStatus}`)(); const status = this._getLinkStatus(); this._reportLinkStatus(status); } /** Report link status. First, check for shader compilation failures if linking fails */ _reportLinkStatus(status) { switch (status) { case 'success': return; default: // First check for shader compilation failures if linking fails if (this.vs.compilationStatus === 'error') { this.vs.debugShader(); throw new Error(`Error during compilation of shader ${this.vs.id}`); } if (this.fs?.compilationStatus === 'error') { this.fs.debugShader(); throw new Error(`Error during compilation of shader ${this.fs.id}`); } throw new Error(`Error during ${status}: ${this.device.gl.getProgramInfoLog(this.handle)}`); } } /** * Get the shader compilation status * TODO - Load log even when no error reported, to catch warnings? * https://gamedev.stackexchange.com/questions/30429/how-to-detect-glsl-warnings */ _getLinkStatus() { const { gl } = this.device; const linked = gl.getProgramParameter(this.handle, 35714); if (!linked) { this.linkStatus = 'error'; return 'linking'; } gl.validateProgram(this.handle); const validated = gl.getProgramParameter(this.handle, 35715); if (!validated) { this.linkStatus = 'error'; return 'validation'; } this.linkStatus = 'success'; return 'success'; } /** Use KHR_parallel_shader_compile extension if available */ async _waitForLinkComplete() { const waitMs = async (ms) => await new Promise(resolve => setTimeout(resolve, ms)); const DELAY_MS = 10; // Shader compilation is typically quite fast (with some exceptions) // If status polling is not available, we can't wait for completion. Just wait a little to minimize blocking if (!this.device.features.has('compilation-status-async-webgl')) { await waitMs(DELAY_MS); return; } const { gl } = this.device; for (;;) { const complete = gl.getProgramParameter(this.handle, 37297); if (complete) { return; } await waitMs(DELAY_MS); } } /** * Checks if all texture-values uniforms are renderable (i.e. loaded) * Update a texture if needed (e.g. from video) * Note: This is currently done before every draw call */ _areTexturesRenderable() { let texturesRenderable = true; for (const [, texture] of Object.entries(this.bindings)) { if (texture instanceof WEBGLTexture) { texture.update(); texturesRenderable = texturesRenderable && texture.loaded; } } return texturesRenderable; } /** Apply any bindings (before each draw call) */ _applyBindings() { // If we are using async linking, we need to wait until linking completes if (this.linkStatus !== 'success') { return; } const { gl } = this.device; gl.useProgram(this.handle); let textureUnit = 0; let uniformBufferIndex = 0; for (const binding of this.shaderLayout.bindings) { // Accept both `xyz` and `xyzUniforms` as valid names for `xyzUniforms` uniform block const value = this.bindings[binding.name] || this.bindings[binding.name.replace(/Uniforms$/, '')]; if (!value) { throw new Error(`No value for binding ${binding.name} in ${this.id}`); } switch (binding.type) { case 'uniform': // Set buffer const { name } = binding; const location = gl.getUniformBlockIndex(this.handle, name); if (location === 4294967295) { throw new Error(`Invalid uniform block name ${name}`); } gl.uniformBlockBinding(this.handle, uniformBufferIndex, location); // console.debug(binding, location); if (value instanceof WEBGLBuffer) { gl.bindBufferBase(35345, uniformBufferIndex, value.handle); } else { gl.bindBufferRange(35345, uniformBufferIndex, // @ts-expect-error value.buffer.handle, // @ts-expect-error value.offset || 0, // @ts-expect-error value.size || value.buffer.byteLength - value.offset); } uniformBufferIndex += 1; break; case 'texture': if (!(value instanceof WEBGLTextureView || value instanceof WEBGLTexture || value instanceof WEBGLFramebuffer)) { throw new Error('texture'); } let texture; if (value instanceof WEBGLTextureView) { texture = value.texture; } else if (value instanceof WEBGLTexture) { texture = value; } else if (value instanceof WEBGLFramebuffer && value.colorAttachments[0] instanceof WEBGLTextureView) { log.warn('Passing framebuffer in texture binding may be deprecated. Use fbo.colorAttachments[0] instead')(); texture = value.colorAttachments[0].texture; } else { throw new Error('No texture'); } gl.activeTexture(33984 + textureUnit); gl.bindTexture(texture.target, texture.handle); // gl.bindSampler(textureUnit, sampler.handle); textureUnit += 1; break; case 'sampler': // ignore break; case 'storage': case 'read-only-storage': throw new Error(`binding type '${binding.type}' not supported in WebGL`); } } } /** * Due to program sharing, uniforms need to be reset before every draw call * (though caching will avoid redundant WebGL calls) */ _applyUniforms() { for (const uniformLayout of this.shaderLayout.uniforms || []) { const { name, location, type, textureUnit } = uniformLayout; const value = this.uniforms[name] ?? textureUnit; if (value !== undefined) { setUniform(this.device.gl, location, type, value); } } } }