UNPKG

speedy-vision

Version:

GPU-accelerated Computer Vision for JavaScript

207 lines (183 loc) 7.11 kB
/* * speedy-vision.js * GPU-accelerated Computer Vision for JavaScript * Copyright 2020-2022 Alexandre Martins <alemartf(at)gmail.com> * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * shader-preprocessor.js * Custom preprocessor for shaders */ import { Utils } from '../utils/utils'; import { PixelComponent } from '../utils/types'; import { FileNotFoundError, ParseError } from '../utils/errors'; // Import numeric globals const globals = require('../utils/globals'); const numericGlobals = Object.keys(globals).filter(key => typeof globals[key] == 'number').reduce( (obj, key) => ((obj[key] = globals[key]), obj), {} ); // Constants accessible by all shaders const constants = Object.freeze({ // numeric globals ...numericGlobals, // fragment shader 'FS_USE_CUSTOM_PRECISION': 0, // use default precision settings 'FS_OUTPUT_TYPE': 0, // normalized RGBA // colors 'PIXELCOMPONENT_RED': PixelComponent.RED, 'PIXELCOMPONENT_GREEN': PixelComponent.GREEN, 'PIXELCOMPONENT_BLUE': PixelComponent.BLUE, 'PIXELCOMPONENT_ALPHA': PixelComponent.ALPHA, }); // Regular Expressions const commentsRegex = [ /\/\*(.|\s)*?\*\//g , /\/\/.*$/gm ]; const includeRegex = /^\s*@\s*include\s+"(.*?)"/gm; const constantRegex = /@(\w+)@/g; const unrollRegex = [ /@\s*unroll\s+?for\s*\(\s*(int|)\s*(?<counter>\w+)\s*=\s*(-?\d+|\w+)\s*;\s*\k<counter>\s*(<=?)\s*(-?\d+|\w+)\s*;\s*\k<counter>\s*\+\+()\s*\)\s*\{\s*([\s\S]+?)\s*\}/g, /@\s*unroll\s+?for\s*\(\s*(int|)\s*(?<counter>\w+)\s*=\s*(-?\d+|\w+)\s*;\s*\k<counter>\s*(<=?)\s*(-?\d+|\w+)\s*;\s*\k<counter>\s*\+=\s*(-?\d+)\s*\)\s*\{\s*([\s\S]+?)\s*\}/g, ]; /** @typedef {Map<string,number>} ShaderDefines */ /** * Custom preprocessor for the shaders */ export class ShaderPreprocessor { /** * Runs the preprocessor * @param {string} code * @param {ShaderDefines} [defines] * @returns {string} preprocessed code */ static run(code, defines = new Map()) { const errors = []; // compile-time errors // // The preprocessor will remove comments from GLSL code, // include requested GLSL files and import global constants // defined for all shaders (see above) // return unrollLoops( String(code) .replace(commentsRegex[0], '') .replace(commentsRegex[1], '') .replace(includeRegex, (_, filename) => // FIXME: no cycle detection for @include ShaderPreprocessor.run(readfileSync(filename), defines) ) .replace(constantRegex, (_, name) => String( // Find a defined constant. If not possible, find a global constant defines.has(name) ? Number(defines.get(name)) : ( constants[name] !== undefined ? Number(constants[name]) : ( errors.push(`Undefined constant: ${name}`), 0 ) ) )), defines ) + (errors.length > 0 ? errors.map(msg => `\n#error ${msg}\n`).join('') : ''); } } /** * Reads a shader from the shaders/include/ folder * @param {string} filename * @returns {string} */ function readfileSync(filename) { if(String(filename).match(/^[a-zA-Z0-9_-]+\.glsl$/)) return require('./shaders/include/' + filename); throw new FileNotFoundError(`Shader preprocessor: can't read file "${filename}"`); } /** * Unroll for loops in our own preprocessor * @param {string} code * @param {ShaderDefines} defines * @returns {string} */ function unrollLoops(code, defines) { // // Currently, only integer for loops with positive step values // can be unrolled. (TODO: negative step values?) // // The current implementation does not support curly braces // inside unrolled loops. You may define macros to get around // this, but do you actually need to unroll such loops? // // Loops that don't fit the supported pattern will crash // the preprocessor if you try to unroll them. // const fn = unroll.bind(defines); // CRAZY! const n = unrollRegex.length; for(let i = 0; i < n; i++) code = code.replace(unrollRegex[i], fn); return code; } /** * Unroll a loop pattern (regexp) * @param {string} match the matched for loop * @param {string} type * @param {string} counter * @param {string} start * @param {string} cmp * @param {string} end * @param {string} step * @param {string} loopcode * @returns {string} unrolled loop */ function unroll(match, type, counter, start, cmp, end, step, loopcode) { const defines = /** @type {ShaderDefines} */ ( this ); // check if the loop limits are numeric constants or #defined numbers from the outside const hasStart = Number.isFinite(+start) || defines.has(start); const hasEnd = Number.isFinite(+end) || defines.has(end); if(!hasStart || !hasEnd) { if(defines.size > 0) throw new ParseError(`Can't unroll loop: unknown limits (start=${start}, end=${end}). Code:\n\n${match}`); else return match; // don't unroll now, because defines is empty - maybe we'll succeed in the next pass } // parse and validate limits & step let istart = defines.has(start) ? defines.get(start) : parseInt(start); let iend = defines.has(end) ? defines.get(end) : parseInt(end); let istep = (step.length == 0) ? 1 : parseInt(step); Utils.assert(istart <= iend && istep > 0); /* // debug console.log(`Encontrei "${match}"`); console.log(`type="${type}"`); console.log(`counter="${counter}"`); console.log(`start="${start}"`); console.log(`cmp="${cmp}"`); console.log(`end="${end}"`); console.log(`step="${step}"`); console.log(`loopcode="${loopcode}"`) console.log('Defines:', defines); */ // continue statements are not supported inside unrolled loops // and will generate a compiler error. Using break is ok. const hasBreak = (loopcode.match(/\bbreak\s*;/) !== null); // create a new scope let unrolledCode = hasBreak ? 'switch(1) { default:\n' : '{\n'; // declare counter unrolledCode += `${type} ${counter};\n`; // unroll loop iend += (cmp == '<=') ? 1 : 0; for(let i = istart; i < iend; i += istep) unrolledCode += `{\n${counter} = ${i};\n${loopcode}\n}\n`; // close scope unrolledCode += '}\n'; //console.log('Unrolled code:\n\n' + unrolledCode); // done! return unrolledCode; }