Skip to content

Commit 8d4f1fe

Browse files
committed
feat: minimal preamble for GLSL compilation
1 parent 2774b7a commit 8d4f1fe

5 files changed

Lines changed: 552 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,16 @@
2020
compiler now filters out `Nothing` symbols and empty compilation results
2121
before generating the block IIFE.
2222

23+
- **Subscripted variable names in blocks**: Subscripted identifiers like `r_1`
24+
are now treated as compound symbols (not `Subscript` expressions) when the
25+
base is not a known collection. This means `r_1 \coloneq x^2; \frac{1}{r_1}`
26+
correctly declares and assigns to a local variable named `r_1`.
27+
28+
- **Selective GLSL interval preamble**: The interval-GLSL compilation target now
29+
emits only the preamble functions actually used by the compiled expression
30+
(plus their transitive dependencies), instead of the full ~29KB library.
31+
Typical preambles are 60–80% smaller.
32+
2333
- **Fix recursive GLSL gamma function**: The `_gpu_gamma()` preamble in the GPU
2434
and interval-GLSL compilation targets used recursion for the reflection formula
2535
(z < 0.5), which is illegal in GLSL. Replaced with a non-recursive

src/compute-engine/compilation/interval-glsl-target.ts

Lines changed: 201 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1180,6 +1180,205 @@ float ia_notEqual(IntervalResult a, IntervalResult b) {
11801180
}
11811181
`;
11821182

1183+
// ---------------------------------------------------------------------------
1184+
// Selective preamble builder
1185+
//
1186+
// Parses the monolithic GLSL_INTERVAL_LIBRARY into individual function blocks
1187+
// with auto-detected dependencies. At compile time, only the functions
1188+
// referenced by the compiled expression (plus their transitive dependencies)
1189+
// are emitted.
1190+
// ---------------------------------------------------------------------------
1191+
1192+
interface GLSLFunctionBlock {
1193+
/** The function name (e.g. "ia_sin") */
1194+
name: string;
1195+
/** Full source text of the function (including preceding comment lines) */
1196+
source: string;
1197+
/** Names of other ia_ or _gpu_ functions called from this function's body */
1198+
deps: string[];
1199+
}
1200+
1201+
/** Header: constants, struct, epsilon — always emitted */
1202+
let _preambleHeader = '';
1203+
/** Mid-preamble constant blocks (e.g. IA_TRUE/FALSE/MAYBE) keyed by marker */
1204+
const _preambleConstants: Map<string, string> = new Map();
1205+
/** Individual function blocks keyed by function name */
1206+
const _preambleFunctions: Map<string, GLSLFunctionBlock> = new Map();
1207+
/** Set to true once parsing is done */
1208+
let _preambleParsed = false;
1209+
1210+
/** Regex matching function declarations in the GLSL preamble */
1211+
const GLSL_FUNC_RE =
1212+
/^(IntervalResult|vec2|float|bool|void)\s+(ia_\w+|_gpu_\w+)\s*\(/;
1213+
1214+
/** Regex to find calls to ia_ or _gpu_ functions within a body */
1215+
const GLSL_CALL_RE = /\b(ia_\w+|_gpu_\w+)\s*\(/g;
1216+
1217+
/** Regex for constant declarations like `const float IA_TRUE = 1.0;` */
1218+
const GLSL_CONST_RE = /^const\s+float\s+(IA_\w+)\s*=/;
1219+
1220+
function parsePreamble(): void {
1221+
if (_preambleParsed) return;
1222+
_preambleParsed = true;
1223+
1224+
const lines = GLSL_INTERVAL_LIBRARY.split('\n');
1225+
let headerDone = false;
1226+
const headerLines: string[] = [];
1227+
let currentBlock: string[] = [];
1228+
let currentName: string | null = null;
1229+
let braceDepth = 0;
1230+
let inFunction = false;
1231+
let pendingComments: string[] = [];
1232+
let pendingConstants: string[] = [];
1233+
1234+
for (const line of lines) {
1235+
// Check if this is a constant declaration outside a function
1236+
const constMatch = !inFunction && GLSL_CONST_RE.exec(line);
1237+
if (constMatch && headerDone) {
1238+
// Mid-preamble constant (e.g. IA_TRUE) — accumulate
1239+
pendingConstants.push(line);
1240+
continue;
1241+
}
1242+
1243+
const funcMatch = !inFunction && GLSL_FUNC_RE.exec(line);
1244+
1245+
if (funcMatch) {
1246+
if (!headerDone) {
1247+
_preambleHeader = headerLines.join('\n');
1248+
headerDone = true;
1249+
}
1250+
1251+
currentName = funcMatch[2];
1252+
currentBlock = [...pendingComments, ...pendingConstants, line];
1253+
pendingComments = [];
1254+
pendingConstants = [];
1255+
inFunction = true;
1256+
braceDepth = 0;
1257+
1258+
// Count braces on this line
1259+
for (const ch of line) {
1260+
if (ch === '{') braceDepth++;
1261+
if (ch === '}') braceDepth--;
1262+
}
1263+
if (braceDepth <= 0) {
1264+
// Single-line function
1265+
finishFunction(currentName, currentBlock.join('\n'));
1266+
inFunction = false;
1267+
currentName = null;
1268+
}
1269+
} else if (inFunction) {
1270+
currentBlock.push(line);
1271+
for (const ch of line) {
1272+
if (ch === '{') braceDepth++;
1273+
if (ch === '}') braceDepth--;
1274+
}
1275+
if (braceDepth <= 0) {
1276+
finishFunction(currentName!, currentBlock.join('\n'));
1277+
inFunction = false;
1278+
currentName = null;
1279+
}
1280+
} else if (!headerDone) {
1281+
headerLines.push(line);
1282+
} else {
1283+
// Between functions — could be comment or blank
1284+
const trimmed = line.trim();
1285+
if (trimmed.startsWith('//') || trimmed === '') {
1286+
pendingComments.push(line);
1287+
}
1288+
}
1289+
}
1290+
1291+
// Flush any remaining pending constants into the header
1292+
if (pendingConstants.length > 0) {
1293+
_preambleHeader += '\n' + pendingConstants.join('\n');
1294+
}
1295+
}
1296+
1297+
function finishFunction(name: string, source: string): void {
1298+
// Auto-detect dependencies by scanning the body for ia_*/​_gpu_* calls
1299+
const deps = new Set<string>();
1300+
let match: RegExpExecArray | null;
1301+
const callRe = new RegExp(GLSL_CALL_RE.source, 'g');
1302+
while ((match = callRe.exec(source)) !== null) {
1303+
const callee = match[1];
1304+
if (callee !== name) deps.add(callee);
1305+
}
1306+
1307+
// GLSL overloads: the IntervalResult overload of ia_sin calls ia_sin(vec2)
1308+
// Since they share a name, we don't add self-references as deps.
1309+
// But we do need to ensure all overloads of a function are emitted together.
1310+
1311+
const existing = _preambleFunctions.get(name);
1312+
if (existing) {
1313+
// Multiple overloads — merge source and deps
1314+
existing.source += '\n\n' + source;
1315+
for (const d of deps) existing.deps.push(d);
1316+
} else {
1317+
_preambleFunctions.set(name, {
1318+
name,
1319+
source,
1320+
deps: [...deps],
1321+
});
1322+
}
1323+
}
1324+
1325+
/**
1326+
* Build a minimal interval GLSL preamble containing only the functions
1327+
* that the compiled code actually uses (plus transitive dependencies).
1328+
*/
1329+
function buildIntervalPreamble(code: string): string {
1330+
parsePreamble();
1331+
1332+
// 1. Find all ia_*/​_gpu_* calls in the compiled expression code
1333+
const needed = new Set<string>();
1334+
let match: RegExpExecArray | null;
1335+
const callRe = new RegExp(GLSL_CALL_RE.source, 'g');
1336+
while ((match = callRe.exec(code)) !== null) {
1337+
needed.add(match[1]);
1338+
}
1339+
1340+
if (needed.size === 0) return _preambleHeader;
1341+
1342+
// 2. Resolve transitive dependencies
1343+
const resolved = new Set<string>();
1344+
function resolve(name: string): void {
1345+
if (resolved.has(name)) return;
1346+
const block = _preambleFunctions.get(name);
1347+
if (!block) return;
1348+
for (const dep of block.deps) resolve(dep);
1349+
resolved.add(name);
1350+
}
1351+
for (const name of needed) resolve(name);
1352+
1353+
// 3. Emit in dependency order (resolved set preserves insertion order,
1354+
// and deps are resolved before dependents)
1355+
const parts: string[] = [_preambleHeader];
1356+
1357+
// Check if any comparison functions are needed — if so, include IA_TRUE/FALSE/MAYBE
1358+
const needsComparisonConstants = [...resolved].some(
1359+
(name) =>
1360+
name.startsWith('ia_less') ||
1361+
name.startsWith('ia_greater') ||
1362+
name.startsWith('ia_equal') ||
1363+
name.startsWith('ia_notEqual') ||
1364+
name === 'ia_and' ||
1365+
name === 'ia_or' ||
1366+
name === 'ia_not'
1367+
);
1368+
if (needsComparisonConstants) {
1369+
parts.push(
1370+
'\nconst float IA_TRUE = 1.0;\nconst float IA_FALSE = 0.0;\nconst float IA_MAYBE = 0.5;'
1371+
);
1372+
}
1373+
1374+
for (const name of resolved) {
1375+
const block = _preambleFunctions.get(name);
1376+
if (block) parts.push('\n' + block.source);
1377+
}
1378+
1379+
return parts.join('\n');
1380+
}
1381+
11831382
/**
11841383
* GLSL interval operators - all become function calls
11851384
*/
@@ -1450,7 +1649,7 @@ export class IntervalGLSLTarget implements LanguageTarget<Expression> {
14501649
target: 'interval-glsl',
14511650
success: true,
14521651
code: glslCode,
1453-
preamble: GLSL_INTERVAL_LIBRARY,
1652+
preamble: buildIntervalPreamble(glslCode),
14541653
};
14551654
}
14561655

@@ -1514,7 +1713,7 @@ export class IntervalGLSLTarget implements LanguageTarget<Expression> {
15141713
return `#version ${version}
15151714
precision highp float;
15161715
1517-
${GLSL_INTERVAL_LIBRARY}
1716+
${buildIntervalPreamble(body)}
15181717
15191718
IntervalResult ${functionName}(${params}) {
15201719
return ${body};

0 commit comments

Comments
 (0)