Skip to content

Commit

Permalink
Preprocessor to resolve ifdefs is applied to shader programs (#4156)
Browse files Browse the repository at this point in the history
* Preprocessor to resolve ifdefs is applied to shader programs

* Update test/core/preprocessor.test.mjs

Co-authored-by: Will Eastcott <will@playcanvas.com>

* try to disable lgtm warnings

* lgtm - other warning

* remove lgtm blocking, let them show to keep track of

* fixed handling if ‘undef’ inside false block

* support for Debug.trace

* cleaned up define

* improved error handling

* jsdocs update

* marking  _traceChannels private

Co-authored-by: Martin Valigursky <mvaligursky@snapchat.com>
Co-authored-by: Will Eastcott <will@playcanvas.com>
  • Loading branch information
3 people authored Apr 5, 2022
1 parent 6bac479 commit 0720039
Show file tree
Hide file tree
Showing 7 changed files with 506 additions and 31 deletions.
1 change: 1 addition & 0 deletions rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ const stripOptions = {
'Debug.warn',
'Debug.error',
'Debug.log',
'Debug.trace',
'DebugGraphics.pushGpuMarker',
'DebugGraphics.popGpuMarker',
'WorldClustersDebug.render'
Expand Down
39 changes: 38 additions & 1 deletion src/core/debug.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,31 @@ class Debug {
*/
static _deprecatedMessages = new Set();

/**
* Set storing names of enabled trace channels
*
* @type {Set<string>}
* @private
*/
static _traceChannels = new Set();

/**
* Enable or disable trace channel
*
* @param {string} channel - Name of the trace channel
* @param {boolean} enabled - new enabled state for it
*/
static setTrace(channel, enabled = true) {

// #if _DEBUG
if (enabled) {
Debug._traceChannels.add(channel);
} else {
Debug._traceChannels.delete(channel);
}
// #endif
}

/**
* Deprecated warning message.
*
Expand All @@ -28,7 +53,7 @@ class Debug {
/**
* Assertion error message. If the assertion is false, the error message is written to the log.
*
* @param {boolean} assertion - The assertion to check.
* @param {boolean|object} assertion - The assertion to check.
* @param {...*} args - The values to be written to the log.
*/
static assert(assertion, ...args) {
Expand Down Expand Up @@ -63,6 +88,18 @@ class Debug {
static error(...args) {
console.error(...args);
}

/**
* Trace message, which is logged to the console if the tracing for the channel is enabled
*
* @param {string} channel - The trace channel
* @param {...*} args - The values to be written to the log.
*/
static trace(channel, ...args) {
if (Debug._traceChannels.has(channel)) {
console.log(`[${channel}] `, ...args);
}
}
}

export { Debug };
277 changes: 277 additions & 0 deletions src/core/preprocessor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
import { Debug } from './debug.js';

// id for debug tracing
const TRACEID = 'Preprocessor';

// accepted keywords
const KEYWORD = /([ ]*)+#(ifn?def|if|endif|else|elif|define|undef)/g;

// #define EXPRESSION
const DEFINE = /define[ ]+([^\n]+)\r?(?:\n|$)/g;

// #undef EXPRESSION
const UNDEF = /undef[ ]+([^\n]+)\r?(?:\n|$)/g;

// #ifdef/#ifndef SOMEDEFINE, #if EXPRESSION
const IF = /(ifdef|ifndef|if)[ ]*([^\r\n]+)\r?\n/g;

// #endif/#else or #elif EXPRESSION
const ENDIF = /(endif|else|elif)([ ]+[^\r\n]+)?\r?(?:\n|$)/g;

// identifier
const IDENTIFIER = /([\w-]+)/;

// [!]defined(EXPRESSION)
const DEFINED = /(!|\s)?defined\(([\w-]+)\)/;

// currently unsupported characters in the expression: | & < > = + -
const INVALID = /[><=|&+-]/g;

/**
* Pure static class implementing subset of C-style preprocessor.
* inspired by: https://github.com/dcodeIO/Preprocessor.js
*
* @ignore
*/
class Preprocessor {
/**
* Run c-like proprocessor on the source code, and resolves the code based on the defines and ifdefs
*
* @param {string} source - The source code to work on.
* @returns {string|null} Returns preprocessed source code, or null in case of error.
*/
static run(source) {

// strips comments, handles // and many cases of /*
source = source.replace(/\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/gm, '$1');

// right trim each line
source = source.split(/\r?\n/)
.map(line => line.trimEnd())
.join('\n');

// proprocess defines / ifdefs ..
source = this._preprocess(source);

if (source !== null) {
// convert lines with only white space into empty string
source = source.split(/\r?\n/)
.map(line => (line.trim() === '' ? '' : line))
.join('\n');

// remove more than 1 consecutive empty lines
source = source.replace(/(\n\n){3,}/gm, '\n\n');
}

return source;
}

static _preprocess(source) {

const originalSource = source;

// stack, storing info about ifdef blocks
const stack = [];

// true if the function encounter a problem
let error = false;

// active defines, maps define name to its value
/** @type {Map<string, string>} */
const defines = new Map();

let match;
while ((match = KEYWORD.exec(source)) !== null) {

const keyword = match[2];
switch (keyword) {
case 'define': {

// read the rest of the define line
DEFINE.lastIndex = match.index;
const define = DEFINE.exec(source);
Debug.assert(define, `Invalid [${keyword}]: ${source.substring(match.index, match.index + 100)}...`);
error ||= define === null;
const expression = define[1];

// split it to identifier name and a value
IDENTIFIER.lastIndex = define.index;
const identifierValue = IDENTIFIER.exec(expression);
const identifier = identifierValue[1];
let value = expression.substring(identifier.length).trim();
if (value === "") value = "true";

// are we inside if-blocks that are accepted
const keep = Preprocessor._keep(stack);

if (keep) {
defines.set(identifier, value);
}

Debug.trace(TRACEID, `${keyword}: [${identifier}] ${value} ${keep ? "" : "IGNORED"}`);

// continue on the next line
KEYWORD.lastIndex = define.index + define[0].length;
break;
}

case 'undef': {

// read the rest of the define line
UNDEF.lastIndex = match.index;
const undef = UNDEF.exec(source);
const identifier = undef[1].trim();

// are we inside if-blocks that are accepted
const keep = Preprocessor._keep(stack);

// remove it from defines
if (keep) {
defines.delete(identifier);
}

Debug.trace(TRACEID, `${keyword}: [${identifier}] ${keep ? "" : "IGNORED"}`);

// continue on the next line
KEYWORD.lastIndex = undef.index + undef[0].length;
break;
}

case 'ifdef':
case 'ifndef':
case 'if': {

// read the if line
IF.lastIndex = match.index;
const iff = IF.exec(source);
const expression = iff[2];

// evaluate expression
const evaluated = Preprocessor.evaluate(expression, defines);
error ||= evaluated.error;
let result = evaluated.result;
if (keyword === 'ifndef') {
result = !result;
}

// add info to the stack (to be handled later)
stack.push({
anyKeep: result, // true if any branch was already accepted
keep: result, // true if this branch is being taken
start: match.index, // start index if IF line
end: IF.lastIndex // end index of IF line
});

Debug.trace(TRACEID, `${keyword}: [${expression}] => ${result}`);

// continue on the next line
KEYWORD.lastIndex = iff.index + iff[0].length;
break;
}

case 'endif':
case 'else':
case 'elif': {

// match the endif
ENDIF.lastIndex = match.index;
const endif = ENDIF.exec(source);

const blockInfo = stack.pop();

// code between if and endif
const blockCode = blockInfo.keep ? source.substring(blockInfo.end, match.index) : "";
Debug.trace(TRACEID, `${keyword}: [previous block] => ${blockCode !== ""}`);

// cut out the IF and ENDIF lines, leave block if required
source = source.substring(0, blockInfo.start) + blockCode + source.substring(ENDIF.lastIndex);
KEYWORD.lastIndex = blockInfo.start + blockCode.length;

// handle else if
const endifCommand = endif[1];
if (endifCommand === 'else' || endifCommand === 'elif') {

// if any branch was already accepted, all else branches need to fail regardless of the result
let result = false;
if (!blockInfo.anyKeep) {
if (endifCommand === 'else') {
result = !blockInfo.keep;
} else {
const evaluated = Preprocessor.evaluate(endif[2], defines);
result = evaluated.result;
error ||= evaluated.error;
}
}

// add back to stack
stack.push({
anyKeep: blockInfo.anyKeep || result,
keep: result,
start: KEYWORD.lastIndex,
end: KEYWORD.lastIndex
});
Debug.trace(TRACEID, `${keyword}: [${endif[2]}] => ${result}`);
}

break;
}
}
}

if (error) {
Debug.error("Failed to preprocess shader: ", { source: originalSource });
return null;
}

return source;
}

// function returns true if the evaluation is inside keep branches
static _keep(stack) {
for (let i = 0; i < stack.length; i++) {
if (!stack[i].keep)
return false;
}

return true;
}

/**
* Very simple expression evaluation, handles cases:
* expression
* defined(expression)
* !defined(expression)
*
* But does not handle more complex cases, which would require more complex system:
* defined(A) || defined(B)
*/
static evaluate(expression, defines) {

const correct = INVALID.exec(expression) === null;
Debug.assert(correct, `Resolving expression like this is not supported: ${expression}`);

// if the format is defined(expression), extract expression
let invert = false;
const defined = DEFINED.exec(expression);
if (defined) {
invert = defined[1] === '!';
expression = defined[2];
}

// test if expression define exists
expression = expression.trim();
let exists = defines.has(expression);

// handle inversion
if (invert) {
exists = !exists;
}

return {
result: exists,
error: !correct
};
}
}

export { Preprocessor };
23 changes: 19 additions & 4 deletions src/graphics/program-lib/chunks/lit/clusteredLight.frag.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@ uniform sampler2D clusterWorldTexture;
uniform sampler2D lightsTexture8;
uniform highp sampler2D lightsTextureFloat;
// complex ifdef expression are not supported, handle it here
// defined(CLUSTER_COOKIES) || defined(CLUSTER_SHADOWS)
#if defined(CLUSTER_COOKIES)
#define CLUSTER_COOKIES_OR_SHADOWS
#endif
#if defined(CLUSTER_SHADOWS)
#define CLUSTER_COOKIES_OR_SHADOWS
#endif
#ifdef CLUSTER_SHADOWS
#ifdef GL2
// TODO: when VSM shadow is supported, it needs to use sampler2D in webgl2
Expand Down Expand Up @@ -358,7 +367,7 @@ void evaluateLight(ClusterLightData light) {
dAtten *= getSpotEffect(light.direction, light.innerConeAngleCos, light.outerConeAngleCos);
}
#if defined(CLUSTER_COOKIES) || defined(CLUSTER_SHADOWS)
#if defined(CLUSTER_COOKIES_OR_SHADOWS)
if (dAtten > 0.00001) {
Expand Down Expand Up @@ -441,8 +450,10 @@ void evaluateLight(ClusterLightData light) {
{
vec3 areaDiffuse = (dAttenD * dAtten) * light.color * dAtten3;
#if defined(CLUSTER_SPECULAR) && defined(CLUSTER_CONSERVE_ENERGY)
areaDiffuse = mix(areaDiffuse, vec3(0), dLTCSpecFres);
#if defined(CLUSTER_SPECULAR)
#if defined(CLUSTER_CONSERVE_ENERGY)
areaDiffuse = mix(areaDiffuse, vec3(0), dLTCSpecFres);
#endif
#endif
// area light diffuse - it does not mix diffuse lighting into specular attenuation
Expand Down Expand Up @@ -494,9 +505,13 @@ void evaluateLight(ClusterLightData light) {
{
vec3 punctualDiffuse = dAtten * light.color * dAtten3;
#if defined(CLUSTER_AREALIGHTS) && defined(CLUSTER_SPECULAR) && defined(CLUSTER_CONSERVE_ENERGY)
#if defined(CLUSTER_AREALIGHTS)
#if defined(CLUSTER_SPECULAR)
#if defined(CLUSTER_CONSERVE_ENERGY)
punctualDiffuse = mix(punctualDiffuse, vec3(0), dSpecularity);
#endif
#endif
#endif
dDiffuseLight += punctualDiffuse;
}
Expand Down
Loading

0 comments on commit 0720039

Please sign in to comment.