Files
bitfielder/src/shader/core/ShaderCompiler.ts
2025-07-14 21:08:21 +02:00

145 lines
4.8 KiB
TypeScript

import { ShaderFunction } from '../types';
import { PERFORMANCE } from '../../utils/constants';
/**
* Handles shader code compilation and optimization
*/
export class ShaderCompiler {
/**
* Compiles shader code into an executable function
*/
static compile(code: string): ShaderFunction {
const safeCode = this.sanitizeCode(code);
// Check if expression is static (contains no variables)
const isStatic = this.isStaticExpression(safeCode);
if (isStatic) {
// Pre-compute static value
const staticValue = this.evaluateStaticExpression(safeCode);
return (_ctx) => staticValue;
}
return new Function(
'ctx',
`
// Destructure context for backward compatibility with existing shader code
const {
x, y, t, i, r, a, u, v, c, f, d, n, b, bn, bs, be, bw,
w, h, p, z, j, o, g, m, l, k, s, e, mouseX, mouseY,
mousePressed, mouseVX, mouseVY, mouseClickTime, touchCount,
touch0X, touch0Y, touch1X, touch1Y, pinchScale, pinchRotation,
accelX, accelY, accelZ, gyroX, gyroY, gyroZ, audioLevel,
bassLevel, midLevel, trebleLevel, bpm, _t, bx, by, sx, sy, qx, qy
} = ctx;
// Shader-specific helper functions
const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
const lerp = (a, b, t) => a + (b - a) * t;
const smooth = (edge, x) => { const t = Math.min(Math.max((x - edge) / (1 - edge), 0), 1); return t * t * (3 - 2 * t); };
const step = (edge, x) => x < edge ? 0 : 1;
const fract = (x) => x - Math.floor(x);
const mix = (a, b, t) => a + (b - a) * t;
// Timeout protection
const startTime = performance.now();
let iterations = 0;
function checkTimeout() {
iterations++;
if (iterations % ${PERFORMANCE.TIMEOUT_CHECK_INTERVAL} === 0 && performance.now() - startTime > ${PERFORMANCE.MAX_SHADER_TIMEOUT_MS}) {
throw new Error('Shader timeout');
}
}
return (function() {
checkTimeout();
return ${safeCode};
})();
`
) as ShaderFunction;
}
/**
* Checks if shader code contains only static expressions (no variables)
*/
private static isStaticExpression(code: string): boolean {
// Check if code contains any variables using regex for better accuracy
const variablePattern = /\b(x|y|t|i|r|a|u|v|c|f|d|n|b|bn|bs|be|bw|m|l|k|s|e|w|h|p|z|j|o|g|bpm|bx|by|sx|sy|qx|qy|mouse[XY]|mousePressed|mouseV[XY]|mouseClickTime|touchCount|touch[01][XY]|pinchScale|pinchRotation|accel[XYZ]|gyro[XYZ]|audioLevel|bassLevel|midLevel|trebleLevel)\b/;
return !variablePattern.test(code);
}
/**
* Evaluates static expressions safely
*/
private static evaluateStaticExpression(code: string): number {
try {
// Safely evaluate numeric expression
const result = new Function(`return ${code}`)();
return isFinite(result) ? result : 0;
} catch (error) {
return 0;
}
}
/**
* Sanitizes shader code by auto-prefixing Math functions and constants
*/
private static sanitizeCode(code: string): string {
// Create a single regex pattern for all replacements
const mathFunctions = [
'abs', 'acos', 'asin', 'atan', 'atan2', 'ceil', 'cos', 'exp',
'floor', 'log', 'max', 'min', 'pow', 'random', 'round', 'sin',
'sqrt', 'tan', 'trunc', 'sign', 'cbrt', 'hypot', 'imul', 'fround',
'clz32', 'acosh', 'asinh', 'atanh', 'cosh', 'sinh', 'tanh',
'expm1', 'log1p', 'log10', 'log2'
];
const mathConstants = {
'PI': 'Math.PI',
'E': 'Math.E',
'LN2': 'Math.LN2',
'LN10': 'Math.LN10',
'LOG2E': 'Math.LOG2E',
'LOG10E': 'Math.LOG10E',
'SQRT1_2': 'Math.SQRT1_2',
'SQRT2': 'Math.SQRT2'
};
// Build combined regex pattern
const functionPattern = mathFunctions.join('|');
const constantPattern = Object.keys(mathConstants).join('|');
const combinedPattern = new RegExp(
`\\b(${functionPattern})\\(|\\b(${constantPattern})\\b|\\bt\\s*\\(`,
'g'
);
// Single pass replacement
const processedCode = code.replace(combinedPattern, (match, func, constant) => {
if (func) {
return `Math.${func}(`;
} else if (constant) {
return mathConstants[constant as keyof typeof mathConstants];
} else if (match.startsWith('t')) {
return '_t(';
}
return match;
});
return processedCode;
}
/**
* Generates a hash for shader code caching
*/
static hashCode(str: string): string {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = (hash << 5) - hash + char;
hash = hash & hash; // Convert to 32-bit integer
}
return hash.toString(36);
}
}