shaderCodeInliner.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408
  1. interface IInlineFunctionDescr {
  2. name: string;
  3. type: string;
  4. parameters: string[];
  5. body: string;
  6. callIndex: number;
  7. }
  8. /** @hidden */
  9. export class ShaderCodeInliner {
  10. static readonly InlineToken = "#define inline";
  11. static readonly RegexpFindFunctionNameAndType = /(?<=\s+?(\w+)\s+(\w+)\s*?)$/;
  12. private _sourceCode: string;
  13. private _functionDescr: IInlineFunctionDescr[];
  14. private _numMaxIterations: number;
  15. public debug: boolean = false;
  16. public get code(): string {
  17. return this._sourceCode;
  18. }
  19. constructor(sourceCode: string, numMaxIterations = 20) {
  20. this._sourceCode = sourceCode;
  21. this._numMaxIterations = numMaxIterations;
  22. this._functionDescr = [];
  23. }
  24. public processCode() {
  25. if (this.debug) {
  26. console.log(`Start inlining process (code size=${this._sourceCode.length})...`);
  27. }
  28. this._collectFunctions();
  29. this._processInlining(this._numMaxIterations);
  30. if (this.debug) {
  31. console.log("End of inlining process.");
  32. }
  33. }
  34. private _collectFunctions() {
  35. let startIndex = 0;
  36. while (startIndex < this._sourceCode.length) {
  37. // locate the function to inline and extract its name
  38. const inlineTokenIndex = this._sourceCode.indexOf(ShaderCodeInliner.InlineToken, startIndex);
  39. if (inlineTokenIndex < 0) {
  40. break;
  41. }
  42. const funcParamsStartIndex = this._sourceCode.indexOf("(", inlineTokenIndex + ShaderCodeInliner.InlineToken.length);
  43. if (funcParamsStartIndex < 0) {
  44. if (this.debug) {
  45. console.warn(`Could not find the opening parenthesis after the token. startIndex=${startIndex}`);
  46. }
  47. startIndex = inlineTokenIndex + ShaderCodeInliner.InlineToken.length;
  48. continue;
  49. }
  50. const funcNameMatch = ShaderCodeInliner.RegexpFindFunctionNameAndType.exec(this._sourceCode.substring(inlineTokenIndex + ShaderCodeInliner.InlineToken.length, funcParamsStartIndex));
  51. if (!funcNameMatch) {
  52. if (this.debug) {
  53. console.warn(`Could not extract the name/type of the function from: ${this._sourceCode.substring(inlineTokenIndex + ShaderCodeInliner.InlineToken.length, funcParamsStartIndex)}`);
  54. }
  55. startIndex = inlineTokenIndex + ShaderCodeInliner.InlineToken.length;
  56. continue;
  57. }
  58. const [funcType, funcName] = [funcNameMatch[1], funcNameMatch[2]];
  59. // extract the parameters of the function as a whole string (without the leading / trailing parenthesis)
  60. const funcParamsEndIndex = this._extractBetweenMarkers('(', ')', this._sourceCode, funcParamsStartIndex);
  61. if (funcParamsEndIndex < 0) {
  62. if (this.debug) {
  63. console.warn(`Could not extract the parameters the function '${funcName}' (type=${funcType}). funcParamsStartIndex=${funcParamsStartIndex}`);
  64. }
  65. startIndex = inlineTokenIndex + ShaderCodeInliner.InlineToken.length;
  66. continue;
  67. }
  68. const funcParams = this._sourceCode.substring(funcParamsStartIndex + 1, funcParamsEndIndex);
  69. // extract the body of the function (with the curly brackets)
  70. const funcBodyStartIndex = this._skipWhitespaces(this._sourceCode, funcParamsEndIndex + 1);
  71. if (funcBodyStartIndex === this._sourceCode.length) {
  72. if (this.debug) {
  73. console.warn(`Could not extract the body of the function '${funcName}' (type=${funcType}). funcParamsEndIndex=${funcParamsEndIndex}`);
  74. }
  75. startIndex = inlineTokenIndex + ShaderCodeInliner.InlineToken.length;
  76. continue;
  77. }
  78. const funcBodyEndIndex = this._extractBetweenMarkers('{', '}', this._sourceCode, funcBodyStartIndex);
  79. if (funcBodyEndIndex < 0) {
  80. if (this.debug) {
  81. console.warn(`Could not extract the body of the function '${funcName}' (type=${funcType}). funcBodyStartIndex=${funcBodyStartIndex}`);
  82. }
  83. startIndex = inlineTokenIndex + ShaderCodeInliner.InlineToken.length;
  84. continue;
  85. }
  86. const funcBody = this._sourceCode.substring(funcBodyStartIndex, funcBodyEndIndex + 1);
  87. // process the parameters: extract each names
  88. const params = this._removeComments(funcParams).split(",");
  89. const paramNames = [];
  90. for (let p = 0; p < params.length; ++p) {
  91. const param = params[p].trim();
  92. const idx = param.lastIndexOf(" ");
  93. if (idx >= 0) {
  94. paramNames.push(param.substring(idx + 1));
  95. }
  96. }
  97. if (funcType !== 'void') {
  98. // for functions that return a value, we will replace "return" by "tempvarname = ", tempvarname being a unique generated name
  99. paramNames.push('return');
  100. }
  101. // collect the function
  102. this._functionDescr.push({
  103. "name": funcName,
  104. "type": funcType,
  105. "parameters": paramNames,
  106. "body": funcBody,
  107. "callIndex": 0,
  108. });
  109. startIndex = funcBodyEndIndex + 1;
  110. // remove the function from the source code
  111. const partBefore = inlineTokenIndex > 0 ? this._sourceCode.substring(0, inlineTokenIndex) : "";
  112. const partAfter = funcBodyEndIndex + 1 < this._sourceCode.length - 1 ? this._sourceCode.substring(funcBodyEndIndex + 1) : "";
  113. this._sourceCode = partBefore + partAfter;
  114. startIndex -= funcBodyEndIndex + 1 - inlineTokenIndex;
  115. }
  116. if (this.debug) {
  117. console.log(`Collect functions: ${this._functionDescr.length} functions found. functionDescr=`, this._functionDescr);
  118. }
  119. }
  120. private _processInlining(numMaxIterations: number = 20): boolean {
  121. while (numMaxIterations-- >= 0) {
  122. if (!this._replaceFunctionCallsByCode()) {
  123. break;
  124. }
  125. }
  126. if (this.debug) {
  127. console.log(`numMaxIterations is ${numMaxIterations} after inlining process`);
  128. }
  129. return numMaxIterations >= 0;
  130. }
  131. private _extractBetweenMarkers(markerOpen: string, markerClose: string, block: string, startIndex: number): number {
  132. let currPos = startIndex,
  133. openMarkers = 0,
  134. waitForChar = '';
  135. while (currPos < block.length) {
  136. let currChar = block.charAt(currPos);
  137. if (!waitForChar) {
  138. switch (currChar) {
  139. case markerOpen:
  140. openMarkers++;
  141. break;
  142. case markerClose:
  143. openMarkers--;
  144. break;
  145. case '"':
  146. case "'":
  147. case "`":
  148. waitForChar = currChar;
  149. break;
  150. case '/':
  151. if (currPos + 1 < block.length) {
  152. const nextChar = block.charAt(currPos + 1);
  153. if (nextChar === '/') {
  154. waitForChar = '\n';
  155. } else if (nextChar === '*') {
  156. waitForChar = '*/';
  157. }
  158. }
  159. break;
  160. }
  161. } else {
  162. if (currChar === waitForChar) {
  163. if (waitForChar === '"' || waitForChar === "'") {
  164. block.charAt(currPos - 1) !== '\\' && (waitForChar = '');
  165. } else {
  166. waitForChar = '';
  167. }
  168. } else if (waitForChar === '*/' && currChar === '*' && currPos + 1 < block.length) {
  169. block.charAt(currPos + 1) === '/' && (waitForChar = '');
  170. if (waitForChar === '') {
  171. currPos++;
  172. }
  173. }
  174. }
  175. currPos++ ;
  176. if (openMarkers === 0) {
  177. break;
  178. }
  179. }
  180. return openMarkers === 0 ? currPos - 1 : -1;
  181. }
  182. private _skipWhitespaces(s: string, index: number): number {
  183. while (index < s.length) {
  184. const c = s[index];
  185. if (c !== ' ' && c !== '\n' && c !== '\r' && c !== '\t' && c !== '\u000a' && c !== '\u00a0') {
  186. break;
  187. }
  188. index++;
  189. }
  190. return index;
  191. }
  192. private _removeComments(block: string): string {
  193. let currPos = 0,
  194. waitForChar = '',
  195. inComments = false,
  196. s = [];
  197. while (currPos < block.length) {
  198. let currChar = block.charAt(currPos);
  199. if (!waitForChar) {
  200. switch (currChar) {
  201. case '"':
  202. case "'":
  203. case "`":
  204. waitForChar = currChar;
  205. break;
  206. case '/':
  207. if (currPos + 1 < block.length) {
  208. const nextChar = block.charAt(currPos + 1);
  209. if (nextChar === '/') {
  210. waitForChar = '\n';
  211. inComments = true;
  212. } else if (nextChar === '*') {
  213. waitForChar = '*/';
  214. inComments = true;
  215. }
  216. }
  217. break;
  218. }
  219. if (!inComments) {
  220. s.push(currChar);
  221. }
  222. } else {
  223. if (currChar === waitForChar) {
  224. if (waitForChar === '"' || waitForChar === "'") {
  225. block.charAt(currPos - 1) !== '\\' && (waitForChar = '');
  226. s.push(currChar);
  227. } else {
  228. waitForChar = '';
  229. inComments = false;
  230. }
  231. } else if (waitForChar === '*/' && currChar === '*' && currPos + 1 < block.length) {
  232. block.charAt(currPos + 1) === '/' && (waitForChar = '');
  233. if (waitForChar === '') {
  234. inComments = false;
  235. currPos++;
  236. }
  237. } else {
  238. if (!inComments) {
  239. s.push(currChar);
  240. }
  241. }
  242. }
  243. currPos++ ;
  244. }
  245. return s.join('');
  246. }
  247. private _replaceFunctionCallsByCode(): boolean {
  248. let doAgain = false;
  249. for (const func of this._functionDescr) {
  250. const { name, type, parameters, body } = func;
  251. let startIndex = 0;
  252. while (startIndex < this._sourceCode.length) {
  253. // Look for the function name in the source code
  254. const functionCallIndex = this._sourceCode.indexOf(name, startIndex);
  255. if (functionCallIndex < 0) {
  256. break;
  257. }
  258. // Find the opening parenthesis
  259. const callParamsStartIndex = this._skipWhitespaces(this._sourceCode, functionCallIndex + name.length);
  260. if (callParamsStartIndex === this._sourceCode.length || this._sourceCode.charAt(callParamsStartIndex) !== '(') {
  261. startIndex = functionCallIndex + name.length;
  262. continue;
  263. }
  264. // extract the parameters of the function call as a whole string (without the leading / trailing parenthesis)
  265. const callParamsEndIndex = this._extractBetweenMarkers('(', ')', this._sourceCode, callParamsStartIndex);
  266. if (callParamsEndIndex < 0) {
  267. if (this.debug) {
  268. console.warn(`Could not extract the parameters of the function call. Function '${name}' (type=${type}). callParamsStartIndex=${callParamsStartIndex}`);
  269. }
  270. startIndex = functionCallIndex + name.length;
  271. continue;
  272. }
  273. const callParams = this._sourceCode.substring(callParamsStartIndex + 1, callParamsEndIndex);
  274. // process the parameter call: extract each names
  275. const params = this._removeComments(callParams).split(",");
  276. const paramNames = [];
  277. for (let p = 0; p < params.length; ++p) {
  278. const param = params[p].trim();
  279. paramNames.push(param);
  280. }
  281. const retParamName = type !== 'void' ? name + '_' + (func.callIndex++) : null;
  282. if (retParamName) {
  283. paramNames.push(retParamName + ' =');
  284. }
  285. if (paramNames.length !== parameters.length) {
  286. if (this.debug) {
  287. console.warn(`Invalid function call: not the same number of parameters for the call than the number expected by the function. Function '${name}' (type=${type}). function parameters=${parameters}, call parameters=${paramNames}`);
  288. }
  289. startIndex = functionCallIndex + name.length;
  290. continue;
  291. }
  292. startIndex = callParamsEndIndex + 1;
  293. // replace the function call by the body function
  294. const funcBody = this._replaceNames(body, parameters, paramNames);
  295. let partBefore = functionCallIndex > 0 ? this._sourceCode.substring(0, functionCallIndex) : "";
  296. let partAfter = callParamsEndIndex + 1 < this._sourceCode.length - 1 ? this._sourceCode.substring(callParamsEndIndex + 1) : "";
  297. if (retParamName) {
  298. // case where the function returns a value. We generate:
  299. // FUNCTYPE retParamName;
  300. // {function body}
  301. // and replace the function call by retParamName
  302. const injectDeclarationIndex = this._findBackward(this._sourceCode, functionCallIndex - 1, '\n');
  303. partBefore = this._sourceCode.substring(0, injectDeclarationIndex + 1);
  304. let partBetween = this._sourceCode.substring(injectDeclarationIndex + 1, functionCallIndex);
  305. this._sourceCode = partBefore + type + " " + retParamName + ";\n" + funcBody + "\n" + partBetween + retParamName + partAfter;
  306. if (this.debug) {
  307. console.log(`Replace function call by code. Function '${name}' (type=${type}). injectDeclarationIndex=${injectDeclarationIndex}`);
  308. }
  309. } else {
  310. // simple case where the return value of the function is "void"
  311. this._sourceCode = partBefore + funcBody + partAfter;
  312. startIndex += funcBody.length - (callParamsEndIndex + 1 - functionCallIndex);
  313. if (this.debug) {
  314. console.log(`Replace function call by code. Function '${name}' (type=${type}). functionCallIndex=${functionCallIndex}`);
  315. }
  316. }
  317. doAgain = true;
  318. }
  319. }
  320. return doAgain;
  321. }
  322. private _findBackward(s: string, index: number, c: string): number {
  323. while (index >= 0 && s.charAt(index) !== c) {
  324. index--;
  325. }
  326. return index;
  327. }
  328. private _escapeRegExp(s: string): string {
  329. return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  330. }
  331. private _replaceNames(code: string, sources: string[], destinations: string[]): string {
  332. for (let i = 0; i < sources.length; ++i) {
  333. const source = new RegExp(this._escapeRegExp(sources[i]), 'g'),
  334. destination = destinations[i];
  335. code = code.replace(source, destination);
  336. }
  337. return code;
  338. }
  339. }