vgl/shaderProgram.js

var vgl = require('./vgl');
var inherit = require('../inherit');
var timestamp = require('../timestamp');

/**
 * Create a new instance of class shaderProgram.
 *
 * @class
 * @alias vgl.shaderProgram
 * @returns {vgl.shaderProgram}
 */
vgl.shaderProgram = function () {
  'use strict';

  if (!(this instanceof vgl.shaderProgram)) {
    return new vgl.shaderProgram();
  }
  vgl.materialAttribute.call(
    this, vgl.materialAttributeType.ShaderProgram);

  var m_this = this,
      m_programHandle = 0,
      m_programContext = null,
      m_compileTimestamp = timestamp(),
      m_bindTimestamp = timestamp(),
      m_shaders = [],
      m_uniforms = [],
      m_vertexAttributes = {},
      m_uniformNameToLocation = {},
      m_vertexAttributeNameToLocation = {};

  function hasContextChanged(renderState) {
    return m_programContext !== renderState.m_context ||
      renderState.m_contextChanged;
  }

  function clearLocationCaches() {
    m_uniformNameToLocation = {};
    m_vertexAttributeNameToLocation = {};
  }

  function resetProgramState() {
    m_programHandle = 0;
    m_programContext = null;
    clearLocationCaches();
  }

  /**
   * Query uniform location in the program.
   *
   * @param {vgl.renderState} renderState
   * @param {string} name
   * @returns {number}
   */
  this.queryUniformLocation = function (renderState, name) {
    return renderState.m_context.getUniformLocation(m_programHandle, name);
  };

  /**
   * Query attribute location in the program.
   *
   * @param {vgl.renderState} renderState
   * @param {string} name
   * @returns {number}
   */
  this.queryAttributeLocation = function (renderState, name) {
    return renderState.m_context.getAttribLocation(m_programHandle, name);
  };

  /**
   * Add a new shader to the program.
   *
   * @param {string} shader
   * @returns {boolean}
   */
  this.addShader = function (shader) {
    if (m_shaders.indexOf(shader) > -1) {
      return false;
    }

    var i;
    for (i = m_shaders.length - 2; i >= 0; i -= 1) {
      if (m_shaders[i].shaderType() === shader.shaderType()) {
        m_shaders.splice(i, 1);
      }
    }

    m_shaders.push(shader);
    m_this.modified();
    return true;
  };

  /**
   * Add a new uniform to the program.
   *
   * @param {vgl.uniform} uniform
   * @returns {boolean}
   */
  this.addUniform = function (uniform) {
    if (m_uniforms.indexOf(uniform) > -1) {
      return false;
    }

    m_uniforms.push(uniform);
    m_this.modified();
    return true;
  };

  /**
   * Add a new vertex attribute to the program.
   *
   * @param {vgl.vertexAttribute} attr
   * @param {string} key
   */
  this.addVertexAttribute = function (attr, key) {
    m_vertexAttributes[key] = attr;
    m_this.modified();
  };

  /**
   * Get uniform location.
   *
   * This method does not perform any query into the program but relies on
   * the fact that it depends on a call to queryUniformLocation earlier.
   *
   * @param {string} name
   * @returns {number}
   */
  this.uniformLocation = function (name) {
    return m_uniformNameToLocation[name];
  };

  /**
   * Get attribute location.
   *
   * This method does not perform any query into the program but relies on the
   * fact that it depends on a call to queryUniformLocation earlier.
   *
   * @param {string} name
   * @returns {number}
   */
  this.attributeLocation = function (name) {
    return m_vertexAttributeNameToLocation[name];
  };

  /**
   * Get uniform object using name as the key.
   *
   * @param {string} name
   * @returns {vgl.uniform}
   */
  this.uniform = function (name) {
    var i;
    for (i = 0; i < m_uniforms.length; i += 1) {
      if (m_uniforms[i].name() === name) {
        return m_uniforms[i];
      }
    }

    return null;
  };

  /**
   * Update all uniforms.
   *
   * This method should not be used directly unless required.
   *
   * @param {vgl.renderState} renderState
   */
  this.updateUniforms = function (renderState) {
    var i;

    for (i = 0; i < m_uniforms.length; i += 1) {
      m_uniforms[i].callGL(renderState,
                           m_uniformNameToLocation[m_uniforms[i].name()]);
    }
  };

  /**
   * Link shader program.
   *
   * @param {vgl.renderState} renderState
   * @returns {boolean}
   */
  this.link = function (renderState) {
    renderState.m_context.linkProgram(m_programHandle);

    // If creating the shader program failed, alert
    if (!renderState.m_context.getProgramParameter(m_programHandle,
                                                   vgl.GL.LINK_STATUS)) {
      console.log('[ERROR] Unable to initialize the shader program.');  // eslint-disable-line no-console
      return false;
    }

    return true;
  };

  /**
   * Use the shader program.
   *
   * @param {vgl.renderState} renderState
   */
  this.use = function (renderState) {
    renderState.m_context.useProgram(m_programHandle || null);
  };

  /**
   * Perform any initialization required.
   *
   * @param {vgl.renderState} renderState
   */
  this._setup = function (renderState) {
    if (m_programHandle && hasContextChanged(renderState)) {
      // A restored/replaced context invalidates all cached program state.
      resetProgramState();
    }
    if (m_programHandle === 0) {
      m_programHandle = renderState.m_context.createProgram();
      m_programContext = renderState.m_context;
    }
  };

  /**
   * Perform any clean up required when the program gets deleted.
   *
   * @param {vgl.renderState} renderState
   */
  this._cleanup = function (renderState) {
    m_this.deleteVertexAndFragment(renderState);
    m_this.deleteProgram(renderState);
    m_this.modified();
  };

  /**
   * Delete the shader program.
   *
   * @param {vgl.renderState} renderState
   */
  this.deleteProgram = function (renderState) {
    if (m_programHandle && renderState &&
        m_programContext === renderState.m_context) {
      renderState.m_context.deleteProgram(m_programHandle);
    }
    resetProgramState();
  };

  /**
   * Delete vertex and fragment shaders.
   *
   * @param {vgl.renderState} renderState
   */
  this.deleteVertexAndFragment = function (renderState) {
    var i;
    for (i = 0; i < m_shaders.length; i += 1) {
      if (renderState && renderState.m_contextChanged) {
        // After context loss there is nothing to detach/delete in GL.
        m_shaders[i].removeContext(renderState);
        continue;
      }
      if (m_shaders[i].shaderHandle(renderState)) {
        renderState.m_context.detachShader(
          m_programHandle, m_shaders[i].shaderHandle(renderState));
      }
      renderState.m_context.deleteShader(m_shaders[i].shaderHandle(renderState));
      m_shaders[i].removeContext(renderState);
    }
  };

  /**
   * Compile and link a shader.
   *
   * @param {vgl.renderState} renderState
   * @returns {boolean} True if the program was compiled and linked successfully, false otherwise.
   */
  this.compileAndLink = function (renderState) {
    var i;
    // Rebuild if timestamps are stale or we switched GL contexts.
    var contextChanged = hasContextChanged(renderState) || !m_programHandle;

    m_this._setup(renderState);

    if (!contextChanged && m_compileTimestamp.getMTime() >= this.getMTime()) {
      return !!m_programHandle;
    }

    clearLocationCaches();

    // Compile shaders
    for (i = 0; i < m_shaders.length; i += 1) {
      m_shaders[i].compile(renderState);
      m_shaders[i].attachShader(renderState, m_programHandle);
    }

    m_this.bindAttributes(renderState);

    // link program
    if (!m_this.link(renderState)) {
      console.log('[ERROR] Failed to link Program');  // eslint-disable-line no-console
      m_this._cleanup(renderState);
      return false;
    }

    m_compileTimestamp.modified();
    return true;
  };

  /**
   * Bind the program with its shaders.
   *
   * @param {vgl.renderState} renderState
   */
  this.bind = function (renderState) {
    var i = 0;
    var needBind = renderState.m_contextChanged ||
      m_bindTimestamp.getMTime() < m_this.getMTime() ||
      m_programContext !== renderState.m_context;

    if (needBind) {

      // Compile shaders
      if (!m_this.compileAndLink(renderState)) {
        return;
      }

      m_this.use(renderState);
      m_this.bindUniforms(renderState);
      m_bindTimestamp.modified();
    } else {
      if (!m_programHandle) {
        return;
      }
      m_this.use(renderState);
    }

    // Call update callback.
    for (i = 0; i < m_uniforms.length; i += 1) {
      m_uniforms[i].update(renderState, m_this);
    }

    // Now update values to GL.
    m_this.updateUniforms(renderState);
  };

  /**
   * Undo binding of the shader program.
   *
   * @param {vgl.renderState} renderState
   */
  this.undoBind = function (renderState) {
    // REF https://www.khronos.org/opengles/sdk/docs/man/xhtml/glUseProgram.xml
    // If program is 0, then the current rendering state refers to an invalid
    // program object, and the results of vertex and fragment shader execution
    // due to any glDrawArrays or glDrawElements commands are undefined
    renderState.m_context.useProgram(null);
  };

  /**
   * Bind vertex data.
   *
   * @param {vgl.renderState} renderState
   * @param {string} key
   */
  this.bindVertexData = function (renderState, key) {
    if (m_vertexAttributes.hasOwnProperty(key)) {
      m_vertexAttributes[key].bindVertexData(renderState, key);
    }
  };

  /**
   * Undo bind vertex data.
   *
   * @param {vgl.renderState} renderState
   * @param {string} key
   */
  this.undoBindVertexData = function (renderState, key) {
    if (m_vertexAttributes.hasOwnProperty(key)) {
      m_vertexAttributes[key].undoBindVertexData(renderState, key);
    }
  };

  /**
   * Bind uniforms.
   *
   * @param {vgl.renderState} renderState
   */
  this.bindUniforms = function (renderState) {
    var i;
    for (i = 0; i < m_uniforms.length; i += 1) {
      m_uniformNameToLocation[m_uniforms[i].name()] = this
          .queryUniformLocation(renderState, m_uniforms[i].name());
    }
  };

  /**
   * Bind vertex attributes.
   *
   * @param {vgl.renderState} renderState
   */
  this.bindAttributes = function (renderState) {
    var key, name;
    for (key in m_vertexAttributes) {
      if (m_vertexAttributes.hasOwnProperty(key)) {
        name = m_vertexAttributes[key].name();
        renderState.m_context.bindAttribLocation(m_programHandle, key, name);
        m_vertexAttributeNameToLocation[name] = key;
      }
    }
  };

  return m_this;
};

inherit(vgl.shaderProgram, vgl.materialAttribute);