Source: Renderer/ShaderProgram.js

/*global define*/
define([
        '../Core/defaultValue',
        '../Core/defined',
        '../Core/defineProperties',
        '../Core/destroyObject',
        '../Core/DeveloperError',
        '../Core/RuntimeError',
        './AutomaticUniforms',
        './ContextLimits',
        './createUniform',
        './createUniformArray'
    ], function(
        defaultValue,
        defined,
        defineProperties,
        destroyObject,
        DeveloperError,
        RuntimeError,
        AutomaticUniforms,
        ContextLimits,
        createUniform,
        createUniformArray) {
    'use strict';

    var nextShaderProgramId = 0;

    /**
     * @private
     */
    function ShaderProgram(options) {
        var modifiedFS = handleUniformPrecisionMismatches(options.vertexShaderText, options.fragmentShaderText);

        this._gl = options.gl;
        this._logShaderCompilation = options.logShaderCompilation;
        this._debugShaders = options.debugShaders;
        this._attributeLocations = options.attributeLocations;

        this._program = undefined;
        this._numberOfVertexAttributes = undefined;
        this._vertexAttributes = undefined;
        this._uniformsByName = undefined;
        this._uniforms = undefined;
        this._automaticUniforms = undefined;
        this._manualUniforms = undefined;
        this._duplicateUniformNames = modifiedFS.duplicateUniformNames;
        this._cachedShader = undefined; // Used by ShaderCache

        /**
         * @private
         */
        this.maximumTextureUnitIndex = undefined;

        this._vertexShaderSource = options.vertexShaderSource;
        this._vertexShaderText = options.vertexShaderText;
        this._fragmentShaderSource = options.fragmentShaderSource;
        this._fragmentShaderText = modifiedFS.fragmentShaderText;

        /**
         * @private
         */
        this.id = nextShaderProgramId++;
    }

    ShaderProgram.fromCache = function(options) {
        options = defaultValue(options, defaultValue.EMPTY_OBJECT);

        //>>includeStart('debug', pragmas.debug);
        if (!defined(options.context)) {
            throw new DeveloperError('options.context is required.');
        }
        //>>includeEnd('debug');

        return options.context.shaderCache.getShaderProgram(options);
    };

    ShaderProgram.replaceCache = function(options) {
        options = defaultValue(options, defaultValue.EMPTY_OBJECT);

        //>>includeStart('debug', pragmas.debug);
        if (!defined(options.context)) {
            throw new DeveloperError('options.context is required.');
        }
        //>>includeEnd('debug');

        return options.context.shaderCache.replaceShaderProgram(options);
    };

    defineProperties(ShaderProgram.prototype, {
        /**
         * GLSL source for the shader program's vertex shader.
         * @memberof ShaderProgram.prototype
         *
         * @type {ShaderSource}
         * @readonly
         */
        vertexShaderSource : {
            get : function() {
                return this._vertexShaderSource;
            }
        },
        /**
         * GLSL source for the shader program's fragment shader.
         * @memberof ShaderProgram.prototype
         *
         * @type {ShaderSource}
         * @readonly
         */
        fragmentShaderSource : {
            get : function() {
                return this._fragmentShaderSource;
            }
        },
        vertexAttributes : {
            get : function() {
                initialize(this);
                return this._vertexAttributes;
            }
        },
        numberOfVertexAttributes : {
            get : function() {
                initialize(this);
                return this._numberOfVertexAttributes;
            }
        },
        allUniforms : {
            get : function() {
                initialize(this);
                return this._uniformsByName;
            }
        }
    });

    function extractUniforms(shaderText) {
        var uniformNames = [];
        var uniformLines = shaderText.match(/uniform.*?(?![^{]*})(?=[=\[;])/g);
        if (defined(uniformLines)) {
            var len = uniformLines.length;
            for (var i = 0; i < len; i++) {
                var line = uniformLines[i].trim();
                var name = line.slice(line.lastIndexOf(' ') + 1);
                uniformNames.push(name);
            }
        }
        return uniformNames;
    }

    function handleUniformPrecisionMismatches(vertexShaderText, fragmentShaderText) {
        // If a uniform exists in both the vertex and fragment shader but with different precision qualifiers,
        // give the fragment shader uniform a different name. This fixes shader compilation errors on devices
        // that only support mediump in the fragment shader.
        var duplicateUniformNames = {};

        if (!ContextLimits.highpFloatSupported || !ContextLimits.highpIntSupported) {
            var i, j;
            var uniformName;
            var duplicateName;
            var vertexShaderUniforms = extractUniforms(vertexShaderText);
            var fragmentShaderUniforms = extractUniforms(fragmentShaderText);
            var vertexUniformsCount = vertexShaderUniforms.length;
            var fragmentUniformsCount = fragmentShaderUniforms.length;

            for (i = 0; i < vertexUniformsCount; i++) {
                for (j = 0; j < fragmentUniformsCount; j++) {
                    if (vertexShaderUniforms[i] === fragmentShaderUniforms[j]) {
                        uniformName = vertexShaderUniforms[i];
                        duplicateName = 'czm_mediump_' + uniformName;
                        // Update fragmentShaderText with renamed uniforms
                        var re = new RegExp(uniformName + '\\b', 'g');
                        fragmentShaderText = fragmentShaderText.replace(re, duplicateName);
                        duplicateUniformNames[duplicateName] = uniformName;
                    }
                }
            }
        }

        return {
            fragmentShaderText : fragmentShaderText,
            duplicateUniformNames : duplicateUniformNames
        };
    }

    var consolePrefix = '[Cesium WebGL] ';

    function createAndLinkProgram(gl, shader) {
        var vsSource = shader._vertexShaderText;
        var fsSource = shader._fragmentShaderText;

        var vertexShader = gl.createShader(gl.VERTEX_SHADER);
        gl.shaderSource(vertexShader, vsSource);
        gl.compileShader(vertexShader);

        var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
        gl.shaderSource(fragmentShader, fsSource);
        gl.compileShader(fragmentShader);

        var program = gl.createProgram();
        gl.attachShader(program, vertexShader);
        gl.attachShader(program, fragmentShader);

        gl.deleteShader(vertexShader);
        gl.deleteShader(fragmentShader);

        var attributeLocations = shader._attributeLocations;
        if (defined(attributeLocations)) {
            for ( var attribute in attributeLocations) {
                if (attributeLocations.hasOwnProperty(attribute)) {
                    gl.bindAttribLocation(program, attributeLocations[attribute], attribute);
                }
            }
        }

        gl.linkProgram(program);

        var log;
        if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
            var debugShaders = shader._debugShaders;

            // For performance, only check compile errors if there is a linker error.
            if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) {
                log = gl.getShaderInfoLog(fragmentShader);
                console.error(consolePrefix + 'Fragment shader compile log: ' + log);
                if (defined(debugShaders)) {
                    var fragmentSourceTranslation = debugShaders.getTranslatedShaderSource(fragmentShader);
                    if (fragmentSourceTranslation !== '') {
                        console.error(consolePrefix + 'Translated fragment shader source:\n' + fragmentSourceTranslation);
                    } else {
                        console.error(consolePrefix + 'Fragment shader translation failed.');
                    }
                }

                gl.deleteProgram(program);
                throw new RuntimeError('Fragment shader failed to compile.  Compile log: ' + log);
            }

            if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) {
                log = gl.getShaderInfoLog(vertexShader);
                console.error(consolePrefix + 'Vertex shader compile log: ' + log);
                if (defined(debugShaders)) {
                    var vertexSourceTranslation = debugShaders.getTranslatedShaderSource(vertexShader);
                    if (vertexSourceTranslation !== '') {
                        console.error(consolePrefix + 'Translated vertex shader source:\n' + vertexSourceTranslation);
                    } else {
                        console.error(consolePrefix + 'Vertex shader translation failed.');
                    }
                }

                gl.deleteProgram(program);
                throw new RuntimeError('Vertex shader failed to compile.  Compile log: ' + log);
            }

            log = gl.getProgramInfoLog(program);
            console.error(consolePrefix + 'Shader program link log: ' + log);
            if (defined(debugShaders)) {
                console.error(consolePrefix + 'Translated vertex shader source:\n' + debugShaders.getTranslatedShaderSource(vertexShader));
                console.error(consolePrefix + 'Translated fragment shader source:\n' + debugShaders.getTranslatedShaderSource(fragmentShader));
            }

            gl.deleteProgram(program);
            throw new RuntimeError('Program failed to link.  Link log: ' + log);
        }

        var logShaderCompilation = shader._logShaderCompilation;

        if (logShaderCompilation) {
            log = gl.getShaderInfoLog(vertexShader);
            if (defined(log) && (log.length > 0)) {
                console.log(consolePrefix + 'Vertex shader compile log: ' + log);
            }
        }

        if (logShaderCompilation) {
            log = gl.getShaderInfoLog(fragmentShader);
            if (defined(log) && (log.length > 0)) {
                console.log(consolePrefix + 'Fragment shader compile log: ' + log);
            }
        }

        if (logShaderCompilation) {
            log = gl.getProgramInfoLog(program);
            if (defined(log) && (log.length > 0)) {
                console.log(consolePrefix + 'Shader program link log: ' + log);
            }
        }

        return program;
    }

    function findVertexAttributes(gl, program, numberOfAttributes) {
        var attributes = {};
        for (var i = 0; i < numberOfAttributes; ++i) {
            var attr = gl.getActiveAttrib(program, i);
            var location = gl.getAttribLocation(program, attr.name);

            attributes[attr.name] = {
                name : attr.name,
                type : attr.type,
                index : location
            };
        }

        return attributes;
    }

    function findUniforms(gl, program) {
        var uniformsByName = {};
        var uniforms = [];
        var samplerUniforms = [];

        var numberOfUniforms = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS);

        for (var i = 0; i < numberOfUniforms; ++i) {
            var activeUniform = gl.getActiveUniform(program, i);
            var suffix = '[0]';
            var uniformName = activeUniform.name.indexOf(suffix, activeUniform.name.length - suffix.length) !== -1 ? activeUniform.name.slice(0, activeUniform.name.length - 3) : activeUniform.name;

            // Ignore GLSL built-in uniforms returned in Firefox.
            if (uniformName.indexOf('gl_') !== 0) {
                if (activeUniform.name.indexOf('[') < 0) {
                    // Single uniform
                    var location = gl.getUniformLocation(program, uniformName);

                    // IE 11.0.9 needs this check since getUniformLocation can return null
                    // if the uniform is not active (e.g., it is optimized out).  Looks like
                    // getActiveUniform() above returns uniforms that are not actually active.
                    if (location !== null) {
                        var uniform = createUniform(gl, activeUniform, uniformName, location);

                        uniformsByName[uniformName] = uniform;
                        uniforms.push(uniform);

                        if (uniform._setSampler) {
                            samplerUniforms.push(uniform);
                        }
                    }
                } else {
                    // Uniform array

                    var uniformArray;
                    var locations;
                    var value;
                    var loc;

                    // On some platforms - Nexus 4 in Firefox for one - an array of sampler2D ends up being represented
                    // as separate uniforms, one for each array element.  Check for and handle that case.
                    var indexOfBracket = uniformName.indexOf('[');
                    if (indexOfBracket >= 0) {
                        // We're assuming the array elements show up in numerical order - it seems to be true.
                        uniformArray = uniformsByName[uniformName.slice(0, indexOfBracket)];

                        // Nexus 4 with Android 4.3 needs this check, because it reports a uniform
                        // with the strange name webgl_3467e0265d05c3c1[1] in our globe surface shader.
                        if (!defined(uniformArray)) {
                            continue;
                        }

                        locations = uniformArray._locations;

                        // On the Nexus 4 in Chrome, we get one uniform per sampler, just like in Firefox,
                        // but the size is not 1 like it is in Firefox.  So if we push locations here,
                        // we'll end up adding too many locations.
                        if (locations.length <= 1) {
                            value = uniformArray.value;
                            loc = gl.getUniformLocation(program, uniformName);

                            // Workaround for IE 11.0.9.  See above.
                            if (loc !== null) {
                                locations.push(loc);
                                value.push(gl.getUniform(program, loc));
                            }
                        }
                    } else {
                        locations = [];
                        for (var j = 0; j < activeUniform.size; ++j) {
                            loc = gl.getUniformLocation(program, uniformName + '[' + j + ']');

                            // Workaround for IE 11.0.9.  See above.
                            if (loc !== null) {
                                locations.push(loc);
                            }
                        }
                        uniformArray = createUniformArray(gl, activeUniform, uniformName, locations);

                        uniformsByName[uniformName] = uniformArray;
                        uniforms.push(uniformArray);

                        if (uniformArray._setSampler) {
                            samplerUniforms.push(uniformArray);
                        }
                    }
                }
            }
        }

        return {
            uniformsByName : uniformsByName,
            uniforms : uniforms,
            samplerUniforms : samplerUniforms
        };
    }

    function partitionUniforms(shader, uniforms) {
        var automaticUniforms = [];
        var manualUniforms = [];

        for (var uniform in uniforms) {
            if (uniforms.hasOwnProperty(uniform)) {
                var uniformObject = uniforms[uniform];
                var uniformName = uniform;
                // if it's a duplicate uniform, use its original name so it is updated correctly
                var duplicateUniform = shader._duplicateUniformNames[uniformName];
                if (defined(duplicateUniform)) {
                    uniformObject.name = duplicateUniform;
                    uniformName = duplicateUniform;
                }
                var automaticUniform = AutomaticUniforms[uniformName];
                if (defined(automaticUniform)) {
                    automaticUniforms.push({
                        uniform : uniformObject,
                        automaticUniform : automaticUniform
                    });
                } else {
                    manualUniforms.push(uniformObject);
                }
            }
        }

        return {
            automaticUniforms : automaticUniforms,
            manualUniforms : manualUniforms
        };
    }

    function setSamplerUniforms(gl, program, samplerUniforms) {
        gl.useProgram(program);

        var textureUnitIndex = 0;
        var length = samplerUniforms.length;
        for (var i = 0; i < length; ++i) {
            textureUnitIndex = samplerUniforms[i]._setSampler(textureUnitIndex);
        }

        gl.useProgram(null);

        return textureUnitIndex;
    }

    function initialize(shader) {
        if (defined(shader._program)) {
            return;
        }

        var gl = shader._gl;
        var program = createAndLinkProgram(gl, shader, shader._debugShaders);
        var numberOfVertexAttributes = gl.getProgramParameter(program, gl.ACTIVE_ATTRIBUTES);
        var uniforms = findUniforms(gl, program);
        var partitionedUniforms = partitionUniforms(shader, uniforms.uniformsByName);

        shader._program = program;
        shader._numberOfVertexAttributes = numberOfVertexAttributes;
        shader._vertexAttributes = findVertexAttributes(gl, program, numberOfVertexAttributes);
        shader._uniformsByName = uniforms.uniformsByName;
        shader._uniforms = uniforms.uniforms;
        shader._automaticUniforms = partitionedUniforms.automaticUniforms;
        shader._manualUniforms = partitionedUniforms.manualUniforms;

        shader.maximumTextureUnitIndex = setSamplerUniforms(gl, program, uniforms.samplerUniforms);
    }

    ShaderProgram.prototype._bind = function() {
        initialize(this);
        this._gl.useProgram(this._program);
    };

    ShaderProgram.prototype._setUniforms = function(uniformMap, uniformState, validate) {
        var len;
        var i;

        if (defined(uniformMap)) {
            var manualUniforms = this._manualUniforms;
            len = manualUniforms.length;
            for (i = 0; i < len; ++i) {
                var mu = manualUniforms[i];
                mu.value = uniformMap[mu.name]();
            }
        }

        var automaticUniforms = this._automaticUniforms;
        len = automaticUniforms.length;
        for (i = 0; i < len; ++i) {
            var au = automaticUniforms[i];
            au.uniform.value = au.automaticUniform.getValue(uniformState);
        }

        ///////////////////////////////////////////////////////////////////

        // It appears that assigning the uniform values above and then setting them here
        // (which makes the GL calls) is faster than removing this loop and making
        // the GL calls above.  I suspect this is because each GL call pollutes the
        // L2 cache making our JavaScript and the browser/driver ping-pong cache lines.
        var uniforms = this._uniforms;
        len = uniforms.length;
        for (i = 0; i < len; ++i) {
            uniforms[i].set();
        }

        if (validate) {
            var gl = this._gl;
            var program = this._program;

            gl.validateProgram(program);
            //>>includeStart('debug', pragmas.debug);
            if (!gl.getProgramParameter(program, gl.VALIDATE_STATUS)) {
                throw new DeveloperError('Program validation failed.  Program info log: ' + gl.getProgramInfoLog(program));
            }
            //>>includeEnd('debug');
        }
    };

    ShaderProgram.prototype.isDestroyed = function() {
        return false;
    };

    ShaderProgram.prototype.destroy = function() {
        this._cachedShader.cache.releaseShaderProgram(this);
        return undefined;
    };

    ShaderProgram.prototype.finalDestroy = function() {
        this._gl.deleteProgram(this._program);
        return destroyObject(this);
    };

    return ShaderProgram;
});