playcanvas
Version:
Open-source WebGL/WebGPU 3D engine for the web
223 lines (221 loc) • 6.71 kB
JavaScript
import { TRACEID_SHADER_COMPILE } from "../../../core/constants.js";
import { now } from "../../../core/time.js";
import { WebglShaderInput } from "./webgl-shader-input.js";
import { SHADERTAG_MATERIAL, semanticToLocation } from "../constants.js";
import { DeviceCache } from "../device-cache.js";
let _totalCompileTime = 0;
const _vertexShaderBuiltins = /* @__PURE__ */ new Set([
"gl_VertexID",
"gl_InstanceID",
"gl_DrawID",
"gl_BaseVertex",
"gl_BaseInstance"
]);
class CompiledShaderCache {
// maps shader source to a compiled WebGL shader
map = /* @__PURE__ */ new Map();
// 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();
}
}
const _vertexShaderCache = new DeviceCache();
const _fragmentShaderCache = new DeviceCache();
class WebglShader {
compileDuration = 0;
constructor(shader) {
this.init();
this.compile(shader.device, shader);
this.link(shader.device, shader);
shader.device.shaders.push(shader);
}
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;
}
loseContext() {
this.init();
}
restoreContext(device, shader) {
this.compile(device, shader);
this.link(device, shader);
}
compile(device, shader) {
const definition = shader.definition;
this.glVertexShader = this._compileShaderSource(device, definition.vshader, true);
this.glFragmentShader = this._compileShaderSource(device, definition.fshader, false);
}
link(device, shader) {
if (this.glProgram) {
return;
}
const gl = device.gl;
if (gl.isContextLost()) {
return;
}
let startTime = 0;
const glProgram = gl.createProgram();
this.glProgram = glProgram;
gl.attachShader(glProgram, this.glVertexShader);
gl.attachShader(glProgram, this.glFragmentShader);
const definition = shader.definition;
const attrs = definition.attributes;
if (definition.useTransformFeedback) {
let outNames = definition.feedbackVaryings;
if (!outNames) {
outNames = [];
for (const attr in attrs) {
if (attrs.hasOwnProperty(attr)) {
outNames.push(`out_${attr}`);
}
}
}
gl.transformFeedbackVaryings(glProgram, outNames, gl.INTERLEAVED_ATTRIBS);
}
const locations = {};
for (const attr in attrs) {
if (attrs.hasOwnProperty(attr)) {
const semantic = attrs[attr];
const loc = semanticToLocation[semantic];
locations[loc] = attr;
gl.bindAttribLocation(glProgram, loc, attr);
}
}
gl.linkProgram(glProgram);
}
_compileShaderSource(device, src, isVertexShader) {
const gl = device.gl;
if (gl.isContextLost()) {
return null;
}
const shaderDeviceCache = isVertexShader ? _vertexShaderCache : _fragmentShaderCache;
const shaderCache = shaderDeviceCache.get(device, () => {
return new CompiledShaderCache();
});
let glShader = shaderCache.map.get(src);
if (!glShader) {
glShader = gl.createShader(isVertexShader ? gl.VERTEX_SHADER : gl.FRAGMENT_SHADER);
gl.shaderSource(glShader, src);
gl.compileShader(glShader);
shaderCache.map.set(src, glShader);
}
return glShader;
}
finalize(device, shader) {
const gl = device.gl;
if (gl.isContextLost()) {
return true;
}
const glProgram = this.glProgram;
const definition = shader.definition;
let linkStartTime = 0;
const linkStatus = gl.getProgramParameter(glProgram, gl.LINK_STATUS);
if (!linkStatus) {
if (!this._isCompiled(device, shader, this.glVertexShader, definition.vshader, "vertex")) {
return false;
}
if (!this._isCompiled(device, shader, this.glFragmentShader, definition.fshader, "fragment")) {
return false;
}
const message = `Failed to link shader program. Error: ${gl.getProgramInfoLog(glProgram)}`;
console.error(message);
return false;
}
const numAttributes = gl.getProgramParameter(glProgram, gl.ACTIVE_ATTRIBUTES);
shader.attributes.clear();
for (let i = 0; i < numAttributes; i++) {
const info = gl.getActiveAttrib(glProgram, i);
const location = gl.getAttribLocation(glProgram, info.name);
if (_vertexShaderBuiltins.has(info.name)) {
continue;
}
if (definition.attributes[info.name] === void 0) {
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);
}
}
const samplerTypes = device._samplerTypes;
const numUniforms = gl.getProgramParameter(glProgram, gl.ACTIVE_UNIFORMS);
for (let i = 0; i < numUniforms; i++) {
const info = gl.getActiveUniform(glProgram, i);
const location = gl.getUniformLocation(glProgram, info.name);
if (_vertexShaderBuiltins.has(info.name)) {
continue;
}
const shaderInput = new WebglShaderInput(device, info.name, device.pcUniformType[info.type], location);
if (samplerTypes.has(info.type)) {
this.samplers.push(shaderInput);
} else {
this.uniforms.push(shaderInput);
}
}
shader.ready = true;
return true;
}
_isCompiled(device, shader, glShader, source, shaderType) {
const gl = device.gl;
if (!gl.getShaderParameter(glShader, gl.COMPILE_STATUS)) {
const infoLog = gl.getShaderInfoLog(glShader);
const [code, error] = this._processError(source, infoLog);
const message = `Failed to compile ${shaderType} shader:
${infoLog}
${code} while rendering ${void 0}`;
console.error(message);
return false;
}
return true;
}
isLinked(device) {
const { extParallelShaderCompile } = device;
if (extParallelShaderCompile) {
return device.gl.getProgramParameter(this.glProgram, extParallelShaderCompile.COMPLETION_STATUS_KHR);
}
return true;
}
_processError(src, infoLog) {
const error = {};
let code = "";
if (src) {
const lines = src.split("\n");
let from = 0;
let to = lines.length;
if (infoLog && infoLog.startsWith("ERROR:")) {
const 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);
}
}
for (let i = from; i < to; i++) {
const linePrefix = i + 1 === error.line ? "> " : " ";
code += `${linePrefix}${i + 1}: ${lines[i]}
`;
}
error.source = src;
}
return [code, error];
}
}
export {
WebglShader
};