Skip to content

Commit 63ed355

Browse files
committed
Add tests + get varyings working
1 parent 8f23e69 commit 63ed355

7 files changed

Lines changed: 243 additions & 10 deletions

File tree

src/strands/p5.strands.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ function strands(p5, fn) {
2525
ctx.vertexDeclarations = new Set();
2626
ctx.fragmentDeclarations = new Set();
2727
ctx.hooks = [];
28+
ctx.globalAssignments = [];
2829
ctx.backend = backend;
2930
ctx.active = active;
3031
ctx.previousFES = p5.disableFriendlyErrors;
@@ -42,6 +43,7 @@ function strands(p5, fn) {
4243
ctx.vertexDeclarations = new Set();
4344
ctx.fragmentDeclarations = new Set();
4445
ctx.hooks = [];
46+
ctx.globalAssignments = [];
4547
ctx.active = false;
4648
p5.disableFriendlyErrors = ctx.previousFES;
4749
for (const key in ctx.windowOverrides) {

src/strands/strands_api.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,10 +149,17 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) {
149149
strandsContext.uniforms.push({ name, typeInfo, defaultValue });
150150
return createStrandsNode(id, dimension, strandsContext);
151151
};
152+
fn[`varying${pascalTypeName}`] = function(name) {
153+
const { id, dimension } = build.variableNode(strandsContext, typeInfo, name);
154+
strandsContext.vertexDeclarations.add(`out ${typeInfo.fnName} ${name};`);
155+
strandsContext.fragmentDeclarations.add(`in ${typeInfo.fnName} ${name};`);
156+
return createStrandsNode(id, dimension, strandsContext);
157+
};
152158
if (pascalTypeName.startsWith('Vec')) {
153159
// For compatibility, also alias uniformVec2 as uniformVector2, what we initially
154160
// documented these as
155161
fn[`uniform${pascalTypeName.replace('Vec', 'Vector')}`] = fn[`uniform${pascalTypeName}`];
162+
fn[`varying${pascalTypeName.replace('Vec', 'Vector')}`] = fn[`varying${pascalTypeName}`];
156163
}
157164
const originalp5Fn = fn[typeInfo.fnName];
158165
fn[typeInfo.fnName] = function(...args) {

src/strands/strands_codegen.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,26 @@ export function generateShaderCode(strandsContext) {
2828
tempNames: {},
2929
declarations: [],
3030
nextTempID: 0,
31+
visitedNodes: new Set(),
3132
};
3233

3334
const blocks = sortCFG(cfg.outgoingEdges, entryBlockID);
3435
for (const blockID of blocks) {
3536
backend.generateBlock(blockID, strandsContext, generationContext);
3637
}
3738

39+
// Process any unvisited global assignments to ensure side effects are generated
40+
for (const assignmentNodeID of strandsContext.globalAssignments) {
41+
if (!generationContext.visitedNodes.has(assignmentNodeID)) {
42+
// This assignment hasn't been visited yet, so we need to generate it
43+
backend.generateAssignment(generationContext, strandsContext.dag, assignmentNodeID);
44+
generationContext.visitedNodes.add(assignmentNodeID);
45+
}
46+
}
47+
48+
// Reset global assignments for next hook
49+
strandsContext.globalAssignments = [];
50+
3851
const firstLine = backend.hookEntry(hookType);
3952
let returnType = hookType.returnType.properties
4053
? structType(hookType.returnType)

src/strands/strands_glslBackend.js

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ const cfgHandlers = {
4040
}
4141
if (nodeType === NodeType.ASSIGNMENT) {
4242
glslBackend.generateAssignment(generationContext, dag, nodeID);
43+
generationContext.visitedNodes.add(nodeID);
4344
}
4445
}
4546
},
@@ -127,6 +128,7 @@ const cfgHandlers = {
127128
}
128129
if (node.nodeType === NodeType.ASSIGNMENT) {
129130
glslBackend.generateAssignment(generationContext, dag, nodeID);
131+
generationContext.visitedNodes.add(nodeID);
130132
}
131133
}
132134

