playcanvas
Version:
PlayCanvas WebGL game engine
364 lines (361 loc) • 15 kB
JavaScript
import { Debug } from '../../../core/debug.js';
import { TRACEID_SHADER_COMPILE } from '../../../core/constants.js';
import { now } from '../../../core/time.js';
import { WebglShaderInput } from './webgl-shader-input.js';
import { semanticToLocation, SHADERTAG_MATERIAL } from '../constants.js';
import { DeviceCache } from '../device-cache.js';
import { DebugGraphics } from '../debug-graphics.js';
/**
* @import { Shader } from '../shader.js'
* @import { WebglGraphicsDevice } from './webgl-graphics-device.js'
*/ var _totalCompileTime = 0;
var _vertexShaderBuiltins = new Set([
'gl_VertexID',
'gl_InstanceID',
'gl_DrawID',
'gl_BaseVertex',
'gl_BaseInstance'
]);
// class used to hold compiled WebGL vertex or fragment shaders in the device cache
class CompiledShaderCache {
// destroy all created shaders when the device is destroyed
destroy(device) {
this.map.forEach((shader)=>{
device.gl.deleteShader(shader);
});
}
// just empty the cache when the context is lost
loseContext(device) {
this.map.clear();
}
constructor(){
// maps shader source to a compiled WebGL shader
this.map = new Map();
}
}
var _vertexShaderCache = new DeviceCache();
var _fragmentShaderCache = new DeviceCache();
/**
* A WebGL implementation of the Shader.
*
* @ignore
*/ class WebglShader {
/**
* Free the WebGL resources associated with a shader.
*
* @param {Shader} shader - The shader to free.
*/ destroy(shader) {
if (this.glProgram) {
shader.device.gl.deleteProgram(this.glProgram);
this.glProgram = null;
}
}
init() {
this.uniforms = [];
this.samplers = [];
this.attributes = [];
this.glProgram = null;
this.glVertexShader = null;
this.glFragmentShader = null;
}
/**
* Dispose the shader when the context has been lost.
*/ loseContext() {
this.init();
}
/**
* Restore shader after the context has been obtained.
*
* @param {WebglGraphicsDevice} device - The graphics device.
* @param {Shader} shader - The shader to restore.
*/ restoreContext(device, shader) {
this.compile(device, shader);
this.link(device, shader);
}
/**
* Compile shader programs.
*
* @param {WebglGraphicsDevice} device - The graphics device.
* @param {Shader} shader - The shader to compile.
*/ compile(device, shader) {
var definition = shader.definition;
this.glVertexShader = this._compileShaderSource(device, definition.vshader, true);
this.glFragmentShader = this._compileShaderSource(device, definition.fshader, false);
}
/**
* Link shader programs. This is called at a later stage, to allow many shaders to compile in parallel.
*
* @param {WebglGraphicsDevice} device - The graphics device.
* @param {Shader} shader - The shader to compile.
*/ link(device, shader) {
// if the shader was already linked
if (this.glProgram) {
return;
}
// if the device is lost, silently ignore
var gl = device.gl;
if (gl.isContextLost()) {
return;
}
var startTime = 0;
Debug.call(()=>{
this.compileDuration = 0;
startTime = now();
});
var glProgram = gl.createProgram();
this.glProgram = glProgram;
gl.attachShader(glProgram, this.glVertexShader);
gl.attachShader(glProgram, this.glFragmentShader);
var definition = shader.definition;
var attrs = definition.attributes;
if (definition.useTransformFeedback) {
// Collect all "out_" attributes and use them for output
var outNames = [];
for(var attr in attrs){
if (attrs.hasOwnProperty(attr)) {
outNames.push("out_" + attr);
}
}
gl.transformFeedbackVaryings(glProgram, outNames, gl.INTERLEAVED_ATTRIBS);
}
// map all vertex input attributes to fixed locations
var locations = {};
for(var attr1 in attrs){
if (attrs.hasOwnProperty(attr1)) {
var semantic = attrs[attr1];
var loc = semanticToLocation[semantic];
Debug.assert(!locations.hasOwnProperty(loc), "WARNING: Two attributes are mapped to the same location in a shader: " + locations[loc] + " and " + attr1);
locations[loc] = attr1;
gl.bindAttribLocation(glProgram, loc, attr1);
}
}
gl.linkProgram(glProgram);
Debug.call(()=>{
this.compileDuration = now() - startTime;
});
device._shaderStats.linked++;
if (definition.tag === SHADERTAG_MATERIAL) {
device._shaderStats.materialShaders++;
}
}
/**
* Compiles an individual shader.
*
* @param {WebglGraphicsDevice} device - The graphics device.
* @param {string} src - The shader source code.
* @param {boolean} isVertexShader - True if the shader is a vertex shader, false if it is a
* fragment shader.
* @returns {WebGLShader|null} The compiled shader, or null if the device is lost.
* @private
*/ _compileShaderSource(device, src, isVertexShader) {
var gl = device.gl;
// if the device is lost, silently ignore
if (gl.isContextLost()) {
return null;
}
// device cache for current device, containing cache of compiled shaders
var shaderDeviceCache = isVertexShader ? _vertexShaderCache : _fragmentShaderCache;
var shaderCache = shaderDeviceCache.get(device, ()=>{
return new CompiledShaderCache();
});
// try to get compiled shader from the cache
var glShader = shaderCache.map.get(src);
if (!glShader) {
var startTime = now();
device.fire('shader:compile:start', {
timestamp: startTime,
target: device
});
glShader = gl.createShader(isVertexShader ? gl.VERTEX_SHADER : gl.FRAGMENT_SHADER);
gl.shaderSource(glShader, src);
gl.compileShader(glShader);
shaderCache.map.set(src, glShader);
var endTime = now();
device.fire('shader:compile:end', {
timestamp: endTime,
target: device
});
device._shaderStats.compileTime += endTime - startTime;
if (isVertexShader) {
device._shaderStats.vsCompiled++;
} else {
device._shaderStats.fsCompiled++;
}
}
return glShader;
}
/**
* Link the shader, and extract its attributes and uniform information.
*
* @param {WebglGraphicsDevice} device - The graphics device.
* @param {Shader} shader - The shader to query.
* @returns {boolean} True if the shader was successfully queried and false otherwise.
*/ finalize(device, shader) {
// if the device is lost, silently ignore
var gl = device.gl;
if (gl.isContextLost()) {
return true;
}
var glProgram = this.glProgram;
var definition = shader.definition;
var startTime = now();
device.fire('shader:link:start', {
timestamp: startTime,
target: device
});
// this is the main thead blocking part of the shader compilation, time it
var linkStartTime = 0;
Debug.call(()=>{
linkStartTime = now();
});
// check the link status of a shader - this is a blocking operation waiting for the shader
// to finish compiling and linking
var linkStatus = gl.getProgramParameter(glProgram, gl.LINK_STATUS);
if (!linkStatus) {
var _gl_getExtension, _gl_getExtension1;
// Check for compilation errors
if (!this._isCompiled(device, shader, this.glVertexShader, definition.vshader, 'vertex')) {
return false;
}
if (!this._isCompiled(device, shader, this.glFragmentShader, definition.fshader, 'fragment')) {
return false;
}
var message = "Failed to link shader program. Error: " + gl.getProgramInfoLog(glProgram);
// log translated shaders
definition.translatedFrag = (_gl_getExtension = gl.getExtension('WEBGL_debug_shaders')) == null ? void 0 : _gl_getExtension.getTranslatedShaderSource(this.glFragmentShader);
definition.translatedVert = (_gl_getExtension1 = gl.getExtension('WEBGL_debug_shaders')) == null ? void 0 : _gl_getExtension1.getTranslatedShaderSource(this.glVertexShader);
console.error(message, definition);
return false;
}
// Query the program for each vertex buffer input (GLSL 'attribute')
var numAttributes = gl.getProgramParameter(glProgram, gl.ACTIVE_ATTRIBUTES);
shader.attributes.clear();
for(var i = 0; i < numAttributes; i++){
var info = gl.getActiveAttrib(glProgram, i);
var location = gl.getAttribLocation(glProgram, info.name);
// a built-in attributes for which we do not need to provide any data
if (_vertexShaderBuiltins.has(info.name)) {
continue;
}
// Check attributes are correctly linked up
if (definition.attributes[info.name] === undefined) {
console.error('Vertex shader attribute "' + info.name + '" is not mapped to a semantic in shader definition, shader [' + shader.label + "]", shader);
shader.failed = true;
} else {
shader.attributes.set(location, info.name);
}
}
// Query the program for each shader state (GLSL 'uniform')
var samplerTypes = device._samplerTypes;
var numUniforms = gl.getProgramParameter(glProgram, gl.ACTIVE_UNIFORMS);
for(var i1 = 0; i1 < numUniforms; i1++){
var info1 = gl.getActiveUniform(glProgram, i1);
var location1 = gl.getUniformLocation(glProgram, info1.name);
var shaderInput = new WebglShaderInput(device, info1.name, device.pcUniformType[info1.type], location1);
if (samplerTypes.has(info1.type)) {
this.samplers.push(shaderInput);
} else {
this.uniforms.push(shaderInput);
}
}
shader.ready = true;
var endTime = now();
device.fire('shader:link:end', {
timestamp: endTime,
target: device
});
device._shaderStats.compileTime += endTime - startTime;
Debug.call(()=>{
var duration = now() - linkStartTime;
this.compileDuration += duration;
_totalCompileTime += this.compileDuration;
Debug.trace(TRACEID_SHADER_COMPILE, "[id: " + shader.id + "] " + shader.name + ": " + this.compileDuration.toFixed(1) + "ms, TOTAL: " + _totalCompileTime.toFixed(1) + "ms");
});
return true;
}
/**
* Check the compilation status of a shader.
*
* @param {WebglGraphicsDevice} device - The graphics device.
* @param {Shader} shader - The shader to query.
* @param {WebGLShader} glShader - The WebGL shader.
* @param {string} source - The shader source code.
* @param {string} shaderType - The shader type. Can be 'vertex' or 'fragment'.
* @returns {boolean} True if the shader compiled successfully, false otherwise.
* @private
*/ _isCompiled(device, shader, glShader, source, shaderType) {
var gl = device.gl;
if (!gl.getShaderParameter(glShader, gl.COMPILE_STATUS)) {
var infoLog = gl.getShaderInfoLog(glShader);
var [code, error] = this._processError(source, infoLog);
var message = "Failed to compile " + shaderType + " shader:\n\n" + infoLog + "\n" + code + " while rendering " + DebugGraphics.toString();
error.shader = shader;
console.error(message, error);
return false;
}
return true;
}
/**
* Check the linking status of a shader.
*
* @param {WebglGraphicsDevice} device - The graphics device.
* @returns {boolean} True if the shader is already linked, false otherwise. Note that unless the
* device supports the KHR_parallel_shader_compile extension, this will always return true.
*/ isLinked(device) {
var { extParallelShaderCompile } = device;
if (extParallelShaderCompile) {
return device.gl.getProgramParameter(this.glProgram, extParallelShaderCompile.COMPLETION_STATUS_KHR);
}
return true;
}
/**
* Truncate the WebGL shader compilation log to just include the error line plus the 5 lines
* before and after it.
*
* @param {string} src - The shader source code.
* @param {string} infoLog - The info log returned from WebGL on a failed shader compilation.
* @returns {Array} An array where the first element is the 10 lines of code around the first
* detected error, and the second element an object storing the error message, line number and
* complete shader source.
* @private
*/ _processError(src, infoLog) {
var error = {};
var code = '';
if (src) {
var lines = src.split('\n');
var from = 0;
var to = lines.length;
// if error is in the code, only show nearby lines instead of whole shader code
if (infoLog && infoLog.startsWith('ERROR:')) {
var match = infoLog.match(/^ERROR:\s(\d+):(\d+):\s*(.+)/);
if (match) {
error.message = match[3];
error.line = parseInt(match[2], 10);
from = Math.max(0, error.line - 6);
to = Math.min(lines.length, error.line + 5);
}
}
// Chrome reports shader errors on lines indexed from 1
for(var i = from; i < to; i++){
var linePrefix = i + 1 === error.line ? '> ' : ' ';
code += "" + linePrefix + (i + 1) + ": " + lines[i] + "\n";
}
error.source = src;
}
return [
code,
error
];
}
constructor(shader){
this.compileDuration = 0;
this.init();
// kick off vertex and fragment shader compilation
this.compile(shader.device, shader);
// kick off linking, as this is non-blocking too
this.link(shader.device, shader);
// add it to a device list of all shaders
shader.device.shaders.push(shader);
}
}
export { WebglShader };