diff --git a/contributor_docs/p5.strands.md b/contributor_docs/p5.strands.md
new file mode 100644
index 0000000000..271e72439f
--- /dev/null
+++ b/contributor_docs/p5.strands.md
@@ -0,0 +1,251 @@
+
+
+# p5.strands Overview
+
+Shader programming is an area of creative coding that can feel like a dark art to many. People share lots of stunning visuals that are created with shaders, but shaders feel like a completely different way of coding, requiring you to learn a new language, pipeline, and paradigm.
+
+p5.strands hopes to address all of those issues by letting you write shader snippets in JavaScript and compiling it to OpenGL Shading Language (GLSL) for you!
+
+## Code processing pipeline
+
+At its core, p5.strands works in four steps:
+1. The user writes a function in pseudo-JavaScript.
+2. p5.strands transpiles that into actual JavaScript and rewrites aspects of your code.
+3. The transpiled code is run. Variable modification function calls are tracked in a graph data structure.
+4. p5.strands generates GLSL code from that graph.
+
+## Why pseudo-JavaScript?
+
+The code the user writes when using p5.strands is mostly JavaScript, with some extensions. Shader code heavily encourages use of vectors, and the extensions all make this as easy in JavaScript as in GLSL.
+- In JavaScript, there is not a vector data type. In p5.strands, you create vectors by creating array, e.g. `myVec = [1, 0, 0]`. You can't use actual arrays in p5.strands; all arrays are fixed-size vectors.
+- In JavaScript, you can only use mathematical operators like `+` between numbers and strings, not with vectors. In p5.strands, we allow use of these operators between vectors.
+- In GLSL, you can do something called *swizzling*, where you can create new vectors out of the components of an existing vector, e.g. `myvec.xy`, `myvec.bgr`, or even `myvec.zzzz`. p5.strands adds support for this on its vectors.
+
+When we transpile the input code, we rewrite these into valid JavaScript. Array literals are turned into function calls like `vec3(1, 0, 0)` which return vector class instances. These instances are wrapped in a `Proxy` that handles property accesses that look like swizzles, and converts them into sub-vector references. Operators between vectors like `a + b` are rewritten into method calls, like `a.add(b)`.
+
+If a user writes something like this:
+
+```js
+baseMaterialShader().modify(() => {
+ const t = uniformFloat(() => millis())
+ getWorldInputs((inputs) => {
+ inputs.position += [20, 25, 20] * sin(inputs.position.y * 0.05 + t * 0.004)
+ return inputs
+ })
+})
+```
+
+...it gets transpiled to something like this:
+```js
+baseMaterialShader().modify(() => {
+ const t = uniformFloat('t', () => millis())
+ getWorldInputs((inputs) => {
+ inputs.position = inputs.position.add(strandsNode([20, 25, 20]).mult(sin(inputs.position.y.mult(0.05).add(strandsNode(t).mult(0.004)))))
+ return inputs
+ })
+})
+```
+
+## The program graph
+
+The overall structure of a shader program is represented by a **control-flow graph (CFG)**. This divides up a program into chunks that need to be outputted in linear order based on control flow. A program like the one below would get chunked up around the if statement:
+
+```js
+// Start chunk 1
+let a = 0;
+let b = 1;
+// End chunk 1
+
+// Start chunk 2
+if (a < 2) {
+ b = 10;
+}
+// End chunk 2
+
+// Start chunk 3
+b += 2;
+return b;
+// End chunk 3
+```
+
+We store the individual states that variables can be in as nodes in a **directed acyclic graph (DAG)**. This is a fancy name that basically means each of these variable states may depend on previous variable states, and outputs can't feed back into inputs. Each time you modify a variable, that represents a new state of that variable. For example, below, it is not sufficient to know that `c` depends on `a` and `b`; you also need to know *which version of `b`* it branched off from:
+
+```js
+let a = 0;
+let b = 1;
+b += 1;
+let c = a + b;
+return c;
+```
+
+We can imagine giving each of these states a separate name to make it clearer. In fact, that's what we do when we output GLSL, because we don't need to preserve variable names.
+```js
+let a_0 = 0;
+let b_0 = 1;
+let b_1 = b_0 + 1;
+let c_0 = b_1 + a_0;
+return c_0;
+```
+
+When we generate GLSL from the graph, we start from the variables we need to output, the return values of the function (e.g. `c_0` in the example above.) From there, we can track dependencies through the DAG (in this case, `b_1` and `a_1`). Each dependency has their own dependencies. We make sure we output the dependencies for a node before the node itself.
+
+Each node in the DAG belongs to a chunk in the CFG. This helps us keep track of key points in the code. If we need to, for example, generate a temporary variable at the end of an if statement, we can refer to that CFG chunk rather than whatever the last value node in the if statement happens to be.
+
+## Control flow
+
+p5.strands has to convert any control flow that should show up in GLSL into function calls instead of JavaScript keywords. If we don't, they run in JavaScript, and are invisible to GLSL generation. For example, if you had a loop that runs 10 times that adds 1 each time, it would output the add 1 line 10 times rather than outputting a for loop.
+
+
+
+Input |
+Output without converting control flow |
+
+
+
+
+```js
+let a = 0;
+for (let i = 0; i < 10; i++) {
+ a += 2;
+}
+return a;
+```
+
+ |
+
+
+```glsl
+float a = 0.0;
+a += 2.0;
+a += 2.0;
+a += 2.0;
+a += 2.0;
+a += 2.0;
+a += 2.0;
+a += 2.0;
+a += 2.0;
+a += 2.0;
+a += 2.0;
+return a;
+```
+
+ |
+
+
+
+However, once we have a function call instead of real control flow, we also need a way to make sure that when the users' javascript subsequently references nodes that were updated in the control flow, they properly reference the modified value after the `if` or `for` and not the original value.
+
+
+
+Input |
+Transpiled without updating references |
+States without updating references |
+
+
+
+
+```js
+let a = 0;
+for (let i = 0; i < 10; i++) {
+ a += 2;
+}
+let b = a + 1;
+return b;
+```
+
+ |
+
+
+```js
+let a = 0;
+p5.strandsFor(
+ () => 0,
+ (i) => i.lessThan(10),
+ (i) => i.add(1),
+
+ () => {
+ a = a.add(2);
+ }
+);
+let b = a.add(1);
+return b;
+```
+
+ |
+
+
+```js
+let a_0 = 0;
+
+p5.strandsFor(
+ // ...
+)
+// At this point, the final state of a is a_n
+
+// ...but since we didn't actually run the loop,
+// b still refers to the initial state of a!
+let b_0 = a_0.add(1);
+return b;
+```
+
+ |
+
+
+
+For that, we make the function calls return updated values, and we generate JS code that assigns these updated values back to the original JS variables. So for loops end up transpiled to something like this, inspired by the JavaScript `reduce` function:
+
+
+
+Input |
+Transpiled with updated references |
+
+
+
+
+```js
+let a = 0;
+for (let i = 0; i < 10; i++) {
+ a += 2;
+}
+let b = a + 1;
+return b;
+```
+
+ |
+
+
+```js
+let a = 0;
+
+const outputState = p5.strandsFor(
+ () => 0,
+ (i) => i.lessThan(10),
+ (i) => i.add(1),
+
+ // Explicitly output new state based on prev state
+ (i, prevState) => {
+ return { a: prevState.a.add(2) };
+ },
+
+ { a } // Pass in initial state
+);
+a = outputState.a; // Update reference
+
+// b now correctly is based off of the final state of a
+let b = a.add(1);
+return b;
+```
+
+ |
+
+
+
+We use a special kind of node in the DAG called a **phi node**, something used in compilers to refer to the result of some conditional execution. In the example above, the state of `a` in the output state is represented by a phi node.
+
+In the CFG, we surround chunks producing phi nodes by a `BRANCH` and a `MERGE` chunk. In the `BRANCH` chunk, we can initialize phi nodes, sometimes giving them initial values. In the `MERGE` chunk, the value of the phi node has stabilized, and other nodes can use them as a dependency.
+
+## GLSL generation
+
+GLSL is currently the only output format we support, but p5.strands is designed to be able to generate multiple formats. Specifically, in WebGPU, they use the WebGPU Shading Language (WGSL). Our goal is that your same JavaScript p5.strands code can be used in WebGL or WebGPU without you having to do any modifications.
+
+To support this, p5.strands separates out code generation into **backends.** A backend is responsible for converting each type of CFG chunk into a string of shader source code. We currently have a GLSL backend, but in the future we'll have a WGSL backend too!
diff --git a/src/strands/ir_builders.js b/src/strands/ir_builders.js
index dcecc7d0f9..127d19e235 100644
--- a/src/strands/ir_builders.js
+++ b/src/strands/ir_builders.js
@@ -2,7 +2,7 @@ import * as DAG from './ir_dag'
import * as CFG from './ir_cfg'
import * as FES from './strands_FES'
import { NodeType, OpCode, BaseType, DataType, BasePriority, OpCodeToSymbol, typeEquals, } from './ir_types';
-import { createStrandsNode, StrandsNode } from './strands_api';
+import { createStrandsNode, StrandsNode } from './strands_node';
import { strandsBuiltinFunctions } from './strands_builtins';
//////////////////////////////////////////////
@@ -165,7 +165,6 @@ export function memberAccessNode(strandsContext, parentNode, componentNode, memb
export function structInstanceNode(strandsContext, structTypeInfo, identifier, dependsOn) {
const { cfg, dag, } = strandsContext;
-
if (dependsOn.length === 0) {
for (const prop of structTypeInfo.properties) {
const typeInfo = prop.dataType;
@@ -266,7 +265,6 @@ export function primitiveConstructorNode(strandsContext, typeInfo, dependsOn) {
};
const id = constructTypeFromIDs(strandsContext, finalType, mappedDependencies);
-
if (typeInfo.baseType !== BaseType.DEFER) {
CFG.recordInBasicBlock(cfg, cfg.currentBlock, id);
}
@@ -419,11 +417,11 @@ export function functionCallNode(
return { id, dimension: inferredReturnType.dimension };
}
-export function statementNode(strandsContext, opCode) {
+export function statementNode(strandsContext, statementType) {
const { dag, cfg } = strandsContext;
const nodeData = DAG.createNodeData({
nodeType: NodeType.STATEMENT,
- opCode
+ statementType
});
const id = DAG.getOrCreateNode(dag, nodeData);
CFG.recordInBasicBlock(cfg, cfg.currentBlock, id);
@@ -458,7 +456,7 @@ export function swizzleTrap(id, dimension, strandsContext, onRebind) {
return Reflect.get(...arguments);
} else {
for (const set of swizzleSets) {
- if ([...property].every(char => set.includes(char))) {
+ if ([...property.toString()].every(char => set.includes(char))) {
const swizzle = [...property].map(char => {
const index = set.indexOf(char);
return swizzleSets[0][index];
@@ -476,12 +474,11 @@ export function swizzleTrap(id, dimension, strandsContext, onRebind) {
chars.every(c => swizzleSet.includes(c)) &&
new Set(chars).size === chars.length &&
target.dimension >= chars.length;
-
if (!valid) continue;
const dim = target.dimension;
- // lanes are the underlying values of the target vector
+ // lanes are the underlying values of the target vector
// e.g. lane 0 holds the value aliased by 'x', 'r', and 's'
// the lanes array is in the 'correct' order
const lanes = new Array(dim);
@@ -521,7 +518,7 @@ export function swizzleTrap(id, dimension, strandsContext, onRebind) {
}
// The canonical index refers to the actual value's position in the vector lanes
- // i.e. we are finding (3,2,1) from .zyx
+ // i.e. we are finding (3,2,1) from .zyx
// We set the correct value in the lanes array
for (let j = 0; j < chars.length; j++) {
const canonicalIndex = swizzleSet.indexOf(chars[j]);
@@ -538,9 +535,9 @@ export function swizzleTrap(id, dimension, strandsContext, onRebind) {
target.id = newID;
- // If we swizzle assign on a struct component i.e.
+ // If we swizzle assign on a struct component i.e.
// inputs.position.rg = [1, 2]
- // The onRebind callback will update the structs components so that it refers to the new values,
+ // The onRebind callback will update the structs components so that it refers to the new values,
// and make a new ID for the struct with these new values
if (typeof onRebind === 'function') {
onRebind(newID);
diff --git a/src/strands/ir_cfg.js b/src/strands/ir_cfg.js
index 7bdcf09382..cf25a23d53 100644
--- a/src/strands/ir_cfg.js
+++ b/src/strands/ir_cfg.js
@@ -31,6 +31,11 @@ export function popBlock(graph) {
graph.currentBlock = graph.blockStack[len-1];
}
+export function pushBlockForModification(graph, blockID) {
+ graph.blockStack.push(blockID);
+ graph.currentBlock = blockID;
+}
+
export function createBasicBlock(graph, blockType) {
const id = graph.nextID++;
graph.blockTypes[id] = blockType;
@@ -75,7 +80,6 @@ export function printBlockData(graph, id) {
export function sortCFG(adjacencyList, start) {
const visited = new Set();
const postOrder = [];
-
function dfs(v) {
if (visited.has(v)) {
return;
@@ -86,7 +90,7 @@ export function sortCFG(adjacencyList, start) {
}
postOrder.push(v);
}
-
+
dfs(start);
return postOrder.reverse();
}
\ No newline at end of file
diff --git a/src/strands/ir_dag.js b/src/strands/ir_dag.js
index 7633d534c6..45eadb473a 100644
--- a/src/strands/ir_dag.js
+++ b/src/strands/ir_dag.js
@@ -6,8 +6,8 @@ import * as FES from './strands_FES';
/////////////////////////////////
export function createDirectedAcyclicGraph() {
- const graph = {
- nextID: 0,
+ const graph = {
+ nextID: 0,
cache: new Map(),
nodeTypes: [],
baseTypes: [],
@@ -21,16 +21,16 @@ export function createDirectedAcyclicGraph() {
statementTypes: [],
swizzles: [],
};
-
+
return graph;
}
export function getOrCreateNode(graph, node) {
// const key = getNodeKey(node);
// const existing = graph.cache.get(key);
-
+
// if (existing !== undefined) {
- // return existing;
+ // return existing;
// } else {
const id = createNode(graph, node);
// graph.cache.set(key, id);
@@ -66,7 +66,7 @@ export function getNodeDataFromID(graph, id) {
dependsOn: graph.dependsOn[id],
usedBy: graph.usedBy[id],
phiBlocks: graph.phiBlocks[id],
- dimension: graph.dimensions[id],
+ dimension: graph.dimensions[id],
baseType: graph.baseTypes[id],
statementType: graph.statementTypes[id],
swizzle: graph.swizzles[id],
@@ -115,7 +115,7 @@ function getNodeKey(node) {
function validateNode(node){
const nodeType = node.nodeType;
const requiredFields = NodeTypeRequiredFields[nodeType];
- if (requiredFields.length === 2) {
+ if (requiredFields.length === 2) {
FES.internalError(`Required fields for node type '${NodeTypeToName[nodeType]}' not defined. Please add them to the utils.js file in p5.strands!`)
}
const missingFields = [];
@@ -124,7 +124,10 @@ function validateNode(node){
missingFields.push(field);
}
}
+ if (node.dependsOn?.some(v => v === undefined)) {
+ throw new Error('Undefined dependency!');
+ }
if (missingFields.length > 0) {
FES.internalError(`Missing fields ${missingFields.join(', ')} for a node type '${NodeTypeToName[nodeType]}'.`);
}
-}
\ No newline at end of file
+}
diff --git a/src/strands/ir_types.js b/src/strands/ir_types.js
index 67829d6b03..966e9d2ec2 100644
--- a/src/strands/ir_types.js
+++ b/src/strands/ir_types.js
@@ -9,12 +9,11 @@ export const NodeType = {
STRUCT: 'struct',
PHI: 'phi',
STATEMENT: 'statement',
+ ASSIGNMENT: 'assignment',
};
-
export const NodeTypeToName = Object.fromEntries(
Object.entries(NodeType).map(([key, val]) => [val, key])
);
-
export const NodeTypeRequiredFields = {
[NodeType.OPERATION]: ["opCode", "dependsOn", "dimension", "baseType"],
[NodeType.LITERAL]: ["value", "dimension", "baseType"],
@@ -22,13 +21,15 @@ export const NodeTypeRequiredFields = {
[NodeType.CONSTANT]: ["value", "dimension", "baseType"],
[NodeType.STRUCT]: [""],
[NodeType.PHI]: ["dependsOn", "phiBlocks", "dimension", "baseType"],
- [NodeType.STATEMENT]: ["opCode"]
+ [NodeType.STATEMENT]: ["statementType"],
+ [NodeType.ASSIGNMENT]: ["dependsOn"]
};
-
export const StatementType = {
DISCARD: 'discard',
+ BREAK: 'break',
+ EXPRESSION: 'expression', // Used when we want to output a single expression as a statement, e.g. a for loop condition
+ EMPTY: 'empty', // Used for empty statements like ; in for loops
};
-
export const BaseType = {
FLOAT: "float",
INT: "int",
@@ -37,7 +38,6 @@ export const BaseType = {
DEFER: "defer",
SAMPLER2D: "sampler2D",
};
-
export const BasePriority = {
[BaseType.FLOAT]: 3,
[BaseType.INT]: 2,
@@ -46,7 +46,6 @@ export const BasePriority = {
[BaseType.DEFER]: -1,
[BaseType.SAMPLER2D]: -10,
};
-
export const DataType = {
float1: { fnName: "float", baseType: BaseType.FLOAT, dimension:1, priority: 3, },
float2: { fnName: "vec2", baseType: BaseType.FLOAT, dimension:2, priority: 3, },
@@ -64,7 +63,7 @@ export const DataType = {
mat3: { fnName: "mat3x3", baseType: BaseType.MAT, dimension:3, priority: 0, },
mat4: { fnName: "mat4x4", baseType: BaseType.MAT, dimension:4, priority: 0, },
defer: { fnName: null, baseType: BaseType.DEFER, dimension: null, priority: -1 },
- sampler2D: { fnName: "texture", baseType: BaseType.SAMPLER2D, dimension: 1, priority: -10 },
+ sampler2D: { fnName: "sampler2D", baseType: BaseType.SAMPLER2D, dimension: 1, priority: -10 },
}
export const structType = function (hookType) {
let T = hookType.type === undefined ? hookType : hookType.type;
@@ -82,31 +81,50 @@ export const structType = function (hookType) {
}
return structType;
};
-
export function isStructType(typeName) {
return !isNativeType(typeName);
}
-
export function isNativeType(typeName) {
- return Object.keys(DataType).includes(typeName);
-}
+ // Check if it's in DataType keys (internal names like 'float4')
+ if (Object.keys(DataType).includes(typeName)) {
+ return true;
+ }
+
+ // Check if it's a GLSL type name (like 'vec4', 'float', etc.)
+ const glslNativeTypes = {
+ 'float': true,
+ 'vec2': true,
+ 'vec3': true,
+ 'vec4': true,
+ 'int': true,
+ 'ivec2': true,
+ 'ivec3': true,
+ 'ivec4': true,
+ 'bool': true,
+ 'bvec2': true,
+ 'bvec3': true,
+ 'bvec4': true,
+ 'mat2': true,
+ 'mat3': true,
+ 'mat4': true,
+ 'sampler2D': true
+ };
+ return !!glslNativeTypes[typeName];
+}
export const GenType = {
FLOAT: { baseType: BaseType.FLOAT, dimension: null, priority: 3 },
INT: { baseType: BaseType.INT, dimension: null, priority: 2 },
BOOL: { baseType: BaseType.BOOL, dimension: null, priority: 1 },
}
-
export function typeEquals(nodeA, nodeB) {
return (nodeA.dimension === nodeB.dimension) && (nodeA.baseType === nodeB.baseType);
}
-
export const TypeInfoFromGLSLName = Object.fromEntries(
Object.values(DataType)
.filter(info => info.fnName !== null)
- .map(info => [info.fnName === 'texture' ? 'sampler2D' : info.fnName, info])
+ .map(info => [info.fnName, info])
);
-
export const OpCode = {
Binary: {
ADD: 0,
@@ -139,9 +157,9 @@ export const OpCode = {
JUMP: 301,
BRANCH_IF_FALSE: 302,
DISCARD: 303,
+ BREAK: 304,
}
};
-
export const OperatorTable = [
{ arity: "unary", name: "not", symbol: "!", opCode: OpCode.Unary.LOGICAL_NOT },
{ arity: "unary", name: "neg", symbol: "-", opCode: OpCode.Unary.NEGATE },
@@ -160,7 +178,6 @@ export const OperatorTable = [
{ arity: "binary", name: "and", symbol: "&&", opCode: OpCode.Binary.LOGICAL_AND },
{ arity: "binary", name: "or", symbol: "||", opCode: OpCode.Binary.LOGICAL_OR },
];
-
export const ConstantFolding = {
[OpCode.Binary.ADD]: (a, b) => a + b,
[OpCode.Binary.SUBTRACT]: (a, b) => a - b,
@@ -176,12 +193,10 @@ export const ConstantFolding = {
[OpCode.Binary.LOGICAL_AND]: (a, b) => a && b,
[OpCode.Binary.LOGICAL_OR]: (a, b) => a || b,
};
-
// export const SymbolToOpCode = {};
export const OpCodeToSymbol = {};
export const UnarySymbolToName = {};
export const BinarySymbolToName = {};
-
for (const { symbol, opCode, name, arity } of OperatorTable) {
// SymbolToOpCode[symbol] = opCode;
OpCodeToSymbol[opCode] = symbol;
@@ -192,20 +207,19 @@ for (const { symbol, opCode, name, arity } of OperatorTable) {
BinarySymbolToName[symbol] = name;
}
}
-
export const BlockType = {
GLOBAL: 'global',
FUNCTION: 'function',
+ BRANCH: 'branch',
IF_COND: 'if_cond',
IF_BODY: 'if_body',
- ELIF_BODY: 'elif_body',
- ELIF_COND: 'elif_cond',
- ELSE_BODY: 'else_body',
+ ELSE_COND: 'else_cond',
+ SCOPE_START: 'scope_start',
+ SCOPE_END: 'scope_end',
FOR: 'for',
MERGE: 'merge',
DEFAULT: 'default',
}
-
export const BlockTypeToName = Object.fromEntries(
Object.entries(BlockType).map(([key, val]) => [val, key])
);
diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js
index 0bb0ce9866..47c29fa226 100644
--- a/src/strands/strands_api.js
+++ b/src/strands/strands_api.js
@@ -8,33 +8,20 @@ import {
TypeInfoFromGLSLName,
isStructType,
OpCode,
+ StatementType,
// isNativeType
} from './ir_types'
import { strandsBuiltinFunctions } from './strands_builtins'
import { StrandsConditional } from './strands_conditionals'
+import { StrandsFor } from './strands_for'
import * as CFG from './ir_cfg'
import * as FES from './strands_FES'
import { getNodeDataFromID } from './ir_dag'
+import { StrandsNode, createStrandsNode } from './strands_node'
import noiseGLSL from '../webgl/shaders/functions/noiseGLSL.glsl';
-
//////////////////////////////////////////////
// User nodes
//////////////////////////////////////////////
-export class StrandsNode {
- constructor(id, dimension, strandsContext) {
- this.id = id;
- this.strandsContext = strandsContext;
- this.dimension = dimension;
- }
-}
-
-export function createStrandsNode(id, dimension, strandsContext, onRebind) {
- return new Proxy(
- new StrandsNode(id, dimension, strandsContext),
- build.swizzleTrap(id, dimension, strandsContext, onRebind)
- );
-}
-
export function initGlobalStrandsAPI(p5, fn, strandsContext) {
// We augment the strands node with operations programatically
// this means methods like .add, .sub, etc can be chained
@@ -52,27 +39,30 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) {
}
}
}
-
//////////////////////////////////////////////
// Unique Functions
//////////////////////////////////////////////
fn.discard = function() {
- build.statementNode(strandsContext, OpCode.ControlFlow.DISCARD);
+ build.statementNode(strandsContext, StatementType.DISCARD);
}
-
+ fn.break = function() {
+ build.statementNode(strandsContext, StatementType.BREAK);
+ };
+ p5.break = fn.break;
fn.instanceID = function() {
const node = build.variableNode(strandsContext, { baseType: BaseType.INT, dimension: 1 }, 'gl_InstanceID');
return createStrandsNode(node.id, node.dimension, strandsContext);
}
-
- fn.strandsIf = function(conditionNode, ifBody) {
+ // Internal methods use p5 static methods; user-facing methods use fn.
+ // Some methods need to be used by both.
+ p5.strandsIf = function(conditionNode, ifBody) {
return new StrandsConditional(strandsContext, conditionNode, ifBody);
}
-
- fn.strandsLoop = function(a, b, loopBody) {
- return null;
- }
-
+ fn.strandsIf = p5.strandsIf;
+ p5.strandsFor = function(initialCb, conditionCb, updateCb, bodyCb, initialVars) {
+ return new StrandsFor(strandsContext, initialCb, conditionCb, updateCb, bodyCb, initialVars).build();
+ };
+ fn.strandsFor = p5.strandsFor;
p5.strandsNode = function(...args) {
if (args.length === 1 && args[0] instanceof StrandsNode) {
return args[0];
@@ -83,13 +73,11 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) {
const { id, dimension } = build.primitiveConstructorNode(strandsContext, { baseType: BaseType.FLOAT, dimension: null }, args.flat());
return createStrandsNode(id, dimension, strandsContext);//new StrandsNode(id, dimension, strandsContext);
}
-
//////////////////////////////////////////////
// Builtins, uniforms, variable constructors
//////////////////////////////////////////////
for (const [functionName, overrides] of Object.entries(strandsBuiltinFunctions)) {
const isp5Function = overrides[0].isp5Function;
-
if (isp5Function) {
const originalFn = fn[functionName];
fn[functionName] = function(...args) {
@@ -113,14 +101,12 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) {
}
}
}
-
// Add GLSL noise. TODO: Replace this with a backend-agnostic implementation
const originalNoise = fn.noise;
fn.noise = function (...args) {
if (!strandsContext.active) {
return originalNoise.apply(this, args); // fallback to regular p5.js noise
}
-
strandsContext.vertexDeclarations.add(noiseGLSL);
strandsContext.fragmentDeclarations.add(noiseGLSL);
// Handle noise(x, y) as noise(vec2)
@@ -130,7 +116,6 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) {
} else {
nodeArgs = args;
}
-
const { id, dimension } = build.functionCallNode(strandsContext, 'noise', nodeArgs, {
overloads: [{
params: [DataType.float2],
@@ -139,14 +124,12 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) {
});
return createStrandsNode(id, dimension, strandsContext);
};
-
// Next is type constructors and uniform functions
for (const type in DataType) {
if (type === BaseType.DEFER) {
continue;
}
const typeInfo = DataType[type];
-
let pascalTypeName;
if (/^[ib]vec/.test(typeInfo.fnName)) {
pascalTypeName = typeInfo.fnName
@@ -168,7 +151,6 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) {
// documented these as
fn[`uniform${pascalTypeName.replace('Vec', 'Vector')}`] = fn[`uniform${pascalTypeName}`];
}
-
const originalp5Fn = fn[typeInfo.fnName];
fn[typeInfo.fnName] = function(...args) {
if (strandsContext.active) {
@@ -184,14 +166,12 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) {
}
}
}
-
//////////////////////////////////////////////
// Per-Hook functions
//////////////////////////////////////////////
function createHookArguments(strandsContext, parameters){
const args = [];
const dag = strandsContext.dag;
-
for (const param of parameters) {
if(isStructType(param.type.typeName)) {
const structTypeInfo = structType(param);
@@ -218,7 +198,6 @@ function createHookArguments(strandsContext, parameters){
set(val) {
const oldDependsOn = dag.dependsOn[structNode.id];
const newDependsOn = [...oldDependsOn];
-
let newValueID;
if (val instanceof StrandsNode) {
newValueID = val.id;
@@ -227,14 +206,12 @@ function createHookArguments(strandsContext, parameters){
let newVal = build.primitiveConstructorNode(strandsContext, propertyType.dataType, val);
newValueID = newVal.id;
}
-
newDependsOn[i] = newValueID;
const newStructInfo = build.structInstanceNode(strandsContext, structTypeInfo, param.name, newDependsOn);
structNode.id = newStructInfo.id;
}
})
}
-
args.push(structNode);
}
else /*if(isNativeType(paramType.typeName))*/ {
@@ -246,7 +223,6 @@ function createHookArguments(strandsContext, parameters){
}
return args;
}
-
function enforceReturnTypeMatch(strandsContext, expectedType, returned, hookName) {
if (!(returned instanceof StrandsNode)) {
// try {
@@ -262,7 +238,6 @@ function enforceReturnTypeMatch(strandsContext, expectedType, returned, hookName
// );
// }
}
-
const dag = strandsContext.dag;
let returnedNodeID = returned.id;
const receivedType = {
@@ -282,10 +257,8 @@ function enforceReturnTypeMatch(strandsContext, expectedType, returned, hookName
const result = build.primitiveConstructorNode(strandsContext, expectedType, returned);
returnedNodeID = result.id;
}
-
return returnedNodeID;
}
-
export function createShaderHooksFunctions(strandsContext, fn, shader) {
const availableHooks = {
...shader.hooks.vertex,
@@ -293,36 +266,28 @@ export function createShaderHooksFunctions(strandsContext, fn, shader) {
}
const hookTypes = Object.keys(availableHooks).map(name => shader.hookTypes(name));
const { cfg, dag } = strandsContext;
-
for (const hookType of hookTypes) {
const hookImplementation = function(hookUserCallback) {
const entryBlockID = CFG.createBasicBlock(cfg, BlockType.FUNCTION);
CFG.addEdge(cfg, cfg.currentBlock, entryBlockID);
CFG.pushBlock(cfg, entryBlockID);
-
const args = createHookArguments(strandsContext, hookType.parameters);
const userReturned = hookUserCallback(...args);
const expectedReturnType = hookType.returnType;
-
let rootNodeID = null;
-
if(isStructType(expectedReturnType.typeName)) {
const expectedStructType = structType(expectedReturnType);
if (userReturned instanceof StrandsNode) {
const returnedNode = getNodeDataFromID(strandsContext.dag, userReturned.id);
-
if (returnedNode.baseType !== expectedStructType.typeName) {
FES.userError("type error", `You have returned a ${userReturned.baseType} from ${hookType.name} when a ${expectedStructType.typeName} was expected.`);
}
-
const newDeps = returnedNode.dependsOn.slice();
-
for (let i = 0; i < expectedStructType.properties.length; i++) {
const expectedType = expectedStructType.properties[i].dataType;
const receivedNode = createStrandsNode(returnedNode.dependsOn[i], dag.dependsOn[userReturned.id], strandsContext);
newDeps[i] = enforceReturnTypeMatch(strandsContext, expectedType, receivedNode, hookType.name);
}
-
dag.dependsOn[userReturned.id] = newDeps;
rootNodeID = userReturned.id;
}
@@ -346,13 +311,11 @@ export function createShaderHooksFunctions(strandsContext, fn, shader) {
const newStruct = build.structConstructorNode(strandsContext, expectedStructType, newStructDependencies);
rootNodeID = newStruct.id;
}
-
}
else /*if(isNativeType(expectedReturnType.typeName))*/ {
const expectedTypeInfo = TypeInfoFromGLSLName[expectedReturnType.typeName];
rootNodeID = enforceReturnTypeMatch(strandsContext, expectedTypeInfo, userReturned, hookType.name);
}
-
strandsContext.hooks.push({
hookType,
entryBlockID,
@@ -362,7 +325,6 @@ export function createShaderHooksFunctions(strandsContext, fn, shader) {
}
strandsContext.windowOverrides[hookType.name] = window[hookType.name];
strandsContext.fnOverrides[hookType.name] = fn[hookType.name];
-
window[hookType.name] = hookImplementation;
fn[hookType.name] = hookImplementation;
}
diff --git a/src/strands/strands_codegen.js b/src/strands/strands_codegen.js
index 911ab8376b..84c83a77da 100644
--- a/src/strands/strands_codegen.js
+++ b/src/strands/strands_codegen.js
@@ -41,7 +41,6 @@ export function generateShaderCode(strandsContext) {
: TypeInfoFromGLSLName[hookType.returnType.typeName];
backend.generateReturnStatement(strandsContext, generationContext, rootNodeID, returnType);
hooksObj[`${hookType.returnType.typeName} ${hookType.name}`] = [firstLine, ...generationContext.codeLines, '}'].join('\n');
- console.log(hooksObj[`${hookType.returnType.typeName} ${hookType.name}`]);
}
hooksObj.vertexDeclarations = [...vertexDeclarations].join('\n');
diff --git a/src/strands/strands_conditionals.js b/src/strands/strands_conditionals.js
index 1ce888cc91..cd40e8cd91 100644
--- a/src/strands/strands_conditionals.js
+++ b/src/strands/strands_conditionals.js
@@ -1,6 +1,8 @@
-import * as CFG from './ir_cfg'
-import { BlockType } from './ir_types';
-
+import * as CFG from './ir_cfg';
+import * as DAG from './ir_dag';
+import { BlockType, NodeType } from './ir_types';
+import { StrandsNode, createStrandsNode } from './strands_node';
+import { createPhiNode } from './strands_phi_utils';
export class StrandsConditional {
constructor(strandsContext, condition, branchCallback) {
// Condition must be a node...
@@ -11,38 +13,44 @@ export class StrandsConditional {
}];
this.ctx = strandsContext;
}
-
ElseIf(condition, branchCallback) {
- this.branches.push({
+ this.branches.push({
condition,
branchCallback,
- blockType: BlockType.ELIF_BODY
+ blockType: BlockType.IF_BODY
});
return this;
}
-
Else(branchCallback = () => ({})) {
- this.branches.push({
- condition: null,
- branchCallback,
- blockType: BlockType.ELSE_BODY
+ this.branches.push({
+ condition: null,
+ branchCallback,
+ blockType: BlockType.IF_BODY
});
- return buildConditional(this.ctx, this);
+ const phiNodes = buildConditional(this.ctx, this);
+ const assignments = {};
+ for (const [varName, phiNode] of Object.entries(phiNodes)) {
+ assignments[varName] = createStrandsNode(phiNode.id, phiNode.dimension, this.ctx);
+ }
+ return assignments;
}
}
-
function buildConditional(strandsContext, conditional) {
const cfg = strandsContext.cfg;
- const branches = conditional.branches;
-
+ const branches = conditional.branches;
const mergeBlock = CFG.createBasicBlock(cfg, BlockType.MERGE);
const results = [];
-
- let previousBlock = cfg.currentBlock;
-
+ const branchBlocks = [];
+ const branchEndBlocks = [];
+ const mergedAssignments = {};
+ const phiBlockDependencies = {};
+ // Create a BRANCH block to handle phi node declarations
+ const branchBlock = CFG.createBasicBlock(cfg, BlockType.BRANCH);
+ CFG.addEdge(cfg, cfg.currentBlock, branchBlock);
+ CFG.addEdge(cfg, branchBlock, mergeBlock);
+ let previousBlock = branchBlock;
for (let i = 0; i < branches.length; i++) {
const { condition, branchCallback, blockType } = branches[i];
-
if (condition !== null) {
const conditionBlock = CFG.createBasicBlock(cfg, BlockType.IF_COND);
CFG.addEdge(cfg, previousBlock, conditionBlock);
@@ -50,22 +58,67 @@ function buildConditional(strandsContext, conditional) {
cfg.blockConditions[conditionBlock] = condition.id;
previousBlock = conditionBlock;
CFG.popBlock(cfg);
+ } else {
+ const elseCondBlock = CFG.createBasicBlock(cfg, BlockType.ELSE_COND);
+ CFG.addEdge(cfg, previousBlock, elseCondBlock);
+ previousBlock = elseCondBlock;
}
-
- const branchBlock = CFG.createBasicBlock(cfg, blockType);
- CFG.addEdge(cfg, previousBlock, branchBlock);
-
- CFG.pushBlock(cfg, branchBlock);
+ const scopeStartBlock = CFG.createBasicBlock(cfg, BlockType.SCOPE_START);
+ CFG.addEdge(cfg, previousBlock, scopeStartBlock);
+ const branchContentBlock = CFG.createBasicBlock(cfg, blockType);
+ CFG.addEdge(cfg, scopeStartBlock, branchContentBlock);
+ branchBlocks.push(branchContentBlock);
+ CFG.pushBlock(cfg, branchContentBlock);
const branchResults = branchCallback();
+ for (const key in branchResults) {
+ if (!phiBlockDependencies[key]) {
+ phiBlockDependencies[key] = [{ value: branchResults[key], blockId: branchContentBlock }];
+ } else {
+ phiBlockDependencies[key].push({ value: branchResults[key], blockId: branchContentBlock });
+ }
+ }
results.push(branchResults);
- if (cfg.currentBlock !== branchBlock) {
- CFG.addEdge(cfg, cfg.currentBlock, mergeBlock);
- CFG.popBlock();
+
+ // Create BRANCH_END block for phi assignments
+ const branchEndBlock = CFG.createBasicBlock(cfg, BlockType.DEFAULT);
+ CFG.addEdge(cfg, cfg.currentBlock, branchEndBlock);
+ branchEndBlocks.push(branchEndBlock);
+ CFG.popBlock(cfg);
+
+ const scopeEndBlock = CFG.createBasicBlock(cfg, BlockType.SCOPE_END);
+ CFG.addEdge(cfg, branchEndBlock, scopeEndBlock);
+ CFG.addEdge(cfg, scopeEndBlock, mergeBlock);
+ previousBlock = scopeStartBlock;
+ }
+ // Push the branch block for modification to avoid changing the ordering
+ CFG.pushBlockForModification(cfg, branchBlock);
+ for (const key in phiBlockDependencies) {
+ mergedAssignments[key] = createPhiNode(strandsContext, phiBlockDependencies[key], key);
+ }
+ CFG.popBlock(cfg);
+ for (let i = 0; i < results.length; i++) {
+ const branchResult = results[i];
+ const branchEndBlockID = branchEndBlocks[i];
+ CFG.pushBlockForModification(cfg, branchEndBlockID);
+ for (const key in branchResult) {
+ if (mergedAssignments[key]) {
+ // Create an assignment statement: phiNode = branchResult[key]
+ const phiNodeID = mergedAssignments[key].id;
+ const sourceNodeID = branchResult[key].id;
+ // Create an assignment operation node
+ // Use dependsOn[0] for phiNodeID and dependsOn[1] for sourceNodeID
+ // This represents: dependsOn[0] = dependsOn[1] (phiNode = sourceNode)
+ const assignmentNode = {
+ nodeType: NodeType.ASSIGNMENT,
+ dependsOn: [phiNodeID, sourceNodeID],
+ phiBlocks: []
+ };
+ const assignmentID = DAG.getOrCreateNode(strandsContext.dag, assignmentNode);
+ CFG.recordInBasicBlock(cfg, branchEndBlockID, assignmentID);
+ }
}
- CFG.addEdge(cfg, cfg.currentBlock, mergeBlock);
CFG.popBlock(cfg);
}
CFG.pushBlock(cfg, mergeBlock);
-
- return results;
-}
\ No newline at end of file
+ return mergedAssignments;
+}
diff --git a/src/strands/strands_for.js b/src/strands/strands_for.js
new file mode 100644
index 0000000000..a9f0749c44
--- /dev/null
+++ b/src/strands/strands_for.js
@@ -0,0 +1,426 @@
+import * as CFG from './ir_cfg';
+import * as DAG from './ir_dag';
+import { BlockType, NodeType, BaseType, StatementType, OpCode } from './ir_types';
+import { StrandsNode, createStrandsNode } from './strands_node';
+import { primitiveConstructorNode } from './ir_builders';
+import { createPhiNode } from './strands_phi_utils';
+
+export class StrandsFor {
+ constructor(strandsContext, initialCb, conditionCb, updateCb, bodyCb, initialVars) {
+ this.strandsContext = strandsContext;
+ this.initialCb = initialCb;
+ this.conditionCb = conditionCb;
+ this.updateCb = updateCb;
+ this.bodyCb = bodyCb;
+ this.initialVars = initialVars;
+ }
+
+ build() {
+ const cfg = this.strandsContext.cfg;
+ const mergeBlock = CFG.createBasicBlock(cfg, BlockType.MERGE);
+
+ // Create a BRANCH block to handle phi node declarations
+ const branchBlock = CFG.createBasicBlock(cfg, BlockType.BRANCH);
+ CFG.addEdge(cfg, cfg.currentBlock, branchBlock);
+ CFG.addEdge(cfg, branchBlock, mergeBlock);
+
+ // Initialize loop variable phi node
+ const { initialVar, phiNode } = this.initializeLoopVariable(cfg, branchBlock);
+
+ // Execute condition and update callbacks to get nodes for analysis
+ CFG.pushBlock(cfg, cfg.currentBlock);
+ const loopVarNode = createStrandsNode(phiNode.id, phiNode.dimension, this.strandsContext);
+ const conditionNode = this.conditionCb(loopVarNode);
+ const updateResult = this.updateCb(loopVarNode);
+ CFG.popBlock(cfg);
+
+ // Check if loop has bounded iteration count
+ const isBounded = this.loopIsBounded(initialVar, conditionNode, updateResult);
+
+ if (isBounded) {
+ this.buildBoundedLoop(cfg, branchBlock, mergeBlock, initialVar, phiNode, conditionNode, updateResult);
+ } else {
+ this.buildUnboundedLoop(cfg, branchBlock, mergeBlock, initialVar, phiNode, conditionNode, updateResult);
+ }
+
+ // Update the phi nodes created in buildBoundedLoop with actual body results
+ const finalPhiNodes = this.phiNodesForBody;
+ CFG.pushBlockForModification(cfg, branchBlock);
+ for (const [varName, resultNode] of Object.entries(this.bodyResults)) {
+ if (varName !== 'loopVar' && finalPhiNodes[varName]) {
+ // Update the phi node's second input to use the actual body result
+ const phiNodeID = finalPhiNodes[varName].id;
+ const phiNodeData = DAG.getNodeDataFromID(this.strandsContext.dag, phiNodeID);
+ // Update the dependsOn array to include the actual body result
+ if (phiNodeData.dependsOn.length > 1) {
+ phiNodeData.dependsOn[1] = resultNode.id;
+ }
+ if (phiNodeData.phiInputs && phiNodeData.phiInputs.length > 1) {
+ phiNodeData.phiInputs[1].value = resultNode;
+ }
+ }
+ }
+ CFG.popBlock(cfg);
+
+ // Create assignment nodes in the branch block for initial values
+ CFG.pushBlockForModification(cfg, branchBlock);
+ for (const [varName, initialValueNode] of Object.entries(this.initialVars)) {
+ if (varName !== 'loopVar' && finalPhiNodes[varName]) {
+ // Create an assignment statement: phiNode = initialValue
+ const phiNodeID = finalPhiNodes[varName].id;
+ const sourceNodeID = initialValueNode.id;
+ // Create an assignment operation node for the initial value
+ const assignmentNode = DAG.createNodeData({
+ nodeType: NodeType.ASSIGNMENT,
+ dependsOn: [phiNodeID, sourceNodeID],
+ phiBlocks: []
+ });
+ const assignmentID = DAG.getOrCreateNode(this.strandsContext.dag, assignmentNode);
+ CFG.recordInBasicBlock(cfg, branchBlock, assignmentID);
+ }
+ }
+ CFG.popBlock(cfg);
+
+ // Create assignment nodes in the final block after body execution (following conditionals pattern)
+ // After executing the body callback, cfg.currentBlock should be the final block in the control flow
+ CFG.pushBlockForModification(cfg, this.finalBodyBlock);
+ for (const [varName, resultNode] of Object.entries(this.bodyResults)) {
+ if (varName !== 'loopVar' && finalPhiNodes[varName]) {
+ // Create an assignment statement: phiNode = bodyResult[varName]
+ const phiNodeID = finalPhiNodes[varName].id;
+ const sourceNodeID = resultNode.id;
+ // Create an assignment operation node
+ // Use dependsOn[0] for phiNodeID and dependsOn[1] for sourceNodeID
+ // This represents: dependsOn[0] = dependsOn[1] (phiNode = sourceNode)
+ const assignmentNode = DAG.createNodeData({
+ nodeType: NodeType.ASSIGNMENT,
+ dependsOn: [phiNodeID, sourceNodeID],
+ phiBlocks: []
+ });
+ const assignmentID = DAG.getOrCreateNode(this.strandsContext.dag, assignmentNode);
+ CFG.recordInBasicBlock(cfg, this.finalBodyBlock, assignmentID);
+ }
+ }
+ CFG.popBlock(cfg);
+
+ // Convert phi nodes to StrandsNodes for the final result
+ const finalBodyResults = {};
+ for (const [varName, phiNode] of Object.entries(finalPhiNodes)) {
+ finalBodyResults[varName] = createStrandsNode(phiNode.id, phiNode.dimension, this.strandsContext);
+ }
+
+ CFG.pushBlock(cfg, mergeBlock);
+
+ return finalBodyResults;
+ }
+
+ buildBoundedLoop(cfg, branchBlock, mergeBlock, initialVar, phiNode, conditionNode, updateResult) {
+ // For bounded loops, create FOR block with three statements: init, condition, update
+ const forBlock = CFG.createBasicBlock(cfg, BlockType.FOR);
+ CFG.addEdge(cfg, branchBlock, forBlock);
+
+ // Now add only the specific nodes we need to the FOR block
+ CFG.pushBlock(cfg, forBlock);
+
+ // 1. Init statement - assign initial value to phi node (or empty if no initializer)
+ if (initialVar) {
+ const initAssignmentNode = DAG.createNodeData({
+ nodeType: NodeType.ASSIGNMENT,
+ dependsOn: [phiNode.id, initialVar.id],
+ phiBlocks: []
+ });
+ const initAssignmentID = DAG.getOrCreateNode(this.strandsContext.dag, initAssignmentNode);
+ CFG.recordInBasicBlock(cfg, forBlock, initAssignmentID);
+ }
+
+ // 2. Condition statement - wrap in ExpressionStatement to force generation
+ const conditionStatementNode = DAG.createNodeData({
+ nodeType: NodeType.STATEMENT,
+ statementType: StatementType.EXPRESSION,
+ dependsOn: [conditionNode.id],
+ phiBlocks: []
+ });
+ const conditionStatementID = DAG.getOrCreateNode(this.strandsContext.dag, conditionStatementNode);
+ CFG.recordInBasicBlock(cfg, forBlock, conditionStatementID);
+
+ // 3. Update statement - create assignment of update result to phi node
+ const updateAssignmentNode = DAG.createNodeData({
+ nodeType: NodeType.ASSIGNMENT,
+ dependsOn: [phiNode.id, updateResult.id],
+ phiBlocks: []
+ });
+ const updateAssignmentID = DAG.getOrCreateNode(this.strandsContext.dag, updateAssignmentNode);
+ CFG.recordInBasicBlock(cfg, forBlock, updateAssignmentID);
+
+ CFG.popBlock(cfg);
+
+ // Verify we have the right number of statements (2 or 3 depending on initializer)
+ const instructions = cfg.blockInstructions[forBlock] || [];
+ const expectedLength = initialVar ? 3 : 2;
+ if (instructions.length !== expectedLength) {
+ throw new Error(`FOR block must have exactly ${expectedLength} statements, got ${instructions.length}`);
+ }
+
+ const scopeStartBlock = CFG.createBasicBlock(cfg, BlockType.SCOPE_START);
+ CFG.addEdge(cfg, forBlock, scopeStartBlock);
+
+ const bodyBlock = CFG.createBasicBlock(cfg, BlockType.DEFAULT);
+ this.bodyBlock = bodyBlock;
+ CFG.addEdge(cfg, scopeStartBlock, bodyBlock);
+
+ this.executeBodyCallback(cfg, branchBlock, bodyBlock, phiNode);
+
+ const scopeEndBlock = CFG.createBasicBlock(cfg, BlockType.SCOPE_END);
+ CFG.addEdge(cfg, bodyBlock, scopeEndBlock);
+ CFG.addEdge(cfg, scopeEndBlock, mergeBlock);
+ }
+
+ buildUnboundedLoop(cfg, branchBlock, mergeBlock, initialVar, phiNode, conditionNode, updateResult) {
+ // For unbounded loops, create FOR block with infinite loop and break condition
+ const forBlock = CFG.createBasicBlock(cfg, BlockType.FOR);
+ CFG.addEdge(cfg, branchBlock, forBlock);
+
+ // Create FOR block with three empty statements for for(;;) syntax
+ CFG.pushBlock(cfg, forBlock);
+
+ // 1. Init statement - initialize loop variable or empty
+ if (initialVar) {
+ const initAssignmentNode = DAG.createNodeData({
+ nodeType: NodeType.ASSIGNMENT,
+ dependsOn: [phiNode.id, initialVar.id],
+ phiBlocks: []
+ });
+ const initAssignmentID = DAG.getOrCreateNode(this.strandsContext.dag, initAssignmentNode);
+ CFG.recordInBasicBlock(cfg, forBlock, initAssignmentID);
+ } else {
+ // Create empty statement for init
+ const emptyInitNode = DAG.createNodeData({
+ nodeType: NodeType.STATEMENT,
+ statementType: StatementType.EMPTY,
+ dependsOn: [],
+ phiBlocks: []
+ });
+ const emptyInitID = DAG.getOrCreateNode(this.strandsContext.dag, emptyInitNode);
+ CFG.recordInBasicBlock(cfg, forBlock, emptyInitID);
+ }
+
+ // 2. Condition statement - empty for infinite loop
+ const emptyConditionNode = DAG.createNodeData({
+ nodeType: NodeType.STATEMENT,
+ statementType: StatementType.EMPTY,
+ dependsOn: [],
+ phiBlocks: []
+ });
+ const emptyConditionID = DAG.getOrCreateNode(this.strandsContext.dag, emptyConditionNode);
+ CFG.recordInBasicBlock(cfg, forBlock, emptyConditionID);
+
+ // 3. Update statement - empty for infinite loop
+ const emptyUpdateNode = DAG.createNodeData({
+ nodeType: NodeType.STATEMENT,
+ statementType: StatementType.EMPTY,
+ dependsOn: [],
+ phiBlocks: []
+ });
+ const emptyUpdateID = DAG.getOrCreateNode(this.strandsContext.dag, emptyUpdateNode);
+ CFG.recordInBasicBlock(cfg, forBlock, emptyUpdateID);
+
+ CFG.popBlock(cfg);
+
+ const scopeStartBlock = CFG.createBasicBlock(cfg, BlockType.SCOPE_START);
+ CFG.addEdge(cfg, forBlock, scopeStartBlock);
+
+ // Add break condition check right after scope start
+ const breakCheckBlock = CFG.createBasicBlock(cfg, BlockType.DEFAULT);
+ CFG.addEdge(cfg, scopeStartBlock, breakCheckBlock);
+
+ CFG.pushBlock(cfg, breakCheckBlock);
+
+ // Generate break statement: if (!condition) break;
+ // First, create the logical NOT of the condition: !condition
+ const condition = conditionNode;
+ const negatedCondition = this.createLogicalNotNode(condition);
+
+ // Create a conditional break using the existing conditional structure
+ // We'll create an IF_COND block that leads to a break statement
+ const breakConditionBlock = CFG.createBasicBlock(cfg, BlockType.IF_COND);
+ CFG.addEdge(cfg, breakCheckBlock, breakConditionBlock);
+ cfg.blockConditions[breakConditionBlock] = negatedCondition.id;
+
+ const breakStatementBlock = CFG.createBasicBlock(cfg, BlockType.DEFAULT);
+ CFG.addEdge(cfg, breakConditionBlock, breakStatementBlock);
+
+ // Create the break statement in the break statement block
+ CFG.pushBlock(cfg, breakStatementBlock);
+ const breakStatementNode = DAG.createNodeData({
+ nodeType: NodeType.STATEMENT,
+ statementType: StatementType.BREAK,
+ dependsOn: [],
+ phiBlocks: []
+ });
+ const breakStatementID = DAG.getOrCreateNode(this.strandsContext.dag, breakStatementNode);
+ CFG.recordInBasicBlock(cfg, breakStatementBlock, breakStatementID);
+ CFG.popBlock(cfg);
+
+ // The break statement block leads to the merge block (exits the loop)
+ CFG.addEdge(cfg, breakStatementBlock, mergeBlock);
+
+ CFG.popBlock(cfg);
+
+ const bodyBlock = CFG.createBasicBlock(cfg, BlockType.DEFAULT);
+ this.bodyBlock = bodyBlock;
+ CFG.addEdge(cfg, breakCheckBlock, bodyBlock);
+
+ this.executeBodyCallback(cfg, branchBlock, bodyBlock, phiNode);
+
+ const updateBlock = CFG.createBasicBlock(cfg, BlockType.DEFAULT);
+ CFG.addEdge(cfg, bodyBlock, updateBlock);
+
+ // Update the loop variable in the update block (like bounded loops)
+ CFG.pushBlock(cfg, updateBlock);
+ const updateAssignmentNode = DAG.createNodeData({
+ nodeType: NodeType.ASSIGNMENT,
+ dependsOn: [phiNode.id, updateResult.id],
+ phiBlocks: []
+ });
+ const updateAssignmentID = DAG.getOrCreateNode(this.strandsContext.dag, updateAssignmentNode);
+ CFG.recordInBasicBlock(cfg, updateBlock, updateAssignmentID);
+ CFG.popBlock(cfg);
+
+ const scopeEndBlock = CFG.createBasicBlock(cfg, BlockType.SCOPE_END);
+ CFG.addEdge(cfg, updateBlock, scopeEndBlock);
+
+ // Loop back to break check
+ CFG.addEdge(cfg, scopeEndBlock, breakCheckBlock);
+
+ // Break condition exits to merge
+ CFG.addEdge(cfg, breakCheckBlock, mergeBlock);
+ }
+
+ initializeLoopVariable(cfg, branchBlock) {
+ CFG.pushBlock(cfg, branchBlock);
+ let initialVar = this.initialCb();
+
+ // Convert to StrandsNode if it's not already one
+ if (!(initialVar instanceof StrandsNode)) {
+ const { id, dimension } = primitiveConstructorNode(this.strandsContext, { baseType: BaseType.FLOAT, dimension: 1 }, initialVar);
+ initialVar = createStrandsNode(id, dimension, this.strandsContext);
+ }
+
+ // Create phi node for the loop variable in the BRANCH block
+ const phiNode = createPhiNode(this.strandsContext, [
+ { value: initialVar, blockId: branchBlock },
+ { value: initialVar, blockId: branchBlock } // Placeholder, will be updated later
+ ], 'loopVar');
+ CFG.popBlock(cfg);
+
+ return { initialVar, phiNode };
+ }
+
+ createLogicalNotNode(conditionNode) {
+ const notOperationNode = DAG.createNodeData({
+ nodeType: NodeType.OPERATION,
+ opCode: OpCode.Unary.LOGICAL_NOT,
+ baseType: BaseType.BOOL,
+ dimension: 1,
+ dependsOn: [conditionNode.id],
+ phiBlocks: [],
+ usedBy: []
+ });
+ const notOperationID = DAG.getOrCreateNode(this.strandsContext.dag, notOperationNode);
+ return createStrandsNode(notOperationID, 1, this.strandsContext);
+ }
+
+ executeBodyCallback(cfg, branchBlock, bodyBlock, phiNode) {
+ CFG.pushBlock(cfg, bodyBlock);
+
+ // Create phi node references to pass to the body callback
+ const phiVars = {};
+ const phiNodesForBody = {};
+ CFG.pushBlockForModification(cfg, branchBlock);
+ for (const [varName, initialValueNode] of Object.entries(this.initialVars)) {
+ if (varName !== 'loopVar') {
+ // Create phi node that will be used for the final result
+ const varPhiNode = createPhiNode(this.strandsContext, [
+ { value: initialValueNode, blockId: branchBlock }, // Initial value
+ { value: initialValueNode, blockId: bodyBlock } // Placeholder - will update after body execution
+ ], varName);
+ phiNodesForBody[varName] = varPhiNode;
+ phiVars[varName] = createStrandsNode(varPhiNode.id, varPhiNode.dimension, this.strandsContext);
+ }
+ }
+ CFG.popBlock(cfg);
+
+ const loopVarNode = createStrandsNode(phiNode.id, phiNode.dimension, this.strandsContext);
+ this.bodyResults = this.bodyCb(loopVarNode, phiVars);
+ this.phiNodesForBody = phiNodesForBody;
+ // Capture the final block after body execution before popping
+ this.finalBodyBlock = cfg.currentBlock;
+ CFG.popBlock(cfg);
+ }
+
+ loopIsBounded(initialVar, conditionNode, updateVar) {
+ // A loop is considered "bounded" if we can determine at compile time that it will
+ // execute a known number of iterations. This happens when:
+ // 1. The condition compares the loop variable against a compile-time constant
+ // 2. At least one side of the comparison uses only literals (no variables/uniforms)
+
+ if (!conditionNode) return false;
+
+ // Analyze the condition node - it should be a comparison operation
+ const conditionData = DAG.getNodeDataFromID(this.strandsContext.dag, conditionNode.id);
+
+ if (conditionData.nodeType !== NodeType.OPERATION) {
+ return false;
+ }
+
+ // For a comparison like "i < bound", we need at least one side to use only literals
+ // The condition should have two dependencies: left and right operands
+ if (!conditionData.dependsOn || conditionData.dependsOn.length !== 2) {
+ return false;
+ }
+
+ // Check if either operand uses only literals
+ const leftOperand = createStrandsNode(conditionData.dependsOn[0], 1, this.strandsContext);
+ const rightOperand = createStrandsNode(conditionData.dependsOn[1], 1, this.strandsContext);
+
+ const leftUsesOnlyLiterals = this.nodeUsesOnlyLiterals(leftOperand);
+ const rightUsesOnlyLiterals = this.nodeUsesOnlyLiterals(rightOperand);
+
+ // At least one side should use only literals for the loop to be bounded
+ return leftUsesOnlyLiterals || rightUsesOnlyLiterals;
+ }
+
+ nodeUsesOnlyLiterals(node) {
+ // Recursively check if a node and all its dependencies use only literals
+ const nodeData = DAG.getNodeDataFromID(this.strandsContext.dag, node.id);
+
+ switch (nodeData.nodeType) {
+ case NodeType.LITERAL:
+ return true;
+
+ case NodeType.VARIABLE:
+ // Variables (like uniforms) make this branch unbounded
+ return false;
+
+ case NodeType.PHI:
+ // Phi nodes (like loop variables) are not literals
+ return false;
+
+ case NodeType.OPERATION:
+ // For operations, all dependencies must use only literals
+ if (nodeData.dependsOn) {
+ for (const depId of nodeData.dependsOn) {
+ const depNode = createStrandsNode(depId, 1, this.strandsContext);
+ if (!this.nodeUsesOnlyLiterals(depNode)) {
+ return false;
+ }
+ }
+ }
+ return true;
+
+ default:
+ // Conservative: if we don't know the node type, assume not literal
+ return false;
+ }
+ }
+}
diff --git a/src/strands/strands_glslBackend.js b/src/strands/strands_glslBackend.js
index 70552bad1b..2ecda9ee7a 100644
--- a/src/strands/strands_glslBackend.js
+++ b/src/strands/strands_glslBackend.js
@@ -1,7 +1,6 @@
-import { NodeType, OpCodeToSymbol, BlockType, OpCode, NodeTypeToName, isStructType, BaseType } from "./ir_types";
+import { NodeType, OpCodeToSymbol, BlockType, OpCode, NodeTypeToName, isStructType, BaseType, StatementType } from "./ir_types";
import { getNodeDataFromID, extractNodeTypeInfo } from "./ir_dag";
import * as FES from './strands_FES'
-
function shouldCreateTemp(dag, nodeID) {
const nodeType = dag.nodeTypes[nodeID];
if (nodeType !== NodeType.OPERATION) return false;
@@ -9,7 +8,6 @@ function shouldCreateTemp(dag, nodeID) {
const uses = dag.usedBy[nodeID] || [];
return uses.length > 1;
}
-
const TypeNames = {
'float1': 'float',
'float2': 'vec2',
@@ -27,11 +25,9 @@ const TypeNames = {
'mat3': 'mat3x3',
'mat4': 'mat4x4',
}
-
const cfgHandlers = {
[BlockType.DEFAULT]: (blockID, strandsContext, generationContext) => {
const { dag, cfg } = strandsContext;
-
const instructions = cfg.blockInstructions[blockID] || [];
for (const nodeID of instructions) {
const nodeType = dag.nodeTypes[nodeID];
@@ -42,43 +38,127 @@ const cfgHandlers = {
if (nodeType === NodeType.STATEMENT) {
glslBackend.generateStatement(generationContext, dag, nodeID);
}
+ if (nodeType === NodeType.ASSIGNMENT) {
+ glslBackend.generateAssignment(generationContext, dag, nodeID);
+ }
}
},
+ [BlockType.BRANCH](blockID, strandsContext, generationContext) {
+ const { dag, cfg } = strandsContext;
+ // Find all phi nodes in this branch block and declare them
+ const blockInstructions = cfg.blockInstructions[blockID] || [];
+ for (const nodeID of blockInstructions) {
+ const node = getNodeDataFromID(dag, nodeID);
+ if (node.nodeType === NodeType.PHI) {
+ // Check if the phi node's first dependency already has a temp name
+ const dependsOn = node.dependsOn || [];
+ if (dependsOn.length > 0) {
+ const firstDependency = dependsOn[0];
+ const existingTempName = generationContext.tempNames[firstDependency];
+ if (existingTempName) {
+ // Reuse the existing temp name instead of creating a new one
+ generationContext.tempNames[nodeID] = existingTempName;
+ continue; // Skip declaration, just alias to existing variable
+ }
+ }
+ // Otherwise, create a new temp variable for the phi node
+ const tmp = `T${generationContext.nextTempID++}`;
+ generationContext.tempNames[nodeID] = tmp;
+ const T = extractNodeTypeInfo(dag, nodeID);
+ const typeName = glslBackend.getTypeName(T.baseType, T.dimension);
+ generationContext.write(`${typeName} ${tmp};`);
+ }
+ }
+ this[BlockType.DEFAULT](blockID, strandsContext, generationContext);
+ },
[BlockType.IF_COND](blockID, strandsContext, generationContext) {
const { dag, cfg } = strandsContext;
const conditionID = cfg.blockConditions[blockID];
const condExpr = glslBackend.generateExpression(generationContext, dag, conditionID);
- generationContext.write(`if (${condExpr}) {`)
- generationContext.indent++;
+ generationContext.write(`if (${condExpr})`);
+ this[BlockType.DEFAULT](blockID, strandsContext, generationContext);
+ },
+ [BlockType.ELSE_COND](blockID, strandsContext, generationContext) {
+ generationContext.write(`else`);
this[BlockType.DEFAULT](blockID, strandsContext, generationContext);
- generationContext.indent--;
- generationContext.write(`}`)
- return;
},
-
[BlockType.IF_BODY](blockID, strandsContext, generationContext) {
-
+ this[BlockType.DEFAULT](blockID, strandsContext, generationContext);
+ this.assignPhiNodeValues(blockID, strandsContext, generationContext);
},
-
- [BlockType.ELIF_BODY](blockID, strandsContext, generationContext) {
-
+ [BlockType.SCOPE_START](blockID, strandsContext, generationContext) {
+ generationContext.write(`{`);
+ generationContext.indent++;
},
-
- [BlockType.ELSE_BODY](blockID, strandsContext, generationContext) {
-
+ [BlockType.SCOPE_END](blockID, strandsContext, generationContext) {
+ generationContext.indent--;
+ generationContext.write(`}`);
},
-
[BlockType.MERGE](blockID, strandsContext, generationContext) {
this[BlockType.DEFAULT](blockID, strandsContext, generationContext);
},
-
[BlockType.FUNCTION](blockID, strandsContext, generationContext) {
this[BlockType.DEFAULT](blockID, strandsContext, generationContext);
},
-}
+ [BlockType.FOR](blockID, strandsContext, generationContext) {
+ const { dag, cfg } = strandsContext;
+ const instructions = cfg.blockInstructions[blockID] || [];
+
+ generationContext.write(`for (`);
+ // Set flag to suppress semicolon on the last statement
+ const originalSuppressSemicolon = generationContext.suppressSemicolon;
+ for (let i = 0; i < instructions.length; i++) {
+ const nodeID = instructions[i];
+ const node = getNodeDataFromID(dag, nodeID);
+ const isLast = i === instructions.length - 1;
+
+ // Suppress semicolon on the last statement
+ generationContext.suppressSemicolon = isLast;
+
+ if (shouldCreateTemp(dag, nodeID)) {
+ const declaration = glslBackend.generateDeclaration(generationContext, dag, nodeID);
+ generationContext.write(declaration);
+ }
+ if (node.nodeType === NodeType.STATEMENT) {
+ glslBackend.generateStatement(generationContext, dag, nodeID);
+ }
+ if (node.nodeType === NodeType.ASSIGNMENT) {
+ glslBackend.generateAssignment(generationContext, dag, nodeID);
+ }
+ }
+
+ // Restore original flag
+ generationContext.suppressSemicolon = originalSuppressSemicolon;
+
+ generationContext.write(`)`);
+ },
+ assignPhiNodeValues(blockID, strandsContext, generationContext) {
+ const { dag, cfg } = strandsContext;
+ // Find all phi nodes that this block feeds into
+ const successors = cfg.outgoingEdges[blockID] || [];
+ for (const successorBlockID of successors) {
+ const instructions = cfg.blockInstructions[successorBlockID] || [];
+ for (const nodeID of instructions) {
+ const node = getNodeDataFromID(dag, nodeID);
+ if (node.nodeType === NodeType.PHI) {
+ // Find which input of this phi node corresponds to our block
+ const branchIndex = node.phiBlocks?.indexOf(blockID);
+ if (branchIndex !== -1 && branchIndex < node.dependsOn.length) {
+ const sourceNodeID = node.dependsOn[branchIndex];
+ const tempName = generationContext.tempNames[nodeID];
+ if (tempName && sourceNodeID !== null) {
+ const sourceExpr = glslBackend.generateExpression(generationContext, dag, sourceNodeID);
+ generationContext.write(`${tempName} = ${sourceExpr};`);
+ }
+ }
+ }
+ }
+ }
+ },
+}
export const glslBackend = {
hookEntry(hookType) {
const firstLine = `(${hookType.parameters.flatMap((param) => {
@@ -86,7 +166,6 @@ export const glslBackend = {
}).join(', ')}) {`;
return firstLine;
},
-
getTypeName(baseType, dimension) {
const primitiveTypeName = TypeNames[baseType + dimension]
if (!primitiveTypeName) {
@@ -94,28 +173,48 @@ export const glslBackend = {
}
return primitiveTypeName;
},
-
generateUniformDeclaration(name, typeInfo) {
return `${this.getTypeName(typeInfo.baseType, typeInfo.dimension)} ${name}`;
},
-
generateStatement(generationContext, dag, nodeID) {
const node = getNodeDataFromID(dag, nodeID);
- if (node.statementType === OpCode.ControlFlow.DISCARD) {
- generationContext.write('discard;');
+ const semicolon = generationContext.suppressSemicolon ? '' : ';';
+ if (node.statementType === StatementType.DISCARD) {
+ generationContext.write(`discard${semicolon}`);
+ } else if (node.statementType === StatementType.BREAK) {
+ generationContext.write(`break${semicolon}`);
+ } else if (node.statementType === StatementType.EXPRESSION) {
+ // Generate the expression followed by semicolon (unless suppressed)
+ const exprNodeID = node.dependsOn[0];
+ const expr = this.generateExpression(generationContext, dag, exprNodeID);
+ generationContext.write(`${expr}${semicolon}`);
+ } else if (node.statementType === StatementType.EMPTY) {
+ // Generate just a semicolon (unless suppressed)
+ generationContext.write(semicolon);
}
},
+ generateAssignment(generationContext, dag, nodeID) {
+ const node = getNodeDataFromID(dag, nodeID);
+ // dependsOn[0] = phiNodeID, dependsOn[1] = sourceNodeID
+ const phiNodeID = node.dependsOn[0];
+ const sourceNodeID = node.dependsOn[1];
+ const phiTempName = generationContext.tempNames[phiNodeID];
+ const sourceExpr = this.generateExpression(generationContext, dag, sourceNodeID);
+ const semicolon = generationContext.suppressSemicolon ? '' : ';';
+ // Skip assignment if target and source are the same variable
+ if (phiTempName && sourceExpr && phiTempName !== sourceExpr) {
+ generationContext.write(`${phiTempName} = ${sourceExpr}${semicolon}`);
+ }
+ },
generateDeclaration(generationContext, dag, nodeID) {
const expr = this.generateExpression(generationContext, dag, nodeID);
const tmp = `T${generationContext.nextTempID++}`;
generationContext.tempNames[nodeID] = tmp;
-
const T = extractNodeTypeInfo(dag, nodeID);
const typeName = this.getTypeName(T.baseType, T.dimension);
return `${typeName} ${tmp} = ${expr};`;
},
-
generateReturnStatement(strandsContext, generationContext, rootNodeID, returnType) {
const dag = strandsContext.dag;
const rootNode = getNodeDataFromID(dag, rootNodeID);
@@ -133,7 +232,6 @@ export const glslBackend = {
}
generationContext.write(`return ${this.generateExpression(generationContext, dag, rootNodeID)};`);
},
-
generateExpression(generationContext, dag, nodeID) {
const node = getNodeDataFromID(dag, nodeID);
if (generationContext.tempNames?.[nodeID]) {
@@ -147,10 +245,8 @@ export const glslBackend = {
else {
return node.value;
}
-
case NodeType.VARIABLE:
return node.identifier;
-
case NodeType.OPERATION:
const useParantheses = node.usedBy.length > 0;
if (node.opCode === OpCode.Nary.CONSTRUCTOR) {
@@ -184,6 +280,19 @@ export const glslBackend = {
const [lID, rID] = node.dependsOn;
const left = this.generateExpression(generationContext, dag, lID);
const right = this.generateExpression(generationContext, dag, rID);
+
+ // Special case for modulo: use mod() function for floats in GLSL
+ if (node.opCode === OpCode.Binary.MODULO) {
+ const leftNode = getNodeDataFromID(dag, lID);
+ const rightNode = getNodeDataFromID(dag, rID);
+ // If either operand is float, use mod() function
+ if (leftNode.baseType === BaseType.FLOAT || rightNode.baseType === BaseType.FLOAT) {
+ return `mod(${left}, ${right})`;
+ }
+ // For integers, use % operator
+ return `(${left} % ${right})`;
+ }
+
const opSym = OpCodeToSymbol[node.opCode];
if (useParantheses) {
return `(${left} ${opSym} ${right})`;
@@ -200,11 +309,35 @@ export const glslBackend = {
const sym = OpCodeToSymbol[node.opCode];
return `${sym}${val}`;
}
+ case NodeType.PHI:
+ // Phi nodes represent conditional merging of values
+ // They should already have been declared as temporary variables
+ // and assigned in the appropriate branches
+ if (generationContext.tempNames?.[nodeID]) {
+ return generationContext.tempNames[nodeID];
+ } else {
+ // If no temp was created, this phi node only has one input
+ // so we can just use that directly
+ const validInputs = node.dependsOn.filter(id => id !== null);
+ if (validInputs.length > 0) {
+ return this.generateExpression(generationContext, dag, validInputs[0]);
+ } else {
+ throw new Error(`No valid inputs for node`)
+ // Fallback: create a default value
+ const typeName = this.getTypeName(node.baseType, node.dimension);
+ if (node.dimension === 1) {
+ return node.baseType === BaseType.FLOAT ? '0.0' : '0';
+ } else {
+ return `${typeName}(0.0)`;
+ }
+ }
+ }
+ case NodeType.ASSIGNMENT:
+ FES.internalError(`ASSIGNMENT nodes should not be used as expressions`)
default:
FES.internalError(`${NodeTypeToName[node.nodeType]} code generation not implemented yet`)
}
},
-
generateBlock(blockID, strandsContext, generationContext) {
const type = strandsContext.cfg.blockTypes[blockID];
const handler = cfgHandlers[type] || cfgHandlers[BlockType.DEFAULT];
diff --git a/src/strands/strands_node.js b/src/strands/strands_node.js
new file mode 100644
index 0000000000..c706fae783
--- /dev/null
+++ b/src/strands/strands_node.js
@@ -0,0 +1,17 @@
+import { swizzleTrap } from './ir_builders';
+export class StrandsNode {
+ constructor(id, dimension, strandsContext) {
+ this.id = id;
+ this.strandsContext = strandsContext;
+ this.dimension = dimension;
+ }
+ copy() {
+ return createStrandsNode(this.id, this.dimension, this.strandsContext);
+ }
+}
+export function createStrandsNode(id, dimension, strandsContext, onRebind) {
+ return new Proxy(
+ new StrandsNode(id, dimension, strandsContext),
+ swizzleTrap(id, dimension, strandsContext, onRebind)
+ );
+}
\ No newline at end of file
diff --git a/src/strands/strands_phi_utils.js b/src/strands/strands_phi_utils.js
new file mode 100644
index 0000000000..e73c4c34cd
--- /dev/null
+++ b/src/strands/strands_phi_utils.js
@@ -0,0 +1,30 @@
+import * as CFG from './ir_cfg';
+import * as DAG from './ir_dag';
+import { NodeType } from './ir_types';
+
+export function createPhiNode(strandsContext, phiInputs, varName) {
+ // Determine the proper dimension and baseType from the inputs
+ const validInputs = phiInputs.filter(input => input.value.id !== null);
+ if (validInputs.length === 0) {
+ throw new Error(`No valid inputs for phi node for variable ${varName}`);
+ }
+ // Get dimension and baseType from first valid input
+ const firstInput = DAG.getNodeDataFromID(strandsContext.dag, validInputs[0].value.id);
+ const dimension = firstInput.dimension;
+ const baseType = firstInput.baseType;
+ const nodeData = {
+ nodeType: NodeType.PHI,
+ dimension,
+ baseType,
+ dependsOn: phiInputs.map(input => input.value.id).filter(id => id !== null),
+ phiBlocks: phiInputs.map(input => input.blockId),
+ phiInputs // Store the full phi input information
+ };
+ const id = DAG.getOrCreateNode(strandsContext.dag, nodeData);
+ CFG.recordInBasicBlock(strandsContext.cfg, strandsContext.cfg.currentBlock, id);
+ return {
+ id,
+ dimension,
+ baseType
+ };
+}
\ No newline at end of file
diff --git a/src/strands/strands_transpiler.js b/src/strands/strands_transpiler.js
index dd39a21c87..746a238278 100644
--- a/src/strands/strands_transpiler.js
+++ b/src/strands/strands_transpiler.js
@@ -1,8 +1,8 @@
import { parse } from 'acorn';
-import { ancestor } from 'acorn-walk';
+import { ancestor, recursive } from 'acorn-walk';
import escodegen from 'escodegen';
import { UnarySymbolToName } from './ir_types';
-
+let blockVarCounter = 0;
function replaceBinaryOperator(codeSource) {
switch (codeSource) {
case '+': return 'add';
@@ -12,14 +12,18 @@ function replaceBinaryOperator(codeSource) {
case '%': return 'mod';
case '==':
case '===': return 'equalTo';
+ case '!=':
+ case '!==': return 'notEqual';
case '>': return 'greaterThan';
- case '>=': return 'greaterThanEqualTo';
+ case '>=': return 'greaterEqual';
case '<': return 'lessThan';
+ case '<=': return 'lessEqual';
case '&&': return 'and';
case '||': return 'or';
+ // TODO: handle ** --> pow, but make it stay pow in
+ // GLSL instead of turning it back into **
}
}
-
function nodeIsUniform(ancestor) {
return ancestor.type === 'CallExpression'
&& (
@@ -34,12 +38,9 @@ function nodeIsUniform(ancestor) {
)
);
}
-
const ASTCallbacks = {
UnaryExpression(node, _state, ancestors) {
if (ancestors.some(nodeIsUniform)) { return; }
-
-
const unaryFnName = UnarySymbolToName[node.operator];
const standardReplacement = (node) => {
node.type = 'CallExpression'
@@ -49,7 +50,6 @@ const ASTCallbacks = {
}
node.arguments = [node.argument]
}
-
if (node.type === 'MemberExpression') {
const property = node.argument.property.name;
const swizzleSets = [
@@ -57,11 +57,9 @@ const ASTCallbacks = {
['r', 'g', 'b', 'a'],
['s', 't', 'p', 'q']
];
-
let isSwizzle = swizzleSets.some(set =>
[...property].every(char => set.includes(char))
) && node.argument.type === 'MemberExpression';
-
if (isSwizzle) {
node.type = 'MemberExpression';
node.object = {
@@ -85,6 +83,15 @@ const ASTCallbacks = {
delete node.argument;
delete node.operator;
},
+ BreakStatement(node, _state, ancestors) {
+ if (ancestors.some(nodeIsUniform)) { return; }
+ node.callee = {
+ type: 'Identifier',
+ name: '__p5.break'
+ };
+ node.arguments = [];
+ node.type = 'CallExpression';
+ },
VariableDeclarator(node, _state, ancestors) {
if (ancestors.some(nodeIsUniform)) { return; }
if (nodeIsUniform(node.init)) {
@@ -206,14 +213,687 @@ const ASTCallbacks = {
};
node.arguments = [node.right];
},
+ IfStatement(node, _state, ancestors) {
+ if (ancestors.some(nodeIsUniform)) { return; }
+ // Transform if statement into strandsIf() call
+ // The condition is evaluated directly, not wrapped in a function
+ const condition = node.test;
+ // Create the then function
+ const thenFunction = {
+ type: 'ArrowFunctionExpression',
+ params: [],
+ body: node.consequent.type === 'BlockStatement' ? node.consequent : {
+ type: 'BlockStatement',
+ body: [node.consequent]
+ }
+ };
+ // Start building the call chain: __p5.strandsIf(condition, then)
+ let callExpression = {
+ type: 'CallExpression',
+ callee: {
+ type: 'Identifier',
+ name: '__p5.strandsIf'
+ },
+ arguments: [condition, thenFunction]
+ };
+ // Always chain .Else() even if there's no explicit else clause
+ // This ensures the conditional completes and returns phi nodes
+ let elseFunction;
+ if (node.alternate) {
+ elseFunction = {
+ type: 'ArrowFunctionExpression',
+ params: [],
+ body: node.alternate.type === 'BlockStatement' ? node.alternate : {
+ type: 'BlockStatement',
+ body: [node.alternate]
+ }
+ };
+ } else {
+ // Create an empty else function
+ elseFunction = {
+ type: 'ArrowFunctionExpression',
+ params: [],
+ body: {
+ type: 'BlockStatement',
+ body: []
+ }
+ };
+ }
+ callExpression = {
+ type: 'CallExpression',
+ callee: {
+ type: 'MemberExpression',
+ object: callExpression,
+ property: {
+ type: 'Identifier',
+ name: 'Else'
+ }
+ },
+ arguments: [elseFunction]
+ };
+ // Analyze which outer scope variables are assigned in any branch
+ const assignedVars = new Set();
+ const analyzeBlock = (body) => {
+ if (body.type !== 'BlockStatement') return;
+ // First pass: collect variable declarations within this block
+ const localVars = new Set();
+ for (const stmt of body.body) {
+ if (stmt.type === 'VariableDeclaration') {
+ for (const decl of stmt.declarations) {
+ if (decl.id.type === 'Identifier') {
+ localVars.add(decl.id.name);
+ }
+ }
+ }
+ }
+ // Second pass: find assignments to non-local variables
+ for (const stmt of body.body) {
+ if (stmt.type === 'ExpressionStatement' &&
+ stmt.expression.type === 'AssignmentExpression') {
+ const left = stmt.expression.left;
+ if (left.type === 'Identifier') {
+ // Direct variable assignment: x = value
+ if (!localVars.has(left.name)) {
+ assignedVars.add(left.name);
+ }
+ } else if (left.type === 'MemberExpression' &&
+ left.object.type === 'Identifier') {
+ // Property assignment: obj.prop = value
+ if (!localVars.has(left.object.name)) {
+ assignedVars.add(left.object.name);
+ }
+ } else if (stmt.type === 'BlockStatement') {
+ // Recursively analyze nested block statements
+ analyzeBlock(stmt);
+ }
+ }
+ }
+ };
+ // Analyze all branches for assignments to outer scope variables
+ analyzeBlock(thenFunction.body);
+ analyzeBlock(elseFunction.body);
+ if (assignedVars.size > 0) {
+ // Add copying, reference replacement, and return statements to branch functions
+ const addCopyingAndReturn = (functionBody, varsToReturn) => {
+ if (functionBody.type === 'BlockStatement') {
+ // Create temporary variables and copy statements
+ const tempVarMap = new Map(); // original name -> temp name
+ const copyStatements = [];
+ for (const varName of varsToReturn) {
+ const tempName = `__copy_${varName}_${blockVarCounter++}`;
+ tempVarMap.set(varName, tempName);
+ // let tempName = originalVar.copy()
+ copyStatements.push({
+ type: 'VariableDeclaration',
+ declarations: [{
+ type: 'VariableDeclarator',
+ id: { type: 'Identifier', name: tempName },
+ init: {
+ type: 'CallExpression',
+ callee: {
+ type: 'MemberExpression',
+ object: { type: 'Identifier', name: varName },
+ property: { type: 'Identifier', name: 'copy' },
+ computed: false
+ },
+ arguments: []
+ }
+ }],
+ kind: 'let'
+ });
+ }
+ // Replace all references to original variables with temp variables
+ // and wrap literal assignments in strandsNode calls
+ const replaceReferences = (node) => {
+ if (!node || typeof node !== 'object') return;
+ if (node.type === 'Identifier' && tempVarMap.has(node.name)) {
+ node.name = tempVarMap.get(node.name);
+ } else if (node.type === 'MemberExpression' &&
+ node.object.type === 'Identifier' &&
+ tempVarMap.has(node.object.name)) {
+ node.object.name = tempVarMap.get(node.object.name);
+ }
+ // Handle literal assignments to temp variables
+ if (node.type === 'AssignmentExpression' &&
+ node.left.type === 'Identifier' &&
+ tempVarMap.has(node.left.name) &&
+ (node.right.type === 'Literal' || node.right.type === 'ArrayExpression')) {
+ // Wrap the right hand side in a strandsNode call to make sure
+ // it's not just a literal and has a type
+ node.right = {
+ type: 'CallExpression',
+ callee: {
+ type: 'Identifier',
+ name: '__p5.strandsNode'
+ },
+ arguments: [node.right]
+ };
+ }
+ // Recursively process all properties
+ for (const key in node) {
+ if (node.hasOwnProperty(key) && key !== 'parent') {
+ if (Array.isArray(node[key])) {
+ node[key].forEach(replaceReferences);
+ } else if (typeof node[key] === 'object') {
+ replaceReferences(node[key]);
+ }
+ }
+ }
+ };
+ // Apply reference replacement to all statements
+ functionBody.body.forEach(replaceReferences);
+ // Insert copy statements at the beginning
+ functionBody.body.unshift(...copyStatements);
+ // Add return statement with temp variable names
+ const returnObj = {
+ type: 'ObjectExpression',
+ properties: Array.from(varsToReturn).map(varName => ({
+ type: 'Property',
+ key: { type: 'Identifier', name: varName },
+ value: { type: 'Identifier', name: tempVarMap.get(varName) },
+ kind: 'init',
+ computed: false,
+ shorthand: false
+ }))
+ };
+ functionBody.body.push({
+ type: 'ReturnStatement',
+ argument: returnObj
+ });
+ }
+ };
+ addCopyingAndReturn(thenFunction.body, assignedVars);
+ addCopyingAndReturn(elseFunction.body, assignedVars);
+ // Create a block variable to capture the return value
+ const blockVar = `__block_${blockVarCounter++}`;
+ // Replace with a block statement
+ const statements = [];
+ // Make sure every assigned variable starts as a node
+ for (const varName of assignedVars) {
+ statements.push({
+ type: 'ExpressionStatement',
+ expression: {
+ type: 'AssignmentExpression',
+ operator: '=',
+ left: { type: 'Identifier', name: varName },
+ right: {
+ type: 'CallExpression',
+ callee: { type: 'Identifier', name: '__p5.strandsNode' },
+ arguments: [{ type: 'Identifier', name: varName }],
+ }
+ }
+ });
+ }
+ statements.push({
+ type: 'VariableDeclaration',
+ declarations: [{
+ type: 'VariableDeclarator',
+ id: { type: 'Identifier', name: blockVar },
+ init: callExpression
+ }],
+ kind: 'const'
+ });
+ // 2. Assignments for each modified variable
+ for (const varName of assignedVars) {
+ statements.push({
+ type: 'ExpressionStatement',
+ expression: {
+ type: 'AssignmentExpression',
+ operator: '=',
+ left: { type: 'Identifier', name: varName },
+ right: {
+ type: 'MemberExpression',
+ object: { type: 'Identifier', name: blockVar },
+ property: { type: 'Identifier', name: varName },
+ computed: false
+ }
+ }
+ });
+ }
+ // Replace the if statement with a block statement
+ node.type = 'BlockStatement';
+ node.body = statements;
+ } else {
+ // No assignments, just replace with the call expression
+ node.type = 'ExpressionStatement';
+ node.expression = callExpression;
+ }
+ delete node.test;
+ delete node.consequent;
+ delete node.alternate;
+ },
+ UpdateExpression(node, _state, ancestors) {
+ if (ancestors.some(nodeIsUniform)) { return; }
+
+ // Transform ++var, var++, --var, var-- into assignment expressions
+ let operator;
+ if (node.operator === '++') {
+ operator = '+';
+ } else if (node.operator === '--') {
+ operator = '-';
+ } else {
+ return; // Unknown update operator
+ }
+
+ // Convert to: var = var + 1 or var = var - 1
+ const assignmentExpr = {
+ type: 'AssignmentExpression',
+ operator: '=',
+ left: node.argument,
+ right: {
+ type: 'BinaryExpression',
+ operator: operator,
+ left: node.argument,
+ right: {
+ type: 'Literal',
+ value: 1
+ }
+ }
+ };
+
+ // Replace the update expression with the assignment expression
+ Object.assign(node, assignmentExpr);
+ delete node.prefix;
+ this.BinaryExpression(node.right, _state, [...ancestors, node]);
+ this.AssignmentExpression(node, _state, ancestors);
+ },
+ ForStatement(node, _state, ancestors) {
+ if (ancestors.some(nodeIsUniform)) { return; }
+
+ // Transform for statement into strandsFor() call
+ // for (init; test; update) body -> strandsFor(initCb, conditionCb, updateCb, bodyCb, initialVars)
+
+ // Create the initial callback from the for loop's init
+ let initialFunction;
+ if (node.init && node.init.type === 'VariableDeclaration') {
+ // Handle: for (let i = 0; ...)
+ const declaration = node.init.declarations[0];
+ let initValue = declaration.init;
+
+ const initAst = { type: 'Program', body: [{ type: 'ExpressionStatement', expression: initValue }] };
+ initValue = initAst.body[0].expression;
+
+ initialFunction = {
+ type: 'ArrowFunctionExpression',
+ params: [],
+ body: {
+ type: 'BlockStatement',
+ body: [{
+ type: 'ReturnStatement',
+ argument: initValue
+ }]
+ }
+ };
+ } else {
+ // Handle other cases - return a default value
+ initialFunction = {
+ type: 'ArrowFunctionExpression',
+ params: [],
+ body: {
+ type: 'BlockStatement',
+ body: [{
+ type: 'ReturnStatement',
+ argument: {
+ type: 'Literal',
+ value: 0
+ }
+ }]
+ }
+ };
+ }
+
+ // Create the condition callback
+ let conditionBody = node.test || { type: 'Literal', value: true };
+ // Replace loop variable references with the parameter
+ if (node.init?.type === 'VariableDeclaration') {
+ const loopVarName = node.init.declarations[0].id.name;
+ conditionBody = this.replaceIdentifierReferences(conditionBody, loopVarName, 'loopVar');
+ }
+ const conditionAst = { type: 'Program', body: [{ type: 'ExpressionStatement', expression: conditionBody }] };
+ conditionBody = conditionAst.body[0].expression;
+
+ const conditionFunction = {
+ type: 'ArrowFunctionExpression',
+ params: [{ type: 'Identifier', name: 'loopVar' }],
+ body: conditionBody
+ };
+
+ // Create the update callback
+ let updateFunction;
+ if (node.update) {
+ let updateExpr = node.update;
+ // Replace loop variable references with the parameter
+ if (node.init?.type === 'VariableDeclaration') {
+ const loopVarName = node.init.declarations[0].id.name;
+ updateExpr = this.replaceIdentifierReferences(updateExpr, loopVarName, 'loopVar');
+ }
+ const updateAst = { type: 'Program', body: [{ type: 'ExpressionStatement', expression: updateExpr }] };
+ // const nonControlFlowCallbacks = { ...ASTCallbacks };
+ // delete nonControlFlowCallbacks.IfStatement;
+ // delete nonControlFlowCallbacks.ForStatement;
+ // ancestor(updateAst, nonControlFlowCallbacks, undefined, _state);
+ updateExpr = updateAst.body[0].expression;
+
+ updateFunction = {
+ type: 'ArrowFunctionExpression',
+ params: [{ type: 'Identifier', name: 'loopVar' }],
+ body: {
+ type: 'BlockStatement',
+ body: [{
+ type: 'ReturnStatement',
+ argument: updateExpr
+ }]
+ }
+ };
+ } else {
+ updateFunction = {
+ type: 'ArrowFunctionExpression',
+ params: [{ type: 'Identifier', name: 'loopVar' }],
+ body: {
+ type: 'BlockStatement',
+ body: [{
+ type: 'ReturnStatement',
+ argument: { type: 'Identifier', name: 'loopVar' }
+ }]
+ }
+ };
+ }
+
+ // Create the body callback
+ let bodyBlock = node.body.type === 'BlockStatement' ? node.body : {
+ type: 'BlockStatement',
+ body: [node.body]
+ };
+
+ // Replace loop variable references in the body
+ if (node.init?.type === 'VariableDeclaration') {
+ const loopVarName = node.init.declarations[0].id.name;
+ bodyBlock = this.replaceIdentifierReferences(bodyBlock, loopVarName, 'loopVar');
+ }
+
+ const bodyFunction = {
+ type: 'ArrowFunctionExpression',
+ params: [
+ { type: 'Identifier', name: 'loopVar' },
+ { type: 'Identifier', name: 'vars' }
+ ],
+ body: bodyBlock
+ };
+
+ // Analyze which outer scope variables are assigned in the loop body
+ const assignedVars = new Set();
+ const analyzeBlock = (body, parentLocalVars = new Set()) => {
+ if (body.type !== 'BlockStatement') return;
+
+ // First pass: collect variable declarations within this block
+ const localVars = new Set([...parentLocalVars]);
+ for (const stmt of body.body) {
+ if (stmt.type === 'VariableDeclaration') {
+ for (const decl of stmt.declarations) {
+ if (decl.id.type === 'Identifier') {
+ localVars.add(decl.id.name);
+ }
+ }
+ }
+ }
+
+ // Second pass: find assignments to non-local variables
+ for (const stmt of body.body) {
+ if (stmt.type === 'ExpressionStatement' &&
+ stmt.expression.type === 'AssignmentExpression') {
+ const left = stmt.expression.left;
+ if (left.type === 'Identifier') {
+ // Direct variable assignment: x = value
+ if (!localVars.has(left.name)) {
+ assignedVars.add(left.name);
+ }
+ } else if (left.type === 'MemberExpression' &&
+ left.object.type === 'Identifier') {
+ // Property assignment: obj.prop = value (includes swizzles)
+ if (!localVars.has(left.object.name)) {
+ assignedVars.add(left.object.name);
+ }
+ }
+ } else if (stmt.type === 'BlockStatement') {
+ // Recursively analyze nested block statements, passing down local vars
+ analyzeBlock(stmt, localVars);
+ }
+ }
+ };
+
+ analyzeBlock(bodyFunction.body);
+
+ if (assignedVars.size > 0) {
+ // Add copying, reference replacement, and return statements similar to if statements
+ const addCopyingAndReturn = (functionBody, varsToReturn) => {
+ if (functionBody.type === 'BlockStatement') {
+ const tempVarMap = new Map();
+ const copyStatements = [];
+
+ for (const varName of varsToReturn) {
+ const tempName = `__copy_${varName}_${blockVarCounter++}`;
+ tempVarMap.set(varName, tempName);
+
+ copyStatements.push({
+ type: 'VariableDeclaration',
+ declarations: [{
+ type: 'VariableDeclarator',
+ id: { type: 'Identifier', name: tempName },
+ init: {
+ type: 'CallExpression',
+ callee: {
+ type: 'MemberExpression',
+ object: {
+ type: 'MemberExpression',
+ object: { type: 'Identifier', name: 'vars' },
+ property: { type: 'Identifier', name: varName },
+ computed: false
+ },
+ property: { type: 'Identifier', name: 'copy' },
+ computed: false
+ },
+ arguments: []
+ }
+ }],
+ kind: 'let'
+ });
+ }
+
+ // Replace references to original variables with temp variables
+ const replaceReferences = (node) => {
+ if (!node || typeof node !== 'object') return;
+ if (node.type === 'Identifier' && tempVarMap.has(node.name)) {
+ node.name = tempVarMap.get(node.name);
+ }
+
+ for (const key in node) {
+ if (node.hasOwnProperty(key) && key !== 'parent') {
+ if (Array.isArray(node[key])) {
+ node[key].forEach(replaceReferences);
+ } else if (typeof node[key] === 'object') {
+ replaceReferences(node[key]);
+ }
+ }
+ }
+ };
+
+ functionBody.body.forEach(replaceReferences);
+ functionBody.body.unshift(...copyStatements);
+
+ // Add return statement
+ const returnObj = {
+ type: 'ObjectExpression',
+ properties: Array.from(varsToReturn).map(varName => ({
+ type: 'Property',
+ key: { type: 'Identifier', name: varName },
+ value: { type: 'Identifier', name: tempVarMap.get(varName) },
+ kind: 'init',
+ computed: false,
+ shorthand: false
+ }))
+ };
+
+ functionBody.body.push({
+ type: 'ReturnStatement',
+ argument: returnObj
+ });
+ }
+ };
+
+ addCopyingAndReturn(bodyFunction.body, assignedVars);
+
+ // Create block variable and assignments similar to if statements
+ const blockVar = `__block_${blockVarCounter++}`;
+ const statements = [];
+
+ // Create initial vars object from assigned variables
+ const initialVarsProperties = [];
+ for (const varName of assignedVars) {
+ initialVarsProperties.push({
+ type: 'Property',
+ key: { type: 'Identifier', name: varName },
+ value: {
+ type: 'CallExpression',
+ callee: {
+ type: 'Identifier',
+ name: '__p5.strandsNode',
+ },
+ arguments: [
+ { type: 'Identifier', name: varName },
+ ],
+ },
+ kind: 'init',
+ method: false,
+ shorthand: false,
+ computed: false
+ });
+ }
+
+ const initialVarsObject = {
+ type: 'ObjectExpression',
+ properties: initialVarsProperties
+ };
+
+ // Create the strandsFor call
+ const callExpression = {
+ type: 'CallExpression',
+ callee: {
+ type: 'Identifier',
+ name: '__p5.strandsFor'
+ },
+ arguments: [initialFunction, conditionFunction, updateFunction, bodyFunction, initialVarsObject]
+ };
+
+ statements.push({
+ type: 'VariableDeclaration',
+ declarations: [{
+ type: 'VariableDeclarator',
+ id: { type: 'Identifier', name: blockVar },
+ init: callExpression
+ }],
+ kind: 'const'
+ });
+
+ // Add assignments back to original variables
+ for (const varName of assignedVars) {
+ statements.push({
+ type: 'ExpressionStatement',
+ expression: {
+ type: 'AssignmentExpression',
+ operator: '=',
+ left: { type: 'Identifier', name: varName },
+ right: {
+ type: 'MemberExpression',
+ object: { type: 'Identifier', name: blockVar },
+ property: { type: 'Identifier', name: varName },
+ computed: false
+ }
+ }
+ });
+ }
+
+ node.type = 'BlockStatement';
+ node.body = statements;
+ } else {
+ // No assignments, just replace with call expression
+ node.type = 'ExpressionStatement';
+ node.expression = {
+ type: 'CallExpression',
+ callee: {
+ type: 'Identifier',
+ name: '__p5.strandsFor'
+ },
+ arguments: [initialFunction, conditionFunction, updateFunction, bodyFunction, {
+ type: 'ObjectExpression',
+ properties: []
+ }]
+ };
+ }
+
+ delete node.init;
+ delete node.test;
+ delete node.update;
+ },
+
+ // Helper method to replace identifier references in AST nodes
+ replaceIdentifierReferences(node, oldName, newName) {
+ if (!node || typeof node !== 'object') return node;
+
+ const replaceInNode = (n) => {
+ if (!n || typeof n !== 'object') return n;
+
+ if (n.type === 'Identifier' && n.name === oldName) {
+ return { ...n, name: newName };
+ }
+
+ // Create a copy and recursively process properties
+ const newNode = { ...n };
+ for (const key in n) {
+ if (n.hasOwnProperty(key) && key !== 'parent') {
+ if (Array.isArray(n[key])) {
+ newNode[key] = n[key].map(replaceInNode);
+ } else if (typeof n[key] === 'object') {
+ newNode[key] = replaceInNode(n[key]);
+ }
+ }
+ }
+ return newNode;
+ };
+
+ return replaceInNode(node);
+ }
}
-
export function transpileStrandsToJS(p5, sourceString, srcLocations, scope) {
const ast = parse(sourceString, {
ecmaVersion: 2021,
locations: srcLocations
});
- ancestor(ast, ASTCallbacks, undefined, { varyings: {} });
+ // First pass: transform everything except if/for statements using normal ancestor traversal
+ const nonControlFlowCallbacks = { ...ASTCallbacks };
+ delete nonControlFlowCallbacks.IfStatement;
+ delete nonControlFlowCallbacks.ForStatement;
+ ancestor(ast, nonControlFlowCallbacks, undefined, { varyings: {} });
+ // Second pass: transform if/for statements in post-order using recursive traversal
+ const postOrderControlFlowTransform = {
+ IfStatement(node, state, c) {
+ // First recursively process children
+ if (node.test) c(node.test, state);
+ if (node.consequent) c(node.consequent, state);
+ if (node.alternate) c(node.alternate, state);
+ // Then apply the transformation to this node
+ ASTCallbacks.IfStatement(node, state, []);
+ },
+ ForStatement(node, state, c) {
+ // First recursively process children
+ if (node.init) c(node.init, state);
+ if (node.test) c(node.test, state);
+ if (node.update) c(node.update, state);
+ if (node.body) c(node.body, state);
+ // Then apply the transformation to this node
+ ASTCallbacks.ForStatement(node, state, []);
+ }
+ };
+ recursive(ast, { varyings: {} }, postOrderControlFlowTransform);
const transpiledSource = escodegen.generate(ast);
const scopeKeys = Object.keys(scope);
const internalStrandsCallback = new Function(
@@ -231,4 +911,3 @@ const ASTCallbacks = {
);
return () => internalStrandsCallback(p5, ...scopeKeys.map(key => scope[key]));
}
-
\ No newline at end of file
diff --git a/test/unit/webgl/p5.Shader.js b/test/unit/webgl/p5.Shader.js
index d4e74efa36..11a5797906 100644
--- a/test/unit/webgl/p5.Shader.js
+++ b/test/unit/webgl/p5.Shader.js
@@ -1,9 +1,8 @@
import p5 from '../../../src/app.js';
-
suite('p5.Shader', function() {
var myp5;
-
beforeAll(function() {
+ window.IS_MINIFIED = true;
myp5 = new p5(function(p) {
p.setup = function() {
p.createCanvas(100, 100, p.WEBGL);
@@ -12,12 +11,10 @@ suite('p5.Shader', function() {
};
});
});
-
var testUniforms = function(shaderName, uniforms, expectedUniforms) {
// assert(expectedUniforms.length === Object.keys(uniforms).length,
// shaderName + ' expected ' + expectedUniforms.length + ' uniforms but has ' +
// Object.keys(uniforms).length);
-
// test each one
for (var i = 0; i < expectedUniforms.length; i++) {
var uniform = uniforms[expectedUniforms[i]];
@@ -27,12 +24,10 @@ suite('p5.Shader', function() {
);
}
};
-
var testAttributes = function(shaderName, attributes, expectedAttributes) {
// assert(expectedAttributes.length === Object.keys(attributes).length,
// shaderName + ' expected ' + expectedAttributes.length +
// ' attributes but has ' + Object.keys(attributes).length);
-
// test each one
for (var i = 0; i < expectedAttributes.length; i++) {
var attribute = attributes[expectedAttributes[i]];
@@ -42,7 +37,6 @@ suite('p5.Shader', function() {
);
}
};
-
var testShader = function(
shaderName,
shaderObj,
@@ -54,15 +48,12 @@ suite('p5.Shader', function() {
testUniforms(shaderName, shaderObj.uniforms, expectedUniforms);
shaderObj.unbindShader();
};
-
afterAll(function() {
myp5.remove();
});
-
suite('Shader', function() {
test('Light Shader', function() {
var expectedAttributes = ['aPosition', 'aNormal', 'aTexCoord'];
-
var expectedUniforms = [
'uModelViewMatrix',
'uProjectionMatrix',
@@ -93,7 +84,6 @@ suite('p5.Shader', function() {
'uLinearAttenuation',
'uQuadraticAttenuation'
];
-
testShader(
'Light Shader',
myp5._renderer._getLightShader(),
@@ -103,13 +93,11 @@ suite('p5.Shader', function() {
});
test('Color Shader definition', function() {
var expectedAttributes = ['aPosition'];
-
var expectedUniforms = [
'uModelViewMatrix',
'uProjectionMatrix',
'uMaterialColor'
];
-
testShader(
'Color Shader',
myp5._renderer._getColorShader(),
@@ -119,12 +107,10 @@ suite('p5.Shader', function() {
});
test('Immediate Mode Shader definition', function() {
var expectedAttributes = ['aPosition', 'aVertexColor'];
-
var expectedUniforms = [
'uModelViewMatrix',
'uProjectionMatrix'
];
-
testShader(
'Immediate Mode Shader',
myp5._renderer._getColorShader(),
@@ -134,13 +120,11 @@ suite('p5.Shader', function() {
});
test('Normal Shader definition', function() {
var expectedAttributes = ['aPosition', 'aNormal'];
-
var expectedUniforms = [
'uModelViewMatrix',
'uProjectionMatrix',
'uNormalMatrix'
];
-
testShader(
'Normal Shader',
myp5._renderer._getNormalShader(),
@@ -155,7 +139,6 @@ suite('p5.Shader', function() {
var immediateColorShader = myp5._renderer._getColorShader();
var selectedRetainedShader = myp5._renderer._getFillShader();
var selectedImmediateShader = myp5._renderer._getFillShader();
-
// both color and light shader are valid, depending on
// conditions set earlier.
assert(
@@ -231,60 +214,47 @@ suite('p5.Shader', function() {
'after call to emissiveMaterial()'
);
});
-
test('Able to setUniform empty arrays', function() {
myp5.shader(myp5._renderer._getLightShader());
var s = myp5._renderer.states.userFillShader;
-
s.setUniform('uMaterialColor', []);
s.setUniform('uLightingDirection', []);
});
-
test('Able to set shininess', function() {
assert.deepEqual(myp5._renderer.states._useShininess, 1);
myp5.shininess(50);
assert.deepEqual(myp5._renderer.states._useShininess, 50);
});
-
test('Shader is reset after resetShader is called', function() {
myp5.shader(myp5._renderer._getColorShader());
var prevShader = myp5._renderer.states.userFillShader;
assert.isTrue(prevShader !== null);
-
myp5.resetShader();
var curShader = myp5._renderer.states.userFillShader;
assert.isTrue(curShader === null);
});
-
suite('Hooks', function() {
let myShader;
-
beforeEach(function() {
myShader = myp5.createShader(
`
precision highp float;
-
attribute vec3 aPosition;
attribute vec2 aTexCoord;
attribute vec4 aVertexColor;
-
uniform mat4 uModelViewMatrix;
uniform mat4 uProjectionMatrix;
-
varying vec2 vTexCoord;
varying vec4 vVertexColor;
-
void main() {
// Apply the camera transform
vec4 viewModelPosition =
uModelViewMatrix *
vec4(aPosition, 1.0);
-
// Tell WebGL where the vertex goes
gl_Position =
uProjectionMatrix *
viewModelPosition;
-
// Pass along data to the fragment shader
vTexCoord = aTexCoord;
vVertexColor = aVertexColor;
@@ -292,10 +262,8 @@ suite('p5.Shader', function() {
`,
`
precision highp float;
-
varying vec2 vTexCoord;
varying vec4 vVertexColor;
-
void main() {
// Tell WebGL what color to make the pixel
gl_FragColor = HOOK_getVertexColor(vVertexColor);
@@ -308,7 +276,6 @@ suite('p5.Shader', function() {
}
);
});
-
test('available hooks show up in inspectHooks()', function() {
const logs = [];
const myLog = (...data) => logs.push(data.join(', '));
@@ -318,17 +285,14 @@ suite('p5.Shader', function() {
console.log = oldLog;
expect(logs.join('\n')).to.match(/vec4 getVertexColor/);
});
-
test('unfilled hooks do not have an AUGMENTED_HOOK define', function() {
const modified = myShader.modify({});
expect(modified.fragSrc()).not.to.match(/#define AUGMENTED_HOOK_getVertexColor/);
});
-
test('anonymous function shaderModifier does not throw when parsed', function() {
const callModify = () => myShader.modify(function() {});
expect(callModify).not.toThrowError();
});
-
test('filled hooks do have an AUGMENTED_HOOK define', function() {
const modified = myShader.modify({
'vec4 getVertexColor': `(vec4 c) {
@@ -338,7 +302,6 @@ suite('p5.Shader', function() {
expect(modified.fragSrc()).to.match(/#define AUGMENTED_HOOK_getVertexColor/);
});
});
-
test('framebuffer textures are unbound when you draw to the framebuffer', function() {
const sh = myp5.baseMaterialShader().modify({
uniforms: {
@@ -349,19 +312,15 @@ suite('p5.Shader', function() {
}`
});
const fbo = myp5.createFramebuffer();
-
myp5.shader(sh);
sh.setUniform('myTex', fbo);
-
fbo.draw(() => myp5.background('red'));
-
sh.setUniform('myTex', fbo);
myp5.noStroke();
myp5.plane(myp5.width, myp5.height);
assert.deepEqual(myp5.get(0, 0), [255, 0, 0, 255]);
});
});
-
suite('hookTypes', function() {
test('Produces expected types on baseFilterShader()', function() {
const types = myp5.baseFilterShader().hookTypes('vec4 getColor');
@@ -418,9 +377,8 @@ suite('p5.Shader', function() {
});
});
});
-
suite('p5.strands', () => {
- it('does not break when arrays are in uniform callbacks', () => {
+ test('does not break when arrays are in uniform callbacks', () => {
myp5.createCanvas(5, 5, myp5.WEBGL);
const myShader = myp5.baseMaterialShader().modify(() => {
const size = myp5.uniformVector2(() => [myp5.width, myp5.height]);
@@ -438,5 +396,749 @@ suite('p5.Shader', function() {
myp5.plane(myp5.width, myp5.height);
}).not.toThrowError();
});
+ suite('if statement conditionals', () => {
+ test('handle simple if statement with true condition', () => {
+ myp5.createCanvas(50, 50, myp5.WEBGL);
+ const testShader = myp5.baseMaterialShader().modify(() => {
+ const condition = myp5.uniformFloat(() => 1.0); // true condition
+ myp5.getPixelInputs(inputs => {
+ let color = myp5.float(0.5); // initial gray
+ if (condition > 0.5) {
+ color = myp5.float(1.0); // set to white in if branch
+ }
+ inputs.color = [color, color, color, 1.0];
+ return inputs;
+ });
+ }, { myp5 });
+ myp5.noStroke();
+ myp5.shader(testShader);
+ myp5.plane(myp5.width, myp5.height);
+ // Check that the center pixel is white (condition was true)
+ const pixelColor = myp5.get(25, 25);
+ assert.approximately(pixelColor[0], 255, 5); // Red channel should be 255 (white)
+ assert.approximately(pixelColor[1], 255, 5); // Green channel should be 255
+ assert.approximately(pixelColor[2], 255, 5); // Blue channel should be 255
+ });
+ test('handle simple if statement with simpler assignment', () => {
+ myp5.createCanvas(50, 50, myp5.WEBGL);
+ const testShader = myp5.baseMaterialShader().modify(() => {
+ const condition = myp5.uniformFloat(() => 1.0); // true condition
+ myp5.getPixelInputs(inputs => {
+ let color = 1; // initial gray
+ if (condition > 0.5) {
+ color = 1; // set to white in if branch
+ }
+ inputs.color = [color, color, color, 1.0];
+ return inputs;
+ });
+ }, { myp5 });
+ myp5.noStroke();
+ myp5.shader(testShader);
+ myp5.plane(myp5.width, myp5.height);
+ // Check that the center pixel is white (condition was true)
+ const pixelColor = myp5.get(25, 25);
+ assert.approximately(pixelColor[0], 255, 5); // Red channel should be 255 (white)
+ assert.approximately(pixelColor[1], 255, 5); // Green channel should be 255
+ assert.approximately(pixelColor[2], 255, 5); // Blue channel should be 255
+ });
+ test('handle simple if statement with false condition', () => {
+ myp5.createCanvas(50, 50, myp5.WEBGL);
+ const testShader = myp5.baseMaterialShader().modify(() => {
+ const condition = myp5.uniformFloat(() => 0.0); // false condition
+ myp5.getPixelInputs(inputs => {
+ let color = myp5.float(0.5); // initial gray
+ if (condition > 0.5) {
+ color = myp5.float(1.0); // set to white in if branch
+ }
+ inputs.color = [color, color, color, 1.0];
+ return inputs;
+ });
+ }, { myp5 });
+ myp5.noStroke();
+ myp5.shader(testShader);
+ myp5.plane(myp5.width, myp5.height);
+ // Check that the center pixel is gray (condition was false, original value kept)
+ const pixelColor = myp5.get(25, 25);
+ assert.approximately(pixelColor[0], 127, 5); // Red channel should be ~127 (gray)
+ assert.approximately(pixelColor[1], 127, 5); // Green channel should be ~127
+ assert.approximately(pixelColor[2], 127, 5); // Blue channel should be ~127
+ });
+ test('handle if-else statement', () => {
+ myp5.createCanvas(50, 50, myp5.WEBGL);
+ const testShader = myp5.baseMaterialShader().modify(() => {
+ const condition = myp5.uniformFloat(() => 0.0); // false condition
+ myp5.getPixelInputs(inputs => {
+ let color = myp5.float(0.5); // initial gray
+ if (condition > 0.5) {
+ color = myp5.float(1.0); // white for true
+ } else {
+ color = myp5.float(0.0); // black for false
+ }
+ inputs.color = [color, color, color, 1.0];
+ return inputs;
+ });
+ }, { myp5 });
+ myp5.noStroke();
+ myp5.shader(testShader);
+ myp5.plane(myp5.width, myp5.height);
+ // Check that the center pixel is black (else branch executed)
+ const pixelColor = myp5.get(25, 25);
+ assert.approximately(pixelColor[0], 0, 5); // Red channel should be ~0 (black)
+ assert.approximately(pixelColor[1], 0, 5); // Green channel should be ~0
+ assert.approximately(pixelColor[2], 0, 5); // Blue channel should be ~0
+ });
+ test('handle multiple variable assignments in if statement', () => {
+ myp5.createCanvas(50, 50, myp5.WEBGL);
+ const testShader = myp5.baseMaterialShader().modify(() => {
+ const condition = myp5.uniformFloat(() => 1.0); // true condition
+ myp5.getPixelInputs(inputs => {
+ let red = myp5.float(0.0);
+ let green = myp5.float(0.0);
+ let blue = myp5.float(0.0);
+ if (condition > 0.5) {
+ red = myp5.float(1.0);
+ green = myp5.float(0.5);
+ blue = myp5.float(0.0);
+ }
+ inputs.color = [red, green, blue, 1.0];
+ return inputs;
+ });
+ }, { myp5 });
+ myp5.noStroke();
+ myp5.shader(testShader);
+ myp5.plane(myp5.width, myp5.height);
+ // Check that the center pixel has the expected color (red=1.0, green=0.5, blue=0.0)
+ const pixelColor = myp5.get(25, 25);
+ assert.approximately(pixelColor[0], 255, 5); // Red channel should be 255
+ assert.approximately(pixelColor[1], 127, 5); // Green channel should be ~127
+ assert.approximately(pixelColor[2], 0, 5); // Blue channel should be ~0
+ });
+ test('handle modifications after if statement', () => {
+ myp5.createCanvas(50, 50, myp5.WEBGL);
+ const testShader = myp5.baseMaterialShader().modify(() => {
+ const condition = myp5.uniformFloat(() => 1.0); // true condition
+ myp5.getPixelInputs(inputs => {
+ let color = myp5.float(0.0); // start with black
+ if (condition > 0.5) {
+ color = myp5.float(1.0); // set to white in if branch
+ } else {
+ color = myp5.float(0.5); // set to gray in else branch
+ }
+ // Modify the color after the if statement
+ color = color * 0.5; // Should result in 0.5 * 1.0 = 0.5 (gray)
+ inputs.color = [color, color, color, 1.0];
+ return inputs;
+ });
+ }, { myp5 });
+ myp5.noStroke();
+ myp5.shader(testShader);
+ myp5.plane(myp5.width, myp5.height);
+ // Check that the center pixel is gray (white * 0.5 = gray)
+ const pixelColor = myp5.get(25, 25);
+ assert.approximately(pixelColor[0], 127, 5); // Red channel should be ~127 (gray)
+ assert.approximately(pixelColor[1], 127, 5); // Green channel should be ~127
+ assert.approximately(pixelColor[2], 127, 5); // Blue channel should be ~127
+ });
+ test('handle modifications after if statement in both branches', () => {
+ myp5.createCanvas(100, 50, myp5.WEBGL);
+ const testShader = myp5.baseMaterialShader().modify(() => {
+ myp5.getPixelInputs(inputs => {
+ debugger
+ const uv = inputs.texCoord;
+ const condition = uv.x > 0.5; // left half false, right half true
+ let color = myp5.float(0.0);
+ if (condition) {
+ color = myp5.float(1.0); // white on right side
+ } else {
+ color = myp5.float(0.8); // light gray on left side
+ }
+ // Multiply by 0.5 after the if statement
+ color = color * 0.5;
+ // Right side: 1.0 * 0.5 = 0.5 (medium gray)
+ // Left side: 0.8 * 0.5 = 0.4 (darker gray)
+ inputs.color = [color, color, color, 1.0];
+ return inputs;
+ });
+ }, { myp5 });
+ myp5.noStroke();
+ myp5.shader(testShader);
+ myp5.plane(myp5.width, myp5.height);
+ // Check left side (false condition)
+ const leftPixel = myp5.get(25, 25);
+ assert.approximately(leftPixel[0], 102, 5); // 0.4 * 255 ≈ 102
+ // Check right side (true condition)
+ const rightPixel = myp5.get(75, 25);
+ assert.approximately(rightPixel[0], 127, 5); // 0.5 * 255 ≈ 127
+ });
+ test('handle if-else-if chains', () => {
+ myp5.createCanvas(50, 50, myp5.WEBGL);
+ const testShader = myp5.baseMaterialShader().modify(() => {
+ const value = myp5.uniformFloat(() => 0.5); // middle value
+ myp5.getPixelInputs(inputs => {
+ let color = myp5.float(0.0);
+ if (value > 0.8) {
+ color = myp5.float(1.0); // white for high values
+ } else if (value > 0.3) {
+ color = myp5.float(0.5); // gray for medium values
+ } else {
+ color = myp5.float(0.0); // black for low values
+ }
+ inputs.color = [color, color, color, 1.0];
+ return inputs;
+ });
+ }, { myp5 });
+ myp5.noStroke();
+ myp5.shader(testShader);
+ myp5.plane(myp5.width, myp5.height);
+ // Check that the center pixel is gray (medium condition was true)
+ const pixelColor = myp5.get(25, 25);
+ assert.approximately(pixelColor[0], 127, 5); // Red channel should be ~127 (gray)
+ assert.approximately(pixelColor[1], 127, 5); // Green channel should be ~127
+ assert.approximately(pixelColor[2], 127, 5); // Blue channel should be ~127
+ });
+ test('handle if-else-if chains in the else branch', () => {
+ myp5.createCanvas(50, 50, myp5.WEBGL);
+ const testShader = myp5.baseMaterialShader().modify(() => {
+ const value = myp5.uniformFloat(() => 0.2); // middle value
+ myp5.getPixelInputs(inputs => {
+ let color = myp5.float(0.0);
+ if (value > 0.8) {
+ color = myp5.float(1.0); // white for high values
+ } else if (value > 0.3) {
+ color = myp5.float(0.5); // gray for medium values
+ } else {
+ color = myp5.float(0.0); // black for low values
+ }
+ inputs.color = [color, color, color, 1.0];
+ return inputs;
+ });
+ }, { myp5 });
+ myp5.noStroke();
+ myp5.shader(testShader);
+ myp5.plane(myp5.width, myp5.height);
+ // Check that the center pixel is gray (medium condition was true)
+ const pixelColor = myp5.get(25, 25);
+ assert.approximately(pixelColor[0], 0, 5); // Red channel should be ~127 (gray)
+ assert.approximately(pixelColor[1], 0, 5); // Green channel should be ~127
+ assert.approximately(pixelColor[2], 0, 5); // Blue channel should be ~127
+ });
+ test('handle nested if statements', () => {
+ myp5.createCanvas(50, 50, myp5.WEBGL);
+ const testShader = myp5.baseMaterialShader().modify(() => {
+ const outerCondition = myp5.uniformFloat(() => 1.0); // true
+ const innerCondition = myp5.uniformFloat(() => 1.0); // true
+ myp5.getPixelInputs(inputs => {
+ let color = myp5.float(0.0);
+ if (outerCondition > 0.5) {
+ if (innerCondition > 0.5) {
+ color = myp5.float(1.0); // white for both conditions true
+ } else {
+ color = myp5.float(0.5); // gray for outer true, inner false
+ }
+ } else {
+ color = myp5.float(0.0); // black for outer false
+ }
+ inputs.color = [color, color, color, 1.0];
+ return inputs;
+ });
+ }, { myp5 });
+ myp5.noStroke();
+ myp5.shader(testShader);
+ myp5.plane(myp5.width, myp5.height);
+ // Check that the center pixel is white (both conditions were true)
+ const pixelColor = myp5.get(25, 25);
+ assert.approximately(pixelColor[0], 255, 5); // Red channel should be 255 (white)
+ assert.approximately(pixelColor[1], 255, 5); // Green channel should be 255
+ assert.approximately(pixelColor[2], 255, 5); // Blue channel should be 255
+ });
+ // Keep one direct API test for completeness
+ test('handle direct StrandsIf API usage', () => {
+ myp5.createCanvas(50, 50, myp5.WEBGL);
+ const testShader = myp5.baseMaterialShader().modify(() => {
+ const conditionValue = myp5.uniformFloat(() => 1.0); // true condition
+ myp5.getPixelInputs(inputs => {
+ let color = myp5.float(0.5); // initial gray
+ const assignments = myp5.strandsIf(
+ conditionValue.greaterThan(0),
+ () => {
+ let tmp = color.copy();
+ tmp = myp5.float(1.0); // set to white in if branch
+ return { color: tmp };
+ }
+ ).Else(() => {
+ return { color: color }; // keep original in else branch
+ });
+ color = assignments.color;
+ inputs.color = [color, color, color, 1.0];
+ return inputs;
+ });
+ }, { myp5 });
+ myp5.noStroke();
+ myp5.shader(testShader);
+ myp5.plane(myp5.width, myp5.height);
+ // Check that the center pixel is white (condition was true)
+ const pixelColor = myp5.get(25, 25);
+ assert.approximately(pixelColor[0], 255, 5); // Red channel should be 255 (white)
+ assert.approximately(pixelColor[1], 255, 5); // Green channel should be 255
+ assert.approximately(pixelColor[2], 255, 5); // Blue channel should be 255
+ });
+ test('handle direct StrandsIf ElseIf API usage', () => {
+ myp5.createCanvas(50, 50, myp5.WEBGL);
+ const testShader = myp5.baseMaterialShader().modify(() => {
+ const value = myp5.uniformFloat(() => 0.5); // middle value
+ myp5.getPixelInputs(inputs => {
+ let color = myp5.float(0.0); // initial black
+ const assignments = myp5.strandsIf(
+ value.greaterThan(0.8),
+ () => {
+ let tmp = color.copy();
+ tmp = myp5.float(1.0); // white for high values
+ return { color: tmp };
+ }
+ ).ElseIf(
+ value.greaterThan(0.3),
+ () => {
+ let tmp = color.copy();
+ tmp = myp5.float(0.5); // gray for medium values
+ return { color: tmp };
+ }
+ ).Else(() => {
+ let tmp = color.copy();
+ tmp = myp5.float(0.0); // black for low values
+ return { color: tmp };
+ });
+ color = assignments.color;
+ inputs.color = [color, color, color, 1.0];
+ return inputs;
+ });
+ }, { myp5 });
+ myp5.noStroke();
+ myp5.shader(testShader);
+ myp5.plane(myp5.width, myp5.height);
+ // Check that the center pixel is gray (medium condition was true)
+ const pixelColor = myp5.get(25, 25);
+ assert.approximately(pixelColor[0], 127, 5); // Red channel should be ~127 (gray)
+ assert.approximately(pixelColor[1], 127, 5); // Green channel should be ~127
+ assert.approximately(pixelColor[2], 127, 5); // Blue channel should be ~127
+ });
+ });
+
+ suite('for loop statements', () => {
+ test('handle simple for loop with known iteration count', () => {
+ myp5.createCanvas(50, 50, myp5.WEBGL);
+
+ const testShader = myp5.baseMaterialShader().modify(() => {
+ myp5.getPixelInputs(inputs => {
+ let color = myp5.float(0.0);
+
+ for (let i = 0; i < 3; i++) {
+ color = color + 0.1;
+ }
+
+ inputs.color = [color, color, color, 1.0];
+ return inputs;
+ });
+ }, { myp5 });
+
+ myp5.noStroke();
+ myp5.shader(testShader);
+ myp5.plane(myp5.width, myp5.height);
+
+ // Should loop 3 times: 0.0 + 0.1 + 0.1 + 0.1 = 0.3
+ const pixelColor = myp5.get(25, 25);
+ assert.approximately(pixelColor[0], 77, 5); // 0.3 * 255 ≈ 77
+ assert.approximately(pixelColor[1], 77, 5);
+ assert.approximately(pixelColor[2], 77, 5);
+ });
+
+ test('handle swizzle assignments in loops', () => {
+ myp5.createCanvas(50, 50, myp5.WEBGL);
+
+ const testShader = myp5.baseMaterialShader().modify(() => {
+ myp5.getPixelInputs(inputs => {
+ let color = [0, 0, 0, 1];
+
+ for (let i = 0; i < 3; i++) {
+ color.rgb += 0.1;
+ }
+
+ inputs.color = color;
+ return inputs;
+ });
+ }, { myp5 });
+
+ myp5.noStroke();
+ myp5.shader(testShader);
+ myp5.plane(myp5.width, myp5.height);
+
+ // Should loop 3 times: 0.0 + 0.1 + 0.1 + 0.1 = 0.3
+ const pixelColor = myp5.get(25, 25);
+ assert.approximately(pixelColor[0], 77, 5); // 0.3 * 255 ≈ 77
+ assert.approximately(pixelColor[1], 77, 5);
+ assert.approximately(pixelColor[2], 77, 5);
+ });
+
+ test('handle for loop with variable as loop bound', () => {
+ myp5.createCanvas(50, 50, myp5.WEBGL);
+
+ const testShader = myp5.baseMaterialShader().modify(() => {
+ const maxIterations = myp5.uniformInt(() => 2);
+
+ myp5.getPixelInputs(inputs => {
+ let result = myp5.float(0.0);
+
+ for (let i = 0; i < maxIterations; i++) {
+ result = result + 0.25;
+ }
+
+ inputs.color = [result, result, result, 1.0];
+ return inputs;
+ });
+ }, { myp5 });
+
+ myp5.noStroke();
+ myp5.shader(testShader);
+ myp5.plane(myp5.width, myp5.height);
+
+ // Should loop 2 times: 0.0 + 0.25 + 0.25 = 0.5
+ const pixelColor = myp5.get(25, 25);
+ assert.approximately(pixelColor[0], 127, 5); // 0.5 * 255 ≈ 127
+ assert.approximately(pixelColor[1], 127, 5);
+ assert.approximately(pixelColor[2], 127, 5);
+ });
+
+ test('handle for loop modifying multiple variables', () => {
+ myp5.createCanvas(50, 50, myp5.WEBGL);
+
+ const testShader = myp5.baseMaterialShader().modify(() => {
+ myp5.getPixelInputs(inputs => {
+ let red = myp5.float(0.0);
+ let green = myp5.float(0.0);
+
+ for (let i = 0; i < 4; i++) {
+ red = red + 0.125; // 4 * 0.125 = 0.5
+ green = green + 0.25; // 4 * 0.25 = 1.0
+ }
+
+ inputs.color = [red, green, 0.0, 1.0];
+ return inputs;
+ });
+ }, { myp5 });
+
+ myp5.noStroke();
+ myp5.shader(testShader);
+ myp5.plane(myp5.width, myp5.height);
+
+ const pixelColor = myp5.get(25, 25);
+ assert.approximately(pixelColor[0], 127, 5); // 0.5 * 255 ≈ 127
+ assert.approximately(pixelColor[1], 255, 5); // 1.0 * 255 = 255
+ assert.approximately(pixelColor[2], 0, 5); // 0.0 * 255 = 0
+ });
+
+ test('handle for loop with conditional inside', () => {
+ myp5.createCanvas(50, 50, myp5.WEBGL);
+
+ const testShader = myp5.baseMaterialShader().modify(() => {
+ myp5.getPixelInputs(inputs => {
+ let sum = myp5.float(0.0);
+
+ for (let i = 0; i < 5; i++) {
+ if (i % 2 === 0) {
+ sum = sum + 0.1; // Add on even iterations: 0, 2, 4
+ }
+ }
+
+ inputs.color = [sum, sum, sum, 1.0];
+ return inputs;
+ });
+ }, { myp5 });
+
+ myp5.noStroke();
+ myp5.shader(testShader);
+ myp5.plane(myp5.width, myp5.height);
+
+ // Should add 0.1 three times (iterations 0, 2, 4): 3 * 0.1 = 0.3
+ const pixelColor = myp5.get(25, 25);
+ assert.approximately(pixelColor[0], 77, 5); // 0.3 * 255 ≈ 77
+ assert.approximately(pixelColor[1], 77, 5);
+ assert.approximately(pixelColor[2], 77, 5);
+ });
+
+ test('handle nested for loops', () => {
+ myp5.createCanvas(50, 50, myp5.WEBGL);
+
+ const testShader = myp5.baseMaterialShader().modify(() => {
+ myp5.getPixelInputs(inputs => {
+ let total = myp5.float(0.0);
+
+ for (let i = 0; i < 2; i++) {
+ for (let j = 0; j < 3; j++) {
+ total = total + 0.05; // 2 * 3 = 6 iterations
+ }
+ }
+
+ inputs.color = [total, total, total, 1.0];
+ return inputs;
+ });
+ }, { myp5 });
+
+ myp5.noStroke();
+ myp5.shader(testShader);
+ myp5.plane(myp5.width, myp5.height);
+
+ // Should run 6 times: 6 * 0.05 = 0.3
+ const pixelColor = myp5.get(25, 25);
+ assert.approximately(pixelColor[0], 77, 5); // 0.3 * 255 ≈ 77
+ assert.approximately(pixelColor[1], 77, 5);
+ assert.approximately(pixelColor[2], 77, 5);
+ });
+
+ test('handle complex nested for loops with multiple phi assignments', () => {
+ myp5.createCanvas(50, 50, myp5.WEBGL);
+
+ const testShader = myp5.baseMaterialShader().modify(() => {
+ myp5.getPixelInputs(inputs => {
+ let outerSum = myp5.float(0.0);
+ let globalCounter = myp5.float(0.0);
+
+ // Outer for loop modifying multiple variables
+ for (let i = 0; i < 2; i++) {
+ let innerSum = myp5.float(0.0);
+ let localCounter = myp5.float(0.0);
+
+ // Inner for loop also modifying multiple variables
+ for (let j = 0; j < 2; j++) {
+ innerSum = innerSum + 0.1;
+ localCounter = localCounter + 1.0;
+ globalCounter = globalCounter + 0.5; // This modifies outer scope
+ }
+
+ // Complex state modification between loops involving all variables
+ innerSum = innerSum * localCounter; // 0.2 * 2.0 = 0.4
+ outerSum = outerSum + innerSum; // Add to outer sum
+ globalCounter = globalCounter * 0.5; // Modify global again
+ }
+
+ // Final result should be: 2 iterations * 0.4 = 0.8 for outerSum
+ // globalCounter: ((0 + 2*0.5)*0.5 + 2*0.5)*0.5 = ((1)*0.5 + 1)*0.5 = 1.5*0.5 = 0.75
+ inputs.color = [outerSum, globalCounter, 0.0, 1.0];
+ return inputs;
+ });
+ }, { myp5 });
+
+ myp5.noStroke();
+ myp5.shader(testShader);
+ myp5.plane(myp5.width, myp5.height);
+
+ const pixelColor = myp5.get(25, 25);
+ assert.approximately(pixelColor[0], 204, 5); // 0.8 * 255 ≈ 204
+ assert.approximately(pixelColor[1], 191, 5); // 0.75 * 255 ≈ 191
+ assert.approximately(pixelColor[2], 0, 5);
+ });
+
+ test('handle nested for loops with state modification between loops', () => {
+ myp5.createCanvas(50, 50, myp5.WEBGL);
+
+ const testShader = myp5.baseMaterialShader().modify(() => {
+ myp5.getPixelInputs(inputs => {
+ let total = myp5.float(0.0);
+
+ // Outer for loop
+ for (let i = 0; i < 2; i++) {
+ let innerSum = myp5.float(0.0);
+
+ // Inner for loop
+ for (let j = 0; j < 3; j++) {
+ innerSum = innerSum + 0.1; // 3 * 0.1 = 0.3 per outer iteration
+ }
+
+ // State modification between inner and outer loop
+ innerSum = innerSum * 0.5; // Multiply by 0.5: 0.3 * 0.5 = 0.15
+ total = total + innerSum; // Add to total: 2 * 0.15 = 0.3
+ }
+
+ inputs.color = [total, total, total, 1.0];
+ return inputs;
+ });
+ }, { myp5 });
+
+ myp5.noStroke();
+ myp5.shader(testShader);
+ myp5.plane(myp5.width, myp5.height);
+
+ // Should be: 2 iterations * (3 * 0.1 * 0.5) = 2 * 0.15 = 0.3
+ const pixelColor = myp5.get(25, 25);
+ assert.approximately(pixelColor[0], 77, 5); // 0.3 * 255 ≈ 77
+ assert.approximately(pixelColor[1], 77, 5);
+ assert.approximately(pixelColor[2], 77, 5);
+ });
+
+ test('handle for loop using loop variable in calculations', () => {
+ myp5.createCanvas(50, 50, myp5.WEBGL);
+
+ const testShader = myp5.baseMaterialShader().modify(() => {
+ myp5.getPixelInputs(inputs => {
+ let sum = myp5.float(0.0);
+
+ for (let i = 1; i <= 3; i++) {
+ sum = sum + (i * 0.1); // 1*0.1 + 2*0.1 + 3*0.1 = 0.6
+ }
+
+ inputs.color = [sum, sum, sum, 1.0];
+ return inputs;
+ });
+ }, { myp5 });
+
+ myp5.noStroke();
+ myp5.shader(testShader);
+ myp5.plane(myp5.width, myp5.height);
+
+ // Should be: 0.1 + 0.2 + 0.3 = 0.6
+ const pixelColor = myp5.get(25, 25);
+ assert.approximately(pixelColor[0], 153, 5); // 0.6 * 255 ≈ 153
+ assert.approximately(pixelColor[1], 153, 5);
+ assert.approximately(pixelColor[2], 153, 5);
+ });
+
+ // Keep one direct API test for completeness
+ test('handle direct StrandsFor API usage', () => {
+ myp5.createCanvas(50, 50, myp5.WEBGL);
+
+ const testShader = myp5.baseMaterialShader().modify(() => {
+ myp5.getPixelInputs(inputs => {
+ let accumulator = myp5.float(0.0);
+
+ const loopResult = myp5.strandsFor(
+ () => 0,
+ (loopVar) => loopVar < 4,
+ (loopVar) => loopVar + 1,
+ (loopVar, vars) => {
+ let newValue = vars.accumulator.copy();
+ newValue = newValue + 0.125;
+ return { accumulator: newValue };
+ },
+ { accumulator: accumulator.copy() },
+ );
+
+ accumulator = loopResult.accumulator;
+ inputs.color = [accumulator, accumulator, accumulator, 1.0];
+ return inputs;
+ });
+ }, { myp5 });
+
+ myp5.noStroke();
+ myp5.shader(testShader);
+ myp5.plane(myp5.width, myp5.height);
+
+ // Should loop 4 times: 4 * 0.125 = 0.5
+ const pixelColor = myp5.get(25, 25);
+ assert.approximately(pixelColor[0], 127, 5); // 0.5 * 255 ≈ 127
+ assert.approximately(pixelColor[1], 127, 5);
+ assert.approximately(pixelColor[2], 127, 5);
+ });
+
+ test('handle for loop with break statement', () => {
+ myp5.createCanvas(50, 50, myp5.WEBGL);
+
+ const testShader = myp5.baseMaterialShader().modify(() => {
+ myp5.getPixelInputs(inputs => {
+ let color = 0;
+ let maxIterations = 5;
+
+ for (let i = 0; i < 100; i++) {
+ if (i >= maxIterations) {
+ break;
+ }
+ color = color + 0.1;
+ }
+
+ inputs.color = [color, color, color, 1.0];
+ return inputs;
+ });
+ }, { myp5 });
+
+ myp5.noStroke();
+ myp5.shader(testShader);
+ myp5.plane(myp5.width, myp5.height);
+
+ // Should break after 5 iterations: 5 * 0.1 = 0.5
+ const pixelColor = myp5.get(25, 25);
+ assert.approximately(pixelColor[0], 127, 5); // 0.5 * 255 ≈ 127
+ });
+ });
+
+ suite('filter shader hooks', () => {
+ test('handle getColor hook with non-struct return type', () => {
+ myp5.createCanvas(50, 50, myp5.WEBGL);
+
+ const testShader = myp5.baseFilterShader().modify(() => {
+ myp5.getColor((inputs, canvasContent) => {
+ // Simple test - just return a constant color
+ return [1.0, 0.5, 0.0, 1.0]; // Orange color
+ });
+ }, { myp5 });
+
+ // Create a simple scene to filter
+ myp5.background(0, 0, 255); // Blue background
+
+ // Apply the filter
+ myp5.filter(testShader);
+
+ // Check that the filter was applied (should be orange)
+ const pixelColor = myp5.get(25, 25);
+ assert.approximately(pixelColor[0], 255, 5); // Red channel should be 255
+ assert.approximately(pixelColor[1], 127, 5); // Green channel should be ~127
+ assert.approximately(pixelColor[2], 0, 5); // Blue channel should be 0
+ });
+
+ test('simple vector multiplication in filter shader', () => {
+ myp5.createCanvas(50, 50, myp5.WEBGL);
+
+ const testShader = myp5.baseFilterShader().modify(() => {
+ myp5.getColor((inputs, canvasContent) => {
+ // Test simple scalar * vector operation
+ const scalar = 0.5;
+ const vector = [1, 2];
+ const result = scalar * vector;
+ return [result.x, result.y, 0, 1];
+ });
+ }, { myp5 });
+ });
+
+ test('handle complex filter shader with for loop and vector operations', () => {
+ myp5.createCanvas(50, 50, myp5.WEBGL);
+
+ const testShader = myp5.baseFilterShader().modify(() => {
+ const r = myp5.uniformFloat(() => 3); // Small value for testing
+ myp5.getColor((inputs, canvasContent) => {
+ let sum = [0, 0, 0, 0];
+ let samples = 1;
+
+ for (let i = 0; i < r; i++) {
+ samples++;
+ sum += myp5.texture(canvasContent, inputs.texCoord + (i / r) * [
+ myp5.sin(4 * myp5.PI * i / r),
+ myp5.cos(4 * myp5.PI * i / r)
+ ]);
+ }
+
+ return sum / samples;
+ });
+ }, { myp5 });
+
+ // Create a simple scene to filter
+ myp5.background(255, 0, 0); // Red background
+
+ // Apply the filter
+ myp5.filter(testShader);
+
+ // The result should be some variation of the red background
+ const pixelColor = myp5.get(25, 25);
+ // Just verify it ran without crashing - exact color will depend on sampling
+ assert.isNumber(pixelColor[0]);
+ assert.isNumber(pixelColor[1]);
+ assert.isNumber(pixelColor[2]);
+ });
+ });
});
});