@@ -195,16 +197,25 @@ export const glslBackend = {
195197
},
196198
generateAssignment(generationContext, dag, nodeID) {
197199
const node = getNodeDataFromID(dag, nodeID);
198-
// dependsOn[0] = phiNodeID, dependsOn[1] = sourceNodeID
199-
const phiNodeID = node.dependsOn[0];
200+
// dependsOn[0] = targetNodeID, dependsOn[1] = sourceNodeID
201+
const targetNodeID = node.dependsOn[0];
200202
const sourceNodeID = node.dependsOn[1];
201-
const phiTempName = generationContext.tempNames[phiNodeID];
203+
const targetNode = getNodeDataFromID(dag, targetNodeID);
204+
205+
// Check if this is a varying variable assignment (target has identifier)
206+
let targetName;
207+
if (targetNode && targetNode.identifier) {
208+
targetName = targetNode.identifier; // Use variable identifier directly
209+
} else {
210+
targetName = generationContext.tempNames[targetNodeID]; // Use temp name for phi nodes
211+
}
212+
202213
const sourceExpr = this.generateExpression(generationContext, dag, sourceNodeID);
203214
const semicolon = generationContext.suppressSemicolon ? '' : ';';
204-
205-
// Skip assignment if target and source are the same variable
206-
if (phiTempName && sourceExpr && phiTempName !== sourceExpr) {
207-
generationContext.write(`${phiTempName} = ${sourceExpr}${semicolon}`);
215+
216+
// Generate assignment if we have both target and source
217+
if (targetName && sourceExpr && targetName !== sourceExpr) {
218+
generationContext.write(`${targetName} = ${sourceExpr}${semicolon}`);
208219
}
209220
},
210221
generateDeclaration(generationContext, dag, nodeID) {
@@ -311,7 +322,11 @@ export const glslBackend = {
311322
}
312323
case NodeType.PHI:
313324
// Phi nodes represent conditional merging of values
314-
// They should already have been declared as temporary variables
325+
// If this phi node has an identifier (like varying variables), use that
326+
if (node.identifier) {
327+
return node.identifier;
328+
}
329+
// Otherwise, they should have been declared as temporary variables
315330
// and assigned in the appropriate branches
316331
if (generationContext.tempNames?.[nodeID]) {
317332
return generationContext.tempNames[nodeID];

src/strands/strands_node.js

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,92 @@
1-
import { swizzleTrap } from './ir_builders';
1+
import { swizzleTrap, primitiveConstructorNode, variableNode } from './ir_builders';
2+
import { BaseType, NodeType } from './ir_types';
3+
import { getNodeDataFromID, createNodeData, getOrCreateNode } from './ir_dag';
4+
import { recordInBasicBlock } from './ir_cfg';
25
export class StrandsNode {
36
constructor(id, dimension, strandsContext) {
47
this.id = id;
58
this.strandsContext = strandsContext;
69
this.dimension = dimension;
10+
11+
// Store original identifier for varying variables
12+
const dag = this.strandsContext.dag;
13+
const nodeData = getNodeDataFromID(dag, this.id);
14+
if (nodeData && nodeData.identifier) {
15+
this._originalIdentifier = nodeData.identifier;
16+
this._originalBaseType = nodeData.baseType;
17+
this._originalDimension = nodeData.dimension;
18+
}
719
}
820
copy() {
921
return createStrandsNode(this.id, this.dimension, this.strandsContext);
1022
}
23+
bridge(value) {
24+
const { dag, cfg } = this.strandsContext;
25+
const orig = getNodeDataFromID(dag, this.id);
26+
const baseType = orig?.baseType ?? BaseType.FLOAT;
27+
28+
let newValueID;
29+
if (value instanceof StrandsNode) {
30+
newValueID = value.id;
31+
} else {
32+
const newVal = primitiveConstructorNode(
33+
this.strandsContext,
34+
{ baseType, dimension: this.dimension },
35+
value
36+
);
37+
newValueID = newVal.id;
38+
}
39+
40+
// For varying variables, we need both assignment generation AND a way to reference by identifier
41+
if (this._originalIdentifier) {
42+
// Create a variable node for the target (the varying variable)
43+
const { id: targetVarID } = variableNode(
44+
this.strandsContext,
45+
{ baseType: this._originalBaseType, dimension: this._originalDimension },
46+
this._originalIdentifier
47+
);
48+
49+
// Create assignment node for GLSL generation
50+
const assignmentNode = createNodeData({
51+
nodeType: NodeType.ASSIGNMENT,
52+
dependsOn: [targetVarID, newValueID],
53+
phiBlocks: []
54+
});
55+
const assignmentID = getOrCreateNode(dag, assignmentNode);
56+
recordInBasicBlock(cfg, cfg.currentBlock, assignmentID);
57+
58+
// Track for global assignments processing
59+
this.strandsContext.globalAssignments.push(assignmentID);
60+
61+
// Simply update this node to be a variable node with the identifier
62+
// This ensures it always generates the variable name in expressions
63+
const variableNodeData = createNodeData({
64+
nodeType: NodeType.VARIABLE,
65+
baseType: this._originalBaseType,
66+
dimension: this._originalDimension,
67+
identifier: this._originalIdentifier
68+
});
69+
const variableID = getOrCreateNode(dag, variableNodeData);
70+
71+
this.id = variableID; // Point to the variable node for expression generation
72+
} else {
73+
this.id = newValueID; // For non-varying variables, just update to new value
74+
}
75+
76+
return this;
77+
}
78+
getValue() {
79+
if (this._originalIdentifier) {
80+
const { id, dimension } = variableNode(
81+
this.strandsContext,
82+
{ baseType: this._originalBaseType, dimension: this._originalDimension },
83+
this._originalIdentifier
84+
);
85+
return createStrandsNode(id, dimension, this.strandsContext);
86+
}
87+
88+
return this;
89+
}
1190
}
1291
export function createStrandsNode(id, dimension, strandsContext, onRebind) {
1392
return new Proxy(

src/strands/strands_transpiler.js

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,21 @@ function nodeIsUniform(ancestor) {
3838
)
3939
);
4040
}
41+
42+
function nodeIsVarying(node) {
43+
return node?.type === 'CallExpression'
44+
&& (
45+
(
46+
// Global mode
47+
node.callee?.type === 'Identifier' &&
48+
node.callee?.name.startsWith('varying')
49+
) || (
50+
// Instance mode
51+
node.callee?.type === 'MemberExpression' &&
52+
node.callee?.property.name.startsWith('varying')
53+
)
54+
);
55+
}
4156
const ASTCallbacks = {
4257
UnaryExpression(node, _state, ancestors) {
4358
if (ancestors.some(nodeIsUniform)) { return; }
@@ -101,7 +116,7 @@ const ASTCallbacks = {
101116
}
102117
node.init.arguments.unshift(uniformNameLiteral);
103118
}
104-
if (node.init.callee && node.init.callee.name?.startsWith('varying')) {
119+
if (nodeIsVarying(node.init)) {
105120
const varyingNameLiteral = {
106121
type: 'Literal',
107122
value: node.id.name

test/unit/webgl/p5.Shader.js

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1067,6 +1067,108 @@ suite('p5.Shader', function() {
10671067
});
10681068
});
10691069

1070+
suite('passing data between shaders', () => {
1071+
test('handle passing a value from a vertex hook to a fragment hook', () => {
1072+
myp5.createCanvas(50, 50, myp5.WEBGL);
1073+
1074+
const testShader = myp5.baseMaterialShader().modify(() => {
1075+
let worldPos = myp5.varyingVec3();
1076+
myp5.getWorldInputs((inputs) => {
1077+
worldPos = inputs.position.xyz;
1078+
return inputs;
1079+
});
1080+
myp5.getFinalColor((c) => {
1081+
return [myp5.abs(worldPos / 25), 1];
1082+
});
1083+
}, { myp5 });
1084+
1085+
myp5.background(0, 0, 255); // Make the background blue to tell it apart
1086+
myp5.noStroke();
1087+
myp5.shader(testShader);
1088+
myp5.plane(myp5.width, myp5.height);
1089+
1090+
// The middle should have position 0,0 which translates to black
1091+
const midColor = myp5.get(25, 25);
1092+
assert.approximately(midColor[0], 0, 5);
1093+
assert.approximately(midColor[1], 0, 5);
1094+
assert.approximately(midColor[2], 0, 5);
1095+
1096+
// The corner should have position 1,1 which translates to yellow
1097+
const cornerColor = myp5.get(0, 0);
1098+
assert.approximately(cornerColor[0], 255, 5);
1099+
assert.approximately(cornerColor[1], 255, 5);
1100+
assert.approximately(cornerColor[2], 0, 5);
1101+
});
1102+
1103+
test.only('handle passing a value from a vertex hook to a fragment hook', () => {
1104+
myp5.createCanvas(50, 50, myp5.WEBGL);
1105+
1106+
const testShader = myp5.baseMaterialShader().modify(() => {
1107+
let worldPos = myp5.varyingVec3();
1108+
myp5.getWorldInputs((inputs) => {
1109+
worldPos.xyz = inputs.position.xyz;
1110+
return inputs;
1111+
});
1112+
myp5.getFinalColor((c) => {
1113+
return [myp5.abs(worldPos / 25), 1];
1114+
});
1115+
}, { myp5 });
1116+
1117+
console.log('VERTEX SHADER OUTPUT:');
1118+
console.log(testShader.vertSrc());
1119+
1120+
myp5.background(0, 0, 255); // Make the background blue to tell it apart
1121+
myp5.noStroke();
1122+
myp5.shader(testShader);
1123+
myp5.plane(myp5.width, myp5.height);
1124+
1125+
// The middle should have position 0,0 which translates to black
1126+
const midColor = myp5.get(25, 25);
1127+
assert.approximately(midColor[0], 0, 5);
1128+
assert.approximately(midColor[1], 0, 5);
1129+
assert.approximately(midColor[2], 0, 5);
1130+
1131+
// The corner should have position 1,1 which translates to yellow
1132+
const cornerColor = myp5.get(0, 0);
1133+
assert.approximately(cornerColor[0], 255, 5);
1134+
assert.approximately(cornerColor[1], 255, 5);
1135+
assert.approximately(cornerColor[2], 0, 5);
1136+
});
1137+
1138+
test('handle passing a value from a vertex hook to a fragment hook as part of hook output', () => {
1139+
myp5.createCanvas(50, 50, myp5.WEBGL);
1140+
1141+
const testShader = myp5.baseMaterialShader().modify(() => {
1142+
let worldPos = myp5.varyingVec3();
1143+
myp5.getWorldInputs((inputs) => {
1144+
worldPos = inputs.position.xyz;
1145+
inputs.position.xyz = worldPos + [25, 25, 0];
1146+
return inputs;
1147+
});
1148+
myp5.getFinalColor((c) => {
1149+
return [myp5.abs(worldPos / 25), 1];
1150+
});
1151+
}, { myp5 });
1152+
1153+
myp5.background(0, 0, 255); // Make the background blue to tell it apart
1154+
myp5.noStroke();
1155+
myp5.shader(testShader);
1156+
myp5.plane(myp5.width, myp5.height);
1157+
1158+
// The middle (shifted +25,25) should have position 0,0 which translates to black
1159+
const midColor = myp5.get(49, 49);
1160+
assert.approximately(midColor[0], 0, 5);
1161+
assert.approximately(midColor[1], 0, 5);
1162+
assert.approximately(midColor[2], 0, 5);
1163+
1164+
// The corner (shifted +25,25) should have position 1,1 which translates to yellow
1165+
const cornerColor = myp5.get(25, 25);
1166+
assert.approximately(cornerColor[0], 255, 5);
1167+
assert.approximately(cornerColor[1], 255, 5);
1168+
assert.approximately(cornerColor[2], 0, 5);
1169+
});
1170+
});
1171+
10701172
suite('filter shader hooks', () => {
10711173
test('handle getColor hook with non-struct return type', () => {
10721174
myp5.createCanvas(50, 50, myp5.WEBGL);

0 commit comments

Comments
 (0)