Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Preprocessor to resolve ifdefs is applied to shader programs #4156

Merged
merged 12 commits into from
Apr 5, 2022
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',
slimbuck marked this conversation as resolved.
Show resolved Hide resolved
'DebugGraphics.pushGpuMarker',
'DebugGraphics.popGpuMarker',
'WorldClustersDebug.render'
Expand Down
38 changes: 37 additions & 1 deletion src/core/debug.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,30 @@ class Debug {
*/
static _deprecatedMessages = new Set();

/**
* Set storing names of enabled trace channels
*
* @type {Set<string>}
*/
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 +52,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 +87,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} Returns preprocessed source code.
*/
static run(source) {
mvaligursky marked this conversation as resolved.
Show resolved Hide resolved

// 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