From 23ff7e6b7af635a50e67a200d81a4aaf68aaaedf Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Tue, 24 Jun 2025 16:47:20 +0100 Subject: [PATCH 01/69] syntax/ remove unneccessary --- src/webgl/ShaderGenerator.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/webgl/ShaderGenerator.js b/src/webgl/ShaderGenerator.js index d029ed44ef..a4db0296fc 100644 --- a/src/webgl/ShaderGenerator.js +++ b/src/webgl/ShaderGenerator.js @@ -1094,13 +1094,12 @@ function shadergenerator(p5, fn) { GLOBAL_SHADER = this; this.userCallback = userCallback; this.srcLocations = srcLocations; - this.cleanup = () => {}; this.generateHookOverrides(originalShader); this.output = { vertexDeclarations: new Set(), fragmentDeclarations: new Set(), uniforms: {}, - } + }; this.uniformNodes = []; this.resetGLSLContext(); this.isGenerating = false; From 1511ffbe238ffc5d8f8a02dad60fb5876a75ae2b Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Fri, 27 Jun 2025 11:17:47 +0100 Subject: [PATCH 02/69] blocking out new modular strands structure --- preview/global/sketch.js | 116 +------------- src/strands/code_transpiler.js | 222 ++++++++++++++++++++++++++ src/strands/control_flow_graph.js | 0 src/strands/directed_acyclic_graph.js | 85 ++++++++++ src/strands/p5.StrandsNode.js | 40 +++++ src/strands/p5.strands.js | 95 +++++++++++ src/strands/strands_FES.js | 4 + src/strands/utils.js | 109 +++++++++++++ src/webgl/index.js | 2 + 9 files changed, 559 insertions(+), 114 deletions(-) create mode 100644 src/strands/code_transpiler.js create mode 100644 src/strands/control_flow_graph.js create mode 100644 src/strands/directed_acyclic_graph.js create mode 100644 src/strands/p5.StrandsNode.js create mode 100644 src/strands/p5.strands.js create mode 100644 src/strands/strands_FES.js create mode 100644 src/strands/utils.js diff --git a/preview/global/sketch.js b/preview/global/sketch.js index b0cd6c8045..c52148e7d3 100644 --- a/preview/global/sketch.js +++ b/preview/global/sketch.js @@ -1,124 +1,12 @@ p5.disableFriendlyErrors = true; -function windowResized() { - resizeCanvas(windowWidth, windowHeight); -} - -let starShader; -let starStrokeShader; -let stars; -let originalFrameBuffer; -let pixellizeShader; -let fresnelShader; -let bloomShader; - -function fresnelShaderCallback() { - const fresnelPower = uniformFloat(2); - const fresnelBias = uniformFloat(-0.1); - const fresnelScale = uniformFloat(2); - getCameraInputs((inputs) => { - let n = normalize(inputs.normal); - let v = normalize(-inputs.position); - let base = 1.0 - dot(n, v); - let fresnel = fresnelScale * pow(base, fresnelPower) + fresnelBias; - let col = mix([0, 0, 0], [1, .5, .7], fresnel); - inputs.color = [col, 1]; - return inputs; - }); -} - -function starShaderCallback() { - const time = uniformFloat(() => millis()); - const skyRadius = uniformFloat(1000); - - function rand2(st) { - return fract(sin(dot(st, [12.9898, 78.233])) * 43758.5453123); - } - - function semiSphere() { - let id = instanceID(); - let theta = rand2([id, 0.1234]) * TWO_PI; - let phi = rand2([id, 3.321]) * PI+time/10000; - let r = skyRadius; - r *= 1.5 * sin(phi); - let x = r * sin(phi) * cos(theta); - let y = r * 1.5 * cos(phi); - let z = r * sin(phi) * sin(theta); - return [x, y, z]; - } - - getWorldInputs((inputs) => { - inputs.position += semiSphere(); - return inputs; - }); - - getObjectInputs((inputs) => { - let scale = 1 + 0.1 * sin(time * 0.002 + instanceID()); - inputs.position *= scale; - return inputs; - }); -} - -function pixellizeShaderCallback() { - const pixelSize = uniformFloat(()=> width*.75); - getColor((input, canvasContent) => { - let coord = input.texCoord; - coord = floor(coord * pixelSize) / pixelSize; - let col = texture(canvasContent, coord); - return col; - }); -} - function bloomShaderCallback() { - const preBlur = uniformTexture(() => originalFrameBuffer); - getColor((input, canvasContent) => { - const blurredCol = texture(canvasContent, input.texCoord); - const originalCol = texture(preBlur, input.texCoord); - const brightPass = max(originalCol, 0.3) * 1.5; - const bloom = originalCol + blurredCol * brightPass; - return bloom; - }); + createFloat(1.0); } async function setup(){ - createCanvas(windowWidth, windowHeight, WEBGL); - stars = buildGeometry(() => sphere(30, 4, 2)) - originalFrameBuffer = createFramebuffer(); - - starShader = baseMaterialShader().modify(starShaderCallback); - starStrokeShader = baseStrokeShader().modify(starShaderCallback) - fresnelShader = baseColorShader().modify(fresnelShaderCallback); - bloomShader = baseFilterShader().modify(bloomShaderCallback); - pixellizeShader = baseFilterShader().modify(pixellizeShaderCallback); + bloomShader = baseFilterShader().newModify(bloomShaderCallback); } function draw(){ - originalFrameBuffer.begin(); - background(0); - orbitControl(); - - push() - strokeWeight(4) - stroke(255,0,0) - rotateX(PI/2 + millis() * 0.0005); - fill(255,100, 150) - strokeShader(starStrokeShader) - shader(starShader); - model(stars, 2000); - pop() - - push() - shader(fresnelShader) - noStroke() - sphere(500); - pop() - filter(pixellizeShader); - - originalFrameBuffer.end(); - - imageMode(CENTER) - image(originalFrameBuffer, 0, 0) - - filter(BLUR, 20) - filter(bloomShader); } diff --git a/src/strands/code_transpiler.js b/src/strands/code_transpiler.js new file mode 100644 index 0000000000..6692c574a0 --- /dev/null +++ b/src/strands/code_transpiler.js @@ -0,0 +1,222 @@ +import { parse } from 'acorn'; +import { ancestor } from 'acorn-walk'; +import escodegen from 'escodegen'; + +import { OperatorTable } from './utils'; + +// TODO: Switch this to operator table, cleanup whole file too + +function replaceBinaryOperator(codeSource) { + switch (codeSource) { + case '+': return 'add'; + case '-': return 'sub'; + case '*': return 'mult'; + case '/': return 'div'; + case '%': return 'mod'; + case '==': + case '===': return 'equalTo'; + case '>': return 'greaterThan'; + case '>=': return 'greaterThanEqualTo'; + case '<': return 'lessThan'; + case '&&': return 'and'; + case '||': return 'or'; + } +} + +function ancestorIsUniform(ancestor) { + return ancestor.type === 'CallExpression' + && ancestor.callee?.type === 'Identifier' + && ancestor.callee?.name.startsWith('uniform'); +} + +const ASTCallbacks = { + UnaryExpression(node, _state, _ancestors) { + if (_ancestors.some(ancestorIsUniform)) { return; } + + const signNode = { + type: 'Literal', + value: node.operator, + } + + const standardReplacement = (node) => { + node.type = 'CallExpression' + node.callee = { + type: 'Identifier', + name: 'unaryNode', + } + node.arguments = [node.argument, signNode] + } + + if (node.type === 'MemberExpression') { + const property = node.argument.property.name; + const swizzleSets = [ + ['x', 'y', 'z', 'w'], + ['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 = { + type: 'CallExpression', + callee: { + type: 'Identifier', + name: 'unaryNode' + }, + arguments: [node.argument.object, signNode], + }; + node.property = { + type: 'Identifier', + name: property + }; + } else { + standardReplacement(node); + } + } else { + standardReplacement(node); + } + delete node.argument; + delete node.operator; + }, + VariableDeclarator(node, _state, _ancestors) { + if (node.init.callee && node.init.callee.name?.startsWith('uniform')) { + const uniformNameLiteral = { + type: 'Literal', + value: node.id.name + } + node.init.arguments.unshift(uniformNameLiteral); + } + if (node.init.callee && node.init.callee.name?.startsWith('varying')) { + const varyingNameLiteral = { + type: 'Literal', + value: node.id.name + } + node.init.arguments.unshift(varyingNameLiteral); + _state.varyings[node.id.name] = varyingNameLiteral; + } + }, + Identifier(node, _state, _ancestors) { + if (_state.varyings[node.name] + && !_ancestors.some(a => a.type === 'AssignmentExpression' && a.left === node)) { + node.type = 'ExpressionStatement'; + node.expression = { + type: 'CallExpression', + callee: { + type: 'MemberExpression', + object: { + type: 'Identifier', + name: node.name + }, + property: { + type: 'Identifier', + name: 'getValue' + }, + }, + arguments: [], + } + } + }, + // The callbacks for AssignmentExpression and BinaryExpression handle + // operator overloading including +=, *= assignment expressions + ArrayExpression(node, _state, _ancestors) { + const original = JSON.parse(JSON.stringify(node)); + node.type = 'CallExpression'; + node.callee = { + type: 'Identifier', + name: 'dynamicNode', + }; + node.arguments = [original]; + }, + AssignmentExpression(node, _state, _ancestors) { + if (node.operator !== '=') { + const methodName = replaceBinaryOperator(node.operator.replace('=','')); + const rightReplacementNode = { + type: 'CallExpression', + callee: { + type: 'MemberExpression', + object: node.left, + property: { + type: 'Identifier', + name: methodName, + }, + }, + arguments: [node.right] + } + node.operator = '='; + node.right = rightReplacementNode; + } + if (_state.varyings[node.left.name]) { + node.type = 'ExpressionStatement'; + node.expression = { + type: 'CallExpression', + callee: { + type: 'MemberExpression', + object: { + type: 'Identifier', + name: node.left.name + }, + property: { + type: 'Identifier', + name: 'bridge', + } + }, + arguments: [node.right], + } + } + }, + BinaryExpression(node, _state, _ancestors) { + // Don't convert uniform default values to node methods, as + // they should be evaluated at runtime, not compiled. + if (_ancestors.some(ancestorIsUniform)) { return; } + // If the left hand side of an expression is one of these types, + // we should construct a node from it. + const unsafeTypes = ['Literal', 'ArrayExpression', 'Identifier']; + if (unsafeTypes.includes(node.left.type)) { + const leftReplacementNode = { + type: 'CallExpression', + callee: { + type: 'Identifier', + name: 'dynamicNode', + }, + arguments: [node.left] + } + node.left = leftReplacementNode; + } + // Replace the binary operator with a call expression + // in other words a call to BaseNode.mult(), .div() etc. + node.type = 'CallExpression'; + node.callee = { + type: 'MemberExpression', + object: node.left, + property: { + type: 'Identifier', + name: replaceBinaryOperator(node.operator), + }, + }; + node.arguments = [node.right]; + }, + } + + export function transpileStrandsToJS(sourceString, srcLocations) { + const ast = parse(sourceString, { + ecmaVersion: 2021, + locations: srcLocations + }); + ancestor(ast, ASTCallbacks, undefined, { varyings: {} }); + const transpiledSource = escodegen.generate(ast); + const strandsCallback = new Function( + transpiledSource + .slice( + transpiledSource.indexOf('{') + 1, + transpiledSource.lastIndexOf('}') + ).replaceAll(';', '') + ); + + console.log(transpiledSource); + return strandsCallback; + } + \ No newline at end of file diff --git a/src/strands/control_flow_graph.js b/src/strands/control_flow_graph.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/strands/directed_acyclic_graph.js b/src/strands/directed_acyclic_graph.js new file mode 100644 index 0000000000..6da09d4921 --- /dev/null +++ b/src/strands/directed_acyclic_graph.js @@ -0,0 +1,85 @@ +import { NodeTypeRequiredFields, NodeTypeName } from './utils' +import * as strandsFES from './strands_FES' + +// Properties of the Directed Acyclic Graph and its nodes +const graphProperties = [ + 'nodeTypes', + 'dataTypes', + 'opCodes', + 'values', + 'identifiers', + // sparse adjancey list for dependencies (indegree) + 'dependsOnStartIndex', + 'dependsOnCount', + 'dependsOnList', +]; + +const nodeProperties = [ + 'nodeType', + 'dataType', + 'opCodes', + 'value', + 'identifier', + 'dependsOn' +]; + +// Public functions for for strands runtime +export function createGraph() { + const graph = { + _nextID: 0, + _nodeCache: new Map(), + } + for (const prop of graphProperties) { + graph[prop] = []; + } + return graph; +} + + +export function getOrCreateNode(graph, node) { + const result = getNode(graph, node); + if (!result){ + return createNode(graph, node) + } else { + return result; + } +} + +export function createNodeData(data = {}) { + const node = {}; + for (const key of nodeProperties) { + node[key] = data[key] ?? NaN; + } + validateNode(node); + return node; +} + +// Private functions to this file +function getNodeKey(node) { + +} + +function validateNode(node){ + const requiredFields = NodeTypeRequiredFields[node.NodeType]; + const missingFields = []; + for (const field of requiredFields) { + if (node[field] === NaN) { + missingFields.push(field); + } + } + if (missingFields.length > 0) { + strandsFES.internalError(`[p5.strands internal error]: Missing fields ${missingFields.join(', ')} for a node type ${NodeTypeName(node.nodeType)}`); + } +} + +function getNode(graph, node) { + if (graph) + + if (!node) { + return null; + } +} + +function createNode(graph, nodeData) { + +} \ No newline at end of file diff --git a/src/strands/p5.StrandsNode.js b/src/strands/p5.StrandsNode.js new file mode 100644 index 0000000000..ffddc7e83e --- /dev/null +++ b/src/strands/p5.StrandsNode.js @@ -0,0 +1,40 @@ +////////////////////////////////////////////// +// User API +////////////////////////////////////////////// + +import { OperatorTable } from './utils' + +export class StrandsNode { + constructor(id) { + this.id = id; + } +} + +export function createStrandsAPI(strands, fn) { + // Attach operators to StrandsNode: + for (const { name, symbol, arity } of OperatorTable) { + if (arity === 'binary') { + StrandsNode.prototype[name] = function (rightNode) { + const id = strands.createBinaryExpressionNode(this, rightNode, symbol); + return new StrandsNode(id); + }; + } + if (arity === 'unary') { + StrandsNode.prototype[name] = function () { + const id = strands.createUnaryExpressionNode(this, symbol); + return new StrandsNode(id); + }; + } + } + + // Attach p5 Globals + fn.uniformFloat = function(name, value) { + const id = strands.createVariableNode(DataType.FLOAT, name); + return new StrandsNode(id); + }, + + fn.createFloat = function(value) { + const id = strands.createLiteralNode(DataType.FLOAT, value); + return new StrandsNode(id); + } +} \ No newline at end of file diff --git a/src/strands/p5.strands.js b/src/strands/p5.strands.js new file mode 100644 index 0000000000..0bdfe7bda5 --- /dev/null +++ b/src/strands/p5.strands.js @@ -0,0 +1,95 @@ +/** +* @module 3D +* @submodule strands +* @for p5 +* @requires core +*/ + +import { transpileStrandsToJS } from './code_transpiler'; +import { DataType, NodeType, OpCode, SymbolToOpCode, OpCodeToSymbol, OpCodeArgs } from './utils'; + +import { createStrandsAPI } from './p5.StrandsNode' +import * as DAG from './directed_acyclic_graph'; +import * as CFG from './control_flow_graph' +import { create } from '@davepagurek/bezier-path'; + +function strands(p5, fn) { + + ////////////////////////////////////////////// + // Global Runtime + ////////////////////////////////////////////// + + class StrandsRuntime { + constructor() { + this.reset(); + } + + reset() { + this._scopeStack = []; + this._allScopes = new Map(); + } + + createBinaryExpressionNode(left, right, operatorSymbol) { + const activeGraph = this._currentScope().graph; + const opCode = SymbolToOpCode.get(operatorSymbol); + + const dataType = DataType.FLOAT; // lookUpBinaryOperatorResult(); + return activeGraph._getOrCreateNode(NodeType.OPERATION, dataType, opCode, null, null, [left, right]); + } + + createLiteralNode(dataType, value) { + const activeGraph = this._currentScope().graph; + return activeGraph._getOrCreateNode(NodeType.LITERAL, dataType, value, null, null, null); + } + } + + ////////////////////////////////////////////// + // Entry Point + ////////////////////////////////////////////// + + const strands = new StrandsRuntime(); + const API = createStrandsAPI(strands, fn); + + const oldModify = p5.Shader.prototype.modify + + for (const [fnName, fnBody] of Object.entries(userFunctions)) { + fn[fnName] = fnBody; + } + + p5.Shader.prototype.newModify = function(shaderModifier, options = { parser: true, srcLocations: false }) { + if (shaderModifier instanceof Function) { + + // 1. Transpile from strands DSL to JS + let strandsCallback; + if (options.parser) { + strandsCallback = transpileStrandsToJS(shaderModifier.toString(), options.srcLocations); + } else { + strandsCallback = shaderModifier; + } + + // 2. Build the IR from JavaScript API + strands.enterScope('GLOBAL'); + strandsCallback(); + strands.exitScope('GLOBAL'); + + + // 3. Generate shader code hooks object from the IR + // ....... + + // Call modify with the generated hooks object + // return oldModify.call(this, generatedModifyArgument); + + // Reset the strands runtime context + // strands.reset(); + } + else { + return oldModify.call(this, shaderModifier) + } + } +} + +export default strands; + +if (typeof p5 !== 'undefined') { + p5.registerAddon(strands) +} diff --git a/src/strands/strands_FES.js b/src/strands/strands_FES.js new file mode 100644 index 0000000000..695b220e6a --- /dev/null +++ b/src/strands/strands_FES.js @@ -0,0 +1,4 @@ +export function internalError(message) { + const prefixedMessage = `[p5.strands internal error]: ${message}` + throw new Error(prefixedMessage); +} \ No newline at end of file diff --git a/src/strands/utils.js b/src/strands/utils.js new file mode 100644 index 0000000000..29a3e1d1ab --- /dev/null +++ b/src/strands/utils.js @@ -0,0 +1,109 @@ +///////////////////// +// Enums for nodes // +///////////////////// + +export const NodeType = { + // Internal Nodes: + OPERATION: 0, + // Leaf Nodes + LITERAL: 1, + VARIABLE: 2, + CONSTANT: 3, +}; + +export const NodeTypeRequiredFields = { + [NodeType.OPERATION]: ['opCodes', 'dependsOn'], + [NodeType.LITERAL]: ['values'], + [NodeType.VARIABLE]: ['identifiers'], + [NodeType.CONSTANT]: ['values'], +}; + +export const NodeTypeName = Object.fromEntries( + Object.entries(NodeType).map(([key, val]) => [val, key]) +); + +export const DataType = { + FLOAT: 0, + VEC2: 1, + VEC3: 2, + VEC4: 3, + + INT: 100, + IVEC2: 101, + IVEC3: 102, + IVEC4: 103, + + BOOL: 200, + BVEC2: 201, + BVEC3: 202, + BVEC4: 203, + + MAT2X2: 300, + MAT3X3: 301, + MAT4X4: 302, +} + +export const OpCode = { + Binary: { + ADD: 0, + SUBTRACT: 1, + MULTIPLY: 2, + DIVIDE: 3, + MODULO: 4, + EQUAL: 5, + NOT_EQUAL: 6, + GREATER_THAN: 7, + GREATER_EQUAL: 8, + LESS_THAN: 9, + LESS_EQUAL: 10, + LOGICAL_AND: 11, + LOGICAL_OR: 12, + MEMBER_ACCESS: 13, + }, + Unary: { + LOGICAL_NOT: 100, + NEGATE: 101, + PLUS: 102, + SWIZZLE: 103, + }, + Nary: { + FUNCTION_CALL: 200, + }, + ControlFlow: { + RETURN: 300, + JUMP: 301, + BRANCH_IF_FALSE: 302, + DISCARD: 303, + } +}; + +export const OperatorTable = [ + { arity: "unary", name: "not", symbol: "!", opcode: OpCode.Unary.LOGICAL_NOT }, + { arity: "unary", name: "neg", symbol: "-", opcode: OpCode.Unary.NEGATE }, + { arity: "unary", name: "plus", symbol: "+", opcode: OpCode.Unary.PLUS }, + { arity: "binary", name: "add", symbol: "+", opcode: OpCode.Binary.ADD }, + { arity: "binary", name: "min", symbol: "-", opcode: OpCode.Binary.SUBTRACT }, + { arity: "binary", name: "mult", symbol: "*", opcode: OpCode.Binary.MULTIPLY }, + { arity: "binary", name: "div", symbol: "/", opcode: OpCode.Binary.DIVIDE }, + { arity: "binary", name: "mod", symbol: "%", opcode: OpCode.Binary.MODULO }, + { arity: "binary", name: "equalTo", symbol: "==", opcode: OpCode.Binary.EQUAL }, + { arity: "binary", name: "notEqual", symbol: "!=", opcode: OpCode.Binary.NOT_EQUAL }, + { arity: "binary", name: "greaterThan", symbol: ">", opcode: OpCode.Binary.GREATER_THAN }, + { arity: "binary", name: "greaterEqual", symbol: ">=", opcode: OpCode.Binary.GREATER_EQUAL }, + { arity: "binary", name: "lessThan", symbol: "<", opcode: OpCode.Binary.LESS_THAN }, + { arity: "binary", name: "lessEqual", symbol: "<=", opcode: OpCode.Binary.LESS_EQUAL }, + { arity: "binary", name: "and", symbol: "&&", opcode: OpCode.Binary.LOGICAL_AND }, + { arity: "binary", name: "or", symbol: "||", opcode: OpCode.Binary.LOGICAL_OR }, +]; + + +export const SymbolToOpCode = {}; +export const OpCodeToSymbol = {}; +export const OpCodeArgs = {}; + +for (const { arity: args, symbol, opcode } of OperatorTable) { + SymbolToOpCode[symbol] = opcode; + OpCodeToSymbol[opcode] = symbol; + OpCodeArgs[opcode] = args; + +} \ No newline at end of file diff --git a/src/webgl/index.js b/src/webgl/index.js index 7ba587b132..355125b36e 100644 --- a/src/webgl/index.js +++ b/src/webgl/index.js @@ -15,6 +15,7 @@ import camera from './p5.Camera'; import texture from './p5.Texture'; import rendererGL from './p5.RendererGL'; import shadergenerator from './ShaderGenerator'; +import strands from '../strands/p5.strands'; export default function(p5){ rendererGL(p5, p5.prototype); @@ -34,4 +35,5 @@ export default function(p5){ shader(p5, p5.prototype); texture(p5, p5.prototype); shadergenerator(p5, p5.prototype); + strands(p5, p5.prototype); } From 604c2dd5d8a426b88f3ad4d383e97f6818e8e1b7 Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Tue, 1 Jul 2025 19:56:36 +0100 Subject: [PATCH 03/69] chipping away at DOD approach. --- preview/global/sketch.js | 10 +- src/strands/CFG.js | 35 ++++ src/strands/DAG.js | 109 +++++++++++++ src/strands/GLSL_generator.js | 5 + src/strands/control_flow_graph.js | 0 src/strands/directed_acyclic_graph.js | 85 ---------- src/strands/p5.StrandsNode.js | 40 ----- src/strands/p5.strands.js | 224 +++++++++++++++++++++----- src/strands/utils.js | 22 ++- 9 files changed, 360 insertions(+), 170 deletions(-) create mode 100644 src/strands/CFG.js create mode 100644 src/strands/DAG.js create mode 100644 src/strands/GLSL_generator.js delete mode 100644 src/strands/control_flow_graph.js delete mode 100644 src/strands/directed_acyclic_graph.js delete mode 100644 src/strands/p5.StrandsNode.js diff --git a/preview/global/sketch.js b/preview/global/sketch.js index c52148e7d3..ec77fd8c0e 100644 --- a/preview/global/sketch.js +++ b/preview/global/sketch.js @@ -1,11 +1,15 @@ p5.disableFriendlyErrors = true; -function bloomShaderCallback() { - createFloat(1.0); +function callback() { + let x = createFloat(1.0); + getFinalColor((col) => { + return x; + }) } async function setup(){ - bloomShader = baseFilterShader().newModify(bloomShaderCallback); + createCanvas(300,400, WEBGL) + bloomShader = baseColorShader().newModify(callback, {parser: false}); } function draw(){ diff --git a/src/strands/CFG.js b/src/strands/CFG.js new file mode 100644 index 0000000000..28b4007e9c --- /dev/null +++ b/src/strands/CFG.js @@ -0,0 +1,35 @@ +export function createControlFlowGraph() { + const graph = { + nextID: 0, + blockTypes: [], + incomingEdges:[], + incomingEdgesIndex: [], + incomingEdgesCount: [], + outgoingEdges: [], + outgoingEdgesIndex: [], + outgoingEdgesCount: [], + blockInstructionsStart: [], + blockInstructionsCount: [], + blockInstructionsList: [], + }; + + return graph; +} + +export function createBasicBlock(graph, blockType) { + const i = graph.nextID++; + graph.blockTypes.push(blockType), + graph.incomingEdges.push(graph.incomingEdges.length); + graph.incomingEdgesCount.push(0); + graph.outgoingEdges.push(graph.outgoingEdges.length); + graph.outgoingEdges.push(0); + return i; +} + + +export function addEdge(graph, from, to) { + graph.incomingEdges.push(from); + graph.outgoingEdges.push(to); + graph.outgoingEdgesCount[from]++; + graph.incomingEdgesCount[to]++; +} \ No newline at end of file diff --git a/src/strands/DAG.js b/src/strands/DAG.js new file mode 100644 index 0000000000..0090971841 --- /dev/null +++ b/src/strands/DAG.js @@ -0,0 +1,109 @@ +import { NodeTypeRequiredFields, NodeType, NodeTypeToName } from './utils' +import * as FES from './strands_FES' + +// Properties of the Directed Acyclic Graph and its nodes +const graphProperties = [ + 'nodeTypes', + 'dataTypes', + 'opCodes', + 'values', + 'identifiers', + // sparse adjancey list for dependencies (indegree) + 'dependsOnStart', + 'dependsOnCount', + 'dependsOnList', + // sparse adjacency list for phi inputs + 'phiBlocksStart', + 'phiBlocksCount', + 'phiBlocksList' +]; + +const nodeProperties = [ + 'nodeType', + 'dataType', + 'opCode', + 'value', + 'identifier', + 'dependsOn', +]; + +// Public functions for for strands runtime +export function createDirectedAcyclicGraph() { + const graph = { + nextID: 0, + cache: new Map(), + } + for (const prop of graphProperties) { + graph[prop] = []; + } + return graph; +} + +export function getOrCreateNode(graph, node) { + const key = getNodeKey(node); + const existing = graph.cache.get(key); + + if (existing !== undefined) { + return existing; + } else { + const id = createNode(graph, node); + graph.cache.set(key, id); + return id; + } +} + +export function createNodeData(data = {}) { + const node = {}; + for (const key of nodeProperties) { + node[key] = data[key] ?? NaN; + } + validateNode(node); + return node; +} + +///////////////////////////////// +// Private functions +///////////////////////////////// + +function getNodeKey(node) { + const key = JSON.stringify(node); + return key; +} + +function validateNode(node){ + const requiredFields = NodeTypeRequiredFields[node.nodeType]; + const missingFields = []; + for (const field of requiredFields) { + if (node[field] === NaN) { + missingFields.push(field); + } + } + if (missingFields.length > 0) { + FES.internalError(`[p5.strands internal error]: Missing fields ${missingFields.join(', ')} for a node type ${NodeTypeToName(node.nodeType)}`); + } +} + +function createNode(graph, node) { + const id = graph.nextID++; + + for (const prop of nodeProperties) { + if (prop === 'dependsOn' || 'phiBlocks') { + continue; + } + + const plural = prop + 's'; + graph[plural][id] = node[prop]; + } + + const depends = Array.isArray(node.dependsOn) ? node.dependsOn : []; + graph.dependsOnStart[id] = graph.dependsOnList.length; + graph.dependsOnCount[id] = depends.length; + graph.dependsOnList.push(...depends); + + const phis = Array.isArray(node.phiBlocks) ? node.phiBlocks : []; + graph.phiBlocksStart[id] = graph.phiBlocksList.length; + graph.phiBlocksCount[id] = phis.length; + graph.phiBlocksList.push(...phis); + + return id; +} \ No newline at end of file diff --git a/src/strands/GLSL_generator.js b/src/strands/GLSL_generator.js new file mode 100644 index 0000000000..a63b93277b --- /dev/null +++ b/src/strands/GLSL_generator.js @@ -0,0 +1,5 @@ +import * as utils from './utils' + +export function generateGLSL(strandsContext) { + +} \ No newline at end of file diff --git a/src/strands/control_flow_graph.js b/src/strands/control_flow_graph.js deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/strands/directed_acyclic_graph.js b/src/strands/directed_acyclic_graph.js deleted file mode 100644 index 6da09d4921..0000000000 --- a/src/strands/directed_acyclic_graph.js +++ /dev/null @@ -1,85 +0,0 @@ -import { NodeTypeRequiredFields, NodeTypeName } from './utils' -import * as strandsFES from './strands_FES' - -// Properties of the Directed Acyclic Graph and its nodes -const graphProperties = [ - 'nodeTypes', - 'dataTypes', - 'opCodes', - 'values', - 'identifiers', - // sparse adjancey list for dependencies (indegree) - 'dependsOnStartIndex', - 'dependsOnCount', - 'dependsOnList', -]; - -const nodeProperties = [ - 'nodeType', - 'dataType', - 'opCodes', - 'value', - 'identifier', - 'dependsOn' -]; - -// Public functions for for strands runtime -export function createGraph() { - const graph = { - _nextID: 0, - _nodeCache: new Map(), - } - for (const prop of graphProperties) { - graph[prop] = []; - } - return graph; -} - - -export function getOrCreateNode(graph, node) { - const result = getNode(graph, node); - if (!result){ - return createNode(graph, node) - } else { - return result; - } -} - -export function createNodeData(data = {}) { - const node = {}; - for (const key of nodeProperties) { - node[key] = data[key] ?? NaN; - } - validateNode(node); - return node; -} - -// Private functions to this file -function getNodeKey(node) { - -} - -function validateNode(node){ - const requiredFields = NodeTypeRequiredFields[node.NodeType]; - const missingFields = []; - for (const field of requiredFields) { - if (node[field] === NaN) { - missingFields.push(field); - } - } - if (missingFields.length > 0) { - strandsFES.internalError(`[p5.strands internal error]: Missing fields ${missingFields.join(', ')} for a node type ${NodeTypeName(node.nodeType)}`); - } -} - -function getNode(graph, node) { - if (graph) - - if (!node) { - return null; - } -} - -function createNode(graph, nodeData) { - -} \ No newline at end of file diff --git a/src/strands/p5.StrandsNode.js b/src/strands/p5.StrandsNode.js deleted file mode 100644 index ffddc7e83e..0000000000 --- a/src/strands/p5.StrandsNode.js +++ /dev/null @@ -1,40 +0,0 @@ -////////////////////////////////////////////// -// User API -////////////////////////////////////////////// - -import { OperatorTable } from './utils' - -export class StrandsNode { - constructor(id) { - this.id = id; - } -} - -export function createStrandsAPI(strands, fn) { - // Attach operators to StrandsNode: - for (const { name, symbol, arity } of OperatorTable) { - if (arity === 'binary') { - StrandsNode.prototype[name] = function (rightNode) { - const id = strands.createBinaryExpressionNode(this, rightNode, symbol); - return new StrandsNode(id); - }; - } - if (arity === 'unary') { - StrandsNode.prototype[name] = function () { - const id = strands.createUnaryExpressionNode(this, symbol); - return new StrandsNode(id); - }; - } - } - - // Attach p5 Globals - fn.uniformFloat = function(name, value) { - const id = strands.createVariableNode(DataType.FLOAT, name); - return new StrandsNode(id); - }, - - fn.createFloat = function(value) { - const id = strands.createLiteralNode(DataType.FLOAT, value); - return new StrandsNode(id); - } -} \ No newline at end of file diff --git a/src/strands/p5.strands.js b/src/strands/p5.strands.js index 0bdfe7bda5..baf3496f77 100644 --- a/src/strands/p5.strands.js +++ b/src/strands/p5.strands.js @@ -6,59 +6,208 @@ */ import { transpileStrandsToJS } from './code_transpiler'; -import { DataType, NodeType, OpCode, SymbolToOpCode, OpCodeToSymbol, OpCodeArgs } from './utils'; +import { DataType, NodeType, SymbolToOpCode, OperatorTable, BlockType } from './utils'; -import { createStrandsAPI } from './p5.StrandsNode' -import * as DAG from './directed_acyclic_graph'; -import * as CFG from './control_flow_graph' -import { create } from '@davepagurek/bezier-path'; +import * as DAG from './DAG'; +import * as CFG from './CFG' function strands(p5, fn) { - ////////////////////////////////////////////// // Global Runtime ////////////////////////////////////////////// + function initStrands(ctx) { + ctx.cfg = CFG.createControlFlowGraph(); + ctx.dag = DAG.createDirectedAcyclicGraph(); + ctx.blockStack = []; + ctx.currentBlock = null; + ctx.uniforms = []; + ctx.hooks = []; + } - class StrandsRuntime { - constructor() { - this.reset(); - } - - reset() { - this._scopeStack = []; - this._allScopes = new Map(); + function deinitStrands(ctx) { + Object.keys(ctx).forEach(prop => { + delete ctx[prop]; + }); + } + + // Stubs + function overrideGlobalFunctions() {} + function restoreGlobalFunctions() {} + function overrideFES() {} + function restoreFES() {} + + ////////////////////////////////////////////// + // User nodes + ////////////////////////////////////////////// + class StrandsNode { + constructor(id) { + this.id = id; } - - createBinaryExpressionNode(left, right, operatorSymbol) { - const activeGraph = this._currentScope().graph; - const opCode = SymbolToOpCode.get(operatorSymbol); - - const dataType = DataType.FLOAT; // lookUpBinaryOperatorResult(); - return activeGraph._getOrCreateNode(NodeType.OPERATION, dataType, opCode, null, null, [left, right]); + } + + // We augment the strands node with operations programatically + // this means methods like .add, .sub, etc can be chained + for (const { name, symbol, arity } of OperatorTable) { + if (arity === 'binary') { + StrandsNode.prototype[name] = function (rightNode) { + const id = emitBinaryOp(this.id, rightNode, SymbolToOpCode[symbol]); + return new StrandsNode(id); + }; } - - createLiteralNode(dataType, value) { - const activeGraph = this._currentScope().graph; - return activeGraph._getOrCreateNode(NodeType.LITERAL, dataType, value, null, null, null); + if (arity === 'unary') { + StrandsNode.prototype[name] = function () { + const id = NaN; //createUnaryExpressionNode(this, SymbolToOpCode[symbol]); + return new StrandsNode(id); + }; } } ////////////////////////////////////////////// // Entry Point ////////////////////////////////////////////// + const strandsContext = {}; + initStrands(strandsContext); - const strands = new StrandsRuntime(); - const API = createStrandsAPI(strands, fn); + function recordInBlock(blockID, nodeID) { + const graph = strandsContext.cfg + if (graph.blockInstructionsCount[blockID] === undefined) { + graph.blockInstructionsStart[blockID] = graph.blockInstructionsList.length; + graph.blockInstructionsCount[blockID] = 0; + } + graph.blockInstructionsList.push(nodeID); + graph.blockInstructionsCount[blockID] += 1; + } + + function emitLiteralNode(dataType, value) { + const nodeData = DAG.createNodeData({ + nodeType: NodeType.LITERAL, + dataType, + value + }); + const id = DAG.getOrCreateNode(strandsContext.dag, nodeData); + const b = strandsContext.currentBlock; + recordInBlock(strandsContext.currentBlock, id); + return id; + } - const oldModify = p5.Shader.prototype.modify + function emitBinaryOp(left, right, opCode) { + const nodeData = DAG.createNodeData({ + nodeType: NodeType.OPERATION, + dependsOn: [left, right], + opCode + }); + const id = DAG.getOrCreateNode(strandsContext.dag, nodeData); + recordInBlock(strandsContext.currentBlock, id); + return id; + } + + function emitVariableNode(dataType, identifier) { + const nodeData = DAG.createNodeData({ + nodeType: NodeType.VARIABLE, + dataType, + identifier + }) + const id = DAG.getOrCreateNode(strandsContext.dag, nodeData); + recordInBlock(strandsContext.currentBlock, id); + return id; + } + + function enterBlock(blockID) { + if (strandsContext.currentBlock) { + CFG.addEdge(strandsContext.cfg, strandsContext.currentBlock, blockID); + } + strandsContext.currentBlock = blockID; + strandsContext.blockStack.push(blockID); + } + + function exitBlock() { + strandsContext.blockStack.pop(); + strandsContext.currentBlock = strandsContext.blockStack[strandsContext.blockStack-1]; + } - for (const [fnName, fnBody] of Object.entries(userFunctions)) { - fn[fnName] = fnBody; + fn.uniformFloat = function(name, defaultValue) { + const id = emitVariableNode(DataType.FLOAT, name); + strandsContext.uniforms.push({ name, dataType: DataType.FLOAT, defaultValue }); + return new StrandsNode(id); + } + + fn.createFloat = function(value) { + const id = emitLiteralNode(DataType.FLOAT, value); + return new StrandsNode(id); + } + + fn.strandsIf = function(condition, ifBody, elseBody) { + const conditionBlock = CFG.createBasicBlock(strandsContext.cfg, BlockType.IF_COND); + enterBlock(conditionBlock); + + const trueBlock = CFG.createBasicBlock(strandsContext.cfg, BlockType.IF); + enterBlock(trueBlock); + ifBody(); + exitBlock(); + + const mergeBlock = CFG.createBasicBlock(strandsContext.cfg, BlockType.MERGE); + enterBlock(mergeBlock); + } + + function createHookArguments(parameters){ + const structTypes = ['Vertex', ] + const args = []; + + for (const param of parameters) { + const T = param.type; + if(structTypes.includes(T.typeName)) { + const propertiesNodes = T.properties.map( + (prop) => [prop.name, emitVariableNode(DataType[prop.dataType], prop.name)] + ); + const argObj = Object.fromEntries(propertiesNodes); + args.push(argObj); + } else { + const arg = emitVariableNode(DataType[param.dataType], param.name); + args.push(arg) + } + } + return args; } + function generateHookOverrides(shader) { + const availableHooks = { + ...shader.hooks.vertex, + ...shader.hooks.fragment, + } + const hookTypes = Object.keys(availableHooks).map(name => shader.hookTypes(name)); + + for (const hookType of hookTypes) { + window[hookType.name] = function(callback) { + const funcBlock = CFG.createBasicBlock(strandsContext.cfg, BlockType.FUNCTION); + enterBlock(funcBlock); + const args = createHookArguments(hookType.parameters); + console.log(hookType, args); + runHook(hookType, callback, args); + exitBlock(); + } + } + } + + function runHook(hookType, callback, inputs) { + const blockID = CFG.createBasicBlock(strandsContext.cfg, BlockType.FUNCTION) + + enterBlock(blockID); + const rootNode = callback(inputs); + exitBlock(); + + strandsContext.hooks.push({ + hookType, + blockID, + rootNode, + }); + } + + const oldModify = p5.Shader.prototype.modify p5.Shader.prototype.newModify = function(shaderModifier, options = { parser: true, srcLocations: false }) { if (shaderModifier instanceof Function) { - + // Reset the context object every time modify is called; + initStrands(strandsContext) + generateHookOverrides(this); // 1. Transpile from strands DSL to JS let strandsCallback; if (options.parser) { @@ -68,19 +217,22 @@ function strands(p5, fn) { } // 2. Build the IR from JavaScript API - strands.enterScope('GLOBAL'); + const globalScope = CFG.createBasicBlock(strandsContext.cfg, BlockType.GLOBAL); + enterBlock(globalScope); strandsCallback(); - strands.exitScope('GLOBAL'); - + exitBlock(); // 3. Generate shader code hooks object from the IR // ....... - + for (const {hookType, blockID, rootNode} of strandsContext.hooks) { + // console.log(hookType); + } + // Call modify with the generated hooks object // return oldModify.call(this, generatedModifyArgument); // Reset the strands runtime context - // strands.reset(); + // deinitStrands(strandsContext); } else { return oldModify.call(this, shaderModifier) diff --git a/src/strands/utils.js b/src/strands/utils.js index 29a3e1d1ab..a5bdeef355 100644 --- a/src/strands/utils.js +++ b/src/strands/utils.js @@ -9,16 +9,18 @@ export const NodeType = { LITERAL: 1, VARIABLE: 2, CONSTANT: 3, + PHI: 4, }; export const NodeTypeRequiredFields = { - [NodeType.OPERATION]: ['opCodes', 'dependsOn'], - [NodeType.LITERAL]: ['values'], - [NodeType.VARIABLE]: ['identifiers'], - [NodeType.CONSTANT]: ['values'], + [NodeType.OPERATION]: ['opCode', 'dependsOn'], + [NodeType.LITERAL]: ['value'], + [NodeType.VARIABLE]: ['identifier', 'dataType'], + [NodeType.CONSTANT]: ['value'], + [NodeType.PHI]: ['dependsOn', 'phiBlocks'] }; -export const NodeTypeName = Object.fromEntries( +export const NodeTypeToName = Object.fromEntries( Object.entries(NodeType).map(([key, val]) => [val, key]) ); @@ -105,5 +107,13 @@ for (const { arity: args, symbol, opcode } of OperatorTable) { SymbolToOpCode[symbol] = opcode; OpCodeToSymbol[opcode] = symbol; OpCodeArgs[opcode] = args; - +} + +export const BlockType = { + GLOBAL: 0, + IF: 1, + ELSE_IF: 2, + ELSE: 3, + FOR: 4, + MERGE: 5, } \ No newline at end of file From 89508172d6d3c832090980971ccdce37ee4cd616 Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Sat, 5 Jul 2025 12:27:49 +0100 Subject: [PATCH 04/69] nested ifs --- preview/global/sketch.js | 21 +++++- src/strands/CFG.js | 50 +++++++------ src/strands/DAG.js | 119 +++++++++++++++---------------- src/strands/GLSL_generator.js | 122 ++++++++++++++++++++++++++++++- src/strands/p5.strands.js | 130 ++++++++++++++++------------------ src/strands/utils.js | 24 +++++++ 6 files changed, 308 insertions(+), 158 deletions(-) diff --git a/preview/global/sketch.js b/preview/global/sketch.js index ec77fd8c0e..772c8b8c7c 100644 --- a/preview/global/sketch.js +++ b/preview/global/sketch.js @@ -1,10 +1,25 @@ p5.disableFriendlyErrors = true; function callback() { - let x = createFloat(1.0); + // let x = createFloat(1.0); + getFinalColor((col) => { - return x; - }) + let y = createFloat(10); + let x = y.add(y); + + strandsIf(x.greaterThan(createFloat(0.0)), () => { + x = createFloat(20); + strandsIf(x.greaterThan(createFloat(0.0)), () => { + x = createFloat(20); + }); + }); + strandsIf(x.greaterThan(createFloat(0.0)), () => { + x = createFloat(20); + }); + const z = createFloat(200); + + return x.add(z); + }); } async function setup(){ diff --git a/src/strands/CFG.js b/src/strands/CFG.js index 28b4007e9c..5b8fa9ac96 100644 --- a/src/strands/CFG.js +++ b/src/strands/CFG.js @@ -1,35 +1,39 @@ export function createControlFlowGraph() { - const graph = { + return { nextID: 0, + graphType: 'CFG', blockTypes: [], - incomingEdges:[], - incomingEdgesIndex: [], - incomingEdgesCount: [], + incomingEdges: [], outgoingEdges: [], - outgoingEdgesIndex: [], - outgoingEdgesCount: [], - blockInstructionsStart: [], - blockInstructionsCount: [], - blockInstructionsList: [], + blockInstructions: [], }; - - return graph; } export function createBasicBlock(graph, blockType) { - const i = graph.nextID++; - graph.blockTypes.push(blockType), - graph.incomingEdges.push(graph.incomingEdges.length); - graph.incomingEdgesCount.push(0); - graph.outgoingEdges.push(graph.outgoingEdges.length); - graph.outgoingEdges.push(0); - return i; + const id = graph.nextID++; + graph.blockTypes[id] = blockType; + graph.incomingEdges[id] = []; + graph.outgoingEdges[id] = []; + graph.blockInstructions[id]= []; + return id; } - export function addEdge(graph, from, to) { - graph.incomingEdges.push(from); - graph.outgoingEdges.push(to); - graph.outgoingEdgesCount[from]++; - graph.incomingEdgesCount[to]++; + graph.outgoingEdges[from].push(to); + graph.incomingEdges[to].push(from); +} + +export function recordInBasicBlock(graph, blockID, nodeID) { + graph.blockInstructions[blockID] = graph.blockInstructions[blockID] || []; + graph.blockInstructions[blockID].push(nodeID); +} + +export function getBlockDataFromID(graph, id) { + return { + id, + blockType: graph.blockTypes[id], + incomingEdges: graph.incomingEdges[id], + outgoingEdges: graph.outgoingEdges[id], + blockInstructions: graph.blockInstructions[id], + } } \ No newline at end of file diff --git a/src/strands/DAG.js b/src/strands/DAG.js index 0090971841..b095fe3efc 100644 --- a/src/strands/DAG.js +++ b/src/strands/DAG.js @@ -1,48 +1,32 @@ -import { NodeTypeRequiredFields, NodeType, NodeTypeToName } from './utils' +import { NodeTypeRequiredFields, NodeTypeToName } from './utils' import * as FES from './strands_FES' -// Properties of the Directed Acyclic Graph and its nodes -const graphProperties = [ - 'nodeTypes', - 'dataTypes', - 'opCodes', - 'values', - 'identifiers', - // sparse adjancey list for dependencies (indegree) - 'dependsOnStart', - 'dependsOnCount', - 'dependsOnList', - // sparse adjacency list for phi inputs - 'phiBlocksStart', - 'phiBlocksCount', - 'phiBlocksList' -]; - -const nodeProperties = [ - 'nodeType', - 'dataType', - 'opCode', - 'value', - 'identifier', - 'dependsOn', -]; - +///////////////////////////////// // Public functions for for strands runtime +///////////////////////////////// + export function createDirectedAcyclicGraph() { - const graph = { - nextID: 0, + const graph = { + nextID: 0, cache: new Map(), - } - for (const prop of graphProperties) { - graph[prop] = []; - } + nodeTypes: [], + dataTypes: [], + opCodes: [], + values: [], + identifiers: [], + phiBlocks: [], + dependsOn: [], + usedBy: [], + graphType: 'DAG', + }; + return graph; } export function getOrCreateNode(graph, node) { const key = getNodeKey(node); const existing = graph.cache.get(key); - + if (existing !== undefined) { return existing; } else { @@ -53,17 +37,51 @@ export function getOrCreateNode(graph, node) { } export function createNodeData(data = {}) { - const node = {}; - for (const key of nodeProperties) { - node[key] = data[key] ?? NaN; - } + const node = { + nodeType: data.nodeType ?? null, + dataType: data.dataType ?? null, + opCode: data.opCode ?? null, + value: data.value ?? null, + identifier: data.identifier ?? null, + dependsOn: Array.isArray(data.dependsOn) ? data.dependsOn : [], + usedBy: Array.isArray(data.usedBy) ? data.usedBy : [], + phiBlocks: Array.isArray(data.phiBlocks) ? data.phiBlocks : [] + }; validateNode(node); return node; } +export function getNodeDataFromID(graph, id) { + return { + nodeType: graph.nodeTypes[id], + dataType: graph.dataTypes[id], + opCode: graph.opCodes[id], + value: graph.values[id], + identifier: graph.identifiers[id], + dependsOn: graph.dependsOn[id], + usedBy: graph.usedBy[id], + phiBlocks: graph.phiBlocks[id], + } +} + ///////////////////////////////// // Private functions ///////////////////////////////// +function createNode(graph, node) { + const id = graph.nextID++; + graph.nodeTypes[id] = node.nodeType; + graph.dataTypes[id] = node.dataType; + graph.opCodes[id] = node.opCode; + graph.values[id] = node.value; + graph.identifiers[id] = node.identifier; + graph.dependsOn[id] = node.dependsOn.slice(); + graph.usedBy[id] = node.usedBy; + graph.phiBlocks[id] = node.phiBlocks.slice(); + for (const dep of node.dependsOn) { + graph.usedBy[dep].push(id); + } + return id; +} function getNodeKey(node) { const key = JSON.stringify(node); @@ -81,29 +99,4 @@ function validateNode(node){ if (missingFields.length > 0) { FES.internalError(`[p5.strands internal error]: Missing fields ${missingFields.join(', ')} for a node type ${NodeTypeToName(node.nodeType)}`); } -} - -function createNode(graph, node) { - const id = graph.nextID++; - - for (const prop of nodeProperties) { - if (prop === 'dependsOn' || 'phiBlocks') { - continue; - } - - const plural = prop + 's'; - graph[plural][id] = node[prop]; - } - - const depends = Array.isArray(node.dependsOn) ? node.dependsOn : []; - graph.dependsOnStart[id] = graph.dependsOnList.length; - graph.dependsOnCount[id] = depends.length; - graph.dependsOnList.push(...depends); - - const phis = Array.isArray(node.phiBlocks) ? node.phiBlocks : []; - graph.phiBlocksStart[id] = graph.phiBlocksList.length; - graph.phiBlocksCount[id] = phis.length; - graph.phiBlocksList.push(...phis); - - return id; } \ No newline at end of file diff --git a/src/strands/GLSL_generator.js b/src/strands/GLSL_generator.js index a63b93277b..488510a27f 100644 --- a/src/strands/GLSL_generator.js +++ b/src/strands/GLSL_generator.js @@ -1,5 +1,125 @@ -import * as utils from './utils' +import { dfsPostOrder, NodeType, OpCodeToSymbol, BlockType } from "./utils"; +import { getNodeDataFromID } from "./DAG"; +import { getBlockDataFromID } from "./CFG"; + +let globalTempCounter = 0; + +function nodeToGLSL(dag, nodeID, hookContext) { + const node = getNodeDataFromID(dag, nodeID); + if (hookContext.tempName?.[nodeID]) { + return hookContext.tempName[nodeID]; + } + switch (node.nodeType) { + case NodeType.LITERAL: + return node.value.toFixed(4); + + case NodeType.VARIABLE: + return node.identifier; + + case NodeType.OPERATION: + const [lID, rID] = node.dependsOn; + const left = nodeToGLSL(dag, lID, hookContext); + const right = nodeToGLSL(dag, rID, hookContext); + const opSym = OpCodeToSymbol[node.opCode]; + return `(${left} ${opSym} ${right})`; + + default: + throw new Error(`${node.nodeType} not working yet`); + } +} + +function computeDeclarations(dag, dagOrder) { + const usedCount = {}; + for (const nodeID of dagOrder) { + usedCount[nodeID] = (dag.usedBy[nodeID] || []).length; + } + + const tempName = {}; + const declarations = []; + + for (const nodeID of dagOrder) { + if (dag.nodeTypes[nodeID] !== NodeType.OPERATION) { + continue; + } + + if (usedCount[nodeID] > 1) { + const tmp = `t${globalTempCounter++}`; + tempName[nodeID] = tmp; + + const expr = nodeToGLSL(dag, nodeID, {}); + declarations.push(`float ${tmp} = ${expr};`); + } + } + + return { declarations, tempName }; +} + +const cfgHandlers = { + Condition(strandsContext, hookContext) { + const conditionID = strandsContext.blockConditions[blockID]; + const condExpr = nodeToGLSL(dag, conditionID, hookContext); + write(`if (${condExpr}) {`) + indent++; + return; + } +} export function generateGLSL(strandsContext) { + const hooksObj = {}; + + for (const { hookType, entryBlockID, rootNodeID} of strandsContext.hooks) { + const { cfg, dag } = strandsContext; + const dagSorted = dfsPostOrder(dag.dependsOn, rootNodeID); + const cfgSorted = dfsPostOrder(cfg.outgoingEdges, entryBlockID).reverse(); + + console.log("BLOCK ORDER: ", cfgSorted.map(id => getBlockDataFromID(cfg, id))); + + const hookContext = { + ...computeDeclarations(dag, dagSorted), + indent: 0, + currentBlock: cfgSorted[0] + }; + + let indent = 0; + let nested = 1; + let codeLines = hookContext.declarations.map((decl) => pad() + decl); + const write = (line) => codeLines.push(' '.repeat(indent) + line); + + cfgSorted.forEach((blockID, i) => { + const type = cfg.blockTypes[blockID]; + const nextID = cfgSorted[i + 1]; + const nextType = cfg.blockTypes[nextID]; + + switch (type) { + case BlockType.COND: + const condID = strandsContext.blockConditions[blockID]; + const condExpr = nodeToGLSL(dag, condID, hookContext); + write(`if (${condExpr}) {`) + indent++; + return; + case BlockType.MERGE: + indent--; + write('MERGE'); + write('}'); + return; + default: + const instructions = new Set(cfg.blockInstructions[blockID] || []); + for (let nodeID of dagSorted) { + if (!instructions.has(nodeID)) { + continue; + } + const snippet = hookContext.tempName[nodeID] + ? hookContext.tempName[nodeID] + : nodeToGLSL(dag, nodeID, hookContext); + write(snippet); + } + } + }); + + const finalExpression = `return ${nodeToGLSL(dag, rootNodeID, hookContext)};`; + write(finalExpression); + hooksObj[hookType.name] = codeLines.join('\n'); + } + return hooksObj; } \ No newline at end of file diff --git a/src/strands/p5.strands.js b/src/strands/p5.strands.js index baf3496f77..f72bba9f41 100644 --- a/src/strands/p5.strands.js +++ b/src/strands/p5.strands.js @@ -10,6 +10,7 @@ import { DataType, NodeType, SymbolToOpCode, OperatorTable, BlockType } from './ import * as DAG from './DAG'; import * as CFG from './CFG' +import { generateGLSL } from './GLSL_generator'; function strands(p5, fn) { ////////////////////////////////////////////// @@ -19,7 +20,8 @@ function strands(p5, fn) { ctx.cfg = CFG.createControlFlowGraph(); ctx.dag = DAG.createDirectedAcyclicGraph(); ctx.blockStack = []; - ctx.currentBlock = null; + ctx.currentBlock = -1; + ctx.blockConditions = {}; ctx.uniforms = []; ctx.hooks = []; } @@ -50,7 +52,7 @@ function strands(p5, fn) { for (const { name, symbol, arity } of OperatorTable) { if (arity === 'binary') { StrandsNode.prototype[name] = function (rightNode) { - const id = emitBinaryOp(this.id, rightNode, SymbolToOpCode[symbol]); + const id = createBinaryOpNode(this.id, rightNode.id, SymbolToOpCode[symbol]); return new StrandsNode(id); }; } @@ -62,23 +64,7 @@ function strands(p5, fn) { } } - ////////////////////////////////////////////// - // Entry Point - ////////////////////////////////////////////// - const strandsContext = {}; - initStrands(strandsContext); - - function recordInBlock(blockID, nodeID) { - const graph = strandsContext.cfg - if (graph.blockInstructionsCount[blockID] === undefined) { - graph.blockInstructionsStart[blockID] = graph.blockInstructionsList.length; - graph.blockInstructionsCount[blockID] = 0; - } - graph.blockInstructionsList.push(nodeID); - graph.blockInstructionsCount[blockID] += 1; - } - - function emitLiteralNode(dataType, value) { + function createLiteralNode(dataType, value) { const nodeData = DAG.createNodeData({ nodeType: NodeType.LITERAL, dataType, @@ -86,67 +72,82 @@ function strands(p5, fn) { }); const id = DAG.getOrCreateNode(strandsContext.dag, nodeData); const b = strandsContext.currentBlock; - recordInBlock(strandsContext.currentBlock, id); + CFG.recordInBasicBlock(strandsContext.cfg, strandsContext.currentBlock, id); return id; } - function emitBinaryOp(left, right, opCode) { + function createBinaryOpNode(left, right, opCode) { const nodeData = DAG.createNodeData({ nodeType: NodeType.OPERATION, dependsOn: [left, right], opCode }); const id = DAG.getOrCreateNode(strandsContext.dag, nodeData); - recordInBlock(strandsContext.currentBlock, id); + CFG.recordInBasicBlock(strandsContext.cfg, strandsContext.currentBlock, id); return id; } - function emitVariableNode(dataType, identifier) { + function createVariableNode(dataType, identifier) { const nodeData = DAG.createNodeData({ nodeType: NodeType.VARIABLE, dataType, identifier }) const id = DAG.getOrCreateNode(strandsContext.dag, nodeData); - recordInBlock(strandsContext.currentBlock, id); + CFG.recordInBasicBlock(strandsContext.cfg, strandsContext.currentBlock, id); return id; } - function enterBlock(blockID) { - if (strandsContext.currentBlock) { - CFG.addEdge(strandsContext.cfg, strandsContext.currentBlock, blockID); - } - strandsContext.currentBlock = blockID; + function pushBlockWithEdgeFromCurrent(blockID) { + CFG.addEdge(strandsContext.cfg, strandsContext.currentBlock, blockID); + pushBlock(blockID); + } + + function pushBlock(blockID) { strandsContext.blockStack.push(blockID); + strandsContext.currentBlock = blockID; } - function exitBlock() { + function popBlock() { strandsContext.blockStack.pop(); - strandsContext.currentBlock = strandsContext.blockStack[strandsContext.blockStack-1]; + const len = strandsContext.blockStack.length; + strandsContext.currentBlock = strandsContext.blockStack[len-1]; } fn.uniformFloat = function(name, defaultValue) { - const id = emitVariableNode(DataType.FLOAT, name); + const id = createVariableNode(DataType.FLOAT, name); strandsContext.uniforms.push({ name, dataType: DataType.FLOAT, defaultValue }); return new StrandsNode(id); } fn.createFloat = function(value) { - const id = emitLiteralNode(DataType.FLOAT, value); + const id = createLiteralNode(DataType.FLOAT, value); return new StrandsNode(id); } - fn.strandsIf = function(condition, ifBody, elseBody) { - const conditionBlock = CFG.createBasicBlock(strandsContext.cfg, BlockType.IF_COND); - enterBlock(conditionBlock); + fn.strandsIf = function(conditionNode, ifBody) { + const { cfg } = strandsContext; + + const conditionBlock = CFG.createBasicBlock(cfg, BlockType.COND); + pushBlockWithEdgeFromCurrent(conditionBlock); + strandsContext.blockConditions[conditionBlock] = conditionNode.id; - const trueBlock = CFG.createBasicBlock(strandsContext.cfg, BlockType.IF); - enterBlock(trueBlock); + const thenBlock = CFG.createBasicBlock(cfg, BlockType.IF); + pushBlockWithEdgeFromCurrent(thenBlock); ifBody(); - exitBlock(); - const mergeBlock = CFG.createBasicBlock(strandsContext.cfg, BlockType.MERGE); - enterBlock(mergeBlock); + const mergeBlock = CFG.createBasicBlock(cfg, BlockType.MERGE); + if (strandsContext.currentBlock !== thenBlock) { + const nestedBlock = strandsContext.currentBlock; + CFG.addEdge(cfg, nestedBlock, mergeBlock); + // Pop the previous merge! + popBlock(); + } + // Pop the thenBlock after checking + popBlock(); + + pushBlock(mergeBlock); + CFG.addEdge(cfg, conditionBlock, mergeBlock); } function createHookArguments(parameters){ @@ -157,12 +158,12 @@ function strands(p5, fn) { const T = param.type; if(structTypes.includes(T.typeName)) { const propertiesNodes = T.properties.map( - (prop) => [prop.name, emitVariableNode(DataType[prop.dataType], prop.name)] + (prop) => [prop.name, createVariableNode(DataType[prop.dataType], prop.name)] ); const argObj = Object.fromEntries(propertiesNodes); args.push(argObj); } else { - const arg = emitVariableNode(DataType[param.dataType], param.name); + const arg = createVariableNode(DataType[param.dataType], param.name); args.push(arg) } } @@ -175,34 +176,28 @@ function strands(p5, fn) { ...shader.hooks.fragment, } const hookTypes = Object.keys(availableHooks).map(name => shader.hookTypes(name)); - for (const hookType of hookTypes) { window[hookType.name] = function(callback) { - const funcBlock = CFG.createBasicBlock(strandsContext.cfg, BlockType.FUNCTION); - enterBlock(funcBlock); + const entryBlockID = CFG.createBasicBlock(strandsContext.cfg, BlockType.FUNCTION); + pushBlockWithEdgeFromCurrent(entryBlockID); const args = createHookArguments(hookType.parameters); - console.log(hookType, args); - runHook(hookType, callback, args); - exitBlock(); + const rootNodeID = callback(args).id; + strandsContext.hooks.push({ + hookType, + entryBlockID, + rootNodeID, + }); + popBlock(); } } } - - function runHook(hookType, callback, inputs) { - const blockID = CFG.createBasicBlock(strandsContext.cfg, BlockType.FUNCTION) - - enterBlock(blockID); - const rootNode = callback(inputs); - exitBlock(); - - strandsContext.hooks.push({ - hookType, - blockID, - rootNode, - }); - } + ////////////////////////////////////////////// + // Entry Point + ////////////////////////////////////////////// + const strandsContext = {}; const oldModify = p5.Shader.prototype.modify + p5.Shader.prototype.newModify = function(shaderModifier, options = { parser: true, srcLocations: false }) { if (shaderModifier instanceof Function) { // Reset the context object every time modify is called; @@ -218,15 +213,14 @@ function strands(p5, fn) { // 2. Build the IR from JavaScript API const globalScope = CFG.createBasicBlock(strandsContext.cfg, BlockType.GLOBAL); - enterBlock(globalScope); + pushBlock(globalScope); strandsCallback(); - exitBlock(); + popBlock(); // 3. Generate shader code hooks object from the IR // ....... - for (const {hookType, blockID, rootNode} of strandsContext.hooks) { - // console.log(hookType); - } + const glsl = generateGLSL(strandsContext); + console.log(glsl.getFinalColor); // Call modify with the generated hooks object // return oldModify.call(this, generatedModifyArgument); diff --git a/src/strands/utils.js b/src/strands/utils.js index a5bdeef355..2b2ee88621 100644 --- a/src/strands/utils.js +++ b/src/strands/utils.js @@ -116,4 +116,28 @@ export const BlockType = { ELSE: 3, FOR: 4, MERGE: 5, + COND: 6, + FUNCTION: 7 +} + +//////////////////////////// +// Graph utils +//////////////////////////// +export function dfsPostOrder(adjacencyList, start) { + const visited = new Set(); + const postOrder = []; + + function dfs(v) { + if (visited.has(v)) { + return; + } + visited.add(v); + for (let w of adjacencyList[v] || []) { + dfs(w); + } + postOrder.push(v); + } + + dfs(start); + return postOrder; } \ No newline at end of file From f6369e7bd825d4d2b60a9b02209159103e595ae9 Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Mon, 7 Jul 2025 12:39:15 +0100 Subject: [PATCH 05/69] if/else semi working --- preview/global/sketch.js | 15 +++--------- src/strands/GLSL_generator.js | 43 ++++++++++++++++++++--------------- src/strands/p5.strands.js | 35 +++++++++++++++++----------- src/strands/utils.js | 41 +++++++++++++++++++++++++-------- 4 files changed, 82 insertions(+), 52 deletions(-) diff --git a/preview/global/sketch.js b/preview/global/sketch.js index 772c8b8c7c..486d553d6f 100644 --- a/preview/global/sketch.js +++ b/preview/global/sketch.js @@ -1,24 +1,15 @@ p5.disableFriendlyErrors = true; function callback() { - // let x = createFloat(1.0); getFinalColor((col) => { - let y = createFloat(10); - let x = y.add(y); + let x = createFloat(2.5); strandsIf(x.greaterThan(createFloat(0.0)), () => { - x = createFloat(20); - strandsIf(x.greaterThan(createFloat(0.0)), () => { - x = createFloat(20); - }); + x = createFloat(100); }); - strandsIf(x.greaterThan(createFloat(0.0)), () => { - x = createFloat(20); - }); - const z = createFloat(200); - return x.add(z); + return x; }); } diff --git a/src/strands/GLSL_generator.js b/src/strands/GLSL_generator.js index 488510a27f..400789cf43 100644 --- a/src/strands/GLSL_generator.js +++ b/src/strands/GLSL_generator.js @@ -1,4 +1,4 @@ -import { dfsPostOrder, NodeType, OpCodeToSymbol, BlockType } from "./utils"; +import { dfsPostOrder, NodeType, OpCodeToSymbol, BlockType, OpCodeToOperation, BlockTypeToName } from "./utils"; import { getNodeDataFromID } from "./DAG"; import { getBlockDataFromID } from "./CFG"; @@ -18,6 +18,10 @@ function nodeToGLSL(dag, nodeID, hookContext) { case NodeType.OPERATION: const [lID, rID] = node.dependsOn; + // if (dag.nodeTypes[lID] === NodeType.LITERAL && dag.nodeTypes[lID] === dag.nodeTypes[rID]) { + // const constantFolded = OpCodeToOperation[dag.opCodes[nodeID]](dag.values[lID], dag.values[rID]); + // if (!(constantFolded === undefined)) return constantFolded; + // } const left = nodeToGLSL(dag, lID, hookContext); const right = nodeToGLSL(dag, rID, hookContext); const opSym = OpCodeToSymbol[node.opCode]; @@ -34,9 +38,8 @@ function computeDeclarations(dag, dagOrder) { usedCount[nodeID] = (dag.usedBy[nodeID] || []).length; } - const tempName = {}; + const tempNames = {}; const declarations = []; - for (const nodeID of dagOrder) { if (dag.nodeTypes[nodeID] !== NodeType.OPERATION) { continue; @@ -44,14 +47,14 @@ function computeDeclarations(dag, dagOrder) { if (usedCount[nodeID] > 1) { const tmp = `t${globalTempCounter++}`; - tempName[nodeID] = tmp; + tempNames[nodeID] = tmp; const expr = nodeToGLSL(dag, nodeID, {}); declarations.push(`float ${tmp} = ${expr};`); } } - return { declarations, tempName }; + return { declarations, tempNames }; } const cfgHandlers = { @@ -72,44 +75,48 @@ export function generateGLSL(strandsContext) { const dagSorted = dfsPostOrder(dag.dependsOn, rootNodeID); const cfgSorted = dfsPostOrder(cfg.outgoingEdges, entryBlockID).reverse(); - console.log("BLOCK ORDER: ", cfgSorted.map(id => getBlockDataFromID(cfg, id))); + console.log("BLOCK ORDER: ", cfgSorted.map(id => { + const node = getBlockDataFromID(cfg, id); + node.blockType = BlockTypeToName[node.blockType]; + return node; + } + )); const hookContext = { ...computeDeclarations(dag, dagSorted), indent: 0, - currentBlock: cfgSorted[0] }; let indent = 0; - let nested = 1; let codeLines = hookContext.declarations.map((decl) => pad() + decl); const write = (line) => codeLines.push(' '.repeat(indent) + line); - cfgSorted.forEach((blockID, i) => { + cfgSorted.forEach((blockID) => { const type = cfg.blockTypes[blockID]; - const nextID = cfgSorted[i + 1]; - const nextType = cfg.blockTypes[nextID]; - switch (type) { - case BlockType.COND: + case BlockType.CONDITION: const condID = strandsContext.blockConditions[blockID]; const condExpr = nodeToGLSL(dag, condID, hookContext); write(`if (${condExpr}) {`) indent++; return; + // case BlockType.ELSE_BODY: + // write('else {'); + // indent++; + // return; case BlockType.MERGE: indent--; - write('MERGE'); write('}'); return; default: - const instructions = new Set(cfg.blockInstructions[blockID] || []); + const blockInstructions = new Set(cfg.blockInstructions[blockID] || []); + console.log(blockID, blockInstructions); for (let nodeID of dagSorted) { - if (!instructions.has(nodeID)) { + if (!blockInstructions.has(nodeID)) { continue; } - const snippet = hookContext.tempName[nodeID] - ? hookContext.tempName[nodeID] + const snippet = hookContext.tempNames[nodeID] + ? hookContext.tempNames[nodeID] : nodeToGLSL(dag, nodeID, hookContext); write(snippet); } diff --git a/src/strands/p5.strands.js b/src/strands/p5.strands.js index f72bba9f41..b3bc462d61 100644 --- a/src/strands/p5.strands.js +++ b/src/strands/p5.strands.js @@ -125,29 +125,38 @@ function strands(p5, fn) { return new StrandsNode(id); } - fn.strandsIf = function(conditionNode, ifBody) { - const { cfg } = strandsContext; + fn.strandsIf = function(conditionNode, ifBody, elseBody) { + const { cfg } = strandsContext; + const mergeBlock = CFG.createBasicBlock(cfg, BlockType.MERGE); - const conditionBlock = CFG.createBasicBlock(cfg, BlockType.COND); + const conditionBlock = CFG.createBasicBlock(cfg, BlockType.CONDITION); pushBlockWithEdgeFromCurrent(conditionBlock); strandsContext.blockConditions[conditionBlock] = conditionNode.id; - const thenBlock = CFG.createBasicBlock(cfg, BlockType.IF); - pushBlockWithEdgeFromCurrent(thenBlock); + const ifBodyBlock = CFG.createBasicBlock(cfg, BlockType.IF_BODY); + pushBlockWithEdgeFromCurrent(ifBodyBlock); ifBody(); - - const mergeBlock = CFG.createBasicBlock(cfg, BlockType.MERGE); - if (strandsContext.currentBlock !== thenBlock) { - const nestedBlock = strandsContext.currentBlock; - CFG.addEdge(cfg, nestedBlock, mergeBlock); - // Pop the previous merge! + if (strandsContext.currentBlock !== ifBodyBlock) { + CFG.addEdge(cfg, strandsContext.currentBlock, mergeBlock); popBlock(); } - // Pop the thenBlock after checking + popBlock(); + + const elseBodyBlock = CFG.createBasicBlock(cfg, BlockType.ELSE_BODY); + pushBlock(elseBodyBlock); + CFG.addEdge(cfg, conditionBlock, elseBodyBlock); + if (elseBody) { + elseBody(); + if (strandsContext.currentBlock !== ifBodyBlock) { + CFG.addEdge(cfg, strandsContext.currentBlock, mergeBlock); + popBlock(); + } + } popBlock(); pushBlock(mergeBlock); - CFG.addEdge(cfg, conditionBlock, mergeBlock); + CFG.addEdge(cfg, elseBodyBlock, mergeBlock); + CFG.addEdge(cfg, ifBodyBlock, mergeBlock); } function createHookArguments(parameters){ diff --git a/src/strands/utils.js b/src/strands/utils.js index 2b2ee88621..66ed42c03f 100644 --- a/src/strands/utils.js +++ b/src/strands/utils.js @@ -98,27 +98,50 @@ export const OperatorTable = [ { arity: "binary", name: "or", symbol: "||", opcode: OpCode.Binary.LOGICAL_OR }, ]; +const BinaryOperations = { + "+": (a, b) => a + b, + "-": (a, b) => a - b, + "*": (a, b) => a * b, + "/": (a, b) => a / b, + "%": (a, b) => a % b, + "==": (a, b) => a == b, + "!=": (a, b) => a != b, + ">": (a, b) => a > b, + ">=": (a, b) => a >= b, + "<": (a, b) => a < b, + "<=": (a, b) => a <= b, + "&&": (a, b) => a && b, + "||": (a, b) => a || b, +}; + export const SymbolToOpCode = {}; export const OpCodeToSymbol = {}; export const OpCodeArgs = {}; +export const OpCodeToOperation = {}; -for (const { arity: args, symbol, opcode } of OperatorTable) { +for (const { arity, symbol, opcode } of OperatorTable) { SymbolToOpCode[symbol] = opcode; OpCodeToSymbol[opcode] = symbol; OpCodeArgs[opcode] = args; + if (arity === "binary" && BinaryOperations[symbol]) { + OpCodeToOperation[opcode] = BinaryOperations[symbol]; + } } export const BlockType = { GLOBAL: 0, - IF: 1, - ELSE_IF: 2, - ELSE: 3, - FOR: 4, - MERGE: 5, - COND: 6, - FUNCTION: 7 + FUNCTION: 1, + IF_BODY: 2, + ELSE_BODY: 3, + EL_IF_BODY: 4, + CONDITION: 5, + FOR: 6, + MERGE: 7, } +export const BlockTypeToName = Object.fromEntries( + Object.entries(BlockType).map(([key, val]) => [val, key]) +); //////////////////////////// // Graph utils @@ -132,7 +155,7 @@ export function dfsPostOrder(adjacencyList, start) { return; } visited.add(v); - for (let w of adjacencyList[v] || []) { + for (let w of adjacencyList[v].sort((a, b) => b-a) || []) { dfs(w); } postOrder.push(v); From a355416818817c9bc0352b397215ff2ee73935a4 Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Wed, 9 Jul 2025 18:16:37 +0100 Subject: [PATCH 06/69] change if/elseif/else api to be chainable and functional (return assignments) --- preview/global/sketch.js | 10 +- src/strands/CFG.js | 8 ++ src/strands/GLSL_generator.js | 9 -- src/strands/p5.strands.js | 145 ++++++++++++++++++++++------ src/strands/strands_conditionals.js | 61 ++++++++++++ 5 files changed, 192 insertions(+), 41 deletions(-) create mode 100644 src/strands/strands_conditionals.js diff --git a/preview/global/sketch.js b/preview/global/sketch.js index 486d553d6f..fe73718b0d 100644 --- a/preview/global/sketch.js +++ b/preview/global/sketch.js @@ -6,8 +6,14 @@ function callback() { let x = createFloat(2.5); strandsIf(x.greaterThan(createFloat(0.0)), () => { - x = createFloat(100); - }); + return {x: createFloat(100)} + }).Else(); + // strandsIf(x.greaterThan(createFloat(0.0)), () => { + // strandsIf(x.greaterThan(createFloat(0.0)), () => { + // return x = createFloat(100); + // }); + // return x = createFloat(100); + // }); return x; }); diff --git a/src/strands/CFG.js b/src/strands/CFG.js index 5b8fa9ac96..f15f033443 100644 --- a/src/strands/CFG.js +++ b/src/strands/CFG.js @@ -1,3 +1,5 @@ +import { BlockTypeToName } from "./utils"; + export function createControlFlowGraph() { return { nextID: 0, @@ -36,4 +38,10 @@ export function getBlockDataFromID(graph, id) { outgoingEdges: graph.outgoingEdges[id], blockInstructions: graph.blockInstructions[id], } +} + +export function printBlockData(graph, id) { + const block = getBlockDataFromID(graph, id); + block.blockType = BlockTypeToName[block.blockType]; + console.log(block); } \ No newline at end of file diff --git a/src/strands/GLSL_generator.js b/src/strands/GLSL_generator.js index 400789cf43..1ac3a34103 100644 --- a/src/strands/GLSL_generator.js +++ b/src/strands/GLSL_generator.js @@ -1,6 +1,5 @@ import { dfsPostOrder, NodeType, OpCodeToSymbol, BlockType, OpCodeToOperation, BlockTypeToName } from "./utils"; import { getNodeDataFromID } from "./DAG"; -import { getBlockDataFromID } from "./CFG"; let globalTempCounter = 0; @@ -75,13 +74,6 @@ export function generateGLSL(strandsContext) { const dagSorted = dfsPostOrder(dag.dependsOn, rootNodeID); const cfgSorted = dfsPostOrder(cfg.outgoingEdges, entryBlockID).reverse(); - console.log("BLOCK ORDER: ", cfgSorted.map(id => { - const node = getBlockDataFromID(cfg, id); - node.blockType = BlockTypeToName[node.blockType]; - return node; - } - )); - const hookContext = { ...computeDeclarations(dag, dagSorted), indent: 0, @@ -110,7 +102,6 @@ export function generateGLSL(strandsContext) { return; default: const blockInstructions = new Set(cfg.blockInstructions[blockID] || []); - console.log(blockID, blockInstructions); for (let nodeID of dagSorted) { if (!blockInstructions.has(nodeID)) { continue; diff --git a/src/strands/p5.strands.js b/src/strands/p5.strands.js index b3bc462d61..908a9a85a1 100644 --- a/src/strands/p5.strands.js +++ b/src/strands/p5.strands.js @@ -56,12 +56,12 @@ function strands(p5, fn) { return new StrandsNode(id); }; } - if (arity === 'unary') { - StrandsNode.prototype[name] = function () { - const id = NaN; //createUnaryExpressionNode(this, SymbolToOpCode[symbol]); - return new StrandsNode(id); - }; - } + // if (arity === 'unary') { + // StrandsNode.prototype[name] = function () { + // const id = createUnaryExpressionNode(this, SymbolToOpCode[symbol]); + // return new StrandsNode(id); + // }; + // } } function createLiteralNode(dataType, value) { @@ -124,40 +124,125 @@ function strands(p5, fn) { const id = createLiteralNode(DataType.FLOAT, value); return new StrandsNode(id); } - - fn.strandsIf = function(conditionNode, ifBody, elseBody) { - const { cfg } = strandsContext; - const mergeBlock = CFG.createBasicBlock(cfg, BlockType.MERGE); + + class StrandsConditional { + constructor(condition, branchCallback) { + // Condition must be a node... + this.branches = [{ + condition, + branchCallback, + blockType: BlockType.IF_BODY + }]; + } - const conditionBlock = CFG.createBasicBlock(cfg, BlockType.CONDITION); - pushBlockWithEdgeFromCurrent(conditionBlock); - strandsContext.blockConditions[conditionBlock] = conditionNode.id; + ElseIf(condition, branchCallback) { + this.branches.push({ + condition, + branchCallback, + blockType: BlockType.EL_IF_BODY + }); + return this; + } - const ifBodyBlock = CFG.createBasicBlock(cfg, BlockType.IF_BODY); - pushBlockWithEdgeFromCurrent(ifBodyBlock); - ifBody(); - if (strandsContext.currentBlock !== ifBodyBlock) { - CFG.addEdge(cfg, strandsContext.currentBlock, mergeBlock); - popBlock(); + Else(branchCallback = () => ({})) { + this.branches.push({ + condition: null, + branchCallback, + blockType: BlockType.ELSE_BODY + }); + return buildConditional(this); } - popBlock(); + } + + function buildConditional(conditional) { + const { blockConditions, cfg } = strandsContext; + const branches = conditional.branches; + const mergeBlock = CFG.createBasicBlock(cfg, BlockType.MERGE); + const allResults = []; + // First conditional connects from outer block, everything else + // connects to previous condition (when false) + let prevCondition = strandsContext.currentBlock - const elseBodyBlock = CFG.createBasicBlock(cfg, BlockType.ELSE_BODY); - pushBlock(elseBodyBlock); - CFG.addEdge(cfg, conditionBlock, elseBodyBlock); - if (elseBody) { - elseBody(); - if (strandsContext.currentBlock !== ifBodyBlock) { + for (let i = 0; i < branches.length; i++) { + console.log(branches[i]); + const { condition, branchCallback, blockType } = branches[i]; + const isElseBlock = (i === branches.length - 1); + + if (!isElseBlock) { + const conditionBlock = CFG.createBasicBlock(cfg, BlockType.CONDITION); + CFG.addEdge(cfg, prevCondition, conditionBlock); + pushBlock(conditionBlock); + blockConditions[conditionBlock] = condition.id; + prevCondition = conditionBlock; + popBlock(); + } + + const branchBlock = CFG.createBasicBlock(cfg, blockType); + CFG.addEdge(cfg, prevCondition, branchBlock); + + pushBlock(branchBlock); + const branchResults = branchCallback(); + allResults.push(branchResults); + if (strandsContext.currentBlock !== branchBlock) { CFG.addEdge(cfg, strandsContext.currentBlock, mergeBlock); popBlock(); } + CFG.addEdge(cfg, strandsContext.currentBlock, mergeBlock); + popBlock(); } - popBlock(); - pushBlock(mergeBlock); - CFG.addEdge(cfg, elseBodyBlock, mergeBlock); - CFG.addEdge(cfg, ifBodyBlock, mergeBlock); + + return allResults; } + + + fn.strandsIf = function(conditionNode, ifBody) { + return new StrandsConditional(conditionNode, ifBody); + } + // fn.strandsIf = function(conditionNode, ifBody, elseBody) { + // const { cfg } = strandsContext; + + // console.log('Before if:', strandsContext.blockStack) + // strandsContext.blockStack.forEach(block => { + // CFG.printBlockData(cfg, block) + // }) + + // const mergeBlock = CFG.createBasicBlock(cfg, BlockType.MERGE); + + // const conditionBlock = CFG.createBasicBlock(cfg, BlockType.CONDITION); + // pushBlockWithEdgeFromCurrent(conditionBlock); + // strandsContext.blockConditions[conditionBlock] = conditionNode.id; + + // const ifBodyBlock = CFG.createBasicBlock(cfg, BlockType.IF_BODY); + // pushBlockWithEdgeFromCurrent(ifBodyBlock); + // ifBody(); + // if (strandsContext.currentBlock !== ifBodyBlock) { + // CFG.addEdge(cfg, strandsContext.currentBlock, mergeBlock); + // popBlock(); + // } + // popBlock(); + + // const elseBodyBlock = CFG.createBasicBlock(cfg, BlockType.ELSE_BODY); + // pushBlock(elseBodyBlock); + // CFG.addEdge(cfg, conditionBlock, elseBodyBlock); + // if (elseBody) { + // elseBody(); + // if (strandsContext.currentBlock !== ifBodyBlock) { + // CFG.addEdge(cfg, strandsContext.currentBlock, mergeBlock); + // popBlock(); + // } + // } + // popBlock(); + // popBlock(); + + // pushBlock(mergeBlock); + // console.log('After if:', strandsContext.blockStack) + // strandsContext.blockStack.forEach(block => { + // CFG.printBlockData(cfg, block) + // }) + // CFG.addEdge(cfg, elseBodyBlock, mergeBlock); + // CFG.addEdge(cfg, ifBodyBlock, mergeBlock); + // } function createHookArguments(parameters){ const structTypes = ['Vertex', ] diff --git a/src/strands/strands_conditionals.js b/src/strands/strands_conditionals.js new file mode 100644 index 0000000000..8ff9329348 --- /dev/null +++ b/src/strands/strands_conditionals.js @@ -0,0 +1,61 @@ +import * as CFG from './CFG' +import { BlockType } from './utils'; + +export class StrandsConditional { + constructor(condition, branchCallback) { + // Condition must be a node... + this.branches = [{ + condition, + branchCallback, + blockType: BlockType.IF_BODY + }]; + } + + ElseIf(condition, branchCallback) { + this.branches.push({ condition, branchCallback, blockType: BlockType.EL_IF_BODY }); + return this; + } + + Else(branchCallback = () => ({})) { + this.branches.push({ condition, branchCallback: null, blockType: BlockType.ELSE_BODY }); + return buildConditional(this); + } +} + +function buildConditional(conditional) { + const { blockConditions, cfg } = strandsContext; + const branches = conditional.branches; + const mergeBlock = CFG.createBasicBlock(cfg, BlockType.MERGE); + + // First conditional connects from outer block, everything else + // connects to previous condition (when false) + let prevCondition = strandsContext.currentBlock + + for (let i = 0; i < branches.length; i++) { + const { condition, branchCallback, blockType } = branches[i]; + const isElseBlock = (i === branches.length - 1); + + if (!isElseBlock) { + const conditionBlock = CFG.createBasicBlock(cfg, BlockType.CONDITION); + CFG.addEdge(cfg, prevCondition, conditionBlock); + pushBlock(conditionBlock); + blockConditions[conditionBlock] = condition.id; + prevCondition = conditionBlock; + popBlock(); + } + + const branchBlock = CFG.createBasicBlock(cfg, blockType); + CFG.addEdge(cfg, prevCondition, branchBlock); + + pushBlock(branchBlock); + const branchResults = branchCallback(); + allResults.push(branchResults); + if (strandsContext.currentBlock !== branchBlock) { + CFG.addEdge(cfg, strandsContext.currentBlock, mergeBlock); + popBlock(); + } + CFG.addEdge(cfg, strandsContext.currentBlock, mergeBlock); + popBlock(); + } + pushBlock(mergeBlock); +} \ No newline at end of file From 3e1e1492ce15ce1ded59540b5d1489f043dffcf3 Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Wed, 16 Jul 2025 10:25:38 +0100 Subject: [PATCH 07/69] binary ops and contructors prototyped --- preview/global/sketch.js | 15 +- src/strands/GLSL_backend.js | 110 +++++++ src/strands/GLSL_generator.js | 123 ------- src/strands/builder.js | 175 ++++++++++ src/strands/code_generation.js | 67 ++++ src/strands/code_transpiler.js | 2 - src/strands/{CFG.js => control_flow_graph.js} | 19 +- .../{DAG.js => directed_acyclic_graph.js} | 28 +- src/strands/p5.strands.js | 310 ++---------------- src/strands/shader_functions.js | 83 +++++ src/strands/strands_FES.js | 9 +- src/strands/strands_conditionals.js | 70 ++-- src/strands/user_API.js | 176 ++++++++++ src/strands/utils.js | 131 +++++++- src/webgl/ShaderGenerator.js | 22 +- 15 files changed, 863 insertions(+), 477 deletions(-) create mode 100644 src/strands/GLSL_backend.js delete mode 100644 src/strands/GLSL_generator.js create mode 100644 src/strands/builder.js create mode 100644 src/strands/code_generation.js rename src/strands/{CFG.js => control_flow_graph.js} (74%) rename src/strands/{DAG.js => directed_acyclic_graph.js} (73%) create mode 100644 src/strands/shader_functions.js create mode 100644 src/strands/user_API.js diff --git a/preview/global/sketch.js b/preview/global/sketch.js index fe73718b0d..e8480e10b4 100644 --- a/preview/global/sketch.js +++ b/preview/global/sketch.js @@ -3,19 +3,9 @@ p5.disableFriendlyErrors = true; function callback() { getFinalColor((col) => { - let x = createFloat(2.5); - strandsIf(x.greaterThan(createFloat(0.0)), () => { - return {x: createFloat(100)} - }).Else(); - // strandsIf(x.greaterThan(createFloat(0.0)), () => { - // strandsIf(x.greaterThan(createFloat(0.0)), () => { - // return x = createFloat(100); - // }); - // return x = createFloat(100); - // }); - - return x; + // return vec3(1, 2, 4).add(float(2.0).sub(10)); + return (float(10).sub(10)); }); } @@ -25,4 +15,5 @@ async function setup(){ } function draw(){ + } diff --git a/src/strands/GLSL_backend.js b/src/strands/GLSL_backend.js new file mode 100644 index 0000000000..1723291280 --- /dev/null +++ b/src/strands/GLSL_backend.js @@ -0,0 +1,110 @@ +import { NodeType, OpCodeToSymbol, BlockType, OpCode, DataType, DataTypeName} from "./utils"; +import { getNodeDataFromID } from "./directed_acyclic_graph"; +import * as FES from './strands_FES' + +const cfgHandlers = { + [BlockType.DEFAULT]: (blockID, strandsContext, generationContext) => { + const { dag, cfg } = strandsContext; + + const blockInstructions = new Set(cfg.blockInstructions[blockID] || []); + for (let nodeID of generationContext.dagSorted) { + if (!blockInstructions.has(nodeID)) { + continue; + } + // const snippet = glslBackend.generateExpression(dag, nodeID, generationContext); + // generationContext.write(snippet); + } + }, + + [BlockType.IF_COND](blockID, strandsContext, generationContext) { + const { dag, cfg } = strandsContext; + const conditionID = cfg.blockConditions[blockID]; + const condExpr = glslBackend.generateExpression (dag, conditionID, generationContext); + generationContext.write(`if (${condExpr}) {`) + generationContext.indent++; + this[BlockType.DEFAULT](blockID, strandsContext, generationContext); + generationContext.indent--; + generationContext.write(`}`) + return; + }, + + [BlockType.IF_BODY](blockID, strandsContext, generationContext) { + + }, + + [BlockType.ELIF_BODY](blockID, strandsContext, generationContext) { + + }, + + [BlockType.ELSE_BODY](blockID, strandsContext, generationContext) { + + }, + + [BlockType.MERGE](blockID, strandsContext, generationContext) { + + }, + + [BlockType.FUNCTION](blockID, strandsContext, generationContext) { + this[BlockType.DEFAULT](blockID, strandsContext, generationContext); + }, +} + + +export const glslBackend = { + hookEntry(hookType) { + const firstLine = `(${hookType.parameters.flatMap((param) => { + return `${param.qualifiers?.length ? param.qualifiers.join(' ') : ''}${param.type.typeName} ${param.name}`; + }).join(', ')}) {`; + return firstLine; + }, + generateDataTypeName(dataType) { + return DataTypeName[dataType]; + }, + generateDeclaration() { + + }, + generateExpression(dag, nodeID, generationContext) { + const node = getNodeDataFromID(dag, nodeID); + if (generationContext.tempNames?.[nodeID]) { + return generationContext.tempNames[nodeID]; + } + switch (node.nodeType) { + case NodeType.LITERAL: + return node.value.toFixed(4); + + case NodeType.VARIABLE: + return node.identifier; + + case NodeType.OPERATION: + if (node.opCode === OpCode.Nary.CONSTRUCTOR) { + const T = this.generateDataTypeName(node.dataType); + const deps = node.dependsOn.map((dep) => this.generateExpression(dag, dep, generationContext)); + return `${T}(${deps.join(', ')})`; + } + if (node.opCode === OpCode.Nary.FUNCTION) { + return "functioncall!"; + } + if (node.dependsOn.length === 2) { + const [lID, rID] = node.dependsOn; + const left = this.generateExpression(dag, lID, generationContext); + const right = this.generateExpression(dag, rID, generationContext); + const opSym = OpCodeToSymbol[node.opCode]; + return `${left} ${opSym} ${right}`; + } + if (node.dependsOn.length === 1) { + const [i] = node.dependsOn; + const val = this.generateExpression(dag, i, generationContext); + const sym = OpCodeToSymbol[node.opCode]; + return `${sym}${val}`; + } + + default: + FES.internalError(`${node.nodeType} not working yet`) + } + }, + generateBlock(blockID, strandsContext, generationContext) { + const type = strandsContext.cfg.blockTypes[blockID]; + const handler = cfgHandlers[type] || cfgHandlers[BlockType.DEFAULT]; + handler.call(cfgHandlers, blockID, strandsContext, generationContext); + } +} diff --git a/src/strands/GLSL_generator.js b/src/strands/GLSL_generator.js deleted file mode 100644 index 1ac3a34103..0000000000 --- a/src/strands/GLSL_generator.js +++ /dev/null @@ -1,123 +0,0 @@ -import { dfsPostOrder, NodeType, OpCodeToSymbol, BlockType, OpCodeToOperation, BlockTypeToName } from "./utils"; -import { getNodeDataFromID } from "./DAG"; - -let globalTempCounter = 0; - -function nodeToGLSL(dag, nodeID, hookContext) { - const node = getNodeDataFromID(dag, nodeID); - if (hookContext.tempName?.[nodeID]) { - return hookContext.tempName[nodeID]; - } - switch (node.nodeType) { - case NodeType.LITERAL: - return node.value.toFixed(4); - - case NodeType.VARIABLE: - return node.identifier; - - case NodeType.OPERATION: - const [lID, rID] = node.dependsOn; - // if (dag.nodeTypes[lID] === NodeType.LITERAL && dag.nodeTypes[lID] === dag.nodeTypes[rID]) { - // const constantFolded = OpCodeToOperation[dag.opCodes[nodeID]](dag.values[lID], dag.values[rID]); - // if (!(constantFolded === undefined)) return constantFolded; - // } - const left = nodeToGLSL(dag, lID, hookContext); - const right = nodeToGLSL(dag, rID, hookContext); - const opSym = OpCodeToSymbol[node.opCode]; - return `(${left} ${opSym} ${right})`; - - default: - throw new Error(`${node.nodeType} not working yet`); - } -} - -function computeDeclarations(dag, dagOrder) { - const usedCount = {}; - for (const nodeID of dagOrder) { - usedCount[nodeID] = (dag.usedBy[nodeID] || []).length; - } - - const tempNames = {}; - const declarations = []; - for (const nodeID of dagOrder) { - if (dag.nodeTypes[nodeID] !== NodeType.OPERATION) { - continue; - } - - if (usedCount[nodeID] > 1) { - const tmp = `t${globalTempCounter++}`; - tempNames[nodeID] = tmp; - - const expr = nodeToGLSL(dag, nodeID, {}); - declarations.push(`float ${tmp} = ${expr};`); - } - } - - return { declarations, tempNames }; -} - -const cfgHandlers = { - Condition(strandsContext, hookContext) { - const conditionID = strandsContext.blockConditions[blockID]; - const condExpr = nodeToGLSL(dag, conditionID, hookContext); - write(`if (${condExpr}) {`) - indent++; - return; - } -} - -export function generateGLSL(strandsContext) { - const hooksObj = {}; - - for (const { hookType, entryBlockID, rootNodeID} of strandsContext.hooks) { - const { cfg, dag } = strandsContext; - const dagSorted = dfsPostOrder(dag.dependsOn, rootNodeID); - const cfgSorted = dfsPostOrder(cfg.outgoingEdges, entryBlockID).reverse(); - - const hookContext = { - ...computeDeclarations(dag, dagSorted), - indent: 0, - }; - - let indent = 0; - let codeLines = hookContext.declarations.map((decl) => pad() + decl); - const write = (line) => codeLines.push(' '.repeat(indent) + line); - - cfgSorted.forEach((blockID) => { - const type = cfg.blockTypes[blockID]; - switch (type) { - case BlockType.CONDITION: - const condID = strandsContext.blockConditions[blockID]; - const condExpr = nodeToGLSL(dag, condID, hookContext); - write(`if (${condExpr}) {`) - indent++; - return; - // case BlockType.ELSE_BODY: - // write('else {'); - // indent++; - // return; - case BlockType.MERGE: - indent--; - write('}'); - return; - default: - const blockInstructions = new Set(cfg.blockInstructions[blockID] || []); - for (let nodeID of dagSorted) { - if (!blockInstructions.has(nodeID)) { - continue; - } - const snippet = hookContext.tempNames[nodeID] - ? hookContext.tempNames[nodeID] - : nodeToGLSL(dag, nodeID, hookContext); - write(snippet); - } - } - }); - - const finalExpression = `return ${nodeToGLSL(dag, rootNodeID, hookContext)};`; - write(finalExpression); - hooksObj[hookType.name] = codeLines.join('\n'); - } - - return hooksObj; -} \ No newline at end of file diff --git a/src/strands/builder.js b/src/strands/builder.js new file mode 100644 index 0000000000..3459f5f7ed --- /dev/null +++ b/src/strands/builder.js @@ -0,0 +1,175 @@ +import * as DAG from './directed_acyclic_graph' +import * as CFG from './control_flow_graph' +import * as FES from './strands_FES' +import { DataType, DataTypeInfo, NodeType, OpCode, DataTypeName} from './utils'; +import { StrandsNode } from './user_API'; + +////////////////////////////////////////////// +// Builders for node graphs +////////////////////////////////////////////// +export function createLiteralNode(strandsContext, typeInfo, value) { + const { cfg, dag } = strandsContext + const nodeData = DAG.createNodeData({ + nodeType: NodeType.LITERAL, + dataType, + value + }); + const id = DAG.getOrCreateNode(dag, nodeData); + CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); + return id; +} + +export function createVariableNode(strandsContext, typeInfo, identifier) { + const { cfg, dag } = strandsContext; + const nodeData = DAG.createNodeData({ + nodeType: NodeType.VARIABLE, + dataType, + identifier + }) + const id = DAG.getOrCreateNode(dag, nodeData); + CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); + return id; +} + +export function createBinaryOpNode(strandsContext, leftNode, rightArg, opCode) { + const { dag, cfg } = strandsContext; + + let inferRightType, rightNodeID, rightNode; + if (rightArg instanceof StrandsNode) { + rightNode = rightArg; + rightNodeID = rightArg.id; + inferRightType = dag.dataTypes[rightNodeID]; + } else { + const rightDependsOn = Array.isArray(rightArg) ? rightArg : [rightArg]; + inferRightType = DataType.DEFER; + rightNodeID = createTypeConstructorNode(strandsContext, inferRightType, rightDependsOn); + rightNode = new StrandsNode(rightNodeID); + } + const origRightType = inferRightType; + const leftNodeID = leftNode.id; + const origLeftType = dag.dataTypes[leftNodeID]; + + + const cast = { node: null, toType: origLeftType }; + // Check if we have to cast either node + if (origLeftType !== origRightType) { + const L = DataTypeInfo[origLeftType]; + const R = DataTypeInfo[origRightType]; + + if (L.base === DataType.DEFER) { + L.dimension = dag.dependsOn[leftNodeID].length; + } + if (R.base === DataType.DEFER) { + R.dimension = dag.dependsOn[rightNodeID].length; + } + + if (L.dimension === 1 && R.dimension > 1) { + // e.g. op(scalar, vector): cast scalar up + cast.node = leftNode; + cast.toType = origRightType; + } + else if (R.dimension === 1 && L.dimension > 1) { + cast.node = rightNode; + cast.toType = origLeftType; + } + else if (L.priority > R.priority && L.dimension === R.dimension) { + // e.g. op(float vector, int vector): cast priority is float > int > bool + cast.node = rightNode; + cast.toType = origLeftType; + } + else if (R.priority > L.priority && L.dimension === R.dimension) { + cast.node = leftNode; + cast.toType = origRightType; + } + else { + FES.userError('type error', `A vector of length ${L.dimension} operated with a vector of length ${R.dimension} is not allowed.`); + } + const castedID = createTypeConstructorNode(strandsContext, cast.toType, cast.node); + if (cast.node === leftNode) { + leftNodeID = castedID; + } else { + rightNodeID = castedID; + } + } + + const nodeData = DAG.createNodeData({ + nodeType: NodeType.OPERATION, + dependsOn: [leftNodeID, rightNodeID], + dataType: cast.toType, + opCode + }); + const id = DAG.getOrCreateNode(dag, nodeData); + CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); + return id; +} + +function mapConstructorDependencies(strandsContext, typeInfo, dependsOn) { + const mapped = []; + const T = DataTypeInfo[dataType]; + const dag = strandsContext.dag; + let calculatedDimensions = 0; + + for (const dep of dependsOn.flat()) { + if (dep instanceof StrandsNode) { + const node = DAG.getNodeDataFromID(dag, dep.id); + + if (node.opCode === OpCode.Nary.CONSTRUCTOR && dataType === dataType) { + for (const inner of node.dependsOn) { + mapped.push(inner); + } + } + const depDataType = dag.dataTypes[dep.id]; + calculatedDimensions += DataTypeInfo[depDataType].dimension; + continue; + } + if (typeof dep === 'number') { + const newNode = createLiteralNode(strandsContext, T.base, dep); + calculatedDimensions += 1; + mapped.push(newNode); + continue; + } + else { + FES.userError('type error', `You've tried to construct a scalar or vector type with a non-numeric value: ${dep}`); + } + } + + if(calculatedDimensions !== 1 && calculatedDimensions !== T.dimension) { + FES.userError('type error', `You've tried to construct a ${DataTypeName[dataType]} with ${calculatedDimensions} components`); + } + return mapped; +} + +export function createTypeConstructorNode(strandsContext, typeInfo, dependsOn) { + const { cfg, dag } = strandsContext; + dependsOn = Array.isArray(dependsOn) ? dependsOn : [dependsOn]; + const mappedDependencies = mapConstructorDependencies(strandsContext, dataType, dependsOn); + const nodeData = DAG.createNodeData({ + nodeType: NodeType.OPERATION, + opCode: OpCode.Nary.CONSTRUCTOR, + dataType, + dependsOn: mappedDependencies + }) + const id = DAG.getOrCreateNode(dag, nodeData); + CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); + return id; +} + +export function createFunctionCallNode(strandsContext, identifier, overrides, dependsOn) { + const { cfg, dag } = strandsContext; + let dataType = dataType.DEFER; + const nodeData = DAG.createNodeData({ + nodeType: NodeType.OPERATION, + opCode: OpCode.Nary.FUNCTION_CALL, + identifier, + overrides, + dependsOn, + dataType + }) + const id = DAG.getOrCreateNode(dag, nodeData); + CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); + return id; +} + +export function createStatementNode(strandsContext, type) { + return -99; +} \ No newline at end of file diff --git a/src/strands/code_generation.js b/src/strands/code_generation.js new file mode 100644 index 0000000000..b8aba9a642 --- /dev/null +++ b/src/strands/code_generation.js @@ -0,0 +1,67 @@ +import { WEBGL } from '../core/constants'; +import { glslBackend } from './GLSL_backend'; +import { dfsPostOrder, dfsReversePostOrder, NodeType } from './utils'; + +let globalTempCounter = 0; +let backend; + +function generateTopLevelDeclarations(dag, dagOrder) { + const usedCount = {}; + for (const nodeID of dagOrder) { + usedCount[nodeID] = (dag.usedBy[nodeID] || []).length; + } + + const tempNames = {}; + const declarations = []; + for (const nodeID of dagOrder) { + if (dag.nodeTypes[nodeID] !== NodeType.OPERATION) { + continue; + } + + // if (usedCount[nodeID] > 1) { + // const tmp = `t${globalTempCounter++}`; + // tempNames[nodeID] = tmp; + + // const expr = backend.generateExpression(dag, nodeID, {}); + // declarations.push(`float ${tmp} = ${expr};`); + // } + } + + return { declarations, tempNames }; +} + +export function generateShaderCode(strandsContext) { + if (strandsContext.backend === WEBGL) { + backend = glslBackend; + } + const hooksObj = {}; + + for (const { hookType, entryBlockID, rootNodeID} of strandsContext.hooks) { + const { cfg, dag } = strandsContext; + const dagSorted = dfsPostOrder(dag.dependsOn, rootNodeID); + const cfgSorted = dfsReversePostOrder(cfg.outgoingEdges, entryBlockID); + + const generationContext = { + ...generateTopLevelDeclarations(dag, dagSorted), + indent: 1, + codeLines: [], + write(line) { + this.codeLines.push(' '.repeat(this.indent) + line); + }, + dagSorted, + }; + + generationContext.declarations.forEach(decl => generationContext.write(decl)); + for (const blockID of cfgSorted) { + backend.generateBlock(blockID, strandsContext, generationContext); + } + + const firstLine = backend.hookEntry(hookType); + const finalExpression = `return ${backend.generateExpression(dag, rootNodeID, generationContext)};`; + generationContext.write(finalExpression); + console.log(hookType); + hooksObj[hookType.name] = [firstLine, ...generationContext.codeLines, '}'].join('\n'); + } + + return hooksObj; +} \ No newline at end of file diff --git a/src/strands/code_transpiler.js b/src/strands/code_transpiler.js index 6692c574a0..a804d3dcfd 100644 --- a/src/strands/code_transpiler.js +++ b/src/strands/code_transpiler.js @@ -2,8 +2,6 @@ import { parse } from 'acorn'; import { ancestor } from 'acorn-walk'; import escodegen from 'escodegen'; -import { OperatorTable } from './utils'; - // TODO: Switch this to operator table, cleanup whole file too function replaceBinaryOperator(codeSource) { diff --git a/src/strands/CFG.js b/src/strands/control_flow_graph.js similarity index 74% rename from src/strands/CFG.js rename to src/strands/control_flow_graph.js index f15f033443..cee0f0da42 100644 --- a/src/strands/CFG.js +++ b/src/strands/control_flow_graph.js @@ -2,15 +2,30 @@ import { BlockTypeToName } from "./utils"; export function createControlFlowGraph() { return { - nextID: 0, - graphType: 'CFG', + // graph structure blockTypes: [], incomingEdges: [], outgoingEdges: [], blockInstructions: [], + // runtime data for constructing graph + nextID: 0, + blockStack: [], + blockConditions: {}, + currentBlock: -1, }; } +export function pushBlock(graph, blockID) { + graph.blockStack.push(blockID); + graph.currentBlock = blockID; +} + +export function popBlock(graph) { + graph.blockStack.pop(); + const len = graph.blockStack.length; + graph.currentBlock = graph.blockStack[len-1]; +} + export function createBasicBlock(graph, blockType) { const id = graph.nextID++; graph.blockTypes[id] = blockType; diff --git a/src/strands/DAG.js b/src/strands/directed_acyclic_graph.js similarity index 73% rename from src/strands/DAG.js rename to src/strands/directed_acyclic_graph.js index b095fe3efc..54232cc5ff 100644 --- a/src/strands/DAG.js +++ b/src/strands/directed_acyclic_graph.js @@ -2,7 +2,7 @@ import { NodeTypeRequiredFields, NodeTypeToName } from './utils' import * as FES from './strands_FES' ///////////////////////////////// -// Public functions for for strands runtime +// Public functions for strands runtime ///////////////////////////////// export function createDirectedAcyclicGraph() { @@ -11,6 +11,8 @@ export function createDirectedAcyclicGraph() { cache: new Map(), nodeTypes: [], dataTypes: [], + baseTypes: [], + dimensions: [], opCodes: [], values: [], identifiers: [], @@ -40,12 +42,14 @@ export function createNodeData(data = {}) { const node = { nodeType: data.nodeType ?? null, dataType: data.dataType ?? null, + baseType: data.baseType ?? null, + dimension: data.baseType ?? null, opCode: data.opCode ?? null, value: data.value ?? null, identifier: data.identifier ?? null, dependsOn: Array.isArray(data.dependsOn) ? data.dependsOn : [], usedBy: Array.isArray(data.usedBy) ? data.usedBy : [], - phiBlocks: Array.isArray(data.phiBlocks) ? data.phiBlocks : [] + phiBlocks: Array.isArray(data.phiBlocks) ? data.phiBlocks : [], }; validateNode(node); return node; @@ -61,6 +65,8 @@ export function getNodeDataFromID(graph, id) { dependsOn: graph.dependsOn[id], usedBy: graph.usedBy[id], phiBlocks: graph.phiBlocks[id], + dimension: graph.dimensions[id], + baseType: graph.baseTypes[id], } } @@ -77,7 +83,15 @@ function createNode(graph, node) { graph.dependsOn[id] = node.dependsOn.slice(); graph.usedBy[id] = node.usedBy; graph.phiBlocks[id] = node.phiBlocks.slice(); + + graph.baseTypes[id] = node.baseType + graph.dimensions[id] = node.dimension; + + for (const dep of node.dependsOn) { + if (!Array.isArray(graph.usedBy[dep])) { + graph.usedBy[dep] = []; + } graph.usedBy[dep].push(id); } return id; @@ -89,14 +103,18 @@ function getNodeKey(node) { } function validateNode(node){ - const requiredFields = NodeTypeRequiredFields[node.nodeType]; + const nodeType = node.nodeType; + const requiredFields = [...NodeTypeRequiredFields[nodeType], 'baseType', 'dimension']; + 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 = []; for (const field of requiredFields) { - if (node[field] === NaN) { + if (node[field] === null) { missingFields.push(field); } } if (missingFields.length > 0) { - FES.internalError(`[p5.strands internal error]: Missing fields ${missingFields.join(', ')} for a node type ${NodeTypeToName(node.nodeType)}`); + FES.internalError(`Missing fields ${missingFields.join(', ')} for a node type '${NodeTypeToName[nodeType]}'.`); } } \ No newline at end of file diff --git a/src/strands/p5.strands.js b/src/strands/p5.strands.js index 908a9a85a1..6089c21e18 100644 --- a/src/strands/p5.strands.js +++ b/src/strands/p5.strands.js @@ -4,299 +4,55 @@ * @for p5 * @requires core */ +import { WEBGL, /*WEBGPU*/ } from '../core/constants' import { transpileStrandsToJS } from './code_transpiler'; -import { DataType, NodeType, SymbolToOpCode, OperatorTable, BlockType } from './utils'; +import { BlockType } from './utils'; -import * as DAG from './DAG'; -import * as CFG from './CFG' -import { generateGLSL } from './GLSL_generator'; +import { createDirectedAcyclicGraph } from './directed_acyclic_graph' +import { createControlFlowGraph, createBasicBlock, pushBlock, popBlock } from './control_flow_graph'; +import { generateShaderCode } from './code_generation'; +import { initGlobalStrandsAPI, initShaderHooksFunctions } from './user_API'; function strands(p5, fn) { ////////////////////////////////////////////// // Global Runtime ////////////////////////////////////////////// - function initStrands(ctx) { - ctx.cfg = CFG.createControlFlowGraph(); - ctx.dag = DAG.createDirectedAcyclicGraph(); - ctx.blockStack = []; - ctx.currentBlock = -1; - ctx.blockConditions = {}; + function initStrandsContext(ctx, backend) { + ctx.dag = createDirectedAcyclicGraph(); + ctx.cfg = createControlFlowGraph(); ctx.uniforms = []; ctx.hooks = []; + ctx.backend = backend; + ctx.active = true; + ctx.previousFES = p5.disableFriendlyErrors; + p5.disableFriendlyErrors = true; } - function deinitStrands(ctx) { - Object.keys(ctx).forEach(prop => { - delete ctx[prop]; - }); - } - - // Stubs - function overrideGlobalFunctions() {} - function restoreGlobalFunctions() {} - function overrideFES() {} - function restoreFES() {} - - ////////////////////////////////////////////// - // User nodes - ////////////////////////////////////////////// - class StrandsNode { - constructor(id) { - this.id = id; - } - } - - // We augment the strands node with operations programatically - // this means methods like .add, .sub, etc can be chained - for (const { name, symbol, arity } of OperatorTable) { - if (arity === 'binary') { - StrandsNode.prototype[name] = function (rightNode) { - const id = createBinaryOpNode(this.id, rightNode.id, SymbolToOpCode[symbol]); - return new StrandsNode(id); - }; - } - // if (arity === 'unary') { - // StrandsNode.prototype[name] = function () { - // const id = createUnaryExpressionNode(this, SymbolToOpCode[symbol]); - // return new StrandsNode(id); - // }; - // } - } - - function createLiteralNode(dataType, value) { - const nodeData = DAG.createNodeData({ - nodeType: NodeType.LITERAL, - dataType, - value - }); - const id = DAG.getOrCreateNode(strandsContext.dag, nodeData); - const b = strandsContext.currentBlock; - CFG.recordInBasicBlock(strandsContext.cfg, strandsContext.currentBlock, id); - return id; - } - - function createBinaryOpNode(left, right, opCode) { - const nodeData = DAG.createNodeData({ - nodeType: NodeType.OPERATION, - dependsOn: [left, right], - opCode - }); - const id = DAG.getOrCreateNode(strandsContext.dag, nodeData); - CFG.recordInBasicBlock(strandsContext.cfg, strandsContext.currentBlock, id); - return id; - } - - function createVariableNode(dataType, identifier) { - const nodeData = DAG.createNodeData({ - nodeType: NodeType.VARIABLE, - dataType, - identifier - }) - const id = DAG.getOrCreateNode(strandsContext.dag, nodeData); - CFG.recordInBasicBlock(strandsContext.cfg, strandsContext.currentBlock, id); - return id; - } - - function pushBlockWithEdgeFromCurrent(blockID) { - CFG.addEdge(strandsContext.cfg, strandsContext.currentBlock, blockID); - pushBlock(blockID); - } - - function pushBlock(blockID) { - strandsContext.blockStack.push(blockID); - strandsContext.currentBlock = blockID; - } - - function popBlock() { - strandsContext.blockStack.pop(); - const len = strandsContext.blockStack.length; - strandsContext.currentBlock = strandsContext.blockStack[len-1]; - } - - fn.uniformFloat = function(name, defaultValue) { - const id = createVariableNode(DataType.FLOAT, name); - strandsContext.uniforms.push({ name, dataType: DataType.FLOAT, defaultValue }); - return new StrandsNode(id); - } - - fn.createFloat = function(value) { - const id = createLiteralNode(DataType.FLOAT, value); - return new StrandsNode(id); - } - - class StrandsConditional { - constructor(condition, branchCallback) { - // Condition must be a node... - this.branches = [{ - condition, - branchCallback, - blockType: BlockType.IF_BODY - }]; - } - - ElseIf(condition, branchCallback) { - this.branches.push({ - condition, - branchCallback, - blockType: BlockType.EL_IF_BODY - }); - return this; - } - - Else(branchCallback = () => ({})) { - this.branches.push({ - condition: null, - branchCallback, - blockType: BlockType.ELSE_BODY - }); - return buildConditional(this); - } - } - - function buildConditional(conditional) { - const { blockConditions, cfg } = strandsContext; - const branches = conditional.branches; - const mergeBlock = CFG.createBasicBlock(cfg, BlockType.MERGE); - const allResults = []; - // First conditional connects from outer block, everything else - // connects to previous condition (when false) - let prevCondition = strandsContext.currentBlock - - for (let i = 0; i < branches.length; i++) { - console.log(branches[i]); - const { condition, branchCallback, blockType } = branches[i]; - const isElseBlock = (i === branches.length - 1); - - if (!isElseBlock) { - const conditionBlock = CFG.createBasicBlock(cfg, BlockType.CONDITION); - CFG.addEdge(cfg, prevCondition, conditionBlock); - pushBlock(conditionBlock); - blockConditions[conditionBlock] = condition.id; - prevCondition = conditionBlock; - popBlock(); - } - - const branchBlock = CFG.createBasicBlock(cfg, blockType); - CFG.addEdge(cfg, prevCondition, branchBlock); - - pushBlock(branchBlock); - const branchResults = branchCallback(); - allResults.push(branchResults); - if (strandsContext.currentBlock !== branchBlock) { - CFG.addEdge(cfg, strandsContext.currentBlock, mergeBlock); - popBlock(); - } - CFG.addEdge(cfg, strandsContext.currentBlock, mergeBlock); - popBlock(); - } - pushBlock(mergeBlock); - - return allResults; - } - - - fn.strandsIf = function(conditionNode, ifBody) { - return new StrandsConditional(conditionNode, ifBody); + function deinitStrandsContext(ctx) { + ctx.dag = createDirectedAcyclicGraph(); + ctx.cfg = createControlFlowGraph(); + ctx.uniforms = []; + ctx.hooks = []; + p5.disableFriendlyErrors = ctx.previousFES; } - // fn.strandsIf = function(conditionNode, ifBody, elseBody) { - // const { cfg } = strandsContext; - - // console.log('Before if:', strandsContext.blockStack) - // strandsContext.blockStack.forEach(block => { - // CFG.printBlockData(cfg, block) - // }) - - // const mergeBlock = CFG.createBasicBlock(cfg, BlockType.MERGE); - - // const conditionBlock = CFG.createBasicBlock(cfg, BlockType.CONDITION); - // pushBlockWithEdgeFromCurrent(conditionBlock); - // strandsContext.blockConditions[conditionBlock] = conditionNode.id; - - // const ifBodyBlock = CFG.createBasicBlock(cfg, BlockType.IF_BODY); - // pushBlockWithEdgeFromCurrent(ifBodyBlock); - // ifBody(); - // if (strandsContext.currentBlock !== ifBodyBlock) { - // CFG.addEdge(cfg, strandsContext.currentBlock, mergeBlock); - // popBlock(); - // } - // popBlock(); - - // const elseBodyBlock = CFG.createBasicBlock(cfg, BlockType.ELSE_BODY); - // pushBlock(elseBodyBlock); - // CFG.addEdge(cfg, conditionBlock, elseBodyBlock); - // if (elseBody) { - // elseBody(); - // if (strandsContext.currentBlock !== ifBodyBlock) { - // CFG.addEdge(cfg, strandsContext.currentBlock, mergeBlock); - // popBlock(); - // } - // } - // popBlock(); - // popBlock(); - - // pushBlock(mergeBlock); - // console.log('After if:', strandsContext.blockStack) - // strandsContext.blockStack.forEach(block => { - // CFG.printBlockData(cfg, block) - // }) - // CFG.addEdge(cfg, elseBodyBlock, mergeBlock); - // CFG.addEdge(cfg, ifBodyBlock, mergeBlock); - // } - function createHookArguments(parameters){ - const structTypes = ['Vertex', ] - const args = []; - - for (const param of parameters) { - const T = param.type; - if(structTypes.includes(T.typeName)) { - const propertiesNodes = T.properties.map( - (prop) => [prop.name, createVariableNode(DataType[prop.dataType], prop.name)] - ); - const argObj = Object.fromEntries(propertiesNodes); - args.push(argObj); - } else { - const arg = createVariableNode(DataType[param.dataType], param.name); - args.push(arg) - } - } - return args; - } + const strandsContext = {}; + initStrandsContext(strandsContext); + initGlobalStrandsAPI(p5, fn, strandsContext) - function generateHookOverrides(shader) { - const availableHooks = { - ...shader.hooks.vertex, - ...shader.hooks.fragment, - } - const hookTypes = Object.keys(availableHooks).map(name => shader.hookTypes(name)); - for (const hookType of hookTypes) { - window[hookType.name] = function(callback) { - const entryBlockID = CFG.createBasicBlock(strandsContext.cfg, BlockType.FUNCTION); - pushBlockWithEdgeFromCurrent(entryBlockID); - const args = createHookArguments(hookType.parameters); - const rootNodeID = callback(args).id; - strandsContext.hooks.push({ - hookType, - entryBlockID, - rootNodeID, - }); - popBlock(); - } - } - } - ////////////////////////////////////////////// // Entry Point ////////////////////////////////////////////// - const strandsContext = {}; const oldModify = p5.Shader.prototype.modify - + p5.Shader.prototype.newModify = function(shaderModifier, options = { parser: true, srcLocations: false }) { if (shaderModifier instanceof Function) { // Reset the context object every time modify is called; - initStrands(strandsContext) - generateHookOverrides(this); + const backend = WEBGL; + initStrandsContext(strandsContext, backend); + initShaderHooksFunctions(strandsContext, fn, this); + // 1. Transpile from strands DSL to JS let strandsCallback; if (options.parser) { @@ -306,21 +62,21 @@ function strands(p5, fn) { } // 2. Build the IR from JavaScript API - const globalScope = CFG.createBasicBlock(strandsContext.cfg, BlockType.GLOBAL); - pushBlock(globalScope); + const globalScope = createBasicBlock(strandsContext.cfg, BlockType.GLOBAL); + pushBlock(strandsContext.cfg, globalScope); strandsCallback(); - popBlock(); + popBlock(strandsContext.cfg); // 3. Generate shader code hooks object from the IR // ....... - const glsl = generateGLSL(strandsContext); - console.log(glsl.getFinalColor); + const hooksObject = generateShaderCode(strandsContext); + console.log(hooksObject.getFinalColor); // Call modify with the generated hooks object // return oldModify.call(this, generatedModifyArgument); // Reset the strands runtime context - // deinitStrands(strandsContext); + // deinitStrandsContext(strandsContext); } else { return oldModify.call(this, shaderModifier) diff --git a/src/strands/shader_functions.js b/src/strands/shader_functions.js new file mode 100644 index 0000000000..1c95d0702a --- /dev/null +++ b/src/strands/shader_functions.js @@ -0,0 +1,83 @@ +// GLSL Built in functions +// https://docs.gl/el3/abs +const builtInGLSLFunctions = { + //////////// Trigonometry ////////// + 'acos': [{ args: ['genType'], returnType: 'genType', isp5Function: true}], + 'acosh': [{ args: ['genType'], returnType: 'genType', isp5Function: false}], + 'asin': [{ args: ['genType'], returnType: 'genType', isp5Function: true}], + 'asinh': [{ args: ['genType'], returnType: 'genType', isp5Function: false}], + 'atan': [ + { args: ['genType'], returnType: 'genType', isp5Function: false}, + { args: ['genType', 'genType'], returnType: 'genType', isp5Function: false}, + ], + 'atanh': [{ args: ['genType'], returnType: 'genType', isp5Function: false}], + 'cos': [{ args: ['genType'], returnType: 'genType', isp5Function: true}], + 'cosh': [{ args: ['genType'], returnType: 'genType', isp5Function: false}], + 'degrees': [{ args: ['genType'], returnType: 'genType', isp5Function: true}], + 'radians': [{ args: ['genType'], returnType: 'genType', isp5Function: true}], + 'sin': [{ args: ['genType'], returnType: 'genType' , isp5Function: true}], + 'sinh': [{ args: ['genType'], returnType: 'genType', isp5Function: false}], + 'tan': [{ args: ['genType'], returnType: 'genType', isp5Function: true}], + 'tanh': [{ args: ['genType'], returnType: 'genType', isp5Function: false}], + ////////// Mathematics ////////// + 'abs': [{ args: ['genType'], returnType: 'genType', isp5Function: true}], + 'ceil': [{ args: ['genType'], returnType: 'genType', isp5Function: true}], + 'clamp': [{ args: ['genType', 'genType', 'genType'], returnType: 'genType', isp5Function: false}], + 'dFdx': [{ args: ['genType'], returnType: 'genType', isp5Function: false}], + 'dFdy': [{ args: ['genType'], returnType: 'genType', isp5Function: false}], + 'exp': [{ args: ['genType'], returnType: 'genType', isp5Function: true}], + 'exp2': [{ args: ['genType'], returnType: 'genType', isp5Function: false}], + 'floor': [{ args: ['genType'], returnType: 'genType', isp5Function: true}], + 'fma': [{ args: ['genType', 'genType', 'genType'], returnType: 'genType', isp5Function: false}], + 'fract': [{ args: ['genType'], returnType: 'genType', isp5Function: true}], + 'fwidth': [{ args: ['genType'], returnType: 'genType', isp5Function: false}], + 'inversesqrt': [{ args: ['genType'], returnType: 'genType', isp5Function: true}], + // 'isinf': [{}], + // 'isnan': [{}], + 'log': [{ args: ['genType'], returnType: 'genType', isp5Function: true}], + 'log2': [{ args: ['genType'], returnType: 'genType', isp5Function: false}], + 'max': [ + { args: ['genType', 'genType'], returnType: 'genType', isp5Function: true}, + { args: ['genType', 'float'], returnType: 'genType', isp5Function: true}, + ], + 'min': [ + { args: ['genType', 'genType'], returnType: 'genType', isp5Function: true}, + { args: ['genType', 'float'], returnType: 'genType', isp5Function: true}, + ], + 'mix': [ + { args: ['genType', 'genType', 'genType'], returnType: 'genType', isp5Function: false}, + { args: ['genType', 'genType', 'float'], returnType: 'genType', isp5Function: false}, + ], + // 'mod': [{}], + // 'modf': [{}], + 'pow': [{ args: ['genType', 'genType'], returnType: 'genType', isp5Function: true}], + 'round': [{ args: ['genType'], returnType: 'genType', isp5Function: true}], + 'roundEven': [{ args: ['genType'], returnType: 'genType', isp5Function: false}], + // 'sign': [{}], + 'smoothstep': [ + { args: ['genType', 'genType', 'genType'], returnType: 'genType', isp5Function: false}, + { args: ['float', 'float', 'genType'], returnType: 'genType', isp5Function: false}, + ], + 'sqrt': [{ args: ['genType'], returnType: 'genType', isp5Function: true}], + 'step': [{ args: ['genType', 'genType'], returnType: 'genType', isp5Function: false}], + 'trunc': [{ args: ['genType'], returnType: 'genType', isp5Function: false}], + + ////////// Vector ////////// + 'cross': [{ args: ['vec3', 'vec3'], returnType: 'vec3', isp5Function: true}], + 'distance': [{ args: ['genType', 'genType'], returnType: 'float', isp5Function: true}], + 'dot': [{ args: ['genType', 'genType'], returnType: 'float', isp5Function: true}], + // 'equal': [{}], + 'faceforward': [{ args: ['genType', 'genType', 'genType'], returnType: 'genType', isp5Function: false}], + 'length': [{ args: ['genType'], returnType: 'float', isp5Function: false}], + 'normalize': [{ args: ['genType'], returnType: 'genType', isp5Function: true}], + // 'notEqual': [{}], + 'reflect': [{ args: ['genType', 'genType'], returnType: 'genType', isp5Function: false}], + 'refract': [{ args: ['genType', 'genType', 'float'], returnType: 'genType', isp5Function: false}], + + ////////// Texture sampling ////////// + 'texture': [{args: ['sampler2D', 'vec2'], returnType: 'vec4', isp5Function: true}], +} + +export const strandsShaderFunctions = { + ...builtInGLSLFunctions, +} \ No newline at end of file diff --git a/src/strands/strands_FES.js b/src/strands/strands_FES.js index 695b220e6a..3af0aca90b 100644 --- a/src/strands/strands_FES.js +++ b/src/strands/strands_FES.js @@ -1,4 +1,9 @@ -export function internalError(message) { - const prefixedMessage = `[p5.strands internal error]: ${message}` +export function internalError(errorMessage) { + const prefixedMessage = `[p5.strands internal error]: ${errorMessage}` + throw new Error(prefixedMessage); +} + +export function userError(errorType, errorMessage) { + const prefixedMessage = `[p5.strands ${errorType}]: ${errorMessage}`; throw new Error(prefixedMessage); } \ No newline at end of file diff --git a/src/strands/strands_conditionals.js b/src/strands/strands_conditionals.js index 8ff9329348..e1da496c02 100644 --- a/src/strands/strands_conditionals.js +++ b/src/strands/strands_conditionals.js @@ -1,61 +1,71 @@ -import * as CFG from './CFG' +import * as CFG from './control_flow_graph' import { BlockType } from './utils'; export class StrandsConditional { - constructor(condition, branchCallback) { + constructor(strandsContext, condition, branchCallback) { // Condition must be a node... this.branches = [{ condition, branchCallback, blockType: BlockType.IF_BODY }]; + this.ctx = strandsContext; } ElseIf(condition, branchCallback) { - this.branches.push({ condition, branchCallback, blockType: BlockType.EL_IF_BODY }); + this.branches.push({ + condition, + branchCallback, + blockType: BlockType.ELIF_BODY + }); return this; } Else(branchCallback = () => ({})) { - this.branches.push({ condition, branchCallback: null, blockType: BlockType.ELSE_BODY }); - return buildConditional(this); + this.branches.push({ + condition: null, + branchCallback, + blockType: BlockType.ELSE_BODY + }); + return buildConditional(this.ctx, this); } } -function buildConditional(conditional) { - const { blockConditions, cfg } = strandsContext; +function buildConditional(strandsContext, conditional) { + const cfg = strandsContext.cfg; const branches = conditional.branches; + const mergeBlock = CFG.createBasicBlock(cfg, BlockType.MERGE); + const results = []; + + let previousBlock = cfg.currentBlock; - // First conditional connects from outer block, everything else - // connects to previous condition (when false) - let prevCondition = strandsContext.currentBlock - for (let i = 0; i < branches.length; i++) { const { condition, branchCallback, blockType } = branches[i]; - const isElseBlock = (i === branches.length - 1); - - if (!isElseBlock) { - const conditionBlock = CFG.createBasicBlock(cfg, BlockType.CONDITION); - CFG.addEdge(cfg, prevCondition, conditionBlock); - pushBlock(conditionBlock); - blockConditions[conditionBlock] = condition.id; - prevCondition = conditionBlock; - popBlock(); + + if (condition !== null) { + const conditionBlock = CFG.createBasicBlock(cfg, BlockType.IF_COND); + CFG.addEdge(cfg, previousBlock, conditionBlock); + CFG.pushBlock(cfg, conditionBlock); + cfg.blockConditions[conditionBlock] = condition.id; + previousBlock = conditionBlock; + CFG.popBlock(cfg); } - + const branchBlock = CFG.createBasicBlock(cfg, blockType); - CFG.addEdge(cfg, prevCondition, branchBlock); + CFG.addEdge(cfg, previousBlock, branchBlock); - pushBlock(branchBlock); + CFG.pushBlock(cfg, branchBlock); const branchResults = branchCallback(); - allResults.push(branchResults); - if (strandsContext.currentBlock !== branchBlock) { - CFG.addEdge(cfg, strandsContext.currentBlock, mergeBlock); - popBlock(); + results.push(branchResults); + if (cfg.currentBlock !== branchBlock) { + CFG.addEdge(cfg, cfg.currentBlock, mergeBlock); + CFG.popBlock(); } - CFG.addEdge(cfg, strandsContext.currentBlock, mergeBlock); - popBlock(); + CFG.addEdge(cfg, cfg.currentBlock, mergeBlock); + CFG.popBlock(cfg); } - pushBlock(mergeBlock); + CFG.pushBlock(cfg, mergeBlock); + + return results; } \ No newline at end of file diff --git a/src/strands/user_API.js b/src/strands/user_API.js new file mode 100644 index 0000000000..3482c57fb4 --- /dev/null +++ b/src/strands/user_API.js @@ -0,0 +1,176 @@ +import { + createBinaryOpNode, + createFunctionCallNode, + createVariableNode, + createStatementNode, + createTypeConstructorNode, +} from './builder' +import { DataType, OperatorTable, SymbolToOpCode, BlockType, arrayToFloatType } from './utils' +import { strandsShaderFunctions } from './shader_functions' +import { StrandsConditional } from './strands_conditionals' +import * as CFG from './control_flow_graph' +import * as FES from './strands_FES' + +////////////////////////////////////////////// +// User nodes +////////////////////////////////////////////// +export class StrandsNode { + constructor(id) { + this.id = id; + } +} + +export function initGlobalStrandsAPI(p5, fn, strandsContext) { + // We augment the strands node with operations programatically + // this means methods like .add, .sub, etc can be chained + for (const { name, symbol, arity } of OperatorTable) { + if (arity === 'binary') { + StrandsNode.prototype[name] = function (right) { + const id = createBinaryOpNode(strandsContext, this, right, SymbolToOpCode[symbol]); + return new StrandsNode(id); + }; + } + // if (arity === 'unary') { + // StrandsNode.prototype[name] = function () { + // const id = createUnaryExpressionNode(this, SymbolToOpCode[symbol]); + // return new StrandsNode(id); + // }; + // } + } + + ////////////////////////////////////////////// + // Unique Functions + ////////////////////////////////////////////// + fn.discard = function() { + const id = createStatementNode('discard'); + CFG.recordInBasicBlock(strandsContext.cfg, strandsContext.cfg.currentBlock, id); + } + + fn.strandsIf = function(conditionNode, ifBody) { + return new StrandsConditional(strandsContext, conditionNode, ifBody); + } + + fn.strandsLoop = function(a, b, loopBody) { + return null; + } + + fn.strandsNode = function(...args) { + if (args.length > 4) { + FES.userError('type error', "It looks like you've tried to construct a p5.strands node implicitly, with more than 4 components. This is currently not supported.") + } + const id = createTypeConstructorNode(strandsContext, DataType.DEFER, args); + return new StrandsNode(id); + } + + ////////////////////////////////////////////// + // Builtins, uniforms, variable constructors + ////////////////////////////////////////////// + for (const [fnName, overrides] of Object.entries(strandsShaderFunctions)) { + const isp5Function = overrides[0].isp5Function; + + if (isp5Function) { + const originalFn = fn[fnName]; + fn[fnName] = function(...args) { + if (strandsContext.active) { + return createFunctionCallNode(strandsContext, fnName, overrides, args); + } else { + return originalFn.apply(this, args); + } + } + } else { + fn[fnName] = function (...args) { + if (strandsContext.active) { + return createFunctionCallNode(strandsContext, fnName, overrides, args); + } else { + p5._friendlyError( + `It looks like you've called ${fnName} outside of a shader's modify() function.` + ) + } + } + } + } + + // Next is type constructors and uniform functions + for (const typeName in DataType) { + const lowerTypeName = typeName.toLowerCase(); + let pascalTypeName; + if (/^[ib]vec/.test(lowerTypeName)) { + pascalTypeName = lowerTypeName + .slice(0, 2).toUpperCase() + + lowerTypeName + .slice(2) + .toLowerCase(); + } else { + pascalTypeName = lowerTypeName.charAt(0).toUpperCase() + + lowerTypeName.slice(1).toLowerCase(); + } + + fn[`uniform${pascalTypeName}`] = function(...args) { + let [name, ...defaultValue] = args; + const id = createVariableNode(strandsContext, DataType.FLOAT, name); + strandsContext.uniforms.push({ name, dataType: DataType.FLOAT, defaultValue }); + return new StrandsNode(id); + }; + + const typeConstructor = fn[lowerTypeName]; + fn[lowerTypeName] = function(...args) { + if (strandsContext.active) { + const id = createTypeConstructorNode(strandsContext, DataType[typeName], args); + return new StrandsNode(id); + } else if (typeConstructor) { + return typeConstructor.apply(this, args); + } else { + p5._friendlyError( + `It looks like you've called ${lowerTypeName} outside of a shader's modify() function.` + ); + } + } + } +} + +////////////////////////////////////////////// +// Per-Hook functions +////////////////////////////////////////////// +function createHookArguments(strandsContext, parameters){ + const structTypes = ['Vertex', ] + const args = []; + + for (const param of parameters) { + const T = param.type; + if(structTypes.includes(T.typeName)) { + const propertiesNodes = T.properties.map( + (prop) => [prop.name, createVariableNode(strandsContext, DataType[prop.dataType], prop.name)] + ); + const argObject = Object.fromEntries(propertiesNodes); + args.push(argObject); + } else { + const arg = createVariableNode(strandsContext, DataType[param.dataType], param.name); + args.push(arg) + } + } + return args; +} + +export function initShaderHooksFunctions(strandsContext, fn, shader) { + const availableHooks = { + ...shader.hooks.vertex, + ...shader.hooks.fragment, + } + const hookTypes = Object.keys(availableHooks).map(name => shader.hookTypes(name)); + const { cfg } = strandsContext; + for (const hookType of hookTypes) { + window[hookType.name] = 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 rootNodeID = hookUserCallback(args).id; + strandsContext.hooks.push({ + hookType, + entryBlockID, + rootNodeID, + }); + CFG.popBlock(cfg); + } + } +} \ No newline at end of file diff --git a/src/strands/utils.js b/src/strands/utils.js index 66ed42c03f..6f38092381 100644 --- a/src/strands/utils.js +++ b/src/strands/utils.js @@ -1,11 +1,8 @@ ///////////////////// // Enums for nodes // ///////////////////// - export const NodeType = { - // Internal Nodes: OPERATION: 0, - // Leaf Nodes LITERAL: 1, VARIABLE: 2, CONSTANT: 3, @@ -15,7 +12,7 @@ export const NodeType = { export const NodeTypeRequiredFields = { [NodeType.OPERATION]: ['opCode', 'dependsOn'], [NodeType.LITERAL]: ['value'], - [NodeType.VARIABLE]: ['identifier', 'dataType'], + [NodeType.VARIABLE]: ['identifier'], [NodeType.CONSTANT]: ['value'], [NodeType.PHI]: ['dependsOn', 'phiBlocks'] }; @@ -24,6 +21,32 @@ export const NodeTypeToName = Object.fromEntries( Object.entries(NodeType).map(([key, val]) => [val, key]) ); +export const BaseType = { + FLOAT: 'float', + INT: 'int', + BOOl: 'bool', + MAT: 'mat', + DEFER: 'deferred', +}; + +export const AllTypes = [ + 'float1', + 'float2', + 'float3', + 'float4', + 'int1', + 'int2', + 'int3', + 'int4', + 'bool1', + 'bool2', + 'bool3', + 'bool4', + 'mat2x2', + 'mat3x3', + 'mat4x4', +] + export const DataType = { FLOAT: 0, VEC2: 1, @@ -43,8 +66,54 @@ export const DataType = { MAT2X2: 300, MAT3X3: 301, MAT4X4: 302, + + DEFER: 999, +} + +export const DataTypeInfo = { + [DataType.FLOAT]: { base: DataType.FLOAT, dimension: 1, priority: 2 }, + [DataType.VEC2]: { base: DataType.FLOAT, dimension: 2, priority: 2 }, + [DataType.VEC3]: { base: DataType.FLOAT, dimension: 3, priority: 2 }, + [DataType.VEC4]: { base: DataType.FLOAT, dimension: 4, priority: 2 }, + [DataType.INT]: { base: DataType.INT, dimension: 1, priority: 1 }, + [DataType.IVEC2]: { base: DataType.INT, dimension: 2, priority: 1 }, + [DataType.IVEC3]: { base: DataType.INT, dimension: 3, priority: 1 }, + [DataType.IVEC4]: { base: DataType.INT, dimension: 4, priority: 1 }, + [DataType.BOOL]: { base: DataType.BOOL, dimension: 1, priority: 0 }, + [DataType.BVEC2]: { base: DataType.BOOL, dimension: 2, priority: 0 }, + [DataType.BVEC3]: { base: DataType.BOOL, dimension: 3, priority: 0 }, + [DataType.BVEC4]: { base: DataType.BOOL, dimension: 4, priority: 0 }, + [DataType.MAT2]: { base: DataType.FLOAT, dimension: 2, priority: -1 }, + [DataType.MAT3]: { base: DataType.FLOAT, dimension: 3, priority: -1 }, + [DataType.MAT4]: { base: DataType.FLOAT, dimension: 4, priority: -1 }, + + [DataType.DEFER]: { base: DataType.DEFER, dimension: null, priority: -2 }, + [DataType.DEFER]: { base: DataType.DEFER, dimension: null, priority: -2 }, + [DataType.DEFER]: { base: DataType.DEFER, dimension: null, priority: -2 }, + [DataType.DEFER]: { base: DataType.DEFER, dimension: null, priority: -2 }, +}; + +// 2) A separate nested lookup table: +export const DataTypeTable = { + [DataType.FLOAT]: { 1: DataType.FLOAT, 2: DataType.VEC2, 3: DataType.VEC3, 4: DataType.VEC4 }, + [DataType.INT]: { 1: DataType.INT, 2: DataType.IVEC2, 3: DataType.IVEC3, 4: DataType.IVEC4 }, + [DataType.BOOL]: { 1: DataType.BOOL, 2: DataType.BVEC2, 3: DataType.BVEC3, 4: DataType.BVEC4 }, + // [DataType.MAT2]: { 2: DataType.MAT2, 3: DataType.MAT3, 4: DataType.MAT4 }, + [DataType.DEFER]: { 0: DataType.DEFER, 1: DataType.DEFER, 2: DataType.DEFER, 3: DataType.DEFER, 4: DataType.DEFER }, +}; + +export function lookupDataType(baseCode, dim) { + const map = DataTypeTable[baseCode]; + if (!map || map[dim] == null) { + throw new Error(`Invalid type combination: base=${baseCode}, dim=${dim}`); + } + return map[dim]; } +export const DataTypeName = Object.fromEntries( + Object.entries(DataType).map(([key,val])=>[val, key.toLowerCase()]) +); + export const OpCode = { Binary: { ADD: 0, @@ -70,6 +139,7 @@ export const OpCode = { }, Nary: { FUNCTION_CALL: 200, + CONSTRUCTOR: 201, }, ControlFlow: { RETURN: 300, @@ -84,7 +154,7 @@ export const OperatorTable = [ { arity: "unary", name: "neg", symbol: "-", opcode: OpCode.Unary.NEGATE }, { arity: "unary", name: "plus", symbol: "+", opcode: OpCode.Unary.PLUS }, { arity: "binary", name: "add", symbol: "+", opcode: OpCode.Binary.ADD }, - { arity: "binary", name: "min", symbol: "-", opcode: OpCode.Binary.SUBTRACT }, + { arity: "binary", name: "sub", symbol: "-", opcode: OpCode.Binary.SUBTRACT }, { arity: "binary", name: "mult", symbol: "*", opcode: OpCode.Binary.MULTIPLY }, { arity: "binary", name: "div", symbol: "/", opcode: OpCode.Binary.DIVIDE }, { arity: "binary", name: "mod", symbol: "%", opcode: OpCode.Binary.MODULO }, @@ -114,7 +184,6 @@ const BinaryOperations = { "||": (a, b) => a || b, }; - export const SymbolToOpCode = {}; export const OpCodeToSymbol = {}; export const OpCodeArgs = {}; @@ -132,17 +201,34 @@ for (const { arity, symbol, opcode } of OperatorTable) { export const BlockType = { GLOBAL: 0, FUNCTION: 1, - IF_BODY: 2, - ELSE_BODY: 3, - EL_IF_BODY: 4, - CONDITION: 5, - FOR: 6, - MERGE: 7, + IF_COND: 2, + IF_BODY: 3, + ELIF_BODY: 4, + ELIF_COND: 5, + ELSE_BODY: 6, + FOR: 7, + MERGE: 8, + DEFAULT: 9, + } export const BlockTypeToName = Object.fromEntries( Object.entries(BlockType).map(([key, val]) => [val, key]) ); +//////////////////////////// +// Type Checking helpers +//////////////////////////// +export function arrayToFloatType(array) { + let type = false; + if (array.length === 1) { + type = `FLOAT`; + } else if (array.length >= 2 && array.length <= 4) { + type = `VEC${array.length}`; + } else { + throw new Error('Tried to construct a float / vector with and empty array, or more than 4 components!') + } +} + //////////////////////////// // Graph utils //////////////////////////// @@ -155,7 +241,7 @@ export function dfsPostOrder(adjacencyList, start) { return; } visited.add(v); - for (let w of adjacencyList[v].sort((a, b) => b-a) || []) { + for (let w of adjacencyList[v]) { dfs(w); } postOrder.push(v); @@ -163,4 +249,23 @@ export function dfsPostOrder(adjacencyList, start) { dfs(start); return postOrder; +} + +export function dfsReversePostOrder(adjacencyList, start) { + const visited = new Set(); + const postOrder = []; + + function dfs(v) { + if (visited.has(v)) { + return; + } + visited.add(v); + for (let w of adjacencyList[v].sort((a, b) => b-a) || []) { + dfs(w); + } + postOrder.push(v); + } + + dfs(start); + return postOrder.reverse(); } \ No newline at end of file diff --git a/src/webgl/ShaderGenerator.js b/src/webgl/ShaderGenerator.js index a4db0296fc..416d2d3b45 100644 --- a/src/webgl/ShaderGenerator.js +++ b/src/webgl/ShaderGenerator.js @@ -1391,17 +1391,6 @@ function shadergenerator(p5, fn) { return fnNodeConstructor('getTexture', userArgs, props); } - // Generating uniformFloat, uniformVec, createFloat, etc functions - // Maps a GLSL type to the name suffix for method names - const GLSLTypesToIdentifiers = { - int: 'Int', - float: 'Float', - vec2: 'Vector2', - vec3: 'Vector3', - vec4: 'Vector4', - sampler2D: 'Texture', - }; - function dynamicAddSwizzleTrap(node, _size) { if (node.type.startsWith('vec') || _size) { const size = _size ? _size : parseInt(node.type.slice(3)); @@ -1457,6 +1446,17 @@ function shadergenerator(p5, fn) { }, }; + // Generating uniformFloat, uniformVec, createFloat, etc functions + // Maps a GLSL type to the name suffix for method names + const GLSLTypesToIdentifiers = { + int: 'Int', + float: 'Float', + vec2: 'Vector2', + vec3: 'Vector3', + vec4: 'Vector4', + sampler2D: 'Texture', + }; + for (const glslType in GLSLTypesToIdentifiers) { // Generate uniform*() Methods for creating uniforms const typeIdentifier = GLSLTypesToIdentifiers[glslType]; From f71871762c86a1a5211c4d2670fde392e5ae3ca0 Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Wed, 16 Jul 2025 16:42:40 +0100 Subject: [PATCH 08/69] simplify type system --- preview/global/sketch.js | 4 +- src/strands/GLSL_backend.js | 10 +- src/strands/builder.js | 154 +++++++++++++++----------- src/strands/directed_acyclic_graph.js | 10 +- src/strands/user_API.js | 54 ++++----- src/strands/utils.js | 147 ++++++++---------------- 6 files changed, 176 insertions(+), 203 deletions(-) diff --git a/preview/global/sketch.js b/preview/global/sketch.js index e8480e10b4..bd019b77df 100644 --- a/preview/global/sketch.js +++ b/preview/global/sketch.js @@ -3,9 +3,7 @@ p5.disableFriendlyErrors = true; function callback() { getFinalColor((col) => { - - // return vec3(1, 2, 4).add(float(2.0).sub(10)); - return (float(10).sub(10)); + return ivec3(1, 2, 4).mult(2.0, 2, 3); }); } diff --git a/src/strands/GLSL_backend.js b/src/strands/GLSL_backend.js index 1723291280..3813465e38 100644 --- a/src/strands/GLSL_backend.js +++ b/src/strands/GLSL_backend.js @@ -1,4 +1,4 @@ -import { NodeType, OpCodeToSymbol, BlockType, OpCode, DataType, DataTypeName} from "./utils"; +import { NodeType, OpCodeToSymbol, BlockType, OpCode } from "./utils"; import { getNodeDataFromID } from "./directed_acyclic_graph"; import * as FES from './strands_FES' @@ -57,8 +57,8 @@ export const glslBackend = { }).join(', ')}) {`; return firstLine; }, - generateDataTypeName(dataType) { - return DataTypeName[dataType]; + generateDataTypeName(baseType, dimension) { + return baseType + dimension; }, generateDeclaration() { @@ -77,7 +77,7 @@ export const glslBackend = { case NodeType.OPERATION: if (node.opCode === OpCode.Nary.CONSTRUCTOR) { - const T = this.generateDataTypeName(node.dataType); + const T = this.generateDataTypeName(node.baseType, node.dimension); const deps = node.dependsOn.map((dep) => this.generateExpression(dag, dep, generationContext)); return `${T}(${deps.join(', ')})`; } @@ -89,7 +89,7 @@ export const glslBackend = { const left = this.generateExpression(dag, lID, generationContext); const right = this.generateExpression(dag, rID, generationContext); const opSym = OpCodeToSymbol[node.opCode]; - return `${left} ${opSym} ${right}`; + return `(${left} ${opSym} ${right})`; } if (node.dependsOn.length === 1) { const [i] = node.dependsOn; diff --git a/src/strands/builder.js b/src/strands/builder.js index 3459f5f7ed..66c5e32d33 100644 --- a/src/strands/builder.js +++ b/src/strands/builder.js @@ -1,7 +1,7 @@ import * as DAG from './directed_acyclic_graph' import * as CFG from './control_flow_graph' import * as FES from './strands_FES' -import { DataType, DataTypeInfo, NodeType, OpCode, DataTypeName} from './utils'; +import { NodeType, OpCode, BaseType, BasePriority } from './utils'; import { StrandsNode } from './user_API'; ////////////////////////////////////////////// @@ -9,9 +9,15 @@ import { StrandsNode } from './user_API'; ////////////////////////////////////////////// export function createLiteralNode(strandsContext, typeInfo, value) { const { cfg, dag } = strandsContext + let { dimension, baseType } = typeInfo; + + if (dimension !== 1) { + FES.internalError('Created a literal node with dimension > 1.') + } const nodeData = DAG.createNodeData({ nodeType: NodeType.LITERAL, - dataType, + dimension, + baseType, value }); const id = DAG.getOrCreateNode(dag, nodeData); @@ -21,9 +27,11 @@ export function createLiteralNode(strandsContext, typeInfo, value) { export function createVariableNode(strandsContext, typeInfo, identifier) { const { cfg, dag } = strandsContext; + const { dimension, baseType } = typeInfo; const nodeData = DAG.createNodeData({ nodeType: NodeType.VARIABLE, - dataType, + dimension, + baseType, identifier }) const id = DAG.getOrCreateNode(dag, nodeData); @@ -31,71 +39,78 @@ export function createVariableNode(strandsContext, typeInfo, identifier) { return id; } -export function createBinaryOpNode(strandsContext, leftNode, rightArg, opCode) { +function extractTypeInfo(strandsContext, nodeID) { + const dag = strandsContext.dag; + const baseType = dag.baseTypes[nodeID]; + return { + baseType, + dimension: dag.dimensions[nodeID], + priority: BasePriority[baseType], + }; +} + +export function createBinaryOpNode(strandsContext, leftStrandsNode, rightArg, opCode) { const { dag, cfg } = strandsContext; - - let inferRightType, rightNodeID, rightNode; - if (rightArg instanceof StrandsNode) { - rightNode = rightArg; - rightNodeID = rightArg.id; - inferRightType = dag.dataTypes[rightNodeID]; + // Construct a node for right if its just an array or number etc. + let rightStrandsNode; + if (rightArg[0] instanceof StrandsNode && rightArg.length === 1) { + rightStrandsNode = rightArg[0]; } else { - const rightDependsOn = Array.isArray(rightArg) ? rightArg : [rightArg]; - inferRightType = DataType.DEFER; - rightNodeID = createTypeConstructorNode(strandsContext, inferRightType, rightDependsOn); - rightNode = new StrandsNode(rightNodeID); + const id = createTypeConstructorNode(strandsContext, { baseType: BaseType.DEFER, dimension: null }, rightArg); + rightStrandsNode = new StrandsNode(id); } - const origRightType = inferRightType; - const leftNodeID = leftNode.id; - const origLeftType = dag.dataTypes[leftNodeID]; + let finalLeftNodeID = leftStrandsNode.id; + let finalRightNodeID = rightStrandsNode.id; - - const cast = { node: null, toType: origLeftType }; // Check if we have to cast either node - if (origLeftType !== origRightType) { - const L = DataTypeInfo[origLeftType]; - const R = DataTypeInfo[origRightType]; - - if (L.base === DataType.DEFER) { - L.dimension = dag.dependsOn[leftNodeID].length; - } - if (R.base === DataType.DEFER) { - R.dimension = dag.dependsOn[rightNodeID].length; - } + const leftType = extractTypeInfo(strandsContext, leftStrandsNode.id); + const rightType = extractTypeInfo(strandsContext, rightStrandsNode.id); + const cast = { node: null, toType: leftType }; + const bothDeferred = leftType.baseType === rightType.baseType && leftType.baseType === BaseType.DEFER; + + if (bothDeferred) { + finalLeftNodeID = createTypeConstructorNode(strandsContext, { baseType:BaseType.FLOAT, dimension: leftType.dimension }, leftStrandsNode); + finalRightNodeID = createTypeConstructorNode(strandsContext, { baseType:BaseType.FLOAT, dimension: leftType.dimension }, rightStrandsNode); + } + else if (leftType.baseType !== rightType.baseType || + leftType.dimension !== rightType.dimension) { - if (L.dimension === 1 && R.dimension > 1) { + if (leftType.dimension === 1 && rightType.dimension > 1) { // e.g. op(scalar, vector): cast scalar up - cast.node = leftNode; - cast.toType = origRightType; + cast.node = leftStrandsNode; + cast.toType = rightType; } - else if (R.dimension === 1 && L.dimension > 1) { - cast.node = rightNode; - cast.toType = origLeftType; + else if (rightType.dimension === 1 && leftType.dimension > 1) { + cast.node = rightStrandsNode; + cast.toType = leftType; } - else if (L.priority > R.priority && L.dimension === R.dimension) { + else if (leftType.priority > rightType.priority) { // e.g. op(float vector, int vector): cast priority is float > int > bool - cast.node = rightNode; - cast.toType = origLeftType; + cast.node = rightStrandsNode; + cast.toType = leftType; } - else if (R.priority > L.priority && L.dimension === R.dimension) { - cast.node = leftNode; - cast.toType = origRightType; + else if (rightType.priority > leftType.priority) { + cast.node = leftStrandsNode; + cast.toType = rightType; } else { - FES.userError('type error', `A vector of length ${L.dimension} operated with a vector of length ${R.dimension} is not allowed.`); + FES.userError('type error', `A vector of length ${leftType.dimension} operated with a vector of length ${rightType.dimension} is not allowed.`); } + const castedID = createTypeConstructorNode(strandsContext, cast.toType, cast.node); - if (cast.node === leftNode) { - leftNodeID = castedID; + if (cast.node === leftStrandsNode) { + finalLeftNodeID = castedID; } else { - rightNodeID = castedID; + finalRightNodeID = castedID; } } - + const nodeData = DAG.createNodeData({ nodeType: NodeType.OPERATION, - dependsOn: [leftNodeID, rightNodeID], - dataType: cast.toType, + dependsOn: [finalLeftNodeID, finalRightNodeID], + dimension, + baseType: cast.toType.baseType, + dimension: cast.toType.dimension, opCode }); const id = DAG.getOrCreateNode(dag, nodeData); @@ -104,8 +119,9 @@ export function createBinaryOpNode(strandsContext, leftNode, rightArg, opCode) { } function mapConstructorDependencies(strandsContext, typeInfo, dependsOn) { - const mapped = []; - const T = DataTypeInfo[dataType]; + const mappedDependencies = []; + let { dimension, baseType } = typeInfo; + const dag = strandsContext.dag; let calculatedDimensions = 0; @@ -113,40 +129,48 @@ function mapConstructorDependencies(strandsContext, typeInfo, dependsOn) { if (dep instanceof StrandsNode) { const node = DAG.getNodeDataFromID(dag, dep.id); - if (node.opCode === OpCode.Nary.CONSTRUCTOR && dataType === dataType) { + if (node.opCode === OpCode.Nary.CONSTRUCTOR) { for (const inner of node.dependsOn) { - mapped.push(inner); + mappedDependencies.push(inner); } + } else { + mappedDependencies.push(dep.id); } - const depDataType = dag.dataTypes[dep.id]; - calculatedDimensions += DataTypeInfo[depDataType].dimension; + + calculatedDimensions += node.dimension; continue; } if (typeof dep === 'number') { - const newNode = createLiteralNode(strandsContext, T.base, dep); + const newNode = createLiteralNode(strandsContext, { dimension: 1, baseType }, dep); + mappedDependencies.push(newNode); calculatedDimensions += 1; - mapped.push(newNode); continue; } else { FES.userError('type error', `You've tried to construct a scalar or vector type with a non-numeric value: ${dep}`); } } - - if(calculatedDimensions !== 1 && calculatedDimensions !== T.dimension) { - FES.userError('type error', `You've tried to construct a ${DataTypeName[dataType]} with ${calculatedDimensions} components`); + if (dimension === null) { + dimension = calculatedDimensions; + } else if (dimension > calculatedDimensions && calculatedDimensions === 1) { + calculatedDimensions = dimension; + } else if(calculatedDimensions !== 1 && calculatedDimensions !== dimension) { + FES.userError('type error', `You've tried to construct a ${baseType + dimension} with ${calculatedDimensions} components`); } - return mapped; + + return { mappedDependencies, dimension }; } export function createTypeConstructorNode(strandsContext, typeInfo, dependsOn) { const { cfg, dag } = strandsContext; dependsOn = Array.isArray(dependsOn) ? dependsOn : [dependsOn]; - const mappedDependencies = mapConstructorDependencies(strandsContext, dataType, dependsOn); + const { mappedDependencies, dimension } = mapConstructorDependencies(strandsContext, typeInfo, dependsOn); + const nodeData = DAG.createNodeData({ nodeType: NodeType.OPERATION, opCode: OpCode.Nary.CONSTRUCTOR, - dataType, + dimension, + baseType: typeInfo.baseType, dependsOn: mappedDependencies }) const id = DAG.getOrCreateNode(dag, nodeData); @@ -156,14 +180,16 @@ export function createTypeConstructorNode(strandsContext, typeInfo, dependsOn) { export function createFunctionCallNode(strandsContext, identifier, overrides, dependsOn) { const { cfg, dag } = strandsContext; - let dataType = dataType.DEFER; + let typeInfo = { baseType: null, dimension: null }; + const nodeData = DAG.createNodeData({ nodeType: NodeType.OPERATION, opCode: OpCode.Nary.FUNCTION_CALL, identifier, overrides, dependsOn, - dataType + // no type info yet + ...typeInfo, }) const id = DAG.getOrCreateNode(dag, nodeData); CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); diff --git a/src/strands/directed_acyclic_graph.js b/src/strands/directed_acyclic_graph.js index 54232cc5ff..d05c4f6841 100644 --- a/src/strands/directed_acyclic_graph.js +++ b/src/strands/directed_acyclic_graph.js @@ -1,5 +1,5 @@ -import { NodeTypeRequiredFields, NodeTypeToName } from './utils' -import * as FES from './strands_FES' +import { NodeTypeRequiredFields, NodeTypeToName, TypeInfo } from './utils'; +import * as FES from './strands_FES'; ///////////////////////////////// // Public functions for strands runtime @@ -10,7 +10,6 @@ export function createDirectedAcyclicGraph() { nextID: 0, cache: new Map(), nodeTypes: [], - dataTypes: [], baseTypes: [], dimensions: [], opCodes: [], @@ -41,9 +40,8 @@ export function getOrCreateNode(graph, node) { export function createNodeData(data = {}) { const node = { nodeType: data.nodeType ?? null, - dataType: data.dataType ?? null, baseType: data.baseType ?? null, - dimension: data.baseType ?? null, + dimension: data.dimension ?? null, opCode: data.opCode ?? null, value: data.value ?? null, identifier: data.identifier ?? null, @@ -58,7 +56,6 @@ export function createNodeData(data = {}) { export function getNodeDataFromID(graph, id) { return { nodeType: graph.nodeTypes[id], - dataType: graph.dataTypes[id], opCode: graph.opCodes[id], value: graph.values[id], identifier: graph.identifiers[id], @@ -76,7 +73,6 @@ export function getNodeDataFromID(graph, id) { function createNode(graph, node) { const id = graph.nextID++; graph.nodeTypes[id] = node.nodeType; - graph.dataTypes[id] = node.dataType; graph.opCodes[id] = node.opCode; graph.values[id] = node.value; graph.identifiers[id] = node.identifier; diff --git a/src/strands/user_API.js b/src/strands/user_API.js index 3482c57fb4..44c9790aaa 100644 --- a/src/strands/user_API.js +++ b/src/strands/user_API.js @@ -5,7 +5,7 @@ import { createStatementNode, createTypeConstructorNode, } from './builder' -import { DataType, OperatorTable, SymbolToOpCode, BlockType, arrayToFloatType } from './utils' +import { OperatorTable, SymbolToOpCode, BlockType, TypeInfo, BaseType, TypeInfoFromGLSLName } from './utils' import { strandsShaderFunctions } from './shader_functions' import { StrandsConditional } from './strands_conditionals' import * as CFG from './control_flow_graph' @@ -25,7 +25,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { // this means methods like .add, .sub, etc can be chained for (const { name, symbol, arity } of OperatorTable) { if (arity === 'binary') { - StrandsNode.prototype[name] = function (right) { + StrandsNode.prototype[name] = function (...right) { const id = createBinaryOpNode(strandsContext, this, right, SymbolToOpCode[symbol]); return new StrandsNode(id); }; @@ -58,7 +58,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { if (args.length > 4) { FES.userError('type error', "It looks like you've tried to construct a p5.strands node implicitly, with more than 4 components. This is currently not supported.") } - const id = createTypeConstructorNode(strandsContext, DataType.DEFER, args); + const id = createTypeConstructorNode(strandsContext, { baseType: BaseType.DEFER, dimension: null }, args); return new StrandsNode(id); } @@ -91,37 +91,40 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { } // Next is type constructors and uniform functions - for (const typeName in DataType) { - const lowerTypeName = typeName.toLowerCase(); + for (const type in TypeInfo) { + if (type === BaseType.DEFER) { + continue; + } + const typeInfo = TypeInfo[type]; + let pascalTypeName; - if (/^[ib]vec/.test(lowerTypeName)) { - pascalTypeName = lowerTypeName + if (/^[ib]vec/.test(typeInfo.fnName)) { + pascalTypeName = typeInfo.fnName .slice(0, 2).toUpperCase() - + lowerTypeName + + typeInfo.fnName .slice(2) .toLowerCase(); } else { - pascalTypeName = lowerTypeName.charAt(0).toUpperCase() - + lowerTypeName.slice(1).toLowerCase(); + pascalTypeName = typeInfo.fnName.charAt(0).toUpperCase() + + typeInfo.fnName.slice(1).toLowerCase(); } - fn[`uniform${pascalTypeName}`] = function(...args) { - let [name, ...defaultValue] = args; - const id = createVariableNode(strandsContext, DataType.FLOAT, name); - strandsContext.uniforms.push({ name, dataType: DataType.FLOAT, defaultValue }); + fn[`uniform${pascalTypeName}`] = function(name, ...defaultValue) { + const id = createVariableNode(strandsContext, typeInfo, name); + strandsContext.uniforms.push({ name, typeInfo, defaultValue }); return new StrandsNode(id); }; - const typeConstructor = fn[lowerTypeName]; - fn[lowerTypeName] = function(...args) { + const originalp5Fn = fn[typeInfo.fnName]; + fn[typeInfo.fnName] = function(...args) { if (strandsContext.active) { - const id = createTypeConstructorNode(strandsContext, DataType[typeName], args); + const id = createTypeConstructorNode(strandsContext, typeInfo, args); return new StrandsNode(id); - } else if (typeConstructor) { - return typeConstructor.apply(this, args); + } else if (originalp5Fn) { + return originalp5Fn.apply(this, args); } else { p5._friendlyError( - `It looks like you've called ${lowerTypeName} outside of a shader's modify() function.` + `It looks like you've called ${typeInfo.fnName} outside of a shader's modify() function.` ); } } @@ -136,15 +139,16 @@ function createHookArguments(strandsContext, parameters){ const args = []; for (const param of parameters) { - const T = param.type; - if(structTypes.includes(T.typeName)) { - const propertiesNodes = T.properties.map( - (prop) => [prop.name, createVariableNode(strandsContext, DataType[prop.dataType], prop.name)] + const paramType = param.type; + if(structTypes.includes(paramType.typeName)) { + const propertiesNodes = paramType.properties.map( + (prop) => [prop.name, createVariableNode(strandsContext, TypeInfoFromGLSLName[prop.dataType], prop.name)] ); const argObject = Object.fromEntries(propertiesNodes); args.push(argObject); } else { - const arg = createVariableNode(strandsContext, DataType[param.dataType], param.name); + const typeInfo = TypeInfoFromGLSLName[paramType.typeName]; + const arg = createVariableNode(strandsContext, typeInfo, param.name); args.push(arg) } } diff --git a/src/strands/utils.js b/src/strands/utils.js index 6f38092381..07308db711 100644 --- a/src/strands/utils.js +++ b/src/strands/utils.js @@ -9,6 +9,10 @@ export const NodeType = { PHI: 4, }; +export const NodeTypeToName = Object.fromEntries( + Object.entries(NodeType).map(([key, val]) => [val, key]) +); + export const NodeTypeRequiredFields = { [NodeType.OPERATION]: ['opCode', 'dependsOn'], [NodeType.LITERAL]: ['value'], @@ -17,101 +21,49 @@ export const NodeTypeRequiredFields = { [NodeType.PHI]: ['dependsOn', 'phiBlocks'] }; -export const NodeTypeToName = Object.fromEntries( - Object.entries(NodeType).map(([key, val]) => [val, key]) -); - export const BaseType = { FLOAT: 'float', INT: 'int', - BOOl: 'bool', + BOOL: 'bool', MAT: 'mat', - DEFER: 'deferred', + DEFER: 'defer', }; -export const AllTypes = [ - 'float1', - 'float2', - 'float3', - 'float4', - 'int1', - 'int2', - 'int3', - 'int4', - 'bool1', - 'bool2', - 'bool3', - 'bool4', - 'mat2x2', - 'mat3x3', - 'mat4x4', -] - -export const DataType = { - FLOAT: 0, - VEC2: 1, - VEC3: 2, - VEC4: 3, - - INT: 100, - IVEC2: 101, - IVEC3: 102, - IVEC4: 103, - - BOOL: 200, - BVEC2: 201, - BVEC3: 202, - BVEC4: 203, - - MAT2X2: 300, - MAT3X3: 301, - MAT4X4: 302, +export const BasePriority = { + [BaseType.FLOAT]: 3, + [BaseType.INT]: 2, + [BaseType.BOOL]: 1, + [BaseType.MAT]: 0, + [BaseType.DEFER]: -1, +}; - DEFER: 999, -} +export const TypeInfo = { + 'float1': { fnName: 'float', baseType: BaseType.FLOAT, dimension:1, priority: 3, }, + 'float2': { fnName: 'vec2', baseType: BaseType.FLOAT, dimension:2, priority: 3, }, + 'float3': { fnName: 'vec3', baseType: BaseType.FLOAT, dimension:3, priority: 3, }, + 'float4': { fnName: 'vec4', baseType: BaseType.FLOAT, dimension:4, priority: 3, }, -export const DataTypeInfo = { - [DataType.FLOAT]: { base: DataType.FLOAT, dimension: 1, priority: 2 }, - [DataType.VEC2]: { base: DataType.FLOAT, dimension: 2, priority: 2 }, - [DataType.VEC3]: { base: DataType.FLOAT, dimension: 3, priority: 2 }, - [DataType.VEC4]: { base: DataType.FLOAT, dimension: 4, priority: 2 }, - [DataType.INT]: { base: DataType.INT, dimension: 1, priority: 1 }, - [DataType.IVEC2]: { base: DataType.INT, dimension: 2, priority: 1 }, - [DataType.IVEC3]: { base: DataType.INT, dimension: 3, priority: 1 }, - [DataType.IVEC4]: { base: DataType.INT, dimension: 4, priority: 1 }, - [DataType.BOOL]: { base: DataType.BOOL, dimension: 1, priority: 0 }, - [DataType.BVEC2]: { base: DataType.BOOL, dimension: 2, priority: 0 }, - [DataType.BVEC3]: { base: DataType.BOOL, dimension: 3, priority: 0 }, - [DataType.BVEC4]: { base: DataType.BOOL, dimension: 4, priority: 0 }, - [DataType.MAT2]: { base: DataType.FLOAT, dimension: 2, priority: -1 }, - [DataType.MAT3]: { base: DataType.FLOAT, dimension: 3, priority: -1 }, - [DataType.MAT4]: { base: DataType.FLOAT, dimension: 4, priority: -1 }, + 'int1': { fnName: 'int', baseType: BaseType.INT, dimension:1, priority: 2, }, + 'int2': { fnName: 'ivec2', baseType: BaseType.INT, dimension:2, priority: 2, }, + 'int3': { fnName: 'ivec3', baseType: BaseType.INT, dimension:3, priority: 2, }, + 'int4': { fnName: 'ivec4', baseType: BaseType.INT, dimension:4, priority: 2, }, - [DataType.DEFER]: { base: DataType.DEFER, dimension: null, priority: -2 }, - [DataType.DEFER]: { base: DataType.DEFER, dimension: null, priority: -2 }, - [DataType.DEFER]: { base: DataType.DEFER, dimension: null, priority: -2 }, - [DataType.DEFER]: { base: DataType.DEFER, dimension: null, priority: -2 }, -}; + 'bool1': { fnName: 'bool', baseType: BaseType.BOOL, dimension:1, priority: 1, }, + 'bool2': { fnName: 'bvec2', baseType: BaseType.BOOL, dimension:2, priority: 1, }, + 'bool3': { fnName: 'bvec3', baseType: BaseType.BOOL, dimension:3, priority: 1, }, + 'bool4': { fnName: 'bvec4', baseType: BaseType.BOOL, dimension:4, priority: 1, }, -// 2) A separate nested lookup table: -export const DataTypeTable = { - [DataType.FLOAT]: { 1: DataType.FLOAT, 2: DataType.VEC2, 3: DataType.VEC3, 4: DataType.VEC4 }, - [DataType.INT]: { 1: DataType.INT, 2: DataType.IVEC2, 3: DataType.IVEC3, 4: DataType.IVEC4 }, - [DataType.BOOL]: { 1: DataType.BOOL, 2: DataType.BVEC2, 3: DataType.BVEC3, 4: DataType.BVEC4 }, - // [DataType.MAT2]: { 2: DataType.MAT2, 3: DataType.MAT3, 4: DataType.MAT4 }, - [DataType.DEFER]: { 0: DataType.DEFER, 1: DataType.DEFER, 2: DataType.DEFER, 3: DataType.DEFER, 4: DataType.DEFER }, -}; + 'mat2': { fnName: 'mat2x2', baseType: BaseType.MAT, dimension:2, priority: 0, }, + 'mat3': { fnName: 'mat3x3', baseType: BaseType.MAT, dimension:3, priority: 0, }, + 'mat4': { fnName: 'mat4x4', baseType: BaseType.MAT, dimension:4, priority: 0, }, -export function lookupDataType(baseCode, dim) { - const map = DataTypeTable[baseCode]; - if (!map || map[dim] == null) { - throw new Error(`Invalid type combination: base=${baseCode}, dim=${dim}`); - } - return map[dim]; + 'defer': { fnName: null, baseType: BaseType.DEFER, dimension: null, priority: -1 }, } -export const DataTypeName = Object.fromEntries( - Object.entries(DataType).map(([key,val])=>[val, key.toLowerCase()]) +export const TypeInfoFromGLSLName = Object.fromEntries( + Object.values(TypeInfo) + .filter(info => info.fnName !== null) + .map(info => [info.fnName, info]) ); export const OpCode = { @@ -168,20 +120,20 @@ export const OperatorTable = [ { arity: "binary", name: "or", symbol: "||", opcode: OpCode.Binary.LOGICAL_OR }, ]; -const BinaryOperations = { - "+": (a, b) => a + b, - "-": (a, b) => a - b, - "*": (a, b) => a * b, - "/": (a, b) => a / b, - "%": (a, b) => a % b, - "==": (a, b) => a == b, - "!=": (a, b) => a != b, - ">": (a, b) => a > b, - ">=": (a, b) => a >= b, - "<": (a, b) => a < b, - "<=": (a, b) => a <= b, - "&&": (a, b) => a && b, - "||": (a, b) => a || b, +export const ConstantFolding = { + [OpCode.Binary.ADD]: (a, b) => a + b, + [OpCode.Binary.SUBTRACT]: (a, b) => a - b, + [OpCode.Binary.MULTIPLY]: (a, b) => a * b, + [OpCode.Binary.DIVIDE]: (a, b) => a / b, + [OpCode.Binary.MODULO]: (a, b) => a % b, + [OpCode.Binary.EQUAL]: (a, b) => a == b, + [OpCode.Binary.NOT_EQUAL]: (a, b) => a != b, + [OpCode.Binary.GREATER_THAN]: (a, b) => a > b, + [OpCode.Binary.GREATER_EQUAL]: (a, b) => a >= b, + [OpCode.Binary.LESS_THAN]: (a, b) => a < b, + [OpCode.Binary.LESS_EQUAL]: (a, b) => a <= b, + [OpCode.Binary.LOGICAL_AND]: (a, b) => a && b, + [OpCode.Binary.LOGICAL_OR]: (a, b) => a || b, }; export const SymbolToOpCode = {}; @@ -193,9 +145,6 @@ for (const { arity, symbol, opcode } of OperatorTable) { SymbolToOpCode[symbol] = opcode; OpCodeToSymbol[opcode] = symbol; OpCodeArgs[opcode] = args; - if (arity === "binary" && BinaryOperations[symbol]) { - OpCodeToOperation[opcode] = BinaryOperations[symbol]; - } } export const BlockType = { From 24f0c46a19662d7e3b45a6278a47d9231590e065 Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Wed, 16 Jul 2025 17:15:23 +0100 Subject: [PATCH 09/69] SSA --- preview/global/sketch.js | 3 ++- src/strands/GLSL_backend.js | 25 +++++++++++++++++-------- src/strands/builder.js | 2 +- src/strands/code_generation.js | 19 +++++++++++-------- 4 files changed, 31 insertions(+), 18 deletions(-) diff --git a/preview/global/sketch.js b/preview/global/sketch.js index bd019b77df..50b003acc9 100644 --- a/preview/global/sketch.js +++ b/preview/global/sketch.js @@ -3,7 +3,8 @@ p5.disableFriendlyErrors = true; function callback() { getFinalColor((col) => { - return ivec3(1, 2, 4).mult(2.0, 2, 3); + let x = vec3(1); + return vec3(1).div(ivec3(1, 2, 4).mult(ivec3(2.0, 2, 3))); }); } diff --git a/src/strands/GLSL_backend.js b/src/strands/GLSL_backend.js index 3813465e38..cb13ac388c 100644 --- a/src/strands/GLSL_backend.js +++ b/src/strands/GLSL_backend.js @@ -4,16 +4,16 @@ import * as FES from './strands_FES' const cfgHandlers = { [BlockType.DEFAULT]: (blockID, strandsContext, generationContext) => { - const { dag, cfg } = strandsContext; + // const { dag, cfg } = strandsContext; - const blockInstructions = new Set(cfg.blockInstructions[blockID] || []); - for (let nodeID of generationContext.dagSorted) { - if (!blockInstructions.has(nodeID)) { - continue; - } + // const blockInstructions = new Set(cfg.blockInstructions[blockID] || []); + // for (let nodeID of generationContext.dagSorted) { + // if (!blockInstructions.has(nodeID)) { + // continue; + // } // const snippet = glslBackend.generateExpression(dag, nodeID, generationContext); // generationContext.write(snippet); - } + // } }, [BlockType.IF_COND](blockID, strandsContext, generationContext) { @@ -76,7 +76,12 @@ export const glslBackend = { return node.identifier; case NodeType.OPERATION: + const useParantheses = node.usedBy.length > 0; if (node.opCode === OpCode.Nary.CONSTRUCTOR) { + if (node.dependsOn.length === 1 && node.dimension === 1) { + console.log("AARK") + return this.generateExpression(dag, node.dependsOn[0], generationContext); + } const T = this.generateDataTypeName(node.baseType, node.dimension); const deps = node.dependsOn.map((dep) => this.generateExpression(dag, dep, generationContext)); return `${T}(${deps.join(', ')})`; @@ -89,7 +94,11 @@ export const glslBackend = { const left = this.generateExpression(dag, lID, generationContext); const right = this.generateExpression(dag, rID, generationContext); const opSym = OpCodeToSymbol[node.opCode]; - return `(${left} ${opSym} ${right})`; + if (useParantheses) { + return `(${left} ${opSym} ${right})`; + } else { + return `${left} ${opSym} ${right}`; + } } if (node.dependsOn.length === 1) { const [i] = node.dependsOn; diff --git a/src/strands/builder.js b/src/strands/builder.js index 66c5e32d33..671870bbd0 100644 --- a/src/strands/builder.js +++ b/src/strands/builder.js @@ -39,7 +39,7 @@ export function createVariableNode(strandsContext, typeInfo, identifier) { return id; } -function extractTypeInfo(strandsContext, nodeID) { +export function extractTypeInfo(strandsContext, nodeID) { const dag = strandsContext.dag; const baseType = dag.baseTypes[nodeID]; return { diff --git a/src/strands/code_generation.js b/src/strands/code_generation.js index b8aba9a642..30f8e47f00 100644 --- a/src/strands/code_generation.js +++ b/src/strands/code_generation.js @@ -1,12 +1,14 @@ import { WEBGL } from '../core/constants'; import { glslBackend } from './GLSL_backend'; import { dfsPostOrder, dfsReversePostOrder, NodeType } from './utils'; +import { extractTypeInfo } from './builder'; let globalTempCounter = 0; let backend; -function generateTopLevelDeclarations(dag, dagOrder) { +function generateTopLevelDeclarations(strandsContext, dagOrder) { const usedCount = {}; + const dag = strandsContext.dag; for (const nodeID of dagOrder) { usedCount[nodeID] = (dag.usedBy[nodeID] || []).length; } @@ -18,13 +20,14 @@ function generateTopLevelDeclarations(dag, dagOrder) { continue; } - // if (usedCount[nodeID] > 1) { - // const tmp = `t${globalTempCounter++}`; - // tempNames[nodeID] = tmp; + if (usedCount[nodeID] > 0) { + const expr = backend.generateExpression(dag, nodeID, { tempNames }); + const tmp = `T${globalTempCounter++}`; + tempNames[nodeID] = tmp; - // const expr = backend.generateExpression(dag, nodeID, {}); - // declarations.push(`float ${tmp} = ${expr};`); - // } + const T = extractTypeInfo(strandsContext, nodeID); + declarations.push(`${T.baseType+T.dimension} ${tmp} = ${expr};`); + } } return { declarations, tempNames }; @@ -42,7 +45,7 @@ export function generateShaderCode(strandsContext) { const cfgSorted = dfsReversePostOrder(cfg.outgoingEdges, entryBlockID); const generationContext = { - ...generateTopLevelDeclarations(dag, dagSorted), + ...generateTopLevelDeclarations(strandsContext, dagSorted), indent: 1, codeLines: [], write(line) { From 085128519237b91c59c79ec0d586e01dd9e21207 Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Wed, 23 Jul 2025 14:55:32 +0100 Subject: [PATCH 10/69] Return type checking for hooks with native types reimplemented (i.e. not p5 defined structs such as Vertex inputs) --- preview/global/sketch.js | 5 +- src/strands/builder.js | 13 ++++ src/strands/code_generation.js | 8 ++- src/strands/control_flow_graph.js | 19 +++++ src/strands/directed_acyclic_graph.js | 21 +++++- src/strands/p5.strands.js | 4 +- src/strands/user_API.js | 79 +++++++++++++++----- src/strands/utils.js | 100 ++++++-------------------- 8 files changed, 144 insertions(+), 105 deletions(-) diff --git a/preview/global/sketch.js b/preview/global/sketch.js index 50b003acc9..3b16229412 100644 --- a/preview/global/sketch.js +++ b/preview/global/sketch.js @@ -3,8 +3,9 @@ p5.disableFriendlyErrors = true; function callback() { getFinalColor((col) => { - let x = vec3(1); - return vec3(1).div(ivec3(1, 2, 4).mult(ivec3(2.0, 2, 3))); + let x = vec4(1); + // return 1; + return vec4(1).div(ivec4(1).mult(ivec4(2.0, 3.0, 2, 3))); }); } diff --git a/src/strands/builder.js b/src/strands/builder.js index 671870bbd0..a73669753f 100644 --- a/src/strands/builder.js +++ b/src/strands/builder.js @@ -196,6 +196,19 @@ export function createFunctionCallNode(strandsContext, identifier, overrides, de return id; } +export function createUnaryOpNode(strandsContext, strandsNode, opCode) { + const { dag, cfg } = strandsContext; + const nodeData = DAG.createNodeData({ + nodeType: NodeType.OPERATION, + opCode, + dependsOn: strandsNode.id, + baseType: dag.baseTypes[strandsNode.id], + dimension: dag.dimensions[strandsNode.id], + }) + CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); + return id; +} + export function createStatementNode(strandsContext, type) { return -99; } \ No newline at end of file diff --git a/src/strands/code_generation.js b/src/strands/code_generation.js index 30f8e47f00..9d47aff468 100644 --- a/src/strands/code_generation.js +++ b/src/strands/code_generation.js @@ -1,7 +1,9 @@ import { WEBGL } from '../core/constants'; import { glslBackend } from './GLSL_backend'; -import { dfsPostOrder, dfsReversePostOrder, NodeType } from './utils'; +import { NodeType } from './utils'; import { extractTypeInfo } from './builder'; +import { sortCFG } from './control_flow_graph'; +import { sortDAG } from './directed_acyclic_graph'; let globalTempCounter = 0; let backend; @@ -41,8 +43,8 @@ export function generateShaderCode(strandsContext) { for (const { hookType, entryBlockID, rootNodeID} of strandsContext.hooks) { const { cfg, dag } = strandsContext; - const dagSorted = dfsPostOrder(dag.dependsOn, rootNodeID); - const cfgSorted = dfsReversePostOrder(cfg.outgoingEdges, entryBlockID); + const dagSorted = sortDAG(dag.dependsOn, rootNodeID); + const cfgSorted = sortCFG(cfg.outgoingEdges, entryBlockID); const generationContext = { ...generateTopLevelDeclarations(strandsContext, dagSorted), diff --git a/src/strands/control_flow_graph.js b/src/strands/control_flow_graph.js index cee0f0da42..341f62871d 100644 --- a/src/strands/control_flow_graph.js +++ b/src/strands/control_flow_graph.js @@ -59,4 +59,23 @@ export function printBlockData(graph, id) { const block = getBlockDataFromID(graph, id); block.blockType = BlockTypeToName[block.blockType]; console.log(block); +} + +export function sortCFG(adjacencyList, start) { + const visited = new Set(); + const postOrder = []; + + function dfs(v) { + if (visited.has(v)) { + return; + } + visited.add(v); + for (let w of adjacencyList[v].sort((a, b) => b-a) || []) { + dfs(w); + } + postOrder.push(v); + } + + dfs(start); + return postOrder.reverse(); } \ No newline at end of file diff --git a/src/strands/directed_acyclic_graph.js b/src/strands/directed_acyclic_graph.js index d05c4f6841..34b63d919f 100644 --- a/src/strands/directed_acyclic_graph.js +++ b/src/strands/directed_acyclic_graph.js @@ -1,4 +1,4 @@ -import { NodeTypeRequiredFields, NodeTypeToName, TypeInfo } from './utils'; +import { NodeTypeRequiredFields, NodeTypeToName } from './utils'; import * as FES from './strands_FES'; ///////////////////////////////// @@ -113,4 +113,23 @@ function validateNode(node){ if (missingFields.length > 0) { FES.internalError(`Missing fields ${missingFields.join(', ')} for a node type '${NodeTypeToName[nodeType]}'.`); } +} + +export function sortDAG(adjacencyList, start) { + const visited = new Set(); + const postOrder = []; + + function dfs(v) { + if (visited.has(v)) { + return; + } + visited.add(v); + for (let w of adjacencyList[v]) { + dfs(w); + } + postOrder.push(v); + } + + dfs(start); + return postOrder; } \ No newline at end of file diff --git a/src/strands/p5.strands.js b/src/strands/p5.strands.js index 6089c21e18..6d9bc8a0d6 100644 --- a/src/strands/p5.strands.js +++ b/src/strands/p5.strands.js @@ -12,7 +12,7 @@ import { BlockType } from './utils'; import { createDirectedAcyclicGraph } from './directed_acyclic_graph' import { createControlFlowGraph, createBasicBlock, pushBlock, popBlock } from './control_flow_graph'; import { generateShaderCode } from './code_generation'; -import { initGlobalStrandsAPI, initShaderHooksFunctions } from './user_API'; +import { initGlobalStrandsAPI, createShaderHooksFunctions } from './user_API'; function strands(p5, fn) { ////////////////////////////////////////////// @@ -51,7 +51,7 @@ function strands(p5, fn) { // Reset the context object every time modify is called; const backend = WEBGL; initStrandsContext(strandsContext, backend); - initShaderHooksFunctions(strandsContext, fn, this); + createShaderHooksFunctions(strandsContext, fn, this); // 1. Transpile from strands DSL to JS let strandsCallback; diff --git a/src/strands/user_API.js b/src/strands/user_API.js index 44c9790aaa..1ddb7dc6c9 100644 --- a/src/strands/user_API.js +++ b/src/strands/user_API.js @@ -4,8 +4,9 @@ import { createVariableNode, createStatementNode, createTypeConstructorNode, + createUnaryOpNode, } from './builder' -import { OperatorTable, SymbolToOpCode, BlockType, TypeInfo, BaseType, TypeInfoFromGLSLName } from './utils' +import { OperatorTable, BlockType, TypeInfo, BaseType, TypeInfoFromGLSLName } from './utils' import { strandsShaderFunctions } from './shader_functions' import { StrandsConditional } from './strands_conditionals' import * as CFG from './control_flow_graph' @@ -23,19 +24,19 @@ export class StrandsNode { export function initGlobalStrandsAPI(p5, fn, strandsContext) { // We augment the strands node with operations programatically // this means methods like .add, .sub, etc can be chained - for (const { name, symbol, arity } of OperatorTable) { + for (const { name, arity, opCode, symbol } of OperatorTable) { if (arity === 'binary') { StrandsNode.prototype[name] = function (...right) { - const id = createBinaryOpNode(strandsContext, this, right, SymbolToOpCode[symbol]); + const id = createBinaryOpNode(strandsContext, this, right, opCode); return new StrandsNode(id); }; } - // if (arity === 'unary') { - // StrandsNode.prototype[name] = function () { - // const id = createUnaryExpressionNode(this, SymbolToOpCode[symbol]); - // return new StrandsNode(id); - // }; - // } + if (arity === 'unary') { + fn[name] = function (strandsNode) { + const id = createUnaryOpNode(strandsContext, strandsNode, opCode); + return new StrandsNode(id); + } + } } ////////////////////////////////////////////// @@ -134,17 +135,20 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { ////////////////////////////////////////////// // Per-Hook functions ////////////////////////////////////////////// +const structTypes = ['Vertex', ] + function createHookArguments(strandsContext, parameters){ - const structTypes = ['Vertex', ] const args = []; for (const param of parameters) { const paramType = param.type; if(structTypes.includes(paramType.typeName)) { - const propertiesNodes = paramType.properties.map( - (prop) => [prop.name, createVariableNode(strandsContext, TypeInfoFromGLSLName[prop.dataType], prop.name)] - ); - const argObject = Object.fromEntries(propertiesNodes); + const propertyEntries = paramType.properties.map((prop) => { + const typeInfo = TypeInfoFromGLSLName[prop.dataType]; + const variableNode = createVariableNode(strandsContext, typeInfo, prop.name); + return [prop.name, variableNode]; + }); + const argObject = Object.fromEntries(propertyEntries); args.push(argObject); } else { const typeInfo = TypeInfoFromGLSLName[paramType.typeName]; @@ -155,24 +159,63 @@ function createHookArguments(strandsContext, parameters){ return args; } -export function initShaderHooksFunctions(strandsContext, fn, shader) { +export function createShaderHooksFunctions(strandsContext, fn, shader) { const availableHooks = { ...shader.hooks.vertex, ...shader.hooks.fragment, } const hookTypes = Object.keys(availableHooks).map(name => shader.hookTypes(name)); - const { cfg } = strandsContext; + const { cfg, dag } = strandsContext; + for (const hookType of hookTypes) { window[hookType.name] = 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 rootNodeID = hookUserCallback(args).id; + const returned = hookUserCallback(args); + let returnedNode; + + const expectedReturnType = hookType.returnType; + if(structTypes.includes(expectedReturnType.typeName)) { + + } + else { + // In this case we are expecting a native shader type, probably vec4 or vec3. + const expected = TypeInfoFromGLSLName[expectedReturnType.typeName]; + // User may have returned a raw value like [1,1,1,1] or 25. + if (!(returned instanceof StrandsNode)) { + const id = createTypeConstructorNode(strandsContext, { baseType: BaseType.DEFER, dimension: null }, returned); + returnedNode = new StrandsNode(id); + } + else { + returnedNode = returned; + } + + const received = { + baseType: dag.baseTypes[returnedNode.id], + dimension: dag.dimensions[returnedNode.id], + } + if (received.dimension !== expected.dimension) { + if (received.dimension !== 1) { + FES.userError('type error', `You have returned a vector with ${received.dimension} components in ${hookType.name} when a ${expected.baseType + expected.dimension} was expected!`); + } + else { + const newID = createTypeConstructorNode(strandsContext, expected, returnedNode); + returnedNode = new StrandsNode(newID); + } + } + else if (received.baseType !== expected.baseType) { + const newID = createTypeConstructorNode(strandsContext, expected, returnedNode); + returnedNode = new StrandsNode(newID); + } + } + strandsContext.hooks.push({ hookType, entryBlockID, - rootNodeID, + rootNodeID: returnedNode.id, }); CFG.popBlock(cfg); } diff --git a/src/strands/utils.js b/src/strands/utils.js index 07308db711..bcb00c32e5 100644 --- a/src/strands/utils.js +++ b/src/strands/utils.js @@ -102,22 +102,22 @@ export const OpCode = { }; export const OperatorTable = [ - { arity: "unary", name: "not", symbol: "!", opcode: OpCode.Unary.LOGICAL_NOT }, - { arity: "unary", name: "neg", symbol: "-", opcode: OpCode.Unary.NEGATE }, - { arity: "unary", name: "plus", symbol: "+", opcode: OpCode.Unary.PLUS }, - { arity: "binary", name: "add", symbol: "+", opcode: OpCode.Binary.ADD }, - { arity: "binary", name: "sub", symbol: "-", opcode: OpCode.Binary.SUBTRACT }, - { arity: "binary", name: "mult", symbol: "*", opcode: OpCode.Binary.MULTIPLY }, - { arity: "binary", name: "div", symbol: "/", opcode: OpCode.Binary.DIVIDE }, - { arity: "binary", name: "mod", symbol: "%", opcode: OpCode.Binary.MODULO }, - { arity: "binary", name: "equalTo", symbol: "==", opcode: OpCode.Binary.EQUAL }, - { arity: "binary", name: "notEqual", symbol: "!=", opcode: OpCode.Binary.NOT_EQUAL }, - { arity: "binary", name: "greaterThan", symbol: ">", opcode: OpCode.Binary.GREATER_THAN }, - { arity: "binary", name: "greaterEqual", symbol: ">=", opcode: OpCode.Binary.GREATER_EQUAL }, - { arity: "binary", name: "lessThan", symbol: "<", opcode: OpCode.Binary.LESS_THAN }, - { arity: "binary", name: "lessEqual", symbol: "<=", opcode: OpCode.Binary.LESS_EQUAL }, - { arity: "binary", name: "and", symbol: "&&", opcode: OpCode.Binary.LOGICAL_AND }, - { arity: "binary", name: "or", symbol: "||", opcode: OpCode.Binary.LOGICAL_OR }, + { arity: "unary", name: "not", symbol: "!", opCode: OpCode.Unary.LOGICAL_NOT }, + { arity: "unary", name: "neg", symbol: "-", opCode: OpCode.Unary.NEGATE }, + { arity: "unary", name: "plus", symbol: "+", opCode: OpCode.Unary.PLUS }, + { arity: "binary", name: "add", symbol: "+", opCode: OpCode.Binary.ADD }, + { arity: "binary", name: "sub", symbol: "-", opCode: OpCode.Binary.SUBTRACT }, + { arity: "binary", name: "mult", symbol: "*", opCode: OpCode.Binary.MULTIPLY }, + { arity: "binary", name: "div", symbol: "/", opCode: OpCode.Binary.DIVIDE }, + { arity: "binary", name: "mod", symbol: "%", opCode: OpCode.Binary.MODULO }, + { arity: "binary", name: "equalTo", symbol: "==", opCode: OpCode.Binary.EQUAL }, + { arity: "binary", name: "notEqual", symbol: "!=", opCode: OpCode.Binary.NOT_EQUAL }, + { arity: "binary", name: "greaterThan", symbol: ">", opCode: OpCode.Binary.GREATER_THAN }, + { arity: "binary", name: "greaterEqual", symbol: ">=", opCode: OpCode.Binary.GREATER_EQUAL }, + { arity: "binary", name: "lessThan", symbol: "<", opCode: OpCode.Binary.LESS_THAN }, + { arity: "binary", name: "lessEqual", symbol: "<=", opCode: OpCode.Binary.LESS_EQUAL }, + { arity: "binary", name: "and", symbol: "&&", opCode: OpCode.Binary.LOGICAL_AND }, + { arity: "binary", name: "or", symbol: "||", opCode: OpCode.Binary.LOGICAL_OR }, ]; export const ConstantFolding = { @@ -138,13 +138,10 @@ export const ConstantFolding = { export const SymbolToOpCode = {}; export const OpCodeToSymbol = {}; -export const OpCodeArgs = {}; -export const OpCodeToOperation = {}; -for (const { arity, symbol, opcode } of OperatorTable) { - SymbolToOpCode[symbol] = opcode; - OpCodeToSymbol[opcode] = symbol; - OpCodeArgs[opcode] = args; +for (const { symbol, opCode } of OperatorTable) { + SymbolToOpCode[symbol] = opCode; + OpCodeToSymbol[opCode] = symbol; } export const BlockType = { @@ -158,63 +155,8 @@ export const BlockType = { FOR: 7, MERGE: 8, DEFAULT: 9, - } + export const BlockTypeToName = Object.fromEntries( Object.entries(BlockType).map(([key, val]) => [val, key]) -); - -//////////////////////////// -// Type Checking helpers -//////////////////////////// -export function arrayToFloatType(array) { - let type = false; - if (array.length === 1) { - type = `FLOAT`; - } else if (array.length >= 2 && array.length <= 4) { - type = `VEC${array.length}`; - } else { - throw new Error('Tried to construct a float / vector with and empty array, or more than 4 components!') - } -} - -//////////////////////////// -// Graph utils -//////////////////////////// -export function dfsPostOrder(adjacencyList, start) { - const visited = new Set(); - const postOrder = []; - - function dfs(v) { - if (visited.has(v)) { - return; - } - visited.add(v); - for (let w of adjacencyList[v]) { - dfs(w); - } - postOrder.push(v); - } - - dfs(start); - return postOrder; -} - -export function dfsReversePostOrder(adjacencyList, start) { - const visited = new Set(); - const postOrder = []; - - function dfs(v) { - if (visited.has(v)) { - return; - } - visited.add(v); - for (let w of adjacencyList[v].sort((a, b) => b-a) || []) { - dfs(w); - } - postOrder.push(v); - } - - dfs(start); - return postOrder.reverse(); -} \ No newline at end of file +); \ No newline at end of file From 9b84f6feafd01a6e21aa0aff966fa2d34a40331b Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Wed, 23 Jul 2025 17:16:40 +0100 Subject: [PATCH 11/69] declarations moved to backend, hook arguments fixed --- preview/global/sketch.js | 11 ++--- src/strands/GLSL_backend.js | 59 +++++++++++++++++++++------ src/strands/builder.js | 12 +----- src/strands/code_generation.js | 25 +++++------- src/strands/directed_acyclic_graph.js | 9 +++- src/strands/p5.strands.js | 9 ++-- src/strands/user_API.js | 7 ++-- 7 files changed, 82 insertions(+), 50 deletions(-) diff --git a/preview/global/sketch.js b/preview/global/sketch.js index 3b16229412..fe768cb428 100644 --- a/preview/global/sketch.js +++ b/preview/global/sketch.js @@ -1,11 +1,9 @@ p5.disableFriendlyErrors = true; function callback() { - getFinalColor((col) => { - let x = vec4(1); - // return 1; - return vec4(1).div(ivec4(1).mult(ivec4(2.0, 3.0, 2, 3))); + let y = col.sub(-1,1,0,0); + return col.add(y); }); } @@ -15,5 +13,8 @@ async function setup(){ } function draw(){ - + orbitControl(); + background(0); + shader(bloomShader); + sphere(100) } diff --git a/src/strands/GLSL_backend.js b/src/strands/GLSL_backend.js index cb13ac388c..c92e3f688f 100644 --- a/src/strands/GLSL_backend.js +++ b/src/strands/GLSL_backend.js @@ -1,7 +1,28 @@ import { NodeType, OpCodeToSymbol, BlockType, OpCode } from "./utils"; -import { getNodeDataFromID } from "./directed_acyclic_graph"; +import { getNodeDataFromID, extractTypeInfo } from "./directed_acyclic_graph"; import * as FES from './strands_FES' +const TypeNames = { + 'float1': 'float', + 'float2': 'vec2', + 'float3': 'vec3', + 'float4': 'vec4', + + 'int1': 'int', + 'int2': 'ivec2', + 'int3': 'ivec3', + 'int4': 'ivec4', + + 'bool1': 'bool', + 'bool2': 'bvec2', + 'bool3': 'bvec3', + 'bool4': 'bvec4', + + 'mat2': 'mat2x2', + 'mat3': 'mat3x3', + 'mat4': 'mat4x4', +} + const cfgHandlers = { [BlockType.DEFAULT]: (blockID, strandsContext, generationContext) => { // const { dag, cfg } = strandsContext; @@ -19,7 +40,7 @@ const cfgHandlers = { [BlockType.IF_COND](blockID, strandsContext, generationContext) { const { dag, cfg } = strandsContext; const conditionID = cfg.blockConditions[blockID]; - const condExpr = glslBackend.generateExpression (dag, conditionID, generationContext); + const condExpr = glslBackend.generateExpression(generationContext, dag, conditionID); generationContext.write(`if (${condExpr}) {`) generationContext.indent++; this[BlockType.DEFAULT](blockID, strandsContext, generationContext); @@ -57,13 +78,26 @@ export const glslBackend = { }).join(', ')}) {`; return firstLine; }, - generateDataTypeName(baseType, dimension) { - return baseType + dimension; + + getTypeName(baseType, dimension) { + return TypeNames[baseType + dimension] }, - generateDeclaration() { + + generateDeclaration(generationContext, dag, nodeID) { + const expr = this.generateExpression(generationContext, dag, nodeID); + const tmp = `T${generationContext.nextTempID++}`; + generationContext.tempNames[nodeID] = tmp; + const T = extractTypeInfo(dag, nodeID); + const typeName = this.getTypeName(T.baseType, T.dimension); + return `${typeName} ${tmp} = ${expr};`; + }, + + generateReturn(generationContext, dag, nodeID) { + }, - generateExpression(dag, nodeID, generationContext) { + + generateExpression(generationContext, dag, nodeID) { const node = getNodeDataFromID(dag, nodeID); if (generationContext.tempNames?.[nodeID]) { return generationContext.tempNames[nodeID]; @@ -80,10 +114,10 @@ export const glslBackend = { if (node.opCode === OpCode.Nary.CONSTRUCTOR) { if (node.dependsOn.length === 1 && node.dimension === 1) { console.log("AARK") - return this.generateExpression(dag, node.dependsOn[0], generationContext); + return this.generateExpression(generationContext, dag, node.dependsOn[0]); } - const T = this.generateDataTypeName(node.baseType, node.dimension); - const deps = node.dependsOn.map((dep) => this.generateExpression(dag, dep, generationContext)); + const T = this.getTypeName(node.baseType, node.dimension); + const deps = node.dependsOn.map((dep) => this.generateExpression(generationContext, dag, dep)); return `${T}(${deps.join(', ')})`; } if (node.opCode === OpCode.Nary.FUNCTION) { @@ -91,8 +125,8 @@ export const glslBackend = { } if (node.dependsOn.length === 2) { const [lID, rID] = node.dependsOn; - const left = this.generateExpression(dag, lID, generationContext); - const right = this.generateExpression(dag, rID, generationContext); + const left = this.generateExpression(generationContext, dag, lID); + const right = this.generateExpression(generationContext, dag, rID); const opSym = OpCodeToSymbol[node.opCode]; if (useParantheses) { return `(${left} ${opSym} ${right})`; @@ -102,7 +136,7 @@ export const glslBackend = { } if (node.dependsOn.length === 1) { const [i] = node.dependsOn; - const val = this.generateExpression(dag, i, generationContext); + const val = this.generateExpression(generationContext, dag, i); const sym = OpCodeToSymbol[node.opCode]; return `${sym}${val}`; } @@ -111,6 +145,7 @@ export const glslBackend = { FES.internalError(`${node.nodeType} not working yet`) } }, + generateBlock(blockID, strandsContext, generationContext) { const type = strandsContext.cfg.blockTypes[blockID]; const handler = cfgHandlers[type] || cfgHandlers[BlockType.DEFAULT]; diff --git a/src/strands/builder.js b/src/strands/builder.js index a73669753f..b1121bba1c 100644 --- a/src/strands/builder.js +++ b/src/strands/builder.js @@ -1,7 +1,7 @@ import * as DAG from './directed_acyclic_graph' import * as CFG from './control_flow_graph' import * as FES from './strands_FES' -import { NodeType, OpCode, BaseType, BasePriority } from './utils'; +import { NodeType, OpCode, BaseType, extractTypeInfo } from './utils'; import { StrandsNode } from './user_API'; ////////////////////////////////////////////// @@ -39,16 +39,6 @@ export function createVariableNode(strandsContext, typeInfo, identifier) { return id; } -export function extractTypeInfo(strandsContext, nodeID) { - const dag = strandsContext.dag; - const baseType = dag.baseTypes[nodeID]; - return { - baseType, - dimension: dag.dimensions[nodeID], - priority: BasePriority[baseType], - }; -} - export function createBinaryOpNode(strandsContext, leftStrandsNode, rightArg, opCode) { const { dag, cfg } = strandsContext; // Construct a node for right if its just an array or number etc. diff --git a/src/strands/code_generation.js b/src/strands/code_generation.js index 9d47aff468..d807797499 100644 --- a/src/strands/code_generation.js +++ b/src/strands/code_generation.js @@ -1,21 +1,19 @@ import { WEBGL } from '../core/constants'; import { glslBackend } from './GLSL_backend'; import { NodeType } from './utils'; -import { extractTypeInfo } from './builder'; import { sortCFG } from './control_flow_graph'; import { sortDAG } from './directed_acyclic_graph'; let globalTempCounter = 0; let backend; -function generateTopLevelDeclarations(strandsContext, dagOrder) { +function generateTopLevelDeclarations(strandsContext, generationContext, dagOrder) { const usedCount = {}; const dag = strandsContext.dag; for (const nodeID of dagOrder) { usedCount[nodeID] = (dag.usedBy[nodeID] || []).length; } - const tempNames = {}; const declarations = []; for (const nodeID of dagOrder) { if (dag.nodeTypes[nodeID] !== NodeType.OPERATION) { @@ -23,16 +21,12 @@ function generateTopLevelDeclarations(strandsContext, dagOrder) { } if (usedCount[nodeID] > 0) { - const expr = backend.generateExpression(dag, nodeID, { tempNames }); - const tmp = `T${globalTempCounter++}`; - tempNames[nodeID] = tmp; - - const T = extractTypeInfo(strandsContext, nodeID); - declarations.push(`${T.baseType+T.dimension} ${tmp} = ${expr};`); + const newDeclaration = backend.generateDeclaration(generationContext, dag, nodeID); + declarations.push(newDeclaration); } } - return { declarations, tempNames }; + return declarations; } export function generateShaderCode(strandsContext) { @@ -47,14 +41,18 @@ export function generateShaderCode(strandsContext) { const cfgSorted = sortCFG(cfg.outgoingEdges, entryBlockID); const generationContext = { - ...generateTopLevelDeclarations(strandsContext, dagSorted), indent: 1, codeLines: [], write(line) { this.codeLines.push(' '.repeat(this.indent) + line); }, dagSorted, + tempNames: {}, + declarations: [], + nextTempID: 0, }; + generationContext.declarations = generateTopLevelDeclarations(strandsContext, generationContext, dagSorted); + generationContext.declarations.forEach(decl => generationContext.write(decl)); for (const blockID of cfgSorted) { @@ -62,10 +60,9 @@ export function generateShaderCode(strandsContext) { } const firstLine = backend.hookEntry(hookType); - const finalExpression = `return ${backend.generateExpression(dag, rootNodeID, generationContext)};`; + const finalExpression = `return ${backend.generateExpression(generationContext, dag, rootNodeID)};`; generationContext.write(finalExpression); - console.log(hookType); - hooksObj[hookType.name] = [firstLine, ...generationContext.codeLines, '}'].join('\n'); + hooksObj[`${hookType.returnType.typeName} ${hookType.name}`] = [firstLine, ...generationContext.codeLines, '}'].join('\n'); } return hooksObj; diff --git a/src/strands/directed_acyclic_graph.js b/src/strands/directed_acyclic_graph.js index 34b63d919f..5c5200438e 100644 --- a/src/strands/directed_acyclic_graph.js +++ b/src/strands/directed_acyclic_graph.js @@ -1,4 +1,4 @@ -import { NodeTypeRequiredFields, NodeTypeToName } from './utils'; +import { NodeTypeRequiredFields, NodeTypeToName, BasePriority } from './utils'; import * as FES from './strands_FES'; ///////////////////////////////// @@ -67,6 +67,13 @@ export function getNodeDataFromID(graph, id) { } } +export function extractTypeInfo(dag, nodeID) { + return { + baseType: dag.baseTypes[nodeID], + dimension: dag.dimensions[nodeID], + priority: BasePriority[dag.baseTypes[nodeID]], + }; +} ///////////////////////////////// // Private functions ///////////////////////////////// diff --git a/src/strands/p5.strands.js b/src/strands/p5.strands.js index 6d9bc8a0d6..77f9d8b73a 100644 --- a/src/strands/p5.strands.js +++ b/src/strands/p5.strands.js @@ -70,13 +70,14 @@ function strands(p5, fn) { // 3. Generate shader code hooks object from the IR // ....... const hooksObject = generateShaderCode(strandsContext); - console.log(hooksObject.getFinalColor); - - // Call modify with the generated hooks object - // return oldModify.call(this, generatedModifyArgument); + console.log(hooksObject); + console.log(hooksObject['vec4 getFinalColor']); // Reset the strands runtime context // deinitStrandsContext(strandsContext); + + // Call modify with the generated hooks object + return oldModify.call(this, hooksObject); } else { return oldModify.call(this, shaderModifier) diff --git a/src/strands/user_API.js b/src/strands/user_API.js index 1ddb7dc6c9..08ddaf8237 100644 --- a/src/strands/user_API.js +++ b/src/strands/user_API.js @@ -152,8 +152,9 @@ function createHookArguments(strandsContext, parameters){ args.push(argObject); } else { const typeInfo = TypeInfoFromGLSLName[paramType.typeName]; - const arg = createVariableNode(strandsContext, typeInfo, param.name); - args.push(arg) + const id = createVariableNode(strandsContext, typeInfo, param.name); + const arg = new StrandsNode(id); + args.push(arg); } } return args; @@ -174,7 +175,7 @@ export function createShaderHooksFunctions(strandsContext, fn, shader) { CFG.pushBlock(cfg, entryBlockID); const args = createHookArguments(strandsContext, hookType.parameters); - const returned = hookUserCallback(args); + const returned = hookUserCallback(...args); let returnedNode; const expectedReturnType = hookType.returnType; From 850923155b20ebca69da1cc69c13756db4ae1647 Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Thu, 24 Jul 2025 14:27:17 +0100 Subject: [PATCH 12/69] rename file --- src/strands/{user_API.js => strands_api.js} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/strands/{user_API.js => strands_api.js} (100%) diff --git a/src/strands/user_API.js b/src/strands/strands_api.js similarity index 100% rename from src/strands/user_API.js rename to src/strands/strands_api.js From 47eda1a5d70b660d8b72b292a3fa6945b8aa9b29 Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Thu, 24 Jul 2025 14:27:51 +0100 Subject: [PATCH 13/69] update api imports for new filename --- src/strands/builder.js | 4 ++-- src/strands/p5.strands.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/strands/builder.js b/src/strands/builder.js index b1121bba1c..421fa9bdb5 100644 --- a/src/strands/builder.js +++ b/src/strands/builder.js @@ -1,8 +1,8 @@ import * as DAG from './directed_acyclic_graph' import * as CFG from './control_flow_graph' import * as FES from './strands_FES' -import { NodeType, OpCode, BaseType, extractTypeInfo } from './utils'; -import { StrandsNode } from './user_API'; +import { NodeType, OpCode, BaseType } from './utils'; +import { StrandsNode } from './strands_api'; ////////////////////////////////////////////// // Builders for node graphs diff --git a/src/strands/p5.strands.js b/src/strands/p5.strands.js index 77f9d8b73a..a3e85ac945 100644 --- a/src/strands/p5.strands.js +++ b/src/strands/p5.strands.js @@ -12,7 +12,7 @@ import { BlockType } from './utils'; import { createDirectedAcyclicGraph } from './directed_acyclic_graph' import { createControlFlowGraph, createBasicBlock, pushBlock, popBlock } from './control_flow_graph'; import { generateShaderCode } from './code_generation'; -import { initGlobalStrandsAPI, createShaderHooksFunctions } from './user_API'; +import { initGlobalStrandsAPI, createShaderHooksFunctions } from './strands_api'; function strands(p5, fn) { ////////////////////////////////////////////// From 1088b4de33bb78d112b757d146edbadf1fa2bc1b Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Thu, 24 Jul 2025 14:28:45 +0100 Subject: [PATCH 14/69] move extractTypeInfo and rename to extractNodeTypeInfo --- src/strands/GLSL_backend.js | 4 ++-- src/strands/builder.js | 4 ++-- src/strands/directed_acyclic_graph.js | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/strands/GLSL_backend.js b/src/strands/GLSL_backend.js index c92e3f688f..d921aed364 100644 --- a/src/strands/GLSL_backend.js +++ b/src/strands/GLSL_backend.js @@ -1,5 +1,5 @@ import { NodeType, OpCodeToSymbol, BlockType, OpCode } from "./utils"; -import { getNodeDataFromID, extractTypeInfo } from "./directed_acyclic_graph"; +import { getNodeDataFromID, extractNodeTypeInfo } from "./directed_acyclic_graph"; import * as FES from './strands_FES' const TypeNames = { @@ -88,7 +88,7 @@ export const glslBackend = { const tmp = `T${generationContext.nextTempID++}`; generationContext.tempNames[nodeID] = tmp; - const T = extractTypeInfo(dag, nodeID); + const T = extractNodeTypeInfo(dag, nodeID); const typeName = this.getTypeName(T.baseType, T.dimension); return `${typeName} ${tmp} = ${expr};`; }, diff --git a/src/strands/builder.js b/src/strands/builder.js index 421fa9bdb5..b5e12ebeca 100644 --- a/src/strands/builder.js +++ b/src/strands/builder.js @@ -53,8 +53,8 @@ export function createBinaryOpNode(strandsContext, leftStrandsNode, rightArg, op let finalRightNodeID = rightStrandsNode.id; // Check if we have to cast either node - const leftType = extractTypeInfo(strandsContext, leftStrandsNode.id); - const rightType = extractTypeInfo(strandsContext, rightStrandsNode.id); + const leftType = DAG.extractNodeTypeInfo(dag, leftStrandsNode.id); + const rightType = DAG.extractNodeTypeInfo(dag, rightStrandsNode.id); const cast = { node: null, toType: leftType }; const bothDeferred = leftType.baseType === rightType.baseType && leftType.baseType === BaseType.DEFER; diff --git a/src/strands/directed_acyclic_graph.js b/src/strands/directed_acyclic_graph.js index 5c5200438e..5efc98080f 100644 --- a/src/strands/directed_acyclic_graph.js +++ b/src/strands/directed_acyclic_graph.js @@ -67,7 +67,7 @@ export function getNodeDataFromID(graph, id) { } } -export function extractTypeInfo(dag, nodeID) { +export function extractNodeTypeInfo(dag, nodeID) { return { baseType: dag.baseTypes[nodeID], dimension: dag.dimensions[nodeID], From 87e8a99ba2d1e42afb867546b26a6046b612ba6e Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Thu, 24 Jul 2025 14:58:37 +0100 Subject: [PATCH 15/69] rename files for clarity --- preview/global/sketch.js | 11 +++- src/strands/{builder.js => ir_builders.js} | 7 +-- .../{control_flow_graph.js => ir_cfg.js} | 2 +- .../{directed_acyclic_graph.js => ir_dag.js} | 54 +++++++++-------- src/strands/{utils.js => ir_types.js} | 58 +++++++++---------- src/strands/p5.strands.js | 15 ++--- src/strands/strands_api.js | 8 +-- ...hader_functions.js => strands_builtins.js} | 18 +++--- ...{code_generation.js => strands_codegen.js} | 20 +++---- src/strands/strands_conditionals.js | 4 +- ...GLSL_backend.js => strands_glslBackend.js} | 4 +- ...de_transpiler.js => strands_transpiler.js} | 0 12 files changed, 99 insertions(+), 102 deletions(-) rename src/strands/{builder.js => ir_builders.js} (97%) rename src/strands/{control_flow_graph.js => ir_cfg.js} (97%) rename src/strands/{directed_acyclic_graph.js => ir_dag.js} (73%) rename src/strands/{utils.js => ir_types.js} (68%) rename src/strands/{shader_functions.js => strands_builtins.js} (86%) rename src/strands/{code_generation.js => strands_codegen.js} (81%) rename src/strands/{GLSL_backend.js => strands_glslBackend.js} (96%) rename src/strands/{code_transpiler.js => strands_transpiler.js} (100%) diff --git a/preview/global/sketch.js b/preview/global/sketch.js index fe768cb428..208260102a 100644 --- a/preview/global/sketch.js +++ b/preview/global/sketch.js @@ -3,18 +3,23 @@ p5.disableFriendlyErrors = true; function callback() { getFinalColor((col) => { let y = col.sub(-1,1,0,0); - return col.add(y); + + return y//mix(0, col.add(y), 1); }); } async function setup(){ - createCanvas(300,400, WEBGL) + createCanvas(windowWidth,windowHeight, WEBGL) bloomShader = baseColorShader().newModify(callback, {parser: false}); } +function windowResized() { + resizeCanvas(windowWidth, windowHeight); +} + function draw(){ orbitControl(); background(0); shader(bloomShader); - sphere(100) + sphere(300) } diff --git a/src/strands/builder.js b/src/strands/ir_builders.js similarity index 97% rename from src/strands/builder.js rename to src/strands/ir_builders.js index b5e12ebeca..2acd29b986 100644 --- a/src/strands/builder.js +++ b/src/strands/ir_builders.js @@ -1,7 +1,7 @@ -import * as DAG from './directed_acyclic_graph' -import * as CFG from './control_flow_graph' +import * as DAG from './ir_dag' +import * as CFG from './ir_cfg' import * as FES from './strands_FES' -import { NodeType, OpCode, BaseType } from './utils'; +import { NodeType, OpCode, BaseType } from './ir_types'; import { StrandsNode } from './strands_api'; ////////////////////////////////////////////// @@ -57,7 +57,6 @@ export function createBinaryOpNode(strandsContext, leftStrandsNode, rightArg, op const rightType = DAG.extractNodeTypeInfo(dag, rightStrandsNode.id); const cast = { node: null, toType: leftType }; const bothDeferred = leftType.baseType === rightType.baseType && leftType.baseType === BaseType.DEFER; - if (bothDeferred) { finalLeftNodeID = createTypeConstructorNode(strandsContext, { baseType:BaseType.FLOAT, dimension: leftType.dimension }, leftStrandsNode); finalRightNodeID = createTypeConstructorNode(strandsContext, { baseType:BaseType.FLOAT, dimension: leftType.dimension }, rightStrandsNode); diff --git a/src/strands/control_flow_graph.js b/src/strands/ir_cfg.js similarity index 97% rename from src/strands/control_flow_graph.js rename to src/strands/ir_cfg.js index 341f62871d..27a323b885 100644 --- a/src/strands/control_flow_graph.js +++ b/src/strands/ir_cfg.js @@ -1,4 +1,4 @@ -import { BlockTypeToName } from "./utils"; +import { BlockTypeToName } from "./ir_types"; export function createControlFlowGraph() { return { diff --git a/src/strands/directed_acyclic_graph.js b/src/strands/ir_dag.js similarity index 73% rename from src/strands/directed_acyclic_graph.js rename to src/strands/ir_dag.js index 5efc98080f..ae384aa346 100644 --- a/src/strands/directed_acyclic_graph.js +++ b/src/strands/ir_dag.js @@ -1,4 +1,4 @@ -import { NodeTypeRequiredFields, NodeTypeToName, BasePriority } from './utils'; +import { NodeTypeRequiredFields, NodeTypeToName, BasePriority } from './ir_types'; import * as FES from './strands_FES'; ///////////////////////////////// @@ -39,15 +39,15 @@ export function getOrCreateNode(graph, node) { export function createNodeData(data = {}) { const node = { - nodeType: data.nodeType ?? null, - baseType: data.baseType ?? null, - dimension: data.dimension ?? null, - opCode: data.opCode ?? null, - value: data.value ?? null, + nodeType: data.nodeType ?? null, + baseType: data.baseType ?? null, + dimension: data.dimension ?? null, + opCode: data.opCode ?? null, + value: data.value ?? null, identifier: data.identifier ?? null, - dependsOn: Array.isArray(data.dependsOn) ? data.dependsOn : [], + dependsOn: Array.isArray(data.dependsOn) ? data.dependsOn : [], usedBy: Array.isArray(data.usedBy) ? data.usedBy : [], - phiBlocks: Array.isArray(data.phiBlocks) ? data.phiBlocks : [], + phiBlocks: Array.isArray(data.phiBlocks) ? data.phiBlocks : [], }; validateNode(node); return node; @@ -55,15 +55,15 @@ export function createNodeData(data = {}) { export function getNodeDataFromID(graph, id) { return { - nodeType: graph.nodeTypes[id], - opCode: graph.opCodes[id], - value: graph.values[id], + nodeType: graph.nodeTypes[id], + opCode: graph.opCodes[id], + value: graph.values[id], identifier: graph.identifiers[id], - dependsOn: graph.dependsOn[id], - usedBy: graph.usedBy[id], - phiBlocks: graph.phiBlocks[id], - dimension: graph.dimensions[id], - baseType: graph.baseTypes[id], + dependsOn: graph.dependsOn[id], + usedBy: graph.usedBy[id], + phiBlocks: graph.phiBlocks[id], + dimension: graph.dimensions[id], + baseType: graph.baseTypes[id], } } @@ -79,18 +79,16 @@ export function extractNodeTypeInfo(dag, nodeID) { ///////////////////////////////// function createNode(graph, node) { const id = graph.nextID++; - graph.nodeTypes[id] = node.nodeType; - graph.opCodes[id] = node.opCode; - graph.values[id] = node.value; + graph.nodeTypes[id] = node.nodeType; + graph.opCodes[id] = node.opCode; + graph.values[id] = node.value; graph.identifiers[id] = node.identifier; - graph.dependsOn[id] = node.dependsOn.slice(); - graph.usedBy[id] = node.usedBy; - graph.phiBlocks[id] = node.phiBlocks.slice(); - - graph.baseTypes[id] = node.baseType - graph.dimensions[id] = node.dimension; - - + graph.dependsOn[id] = node.dependsOn.slice(); + graph.usedBy[id] = node.usedBy; + graph.phiBlocks[id] = node.phiBlocks.slice(); + graph.baseTypes[id] = node.baseType + graph.dimensions[id] = node.dimension; + for (const dep of node.dependsOn) { if (!Array.isArray(graph.usedBy[dep])) { graph.usedBy[dep] = []; @@ -125,7 +123,7 @@ function validateNode(node){ export function sortDAG(adjacencyList, start) { const visited = new Set(); const postOrder = []; - + function dfs(v) { if (visited.has(v)) { return; diff --git a/src/strands/utils.js b/src/strands/ir_types.js similarity index 68% rename from src/strands/utils.js rename to src/strands/ir_types.js index bcb00c32e5..f84a2e8aa9 100644 --- a/src/strands/utils.js +++ b/src/strands/ir_types.js @@ -14,19 +14,19 @@ export const NodeTypeToName = Object.fromEntries( ); export const NodeTypeRequiredFields = { - [NodeType.OPERATION]: ['opCode', 'dependsOn'], - [NodeType.LITERAL]: ['value'], - [NodeType.VARIABLE]: ['identifier'], - [NodeType.CONSTANT]: ['value'], - [NodeType.PHI]: ['dependsOn', 'phiBlocks'] + [NodeType.OPERATION]: ["opCode", "dependsOn"], + [NodeType.LITERAL]: ["value"], + [NodeType.VARIABLE]: ["identifier"], + [NodeType.CONSTANT]: ["value"], + [NodeType.PHI]: ["dependsOn", "phiBlocks"] }; export const BaseType = { - FLOAT: 'float', - INT: 'int', - BOOL: 'bool', - MAT: 'mat', - DEFER: 'defer', + FLOAT: "float", + INT: "int", + BOOL: "bool", + MAT: "mat", + DEFER: "defer", }; export const BasePriority = { @@ -38,26 +38,26 @@ export const BasePriority = { }; export const TypeInfo = { - 'float1': { fnName: 'float', baseType: BaseType.FLOAT, dimension:1, priority: 3, }, - 'float2': { fnName: 'vec2', baseType: BaseType.FLOAT, dimension:2, priority: 3, }, - 'float3': { fnName: 'vec3', baseType: BaseType.FLOAT, dimension:3, priority: 3, }, - 'float4': { fnName: 'vec4', baseType: BaseType.FLOAT, dimension:4, priority: 3, }, - - 'int1': { fnName: 'int', baseType: BaseType.INT, dimension:1, priority: 2, }, - 'int2': { fnName: 'ivec2', baseType: BaseType.INT, dimension:2, priority: 2, }, - 'int3': { fnName: 'ivec3', baseType: BaseType.INT, dimension:3, priority: 2, }, - 'int4': { fnName: 'ivec4', baseType: BaseType.INT, dimension:4, priority: 2, }, - - 'bool1': { fnName: 'bool', baseType: BaseType.BOOL, dimension:1, priority: 1, }, - 'bool2': { fnName: 'bvec2', baseType: BaseType.BOOL, dimension:2, priority: 1, }, - 'bool3': { fnName: 'bvec3', baseType: BaseType.BOOL, dimension:3, priority: 1, }, - 'bool4': { fnName: 'bvec4', baseType: BaseType.BOOL, dimension:4, priority: 1, }, - - 'mat2': { fnName: 'mat2x2', baseType: BaseType.MAT, dimension:2, priority: 0, }, - 'mat3': { fnName: 'mat3x3', baseType: BaseType.MAT, dimension:3, priority: 0, }, - 'mat4': { fnName: 'mat4x4', baseType: BaseType.MAT, dimension:4, priority: 0, }, + float1: { fnName: "float", baseType: BaseType.FLOAT, dimension:1, priority: 3, }, + float2: { fnName: "vec2", baseType: BaseType.FLOAT, dimension:2, priority: 3, }, + float3: { fnName: "vec3", baseType: BaseType.FLOAT, dimension:3, priority: 3, }, + float4: { fnName: "vec4", baseType: BaseType.FLOAT, dimension:4, priority: 3, }, + int1: { fnName: "int", baseType: BaseType.INT, dimension:1, priority: 2, }, + int2: { fnName: "ivec2", baseType: BaseType.INT, dimension:2, priority: 2, }, + int3: { fnName: "ivec3", baseType: BaseType.INT, dimension:3, priority: 2, }, + int4: { fnName: "ivec4", baseType: BaseType.INT, dimension:4, priority: 2, }, + bool1: { fnName: "bool", baseType: BaseType.BOOL, dimension:1, priority: 1, }, + bool2: { fnName: "bvec2", baseType: BaseType.BOOL, dimension:2, priority: 1, }, + bool3: { fnName: "bvec3", baseType: BaseType.BOOL, dimension:3, priority: 1, }, + bool4: { fnName: "bvec4", baseType: BaseType.BOOL, dimension:4, priority: 1, }, + mat2: { fnName: "mat2x2", baseType: BaseType.MAT, dimension:2, priority: 0, }, + 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 }, +} - 'defer': { fnName: null, baseType: BaseType.DEFER, dimension: null, priority: -1 }, +export function typeEquals(nodeA, nodeB) { + return (nodeA.dimension === nodeB.dimension) && (nodeA.baseType === nodeB.baseType); } export const TypeInfoFromGLSLName = Object.fromEntries( diff --git a/src/strands/p5.strands.js b/src/strands/p5.strands.js index a3e85ac945..0c31a499ff 100644 --- a/src/strands/p5.strands.js +++ b/src/strands/p5.strands.js @@ -5,13 +5,14 @@ * @requires core */ import { WEBGL, /*WEBGPU*/ } from '../core/constants' +import { glslBackend } from './strands_glslBackend'; -import { transpileStrandsToJS } from './code_transpiler'; -import { BlockType } from './utils'; +import { transpileStrandsToJS } from './strands_transpiler'; +import { BlockType } from './ir_types'; -import { createDirectedAcyclicGraph } from './directed_acyclic_graph' -import { createControlFlowGraph, createBasicBlock, pushBlock, popBlock } from './control_flow_graph'; -import { generateShaderCode } from './code_generation'; +import { createDirectedAcyclicGraph } from './ir_dag' +import { createControlFlowGraph, createBasicBlock, pushBlock, popBlock } from './ir_cfg'; +import { generateShaderCode } from './strands_codegen'; import { initGlobalStrandsAPI, createShaderHooksFunctions } from './strands_api'; function strands(p5, fn) { @@ -49,8 +50,8 @@ function strands(p5, fn) { p5.Shader.prototype.newModify = function(shaderModifier, options = { parser: true, srcLocations: false }) { if (shaderModifier instanceof Function) { // Reset the context object every time modify is called; - const backend = WEBGL; - initStrandsContext(strandsContext, backend); + const backend = glslBackend; + initStrandsContext(strandsContext, glslBackend); createShaderHooksFunctions(strandsContext, fn, this); // 1. Transpile from strands DSL to JS diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index 08ddaf8237..7410368912 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -5,11 +5,11 @@ import { createStatementNode, createTypeConstructorNode, createUnaryOpNode, -} from './builder' -import { OperatorTable, BlockType, TypeInfo, BaseType, TypeInfoFromGLSLName } from './utils' -import { strandsShaderFunctions } from './shader_functions' +} from './ir_builders' +import { OperatorTable, BlockType, TypeInfo, BaseType, TypeInfoFromGLSLName } from './ir_types' +import { strandsShaderFunctions } from './strands_builtins' import { StrandsConditional } from './strands_conditionals' -import * as CFG from './control_flow_graph' +import * as CFG from './ir_cfg' import * as FES from './strands_FES' ////////////////////////////////////////////// diff --git a/src/strands/shader_functions.js b/src/strands/strands_builtins.js similarity index 86% rename from src/strands/shader_functions.js rename to src/strands/strands_builtins.js index 1c95d0702a..946089e245 100644 --- a/src/strands/shader_functions.js +++ b/src/strands/strands_builtins.js @@ -38,15 +38,15 @@ const builtInGLSLFunctions = { 'log2': [{ args: ['genType'], returnType: 'genType', isp5Function: false}], 'max': [ { args: ['genType', 'genType'], returnType: 'genType', isp5Function: true}, - { args: ['genType', 'float'], returnType: 'genType', isp5Function: true}, + { args: ['genType', 'float1'], returnType: 'genType', isp5Function: true}, ], 'min': [ { args: ['genType', 'genType'], returnType: 'genType', isp5Function: true}, - { args: ['genType', 'float'], returnType: 'genType', isp5Function: true}, + { args: ['genType', 'float1'], returnType: 'genType', isp5Function: true}, ], 'mix': [ { args: ['genType', 'genType', 'genType'], returnType: 'genType', isp5Function: false}, - { args: ['genType', 'genType', 'float'], returnType: 'genType', isp5Function: false}, + { args: ['genType', 'genType', 'float1'], returnType: 'genType', isp5Function: false}, ], // 'mod': [{}], // 'modf': [{}], @@ -56,7 +56,7 @@ const builtInGLSLFunctions = { // 'sign': [{}], 'smoothstep': [ { args: ['genType', 'genType', 'genType'], returnType: 'genType', isp5Function: false}, - { args: ['float', 'float', 'genType'], returnType: 'genType', isp5Function: false}, + { args: ['float1', 'float1', 'genType'], returnType: 'genType', isp5Function: false}, ], 'sqrt': [{ args: ['genType'], returnType: 'genType', isp5Function: true}], 'step': [{ args: ['genType', 'genType'], returnType: 'genType', isp5Function: false}], @@ -64,18 +64,18 @@ const builtInGLSLFunctions = { ////////// Vector ////////// 'cross': [{ args: ['vec3', 'vec3'], returnType: 'vec3', isp5Function: true}], - 'distance': [{ args: ['genType', 'genType'], returnType: 'float', isp5Function: true}], - 'dot': [{ args: ['genType', 'genType'], returnType: 'float', isp5Function: true}], + 'distance': [{ args: ['genType', 'genType'], returnType: 'float1', isp5Function: true}], + 'dot': [{ args: ['genType', 'genType'], returnType: 'float1', isp5Function: true}], // 'equal': [{}], 'faceforward': [{ args: ['genType', 'genType', 'genType'], returnType: 'genType', isp5Function: false}], - 'length': [{ args: ['genType'], returnType: 'float', isp5Function: false}], + 'length': [{ args: ['genType'], returnType: 'float1', isp5Function: false}], 'normalize': [{ args: ['genType'], returnType: 'genType', isp5Function: true}], // 'notEqual': [{}], 'reflect': [{ args: ['genType', 'genType'], returnType: 'genType', isp5Function: false}], - 'refract': [{ args: ['genType', 'genType', 'float'], returnType: 'genType', isp5Function: false}], + 'refract': [{ args: ['genType', 'genType', 'float1'], returnType: 'genType', isp5Function: false}], ////////// Texture sampling ////////// - 'texture': [{args: ['sampler2D', 'vec2'], returnType: 'vec4', isp5Function: true}], + 'texture': [{args: ['sampler2D', 'float2'], returnType: 'float4', isp5Function: true}], } export const strandsShaderFunctions = { diff --git a/src/strands/code_generation.js b/src/strands/strands_codegen.js similarity index 81% rename from src/strands/code_generation.js rename to src/strands/strands_codegen.js index d807797499..904add554d 100644 --- a/src/strands/code_generation.js +++ b/src/strands/strands_codegen.js @@ -1,15 +1,11 @@ -import { WEBGL } from '../core/constants'; -import { glslBackend } from './GLSL_backend'; -import { NodeType } from './utils'; -import { sortCFG } from './control_flow_graph'; -import { sortDAG } from './directed_acyclic_graph'; - -let globalTempCounter = 0; -let backend; +import { NodeType } from './ir_types'; +import { sortCFG } from './ir_cfg'; +import { sortDAG } from './ir_dag'; function generateTopLevelDeclarations(strandsContext, generationContext, dagOrder) { + const { dag, backend } = strandsContext; + const usedCount = {}; - const dag = strandsContext.dag; for (const nodeID of dagOrder) { usedCount[nodeID] = (dag.usedBy[nodeID] || []).length; } @@ -30,13 +26,11 @@ function generateTopLevelDeclarations(strandsContext, generationContext, dagOrde } export function generateShaderCode(strandsContext) { - if (strandsContext.backend === WEBGL) { - backend = glslBackend; - } + const { cfg, dag, backend } = strandsContext; + const hooksObj = {}; for (const { hookType, entryBlockID, rootNodeID} of strandsContext.hooks) { - const { cfg, dag } = strandsContext; const dagSorted = sortDAG(dag.dependsOn, rootNodeID); const cfgSorted = sortCFG(cfg.outgoingEdges, entryBlockID); diff --git a/src/strands/strands_conditionals.js b/src/strands/strands_conditionals.js index e1da496c02..1ce888cc91 100644 --- a/src/strands/strands_conditionals.js +++ b/src/strands/strands_conditionals.js @@ -1,5 +1,5 @@ -import * as CFG from './control_flow_graph' -import { BlockType } from './utils'; +import * as CFG from './ir_cfg' +import { BlockType } from './ir_types'; export class StrandsConditional { constructor(strandsContext, condition, branchCallback) { diff --git a/src/strands/GLSL_backend.js b/src/strands/strands_glslBackend.js similarity index 96% rename from src/strands/GLSL_backend.js rename to src/strands/strands_glslBackend.js index d921aed364..5862adb184 100644 --- a/src/strands/GLSL_backend.js +++ b/src/strands/strands_glslBackend.js @@ -1,5 +1,5 @@ -import { NodeType, OpCodeToSymbol, BlockType, OpCode } from "./utils"; -import { getNodeDataFromID, extractNodeTypeInfo } from "./directed_acyclic_graph"; +import { NodeType, OpCodeToSymbol, BlockType, OpCode } from "./ir_types"; +import { getNodeDataFromID, extractNodeTypeInfo } from "./ir_dag"; import * as FES from './strands_FES' const TypeNames = { diff --git a/src/strands/code_transpiler.js b/src/strands/strands_transpiler.js similarity index 100% rename from src/strands/code_transpiler.js rename to src/strands/strands_transpiler.js From e32fd47267cc3beea72788c17ba85947c059c3d0 Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Thu, 24 Jul 2025 18:56:47 +0100 Subject: [PATCH 16/69] builtin function overloads type checking --- preview/global/sketch.js | 2 +- src/strands/ir_builders.js | 84 ++++++++++++++-- src/strands/ir_types.js | 10 +- src/strands/p5.strands.js | 1 - src/strands/strands_api.js | 22 ++-- src/strands/strands_builtins.js | 160 ++++++++++++++++++------------ src/strands/strands_transpiler.js | 2 - 7 files changed, 192 insertions(+), 89 deletions(-) diff --git a/preview/global/sketch.js b/preview/global/sketch.js index 208260102a..25ec2fd398 100644 --- a/preview/global/sketch.js +++ b/preview/global/sketch.js @@ -4,7 +4,7 @@ function callback() { getFinalColor((col) => { let y = col.sub(-1,1,0,0); - return y//mix(0, col.add(y), 1); + return mix(float(0), col.add(y), float(1)); }); } diff --git a/src/strands/ir_builders.js b/src/strands/ir_builders.js index 2acd29b986..8a4ffb399a 100644 --- a/src/strands/ir_builders.js +++ b/src/strands/ir_builders.js @@ -1,8 +1,10 @@ import * as DAG from './ir_dag' import * as CFG from './ir_cfg' import * as FES from './strands_FES' -import { NodeType, OpCode, BaseType } from './ir_types'; +import { NodeType, OpCode, BaseType, typeEquals, GenType } from './ir_types'; import { StrandsNode } from './strands_api'; +import { strandsBuiltinFunctions } from './strands_builtins'; +import { ar } from 'vitest/dist/chunks/reporters.D7Jzd9GS.js'; ////////////////////////////////////////////// // Builders for node graphs @@ -167,18 +169,86 @@ export function createTypeConstructorNode(strandsContext, typeInfo, dependsOn) { return id; } -export function createFunctionCallNode(strandsContext, identifier, overrides, dependsOn) { +export function createFunctionCallNode(strandsContext, functionName, userArgs) { const { cfg, dag } = strandsContext; - let typeInfo = { baseType: null, dimension: null }; + console.log("HELLOOOOOOOO") + const overloads = strandsBuiltinFunctions[functionName]; + const matchingArgsCounts = overloads.filter(overload => overload.params.length === userArgs.length); + if (matchingArgsCounts.length === 0) { + const argsLengthSet = new Set(); + const argsLengthArr = []; + overloads.forEach((overload) => argsLengthSet.add(overload.params.length)); + argsLengthSet.forEach((len) => argsLengthArr.push(`${len}`)); + const argsLengthStr = argsLengthArr.join(' or '); + FES.userError("parameter validation error",`Function '${functionName}' has ${overloads.length} variants which expect ${argsLengthStr} arguments, but ${userArgs.length} arguments were provided.`); + } + + let bestOverload = null; + let bestScore = 0; + let inferredReturnType = null; + for (const overload of matchingArgsCounts) { + let isValid = true; + let overloadParamTypes = []; + let inferredDimension = null; + let similarity = 0; + + for (let i = 0; i < userArgs.length; i++) { + const argType = DAG.extractNodeTypeInfo(userArgs[i]); + const expectedType = overload.params[i]; + let dimension = expectedType.dimension; + + const isGeneric = (T) => T.dimension === null; + if (isGeneric(expectedType)) { + if (inferredDimension === null || inferredDimension === 1) { + inferredDimension = argType.dimension; + } + if (inferredDimension !== argType.dimension) { + isValid = false; + } + dimension = inferredDimension; + } + else { + if (argType.dimension > dimension) { + isValid = false; + } + } + + if (argType.baseType === expectedType.baseType) { + similarity += 2; + } + else if(expectedType.priority > argType.priority) { + similarity += 1; + } + + overloadParamTypes.push({ baseType: expectedType.baseType, dimension }); + } + + if (isValid && (!bestOverload || similarity > bestScore)) { + bestOverload = overloadParamTypes; + bestScore = similarity; + inferredReturnType = overload.returnType; + if (isGeneric(inferredReturnType)) { + inferredReturnType.dimension = inferredDimension; + } + } + } + + if (bestOverload === null) { + const paramsString = (params) => `(${params.map((param) => param).join(', ')})`; + const expectedArgsString = overloads.map(overload => paramsString(overload.params)).join(' or '); + const providedArgsString = paramsString(userArgs.map((arg)=>arg.baseType+arg.dimension)); + throw new Error(`Function '${functionName}' was called with wrong arguments. Most likely, you provided mixed lengths vectors as arguments.\nExpected argument types: ${expectedArgsString}\nProvided argument types: ${providedArgsString}\nAll of the arguments with expected type 'genType' should have a matching type. If one of those is different, try to find where it was created. + `); + } const nodeData = DAG.createNodeData({ nodeType: NodeType.OPERATION, opCode: OpCode.Nary.FUNCTION_CALL, - identifier, - overrides, - dependsOn, + identifier: functionName, + dependsOn: userArgs, // no type info yet - ...typeInfo, + baseType: inferredReturnType.baseType, + dimension: inferredReturnType.dimension }) const id = DAG.getOrCreateNode(dag, nodeData); CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); diff --git a/src/strands/ir_types.js b/src/strands/ir_types.js index f84a2e8aa9..007f22de51 100644 --- a/src/strands/ir_types.js +++ b/src/strands/ir_types.js @@ -37,7 +37,7 @@ export const BasePriority = { [BaseType.DEFER]: -1, }; -export const TypeInfo = { +export const DataType = { float1: { fnName: "float", baseType: BaseType.FLOAT, dimension:1, priority: 3, }, float2: { fnName: "vec2", baseType: BaseType.FLOAT, dimension:2, priority: 3, }, float3: { fnName: "vec3", baseType: BaseType.FLOAT, dimension:3, priority: 3, }, @@ -56,12 +56,18 @@ export const TypeInfo = { defer: { fnName: null, baseType: BaseType.DEFER, dimension: null, priority: -1 }, } +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(TypeInfo) + Object.values(DataType) .filter(info => info.fnName !== null) .map(info => [info.fnName, info]) ); diff --git a/src/strands/p5.strands.js b/src/strands/p5.strands.js index 0c31a499ff..be2be91595 100644 --- a/src/strands/p5.strands.js +++ b/src/strands/p5.strands.js @@ -4,7 +4,6 @@ * @for p5 * @requires core */ -import { WEBGL, /*WEBGPU*/ } from '../core/constants' import { glslBackend } from './strands_glslBackend'; import { transpileStrandsToJS } from './strands_transpiler'; diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index 7410368912..5779d04b28 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -6,8 +6,8 @@ import { createTypeConstructorNode, createUnaryOpNode, } from './ir_builders' -import { OperatorTable, BlockType, TypeInfo, BaseType, TypeInfoFromGLSLName } from './ir_types' -import { strandsShaderFunctions } from './strands_builtins' +import { OperatorTable, BlockType, DataType, BaseType, TypeInfoFromGLSLName } from './ir_types' +import { strandsBuiltinFunctions } from './strands_builtins' import { StrandsConditional } from './strands_conditionals' import * as CFG from './ir_cfg' import * as FES from './strands_FES' @@ -66,25 +66,25 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { ////////////////////////////////////////////// // Builtins, uniforms, variable constructors ////////////////////////////////////////////// - for (const [fnName, overrides] of Object.entries(strandsShaderFunctions)) { + for (const [functionName, overrides] of Object.entries(strandsBuiltinFunctions)) { const isp5Function = overrides[0].isp5Function; if (isp5Function) { - const originalFn = fn[fnName]; - fn[fnName] = function(...args) { + const originalFn = fn[functionName]; + fn[functionName] = function(...args) { if (strandsContext.active) { - return createFunctionCallNode(strandsContext, fnName, overrides, args); + return createFunctionCallNode(strandsContext, functionName, args); } else { return originalFn.apply(this, args); } } } else { - fn[fnName] = function (...args) { + fn[functionName] = function (...args) { if (strandsContext.active) { - return createFunctionCallNode(strandsContext, fnName, overrides, args); + return createFunctionCallNode(strandsContext, functionName, args); } else { p5._friendlyError( - `It looks like you've called ${fnName} outside of a shader's modify() function.` + `It looks like you've called ${functionName} outside of a shader's modify() function.` ) } } @@ -92,11 +92,11 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { } // Next is type constructors and uniform functions - for (const type in TypeInfo) { + for (const type in DataType) { if (type === BaseType.DEFER) { continue; } - const typeInfo = TypeInfo[type]; + const typeInfo = DataType[type]; let pascalTypeName; if (/^[ib]vec/.test(typeInfo.fnName)) { diff --git a/src/strands/strands_builtins.js b/src/strands/strands_builtins.js index 946089e245..e931b0b880 100644 --- a/src/strands/strands_builtins.js +++ b/src/strands/strands_builtins.js @@ -1,83 +1,113 @@ +import { GenType, DataType } from "./ir_types" + // GLSL Built in functions // https://docs.gl/el3/abs const builtInGLSLFunctions = { //////////// Trigonometry ////////// - 'acos': [{ args: ['genType'], returnType: 'genType', isp5Function: true}], - 'acosh': [{ args: ['genType'], returnType: 'genType', isp5Function: false}], - 'asin': [{ args: ['genType'], returnType: 'genType', isp5Function: true}], - 'asinh': [{ args: ['genType'], returnType: 'genType', isp5Function: false}], - 'atan': [ - { args: ['genType'], returnType: 'genType', isp5Function: false}, - { args: ['genType', 'genType'], returnType: 'genType', isp5Function: false}, + acos: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}], + acosh: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}], + asin: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}], + asinh: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}], + atan: [ + { params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}, + { params: [GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}, ], - 'atanh': [{ args: ['genType'], returnType: 'genType', isp5Function: false}], - 'cos': [{ args: ['genType'], returnType: 'genType', isp5Function: true}], - 'cosh': [{ args: ['genType'], returnType: 'genType', isp5Function: false}], - 'degrees': [{ args: ['genType'], returnType: 'genType', isp5Function: true}], - 'radians': [{ args: ['genType'], returnType: 'genType', isp5Function: true}], - 'sin': [{ args: ['genType'], returnType: 'genType' , isp5Function: true}], - 'sinh': [{ args: ['genType'], returnType: 'genType', isp5Function: false}], - 'tan': [{ args: ['genType'], returnType: 'genType', isp5Function: true}], - 'tanh': [{ args: ['genType'], returnType: 'genType', isp5Function: false}], + atanh: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}], + cos: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}], + cosh: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}], + degrees: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}], + radians: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}], + sin: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT , isp5Function: true}], + sinh: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}], + tan: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}], + tanh: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}], + ////////// Mathematics ////////// - 'abs': [{ args: ['genType'], returnType: 'genType', isp5Function: true}], - 'ceil': [{ args: ['genType'], returnType: 'genType', isp5Function: true}], - 'clamp': [{ args: ['genType', 'genType', 'genType'], returnType: 'genType', isp5Function: false}], - 'dFdx': [{ args: ['genType'], returnType: 'genType', isp5Function: false}], - 'dFdy': [{ args: ['genType'], returnType: 'genType', isp5Function: false}], - 'exp': [{ args: ['genType'], returnType: 'genType', isp5Function: true}], - 'exp2': [{ args: ['genType'], returnType: 'genType', isp5Function: false}], - 'floor': [{ args: ['genType'], returnType: 'genType', isp5Function: true}], - 'fma': [{ args: ['genType', 'genType', 'genType'], returnType: 'genType', isp5Function: false}], - 'fract': [{ args: ['genType'], returnType: 'genType', isp5Function: true}], - 'fwidth': [{ args: ['genType'], returnType: 'genType', isp5Function: false}], - 'inversesqrt': [{ args: ['genType'], returnType: 'genType', isp5Function: true}], - // 'isinf': [{}], - // 'isnan': [{}], - 'log': [{ args: ['genType'], returnType: 'genType', isp5Function: true}], - 'log2': [{ args: ['genType'], returnType: 'genType', isp5Function: false}], - 'max': [ - { args: ['genType', 'genType'], returnType: 'genType', isp5Function: true}, - { args: ['genType', 'float1'], returnType: 'genType', isp5Function: true}, + abs: [ + { params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}, + { params: [GenType.FLOAT], returnType: GenType.INT, isp5Function: true} + ], + ceil: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}], + clamp: [ + { params: [GenType.FLOAT, GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}, + { params: [GenType.FLOAT,DataType.float1,DataType.float1], returnType: GenType.FLOAT, isp5Function: false}, + { params: [GenType.INT, GenType.INT, GenType.INT], returnType: GenType.INT, isp5Function: false}, + { params: [GenType.INT, DataType.int1, DataType.int1], returnType: GenType.INT, isp5Function: false}, + ], + dFdx: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}], + dFdy: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}], + exp: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}], + exp2: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}], + floor: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}], + fma: [{ params: [GenType.FLOAT, GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}], + fract: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}], + fwidth: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}], + inversesqrt: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}], + // "isinf": [{}], + // "isnan": [{}], + log: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}], + log2: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}], + max: [ + { params: [GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}, + { params: [GenType.FLOAT,DataType.float1], returnType: GenType.FLOAT, isp5Function: true}, + { params: [GenType.INT, GenType.INT], returnType: GenType.INT, isp5Function: true}, + { params: [GenType.INT, DataType.int1], returnType: GenType.INT, isp5Function: true}, ], - 'min': [ - { args: ['genType', 'genType'], returnType: 'genType', isp5Function: true}, - { args: ['genType', 'float1'], returnType: 'genType', isp5Function: true}, + min: [ + { params: [GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}, + { params: [GenType.FLOAT,DataType.float1], returnType: GenType.FLOAT, isp5Function: true}, + { params: [GenType.INT, GenType.INT], returnType: GenType.INT, isp5Function: true}, + { params: [GenType.INT, DataType.int1], returnType: GenType.INT, isp5Function: true}, ], - 'mix': [ - { args: ['genType', 'genType', 'genType'], returnType: 'genType', isp5Function: false}, - { args: ['genType', 'genType', 'float1'], returnType: 'genType', isp5Function: false}, + mix: [ + { params: [GenType.FLOAT, GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}, + { params: [GenType.FLOAT, GenType.FLOAT,DataType.float1], returnType: GenType.FLOAT, isp5Function: false}, + { params: [GenType.FLOAT, GenType.FLOAT, GenType.BOOL], returnType: GenType.FLOAT, isp5Function: false}, ], - // 'mod': [{}], - // 'modf': [{}], - 'pow': [{ args: ['genType', 'genType'], returnType: 'genType', isp5Function: true}], - 'round': [{ args: ['genType'], returnType: 'genType', isp5Function: true}], - 'roundEven': [{ args: ['genType'], returnType: 'genType', isp5Function: false}], - // 'sign': [{}], - 'smoothstep': [ - { args: ['genType', 'genType', 'genType'], returnType: 'genType', isp5Function: false}, - { args: ['float1', 'float1', 'genType'], returnType: 'genType', isp5Function: false}, + mod: [ + { params: [GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}, + { params: [GenType.FLOAT,DataType.float1], returnType: GenType.FLOAT, isp5Function: true}, ], - 'sqrt': [{ args: ['genType'], returnType: 'genType', isp5Function: true}], - 'step': [{ args: ['genType', 'genType'], returnType: 'genType', isp5Function: false}], - 'trunc': [{ args: ['genType'], returnType: 'genType', isp5Function: false}], + // "modf": [{}], + pow: [{ params: [GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}], + round: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}], + roundEven: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}], + sign: [ + { params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}, + { params: [GenType.INT], returnType: GenType.INT, isp5Function: false}, + ], + smoothstep: [ + { params: [GenType.FLOAT, GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}, + { params: [ DataType.float1,DataType.float1, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}, + ], + sqrt: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}], + step: [{ params: [GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}], + trunc: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}], ////////// Vector ////////// - 'cross': [{ args: ['vec3', 'vec3'], returnType: 'vec3', isp5Function: true}], - 'distance': [{ args: ['genType', 'genType'], returnType: 'float1', isp5Function: true}], - 'dot': [{ args: ['genType', 'genType'], returnType: 'float1', isp5Function: true}], - // 'equal': [{}], - 'faceforward': [{ args: ['genType', 'genType', 'genType'], returnType: 'genType', isp5Function: false}], - 'length': [{ args: ['genType'], returnType: 'float1', isp5Function: false}], - 'normalize': [{ args: ['genType'], returnType: 'genType', isp5Function: true}], - // 'notEqual': [{}], - 'reflect': [{ args: ['genType', 'genType'], returnType: 'genType', isp5Function: false}], - 'refract': [{ args: ['genType', 'genType', 'float1'], returnType: 'genType', isp5Function: false}], + cross: [{ params: [DataType.float3, DataType.float3], returnType: DataType.float3, isp5Function: true}], + distance: [{ params: [GenType.FLOAT, GenType.FLOAT], returnType:DataType.float1, isp5Function: true}], + dot: [{ params: [GenType.FLOAT, GenType.FLOAT], returnType:DataType.float1, isp5Function: true}], + equal: [ + { params: [GenType.FLOAT, GenType.FLOAT], returnType: GenType.BOOL, isp5Function: false}, + { params: [GenType.INT, GenType.INT], returnType: GenType.BOOL, isp5Function: false}, + { params: [GenType.BOOL, GenType.BOOL], returnType: GenType.BOOL, isp5Function: false}, + ], + faceforward: [{ params: [GenType.FLOAT, GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}], + length: [{ params: [GenType.FLOAT], returnType:DataType.float1, isp5Function: false}], + normalize: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}], + notEqual: [ + { params: [GenType.FLOAT, GenType.FLOAT], returnType: GenType.BOOL, isp5Function: false}, + { params: [GenType.INT, GenType.INT], returnType: GenType.BOOL, isp5Function: false}, + { params: [GenType.BOOL, GenType.BOOL], returnType: GenType.BOOL, isp5Function: false}, + ], + reflect: [{ params: [GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}], + refract: [{ params: [GenType.FLOAT, GenType.FLOAT,DataType.float1], returnType: GenType.FLOAT, isp5Function: false}], ////////// Texture sampling ////////// - 'texture': [{args: ['sampler2D', 'float2'], returnType: 'float4', isp5Function: true}], + texture: [{params: ["texture2D", DataType.float2], returnType: DataType.float4, isp5Function: true}], } -export const strandsShaderFunctions = { +export const strandsBuiltinFunctions = { ...builtInGLSLFunctions, } \ No newline at end of file diff --git a/src/strands/strands_transpiler.js b/src/strands/strands_transpiler.js index a804d3dcfd..47ad8469f9 100644 --- a/src/strands/strands_transpiler.js +++ b/src/strands/strands_transpiler.js @@ -2,8 +2,6 @@ import { parse } from 'acorn'; import { ancestor } from 'acorn-walk'; import escodegen from 'escodegen'; -// TODO: Switch this to operator table, cleanup whole file too - function replaceBinaryOperator(codeSource) { switch (codeSource) { case '+': return 'add'; From 11a1610b305f7f1ad8d8680602942a70a6613298 Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Thu, 24 Jul 2025 19:26:54 +0100 Subject: [PATCH 17/69] function calls partially reimplemented. Still needs more error checking. --- preview/global/sketch.js | 4 +--- src/strands/ir_builders.js | 16 +++++----------- src/strands/strands_api.js | 6 ++++-- src/strands/strands_glslBackend.js | 6 +++--- 4 files changed, 13 insertions(+), 19 deletions(-) diff --git a/preview/global/sketch.js b/preview/global/sketch.js index 25ec2fd398..01ec27f494 100644 --- a/preview/global/sketch.js +++ b/preview/global/sketch.js @@ -2,9 +2,7 @@ p5.disableFriendlyErrors = true; function callback() { getFinalColor((col) => { - let y = col.sub(-1,1,0,0); - - return mix(float(0), col.add(y), float(1)); + return mix(vec4(1,0, 1, 1), vec4(1, 1, 0.3, 1), float(1)); }); } diff --git a/src/strands/ir_builders.js b/src/strands/ir_builders.js index 8a4ffb399a..1b1adc6106 100644 --- a/src/strands/ir_builders.js +++ b/src/strands/ir_builders.js @@ -4,7 +4,6 @@ import * as FES from './strands_FES' import { NodeType, OpCode, BaseType, typeEquals, GenType } from './ir_types'; import { StrandsNode } from './strands_api'; import { strandsBuiltinFunctions } from './strands_builtins'; -import { ar } from 'vitest/dist/chunks/reporters.D7Jzd9GS.js'; ////////////////////////////////////////////// // Builders for node graphs @@ -171,7 +170,6 @@ export function createTypeConstructorNode(strandsContext, typeInfo, dependsOn) { export function createFunctionCallNode(strandsContext, functionName, userArgs) { const { cfg, dag } = strandsContext; - console.log("HELLOOOOOOOO") const overloads = strandsBuiltinFunctions[functionName]; const matchingArgsCounts = overloads.filter(overload => overload.params.length === userArgs.length); if (matchingArgsCounts.length === 0) { @@ -179,7 +177,7 @@ export function createFunctionCallNode(strandsContext, functionName, userArgs) { const argsLengthArr = []; overloads.forEach((overload) => argsLengthSet.add(overload.params.length)); argsLengthSet.forEach((len) => argsLengthArr.push(`${len}`)); - const argsLengthStr = argsLengthArr.join(' or '); + const argsLengthStr = argsLengthArr.join(', or '); FES.userError("parameter validation error",`Function '${functionName}' has ${overloads.length} variants which expect ${argsLengthStr} arguments, but ${userArgs.length} arguments were provided.`); } @@ -187,17 +185,17 @@ export function createFunctionCallNode(strandsContext, functionName, userArgs) { let bestScore = 0; let inferredReturnType = null; for (const overload of matchingArgsCounts) { + const isGeneric = (T) => T.dimension === null; let isValid = true; let overloadParamTypes = []; let inferredDimension = null; let similarity = 0; for (let i = 0; i < userArgs.length; i++) { - const argType = DAG.extractNodeTypeInfo(userArgs[i]); + const argType = DAG.extractNodeTypeInfo(dag, userArgs[i].id); const expectedType = overload.params[i]; let dimension = expectedType.dimension; - const isGeneric = (T) => T.dimension === null; if (isGeneric(expectedType)) { if (inferredDimension === null || inferredDimension === 1) { inferredDimension = argType.dimension; @@ -234,18 +232,14 @@ export function createFunctionCallNode(strandsContext, functionName, userArgs) { } if (bestOverload === null) { - const paramsString = (params) => `(${params.map((param) => param).join(', ')})`; - const expectedArgsString = overloads.map(overload => paramsString(overload.params)).join(' or '); - const providedArgsString = paramsString(userArgs.map((arg)=>arg.baseType+arg.dimension)); - throw new Error(`Function '${functionName}' was called with wrong arguments. Most likely, you provided mixed lengths vectors as arguments.\nExpected argument types: ${expectedArgsString}\nProvided argument types: ${providedArgsString}\nAll of the arguments with expected type 'genType' should have a matching type. If one of those is different, try to find where it was created. - `); + FES.userError('parameter validation', 'No matching overload found!'); } const nodeData = DAG.createNodeData({ nodeType: NodeType.OPERATION, opCode: OpCode.Nary.FUNCTION_CALL, identifier: functionName, - dependsOn: userArgs, + dependsOn: userArgs.map(arg => arg.id), // no type info yet baseType: inferredReturnType.baseType, dimension: inferredReturnType.dimension diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index 5779d04b28..3842ebab59 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -73,7 +73,8 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { const originalFn = fn[functionName]; fn[functionName] = function(...args) { if (strandsContext.active) { - return createFunctionCallNode(strandsContext, functionName, args); + const id = createFunctionCallNode(strandsContext, functionName, args); + return new StrandsNode(id); } else { return originalFn.apply(this, args); } @@ -81,7 +82,8 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { } else { fn[functionName] = function (...args) { if (strandsContext.active) { - return createFunctionCallNode(strandsContext, functionName, args); + const id = createFunctionCallNode(strandsContext, functionName, args); + return new StrandsNode(id); } else { p5._friendlyError( `It looks like you've called ${functionName} outside of a shader's modify() function.` diff --git a/src/strands/strands_glslBackend.js b/src/strands/strands_glslBackend.js index 5862adb184..8b673477d4 100644 --- a/src/strands/strands_glslBackend.js +++ b/src/strands/strands_glslBackend.js @@ -113,15 +113,15 @@ export const glslBackend = { const useParantheses = node.usedBy.length > 0; if (node.opCode === OpCode.Nary.CONSTRUCTOR) { if (node.dependsOn.length === 1 && node.dimension === 1) { - console.log("AARK") return this.generateExpression(generationContext, dag, node.dependsOn[0]); } const T = this.getTypeName(node.baseType, node.dimension); const deps = node.dependsOn.map((dep) => this.generateExpression(generationContext, dag, dep)); return `${T}(${deps.join(', ')})`; } - if (node.opCode === OpCode.Nary.FUNCTION) { - return "functioncall!"; + if (node.opCode === OpCode.Nary.FUNCTION_CALL) { + const functionArgs = node.dependsOn.map(arg =>this.generateExpression(generationContext, dag, arg)); + return `${node.identifier}(${functionArgs.join(', ')})`; } if (node.dependsOn.length === 2) { const [lID, rID] = node.dependsOn; From e8f03d6292f67e5ccfaf5d7cd5ca4c81b0fc0c7f Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Fri, 25 Jul 2025 13:59:46 +0100 Subject: [PATCH 18/69] update function calls to conform parameters when raw numbers are handed --- preview/global/sketch.js | 7 ++- src/strands/ir_builders.js | 90 +++++++++++++++++++++---------- src/strands/strands_api.js | 4 +- src/strands/strands_transpiler.js | 4 +- 4 files changed, 70 insertions(+), 35 deletions(-) diff --git a/preview/global/sketch.js b/preview/global/sketch.js index 01ec27f494..bc37b09883 100644 --- a/preview/global/sketch.js +++ b/preview/global/sketch.js @@ -2,7 +2,10 @@ p5.disableFriendlyErrors = true; function callback() { getFinalColor((col) => { - return mix(vec4(1,0, 1, 1), vec4(1, 1, 0.3, 1), float(1)); + let x = [12, 1]; + let y= [10, 100]; + let z = [x, y]; + return mix(vec4([1,0], 1, 1), z, 0.4); }); } @@ -15,7 +18,7 @@ function windowResized() { resizeCanvas(windowWidth, windowHeight); } -function draw(){ +function draw() { orbitControl(); background(0); shader(bloomShader); diff --git a/src/strands/ir_builders.js b/src/strands/ir_builders.js index 1b1adc6106..b6e2f2a579 100644 --- a/src/strands/ir_builders.js +++ b/src/strands/ir_builders.js @@ -1,7 +1,7 @@ import * as DAG from './ir_dag' import * as CFG from './ir_cfg' import * as FES from './strands_FES' -import { NodeType, OpCode, BaseType, typeEquals, GenType } from './ir_types'; +import { NodeType, OpCode, BaseType, DataType, BasePriority, } from './ir_types'; import { StrandsNode } from './strands_api'; import { strandsBuiltinFunctions } from './strands_builtins'; @@ -108,17 +108,20 @@ export function createBinaryOpNode(strandsContext, leftStrandsNode, rightArg, op return id; } -function mapConstructorDependencies(strandsContext, typeInfo, dependsOn) { +function mapPrimitiveDependencies(strandsContext, typeInfo, dependsOn) { + dependsOn = Array.isArray(dependsOn) ? dependsOn : [dependsOn]; const mappedDependencies = []; let { dimension, baseType } = typeInfo; const dag = strandsContext.dag; let calculatedDimensions = 0; - - for (const dep of dependsOn.flat()) { + let originalNodeID = null; + for (const dep of dependsOn.flat(Infinity)) { if (dep instanceof StrandsNode) { const node = DAG.getNodeDataFromID(dag, dep.id); - + originalNodeID = dep.id; + baseType = node.baseType; + if (node.opCode === OpCode.Nary.CONSTRUCTOR) { for (const inner of node.dependsOn) { mappedDependencies.push(inner); @@ -130,7 +133,7 @@ function mapConstructorDependencies(strandsContext, typeInfo, dependsOn) { calculatedDimensions += node.dimension; continue; } - if (typeof dep === 'number') { + else if (typeof dep === 'number') { const newNode = createLiteralNode(strandsContext, { dimension: 1, baseType }, dep); mappedDependencies.push(newNode); calculatedDimensions += 1; @@ -140,6 +143,7 @@ function mapConstructorDependencies(strandsContext, typeInfo, dependsOn) { FES.userError('type error', `You've tried to construct a scalar or vector type with a non-numeric value: ${dep}`); } } + // Sometimes, the dimension is undefined if (dimension === null) { dimension = calculatedDimensions; } else if (dimension > calculatedDimensions && calculatedDimensions === 1) { @@ -147,38 +151,52 @@ function mapConstructorDependencies(strandsContext, typeInfo, dependsOn) { } else if(calculatedDimensions !== 1 && calculatedDimensions !== dimension) { FES.userError('type error', `You've tried to construct a ${baseType + dimension} with ${calculatedDimensions} components`); } - - return { mappedDependencies, dimension }; + const inferredTypeInfo = { + dimension, + baseType, + priority: BasePriority[baseType], + } + return { originalNodeID, mappedDependencies, inferredTypeInfo }; } -export function createTypeConstructorNode(strandsContext, typeInfo, dependsOn) { - const { cfg, dag } = strandsContext; - dependsOn = Array.isArray(dependsOn) ? dependsOn : [dependsOn]; - const { mappedDependencies, dimension } = mapConstructorDependencies(strandsContext, typeInfo, dependsOn); - +function constructTypeFromIDs(strandsContext, strandsNodesArray, newTypeInfo) { const nodeData = DAG.createNodeData({ nodeType: NodeType.OPERATION, opCode: OpCode.Nary.CONSTRUCTOR, - dimension, - baseType: typeInfo.baseType, - dependsOn: mappedDependencies - }) - const id = DAG.getOrCreateNode(dag, nodeData); + dimension: newTypeInfo.dimension, + baseType: newTypeInfo.baseType, + dependsOn: strandsNodesArray + }); + const id = DAG.getOrCreateNode(strandsContext.dag, nodeData); + return id; +} + +export function createTypeConstructorNode(strandsContext, typeInfo, dependsOn) { + const { cfg, dag } = strandsContext; + const { mappedDependencies, inferredTypeInfo } = mapPrimitiveDependencies(strandsContext, typeInfo, dependsOn); + const finalType = { + baseType: typeInfo.baseType, + dimension: inferredTypeInfo.dimension + }; + const id = constructTypeFromIDs(strandsContext, mappedDependencies, finalType); CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); return id; } -export function createFunctionCallNode(strandsContext, functionName, userArgs) { +export function createFunctionCallNode(strandsContext, functionName, rawUserArgs) { const { cfg, dag } = strandsContext; const overloads = strandsBuiltinFunctions[functionName]; - const matchingArgsCounts = overloads.filter(overload => overload.params.length === userArgs.length); + + const preprocessedArgs = rawUserArgs.map((rawUserArg) => mapPrimitiveDependencies(strandsContext, DataType.defer, rawUserArg)); + console.log(preprocessedArgs); + const matchingArgsCounts = overloads.filter(overload => overload.params.length === preprocessedArgs.length); if (matchingArgsCounts.length === 0) { const argsLengthSet = new Set(); const argsLengthArr = []; overloads.forEach((overload) => argsLengthSet.add(overload.params.length)); argsLengthSet.forEach((len) => argsLengthArr.push(`${len}`)); const argsLengthStr = argsLengthArr.join(', or '); - FES.userError("parameter validation error",`Function '${functionName}' has ${overloads.length} variants which expect ${argsLengthStr} arguments, but ${userArgs.length} arguments were provided.`); + FES.userError("parameter validation error",`Function '${functionName}' has ${overloads.length} variants which expect ${argsLengthStr} arguments, but ${preprocessedArgs.length} arguments were provided.`); } let bestOverload = null; @@ -187,12 +205,13 @@ export function createFunctionCallNode(strandsContext, functionName, userArgs) { for (const overload of matchingArgsCounts) { const isGeneric = (T) => T.dimension === null; let isValid = true; - let overloadParamTypes = []; + let overloadParameters = []; let inferredDimension = null; let similarity = 0; - for (let i = 0; i < userArgs.length; i++) { - const argType = DAG.extractNodeTypeInfo(dag, userArgs[i].id); + for (let i = 0; i < preprocessedArgs.length; i++) { + const preArg = preprocessedArgs[i]; + const argType = preArg.inferredTypeInfo; const expectedType = overload.params[i]; let dimension = expectedType.dimension; @@ -218,11 +237,11 @@ export function createFunctionCallNode(strandsContext, functionName, userArgs) { similarity += 1; } - overloadParamTypes.push({ baseType: expectedType.baseType, dimension }); + overloadParameters.push({ baseType: expectedType.baseType, dimension }); } if (isValid && (!bestOverload || similarity > bestScore)) { - bestOverload = overloadParamTypes; + bestOverload = overloadParameters; bestScore = similarity; inferredReturnType = overload.returnType; if (isGeneric(inferredReturnType)) { @@ -233,14 +252,27 @@ export function createFunctionCallNode(strandsContext, functionName, userArgs) { if (bestOverload === null) { FES.userError('parameter validation', 'No matching overload found!'); - } + } + + let dependsOn = []; + for (let i = 0; i < bestOverload.length; i++) { + const arg = preprocessedArgs[i]; + if (arg.originalNodeID) { + dependsOn.push(arg.originalNodeID); + } + else { + const paramType = bestOverload[i]; + const castedArgID = constructTypeFromIDs(strandsContext, arg.mappedDependencies, paramType); + CFG.recordInBasicBlock(cfg, cfg.currentBlock, castedArgID); + dependsOn.push(castedArgID); + } + } const nodeData = DAG.createNodeData({ nodeType: NodeType.OPERATION, opCode: OpCode.Nary.FUNCTION_CALL, identifier: functionName, - dependsOn: userArgs.map(arg => arg.id), - // no type info yet + dependsOn, baseType: inferredReturnType.baseType, dimension: inferredReturnType.dimension }) diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index 3842ebab59..d3e6948e11 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -57,9 +57,9 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { fn.strandsNode = function(...args) { if (args.length > 4) { - FES.userError('type error', "It looks like you've tried to construct a p5.strands node implicitly, with more than 4 components. This is currently not supported.") + FES.userError("type error", "It looks like you've tried to construct a p5.strands node implicitly, with more than 4 components. This is currently not supported.") } - const id = createTypeConstructorNode(strandsContext, { baseType: BaseType.DEFER, dimension: null }, args); + const id = createTypeConstructorNode(strandsContext, { baseType: BaseType.DEFER, dimension: null }, args.flat()); return new StrandsNode(id); } diff --git a/src/strands/strands_transpiler.js b/src/strands/strands_transpiler.js index 47ad8469f9..b7e8e35f4f 100644 --- a/src/strands/strands_transpiler.js +++ b/src/strands/strands_transpiler.js @@ -123,7 +123,7 @@ const ASTCallbacks = { node.type = 'CallExpression'; node.callee = { type: 'Identifier', - name: 'dynamicNode', + name: 'strandsNode', }; node.arguments = [original]; }, @@ -176,7 +176,7 @@ const ASTCallbacks = { type: 'CallExpression', callee: { type: 'Identifier', - name: 'dynamicNode', + name: 'strandsNode', }, arguments: [node.left] } From 1ddd9a2f82d18d06b494646e310755bf9ea4763e Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Fri, 25 Jul 2025 15:01:25 +0100 Subject: [PATCH 19/69] adding struct types --- preview/global/sketch.js | 15 +++++++----- src/strands/ir_builders.js | 1 - src/strands/strands_api.js | 44 +++++++++++++++++++++++++++------- src/strands/strands_codegen.js | 3 ++- 4 files changed, 47 insertions(+), 16 deletions(-) diff --git a/preview/global/sketch.js b/preview/global/sketch.js index bc37b09883..2436c5a871 100644 --- a/preview/global/sketch.js +++ b/preview/global/sketch.js @@ -1,12 +1,15 @@ p5.disableFriendlyErrors = true; function callback() { - getFinalColor((col) => { - let x = [12, 1]; - let y= [10, 100]; - let z = [x, y]; - return mix(vec4([1,0], 1, 1), z, 0.4); - }); + // getFinalColor((col) => { + // let x = [12, 1]; + // let y= [10, 100]; + // let z = [x, y]; + // return mix(vec4([1,0], 1, 1), z, 0.4); + // }); + getWorldInputs(inputs => { + return inputs; + }) } async function setup(){ diff --git a/src/strands/ir_builders.js b/src/strands/ir_builders.js index b6e2f2a579..9d85f72334 100644 --- a/src/strands/ir_builders.js +++ b/src/strands/ir_builders.js @@ -188,7 +188,6 @@ export function createFunctionCallNode(strandsContext, functionName, rawUserArgs const overloads = strandsBuiltinFunctions[functionName]; const preprocessedArgs = rawUserArgs.map((rawUserArg) => mapPrimitiveDependencies(strandsContext, DataType.defer, rawUserArg)); - console.log(preprocessedArgs); const matchingArgsCounts = overloads.filter(overload => overload.params.length === preprocessedArgs.length); if (matchingArgsCounts.length === 0) { const argsLengthSet = new Set(); diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index d3e6948e11..8acab76b45 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -146,9 +146,9 @@ function createHookArguments(strandsContext, parameters){ const paramType = param.type; if(structTypes.includes(paramType.typeName)) { const propertyEntries = paramType.properties.map((prop) => { - const typeInfo = TypeInfoFromGLSLName[prop.dataType]; + const typeInfo = TypeInfoFromGLSLName[prop.type.typeName]; const variableNode = createVariableNode(strandsContext, typeInfo, prop.name); - return [prop.name, variableNode]; + return [prop.name, new StrandsNode(variableNode)]; }); const argObject = Object.fromEntries(propertyEntries); args.push(argObject); @@ -182,7 +182,36 @@ export function createShaderHooksFunctions(strandsContext, fn, shader) { const expectedReturnType = hookType.returnType; if(structTypes.includes(expectedReturnType.typeName)) { + const rootStruct = { + identifier: expectedReturnType.typeName, + properties: {} + }; + const expectedProperties = expectedReturnType.properties; + for (let i = 0; i < expectedProperties.length; i++) { + const expectedProp = expectedProperties[i]; + const propName = expectedProp.name; + const receivedValue = returned[propName]; + + if (receivedValue === undefined) { + FES.userError('type error', `You've returned an incomplete object from ${hookType.name}.\n` + + `Expected: { ${expectedReturnType.properties.map(p => p.name).join(', ')} }\n` + + `Received: { ${Object.keys(returned).join(', ')} }\n` + + `All of the properties are required!`); + } + + let propID = receivedValue?.id; + if (!(receivedValue instanceof StrandsNode)) { + const typeInfo = TypeInfoFromGLSLName[expectedProp.type.typeName]; + propID = createTypeConstructorNode(strandsContext, typeInfo, receivedValue); + } + rootStruct.properties[propName] = propID; + } + strandsContext.hooks.push({ + hookType, + entryBlockID, + rootStruct + }); } else { // In this case we are expecting a native shader type, probably vec4 or vec3. @@ -213,13 +242,12 @@ export function createShaderHooksFunctions(strandsContext, fn, shader) { const newID = createTypeConstructorNode(strandsContext, expected, returnedNode); returnedNode = new StrandsNode(newID); } + strandsContext.hooks.push({ + hookType, + entryBlockID, + rootNodeID: returnedNode.id, + }); } - - strandsContext.hooks.push({ - hookType, - entryBlockID, - rootNodeID: returnedNode.id, - }); CFG.popBlock(cfg); } } diff --git a/src/strands/strands_codegen.js b/src/strands/strands_codegen.js index 904add554d..6cedbce52a 100644 --- a/src/strands/strands_codegen.js +++ b/src/strands/strands_codegen.js @@ -30,7 +30,8 @@ export function generateShaderCode(strandsContext) { const hooksObj = {}; - for (const { hookType, entryBlockID, rootNodeID} of strandsContext.hooks) { + for (const { hookType, entryBlockID, rootNodeID, rootStruct} of strandsContext.hooks) { + console.log(rootStruct) const dagSorted = sortDAG(dag.dependsOn, rootNodeID); const cfgSorted = sortCFG(cfg.outgoingEdges, entryBlockID); From f3155e663ba780f627626a083908c49dc8c64b1d Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Fri, 25 Jul 2025 15:01:25 +0100 Subject: [PATCH 20/69] adding struct types --- preview/global/sketch.js | 9 +- src/strands/ir_builders.js | 11 ++- src/strands/ir_types.js | 20 +++++ src/strands/strands_api.js | 146 +++++++++++++++++++++------------ src/strands/strands_codegen.js | 2 +- 5 files changed, 127 insertions(+), 61 deletions(-) diff --git a/preview/global/sketch.js b/preview/global/sketch.js index bc37b09883..0a05adcd29 100644 --- a/preview/global/sketch.js +++ b/preview/global/sketch.js @@ -2,11 +2,12 @@ p5.disableFriendlyErrors = true; function callback() { getFinalColor((col) => { - let x = [12, 1]; - let y= [10, 100]; - let z = [x, y]; - return mix(vec4([1,0], 1, 1), z, 0.4); + + return [1, 1, 0, 1]; }); + getWorldInputs(inputs => { + return inputs; + }) } async function setup(){ diff --git a/src/strands/ir_builders.js b/src/strands/ir_builders.js index b6e2f2a579..db00ec848f 100644 --- a/src/strands/ir_builders.js +++ b/src/strands/ir_builders.js @@ -26,6 +26,10 @@ export function createLiteralNode(strandsContext, typeInfo, value) { return id; } +export function createStructNode(strandsContext, structTypeInfo, dependsOn) { + +} + export function createVariableNode(strandsContext, typeInfo, identifier) { const { cfg, dag } = strandsContext; const { dimension, baseType } = typeInfo; @@ -159,12 +163,12 @@ function mapPrimitiveDependencies(strandsContext, typeInfo, dependsOn) { return { originalNodeID, mappedDependencies, inferredTypeInfo }; } -function constructTypeFromIDs(strandsContext, strandsNodesArray, newTypeInfo) { +function constructTypeFromIDs(strandsContext, strandsNodesArray, typeInfo) { const nodeData = DAG.createNodeData({ nodeType: NodeType.OPERATION, opCode: OpCode.Nary.CONSTRUCTOR, - dimension: newTypeInfo.dimension, - baseType: newTypeInfo.baseType, + dimension: typeInfo.dimension, + baseType: typeInfo.baseType, dependsOn: strandsNodesArray }); const id = DAG.getOrCreateNode(strandsContext.dag, nodeData); @@ -188,7 +192,6 @@ export function createFunctionCallNode(strandsContext, functionName, rawUserArgs const overloads = strandsBuiltinFunctions[functionName]; const preprocessedArgs = rawUserArgs.map((rawUserArg) => mapPrimitiveDependencies(strandsContext, DataType.defer, rawUserArg)); - console.log(preprocessedArgs); const matchingArgsCounts = overloads.filter(overload => overload.params.length === preprocessedArgs.length); if (matchingArgsCounts.length === 0) { const argsLengthSet = new Set(); diff --git a/src/strands/ir_types.js b/src/strands/ir_types.js index 007f22de51..76fd39f551 100644 --- a/src/strands/ir_types.js +++ b/src/strands/ir_types.js @@ -56,6 +56,26 @@ export const DataType = { defer: { fnName: null, baseType: BaseType.DEFER, dimension: null, priority: -1 }, } +export const StructType = { + Vertex: { + identifer: 'Vertex', + properties: [ + { name: "position", dataType: DataType.float3 }, + { name: "normal", dataType: DataType.float3 }, + { name: "color", dataType: DataType.float4 }, + { name: "texCoord", dataType: DataType.float2 }, + ] + } +} + +export function isStructType(typeName) { + return Object.keys(StructType).includes(typeName); +} + +export function isNativeType(typeName) { + return Object.keys(DataType).includes(typeName); +} + export const GenType = { FLOAT: { baseType: BaseType.FLOAT, dimension: null, priority: 3 }, INT: { baseType: BaseType.INT, dimension: null, priority: 2 }, diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index d3e6948e11..b377c691b6 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -6,7 +6,16 @@ import { createTypeConstructorNode, createUnaryOpNode, } from './ir_builders' -import { OperatorTable, BlockType, DataType, BaseType, TypeInfoFromGLSLName } from './ir_types' +import { + OperatorTable, + BlockType, + DataType, + BaseType, + StructType, + TypeInfoFromGLSLName, + isStructType, + // isNativeType +} from './ir_types' import { strandsBuiltinFunctions } from './strands_builtins' import { StrandsConditional } from './strands_conditionals' import * as CFG from './ir_cfg' @@ -137,22 +146,21 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { ////////////////////////////////////////////// // Per-Hook functions ////////////////////////////////////////////// -const structTypes = ['Vertex', ] - function createHookArguments(strandsContext, parameters){ const args = []; for (const param of parameters) { const paramType = param.type; - if(structTypes.includes(paramType.typeName)) { - const propertyEntries = paramType.properties.map((prop) => { - const typeInfo = TypeInfoFromGLSLName[prop.dataType]; - const variableNode = createVariableNode(strandsContext, typeInfo, prop.name); - return [prop.name, variableNode]; - }); - const argObject = Object.fromEntries(propertyEntries); - args.push(argObject); - } else { + if(isStructType(paramType.typeName)) { + const structType = StructType[paramType.typeName]; + const argStruct = {}; + for (const prop of structType.properties) { + const memberNode = createVariableNode(strandsContext, prop.dataType, prop.name); + argStruct[prop.name] = memberNode; + } + args.push(argStruct); + } + else /*if(isNativeType(paramType.typeName))*/ { const typeInfo = TypeInfoFromGLSLName[paramType.typeName]; const id = createVariableNode(strandsContext, typeInfo, param.name); const arg = new StrandsNode(id); @@ -162,64 +170,98 @@ function createHookArguments(strandsContext, parameters){ return args; } +function enforceReturnTypeMatch(strandsContext, expectedType, returned, hookName) { + if (!(returned instanceof StrandsNode)) { + try { + return createTypeConstructorNode(strandsContext, expectedType, returned); + } catch (e) { + FES.userError('type error', + `There was a type mismatch for a value returned from ${hookName}.\n` + + `The value in question was supposed to be:\n` + + `${expectedType.baseType + expectedType.dimension}\n` + + `But you returned:\n` + + `${returned}` + ); + } + } + + const dag = strandsContext.dag; + let returnedNodeID = returned.id; + const receivedType = { + baseType: dag.baseTypes[returnedNodeID], + dimension: dag.dimensions[returnedNodeID], + } + if (receivedType.dimension !== expectedType.dimension) { + if (receivedType.dimension !== 1) { + FES.userError('type error', `You have returned a vector with ${receivedType.dimension} components in ${hookType.name} when a ${expectedType.baseType + expectedType.dimension} was expected!`); + } + else { + returnedNodeID = createTypeConstructorNode(strandsContext, expectedType, returnedNodeID); + } + } + else if (receivedType.baseType !== expectedType.baseType) { + returnedNodeID = createTypeConstructorNode(strandsContext, expectedType, returnedNodeID); + } + + return returnedNodeID; +} + export function createShaderHooksFunctions(strandsContext, fn, shader) { const availableHooks = { ...shader.hooks.vertex, ...shader.hooks.fragment, } const hookTypes = Object.keys(availableHooks).map(name => shader.hookTypes(name)); - const { cfg, dag } = strandsContext; + const cfg = strandsContext.cfg; for (const hookType of hookTypes) { window[hookType.name] = 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 returned = hookUserCallback(...args); - let returnedNode; + const args = createHookArguments(strandsContext, hookType.parameters); + const userReturned = hookUserCallback(...args); const expectedReturnType = hookType.returnType; - if(structTypes.includes(expectedReturnType.typeName)) { - } - else { - // In this case we are expecting a native shader type, probably vec4 or vec3. - const expected = TypeInfoFromGLSLName[expectedReturnType.typeName]; - // User may have returned a raw value like [1,1,1,1] or 25. - if (!(returned instanceof StrandsNode)) { - const id = createTypeConstructorNode(strandsContext, { baseType: BaseType.DEFER, dimension: null }, returned); - returnedNode = new StrandsNode(id); - } - else { - returnedNode = returned; - } - - const received = { - baseType: dag.baseTypes[returnedNode.id], - dimension: dag.dimensions[returnedNode.id], - } - if (received.dimension !== expected.dimension) { - if (received.dimension !== 1) { - FES.userError('type error', `You have returned a vector with ${received.dimension} components in ${hookType.name} when a ${expected.baseType + expected.dimension} was expected!`); - } - else { - const newID = createTypeConstructorNode(strandsContext, expected, returnedNode); - returnedNode = new StrandsNode(newID); + if(isStructType(expectedReturnType.typeName)) { + const expectedStructType = StructType[expectedReturnType.typeName]; + const rootStruct = { + identifier: expectedReturnType.typeName, + properties: {} + }; + const expectedProperties = expectedStructType.properties; + + for (let i = 0; i < expectedProperties.length; i++) { + const expectedProp = expectedProperties[i]; + const propName = expectedProp.name; + const receivedValue = userReturned[propName]; + if (receivedValue === undefined) { + FES.userError('type error', `You've returned an incomplete object from ${hookType.name}.\n` + + `Expected: { ${expectedReturnType.properties.map(p => p.name).join(', ')} }\n` + + `Received: { ${Object.keys(userReturned).join(', ')} }\n` + + `All of the properties are required!`); } - } - else if (received.baseType !== expected.baseType) { - const newID = createTypeConstructorNode(strandsContext, expected, returnedNode); - returnedNode = new StrandsNode(newID); + + const expectedTypeInfo = expectedProp.dataType; + const returnedPropID = enforceReturnTypeMatch(strandsContext, expectedTypeInfo, receivedValue, hookType.name); + rootStruct.properties[propName] = returnedPropID; } + strandsContext.hooks.push({ + hookType, + entryBlockID, + rootStruct + }); + } + else /*if(isNativeType(expectedReturnType.typeName))*/ { + const expectedTypeInfo = TypeInfoFromGLSLName[expectedReturnType.typeName]; + const returnedNodeID = enforceReturnTypeMatch(strandsContext, expectedTypeInfo, userReturned, hookType.name); + strandsContext.hooks.push({ + hookType, + entryBlockID, + rootNodeID: returnedNodeID, + }); } - - strandsContext.hooks.push({ - hookType, - entryBlockID, - rootNodeID: returnedNode.id, - }); CFG.popBlock(cfg); } } diff --git a/src/strands/strands_codegen.js b/src/strands/strands_codegen.js index 904add554d..c3b1606ce1 100644 --- a/src/strands/strands_codegen.js +++ b/src/strands/strands_codegen.js @@ -30,7 +30,7 @@ export function generateShaderCode(strandsContext) { const hooksObj = {}; - for (const { hookType, entryBlockID, rootNodeID} of strandsContext.hooks) { + for (const { hookType, entryBlockID, rootNodeID, rootStruct} of strandsContext.hooks) { const dagSorted = sortDAG(dag.dependsOn, rootNodeID); const cfgSorted = sortCFG(cfg.outgoingEdges, entryBlockID); From afff707036d39dbb96b286f17e12ee6bdf12517d Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Sat, 26 Jul 2025 18:39:55 +0100 Subject: [PATCH 21/69] struct types working --- preview/global/sketch.js | 14 ++- src/strands/ir_builders.js | 133 +++++++++++++++++++----- src/strands/ir_types.js | 12 ++- src/strands/p5.strands.js | 2 +- src/strands/strands_api.js | 160 ++++++++++++++++++----------- src/strands/strands_codegen.js | 14 ++- src/strands/strands_glslBackend.js | 38 ++++++- 7 files changed, 267 insertions(+), 106 deletions(-) diff --git a/preview/global/sketch.js b/preview/global/sketch.js index 0a05adcd29..2c8a560128 100644 --- a/preview/global/sketch.js +++ b/preview/global/sketch.js @@ -1,18 +1,21 @@ p5.disableFriendlyErrors = true; function callback() { - getFinalColor((col) => { + const time = uniformFloat(() =>millis()*0.001) + // getFinalColor((col) => { + // return [1, 1, 0, 1]; + // }); - return [1, 1, 0, 1]; - }); getWorldInputs(inputs => { + inputs.color = vec4(inputs.position, 1); + inputs.position = inputs.position + sin(time) * 100; return inputs; - }) + }); } async function setup(){ createCanvas(windowWidth,windowHeight, WEBGL) - bloomShader = baseColorShader().newModify(callback, {parser: false}); + bloomShader = baseColorShader().newModify(callback); } function windowResized() { @@ -23,5 +26,6 @@ function draw() { orbitControl(); background(0); shader(bloomShader); + noStroke(); sphere(300) } diff --git a/src/strands/ir_builders.js b/src/strands/ir_builders.js index db00ec848f..2b64471161 100644 --- a/src/strands/ir_builders.js +++ b/src/strands/ir_builders.js @@ -23,11 +23,7 @@ export function createLiteralNode(strandsContext, typeInfo, value) { }); const id = DAG.getOrCreateNode(dag, nodeData); CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); - return id; -} - -export function createStructNode(strandsContext, structTypeInfo, dependsOn) { - + return { id, components: dimension }; } export function createVariableNode(strandsContext, typeInfo, identifier) { @@ -41,7 +37,7 @@ export function createVariableNode(strandsContext, typeInfo, identifier) { }) const id = DAG.getOrCreateNode(dag, nodeData); CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); - return id; + return { id, components: dimension }; } export function createBinaryOpNode(strandsContext, leftStrandsNode, rightArg, opCode) { @@ -51,7 +47,7 @@ export function createBinaryOpNode(strandsContext, leftStrandsNode, rightArg, op if (rightArg[0] instanceof StrandsNode && rightArg.length === 1) { rightStrandsNode = rightArg[0]; } else { - const id = createTypeConstructorNode(strandsContext, { baseType: BaseType.DEFER, dimension: null }, rightArg); + const { id, components } = createPrimitiveConstructorNode(strandsContext, { baseType: BaseType.DEFER, dimension: null }, rightArg); rightStrandsNode = new StrandsNode(id); } let finalLeftNodeID = leftStrandsNode.id; @@ -63,8 +59,8 @@ export function createBinaryOpNode(strandsContext, leftStrandsNode, rightArg, op const cast = { node: null, toType: leftType }; const bothDeferred = leftType.baseType === rightType.baseType && leftType.baseType === BaseType.DEFER; if (bothDeferred) { - finalLeftNodeID = createTypeConstructorNode(strandsContext, { baseType:BaseType.FLOAT, dimension: leftType.dimension }, leftStrandsNode); - finalRightNodeID = createTypeConstructorNode(strandsContext, { baseType:BaseType.FLOAT, dimension: leftType.dimension }, rightStrandsNode); + finalLeftNodeID = createPrimitiveConstructorNode(strandsContext, { baseType:BaseType.FLOAT, dimension: leftType.dimension }, leftStrandsNode); + finalRightNodeID = createPrimitiveConstructorNode(strandsContext, { baseType:BaseType.FLOAT, dimension: leftType.dimension }, rightStrandsNode); } else if (leftType.baseType !== rightType.baseType || leftType.dimension !== rightType.dimension) { @@ -91,28 +87,73 @@ export function createBinaryOpNode(strandsContext, leftStrandsNode, rightArg, op FES.userError('type error', `A vector of length ${leftType.dimension} operated with a vector of length ${rightType.dimension} is not allowed.`); } - const castedID = createTypeConstructorNode(strandsContext, cast.toType, cast.node); + const casted = createPrimitiveConstructorNode(strandsContext, cast.toType, cast.node); if (cast.node === leftStrandsNode) { - finalLeftNodeID = castedID; + finalLeftNodeID = casted.id; } else { - finalRightNodeID = castedID; + finalRightNodeID = casted.id; } } const nodeData = DAG.createNodeData({ nodeType: NodeType.OPERATION, + opCode, dependsOn: [finalLeftNodeID, finalRightNodeID], - dimension, baseType: cast.toType.baseType, dimension: cast.toType.dimension, - opCode }); const id = DAG.getOrCreateNode(dag, nodeData); CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); - return id; + return { id, components: nodeData.dimension }; } -function mapPrimitiveDependencies(strandsContext, typeInfo, dependsOn) { +export function createMemberAccessNode(strandsContext, parentNode, componentNode, memberTypeInfo) { + const { dag, cfg } = strandsContext; + const nodeData = DAG.createNodeData({ + nodeType: NodeType.OPERATION, + opCode: OpCode.Binary.MEMBER_ACCESS, + dimension: memberTypeInfo.dimension, + baseType: memberTypeInfo.baseType, + dependsOn: [parentNode.id, componentNode.id], + }); + const id = DAG.getOrCreateNode(dag, nodeData); + CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); + return { id, components: memberTypeInfo.dimension }; +} + + +export function createStructInstanceNode(strandsContext, structTypeInfo, identifier, dependsOn) { + const { cfg, dag, } = strandsContext; + + if (dependsOn.length === 0) { + for (const prop of structTypeInfo.properties) { + const typeInfo = prop.dataType; + const nodeData = DAG.createNodeData({ + nodeType: NodeType.VARIABLE, + baseType: typeInfo.baseType, + dimension: typeInfo.dimension, + identifier: `${identifier}.${prop.name}`, + }); + const component = DAG.getOrCreateNode(dag, nodeData); + CFG.recordInBasicBlock(cfg, cfg.currentBlock, component.id); + dependsOn.push(component); + } + } + + const nodeData = DAG.createNodeData({ + nodeType: NodeType.VARIABLE, + dimension: structTypeInfo.properties.length, + baseType: structTypeInfo.name, + identifier, + dependsOn + }) + const structID = DAG.getOrCreateNode(dag, nodeData); + CFG.recordInBasicBlock(cfg, cfg.currentBlock, structID); + + return { id: structID, components: dependsOn }; +} + +function mapPrimitiveDepsToIDs(strandsContext, typeInfo, dependsOn) { dependsOn = Array.isArray(dependsOn) ? dependsOn : [dependsOn]; const mappedDependencies = []; let { dimension, baseType } = typeInfo; @@ -138,8 +179,8 @@ function mapPrimitiveDependencies(strandsContext, typeInfo, dependsOn) { continue; } else if (typeof dep === 'number') { - const newNode = createLiteralNode(strandsContext, { dimension: 1, baseType }, dep); - mappedDependencies.push(newNode); + const { id, components } = createLiteralNode(strandsContext, { dimension: 1, baseType }, dep); + mappedDependencies.push(id); calculatedDimensions += 1; continue; } @@ -163,7 +204,7 @@ function mapPrimitiveDependencies(strandsContext, typeInfo, dependsOn) { return { originalNodeID, mappedDependencies, inferredTypeInfo }; } -function constructTypeFromIDs(strandsContext, strandsNodesArray, typeInfo) { +export function constructTypeFromIDs(strandsContext, typeInfo, strandsNodesArray) { const nodeData = DAG.createNodeData({ nodeType: NodeType.OPERATION, opCode: OpCode.Nary.CONSTRUCTOR, @@ -175,23 +216,61 @@ function constructTypeFromIDs(strandsContext, strandsNodesArray, typeInfo) { return id; } -export function createTypeConstructorNode(strandsContext, typeInfo, dependsOn) { +export function createPrimitiveConstructorNode(strandsContext, typeInfo, dependsOn) { const { cfg, dag } = strandsContext; - const { mappedDependencies, inferredTypeInfo } = mapPrimitiveDependencies(strandsContext, typeInfo, dependsOn); + const { mappedDependencies, inferredTypeInfo } = mapPrimitiveDepsToIDs(strandsContext, typeInfo, dependsOn); const finalType = { baseType: typeInfo.baseType, dimension: inferredTypeInfo.dimension }; - const id = constructTypeFromIDs(strandsContext, mappedDependencies, finalType); + const id = constructTypeFromIDs(strandsContext, finalType, mappedDependencies); CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); - return id; + return { id, components: finalType.dimension }; +} + +export function createStructConstructorNode(strandsContext, structTypeInfo, rawUserArgs) { + const { cfg, dag } = strandsContext; + const { identifer, properties } = structTypeInfo; + + if (!(rawUserArgs.length === properties.length)) { + FES.userError('type error', + `You've tried to construct a ${structTypeInfo.name} struct with ${rawUserArgs.length} properties, but it expects ${properties.length} properties.\n` + + `The properties it expects are:\n` + + `${properties.map(prop => prop.name + ' ' + prop.DataType.baseType + prop.DataType.dimension)}` + ); + } + + const dependsOn = []; + for (let i = 0; i < properties.length; i++) { + const expectedProperty = properties[i]; + const { originalNodeID, mappedDependencies } = mapPrimitiveDepsToIDs(strandsContext, expectedProperty.dataType, rawUserArgs[i]); + if (originalNodeID) { + dependsOn.push(originalNodeID); + } + else { + dependsOn.push( + constructTypeFromIDs(strandsContext, expectedProperty.dataType, mappedDependencies) + ); + } + } + + const nodeData = DAG.createNodeData({ + nodeType: NodeType.OPERATION, + opCode: OpCode.Nary.CONSTRUCTOR, + dimension: properties.length, + baseType: structTypeInfo.name, + dependsOn + }); + const id = DAG.getOrCreateNode(dag, nodeData); + CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); + return { id, components: structTypeInfo.components }; } export function createFunctionCallNode(strandsContext, functionName, rawUserArgs) { const { cfg, dag } = strandsContext; const overloads = strandsBuiltinFunctions[functionName]; - const preprocessedArgs = rawUserArgs.map((rawUserArg) => mapPrimitiveDependencies(strandsContext, DataType.defer, rawUserArg)); + const preprocessedArgs = rawUserArgs.map((rawUserArg) => mapPrimitiveDepsToIDs(strandsContext, DataType.defer, rawUserArg)); const matchingArgsCounts = overloads.filter(overload => overload.params.length === preprocessedArgs.length); if (matchingArgsCounts.length === 0) { const argsLengthSet = new Set(); @@ -265,7 +344,7 @@ export function createFunctionCallNode(strandsContext, functionName, rawUserArgs } else { const paramType = bestOverload[i]; - const castedArgID = constructTypeFromIDs(strandsContext, arg.mappedDependencies, paramType); + const castedArgID = constructTypeFromIDs(strandsContext, paramType, arg.mappedDependencies); CFG.recordInBasicBlock(cfg, cfg.currentBlock, castedArgID); dependsOn.push(castedArgID); } @@ -281,7 +360,7 @@ export function createFunctionCallNode(strandsContext, functionName, rawUserArgs }) const id = DAG.getOrCreateNode(dag, nodeData); CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); - return id; + return { id, components: nodeData.dimension }; } export function createUnaryOpNode(strandsContext, strandsNode, opCode) { @@ -294,7 +373,7 @@ export function createUnaryOpNode(strandsContext, strandsNode, opCode) { dimension: dag.dimensions[strandsNode.id], }) CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); - return id; + return { id, components: nodeData.dimension }; } export function createStatementNode(strandsContext, type) { diff --git a/src/strands/ir_types.js b/src/strands/ir_types.js index 76fd39f551..021ee0f404 100644 --- a/src/strands/ir_types.js +++ b/src/strands/ir_types.js @@ -6,7 +6,8 @@ export const NodeType = { LITERAL: 1, VARIABLE: 2, CONSTANT: 3, - PHI: 4, + STRUCT: 4, + PHI: 5, }; export const NodeTypeToName = Object.fromEntries( @@ -18,6 +19,7 @@ export const NodeTypeRequiredFields = { [NodeType.LITERAL]: ["value"], [NodeType.VARIABLE]: ["identifier"], [NodeType.CONSTANT]: ["value"], + [NodeType.STRUCT]: [""], [NodeType.PHI]: ["dependsOn", "phiBlocks"] }; @@ -58,12 +60,12 @@ export const DataType = { export const StructType = { Vertex: { - identifer: 'Vertex', + name: 'Vertex', properties: [ { name: "position", dataType: DataType.float3 }, { name: "normal", dataType: DataType.float3 }, - { name: "color", dataType: DataType.float4 }, { name: "texCoord", dataType: DataType.float2 }, + { name: "color", dataType: DataType.float4 }, ] } } @@ -162,11 +164,11 @@ export const ConstantFolding = { [OpCode.Binary.LOGICAL_OR]: (a, b) => a || b, }; -export const SymbolToOpCode = {}; +// export const SymbolToOpCode = {}; export const OpCodeToSymbol = {}; for (const { symbol, opCode } of OperatorTable) { - SymbolToOpCode[symbol] = opCode; + // SymbolToOpCode[symbol] = opCode; OpCodeToSymbol[opCode] = symbol; } diff --git a/src/strands/p5.strands.js b/src/strands/p5.strands.js index be2be91595..ec4c70d2db 100644 --- a/src/strands/p5.strands.js +++ b/src/strands/p5.strands.js @@ -71,7 +71,7 @@ function strands(p5, fn) { // ....... const hooksObject = generateShaderCode(strandsContext); console.log(hooksObject); - console.log(hooksObject['vec4 getFinalColor']); + console.log(hooksObject['Vertex getWorldInputs']); // Reset the strands runtime context // deinitStrandsContext(strandsContext); diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index b377c691b6..2792f14fd1 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -3,8 +3,11 @@ import { createFunctionCallNode, createVariableNode, createStatementNode, - createTypeConstructorNode, + createPrimitiveConstructorNode, createUnaryOpNode, + createMemberAccessNode, + createStructInstanceNode, + createStructConstructorNode, } from './ir_builders' import { OperatorTable, @@ -20,6 +23,7 @@ import { strandsBuiltinFunctions } from './strands_builtins' import { StrandsConditional } from './strands_conditionals' import * as CFG from './ir_cfg' import * as FES from './strands_FES' +import { getNodeDataFromID } from './ir_dag' ////////////////////////////////////////////// // User nodes @@ -33,16 +37,16 @@ export class StrandsNode { export function initGlobalStrandsAPI(p5, fn, strandsContext) { // We augment the strands node with operations programatically // this means methods like .add, .sub, etc can be chained - for (const { name, arity, opCode, symbol } of OperatorTable) { + for (const { name, arity, opCode } of OperatorTable) { if (arity === 'binary') { StrandsNode.prototype[name] = function (...right) { - const id = createBinaryOpNode(strandsContext, this, right, opCode); + const { id, components } = createBinaryOpNode(strandsContext, this, right, opCode); return new StrandsNode(id); }; } if (arity === 'unary') { fn[name] = function (strandsNode) { - const id = createUnaryOpNode(strandsContext, strandsNode, opCode); + const { id, components } = createUnaryOpNode(strandsContext, strandsNode, opCode); return new StrandsNode(id); } } @@ -52,7 +56,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { // Unique Functions ////////////////////////////////////////////// fn.discard = function() { - const id = createStatementNode('discard'); + const { id, components } = createStatementNode('discard'); CFG.recordInBasicBlock(strandsContext.cfg, strandsContext.cfg.currentBlock, id); } @@ -68,7 +72,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { if (args.length > 4) { FES.userError("type error", "It looks like you've tried to construct a p5.strands node implicitly, with more than 4 components. This is currently not supported.") } - const id = createTypeConstructorNode(strandsContext, { baseType: BaseType.DEFER, dimension: null }, args.flat()); + const { id, components } = createPrimitiveConstructorNode(strandsContext, { baseType: BaseType.DEFER, dimension: null }, args.flat()); return new StrandsNode(id); } @@ -82,7 +86,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { const originalFn = fn[functionName]; fn[functionName] = function(...args) { if (strandsContext.active) { - const id = createFunctionCallNode(strandsContext, functionName, args); + const { id, components } = createFunctionCallNode(strandsContext, functionName, args); return new StrandsNode(id); } else { return originalFn.apply(this, args); @@ -91,7 +95,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { } else { fn[functionName] = function (...args) { if (strandsContext.active) { - const id = createFunctionCallNode(strandsContext, functionName, args); + const { id, components } = createFunctionCallNode(strandsContext, functionName, args); return new StrandsNode(id); } else { p5._friendlyError( @@ -121,8 +125,8 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { + typeInfo.fnName.slice(1).toLowerCase(); } - fn[`uniform${pascalTypeName}`] = function(name, ...defaultValue) { - const id = createVariableNode(strandsContext, typeInfo, name); + fn[`uniform${pascalTypeName}`] = function(name, defaultValue) { + const { id, components } = createVariableNode(strandsContext, typeInfo, name); strandsContext.uniforms.push({ name, typeInfo, defaultValue }); return new StrandsNode(id); }; @@ -130,7 +134,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { const originalp5Fn = fn[typeInfo.fnName]; fn[typeInfo.fnName] = function(...args) { if (strandsContext.active) { - const id = createTypeConstructorNode(strandsContext, typeInfo, args); + const { id, components } = createPrimitiveConstructorNode(strandsContext, typeInfo, args); return new StrandsNode(id); } else if (originalp5Fn) { return originalp5Fn.apply(this, args); @@ -153,16 +157,44 @@ function createHookArguments(strandsContext, parameters){ const paramType = param.type; if(isStructType(paramType.typeName)) { const structType = StructType[paramType.typeName]; - const argStruct = {}; - for (const prop of structType.properties) { - const memberNode = createVariableNode(strandsContext, prop.dataType, prop.name); - argStruct[prop.name] = memberNode; + const originalInstanceInfo = createStructInstanceNode(strandsContext, structType, param.name, []); + const structNode = new StrandsNode(originalInstanceInfo.id); + const componentNodes = originalInstanceInfo.components.map(id => new StrandsNode(id)) + + for (let i = 0; i < structType.properties.length; i++) { + const componentTypeInfo = structType.properties[i]; + Object.defineProperty(structNode, componentTypeInfo.name, { + get() { + return new StrandsNode(strandsContext.dag.dependsOn[structNode.id][i]) + // const { id, components } = createMemberAccessNode(strandsContext, structNode, componentNodes[i], componentTypeInfo.dataType); + // const memberAccessNode = new StrandsNode(id); + // return memberAccessNode; + }, + set(val) { + const oldDependsOn = strandsContext.dag.dependsOn[structNode.id]; + const newDependsOn = [...oldDependsOn]; + + let newValueID; + if (val instanceof StrandsNode) { + newValueID = val.id; + } + else { + let newVal = createPrimitiveConstructorNode(strandsContext, componentTypeInfo.dataType, val); + newValueID = newVal.id; + } + + newDependsOn[i] = newValueID; + const newStructInfo = createStructInstanceNode(strandsContext, structType, param.name, newDependsOn); + structNode.id = newStructInfo.id; + } + }) } - args.push(argStruct); + + args.push(structNode); } else /*if(isNativeType(paramType.typeName))*/ { const typeInfo = TypeInfoFromGLSLName[paramType.typeName]; - const id = createVariableNode(strandsContext, typeInfo, param.name); + const { id, components } = createVariableNode(strandsContext, typeInfo, param.name); const arg = new StrandsNode(id); args.push(arg); } @@ -172,17 +204,18 @@ function createHookArguments(strandsContext, parameters){ function enforceReturnTypeMatch(strandsContext, expectedType, returned, hookName) { if (!(returned instanceof StrandsNode)) { - try { - return createTypeConstructorNode(strandsContext, expectedType, returned); - } catch (e) { - FES.userError('type error', - `There was a type mismatch for a value returned from ${hookName}.\n` + - `The value in question was supposed to be:\n` + - `${expectedType.baseType + expectedType.dimension}\n` + - `But you returned:\n` + - `${returned}` - ); - } + // try { + const result = createPrimitiveConstructorNode(strandsContext, expectedType, returned); + return result.id; + // } catch (e) { + // FES.userError('type error', + // `There was a type mismatch for a value returned from ${hookName}.\n` + + // `The value in question was supposed to be:\n` + + // `${expectedType.baseType + expectedType.dimension}\n` + + // `But you returned:\n` + + // `${returned}` + // ); + // } } const dag = strandsContext.dag; @@ -196,11 +229,13 @@ function enforceReturnTypeMatch(strandsContext, expectedType, returned, hookName FES.userError('type error', `You have returned a vector with ${receivedType.dimension} components in ${hookType.name} when a ${expectedType.baseType + expectedType.dimension} was expected!`); } else { - returnedNodeID = createTypeConstructorNode(strandsContext, expectedType, returnedNodeID); + const result = createPrimitiveConstructorNode(strandsContext, expectedType, returned); + returnedNodeID = result.id; } } else if (receivedType.baseType !== expectedType.baseType) { - returnedNodeID = createTypeConstructorNode(strandsContext, expectedType, returnedNodeID); + const result = createPrimitiveConstructorNode(strandsContext, expectedType, returned); + returnedNodeID = result.id; } return returnedNodeID; @@ -224,44 +259,49 @@ export function createShaderHooksFunctions(strandsContext, fn, shader) { const userReturned = hookUserCallback(...args); const expectedReturnType = hookType.returnType; + let rootNodeID = null; + if(isStructType(expectedReturnType.typeName)) { const expectedStructType = StructType[expectedReturnType.typeName]; - const rootStruct = { - identifier: expectedReturnType.typeName, - properties: {} - }; - const expectedProperties = expectedStructType.properties; - - for (let i = 0; i < expectedProperties.length; i++) { - const expectedProp = expectedProperties[i]; - const propName = expectedProp.name; - const receivedValue = userReturned[propName]; - if (receivedValue === undefined) { - FES.userError('type error', `You've returned an incomplete object from ${hookType.name}.\n` + - `Expected: { ${expectedReturnType.properties.map(p => p.name).join(', ')} }\n` + - `Received: { ${Object.keys(userReturned).join(', ')} }\n` + - `All of the properties are required!`); + 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.`); + } + rootNodeID = userReturned.id; + } + else { + const expectedProperties = expectedStructType.properties; + const newStructDependencies = []; + for (let i = 0; i < expectedProperties.length; i++) { + const expectedProp = expectedProperties[i]; + const propName = expectedProp.name; + const receivedValue = userReturned[propName]; + if (receivedValue === undefined) { + FES.userError('type error', `You've returned an incomplete struct from ${hookType.name}.\n` + + `Expected: { ${expectedReturnType.properties.map(p => p.name).join(', ')} }\n` + + `Received: { ${Object.keys(userReturned).join(', ')} }\n` + + `All of the properties are required!`); + } + const expectedTypeInfo = expectedProp.dataType; + const returnedPropID = enforceReturnTypeMatch(strandsContext, expectedTypeInfo, receivedValue, hookType.name); + newStructDependencies.push(returnedPropID); } - - const expectedTypeInfo = expectedProp.dataType; - const returnedPropID = enforceReturnTypeMatch(strandsContext, expectedTypeInfo, receivedValue, hookType.name); - rootStruct.properties[propName] = returnedPropID; + const newStruct = createStructConstructorNode(strandsContext, expectedStructType, newStructDependencies); + rootNodeID = newStruct.id; } - strandsContext.hooks.push({ - hookType, - entryBlockID, - rootStruct - }); + } else /*if(isNativeType(expectedReturnType.typeName))*/ { const expectedTypeInfo = TypeInfoFromGLSLName[expectedReturnType.typeName]; - const returnedNodeID = enforceReturnTypeMatch(strandsContext, expectedTypeInfo, userReturned, hookType.name); - strandsContext.hooks.push({ - hookType, - entryBlockID, - rootNodeID: returnedNodeID, - }); + rootNodeID = enforceReturnTypeMatch(strandsContext, expectedTypeInfo, userReturned, hookType.name); } + + strandsContext.hooks.push({ + hookType, + entryBlockID, + rootNodeID, + }); CFG.popBlock(cfg); } } diff --git a/src/strands/strands_codegen.js b/src/strands/strands_codegen.js index c3b1606ce1..5f892c6909 100644 --- a/src/strands/strands_codegen.js +++ b/src/strands/strands_codegen.js @@ -1,6 +1,7 @@ import { NodeType } from './ir_types'; import { sortCFG } from './ir_cfg'; import { sortDAG } from './ir_dag'; +import strands from './p5.strands'; function generateTopLevelDeclarations(strandsContext, generationContext, dagOrder) { const { dag, backend } = strandsContext; @@ -28,7 +29,14 @@ function generateTopLevelDeclarations(strandsContext, generationContext, dagOrde export function generateShaderCode(strandsContext) { const { cfg, dag, backend } = strandsContext; - const hooksObj = {}; + const hooksObj = { + uniforms: {}, + }; + + for (const {name, typeInfo, defaultValue} of strandsContext.uniforms) { + const declaration = backend.generateUniformDeclaration(name, typeInfo); + hooksObj.uniforms[declaration] = defaultValue; + } for (const { hookType, entryBlockID, rootNodeID, rootStruct} of strandsContext.hooks) { const dagSorted = sortDAG(dag.dependsOn, rootNodeID); @@ -54,8 +62,8 @@ export function generateShaderCode(strandsContext) { } const firstLine = backend.hookEntry(hookType); - const finalExpression = `return ${backend.generateExpression(generationContext, dag, rootNodeID)};`; - generationContext.write(finalExpression); + backend.generateReturnStatement(strandsContext, generationContext, rootNodeID); + // generationContext.write(finalExpression); hooksObj[`${hookType.returnType.typeName} ${hookType.name}`] = [firstLine, ...generationContext.codeLines, '}'].join('\n'); } diff --git a/src/strands/strands_glslBackend.js b/src/strands/strands_glslBackend.js index 8b673477d4..9d138f9030 100644 --- a/src/strands/strands_glslBackend.js +++ b/src/strands/strands_glslBackend.js @@ -1,4 +1,4 @@ -import { NodeType, OpCodeToSymbol, BlockType, OpCode } from "./ir_types"; +import { NodeType, OpCodeToSymbol, BlockType, OpCode, NodeTypeToName, isStructType, StructType } from "./ir_types"; import { getNodeDataFromID, extractNodeTypeInfo } from "./ir_dag"; import * as FES from './strands_FES' @@ -80,7 +80,15 @@ export const glslBackend = { }, getTypeName(baseType, dimension) { - return TypeNames[baseType + dimension] + const primitiveTypeName = TypeNames[baseType + dimension] + if (!primitiveTypeName) { + return baseType; + } + return primitiveTypeName; + }, + + generateUniformDeclaration(name, typeInfo) { + return `${this.getTypeName(typeInfo.baseType, typeInfo.dimension)} ${name}`; }, generateDeclaration(generationContext, dag, nodeID) { @@ -93,8 +101,22 @@ export const glslBackend = { return `${typeName} ${tmp} = ${expr};`; }, - generateReturn(generationContext, dag, nodeID) { - + generateReturnStatement(strandsContext, generationContext, rootNodeID) { + const dag = strandsContext.dag; + const rootNode = getNodeDataFromID(dag, rootNodeID); + if (isStructType(rootNode.baseType)) { + const structTypeInfo = StructType[rootNode.baseType]; + for (let i = 0; i < structTypeInfo.properties.length; i++) { + const prop = structTypeInfo.properties[i]; + const val = this.generateExpression(generationContext, dag, rootNode.dependsOn[i]); + if (prop.name !== val) { + generationContext.write( + `${rootNode.identifier}.${prop.name} = ${val};` + ) + } + } + } + generationContext.write(`return ${this.generateExpression(generationContext, dag, rootNodeID)};`); }, generateExpression(generationContext, dag, nodeID) { @@ -123,6 +145,12 @@ export const glslBackend = { const functionArgs = node.dependsOn.map(arg =>this.generateExpression(generationContext, dag, arg)); return `${node.identifier}(${functionArgs.join(', ')})`; } + if (node.opCode === OpCode.Binary.MEMBER_ACCESS) { + const [lID, rID] = node.dependsOn; + const lName = this.generateExpression(generationContext, dag, lID); + const rName = this.generateExpression(generationContext, dag, rID); + return `${lName}.${rName}`; + } if (node.dependsOn.length === 2) { const [lID, rID] = node.dependsOn; const left = this.generateExpression(generationContext, dag, lID); @@ -142,7 +170,7 @@ export const glslBackend = { } default: - FES.internalError(`${node.nodeType} not working yet`) + FES.internalError(`${NodeTypeToName[node.nodeType]} code generation not implemented yet`) } }, From 2e70e0e987646c54a94a694a6019ccc5b2858eb4 Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Sat, 26 Jul 2025 18:46:07 +0100 Subject: [PATCH 22/69] comment old line. Should revisit structs if needs optimisation. --- src/strands/strands_api.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index 2792f14fd1..d48faa3624 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -159,7 +159,7 @@ function createHookArguments(strandsContext, parameters){ const structType = StructType[paramType.typeName]; const originalInstanceInfo = createStructInstanceNode(strandsContext, structType, param.name, []); const structNode = new StrandsNode(originalInstanceInfo.id); - const componentNodes = originalInstanceInfo.components.map(id => new StrandsNode(id)) + // const componentNodes = originalInstanceInfo.components.map(id => new StrandsNode(id)) for (let i = 0; i < structType.properties.length; i++) { const componentTypeInfo = structType.properties[i]; From 6d5913ae4f25bc9b608482a08afe18a227284ac2 Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Sat, 26 Jul 2025 18:59:20 +0100 Subject: [PATCH 23/69] fix wrong ID in binary op node --- src/strands/ir_builders.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/strands/ir_builders.js b/src/strands/ir_builders.js index 2b64471161..36322a5d31 100644 --- a/src/strands/ir_builders.js +++ b/src/strands/ir_builders.js @@ -59,8 +59,10 @@ export function createBinaryOpNode(strandsContext, leftStrandsNode, rightArg, op const cast = { node: null, toType: leftType }; const bothDeferred = leftType.baseType === rightType.baseType && leftType.baseType === BaseType.DEFER; if (bothDeferred) { - finalLeftNodeID = createPrimitiveConstructorNode(strandsContext, { baseType:BaseType.FLOAT, dimension: leftType.dimension }, leftStrandsNode); - finalRightNodeID = createPrimitiveConstructorNode(strandsContext, { baseType:BaseType.FLOAT, dimension: leftType.dimension }, rightStrandsNode); + const l = createPrimitiveConstructorNode(strandsContext, { baseType:BaseType.FLOAT, dimension: leftType.dimension }, leftStrandsNode); + const r = createPrimitiveConstructorNode(strandsContext, { baseType:BaseType.FLOAT, dimension: leftType.dimension }, rightStrandsNode); + finalLeftNodeID = l.id; + finalRightNodeID = r.id; } else if (leftType.baseType !== rightType.baseType || leftType.dimension !== rightType.dimension) { From 2745bda0e05399dcd5640c690133cd0ec7d782d3 Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Sat, 26 Jul 2025 19:14:43 +0100 Subject: [PATCH 24/69] fix bug with binary op, and make strandsNode return node if arg is already a node. --- src/strands/ir_builders.js | 25 ++++++++++++++++++++----- src/strands/strands_api.js | 3 +++ 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/strands/ir_builders.js b/src/strands/ir_builders.js index 36322a5d31..e88efb9469 100644 --- a/src/strands/ir_builders.js +++ b/src/strands/ir_builders.js @@ -1,7 +1,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, } from './ir_types'; +import { NodeType, OpCode, BaseType, DataType, BasePriority, OpCodeToSymbol, } from './ir_types'; import { StrandsNode } from './strands_api'; import { strandsBuiltinFunctions } from './strands_builtins'; @@ -59,16 +59,31 @@ export function createBinaryOpNode(strandsContext, leftStrandsNode, rightArg, op const cast = { node: null, toType: leftType }; const bothDeferred = leftType.baseType === rightType.baseType && leftType.baseType === BaseType.DEFER; if (bothDeferred) { - const l = createPrimitiveConstructorNode(strandsContext, { baseType:BaseType.FLOAT, dimension: leftType.dimension }, leftStrandsNode); - const r = createPrimitiveConstructorNode(strandsContext, { baseType:BaseType.FLOAT, dimension: leftType.dimension }, rightStrandsNode); - finalLeftNodeID = l.id; + cast.toType.baseType = BaseType.FLOAT; + if (leftType.dimension === rightType.dimension) { + cast.toType.dimension = leftType.dimension; + } + else if (leftType.dimension === 1 && rightType.dimension > 1) { + cast.toType.dimension = rightType.dimension; + } + else if (rightType.dimension === 1 && leftType.dimension > 1) { + cast.toType.dimension = leftType.dimension; + } + else { + FES.userError("type error", `You have tried to perform a binary operation:\n`+ + `${leftType.baseType+leftType.dimension} ${OpCodeToSymbol[opCode]} ${rightType.baseType+rightType.dimension}\n` + + `It's only possible to operate on two nodes with the same dimension, or a scalar value and a vector.` + ); + } + const l = createPrimitiveConstructorNode(strandsContext, cast.toType, leftStrandsNode); + const r = createPrimitiveConstructorNode(strandsContext, cast.toType, rightStrandsNode); + finalLeftNodeID = l.id; finalRightNodeID = r.id; } else if (leftType.baseType !== rightType.baseType || leftType.dimension !== rightType.dimension) { if (leftType.dimension === 1 && rightType.dimension > 1) { - // e.g. op(scalar, vector): cast scalar up cast.node = leftStrandsNode; cast.toType = rightType; } diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index d48faa3624..83a97aaf07 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -69,6 +69,9 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { } fn.strandsNode = function(...args) { + if (args.length === 1 && args[0] instanceof StrandsNode) { + return args[0]; + } if (args.length > 4) { FES.userError("type error", "It looks like you've tried to construct a p5.strands node implicitly, with more than 4 components. This is currently not supported.") } From 4133faed77f3c06841884e0fa16daa82cb45503c Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Tue, 29 Jul 2025 10:35:06 +0100 Subject: [PATCH 25/69] fix function call bugs --- preview/global/sketch.js | 6 ++-- src/strands/ir_builders.js | 29 +++++++++++-------- src/strands/ir_dag.js | 53 +++++++++++++++++----------------- src/strands/strands_codegen.js | 1 - 4 files changed, 47 insertions(+), 42 deletions(-) diff --git a/preview/global/sketch.js b/preview/global/sketch.js index 2c8a560128..4a34d4cbce 100644 --- a/preview/global/sketch.js +++ b/preview/global/sketch.js @@ -2,9 +2,9 @@ p5.disableFriendlyErrors = true; function callback() { const time = uniformFloat(() =>millis()*0.001) - // getFinalColor((col) => { - // return [1, 1, 0, 1]; - // }); + getFinalColor((col) => { + return [1,0,0, 1] +[1, 0, 0.1, 0] + pow(col,sin(time)); + }); getWorldInputs(inputs => { inputs.color = vec4(inputs.position, 1); diff --git a/src/strands/ir_builders.js b/src/strands/ir_builders.js index e88efb9469..3e159ec14a 100644 --- a/src/strands/ir_builders.js +++ b/src/strands/ir_builders.js @@ -1,7 +1,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, } from './ir_types'; +import { NodeType, OpCode, BaseType, DataType, BasePriority, OpCodeToSymbol, typeEquals, } from './ir_types'; import { StrandsNode } from './strands_api'; import { strandsBuiltinFunctions } from './strands_builtins'; @@ -298,14 +298,14 @@ export function createFunctionCallNode(strandsContext, functionName, rawUserArgs FES.userError("parameter validation error",`Function '${functionName}' has ${overloads.length} variants which expect ${argsLengthStr} arguments, but ${preprocessedArgs.length} arguments were provided.`); } + const isGeneric = (T) => T.dimension === null; let bestOverload = null; let bestScore = 0; let inferredReturnType = null; + let inferredDimension = null; + for (const overload of matchingArgsCounts) { - const isGeneric = (T) => T.dimension === null; let isValid = true; - let overloadParameters = []; - let inferredDimension = null; let similarity = 0; for (let i = 0; i < preprocessedArgs.length; i++) { @@ -318,7 +318,10 @@ export function createFunctionCallNode(strandsContext, functionName, rawUserArgs if (inferredDimension === null || inferredDimension === 1) { inferredDimension = argType.dimension; } - if (inferredDimension !== argType.dimension) { + + if (inferredDimension !== argType.dimension && + !(argType.dimension === 1 && inferredDimension >= 1) + ) { isValid = false; } dimension = inferredDimension; @@ -336,13 +339,12 @@ export function createFunctionCallNode(strandsContext, functionName, rawUserArgs similarity += 1; } - overloadParameters.push({ baseType: expectedType.baseType, dimension }); } if (isValid && (!bestOverload || similarity > bestScore)) { - bestOverload = overloadParameters; + bestOverload = overload; bestScore = similarity; - inferredReturnType = overload.returnType; + inferredReturnType = {...overload.returnType }; if (isGeneric(inferredReturnType)) { inferredReturnType.dimension = inferredDimension; } @@ -350,17 +352,20 @@ export function createFunctionCallNode(strandsContext, functionName, rawUserArgs } if (bestOverload === null) { - FES.userError('parameter validation', 'No matching overload found!'); + FES.userError('parameter validation', `No matching overload for ${functionName} was found!`); } let dependsOn = []; - for (let i = 0; i < bestOverload.length; i++) { + for (let i = 0; i < bestOverload.params.length; i++) { const arg = preprocessedArgs[i]; - if (arg.originalNodeID) { + const paramType = { ...bestOverload.params[i] }; + if (isGeneric(paramType)) { + paramType.dimension = inferredDimension; + } + if (arg.originalNodeID && typeEquals(arg.inferredTypeInfo, paramType)) { dependsOn.push(arg.originalNodeID); } else { - const paramType = bestOverload[i]; const castedArgID = constructTypeFromIDs(strandsContext, paramType, arg.mappedDependencies); CFG.recordInBasicBlock(cfg, cfg.currentBlock, castedArgID); dependsOn.push(castedArgID); diff --git a/src/strands/ir_dag.js b/src/strands/ir_dag.js index ae384aa346..6ad54752e1 100644 --- a/src/strands/ir_dag.js +++ b/src/strands/ir_dag.js @@ -25,16 +25,16 @@ export function createDirectedAcyclicGraph() { } export function getOrCreateNode(graph, node) { - const key = getNodeKey(node); - const existing = graph.cache.get(key); + // const key = getNodeKey(node); + // const existing = graph.cache.get(key); - if (existing !== undefined) { - return existing; - } else { + // if (existing !== undefined) { + // return existing; + // } else { const id = createNode(graph, node); - graph.cache.set(key, id); + // graph.cache.set(key, id); return id; - } + // } } export function createNodeData(data = {}) { @@ -74,6 +74,26 @@ export function extractNodeTypeInfo(dag, nodeID) { priority: BasePriority[dag.baseTypes[nodeID]], }; } + +export function sortDAG(adjacencyList, start) { + const visited = new Set(); + const postOrder = []; + + function dfs(v) { + if (visited.has(v)) { + return; + } + visited.add(v); + for (let w of adjacencyList[v]) { + dfs(w); + } + postOrder.push(v); + } + + dfs(start); + return postOrder; +} + ///////////////////////////////// // Private functions ///////////////////////////////// @@ -118,23 +138,4 @@ function validateNode(node){ if (missingFields.length > 0) { FES.internalError(`Missing fields ${missingFields.join(', ')} for a node type '${NodeTypeToName[nodeType]}'.`); } -} - -export function sortDAG(adjacencyList, start) { - const visited = new Set(); - const postOrder = []; - - function dfs(v) { - if (visited.has(v)) { - return; - } - visited.add(v); - for (let w of adjacencyList[v]) { - dfs(w); - } - postOrder.push(v); - } - - dfs(start); - return postOrder; } \ No newline at end of file diff --git a/src/strands/strands_codegen.js b/src/strands/strands_codegen.js index 5f892c6909..26c0a85f14 100644 --- a/src/strands/strands_codegen.js +++ b/src/strands/strands_codegen.js @@ -16,7 +16,6 @@ function generateTopLevelDeclarations(strandsContext, generationContext, dagOrde if (dag.nodeTypes[nodeID] !== NodeType.OPERATION) { continue; } - if (usedCount[nodeID] > 0) { const newDeclaration = backend.generateDeclaration(generationContext, dag, nodeID); declarations.push(newDeclaration); From b3ce3ec799579e7e679227c714eeef3b9cb5a203 Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Wed, 30 Jul 2025 11:31:07 +0100 Subject: [PATCH 26/69] remove dag sort, use basic block instructions instead. Also start work on swizzles --- preview/global/sketch.js | 10 +-- src/strands/ir_builders.js | 54 +++++++++++----- src/strands/ir_cfg.js | 7 +++ src/strands/ir_dag.js | 35 ++++------- src/strands/ir_types.js | 17 ++++-- src/strands/p5.strands.js | 8 +-- src/strands/strands_api.js | 98 ++++++++++++++++++++++++------ src/strands/strands_codegen.js | 34 +---------- src/strands/strands_glslBackend.js | 45 ++++++++++---- 9 files changed, 197 insertions(+), 111 deletions(-) diff --git a/preview/global/sketch.js b/preview/global/sketch.js index 4a34d4cbce..35719bcc25 100644 --- a/preview/global/sketch.js +++ b/preview/global/sketch.js @@ -2,12 +2,14 @@ p5.disableFriendlyErrors = true; function callback() { const time = uniformFloat(() =>millis()*0.001) - getFinalColor((col) => { - return [1,0,0, 1] +[1, 0, 0.1, 0] + pow(col,sin(time)); - }); + // getFinalColor((col) => { + // return vec4(1,0,0,1).rgba; + // }); getWorldInputs(inputs => { - inputs.color = vec4(inputs.position, 1); + // strandsIf(inputs.position === vec3(1), () => 0).Else() + console.log(inputs.position); + inputs.color = vec4(inputs.position.xyz, 1); inputs.position = inputs.position + sin(time) * 100; return inputs; }); diff --git a/src/strands/ir_builders.js b/src/strands/ir_builders.js index 3e159ec14a..89e5fe7401 100644 --- a/src/strands/ir_builders.js +++ b/src/strands/ir_builders.js @@ -8,7 +8,7 @@ import { strandsBuiltinFunctions } from './strands_builtins'; ////////////////////////////////////////////// // Builders for node graphs ////////////////////////////////////////////// -export function createLiteralNode(strandsContext, typeInfo, value) { +export function createScalarLiteralNode(strandsContext, typeInfo, value) { const { cfg, dag } = strandsContext let { dimension, baseType } = typeInfo; @@ -40,6 +40,22 @@ export function createVariableNode(strandsContext, typeInfo, identifier) { return { id, components: dimension }; } +export function createSwizzleNode(strandsContext, parentNode, swizzle) { + const { dag, cfg } = strandsContext; + const baseType = dag.baseTypes[parentNode.id]; + const nodeData = DAG.createNodeData({ + nodeType: NodeType.OPERATION, + baseType, + dimension: swizzle.length, + opCode: OpCode.Unary.SWIZZLE, + dependsOn: [parentNode.id], + swizzle, + }); + const id = DAG.getOrCreateNode(dag, nodeData); + CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); + return id; +} + export function createBinaryOpNode(strandsContext, leftStrandsNode, rightArg, opCode) { const { dag, cfg } = strandsContext; // Construct a node for right if its just an array or number etc. @@ -48,7 +64,7 @@ export function createBinaryOpNode(strandsContext, leftStrandsNode, rightArg, op rightStrandsNode = rightArg[0]; } else { const { id, components } = createPrimitiveConstructorNode(strandsContext, { baseType: BaseType.DEFER, dimension: null }, rightArg); - rightStrandsNode = new StrandsNode(id); + rightStrandsNode = new StrandsNode(id, components, strandsContext); } let finalLeftNodeID = leftStrandsNode.id; let finalRightNodeID = rightStrandsNode.id; @@ -138,7 +154,6 @@ export function createMemberAccessNode(strandsContext, parentNode, componentNode return { id, components: memberTypeInfo.dimension }; } - export function createStructInstanceNode(strandsContext, structTypeInfo, identifier, dependsOn) { const { cfg, dag, } = strandsContext; @@ -151,9 +166,9 @@ export function createStructInstanceNode(strandsContext, structTypeInfo, identif dimension: typeInfo.dimension, identifier: `${identifier}.${prop.name}`, }); - const component = DAG.getOrCreateNode(dag, nodeData); - CFG.recordInBasicBlock(cfg, cfg.currentBlock, component.id); - dependsOn.push(component); + const componentID = DAG.getOrCreateNode(dag, nodeData); + CFG.recordInBasicBlock(cfg, cfg.currentBlock, componentID); + dependsOn.push(componentID); } } @@ -196,7 +211,7 @@ function mapPrimitiveDepsToIDs(strandsContext, typeInfo, dependsOn) { continue; } else if (typeof dep === 'number') { - const { id, components } = createLiteralNode(strandsContext, { dimension: 1, baseType }, dep); + const { id, components } = createScalarLiteralNode(strandsContext, { dimension: 1, baseType }, dep); mappedDependencies.push(id); calculatedDimensions += 1; continue; @@ -241,8 +256,10 @@ export function createPrimitiveConstructorNode(strandsContext, typeInfo, depends dimension: inferredTypeInfo.dimension }; const id = constructTypeFromIDs(strandsContext, finalType, mappedDependencies); - CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); - return { id, components: finalType.dimension }; + if (typeInfo.baseType !== BaseType.DEFER) { + CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); + } + return { id, components: mappedDependencies }; } export function createStructConstructorNode(strandsContext, structTypeInfo, rawUserArgs) { @@ -382,22 +399,31 @@ export function createFunctionCallNode(strandsContext, functionName, rawUserArgs }) const id = DAG.getOrCreateNode(dag, nodeData); CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); - return { id, components: nodeData.dimension }; + return { id, components: { dependsOn, dimension: inferredReturnType.dimension } }; } export function createUnaryOpNode(strandsContext, strandsNode, opCode) { const { dag, cfg } = strandsContext; + const dependsOn = strandsNode.id; const nodeData = DAG.createNodeData({ nodeType: NodeType.OPERATION, opCode, - dependsOn: strandsNode.id, + dependsOn, baseType: dag.baseTypes[strandsNode.id], dimension: dag.dimensions[strandsNode.id], }) + const id = DAG.getOrCreateNode(dag, nodeData); CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); - return { id, components: nodeData.dimension }; + return { id, components: {dep} }; } -export function createStatementNode(strandsContext, type) { - return -99; +export function createStatementNode(strandsContext, opCode) { + const { dag, cfg } = strandsContext; + const nodeData = DAG.createNodeData({ + nodeType: NodeType.STATEMENT, + opCode + }); + const id = DAG.getOrCreateNode(dag, nodeData); + CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); + return id; } \ No newline at end of file diff --git a/src/strands/ir_cfg.js b/src/strands/ir_cfg.js index 27a323b885..78528c6789 100644 --- a/src/strands/ir_cfg.js +++ b/src/strands/ir_cfg.js @@ -1,4 +1,5 @@ import { BlockTypeToName } from "./ir_types"; +import * as FES from './strands_FES' export function createControlFlowGraph() { return { @@ -41,6 +42,12 @@ export function addEdge(graph, from, to) { } export function recordInBasicBlock(graph, blockID, nodeID) { + if (nodeID === undefined) { + FES.internalError('undefined nodeID in `recordInBasicBlock()`'); + } + if (blockID === undefined) { + FES.internalError('undefined blockID in `recordInBasicBlock()'); + } graph.blockInstructions[blockID] = graph.blockInstructions[blockID] || []; graph.blockInstructions[blockID].push(nodeID); } diff --git a/src/strands/ir_dag.js b/src/strands/ir_dag.js index 6ad54752e1..8cebf62b90 100644 --- a/src/strands/ir_dag.js +++ b/src/strands/ir_dag.js @@ -1,4 +1,4 @@ -import { NodeTypeRequiredFields, NodeTypeToName, BasePriority } from './ir_types'; +import { NodeTypeRequiredFields, NodeTypeToName, BasePriority, StatementType } from './ir_types'; import * as FES from './strands_FES'; ///////////////////////////////// @@ -18,7 +18,8 @@ export function createDirectedAcyclicGraph() { phiBlocks: [], dependsOn: [], usedBy: [], - graphType: 'DAG', + statementTypes: [], + swizzles: [], }; return graph; @@ -45,6 +46,8 @@ export function createNodeData(data = {}) { opCode: data.opCode ?? null, value: data.value ?? null, identifier: data.identifier ?? null, + statementType: data.statementType ?? null, + swizzle: data.swizzles ?? null, dependsOn: Array.isArray(data.dependsOn) ? data.dependsOn : [], usedBy: Array.isArray(data.usedBy) ? data.usedBy : [], phiBlocks: Array.isArray(data.phiBlocks) ? data.phiBlocks : [], @@ -55,6 +58,7 @@ export function createNodeData(data = {}) { export function getNodeDataFromID(graph, id) { return { + id, nodeType: graph.nodeTypes[id], opCode: graph.opCodes[id], value: graph.values[id], @@ -64,6 +68,8 @@ export function getNodeDataFromID(graph, id) { phiBlocks: graph.phiBlocks[id], dimension: graph.dimensions[id], baseType: graph.baseTypes[id], + statementType: graph.statementTypes[id], + swizzle: graph.swizzles[id], } } @@ -75,25 +81,6 @@ export function extractNodeTypeInfo(dag, nodeID) { }; } -export function sortDAG(adjacencyList, start) { - const visited = new Set(); - const postOrder = []; - - function dfs(v) { - if (visited.has(v)) { - return; - } - visited.add(v); - for (let w of adjacencyList[v]) { - dfs(w); - } - postOrder.push(v); - } - - dfs(start); - return postOrder; -} - ///////////////////////////////// // Private functions ///////////////////////////////// @@ -108,7 +95,9 @@ function createNode(graph, node) { graph.phiBlocks[id] = node.phiBlocks.slice(); graph.baseTypes[id] = node.baseType graph.dimensions[id] = node.dimension; - + graph.statementTypes[id] = node.statementType; + graph.swizzles[id] = node.swizzle + for (const dep of node.dependsOn) { if (!Array.isArray(graph.usedBy[dep])) { graph.usedBy[dep] = []; @@ -125,7 +114,7 @@ function getNodeKey(node) { function validateNode(node){ const nodeType = node.nodeType; - const requiredFields = [...NodeTypeRequiredFields[nodeType], 'baseType', 'dimension']; + const requiredFields = NodeTypeRequiredFields[nodeType]; 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!`) } diff --git a/src/strands/ir_types.js b/src/strands/ir_types.js index 021ee0f404..3082e4fc27 100644 --- a/src/strands/ir_types.js +++ b/src/strands/ir_types.js @@ -8,19 +8,26 @@ export const NodeType = { CONSTANT: 3, STRUCT: 4, PHI: 5, + STATEMENT: 6, }; + export const NodeTypeToName = Object.fromEntries( Object.entries(NodeType).map(([key, val]) => [val, key]) ); export const NodeTypeRequiredFields = { - [NodeType.OPERATION]: ["opCode", "dependsOn"], - [NodeType.LITERAL]: ["value"], - [NodeType.VARIABLE]: ["identifier"], - [NodeType.CONSTANT]: ["value"], + [NodeType.OPERATION]: ["opCode", "dependsOn", "dimension", "baseType"], + [NodeType.LITERAL]: ["value", "dimension", "baseType"], + [NodeType.VARIABLE]: ["identifier", "dimension", "baseType"], + [NodeType.CONSTANT]: ["value", "dimension", "baseType"], [NodeType.STRUCT]: [""], - [NodeType.PHI]: ["dependsOn", "phiBlocks"] + [NodeType.PHI]: ["dependsOn", "phiBlocks", "dimension", "baseType"], + [NodeType.STATEMENT]: ["opCode"] +}; + +export const StatementType = { + DISCARD: 'discard', }; export const BaseType = { diff --git a/src/strands/p5.strands.js b/src/strands/p5.strands.js index ec4c70d2db..da35c7097d 100644 --- a/src/strands/p5.strands.js +++ b/src/strands/p5.strands.js @@ -28,7 +28,7 @@ function strands(p5, fn) { ctx.previousFES = p5.disableFriendlyErrors; p5.disableFriendlyErrors = true; } - + function deinitStrandsContext(ctx) { ctx.dag = createDirectedAcyclicGraph(); ctx.cfg = createControlFlowGraph(); @@ -36,11 +36,11 @@ function strands(p5, fn) { ctx.hooks = []; p5.disableFriendlyErrors = ctx.previousFES; } - + const strandsContext = {}; initStrandsContext(strandsContext); initGlobalStrandsAPI(p5, fn, strandsContext) - + ////////////////////////////////////////////// // Entry Point ////////////////////////////////////////////// @@ -52,7 +52,7 @@ function strands(p5, fn) { const backend = glslBackend; initStrandsContext(strandsContext, glslBackend); createShaderHooksFunctions(strandsContext, fn, this); - + // 1. Transpile from strands DSL to JS let strandsCallback; if (options.parser) { diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index 83a97aaf07..e83f9f447d 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -5,9 +5,9 @@ import { createStatementNode, createPrimitiveConstructorNode, createUnaryOpNode, - createMemberAccessNode, createStructInstanceNode, createStructConstructorNode, + createSwizzleNode, } from './ir_builders' import { OperatorTable, @@ -16,7 +16,8 @@ import { BaseType, StructType, TypeInfoFromGLSLName, - isStructType, + isStructType, + OpCode, // isNativeType } from './ir_types' import { strandsBuiltinFunctions } from './strands_builtins' @@ -28,10 +29,68 @@ import { getNodeDataFromID } from './ir_dag' ////////////////////////////////////////////// // User nodes ////////////////////////////////////////////// +const swizzlesSet = new Set(); + export class StrandsNode { - constructor(id) { + constructor(id, dimension, strandsContext) { this.id = id; + this.strandsContext = strandsContext; + this.dimension = dimension; + installSwizzlesForDimension.call(this, strandsContext, dimension) + } +} + +function generateSwizzles(chars, maxLen = 4) { + const result = []; + + function build(current) { + if (current.length > 0) result.push(current); + if (current.length === maxLen) return; + + for (let c of chars) { + build(current + c); + } + } + + build(''); + return result; +} + +function installSwizzlesForDimension(strandsContext, dimension) { + if (swizzlesSet.has(dimension)) return; + swizzlesSet.add(dimension); + + const swizzleVariants = [ + ['x', 'y', 'z', 'w'], + ['r', 'g', 'b', 'a'], + ['s', 't', 'p', 'q'] + ].map(chars => chars.slice(0, dimension)); + + const descriptors = {}; + + for (const variant of swizzleVariants) { + const swizzleStrings = generateSwizzles(variant); + for (const swizzle of swizzleStrings) { + if (swizzle.length < 1 || swizzle.length > 4) continue; + if (descriptors[swizzle]) continue; + + const hasDuplicates = new Set(swizzle).size !== swizzle.length; + + descriptors[swizzle] = { + get() { + const id = createSwizzleNode(strandsContext, this, swizzle); + return new StrandsNode(id, 0, strandsContext); + }, + ...(hasDuplicates ? {} : { + set(value) { + return assignSwizzleNode(strandsContext, this, swizzle, value); + } + }) + }; + } } + + Object.defineProperties(this, descriptors); } export function initGlobalStrandsAPI(p5, fn, strandsContext) { @@ -41,23 +100,22 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { if (arity === 'binary') { StrandsNode.prototype[name] = function (...right) { const { id, components } = createBinaryOpNode(strandsContext, this, right, opCode); - return new StrandsNode(id); + return new StrandsNode(id, components, strandsContext); }; } if (arity === 'unary') { fn[name] = function (strandsNode) { const { id, components } = createUnaryOpNode(strandsContext, strandsNode, opCode); - return new StrandsNode(id); + return new StrandsNode(id, components, strandsContext); } } } - + ////////////////////////////////////////////// // Unique Functions ////////////////////////////////////////////// fn.discard = function() { - const { id, components } = createStatementNode('discard'); - CFG.recordInBasicBlock(strandsContext.cfg, strandsContext.cfg.currentBlock, id); + createStatementNode(strandsContext, OpCode.ControlFlow.DISCARD); } fn.strandsIf = function(conditionNode, ifBody) { @@ -76,7 +134,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { FES.userError("type error", "It looks like you've tried to construct a p5.strands node implicitly, with more than 4 components. This is currently not supported.") } const { id, components } = createPrimitiveConstructorNode(strandsContext, { baseType: BaseType.DEFER, dimension: null }, args.flat()); - return new StrandsNode(id); + return new StrandsNode(id, components, strandsContext); } ////////////////////////////////////////////// @@ -90,7 +148,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { fn[functionName] = function(...args) { if (strandsContext.active) { const { id, components } = createFunctionCallNode(strandsContext, functionName, args); - return new StrandsNode(id); + return new StrandsNode(id, components, strandsContext); } else { return originalFn.apply(this, args); } @@ -99,7 +157,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { fn[functionName] = function (...args) { if (strandsContext.active) { const { id, components } = createFunctionCallNode(strandsContext, functionName, args); - return new StrandsNode(id); + return new StrandsNode(id, components, strandsContext); } else { p5._friendlyError( `It looks like you've called ${functionName} outside of a shader's modify() function.` @@ -131,14 +189,14 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { fn[`uniform${pascalTypeName}`] = function(name, defaultValue) { const { id, components } = createVariableNode(strandsContext, typeInfo, name); strandsContext.uniforms.push({ name, typeInfo, defaultValue }); - return new StrandsNode(id); + return new StrandsNode(id, components, strandsContext); }; const originalp5Fn = fn[typeInfo.fnName]; fn[typeInfo.fnName] = function(...args) { if (strandsContext.active) { const { id, components } = createPrimitiveConstructorNode(strandsContext, typeInfo, args); - return new StrandsNode(id); + return new StrandsNode(id, components, strandsContext); } else if (originalp5Fn) { return originalp5Fn.apply(this, args); } else { @@ -155,26 +213,28 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { ////////////////////////////////////////////// function createHookArguments(strandsContext, parameters){ const args = []; + const dag = strandsContext.dag; for (const param of parameters) { const paramType = param.type; if(isStructType(paramType.typeName)) { const structType = StructType[paramType.typeName]; const originalInstanceInfo = createStructInstanceNode(strandsContext, structType, param.name, []); - const structNode = new StrandsNode(originalInstanceInfo.id); - // const componentNodes = originalInstanceInfo.components.map(id => new StrandsNode(id)) + const structNode = new StrandsNode(originalInstanceInfo.id, 0, strandsContext); + // const componentNodes = originalInstanceInfo.components.map(id => new StrandsNode(id, components)) for (let i = 0; i < structType.properties.length; i++) { const componentTypeInfo = structType.properties[i]; Object.defineProperty(structNode, componentTypeInfo.name, { get() { - return new StrandsNode(strandsContext.dag.dependsOn[structNode.id][i]) + const propNode = getNodeDataFromID(dag, dag.dependsOn[structNode.id][i]) + return new StrandsNode(propNode.id, propNode.dimension, strandsContext); // const { id, components } = createMemberAccessNode(strandsContext, structNode, componentNodes[i], componentTypeInfo.dataType); - // const memberAccessNode = new StrandsNode(id); + // const memberAccessNode = new StrandsNode(id, components); // return memberAccessNode; }, set(val) { - const oldDependsOn = strandsContext.dag.dependsOn[structNode.id]; + const oldDependsOn = dag.dependsOn[structNode.id]; const newDependsOn = [...oldDependsOn]; let newValueID; @@ -198,7 +258,7 @@ function createHookArguments(strandsContext, parameters){ else /*if(isNativeType(paramType.typeName))*/ { const typeInfo = TypeInfoFromGLSLName[paramType.typeName]; const { id, components } = createVariableNode(strandsContext, typeInfo, param.name); - const arg = new StrandsNode(id); + const arg = new StrandsNode(id, components, strandsContext); args.push(arg); } } diff --git a/src/strands/strands_codegen.js b/src/strands/strands_codegen.js index 26c0a85f14..065c22fb64 100644 --- a/src/strands/strands_codegen.js +++ b/src/strands/strands_codegen.js @@ -1,29 +1,4 @@ -import { NodeType } from './ir_types'; import { sortCFG } from './ir_cfg'; -import { sortDAG } from './ir_dag'; -import strands from './p5.strands'; - -function generateTopLevelDeclarations(strandsContext, generationContext, dagOrder) { - const { dag, backend } = strandsContext; - - const usedCount = {}; - for (const nodeID of dagOrder) { - usedCount[nodeID] = (dag.usedBy[nodeID] || []).length; - } - - const declarations = []; - for (const nodeID of dagOrder) { - if (dag.nodeTypes[nodeID] !== NodeType.OPERATION) { - continue; - } - if (usedCount[nodeID] > 0) { - const newDeclaration = backend.generateDeclaration(generationContext, dag, nodeID); - declarations.push(newDeclaration); - } - } - - return declarations; -} export function generateShaderCode(strandsContext) { const { cfg, dag, backend } = strandsContext; @@ -37,8 +12,8 @@ export function generateShaderCode(strandsContext) { hooksObj.uniforms[declaration] = defaultValue; } - for (const { hookType, entryBlockID, rootNodeID, rootStruct} of strandsContext.hooks) { - const dagSorted = sortDAG(dag.dependsOn, rootNodeID); + for (const { hookType, entryBlockID, rootNodeID} of strandsContext.hooks) { + // const dagSorted = sortDAG(dag.dependsOn, rootNodeID); const cfgSorted = sortCFG(cfg.outgoingEdges, entryBlockID); const generationContext = { @@ -47,15 +22,12 @@ export function generateShaderCode(strandsContext) { write(line) { this.codeLines.push(' '.repeat(this.indent) + line); }, - dagSorted, + // dagSorted, tempNames: {}, declarations: [], nextTempID: 0, }; - generationContext.declarations = generateTopLevelDeclarations(strandsContext, generationContext, dagSorted); - - generationContext.declarations.forEach(decl => generationContext.write(decl)); for (const blockID of cfgSorted) { backend.generateBlock(blockID, strandsContext, generationContext); } diff --git a/src/strands/strands_glslBackend.js b/src/strands/strands_glslBackend.js index 9d138f9030..97e475ac4c 100644 --- a/src/strands/strands_glslBackend.js +++ b/src/strands/strands_glslBackend.js @@ -1,7 +1,14 @@ -import { NodeType, OpCodeToSymbol, BlockType, OpCode, NodeTypeToName, isStructType, StructType } from "./ir_types"; +import { NodeType, OpCodeToSymbol, BlockType, OpCode, NodeTypeToName, isStructType, StructType, 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; + const uses = dag.usedBy[nodeID] || []; + return uses.length > 1; +} + const TypeNames = { 'float1': 'float', 'float2': 'vec2', @@ -25,16 +32,20 @@ const TypeNames = { const cfgHandlers = { [BlockType.DEFAULT]: (blockID, strandsContext, generationContext) => { - // const { dag, cfg } = strandsContext; - - // const blockInstructions = new Set(cfg.blockInstructions[blockID] || []); - // for (let nodeID of generationContext.dagSorted) { - // if (!blockInstructions.has(nodeID)) { - // continue; - // } - // const snippet = glslBackend.generateExpression(dag, nodeID, generationContext); - // generationContext.write(snippet); - // } + const { dag, cfg } = strandsContext; + + const instructions = cfg.blockInstructions[blockID] || []; + for (const nodeID of instructions) { + const nodeType = dag.nodeTypes[nodeID]; + if (shouldCreateTemp(dag, nodeID)) { + const declaration = glslBackend.generateDeclaration(generationContext, dag, nodeID); + generationContext.write(declaration); + } + if (nodeType === NodeType.STATEMENT) { + console.log("HELLO") + glslBackend.generateStatement(generationContext, dag, nodeID); + } + } }, [BlockType.IF_COND](blockID, strandsContext, generationContext) { @@ -91,6 +102,13 @@ export const glslBackend = { 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;'); + } + }, + generateDeclaration(generationContext, dag, nodeID) { const expr = this.generateExpression(generationContext, dag, nodeID); const tmp = `T${generationContext.nextTempID++}`; @@ -151,6 +169,11 @@ export const glslBackend = { const rName = this.generateExpression(generationContext, dag, rID); return `${lName}.${rName}`; } + if (node.opCode === OpCode.Unary.SWIZZLE) { + const parentID = node.dependsOn[0]; + const parentExpr = this.generateExpression(generationContext, dag, parentID); + return `${parentExpr}.${node.swizzle}`; + } if (node.dependsOn.length === 2) { const [lID, rID] = node.dependsOn; const left = this.generateExpression(generationContext, dag, lID); From 9ebf77e480a065f41eef6a2a2384a4ae3149cb2c Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Tue, 24 Jun 2025 16:47:20 +0100 Subject: [PATCH 27/69] syntax/ remove unneccessary --- src/webgl/ShaderGenerator.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/webgl/ShaderGenerator.js b/src/webgl/ShaderGenerator.js index 58b3c7cc34..dece652561 100644 --- a/src/webgl/ShaderGenerator.js +++ b/src/webgl/ShaderGenerator.js @@ -1116,13 +1116,12 @@ function shadergenerator(p5, fn) { GLOBAL_SHADER = this; this.userCallback = userCallback; this.srcLocations = srcLocations; - this.cleanup = () => {}; this.generateHookOverrides(originalShader); this.output = { vertexDeclarations: new Set(), fragmentDeclarations: new Set(), uniforms: {}, - } + }; this.uniformNodes = []; this.resetGLSLContext(); this.isGenerating = false; From faae3aa30390313f26d1b6bdd335c28a8a3f88c4 Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Fri, 27 Jun 2025 11:17:47 +0100 Subject: [PATCH 28/69] blocking out new modular strands structure --- preview/global/sketch.js | 116 +------------- src/strands/code_transpiler.js | 222 ++++++++++++++++++++++++++ src/strands/control_flow_graph.js | 0 src/strands/directed_acyclic_graph.js | 85 ++++++++++ src/strands/p5.StrandsNode.js | 40 +++++ src/strands/p5.strands.js | 95 +++++++++++ src/strands/strands_FES.js | 4 + src/strands/utils.js | 109 +++++++++++++ src/webgl/index.js | 2 + 9 files changed, 559 insertions(+), 114 deletions(-) create mode 100644 src/strands/code_transpiler.js create mode 100644 src/strands/control_flow_graph.js create mode 100644 src/strands/directed_acyclic_graph.js create mode 100644 src/strands/p5.StrandsNode.js create mode 100644 src/strands/p5.strands.js create mode 100644 src/strands/strands_FES.js create mode 100644 src/strands/utils.js diff --git a/preview/global/sketch.js b/preview/global/sketch.js index b0cd6c8045..c52148e7d3 100644 --- a/preview/global/sketch.js +++ b/preview/global/sketch.js @@ -1,124 +1,12 @@ p5.disableFriendlyErrors = true; -function windowResized() { - resizeCanvas(windowWidth, windowHeight); -} - -let starShader; -let starStrokeShader; -let stars; -let originalFrameBuffer; -let pixellizeShader; -let fresnelShader; -let bloomShader; - -function fresnelShaderCallback() { - const fresnelPower = uniformFloat(2); - const fresnelBias = uniformFloat(-0.1); - const fresnelScale = uniformFloat(2); - getCameraInputs((inputs) => { - let n = normalize(inputs.normal); - let v = normalize(-inputs.position); - let base = 1.0 - dot(n, v); - let fresnel = fresnelScale * pow(base, fresnelPower) + fresnelBias; - let col = mix([0, 0, 0], [1, .5, .7], fresnel); - inputs.color = [col, 1]; - return inputs; - }); -} - -function starShaderCallback() { - const time = uniformFloat(() => millis()); - const skyRadius = uniformFloat(1000); - - function rand2(st) { - return fract(sin(dot(st, [12.9898, 78.233])) * 43758.5453123); - } - - function semiSphere() { - let id = instanceID(); - let theta = rand2([id, 0.1234]) * TWO_PI; - let phi = rand2([id, 3.321]) * PI+time/10000; - let r = skyRadius; - r *= 1.5 * sin(phi); - let x = r * sin(phi) * cos(theta); - let y = r * 1.5 * cos(phi); - let z = r * sin(phi) * sin(theta); - return [x, y, z]; - } - - getWorldInputs((inputs) => { - inputs.position += semiSphere(); - return inputs; - }); - - getObjectInputs((inputs) => { - let scale = 1 + 0.1 * sin(time * 0.002 + instanceID()); - inputs.position *= scale; - return inputs; - }); -} - -function pixellizeShaderCallback() { - const pixelSize = uniformFloat(()=> width*.75); - getColor((input, canvasContent) => { - let coord = input.texCoord; - coord = floor(coord * pixelSize) / pixelSize; - let col = texture(canvasContent, coord); - return col; - }); -} - function bloomShaderCallback() { - const preBlur = uniformTexture(() => originalFrameBuffer); - getColor((input, canvasContent) => { - const blurredCol = texture(canvasContent, input.texCoord); - const originalCol = texture(preBlur, input.texCoord); - const brightPass = max(originalCol, 0.3) * 1.5; - const bloom = originalCol + blurredCol * brightPass; - return bloom; - }); + createFloat(1.0); } async function setup(){ - createCanvas(windowWidth, windowHeight, WEBGL); - stars = buildGeometry(() => sphere(30, 4, 2)) - originalFrameBuffer = createFramebuffer(); - - starShader = baseMaterialShader().modify(starShaderCallback); - starStrokeShader = baseStrokeShader().modify(starShaderCallback) - fresnelShader = baseColorShader().modify(fresnelShaderCallback); - bloomShader = baseFilterShader().modify(bloomShaderCallback); - pixellizeShader = baseFilterShader().modify(pixellizeShaderCallback); + bloomShader = baseFilterShader().newModify(bloomShaderCallback); } function draw(){ - originalFrameBuffer.begin(); - background(0); - orbitControl(); - - push() - strokeWeight(4) - stroke(255,0,0) - rotateX(PI/2 + millis() * 0.0005); - fill(255,100, 150) - strokeShader(starStrokeShader) - shader(starShader); - model(stars, 2000); - pop() - - push() - shader(fresnelShader) - noStroke() - sphere(500); - pop() - filter(pixellizeShader); - - originalFrameBuffer.end(); - - imageMode(CENTER) - image(originalFrameBuffer, 0, 0) - - filter(BLUR, 20) - filter(bloomShader); } diff --git a/src/strands/code_transpiler.js b/src/strands/code_transpiler.js new file mode 100644 index 0000000000..6692c574a0 --- /dev/null +++ b/src/strands/code_transpiler.js @@ -0,0 +1,222 @@ +import { parse } from 'acorn'; +import { ancestor } from 'acorn-walk'; +import escodegen from 'escodegen'; + +import { OperatorTable } from './utils'; + +// TODO: Switch this to operator table, cleanup whole file too + +function replaceBinaryOperator(codeSource) { + switch (codeSource) { + case '+': return 'add'; + case '-': return 'sub'; + case '*': return 'mult'; + case '/': return 'div'; + case '%': return 'mod'; + case '==': + case '===': return 'equalTo'; + case '>': return 'greaterThan'; + case '>=': return 'greaterThanEqualTo'; + case '<': return 'lessThan'; + case '&&': return 'and'; + case '||': return 'or'; + } +} + +function ancestorIsUniform(ancestor) { + return ancestor.type === 'CallExpression' + && ancestor.callee?.type === 'Identifier' + && ancestor.callee?.name.startsWith('uniform'); +} + +const ASTCallbacks = { + UnaryExpression(node, _state, _ancestors) { + if (_ancestors.some(ancestorIsUniform)) { return; } + + const signNode = { + type: 'Literal', + value: node.operator, + } + + const standardReplacement = (node) => { + node.type = 'CallExpression' + node.callee = { + type: 'Identifier', + name: 'unaryNode', + } + node.arguments = [node.argument, signNode] + } + + if (node.type === 'MemberExpression') { + const property = node.argument.property.name; + const swizzleSets = [ + ['x', 'y', 'z', 'w'], + ['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 = { + type: 'CallExpression', + callee: { + type: 'Identifier', + name: 'unaryNode' + }, + arguments: [node.argument.object, signNode], + }; + node.property = { + type: 'Identifier', + name: property + }; + } else { + standardReplacement(node); + } + } else { + standardReplacement(node); + } + delete node.argument; + delete node.operator; + }, + VariableDeclarator(node, _state, _ancestors) { + if (node.init.callee && node.init.callee.name?.startsWith('uniform')) { + const uniformNameLiteral = { + type: 'Literal', + value: node.id.name + } + node.init.arguments.unshift(uniformNameLiteral); + } + if (node.init.callee && node.init.callee.name?.startsWith('varying')) { + const varyingNameLiteral = { + type: 'Literal', + value: node.id.name + } + node.init.arguments.unshift(varyingNameLiteral); + _state.varyings[node.id.name] = varyingNameLiteral; + } + }, + Identifier(node, _state, _ancestors) { + if (_state.varyings[node.name] + && !_ancestors.some(a => a.type === 'AssignmentExpression' && a.left === node)) { + node.type = 'ExpressionStatement'; + node.expression = { + type: 'CallExpression', + callee: { + type: 'MemberExpression', + object: { + type: 'Identifier', + name: node.name + }, + property: { + type: 'Identifier', + name: 'getValue' + }, + }, + arguments: [], + } + } + }, + // The callbacks for AssignmentExpression and BinaryExpression handle + // operator overloading including +=, *= assignment expressions + ArrayExpression(node, _state, _ancestors) { + const original = JSON.parse(JSON.stringify(node)); + node.type = 'CallExpression'; + node.callee = { + type: 'Identifier', + name: 'dynamicNode', + }; + node.arguments = [original]; + }, + AssignmentExpression(node, _state, _ancestors) { + if (node.operator !== '=') { + const methodName = replaceBinaryOperator(node.operator.replace('=','')); + const rightReplacementNode = { + type: 'CallExpression', + callee: { + type: 'MemberExpression', + object: node.left, + property: { + type: 'Identifier', + name: methodName, + }, + }, + arguments: [node.right] + } + node.operator = '='; + node.right = rightReplacementNode; + } + if (_state.varyings[node.left.name]) { + node.type = 'ExpressionStatement'; + node.expression = { + type: 'CallExpression', + callee: { + type: 'MemberExpression', + object: { + type: 'Identifier', + name: node.left.name + }, + property: { + type: 'Identifier', + name: 'bridge', + } + }, + arguments: [node.right], + } + } + }, + BinaryExpression(node, _state, _ancestors) { + // Don't convert uniform default values to node methods, as + // they should be evaluated at runtime, not compiled. + if (_ancestors.some(ancestorIsUniform)) { return; } + // If the left hand side of an expression is one of these types, + // we should construct a node from it. + const unsafeTypes = ['Literal', 'ArrayExpression', 'Identifier']; + if (unsafeTypes.includes(node.left.type)) { + const leftReplacementNode = { + type: 'CallExpression', + callee: { + type: 'Identifier', + name: 'dynamicNode', + }, + arguments: [node.left] + } + node.left = leftReplacementNode; + } + // Replace the binary operator with a call expression + // in other words a call to BaseNode.mult(), .div() etc. + node.type = 'CallExpression'; + node.callee = { + type: 'MemberExpression', + object: node.left, + property: { + type: 'Identifier', + name: replaceBinaryOperator(node.operator), + }, + }; + node.arguments = [node.right]; + }, + } + + export function transpileStrandsToJS(sourceString, srcLocations) { + const ast = parse(sourceString, { + ecmaVersion: 2021, + locations: srcLocations + }); + ancestor(ast, ASTCallbacks, undefined, { varyings: {} }); + const transpiledSource = escodegen.generate(ast); + const strandsCallback = new Function( + transpiledSource + .slice( + transpiledSource.indexOf('{') + 1, + transpiledSource.lastIndexOf('}') + ).replaceAll(';', '') + ); + + console.log(transpiledSource); + return strandsCallback; + } + \ No newline at end of file diff --git a/src/strands/control_flow_graph.js b/src/strands/control_flow_graph.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/strands/directed_acyclic_graph.js b/src/strands/directed_acyclic_graph.js new file mode 100644 index 0000000000..6da09d4921 --- /dev/null +++ b/src/strands/directed_acyclic_graph.js @@ -0,0 +1,85 @@ +import { NodeTypeRequiredFields, NodeTypeName } from './utils' +import * as strandsFES from './strands_FES' + +// Properties of the Directed Acyclic Graph and its nodes +const graphProperties = [ + 'nodeTypes', + 'dataTypes', + 'opCodes', + 'values', + 'identifiers', + // sparse adjancey list for dependencies (indegree) + 'dependsOnStartIndex', + 'dependsOnCount', + 'dependsOnList', +]; + +const nodeProperties = [ + 'nodeType', + 'dataType', + 'opCodes', + 'value', + 'identifier', + 'dependsOn' +]; + +// Public functions for for strands runtime +export function createGraph() { + const graph = { + _nextID: 0, + _nodeCache: new Map(), + } + for (const prop of graphProperties) { + graph[prop] = []; + } + return graph; +} + + +export function getOrCreateNode(graph, node) { + const result = getNode(graph, node); + if (!result){ + return createNode(graph, node) + } else { + return result; + } +} + +export function createNodeData(data = {}) { + const node = {}; + for (const key of nodeProperties) { + node[key] = data[key] ?? NaN; + } + validateNode(node); + return node; +} + +// Private functions to this file +function getNodeKey(node) { + +} + +function validateNode(node){ + const requiredFields = NodeTypeRequiredFields[node.NodeType]; + const missingFields = []; + for (const field of requiredFields) { + if (node[field] === NaN) { + missingFields.push(field); + } + } + if (missingFields.length > 0) { + strandsFES.internalError(`[p5.strands internal error]: Missing fields ${missingFields.join(', ')} for a node type ${NodeTypeName(node.nodeType)}`); + } +} + +function getNode(graph, node) { + if (graph) + + if (!node) { + return null; + } +} + +function createNode(graph, nodeData) { + +} \ No newline at end of file diff --git a/src/strands/p5.StrandsNode.js b/src/strands/p5.StrandsNode.js new file mode 100644 index 0000000000..ffddc7e83e --- /dev/null +++ b/src/strands/p5.StrandsNode.js @@ -0,0 +1,40 @@ +////////////////////////////////////////////// +// User API +////////////////////////////////////////////// + +import { OperatorTable } from './utils' + +export class StrandsNode { + constructor(id) { + this.id = id; + } +} + +export function createStrandsAPI(strands, fn) { + // Attach operators to StrandsNode: + for (const { name, symbol, arity } of OperatorTable) { + if (arity === 'binary') { + StrandsNode.prototype[name] = function (rightNode) { + const id = strands.createBinaryExpressionNode(this, rightNode, symbol); + return new StrandsNode(id); + }; + } + if (arity === 'unary') { + StrandsNode.prototype[name] = function () { + const id = strands.createUnaryExpressionNode(this, symbol); + return new StrandsNode(id); + }; + } + } + + // Attach p5 Globals + fn.uniformFloat = function(name, value) { + const id = strands.createVariableNode(DataType.FLOAT, name); + return new StrandsNode(id); + }, + + fn.createFloat = function(value) { + const id = strands.createLiteralNode(DataType.FLOAT, value); + return new StrandsNode(id); + } +} \ No newline at end of file diff --git a/src/strands/p5.strands.js b/src/strands/p5.strands.js new file mode 100644 index 0000000000..0bdfe7bda5 --- /dev/null +++ b/src/strands/p5.strands.js @@ -0,0 +1,95 @@ +/** +* @module 3D +* @submodule strands +* @for p5 +* @requires core +*/ + +import { transpileStrandsToJS } from './code_transpiler'; +import { DataType, NodeType, OpCode, SymbolToOpCode, OpCodeToSymbol, OpCodeArgs } from './utils'; + +import { createStrandsAPI } from './p5.StrandsNode' +import * as DAG from './directed_acyclic_graph'; +import * as CFG from './control_flow_graph' +import { create } from '@davepagurek/bezier-path'; + +function strands(p5, fn) { + + ////////////////////////////////////////////// + // Global Runtime + ////////////////////////////////////////////// + + class StrandsRuntime { + constructor() { + this.reset(); + } + + reset() { + this._scopeStack = []; + this._allScopes = new Map(); + } + + createBinaryExpressionNode(left, right, operatorSymbol) { + const activeGraph = this._currentScope().graph; + const opCode = SymbolToOpCode.get(operatorSymbol); + + const dataType = DataType.FLOAT; // lookUpBinaryOperatorResult(); + return activeGraph._getOrCreateNode(NodeType.OPERATION, dataType, opCode, null, null, [left, right]); + } + + createLiteralNode(dataType, value) { + const activeGraph = this._currentScope().graph; + return activeGraph._getOrCreateNode(NodeType.LITERAL, dataType, value, null, null, null); + } + } + + ////////////////////////////////////////////// + // Entry Point + ////////////////////////////////////////////// + + const strands = new StrandsRuntime(); + const API = createStrandsAPI(strands, fn); + + const oldModify = p5.Shader.prototype.modify + + for (const [fnName, fnBody] of Object.entries(userFunctions)) { + fn[fnName] = fnBody; + } + + p5.Shader.prototype.newModify = function(shaderModifier, options = { parser: true, srcLocations: false }) { + if (shaderModifier instanceof Function) { + + // 1. Transpile from strands DSL to JS + let strandsCallback; + if (options.parser) { + strandsCallback = transpileStrandsToJS(shaderModifier.toString(), options.srcLocations); + } else { + strandsCallback = shaderModifier; + } + + // 2. Build the IR from JavaScript API + strands.enterScope('GLOBAL'); + strandsCallback(); + strands.exitScope('GLOBAL'); + + + // 3. Generate shader code hooks object from the IR + // ....... + + // Call modify with the generated hooks object + // return oldModify.call(this, generatedModifyArgument); + + // Reset the strands runtime context + // strands.reset(); + } + else { + return oldModify.call(this, shaderModifier) + } + } +} + +export default strands; + +if (typeof p5 !== 'undefined') { + p5.registerAddon(strands) +} diff --git a/src/strands/strands_FES.js b/src/strands/strands_FES.js new file mode 100644 index 0000000000..695b220e6a --- /dev/null +++ b/src/strands/strands_FES.js @@ -0,0 +1,4 @@ +export function internalError(message) { + const prefixedMessage = `[p5.strands internal error]: ${message}` + throw new Error(prefixedMessage); +} \ No newline at end of file diff --git a/src/strands/utils.js b/src/strands/utils.js new file mode 100644 index 0000000000..29a3e1d1ab --- /dev/null +++ b/src/strands/utils.js @@ -0,0 +1,109 @@ +///////////////////// +// Enums for nodes // +///////////////////// + +export const NodeType = { + // Internal Nodes: + OPERATION: 0, + // Leaf Nodes + LITERAL: 1, + VARIABLE: 2, + CONSTANT: 3, +}; + +export const NodeTypeRequiredFields = { + [NodeType.OPERATION]: ['opCodes', 'dependsOn'], + [NodeType.LITERAL]: ['values'], + [NodeType.VARIABLE]: ['identifiers'], + [NodeType.CONSTANT]: ['values'], +}; + +export const NodeTypeName = Object.fromEntries( + Object.entries(NodeType).map(([key, val]) => [val, key]) +); + +export const DataType = { + FLOAT: 0, + VEC2: 1, + VEC3: 2, + VEC4: 3, + + INT: 100, + IVEC2: 101, + IVEC3: 102, + IVEC4: 103, + + BOOL: 200, + BVEC2: 201, + BVEC3: 202, + BVEC4: 203, + + MAT2X2: 300, + MAT3X3: 301, + MAT4X4: 302, +} + +export const OpCode = { + Binary: { + ADD: 0, + SUBTRACT: 1, + MULTIPLY: 2, + DIVIDE: 3, + MODULO: 4, + EQUAL: 5, + NOT_EQUAL: 6, + GREATER_THAN: 7, + GREATER_EQUAL: 8, + LESS_THAN: 9, + LESS_EQUAL: 10, + LOGICAL_AND: 11, + LOGICAL_OR: 12, + MEMBER_ACCESS: 13, + }, + Unary: { + LOGICAL_NOT: 100, + NEGATE: 101, + PLUS: 102, + SWIZZLE: 103, + }, + Nary: { + FUNCTION_CALL: 200, + }, + ControlFlow: { + RETURN: 300, + JUMP: 301, + BRANCH_IF_FALSE: 302, + DISCARD: 303, + } +}; + +export const OperatorTable = [ + { arity: "unary", name: "not", symbol: "!", opcode: OpCode.Unary.LOGICAL_NOT }, + { arity: "unary", name: "neg", symbol: "-", opcode: OpCode.Unary.NEGATE }, + { arity: "unary", name: "plus", symbol: "+", opcode: OpCode.Unary.PLUS }, + { arity: "binary", name: "add", symbol: "+", opcode: OpCode.Binary.ADD }, + { arity: "binary", name: "min", symbol: "-", opcode: OpCode.Binary.SUBTRACT }, + { arity: "binary", name: "mult", symbol: "*", opcode: OpCode.Binary.MULTIPLY }, + { arity: "binary", name: "div", symbol: "/", opcode: OpCode.Binary.DIVIDE }, + { arity: "binary", name: "mod", symbol: "%", opcode: OpCode.Binary.MODULO }, + { arity: "binary", name: "equalTo", symbol: "==", opcode: OpCode.Binary.EQUAL }, + { arity: "binary", name: "notEqual", symbol: "!=", opcode: OpCode.Binary.NOT_EQUAL }, + { arity: "binary", name: "greaterThan", symbol: ">", opcode: OpCode.Binary.GREATER_THAN }, + { arity: "binary", name: "greaterEqual", symbol: ">=", opcode: OpCode.Binary.GREATER_EQUAL }, + { arity: "binary", name: "lessThan", symbol: "<", opcode: OpCode.Binary.LESS_THAN }, + { arity: "binary", name: "lessEqual", symbol: "<=", opcode: OpCode.Binary.LESS_EQUAL }, + { arity: "binary", name: "and", symbol: "&&", opcode: OpCode.Binary.LOGICAL_AND }, + { arity: "binary", name: "or", symbol: "||", opcode: OpCode.Binary.LOGICAL_OR }, +]; + + +export const SymbolToOpCode = {}; +export const OpCodeToSymbol = {}; +export const OpCodeArgs = {}; + +for (const { arity: args, symbol, opcode } of OperatorTable) { + SymbolToOpCode[symbol] = opcode; + OpCodeToSymbol[opcode] = symbol; + OpCodeArgs[opcode] = args; + +} \ No newline at end of file diff --git a/src/webgl/index.js b/src/webgl/index.js index 7ba587b132..355125b36e 100644 --- a/src/webgl/index.js +++ b/src/webgl/index.js @@ -15,6 +15,7 @@ import camera from './p5.Camera'; import texture from './p5.Texture'; import rendererGL from './p5.RendererGL'; import shadergenerator from './ShaderGenerator'; +import strands from '../strands/p5.strands'; export default function(p5){ rendererGL(p5, p5.prototype); @@ -34,4 +35,5 @@ export default function(p5){ shader(p5, p5.prototype); texture(p5, p5.prototype); shadergenerator(p5, p5.prototype); + strands(p5, p5.prototype); } From f6783d27a218ef01b1945d0f06c3b8414fc0f855 Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Tue, 1 Jul 2025 19:56:36 +0100 Subject: [PATCH 29/69] chipping away at DOD approach. --- preview/global/sketch.js | 10 +- src/strands/CFG.js | 35 ++++ src/strands/DAG.js | 109 +++++++++++++ src/strands/GLSL_generator.js | 5 + src/strands/control_flow_graph.js | 0 src/strands/directed_acyclic_graph.js | 85 ---------- src/strands/p5.StrandsNode.js | 40 ----- src/strands/p5.strands.js | 224 +++++++++++++++++++++----- src/strands/utils.js | 22 ++- 9 files changed, 360 insertions(+), 170 deletions(-) create mode 100644 src/strands/CFG.js create mode 100644 src/strands/DAG.js create mode 100644 src/strands/GLSL_generator.js delete mode 100644 src/strands/control_flow_graph.js delete mode 100644 src/strands/directed_acyclic_graph.js delete mode 100644 src/strands/p5.StrandsNode.js diff --git a/preview/global/sketch.js b/preview/global/sketch.js index c52148e7d3..ec77fd8c0e 100644 --- a/preview/global/sketch.js +++ b/preview/global/sketch.js @@ -1,11 +1,15 @@ p5.disableFriendlyErrors = true; -function bloomShaderCallback() { - createFloat(1.0); +function callback() { + let x = createFloat(1.0); + getFinalColor((col) => { + return x; + }) } async function setup(){ - bloomShader = baseFilterShader().newModify(bloomShaderCallback); + createCanvas(300,400, WEBGL) + bloomShader = baseColorShader().newModify(callback, {parser: false}); } function draw(){ diff --git a/src/strands/CFG.js b/src/strands/CFG.js new file mode 100644 index 0000000000..28b4007e9c --- /dev/null +++ b/src/strands/CFG.js @@ -0,0 +1,35 @@ +export function createControlFlowGraph() { + const graph = { + nextID: 0, + blockTypes: [], + incomingEdges:[], + incomingEdgesIndex: [], + incomingEdgesCount: [], + outgoingEdges: [], + outgoingEdgesIndex: [], + outgoingEdgesCount: [], + blockInstructionsStart: [], + blockInstructionsCount: [], + blockInstructionsList: [], + }; + + return graph; +} + +export function createBasicBlock(graph, blockType) { + const i = graph.nextID++; + graph.blockTypes.push(blockType), + graph.incomingEdges.push(graph.incomingEdges.length); + graph.incomingEdgesCount.push(0); + graph.outgoingEdges.push(graph.outgoingEdges.length); + graph.outgoingEdges.push(0); + return i; +} + + +export function addEdge(graph, from, to) { + graph.incomingEdges.push(from); + graph.outgoingEdges.push(to); + graph.outgoingEdgesCount[from]++; + graph.incomingEdgesCount[to]++; +} \ No newline at end of file diff --git a/src/strands/DAG.js b/src/strands/DAG.js new file mode 100644 index 0000000000..0090971841 --- /dev/null +++ b/src/strands/DAG.js @@ -0,0 +1,109 @@ +import { NodeTypeRequiredFields, NodeType, NodeTypeToName } from './utils' +import * as FES from './strands_FES' + +// Properties of the Directed Acyclic Graph and its nodes +const graphProperties = [ + 'nodeTypes', + 'dataTypes', + 'opCodes', + 'values', + 'identifiers', + // sparse adjancey list for dependencies (indegree) + 'dependsOnStart', + 'dependsOnCount', + 'dependsOnList', + // sparse adjacency list for phi inputs + 'phiBlocksStart', + 'phiBlocksCount', + 'phiBlocksList' +]; + +const nodeProperties = [ + 'nodeType', + 'dataType', + 'opCode', + 'value', + 'identifier', + 'dependsOn', +]; + +// Public functions for for strands runtime +export function createDirectedAcyclicGraph() { + const graph = { + nextID: 0, + cache: new Map(), + } + for (const prop of graphProperties) { + graph[prop] = []; + } + return graph; +} + +export function getOrCreateNode(graph, node) { + const key = getNodeKey(node); + const existing = graph.cache.get(key); + + if (existing !== undefined) { + return existing; + } else { + const id = createNode(graph, node); + graph.cache.set(key, id); + return id; + } +} + +export function createNodeData(data = {}) { + const node = {}; + for (const key of nodeProperties) { + node[key] = data[key] ?? NaN; + } + validateNode(node); + return node; +} + +///////////////////////////////// +// Private functions +///////////////////////////////// + +function getNodeKey(node) { + const key = JSON.stringify(node); + return key; +} + +function validateNode(node){ + const requiredFields = NodeTypeRequiredFields[node.nodeType]; + const missingFields = []; + for (const field of requiredFields) { + if (node[field] === NaN) { + missingFields.push(field); + } + } + if (missingFields.length > 0) { + FES.internalError(`[p5.strands internal error]: Missing fields ${missingFields.join(', ')} for a node type ${NodeTypeToName(node.nodeType)}`); + } +} + +function createNode(graph, node) { + const id = graph.nextID++; + + for (const prop of nodeProperties) { + if (prop === 'dependsOn' || 'phiBlocks') { + continue; + } + + const plural = prop + 's'; + graph[plural][id] = node[prop]; + } + + const depends = Array.isArray(node.dependsOn) ? node.dependsOn : []; + graph.dependsOnStart[id] = graph.dependsOnList.length; + graph.dependsOnCount[id] = depends.length; + graph.dependsOnList.push(...depends); + + const phis = Array.isArray(node.phiBlocks) ? node.phiBlocks : []; + graph.phiBlocksStart[id] = graph.phiBlocksList.length; + graph.phiBlocksCount[id] = phis.length; + graph.phiBlocksList.push(...phis); + + return id; +} \ No newline at end of file diff --git a/src/strands/GLSL_generator.js b/src/strands/GLSL_generator.js new file mode 100644 index 0000000000..a63b93277b --- /dev/null +++ b/src/strands/GLSL_generator.js @@ -0,0 +1,5 @@ +import * as utils from './utils' + +export function generateGLSL(strandsContext) { + +} \ No newline at end of file diff --git a/src/strands/control_flow_graph.js b/src/strands/control_flow_graph.js deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/strands/directed_acyclic_graph.js b/src/strands/directed_acyclic_graph.js deleted file mode 100644 index 6da09d4921..0000000000 --- a/src/strands/directed_acyclic_graph.js +++ /dev/null @@ -1,85 +0,0 @@ -import { NodeTypeRequiredFields, NodeTypeName } from './utils' -import * as strandsFES from './strands_FES' - -// Properties of the Directed Acyclic Graph and its nodes -const graphProperties = [ - 'nodeTypes', - 'dataTypes', - 'opCodes', - 'values', - 'identifiers', - // sparse adjancey list for dependencies (indegree) - 'dependsOnStartIndex', - 'dependsOnCount', - 'dependsOnList', -]; - -const nodeProperties = [ - 'nodeType', - 'dataType', - 'opCodes', - 'value', - 'identifier', - 'dependsOn' -]; - -// Public functions for for strands runtime -export function createGraph() { - const graph = { - _nextID: 0, - _nodeCache: new Map(), - } - for (const prop of graphProperties) { - graph[prop] = []; - } - return graph; -} - - -export function getOrCreateNode(graph, node) { - const result = getNode(graph, node); - if (!result){ - return createNode(graph, node) - } else { - return result; - } -} - -export function createNodeData(data = {}) { - const node = {}; - for (const key of nodeProperties) { - node[key] = data[key] ?? NaN; - } - validateNode(node); - return node; -} - -// Private functions to this file -function getNodeKey(node) { - -} - -function validateNode(node){ - const requiredFields = NodeTypeRequiredFields[node.NodeType]; - const missingFields = []; - for (const field of requiredFields) { - if (node[field] === NaN) { - missingFields.push(field); - } - } - if (missingFields.length > 0) { - strandsFES.internalError(`[p5.strands internal error]: Missing fields ${missingFields.join(', ')} for a node type ${NodeTypeName(node.nodeType)}`); - } -} - -function getNode(graph, node) { - if (graph) - - if (!node) { - return null; - } -} - -function createNode(graph, nodeData) { - -} \ No newline at end of file diff --git a/src/strands/p5.StrandsNode.js b/src/strands/p5.StrandsNode.js deleted file mode 100644 index ffddc7e83e..0000000000 --- a/src/strands/p5.StrandsNode.js +++ /dev/null @@ -1,40 +0,0 @@ -////////////////////////////////////////////// -// User API -////////////////////////////////////////////// - -import { OperatorTable } from './utils' - -export class StrandsNode { - constructor(id) { - this.id = id; - } -} - -export function createStrandsAPI(strands, fn) { - // Attach operators to StrandsNode: - for (const { name, symbol, arity } of OperatorTable) { - if (arity === 'binary') { - StrandsNode.prototype[name] = function (rightNode) { - const id = strands.createBinaryExpressionNode(this, rightNode, symbol); - return new StrandsNode(id); - }; - } - if (arity === 'unary') { - StrandsNode.prototype[name] = function () { - const id = strands.createUnaryExpressionNode(this, symbol); - return new StrandsNode(id); - }; - } - } - - // Attach p5 Globals - fn.uniformFloat = function(name, value) { - const id = strands.createVariableNode(DataType.FLOAT, name); - return new StrandsNode(id); - }, - - fn.createFloat = function(value) { - const id = strands.createLiteralNode(DataType.FLOAT, value); - return new StrandsNode(id); - } -} \ No newline at end of file diff --git a/src/strands/p5.strands.js b/src/strands/p5.strands.js index 0bdfe7bda5..baf3496f77 100644 --- a/src/strands/p5.strands.js +++ b/src/strands/p5.strands.js @@ -6,59 +6,208 @@ */ import { transpileStrandsToJS } from './code_transpiler'; -import { DataType, NodeType, OpCode, SymbolToOpCode, OpCodeToSymbol, OpCodeArgs } from './utils'; +import { DataType, NodeType, SymbolToOpCode, OperatorTable, BlockType } from './utils'; -import { createStrandsAPI } from './p5.StrandsNode' -import * as DAG from './directed_acyclic_graph'; -import * as CFG from './control_flow_graph' -import { create } from '@davepagurek/bezier-path'; +import * as DAG from './DAG'; +import * as CFG from './CFG' function strands(p5, fn) { - ////////////////////////////////////////////// // Global Runtime ////////////////////////////////////////////// + function initStrands(ctx) { + ctx.cfg = CFG.createControlFlowGraph(); + ctx.dag = DAG.createDirectedAcyclicGraph(); + ctx.blockStack = []; + ctx.currentBlock = null; + ctx.uniforms = []; + ctx.hooks = []; + } - class StrandsRuntime { - constructor() { - this.reset(); - } - - reset() { - this._scopeStack = []; - this._allScopes = new Map(); + function deinitStrands(ctx) { + Object.keys(ctx).forEach(prop => { + delete ctx[prop]; + }); + } + + // Stubs + function overrideGlobalFunctions() {} + function restoreGlobalFunctions() {} + function overrideFES() {} + function restoreFES() {} + + ////////////////////////////////////////////// + // User nodes + ////////////////////////////////////////////// + class StrandsNode { + constructor(id) { + this.id = id; } - - createBinaryExpressionNode(left, right, operatorSymbol) { - const activeGraph = this._currentScope().graph; - const opCode = SymbolToOpCode.get(operatorSymbol); - - const dataType = DataType.FLOAT; // lookUpBinaryOperatorResult(); - return activeGraph._getOrCreateNode(NodeType.OPERATION, dataType, opCode, null, null, [left, right]); + } + + // We augment the strands node with operations programatically + // this means methods like .add, .sub, etc can be chained + for (const { name, symbol, arity } of OperatorTable) { + if (arity === 'binary') { + StrandsNode.prototype[name] = function (rightNode) { + const id = emitBinaryOp(this.id, rightNode, SymbolToOpCode[symbol]); + return new StrandsNode(id); + }; } - - createLiteralNode(dataType, value) { - const activeGraph = this._currentScope().graph; - return activeGraph._getOrCreateNode(NodeType.LITERAL, dataType, value, null, null, null); + if (arity === 'unary') { + StrandsNode.prototype[name] = function () { + const id = NaN; //createUnaryExpressionNode(this, SymbolToOpCode[symbol]); + return new StrandsNode(id); + }; } } ////////////////////////////////////////////// // Entry Point ////////////////////////////////////////////// + const strandsContext = {}; + initStrands(strandsContext); - const strands = new StrandsRuntime(); - const API = createStrandsAPI(strands, fn); + function recordInBlock(blockID, nodeID) { + const graph = strandsContext.cfg + if (graph.blockInstructionsCount[blockID] === undefined) { + graph.blockInstructionsStart[blockID] = graph.blockInstructionsList.length; + graph.blockInstructionsCount[blockID] = 0; + } + graph.blockInstructionsList.push(nodeID); + graph.blockInstructionsCount[blockID] += 1; + } + + function emitLiteralNode(dataType, value) { + const nodeData = DAG.createNodeData({ + nodeType: NodeType.LITERAL, + dataType, + value + }); + const id = DAG.getOrCreateNode(strandsContext.dag, nodeData); + const b = strandsContext.currentBlock; + recordInBlock(strandsContext.currentBlock, id); + return id; + } - const oldModify = p5.Shader.prototype.modify + function emitBinaryOp(left, right, opCode) { + const nodeData = DAG.createNodeData({ + nodeType: NodeType.OPERATION, + dependsOn: [left, right], + opCode + }); + const id = DAG.getOrCreateNode(strandsContext.dag, nodeData); + recordInBlock(strandsContext.currentBlock, id); + return id; + } + + function emitVariableNode(dataType, identifier) { + const nodeData = DAG.createNodeData({ + nodeType: NodeType.VARIABLE, + dataType, + identifier + }) + const id = DAG.getOrCreateNode(strandsContext.dag, nodeData); + recordInBlock(strandsContext.currentBlock, id); + return id; + } + + function enterBlock(blockID) { + if (strandsContext.currentBlock) { + CFG.addEdge(strandsContext.cfg, strandsContext.currentBlock, blockID); + } + strandsContext.currentBlock = blockID; + strandsContext.blockStack.push(blockID); + } + + function exitBlock() { + strandsContext.blockStack.pop(); + strandsContext.currentBlock = strandsContext.blockStack[strandsContext.blockStack-1]; + } - for (const [fnName, fnBody] of Object.entries(userFunctions)) { - fn[fnName] = fnBody; + fn.uniformFloat = function(name, defaultValue) { + const id = emitVariableNode(DataType.FLOAT, name); + strandsContext.uniforms.push({ name, dataType: DataType.FLOAT, defaultValue }); + return new StrandsNode(id); + } + + fn.createFloat = function(value) { + const id = emitLiteralNode(DataType.FLOAT, value); + return new StrandsNode(id); + } + + fn.strandsIf = function(condition, ifBody, elseBody) { + const conditionBlock = CFG.createBasicBlock(strandsContext.cfg, BlockType.IF_COND); + enterBlock(conditionBlock); + + const trueBlock = CFG.createBasicBlock(strandsContext.cfg, BlockType.IF); + enterBlock(trueBlock); + ifBody(); + exitBlock(); + + const mergeBlock = CFG.createBasicBlock(strandsContext.cfg, BlockType.MERGE); + enterBlock(mergeBlock); + } + + function createHookArguments(parameters){ + const structTypes = ['Vertex', ] + const args = []; + + for (const param of parameters) { + const T = param.type; + if(structTypes.includes(T.typeName)) { + const propertiesNodes = T.properties.map( + (prop) => [prop.name, emitVariableNode(DataType[prop.dataType], prop.name)] + ); + const argObj = Object.fromEntries(propertiesNodes); + args.push(argObj); + } else { + const arg = emitVariableNode(DataType[param.dataType], param.name); + args.push(arg) + } + } + return args; } + function generateHookOverrides(shader) { + const availableHooks = { + ...shader.hooks.vertex, + ...shader.hooks.fragment, + } + const hookTypes = Object.keys(availableHooks).map(name => shader.hookTypes(name)); + + for (const hookType of hookTypes) { + window[hookType.name] = function(callback) { + const funcBlock = CFG.createBasicBlock(strandsContext.cfg, BlockType.FUNCTION); + enterBlock(funcBlock); + const args = createHookArguments(hookType.parameters); + console.log(hookType, args); + runHook(hookType, callback, args); + exitBlock(); + } + } + } + + function runHook(hookType, callback, inputs) { + const blockID = CFG.createBasicBlock(strandsContext.cfg, BlockType.FUNCTION) + + enterBlock(blockID); + const rootNode = callback(inputs); + exitBlock(); + + strandsContext.hooks.push({ + hookType, + blockID, + rootNode, + }); + } + + const oldModify = p5.Shader.prototype.modify p5.Shader.prototype.newModify = function(shaderModifier, options = { parser: true, srcLocations: false }) { if (shaderModifier instanceof Function) { - + // Reset the context object every time modify is called; + initStrands(strandsContext) + generateHookOverrides(this); // 1. Transpile from strands DSL to JS let strandsCallback; if (options.parser) { @@ -68,19 +217,22 @@ function strands(p5, fn) { } // 2. Build the IR from JavaScript API - strands.enterScope('GLOBAL'); + const globalScope = CFG.createBasicBlock(strandsContext.cfg, BlockType.GLOBAL); + enterBlock(globalScope); strandsCallback(); - strands.exitScope('GLOBAL'); - + exitBlock(); // 3. Generate shader code hooks object from the IR // ....... - + for (const {hookType, blockID, rootNode} of strandsContext.hooks) { + // console.log(hookType); + } + // Call modify with the generated hooks object // return oldModify.call(this, generatedModifyArgument); // Reset the strands runtime context - // strands.reset(); + // deinitStrands(strandsContext); } else { return oldModify.call(this, shaderModifier) diff --git a/src/strands/utils.js b/src/strands/utils.js index 29a3e1d1ab..a5bdeef355 100644 --- a/src/strands/utils.js +++ b/src/strands/utils.js @@ -9,16 +9,18 @@ export const NodeType = { LITERAL: 1, VARIABLE: 2, CONSTANT: 3, + PHI: 4, }; export const NodeTypeRequiredFields = { - [NodeType.OPERATION]: ['opCodes', 'dependsOn'], - [NodeType.LITERAL]: ['values'], - [NodeType.VARIABLE]: ['identifiers'], - [NodeType.CONSTANT]: ['values'], + [NodeType.OPERATION]: ['opCode', 'dependsOn'], + [NodeType.LITERAL]: ['value'], + [NodeType.VARIABLE]: ['identifier', 'dataType'], + [NodeType.CONSTANT]: ['value'], + [NodeType.PHI]: ['dependsOn', 'phiBlocks'] }; -export const NodeTypeName = Object.fromEntries( +export const NodeTypeToName = Object.fromEntries( Object.entries(NodeType).map(([key, val]) => [val, key]) ); @@ -105,5 +107,13 @@ for (const { arity: args, symbol, opcode } of OperatorTable) { SymbolToOpCode[symbol] = opcode; OpCodeToSymbol[opcode] = symbol; OpCodeArgs[opcode] = args; - +} + +export const BlockType = { + GLOBAL: 0, + IF: 1, + ELSE_IF: 2, + ELSE: 3, + FOR: 4, + MERGE: 5, } \ No newline at end of file From 06faa2c087192f560548fae0f8857b88439387a1 Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Sat, 5 Jul 2025 12:27:49 +0100 Subject: [PATCH 30/69] nested ifs --- preview/global/sketch.js | 21 +++++- src/strands/CFG.js | 50 +++++++------ src/strands/DAG.js | 119 +++++++++++++++---------------- src/strands/GLSL_generator.js | 122 ++++++++++++++++++++++++++++++- src/strands/p5.strands.js | 130 ++++++++++++++++------------------ src/strands/utils.js | 24 +++++++ 6 files changed, 308 insertions(+), 158 deletions(-) diff --git a/preview/global/sketch.js b/preview/global/sketch.js index ec77fd8c0e..772c8b8c7c 100644 --- a/preview/global/sketch.js +++ b/preview/global/sketch.js @@ -1,10 +1,25 @@ p5.disableFriendlyErrors = true; function callback() { - let x = createFloat(1.0); + // let x = createFloat(1.0); + getFinalColor((col) => { - return x; - }) + let y = createFloat(10); + let x = y.add(y); + + strandsIf(x.greaterThan(createFloat(0.0)), () => { + x = createFloat(20); + strandsIf(x.greaterThan(createFloat(0.0)), () => { + x = createFloat(20); + }); + }); + strandsIf(x.greaterThan(createFloat(0.0)), () => { + x = createFloat(20); + }); + const z = createFloat(200); + + return x.add(z); + }); } async function setup(){ diff --git a/src/strands/CFG.js b/src/strands/CFG.js index 28b4007e9c..5b8fa9ac96 100644 --- a/src/strands/CFG.js +++ b/src/strands/CFG.js @@ -1,35 +1,39 @@ export function createControlFlowGraph() { - const graph = { + return { nextID: 0, + graphType: 'CFG', blockTypes: [], - incomingEdges:[], - incomingEdgesIndex: [], - incomingEdgesCount: [], + incomingEdges: [], outgoingEdges: [], - outgoingEdgesIndex: [], - outgoingEdgesCount: [], - blockInstructionsStart: [], - blockInstructionsCount: [], - blockInstructionsList: [], + blockInstructions: [], }; - - return graph; } export function createBasicBlock(graph, blockType) { - const i = graph.nextID++; - graph.blockTypes.push(blockType), - graph.incomingEdges.push(graph.incomingEdges.length); - graph.incomingEdgesCount.push(0); - graph.outgoingEdges.push(graph.outgoingEdges.length); - graph.outgoingEdges.push(0); - return i; + const id = graph.nextID++; + graph.blockTypes[id] = blockType; + graph.incomingEdges[id] = []; + graph.outgoingEdges[id] = []; + graph.blockInstructions[id]= []; + return id; } - export function addEdge(graph, from, to) { - graph.incomingEdges.push(from); - graph.outgoingEdges.push(to); - graph.outgoingEdgesCount[from]++; - graph.incomingEdgesCount[to]++; + graph.outgoingEdges[from].push(to); + graph.incomingEdges[to].push(from); +} + +export function recordInBasicBlock(graph, blockID, nodeID) { + graph.blockInstructions[blockID] = graph.blockInstructions[blockID] || []; + graph.blockInstructions[blockID].push(nodeID); +} + +export function getBlockDataFromID(graph, id) { + return { + id, + blockType: graph.blockTypes[id], + incomingEdges: graph.incomingEdges[id], + outgoingEdges: graph.outgoingEdges[id], + blockInstructions: graph.blockInstructions[id], + } } \ No newline at end of file diff --git a/src/strands/DAG.js b/src/strands/DAG.js index 0090971841..b095fe3efc 100644 --- a/src/strands/DAG.js +++ b/src/strands/DAG.js @@ -1,48 +1,32 @@ -import { NodeTypeRequiredFields, NodeType, NodeTypeToName } from './utils' +import { NodeTypeRequiredFields, NodeTypeToName } from './utils' import * as FES from './strands_FES' -// Properties of the Directed Acyclic Graph and its nodes -const graphProperties = [ - 'nodeTypes', - 'dataTypes', - 'opCodes', - 'values', - 'identifiers', - // sparse adjancey list for dependencies (indegree) - 'dependsOnStart', - 'dependsOnCount', - 'dependsOnList', - // sparse adjacency list for phi inputs - 'phiBlocksStart', - 'phiBlocksCount', - 'phiBlocksList' -]; - -const nodeProperties = [ - 'nodeType', - 'dataType', - 'opCode', - 'value', - 'identifier', - 'dependsOn', -]; - +///////////////////////////////// // Public functions for for strands runtime +///////////////////////////////// + export function createDirectedAcyclicGraph() { - const graph = { - nextID: 0, + const graph = { + nextID: 0, cache: new Map(), - } - for (const prop of graphProperties) { - graph[prop] = []; - } + nodeTypes: [], + dataTypes: [], + opCodes: [], + values: [], + identifiers: [], + phiBlocks: [], + dependsOn: [], + usedBy: [], + graphType: 'DAG', + }; + return graph; } export function getOrCreateNode(graph, node) { const key = getNodeKey(node); const existing = graph.cache.get(key); - + if (existing !== undefined) { return existing; } else { @@ -53,17 +37,51 @@ export function getOrCreateNode(graph, node) { } export function createNodeData(data = {}) { - const node = {}; - for (const key of nodeProperties) { - node[key] = data[key] ?? NaN; - } + const node = { + nodeType: data.nodeType ?? null, + dataType: data.dataType ?? null, + opCode: data.opCode ?? null, + value: data.value ?? null, + identifier: data.identifier ?? null, + dependsOn: Array.isArray(data.dependsOn) ? data.dependsOn : [], + usedBy: Array.isArray(data.usedBy) ? data.usedBy : [], + phiBlocks: Array.isArray(data.phiBlocks) ? data.phiBlocks : [] + }; validateNode(node); return node; } +export function getNodeDataFromID(graph, id) { + return { + nodeType: graph.nodeTypes[id], + dataType: graph.dataTypes[id], + opCode: graph.opCodes[id], + value: graph.values[id], + identifier: graph.identifiers[id], + dependsOn: graph.dependsOn[id], + usedBy: graph.usedBy[id], + phiBlocks: graph.phiBlocks[id], + } +} + ///////////////////////////////// // Private functions ///////////////////////////////// +function createNode(graph, node) { + const id = graph.nextID++; + graph.nodeTypes[id] = node.nodeType; + graph.dataTypes[id] = node.dataType; + graph.opCodes[id] = node.opCode; + graph.values[id] = node.value; + graph.identifiers[id] = node.identifier; + graph.dependsOn[id] = node.dependsOn.slice(); + graph.usedBy[id] = node.usedBy; + graph.phiBlocks[id] = node.phiBlocks.slice(); + for (const dep of node.dependsOn) { + graph.usedBy[dep].push(id); + } + return id; +} function getNodeKey(node) { const key = JSON.stringify(node); @@ -81,29 +99,4 @@ function validateNode(node){ if (missingFields.length > 0) { FES.internalError(`[p5.strands internal error]: Missing fields ${missingFields.join(', ')} for a node type ${NodeTypeToName(node.nodeType)}`); } -} - -function createNode(graph, node) { - const id = graph.nextID++; - - for (const prop of nodeProperties) { - if (prop === 'dependsOn' || 'phiBlocks') { - continue; - } - - const plural = prop + 's'; - graph[plural][id] = node[prop]; - } - - const depends = Array.isArray(node.dependsOn) ? node.dependsOn : []; - graph.dependsOnStart[id] = graph.dependsOnList.length; - graph.dependsOnCount[id] = depends.length; - graph.dependsOnList.push(...depends); - - const phis = Array.isArray(node.phiBlocks) ? node.phiBlocks : []; - graph.phiBlocksStart[id] = graph.phiBlocksList.length; - graph.phiBlocksCount[id] = phis.length; - graph.phiBlocksList.push(...phis); - - return id; } \ No newline at end of file diff --git a/src/strands/GLSL_generator.js b/src/strands/GLSL_generator.js index a63b93277b..488510a27f 100644 --- a/src/strands/GLSL_generator.js +++ b/src/strands/GLSL_generator.js @@ -1,5 +1,125 @@ -import * as utils from './utils' +import { dfsPostOrder, NodeType, OpCodeToSymbol, BlockType } from "./utils"; +import { getNodeDataFromID } from "./DAG"; +import { getBlockDataFromID } from "./CFG"; + +let globalTempCounter = 0; + +function nodeToGLSL(dag, nodeID, hookContext) { + const node = getNodeDataFromID(dag, nodeID); + if (hookContext.tempName?.[nodeID]) { + return hookContext.tempName[nodeID]; + } + switch (node.nodeType) { + case NodeType.LITERAL: + return node.value.toFixed(4); + + case NodeType.VARIABLE: + return node.identifier; + + case NodeType.OPERATION: + const [lID, rID] = node.dependsOn; + const left = nodeToGLSL(dag, lID, hookContext); + const right = nodeToGLSL(dag, rID, hookContext); + const opSym = OpCodeToSymbol[node.opCode]; + return `(${left} ${opSym} ${right})`; + + default: + throw new Error(`${node.nodeType} not working yet`); + } +} + +function computeDeclarations(dag, dagOrder) { + const usedCount = {}; + for (const nodeID of dagOrder) { + usedCount[nodeID] = (dag.usedBy[nodeID] || []).length; + } + + const tempName = {}; + const declarations = []; + + for (const nodeID of dagOrder) { + if (dag.nodeTypes[nodeID] !== NodeType.OPERATION) { + continue; + } + + if (usedCount[nodeID] > 1) { + const tmp = `t${globalTempCounter++}`; + tempName[nodeID] = tmp; + + const expr = nodeToGLSL(dag, nodeID, {}); + declarations.push(`float ${tmp} = ${expr};`); + } + } + + return { declarations, tempName }; +} + +const cfgHandlers = { + Condition(strandsContext, hookContext) { + const conditionID = strandsContext.blockConditions[blockID]; + const condExpr = nodeToGLSL(dag, conditionID, hookContext); + write(`if (${condExpr}) {`) + indent++; + return; + } +} export function generateGLSL(strandsContext) { + const hooksObj = {}; + + for (const { hookType, entryBlockID, rootNodeID} of strandsContext.hooks) { + const { cfg, dag } = strandsContext; + const dagSorted = dfsPostOrder(dag.dependsOn, rootNodeID); + const cfgSorted = dfsPostOrder(cfg.outgoingEdges, entryBlockID).reverse(); + + console.log("BLOCK ORDER: ", cfgSorted.map(id => getBlockDataFromID(cfg, id))); + + const hookContext = { + ...computeDeclarations(dag, dagSorted), + indent: 0, + currentBlock: cfgSorted[0] + }; + + let indent = 0; + let nested = 1; + let codeLines = hookContext.declarations.map((decl) => pad() + decl); + const write = (line) => codeLines.push(' '.repeat(indent) + line); + + cfgSorted.forEach((blockID, i) => { + const type = cfg.blockTypes[blockID]; + const nextID = cfgSorted[i + 1]; + const nextType = cfg.blockTypes[nextID]; + + switch (type) { + case BlockType.COND: + const condID = strandsContext.blockConditions[blockID]; + const condExpr = nodeToGLSL(dag, condID, hookContext); + write(`if (${condExpr}) {`) + indent++; + return; + case BlockType.MERGE: + indent--; + write('MERGE'); + write('}'); + return; + default: + const instructions = new Set(cfg.blockInstructions[blockID] || []); + for (let nodeID of dagSorted) { + if (!instructions.has(nodeID)) { + continue; + } + const snippet = hookContext.tempName[nodeID] + ? hookContext.tempName[nodeID] + : nodeToGLSL(dag, nodeID, hookContext); + write(snippet); + } + } + }); + + const finalExpression = `return ${nodeToGLSL(dag, rootNodeID, hookContext)};`; + write(finalExpression); + hooksObj[hookType.name] = codeLines.join('\n'); + } + return hooksObj; } \ No newline at end of file diff --git a/src/strands/p5.strands.js b/src/strands/p5.strands.js index baf3496f77..f72bba9f41 100644 --- a/src/strands/p5.strands.js +++ b/src/strands/p5.strands.js @@ -10,6 +10,7 @@ import { DataType, NodeType, SymbolToOpCode, OperatorTable, BlockType } from './ import * as DAG from './DAG'; import * as CFG from './CFG' +import { generateGLSL } from './GLSL_generator'; function strands(p5, fn) { ////////////////////////////////////////////// @@ -19,7 +20,8 @@ function strands(p5, fn) { ctx.cfg = CFG.createControlFlowGraph(); ctx.dag = DAG.createDirectedAcyclicGraph(); ctx.blockStack = []; - ctx.currentBlock = null; + ctx.currentBlock = -1; + ctx.blockConditions = {}; ctx.uniforms = []; ctx.hooks = []; } @@ -50,7 +52,7 @@ function strands(p5, fn) { for (const { name, symbol, arity } of OperatorTable) { if (arity === 'binary') { StrandsNode.prototype[name] = function (rightNode) { - const id = emitBinaryOp(this.id, rightNode, SymbolToOpCode[symbol]); + const id = createBinaryOpNode(this.id, rightNode.id, SymbolToOpCode[symbol]); return new StrandsNode(id); }; } @@ -62,23 +64,7 @@ function strands(p5, fn) { } } - ////////////////////////////////////////////// - // Entry Point - ////////////////////////////////////////////// - const strandsContext = {}; - initStrands(strandsContext); - - function recordInBlock(blockID, nodeID) { - const graph = strandsContext.cfg - if (graph.blockInstructionsCount[blockID] === undefined) { - graph.blockInstructionsStart[blockID] = graph.blockInstructionsList.length; - graph.blockInstructionsCount[blockID] = 0; - } - graph.blockInstructionsList.push(nodeID); - graph.blockInstructionsCount[blockID] += 1; - } - - function emitLiteralNode(dataType, value) { + function createLiteralNode(dataType, value) { const nodeData = DAG.createNodeData({ nodeType: NodeType.LITERAL, dataType, @@ -86,67 +72,82 @@ function strands(p5, fn) { }); const id = DAG.getOrCreateNode(strandsContext.dag, nodeData); const b = strandsContext.currentBlock; - recordInBlock(strandsContext.currentBlock, id); + CFG.recordInBasicBlock(strandsContext.cfg, strandsContext.currentBlock, id); return id; } - function emitBinaryOp(left, right, opCode) { + function createBinaryOpNode(left, right, opCode) { const nodeData = DAG.createNodeData({ nodeType: NodeType.OPERATION, dependsOn: [left, right], opCode }); const id = DAG.getOrCreateNode(strandsContext.dag, nodeData); - recordInBlock(strandsContext.currentBlock, id); + CFG.recordInBasicBlock(strandsContext.cfg, strandsContext.currentBlock, id); return id; } - function emitVariableNode(dataType, identifier) { + function createVariableNode(dataType, identifier) { const nodeData = DAG.createNodeData({ nodeType: NodeType.VARIABLE, dataType, identifier }) const id = DAG.getOrCreateNode(strandsContext.dag, nodeData); - recordInBlock(strandsContext.currentBlock, id); + CFG.recordInBasicBlock(strandsContext.cfg, strandsContext.currentBlock, id); return id; } - function enterBlock(blockID) { - if (strandsContext.currentBlock) { - CFG.addEdge(strandsContext.cfg, strandsContext.currentBlock, blockID); - } - strandsContext.currentBlock = blockID; + function pushBlockWithEdgeFromCurrent(blockID) { + CFG.addEdge(strandsContext.cfg, strandsContext.currentBlock, blockID); + pushBlock(blockID); + } + + function pushBlock(blockID) { strandsContext.blockStack.push(blockID); + strandsContext.currentBlock = blockID; } - function exitBlock() { + function popBlock() { strandsContext.blockStack.pop(); - strandsContext.currentBlock = strandsContext.blockStack[strandsContext.blockStack-1]; + const len = strandsContext.blockStack.length; + strandsContext.currentBlock = strandsContext.blockStack[len-1]; } fn.uniformFloat = function(name, defaultValue) { - const id = emitVariableNode(DataType.FLOAT, name); + const id = createVariableNode(DataType.FLOAT, name); strandsContext.uniforms.push({ name, dataType: DataType.FLOAT, defaultValue }); return new StrandsNode(id); } fn.createFloat = function(value) { - const id = emitLiteralNode(DataType.FLOAT, value); + const id = createLiteralNode(DataType.FLOAT, value); return new StrandsNode(id); } - fn.strandsIf = function(condition, ifBody, elseBody) { - const conditionBlock = CFG.createBasicBlock(strandsContext.cfg, BlockType.IF_COND); - enterBlock(conditionBlock); + fn.strandsIf = function(conditionNode, ifBody) { + const { cfg } = strandsContext; + + const conditionBlock = CFG.createBasicBlock(cfg, BlockType.COND); + pushBlockWithEdgeFromCurrent(conditionBlock); + strandsContext.blockConditions[conditionBlock] = conditionNode.id; - const trueBlock = CFG.createBasicBlock(strandsContext.cfg, BlockType.IF); - enterBlock(trueBlock); + const thenBlock = CFG.createBasicBlock(cfg, BlockType.IF); + pushBlockWithEdgeFromCurrent(thenBlock); ifBody(); - exitBlock(); - const mergeBlock = CFG.createBasicBlock(strandsContext.cfg, BlockType.MERGE); - enterBlock(mergeBlock); + const mergeBlock = CFG.createBasicBlock(cfg, BlockType.MERGE); + if (strandsContext.currentBlock !== thenBlock) { + const nestedBlock = strandsContext.currentBlock; + CFG.addEdge(cfg, nestedBlock, mergeBlock); + // Pop the previous merge! + popBlock(); + } + // Pop the thenBlock after checking + popBlock(); + + pushBlock(mergeBlock); + CFG.addEdge(cfg, conditionBlock, mergeBlock); } function createHookArguments(parameters){ @@ -157,12 +158,12 @@ function strands(p5, fn) { const T = param.type; if(structTypes.includes(T.typeName)) { const propertiesNodes = T.properties.map( - (prop) => [prop.name, emitVariableNode(DataType[prop.dataType], prop.name)] + (prop) => [prop.name, createVariableNode(DataType[prop.dataType], prop.name)] ); const argObj = Object.fromEntries(propertiesNodes); args.push(argObj); } else { - const arg = emitVariableNode(DataType[param.dataType], param.name); + const arg = createVariableNode(DataType[param.dataType], param.name); args.push(arg) } } @@ -175,34 +176,28 @@ function strands(p5, fn) { ...shader.hooks.fragment, } const hookTypes = Object.keys(availableHooks).map(name => shader.hookTypes(name)); - for (const hookType of hookTypes) { window[hookType.name] = function(callback) { - const funcBlock = CFG.createBasicBlock(strandsContext.cfg, BlockType.FUNCTION); - enterBlock(funcBlock); + const entryBlockID = CFG.createBasicBlock(strandsContext.cfg, BlockType.FUNCTION); + pushBlockWithEdgeFromCurrent(entryBlockID); const args = createHookArguments(hookType.parameters); - console.log(hookType, args); - runHook(hookType, callback, args); - exitBlock(); + const rootNodeID = callback(args).id; + strandsContext.hooks.push({ + hookType, + entryBlockID, + rootNodeID, + }); + popBlock(); } } } - - function runHook(hookType, callback, inputs) { - const blockID = CFG.createBasicBlock(strandsContext.cfg, BlockType.FUNCTION) - - enterBlock(blockID); - const rootNode = callback(inputs); - exitBlock(); - - strandsContext.hooks.push({ - hookType, - blockID, - rootNode, - }); - } + ////////////////////////////////////////////// + // Entry Point + ////////////////////////////////////////////// + const strandsContext = {}; const oldModify = p5.Shader.prototype.modify + p5.Shader.prototype.newModify = function(shaderModifier, options = { parser: true, srcLocations: false }) { if (shaderModifier instanceof Function) { // Reset the context object every time modify is called; @@ -218,15 +213,14 @@ function strands(p5, fn) { // 2. Build the IR from JavaScript API const globalScope = CFG.createBasicBlock(strandsContext.cfg, BlockType.GLOBAL); - enterBlock(globalScope); + pushBlock(globalScope); strandsCallback(); - exitBlock(); + popBlock(); // 3. Generate shader code hooks object from the IR // ....... - for (const {hookType, blockID, rootNode} of strandsContext.hooks) { - // console.log(hookType); - } + const glsl = generateGLSL(strandsContext); + console.log(glsl.getFinalColor); // Call modify with the generated hooks object // return oldModify.call(this, generatedModifyArgument); diff --git a/src/strands/utils.js b/src/strands/utils.js index a5bdeef355..2b2ee88621 100644 --- a/src/strands/utils.js +++ b/src/strands/utils.js @@ -116,4 +116,28 @@ export const BlockType = { ELSE: 3, FOR: 4, MERGE: 5, + COND: 6, + FUNCTION: 7 +} + +//////////////////////////// +// Graph utils +//////////////////////////// +export function dfsPostOrder(adjacencyList, start) { + const visited = new Set(); + const postOrder = []; + + function dfs(v) { + if (visited.has(v)) { + return; + } + visited.add(v); + for (let w of adjacencyList[v] || []) { + dfs(w); + } + postOrder.push(v); + } + + dfs(start); + return postOrder; } \ No newline at end of file From 5d320898ef2f8db423a1638952051d08f99951cc Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Mon, 7 Jul 2025 12:39:15 +0100 Subject: [PATCH 31/69] if/else semi working --- preview/global/sketch.js | 15 +++--------- src/strands/GLSL_generator.js | 43 ++++++++++++++++++++--------------- src/strands/p5.strands.js | 35 +++++++++++++++++----------- src/strands/utils.js | 41 +++++++++++++++++++++++++-------- 4 files changed, 82 insertions(+), 52 deletions(-) diff --git a/preview/global/sketch.js b/preview/global/sketch.js index 772c8b8c7c..486d553d6f 100644 --- a/preview/global/sketch.js +++ b/preview/global/sketch.js @@ -1,24 +1,15 @@ p5.disableFriendlyErrors = true; function callback() { - // let x = createFloat(1.0); getFinalColor((col) => { - let y = createFloat(10); - let x = y.add(y); + let x = createFloat(2.5); strandsIf(x.greaterThan(createFloat(0.0)), () => { - x = createFloat(20); - strandsIf(x.greaterThan(createFloat(0.0)), () => { - x = createFloat(20); - }); + x = createFloat(100); }); - strandsIf(x.greaterThan(createFloat(0.0)), () => { - x = createFloat(20); - }); - const z = createFloat(200); - return x.add(z); + return x; }); } diff --git a/src/strands/GLSL_generator.js b/src/strands/GLSL_generator.js index 488510a27f..400789cf43 100644 --- a/src/strands/GLSL_generator.js +++ b/src/strands/GLSL_generator.js @@ -1,4 +1,4 @@ -import { dfsPostOrder, NodeType, OpCodeToSymbol, BlockType } from "./utils"; +import { dfsPostOrder, NodeType, OpCodeToSymbol, BlockType, OpCodeToOperation, BlockTypeToName } from "./utils"; import { getNodeDataFromID } from "./DAG"; import { getBlockDataFromID } from "./CFG"; @@ -18,6 +18,10 @@ function nodeToGLSL(dag, nodeID, hookContext) { case NodeType.OPERATION: const [lID, rID] = node.dependsOn; + // if (dag.nodeTypes[lID] === NodeType.LITERAL && dag.nodeTypes[lID] === dag.nodeTypes[rID]) { + // const constantFolded = OpCodeToOperation[dag.opCodes[nodeID]](dag.values[lID], dag.values[rID]); + // if (!(constantFolded === undefined)) return constantFolded; + // } const left = nodeToGLSL(dag, lID, hookContext); const right = nodeToGLSL(dag, rID, hookContext); const opSym = OpCodeToSymbol[node.opCode]; @@ -34,9 +38,8 @@ function computeDeclarations(dag, dagOrder) { usedCount[nodeID] = (dag.usedBy[nodeID] || []).length; } - const tempName = {}; + const tempNames = {}; const declarations = []; - for (const nodeID of dagOrder) { if (dag.nodeTypes[nodeID] !== NodeType.OPERATION) { continue; @@ -44,14 +47,14 @@ function computeDeclarations(dag, dagOrder) { if (usedCount[nodeID] > 1) { const tmp = `t${globalTempCounter++}`; - tempName[nodeID] = tmp; + tempNames[nodeID] = tmp; const expr = nodeToGLSL(dag, nodeID, {}); declarations.push(`float ${tmp} = ${expr};`); } } - return { declarations, tempName }; + return { declarations, tempNames }; } const cfgHandlers = { @@ -72,44 +75,48 @@ export function generateGLSL(strandsContext) { const dagSorted = dfsPostOrder(dag.dependsOn, rootNodeID); const cfgSorted = dfsPostOrder(cfg.outgoingEdges, entryBlockID).reverse(); - console.log("BLOCK ORDER: ", cfgSorted.map(id => getBlockDataFromID(cfg, id))); + console.log("BLOCK ORDER: ", cfgSorted.map(id => { + const node = getBlockDataFromID(cfg, id); + node.blockType = BlockTypeToName[node.blockType]; + return node; + } + )); const hookContext = { ...computeDeclarations(dag, dagSorted), indent: 0, - currentBlock: cfgSorted[0] }; let indent = 0; - let nested = 1; let codeLines = hookContext.declarations.map((decl) => pad() + decl); const write = (line) => codeLines.push(' '.repeat(indent) + line); - cfgSorted.forEach((blockID, i) => { + cfgSorted.forEach((blockID) => { const type = cfg.blockTypes[blockID]; - const nextID = cfgSorted[i + 1]; - const nextType = cfg.blockTypes[nextID]; - switch (type) { - case BlockType.COND: + case BlockType.CONDITION: const condID = strandsContext.blockConditions[blockID]; const condExpr = nodeToGLSL(dag, condID, hookContext); write(`if (${condExpr}) {`) indent++; return; + // case BlockType.ELSE_BODY: + // write('else {'); + // indent++; + // return; case BlockType.MERGE: indent--; - write('MERGE'); write('}'); return; default: - const instructions = new Set(cfg.blockInstructions[blockID] || []); + const blockInstructions = new Set(cfg.blockInstructions[blockID] || []); + console.log(blockID, blockInstructions); for (let nodeID of dagSorted) { - if (!instructions.has(nodeID)) { + if (!blockInstructions.has(nodeID)) { continue; } - const snippet = hookContext.tempName[nodeID] - ? hookContext.tempName[nodeID] + const snippet = hookContext.tempNames[nodeID] + ? hookContext.tempNames[nodeID] : nodeToGLSL(dag, nodeID, hookContext); write(snippet); } diff --git a/src/strands/p5.strands.js b/src/strands/p5.strands.js index f72bba9f41..b3bc462d61 100644 --- a/src/strands/p5.strands.js +++ b/src/strands/p5.strands.js @@ -125,29 +125,38 @@ function strands(p5, fn) { return new StrandsNode(id); } - fn.strandsIf = function(conditionNode, ifBody) { - const { cfg } = strandsContext; + fn.strandsIf = function(conditionNode, ifBody, elseBody) { + const { cfg } = strandsContext; + const mergeBlock = CFG.createBasicBlock(cfg, BlockType.MERGE); - const conditionBlock = CFG.createBasicBlock(cfg, BlockType.COND); + const conditionBlock = CFG.createBasicBlock(cfg, BlockType.CONDITION); pushBlockWithEdgeFromCurrent(conditionBlock); strandsContext.blockConditions[conditionBlock] = conditionNode.id; - const thenBlock = CFG.createBasicBlock(cfg, BlockType.IF); - pushBlockWithEdgeFromCurrent(thenBlock); + const ifBodyBlock = CFG.createBasicBlock(cfg, BlockType.IF_BODY); + pushBlockWithEdgeFromCurrent(ifBodyBlock); ifBody(); - - const mergeBlock = CFG.createBasicBlock(cfg, BlockType.MERGE); - if (strandsContext.currentBlock !== thenBlock) { - const nestedBlock = strandsContext.currentBlock; - CFG.addEdge(cfg, nestedBlock, mergeBlock); - // Pop the previous merge! + if (strandsContext.currentBlock !== ifBodyBlock) { + CFG.addEdge(cfg, strandsContext.currentBlock, mergeBlock); popBlock(); } - // Pop the thenBlock after checking + popBlock(); + + const elseBodyBlock = CFG.createBasicBlock(cfg, BlockType.ELSE_BODY); + pushBlock(elseBodyBlock); + CFG.addEdge(cfg, conditionBlock, elseBodyBlock); + if (elseBody) { + elseBody(); + if (strandsContext.currentBlock !== ifBodyBlock) { + CFG.addEdge(cfg, strandsContext.currentBlock, mergeBlock); + popBlock(); + } + } popBlock(); pushBlock(mergeBlock); - CFG.addEdge(cfg, conditionBlock, mergeBlock); + CFG.addEdge(cfg, elseBodyBlock, mergeBlock); + CFG.addEdge(cfg, ifBodyBlock, mergeBlock); } function createHookArguments(parameters){ diff --git a/src/strands/utils.js b/src/strands/utils.js index 2b2ee88621..66ed42c03f 100644 --- a/src/strands/utils.js +++ b/src/strands/utils.js @@ -98,27 +98,50 @@ export const OperatorTable = [ { arity: "binary", name: "or", symbol: "||", opcode: OpCode.Binary.LOGICAL_OR }, ]; +const BinaryOperations = { + "+": (a, b) => a + b, + "-": (a, b) => a - b, + "*": (a, b) => a * b, + "/": (a, b) => a / b, + "%": (a, b) => a % b, + "==": (a, b) => a == b, + "!=": (a, b) => a != b, + ">": (a, b) => a > b, + ">=": (a, b) => a >= b, + "<": (a, b) => a < b, + "<=": (a, b) => a <= b, + "&&": (a, b) => a && b, + "||": (a, b) => a || b, +}; + export const SymbolToOpCode = {}; export const OpCodeToSymbol = {}; export const OpCodeArgs = {}; +export const OpCodeToOperation = {}; -for (const { arity: args, symbol, opcode } of OperatorTable) { +for (const { arity, symbol, opcode } of OperatorTable) { SymbolToOpCode[symbol] = opcode; OpCodeToSymbol[opcode] = symbol; OpCodeArgs[opcode] = args; + if (arity === "binary" && BinaryOperations[symbol]) { + OpCodeToOperation[opcode] = BinaryOperations[symbol]; + } } export const BlockType = { GLOBAL: 0, - IF: 1, - ELSE_IF: 2, - ELSE: 3, - FOR: 4, - MERGE: 5, - COND: 6, - FUNCTION: 7 + FUNCTION: 1, + IF_BODY: 2, + ELSE_BODY: 3, + EL_IF_BODY: 4, + CONDITION: 5, + FOR: 6, + MERGE: 7, } +export const BlockTypeToName = Object.fromEntries( + Object.entries(BlockType).map(([key, val]) => [val, key]) +); //////////////////////////// // Graph utils @@ -132,7 +155,7 @@ export function dfsPostOrder(adjacencyList, start) { return; } visited.add(v); - for (let w of adjacencyList[v] || []) { + for (let w of adjacencyList[v].sort((a, b) => b-a) || []) { dfs(w); } postOrder.push(v); From 95fa41006adedb49c3cc18e3a4691bc7934a818c Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Wed, 9 Jul 2025 18:16:37 +0100 Subject: [PATCH 32/69] change if/elseif/else api to be chainable and functional (return assignments) --- preview/global/sketch.js | 10 +- src/strands/CFG.js | 8 ++ src/strands/GLSL_generator.js | 9 -- src/strands/p5.strands.js | 145 ++++++++++++++++++++++------ src/strands/strands_conditionals.js | 61 ++++++++++++ 5 files changed, 192 insertions(+), 41 deletions(-) create mode 100644 src/strands/strands_conditionals.js diff --git a/preview/global/sketch.js b/preview/global/sketch.js index 486d553d6f..fe73718b0d 100644 --- a/preview/global/sketch.js +++ b/preview/global/sketch.js @@ -6,8 +6,14 @@ function callback() { let x = createFloat(2.5); strandsIf(x.greaterThan(createFloat(0.0)), () => { - x = createFloat(100); - }); + return {x: createFloat(100)} + }).Else(); + // strandsIf(x.greaterThan(createFloat(0.0)), () => { + // strandsIf(x.greaterThan(createFloat(0.0)), () => { + // return x = createFloat(100); + // }); + // return x = createFloat(100); + // }); return x; }); diff --git a/src/strands/CFG.js b/src/strands/CFG.js index 5b8fa9ac96..f15f033443 100644 --- a/src/strands/CFG.js +++ b/src/strands/CFG.js @@ -1,3 +1,5 @@ +import { BlockTypeToName } from "./utils"; + export function createControlFlowGraph() { return { nextID: 0, @@ -36,4 +38,10 @@ export function getBlockDataFromID(graph, id) { outgoingEdges: graph.outgoingEdges[id], blockInstructions: graph.blockInstructions[id], } +} + +export function printBlockData(graph, id) { + const block = getBlockDataFromID(graph, id); + block.blockType = BlockTypeToName[block.blockType]; + console.log(block); } \ No newline at end of file diff --git a/src/strands/GLSL_generator.js b/src/strands/GLSL_generator.js index 400789cf43..1ac3a34103 100644 --- a/src/strands/GLSL_generator.js +++ b/src/strands/GLSL_generator.js @@ -1,6 +1,5 @@ import { dfsPostOrder, NodeType, OpCodeToSymbol, BlockType, OpCodeToOperation, BlockTypeToName } from "./utils"; import { getNodeDataFromID } from "./DAG"; -import { getBlockDataFromID } from "./CFG"; let globalTempCounter = 0; @@ -75,13 +74,6 @@ export function generateGLSL(strandsContext) { const dagSorted = dfsPostOrder(dag.dependsOn, rootNodeID); const cfgSorted = dfsPostOrder(cfg.outgoingEdges, entryBlockID).reverse(); - console.log("BLOCK ORDER: ", cfgSorted.map(id => { - const node = getBlockDataFromID(cfg, id); - node.blockType = BlockTypeToName[node.blockType]; - return node; - } - )); - const hookContext = { ...computeDeclarations(dag, dagSorted), indent: 0, @@ -110,7 +102,6 @@ export function generateGLSL(strandsContext) { return; default: const blockInstructions = new Set(cfg.blockInstructions[blockID] || []); - console.log(blockID, blockInstructions); for (let nodeID of dagSorted) { if (!blockInstructions.has(nodeID)) { continue; diff --git a/src/strands/p5.strands.js b/src/strands/p5.strands.js index b3bc462d61..908a9a85a1 100644 --- a/src/strands/p5.strands.js +++ b/src/strands/p5.strands.js @@ -56,12 +56,12 @@ function strands(p5, fn) { return new StrandsNode(id); }; } - if (arity === 'unary') { - StrandsNode.prototype[name] = function () { - const id = NaN; //createUnaryExpressionNode(this, SymbolToOpCode[symbol]); - return new StrandsNode(id); - }; - } + // if (arity === 'unary') { + // StrandsNode.prototype[name] = function () { + // const id = createUnaryExpressionNode(this, SymbolToOpCode[symbol]); + // return new StrandsNode(id); + // }; + // } } function createLiteralNode(dataType, value) { @@ -124,40 +124,125 @@ function strands(p5, fn) { const id = createLiteralNode(DataType.FLOAT, value); return new StrandsNode(id); } - - fn.strandsIf = function(conditionNode, ifBody, elseBody) { - const { cfg } = strandsContext; - const mergeBlock = CFG.createBasicBlock(cfg, BlockType.MERGE); + + class StrandsConditional { + constructor(condition, branchCallback) { + // Condition must be a node... + this.branches = [{ + condition, + branchCallback, + blockType: BlockType.IF_BODY + }]; + } - const conditionBlock = CFG.createBasicBlock(cfg, BlockType.CONDITION); - pushBlockWithEdgeFromCurrent(conditionBlock); - strandsContext.blockConditions[conditionBlock] = conditionNode.id; + ElseIf(condition, branchCallback) { + this.branches.push({ + condition, + branchCallback, + blockType: BlockType.EL_IF_BODY + }); + return this; + } - const ifBodyBlock = CFG.createBasicBlock(cfg, BlockType.IF_BODY); - pushBlockWithEdgeFromCurrent(ifBodyBlock); - ifBody(); - if (strandsContext.currentBlock !== ifBodyBlock) { - CFG.addEdge(cfg, strandsContext.currentBlock, mergeBlock); - popBlock(); + Else(branchCallback = () => ({})) { + this.branches.push({ + condition: null, + branchCallback, + blockType: BlockType.ELSE_BODY + }); + return buildConditional(this); } - popBlock(); + } + + function buildConditional(conditional) { + const { blockConditions, cfg } = strandsContext; + const branches = conditional.branches; + const mergeBlock = CFG.createBasicBlock(cfg, BlockType.MERGE); + const allResults = []; + // First conditional connects from outer block, everything else + // connects to previous condition (when false) + let prevCondition = strandsContext.currentBlock - const elseBodyBlock = CFG.createBasicBlock(cfg, BlockType.ELSE_BODY); - pushBlock(elseBodyBlock); - CFG.addEdge(cfg, conditionBlock, elseBodyBlock); - if (elseBody) { - elseBody(); - if (strandsContext.currentBlock !== ifBodyBlock) { + for (let i = 0; i < branches.length; i++) { + console.log(branches[i]); + const { condition, branchCallback, blockType } = branches[i]; + const isElseBlock = (i === branches.length - 1); + + if (!isElseBlock) { + const conditionBlock = CFG.createBasicBlock(cfg, BlockType.CONDITION); + CFG.addEdge(cfg, prevCondition, conditionBlock); + pushBlock(conditionBlock); + blockConditions[conditionBlock] = condition.id; + prevCondition = conditionBlock; + popBlock(); + } + + const branchBlock = CFG.createBasicBlock(cfg, blockType); + CFG.addEdge(cfg, prevCondition, branchBlock); + + pushBlock(branchBlock); + const branchResults = branchCallback(); + allResults.push(branchResults); + if (strandsContext.currentBlock !== branchBlock) { CFG.addEdge(cfg, strandsContext.currentBlock, mergeBlock); popBlock(); } + CFG.addEdge(cfg, strandsContext.currentBlock, mergeBlock); + popBlock(); } - popBlock(); - pushBlock(mergeBlock); - CFG.addEdge(cfg, elseBodyBlock, mergeBlock); - CFG.addEdge(cfg, ifBodyBlock, mergeBlock); + + return allResults; } + + + fn.strandsIf = function(conditionNode, ifBody) { + return new StrandsConditional(conditionNode, ifBody); + } + // fn.strandsIf = function(conditionNode, ifBody, elseBody) { + // const { cfg } = strandsContext; + + // console.log('Before if:', strandsContext.blockStack) + // strandsContext.blockStack.forEach(block => { + // CFG.printBlockData(cfg, block) + // }) + + // const mergeBlock = CFG.createBasicBlock(cfg, BlockType.MERGE); + + // const conditionBlock = CFG.createBasicBlock(cfg, BlockType.CONDITION); + // pushBlockWithEdgeFromCurrent(conditionBlock); + // strandsContext.blockConditions[conditionBlock] = conditionNode.id; + + // const ifBodyBlock = CFG.createBasicBlock(cfg, BlockType.IF_BODY); + // pushBlockWithEdgeFromCurrent(ifBodyBlock); + // ifBody(); + // if (strandsContext.currentBlock !== ifBodyBlock) { + // CFG.addEdge(cfg, strandsContext.currentBlock, mergeBlock); + // popBlock(); + // } + // popBlock(); + + // const elseBodyBlock = CFG.createBasicBlock(cfg, BlockType.ELSE_BODY); + // pushBlock(elseBodyBlock); + // CFG.addEdge(cfg, conditionBlock, elseBodyBlock); + // if (elseBody) { + // elseBody(); + // if (strandsContext.currentBlock !== ifBodyBlock) { + // CFG.addEdge(cfg, strandsContext.currentBlock, mergeBlock); + // popBlock(); + // } + // } + // popBlock(); + // popBlock(); + + // pushBlock(mergeBlock); + // console.log('After if:', strandsContext.blockStack) + // strandsContext.blockStack.forEach(block => { + // CFG.printBlockData(cfg, block) + // }) + // CFG.addEdge(cfg, elseBodyBlock, mergeBlock); + // CFG.addEdge(cfg, ifBodyBlock, mergeBlock); + // } function createHookArguments(parameters){ const structTypes = ['Vertex', ] diff --git a/src/strands/strands_conditionals.js b/src/strands/strands_conditionals.js new file mode 100644 index 0000000000..8ff9329348 --- /dev/null +++ b/src/strands/strands_conditionals.js @@ -0,0 +1,61 @@ +import * as CFG from './CFG' +import { BlockType } from './utils'; + +export class StrandsConditional { + constructor(condition, branchCallback) { + // Condition must be a node... + this.branches = [{ + condition, + branchCallback, + blockType: BlockType.IF_BODY + }]; + } + + ElseIf(condition, branchCallback) { + this.branches.push({ condition, branchCallback, blockType: BlockType.EL_IF_BODY }); + return this; + } + + Else(branchCallback = () => ({})) { + this.branches.push({ condition, branchCallback: null, blockType: BlockType.ELSE_BODY }); + return buildConditional(this); + } +} + +function buildConditional(conditional) { + const { blockConditions, cfg } = strandsContext; + const branches = conditional.branches; + const mergeBlock = CFG.createBasicBlock(cfg, BlockType.MERGE); + + // First conditional connects from outer block, everything else + // connects to previous condition (when false) + let prevCondition = strandsContext.currentBlock + + for (let i = 0; i < branches.length; i++) { + const { condition, branchCallback, blockType } = branches[i]; + const isElseBlock = (i === branches.length - 1); + + if (!isElseBlock) { + const conditionBlock = CFG.createBasicBlock(cfg, BlockType.CONDITION); + CFG.addEdge(cfg, prevCondition, conditionBlock); + pushBlock(conditionBlock); + blockConditions[conditionBlock] = condition.id; + prevCondition = conditionBlock; + popBlock(); + } + + const branchBlock = CFG.createBasicBlock(cfg, blockType); + CFG.addEdge(cfg, prevCondition, branchBlock); + + pushBlock(branchBlock); + const branchResults = branchCallback(); + allResults.push(branchResults); + if (strandsContext.currentBlock !== branchBlock) { + CFG.addEdge(cfg, strandsContext.currentBlock, mergeBlock); + popBlock(); + } + CFG.addEdge(cfg, strandsContext.currentBlock, mergeBlock); + popBlock(); + } + pushBlock(mergeBlock); +} \ No newline at end of file From 627b7a3a820108ee59d6f5fc161c2795ee118b9b Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Wed, 16 Jul 2025 10:25:38 +0100 Subject: [PATCH 33/69] binary ops and contructors prototyped --- preview/global/sketch.js | 15 +- src/strands/GLSL_backend.js | 110 +++++++ src/strands/GLSL_generator.js | 123 ------- src/strands/builder.js | 175 ++++++++++ src/strands/code_generation.js | 67 ++++ src/strands/code_transpiler.js | 2 - src/strands/{CFG.js => control_flow_graph.js} | 19 +- .../{DAG.js => directed_acyclic_graph.js} | 28 +- src/strands/p5.strands.js | 310 ++---------------- src/strands/shader_functions.js | 83 +++++ src/strands/strands_FES.js | 9 +- src/strands/strands_conditionals.js | 70 ++-- src/strands/user_API.js | 176 ++++++++++ src/strands/utils.js | 131 +++++++- src/webgl/ShaderGenerator.js | 22 +- 15 files changed, 863 insertions(+), 477 deletions(-) create mode 100644 src/strands/GLSL_backend.js delete mode 100644 src/strands/GLSL_generator.js create mode 100644 src/strands/builder.js create mode 100644 src/strands/code_generation.js rename src/strands/{CFG.js => control_flow_graph.js} (74%) rename src/strands/{DAG.js => directed_acyclic_graph.js} (73%) create mode 100644 src/strands/shader_functions.js create mode 100644 src/strands/user_API.js diff --git a/preview/global/sketch.js b/preview/global/sketch.js index fe73718b0d..e8480e10b4 100644 --- a/preview/global/sketch.js +++ b/preview/global/sketch.js @@ -3,19 +3,9 @@ p5.disableFriendlyErrors = true; function callback() { getFinalColor((col) => { - let x = createFloat(2.5); - strandsIf(x.greaterThan(createFloat(0.0)), () => { - return {x: createFloat(100)} - }).Else(); - // strandsIf(x.greaterThan(createFloat(0.0)), () => { - // strandsIf(x.greaterThan(createFloat(0.0)), () => { - // return x = createFloat(100); - // }); - // return x = createFloat(100); - // }); - - return x; + // return vec3(1, 2, 4).add(float(2.0).sub(10)); + return (float(10).sub(10)); }); } @@ -25,4 +15,5 @@ async function setup(){ } function draw(){ + } diff --git a/src/strands/GLSL_backend.js b/src/strands/GLSL_backend.js new file mode 100644 index 0000000000..1723291280 --- /dev/null +++ b/src/strands/GLSL_backend.js @@ -0,0 +1,110 @@ +import { NodeType, OpCodeToSymbol, BlockType, OpCode, DataType, DataTypeName} from "./utils"; +import { getNodeDataFromID } from "./directed_acyclic_graph"; +import * as FES from './strands_FES' + +const cfgHandlers = { + [BlockType.DEFAULT]: (blockID, strandsContext, generationContext) => { + const { dag, cfg } = strandsContext; + + const blockInstructions = new Set(cfg.blockInstructions[blockID] || []); + for (let nodeID of generationContext.dagSorted) { + if (!blockInstructions.has(nodeID)) { + continue; + } + // const snippet = glslBackend.generateExpression(dag, nodeID, generationContext); + // generationContext.write(snippet); + } + }, + + [BlockType.IF_COND](blockID, strandsContext, generationContext) { + const { dag, cfg } = strandsContext; + const conditionID = cfg.blockConditions[blockID]; + const condExpr = glslBackend.generateExpression (dag, conditionID, generationContext); + generationContext.write(`if (${condExpr}) {`) + generationContext.indent++; + this[BlockType.DEFAULT](blockID, strandsContext, generationContext); + generationContext.indent--; + generationContext.write(`}`) + return; + }, + + [BlockType.IF_BODY](blockID, strandsContext, generationContext) { + + }, + + [BlockType.ELIF_BODY](blockID, strandsContext, generationContext) { + + }, + + [BlockType.ELSE_BODY](blockID, strandsContext, generationContext) { + + }, + + [BlockType.MERGE](blockID, strandsContext, generationContext) { + + }, + + [BlockType.FUNCTION](blockID, strandsContext, generationContext) { + this[BlockType.DEFAULT](blockID, strandsContext, generationContext); + }, +} + + +export const glslBackend = { + hookEntry(hookType) { + const firstLine = `(${hookType.parameters.flatMap((param) => { + return `${param.qualifiers?.length ? param.qualifiers.join(' ') : ''}${param.type.typeName} ${param.name}`; + }).join(', ')}) {`; + return firstLine; + }, + generateDataTypeName(dataType) { + return DataTypeName[dataType]; + }, + generateDeclaration() { + + }, + generateExpression(dag, nodeID, generationContext) { + const node = getNodeDataFromID(dag, nodeID); + if (generationContext.tempNames?.[nodeID]) { + return generationContext.tempNames[nodeID]; + } + switch (node.nodeType) { + case NodeType.LITERAL: + return node.value.toFixed(4); + + case NodeType.VARIABLE: + return node.identifier; + + case NodeType.OPERATION: + if (node.opCode === OpCode.Nary.CONSTRUCTOR) { + const T = this.generateDataTypeName(node.dataType); + const deps = node.dependsOn.map((dep) => this.generateExpression(dag, dep, generationContext)); + return `${T}(${deps.join(', ')})`; + } + if (node.opCode === OpCode.Nary.FUNCTION) { + return "functioncall!"; + } + if (node.dependsOn.length === 2) { + const [lID, rID] = node.dependsOn; + const left = this.generateExpression(dag, lID, generationContext); + const right = this.generateExpression(dag, rID, generationContext); + const opSym = OpCodeToSymbol[node.opCode]; + return `${left} ${opSym} ${right}`; + } + if (node.dependsOn.length === 1) { + const [i] = node.dependsOn; + const val = this.generateExpression(dag, i, generationContext); + const sym = OpCodeToSymbol[node.opCode]; + return `${sym}${val}`; + } + + default: + FES.internalError(`${node.nodeType} not working yet`) + } + }, + generateBlock(blockID, strandsContext, generationContext) { + const type = strandsContext.cfg.blockTypes[blockID]; + const handler = cfgHandlers[type] || cfgHandlers[BlockType.DEFAULT]; + handler.call(cfgHandlers, blockID, strandsContext, generationContext); + } +} diff --git a/src/strands/GLSL_generator.js b/src/strands/GLSL_generator.js deleted file mode 100644 index 1ac3a34103..0000000000 --- a/src/strands/GLSL_generator.js +++ /dev/null @@ -1,123 +0,0 @@ -import { dfsPostOrder, NodeType, OpCodeToSymbol, BlockType, OpCodeToOperation, BlockTypeToName } from "./utils"; -import { getNodeDataFromID } from "./DAG"; - -let globalTempCounter = 0; - -function nodeToGLSL(dag, nodeID, hookContext) { - const node = getNodeDataFromID(dag, nodeID); - if (hookContext.tempName?.[nodeID]) { - return hookContext.tempName[nodeID]; - } - switch (node.nodeType) { - case NodeType.LITERAL: - return node.value.toFixed(4); - - case NodeType.VARIABLE: - return node.identifier; - - case NodeType.OPERATION: - const [lID, rID] = node.dependsOn; - // if (dag.nodeTypes[lID] === NodeType.LITERAL && dag.nodeTypes[lID] === dag.nodeTypes[rID]) { - // const constantFolded = OpCodeToOperation[dag.opCodes[nodeID]](dag.values[lID], dag.values[rID]); - // if (!(constantFolded === undefined)) return constantFolded; - // } - const left = nodeToGLSL(dag, lID, hookContext); - const right = nodeToGLSL(dag, rID, hookContext); - const opSym = OpCodeToSymbol[node.opCode]; - return `(${left} ${opSym} ${right})`; - - default: - throw new Error(`${node.nodeType} not working yet`); - } -} - -function computeDeclarations(dag, dagOrder) { - const usedCount = {}; - for (const nodeID of dagOrder) { - usedCount[nodeID] = (dag.usedBy[nodeID] || []).length; - } - - const tempNames = {}; - const declarations = []; - for (const nodeID of dagOrder) { - if (dag.nodeTypes[nodeID] !== NodeType.OPERATION) { - continue; - } - - if (usedCount[nodeID] > 1) { - const tmp = `t${globalTempCounter++}`; - tempNames[nodeID] = tmp; - - const expr = nodeToGLSL(dag, nodeID, {}); - declarations.push(`float ${tmp} = ${expr};`); - } - } - - return { declarations, tempNames }; -} - -const cfgHandlers = { - Condition(strandsContext, hookContext) { - const conditionID = strandsContext.blockConditions[blockID]; - const condExpr = nodeToGLSL(dag, conditionID, hookContext); - write(`if (${condExpr}) {`) - indent++; - return; - } -} - -export function generateGLSL(strandsContext) { - const hooksObj = {}; - - for (const { hookType, entryBlockID, rootNodeID} of strandsContext.hooks) { - const { cfg, dag } = strandsContext; - const dagSorted = dfsPostOrder(dag.dependsOn, rootNodeID); - const cfgSorted = dfsPostOrder(cfg.outgoingEdges, entryBlockID).reverse(); - - const hookContext = { - ...computeDeclarations(dag, dagSorted), - indent: 0, - }; - - let indent = 0; - let codeLines = hookContext.declarations.map((decl) => pad() + decl); - const write = (line) => codeLines.push(' '.repeat(indent) + line); - - cfgSorted.forEach((blockID) => { - const type = cfg.blockTypes[blockID]; - switch (type) { - case BlockType.CONDITION: - const condID = strandsContext.blockConditions[blockID]; - const condExpr = nodeToGLSL(dag, condID, hookContext); - write(`if (${condExpr}) {`) - indent++; - return; - // case BlockType.ELSE_BODY: - // write('else {'); - // indent++; - // return; - case BlockType.MERGE: - indent--; - write('}'); - return; - default: - const blockInstructions = new Set(cfg.blockInstructions[blockID] || []); - for (let nodeID of dagSorted) { - if (!blockInstructions.has(nodeID)) { - continue; - } - const snippet = hookContext.tempNames[nodeID] - ? hookContext.tempNames[nodeID] - : nodeToGLSL(dag, nodeID, hookContext); - write(snippet); - } - } - }); - - const finalExpression = `return ${nodeToGLSL(dag, rootNodeID, hookContext)};`; - write(finalExpression); - hooksObj[hookType.name] = codeLines.join('\n'); - } - - return hooksObj; -} \ No newline at end of file diff --git a/src/strands/builder.js b/src/strands/builder.js new file mode 100644 index 0000000000..3459f5f7ed --- /dev/null +++ b/src/strands/builder.js @@ -0,0 +1,175 @@ +import * as DAG from './directed_acyclic_graph' +import * as CFG from './control_flow_graph' +import * as FES from './strands_FES' +import { DataType, DataTypeInfo, NodeType, OpCode, DataTypeName} from './utils'; +import { StrandsNode } from './user_API'; + +////////////////////////////////////////////// +// Builders for node graphs +////////////////////////////////////////////// +export function createLiteralNode(strandsContext, typeInfo, value) { + const { cfg, dag } = strandsContext + const nodeData = DAG.createNodeData({ + nodeType: NodeType.LITERAL, + dataType, + value + }); + const id = DAG.getOrCreateNode(dag, nodeData); + CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); + return id; +} + +export function createVariableNode(strandsContext, typeInfo, identifier) { + const { cfg, dag } = strandsContext; + const nodeData = DAG.createNodeData({ + nodeType: NodeType.VARIABLE, + dataType, + identifier + }) + const id = DAG.getOrCreateNode(dag, nodeData); + CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); + return id; +} + +export function createBinaryOpNode(strandsContext, leftNode, rightArg, opCode) { + const { dag, cfg } = strandsContext; + + let inferRightType, rightNodeID, rightNode; + if (rightArg instanceof StrandsNode) { + rightNode = rightArg; + rightNodeID = rightArg.id; + inferRightType = dag.dataTypes[rightNodeID]; + } else { + const rightDependsOn = Array.isArray(rightArg) ? rightArg : [rightArg]; + inferRightType = DataType.DEFER; + rightNodeID = createTypeConstructorNode(strandsContext, inferRightType, rightDependsOn); + rightNode = new StrandsNode(rightNodeID); + } + const origRightType = inferRightType; + const leftNodeID = leftNode.id; + const origLeftType = dag.dataTypes[leftNodeID]; + + + const cast = { node: null, toType: origLeftType }; + // Check if we have to cast either node + if (origLeftType !== origRightType) { + const L = DataTypeInfo[origLeftType]; + const R = DataTypeInfo[origRightType]; + + if (L.base === DataType.DEFER) { + L.dimension = dag.dependsOn[leftNodeID].length; + } + if (R.base === DataType.DEFER) { + R.dimension = dag.dependsOn[rightNodeID].length; + } + + if (L.dimension === 1 && R.dimension > 1) { + // e.g. op(scalar, vector): cast scalar up + cast.node = leftNode; + cast.toType = origRightType; + } + else if (R.dimension === 1 && L.dimension > 1) { + cast.node = rightNode; + cast.toType = origLeftType; + } + else if (L.priority > R.priority && L.dimension === R.dimension) { + // e.g. op(float vector, int vector): cast priority is float > int > bool + cast.node = rightNode; + cast.toType = origLeftType; + } + else if (R.priority > L.priority && L.dimension === R.dimension) { + cast.node = leftNode; + cast.toType = origRightType; + } + else { + FES.userError('type error', `A vector of length ${L.dimension} operated with a vector of length ${R.dimension} is not allowed.`); + } + const castedID = createTypeConstructorNode(strandsContext, cast.toType, cast.node); + if (cast.node === leftNode) { + leftNodeID = castedID; + } else { + rightNodeID = castedID; + } + } + + const nodeData = DAG.createNodeData({ + nodeType: NodeType.OPERATION, + dependsOn: [leftNodeID, rightNodeID], + dataType: cast.toType, + opCode + }); + const id = DAG.getOrCreateNode(dag, nodeData); + CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); + return id; +} + +function mapConstructorDependencies(strandsContext, typeInfo, dependsOn) { + const mapped = []; + const T = DataTypeInfo[dataType]; + const dag = strandsContext.dag; + let calculatedDimensions = 0; + + for (const dep of dependsOn.flat()) { + if (dep instanceof StrandsNode) { + const node = DAG.getNodeDataFromID(dag, dep.id); + + if (node.opCode === OpCode.Nary.CONSTRUCTOR && dataType === dataType) { + for (const inner of node.dependsOn) { + mapped.push(inner); + } + } + const depDataType = dag.dataTypes[dep.id]; + calculatedDimensions += DataTypeInfo[depDataType].dimension; + continue; + } + if (typeof dep === 'number') { + const newNode = createLiteralNode(strandsContext, T.base, dep); + calculatedDimensions += 1; + mapped.push(newNode); + continue; + } + else { + FES.userError('type error', `You've tried to construct a scalar or vector type with a non-numeric value: ${dep}`); + } + } + + if(calculatedDimensions !== 1 && calculatedDimensions !== T.dimension) { + FES.userError('type error', `You've tried to construct a ${DataTypeName[dataType]} with ${calculatedDimensions} components`); + } + return mapped; +} + +export function createTypeConstructorNode(strandsContext, typeInfo, dependsOn) { + const { cfg, dag } = strandsContext; + dependsOn = Array.isArray(dependsOn) ? dependsOn : [dependsOn]; + const mappedDependencies = mapConstructorDependencies(strandsContext, dataType, dependsOn); + const nodeData = DAG.createNodeData({ + nodeType: NodeType.OPERATION, + opCode: OpCode.Nary.CONSTRUCTOR, + dataType, + dependsOn: mappedDependencies + }) + const id = DAG.getOrCreateNode(dag, nodeData); + CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); + return id; +} + +export function createFunctionCallNode(strandsContext, identifier, overrides, dependsOn) { + const { cfg, dag } = strandsContext; + let dataType = dataType.DEFER; + const nodeData = DAG.createNodeData({ + nodeType: NodeType.OPERATION, + opCode: OpCode.Nary.FUNCTION_CALL, + identifier, + overrides, + dependsOn, + dataType + }) + const id = DAG.getOrCreateNode(dag, nodeData); + CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); + return id; +} + +export function createStatementNode(strandsContext, type) { + return -99; +} \ No newline at end of file diff --git a/src/strands/code_generation.js b/src/strands/code_generation.js new file mode 100644 index 0000000000..b8aba9a642 --- /dev/null +++ b/src/strands/code_generation.js @@ -0,0 +1,67 @@ +import { WEBGL } from '../core/constants'; +import { glslBackend } from './GLSL_backend'; +import { dfsPostOrder, dfsReversePostOrder, NodeType } from './utils'; + +let globalTempCounter = 0; +let backend; + +function generateTopLevelDeclarations(dag, dagOrder) { + const usedCount = {}; + for (const nodeID of dagOrder) { + usedCount[nodeID] = (dag.usedBy[nodeID] || []).length; + } + + const tempNames = {}; + const declarations = []; + for (const nodeID of dagOrder) { + if (dag.nodeTypes[nodeID] !== NodeType.OPERATION) { + continue; + } + + // if (usedCount[nodeID] > 1) { + // const tmp = `t${globalTempCounter++}`; + // tempNames[nodeID] = tmp; + + // const expr = backend.generateExpression(dag, nodeID, {}); + // declarations.push(`float ${tmp} = ${expr};`); + // } + } + + return { declarations, tempNames }; +} + +export function generateShaderCode(strandsContext) { + if (strandsContext.backend === WEBGL) { + backend = glslBackend; + } + const hooksObj = {}; + + for (const { hookType, entryBlockID, rootNodeID} of strandsContext.hooks) { + const { cfg, dag } = strandsContext; + const dagSorted = dfsPostOrder(dag.dependsOn, rootNodeID); + const cfgSorted = dfsReversePostOrder(cfg.outgoingEdges, entryBlockID); + + const generationContext = { + ...generateTopLevelDeclarations(dag, dagSorted), + indent: 1, + codeLines: [], + write(line) { + this.codeLines.push(' '.repeat(this.indent) + line); + }, + dagSorted, + }; + + generationContext.declarations.forEach(decl => generationContext.write(decl)); + for (const blockID of cfgSorted) { + backend.generateBlock(blockID, strandsContext, generationContext); + } + + const firstLine = backend.hookEntry(hookType); + const finalExpression = `return ${backend.generateExpression(dag, rootNodeID, generationContext)};`; + generationContext.write(finalExpression); + console.log(hookType); + hooksObj[hookType.name] = [firstLine, ...generationContext.codeLines, '}'].join('\n'); + } + + return hooksObj; +} \ No newline at end of file diff --git a/src/strands/code_transpiler.js b/src/strands/code_transpiler.js index 6692c574a0..a804d3dcfd 100644 --- a/src/strands/code_transpiler.js +++ b/src/strands/code_transpiler.js @@ -2,8 +2,6 @@ import { parse } from 'acorn'; import { ancestor } from 'acorn-walk'; import escodegen from 'escodegen'; -import { OperatorTable } from './utils'; - // TODO: Switch this to operator table, cleanup whole file too function replaceBinaryOperator(codeSource) { diff --git a/src/strands/CFG.js b/src/strands/control_flow_graph.js similarity index 74% rename from src/strands/CFG.js rename to src/strands/control_flow_graph.js index f15f033443..cee0f0da42 100644 --- a/src/strands/CFG.js +++ b/src/strands/control_flow_graph.js @@ -2,15 +2,30 @@ import { BlockTypeToName } from "./utils"; export function createControlFlowGraph() { return { - nextID: 0, - graphType: 'CFG', + // graph structure blockTypes: [], incomingEdges: [], outgoingEdges: [], blockInstructions: [], + // runtime data for constructing graph + nextID: 0, + blockStack: [], + blockConditions: {}, + currentBlock: -1, }; } +export function pushBlock(graph, blockID) { + graph.blockStack.push(blockID); + graph.currentBlock = blockID; +} + +export function popBlock(graph) { + graph.blockStack.pop(); + const len = graph.blockStack.length; + graph.currentBlock = graph.blockStack[len-1]; +} + export function createBasicBlock(graph, blockType) { const id = graph.nextID++; graph.blockTypes[id] = blockType; diff --git a/src/strands/DAG.js b/src/strands/directed_acyclic_graph.js similarity index 73% rename from src/strands/DAG.js rename to src/strands/directed_acyclic_graph.js index b095fe3efc..54232cc5ff 100644 --- a/src/strands/DAG.js +++ b/src/strands/directed_acyclic_graph.js @@ -2,7 +2,7 @@ import { NodeTypeRequiredFields, NodeTypeToName } from './utils' import * as FES from './strands_FES' ///////////////////////////////// -// Public functions for for strands runtime +// Public functions for strands runtime ///////////////////////////////// export function createDirectedAcyclicGraph() { @@ -11,6 +11,8 @@ export function createDirectedAcyclicGraph() { cache: new Map(), nodeTypes: [], dataTypes: [], + baseTypes: [], + dimensions: [], opCodes: [], values: [], identifiers: [], @@ -40,12 +42,14 @@ export function createNodeData(data = {}) { const node = { nodeType: data.nodeType ?? null, dataType: data.dataType ?? null, + baseType: data.baseType ?? null, + dimension: data.baseType ?? null, opCode: data.opCode ?? null, value: data.value ?? null, identifier: data.identifier ?? null, dependsOn: Array.isArray(data.dependsOn) ? data.dependsOn : [], usedBy: Array.isArray(data.usedBy) ? data.usedBy : [], - phiBlocks: Array.isArray(data.phiBlocks) ? data.phiBlocks : [] + phiBlocks: Array.isArray(data.phiBlocks) ? data.phiBlocks : [], }; validateNode(node); return node; @@ -61,6 +65,8 @@ export function getNodeDataFromID(graph, id) { dependsOn: graph.dependsOn[id], usedBy: graph.usedBy[id], phiBlocks: graph.phiBlocks[id], + dimension: graph.dimensions[id], + baseType: graph.baseTypes[id], } } @@ -77,7 +83,15 @@ function createNode(graph, node) { graph.dependsOn[id] = node.dependsOn.slice(); graph.usedBy[id] = node.usedBy; graph.phiBlocks[id] = node.phiBlocks.slice(); + + graph.baseTypes[id] = node.baseType + graph.dimensions[id] = node.dimension; + + for (const dep of node.dependsOn) { + if (!Array.isArray(graph.usedBy[dep])) { + graph.usedBy[dep] = []; + } graph.usedBy[dep].push(id); } return id; @@ -89,14 +103,18 @@ function getNodeKey(node) { } function validateNode(node){ - const requiredFields = NodeTypeRequiredFields[node.nodeType]; + const nodeType = node.nodeType; + const requiredFields = [...NodeTypeRequiredFields[nodeType], 'baseType', 'dimension']; + 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 = []; for (const field of requiredFields) { - if (node[field] === NaN) { + if (node[field] === null) { missingFields.push(field); } } if (missingFields.length > 0) { - FES.internalError(`[p5.strands internal error]: Missing fields ${missingFields.join(', ')} for a node type ${NodeTypeToName(node.nodeType)}`); + FES.internalError(`Missing fields ${missingFields.join(', ')} for a node type '${NodeTypeToName[nodeType]}'.`); } } \ No newline at end of file diff --git a/src/strands/p5.strands.js b/src/strands/p5.strands.js index 908a9a85a1..6089c21e18 100644 --- a/src/strands/p5.strands.js +++ b/src/strands/p5.strands.js @@ -4,299 +4,55 @@ * @for p5 * @requires core */ +import { WEBGL, /*WEBGPU*/ } from '../core/constants' import { transpileStrandsToJS } from './code_transpiler'; -import { DataType, NodeType, SymbolToOpCode, OperatorTable, BlockType } from './utils'; +import { BlockType } from './utils'; -import * as DAG from './DAG'; -import * as CFG from './CFG' -import { generateGLSL } from './GLSL_generator'; +import { createDirectedAcyclicGraph } from './directed_acyclic_graph' +import { createControlFlowGraph, createBasicBlock, pushBlock, popBlock } from './control_flow_graph'; +import { generateShaderCode } from './code_generation'; +import { initGlobalStrandsAPI, initShaderHooksFunctions } from './user_API'; function strands(p5, fn) { ////////////////////////////////////////////// // Global Runtime ////////////////////////////////////////////// - function initStrands(ctx) { - ctx.cfg = CFG.createControlFlowGraph(); - ctx.dag = DAG.createDirectedAcyclicGraph(); - ctx.blockStack = []; - ctx.currentBlock = -1; - ctx.blockConditions = {}; + function initStrandsContext(ctx, backend) { + ctx.dag = createDirectedAcyclicGraph(); + ctx.cfg = createControlFlowGraph(); ctx.uniforms = []; ctx.hooks = []; + ctx.backend = backend; + ctx.active = true; + ctx.previousFES = p5.disableFriendlyErrors; + p5.disableFriendlyErrors = true; } - function deinitStrands(ctx) { - Object.keys(ctx).forEach(prop => { - delete ctx[prop]; - }); - } - - // Stubs - function overrideGlobalFunctions() {} - function restoreGlobalFunctions() {} - function overrideFES() {} - function restoreFES() {} - - ////////////////////////////////////////////// - // User nodes - ////////////////////////////////////////////// - class StrandsNode { - constructor(id) { - this.id = id; - } - } - - // We augment the strands node with operations programatically - // this means methods like .add, .sub, etc can be chained - for (const { name, symbol, arity } of OperatorTable) { - if (arity === 'binary') { - StrandsNode.prototype[name] = function (rightNode) { - const id = createBinaryOpNode(this.id, rightNode.id, SymbolToOpCode[symbol]); - return new StrandsNode(id); - }; - } - // if (arity === 'unary') { - // StrandsNode.prototype[name] = function () { - // const id = createUnaryExpressionNode(this, SymbolToOpCode[symbol]); - // return new StrandsNode(id); - // }; - // } - } - - function createLiteralNode(dataType, value) { - const nodeData = DAG.createNodeData({ - nodeType: NodeType.LITERAL, - dataType, - value - }); - const id = DAG.getOrCreateNode(strandsContext.dag, nodeData); - const b = strandsContext.currentBlock; - CFG.recordInBasicBlock(strandsContext.cfg, strandsContext.currentBlock, id); - return id; - } - - function createBinaryOpNode(left, right, opCode) { - const nodeData = DAG.createNodeData({ - nodeType: NodeType.OPERATION, - dependsOn: [left, right], - opCode - }); - const id = DAG.getOrCreateNode(strandsContext.dag, nodeData); - CFG.recordInBasicBlock(strandsContext.cfg, strandsContext.currentBlock, id); - return id; - } - - function createVariableNode(dataType, identifier) { - const nodeData = DAG.createNodeData({ - nodeType: NodeType.VARIABLE, - dataType, - identifier - }) - const id = DAG.getOrCreateNode(strandsContext.dag, nodeData); - CFG.recordInBasicBlock(strandsContext.cfg, strandsContext.currentBlock, id); - return id; - } - - function pushBlockWithEdgeFromCurrent(blockID) { - CFG.addEdge(strandsContext.cfg, strandsContext.currentBlock, blockID); - pushBlock(blockID); - } - - function pushBlock(blockID) { - strandsContext.blockStack.push(blockID); - strandsContext.currentBlock = blockID; - } - - function popBlock() { - strandsContext.blockStack.pop(); - const len = strandsContext.blockStack.length; - strandsContext.currentBlock = strandsContext.blockStack[len-1]; - } - - fn.uniformFloat = function(name, defaultValue) { - const id = createVariableNode(DataType.FLOAT, name); - strandsContext.uniforms.push({ name, dataType: DataType.FLOAT, defaultValue }); - return new StrandsNode(id); - } - - fn.createFloat = function(value) { - const id = createLiteralNode(DataType.FLOAT, value); - return new StrandsNode(id); - } - - class StrandsConditional { - constructor(condition, branchCallback) { - // Condition must be a node... - this.branches = [{ - condition, - branchCallback, - blockType: BlockType.IF_BODY - }]; - } - - ElseIf(condition, branchCallback) { - this.branches.push({ - condition, - branchCallback, - blockType: BlockType.EL_IF_BODY - }); - return this; - } - - Else(branchCallback = () => ({})) { - this.branches.push({ - condition: null, - branchCallback, - blockType: BlockType.ELSE_BODY - }); - return buildConditional(this); - } - } - - function buildConditional(conditional) { - const { blockConditions, cfg } = strandsContext; - const branches = conditional.branches; - const mergeBlock = CFG.createBasicBlock(cfg, BlockType.MERGE); - const allResults = []; - // First conditional connects from outer block, everything else - // connects to previous condition (when false) - let prevCondition = strandsContext.currentBlock - - for (let i = 0; i < branches.length; i++) { - console.log(branches[i]); - const { condition, branchCallback, blockType } = branches[i]; - const isElseBlock = (i === branches.length - 1); - - if (!isElseBlock) { - const conditionBlock = CFG.createBasicBlock(cfg, BlockType.CONDITION); - CFG.addEdge(cfg, prevCondition, conditionBlock); - pushBlock(conditionBlock); - blockConditions[conditionBlock] = condition.id; - prevCondition = conditionBlock; - popBlock(); - } - - const branchBlock = CFG.createBasicBlock(cfg, blockType); - CFG.addEdge(cfg, prevCondition, branchBlock); - - pushBlock(branchBlock); - const branchResults = branchCallback(); - allResults.push(branchResults); - if (strandsContext.currentBlock !== branchBlock) { - CFG.addEdge(cfg, strandsContext.currentBlock, mergeBlock); - popBlock(); - } - CFG.addEdge(cfg, strandsContext.currentBlock, mergeBlock); - popBlock(); - } - pushBlock(mergeBlock); - - return allResults; - } - - - fn.strandsIf = function(conditionNode, ifBody) { - return new StrandsConditional(conditionNode, ifBody); + function deinitStrandsContext(ctx) { + ctx.dag = createDirectedAcyclicGraph(); + ctx.cfg = createControlFlowGraph(); + ctx.uniforms = []; + ctx.hooks = []; + p5.disableFriendlyErrors = ctx.previousFES; } - // fn.strandsIf = function(conditionNode, ifBody, elseBody) { - // const { cfg } = strandsContext; - - // console.log('Before if:', strandsContext.blockStack) - // strandsContext.blockStack.forEach(block => { - // CFG.printBlockData(cfg, block) - // }) - - // const mergeBlock = CFG.createBasicBlock(cfg, BlockType.MERGE); - - // const conditionBlock = CFG.createBasicBlock(cfg, BlockType.CONDITION); - // pushBlockWithEdgeFromCurrent(conditionBlock); - // strandsContext.blockConditions[conditionBlock] = conditionNode.id; - - // const ifBodyBlock = CFG.createBasicBlock(cfg, BlockType.IF_BODY); - // pushBlockWithEdgeFromCurrent(ifBodyBlock); - // ifBody(); - // if (strandsContext.currentBlock !== ifBodyBlock) { - // CFG.addEdge(cfg, strandsContext.currentBlock, mergeBlock); - // popBlock(); - // } - // popBlock(); - - // const elseBodyBlock = CFG.createBasicBlock(cfg, BlockType.ELSE_BODY); - // pushBlock(elseBodyBlock); - // CFG.addEdge(cfg, conditionBlock, elseBodyBlock); - // if (elseBody) { - // elseBody(); - // if (strandsContext.currentBlock !== ifBodyBlock) { - // CFG.addEdge(cfg, strandsContext.currentBlock, mergeBlock); - // popBlock(); - // } - // } - // popBlock(); - // popBlock(); - - // pushBlock(mergeBlock); - // console.log('After if:', strandsContext.blockStack) - // strandsContext.blockStack.forEach(block => { - // CFG.printBlockData(cfg, block) - // }) - // CFG.addEdge(cfg, elseBodyBlock, mergeBlock); - // CFG.addEdge(cfg, ifBodyBlock, mergeBlock); - // } - function createHookArguments(parameters){ - const structTypes = ['Vertex', ] - const args = []; - - for (const param of parameters) { - const T = param.type; - if(structTypes.includes(T.typeName)) { - const propertiesNodes = T.properties.map( - (prop) => [prop.name, createVariableNode(DataType[prop.dataType], prop.name)] - ); - const argObj = Object.fromEntries(propertiesNodes); - args.push(argObj); - } else { - const arg = createVariableNode(DataType[param.dataType], param.name); - args.push(arg) - } - } - return args; - } + const strandsContext = {}; + initStrandsContext(strandsContext); + initGlobalStrandsAPI(p5, fn, strandsContext) - function generateHookOverrides(shader) { - const availableHooks = { - ...shader.hooks.vertex, - ...shader.hooks.fragment, - } - const hookTypes = Object.keys(availableHooks).map(name => shader.hookTypes(name)); - for (const hookType of hookTypes) { - window[hookType.name] = function(callback) { - const entryBlockID = CFG.createBasicBlock(strandsContext.cfg, BlockType.FUNCTION); - pushBlockWithEdgeFromCurrent(entryBlockID); - const args = createHookArguments(hookType.parameters); - const rootNodeID = callback(args).id; - strandsContext.hooks.push({ - hookType, - entryBlockID, - rootNodeID, - }); - popBlock(); - } - } - } - ////////////////////////////////////////////// // Entry Point ////////////////////////////////////////////// - const strandsContext = {}; const oldModify = p5.Shader.prototype.modify - + p5.Shader.prototype.newModify = function(shaderModifier, options = { parser: true, srcLocations: false }) { if (shaderModifier instanceof Function) { // Reset the context object every time modify is called; - initStrands(strandsContext) - generateHookOverrides(this); + const backend = WEBGL; + initStrandsContext(strandsContext, backend); + initShaderHooksFunctions(strandsContext, fn, this); + // 1. Transpile from strands DSL to JS let strandsCallback; if (options.parser) { @@ -306,21 +62,21 @@ function strands(p5, fn) { } // 2. Build the IR from JavaScript API - const globalScope = CFG.createBasicBlock(strandsContext.cfg, BlockType.GLOBAL); - pushBlock(globalScope); + const globalScope = createBasicBlock(strandsContext.cfg, BlockType.GLOBAL); + pushBlock(strandsContext.cfg, globalScope); strandsCallback(); - popBlock(); + popBlock(strandsContext.cfg); // 3. Generate shader code hooks object from the IR // ....... - const glsl = generateGLSL(strandsContext); - console.log(glsl.getFinalColor); + const hooksObject = generateShaderCode(strandsContext); + console.log(hooksObject.getFinalColor); // Call modify with the generated hooks object // return oldModify.call(this, generatedModifyArgument); // Reset the strands runtime context - // deinitStrands(strandsContext); + // deinitStrandsContext(strandsContext); } else { return oldModify.call(this, shaderModifier) diff --git a/src/strands/shader_functions.js b/src/strands/shader_functions.js new file mode 100644 index 0000000000..1c95d0702a --- /dev/null +++ b/src/strands/shader_functions.js @@ -0,0 +1,83 @@ +// GLSL Built in functions +// https://docs.gl/el3/abs +const builtInGLSLFunctions = { + //////////// Trigonometry ////////// + 'acos': [{ args: ['genType'], returnType: 'genType', isp5Function: true}], + 'acosh': [{ args: ['genType'], returnType: 'genType', isp5Function: false}], + 'asin': [{ args: ['genType'], returnType: 'genType', isp5Function: true}], + 'asinh': [{ args: ['genType'], returnType: 'genType', isp5Function: false}], + 'atan': [ + { args: ['genType'], returnType: 'genType', isp5Function: false}, + { args: ['genType', 'genType'], returnType: 'genType', isp5Function: false}, + ], + 'atanh': [{ args: ['genType'], returnType: 'genType', isp5Function: false}], + 'cos': [{ args: ['genType'], returnType: 'genType', isp5Function: true}], + 'cosh': [{ args: ['genType'], returnType: 'genType', isp5Function: false}], + 'degrees': [{ args: ['genType'], returnType: 'genType', isp5Function: true}], + 'radians': [{ args: ['genType'], returnType: 'genType', isp5Function: true}], + 'sin': [{ args: ['genType'], returnType: 'genType' , isp5Function: true}], + 'sinh': [{ args: ['genType'], returnType: 'genType', isp5Function: false}], + 'tan': [{ args: ['genType'], returnType: 'genType', isp5Function: true}], + 'tanh': [{ args: ['genType'], returnType: 'genType', isp5Function: false}], + ////////// Mathematics ////////// + 'abs': [{ args: ['genType'], returnType: 'genType', isp5Function: true}], + 'ceil': [{ args: ['genType'], returnType: 'genType', isp5Function: true}], + 'clamp': [{ args: ['genType', 'genType', 'genType'], returnType: 'genType', isp5Function: false}], + 'dFdx': [{ args: ['genType'], returnType: 'genType', isp5Function: false}], + 'dFdy': [{ args: ['genType'], returnType: 'genType', isp5Function: false}], + 'exp': [{ args: ['genType'], returnType: 'genType', isp5Function: true}], + 'exp2': [{ args: ['genType'], returnType: 'genType', isp5Function: false}], + 'floor': [{ args: ['genType'], returnType: 'genType', isp5Function: true}], + 'fma': [{ args: ['genType', 'genType', 'genType'], returnType: 'genType', isp5Function: false}], + 'fract': [{ args: ['genType'], returnType: 'genType', isp5Function: true}], + 'fwidth': [{ args: ['genType'], returnType: 'genType', isp5Function: false}], + 'inversesqrt': [{ args: ['genType'], returnType: 'genType', isp5Function: true}], + // 'isinf': [{}], + // 'isnan': [{}], + 'log': [{ args: ['genType'], returnType: 'genType', isp5Function: true}], + 'log2': [{ args: ['genType'], returnType: 'genType', isp5Function: false}], + 'max': [ + { args: ['genType', 'genType'], returnType: 'genType', isp5Function: true}, + { args: ['genType', 'float'], returnType: 'genType', isp5Function: true}, + ], + 'min': [ + { args: ['genType', 'genType'], returnType: 'genType', isp5Function: true}, + { args: ['genType', 'float'], returnType: 'genType', isp5Function: true}, + ], + 'mix': [ + { args: ['genType', 'genType', 'genType'], returnType: 'genType', isp5Function: false}, + { args: ['genType', 'genType', 'float'], returnType: 'genType', isp5Function: false}, + ], + // 'mod': [{}], + // 'modf': [{}], + 'pow': [{ args: ['genType', 'genType'], returnType: 'genType', isp5Function: true}], + 'round': [{ args: ['genType'], returnType: 'genType', isp5Function: true}], + 'roundEven': [{ args: ['genType'], returnType: 'genType', isp5Function: false}], + // 'sign': [{}], + 'smoothstep': [ + { args: ['genType', 'genType', 'genType'], returnType: 'genType', isp5Function: false}, + { args: ['float', 'float', 'genType'], returnType: 'genType', isp5Function: false}, + ], + 'sqrt': [{ args: ['genType'], returnType: 'genType', isp5Function: true}], + 'step': [{ args: ['genType', 'genType'], returnType: 'genType', isp5Function: false}], + 'trunc': [{ args: ['genType'], returnType: 'genType', isp5Function: false}], + + ////////// Vector ////////// + 'cross': [{ args: ['vec3', 'vec3'], returnType: 'vec3', isp5Function: true}], + 'distance': [{ args: ['genType', 'genType'], returnType: 'float', isp5Function: true}], + 'dot': [{ args: ['genType', 'genType'], returnType: 'float', isp5Function: true}], + // 'equal': [{}], + 'faceforward': [{ args: ['genType', 'genType', 'genType'], returnType: 'genType', isp5Function: false}], + 'length': [{ args: ['genType'], returnType: 'float', isp5Function: false}], + 'normalize': [{ args: ['genType'], returnType: 'genType', isp5Function: true}], + // 'notEqual': [{}], + 'reflect': [{ args: ['genType', 'genType'], returnType: 'genType', isp5Function: false}], + 'refract': [{ args: ['genType', 'genType', 'float'], returnType: 'genType', isp5Function: false}], + + ////////// Texture sampling ////////// + 'texture': [{args: ['sampler2D', 'vec2'], returnType: 'vec4', isp5Function: true}], +} + +export const strandsShaderFunctions = { + ...builtInGLSLFunctions, +} \ No newline at end of file diff --git a/src/strands/strands_FES.js b/src/strands/strands_FES.js index 695b220e6a..3af0aca90b 100644 --- a/src/strands/strands_FES.js +++ b/src/strands/strands_FES.js @@ -1,4 +1,9 @@ -export function internalError(message) { - const prefixedMessage = `[p5.strands internal error]: ${message}` +export function internalError(errorMessage) { + const prefixedMessage = `[p5.strands internal error]: ${errorMessage}` + throw new Error(prefixedMessage); +} + +export function userError(errorType, errorMessage) { + const prefixedMessage = `[p5.strands ${errorType}]: ${errorMessage}`; throw new Error(prefixedMessage); } \ No newline at end of file diff --git a/src/strands/strands_conditionals.js b/src/strands/strands_conditionals.js index 8ff9329348..e1da496c02 100644 --- a/src/strands/strands_conditionals.js +++ b/src/strands/strands_conditionals.js @@ -1,61 +1,71 @@ -import * as CFG from './CFG' +import * as CFG from './control_flow_graph' import { BlockType } from './utils'; export class StrandsConditional { - constructor(condition, branchCallback) { + constructor(strandsContext, condition, branchCallback) { // Condition must be a node... this.branches = [{ condition, branchCallback, blockType: BlockType.IF_BODY }]; + this.ctx = strandsContext; } ElseIf(condition, branchCallback) { - this.branches.push({ condition, branchCallback, blockType: BlockType.EL_IF_BODY }); + this.branches.push({ + condition, + branchCallback, + blockType: BlockType.ELIF_BODY + }); return this; } Else(branchCallback = () => ({})) { - this.branches.push({ condition, branchCallback: null, blockType: BlockType.ELSE_BODY }); - return buildConditional(this); + this.branches.push({ + condition: null, + branchCallback, + blockType: BlockType.ELSE_BODY + }); + return buildConditional(this.ctx, this); } } -function buildConditional(conditional) { - const { blockConditions, cfg } = strandsContext; +function buildConditional(strandsContext, conditional) { + const cfg = strandsContext.cfg; const branches = conditional.branches; + const mergeBlock = CFG.createBasicBlock(cfg, BlockType.MERGE); + const results = []; + + let previousBlock = cfg.currentBlock; - // First conditional connects from outer block, everything else - // connects to previous condition (when false) - let prevCondition = strandsContext.currentBlock - for (let i = 0; i < branches.length; i++) { const { condition, branchCallback, blockType } = branches[i]; - const isElseBlock = (i === branches.length - 1); - - if (!isElseBlock) { - const conditionBlock = CFG.createBasicBlock(cfg, BlockType.CONDITION); - CFG.addEdge(cfg, prevCondition, conditionBlock); - pushBlock(conditionBlock); - blockConditions[conditionBlock] = condition.id; - prevCondition = conditionBlock; - popBlock(); + + if (condition !== null) { + const conditionBlock = CFG.createBasicBlock(cfg, BlockType.IF_COND); + CFG.addEdge(cfg, previousBlock, conditionBlock); + CFG.pushBlock(cfg, conditionBlock); + cfg.blockConditions[conditionBlock] = condition.id; + previousBlock = conditionBlock; + CFG.popBlock(cfg); } - + const branchBlock = CFG.createBasicBlock(cfg, blockType); - CFG.addEdge(cfg, prevCondition, branchBlock); + CFG.addEdge(cfg, previousBlock, branchBlock); - pushBlock(branchBlock); + CFG.pushBlock(cfg, branchBlock); const branchResults = branchCallback(); - allResults.push(branchResults); - if (strandsContext.currentBlock !== branchBlock) { - CFG.addEdge(cfg, strandsContext.currentBlock, mergeBlock); - popBlock(); + results.push(branchResults); + if (cfg.currentBlock !== branchBlock) { + CFG.addEdge(cfg, cfg.currentBlock, mergeBlock); + CFG.popBlock(); } - CFG.addEdge(cfg, strandsContext.currentBlock, mergeBlock); - popBlock(); + CFG.addEdge(cfg, cfg.currentBlock, mergeBlock); + CFG.popBlock(cfg); } - pushBlock(mergeBlock); + CFG.pushBlock(cfg, mergeBlock); + + return results; } \ No newline at end of file diff --git a/src/strands/user_API.js b/src/strands/user_API.js new file mode 100644 index 0000000000..3482c57fb4 --- /dev/null +++ b/src/strands/user_API.js @@ -0,0 +1,176 @@ +import { + createBinaryOpNode, + createFunctionCallNode, + createVariableNode, + createStatementNode, + createTypeConstructorNode, +} from './builder' +import { DataType, OperatorTable, SymbolToOpCode, BlockType, arrayToFloatType } from './utils' +import { strandsShaderFunctions } from './shader_functions' +import { StrandsConditional } from './strands_conditionals' +import * as CFG from './control_flow_graph' +import * as FES from './strands_FES' + +////////////////////////////////////////////// +// User nodes +////////////////////////////////////////////// +export class StrandsNode { + constructor(id) { + this.id = id; + } +} + +export function initGlobalStrandsAPI(p5, fn, strandsContext) { + // We augment the strands node with operations programatically + // this means methods like .add, .sub, etc can be chained + for (const { name, symbol, arity } of OperatorTable) { + if (arity === 'binary') { + StrandsNode.prototype[name] = function (right) { + const id = createBinaryOpNode(strandsContext, this, right, SymbolToOpCode[symbol]); + return new StrandsNode(id); + }; + } + // if (arity === 'unary') { + // StrandsNode.prototype[name] = function () { + // const id = createUnaryExpressionNode(this, SymbolToOpCode[symbol]); + // return new StrandsNode(id); + // }; + // } + } + + ////////////////////////////////////////////// + // Unique Functions + ////////////////////////////////////////////// + fn.discard = function() { + const id = createStatementNode('discard'); + CFG.recordInBasicBlock(strandsContext.cfg, strandsContext.cfg.currentBlock, id); + } + + fn.strandsIf = function(conditionNode, ifBody) { + return new StrandsConditional(strandsContext, conditionNode, ifBody); + } + + fn.strandsLoop = function(a, b, loopBody) { + return null; + } + + fn.strandsNode = function(...args) { + if (args.length > 4) { + FES.userError('type error', "It looks like you've tried to construct a p5.strands node implicitly, with more than 4 components. This is currently not supported.") + } + const id = createTypeConstructorNode(strandsContext, DataType.DEFER, args); + return new StrandsNode(id); + } + + ////////////////////////////////////////////// + // Builtins, uniforms, variable constructors + ////////////////////////////////////////////// + for (const [fnName, overrides] of Object.entries(strandsShaderFunctions)) { + const isp5Function = overrides[0].isp5Function; + + if (isp5Function) { + const originalFn = fn[fnName]; + fn[fnName] = function(...args) { + if (strandsContext.active) { + return createFunctionCallNode(strandsContext, fnName, overrides, args); + } else { + return originalFn.apply(this, args); + } + } + } else { + fn[fnName] = function (...args) { + if (strandsContext.active) { + return createFunctionCallNode(strandsContext, fnName, overrides, args); + } else { + p5._friendlyError( + `It looks like you've called ${fnName} outside of a shader's modify() function.` + ) + } + } + } + } + + // Next is type constructors and uniform functions + for (const typeName in DataType) { + const lowerTypeName = typeName.toLowerCase(); + let pascalTypeName; + if (/^[ib]vec/.test(lowerTypeName)) { + pascalTypeName = lowerTypeName + .slice(0, 2).toUpperCase() + + lowerTypeName + .slice(2) + .toLowerCase(); + } else { + pascalTypeName = lowerTypeName.charAt(0).toUpperCase() + + lowerTypeName.slice(1).toLowerCase(); + } + + fn[`uniform${pascalTypeName}`] = function(...args) { + let [name, ...defaultValue] = args; + const id = createVariableNode(strandsContext, DataType.FLOAT, name); + strandsContext.uniforms.push({ name, dataType: DataType.FLOAT, defaultValue }); + return new StrandsNode(id); + }; + + const typeConstructor = fn[lowerTypeName]; + fn[lowerTypeName] = function(...args) { + if (strandsContext.active) { + const id = createTypeConstructorNode(strandsContext, DataType[typeName], args); + return new StrandsNode(id); + } else if (typeConstructor) { + return typeConstructor.apply(this, args); + } else { + p5._friendlyError( + `It looks like you've called ${lowerTypeName} outside of a shader's modify() function.` + ); + } + } + } +} + +////////////////////////////////////////////// +// Per-Hook functions +////////////////////////////////////////////// +function createHookArguments(strandsContext, parameters){ + const structTypes = ['Vertex', ] + const args = []; + + for (const param of parameters) { + const T = param.type; + if(structTypes.includes(T.typeName)) { + const propertiesNodes = T.properties.map( + (prop) => [prop.name, createVariableNode(strandsContext, DataType[prop.dataType], prop.name)] + ); + const argObject = Object.fromEntries(propertiesNodes); + args.push(argObject); + } else { + const arg = createVariableNode(strandsContext, DataType[param.dataType], param.name); + args.push(arg) + } + } + return args; +} + +export function initShaderHooksFunctions(strandsContext, fn, shader) { + const availableHooks = { + ...shader.hooks.vertex, + ...shader.hooks.fragment, + } + const hookTypes = Object.keys(availableHooks).map(name => shader.hookTypes(name)); + const { cfg } = strandsContext; + for (const hookType of hookTypes) { + window[hookType.name] = 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 rootNodeID = hookUserCallback(args).id; + strandsContext.hooks.push({ + hookType, + entryBlockID, + rootNodeID, + }); + CFG.popBlock(cfg); + } + } +} \ No newline at end of file diff --git a/src/strands/utils.js b/src/strands/utils.js index 66ed42c03f..6f38092381 100644 --- a/src/strands/utils.js +++ b/src/strands/utils.js @@ -1,11 +1,8 @@ ///////////////////// // Enums for nodes // ///////////////////// - export const NodeType = { - // Internal Nodes: OPERATION: 0, - // Leaf Nodes LITERAL: 1, VARIABLE: 2, CONSTANT: 3, @@ -15,7 +12,7 @@ export const NodeType = { export const NodeTypeRequiredFields = { [NodeType.OPERATION]: ['opCode', 'dependsOn'], [NodeType.LITERAL]: ['value'], - [NodeType.VARIABLE]: ['identifier', 'dataType'], + [NodeType.VARIABLE]: ['identifier'], [NodeType.CONSTANT]: ['value'], [NodeType.PHI]: ['dependsOn', 'phiBlocks'] }; @@ -24,6 +21,32 @@ export const NodeTypeToName = Object.fromEntries( Object.entries(NodeType).map(([key, val]) => [val, key]) ); +export const BaseType = { + FLOAT: 'float', + INT: 'int', + BOOl: 'bool', + MAT: 'mat', + DEFER: 'deferred', +}; + +export const AllTypes = [ + 'float1', + 'float2', + 'float3', + 'float4', + 'int1', + 'int2', + 'int3', + 'int4', + 'bool1', + 'bool2', + 'bool3', + 'bool4', + 'mat2x2', + 'mat3x3', + 'mat4x4', +] + export const DataType = { FLOAT: 0, VEC2: 1, @@ -43,8 +66,54 @@ export const DataType = { MAT2X2: 300, MAT3X3: 301, MAT4X4: 302, + + DEFER: 999, +} + +export const DataTypeInfo = { + [DataType.FLOAT]: { base: DataType.FLOAT, dimension: 1, priority: 2 }, + [DataType.VEC2]: { base: DataType.FLOAT, dimension: 2, priority: 2 }, + [DataType.VEC3]: { base: DataType.FLOAT, dimension: 3, priority: 2 }, + [DataType.VEC4]: { base: DataType.FLOAT, dimension: 4, priority: 2 }, + [DataType.INT]: { base: DataType.INT, dimension: 1, priority: 1 }, + [DataType.IVEC2]: { base: DataType.INT, dimension: 2, priority: 1 }, + [DataType.IVEC3]: { base: DataType.INT, dimension: 3, priority: 1 }, + [DataType.IVEC4]: { base: DataType.INT, dimension: 4, priority: 1 }, + [DataType.BOOL]: { base: DataType.BOOL, dimension: 1, priority: 0 }, + [DataType.BVEC2]: { base: DataType.BOOL, dimension: 2, priority: 0 }, + [DataType.BVEC3]: { base: DataType.BOOL, dimension: 3, priority: 0 }, + [DataType.BVEC4]: { base: DataType.BOOL, dimension: 4, priority: 0 }, + [DataType.MAT2]: { base: DataType.FLOAT, dimension: 2, priority: -1 }, + [DataType.MAT3]: { base: DataType.FLOAT, dimension: 3, priority: -1 }, + [DataType.MAT4]: { base: DataType.FLOAT, dimension: 4, priority: -1 }, + + [DataType.DEFER]: { base: DataType.DEFER, dimension: null, priority: -2 }, + [DataType.DEFER]: { base: DataType.DEFER, dimension: null, priority: -2 }, + [DataType.DEFER]: { base: DataType.DEFER, dimension: null, priority: -2 }, + [DataType.DEFER]: { base: DataType.DEFER, dimension: null, priority: -2 }, +}; + +// 2) A separate nested lookup table: +export const DataTypeTable = { + [DataType.FLOAT]: { 1: DataType.FLOAT, 2: DataType.VEC2, 3: DataType.VEC3, 4: DataType.VEC4 }, + [DataType.INT]: { 1: DataType.INT, 2: DataType.IVEC2, 3: DataType.IVEC3, 4: DataType.IVEC4 }, + [DataType.BOOL]: { 1: DataType.BOOL, 2: DataType.BVEC2, 3: DataType.BVEC3, 4: DataType.BVEC4 }, + // [DataType.MAT2]: { 2: DataType.MAT2, 3: DataType.MAT3, 4: DataType.MAT4 }, + [DataType.DEFER]: { 0: DataType.DEFER, 1: DataType.DEFER, 2: DataType.DEFER, 3: DataType.DEFER, 4: DataType.DEFER }, +}; + +export function lookupDataType(baseCode, dim) { + const map = DataTypeTable[baseCode]; + if (!map || map[dim] == null) { + throw new Error(`Invalid type combination: base=${baseCode}, dim=${dim}`); + } + return map[dim]; } +export const DataTypeName = Object.fromEntries( + Object.entries(DataType).map(([key,val])=>[val, key.toLowerCase()]) +); + export const OpCode = { Binary: { ADD: 0, @@ -70,6 +139,7 @@ export const OpCode = { }, Nary: { FUNCTION_CALL: 200, + CONSTRUCTOR: 201, }, ControlFlow: { RETURN: 300, @@ -84,7 +154,7 @@ export const OperatorTable = [ { arity: "unary", name: "neg", symbol: "-", opcode: OpCode.Unary.NEGATE }, { arity: "unary", name: "plus", symbol: "+", opcode: OpCode.Unary.PLUS }, { arity: "binary", name: "add", symbol: "+", opcode: OpCode.Binary.ADD }, - { arity: "binary", name: "min", symbol: "-", opcode: OpCode.Binary.SUBTRACT }, + { arity: "binary", name: "sub", symbol: "-", opcode: OpCode.Binary.SUBTRACT }, { arity: "binary", name: "mult", symbol: "*", opcode: OpCode.Binary.MULTIPLY }, { arity: "binary", name: "div", symbol: "/", opcode: OpCode.Binary.DIVIDE }, { arity: "binary", name: "mod", symbol: "%", opcode: OpCode.Binary.MODULO }, @@ -114,7 +184,6 @@ const BinaryOperations = { "||": (a, b) => a || b, }; - export const SymbolToOpCode = {}; export const OpCodeToSymbol = {}; export const OpCodeArgs = {}; @@ -132,17 +201,34 @@ for (const { arity, symbol, opcode } of OperatorTable) { export const BlockType = { GLOBAL: 0, FUNCTION: 1, - IF_BODY: 2, - ELSE_BODY: 3, - EL_IF_BODY: 4, - CONDITION: 5, - FOR: 6, - MERGE: 7, + IF_COND: 2, + IF_BODY: 3, + ELIF_BODY: 4, + ELIF_COND: 5, + ELSE_BODY: 6, + FOR: 7, + MERGE: 8, + DEFAULT: 9, + } export const BlockTypeToName = Object.fromEntries( Object.entries(BlockType).map(([key, val]) => [val, key]) ); +//////////////////////////// +// Type Checking helpers +//////////////////////////// +export function arrayToFloatType(array) { + let type = false; + if (array.length === 1) { + type = `FLOAT`; + } else if (array.length >= 2 && array.length <= 4) { + type = `VEC${array.length}`; + } else { + throw new Error('Tried to construct a float / vector with and empty array, or more than 4 components!') + } +} + //////////////////////////// // Graph utils //////////////////////////// @@ -155,7 +241,7 @@ export function dfsPostOrder(adjacencyList, start) { return; } visited.add(v); - for (let w of adjacencyList[v].sort((a, b) => b-a) || []) { + for (let w of adjacencyList[v]) { dfs(w); } postOrder.push(v); @@ -163,4 +249,23 @@ export function dfsPostOrder(adjacencyList, start) { dfs(start); return postOrder; +} + +export function dfsReversePostOrder(adjacencyList, start) { + const visited = new Set(); + const postOrder = []; + + function dfs(v) { + if (visited.has(v)) { + return; + } + visited.add(v); + for (let w of adjacencyList[v].sort((a, b) => b-a) || []) { + dfs(w); + } + postOrder.push(v); + } + + dfs(start); + return postOrder.reverse(); } \ No newline at end of file diff --git a/src/webgl/ShaderGenerator.js b/src/webgl/ShaderGenerator.js index dece652561..5cf1ea9b1b 100644 --- a/src/webgl/ShaderGenerator.js +++ b/src/webgl/ShaderGenerator.js @@ -1421,17 +1421,6 @@ function shadergenerator(p5, fn) { return fnNodeConstructor('getTexture', userArgs, props); } - // Generating uniformFloat, uniformVec, createFloat, etc functions - // Maps a GLSL type to the name suffix for method names - const GLSLTypesToIdentifiers = { - int: 'Int', - float: 'Float', - vec2: 'Vector2', - vec3: 'Vector3', - vec4: 'Vector4', - sampler2D: 'Texture', - }; - function dynamicAddSwizzleTrap(node, _size) { if (node.type.startsWith('vec') || _size) { const size = _size ? _size : parseInt(node.type.slice(3)); @@ -1487,6 +1476,17 @@ function shadergenerator(p5, fn) { }, }; + // Generating uniformFloat, uniformVec, createFloat, etc functions + // Maps a GLSL type to the name suffix for method names + const GLSLTypesToIdentifiers = { + int: 'Int', + float: 'Float', + vec2: 'Vector2', + vec3: 'Vector3', + vec4: 'Vector4', + sampler2D: 'Texture', + }; + for (const glslType in GLSLTypesToIdentifiers) { // Generate uniform*() Methods for creating uniforms const typeIdentifier = GLSLTypesToIdentifiers[glslType]; From 7899f0d9f588b9956745f1ee272abf96f6d4de7e Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Wed, 16 Jul 2025 16:42:40 +0100 Subject: [PATCH 34/69] simplify type system --- preview/global/sketch.js | 4 +- src/strands/GLSL_backend.js | 10 +- src/strands/builder.js | 154 +++++++++++++++----------- src/strands/directed_acyclic_graph.js | 10 +- src/strands/user_API.js | 54 ++++----- src/strands/utils.js | 147 ++++++++---------------- 6 files changed, 176 insertions(+), 203 deletions(-) diff --git a/preview/global/sketch.js b/preview/global/sketch.js index e8480e10b4..bd019b77df 100644 --- a/preview/global/sketch.js +++ b/preview/global/sketch.js @@ -3,9 +3,7 @@ p5.disableFriendlyErrors = true; function callback() { getFinalColor((col) => { - - // return vec3(1, 2, 4).add(float(2.0).sub(10)); - return (float(10).sub(10)); + return ivec3(1, 2, 4).mult(2.0, 2, 3); }); } diff --git a/src/strands/GLSL_backend.js b/src/strands/GLSL_backend.js index 1723291280..3813465e38 100644 --- a/src/strands/GLSL_backend.js +++ b/src/strands/GLSL_backend.js @@ -1,4 +1,4 @@ -import { NodeType, OpCodeToSymbol, BlockType, OpCode, DataType, DataTypeName} from "./utils"; +import { NodeType, OpCodeToSymbol, BlockType, OpCode } from "./utils"; import { getNodeDataFromID } from "./directed_acyclic_graph"; import * as FES from './strands_FES' @@ -57,8 +57,8 @@ export const glslBackend = { }).join(', ')}) {`; return firstLine; }, - generateDataTypeName(dataType) { - return DataTypeName[dataType]; + generateDataTypeName(baseType, dimension) { + return baseType + dimension; }, generateDeclaration() { @@ -77,7 +77,7 @@ export const glslBackend = { case NodeType.OPERATION: if (node.opCode === OpCode.Nary.CONSTRUCTOR) { - const T = this.generateDataTypeName(node.dataType); + const T = this.generateDataTypeName(node.baseType, node.dimension); const deps = node.dependsOn.map((dep) => this.generateExpression(dag, dep, generationContext)); return `${T}(${deps.join(', ')})`; } @@ -89,7 +89,7 @@ export const glslBackend = { const left = this.generateExpression(dag, lID, generationContext); const right = this.generateExpression(dag, rID, generationContext); const opSym = OpCodeToSymbol[node.opCode]; - return `${left} ${opSym} ${right}`; + return `(${left} ${opSym} ${right})`; } if (node.dependsOn.length === 1) { const [i] = node.dependsOn; diff --git a/src/strands/builder.js b/src/strands/builder.js index 3459f5f7ed..66c5e32d33 100644 --- a/src/strands/builder.js +++ b/src/strands/builder.js @@ -1,7 +1,7 @@ import * as DAG from './directed_acyclic_graph' import * as CFG from './control_flow_graph' import * as FES from './strands_FES' -import { DataType, DataTypeInfo, NodeType, OpCode, DataTypeName} from './utils'; +import { NodeType, OpCode, BaseType, BasePriority } from './utils'; import { StrandsNode } from './user_API'; ////////////////////////////////////////////// @@ -9,9 +9,15 @@ import { StrandsNode } from './user_API'; ////////////////////////////////////////////// export function createLiteralNode(strandsContext, typeInfo, value) { const { cfg, dag } = strandsContext + let { dimension, baseType } = typeInfo; + + if (dimension !== 1) { + FES.internalError('Created a literal node with dimension > 1.') + } const nodeData = DAG.createNodeData({ nodeType: NodeType.LITERAL, - dataType, + dimension, + baseType, value }); const id = DAG.getOrCreateNode(dag, nodeData); @@ -21,9 +27,11 @@ export function createLiteralNode(strandsContext, typeInfo, value) { export function createVariableNode(strandsContext, typeInfo, identifier) { const { cfg, dag } = strandsContext; + const { dimension, baseType } = typeInfo; const nodeData = DAG.createNodeData({ nodeType: NodeType.VARIABLE, - dataType, + dimension, + baseType, identifier }) const id = DAG.getOrCreateNode(dag, nodeData); @@ -31,71 +39,78 @@ export function createVariableNode(strandsContext, typeInfo, identifier) { return id; } -export function createBinaryOpNode(strandsContext, leftNode, rightArg, opCode) { +function extractTypeInfo(strandsContext, nodeID) { + const dag = strandsContext.dag; + const baseType = dag.baseTypes[nodeID]; + return { + baseType, + dimension: dag.dimensions[nodeID], + priority: BasePriority[baseType], + }; +} + +export function createBinaryOpNode(strandsContext, leftStrandsNode, rightArg, opCode) { const { dag, cfg } = strandsContext; - - let inferRightType, rightNodeID, rightNode; - if (rightArg instanceof StrandsNode) { - rightNode = rightArg; - rightNodeID = rightArg.id; - inferRightType = dag.dataTypes[rightNodeID]; + // Construct a node for right if its just an array or number etc. + let rightStrandsNode; + if (rightArg[0] instanceof StrandsNode && rightArg.length === 1) { + rightStrandsNode = rightArg[0]; } else { - const rightDependsOn = Array.isArray(rightArg) ? rightArg : [rightArg]; - inferRightType = DataType.DEFER; - rightNodeID = createTypeConstructorNode(strandsContext, inferRightType, rightDependsOn); - rightNode = new StrandsNode(rightNodeID); + const id = createTypeConstructorNode(strandsContext, { baseType: BaseType.DEFER, dimension: null }, rightArg); + rightStrandsNode = new StrandsNode(id); } - const origRightType = inferRightType; - const leftNodeID = leftNode.id; - const origLeftType = dag.dataTypes[leftNodeID]; + let finalLeftNodeID = leftStrandsNode.id; + let finalRightNodeID = rightStrandsNode.id; - - const cast = { node: null, toType: origLeftType }; // Check if we have to cast either node - if (origLeftType !== origRightType) { - const L = DataTypeInfo[origLeftType]; - const R = DataTypeInfo[origRightType]; - - if (L.base === DataType.DEFER) { - L.dimension = dag.dependsOn[leftNodeID].length; - } - if (R.base === DataType.DEFER) { - R.dimension = dag.dependsOn[rightNodeID].length; - } + const leftType = extractTypeInfo(strandsContext, leftStrandsNode.id); + const rightType = extractTypeInfo(strandsContext, rightStrandsNode.id); + const cast = { node: null, toType: leftType }; + const bothDeferred = leftType.baseType === rightType.baseType && leftType.baseType === BaseType.DEFER; + + if (bothDeferred) { + finalLeftNodeID = createTypeConstructorNode(strandsContext, { baseType:BaseType.FLOAT, dimension: leftType.dimension }, leftStrandsNode); + finalRightNodeID = createTypeConstructorNode(strandsContext, { baseType:BaseType.FLOAT, dimension: leftType.dimension }, rightStrandsNode); + } + else if (leftType.baseType !== rightType.baseType || + leftType.dimension !== rightType.dimension) { - if (L.dimension === 1 && R.dimension > 1) { + if (leftType.dimension === 1 && rightType.dimension > 1) { // e.g. op(scalar, vector): cast scalar up - cast.node = leftNode; - cast.toType = origRightType; + cast.node = leftStrandsNode; + cast.toType = rightType; } - else if (R.dimension === 1 && L.dimension > 1) { - cast.node = rightNode; - cast.toType = origLeftType; + else if (rightType.dimension === 1 && leftType.dimension > 1) { + cast.node = rightStrandsNode; + cast.toType = leftType; } - else if (L.priority > R.priority && L.dimension === R.dimension) { + else if (leftType.priority > rightType.priority) { // e.g. op(float vector, int vector): cast priority is float > int > bool - cast.node = rightNode; - cast.toType = origLeftType; + cast.node = rightStrandsNode; + cast.toType = leftType; } - else if (R.priority > L.priority && L.dimension === R.dimension) { - cast.node = leftNode; - cast.toType = origRightType; + else if (rightType.priority > leftType.priority) { + cast.node = leftStrandsNode; + cast.toType = rightType; } else { - FES.userError('type error', `A vector of length ${L.dimension} operated with a vector of length ${R.dimension} is not allowed.`); + FES.userError('type error', `A vector of length ${leftType.dimension} operated with a vector of length ${rightType.dimension} is not allowed.`); } + const castedID = createTypeConstructorNode(strandsContext, cast.toType, cast.node); - if (cast.node === leftNode) { - leftNodeID = castedID; + if (cast.node === leftStrandsNode) { + finalLeftNodeID = castedID; } else { - rightNodeID = castedID; + finalRightNodeID = castedID; } } - + const nodeData = DAG.createNodeData({ nodeType: NodeType.OPERATION, - dependsOn: [leftNodeID, rightNodeID], - dataType: cast.toType, + dependsOn: [finalLeftNodeID, finalRightNodeID], + dimension, + baseType: cast.toType.baseType, + dimension: cast.toType.dimension, opCode }); const id = DAG.getOrCreateNode(dag, nodeData); @@ -104,8 +119,9 @@ export function createBinaryOpNode(strandsContext, leftNode, rightArg, opCode) { } function mapConstructorDependencies(strandsContext, typeInfo, dependsOn) { - const mapped = []; - const T = DataTypeInfo[dataType]; + const mappedDependencies = []; + let { dimension, baseType } = typeInfo; + const dag = strandsContext.dag; let calculatedDimensions = 0; @@ -113,40 +129,48 @@ function mapConstructorDependencies(strandsContext, typeInfo, dependsOn) { if (dep instanceof StrandsNode) { const node = DAG.getNodeDataFromID(dag, dep.id); - if (node.opCode === OpCode.Nary.CONSTRUCTOR && dataType === dataType) { + if (node.opCode === OpCode.Nary.CONSTRUCTOR) { for (const inner of node.dependsOn) { - mapped.push(inner); + mappedDependencies.push(inner); } + } else { + mappedDependencies.push(dep.id); } - const depDataType = dag.dataTypes[dep.id]; - calculatedDimensions += DataTypeInfo[depDataType].dimension; + + calculatedDimensions += node.dimension; continue; } if (typeof dep === 'number') { - const newNode = createLiteralNode(strandsContext, T.base, dep); + const newNode = createLiteralNode(strandsContext, { dimension: 1, baseType }, dep); + mappedDependencies.push(newNode); calculatedDimensions += 1; - mapped.push(newNode); continue; } else { FES.userError('type error', `You've tried to construct a scalar or vector type with a non-numeric value: ${dep}`); } } - - if(calculatedDimensions !== 1 && calculatedDimensions !== T.dimension) { - FES.userError('type error', `You've tried to construct a ${DataTypeName[dataType]} with ${calculatedDimensions} components`); + if (dimension === null) { + dimension = calculatedDimensions; + } else if (dimension > calculatedDimensions && calculatedDimensions === 1) { + calculatedDimensions = dimension; + } else if(calculatedDimensions !== 1 && calculatedDimensions !== dimension) { + FES.userError('type error', `You've tried to construct a ${baseType + dimension} with ${calculatedDimensions} components`); } - return mapped; + + return { mappedDependencies, dimension }; } export function createTypeConstructorNode(strandsContext, typeInfo, dependsOn) { const { cfg, dag } = strandsContext; dependsOn = Array.isArray(dependsOn) ? dependsOn : [dependsOn]; - const mappedDependencies = mapConstructorDependencies(strandsContext, dataType, dependsOn); + const { mappedDependencies, dimension } = mapConstructorDependencies(strandsContext, typeInfo, dependsOn); + const nodeData = DAG.createNodeData({ nodeType: NodeType.OPERATION, opCode: OpCode.Nary.CONSTRUCTOR, - dataType, + dimension, + baseType: typeInfo.baseType, dependsOn: mappedDependencies }) const id = DAG.getOrCreateNode(dag, nodeData); @@ -156,14 +180,16 @@ export function createTypeConstructorNode(strandsContext, typeInfo, dependsOn) { export function createFunctionCallNode(strandsContext, identifier, overrides, dependsOn) { const { cfg, dag } = strandsContext; - let dataType = dataType.DEFER; + let typeInfo = { baseType: null, dimension: null }; + const nodeData = DAG.createNodeData({ nodeType: NodeType.OPERATION, opCode: OpCode.Nary.FUNCTION_CALL, identifier, overrides, dependsOn, - dataType + // no type info yet + ...typeInfo, }) const id = DAG.getOrCreateNode(dag, nodeData); CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); diff --git a/src/strands/directed_acyclic_graph.js b/src/strands/directed_acyclic_graph.js index 54232cc5ff..d05c4f6841 100644 --- a/src/strands/directed_acyclic_graph.js +++ b/src/strands/directed_acyclic_graph.js @@ -1,5 +1,5 @@ -import { NodeTypeRequiredFields, NodeTypeToName } from './utils' -import * as FES from './strands_FES' +import { NodeTypeRequiredFields, NodeTypeToName, TypeInfo } from './utils'; +import * as FES from './strands_FES'; ///////////////////////////////// // Public functions for strands runtime @@ -10,7 +10,6 @@ export function createDirectedAcyclicGraph() { nextID: 0, cache: new Map(), nodeTypes: [], - dataTypes: [], baseTypes: [], dimensions: [], opCodes: [], @@ -41,9 +40,8 @@ export function getOrCreateNode(graph, node) { export function createNodeData(data = {}) { const node = { nodeType: data.nodeType ?? null, - dataType: data.dataType ?? null, baseType: data.baseType ?? null, - dimension: data.baseType ?? null, + dimension: data.dimension ?? null, opCode: data.opCode ?? null, value: data.value ?? null, identifier: data.identifier ?? null, @@ -58,7 +56,6 @@ export function createNodeData(data = {}) { export function getNodeDataFromID(graph, id) { return { nodeType: graph.nodeTypes[id], - dataType: graph.dataTypes[id], opCode: graph.opCodes[id], value: graph.values[id], identifier: graph.identifiers[id], @@ -76,7 +73,6 @@ export function getNodeDataFromID(graph, id) { function createNode(graph, node) { const id = graph.nextID++; graph.nodeTypes[id] = node.nodeType; - graph.dataTypes[id] = node.dataType; graph.opCodes[id] = node.opCode; graph.values[id] = node.value; graph.identifiers[id] = node.identifier; diff --git a/src/strands/user_API.js b/src/strands/user_API.js index 3482c57fb4..44c9790aaa 100644 --- a/src/strands/user_API.js +++ b/src/strands/user_API.js @@ -5,7 +5,7 @@ import { createStatementNode, createTypeConstructorNode, } from './builder' -import { DataType, OperatorTable, SymbolToOpCode, BlockType, arrayToFloatType } from './utils' +import { OperatorTable, SymbolToOpCode, BlockType, TypeInfo, BaseType, TypeInfoFromGLSLName } from './utils' import { strandsShaderFunctions } from './shader_functions' import { StrandsConditional } from './strands_conditionals' import * as CFG from './control_flow_graph' @@ -25,7 +25,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { // this means methods like .add, .sub, etc can be chained for (const { name, symbol, arity } of OperatorTable) { if (arity === 'binary') { - StrandsNode.prototype[name] = function (right) { + StrandsNode.prototype[name] = function (...right) { const id = createBinaryOpNode(strandsContext, this, right, SymbolToOpCode[symbol]); return new StrandsNode(id); }; @@ -58,7 +58,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { if (args.length > 4) { FES.userError('type error', "It looks like you've tried to construct a p5.strands node implicitly, with more than 4 components. This is currently not supported.") } - const id = createTypeConstructorNode(strandsContext, DataType.DEFER, args); + const id = createTypeConstructorNode(strandsContext, { baseType: BaseType.DEFER, dimension: null }, args); return new StrandsNode(id); } @@ -91,37 +91,40 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { } // Next is type constructors and uniform functions - for (const typeName in DataType) { - const lowerTypeName = typeName.toLowerCase(); + for (const type in TypeInfo) { + if (type === BaseType.DEFER) { + continue; + } + const typeInfo = TypeInfo[type]; + let pascalTypeName; - if (/^[ib]vec/.test(lowerTypeName)) { - pascalTypeName = lowerTypeName + if (/^[ib]vec/.test(typeInfo.fnName)) { + pascalTypeName = typeInfo.fnName .slice(0, 2).toUpperCase() - + lowerTypeName + + typeInfo.fnName .slice(2) .toLowerCase(); } else { - pascalTypeName = lowerTypeName.charAt(0).toUpperCase() - + lowerTypeName.slice(1).toLowerCase(); + pascalTypeName = typeInfo.fnName.charAt(0).toUpperCase() + + typeInfo.fnName.slice(1).toLowerCase(); } - fn[`uniform${pascalTypeName}`] = function(...args) { - let [name, ...defaultValue] = args; - const id = createVariableNode(strandsContext, DataType.FLOAT, name); - strandsContext.uniforms.push({ name, dataType: DataType.FLOAT, defaultValue }); + fn[`uniform${pascalTypeName}`] = function(name, ...defaultValue) { + const id = createVariableNode(strandsContext, typeInfo, name); + strandsContext.uniforms.push({ name, typeInfo, defaultValue }); return new StrandsNode(id); }; - const typeConstructor = fn[lowerTypeName]; - fn[lowerTypeName] = function(...args) { + const originalp5Fn = fn[typeInfo.fnName]; + fn[typeInfo.fnName] = function(...args) { if (strandsContext.active) { - const id = createTypeConstructorNode(strandsContext, DataType[typeName], args); + const id = createTypeConstructorNode(strandsContext, typeInfo, args); return new StrandsNode(id); - } else if (typeConstructor) { - return typeConstructor.apply(this, args); + } else if (originalp5Fn) { + return originalp5Fn.apply(this, args); } else { p5._friendlyError( - `It looks like you've called ${lowerTypeName} outside of a shader's modify() function.` + `It looks like you've called ${typeInfo.fnName} outside of a shader's modify() function.` ); } } @@ -136,15 +139,16 @@ function createHookArguments(strandsContext, parameters){ const args = []; for (const param of parameters) { - const T = param.type; - if(structTypes.includes(T.typeName)) { - const propertiesNodes = T.properties.map( - (prop) => [prop.name, createVariableNode(strandsContext, DataType[prop.dataType], prop.name)] + const paramType = param.type; + if(structTypes.includes(paramType.typeName)) { + const propertiesNodes = paramType.properties.map( + (prop) => [prop.name, createVariableNode(strandsContext, TypeInfoFromGLSLName[prop.dataType], prop.name)] ); const argObject = Object.fromEntries(propertiesNodes); args.push(argObject); } else { - const arg = createVariableNode(strandsContext, DataType[param.dataType], param.name); + const typeInfo = TypeInfoFromGLSLName[paramType.typeName]; + const arg = createVariableNode(strandsContext, typeInfo, param.name); args.push(arg) } } diff --git a/src/strands/utils.js b/src/strands/utils.js index 6f38092381..07308db711 100644 --- a/src/strands/utils.js +++ b/src/strands/utils.js @@ -9,6 +9,10 @@ export const NodeType = { PHI: 4, }; +export const NodeTypeToName = Object.fromEntries( + Object.entries(NodeType).map(([key, val]) => [val, key]) +); + export const NodeTypeRequiredFields = { [NodeType.OPERATION]: ['opCode', 'dependsOn'], [NodeType.LITERAL]: ['value'], @@ -17,101 +21,49 @@ export const NodeTypeRequiredFields = { [NodeType.PHI]: ['dependsOn', 'phiBlocks'] }; -export const NodeTypeToName = Object.fromEntries( - Object.entries(NodeType).map(([key, val]) => [val, key]) -); - export const BaseType = { FLOAT: 'float', INT: 'int', - BOOl: 'bool', + BOOL: 'bool', MAT: 'mat', - DEFER: 'deferred', + DEFER: 'defer', }; -export const AllTypes = [ - 'float1', - 'float2', - 'float3', - 'float4', - 'int1', - 'int2', - 'int3', - 'int4', - 'bool1', - 'bool2', - 'bool3', - 'bool4', - 'mat2x2', - 'mat3x3', - 'mat4x4', -] - -export const DataType = { - FLOAT: 0, - VEC2: 1, - VEC3: 2, - VEC4: 3, - - INT: 100, - IVEC2: 101, - IVEC3: 102, - IVEC4: 103, - - BOOL: 200, - BVEC2: 201, - BVEC3: 202, - BVEC4: 203, - - MAT2X2: 300, - MAT3X3: 301, - MAT4X4: 302, +export const BasePriority = { + [BaseType.FLOAT]: 3, + [BaseType.INT]: 2, + [BaseType.BOOL]: 1, + [BaseType.MAT]: 0, + [BaseType.DEFER]: -1, +}; - DEFER: 999, -} +export const TypeInfo = { + 'float1': { fnName: 'float', baseType: BaseType.FLOAT, dimension:1, priority: 3, }, + 'float2': { fnName: 'vec2', baseType: BaseType.FLOAT, dimension:2, priority: 3, }, + 'float3': { fnName: 'vec3', baseType: BaseType.FLOAT, dimension:3, priority: 3, }, + 'float4': { fnName: 'vec4', baseType: BaseType.FLOAT, dimension:4, priority: 3, }, -export const DataTypeInfo = { - [DataType.FLOAT]: { base: DataType.FLOAT, dimension: 1, priority: 2 }, - [DataType.VEC2]: { base: DataType.FLOAT, dimension: 2, priority: 2 }, - [DataType.VEC3]: { base: DataType.FLOAT, dimension: 3, priority: 2 }, - [DataType.VEC4]: { base: DataType.FLOAT, dimension: 4, priority: 2 }, - [DataType.INT]: { base: DataType.INT, dimension: 1, priority: 1 }, - [DataType.IVEC2]: { base: DataType.INT, dimension: 2, priority: 1 }, - [DataType.IVEC3]: { base: DataType.INT, dimension: 3, priority: 1 }, - [DataType.IVEC4]: { base: DataType.INT, dimension: 4, priority: 1 }, - [DataType.BOOL]: { base: DataType.BOOL, dimension: 1, priority: 0 }, - [DataType.BVEC2]: { base: DataType.BOOL, dimension: 2, priority: 0 }, - [DataType.BVEC3]: { base: DataType.BOOL, dimension: 3, priority: 0 }, - [DataType.BVEC4]: { base: DataType.BOOL, dimension: 4, priority: 0 }, - [DataType.MAT2]: { base: DataType.FLOAT, dimension: 2, priority: -1 }, - [DataType.MAT3]: { base: DataType.FLOAT, dimension: 3, priority: -1 }, - [DataType.MAT4]: { base: DataType.FLOAT, dimension: 4, priority: -1 }, + 'int1': { fnName: 'int', baseType: BaseType.INT, dimension:1, priority: 2, }, + 'int2': { fnName: 'ivec2', baseType: BaseType.INT, dimension:2, priority: 2, }, + 'int3': { fnName: 'ivec3', baseType: BaseType.INT, dimension:3, priority: 2, }, + 'int4': { fnName: 'ivec4', baseType: BaseType.INT, dimension:4, priority: 2, }, - [DataType.DEFER]: { base: DataType.DEFER, dimension: null, priority: -2 }, - [DataType.DEFER]: { base: DataType.DEFER, dimension: null, priority: -2 }, - [DataType.DEFER]: { base: DataType.DEFER, dimension: null, priority: -2 }, - [DataType.DEFER]: { base: DataType.DEFER, dimension: null, priority: -2 }, -}; + 'bool1': { fnName: 'bool', baseType: BaseType.BOOL, dimension:1, priority: 1, }, + 'bool2': { fnName: 'bvec2', baseType: BaseType.BOOL, dimension:2, priority: 1, }, + 'bool3': { fnName: 'bvec3', baseType: BaseType.BOOL, dimension:3, priority: 1, }, + 'bool4': { fnName: 'bvec4', baseType: BaseType.BOOL, dimension:4, priority: 1, }, -// 2) A separate nested lookup table: -export const DataTypeTable = { - [DataType.FLOAT]: { 1: DataType.FLOAT, 2: DataType.VEC2, 3: DataType.VEC3, 4: DataType.VEC4 }, - [DataType.INT]: { 1: DataType.INT, 2: DataType.IVEC2, 3: DataType.IVEC3, 4: DataType.IVEC4 }, - [DataType.BOOL]: { 1: DataType.BOOL, 2: DataType.BVEC2, 3: DataType.BVEC3, 4: DataType.BVEC4 }, - // [DataType.MAT2]: { 2: DataType.MAT2, 3: DataType.MAT3, 4: DataType.MAT4 }, - [DataType.DEFER]: { 0: DataType.DEFER, 1: DataType.DEFER, 2: DataType.DEFER, 3: DataType.DEFER, 4: DataType.DEFER }, -}; + 'mat2': { fnName: 'mat2x2', baseType: BaseType.MAT, dimension:2, priority: 0, }, + 'mat3': { fnName: 'mat3x3', baseType: BaseType.MAT, dimension:3, priority: 0, }, + 'mat4': { fnName: 'mat4x4', baseType: BaseType.MAT, dimension:4, priority: 0, }, -export function lookupDataType(baseCode, dim) { - const map = DataTypeTable[baseCode]; - if (!map || map[dim] == null) { - throw new Error(`Invalid type combination: base=${baseCode}, dim=${dim}`); - } - return map[dim]; + 'defer': { fnName: null, baseType: BaseType.DEFER, dimension: null, priority: -1 }, } -export const DataTypeName = Object.fromEntries( - Object.entries(DataType).map(([key,val])=>[val, key.toLowerCase()]) +export const TypeInfoFromGLSLName = Object.fromEntries( + Object.values(TypeInfo) + .filter(info => info.fnName !== null) + .map(info => [info.fnName, info]) ); export const OpCode = { @@ -168,20 +120,20 @@ export const OperatorTable = [ { arity: "binary", name: "or", symbol: "||", opcode: OpCode.Binary.LOGICAL_OR }, ]; -const BinaryOperations = { - "+": (a, b) => a + b, - "-": (a, b) => a - b, - "*": (a, b) => a * b, - "/": (a, b) => a / b, - "%": (a, b) => a % b, - "==": (a, b) => a == b, - "!=": (a, b) => a != b, - ">": (a, b) => a > b, - ">=": (a, b) => a >= b, - "<": (a, b) => a < b, - "<=": (a, b) => a <= b, - "&&": (a, b) => a && b, - "||": (a, b) => a || b, +export const ConstantFolding = { + [OpCode.Binary.ADD]: (a, b) => a + b, + [OpCode.Binary.SUBTRACT]: (a, b) => a - b, + [OpCode.Binary.MULTIPLY]: (a, b) => a * b, + [OpCode.Binary.DIVIDE]: (a, b) => a / b, + [OpCode.Binary.MODULO]: (a, b) => a % b, + [OpCode.Binary.EQUAL]: (a, b) => a == b, + [OpCode.Binary.NOT_EQUAL]: (a, b) => a != b, + [OpCode.Binary.GREATER_THAN]: (a, b) => a > b, + [OpCode.Binary.GREATER_EQUAL]: (a, b) => a >= b, + [OpCode.Binary.LESS_THAN]: (a, b) => a < b, + [OpCode.Binary.LESS_EQUAL]: (a, b) => a <= b, + [OpCode.Binary.LOGICAL_AND]: (a, b) => a && b, + [OpCode.Binary.LOGICAL_OR]: (a, b) => a || b, }; export const SymbolToOpCode = {}; @@ -193,9 +145,6 @@ for (const { arity, symbol, opcode } of OperatorTable) { SymbolToOpCode[symbol] = opcode; OpCodeToSymbol[opcode] = symbol; OpCodeArgs[opcode] = args; - if (arity === "binary" && BinaryOperations[symbol]) { - OpCodeToOperation[opcode] = BinaryOperations[symbol]; - } } export const BlockType = { From b731c15da9ef0854a8caa4ca40ab21d2d2adb856 Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Wed, 16 Jul 2025 17:15:23 +0100 Subject: [PATCH 35/69] SSA --- preview/global/sketch.js | 3 ++- src/strands/GLSL_backend.js | 25 +++++++++++++++++-------- src/strands/builder.js | 2 +- src/strands/code_generation.js | 19 +++++++++++-------- 4 files changed, 31 insertions(+), 18 deletions(-) diff --git a/preview/global/sketch.js b/preview/global/sketch.js index bd019b77df..50b003acc9 100644 --- a/preview/global/sketch.js +++ b/preview/global/sketch.js @@ -3,7 +3,8 @@ p5.disableFriendlyErrors = true; function callback() { getFinalColor((col) => { - return ivec3(1, 2, 4).mult(2.0, 2, 3); + let x = vec3(1); + return vec3(1).div(ivec3(1, 2, 4).mult(ivec3(2.0, 2, 3))); }); } diff --git a/src/strands/GLSL_backend.js b/src/strands/GLSL_backend.js index 3813465e38..cb13ac388c 100644 --- a/src/strands/GLSL_backend.js +++ b/src/strands/GLSL_backend.js @@ -4,16 +4,16 @@ import * as FES from './strands_FES' const cfgHandlers = { [BlockType.DEFAULT]: (blockID, strandsContext, generationContext) => { - const { dag, cfg } = strandsContext; + // const { dag, cfg } = strandsContext; - const blockInstructions = new Set(cfg.blockInstructions[blockID] || []); - for (let nodeID of generationContext.dagSorted) { - if (!blockInstructions.has(nodeID)) { - continue; - } + // const blockInstructions = new Set(cfg.blockInstructions[blockID] || []); + // for (let nodeID of generationContext.dagSorted) { + // if (!blockInstructions.has(nodeID)) { + // continue; + // } // const snippet = glslBackend.generateExpression(dag, nodeID, generationContext); // generationContext.write(snippet); - } + // } }, [BlockType.IF_COND](blockID, strandsContext, generationContext) { @@ -76,7 +76,12 @@ export const glslBackend = { return node.identifier; case NodeType.OPERATION: + const useParantheses = node.usedBy.length > 0; if (node.opCode === OpCode.Nary.CONSTRUCTOR) { + if (node.dependsOn.length === 1 && node.dimension === 1) { + console.log("AARK") + return this.generateExpression(dag, node.dependsOn[0], generationContext); + } const T = this.generateDataTypeName(node.baseType, node.dimension); const deps = node.dependsOn.map((dep) => this.generateExpression(dag, dep, generationContext)); return `${T}(${deps.join(', ')})`; @@ -89,7 +94,11 @@ export const glslBackend = { const left = this.generateExpression(dag, lID, generationContext); const right = this.generateExpression(dag, rID, generationContext); const opSym = OpCodeToSymbol[node.opCode]; - return `(${left} ${opSym} ${right})`; + if (useParantheses) { + return `(${left} ${opSym} ${right})`; + } else { + return `${left} ${opSym} ${right}`; + } } if (node.dependsOn.length === 1) { const [i] = node.dependsOn; diff --git a/src/strands/builder.js b/src/strands/builder.js index 66c5e32d33..671870bbd0 100644 --- a/src/strands/builder.js +++ b/src/strands/builder.js @@ -39,7 +39,7 @@ export function createVariableNode(strandsContext, typeInfo, identifier) { return id; } -function extractTypeInfo(strandsContext, nodeID) { +export function extractTypeInfo(strandsContext, nodeID) { const dag = strandsContext.dag; const baseType = dag.baseTypes[nodeID]; return { diff --git a/src/strands/code_generation.js b/src/strands/code_generation.js index b8aba9a642..30f8e47f00 100644 --- a/src/strands/code_generation.js +++ b/src/strands/code_generation.js @@ -1,12 +1,14 @@ import { WEBGL } from '../core/constants'; import { glslBackend } from './GLSL_backend'; import { dfsPostOrder, dfsReversePostOrder, NodeType } from './utils'; +import { extractTypeInfo } from './builder'; let globalTempCounter = 0; let backend; -function generateTopLevelDeclarations(dag, dagOrder) { +function generateTopLevelDeclarations(strandsContext, dagOrder) { const usedCount = {}; + const dag = strandsContext.dag; for (const nodeID of dagOrder) { usedCount[nodeID] = (dag.usedBy[nodeID] || []).length; } @@ -18,13 +20,14 @@ function generateTopLevelDeclarations(dag, dagOrder) { continue; } - // if (usedCount[nodeID] > 1) { - // const tmp = `t${globalTempCounter++}`; - // tempNames[nodeID] = tmp; + if (usedCount[nodeID] > 0) { + const expr = backend.generateExpression(dag, nodeID, { tempNames }); + const tmp = `T${globalTempCounter++}`; + tempNames[nodeID] = tmp; - // const expr = backend.generateExpression(dag, nodeID, {}); - // declarations.push(`float ${tmp} = ${expr};`); - // } + const T = extractTypeInfo(strandsContext, nodeID); + declarations.push(`${T.baseType+T.dimension} ${tmp} = ${expr};`); + } } return { declarations, tempNames }; @@ -42,7 +45,7 @@ export function generateShaderCode(strandsContext) { const cfgSorted = dfsReversePostOrder(cfg.outgoingEdges, entryBlockID); const generationContext = { - ...generateTopLevelDeclarations(dag, dagSorted), + ...generateTopLevelDeclarations(strandsContext, dagSorted), indent: 1, codeLines: [], write(line) { From 7166f3576d06f80e021fa42dcabdcb3e8591a6eb Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Wed, 23 Jul 2025 14:55:32 +0100 Subject: [PATCH 36/69] Return type checking for hooks with native types reimplemented (i.e. not p5 defined structs such as Vertex inputs) --- preview/global/sketch.js | 5 +- src/strands/builder.js | 13 ++++ src/strands/code_generation.js | 8 ++- src/strands/control_flow_graph.js | 19 +++++ src/strands/directed_acyclic_graph.js | 21 +++++- src/strands/p5.strands.js | 4 +- src/strands/user_API.js | 79 +++++++++++++++----- src/strands/utils.js | 100 ++++++-------------------- 8 files changed, 144 insertions(+), 105 deletions(-) diff --git a/preview/global/sketch.js b/preview/global/sketch.js index 50b003acc9..3b16229412 100644 --- a/preview/global/sketch.js +++ b/preview/global/sketch.js @@ -3,8 +3,9 @@ p5.disableFriendlyErrors = true; function callback() { getFinalColor((col) => { - let x = vec3(1); - return vec3(1).div(ivec3(1, 2, 4).mult(ivec3(2.0, 2, 3))); + let x = vec4(1); + // return 1; + return vec4(1).div(ivec4(1).mult(ivec4(2.0, 3.0, 2, 3))); }); } diff --git a/src/strands/builder.js b/src/strands/builder.js index 671870bbd0..a73669753f 100644 --- a/src/strands/builder.js +++ b/src/strands/builder.js @@ -196,6 +196,19 @@ export function createFunctionCallNode(strandsContext, identifier, overrides, de return id; } +export function createUnaryOpNode(strandsContext, strandsNode, opCode) { + const { dag, cfg } = strandsContext; + const nodeData = DAG.createNodeData({ + nodeType: NodeType.OPERATION, + opCode, + dependsOn: strandsNode.id, + baseType: dag.baseTypes[strandsNode.id], + dimension: dag.dimensions[strandsNode.id], + }) + CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); + return id; +} + export function createStatementNode(strandsContext, type) { return -99; } \ No newline at end of file diff --git a/src/strands/code_generation.js b/src/strands/code_generation.js index 30f8e47f00..9d47aff468 100644 --- a/src/strands/code_generation.js +++ b/src/strands/code_generation.js @@ -1,7 +1,9 @@ import { WEBGL } from '../core/constants'; import { glslBackend } from './GLSL_backend'; -import { dfsPostOrder, dfsReversePostOrder, NodeType } from './utils'; +import { NodeType } from './utils'; import { extractTypeInfo } from './builder'; +import { sortCFG } from './control_flow_graph'; +import { sortDAG } from './directed_acyclic_graph'; let globalTempCounter = 0; let backend; @@ -41,8 +43,8 @@ export function generateShaderCode(strandsContext) { for (const { hookType, entryBlockID, rootNodeID} of strandsContext.hooks) { const { cfg, dag } = strandsContext; - const dagSorted = dfsPostOrder(dag.dependsOn, rootNodeID); - const cfgSorted = dfsReversePostOrder(cfg.outgoingEdges, entryBlockID); + const dagSorted = sortDAG(dag.dependsOn, rootNodeID); + const cfgSorted = sortCFG(cfg.outgoingEdges, entryBlockID); const generationContext = { ...generateTopLevelDeclarations(strandsContext, dagSorted), diff --git a/src/strands/control_flow_graph.js b/src/strands/control_flow_graph.js index cee0f0da42..341f62871d 100644 --- a/src/strands/control_flow_graph.js +++ b/src/strands/control_flow_graph.js @@ -59,4 +59,23 @@ export function printBlockData(graph, id) { const block = getBlockDataFromID(graph, id); block.blockType = BlockTypeToName[block.blockType]; console.log(block); +} + +export function sortCFG(adjacencyList, start) { + const visited = new Set(); + const postOrder = []; + + function dfs(v) { + if (visited.has(v)) { + return; + } + visited.add(v); + for (let w of adjacencyList[v].sort((a, b) => b-a) || []) { + dfs(w); + } + postOrder.push(v); + } + + dfs(start); + return postOrder.reverse(); } \ No newline at end of file diff --git a/src/strands/directed_acyclic_graph.js b/src/strands/directed_acyclic_graph.js index d05c4f6841..34b63d919f 100644 --- a/src/strands/directed_acyclic_graph.js +++ b/src/strands/directed_acyclic_graph.js @@ -1,4 +1,4 @@ -import { NodeTypeRequiredFields, NodeTypeToName, TypeInfo } from './utils'; +import { NodeTypeRequiredFields, NodeTypeToName } from './utils'; import * as FES from './strands_FES'; ///////////////////////////////// @@ -113,4 +113,23 @@ function validateNode(node){ if (missingFields.length > 0) { FES.internalError(`Missing fields ${missingFields.join(', ')} for a node type '${NodeTypeToName[nodeType]}'.`); } +} + +export function sortDAG(adjacencyList, start) { + const visited = new Set(); + const postOrder = []; + + function dfs(v) { + if (visited.has(v)) { + return; + } + visited.add(v); + for (let w of adjacencyList[v]) { + dfs(w); + } + postOrder.push(v); + } + + dfs(start); + return postOrder; } \ No newline at end of file diff --git a/src/strands/p5.strands.js b/src/strands/p5.strands.js index 6089c21e18..6d9bc8a0d6 100644 --- a/src/strands/p5.strands.js +++ b/src/strands/p5.strands.js @@ -12,7 +12,7 @@ import { BlockType } from './utils'; import { createDirectedAcyclicGraph } from './directed_acyclic_graph' import { createControlFlowGraph, createBasicBlock, pushBlock, popBlock } from './control_flow_graph'; import { generateShaderCode } from './code_generation'; -import { initGlobalStrandsAPI, initShaderHooksFunctions } from './user_API'; +import { initGlobalStrandsAPI, createShaderHooksFunctions } from './user_API'; function strands(p5, fn) { ////////////////////////////////////////////// @@ -51,7 +51,7 @@ function strands(p5, fn) { // Reset the context object every time modify is called; const backend = WEBGL; initStrandsContext(strandsContext, backend); - initShaderHooksFunctions(strandsContext, fn, this); + createShaderHooksFunctions(strandsContext, fn, this); // 1. Transpile from strands DSL to JS let strandsCallback; diff --git a/src/strands/user_API.js b/src/strands/user_API.js index 44c9790aaa..1ddb7dc6c9 100644 --- a/src/strands/user_API.js +++ b/src/strands/user_API.js @@ -4,8 +4,9 @@ import { createVariableNode, createStatementNode, createTypeConstructorNode, + createUnaryOpNode, } from './builder' -import { OperatorTable, SymbolToOpCode, BlockType, TypeInfo, BaseType, TypeInfoFromGLSLName } from './utils' +import { OperatorTable, BlockType, TypeInfo, BaseType, TypeInfoFromGLSLName } from './utils' import { strandsShaderFunctions } from './shader_functions' import { StrandsConditional } from './strands_conditionals' import * as CFG from './control_flow_graph' @@ -23,19 +24,19 @@ export class StrandsNode { export function initGlobalStrandsAPI(p5, fn, strandsContext) { // We augment the strands node with operations programatically // this means methods like .add, .sub, etc can be chained - for (const { name, symbol, arity } of OperatorTable) { + for (const { name, arity, opCode, symbol } of OperatorTable) { if (arity === 'binary') { StrandsNode.prototype[name] = function (...right) { - const id = createBinaryOpNode(strandsContext, this, right, SymbolToOpCode[symbol]); + const id = createBinaryOpNode(strandsContext, this, right, opCode); return new StrandsNode(id); }; } - // if (arity === 'unary') { - // StrandsNode.prototype[name] = function () { - // const id = createUnaryExpressionNode(this, SymbolToOpCode[symbol]); - // return new StrandsNode(id); - // }; - // } + if (arity === 'unary') { + fn[name] = function (strandsNode) { + const id = createUnaryOpNode(strandsContext, strandsNode, opCode); + return new StrandsNode(id); + } + } } ////////////////////////////////////////////// @@ -134,17 +135,20 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { ////////////////////////////////////////////// // Per-Hook functions ////////////////////////////////////////////// +const structTypes = ['Vertex', ] + function createHookArguments(strandsContext, parameters){ - const structTypes = ['Vertex', ] const args = []; for (const param of parameters) { const paramType = param.type; if(structTypes.includes(paramType.typeName)) { - const propertiesNodes = paramType.properties.map( - (prop) => [prop.name, createVariableNode(strandsContext, TypeInfoFromGLSLName[prop.dataType], prop.name)] - ); - const argObject = Object.fromEntries(propertiesNodes); + const propertyEntries = paramType.properties.map((prop) => { + const typeInfo = TypeInfoFromGLSLName[prop.dataType]; + const variableNode = createVariableNode(strandsContext, typeInfo, prop.name); + return [prop.name, variableNode]; + }); + const argObject = Object.fromEntries(propertyEntries); args.push(argObject); } else { const typeInfo = TypeInfoFromGLSLName[paramType.typeName]; @@ -155,24 +159,63 @@ function createHookArguments(strandsContext, parameters){ return args; } -export function initShaderHooksFunctions(strandsContext, fn, shader) { +export function createShaderHooksFunctions(strandsContext, fn, shader) { const availableHooks = { ...shader.hooks.vertex, ...shader.hooks.fragment, } const hookTypes = Object.keys(availableHooks).map(name => shader.hookTypes(name)); - const { cfg } = strandsContext; + const { cfg, dag } = strandsContext; + for (const hookType of hookTypes) { window[hookType.name] = 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 rootNodeID = hookUserCallback(args).id; + const returned = hookUserCallback(args); + let returnedNode; + + const expectedReturnType = hookType.returnType; + if(structTypes.includes(expectedReturnType.typeName)) { + + } + else { + // In this case we are expecting a native shader type, probably vec4 or vec3. + const expected = TypeInfoFromGLSLName[expectedReturnType.typeName]; + // User may have returned a raw value like [1,1,1,1] or 25. + if (!(returned instanceof StrandsNode)) { + const id = createTypeConstructorNode(strandsContext, { baseType: BaseType.DEFER, dimension: null }, returned); + returnedNode = new StrandsNode(id); + } + else { + returnedNode = returned; + } + + const received = { + baseType: dag.baseTypes[returnedNode.id], + dimension: dag.dimensions[returnedNode.id], + } + if (received.dimension !== expected.dimension) { + if (received.dimension !== 1) { + FES.userError('type error', `You have returned a vector with ${received.dimension} components in ${hookType.name} when a ${expected.baseType + expected.dimension} was expected!`); + } + else { + const newID = createTypeConstructorNode(strandsContext, expected, returnedNode); + returnedNode = new StrandsNode(newID); + } + } + else if (received.baseType !== expected.baseType) { + const newID = createTypeConstructorNode(strandsContext, expected, returnedNode); + returnedNode = new StrandsNode(newID); + } + } + strandsContext.hooks.push({ hookType, entryBlockID, - rootNodeID, + rootNodeID: returnedNode.id, }); CFG.popBlock(cfg); } diff --git a/src/strands/utils.js b/src/strands/utils.js index 07308db711..bcb00c32e5 100644 --- a/src/strands/utils.js +++ b/src/strands/utils.js @@ -102,22 +102,22 @@ export const OpCode = { }; export const OperatorTable = [ - { arity: "unary", name: "not", symbol: "!", opcode: OpCode.Unary.LOGICAL_NOT }, - { arity: "unary", name: "neg", symbol: "-", opcode: OpCode.Unary.NEGATE }, - { arity: "unary", name: "plus", symbol: "+", opcode: OpCode.Unary.PLUS }, - { arity: "binary", name: "add", symbol: "+", opcode: OpCode.Binary.ADD }, - { arity: "binary", name: "sub", symbol: "-", opcode: OpCode.Binary.SUBTRACT }, - { arity: "binary", name: "mult", symbol: "*", opcode: OpCode.Binary.MULTIPLY }, - { arity: "binary", name: "div", symbol: "/", opcode: OpCode.Binary.DIVIDE }, - { arity: "binary", name: "mod", symbol: "%", opcode: OpCode.Binary.MODULO }, - { arity: "binary", name: "equalTo", symbol: "==", opcode: OpCode.Binary.EQUAL }, - { arity: "binary", name: "notEqual", symbol: "!=", opcode: OpCode.Binary.NOT_EQUAL }, - { arity: "binary", name: "greaterThan", symbol: ">", opcode: OpCode.Binary.GREATER_THAN }, - { arity: "binary", name: "greaterEqual", symbol: ">=", opcode: OpCode.Binary.GREATER_EQUAL }, - { arity: "binary", name: "lessThan", symbol: "<", opcode: OpCode.Binary.LESS_THAN }, - { arity: "binary", name: "lessEqual", symbol: "<=", opcode: OpCode.Binary.LESS_EQUAL }, - { arity: "binary", name: "and", symbol: "&&", opcode: OpCode.Binary.LOGICAL_AND }, - { arity: "binary", name: "or", symbol: "||", opcode: OpCode.Binary.LOGICAL_OR }, + { arity: "unary", name: "not", symbol: "!", opCode: OpCode.Unary.LOGICAL_NOT }, + { arity: "unary", name: "neg", symbol: "-", opCode: OpCode.Unary.NEGATE }, + { arity: "unary", name: "plus", symbol: "+", opCode: OpCode.Unary.PLUS }, + { arity: "binary", name: "add", symbol: "+", opCode: OpCode.Binary.ADD }, + { arity: "binary", name: "sub", symbol: "-", opCode: OpCode.Binary.SUBTRACT }, + { arity: "binary", name: "mult", symbol: "*", opCode: OpCode.Binary.MULTIPLY }, + { arity: "binary", name: "div", symbol: "/", opCode: OpCode.Binary.DIVIDE }, + { arity: "binary", name: "mod", symbol: "%", opCode: OpCode.Binary.MODULO }, + { arity: "binary", name: "equalTo", symbol: "==", opCode: OpCode.Binary.EQUAL }, + { arity: "binary", name: "notEqual", symbol: "!=", opCode: OpCode.Binary.NOT_EQUAL }, + { arity: "binary", name: "greaterThan", symbol: ">", opCode: OpCode.Binary.GREATER_THAN }, + { arity: "binary", name: "greaterEqual", symbol: ">=", opCode: OpCode.Binary.GREATER_EQUAL }, + { arity: "binary", name: "lessThan", symbol: "<", opCode: OpCode.Binary.LESS_THAN }, + { arity: "binary", name: "lessEqual", symbol: "<=", opCode: OpCode.Binary.LESS_EQUAL }, + { arity: "binary", name: "and", symbol: "&&", opCode: OpCode.Binary.LOGICAL_AND }, + { arity: "binary", name: "or", symbol: "||", opCode: OpCode.Binary.LOGICAL_OR }, ]; export const ConstantFolding = { @@ -138,13 +138,10 @@ export const ConstantFolding = { export const SymbolToOpCode = {}; export const OpCodeToSymbol = {}; -export const OpCodeArgs = {}; -export const OpCodeToOperation = {}; -for (const { arity, symbol, opcode } of OperatorTable) { - SymbolToOpCode[symbol] = opcode; - OpCodeToSymbol[opcode] = symbol; - OpCodeArgs[opcode] = args; +for (const { symbol, opCode } of OperatorTable) { + SymbolToOpCode[symbol] = opCode; + OpCodeToSymbol[opCode] = symbol; } export const BlockType = { @@ -158,63 +155,8 @@ export const BlockType = { FOR: 7, MERGE: 8, DEFAULT: 9, - } + export const BlockTypeToName = Object.fromEntries( Object.entries(BlockType).map(([key, val]) => [val, key]) -); - -//////////////////////////// -// Type Checking helpers -//////////////////////////// -export function arrayToFloatType(array) { - let type = false; - if (array.length === 1) { - type = `FLOAT`; - } else if (array.length >= 2 && array.length <= 4) { - type = `VEC${array.length}`; - } else { - throw new Error('Tried to construct a float / vector with and empty array, or more than 4 components!') - } -} - -//////////////////////////// -// Graph utils -//////////////////////////// -export function dfsPostOrder(adjacencyList, start) { - const visited = new Set(); - const postOrder = []; - - function dfs(v) { - if (visited.has(v)) { - return; - } - visited.add(v); - for (let w of adjacencyList[v]) { - dfs(w); - } - postOrder.push(v); - } - - dfs(start); - return postOrder; -} - -export function dfsReversePostOrder(adjacencyList, start) { - const visited = new Set(); - const postOrder = []; - - function dfs(v) { - if (visited.has(v)) { - return; - } - visited.add(v); - for (let w of adjacencyList[v].sort((a, b) => b-a) || []) { - dfs(w); - } - postOrder.push(v); - } - - dfs(start); - return postOrder.reverse(); -} \ No newline at end of file +); \ No newline at end of file From e4e54ac3ed49947237e3742fb4b7cef6480d1313 Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Wed, 23 Jul 2025 17:16:40 +0100 Subject: [PATCH 37/69] declarations moved to backend, hook arguments fixed --- preview/global/sketch.js | 11 ++--- src/strands/GLSL_backend.js | 59 +++++++++++++++++++++------ src/strands/builder.js | 12 +----- src/strands/code_generation.js | 25 +++++------- src/strands/directed_acyclic_graph.js | 9 +++- src/strands/p5.strands.js | 9 ++-- src/strands/user_API.js | 7 ++-- 7 files changed, 82 insertions(+), 50 deletions(-) diff --git a/preview/global/sketch.js b/preview/global/sketch.js index 3b16229412..fe768cb428 100644 --- a/preview/global/sketch.js +++ b/preview/global/sketch.js @@ -1,11 +1,9 @@ p5.disableFriendlyErrors = true; function callback() { - getFinalColor((col) => { - let x = vec4(1); - // return 1; - return vec4(1).div(ivec4(1).mult(ivec4(2.0, 3.0, 2, 3))); + let y = col.sub(-1,1,0,0); + return col.add(y); }); } @@ -15,5 +13,8 @@ async function setup(){ } function draw(){ - + orbitControl(); + background(0); + shader(bloomShader); + sphere(100) } diff --git a/src/strands/GLSL_backend.js b/src/strands/GLSL_backend.js index cb13ac388c..c92e3f688f 100644 --- a/src/strands/GLSL_backend.js +++ b/src/strands/GLSL_backend.js @@ -1,7 +1,28 @@ import { NodeType, OpCodeToSymbol, BlockType, OpCode } from "./utils"; -import { getNodeDataFromID } from "./directed_acyclic_graph"; +import { getNodeDataFromID, extractTypeInfo } from "./directed_acyclic_graph"; import * as FES from './strands_FES' +const TypeNames = { + 'float1': 'float', + 'float2': 'vec2', + 'float3': 'vec3', + 'float4': 'vec4', + + 'int1': 'int', + 'int2': 'ivec2', + 'int3': 'ivec3', + 'int4': 'ivec4', + + 'bool1': 'bool', + 'bool2': 'bvec2', + 'bool3': 'bvec3', + 'bool4': 'bvec4', + + 'mat2': 'mat2x2', + 'mat3': 'mat3x3', + 'mat4': 'mat4x4', +} + const cfgHandlers = { [BlockType.DEFAULT]: (blockID, strandsContext, generationContext) => { // const { dag, cfg } = strandsContext; @@ -19,7 +40,7 @@ const cfgHandlers = { [BlockType.IF_COND](blockID, strandsContext, generationContext) { const { dag, cfg } = strandsContext; const conditionID = cfg.blockConditions[blockID]; - const condExpr = glslBackend.generateExpression (dag, conditionID, generationContext); + const condExpr = glslBackend.generateExpression(generationContext, dag, conditionID); generationContext.write(`if (${condExpr}) {`) generationContext.indent++; this[BlockType.DEFAULT](blockID, strandsContext, generationContext); @@ -57,13 +78,26 @@ export const glslBackend = { }).join(', ')}) {`; return firstLine; }, - generateDataTypeName(baseType, dimension) { - return baseType + dimension; + + getTypeName(baseType, dimension) { + return TypeNames[baseType + dimension] }, - generateDeclaration() { + + generateDeclaration(generationContext, dag, nodeID) { + const expr = this.generateExpression(generationContext, dag, nodeID); + const tmp = `T${generationContext.nextTempID++}`; + generationContext.tempNames[nodeID] = tmp; + const T = extractTypeInfo(dag, nodeID); + const typeName = this.getTypeName(T.baseType, T.dimension); + return `${typeName} ${tmp} = ${expr};`; + }, + + generateReturn(generationContext, dag, nodeID) { + }, - generateExpression(dag, nodeID, generationContext) { + + generateExpression(generationContext, dag, nodeID) { const node = getNodeDataFromID(dag, nodeID); if (generationContext.tempNames?.[nodeID]) { return generationContext.tempNames[nodeID]; @@ -80,10 +114,10 @@ export const glslBackend = { if (node.opCode === OpCode.Nary.CONSTRUCTOR) { if (node.dependsOn.length === 1 && node.dimension === 1) { console.log("AARK") - return this.generateExpression(dag, node.dependsOn[0], generationContext); + return this.generateExpression(generationContext, dag, node.dependsOn[0]); } - const T = this.generateDataTypeName(node.baseType, node.dimension); - const deps = node.dependsOn.map((dep) => this.generateExpression(dag, dep, generationContext)); + const T = this.getTypeName(node.baseType, node.dimension); + const deps = node.dependsOn.map((dep) => this.generateExpression(generationContext, dag, dep)); return `${T}(${deps.join(', ')})`; } if (node.opCode === OpCode.Nary.FUNCTION) { @@ -91,8 +125,8 @@ export const glslBackend = { } if (node.dependsOn.length === 2) { const [lID, rID] = node.dependsOn; - const left = this.generateExpression(dag, lID, generationContext); - const right = this.generateExpression(dag, rID, generationContext); + const left = this.generateExpression(generationContext, dag, lID); + const right = this.generateExpression(generationContext, dag, rID); const opSym = OpCodeToSymbol[node.opCode]; if (useParantheses) { return `(${left} ${opSym} ${right})`; @@ -102,7 +136,7 @@ export const glslBackend = { } if (node.dependsOn.length === 1) { const [i] = node.dependsOn; - const val = this.generateExpression(dag, i, generationContext); + const val = this.generateExpression(generationContext, dag, i); const sym = OpCodeToSymbol[node.opCode]; return `${sym}${val}`; } @@ -111,6 +145,7 @@ export const glslBackend = { FES.internalError(`${node.nodeType} not working yet`) } }, + generateBlock(blockID, strandsContext, generationContext) { const type = strandsContext.cfg.blockTypes[blockID]; const handler = cfgHandlers[type] || cfgHandlers[BlockType.DEFAULT]; diff --git a/src/strands/builder.js b/src/strands/builder.js index a73669753f..b1121bba1c 100644 --- a/src/strands/builder.js +++ b/src/strands/builder.js @@ -1,7 +1,7 @@ import * as DAG from './directed_acyclic_graph' import * as CFG from './control_flow_graph' import * as FES from './strands_FES' -import { NodeType, OpCode, BaseType, BasePriority } from './utils'; +import { NodeType, OpCode, BaseType, extractTypeInfo } from './utils'; import { StrandsNode } from './user_API'; ////////////////////////////////////////////// @@ -39,16 +39,6 @@ export function createVariableNode(strandsContext, typeInfo, identifier) { return id; } -export function extractTypeInfo(strandsContext, nodeID) { - const dag = strandsContext.dag; - const baseType = dag.baseTypes[nodeID]; - return { - baseType, - dimension: dag.dimensions[nodeID], - priority: BasePriority[baseType], - }; -} - export function createBinaryOpNode(strandsContext, leftStrandsNode, rightArg, opCode) { const { dag, cfg } = strandsContext; // Construct a node for right if its just an array or number etc. diff --git a/src/strands/code_generation.js b/src/strands/code_generation.js index 9d47aff468..d807797499 100644 --- a/src/strands/code_generation.js +++ b/src/strands/code_generation.js @@ -1,21 +1,19 @@ import { WEBGL } from '../core/constants'; import { glslBackend } from './GLSL_backend'; import { NodeType } from './utils'; -import { extractTypeInfo } from './builder'; import { sortCFG } from './control_flow_graph'; import { sortDAG } from './directed_acyclic_graph'; let globalTempCounter = 0; let backend; -function generateTopLevelDeclarations(strandsContext, dagOrder) { +function generateTopLevelDeclarations(strandsContext, generationContext, dagOrder) { const usedCount = {}; const dag = strandsContext.dag; for (const nodeID of dagOrder) { usedCount[nodeID] = (dag.usedBy[nodeID] || []).length; } - const tempNames = {}; const declarations = []; for (const nodeID of dagOrder) { if (dag.nodeTypes[nodeID] !== NodeType.OPERATION) { @@ -23,16 +21,12 @@ function generateTopLevelDeclarations(strandsContext, dagOrder) { } if (usedCount[nodeID] > 0) { - const expr = backend.generateExpression(dag, nodeID, { tempNames }); - const tmp = `T${globalTempCounter++}`; - tempNames[nodeID] = tmp; - - const T = extractTypeInfo(strandsContext, nodeID); - declarations.push(`${T.baseType+T.dimension} ${tmp} = ${expr};`); + const newDeclaration = backend.generateDeclaration(generationContext, dag, nodeID); + declarations.push(newDeclaration); } } - return { declarations, tempNames }; + return declarations; } export function generateShaderCode(strandsContext) { @@ -47,14 +41,18 @@ export function generateShaderCode(strandsContext) { const cfgSorted = sortCFG(cfg.outgoingEdges, entryBlockID); const generationContext = { - ...generateTopLevelDeclarations(strandsContext, dagSorted), indent: 1, codeLines: [], write(line) { this.codeLines.push(' '.repeat(this.indent) + line); }, dagSorted, + tempNames: {}, + declarations: [], + nextTempID: 0, }; + generationContext.declarations = generateTopLevelDeclarations(strandsContext, generationContext, dagSorted); + generationContext.declarations.forEach(decl => generationContext.write(decl)); for (const blockID of cfgSorted) { @@ -62,10 +60,9 @@ export function generateShaderCode(strandsContext) { } const firstLine = backend.hookEntry(hookType); - const finalExpression = `return ${backend.generateExpression(dag, rootNodeID, generationContext)};`; + const finalExpression = `return ${backend.generateExpression(generationContext, dag, rootNodeID)};`; generationContext.write(finalExpression); - console.log(hookType); - hooksObj[hookType.name] = [firstLine, ...generationContext.codeLines, '}'].join('\n'); + hooksObj[`${hookType.returnType.typeName} ${hookType.name}`] = [firstLine, ...generationContext.codeLines, '}'].join('\n'); } return hooksObj; diff --git a/src/strands/directed_acyclic_graph.js b/src/strands/directed_acyclic_graph.js index 34b63d919f..5c5200438e 100644 --- a/src/strands/directed_acyclic_graph.js +++ b/src/strands/directed_acyclic_graph.js @@ -1,4 +1,4 @@ -import { NodeTypeRequiredFields, NodeTypeToName } from './utils'; +import { NodeTypeRequiredFields, NodeTypeToName, BasePriority } from './utils'; import * as FES from './strands_FES'; ///////////////////////////////// @@ -67,6 +67,13 @@ export function getNodeDataFromID(graph, id) { } } +export function extractTypeInfo(dag, nodeID) { + return { + baseType: dag.baseTypes[nodeID], + dimension: dag.dimensions[nodeID], + priority: BasePriority[dag.baseTypes[nodeID]], + }; +} ///////////////////////////////// // Private functions ///////////////////////////////// diff --git a/src/strands/p5.strands.js b/src/strands/p5.strands.js index 6d9bc8a0d6..77f9d8b73a 100644 --- a/src/strands/p5.strands.js +++ b/src/strands/p5.strands.js @@ -70,13 +70,14 @@ function strands(p5, fn) { // 3. Generate shader code hooks object from the IR // ....... const hooksObject = generateShaderCode(strandsContext); - console.log(hooksObject.getFinalColor); - - // Call modify with the generated hooks object - // return oldModify.call(this, generatedModifyArgument); + console.log(hooksObject); + console.log(hooksObject['vec4 getFinalColor']); // Reset the strands runtime context // deinitStrandsContext(strandsContext); + + // Call modify with the generated hooks object + return oldModify.call(this, hooksObject); } else { return oldModify.call(this, shaderModifier) diff --git a/src/strands/user_API.js b/src/strands/user_API.js index 1ddb7dc6c9..08ddaf8237 100644 --- a/src/strands/user_API.js +++ b/src/strands/user_API.js @@ -152,8 +152,9 @@ function createHookArguments(strandsContext, parameters){ args.push(argObject); } else { const typeInfo = TypeInfoFromGLSLName[paramType.typeName]; - const arg = createVariableNode(strandsContext, typeInfo, param.name); - args.push(arg) + const id = createVariableNode(strandsContext, typeInfo, param.name); + const arg = new StrandsNode(id); + args.push(arg); } } return args; @@ -174,7 +175,7 @@ export function createShaderHooksFunctions(strandsContext, fn, shader) { CFG.pushBlock(cfg, entryBlockID); const args = createHookArguments(strandsContext, hookType.parameters); - const returned = hookUserCallback(args); + const returned = hookUserCallback(...args); let returnedNode; const expectedReturnType = hookType.returnType; From 51e8ddd7bae969903dfe890dc634830008cbae1e Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Thu, 24 Jul 2025 14:27:17 +0100 Subject: [PATCH 38/69] rename file --- src/strands/{user_API.js => strands_api.js} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/strands/{user_API.js => strands_api.js} (100%) diff --git a/src/strands/user_API.js b/src/strands/strands_api.js similarity index 100% rename from src/strands/user_API.js rename to src/strands/strands_api.js From 79c2f8d86eed6327236fe3aa6f6e44d3e049cc25 Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Thu, 24 Jul 2025 14:27:51 +0100 Subject: [PATCH 39/69] update api imports for new filename --- src/strands/builder.js | 4 ++-- src/strands/p5.strands.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/strands/builder.js b/src/strands/builder.js index b1121bba1c..421fa9bdb5 100644 --- a/src/strands/builder.js +++ b/src/strands/builder.js @@ -1,8 +1,8 @@ import * as DAG from './directed_acyclic_graph' import * as CFG from './control_flow_graph' import * as FES from './strands_FES' -import { NodeType, OpCode, BaseType, extractTypeInfo } from './utils'; -import { StrandsNode } from './user_API'; +import { NodeType, OpCode, BaseType } from './utils'; +import { StrandsNode } from './strands_api'; ////////////////////////////////////////////// // Builders for node graphs diff --git a/src/strands/p5.strands.js b/src/strands/p5.strands.js index 77f9d8b73a..a3e85ac945 100644 --- a/src/strands/p5.strands.js +++ b/src/strands/p5.strands.js @@ -12,7 +12,7 @@ import { BlockType } from './utils'; import { createDirectedAcyclicGraph } from './directed_acyclic_graph' import { createControlFlowGraph, createBasicBlock, pushBlock, popBlock } from './control_flow_graph'; import { generateShaderCode } from './code_generation'; -import { initGlobalStrandsAPI, createShaderHooksFunctions } from './user_API'; +import { initGlobalStrandsAPI, createShaderHooksFunctions } from './strands_api'; function strands(p5, fn) { ////////////////////////////////////////////// From 18dc1d3145bbfb74faaf627325bfb0ea0c281169 Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Thu, 24 Jul 2025 14:28:45 +0100 Subject: [PATCH 40/69] move extractTypeInfo and rename to extractNodeTypeInfo --- src/strands/GLSL_backend.js | 4 ++-- src/strands/builder.js | 4 ++-- src/strands/directed_acyclic_graph.js | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/strands/GLSL_backend.js b/src/strands/GLSL_backend.js index c92e3f688f..d921aed364 100644 --- a/src/strands/GLSL_backend.js +++ b/src/strands/GLSL_backend.js @@ -1,5 +1,5 @@ import { NodeType, OpCodeToSymbol, BlockType, OpCode } from "./utils"; -import { getNodeDataFromID, extractTypeInfo } from "./directed_acyclic_graph"; +import { getNodeDataFromID, extractNodeTypeInfo } from "./directed_acyclic_graph"; import * as FES from './strands_FES' const TypeNames = { @@ -88,7 +88,7 @@ export const glslBackend = { const tmp = `T${generationContext.nextTempID++}`; generationContext.tempNames[nodeID] = tmp; - const T = extractTypeInfo(dag, nodeID); + const T = extractNodeTypeInfo(dag, nodeID); const typeName = this.getTypeName(T.baseType, T.dimension); return `${typeName} ${tmp} = ${expr};`; }, diff --git a/src/strands/builder.js b/src/strands/builder.js index 421fa9bdb5..b5e12ebeca 100644 --- a/src/strands/builder.js +++ b/src/strands/builder.js @@ -53,8 +53,8 @@ export function createBinaryOpNode(strandsContext, leftStrandsNode, rightArg, op let finalRightNodeID = rightStrandsNode.id; // Check if we have to cast either node - const leftType = extractTypeInfo(strandsContext, leftStrandsNode.id); - const rightType = extractTypeInfo(strandsContext, rightStrandsNode.id); + const leftType = DAG.extractNodeTypeInfo(dag, leftStrandsNode.id); + const rightType = DAG.extractNodeTypeInfo(dag, rightStrandsNode.id); const cast = { node: null, toType: leftType }; const bothDeferred = leftType.baseType === rightType.baseType && leftType.baseType === BaseType.DEFER; diff --git a/src/strands/directed_acyclic_graph.js b/src/strands/directed_acyclic_graph.js index 5c5200438e..5efc98080f 100644 --- a/src/strands/directed_acyclic_graph.js +++ b/src/strands/directed_acyclic_graph.js @@ -67,7 +67,7 @@ export function getNodeDataFromID(graph, id) { } } -export function extractTypeInfo(dag, nodeID) { +export function extractNodeTypeInfo(dag, nodeID) { return { baseType: dag.baseTypes[nodeID], dimension: dag.dimensions[nodeID], From eb5f1bf2dab1d32dc4a0f3204cb09860b66a7055 Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Thu, 24 Jul 2025 14:58:37 +0100 Subject: [PATCH 41/69] rename files for clarity --- preview/global/sketch.js | 11 +++- src/strands/{builder.js => ir_builders.js} | 7 +-- .../{control_flow_graph.js => ir_cfg.js} | 2 +- .../{directed_acyclic_graph.js => ir_dag.js} | 54 +++++++++-------- src/strands/{utils.js => ir_types.js} | 58 +++++++++---------- src/strands/p5.strands.js | 15 ++--- src/strands/strands_api.js | 8 +-- ...hader_functions.js => strands_builtins.js} | 18 +++--- ...{code_generation.js => strands_codegen.js} | 20 +++---- src/strands/strands_conditionals.js | 4 +- ...GLSL_backend.js => strands_glslBackend.js} | 4 +- ...de_transpiler.js => strands_transpiler.js} | 0 12 files changed, 99 insertions(+), 102 deletions(-) rename src/strands/{builder.js => ir_builders.js} (97%) rename src/strands/{control_flow_graph.js => ir_cfg.js} (97%) rename src/strands/{directed_acyclic_graph.js => ir_dag.js} (73%) rename src/strands/{utils.js => ir_types.js} (68%) rename src/strands/{shader_functions.js => strands_builtins.js} (86%) rename src/strands/{code_generation.js => strands_codegen.js} (81%) rename src/strands/{GLSL_backend.js => strands_glslBackend.js} (96%) rename src/strands/{code_transpiler.js => strands_transpiler.js} (100%) diff --git a/preview/global/sketch.js b/preview/global/sketch.js index fe768cb428..208260102a 100644 --- a/preview/global/sketch.js +++ b/preview/global/sketch.js @@ -3,18 +3,23 @@ p5.disableFriendlyErrors = true; function callback() { getFinalColor((col) => { let y = col.sub(-1,1,0,0); - return col.add(y); + + return y//mix(0, col.add(y), 1); }); } async function setup(){ - createCanvas(300,400, WEBGL) + createCanvas(windowWidth,windowHeight, WEBGL) bloomShader = baseColorShader().newModify(callback, {parser: false}); } +function windowResized() { + resizeCanvas(windowWidth, windowHeight); +} + function draw(){ orbitControl(); background(0); shader(bloomShader); - sphere(100) + sphere(300) } diff --git a/src/strands/builder.js b/src/strands/ir_builders.js similarity index 97% rename from src/strands/builder.js rename to src/strands/ir_builders.js index b5e12ebeca..2acd29b986 100644 --- a/src/strands/builder.js +++ b/src/strands/ir_builders.js @@ -1,7 +1,7 @@ -import * as DAG from './directed_acyclic_graph' -import * as CFG from './control_flow_graph' +import * as DAG from './ir_dag' +import * as CFG from './ir_cfg' import * as FES from './strands_FES' -import { NodeType, OpCode, BaseType } from './utils'; +import { NodeType, OpCode, BaseType } from './ir_types'; import { StrandsNode } from './strands_api'; ////////////////////////////////////////////// @@ -57,7 +57,6 @@ export function createBinaryOpNode(strandsContext, leftStrandsNode, rightArg, op const rightType = DAG.extractNodeTypeInfo(dag, rightStrandsNode.id); const cast = { node: null, toType: leftType }; const bothDeferred = leftType.baseType === rightType.baseType && leftType.baseType === BaseType.DEFER; - if (bothDeferred) { finalLeftNodeID = createTypeConstructorNode(strandsContext, { baseType:BaseType.FLOAT, dimension: leftType.dimension }, leftStrandsNode); finalRightNodeID = createTypeConstructorNode(strandsContext, { baseType:BaseType.FLOAT, dimension: leftType.dimension }, rightStrandsNode); diff --git a/src/strands/control_flow_graph.js b/src/strands/ir_cfg.js similarity index 97% rename from src/strands/control_flow_graph.js rename to src/strands/ir_cfg.js index 341f62871d..27a323b885 100644 --- a/src/strands/control_flow_graph.js +++ b/src/strands/ir_cfg.js @@ -1,4 +1,4 @@ -import { BlockTypeToName } from "./utils"; +import { BlockTypeToName } from "./ir_types"; export function createControlFlowGraph() { return { diff --git a/src/strands/directed_acyclic_graph.js b/src/strands/ir_dag.js similarity index 73% rename from src/strands/directed_acyclic_graph.js rename to src/strands/ir_dag.js index 5efc98080f..ae384aa346 100644 --- a/src/strands/directed_acyclic_graph.js +++ b/src/strands/ir_dag.js @@ -1,4 +1,4 @@ -import { NodeTypeRequiredFields, NodeTypeToName, BasePriority } from './utils'; +import { NodeTypeRequiredFields, NodeTypeToName, BasePriority } from './ir_types'; import * as FES from './strands_FES'; ///////////////////////////////// @@ -39,15 +39,15 @@ export function getOrCreateNode(graph, node) { export function createNodeData(data = {}) { const node = { - nodeType: data.nodeType ?? null, - baseType: data.baseType ?? null, - dimension: data.dimension ?? null, - opCode: data.opCode ?? null, - value: data.value ?? null, + nodeType: data.nodeType ?? null, + baseType: data.baseType ?? null, + dimension: data.dimension ?? null, + opCode: data.opCode ?? null, + value: data.value ?? null, identifier: data.identifier ?? null, - dependsOn: Array.isArray(data.dependsOn) ? data.dependsOn : [], + dependsOn: Array.isArray(data.dependsOn) ? data.dependsOn : [], usedBy: Array.isArray(data.usedBy) ? data.usedBy : [], - phiBlocks: Array.isArray(data.phiBlocks) ? data.phiBlocks : [], + phiBlocks: Array.isArray(data.phiBlocks) ? data.phiBlocks : [], }; validateNode(node); return node; @@ -55,15 +55,15 @@ export function createNodeData(data = {}) { export function getNodeDataFromID(graph, id) { return { - nodeType: graph.nodeTypes[id], - opCode: graph.opCodes[id], - value: graph.values[id], + nodeType: graph.nodeTypes[id], + opCode: graph.opCodes[id], + value: graph.values[id], identifier: graph.identifiers[id], - dependsOn: graph.dependsOn[id], - usedBy: graph.usedBy[id], - phiBlocks: graph.phiBlocks[id], - dimension: graph.dimensions[id], - baseType: graph.baseTypes[id], + dependsOn: graph.dependsOn[id], + usedBy: graph.usedBy[id], + phiBlocks: graph.phiBlocks[id], + dimension: graph.dimensions[id], + baseType: graph.baseTypes[id], } } @@ -79,18 +79,16 @@ export function extractNodeTypeInfo(dag, nodeID) { ///////////////////////////////// function createNode(graph, node) { const id = graph.nextID++; - graph.nodeTypes[id] = node.nodeType; - graph.opCodes[id] = node.opCode; - graph.values[id] = node.value; + graph.nodeTypes[id] = node.nodeType; + graph.opCodes[id] = node.opCode; + graph.values[id] = node.value; graph.identifiers[id] = node.identifier; - graph.dependsOn[id] = node.dependsOn.slice(); - graph.usedBy[id] = node.usedBy; - graph.phiBlocks[id] = node.phiBlocks.slice(); - - graph.baseTypes[id] = node.baseType - graph.dimensions[id] = node.dimension; - - + graph.dependsOn[id] = node.dependsOn.slice(); + graph.usedBy[id] = node.usedBy; + graph.phiBlocks[id] = node.phiBlocks.slice(); + graph.baseTypes[id] = node.baseType + graph.dimensions[id] = node.dimension; + for (const dep of node.dependsOn) { if (!Array.isArray(graph.usedBy[dep])) { graph.usedBy[dep] = []; @@ -125,7 +123,7 @@ function validateNode(node){ export function sortDAG(adjacencyList, start) { const visited = new Set(); const postOrder = []; - + function dfs(v) { if (visited.has(v)) { return; diff --git a/src/strands/utils.js b/src/strands/ir_types.js similarity index 68% rename from src/strands/utils.js rename to src/strands/ir_types.js index bcb00c32e5..f84a2e8aa9 100644 --- a/src/strands/utils.js +++ b/src/strands/ir_types.js @@ -14,19 +14,19 @@ export const NodeTypeToName = Object.fromEntries( ); export const NodeTypeRequiredFields = { - [NodeType.OPERATION]: ['opCode', 'dependsOn'], - [NodeType.LITERAL]: ['value'], - [NodeType.VARIABLE]: ['identifier'], - [NodeType.CONSTANT]: ['value'], - [NodeType.PHI]: ['dependsOn', 'phiBlocks'] + [NodeType.OPERATION]: ["opCode", "dependsOn"], + [NodeType.LITERAL]: ["value"], + [NodeType.VARIABLE]: ["identifier"], + [NodeType.CONSTANT]: ["value"], + [NodeType.PHI]: ["dependsOn", "phiBlocks"] }; export const BaseType = { - FLOAT: 'float', - INT: 'int', - BOOL: 'bool', - MAT: 'mat', - DEFER: 'defer', + FLOAT: "float", + INT: "int", + BOOL: "bool", + MAT: "mat", + DEFER: "defer", }; export const BasePriority = { @@ -38,26 +38,26 @@ export const BasePriority = { }; export const TypeInfo = { - 'float1': { fnName: 'float', baseType: BaseType.FLOAT, dimension:1, priority: 3, }, - 'float2': { fnName: 'vec2', baseType: BaseType.FLOAT, dimension:2, priority: 3, }, - 'float3': { fnName: 'vec3', baseType: BaseType.FLOAT, dimension:3, priority: 3, }, - 'float4': { fnName: 'vec4', baseType: BaseType.FLOAT, dimension:4, priority: 3, }, - - 'int1': { fnName: 'int', baseType: BaseType.INT, dimension:1, priority: 2, }, - 'int2': { fnName: 'ivec2', baseType: BaseType.INT, dimension:2, priority: 2, }, - 'int3': { fnName: 'ivec3', baseType: BaseType.INT, dimension:3, priority: 2, }, - 'int4': { fnName: 'ivec4', baseType: BaseType.INT, dimension:4, priority: 2, }, - - 'bool1': { fnName: 'bool', baseType: BaseType.BOOL, dimension:1, priority: 1, }, - 'bool2': { fnName: 'bvec2', baseType: BaseType.BOOL, dimension:2, priority: 1, }, - 'bool3': { fnName: 'bvec3', baseType: BaseType.BOOL, dimension:3, priority: 1, }, - 'bool4': { fnName: 'bvec4', baseType: BaseType.BOOL, dimension:4, priority: 1, }, - - 'mat2': { fnName: 'mat2x2', baseType: BaseType.MAT, dimension:2, priority: 0, }, - 'mat3': { fnName: 'mat3x3', baseType: BaseType.MAT, dimension:3, priority: 0, }, - 'mat4': { fnName: 'mat4x4', baseType: BaseType.MAT, dimension:4, priority: 0, }, + float1: { fnName: "float", baseType: BaseType.FLOAT, dimension:1, priority: 3, }, + float2: { fnName: "vec2", baseType: BaseType.FLOAT, dimension:2, priority: 3, }, + float3: { fnName: "vec3", baseType: BaseType.FLOAT, dimension:3, priority: 3, }, + float4: { fnName: "vec4", baseType: BaseType.FLOAT, dimension:4, priority: 3, }, + int1: { fnName: "int", baseType: BaseType.INT, dimension:1, priority: 2, }, + int2: { fnName: "ivec2", baseType: BaseType.INT, dimension:2, priority: 2, }, + int3: { fnName: "ivec3", baseType: BaseType.INT, dimension:3, priority: 2, }, + int4: { fnName: "ivec4", baseType: BaseType.INT, dimension:4, priority: 2, }, + bool1: { fnName: "bool", baseType: BaseType.BOOL, dimension:1, priority: 1, }, + bool2: { fnName: "bvec2", baseType: BaseType.BOOL, dimension:2, priority: 1, }, + bool3: { fnName: "bvec3", baseType: BaseType.BOOL, dimension:3, priority: 1, }, + bool4: { fnName: "bvec4", baseType: BaseType.BOOL, dimension:4, priority: 1, }, + mat2: { fnName: "mat2x2", baseType: BaseType.MAT, dimension:2, priority: 0, }, + 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 }, +} - 'defer': { fnName: null, baseType: BaseType.DEFER, dimension: null, priority: -1 }, +export function typeEquals(nodeA, nodeB) { + return (nodeA.dimension === nodeB.dimension) && (nodeA.baseType === nodeB.baseType); } export const TypeInfoFromGLSLName = Object.fromEntries( diff --git a/src/strands/p5.strands.js b/src/strands/p5.strands.js index a3e85ac945..0c31a499ff 100644 --- a/src/strands/p5.strands.js +++ b/src/strands/p5.strands.js @@ -5,13 +5,14 @@ * @requires core */ import { WEBGL, /*WEBGPU*/ } from '../core/constants' +import { glslBackend } from './strands_glslBackend'; -import { transpileStrandsToJS } from './code_transpiler'; -import { BlockType } from './utils'; +import { transpileStrandsToJS } from './strands_transpiler'; +import { BlockType } from './ir_types'; -import { createDirectedAcyclicGraph } from './directed_acyclic_graph' -import { createControlFlowGraph, createBasicBlock, pushBlock, popBlock } from './control_flow_graph'; -import { generateShaderCode } from './code_generation'; +import { createDirectedAcyclicGraph } from './ir_dag' +import { createControlFlowGraph, createBasicBlock, pushBlock, popBlock } from './ir_cfg'; +import { generateShaderCode } from './strands_codegen'; import { initGlobalStrandsAPI, createShaderHooksFunctions } from './strands_api'; function strands(p5, fn) { @@ -49,8 +50,8 @@ function strands(p5, fn) { p5.Shader.prototype.newModify = function(shaderModifier, options = { parser: true, srcLocations: false }) { if (shaderModifier instanceof Function) { // Reset the context object every time modify is called; - const backend = WEBGL; - initStrandsContext(strandsContext, backend); + const backend = glslBackend; + initStrandsContext(strandsContext, glslBackend); createShaderHooksFunctions(strandsContext, fn, this); // 1. Transpile from strands DSL to JS diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index 08ddaf8237..7410368912 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -5,11 +5,11 @@ import { createStatementNode, createTypeConstructorNode, createUnaryOpNode, -} from './builder' -import { OperatorTable, BlockType, TypeInfo, BaseType, TypeInfoFromGLSLName } from './utils' -import { strandsShaderFunctions } from './shader_functions' +} from './ir_builders' +import { OperatorTable, BlockType, TypeInfo, BaseType, TypeInfoFromGLSLName } from './ir_types' +import { strandsShaderFunctions } from './strands_builtins' import { StrandsConditional } from './strands_conditionals' -import * as CFG from './control_flow_graph' +import * as CFG from './ir_cfg' import * as FES from './strands_FES' ////////////////////////////////////////////// diff --git a/src/strands/shader_functions.js b/src/strands/strands_builtins.js similarity index 86% rename from src/strands/shader_functions.js rename to src/strands/strands_builtins.js index 1c95d0702a..946089e245 100644 --- a/src/strands/shader_functions.js +++ b/src/strands/strands_builtins.js @@ -38,15 +38,15 @@ const builtInGLSLFunctions = { 'log2': [{ args: ['genType'], returnType: 'genType', isp5Function: false}], 'max': [ { args: ['genType', 'genType'], returnType: 'genType', isp5Function: true}, - { args: ['genType', 'float'], returnType: 'genType', isp5Function: true}, + { args: ['genType', 'float1'], returnType: 'genType', isp5Function: true}, ], 'min': [ { args: ['genType', 'genType'], returnType: 'genType', isp5Function: true}, - { args: ['genType', 'float'], returnType: 'genType', isp5Function: true}, + { args: ['genType', 'float1'], returnType: 'genType', isp5Function: true}, ], 'mix': [ { args: ['genType', 'genType', 'genType'], returnType: 'genType', isp5Function: false}, - { args: ['genType', 'genType', 'float'], returnType: 'genType', isp5Function: false}, + { args: ['genType', 'genType', 'float1'], returnType: 'genType', isp5Function: false}, ], // 'mod': [{}], // 'modf': [{}], @@ -56,7 +56,7 @@ const builtInGLSLFunctions = { // 'sign': [{}], 'smoothstep': [ { args: ['genType', 'genType', 'genType'], returnType: 'genType', isp5Function: false}, - { args: ['float', 'float', 'genType'], returnType: 'genType', isp5Function: false}, + { args: ['float1', 'float1', 'genType'], returnType: 'genType', isp5Function: false}, ], 'sqrt': [{ args: ['genType'], returnType: 'genType', isp5Function: true}], 'step': [{ args: ['genType', 'genType'], returnType: 'genType', isp5Function: false}], @@ -64,18 +64,18 @@ const builtInGLSLFunctions = { ////////// Vector ////////// 'cross': [{ args: ['vec3', 'vec3'], returnType: 'vec3', isp5Function: true}], - 'distance': [{ args: ['genType', 'genType'], returnType: 'float', isp5Function: true}], - 'dot': [{ args: ['genType', 'genType'], returnType: 'float', isp5Function: true}], + 'distance': [{ args: ['genType', 'genType'], returnType: 'float1', isp5Function: true}], + 'dot': [{ args: ['genType', 'genType'], returnType: 'float1', isp5Function: true}], // 'equal': [{}], 'faceforward': [{ args: ['genType', 'genType', 'genType'], returnType: 'genType', isp5Function: false}], - 'length': [{ args: ['genType'], returnType: 'float', isp5Function: false}], + 'length': [{ args: ['genType'], returnType: 'float1', isp5Function: false}], 'normalize': [{ args: ['genType'], returnType: 'genType', isp5Function: true}], // 'notEqual': [{}], 'reflect': [{ args: ['genType', 'genType'], returnType: 'genType', isp5Function: false}], - 'refract': [{ args: ['genType', 'genType', 'float'], returnType: 'genType', isp5Function: false}], + 'refract': [{ args: ['genType', 'genType', 'float1'], returnType: 'genType', isp5Function: false}], ////////// Texture sampling ////////// - 'texture': [{args: ['sampler2D', 'vec2'], returnType: 'vec4', isp5Function: true}], + 'texture': [{args: ['sampler2D', 'float2'], returnType: 'float4', isp5Function: true}], } export const strandsShaderFunctions = { diff --git a/src/strands/code_generation.js b/src/strands/strands_codegen.js similarity index 81% rename from src/strands/code_generation.js rename to src/strands/strands_codegen.js index d807797499..904add554d 100644 --- a/src/strands/code_generation.js +++ b/src/strands/strands_codegen.js @@ -1,15 +1,11 @@ -import { WEBGL } from '../core/constants'; -import { glslBackend } from './GLSL_backend'; -import { NodeType } from './utils'; -import { sortCFG } from './control_flow_graph'; -import { sortDAG } from './directed_acyclic_graph'; - -let globalTempCounter = 0; -let backend; +import { NodeType } from './ir_types'; +import { sortCFG } from './ir_cfg'; +import { sortDAG } from './ir_dag'; function generateTopLevelDeclarations(strandsContext, generationContext, dagOrder) { + const { dag, backend } = strandsContext; + const usedCount = {}; - const dag = strandsContext.dag; for (const nodeID of dagOrder) { usedCount[nodeID] = (dag.usedBy[nodeID] || []).length; } @@ -30,13 +26,11 @@ function generateTopLevelDeclarations(strandsContext, generationContext, dagOrde } export function generateShaderCode(strandsContext) { - if (strandsContext.backend === WEBGL) { - backend = glslBackend; - } + const { cfg, dag, backend } = strandsContext; + const hooksObj = {}; for (const { hookType, entryBlockID, rootNodeID} of strandsContext.hooks) { - const { cfg, dag } = strandsContext; const dagSorted = sortDAG(dag.dependsOn, rootNodeID); const cfgSorted = sortCFG(cfg.outgoingEdges, entryBlockID); diff --git a/src/strands/strands_conditionals.js b/src/strands/strands_conditionals.js index e1da496c02..1ce888cc91 100644 --- a/src/strands/strands_conditionals.js +++ b/src/strands/strands_conditionals.js @@ -1,5 +1,5 @@ -import * as CFG from './control_flow_graph' -import { BlockType } from './utils'; +import * as CFG from './ir_cfg' +import { BlockType } from './ir_types'; export class StrandsConditional { constructor(strandsContext, condition, branchCallback) { diff --git a/src/strands/GLSL_backend.js b/src/strands/strands_glslBackend.js similarity index 96% rename from src/strands/GLSL_backend.js rename to src/strands/strands_glslBackend.js index d921aed364..5862adb184 100644 --- a/src/strands/GLSL_backend.js +++ b/src/strands/strands_glslBackend.js @@ -1,5 +1,5 @@ -import { NodeType, OpCodeToSymbol, BlockType, OpCode } from "./utils"; -import { getNodeDataFromID, extractNodeTypeInfo } from "./directed_acyclic_graph"; +import { NodeType, OpCodeToSymbol, BlockType, OpCode } from "./ir_types"; +import { getNodeDataFromID, extractNodeTypeInfo } from "./ir_dag"; import * as FES from './strands_FES' const TypeNames = { diff --git a/src/strands/code_transpiler.js b/src/strands/strands_transpiler.js similarity index 100% rename from src/strands/code_transpiler.js rename to src/strands/strands_transpiler.js From 446d3ec52db924ca1fd22e44ac8af3db2d4085e0 Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Thu, 24 Jul 2025 18:56:47 +0100 Subject: [PATCH 42/69] builtin function overloads type checking --- preview/global/sketch.js | 2 +- src/strands/ir_builders.js | 84 ++++++++++++++-- src/strands/ir_types.js | 10 +- src/strands/p5.strands.js | 1 - src/strands/strands_api.js | 22 ++-- src/strands/strands_builtins.js | 160 ++++++++++++++++++------------ src/strands/strands_transpiler.js | 2 - 7 files changed, 192 insertions(+), 89 deletions(-) diff --git a/preview/global/sketch.js b/preview/global/sketch.js index 208260102a..25ec2fd398 100644 --- a/preview/global/sketch.js +++ b/preview/global/sketch.js @@ -4,7 +4,7 @@ function callback() { getFinalColor((col) => { let y = col.sub(-1,1,0,0); - return y//mix(0, col.add(y), 1); + return mix(float(0), col.add(y), float(1)); }); } diff --git a/src/strands/ir_builders.js b/src/strands/ir_builders.js index 2acd29b986..8a4ffb399a 100644 --- a/src/strands/ir_builders.js +++ b/src/strands/ir_builders.js @@ -1,8 +1,10 @@ import * as DAG from './ir_dag' import * as CFG from './ir_cfg' import * as FES from './strands_FES' -import { NodeType, OpCode, BaseType } from './ir_types'; +import { NodeType, OpCode, BaseType, typeEquals, GenType } from './ir_types'; import { StrandsNode } from './strands_api'; +import { strandsBuiltinFunctions } from './strands_builtins'; +import { ar } from 'vitest/dist/chunks/reporters.D7Jzd9GS.js'; ////////////////////////////////////////////// // Builders for node graphs @@ -167,18 +169,86 @@ export function createTypeConstructorNode(strandsContext, typeInfo, dependsOn) { return id; } -export function createFunctionCallNode(strandsContext, identifier, overrides, dependsOn) { +export function createFunctionCallNode(strandsContext, functionName, userArgs) { const { cfg, dag } = strandsContext; - let typeInfo = { baseType: null, dimension: null }; + console.log("HELLOOOOOOOO") + const overloads = strandsBuiltinFunctions[functionName]; + const matchingArgsCounts = overloads.filter(overload => overload.params.length === userArgs.length); + if (matchingArgsCounts.length === 0) { + const argsLengthSet = new Set(); + const argsLengthArr = []; + overloads.forEach((overload) => argsLengthSet.add(overload.params.length)); + argsLengthSet.forEach((len) => argsLengthArr.push(`${len}`)); + const argsLengthStr = argsLengthArr.join(' or '); + FES.userError("parameter validation error",`Function '${functionName}' has ${overloads.length} variants which expect ${argsLengthStr} arguments, but ${userArgs.length} arguments were provided.`); + } + + let bestOverload = null; + let bestScore = 0; + let inferredReturnType = null; + for (const overload of matchingArgsCounts) { + let isValid = true; + let overloadParamTypes = []; + let inferredDimension = null; + let similarity = 0; + + for (let i = 0; i < userArgs.length; i++) { + const argType = DAG.extractNodeTypeInfo(userArgs[i]); + const expectedType = overload.params[i]; + let dimension = expectedType.dimension; + + const isGeneric = (T) => T.dimension === null; + if (isGeneric(expectedType)) { + if (inferredDimension === null || inferredDimension === 1) { + inferredDimension = argType.dimension; + } + if (inferredDimension !== argType.dimension) { + isValid = false; + } + dimension = inferredDimension; + } + else { + if (argType.dimension > dimension) { + isValid = false; + } + } + + if (argType.baseType === expectedType.baseType) { + similarity += 2; + } + else if(expectedType.priority > argType.priority) { + similarity += 1; + } + + overloadParamTypes.push({ baseType: expectedType.baseType, dimension }); + } + + if (isValid && (!bestOverload || similarity > bestScore)) { + bestOverload = overloadParamTypes; + bestScore = similarity; + inferredReturnType = overload.returnType; + if (isGeneric(inferredReturnType)) { + inferredReturnType.dimension = inferredDimension; + } + } + } + + if (bestOverload === null) { + const paramsString = (params) => `(${params.map((param) => param).join(', ')})`; + const expectedArgsString = overloads.map(overload => paramsString(overload.params)).join(' or '); + const providedArgsString = paramsString(userArgs.map((arg)=>arg.baseType+arg.dimension)); + throw new Error(`Function '${functionName}' was called with wrong arguments. Most likely, you provided mixed lengths vectors as arguments.\nExpected argument types: ${expectedArgsString}\nProvided argument types: ${providedArgsString}\nAll of the arguments with expected type 'genType' should have a matching type. If one of those is different, try to find where it was created. + `); + } const nodeData = DAG.createNodeData({ nodeType: NodeType.OPERATION, opCode: OpCode.Nary.FUNCTION_CALL, - identifier, - overrides, - dependsOn, + identifier: functionName, + dependsOn: userArgs, // no type info yet - ...typeInfo, + baseType: inferredReturnType.baseType, + dimension: inferredReturnType.dimension }) const id = DAG.getOrCreateNode(dag, nodeData); CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); diff --git a/src/strands/ir_types.js b/src/strands/ir_types.js index f84a2e8aa9..007f22de51 100644 --- a/src/strands/ir_types.js +++ b/src/strands/ir_types.js @@ -37,7 +37,7 @@ export const BasePriority = { [BaseType.DEFER]: -1, }; -export const TypeInfo = { +export const DataType = { float1: { fnName: "float", baseType: BaseType.FLOAT, dimension:1, priority: 3, }, float2: { fnName: "vec2", baseType: BaseType.FLOAT, dimension:2, priority: 3, }, float3: { fnName: "vec3", baseType: BaseType.FLOAT, dimension:3, priority: 3, }, @@ -56,12 +56,18 @@ export const TypeInfo = { defer: { fnName: null, baseType: BaseType.DEFER, dimension: null, priority: -1 }, } +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(TypeInfo) + Object.values(DataType) .filter(info => info.fnName !== null) .map(info => [info.fnName, info]) ); diff --git a/src/strands/p5.strands.js b/src/strands/p5.strands.js index 0c31a499ff..be2be91595 100644 --- a/src/strands/p5.strands.js +++ b/src/strands/p5.strands.js @@ -4,7 +4,6 @@ * @for p5 * @requires core */ -import { WEBGL, /*WEBGPU*/ } from '../core/constants' import { glslBackend } from './strands_glslBackend'; import { transpileStrandsToJS } from './strands_transpiler'; diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index 7410368912..5779d04b28 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -6,8 +6,8 @@ import { createTypeConstructorNode, createUnaryOpNode, } from './ir_builders' -import { OperatorTable, BlockType, TypeInfo, BaseType, TypeInfoFromGLSLName } from './ir_types' -import { strandsShaderFunctions } from './strands_builtins' +import { OperatorTable, BlockType, DataType, BaseType, TypeInfoFromGLSLName } from './ir_types' +import { strandsBuiltinFunctions } from './strands_builtins' import { StrandsConditional } from './strands_conditionals' import * as CFG from './ir_cfg' import * as FES from './strands_FES' @@ -66,25 +66,25 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { ////////////////////////////////////////////// // Builtins, uniforms, variable constructors ////////////////////////////////////////////// - for (const [fnName, overrides] of Object.entries(strandsShaderFunctions)) { + for (const [functionName, overrides] of Object.entries(strandsBuiltinFunctions)) { const isp5Function = overrides[0].isp5Function; if (isp5Function) { - const originalFn = fn[fnName]; - fn[fnName] = function(...args) { + const originalFn = fn[functionName]; + fn[functionName] = function(...args) { if (strandsContext.active) { - return createFunctionCallNode(strandsContext, fnName, overrides, args); + return createFunctionCallNode(strandsContext, functionName, args); } else { return originalFn.apply(this, args); } } } else { - fn[fnName] = function (...args) { + fn[functionName] = function (...args) { if (strandsContext.active) { - return createFunctionCallNode(strandsContext, fnName, overrides, args); + return createFunctionCallNode(strandsContext, functionName, args); } else { p5._friendlyError( - `It looks like you've called ${fnName} outside of a shader's modify() function.` + `It looks like you've called ${functionName} outside of a shader's modify() function.` ) } } @@ -92,11 +92,11 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { } // Next is type constructors and uniform functions - for (const type in TypeInfo) { + for (const type in DataType) { if (type === BaseType.DEFER) { continue; } - const typeInfo = TypeInfo[type]; + const typeInfo = DataType[type]; let pascalTypeName; if (/^[ib]vec/.test(typeInfo.fnName)) { diff --git a/src/strands/strands_builtins.js b/src/strands/strands_builtins.js index 946089e245..e931b0b880 100644 --- a/src/strands/strands_builtins.js +++ b/src/strands/strands_builtins.js @@ -1,83 +1,113 @@ +import { GenType, DataType } from "./ir_types" + // GLSL Built in functions // https://docs.gl/el3/abs const builtInGLSLFunctions = { //////////// Trigonometry ////////// - 'acos': [{ args: ['genType'], returnType: 'genType', isp5Function: true}], - 'acosh': [{ args: ['genType'], returnType: 'genType', isp5Function: false}], - 'asin': [{ args: ['genType'], returnType: 'genType', isp5Function: true}], - 'asinh': [{ args: ['genType'], returnType: 'genType', isp5Function: false}], - 'atan': [ - { args: ['genType'], returnType: 'genType', isp5Function: false}, - { args: ['genType', 'genType'], returnType: 'genType', isp5Function: false}, + acos: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}], + acosh: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}], + asin: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}], + asinh: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}], + atan: [ + { params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}, + { params: [GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}, ], - 'atanh': [{ args: ['genType'], returnType: 'genType', isp5Function: false}], - 'cos': [{ args: ['genType'], returnType: 'genType', isp5Function: true}], - 'cosh': [{ args: ['genType'], returnType: 'genType', isp5Function: false}], - 'degrees': [{ args: ['genType'], returnType: 'genType', isp5Function: true}], - 'radians': [{ args: ['genType'], returnType: 'genType', isp5Function: true}], - 'sin': [{ args: ['genType'], returnType: 'genType' , isp5Function: true}], - 'sinh': [{ args: ['genType'], returnType: 'genType', isp5Function: false}], - 'tan': [{ args: ['genType'], returnType: 'genType', isp5Function: true}], - 'tanh': [{ args: ['genType'], returnType: 'genType', isp5Function: false}], + atanh: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}], + cos: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}], + cosh: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}], + degrees: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}], + radians: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}], + sin: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT , isp5Function: true}], + sinh: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}], + tan: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}], + tanh: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}], + ////////// Mathematics ////////// - 'abs': [{ args: ['genType'], returnType: 'genType', isp5Function: true}], - 'ceil': [{ args: ['genType'], returnType: 'genType', isp5Function: true}], - 'clamp': [{ args: ['genType', 'genType', 'genType'], returnType: 'genType', isp5Function: false}], - 'dFdx': [{ args: ['genType'], returnType: 'genType', isp5Function: false}], - 'dFdy': [{ args: ['genType'], returnType: 'genType', isp5Function: false}], - 'exp': [{ args: ['genType'], returnType: 'genType', isp5Function: true}], - 'exp2': [{ args: ['genType'], returnType: 'genType', isp5Function: false}], - 'floor': [{ args: ['genType'], returnType: 'genType', isp5Function: true}], - 'fma': [{ args: ['genType', 'genType', 'genType'], returnType: 'genType', isp5Function: false}], - 'fract': [{ args: ['genType'], returnType: 'genType', isp5Function: true}], - 'fwidth': [{ args: ['genType'], returnType: 'genType', isp5Function: false}], - 'inversesqrt': [{ args: ['genType'], returnType: 'genType', isp5Function: true}], - // 'isinf': [{}], - // 'isnan': [{}], - 'log': [{ args: ['genType'], returnType: 'genType', isp5Function: true}], - 'log2': [{ args: ['genType'], returnType: 'genType', isp5Function: false}], - 'max': [ - { args: ['genType', 'genType'], returnType: 'genType', isp5Function: true}, - { args: ['genType', 'float1'], returnType: 'genType', isp5Function: true}, + abs: [ + { params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}, + { params: [GenType.FLOAT], returnType: GenType.INT, isp5Function: true} + ], + ceil: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}], + clamp: [ + { params: [GenType.FLOAT, GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}, + { params: [GenType.FLOAT,DataType.float1,DataType.float1], returnType: GenType.FLOAT, isp5Function: false}, + { params: [GenType.INT, GenType.INT, GenType.INT], returnType: GenType.INT, isp5Function: false}, + { params: [GenType.INT, DataType.int1, DataType.int1], returnType: GenType.INT, isp5Function: false}, + ], + dFdx: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}], + dFdy: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}], + exp: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}], + exp2: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}], + floor: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}], + fma: [{ params: [GenType.FLOAT, GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}], + fract: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}], + fwidth: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}], + inversesqrt: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}], + // "isinf": [{}], + // "isnan": [{}], + log: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}], + log2: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}], + max: [ + { params: [GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}, + { params: [GenType.FLOAT,DataType.float1], returnType: GenType.FLOAT, isp5Function: true}, + { params: [GenType.INT, GenType.INT], returnType: GenType.INT, isp5Function: true}, + { params: [GenType.INT, DataType.int1], returnType: GenType.INT, isp5Function: true}, ], - 'min': [ - { args: ['genType', 'genType'], returnType: 'genType', isp5Function: true}, - { args: ['genType', 'float1'], returnType: 'genType', isp5Function: true}, + min: [ + { params: [GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}, + { params: [GenType.FLOAT,DataType.float1], returnType: GenType.FLOAT, isp5Function: true}, + { params: [GenType.INT, GenType.INT], returnType: GenType.INT, isp5Function: true}, + { params: [GenType.INT, DataType.int1], returnType: GenType.INT, isp5Function: true}, ], - 'mix': [ - { args: ['genType', 'genType', 'genType'], returnType: 'genType', isp5Function: false}, - { args: ['genType', 'genType', 'float1'], returnType: 'genType', isp5Function: false}, + mix: [ + { params: [GenType.FLOAT, GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}, + { params: [GenType.FLOAT, GenType.FLOAT,DataType.float1], returnType: GenType.FLOAT, isp5Function: false}, + { params: [GenType.FLOAT, GenType.FLOAT, GenType.BOOL], returnType: GenType.FLOAT, isp5Function: false}, ], - // 'mod': [{}], - // 'modf': [{}], - 'pow': [{ args: ['genType', 'genType'], returnType: 'genType', isp5Function: true}], - 'round': [{ args: ['genType'], returnType: 'genType', isp5Function: true}], - 'roundEven': [{ args: ['genType'], returnType: 'genType', isp5Function: false}], - // 'sign': [{}], - 'smoothstep': [ - { args: ['genType', 'genType', 'genType'], returnType: 'genType', isp5Function: false}, - { args: ['float1', 'float1', 'genType'], returnType: 'genType', isp5Function: false}, + mod: [ + { params: [GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}, + { params: [GenType.FLOAT,DataType.float1], returnType: GenType.FLOAT, isp5Function: true}, ], - 'sqrt': [{ args: ['genType'], returnType: 'genType', isp5Function: true}], - 'step': [{ args: ['genType', 'genType'], returnType: 'genType', isp5Function: false}], - 'trunc': [{ args: ['genType'], returnType: 'genType', isp5Function: false}], + // "modf": [{}], + pow: [{ params: [GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}], + round: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}], + roundEven: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}], + sign: [ + { params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}, + { params: [GenType.INT], returnType: GenType.INT, isp5Function: false}, + ], + smoothstep: [ + { params: [GenType.FLOAT, GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}, + { params: [ DataType.float1,DataType.float1, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}, + ], + sqrt: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}], + step: [{ params: [GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}], + trunc: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}], ////////// Vector ////////// - 'cross': [{ args: ['vec3', 'vec3'], returnType: 'vec3', isp5Function: true}], - 'distance': [{ args: ['genType', 'genType'], returnType: 'float1', isp5Function: true}], - 'dot': [{ args: ['genType', 'genType'], returnType: 'float1', isp5Function: true}], - // 'equal': [{}], - 'faceforward': [{ args: ['genType', 'genType', 'genType'], returnType: 'genType', isp5Function: false}], - 'length': [{ args: ['genType'], returnType: 'float1', isp5Function: false}], - 'normalize': [{ args: ['genType'], returnType: 'genType', isp5Function: true}], - // 'notEqual': [{}], - 'reflect': [{ args: ['genType', 'genType'], returnType: 'genType', isp5Function: false}], - 'refract': [{ args: ['genType', 'genType', 'float1'], returnType: 'genType', isp5Function: false}], + cross: [{ params: [DataType.float3, DataType.float3], returnType: DataType.float3, isp5Function: true}], + distance: [{ params: [GenType.FLOAT, GenType.FLOAT], returnType:DataType.float1, isp5Function: true}], + dot: [{ params: [GenType.FLOAT, GenType.FLOAT], returnType:DataType.float1, isp5Function: true}], + equal: [ + { params: [GenType.FLOAT, GenType.FLOAT], returnType: GenType.BOOL, isp5Function: false}, + { params: [GenType.INT, GenType.INT], returnType: GenType.BOOL, isp5Function: false}, + { params: [GenType.BOOL, GenType.BOOL], returnType: GenType.BOOL, isp5Function: false}, + ], + faceforward: [{ params: [GenType.FLOAT, GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}], + length: [{ params: [GenType.FLOAT], returnType:DataType.float1, isp5Function: false}], + normalize: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}], + notEqual: [ + { params: [GenType.FLOAT, GenType.FLOAT], returnType: GenType.BOOL, isp5Function: false}, + { params: [GenType.INT, GenType.INT], returnType: GenType.BOOL, isp5Function: false}, + { params: [GenType.BOOL, GenType.BOOL], returnType: GenType.BOOL, isp5Function: false}, + ], + reflect: [{ params: [GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}], + refract: [{ params: [GenType.FLOAT, GenType.FLOAT,DataType.float1], returnType: GenType.FLOAT, isp5Function: false}], ////////// Texture sampling ////////// - 'texture': [{args: ['sampler2D', 'float2'], returnType: 'float4', isp5Function: true}], + texture: [{params: ["texture2D", DataType.float2], returnType: DataType.float4, isp5Function: true}], } -export const strandsShaderFunctions = { +export const strandsBuiltinFunctions = { ...builtInGLSLFunctions, } \ No newline at end of file diff --git a/src/strands/strands_transpiler.js b/src/strands/strands_transpiler.js index a804d3dcfd..47ad8469f9 100644 --- a/src/strands/strands_transpiler.js +++ b/src/strands/strands_transpiler.js @@ -2,8 +2,6 @@ import { parse } from 'acorn'; import { ancestor } from 'acorn-walk'; import escodegen from 'escodegen'; -// TODO: Switch this to operator table, cleanup whole file too - function replaceBinaryOperator(codeSource) { switch (codeSource) { case '+': return 'add'; From 83b4cf4026dfc1ad93a6084f988f4adc979c9983 Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Thu, 24 Jul 2025 19:26:54 +0100 Subject: [PATCH 43/69] function calls partially reimplemented. Still needs more error checking. --- preview/global/sketch.js | 4 +--- src/strands/ir_builders.js | 16 +++++----------- src/strands/strands_api.js | 6 ++++-- src/strands/strands_glslBackend.js | 6 +++--- 4 files changed, 13 insertions(+), 19 deletions(-) diff --git a/preview/global/sketch.js b/preview/global/sketch.js index 25ec2fd398..01ec27f494 100644 --- a/preview/global/sketch.js +++ b/preview/global/sketch.js @@ -2,9 +2,7 @@ p5.disableFriendlyErrors = true; function callback() { getFinalColor((col) => { - let y = col.sub(-1,1,0,0); - - return mix(float(0), col.add(y), float(1)); + return mix(vec4(1,0, 1, 1), vec4(1, 1, 0.3, 1), float(1)); }); } diff --git a/src/strands/ir_builders.js b/src/strands/ir_builders.js index 8a4ffb399a..1b1adc6106 100644 --- a/src/strands/ir_builders.js +++ b/src/strands/ir_builders.js @@ -4,7 +4,6 @@ import * as FES from './strands_FES' import { NodeType, OpCode, BaseType, typeEquals, GenType } from './ir_types'; import { StrandsNode } from './strands_api'; import { strandsBuiltinFunctions } from './strands_builtins'; -import { ar } from 'vitest/dist/chunks/reporters.D7Jzd9GS.js'; ////////////////////////////////////////////// // Builders for node graphs @@ -171,7 +170,6 @@ export function createTypeConstructorNode(strandsContext, typeInfo, dependsOn) { export function createFunctionCallNode(strandsContext, functionName, userArgs) { const { cfg, dag } = strandsContext; - console.log("HELLOOOOOOOO") const overloads = strandsBuiltinFunctions[functionName]; const matchingArgsCounts = overloads.filter(overload => overload.params.length === userArgs.length); if (matchingArgsCounts.length === 0) { @@ -179,7 +177,7 @@ export function createFunctionCallNode(strandsContext, functionName, userArgs) { const argsLengthArr = []; overloads.forEach((overload) => argsLengthSet.add(overload.params.length)); argsLengthSet.forEach((len) => argsLengthArr.push(`${len}`)); - const argsLengthStr = argsLengthArr.join(' or '); + const argsLengthStr = argsLengthArr.join(', or '); FES.userError("parameter validation error",`Function '${functionName}' has ${overloads.length} variants which expect ${argsLengthStr} arguments, but ${userArgs.length} arguments were provided.`); } @@ -187,17 +185,17 @@ export function createFunctionCallNode(strandsContext, functionName, userArgs) { let bestScore = 0; let inferredReturnType = null; for (const overload of matchingArgsCounts) { + const isGeneric = (T) => T.dimension === null; let isValid = true; let overloadParamTypes = []; let inferredDimension = null; let similarity = 0; for (let i = 0; i < userArgs.length; i++) { - const argType = DAG.extractNodeTypeInfo(userArgs[i]); + const argType = DAG.extractNodeTypeInfo(dag, userArgs[i].id); const expectedType = overload.params[i]; let dimension = expectedType.dimension; - const isGeneric = (T) => T.dimension === null; if (isGeneric(expectedType)) { if (inferredDimension === null || inferredDimension === 1) { inferredDimension = argType.dimension; @@ -234,18 +232,14 @@ export function createFunctionCallNode(strandsContext, functionName, userArgs) { } if (bestOverload === null) { - const paramsString = (params) => `(${params.map((param) => param).join(', ')})`; - const expectedArgsString = overloads.map(overload => paramsString(overload.params)).join(' or '); - const providedArgsString = paramsString(userArgs.map((arg)=>arg.baseType+arg.dimension)); - throw new Error(`Function '${functionName}' was called with wrong arguments. Most likely, you provided mixed lengths vectors as arguments.\nExpected argument types: ${expectedArgsString}\nProvided argument types: ${providedArgsString}\nAll of the arguments with expected type 'genType' should have a matching type. If one of those is different, try to find where it was created. - `); + FES.userError('parameter validation', 'No matching overload found!'); } const nodeData = DAG.createNodeData({ nodeType: NodeType.OPERATION, opCode: OpCode.Nary.FUNCTION_CALL, identifier: functionName, - dependsOn: userArgs, + dependsOn: userArgs.map(arg => arg.id), // no type info yet baseType: inferredReturnType.baseType, dimension: inferredReturnType.dimension diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index 5779d04b28..3842ebab59 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -73,7 +73,8 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { const originalFn = fn[functionName]; fn[functionName] = function(...args) { if (strandsContext.active) { - return createFunctionCallNode(strandsContext, functionName, args); + const id = createFunctionCallNode(strandsContext, functionName, args); + return new StrandsNode(id); } else { return originalFn.apply(this, args); } @@ -81,7 +82,8 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { } else { fn[functionName] = function (...args) { if (strandsContext.active) { - return createFunctionCallNode(strandsContext, functionName, args); + const id = createFunctionCallNode(strandsContext, functionName, args); + return new StrandsNode(id); } else { p5._friendlyError( `It looks like you've called ${functionName} outside of a shader's modify() function.` diff --git a/src/strands/strands_glslBackend.js b/src/strands/strands_glslBackend.js index 5862adb184..8b673477d4 100644 --- a/src/strands/strands_glslBackend.js +++ b/src/strands/strands_glslBackend.js @@ -113,15 +113,15 @@ export const glslBackend = { const useParantheses = node.usedBy.length > 0; if (node.opCode === OpCode.Nary.CONSTRUCTOR) { if (node.dependsOn.length === 1 && node.dimension === 1) { - console.log("AARK") return this.generateExpression(generationContext, dag, node.dependsOn[0]); } const T = this.getTypeName(node.baseType, node.dimension); const deps = node.dependsOn.map((dep) => this.generateExpression(generationContext, dag, dep)); return `${T}(${deps.join(', ')})`; } - if (node.opCode === OpCode.Nary.FUNCTION) { - return "functioncall!"; + if (node.opCode === OpCode.Nary.FUNCTION_CALL) { + const functionArgs = node.dependsOn.map(arg =>this.generateExpression(generationContext, dag, arg)); + return `${node.identifier}(${functionArgs.join(', ')})`; } if (node.dependsOn.length === 2) { const [lID, rID] = node.dependsOn; From a743c68d80785709b2de9fd10988e23119bbcb39 Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Fri, 25 Jul 2025 13:59:46 +0100 Subject: [PATCH 44/69] update function calls to conform parameters when raw numbers are handed --- preview/global/sketch.js | 7 ++- src/strands/ir_builders.js | 90 +++++++++++++++++++++---------- src/strands/strands_api.js | 4 +- src/strands/strands_transpiler.js | 4 +- 4 files changed, 70 insertions(+), 35 deletions(-) diff --git a/preview/global/sketch.js b/preview/global/sketch.js index 01ec27f494..bc37b09883 100644 --- a/preview/global/sketch.js +++ b/preview/global/sketch.js @@ -2,7 +2,10 @@ p5.disableFriendlyErrors = true; function callback() { getFinalColor((col) => { - return mix(vec4(1,0, 1, 1), vec4(1, 1, 0.3, 1), float(1)); + let x = [12, 1]; + let y= [10, 100]; + let z = [x, y]; + return mix(vec4([1,0], 1, 1), z, 0.4); }); } @@ -15,7 +18,7 @@ function windowResized() { resizeCanvas(windowWidth, windowHeight); } -function draw(){ +function draw() { orbitControl(); background(0); shader(bloomShader); diff --git a/src/strands/ir_builders.js b/src/strands/ir_builders.js index 1b1adc6106..b6e2f2a579 100644 --- a/src/strands/ir_builders.js +++ b/src/strands/ir_builders.js @@ -1,7 +1,7 @@ import * as DAG from './ir_dag' import * as CFG from './ir_cfg' import * as FES from './strands_FES' -import { NodeType, OpCode, BaseType, typeEquals, GenType } from './ir_types'; +import { NodeType, OpCode, BaseType, DataType, BasePriority, } from './ir_types'; import { StrandsNode } from './strands_api'; import { strandsBuiltinFunctions } from './strands_builtins'; @@ -108,17 +108,20 @@ export function createBinaryOpNode(strandsContext, leftStrandsNode, rightArg, op return id; } -function mapConstructorDependencies(strandsContext, typeInfo, dependsOn) { +function mapPrimitiveDependencies(strandsContext, typeInfo, dependsOn) { + dependsOn = Array.isArray(dependsOn) ? dependsOn : [dependsOn]; const mappedDependencies = []; let { dimension, baseType } = typeInfo; const dag = strandsContext.dag; let calculatedDimensions = 0; - - for (const dep of dependsOn.flat()) { + let originalNodeID = null; + for (const dep of dependsOn.flat(Infinity)) { if (dep instanceof StrandsNode) { const node = DAG.getNodeDataFromID(dag, dep.id); - + originalNodeID = dep.id; + baseType = node.baseType; + if (node.opCode === OpCode.Nary.CONSTRUCTOR) { for (const inner of node.dependsOn) { mappedDependencies.push(inner); @@ -130,7 +133,7 @@ function mapConstructorDependencies(strandsContext, typeInfo, dependsOn) { calculatedDimensions += node.dimension; continue; } - if (typeof dep === 'number') { + else if (typeof dep === 'number') { const newNode = createLiteralNode(strandsContext, { dimension: 1, baseType }, dep); mappedDependencies.push(newNode); calculatedDimensions += 1; @@ -140,6 +143,7 @@ function mapConstructorDependencies(strandsContext, typeInfo, dependsOn) { FES.userError('type error', `You've tried to construct a scalar or vector type with a non-numeric value: ${dep}`); } } + // Sometimes, the dimension is undefined if (dimension === null) { dimension = calculatedDimensions; } else if (dimension > calculatedDimensions && calculatedDimensions === 1) { @@ -147,38 +151,52 @@ function mapConstructorDependencies(strandsContext, typeInfo, dependsOn) { } else if(calculatedDimensions !== 1 && calculatedDimensions !== dimension) { FES.userError('type error', `You've tried to construct a ${baseType + dimension} with ${calculatedDimensions} components`); } - - return { mappedDependencies, dimension }; + const inferredTypeInfo = { + dimension, + baseType, + priority: BasePriority[baseType], + } + return { originalNodeID, mappedDependencies, inferredTypeInfo }; } -export function createTypeConstructorNode(strandsContext, typeInfo, dependsOn) { - const { cfg, dag } = strandsContext; - dependsOn = Array.isArray(dependsOn) ? dependsOn : [dependsOn]; - const { mappedDependencies, dimension } = mapConstructorDependencies(strandsContext, typeInfo, dependsOn); - +function constructTypeFromIDs(strandsContext, strandsNodesArray, newTypeInfo) { const nodeData = DAG.createNodeData({ nodeType: NodeType.OPERATION, opCode: OpCode.Nary.CONSTRUCTOR, - dimension, - baseType: typeInfo.baseType, - dependsOn: mappedDependencies - }) - const id = DAG.getOrCreateNode(dag, nodeData); + dimension: newTypeInfo.dimension, + baseType: newTypeInfo.baseType, + dependsOn: strandsNodesArray + }); + const id = DAG.getOrCreateNode(strandsContext.dag, nodeData); + return id; +} + +export function createTypeConstructorNode(strandsContext, typeInfo, dependsOn) { + const { cfg, dag } = strandsContext; + const { mappedDependencies, inferredTypeInfo } = mapPrimitiveDependencies(strandsContext, typeInfo, dependsOn); + const finalType = { + baseType: typeInfo.baseType, + dimension: inferredTypeInfo.dimension + }; + const id = constructTypeFromIDs(strandsContext, mappedDependencies, finalType); CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); return id; } -export function createFunctionCallNode(strandsContext, functionName, userArgs) { +export function createFunctionCallNode(strandsContext, functionName, rawUserArgs) { const { cfg, dag } = strandsContext; const overloads = strandsBuiltinFunctions[functionName]; - const matchingArgsCounts = overloads.filter(overload => overload.params.length === userArgs.length); + + const preprocessedArgs = rawUserArgs.map((rawUserArg) => mapPrimitiveDependencies(strandsContext, DataType.defer, rawUserArg)); + console.log(preprocessedArgs); + const matchingArgsCounts = overloads.filter(overload => overload.params.length === preprocessedArgs.length); if (matchingArgsCounts.length === 0) { const argsLengthSet = new Set(); const argsLengthArr = []; overloads.forEach((overload) => argsLengthSet.add(overload.params.length)); argsLengthSet.forEach((len) => argsLengthArr.push(`${len}`)); const argsLengthStr = argsLengthArr.join(', or '); - FES.userError("parameter validation error",`Function '${functionName}' has ${overloads.length} variants which expect ${argsLengthStr} arguments, but ${userArgs.length} arguments were provided.`); + FES.userError("parameter validation error",`Function '${functionName}' has ${overloads.length} variants which expect ${argsLengthStr} arguments, but ${preprocessedArgs.length} arguments were provided.`); } let bestOverload = null; @@ -187,12 +205,13 @@ export function createFunctionCallNode(strandsContext, functionName, userArgs) { for (const overload of matchingArgsCounts) { const isGeneric = (T) => T.dimension === null; let isValid = true; - let overloadParamTypes = []; + let overloadParameters = []; let inferredDimension = null; let similarity = 0; - for (let i = 0; i < userArgs.length; i++) { - const argType = DAG.extractNodeTypeInfo(dag, userArgs[i].id); + for (let i = 0; i < preprocessedArgs.length; i++) { + const preArg = preprocessedArgs[i]; + const argType = preArg.inferredTypeInfo; const expectedType = overload.params[i]; let dimension = expectedType.dimension; @@ -218,11 +237,11 @@ export function createFunctionCallNode(strandsContext, functionName, userArgs) { similarity += 1; } - overloadParamTypes.push({ baseType: expectedType.baseType, dimension }); + overloadParameters.push({ baseType: expectedType.baseType, dimension }); } if (isValid && (!bestOverload || similarity > bestScore)) { - bestOverload = overloadParamTypes; + bestOverload = overloadParameters; bestScore = similarity; inferredReturnType = overload.returnType; if (isGeneric(inferredReturnType)) { @@ -233,14 +252,27 @@ export function createFunctionCallNode(strandsContext, functionName, userArgs) { if (bestOverload === null) { FES.userError('parameter validation', 'No matching overload found!'); - } + } + + let dependsOn = []; + for (let i = 0; i < bestOverload.length; i++) { + const arg = preprocessedArgs[i]; + if (arg.originalNodeID) { + dependsOn.push(arg.originalNodeID); + } + else { + const paramType = bestOverload[i]; + const castedArgID = constructTypeFromIDs(strandsContext, arg.mappedDependencies, paramType); + CFG.recordInBasicBlock(cfg, cfg.currentBlock, castedArgID); + dependsOn.push(castedArgID); + } + } const nodeData = DAG.createNodeData({ nodeType: NodeType.OPERATION, opCode: OpCode.Nary.FUNCTION_CALL, identifier: functionName, - dependsOn: userArgs.map(arg => arg.id), - // no type info yet + dependsOn, baseType: inferredReturnType.baseType, dimension: inferredReturnType.dimension }) diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index 3842ebab59..d3e6948e11 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -57,9 +57,9 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { fn.strandsNode = function(...args) { if (args.length > 4) { - FES.userError('type error', "It looks like you've tried to construct a p5.strands node implicitly, with more than 4 components. This is currently not supported.") + FES.userError("type error", "It looks like you've tried to construct a p5.strands node implicitly, with more than 4 components. This is currently not supported.") } - const id = createTypeConstructorNode(strandsContext, { baseType: BaseType.DEFER, dimension: null }, args); + const id = createTypeConstructorNode(strandsContext, { baseType: BaseType.DEFER, dimension: null }, args.flat()); return new StrandsNode(id); } diff --git a/src/strands/strands_transpiler.js b/src/strands/strands_transpiler.js index 47ad8469f9..b7e8e35f4f 100644 --- a/src/strands/strands_transpiler.js +++ b/src/strands/strands_transpiler.js @@ -123,7 +123,7 @@ const ASTCallbacks = { node.type = 'CallExpression'; node.callee = { type: 'Identifier', - name: 'dynamicNode', + name: 'strandsNode', }; node.arguments = [original]; }, @@ -176,7 +176,7 @@ const ASTCallbacks = { type: 'CallExpression', callee: { type: 'Identifier', - name: 'dynamicNode', + name: 'strandsNode', }, arguments: [node.left] } From 295c140d8af62c8a8feecbc0cde18a66d30a2827 Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Fri, 25 Jul 2025 15:01:25 +0100 Subject: [PATCH 45/69] adding struct types --- preview/global/sketch.js | 9 +- src/strands/ir_builders.js | 11 ++- src/strands/ir_types.js | 20 +++++ src/strands/strands_api.js | 146 +++++++++++++++++++++------------ src/strands/strands_codegen.js | 2 +- 5 files changed, 127 insertions(+), 61 deletions(-) diff --git a/preview/global/sketch.js b/preview/global/sketch.js index bc37b09883..0a05adcd29 100644 --- a/preview/global/sketch.js +++ b/preview/global/sketch.js @@ -2,11 +2,12 @@ p5.disableFriendlyErrors = true; function callback() { getFinalColor((col) => { - let x = [12, 1]; - let y= [10, 100]; - let z = [x, y]; - return mix(vec4([1,0], 1, 1), z, 0.4); + + return [1, 1, 0, 1]; }); + getWorldInputs(inputs => { + return inputs; + }) } async function setup(){ diff --git a/src/strands/ir_builders.js b/src/strands/ir_builders.js index b6e2f2a579..db00ec848f 100644 --- a/src/strands/ir_builders.js +++ b/src/strands/ir_builders.js @@ -26,6 +26,10 @@ export function createLiteralNode(strandsContext, typeInfo, value) { return id; } +export function createStructNode(strandsContext, structTypeInfo, dependsOn) { + +} + export function createVariableNode(strandsContext, typeInfo, identifier) { const { cfg, dag } = strandsContext; const { dimension, baseType } = typeInfo; @@ -159,12 +163,12 @@ function mapPrimitiveDependencies(strandsContext, typeInfo, dependsOn) { return { originalNodeID, mappedDependencies, inferredTypeInfo }; } -function constructTypeFromIDs(strandsContext, strandsNodesArray, newTypeInfo) { +function constructTypeFromIDs(strandsContext, strandsNodesArray, typeInfo) { const nodeData = DAG.createNodeData({ nodeType: NodeType.OPERATION, opCode: OpCode.Nary.CONSTRUCTOR, - dimension: newTypeInfo.dimension, - baseType: newTypeInfo.baseType, + dimension: typeInfo.dimension, + baseType: typeInfo.baseType, dependsOn: strandsNodesArray }); const id = DAG.getOrCreateNode(strandsContext.dag, nodeData); @@ -188,7 +192,6 @@ export function createFunctionCallNode(strandsContext, functionName, rawUserArgs const overloads = strandsBuiltinFunctions[functionName]; const preprocessedArgs = rawUserArgs.map((rawUserArg) => mapPrimitiveDependencies(strandsContext, DataType.defer, rawUserArg)); - console.log(preprocessedArgs); const matchingArgsCounts = overloads.filter(overload => overload.params.length === preprocessedArgs.length); if (matchingArgsCounts.length === 0) { const argsLengthSet = new Set(); diff --git a/src/strands/ir_types.js b/src/strands/ir_types.js index 007f22de51..76fd39f551 100644 --- a/src/strands/ir_types.js +++ b/src/strands/ir_types.js @@ -56,6 +56,26 @@ export const DataType = { defer: { fnName: null, baseType: BaseType.DEFER, dimension: null, priority: -1 }, } +export const StructType = { + Vertex: { + identifer: 'Vertex', + properties: [ + { name: "position", dataType: DataType.float3 }, + { name: "normal", dataType: DataType.float3 }, + { name: "color", dataType: DataType.float4 }, + { name: "texCoord", dataType: DataType.float2 }, + ] + } +} + +export function isStructType(typeName) { + return Object.keys(StructType).includes(typeName); +} + +export function isNativeType(typeName) { + return Object.keys(DataType).includes(typeName); +} + export const GenType = { FLOAT: { baseType: BaseType.FLOAT, dimension: null, priority: 3 }, INT: { baseType: BaseType.INT, dimension: null, priority: 2 }, diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index d3e6948e11..b377c691b6 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -6,7 +6,16 @@ import { createTypeConstructorNode, createUnaryOpNode, } from './ir_builders' -import { OperatorTable, BlockType, DataType, BaseType, TypeInfoFromGLSLName } from './ir_types' +import { + OperatorTable, + BlockType, + DataType, + BaseType, + StructType, + TypeInfoFromGLSLName, + isStructType, + // isNativeType +} from './ir_types' import { strandsBuiltinFunctions } from './strands_builtins' import { StrandsConditional } from './strands_conditionals' import * as CFG from './ir_cfg' @@ -137,22 +146,21 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { ////////////////////////////////////////////// // Per-Hook functions ////////////////////////////////////////////// -const structTypes = ['Vertex', ] - function createHookArguments(strandsContext, parameters){ const args = []; for (const param of parameters) { const paramType = param.type; - if(structTypes.includes(paramType.typeName)) { - const propertyEntries = paramType.properties.map((prop) => { - const typeInfo = TypeInfoFromGLSLName[prop.dataType]; - const variableNode = createVariableNode(strandsContext, typeInfo, prop.name); - return [prop.name, variableNode]; - }); - const argObject = Object.fromEntries(propertyEntries); - args.push(argObject); - } else { + if(isStructType(paramType.typeName)) { + const structType = StructType[paramType.typeName]; + const argStruct = {}; + for (const prop of structType.properties) { + const memberNode = createVariableNode(strandsContext, prop.dataType, prop.name); + argStruct[prop.name] = memberNode; + } + args.push(argStruct); + } + else /*if(isNativeType(paramType.typeName))*/ { const typeInfo = TypeInfoFromGLSLName[paramType.typeName]; const id = createVariableNode(strandsContext, typeInfo, param.name); const arg = new StrandsNode(id); @@ -162,64 +170,98 @@ function createHookArguments(strandsContext, parameters){ return args; } +function enforceReturnTypeMatch(strandsContext, expectedType, returned, hookName) { + if (!(returned instanceof StrandsNode)) { + try { + return createTypeConstructorNode(strandsContext, expectedType, returned); + } catch (e) { + FES.userError('type error', + `There was a type mismatch for a value returned from ${hookName}.\n` + + `The value in question was supposed to be:\n` + + `${expectedType.baseType + expectedType.dimension}\n` + + `But you returned:\n` + + `${returned}` + ); + } + } + + const dag = strandsContext.dag; + let returnedNodeID = returned.id; + const receivedType = { + baseType: dag.baseTypes[returnedNodeID], + dimension: dag.dimensions[returnedNodeID], + } + if (receivedType.dimension !== expectedType.dimension) { + if (receivedType.dimension !== 1) { + FES.userError('type error', `You have returned a vector with ${receivedType.dimension} components in ${hookType.name} when a ${expectedType.baseType + expectedType.dimension} was expected!`); + } + else { + returnedNodeID = createTypeConstructorNode(strandsContext, expectedType, returnedNodeID); + } + } + else if (receivedType.baseType !== expectedType.baseType) { + returnedNodeID = createTypeConstructorNode(strandsContext, expectedType, returnedNodeID); + } + + return returnedNodeID; +} + export function createShaderHooksFunctions(strandsContext, fn, shader) { const availableHooks = { ...shader.hooks.vertex, ...shader.hooks.fragment, } const hookTypes = Object.keys(availableHooks).map(name => shader.hookTypes(name)); - const { cfg, dag } = strandsContext; + const cfg = strandsContext.cfg; for (const hookType of hookTypes) { window[hookType.name] = 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 returned = hookUserCallback(...args); - let returnedNode; + const args = createHookArguments(strandsContext, hookType.parameters); + const userReturned = hookUserCallback(...args); const expectedReturnType = hookType.returnType; - if(structTypes.includes(expectedReturnType.typeName)) { - } - else { - // In this case we are expecting a native shader type, probably vec4 or vec3. - const expected = TypeInfoFromGLSLName[expectedReturnType.typeName]; - // User may have returned a raw value like [1,1,1,1] or 25. - if (!(returned instanceof StrandsNode)) { - const id = createTypeConstructorNode(strandsContext, { baseType: BaseType.DEFER, dimension: null }, returned); - returnedNode = new StrandsNode(id); - } - else { - returnedNode = returned; - } - - const received = { - baseType: dag.baseTypes[returnedNode.id], - dimension: dag.dimensions[returnedNode.id], - } - if (received.dimension !== expected.dimension) { - if (received.dimension !== 1) { - FES.userError('type error', `You have returned a vector with ${received.dimension} components in ${hookType.name} when a ${expected.baseType + expected.dimension} was expected!`); - } - else { - const newID = createTypeConstructorNode(strandsContext, expected, returnedNode); - returnedNode = new StrandsNode(newID); + if(isStructType(expectedReturnType.typeName)) { + const expectedStructType = StructType[expectedReturnType.typeName]; + const rootStruct = { + identifier: expectedReturnType.typeName, + properties: {} + }; + const expectedProperties = expectedStructType.properties; + + for (let i = 0; i < expectedProperties.length; i++) { + const expectedProp = expectedProperties[i]; + const propName = expectedProp.name; + const receivedValue = userReturned[propName]; + if (receivedValue === undefined) { + FES.userError('type error', `You've returned an incomplete object from ${hookType.name}.\n` + + `Expected: { ${expectedReturnType.properties.map(p => p.name).join(', ')} }\n` + + `Received: { ${Object.keys(userReturned).join(', ')} }\n` + + `All of the properties are required!`); } - } - else if (received.baseType !== expected.baseType) { - const newID = createTypeConstructorNode(strandsContext, expected, returnedNode); - returnedNode = new StrandsNode(newID); + + const expectedTypeInfo = expectedProp.dataType; + const returnedPropID = enforceReturnTypeMatch(strandsContext, expectedTypeInfo, receivedValue, hookType.name); + rootStruct.properties[propName] = returnedPropID; } + strandsContext.hooks.push({ + hookType, + entryBlockID, + rootStruct + }); + } + else /*if(isNativeType(expectedReturnType.typeName))*/ { + const expectedTypeInfo = TypeInfoFromGLSLName[expectedReturnType.typeName]; + const returnedNodeID = enforceReturnTypeMatch(strandsContext, expectedTypeInfo, userReturned, hookType.name); + strandsContext.hooks.push({ + hookType, + entryBlockID, + rootNodeID: returnedNodeID, + }); } - - strandsContext.hooks.push({ - hookType, - entryBlockID, - rootNodeID: returnedNode.id, - }); CFG.popBlock(cfg); } } diff --git a/src/strands/strands_codegen.js b/src/strands/strands_codegen.js index 904add554d..c3b1606ce1 100644 --- a/src/strands/strands_codegen.js +++ b/src/strands/strands_codegen.js @@ -30,7 +30,7 @@ export function generateShaderCode(strandsContext) { const hooksObj = {}; - for (const { hookType, entryBlockID, rootNodeID} of strandsContext.hooks) { + for (const { hookType, entryBlockID, rootNodeID, rootStruct} of strandsContext.hooks) { const dagSorted = sortDAG(dag.dependsOn, rootNodeID); const cfgSorted = sortCFG(cfg.outgoingEdges, entryBlockID); From 7cd3d42e993114ab504ecf6e4005db7fb9be7963 Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Fri, 25 Jul 2025 15:01:25 +0100 Subject: [PATCH 46/69] adding struct types --- preview/global/sketch.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/preview/global/sketch.js b/preview/global/sketch.js index 0a05adcd29..75d47f28bc 100644 --- a/preview/global/sketch.js +++ b/preview/global/sketch.js @@ -1,10 +1,13 @@ p5.disableFriendlyErrors = true; function callback() { - getFinalColor((col) => { + // getFinalColor((col) => { - return [1, 1, 0, 1]; - }); + // return [1, 1, 0, 1]; + // }); + // getWorldInputs(inputs => { + // return inputs; + // }) getWorldInputs(inputs => { return inputs; }) From f7b133919272b5926835cecfe22125ab7a4aa6bc Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Sat, 26 Jul 2025 18:39:55 +0100 Subject: [PATCH 47/69] struct types working --- preview/global/sketch.js | 7 +- src/strands/ir_builders.js | 133 +++++++++++++++++++----- src/strands/ir_types.js | 12 ++- src/strands/p5.strands.js | 2 +- src/strands/strands_api.js | 160 ++++++++++++++++++----------- src/strands/strands_codegen.js | 14 ++- src/strands/strands_glslBackend.js | 38 ++++++- 7 files changed, 263 insertions(+), 103 deletions(-) diff --git a/preview/global/sketch.js b/preview/global/sketch.js index 75d47f28bc..cec3c38775 100644 --- a/preview/global/sketch.js +++ b/preview/global/sketch.js @@ -9,13 +9,15 @@ function callback() { // return inputs; // }) getWorldInputs(inputs => { + inputs.color = vec4(inputs.position, 1); + inputs.position = inputs.position + sin(time) * 100; return inputs; - }) + }); } async function setup(){ createCanvas(windowWidth,windowHeight, WEBGL) - bloomShader = baseColorShader().newModify(callback, {parser: false}); + bloomShader = baseColorShader().newModify(callback); } function windowResized() { @@ -26,5 +28,6 @@ function draw() { orbitControl(); background(0); shader(bloomShader); + noStroke(); sphere(300) } diff --git a/src/strands/ir_builders.js b/src/strands/ir_builders.js index db00ec848f..2b64471161 100644 --- a/src/strands/ir_builders.js +++ b/src/strands/ir_builders.js @@ -23,11 +23,7 @@ export function createLiteralNode(strandsContext, typeInfo, value) { }); const id = DAG.getOrCreateNode(dag, nodeData); CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); - return id; -} - -export function createStructNode(strandsContext, structTypeInfo, dependsOn) { - + return { id, components: dimension }; } export function createVariableNode(strandsContext, typeInfo, identifier) { @@ -41,7 +37,7 @@ export function createVariableNode(strandsContext, typeInfo, identifier) { }) const id = DAG.getOrCreateNode(dag, nodeData); CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); - return id; + return { id, components: dimension }; } export function createBinaryOpNode(strandsContext, leftStrandsNode, rightArg, opCode) { @@ -51,7 +47,7 @@ export function createBinaryOpNode(strandsContext, leftStrandsNode, rightArg, op if (rightArg[0] instanceof StrandsNode && rightArg.length === 1) { rightStrandsNode = rightArg[0]; } else { - const id = createTypeConstructorNode(strandsContext, { baseType: BaseType.DEFER, dimension: null }, rightArg); + const { id, components } = createPrimitiveConstructorNode(strandsContext, { baseType: BaseType.DEFER, dimension: null }, rightArg); rightStrandsNode = new StrandsNode(id); } let finalLeftNodeID = leftStrandsNode.id; @@ -63,8 +59,8 @@ export function createBinaryOpNode(strandsContext, leftStrandsNode, rightArg, op const cast = { node: null, toType: leftType }; const bothDeferred = leftType.baseType === rightType.baseType && leftType.baseType === BaseType.DEFER; if (bothDeferred) { - finalLeftNodeID = createTypeConstructorNode(strandsContext, { baseType:BaseType.FLOAT, dimension: leftType.dimension }, leftStrandsNode); - finalRightNodeID = createTypeConstructorNode(strandsContext, { baseType:BaseType.FLOAT, dimension: leftType.dimension }, rightStrandsNode); + finalLeftNodeID = createPrimitiveConstructorNode(strandsContext, { baseType:BaseType.FLOAT, dimension: leftType.dimension }, leftStrandsNode); + finalRightNodeID = createPrimitiveConstructorNode(strandsContext, { baseType:BaseType.FLOAT, dimension: leftType.dimension }, rightStrandsNode); } else if (leftType.baseType !== rightType.baseType || leftType.dimension !== rightType.dimension) { @@ -91,28 +87,73 @@ export function createBinaryOpNode(strandsContext, leftStrandsNode, rightArg, op FES.userError('type error', `A vector of length ${leftType.dimension} operated with a vector of length ${rightType.dimension} is not allowed.`); } - const castedID = createTypeConstructorNode(strandsContext, cast.toType, cast.node); + const casted = createPrimitiveConstructorNode(strandsContext, cast.toType, cast.node); if (cast.node === leftStrandsNode) { - finalLeftNodeID = castedID; + finalLeftNodeID = casted.id; } else { - finalRightNodeID = castedID; + finalRightNodeID = casted.id; } } const nodeData = DAG.createNodeData({ nodeType: NodeType.OPERATION, + opCode, dependsOn: [finalLeftNodeID, finalRightNodeID], - dimension, baseType: cast.toType.baseType, dimension: cast.toType.dimension, - opCode }); const id = DAG.getOrCreateNode(dag, nodeData); CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); - return id; + return { id, components: nodeData.dimension }; } -function mapPrimitiveDependencies(strandsContext, typeInfo, dependsOn) { +export function createMemberAccessNode(strandsContext, parentNode, componentNode, memberTypeInfo) { + const { dag, cfg } = strandsContext; + const nodeData = DAG.createNodeData({ + nodeType: NodeType.OPERATION, + opCode: OpCode.Binary.MEMBER_ACCESS, + dimension: memberTypeInfo.dimension, + baseType: memberTypeInfo.baseType, + dependsOn: [parentNode.id, componentNode.id], + }); + const id = DAG.getOrCreateNode(dag, nodeData); + CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); + return { id, components: memberTypeInfo.dimension }; +} + + +export function createStructInstanceNode(strandsContext, structTypeInfo, identifier, dependsOn) { + const { cfg, dag, } = strandsContext; + + if (dependsOn.length === 0) { + for (const prop of structTypeInfo.properties) { + const typeInfo = prop.dataType; + const nodeData = DAG.createNodeData({ + nodeType: NodeType.VARIABLE, + baseType: typeInfo.baseType, + dimension: typeInfo.dimension, + identifier: `${identifier}.${prop.name}`, + }); + const component = DAG.getOrCreateNode(dag, nodeData); + CFG.recordInBasicBlock(cfg, cfg.currentBlock, component.id); + dependsOn.push(component); + } + } + + const nodeData = DAG.createNodeData({ + nodeType: NodeType.VARIABLE, + dimension: structTypeInfo.properties.length, + baseType: structTypeInfo.name, + identifier, + dependsOn + }) + const structID = DAG.getOrCreateNode(dag, nodeData); + CFG.recordInBasicBlock(cfg, cfg.currentBlock, structID); + + return { id: structID, components: dependsOn }; +} + +function mapPrimitiveDepsToIDs(strandsContext, typeInfo, dependsOn) { dependsOn = Array.isArray(dependsOn) ? dependsOn : [dependsOn]; const mappedDependencies = []; let { dimension, baseType } = typeInfo; @@ -138,8 +179,8 @@ function mapPrimitiveDependencies(strandsContext, typeInfo, dependsOn) { continue; } else if (typeof dep === 'number') { - const newNode = createLiteralNode(strandsContext, { dimension: 1, baseType }, dep); - mappedDependencies.push(newNode); + const { id, components } = createLiteralNode(strandsContext, { dimension: 1, baseType }, dep); + mappedDependencies.push(id); calculatedDimensions += 1; continue; } @@ -163,7 +204,7 @@ function mapPrimitiveDependencies(strandsContext, typeInfo, dependsOn) { return { originalNodeID, mappedDependencies, inferredTypeInfo }; } -function constructTypeFromIDs(strandsContext, strandsNodesArray, typeInfo) { +export function constructTypeFromIDs(strandsContext, typeInfo, strandsNodesArray) { const nodeData = DAG.createNodeData({ nodeType: NodeType.OPERATION, opCode: OpCode.Nary.CONSTRUCTOR, @@ -175,23 +216,61 @@ function constructTypeFromIDs(strandsContext, strandsNodesArray, typeInfo) { return id; } -export function createTypeConstructorNode(strandsContext, typeInfo, dependsOn) { +export function createPrimitiveConstructorNode(strandsContext, typeInfo, dependsOn) { const { cfg, dag } = strandsContext; - const { mappedDependencies, inferredTypeInfo } = mapPrimitiveDependencies(strandsContext, typeInfo, dependsOn); + const { mappedDependencies, inferredTypeInfo } = mapPrimitiveDepsToIDs(strandsContext, typeInfo, dependsOn); const finalType = { baseType: typeInfo.baseType, dimension: inferredTypeInfo.dimension }; - const id = constructTypeFromIDs(strandsContext, mappedDependencies, finalType); + const id = constructTypeFromIDs(strandsContext, finalType, mappedDependencies); CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); - return id; + return { id, components: finalType.dimension }; +} + +export function createStructConstructorNode(strandsContext, structTypeInfo, rawUserArgs) { + const { cfg, dag } = strandsContext; + const { identifer, properties } = structTypeInfo; + + if (!(rawUserArgs.length === properties.length)) { + FES.userError('type error', + `You've tried to construct a ${structTypeInfo.name} struct with ${rawUserArgs.length} properties, but it expects ${properties.length} properties.\n` + + `The properties it expects are:\n` + + `${properties.map(prop => prop.name + ' ' + prop.DataType.baseType + prop.DataType.dimension)}` + ); + } + + const dependsOn = []; + for (let i = 0; i < properties.length; i++) { + const expectedProperty = properties[i]; + const { originalNodeID, mappedDependencies } = mapPrimitiveDepsToIDs(strandsContext, expectedProperty.dataType, rawUserArgs[i]); + if (originalNodeID) { + dependsOn.push(originalNodeID); + } + else { + dependsOn.push( + constructTypeFromIDs(strandsContext, expectedProperty.dataType, mappedDependencies) + ); + } + } + + const nodeData = DAG.createNodeData({ + nodeType: NodeType.OPERATION, + opCode: OpCode.Nary.CONSTRUCTOR, + dimension: properties.length, + baseType: structTypeInfo.name, + dependsOn + }); + const id = DAG.getOrCreateNode(dag, nodeData); + CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); + return { id, components: structTypeInfo.components }; } export function createFunctionCallNode(strandsContext, functionName, rawUserArgs) { const { cfg, dag } = strandsContext; const overloads = strandsBuiltinFunctions[functionName]; - const preprocessedArgs = rawUserArgs.map((rawUserArg) => mapPrimitiveDependencies(strandsContext, DataType.defer, rawUserArg)); + const preprocessedArgs = rawUserArgs.map((rawUserArg) => mapPrimitiveDepsToIDs(strandsContext, DataType.defer, rawUserArg)); const matchingArgsCounts = overloads.filter(overload => overload.params.length === preprocessedArgs.length); if (matchingArgsCounts.length === 0) { const argsLengthSet = new Set(); @@ -265,7 +344,7 @@ export function createFunctionCallNode(strandsContext, functionName, rawUserArgs } else { const paramType = bestOverload[i]; - const castedArgID = constructTypeFromIDs(strandsContext, arg.mappedDependencies, paramType); + const castedArgID = constructTypeFromIDs(strandsContext, paramType, arg.mappedDependencies); CFG.recordInBasicBlock(cfg, cfg.currentBlock, castedArgID); dependsOn.push(castedArgID); } @@ -281,7 +360,7 @@ export function createFunctionCallNode(strandsContext, functionName, rawUserArgs }) const id = DAG.getOrCreateNode(dag, nodeData); CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); - return id; + return { id, components: nodeData.dimension }; } export function createUnaryOpNode(strandsContext, strandsNode, opCode) { @@ -294,7 +373,7 @@ export function createUnaryOpNode(strandsContext, strandsNode, opCode) { dimension: dag.dimensions[strandsNode.id], }) CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); - return id; + return { id, components: nodeData.dimension }; } export function createStatementNode(strandsContext, type) { diff --git a/src/strands/ir_types.js b/src/strands/ir_types.js index 76fd39f551..021ee0f404 100644 --- a/src/strands/ir_types.js +++ b/src/strands/ir_types.js @@ -6,7 +6,8 @@ export const NodeType = { LITERAL: 1, VARIABLE: 2, CONSTANT: 3, - PHI: 4, + STRUCT: 4, + PHI: 5, }; export const NodeTypeToName = Object.fromEntries( @@ -18,6 +19,7 @@ export const NodeTypeRequiredFields = { [NodeType.LITERAL]: ["value"], [NodeType.VARIABLE]: ["identifier"], [NodeType.CONSTANT]: ["value"], + [NodeType.STRUCT]: [""], [NodeType.PHI]: ["dependsOn", "phiBlocks"] }; @@ -58,12 +60,12 @@ export const DataType = { export const StructType = { Vertex: { - identifer: 'Vertex', + name: 'Vertex', properties: [ { name: "position", dataType: DataType.float3 }, { name: "normal", dataType: DataType.float3 }, - { name: "color", dataType: DataType.float4 }, { name: "texCoord", dataType: DataType.float2 }, + { name: "color", dataType: DataType.float4 }, ] } } @@ -162,11 +164,11 @@ export const ConstantFolding = { [OpCode.Binary.LOGICAL_OR]: (a, b) => a || b, }; -export const SymbolToOpCode = {}; +// export const SymbolToOpCode = {}; export const OpCodeToSymbol = {}; for (const { symbol, opCode } of OperatorTable) { - SymbolToOpCode[symbol] = opCode; + // SymbolToOpCode[symbol] = opCode; OpCodeToSymbol[opCode] = symbol; } diff --git a/src/strands/p5.strands.js b/src/strands/p5.strands.js index be2be91595..ec4c70d2db 100644 --- a/src/strands/p5.strands.js +++ b/src/strands/p5.strands.js @@ -71,7 +71,7 @@ function strands(p5, fn) { // ....... const hooksObject = generateShaderCode(strandsContext); console.log(hooksObject); - console.log(hooksObject['vec4 getFinalColor']); + console.log(hooksObject['Vertex getWorldInputs']); // Reset the strands runtime context // deinitStrandsContext(strandsContext); diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index b377c691b6..2792f14fd1 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -3,8 +3,11 @@ import { createFunctionCallNode, createVariableNode, createStatementNode, - createTypeConstructorNode, + createPrimitiveConstructorNode, createUnaryOpNode, + createMemberAccessNode, + createStructInstanceNode, + createStructConstructorNode, } from './ir_builders' import { OperatorTable, @@ -20,6 +23,7 @@ import { strandsBuiltinFunctions } from './strands_builtins' import { StrandsConditional } from './strands_conditionals' import * as CFG from './ir_cfg' import * as FES from './strands_FES' +import { getNodeDataFromID } from './ir_dag' ////////////////////////////////////////////// // User nodes @@ -33,16 +37,16 @@ export class StrandsNode { export function initGlobalStrandsAPI(p5, fn, strandsContext) { // We augment the strands node with operations programatically // this means methods like .add, .sub, etc can be chained - for (const { name, arity, opCode, symbol } of OperatorTable) { + for (const { name, arity, opCode } of OperatorTable) { if (arity === 'binary') { StrandsNode.prototype[name] = function (...right) { - const id = createBinaryOpNode(strandsContext, this, right, opCode); + const { id, components } = createBinaryOpNode(strandsContext, this, right, opCode); return new StrandsNode(id); }; } if (arity === 'unary') { fn[name] = function (strandsNode) { - const id = createUnaryOpNode(strandsContext, strandsNode, opCode); + const { id, components } = createUnaryOpNode(strandsContext, strandsNode, opCode); return new StrandsNode(id); } } @@ -52,7 +56,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { // Unique Functions ////////////////////////////////////////////// fn.discard = function() { - const id = createStatementNode('discard'); + const { id, components } = createStatementNode('discard'); CFG.recordInBasicBlock(strandsContext.cfg, strandsContext.cfg.currentBlock, id); } @@ -68,7 +72,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { if (args.length > 4) { FES.userError("type error", "It looks like you've tried to construct a p5.strands node implicitly, with more than 4 components. This is currently not supported.") } - const id = createTypeConstructorNode(strandsContext, { baseType: BaseType.DEFER, dimension: null }, args.flat()); + const { id, components } = createPrimitiveConstructorNode(strandsContext, { baseType: BaseType.DEFER, dimension: null }, args.flat()); return new StrandsNode(id); } @@ -82,7 +86,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { const originalFn = fn[functionName]; fn[functionName] = function(...args) { if (strandsContext.active) { - const id = createFunctionCallNode(strandsContext, functionName, args); + const { id, components } = createFunctionCallNode(strandsContext, functionName, args); return new StrandsNode(id); } else { return originalFn.apply(this, args); @@ -91,7 +95,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { } else { fn[functionName] = function (...args) { if (strandsContext.active) { - const id = createFunctionCallNode(strandsContext, functionName, args); + const { id, components } = createFunctionCallNode(strandsContext, functionName, args); return new StrandsNode(id); } else { p5._friendlyError( @@ -121,8 +125,8 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { + typeInfo.fnName.slice(1).toLowerCase(); } - fn[`uniform${pascalTypeName}`] = function(name, ...defaultValue) { - const id = createVariableNode(strandsContext, typeInfo, name); + fn[`uniform${pascalTypeName}`] = function(name, defaultValue) { + const { id, components } = createVariableNode(strandsContext, typeInfo, name); strandsContext.uniforms.push({ name, typeInfo, defaultValue }); return new StrandsNode(id); }; @@ -130,7 +134,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { const originalp5Fn = fn[typeInfo.fnName]; fn[typeInfo.fnName] = function(...args) { if (strandsContext.active) { - const id = createTypeConstructorNode(strandsContext, typeInfo, args); + const { id, components } = createPrimitiveConstructorNode(strandsContext, typeInfo, args); return new StrandsNode(id); } else if (originalp5Fn) { return originalp5Fn.apply(this, args); @@ -153,16 +157,44 @@ function createHookArguments(strandsContext, parameters){ const paramType = param.type; if(isStructType(paramType.typeName)) { const structType = StructType[paramType.typeName]; - const argStruct = {}; - for (const prop of structType.properties) { - const memberNode = createVariableNode(strandsContext, prop.dataType, prop.name); - argStruct[prop.name] = memberNode; + const originalInstanceInfo = createStructInstanceNode(strandsContext, structType, param.name, []); + const structNode = new StrandsNode(originalInstanceInfo.id); + const componentNodes = originalInstanceInfo.components.map(id => new StrandsNode(id)) + + for (let i = 0; i < structType.properties.length; i++) { + const componentTypeInfo = structType.properties[i]; + Object.defineProperty(structNode, componentTypeInfo.name, { + get() { + return new StrandsNode(strandsContext.dag.dependsOn[structNode.id][i]) + // const { id, components } = createMemberAccessNode(strandsContext, structNode, componentNodes[i], componentTypeInfo.dataType); + // const memberAccessNode = new StrandsNode(id); + // return memberAccessNode; + }, + set(val) { + const oldDependsOn = strandsContext.dag.dependsOn[structNode.id]; + const newDependsOn = [...oldDependsOn]; + + let newValueID; + if (val instanceof StrandsNode) { + newValueID = val.id; + } + else { + let newVal = createPrimitiveConstructorNode(strandsContext, componentTypeInfo.dataType, val); + newValueID = newVal.id; + } + + newDependsOn[i] = newValueID; + const newStructInfo = createStructInstanceNode(strandsContext, structType, param.name, newDependsOn); + structNode.id = newStructInfo.id; + } + }) } - args.push(argStruct); + + args.push(structNode); } else /*if(isNativeType(paramType.typeName))*/ { const typeInfo = TypeInfoFromGLSLName[paramType.typeName]; - const id = createVariableNode(strandsContext, typeInfo, param.name); + const { id, components } = createVariableNode(strandsContext, typeInfo, param.name); const arg = new StrandsNode(id); args.push(arg); } @@ -172,17 +204,18 @@ function createHookArguments(strandsContext, parameters){ function enforceReturnTypeMatch(strandsContext, expectedType, returned, hookName) { if (!(returned instanceof StrandsNode)) { - try { - return createTypeConstructorNode(strandsContext, expectedType, returned); - } catch (e) { - FES.userError('type error', - `There was a type mismatch for a value returned from ${hookName}.\n` + - `The value in question was supposed to be:\n` + - `${expectedType.baseType + expectedType.dimension}\n` + - `But you returned:\n` + - `${returned}` - ); - } + // try { + const result = createPrimitiveConstructorNode(strandsContext, expectedType, returned); + return result.id; + // } catch (e) { + // FES.userError('type error', + // `There was a type mismatch for a value returned from ${hookName}.\n` + + // `The value in question was supposed to be:\n` + + // `${expectedType.baseType + expectedType.dimension}\n` + + // `But you returned:\n` + + // `${returned}` + // ); + // } } const dag = strandsContext.dag; @@ -196,11 +229,13 @@ function enforceReturnTypeMatch(strandsContext, expectedType, returned, hookName FES.userError('type error', `You have returned a vector with ${receivedType.dimension} components in ${hookType.name} when a ${expectedType.baseType + expectedType.dimension} was expected!`); } else { - returnedNodeID = createTypeConstructorNode(strandsContext, expectedType, returnedNodeID); + const result = createPrimitiveConstructorNode(strandsContext, expectedType, returned); + returnedNodeID = result.id; } } else if (receivedType.baseType !== expectedType.baseType) { - returnedNodeID = createTypeConstructorNode(strandsContext, expectedType, returnedNodeID); + const result = createPrimitiveConstructorNode(strandsContext, expectedType, returned); + returnedNodeID = result.id; } return returnedNodeID; @@ -224,44 +259,49 @@ export function createShaderHooksFunctions(strandsContext, fn, shader) { const userReturned = hookUserCallback(...args); const expectedReturnType = hookType.returnType; + let rootNodeID = null; + if(isStructType(expectedReturnType.typeName)) { const expectedStructType = StructType[expectedReturnType.typeName]; - const rootStruct = { - identifier: expectedReturnType.typeName, - properties: {} - }; - const expectedProperties = expectedStructType.properties; - - for (let i = 0; i < expectedProperties.length; i++) { - const expectedProp = expectedProperties[i]; - const propName = expectedProp.name; - const receivedValue = userReturned[propName]; - if (receivedValue === undefined) { - FES.userError('type error', `You've returned an incomplete object from ${hookType.name}.\n` + - `Expected: { ${expectedReturnType.properties.map(p => p.name).join(', ')} }\n` + - `Received: { ${Object.keys(userReturned).join(', ')} }\n` + - `All of the properties are required!`); + 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.`); + } + rootNodeID = userReturned.id; + } + else { + const expectedProperties = expectedStructType.properties; + const newStructDependencies = []; + for (let i = 0; i < expectedProperties.length; i++) { + const expectedProp = expectedProperties[i]; + const propName = expectedProp.name; + const receivedValue = userReturned[propName]; + if (receivedValue === undefined) { + FES.userError('type error', `You've returned an incomplete struct from ${hookType.name}.\n` + + `Expected: { ${expectedReturnType.properties.map(p => p.name).join(', ')} }\n` + + `Received: { ${Object.keys(userReturned).join(', ')} }\n` + + `All of the properties are required!`); + } + const expectedTypeInfo = expectedProp.dataType; + const returnedPropID = enforceReturnTypeMatch(strandsContext, expectedTypeInfo, receivedValue, hookType.name); + newStructDependencies.push(returnedPropID); } - - const expectedTypeInfo = expectedProp.dataType; - const returnedPropID = enforceReturnTypeMatch(strandsContext, expectedTypeInfo, receivedValue, hookType.name); - rootStruct.properties[propName] = returnedPropID; + const newStruct = createStructConstructorNode(strandsContext, expectedStructType, newStructDependencies); + rootNodeID = newStruct.id; } - strandsContext.hooks.push({ - hookType, - entryBlockID, - rootStruct - }); + } else /*if(isNativeType(expectedReturnType.typeName))*/ { const expectedTypeInfo = TypeInfoFromGLSLName[expectedReturnType.typeName]; - const returnedNodeID = enforceReturnTypeMatch(strandsContext, expectedTypeInfo, userReturned, hookType.name); - strandsContext.hooks.push({ - hookType, - entryBlockID, - rootNodeID: returnedNodeID, - }); + rootNodeID = enforceReturnTypeMatch(strandsContext, expectedTypeInfo, userReturned, hookType.name); } + + strandsContext.hooks.push({ + hookType, + entryBlockID, + rootNodeID, + }); CFG.popBlock(cfg); } } diff --git a/src/strands/strands_codegen.js b/src/strands/strands_codegen.js index c3b1606ce1..5f892c6909 100644 --- a/src/strands/strands_codegen.js +++ b/src/strands/strands_codegen.js @@ -1,6 +1,7 @@ import { NodeType } from './ir_types'; import { sortCFG } from './ir_cfg'; import { sortDAG } from './ir_dag'; +import strands from './p5.strands'; function generateTopLevelDeclarations(strandsContext, generationContext, dagOrder) { const { dag, backend } = strandsContext; @@ -28,7 +29,14 @@ function generateTopLevelDeclarations(strandsContext, generationContext, dagOrde export function generateShaderCode(strandsContext) { const { cfg, dag, backend } = strandsContext; - const hooksObj = {}; + const hooksObj = { + uniforms: {}, + }; + + for (const {name, typeInfo, defaultValue} of strandsContext.uniforms) { + const declaration = backend.generateUniformDeclaration(name, typeInfo); + hooksObj.uniforms[declaration] = defaultValue; + } for (const { hookType, entryBlockID, rootNodeID, rootStruct} of strandsContext.hooks) { const dagSorted = sortDAG(dag.dependsOn, rootNodeID); @@ -54,8 +62,8 @@ export function generateShaderCode(strandsContext) { } const firstLine = backend.hookEntry(hookType); - const finalExpression = `return ${backend.generateExpression(generationContext, dag, rootNodeID)};`; - generationContext.write(finalExpression); + backend.generateReturnStatement(strandsContext, generationContext, rootNodeID); + // generationContext.write(finalExpression); hooksObj[`${hookType.returnType.typeName} ${hookType.name}`] = [firstLine, ...generationContext.codeLines, '}'].join('\n'); } diff --git a/src/strands/strands_glslBackend.js b/src/strands/strands_glslBackend.js index 8b673477d4..9d138f9030 100644 --- a/src/strands/strands_glslBackend.js +++ b/src/strands/strands_glslBackend.js @@ -1,4 +1,4 @@ -import { NodeType, OpCodeToSymbol, BlockType, OpCode } from "./ir_types"; +import { NodeType, OpCodeToSymbol, BlockType, OpCode, NodeTypeToName, isStructType, StructType } from "./ir_types"; import { getNodeDataFromID, extractNodeTypeInfo } from "./ir_dag"; import * as FES from './strands_FES' @@ -80,7 +80,15 @@ export const glslBackend = { }, getTypeName(baseType, dimension) { - return TypeNames[baseType + dimension] + const primitiveTypeName = TypeNames[baseType + dimension] + if (!primitiveTypeName) { + return baseType; + } + return primitiveTypeName; + }, + + generateUniformDeclaration(name, typeInfo) { + return `${this.getTypeName(typeInfo.baseType, typeInfo.dimension)} ${name}`; }, generateDeclaration(generationContext, dag, nodeID) { @@ -93,8 +101,22 @@ export const glslBackend = { return `${typeName} ${tmp} = ${expr};`; }, - generateReturn(generationContext, dag, nodeID) { - + generateReturnStatement(strandsContext, generationContext, rootNodeID) { + const dag = strandsContext.dag; + const rootNode = getNodeDataFromID(dag, rootNodeID); + if (isStructType(rootNode.baseType)) { + const structTypeInfo = StructType[rootNode.baseType]; + for (let i = 0; i < structTypeInfo.properties.length; i++) { + const prop = structTypeInfo.properties[i]; + const val = this.generateExpression(generationContext, dag, rootNode.dependsOn[i]); + if (prop.name !== val) { + generationContext.write( + `${rootNode.identifier}.${prop.name} = ${val};` + ) + } + } + } + generationContext.write(`return ${this.generateExpression(generationContext, dag, rootNodeID)};`); }, generateExpression(generationContext, dag, nodeID) { @@ -123,6 +145,12 @@ export const glslBackend = { const functionArgs = node.dependsOn.map(arg =>this.generateExpression(generationContext, dag, arg)); return `${node.identifier}(${functionArgs.join(', ')})`; } + if (node.opCode === OpCode.Binary.MEMBER_ACCESS) { + const [lID, rID] = node.dependsOn; + const lName = this.generateExpression(generationContext, dag, lID); + const rName = this.generateExpression(generationContext, dag, rID); + return `${lName}.${rName}`; + } if (node.dependsOn.length === 2) { const [lID, rID] = node.dependsOn; const left = this.generateExpression(generationContext, dag, lID); @@ -142,7 +170,7 @@ export const glslBackend = { } default: - FES.internalError(`${node.nodeType} not working yet`) + FES.internalError(`${NodeTypeToName[node.nodeType]} code generation not implemented yet`) } }, From ba4be8b514b0095462d8047636bbb2d238e54aa7 Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Sat, 26 Jul 2025 18:46:07 +0100 Subject: [PATCH 48/69] comment old line. Should revisit structs if needs optimisation. --- src/strands/strands_api.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index 2792f14fd1..d48faa3624 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -159,7 +159,7 @@ function createHookArguments(strandsContext, parameters){ const structType = StructType[paramType.typeName]; const originalInstanceInfo = createStructInstanceNode(strandsContext, structType, param.name, []); const structNode = new StrandsNode(originalInstanceInfo.id); - const componentNodes = originalInstanceInfo.components.map(id => new StrandsNode(id)) + // const componentNodes = originalInstanceInfo.components.map(id => new StrandsNode(id)) for (let i = 0; i < structType.properties.length; i++) { const componentTypeInfo = structType.properties[i]; From 4fe4aafb05690ebb38efe8332cb5ea7c2a07ac56 Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Sat, 26 Jul 2025 18:59:20 +0100 Subject: [PATCH 49/69] fix wrong ID in binary op node --- src/strands/ir_builders.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/strands/ir_builders.js b/src/strands/ir_builders.js index 2b64471161..36322a5d31 100644 --- a/src/strands/ir_builders.js +++ b/src/strands/ir_builders.js @@ -59,8 +59,10 @@ export function createBinaryOpNode(strandsContext, leftStrandsNode, rightArg, op const cast = { node: null, toType: leftType }; const bothDeferred = leftType.baseType === rightType.baseType && leftType.baseType === BaseType.DEFER; if (bothDeferred) { - finalLeftNodeID = createPrimitiveConstructorNode(strandsContext, { baseType:BaseType.FLOAT, dimension: leftType.dimension }, leftStrandsNode); - finalRightNodeID = createPrimitiveConstructorNode(strandsContext, { baseType:BaseType.FLOAT, dimension: leftType.dimension }, rightStrandsNode); + const l = createPrimitiveConstructorNode(strandsContext, { baseType:BaseType.FLOAT, dimension: leftType.dimension }, leftStrandsNode); + const r = createPrimitiveConstructorNode(strandsContext, { baseType:BaseType.FLOAT, dimension: leftType.dimension }, rightStrandsNode); + finalLeftNodeID = l.id; + finalRightNodeID = r.id; } else if (leftType.baseType !== rightType.baseType || leftType.dimension !== rightType.dimension) { From 0908e4345a3a931987cbd12e21daf6338ccbfb22 Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Sat, 26 Jul 2025 19:14:43 +0100 Subject: [PATCH 50/69] fix bug with binary op, and make strandsNode return node if arg is already a node. --- src/strands/ir_builders.js | 25 ++++++++++++++++++++----- src/strands/strands_api.js | 3 +++ 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/strands/ir_builders.js b/src/strands/ir_builders.js index 36322a5d31..e88efb9469 100644 --- a/src/strands/ir_builders.js +++ b/src/strands/ir_builders.js @@ -1,7 +1,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, } from './ir_types'; +import { NodeType, OpCode, BaseType, DataType, BasePriority, OpCodeToSymbol, } from './ir_types'; import { StrandsNode } from './strands_api'; import { strandsBuiltinFunctions } from './strands_builtins'; @@ -59,16 +59,31 @@ export function createBinaryOpNode(strandsContext, leftStrandsNode, rightArg, op const cast = { node: null, toType: leftType }; const bothDeferred = leftType.baseType === rightType.baseType && leftType.baseType === BaseType.DEFER; if (bothDeferred) { - const l = createPrimitiveConstructorNode(strandsContext, { baseType:BaseType.FLOAT, dimension: leftType.dimension }, leftStrandsNode); - const r = createPrimitiveConstructorNode(strandsContext, { baseType:BaseType.FLOAT, dimension: leftType.dimension }, rightStrandsNode); - finalLeftNodeID = l.id; + cast.toType.baseType = BaseType.FLOAT; + if (leftType.dimension === rightType.dimension) { + cast.toType.dimension = leftType.dimension; + } + else if (leftType.dimension === 1 && rightType.dimension > 1) { + cast.toType.dimension = rightType.dimension; + } + else if (rightType.dimension === 1 && leftType.dimension > 1) { + cast.toType.dimension = leftType.dimension; + } + else { + FES.userError("type error", `You have tried to perform a binary operation:\n`+ + `${leftType.baseType+leftType.dimension} ${OpCodeToSymbol[opCode]} ${rightType.baseType+rightType.dimension}\n` + + `It's only possible to operate on two nodes with the same dimension, or a scalar value and a vector.` + ); + } + const l = createPrimitiveConstructorNode(strandsContext, cast.toType, leftStrandsNode); + const r = createPrimitiveConstructorNode(strandsContext, cast.toType, rightStrandsNode); + finalLeftNodeID = l.id; finalRightNodeID = r.id; } else if (leftType.baseType !== rightType.baseType || leftType.dimension !== rightType.dimension) { if (leftType.dimension === 1 && rightType.dimension > 1) { - // e.g. op(scalar, vector): cast scalar up cast.node = leftStrandsNode; cast.toType = rightType; } diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index d48faa3624..83a97aaf07 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -69,6 +69,9 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { } fn.strandsNode = function(...args) { + if (args.length === 1 && args[0] instanceof StrandsNode) { + return args[0]; + } if (args.length > 4) { FES.userError("type error", "It looks like you've tried to construct a p5.strands node implicitly, with more than 4 components. This is currently not supported.") } From 5ce945118add666b7a80ebe5dbf8fa3c89558608 Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Tue, 29 Jul 2025 10:35:06 +0100 Subject: [PATCH 51/69] fix function call bugs --- preview/global/sketch.js | 10 +++---- src/strands/ir_builders.js | 29 +++++++++++-------- src/strands/ir_dag.js | 53 +++++++++++++++++----------------- src/strands/strands_codegen.js | 1 - 4 files changed, 48 insertions(+), 45 deletions(-) diff --git a/preview/global/sketch.js b/preview/global/sketch.js index cec3c38775..4a34d4cbce 100644 --- a/preview/global/sketch.js +++ b/preview/global/sketch.js @@ -1,13 +1,11 @@ p5.disableFriendlyErrors = true; function callback() { - // getFinalColor((col) => { + const time = uniformFloat(() =>millis()*0.001) + getFinalColor((col) => { + return [1,0,0, 1] +[1, 0, 0.1, 0] + pow(col,sin(time)); + }); - // return [1, 1, 0, 1]; - // }); - // getWorldInputs(inputs => { - // return inputs; - // }) getWorldInputs(inputs => { inputs.color = vec4(inputs.position, 1); inputs.position = inputs.position + sin(time) * 100; diff --git a/src/strands/ir_builders.js b/src/strands/ir_builders.js index e88efb9469..3e159ec14a 100644 --- a/src/strands/ir_builders.js +++ b/src/strands/ir_builders.js @@ -1,7 +1,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, } from './ir_types'; +import { NodeType, OpCode, BaseType, DataType, BasePriority, OpCodeToSymbol, typeEquals, } from './ir_types'; import { StrandsNode } from './strands_api'; import { strandsBuiltinFunctions } from './strands_builtins'; @@ -298,14 +298,14 @@ export function createFunctionCallNode(strandsContext, functionName, rawUserArgs FES.userError("parameter validation error",`Function '${functionName}' has ${overloads.length} variants which expect ${argsLengthStr} arguments, but ${preprocessedArgs.length} arguments were provided.`); } + const isGeneric = (T) => T.dimension === null; let bestOverload = null; let bestScore = 0; let inferredReturnType = null; + let inferredDimension = null; + for (const overload of matchingArgsCounts) { - const isGeneric = (T) => T.dimension === null; let isValid = true; - let overloadParameters = []; - let inferredDimension = null; let similarity = 0; for (let i = 0; i < preprocessedArgs.length; i++) { @@ -318,7 +318,10 @@ export function createFunctionCallNode(strandsContext, functionName, rawUserArgs if (inferredDimension === null || inferredDimension === 1) { inferredDimension = argType.dimension; } - if (inferredDimension !== argType.dimension) { + + if (inferredDimension !== argType.dimension && + !(argType.dimension === 1 && inferredDimension >= 1) + ) { isValid = false; } dimension = inferredDimension; @@ -336,13 +339,12 @@ export function createFunctionCallNode(strandsContext, functionName, rawUserArgs similarity += 1; } - overloadParameters.push({ baseType: expectedType.baseType, dimension }); } if (isValid && (!bestOverload || similarity > bestScore)) { - bestOverload = overloadParameters; + bestOverload = overload; bestScore = similarity; - inferredReturnType = overload.returnType; + inferredReturnType = {...overload.returnType }; if (isGeneric(inferredReturnType)) { inferredReturnType.dimension = inferredDimension; } @@ -350,17 +352,20 @@ export function createFunctionCallNode(strandsContext, functionName, rawUserArgs } if (bestOverload === null) { - FES.userError('parameter validation', 'No matching overload found!'); + FES.userError('parameter validation', `No matching overload for ${functionName} was found!`); } let dependsOn = []; - for (let i = 0; i < bestOverload.length; i++) { + for (let i = 0; i < bestOverload.params.length; i++) { const arg = preprocessedArgs[i]; - if (arg.originalNodeID) { + const paramType = { ...bestOverload.params[i] }; + if (isGeneric(paramType)) { + paramType.dimension = inferredDimension; + } + if (arg.originalNodeID && typeEquals(arg.inferredTypeInfo, paramType)) { dependsOn.push(arg.originalNodeID); } else { - const paramType = bestOverload[i]; const castedArgID = constructTypeFromIDs(strandsContext, paramType, arg.mappedDependencies); CFG.recordInBasicBlock(cfg, cfg.currentBlock, castedArgID); dependsOn.push(castedArgID); diff --git a/src/strands/ir_dag.js b/src/strands/ir_dag.js index ae384aa346..6ad54752e1 100644 --- a/src/strands/ir_dag.js +++ b/src/strands/ir_dag.js @@ -25,16 +25,16 @@ export function createDirectedAcyclicGraph() { } export function getOrCreateNode(graph, node) { - const key = getNodeKey(node); - const existing = graph.cache.get(key); + // const key = getNodeKey(node); + // const existing = graph.cache.get(key); - if (existing !== undefined) { - return existing; - } else { + // if (existing !== undefined) { + // return existing; + // } else { const id = createNode(graph, node); - graph.cache.set(key, id); + // graph.cache.set(key, id); return id; - } + // } } export function createNodeData(data = {}) { @@ -74,6 +74,26 @@ export function extractNodeTypeInfo(dag, nodeID) { priority: BasePriority[dag.baseTypes[nodeID]], }; } + +export function sortDAG(adjacencyList, start) { + const visited = new Set(); + const postOrder = []; + + function dfs(v) { + if (visited.has(v)) { + return; + } + visited.add(v); + for (let w of adjacencyList[v]) { + dfs(w); + } + postOrder.push(v); + } + + dfs(start); + return postOrder; +} + ///////////////////////////////// // Private functions ///////////////////////////////// @@ -118,23 +138,4 @@ function validateNode(node){ if (missingFields.length > 0) { FES.internalError(`Missing fields ${missingFields.join(', ')} for a node type '${NodeTypeToName[nodeType]}'.`); } -} - -export function sortDAG(adjacencyList, start) { - const visited = new Set(); - const postOrder = []; - - function dfs(v) { - if (visited.has(v)) { - return; - } - visited.add(v); - for (let w of adjacencyList[v]) { - dfs(w); - } - postOrder.push(v); - } - - dfs(start); - return postOrder; } \ No newline at end of file diff --git a/src/strands/strands_codegen.js b/src/strands/strands_codegen.js index 5f892c6909..26c0a85f14 100644 --- a/src/strands/strands_codegen.js +++ b/src/strands/strands_codegen.js @@ -16,7 +16,6 @@ function generateTopLevelDeclarations(strandsContext, generationContext, dagOrde if (dag.nodeTypes[nodeID] !== NodeType.OPERATION) { continue; } - if (usedCount[nodeID] > 0) { const newDeclaration = backend.generateDeclaration(generationContext, dag, nodeID); declarations.push(newDeclaration); From 54851baf95b23db7f44e545e2095df87829449ec Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Wed, 30 Jul 2025 11:31:07 +0100 Subject: [PATCH 52/69] remove dag sort, use basic block instructions instead. Also start work on swizzles --- preview/global/sketch.js | 10 +-- src/strands/ir_builders.js | 54 +++++++++++----- src/strands/ir_cfg.js | 7 +++ src/strands/ir_dag.js | 35 ++++------- src/strands/ir_types.js | 17 ++++-- src/strands/p5.strands.js | 8 +-- src/strands/strands_api.js | 98 ++++++++++++++++++++++++------ src/strands/strands_codegen.js | 34 +---------- src/strands/strands_glslBackend.js | 45 ++++++++++---- 9 files changed, 197 insertions(+), 111 deletions(-) diff --git a/preview/global/sketch.js b/preview/global/sketch.js index 4a34d4cbce..35719bcc25 100644 --- a/preview/global/sketch.js +++ b/preview/global/sketch.js @@ -2,12 +2,14 @@ p5.disableFriendlyErrors = true; function callback() { const time = uniformFloat(() =>millis()*0.001) - getFinalColor((col) => { - return [1,0,0, 1] +[1, 0, 0.1, 0] + pow(col,sin(time)); - }); + // getFinalColor((col) => { + // return vec4(1,0,0,1).rgba; + // }); getWorldInputs(inputs => { - inputs.color = vec4(inputs.position, 1); + // strandsIf(inputs.position === vec3(1), () => 0).Else() + console.log(inputs.position); + inputs.color = vec4(inputs.position.xyz, 1); inputs.position = inputs.position + sin(time) * 100; return inputs; }); diff --git a/src/strands/ir_builders.js b/src/strands/ir_builders.js index 3e159ec14a..89e5fe7401 100644 --- a/src/strands/ir_builders.js +++ b/src/strands/ir_builders.js @@ -8,7 +8,7 @@ import { strandsBuiltinFunctions } from './strands_builtins'; ////////////////////////////////////////////// // Builders for node graphs ////////////////////////////////////////////// -export function createLiteralNode(strandsContext, typeInfo, value) { +export function createScalarLiteralNode(strandsContext, typeInfo, value) { const { cfg, dag } = strandsContext let { dimension, baseType } = typeInfo; @@ -40,6 +40,22 @@ export function createVariableNode(strandsContext, typeInfo, identifier) { return { id, components: dimension }; } +export function createSwizzleNode(strandsContext, parentNode, swizzle) { + const { dag, cfg } = strandsContext; + const baseType = dag.baseTypes[parentNode.id]; + const nodeData = DAG.createNodeData({ + nodeType: NodeType.OPERATION, + baseType, + dimension: swizzle.length, + opCode: OpCode.Unary.SWIZZLE, + dependsOn: [parentNode.id], + swizzle, + }); + const id = DAG.getOrCreateNode(dag, nodeData); + CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); + return id; +} + export function createBinaryOpNode(strandsContext, leftStrandsNode, rightArg, opCode) { const { dag, cfg } = strandsContext; // Construct a node for right if its just an array or number etc. @@ -48,7 +64,7 @@ export function createBinaryOpNode(strandsContext, leftStrandsNode, rightArg, op rightStrandsNode = rightArg[0]; } else { const { id, components } = createPrimitiveConstructorNode(strandsContext, { baseType: BaseType.DEFER, dimension: null }, rightArg); - rightStrandsNode = new StrandsNode(id); + rightStrandsNode = new StrandsNode(id, components, strandsContext); } let finalLeftNodeID = leftStrandsNode.id; let finalRightNodeID = rightStrandsNode.id; @@ -138,7 +154,6 @@ export function createMemberAccessNode(strandsContext, parentNode, componentNode return { id, components: memberTypeInfo.dimension }; } - export function createStructInstanceNode(strandsContext, structTypeInfo, identifier, dependsOn) { const { cfg, dag, } = strandsContext; @@ -151,9 +166,9 @@ export function createStructInstanceNode(strandsContext, structTypeInfo, identif dimension: typeInfo.dimension, identifier: `${identifier}.${prop.name}`, }); - const component = DAG.getOrCreateNode(dag, nodeData); - CFG.recordInBasicBlock(cfg, cfg.currentBlock, component.id); - dependsOn.push(component); + const componentID = DAG.getOrCreateNode(dag, nodeData); + CFG.recordInBasicBlock(cfg, cfg.currentBlock, componentID); + dependsOn.push(componentID); } } @@ -196,7 +211,7 @@ function mapPrimitiveDepsToIDs(strandsContext, typeInfo, dependsOn) { continue; } else if (typeof dep === 'number') { - const { id, components } = createLiteralNode(strandsContext, { dimension: 1, baseType }, dep); + const { id, components } = createScalarLiteralNode(strandsContext, { dimension: 1, baseType }, dep); mappedDependencies.push(id); calculatedDimensions += 1; continue; @@ -241,8 +256,10 @@ export function createPrimitiveConstructorNode(strandsContext, typeInfo, depends dimension: inferredTypeInfo.dimension }; const id = constructTypeFromIDs(strandsContext, finalType, mappedDependencies); - CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); - return { id, components: finalType.dimension }; + if (typeInfo.baseType !== BaseType.DEFER) { + CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); + } + return { id, components: mappedDependencies }; } export function createStructConstructorNode(strandsContext, structTypeInfo, rawUserArgs) { @@ -382,22 +399,31 @@ export function createFunctionCallNode(strandsContext, functionName, rawUserArgs }) const id = DAG.getOrCreateNode(dag, nodeData); CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); - return { id, components: nodeData.dimension }; + return { id, components: { dependsOn, dimension: inferredReturnType.dimension } }; } export function createUnaryOpNode(strandsContext, strandsNode, opCode) { const { dag, cfg } = strandsContext; + const dependsOn = strandsNode.id; const nodeData = DAG.createNodeData({ nodeType: NodeType.OPERATION, opCode, - dependsOn: strandsNode.id, + dependsOn, baseType: dag.baseTypes[strandsNode.id], dimension: dag.dimensions[strandsNode.id], }) + const id = DAG.getOrCreateNode(dag, nodeData); CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); - return { id, components: nodeData.dimension }; + return { id, components: {dep} }; } -export function createStatementNode(strandsContext, type) { - return -99; +export function createStatementNode(strandsContext, opCode) { + const { dag, cfg } = strandsContext; + const nodeData = DAG.createNodeData({ + nodeType: NodeType.STATEMENT, + opCode + }); + const id = DAG.getOrCreateNode(dag, nodeData); + CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); + return id; } \ No newline at end of file diff --git a/src/strands/ir_cfg.js b/src/strands/ir_cfg.js index 27a323b885..78528c6789 100644 --- a/src/strands/ir_cfg.js +++ b/src/strands/ir_cfg.js @@ -1,4 +1,5 @@ import { BlockTypeToName } from "./ir_types"; +import * as FES from './strands_FES' export function createControlFlowGraph() { return { @@ -41,6 +42,12 @@ export function addEdge(graph, from, to) { } export function recordInBasicBlock(graph, blockID, nodeID) { + if (nodeID === undefined) { + FES.internalError('undefined nodeID in `recordInBasicBlock()`'); + } + if (blockID === undefined) { + FES.internalError('undefined blockID in `recordInBasicBlock()'); + } graph.blockInstructions[blockID] = graph.blockInstructions[blockID] || []; graph.blockInstructions[blockID].push(nodeID); } diff --git a/src/strands/ir_dag.js b/src/strands/ir_dag.js index 6ad54752e1..8cebf62b90 100644 --- a/src/strands/ir_dag.js +++ b/src/strands/ir_dag.js @@ -1,4 +1,4 @@ -import { NodeTypeRequiredFields, NodeTypeToName, BasePriority } from './ir_types'; +import { NodeTypeRequiredFields, NodeTypeToName, BasePriority, StatementType } from './ir_types'; import * as FES from './strands_FES'; ///////////////////////////////// @@ -18,7 +18,8 @@ export function createDirectedAcyclicGraph() { phiBlocks: [], dependsOn: [], usedBy: [], - graphType: 'DAG', + statementTypes: [], + swizzles: [], }; return graph; @@ -45,6 +46,8 @@ export function createNodeData(data = {}) { opCode: data.opCode ?? null, value: data.value ?? null, identifier: data.identifier ?? null, + statementType: data.statementType ?? null, + swizzle: data.swizzles ?? null, dependsOn: Array.isArray(data.dependsOn) ? data.dependsOn : [], usedBy: Array.isArray(data.usedBy) ? data.usedBy : [], phiBlocks: Array.isArray(data.phiBlocks) ? data.phiBlocks : [], @@ -55,6 +58,7 @@ export function createNodeData(data = {}) { export function getNodeDataFromID(graph, id) { return { + id, nodeType: graph.nodeTypes[id], opCode: graph.opCodes[id], value: graph.values[id], @@ -64,6 +68,8 @@ export function getNodeDataFromID(graph, id) { phiBlocks: graph.phiBlocks[id], dimension: graph.dimensions[id], baseType: graph.baseTypes[id], + statementType: graph.statementTypes[id], + swizzle: graph.swizzles[id], } } @@ -75,25 +81,6 @@ export function extractNodeTypeInfo(dag, nodeID) { }; } -export function sortDAG(adjacencyList, start) { - const visited = new Set(); - const postOrder = []; - - function dfs(v) { - if (visited.has(v)) { - return; - } - visited.add(v); - for (let w of adjacencyList[v]) { - dfs(w); - } - postOrder.push(v); - } - - dfs(start); - return postOrder; -} - ///////////////////////////////// // Private functions ///////////////////////////////// @@ -108,7 +95,9 @@ function createNode(graph, node) { graph.phiBlocks[id] = node.phiBlocks.slice(); graph.baseTypes[id] = node.baseType graph.dimensions[id] = node.dimension; - + graph.statementTypes[id] = node.statementType; + graph.swizzles[id] = node.swizzle + for (const dep of node.dependsOn) { if (!Array.isArray(graph.usedBy[dep])) { graph.usedBy[dep] = []; @@ -125,7 +114,7 @@ function getNodeKey(node) { function validateNode(node){ const nodeType = node.nodeType; - const requiredFields = [...NodeTypeRequiredFields[nodeType], 'baseType', 'dimension']; + const requiredFields = NodeTypeRequiredFields[nodeType]; 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!`) } diff --git a/src/strands/ir_types.js b/src/strands/ir_types.js index 021ee0f404..3082e4fc27 100644 --- a/src/strands/ir_types.js +++ b/src/strands/ir_types.js @@ -8,19 +8,26 @@ export const NodeType = { CONSTANT: 3, STRUCT: 4, PHI: 5, + STATEMENT: 6, }; + export const NodeTypeToName = Object.fromEntries( Object.entries(NodeType).map(([key, val]) => [val, key]) ); export const NodeTypeRequiredFields = { - [NodeType.OPERATION]: ["opCode", "dependsOn"], - [NodeType.LITERAL]: ["value"], - [NodeType.VARIABLE]: ["identifier"], - [NodeType.CONSTANT]: ["value"], + [NodeType.OPERATION]: ["opCode", "dependsOn", "dimension", "baseType"], + [NodeType.LITERAL]: ["value", "dimension", "baseType"], + [NodeType.VARIABLE]: ["identifier", "dimension", "baseType"], + [NodeType.CONSTANT]: ["value", "dimension", "baseType"], [NodeType.STRUCT]: [""], - [NodeType.PHI]: ["dependsOn", "phiBlocks"] + [NodeType.PHI]: ["dependsOn", "phiBlocks", "dimension", "baseType"], + [NodeType.STATEMENT]: ["opCode"] +}; + +export const StatementType = { + DISCARD: 'discard', }; export const BaseType = { diff --git a/src/strands/p5.strands.js b/src/strands/p5.strands.js index ec4c70d2db..da35c7097d 100644 --- a/src/strands/p5.strands.js +++ b/src/strands/p5.strands.js @@ -28,7 +28,7 @@ function strands(p5, fn) { ctx.previousFES = p5.disableFriendlyErrors; p5.disableFriendlyErrors = true; } - + function deinitStrandsContext(ctx) { ctx.dag = createDirectedAcyclicGraph(); ctx.cfg = createControlFlowGraph(); @@ -36,11 +36,11 @@ function strands(p5, fn) { ctx.hooks = []; p5.disableFriendlyErrors = ctx.previousFES; } - + const strandsContext = {}; initStrandsContext(strandsContext); initGlobalStrandsAPI(p5, fn, strandsContext) - + ////////////////////////////////////////////// // Entry Point ////////////////////////////////////////////// @@ -52,7 +52,7 @@ function strands(p5, fn) { const backend = glslBackend; initStrandsContext(strandsContext, glslBackend); createShaderHooksFunctions(strandsContext, fn, this); - + // 1. Transpile from strands DSL to JS let strandsCallback; if (options.parser) { diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index 83a97aaf07..e83f9f447d 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -5,9 +5,9 @@ import { createStatementNode, createPrimitiveConstructorNode, createUnaryOpNode, - createMemberAccessNode, createStructInstanceNode, createStructConstructorNode, + createSwizzleNode, } from './ir_builders' import { OperatorTable, @@ -16,7 +16,8 @@ import { BaseType, StructType, TypeInfoFromGLSLName, - isStructType, + isStructType, + OpCode, // isNativeType } from './ir_types' import { strandsBuiltinFunctions } from './strands_builtins' @@ -28,10 +29,68 @@ import { getNodeDataFromID } from './ir_dag' ////////////////////////////////////////////// // User nodes ////////////////////////////////////////////// +const swizzlesSet = new Set(); + export class StrandsNode { - constructor(id) { + constructor(id, dimension, strandsContext) { this.id = id; + this.strandsContext = strandsContext; + this.dimension = dimension; + installSwizzlesForDimension.call(this, strandsContext, dimension) + } +} + +function generateSwizzles(chars, maxLen = 4) { + const result = []; + + function build(current) { + if (current.length > 0) result.push(current); + if (current.length === maxLen) return; + + for (let c of chars) { + build(current + c); + } + } + + build(''); + return result; +} + +function installSwizzlesForDimension(strandsContext, dimension) { + if (swizzlesSet.has(dimension)) return; + swizzlesSet.add(dimension); + + const swizzleVariants = [ + ['x', 'y', 'z', 'w'], + ['r', 'g', 'b', 'a'], + ['s', 't', 'p', 'q'] + ].map(chars => chars.slice(0, dimension)); + + const descriptors = {}; + + for (const variant of swizzleVariants) { + const swizzleStrings = generateSwizzles(variant); + for (const swizzle of swizzleStrings) { + if (swizzle.length < 1 || swizzle.length > 4) continue; + if (descriptors[swizzle]) continue; + + const hasDuplicates = new Set(swizzle).size !== swizzle.length; + + descriptors[swizzle] = { + get() { + const id = createSwizzleNode(strandsContext, this, swizzle); + return new StrandsNode(id, 0, strandsContext); + }, + ...(hasDuplicates ? {} : { + set(value) { + return assignSwizzleNode(strandsContext, this, swizzle, value); + } + }) + }; + } } + + Object.defineProperties(this, descriptors); } export function initGlobalStrandsAPI(p5, fn, strandsContext) { @@ -41,23 +100,22 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { if (arity === 'binary') { StrandsNode.prototype[name] = function (...right) { const { id, components } = createBinaryOpNode(strandsContext, this, right, opCode); - return new StrandsNode(id); + return new StrandsNode(id, components, strandsContext); }; } if (arity === 'unary') { fn[name] = function (strandsNode) { const { id, components } = createUnaryOpNode(strandsContext, strandsNode, opCode); - return new StrandsNode(id); + return new StrandsNode(id, components, strandsContext); } } } - + ////////////////////////////////////////////// // Unique Functions ////////////////////////////////////////////// fn.discard = function() { - const { id, components } = createStatementNode('discard'); - CFG.recordInBasicBlock(strandsContext.cfg, strandsContext.cfg.currentBlock, id); + createStatementNode(strandsContext, OpCode.ControlFlow.DISCARD); } fn.strandsIf = function(conditionNode, ifBody) { @@ -76,7 +134,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { FES.userError("type error", "It looks like you've tried to construct a p5.strands node implicitly, with more than 4 components. This is currently not supported.") } const { id, components } = createPrimitiveConstructorNode(strandsContext, { baseType: BaseType.DEFER, dimension: null }, args.flat()); - return new StrandsNode(id); + return new StrandsNode(id, components, strandsContext); } ////////////////////////////////////////////// @@ -90,7 +148,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { fn[functionName] = function(...args) { if (strandsContext.active) { const { id, components } = createFunctionCallNode(strandsContext, functionName, args); - return new StrandsNode(id); + return new StrandsNode(id, components, strandsContext); } else { return originalFn.apply(this, args); } @@ -99,7 +157,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { fn[functionName] = function (...args) { if (strandsContext.active) { const { id, components } = createFunctionCallNode(strandsContext, functionName, args); - return new StrandsNode(id); + return new StrandsNode(id, components, strandsContext); } else { p5._friendlyError( `It looks like you've called ${functionName} outside of a shader's modify() function.` @@ -131,14 +189,14 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { fn[`uniform${pascalTypeName}`] = function(name, defaultValue) { const { id, components } = createVariableNode(strandsContext, typeInfo, name); strandsContext.uniforms.push({ name, typeInfo, defaultValue }); - return new StrandsNode(id); + return new StrandsNode(id, components, strandsContext); }; const originalp5Fn = fn[typeInfo.fnName]; fn[typeInfo.fnName] = function(...args) { if (strandsContext.active) { const { id, components } = createPrimitiveConstructorNode(strandsContext, typeInfo, args); - return new StrandsNode(id); + return new StrandsNode(id, components, strandsContext); } else if (originalp5Fn) { return originalp5Fn.apply(this, args); } else { @@ -155,26 +213,28 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { ////////////////////////////////////////////// function createHookArguments(strandsContext, parameters){ const args = []; + const dag = strandsContext.dag; for (const param of parameters) { const paramType = param.type; if(isStructType(paramType.typeName)) { const structType = StructType[paramType.typeName]; const originalInstanceInfo = createStructInstanceNode(strandsContext, structType, param.name, []); - const structNode = new StrandsNode(originalInstanceInfo.id); - // const componentNodes = originalInstanceInfo.components.map(id => new StrandsNode(id)) + const structNode = new StrandsNode(originalInstanceInfo.id, 0, strandsContext); + // const componentNodes = originalInstanceInfo.components.map(id => new StrandsNode(id, components)) for (let i = 0; i < structType.properties.length; i++) { const componentTypeInfo = structType.properties[i]; Object.defineProperty(structNode, componentTypeInfo.name, { get() { - return new StrandsNode(strandsContext.dag.dependsOn[structNode.id][i]) + const propNode = getNodeDataFromID(dag, dag.dependsOn[structNode.id][i]) + return new StrandsNode(propNode.id, propNode.dimension, strandsContext); // const { id, components } = createMemberAccessNode(strandsContext, structNode, componentNodes[i], componentTypeInfo.dataType); - // const memberAccessNode = new StrandsNode(id); + // const memberAccessNode = new StrandsNode(id, components); // return memberAccessNode; }, set(val) { - const oldDependsOn = strandsContext.dag.dependsOn[structNode.id]; + const oldDependsOn = dag.dependsOn[structNode.id]; const newDependsOn = [...oldDependsOn]; let newValueID; @@ -198,7 +258,7 @@ function createHookArguments(strandsContext, parameters){ else /*if(isNativeType(paramType.typeName))*/ { const typeInfo = TypeInfoFromGLSLName[paramType.typeName]; const { id, components } = createVariableNode(strandsContext, typeInfo, param.name); - const arg = new StrandsNode(id); + const arg = new StrandsNode(id, components, strandsContext); args.push(arg); } } diff --git a/src/strands/strands_codegen.js b/src/strands/strands_codegen.js index 26c0a85f14..065c22fb64 100644 --- a/src/strands/strands_codegen.js +++ b/src/strands/strands_codegen.js @@ -1,29 +1,4 @@ -import { NodeType } from './ir_types'; import { sortCFG } from './ir_cfg'; -import { sortDAG } from './ir_dag'; -import strands from './p5.strands'; - -function generateTopLevelDeclarations(strandsContext, generationContext, dagOrder) { - const { dag, backend } = strandsContext; - - const usedCount = {}; - for (const nodeID of dagOrder) { - usedCount[nodeID] = (dag.usedBy[nodeID] || []).length; - } - - const declarations = []; - for (const nodeID of dagOrder) { - if (dag.nodeTypes[nodeID] !== NodeType.OPERATION) { - continue; - } - if (usedCount[nodeID] > 0) { - const newDeclaration = backend.generateDeclaration(generationContext, dag, nodeID); - declarations.push(newDeclaration); - } - } - - return declarations; -} export function generateShaderCode(strandsContext) { const { cfg, dag, backend } = strandsContext; @@ -37,8 +12,8 @@ export function generateShaderCode(strandsContext) { hooksObj.uniforms[declaration] = defaultValue; } - for (const { hookType, entryBlockID, rootNodeID, rootStruct} of strandsContext.hooks) { - const dagSorted = sortDAG(dag.dependsOn, rootNodeID); + for (const { hookType, entryBlockID, rootNodeID} of strandsContext.hooks) { + // const dagSorted = sortDAG(dag.dependsOn, rootNodeID); const cfgSorted = sortCFG(cfg.outgoingEdges, entryBlockID); const generationContext = { @@ -47,15 +22,12 @@ export function generateShaderCode(strandsContext) { write(line) { this.codeLines.push(' '.repeat(this.indent) + line); }, - dagSorted, + // dagSorted, tempNames: {}, declarations: [], nextTempID: 0, }; - generationContext.declarations = generateTopLevelDeclarations(strandsContext, generationContext, dagSorted); - - generationContext.declarations.forEach(decl => generationContext.write(decl)); for (const blockID of cfgSorted) { backend.generateBlock(blockID, strandsContext, generationContext); } diff --git a/src/strands/strands_glslBackend.js b/src/strands/strands_glslBackend.js index 9d138f9030..97e475ac4c 100644 --- a/src/strands/strands_glslBackend.js +++ b/src/strands/strands_glslBackend.js @@ -1,7 +1,14 @@ -import { NodeType, OpCodeToSymbol, BlockType, OpCode, NodeTypeToName, isStructType, StructType } from "./ir_types"; +import { NodeType, OpCodeToSymbol, BlockType, OpCode, NodeTypeToName, isStructType, StructType, 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; + const uses = dag.usedBy[nodeID] || []; + return uses.length > 1; +} + const TypeNames = { 'float1': 'float', 'float2': 'vec2', @@ -25,16 +32,20 @@ const TypeNames = { const cfgHandlers = { [BlockType.DEFAULT]: (blockID, strandsContext, generationContext) => { - // const { dag, cfg } = strandsContext; - - // const blockInstructions = new Set(cfg.blockInstructions[blockID] || []); - // for (let nodeID of generationContext.dagSorted) { - // if (!blockInstructions.has(nodeID)) { - // continue; - // } - // const snippet = glslBackend.generateExpression(dag, nodeID, generationContext); - // generationContext.write(snippet); - // } + const { dag, cfg } = strandsContext; + + const instructions = cfg.blockInstructions[blockID] || []; + for (const nodeID of instructions) { + const nodeType = dag.nodeTypes[nodeID]; + if (shouldCreateTemp(dag, nodeID)) { + const declaration = glslBackend.generateDeclaration(generationContext, dag, nodeID); + generationContext.write(declaration); + } + if (nodeType === NodeType.STATEMENT) { + console.log("HELLO") + glslBackend.generateStatement(generationContext, dag, nodeID); + } + } }, [BlockType.IF_COND](blockID, strandsContext, generationContext) { @@ -91,6 +102,13 @@ export const glslBackend = { 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;'); + } + }, + generateDeclaration(generationContext, dag, nodeID) { const expr = this.generateExpression(generationContext, dag, nodeID); const tmp = `T${generationContext.nextTempID++}`; @@ -151,6 +169,11 @@ export const glslBackend = { const rName = this.generateExpression(generationContext, dag, rID); return `${lName}.${rName}`; } + if (node.opCode === OpCode.Unary.SWIZZLE) { + const parentID = node.dependsOn[0]; + const parentExpr = this.generateExpression(generationContext, dag, parentID); + return `${parentExpr}.${node.swizzle}`; + } if (node.dependsOn.length === 2) { const [lID, rID] = node.dependsOn; const left = this.generateExpression(generationContext, dag, lID); From ebaaa08e855053cb5411569915b075c24d6dd791 Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Wed, 30 Jul 2025 11:59:31 +0100 Subject: [PATCH 53/69] change example --- preview/global/sketch.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/preview/global/sketch.js b/preview/global/sketch.js index 35719bcc25..494c128856 100644 --- a/preview/global/sketch.js +++ b/preview/global/sketch.js @@ -8,8 +8,7 @@ function callback() { getWorldInputs(inputs => { // strandsIf(inputs.position === vec3(1), () => 0).Else() - console.log(inputs.position); - inputs.color = vec4(inputs.position.xyz, 1); + inputs.color = vec4(inputs.position, 1); inputs.position = inputs.position + sin(time) * 100; return inputs; }); From 3d11637241331510b15d195f32e603e55ec3f959 Mon Sep 17 00:00:00 2001 From: Luke Plowden <62835749+lukeplowden@users.noreply.github.com> Date: Tue, 5 Aug 2025 11:11:34 +0100 Subject: [PATCH 54/69] Update src/strands/ir_builders.js Co-authored-by: Dave Pagurek --- src/strands/ir_builders.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/strands/ir_builders.js b/src/strands/ir_builders.js index 89e5fe7401..45dcf9bd94 100644 --- a/src/strands/ir_builders.js +++ b/src/strands/ir_builders.js @@ -13,7 +13,7 @@ export function createScalarLiteralNode(strandsContext, typeInfo, value) { let { dimension, baseType } = typeInfo; if (dimension !== 1) { - FES.internalError('Created a literal node with dimension > 1.') + FES.internalError('Created a scalar literal node with dimension > 1.') } const nodeData = DAG.createNodeData({ nodeType: NodeType.LITERAL, From 347900f8f6e146ed9a9a012bace9a0f914c9dd09 Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Tue, 5 Aug 2025 14:09:20 +0100 Subject: [PATCH 55/69] remove CFG sorting, make merge block use default behaviour, change types to strings for debug, attach API to fn instead of window --- preview/global/sketch.js | 2 +- src/strands/ir_cfg.js | 23 ++++---------------- src/strands/ir_types.js | 34 +++++++++++++++--------------- src/strands/p5.strands.js | 3 ++- src/strands/strands_api.js | 4 +++- src/strands/strands_codegen.js | 11 ++-------- src/strands/strands_glslBackend.js | 5 +++-- 7 files changed, 32 insertions(+), 50 deletions(-) diff --git a/preview/global/sketch.js b/preview/global/sketch.js index 494c128856..4fae6e678e 100644 --- a/preview/global/sketch.js +++ b/preview/global/sketch.js @@ -7,8 +7,8 @@ function callback() { // }); getWorldInputs(inputs => { - // strandsIf(inputs.position === vec3(1), () => 0).Else() inputs.color = vec4(inputs.position, 1); + strandsIf(inputs.position === vec3(1), () => 0).Else() inputs.position = inputs.position + sin(time) * 100; return inputs; }); diff --git a/src/strands/ir_cfg.js b/src/strands/ir_cfg.js index 78528c6789..a8dabf66ed 100644 --- a/src/strands/ir_cfg.js +++ b/src/strands/ir_cfg.js @@ -1,6 +1,8 @@ import { BlockTypeToName } from "./ir_types"; import * as FES from './strands_FES' +// Todo: remove edges to simplify. Block order is always ordered already. + export function createControlFlowGraph() { return { // graph structure @@ -11,6 +13,7 @@ export function createControlFlowGraph() { // runtime data for constructing graph nextID: 0, blockStack: [], + blockOrder: [], blockConditions: {}, currentBlock: -1, }; @@ -18,6 +21,7 @@ export function createControlFlowGraph() { export function pushBlock(graph, blockID) { graph.blockStack.push(blockID); + graph.blockOrder.push(blockID); graph.currentBlock = blockID; } @@ -66,23 +70,4 @@ export function printBlockData(graph, id) { const block = getBlockDataFromID(graph, id); block.blockType = BlockTypeToName[block.blockType]; console.log(block); -} - -export function sortCFG(adjacencyList, start) { - const visited = new Set(); - const postOrder = []; - - function dfs(v) { - if (visited.has(v)) { - return; - } - visited.add(v); - for (let w of adjacencyList[v].sort((a, b) => b-a) || []) { - dfs(w); - } - postOrder.push(v); - } - - dfs(start); - return postOrder.reverse(); } \ No newline at end of file diff --git a/src/strands/ir_types.js b/src/strands/ir_types.js index 3082e4fc27..724e2d4ea2 100644 --- a/src/strands/ir_types.js +++ b/src/strands/ir_types.js @@ -2,13 +2,13 @@ // Enums for nodes // ///////////////////// export const NodeType = { - OPERATION: 0, - LITERAL: 1, - VARIABLE: 2, - CONSTANT: 3, - STRUCT: 4, - PHI: 5, - STATEMENT: 6, + OPERATION: 'operation', + LITERAL: 'literal', + VARIABLE: 'variable', + CONSTANT: 'constant', + STRUCT: 'struct', + PHI: 'phi', + STATEMENT: 'statement', }; @@ -180,16 +180,16 @@ for (const { symbol, opCode } of OperatorTable) { } export const BlockType = { - GLOBAL: 0, - FUNCTION: 1, - IF_COND: 2, - IF_BODY: 3, - ELIF_BODY: 4, - ELIF_COND: 5, - ELSE_BODY: 6, - FOR: 7, - MERGE: 8, - DEFAULT: 9, + GLOBAL: 'global', + FUNCTION: 'function', + IF_COND: 'if_cond', + IF_BODY: 'if_body', + ELIF_BODY: 'elif_body', + ELIF_COND: 'elif_cond', + ELSE_BODY: 'else_body', + FOR: 'for', + MERGE: 'merge', + DEFAULT: 'default', } export const BlockTypeToName = Object.fromEntries( diff --git a/src/strands/p5.strands.js b/src/strands/p5.strands.js index da35c7097d..82102f1b3b 100644 --- a/src/strands/p5.strands.js +++ b/src/strands/p5.strands.js @@ -35,6 +35,7 @@ function strands(p5, fn) { ctx.uniforms = []; ctx.hooks = []; p5.disableFriendlyErrors = ctx.previousFES; + ctx.active = false; } const strandsContext = {}; @@ -74,7 +75,7 @@ function strands(p5, fn) { console.log(hooksObject['Vertex getWorldInputs']); // Reset the strands runtime context - // deinitStrandsContext(strandsContext); + deinitStrandsContext(strandsContext); // Call modify with the generated hooks object return oldModify.call(this, hooksObject); diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index e83f9f447d..85c0f094c7 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -304,6 +304,8 @@ function enforceReturnTypeMatch(strandsContext, expectedType, returned, hookName return returnedNodeID; } +// TODO: track overridden functions and restore them + export function createShaderHooksFunctions(strandsContext, fn, shader) { const availableHooks = { ...shader.hooks.vertex, @@ -313,7 +315,7 @@ export function createShaderHooksFunctions(strandsContext, fn, shader) { const cfg = strandsContext.cfg; for (const hookType of hookTypes) { - window[hookType.name] = function(hookUserCallback) { + fn[hookType.name] = function(hookUserCallback) { const entryBlockID = CFG.createBasicBlock(cfg, BlockType.FUNCTION); CFG.addEdge(cfg, cfg.currentBlock, entryBlockID); CFG.pushBlock(cfg, entryBlockID); diff --git a/src/strands/strands_codegen.js b/src/strands/strands_codegen.js index 065c22fb64..74c99bce0d 100644 --- a/src/strands/strands_codegen.js +++ b/src/strands/strands_codegen.js @@ -1,7 +1,5 @@ -import { sortCFG } from './ir_cfg'; - export function generateShaderCode(strandsContext) { - const { cfg, dag, backend } = strandsContext; + const { cfg, backend } = strandsContext; const hooksObj = { uniforms: {}, @@ -13,28 +11,23 @@ export function generateShaderCode(strandsContext) { } for (const { hookType, entryBlockID, rootNodeID} of strandsContext.hooks) { - // const dagSorted = sortDAG(dag.dependsOn, rootNodeID); - const cfgSorted = sortCFG(cfg.outgoingEdges, entryBlockID); - const generationContext = { indent: 1, codeLines: [], write(line) { this.codeLines.push(' '.repeat(this.indent) + line); }, - // dagSorted, tempNames: {}, declarations: [], nextTempID: 0, }; - for (const blockID of cfgSorted) { + for (const blockID of cfg.blockOrder) { backend.generateBlock(blockID, strandsContext, generationContext); } const firstLine = backend.hookEntry(hookType); backend.generateReturnStatement(strandsContext, generationContext, rootNodeID); - // generationContext.write(finalExpression); hooksObj[`${hookType.returnType.typeName} ${hookType.name}`] = [firstLine, ...generationContext.codeLines, '}'].join('\n'); } diff --git a/src/strands/strands_glslBackend.js b/src/strands/strands_glslBackend.js index 97e475ac4c..2cc119d3f3 100644 --- a/src/strands/strands_glslBackend.js +++ b/src/strands/strands_glslBackend.js @@ -1,4 +1,4 @@ -import { NodeType, OpCodeToSymbol, BlockType, OpCode, NodeTypeToName, isStructType, StructType, StatementType } from "./ir_types"; +import { NodeType, OpCodeToSymbol, BlockType, OpCode, NodeTypeToName, isStructType, StructType } from "./ir_types"; import { getNodeDataFromID, extractNodeTypeInfo } from "./ir_dag"; import * as FES from './strands_FES' @@ -73,7 +73,7 @@ const cfgHandlers = { }, [BlockType.MERGE](blockID, strandsContext, generationContext) { - + this[BlockType.DEFAULT](blockID, strandsContext, generationContext); }, [BlockType.FUNCTION](blockID, strandsContext, generationContext) { @@ -112,6 +112,7 @@ export const glslBackend = { generateDeclaration(generationContext, dag, nodeID) { const expr = this.generateExpression(generationContext, dag, nodeID); const tmp = `T${generationContext.nextTempID++}`; + console.log(expr); generationContext.tempNames[nodeID] = tmp; const T = extractNodeTypeInfo(dag, nodeID); From 1ddd5f8806fd327a7dc18653fd5701bc24e529e4 Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Tue, 5 Aug 2025 14:12:02 +0100 Subject: [PATCH 56/69] remove old file and imports --- preview/global/sketch.js | 2 +- src/strands/p5.strands.js | 4 ++-- src/webgl/{ShaderGenerator.js => ShaderGenerator.js.temp} | 6 +++--- src/webgl/index.js | 2 -- 4 files changed, 6 insertions(+), 8 deletions(-) rename src/webgl/{ShaderGenerator.js => ShaderGenerator.js.temp} (99%) diff --git a/preview/global/sketch.js b/preview/global/sketch.js index 4fae6e678e..9887bf8d76 100644 --- a/preview/global/sketch.js +++ b/preview/global/sketch.js @@ -16,7 +16,7 @@ function callback() { async function setup(){ createCanvas(windowWidth,windowHeight, WEBGL) - bloomShader = baseColorShader().newModify(callback); + bloomShader = baseColorShader().modify(callback); } function windowResized() { diff --git a/src/strands/p5.strands.js b/src/strands/p5.strands.js index 82102f1b3b..1ff28e02f7 100644 --- a/src/strands/p5.strands.js +++ b/src/strands/p5.strands.js @@ -45,9 +45,9 @@ function strands(p5, fn) { ////////////////////////////////////////////// // Entry Point ////////////////////////////////////////////// - const oldModify = p5.Shader.prototype.modify + const oldModify = p5.Shader.prototype.modify; - p5.Shader.prototype.newModify = function(shaderModifier, options = { parser: true, srcLocations: false }) { + p5.Shader.prototype.modify = function(shaderModifier, options = { parser: true, srcLocations: false }) { if (shaderModifier instanceof Function) { // Reset the context object every time modify is called; const backend = glslBackend; diff --git a/src/webgl/ShaderGenerator.js b/src/webgl/ShaderGenerator.js.temp similarity index 99% rename from src/webgl/ShaderGenerator.js rename to src/webgl/ShaderGenerator.js.temp index 5cf1ea9b1b..998e19cee2 100644 --- a/src/webgl/ShaderGenerator.js +++ b/src/webgl/ShaderGenerator.js.temp @@ -1686,6 +1686,6 @@ function shadergenerator(p5, fn) { export default shadergenerator; -if (typeof p5 !== 'undefined') { - p5.registerAddon(shadergenerator) -} +// if (typeof p5 !== 'undefined') { +// p5.registerAddon(shadergenerator) +// } diff --git a/src/webgl/index.js b/src/webgl/index.js index 355125b36e..52292100e8 100644 --- a/src/webgl/index.js +++ b/src/webgl/index.js @@ -14,7 +14,6 @@ import shader from './p5.Shader'; import camera from './p5.Camera'; import texture from './p5.Texture'; import rendererGL from './p5.RendererGL'; -import shadergenerator from './ShaderGenerator'; import strands from '../strands/p5.strands'; export default function(p5){ @@ -34,6 +33,5 @@ export default function(p5){ dataArray(p5, p5.prototype); shader(p5, p5.prototype); texture(p5, p5.prototype); - shadergenerator(p5, p5.prototype); strands(p5, p5.prototype); } From f80600630fddcb7f79b8adc2e86a58ca38c9a012 Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Thu, 11 Sep 2025 18:30:51 +0100 Subject: [PATCH 57/69] bug fixes, swizzle reads working, swizzle writes WIP --- preview/global/sketch.js | 11 +- src/strands/ir_builders.js | 169 +++++++++++++++++++++-------- src/strands/ir_cfg.js | 19 ++++ src/strands/ir_dag.js | 4 +- src/strands/ir_types.js | 20 +++- src/strands/p5.strands.js | 23 +++- src/strands/strands_api.js | 140 ++++++++---------------- src/strands/strands_codegen.js | 7 +- src/strands/strands_glslBackend.js | 33 +++--- src/strands/strands_transpiler.js | 65 ++++++----- 10 files changed, 290 insertions(+), 201 deletions(-) diff --git a/preview/global/sketch.js b/preview/global/sketch.js index 9887bf8d76..6c37abe5c9 100644 --- a/preview/global/sketch.js +++ b/preview/global/sketch.js @@ -2,14 +2,15 @@ p5.disableFriendlyErrors = true; function callback() { const time = uniformFloat(() =>millis()*0.001) - // getFinalColor((col) => { - // return vec4(1,0,0,1).rgba; - // }); + getFinalColor((col) => { + let test = vec3(1) + col.gra = test; + return col; + }); getWorldInputs(inputs => { inputs.color = vec4(inputs.position, 1); - strandsIf(inputs.position === vec3(1), () => 0).Else() - inputs.position = inputs.position + sin(time) * 100; + // inputs.position = inputs.position + sin(time) * 100; return inputs; }); } diff --git a/src/strands/ir_builders.js b/src/strands/ir_builders.js index 45dcf9bd94..238dda5d03 100644 --- a/src/strands/ir_builders.js +++ b/src/strands/ir_builders.js @@ -2,16 +2,15 @@ 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 { StrandsNode } from './strands_api'; +import { createStrandsNode, StrandsNode } from './strands_api'; import { strandsBuiltinFunctions } from './strands_builtins'; ////////////////////////////////////////////// // Builders for node graphs ////////////////////////////////////////////// -export function createScalarLiteralNode(strandsContext, typeInfo, value) { +export function scalarLiteralNode(strandsContext, typeInfo, value) { const { cfg, dag } = strandsContext let { dimension, baseType } = typeInfo; - if (dimension !== 1) { FES.internalError('Created a scalar literal node with dimension > 1.') } @@ -23,10 +22,10 @@ export function createScalarLiteralNode(strandsContext, typeInfo, value) { }); const id = DAG.getOrCreateNode(dag, nodeData); CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); - return { id, components: dimension }; + return { id, dimension }; } -export function createVariableNode(strandsContext, typeInfo, identifier) { +export function variableNode(strandsContext, typeInfo, identifier) { const { cfg, dag } = strandsContext; const { dimension, baseType } = typeInfo; const nodeData = DAG.createNodeData({ @@ -37,34 +36,41 @@ export function createVariableNode(strandsContext, typeInfo, identifier) { }) const id = DAG.getOrCreateNode(dag, nodeData); CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); - return { id, components: dimension }; + return { id, dimension }; } -export function createSwizzleNode(strandsContext, parentNode, swizzle) { +export function unaryOpNode(strandsContext, nodeOrValue, opCode) { const { dag, cfg } = strandsContext; - const baseType = dag.baseTypes[parentNode.id]; + let dependsOn; + let node; + if (nodeOrValue instanceof StrandsNode) { + node = nodeOrValue; + } else { + const { id, dimension } = primitiveConstructorNode(strandsContext, { baseType: BaseType.FLOAT, dimension: null }, nodeOrValue); + node = createStrandsNode(id, dimension, strandsContext); + } + dependsOn = [node.id]; const nodeData = DAG.createNodeData({ nodeType: NodeType.OPERATION, - baseType, - dimension: swizzle.length, - opCode: OpCode.Unary.SWIZZLE, - dependsOn: [parentNode.id], - swizzle, - }); + opCode, + dependsOn, + baseType: dag.baseTypes[node.id], + dimension: node.dimension + }) const id = DAG.getOrCreateNode(dag, nodeData); CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); - return id; + return { id, dimension: node.dimension }; } -export function createBinaryOpNode(strandsContext, leftStrandsNode, rightArg, opCode) { +export function binaryOpNode(strandsContext, leftStrandsNode, rightArg, opCode) { const { dag, cfg } = strandsContext; // Construct a node for right if its just an array or number etc. let rightStrandsNode; if (rightArg[0] instanceof StrandsNode && rightArg.length === 1) { rightStrandsNode = rightArg[0]; } else { - const { id, components } = createPrimitiveConstructorNode(strandsContext, { baseType: BaseType.DEFER, dimension: null }, rightArg); - rightStrandsNode = new StrandsNode(id, components, strandsContext); + const { id, dimension } = primitiveConstructorNode(strandsContext, { baseType: BaseType.DEFER, dimension: null }, rightArg); + rightStrandsNode = createStrandsNode(id, dimension, strandsContext); } let finalLeftNodeID = leftStrandsNode.id; let finalRightNodeID = rightStrandsNode.id; @@ -91,8 +97,8 @@ export function createBinaryOpNode(strandsContext, leftStrandsNode, rightArg, op `It's only possible to operate on two nodes with the same dimension, or a scalar value and a vector.` ); } - const l = createPrimitiveConstructorNode(strandsContext, cast.toType, leftStrandsNode); - const r = createPrimitiveConstructorNode(strandsContext, cast.toType, rightStrandsNode); + const l = primitiveConstructorNode(strandsContext, cast.toType, leftStrandsNode); + const r = primitiveConstructorNode(strandsContext, cast.toType, rightStrandsNode); finalLeftNodeID = l.id; finalRightNodeID = r.id; } @@ -120,7 +126,7 @@ export function createBinaryOpNode(strandsContext, leftStrandsNode, rightArg, op FES.userError('type error', `A vector of length ${leftType.dimension} operated with a vector of length ${rightType.dimension} is not allowed.`); } - const casted = createPrimitiveConstructorNode(strandsContext, cast.toType, cast.node); + const casted = primitiveConstructorNode(strandsContext, cast.toType, cast.node); if (cast.node === leftStrandsNode) { finalLeftNodeID = casted.id; } else { @@ -137,10 +143,10 @@ export function createBinaryOpNode(strandsContext, leftStrandsNode, rightArg, op }); const id = DAG.getOrCreateNode(dag, nodeData); CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); - return { id, components: nodeData.dimension }; + return { id, dimension: nodeData.dimension }; } -export function createMemberAccessNode(strandsContext, parentNode, componentNode, memberTypeInfo) { +export function memberAccessNode(strandsContext, parentNode, componentNode, memberTypeInfo) { const { dag, cfg } = strandsContext; const nodeData = DAG.createNodeData({ nodeType: NodeType.OPERATION, @@ -151,10 +157,10 @@ export function createMemberAccessNode(strandsContext, parentNode, componentNode }); const id = DAG.getOrCreateNode(dag, nodeData); CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); - return { id, components: memberTypeInfo.dimension }; + return { id, dimension: memberTypeInfo.dimension }; } -export function createStructInstanceNode(strandsContext, structTypeInfo, identifier, dependsOn) { +export function structInstanceNode(strandsContext, structTypeInfo, identifier, dependsOn) { const { cfg, dag, } = strandsContext; if (dependsOn.length === 0) { @@ -182,7 +188,7 @@ export function createStructInstanceNode(strandsContext, structTypeInfo, identif const structID = DAG.getOrCreateNode(dag, nodeData); CFG.recordInBasicBlock(cfg, cfg.currentBlock, structID); - return { id: structID, components: dependsOn }; + return { id: structID, dimension: 0, components: dependsOn }; } function mapPrimitiveDepsToIDs(strandsContext, typeInfo, dependsOn) { @@ -211,9 +217,9 @@ function mapPrimitiveDepsToIDs(strandsContext, typeInfo, dependsOn) { continue; } else if (typeof dep === 'number') { - const { id, components } = createScalarLiteralNode(strandsContext, { dimension: 1, baseType }, dep); + const { id, dimension } = scalarLiteralNode(strandsContext, { dimension: 1, baseType }, dep); mappedDependencies.push(id); - calculatedDimensions += 1; + calculatedDimensions += dimension; continue; } else { @@ -248,21 +254,25 @@ export function constructTypeFromIDs(strandsContext, typeInfo, strandsNodesArray return id; } -export function createPrimitiveConstructorNode(strandsContext, typeInfo, dependsOn) { - const { cfg, dag } = strandsContext; +export function primitiveConstructorNode(strandsContext, typeInfo, dependsOn) { + const cfg = strandsContext.cfg; const { mappedDependencies, inferredTypeInfo } = mapPrimitiveDepsToIDs(strandsContext, typeInfo, dependsOn); + const finalType = { baseType: typeInfo.baseType, dimension: inferredTypeInfo.dimension }; + const id = constructTypeFromIDs(strandsContext, finalType, mappedDependencies); + if (typeInfo.baseType !== BaseType.DEFER) { CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); } - return { id, components: mappedDependencies }; + + return { id, dimension: finalType.dimension, components: mappedDependencies }; } -export function createStructConstructorNode(strandsContext, structTypeInfo, rawUserArgs) { +export function structConstructorNode(strandsContext, structTypeInfo, rawUserArgs) { const { cfg, dag } = strandsContext; const { identifer, properties } = structTypeInfo; @@ -297,10 +307,10 @@ export function createStructConstructorNode(strandsContext, structTypeInfo, rawU }); const id = DAG.getOrCreateNode(dag, nodeData); CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); - return { id, components: structTypeInfo.components }; + return { id, dimension: properties.length, components: structTypeInfo.components }; } -export function createFunctionCallNode(strandsContext, functionName, rawUserArgs) { +export function functionCallNode(strandsContext, functionName, rawUserArgs) { const { cfg, dag } = strandsContext; const overloads = strandsBuiltinFunctions[functionName]; @@ -399,31 +409,94 @@ export function createFunctionCallNode(strandsContext, functionName, rawUserArgs }) const id = DAG.getOrCreateNode(dag, nodeData); CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); - return { id, components: { dependsOn, dimension: inferredReturnType.dimension } }; + return { id, dimension: inferredReturnType.dimension }; } -export function createUnaryOpNode(strandsContext, strandsNode, opCode) { +export function statementNode(strandsContext, opCode) { const { dag, cfg } = strandsContext; - const dependsOn = strandsNode.id; const nodeData = DAG.createNodeData({ - nodeType: NodeType.OPERATION, - opCode, - dependsOn, - baseType: dag.baseTypes[strandsNode.id], - dimension: dag.dimensions[strandsNode.id], - }) + nodeType: NodeType.STATEMENT, + opCode + }); const id = DAG.getOrCreateNode(dag, nodeData); CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); - return { id, components: {dep} }; + return id; } -export function createStatementNode(strandsContext, opCode) { +export function swizzleNode(strandsContext, parentNode, swizzle) { const { dag, cfg } = strandsContext; + const baseType = dag.baseTypes[parentNode.id]; const nodeData = DAG.createNodeData({ - nodeType: NodeType.STATEMENT, - opCode + nodeType: NodeType.OPERATION, + baseType, + dimension: swizzle.length, + opCode: OpCode.Unary.SWIZZLE, + dependsOn: [parentNode.id], + swizzle, }); const id = DAG.getOrCreateNode(dag, nodeData); CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); - return id; + return { id, dimension: swizzle.length }; +} + +export function swizzleTrap(id, dimension, strandsContext) { + // const { dimension, baseType } = typeInfo; + const swizzleSets = [ + ['x', 'y', 'z', 'w'], + ['r', 'g', 'b', 'a'], + ['s', 't', 'p', 'q'] + ].map(s => s.slice(0, dimension)); + const trap = { + get(target, property, receiver) { + if (property in target) { + return Reflect.get(...arguments); + } else { + for (const set of swizzleSets) { + if ([...property].every(char => set.includes(char))) { + const swizzle = [...property].map(char => { + const index = set.indexOf(char); + return swizzleSets[0][index]; + }).join(''); + const node = swizzleNode(strandsContext, target, swizzle); + return createStrandsNode(node.id, node.dimension, strandsContext); + } + } + } + }, + set(target, property, value, receiver) { + for (const set of swizzleSets) { + const chars = [...property]; + const valid = + chars.every(c => set.includes(c)) + && new Set(chars).size === chars.length + && target.dimension >= chars.length; + if (valid) { + const originalNode = DAG.getNodeDataFromID(strandsContext.dag, target.id); + const changed = Object.fromEntries( + chars.map((c, i) => [set.indexOf(c), i]) + ) + if (typeof value === 'number') { + value = new Array(chars.length).fill(value); + } + const newValues = mapPrimitiveDepsToIDs( + strandsContext, + {baseType: originalNode.baseType, dimension: null}, + value + ).mappedDependencies; + const newDeps = new Array(originalNode.dimension); + for (let i = 0; i < target.dimension; i++) { + console.log(changed[i]) + if (changed[i] !== undefined) { + newDeps[i] = newValues[changed[i]]; + } + } + console.log("AFTER: ", newDeps) + // primitiveConstructorNode(strandsContext, {baseType, dimension}, newDeps); + return true; + } + } + return Reflect.set(...arguments); + } + }; + return trap; } \ No newline at end of file diff --git a/src/strands/ir_cfg.js b/src/strands/ir_cfg.js index a8dabf66ed..7bdcf09382 100644 --- a/src/strands/ir_cfg.js +++ b/src/strands/ir_cfg.js @@ -70,4 +70,23 @@ export function printBlockData(graph, id) { const block = getBlockDataFromID(graph, id); block.blockType = BlockTypeToName[block.blockType]; console.log(block); +} + +export function sortCFG(adjacencyList, start) { + const visited = new Set(); + const postOrder = []; + + function dfs(v) { + if (visited.has(v)) { + return; + } + visited.add(v); + for (let w of adjacencyList[v].sort((a, b) => b-a) || []) { + dfs(w); + } + 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 8cebf62b90..7633d534c6 100644 --- a/src/strands/ir_dag.js +++ b/src/strands/ir_dag.js @@ -47,7 +47,7 @@ export function createNodeData(data = {}) { value: data.value ?? null, identifier: data.identifier ?? null, statementType: data.statementType ?? null, - swizzle: data.swizzles ?? null, + swizzle: data.swizzle ?? null, dependsOn: Array.isArray(data.dependsOn) ? data.dependsOn : [], usedBy: Array.isArray(data.usedBy) ? data.usedBy : [], phiBlocks: Array.isArray(data.phiBlocks) ? data.phiBlocks : [], @@ -96,7 +96,7 @@ function createNode(graph, node) { graph.baseTypes[id] = node.baseType graph.dimensions[id] = node.dimension; graph.statementTypes[id] = node.statementType; - graph.swizzles[id] = node.swizzle + graph.swizzles[id] = node.swizzle; for (const dep of node.dependsOn) { if (!Array.isArray(graph.usedBy[dep])) { diff --git a/src/strands/ir_types.js b/src/strands/ir_types.js index 724e2d4ea2..1096e20c22 100644 --- a/src/strands/ir_types.js +++ b/src/strands/ir_types.js @@ -74,6 +74,16 @@ export const StructType = { { name: "texCoord", dataType: DataType.float2 }, { name: "color", dataType: DataType.float4 }, ] + }, + StrokeVertex: { + name: 'StrokeVertex', + properties: [ + { name: "position", dataType: DataType.float3 }, + { name: "tangentIn", dataType: DataType.float3 }, + { name: "tangentOut", dataType: DataType.float3 }, + { name: "color", dataType: DataType.float4 }, + { name: "weight", dataType: DataType.float1 }, + ] } } @@ -173,10 +183,18 @@ export const ConstantFolding = { // export const SymbolToOpCode = {}; export const OpCodeToSymbol = {}; +export const UnarySymbolToName = {}; +export const BinarySymbolToName = {}; -for (const { symbol, opCode } of OperatorTable) { +for (const { symbol, opCode, name, arity } of OperatorTable) { // SymbolToOpCode[symbol] = opCode; OpCodeToSymbol[opCode] = symbol; + if (arity === 'unary') { + UnarySymbolToName[symbol] = name; + } + if (arity === 'binary') { + BinarySymbolToName[symbol] = name; + } } export const BlockType = { diff --git a/src/strands/p5.strands.js b/src/strands/p5.strands.js index 1ff28e02f7..3a42ea45a4 100644 --- a/src/strands/p5.strands.js +++ b/src/strands/p5.strands.js @@ -26,6 +26,8 @@ function strands(p5, fn) { ctx.backend = backend; ctx.active = true; ctx.previousFES = p5.disableFriendlyErrors; + ctx.windowOverrides = {}; + ctx.fnOverrides = {}; p5.disableFriendlyErrors = true; } @@ -34,8 +36,14 @@ function strands(p5, fn) { ctx.cfg = createControlFlowGraph(); ctx.uniforms = []; ctx.hooks = []; - p5.disableFriendlyErrors = ctx.previousFES; ctx.active = false; + p5.disableFriendlyErrors = ctx.previousFES; + for (const key in ctx.windowOverrides) { + window[key] = ctx.windowOverrides[key]; + } + for (const key in ctx.fnOverrides) { + fn[key] = ctx.fnOverrides[key]; + } } const strandsContext = {}; @@ -47,17 +55,21 @@ function strands(p5, fn) { ////////////////////////////////////////////// const oldModify = p5.Shader.prototype.modify; - p5.Shader.prototype.modify = function(shaderModifier, options = { parser: true, srcLocations: false }) { + p5.Shader.prototype.modify = function(shaderModifier, scope = {}) { if (shaderModifier instanceof Function) { // Reset the context object every time modify is called; - const backend = glslBackend; + // const backend = glslBackend; initStrandsContext(strandsContext, glslBackend); createShaderHooksFunctions(strandsContext, fn, this); + const options = { parser: true, srcLocations: false }; // 1. Transpile from strands DSL to JS let strandsCallback; if (options.parser) { - strandsCallback = transpileStrandsToJS(shaderModifier.toString(), options.srcLocations); + // #7955 Wrap function declaration code in brackets so anonymous functions are not top level statements, which causes an error in acorn when parsing + // https://github.com/acornjs/acorn/issues/1385 + const sourceString = `(${shaderModifier.toString()})`; + strandsCallback = transpileStrandsToJS(sourceString, options.srcLocations, scope); } else { strandsCallback = shaderModifier; } @@ -72,10 +84,11 @@ function strands(p5, fn) { // ....... const hooksObject = generateShaderCode(strandsContext); console.log(hooksObject); + console.log(hooksObject['vec4 getFinalColor']); console.log(hooksObject['Vertex getWorldInputs']); // Reset the strands runtime context - deinitStrandsContext(strandsContext); + // deinitStrandsContext(strandsContext); // Call modify with the generated hooks object return oldModify.call(this, hooksObject); diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index 85c0f094c7..6bb8ffd418 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -1,14 +1,4 @@ -import { - createBinaryOpNode, - createFunctionCallNode, - createVariableNode, - createStatementNode, - createPrimitiveConstructorNode, - createUnaryOpNode, - createStructInstanceNode, - createStructConstructorNode, - createSwizzleNode, -} from './ir_builders' +import * as build from './ir_builders' import { OperatorTable, BlockType, @@ -29,68 +19,19 @@ import { getNodeDataFromID } from './ir_dag' ////////////////////////////////////////////// // User nodes ////////////////////////////////////////////// -const swizzlesSet = new Set(); - export class StrandsNode { constructor(id, dimension, strandsContext) { this.id = id; this.strandsContext = strandsContext; this.dimension = dimension; - installSwizzlesForDimension.call(this, strandsContext, dimension) } } -function generateSwizzles(chars, maxLen = 4) { - const result = []; - - function build(current) { - if (current.length > 0) result.push(current); - if (current.length === maxLen) return; - - for (let c of chars) { - build(current + c); - } - } - - build(''); - return result; -} - -function installSwizzlesForDimension(strandsContext, dimension) { - if (swizzlesSet.has(dimension)) return; - swizzlesSet.add(dimension); - - const swizzleVariants = [ - ['x', 'y', 'z', 'w'], - ['r', 'g', 'b', 'a'], - ['s', 't', 'p', 'q'] - ].map(chars => chars.slice(0, dimension)); - - const descriptors = {}; - - for (const variant of swizzleVariants) { - const swizzleStrings = generateSwizzles(variant); - for (const swizzle of swizzleStrings) { - if (swizzle.length < 1 || swizzle.length > 4) continue; - if (descriptors[swizzle]) continue; - - const hasDuplicates = new Set(swizzle).size !== swizzle.length; - - descriptors[swizzle] = { - get() { - const id = createSwizzleNode(strandsContext, this, swizzle); - return new StrandsNode(id, 0, strandsContext); - }, - ...(hasDuplicates ? {} : { - set(value) { - return assignSwizzleNode(strandsContext, this, swizzle, value); - } - }) - }; - } - } - - Object.defineProperties(this, descriptors); +export function createStrandsNode(id, dimension, strandsContext) { + return new Proxy( + new StrandsNode(id, dimension, strandsContext), + build.swizzleTrap(id, dimension, strandsContext) + ); } export function initGlobalStrandsAPI(p5, fn, strandsContext) { @@ -99,14 +40,14 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { for (const { name, arity, opCode } of OperatorTable) { if (arity === 'binary') { StrandsNode.prototype[name] = function (...right) { - const { id, components } = createBinaryOpNode(strandsContext, this, right, opCode); - return new StrandsNode(id, components, strandsContext); + const { id, dimension } = build.binaryOpNode(strandsContext, this, right, opCode); + return createStrandsNode(id, dimension, strandsContext); }; } if (arity === 'unary') { - fn[name] = function (strandsNode) { - const { id, components } = createUnaryOpNode(strandsContext, strandsNode, opCode); - return new StrandsNode(id, components, strandsContext); + fn[name] = function (nodeOrValue) { + const { id, dimension } = build.unaryOpNode(strandsContext, nodeOrValue, opCode); + return createStrandsNode(id, dimension, strandsContext); } } } @@ -115,7 +56,12 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { // Unique Functions ////////////////////////////////////////////// fn.discard = function() { - createStatementNode(strandsContext, OpCode.ControlFlow.DISCARD); + build.statementNode(strandsContext, OpCode.ControlFlow.DISCARD); + } + + 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) { @@ -133,8 +79,8 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { if (args.length > 4) { FES.userError("type error", "It looks like you've tried to construct a p5.strands node implicitly, with more than 4 components. This is currently not supported.") } - const { id, components } = createPrimitiveConstructorNode(strandsContext, { baseType: BaseType.DEFER, dimension: null }, args.flat()); - return new StrandsNode(id, components, strandsContext); + const { id, dimension } = build.primitiveConstructorNode(strandsContext, { baseType: BaseType.FLOAT, dimension: null }, args.flat()); + return createStrandsNode(id, dimension, strandsContext);//new StrandsNode(id, dimension, strandsContext); } ////////////////////////////////////////////// @@ -147,8 +93,8 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { const originalFn = fn[functionName]; fn[functionName] = function(...args) { if (strandsContext.active) { - const { id, components } = createFunctionCallNode(strandsContext, functionName, args); - return new StrandsNode(id, components, strandsContext); + const { id, dimension } = build.functionCallNode(strandsContext, functionName, args); + return createStrandsNode(id, dimension, strandsContext); } else { return originalFn.apply(this, args); } @@ -156,8 +102,8 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { } else { fn[functionName] = function (...args) { if (strandsContext.active) { - const { id, components } = createFunctionCallNode(strandsContext, functionName, args); - return new StrandsNode(id, components, strandsContext); + const { id, dimension } = build.functionCallNode(strandsContext, functionName, args); + return createStrandsNode(id, dimension, strandsContext); } else { p5._friendlyError( `It looks like you've called ${functionName} outside of a shader's modify() function.` @@ -187,16 +133,16 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { } fn[`uniform${pascalTypeName}`] = function(name, defaultValue) { - const { id, components } = createVariableNode(strandsContext, typeInfo, name); + const { id, dimension } = build.variableNode(strandsContext, typeInfo, name); strandsContext.uniforms.push({ name, typeInfo, defaultValue }); - return new StrandsNode(id, components, strandsContext); + return createStrandsNode(id, dimension, strandsContext); }; const originalp5Fn = fn[typeInfo.fnName]; fn[typeInfo.fnName] = function(...args) { if (strandsContext.active) { - const { id, components } = createPrimitiveConstructorNode(strandsContext, typeInfo, args); - return new StrandsNode(id, components, strandsContext); + const { id, dimension } = build.primitiveConstructorNode(strandsContext, typeInfo, args); + return createStrandsNode(id, dimension, strandsContext); } else if (originalp5Fn) { return originalp5Fn.apply(this, args); } else { @@ -219,16 +165,15 @@ function createHookArguments(strandsContext, parameters){ const paramType = param.type; if(isStructType(paramType.typeName)) { const structType = StructType[paramType.typeName]; - const originalInstanceInfo = createStructInstanceNode(strandsContext, structType, param.name, []); - const structNode = new StrandsNode(originalInstanceInfo.id, 0, strandsContext); - // const componentNodes = originalInstanceInfo.components.map(id => new StrandsNode(id, components)) + const { id, dimension } = build.structInstanceNode(strandsContext, structType, param.name, []); + const structNode = createStrandsNode(id, dimension, strandsContext);//new StrandsNode(originalInstanceInfo.id, 0, strandsContext); for (let i = 0; i < structType.properties.length; i++) { const componentTypeInfo = structType.properties[i]; Object.defineProperty(structNode, componentTypeInfo.name, { get() { const propNode = getNodeDataFromID(dag, dag.dependsOn[structNode.id][i]) - return new StrandsNode(propNode.id, propNode.dimension, strandsContext); + return createStrandsNode(propNode.id, propNode.dimension, strandsContext); // const { id, components } = createMemberAccessNode(strandsContext, structNode, componentNodes[i], componentTypeInfo.dataType); // const memberAccessNode = new StrandsNode(id, components); // return memberAccessNode; @@ -242,12 +187,12 @@ function createHookArguments(strandsContext, parameters){ newValueID = val.id; } else { - let newVal = createPrimitiveConstructorNode(strandsContext, componentTypeInfo.dataType, val); + let newVal = build.primitiveConstructorNode(strandsContext, componentTypeInfo.dataType, val); newValueID = newVal.id; } newDependsOn[i] = newValueID; - const newStructInfo = createStructInstanceNode(strandsContext, structType, param.name, newDependsOn); + const newStructInfo = build.structInstanceNode(strandsContext, structType, param.name, newDependsOn); structNode.id = newStructInfo.id; } }) @@ -257,8 +202,8 @@ function createHookArguments(strandsContext, parameters){ } else /*if(isNativeType(paramType.typeName))*/ { const typeInfo = TypeInfoFromGLSLName[paramType.typeName]; - const { id, components } = createVariableNode(strandsContext, typeInfo, param.name); - const arg = new StrandsNode(id, components, strandsContext); + const { id, dimension } = build.variableNode(strandsContext, typeInfo, param.name); + const arg = createStrandsNode(id, dimension, strandsContext); args.push(arg); } } @@ -268,7 +213,7 @@ function createHookArguments(strandsContext, parameters){ function enforceReturnTypeMatch(strandsContext, expectedType, returned, hookName) { if (!(returned instanceof StrandsNode)) { // try { - const result = createPrimitiveConstructorNode(strandsContext, expectedType, returned); + const result = build.primitiveConstructorNode(strandsContext, expectedType, returned); return result.id; // } catch (e) { // FES.userError('type error', @@ -289,15 +234,15 @@ function enforceReturnTypeMatch(strandsContext, expectedType, returned, hookName } if (receivedType.dimension !== expectedType.dimension) { if (receivedType.dimension !== 1) { - FES.userError('type error', `You have returned a vector with ${receivedType.dimension} components in ${hookType.name} when a ${expectedType.baseType + expectedType.dimension} was expected!`); + FES.userError('type error', `You have returned a vector with ${receivedType.dimension} components in ${hookName} when a ${expectedType.baseType + expectedType.dimension} was expected!`); } else { - const result = createPrimitiveConstructorNode(strandsContext, expectedType, returned); + const result = build.primitiveConstructorNode(strandsContext, expectedType, returned); returnedNodeID = result.id; } } else if (receivedType.baseType !== expectedType.baseType) { - const result = createPrimitiveConstructorNode(strandsContext, expectedType, returned); + const result = build.primitiveConstructorNode(strandsContext, expectedType, returned); returnedNodeID = result.id; } @@ -315,7 +260,7 @@ export function createShaderHooksFunctions(strandsContext, fn, shader) { const cfg = strandsContext.cfg; for (const hookType of hookTypes) { - fn[hookType.name] = function(hookUserCallback) { + const hookImplementation = function(hookUserCallback) { const entryBlockID = CFG.createBasicBlock(cfg, BlockType.FUNCTION); CFG.addEdge(cfg, cfg.currentBlock, entryBlockID); CFG.pushBlock(cfg, entryBlockID); @@ -352,7 +297,7 @@ export function createShaderHooksFunctions(strandsContext, fn, shader) { const returnedPropID = enforceReturnTypeMatch(strandsContext, expectedTypeInfo, receivedValue, hookType.name); newStructDependencies.push(returnedPropID); } - const newStruct = createStructConstructorNode(strandsContext, expectedStructType, newStructDependencies); + const newStruct = build.structConstructorNode(strandsContext, expectedStructType, newStructDependencies); rootNodeID = newStruct.id; } @@ -369,5 +314,10 @@ export function createShaderHooksFunctions(strandsContext, fn, shader) { }); CFG.popBlock(cfg); } + strandsContext.windowOverrides[hookType.name] = window[hookType.name]; + strandsContext.fnOverrides[hookType.name] = fn[hookType.name]; + + window[hookType.name] = hookImplementation; + fn[hookType.name] = hookImplementation; } } \ No newline at end of file diff --git a/src/strands/strands_codegen.js b/src/strands/strands_codegen.js index 74c99bce0d..3569079289 100644 --- a/src/strands/strands_codegen.js +++ b/src/strands/strands_codegen.js @@ -1,3 +1,5 @@ +import { sortCFG } from "./ir_cfg"; + export function generateShaderCode(strandsContext) { const { cfg, backend } = strandsContext; @@ -10,7 +12,7 @@ export function generateShaderCode(strandsContext) { hooksObj.uniforms[declaration] = defaultValue; } - for (const { hookType, entryBlockID, rootNodeID} of strandsContext.hooks) { + for (const { hookType, rootNodeID, entryBlockID } of strandsContext.hooks) { const generationContext = { indent: 1, codeLines: [], @@ -22,7 +24,8 @@ export function generateShaderCode(strandsContext) { nextTempID: 0, }; - for (const blockID of cfg.blockOrder) { + const blocks = sortCFG(cfg.outgoingEdges, entryBlockID); + for (const blockID of blocks) { backend.generateBlock(blockID, strandsContext, generationContext); } diff --git a/src/strands/strands_glslBackend.js b/src/strands/strands_glslBackend.js index 2cc119d3f3..2ea8ec6a33 100644 --- a/src/strands/strands_glslBackend.js +++ b/src/strands/strands_glslBackend.js @@ -14,20 +14,17 @@ const TypeNames = { 'float2': 'vec2', 'float3': 'vec3', 'float4': 'vec4', - - 'int1': 'int', - 'int2': 'ivec2', - 'int3': 'ivec3', - 'int4': 'ivec4', - - 'bool1': 'bool', - 'bool2': 'bvec2', - 'bool3': 'bvec3', - 'bool4': 'bvec4', - - 'mat2': 'mat2x2', - 'mat3': 'mat3x3', - 'mat4': 'mat4x4', + 'int1': 'int', + 'int2': 'ivec2', + 'int3': 'ivec3', + 'int4': 'ivec4', + 'bool1': 'bool', + 'bool2': 'bvec2', + 'bool3': 'bvec3', + 'bool4': 'bvec4', + 'mat2': 'mat2x2', + 'mat3': 'mat3x3', + 'mat4': 'mat4x4', } const cfgHandlers = { @@ -104,7 +101,7 @@ export const glslBackend = { generateStatement(generationContext, dag, nodeID) { const node = getNodeDataFromID(dag, nodeID); - if (node.statementType = OpCode.ControlFlow.DISCARD) { + if (node.statementType === OpCode.ControlFlow.DISCARD) { generationContext.write('discard;'); } }, @@ -186,13 +183,15 @@ export const glslBackend = { return `${left} ${opSym} ${right}`; } } - if (node.dependsOn.length === 1) { + if (node.opCode === OpCode.Unary.LOGICAL_NOT + || node.opCode === OpCode.Unary.NEGATE + || node.opCode === OpCode.Unary.PLUS + ) { const [i] = node.dependsOn; const val = this.generateExpression(generationContext, dag, i); const sym = OpCodeToSymbol[node.opCode]; return `${sym}${val}`; } - default: FES.internalError(`${NodeTypeToName[node.nodeType]} code generation not implemented yet`) } diff --git a/src/strands/strands_transpiler.js b/src/strands/strands_transpiler.js index b7e8e35f4f..594af2cbac 100644 --- a/src/strands/strands_transpiler.js +++ b/src/strands/strands_transpiler.js @@ -1,6 +1,7 @@ import { parse } from 'acorn'; import { ancestor } from 'acorn-walk'; import escodegen from 'escodegen'; +import { UnarySymbolToName } from './ir_types'; function replaceBinaryOperator(codeSource) { switch (codeSource) { @@ -19,28 +20,34 @@ function replaceBinaryOperator(codeSource) { } } -function ancestorIsUniform(ancestor) { +function nodeIsUniform(ancestor) { return ancestor.type === 'CallExpression' - && ancestor.callee?.type === 'Identifier' - && ancestor.callee?.name.startsWith('uniform'); + && ( + ( + // Global mode + ancestor.callee?.type === 'Identifier' && + ancestor.callee?.name.startsWith('uniform') + ) || ( + // Instance mode + ancestor.callee?.type === 'MemberExpression' && + ancestor.callee?.property.name.startsWith('uniform') + ) + ); } const ASTCallbacks = { - UnaryExpression(node, _state, _ancestors) { - if (_ancestors.some(ancestorIsUniform)) { return; } - - const signNode = { - type: 'Literal', - value: node.operator, - } + UnaryExpression(node, _state, ancestors) { + if (ancestors.some(nodeIsUniform)) { return; } + + const unaryFnName = UnarySymbolToName[node.operator]; const standardReplacement = (node) => { node.type = 'CallExpression' node.callee = { type: 'Identifier', - name: 'unaryNode', + name: unaryFnName, } - node.arguments = [node.argument, signNode] + node.arguments = [node.argument] } if (node.type === 'MemberExpression') { @@ -61,9 +68,9 @@ const ASTCallbacks = { type: 'CallExpression', callee: { type: 'Identifier', - name: 'unaryNode' + name: unaryFnName }, - arguments: [node.argument.object, signNode], + arguments: [node.argument.object], }; node.property = { type: 'Identifier', @@ -78,8 +85,9 @@ const ASTCallbacks = { delete node.argument; delete node.operator; }, - VariableDeclarator(node, _state, _ancestors) { - if (node.init.callee && node.init.callee.name?.startsWith('uniform')) { + VariableDeclarator(node, _state, ancestors) { + if (ancestors.some(nodeIsUniform)) { return; } + if (nodeIsUniform(node.init)) { const uniformNameLiteral = { type: 'Literal', value: node.id.name @@ -95,9 +103,10 @@ const ASTCallbacks = { _state.varyings[node.id.name] = varyingNameLiteral; } }, - Identifier(node, _state, _ancestors) { + Identifier(node, _state, ancestors) { + if (ancestors.some(nodeIsUniform)) { return; } if (_state.varyings[node.name] - && !_ancestors.some(a => a.type === 'AssignmentExpression' && a.left === node)) { + && !ancestors.some(a => a.type === 'AssignmentExpression' && a.left === node)) { node.type = 'ExpressionStatement'; node.expression = { type: 'CallExpression', @@ -118,7 +127,8 @@ const ASTCallbacks = { }, // The callbacks for AssignmentExpression and BinaryExpression handle // operator overloading including +=, *= assignment expressions - ArrayExpression(node, _state, _ancestors) { + ArrayExpression(node, _state, ancestors) { + if (ancestors.some(nodeIsUniform)) { return; } const original = JSON.parse(JSON.stringify(node)); node.type = 'CallExpression'; node.callee = { @@ -127,7 +137,8 @@ const ASTCallbacks = { }; node.arguments = [original]; }, - AssignmentExpression(node, _state, _ancestors) { + AssignmentExpression(node, _state, ancestors) { + if (ancestors.some(nodeIsUniform)) { return; } if (node.operator !== '=') { const methodName = replaceBinaryOperator(node.operator.replace('=','')); const rightReplacementNode = { @@ -164,10 +175,10 @@ const ASTCallbacks = { } } }, - BinaryExpression(node, _state, _ancestors) { + BinaryExpression(node, _state, ancestors) { // Don't convert uniform default values to node methods, as // they should be evaluated at runtime, not compiled. - if (_ancestors.some(ancestorIsUniform)) { return; } + if (ancestors.some(nodeIsUniform)) { return; } // If the left hand side of an expression is one of these types, // we should construct a node from it. const unsafeTypes = ['Literal', 'ArrayExpression', 'Identifier']; @@ -197,22 +208,24 @@ const ASTCallbacks = { }, } - export function transpileStrandsToJS(sourceString, srcLocations) { + export function transpileStrandsToJS(sourceString, srcLocations, scope) { const ast = parse(sourceString, { ecmaVersion: 2021, locations: srcLocations }); ancestor(ast, ASTCallbacks, undefined, { varyings: {} }); const transpiledSource = escodegen.generate(ast); - const strandsCallback = new Function( + const scopeKeys = Object.keys(scope); + const internalStrandsCallback = new Function( + '__p5', + ...scopeKeys, transpiledSource .slice( transpiledSource.indexOf('{') + 1, transpiledSource.lastIndexOf('}') ).replaceAll(';', '') ); - console.log(transpiledSource); - return strandsCallback; + return () => internalStrandsCallback(p5, ...scopeKeys.map(key => scope[key])); } \ No newline at end of file From d2c17af6d9e2650e1515bb7fb02d63cf19df0b67 Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Mon, 15 Sep 2025 17:46:15 +0100 Subject: [PATCH 58/69] fix textures, struct bugs, and add swizzle assign. --- preview/global/sketch.js | 140 ++++++++++++++++++++++++----- src/strands/ir_builders.js | 115 +++++++++++++++--------- src/strands/ir_types.js | 31 ++++++- src/strands/p5.strands.js | 2 +- src/strands/strands_api.js | 43 +++++---- src/strands/strands_builtins.js | 3 +- src/strands/strands_glslBackend.js | 24 +++-- 7 files changed, 262 insertions(+), 96 deletions(-) diff --git a/preview/global/sketch.js b/preview/global/sketch.js index 6c37abe5c9..1d4b5b1270 100644 --- a/preview/global/sketch.js +++ b/preview/global/sketch.js @@ -1,33 +1,129 @@ -p5.disableFriendlyErrors = true; - -function callback() { - const time = uniformFloat(() =>millis()*0.001) - getFinalColor((col) => { - let test = vec3(1) - col.gra = test; - return col; +let instancedShader; +let instancedStrokeShader; +let stars; +let originalImage; +let pixelateShader; +let fresnelShader; +let bloomShader; + +function fresnelShaderCallback() { + const fresnelPower = uniformFloat(2); + const fresnelBias = uniformFloat(-0.1); + const fresnelScale = uniformFloat(2); + + getCameraInputs((inputs) => { + let n = normalize(inputs.normal); + let v = normalize(-inputs.position); + let base = 1.0 - dot(n, v); + let fresnel = fresnelScale * pow(base, fresnelPower) + fresnelBias; + let col = mix([0, 0, 0], [1, .5, .7], fresnel); + inputs.color = [col, 1]; + return inputs; + }); +} + +function starShaderCallback() { + const time = uniformFloat(() => millis()); + const skyRadius = uniformFloat(250); + + function rand2(st) { + return fract(sin(dot(st, [12.9898, 78.233])) * 43758.5453123); + } + + function semiSphere() { + let id = instanceID(); + let theta = rand2([id, 0.1234]) * TWO_PI + time / 100000; + let phi = rand2([id, 3.321]) * PI + time / 50000; + + let r = skyRadius; + r *= sin(phi); + let x = r * sin(phi) * cos(theta); + let y = r * 1.5 * cos(phi); + let z = r * sin(phi) * sin(theta); + return [x, y, z]; + } + + getWorldInputs((inputs) => { + inputs.position += semiSphere(); + return inputs; }); - getWorldInputs(inputs => { - inputs.color = vec4(inputs.position, 1); - // inputs.position = inputs.position + sin(time) * 100; + getObjectInputs((inputs) => { + let size = 1 + 0.5 * sin(time * 0.002 + instanceID()); + inputs.position *= size; return inputs; }); } -async function setup(){ - createCanvas(windowWidth,windowHeight, WEBGL) - bloomShader = baseColorShader().modify(callback); +function pixelateShaderCallback() { + const pixelCountX = uniformFloat(()=> 280); + + getColor((inputs, canvasContent) => { + const aspectRatio = inputs.canvasSize.x / inputs.canvasSize.y; + const pixelSize = [pixelCountX, pixelCountX / aspectRatio]; + + let coord = inputs.texCoord; + coord = floor(coord * pixelSize) / pixelSize; + + let col = getTexture(canvasContent, coord); + return col//[coord, 0, 1]; + }); } -function windowResized() { - resizeCanvas(windowWidth, windowHeight); +function bloomShaderCallback() { + const preBlur = uniformTexture(() => originalImage); + + getColor((input, canvasContent) => { + const blurredCol = getTexture(canvasContent, input.texCoord); + const originalCol = getTexture(preBlur, input.texCoord); + + const intensity = max(originalCol, 0.1) * 12.2; + + const bloom = originalCol + blurredCol * intensity; + return [bloom.rgb, 1]; + }); } -function draw() { - orbitControl(); - background(0); - shader(bloomShader); - noStroke(); - sphere(300) +async function setup(){ + createCanvas(800, 600, WEBGL); + pixelDensity(1); + stars = buildGeometry(() => sphere(8, 4, 2)) + originalImage = createFramebuffer(); + + starShader = baseMaterialShader().modify(starShaderCallback); + starStrokeShader = baseStrokeShader().modify(starShaderCallback) + fresnelShader = baseColorShader().modify(fresnelShaderCallback); + bloomShader = baseFilterShader().modify(bloomShaderCallback); + pixelateShader = baseFilterShader().modify(pixelateShaderCallback); } + +function draw(){ + originalImage.begin(); + background(0); + orbitControl(); + + push() + strokeWeight(2) + stroke(255,0,0) + rotateX(PI/2 + millis() * 0.0005); + fill(255,100, 150) + strokeShader(starStrokeShader) + shader(starShader); + model(stars, 1000); + pop() + + push() + shader(fresnelShader) + noStroke() + sphere(90); + filter(pixelateShader); + pop() + + originalImage.end(); + + imageMode(CENTER) + image(originalImage, 0, 0) + + filter(BLUR, 15) + filter(bloomShader); +} \ No newline at end of file diff --git a/src/strands/ir_builders.js b/src/strands/ir_builders.js index 238dda5d03..420cd44380 100644 --- a/src/strands/ir_builders.js +++ b/src/strands/ir_builders.js @@ -69,7 +69,7 @@ export function binaryOpNode(strandsContext, leftStrandsNode, rightArg, opCode) if (rightArg[0] instanceof StrandsNode && rightArg.length === 1) { rightStrandsNode = rightArg[0]; } else { - const { id, dimension } = primitiveConstructorNode(strandsContext, { baseType: BaseType.DEFER, dimension: null }, rightArg); + const { id, dimension } = primitiveConstructorNode(strandsContext, { baseType: BaseType.FLOAT, dimension: null }, rightArg); rightStrandsNode = createStrandsNode(id, dimension, strandsContext); } let finalLeftNodeID = leftStrandsNode.id; @@ -127,10 +127,13 @@ export function binaryOpNode(strandsContext, leftStrandsNode, rightArg, opCode) } const casted = primitiveConstructorNode(strandsContext, cast.toType, cast.node); + if (cast.node === leftStrandsNode) { - finalLeftNodeID = casted.id; + leftStrandsNode = createStrandsNode(casted.id, casted.dimension, strandsContext); + finalLeftNodeID = leftStrandsNode.id; } else { - finalRightNodeID = casted.id; + rightStrandsNode = createStrandsNode(casted.id, casted.dimension, strandsContext); + finalRightNodeID = rightStrandsNode.id; } } @@ -181,7 +184,7 @@ export function structInstanceNode(strandsContext, structTypeInfo, identifier, d const nodeData = DAG.createNodeData({ nodeType: NodeType.VARIABLE, dimension: structTypeInfo.properties.length, - baseType: structTypeInfo.name, + baseType: structTypeInfo.typeName, identifier, dependsOn }) @@ -192,14 +195,14 @@ export function structInstanceNode(strandsContext, structTypeInfo, identifier, d } function mapPrimitiveDepsToIDs(strandsContext, typeInfo, dependsOn) { - dependsOn = Array.isArray(dependsOn) ? dependsOn : [dependsOn]; + const inputs = Array.isArray(dependsOn) ? dependsOn : [dependsOn]; const mappedDependencies = []; let { dimension, baseType } = typeInfo; const dag = strandsContext.dag; let calculatedDimensions = 0; let originalNodeID = null; - for (const dep of dependsOn.flat(Infinity)) { + for (const dep of inputs.flat(Infinity)) { if (dep instanceof StrandsNode) { const node = DAG.getNodeDataFromID(dag, dep.id); originalNodeID = dep.id; @@ -226,7 +229,6 @@ function mapPrimitiveDepsToIDs(strandsContext, typeInfo, dependsOn) { FES.userError('type error', `You've tried to construct a scalar or vector type with a non-numeric value: ${dep}`); } } - // Sometimes, the dimension is undefined if (dimension === null) { dimension = calculatedDimensions; } else if (dimension > calculatedDimensions && calculatedDimensions === 1) { @@ -278,7 +280,7 @@ export function structConstructorNode(strandsContext, structTypeInfo, rawUserArg if (!(rawUserArgs.length === properties.length)) { FES.userError('type error', - `You've tried to construct a ${structTypeInfo.name} struct with ${rawUserArgs.length} properties, but it expects ${properties.length} properties.\n` + + `You've tried to construct a ${structTypeInfo.typeName} struct with ${rawUserArgs.length} properties, but it expects ${properties.length} properties.\n` + `The properties it expects are:\n` + `${properties.map(prop => prop.name + ' ' + prop.DataType.baseType + prop.DataType.dimension)}` ); @@ -302,7 +304,7 @@ export function structConstructorNode(strandsContext, structTypeInfo, rawUserArg nodeType: NodeType.OPERATION, opCode: OpCode.Nary.CONSTRUCTOR, dimension: properties.length, - baseType: structTypeInfo.name, + baseType: structTypeInfo.typeName , dependsOn }); const id = DAG.getOrCreateNode(dag, nodeData); @@ -439,8 +441,7 @@ export function swizzleNode(strandsContext, parentNode, swizzle) { return { id, dimension: swizzle.length }; } -export function swizzleTrap(id, dimension, strandsContext) { - // const { dimension, baseType } = typeInfo; +export function swizzleTrap(id, dimension, strandsContext, onRebind) { const swizzleSets = [ ['x', 'y', 'z', 'w'], ['r', 'g', 'b', 'a'], @@ -463,40 +464,72 @@ export function swizzleTrap(id, dimension, strandsContext) { } } }, - set(target, property, value, receiver) { - for (const set of swizzleSets) { - const chars = [...property]; - const valid = - chars.every(c => set.includes(c)) - && new Set(chars).size === chars.length - && target.dimension >= chars.length; - if (valid) { - const originalNode = DAG.getNodeDataFromID(strandsContext.dag, target.id); - const changed = Object.fromEntries( - chars.map((c, i) => [set.indexOf(c), i]) - ) - if (typeof value === 'number') { - value = new Array(chars.length).fill(value); - } - const newValues = mapPrimitiveDepsToIDs( - strandsContext, - {baseType: originalNode.baseType, dimension: null}, - value - ).mappedDependencies; - const newDeps = new Array(originalNode.dimension); - for (let i = 0; i < target.dimension; i++) { - console.log(changed[i]) - if (changed[i] !== undefined) { - newDeps[i] = newValues[changed[i]]; - } + set(target, property, value, receiver) { + for (const basis of swizzleSets) { + const chars = [...property]; + const valid = + chars.every(c => basis.includes(c)) && + new Set(chars).size === chars.length && + target.dimension >= chars.length; + + if (!valid) continue; + + const dim = target.dimension; + + const lanes = new Array(dim); + for (let i = 0; i < dim; i++) { + const { id, dimension } = swizzleNode(strandsContext, target, 'xyzw'[i]); + lanes[i] = createStrandsNode(id, dimension, strandsContext); + } + + let scalars = []; + if (value instanceof StrandsNode) { + if (value.dimension === 1) { + scalars = Array(chars.length).fill(value); + } else if (value.dimension >= chars.length) { + for (let k = 0; k < chars.length; k++) { + const { id, dimension } = swizzleNode(strandsContext, value, 'xyzw'[k]); + scalars.push(createStrandsNode(id, dimension, strandsContext)); } - console.log("AFTER: ", newDeps) - // primitiveConstructorNode(strandsContext, {baseType, dimension}, newDeps); - return true; + } else { + FES.userError('type error', `Swizzle assignment: RHS vector too short (need ${chars.length}, got ${value.dimension}).`); + } + } else if (Array.isArray(value)) { + const flat = value.flat(Infinity); + if (flat.length === 1) { + scalars = Array(chars.length).fill(flat[0]); + } else if (flat.length === chars.length) { + scalars = flat; + } else { + FES.userError('type error', `Swizzle assignment: RHS length ${flat.length} does not match ${chars.length}.`); } + } else if (typeof value === 'number') { + scalars = Array(chars.length).fill(value); + } else { + FES.userError('type error', `Unsupported RHS for swizzle assignment: ${value}`); } - return Reflect.set(...arguments); + + for (let j = 0; j < chars.length; j++) { + const canonicalIndex = basis.indexOf(chars[j]); + lanes[canonicalIndex] = scalars[j]; + } + + const orig = DAG.getNodeDataFromID(strandsContext.dag, target.id); + const baseType = orig?.baseType ?? BaseType.FLOAT; + const { id: newID } = primitiveConstructorNode( + strandsContext, + { baseType, dimension: dim }, + lanes + ); + + target.id = newID; + if (typeof onRebind === 'function') { + onRebind(newID); + } + return true; } + return Reflect.set(...arguments); + } }; return trap; } \ No newline at end of file diff --git a/src/strands/ir_types.js b/src/strands/ir_types.js index 1096e20c22..aa86fcd129 100644 --- a/src/strands/ir_types.js +++ b/src/strands/ir_types.js @@ -11,7 +11,6 @@ export const NodeType = { STATEMENT: 'statement', }; - export const NodeTypeToName = Object.fromEntries( Object.entries(NodeType).map(([key, val]) => [val, key]) ); @@ -36,6 +35,7 @@ export const BaseType = { BOOL: "bool", MAT: "mat", DEFER: "defer", + SAMPLER2D: "sampler2D", }; export const BasePriority = { @@ -44,6 +44,7 @@ export const BasePriority = { [BaseType.BOOL]: 1, [BaseType.MAT]: 0, [BaseType.DEFER]: -1, + [BaseType.SAMPLER2D]: -10, }; export const DataType = { @@ -63,7 +64,24 @@ 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 }, } +export const structType = function (hookType) { + // const hookType = hookType?.name ? hookType + let T = hookType.type === undefined ? hookType : hookType.type; + const structType = { + name: hookType.name, + properties: [], + typeName: T.typeName, + }; + for (const prop of T.properties) { + const propType = TypeInfoFromGLSLName[prop.type.typeName]; + structType.properties.push( + {name: prop.name, dataType: propType } + ); + } + return structType; +}; export const StructType = { Vertex: { @@ -84,11 +102,16 @@ export const StructType = { { name: "color", dataType: DataType.float4 }, { name: "weight", dataType: DataType.float1 }, ] - } + }, + FitlerInputs: { + + }, } export function isStructType(typeName) { - return Object.keys(StructType).includes(typeName); + const cap = typeName.charAt(0).toUpperCase() + return cap === typeName.charAt(0); + // return Object.keys(StructType).includes(typeName); } export function isNativeType(typeName) { @@ -108,7 +131,7 @@ export function typeEquals(nodeA, nodeB) { export const TypeInfoFromGLSLName = Object.fromEntries( Object.values(DataType) .filter(info => info.fnName !== null) - .map(info => [info.fnName, info]) + .map(info => [info.fnName === 'texture' ? 'sampler2D' : info.fnName, info]) ); export const OpCode = { diff --git a/src/strands/p5.strands.js b/src/strands/p5.strands.js index 3a42ea45a4..0f1ea06f25 100644 --- a/src/strands/p5.strands.js +++ b/src/strands/p5.strands.js @@ -88,7 +88,7 @@ function strands(p5, fn) { console.log(hooksObject['Vertex getWorldInputs']); // Reset the strands runtime context - // deinitStrandsContext(strandsContext); + deinitStrandsContext(strandsContext); // Call modify with the generated hooks object return oldModify.call(this, hooksObject); diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index 6bb8ffd418..4cead87299 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -4,7 +4,7 @@ import { BlockType, DataType, BaseType, - StructType, + structType, TypeInfoFromGLSLName, isStructType, OpCode, @@ -27,10 +27,10 @@ export class StrandsNode { } } -export function createStrandsNode(id, dimension, strandsContext) { +export function createStrandsNode(id, dimension, strandsContext, onRebind) { return new Proxy( new StrandsNode(id, dimension, strandsContext), - build.swizzleTrap(id, dimension, strandsContext) + build.swizzleTrap(id, dimension, strandsContext, onRebind) ); } @@ -131,13 +131,12 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { pascalTypeName = typeInfo.fnName.charAt(0).toUpperCase() + typeInfo.fnName.slice(1).toLowerCase(); } - fn[`uniform${pascalTypeName}`] = function(name, defaultValue) { const { id, dimension } = build.variableNode(strandsContext, typeInfo, name); strandsContext.uniforms.push({ name, typeInfo, defaultValue }); return createStrandsNode(id, dimension, strandsContext); }; - + const originalp5Fn = fn[typeInfo.fnName]; fn[typeInfo.fnName] = function(...args) { if (strandsContext.active) { @@ -162,21 +161,27 @@ function createHookArguments(strandsContext, parameters){ const dag = strandsContext.dag; for (const param of parameters) { - const paramType = param.type; - if(isStructType(paramType.typeName)) { - const structType = StructType[paramType.typeName]; - const { id, dimension } = build.structInstanceNode(strandsContext, structType, param.name, []); - const structNode = createStrandsNode(id, dimension, strandsContext);//new StrandsNode(originalInstanceInfo.id, 0, strandsContext); - - for (let i = 0; i < structType.properties.length; i++) { - const componentTypeInfo = structType.properties[i]; - Object.defineProperty(structNode, componentTypeInfo.name, { + if(isStructType(param.type.typeName)) { + const structTypeInfo = structType(param); + const { id, dimension } = build.structInstanceNode(strandsContext, structTypeInfo, param.name, []); + const structNode = createStrandsNode(id, dimension, strandsContext); + for (let i = 0; i < structTypeInfo.properties.length; i++) { + const propertyType = structTypeInfo.properties[i]; + Object.defineProperty(structNode, propertyType.name, { get() { const propNode = getNodeDataFromID(dag, dag.dependsOn[structNode.id][i]) - return createStrandsNode(propNode.id, propNode.dimension, strandsContext); + const onRebind = (newFieldID) => { + const oldDeps = dag.dependsOn[structNode.id]; + const newDeps = oldDeps.slice(); + newDeps[i] = newFieldID; + const rebuilt = build.structInstanceNode(strandsContext, structTypeInfo, param.name, newDeps); + structNode.id = rebuilt.id; + }; + // TODO: implement member access operations // const { id, components } = createMemberAccessNode(strandsContext, structNode, componentNodes[i], componentTypeInfo.dataType); // const memberAccessNode = new StrandsNode(id, components); // return memberAccessNode; + return createStrandsNode(propNode.id, propNode.dimension, strandsContext, onRebind); }, set(val) { const oldDependsOn = dag.dependsOn[structNode.id]; @@ -187,12 +192,12 @@ function createHookArguments(strandsContext, parameters){ newValueID = val.id; } else { - let newVal = build.primitiveConstructorNode(strandsContext, componentTypeInfo.dataType, val); + let newVal = build.primitiveConstructorNode(strandsContext, propertyType.dataType, val); newValueID = newVal.id; } newDependsOn[i] = newValueID; - const newStructInfo = build.structInstanceNode(strandsContext, structType, param.name, newDependsOn); + const newStructInfo = build.structInstanceNode(strandsContext, structTypeInfo, param.name, newDependsOn); structNode.id = newStructInfo.id; } }) @@ -201,7 +206,7 @@ function createHookArguments(strandsContext, parameters){ args.push(structNode); } else /*if(isNativeType(paramType.typeName))*/ { - const typeInfo = TypeInfoFromGLSLName[paramType.typeName]; + const typeInfo = TypeInfoFromGLSLName[param.type.typeName]; const { id, dimension } = build.variableNode(strandsContext, typeInfo, param.name); const arg = createStrandsNode(id, dimension, strandsContext); args.push(arg); @@ -272,7 +277,7 @@ export function createShaderHooksFunctions(strandsContext, fn, shader) { let rootNodeID = null; if(isStructType(expectedReturnType.typeName)) { - const expectedStructType = StructType[expectedReturnType.typeName]; + const expectedStructType = structType(expectedReturnType); if (userReturned instanceof StrandsNode) { const returnedNode = getNodeDataFromID(strandsContext.dag, userReturned.id); if (!returnedNode.baseType === expectedStructType.typeName) { diff --git a/src/strands/strands_builtins.js b/src/strands/strands_builtins.js index e931b0b880..e6b985e3bb 100644 --- a/src/strands/strands_builtins.js +++ b/src/strands/strands_builtins.js @@ -105,7 +105,8 @@ const builtInGLSLFunctions = { refract: [{ params: [GenType.FLOAT, GenType.FLOAT,DataType.float1], returnType: GenType.FLOAT, isp5Function: false}], ////////// Texture sampling ////////// - texture: [{params: ["texture2D", DataType.float2], returnType: DataType.float4, isp5Function: true}], + texture: [{params: [DataType.sampler2D, DataType.float2], returnType: DataType.float4, isp5Function: true}], + getTexture: [{params: [DataType.sampler2D, DataType.float2], returnType: DataType.float4, isp5Function: true}] } export const strandsBuiltinFunctions = { diff --git a/src/strands/strands_glslBackend.js b/src/strands/strands_glslBackend.js index 2ea8ec6a33..c009f807a6 100644 --- a/src/strands/strands_glslBackend.js +++ b/src/strands/strands_glslBackend.js @@ -1,10 +1,11 @@ -import { NodeType, OpCodeToSymbol, BlockType, OpCode, NodeTypeToName, isStructType, StructType } from "./ir_types"; +import { NodeType, OpCodeToSymbol, BlockType, OpCode, NodeTypeToName, isStructType, StructType, BaseType } 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; + if (dag.baseTypes[nodeID] === BaseType.SAMPLER2D) return false; const uses = dag.usedBy[nodeID] || []; return uses.length > 1; } @@ -39,7 +40,6 @@ const cfgHandlers = { generationContext.write(declaration); } if (nodeType === NodeType.STATEMENT) { - console.log("HELLO") glslBackend.generateStatement(generationContext, dag, nodeID); } } @@ -109,7 +109,6 @@ export const glslBackend = { generateDeclaration(generationContext, dag, nodeID) { const expr = this.generateExpression(generationContext, dag, nodeID); const tmp = `T${generationContext.nextTempID++}`; - console.log(expr); generationContext.tempNames[nodeID] = tmp; const T = extractNodeTypeInfo(dag, nodeID); @@ -128,7 +127,7 @@ export const glslBackend = { if (prop.name !== val) { generationContext.write( `${rootNode.identifier}.${prop.name} = ${val};` - ) + ) } } } @@ -142,21 +141,30 @@ export const glslBackend = { } switch (node.nodeType) { case NodeType.LITERAL: - return node.value.toFixed(4); - + if (node.baseType === BaseType.FLOAT) { + return node.value.toFixed(4); + } + 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) { - if (node.dependsOn.length === 1 && node.dimension === 1) { + // TODO: differentiate casts and constructors for more efficient codegen. + // if (node.dependsOn.length === 1 && node.dimension === 1) { + // return this.generateExpression(generationContext, dag, node.dependsOn[0]); + // } + if (node.baseType === BaseType.SAMPLER2D) { return this.generateExpression(generationContext, dag, node.dependsOn[0]); } const T = this.getTypeName(node.baseType, node.dimension); const deps = node.dependsOn.map((dep) => this.generateExpression(generationContext, dag, dep)); return `${T}(${deps.join(', ')})`; - } + } if (node.opCode === OpCode.Nary.FUNCTION_CALL) { const functionArgs = node.dependsOn.map(arg =>this.generateExpression(generationContext, dag, arg)); return `${node.identifier}(${functionArgs.join(', ')})`; From d5c7fe815fcb2dad6f761c72041d62bb752ae43e Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Mon, 15 Sep 2025 17:48:28 +0100 Subject: [PATCH 59/69] remove old shadergenerator file --- src/webgl/ShaderGenerator.js.temp | 1691 ----------------------------- 1 file changed, 1691 deletions(-) delete mode 100644 src/webgl/ShaderGenerator.js.temp diff --git a/src/webgl/ShaderGenerator.js.temp b/src/webgl/ShaderGenerator.js.temp deleted file mode 100644 index 998e19cee2..0000000000 --- a/src/webgl/ShaderGenerator.js.temp +++ /dev/null @@ -1,1691 +0,0 @@ -/** -* @module 3D -* @submodule ShaderGenerator -* @for p5 -* @requires core -*/ -import { parse } from 'acorn'; -import { ancestor } from 'acorn-walk'; -import escodegen from 'escodegen'; -import noiseGLSL from './shaders/functions/noiseGLSL.glsl'; - -function shadergenerator(p5, fn) { - - let GLOBAL_SHADER; - let BRANCH; - - const oldModify = p5.Shader.prototype.modify - - p5.Shader.prototype.modify = function(shaderModifier, scope = {}) { - if (shaderModifier instanceof Function) { - // TODO make this public. Currently for debugging only. - const options = { parser: true, srcLocations: false }; - let generatorFunction; - if (options.parser) { - // #7955 Wrap function declaration code in brackets so anonymous functions are not top level statements, which causes an error in acorn when parsing - // https://github.com/acornjs/acorn/issues/1385 - const sourceString = `(${shaderModifier.toString()})`; - const ast = parse(sourceString, { - ecmaVersion: 2021, - locations: options.srcLocations - }); - ancestor(ast, ASTCallbacks, undefined, { varyings: {} }); - const transpiledSource = escodegen.generate(ast); - const scopeKeys = Object.keys(scope); - const internalGeneratorFunction = new Function( - 'p5', - ...scopeKeys, - transpiledSource - .slice( - transpiledSource.indexOf('{') + 1, - transpiledSource.lastIndexOf('}') - ).replaceAll(';', '') - ); - generatorFunction = () => internalGeneratorFunction(p5, ...scopeKeys.map(key => scope[key])); - } else { - generatorFunction = shaderModifier; - } - const generator = new ShaderGenerator(generatorFunction, this, options.srcLocations); - const generatedModifyArgument = generator.generate(); - return oldModify.call(this, generatedModifyArgument); - } - else { - return oldModify.call(this, shaderModifier) - } - } - - // AST Transpiler Callbacks and helper functions - function replaceBinaryOperator(codeSource) { - switch (codeSource) { - case '+': return 'add'; - case '-': return 'sub'; - case '*': return 'mult'; - case '/': return 'div'; - case '%': return 'mod'; - case '==': - case '===': return 'equalTo'; - case '>': return 'greaterThan'; - case '>=': return 'greaterThanEqualTo'; - case '<': return 'lessThan'; - case '&&': return 'and'; - case '||': return 'or'; - } - } - - function nodeIsUniform(ancestor) { - return ancestor.type === 'CallExpression' - && ( - ( - // Global mode - ancestor.callee?.type === 'Identifier' && - ancestor.callee?.name.startsWith('uniform') - ) || ( - // Instance mode - ancestor.callee?.type === 'MemberExpression' && - ancestor.callee?.property.name.startsWith('uniform') - ) - ); - } - - const ASTCallbacks = { - UnaryExpression(node, _state, ancestors) { - if (ancestors.some(nodeIsUniform)) { return; } - - const signNode = { - type: 'Literal', - value: node.operator, - } - - const standardReplacement = (node) => { - node.type = 'CallExpression' - node.callee = { - type: 'Identifier', - name: 'p5.unaryNode', - } - node.arguments = [node.argument, signNode] - } - - if (node.type === 'MemberExpression') { - const property = node.argument.property.name; - const swizzleSets = [ - ['x', 'y', 'z', 'w'], - ['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 = { - type: 'CallExpression', - callee: { - type: 'Identifier', - name: 'p5.unaryNode' - }, - arguments: [node.argument.object, signNode], - }; - node.property = { - type: 'Identifier', - name: property - }; - } else { - standardReplacement(node); - } - } else { - standardReplacement(node); - } - delete node.argument; - delete node.operator; - }, - VariableDeclarator(node, _state, ancestors) { - if (ancestors.some(nodeIsUniform)) { return; } - if (nodeIsUniform(node.init)) { - const uniformNameLiteral = { - type: 'Literal', - value: node.id.name - } - node.init.arguments.unshift(uniformNameLiteral); - } - if (node.init.callee && node.init.callee.name?.startsWith('varying')) { - const varyingNameLiteral = { - type: 'Literal', - value: node.id.name - } - node.init.arguments.unshift(varyingNameLiteral); - _state.varyings[node.id.name] = varyingNameLiteral; - } - }, - Identifier(node, _state, ancestors) { - if (ancestors.some(nodeIsUniform)) { return; } - if (_state.varyings[node.name] - && !_ancestors.some(a => a.type === 'AssignmentExpression' && a.left === node)) { - node.type = 'ExpressionStatement'; - node.expression = { - type: 'CallExpression', - callee: { - type: 'MemberExpression', - object: { - type: 'Identifier', - name: node.name - }, - property: { - type: 'Identifier', - name: 'getValue' - }, - }, - arguments: [], - } - } - }, - // The callbacks for AssignmentExpression and BinaryExpression handle - // operator overloading including +=, *= assignment expressions - ArrayExpression(node, _state, ancestors) { - if (ancestors.some(nodeIsUniform)) { return; } - const original = JSON.parse(JSON.stringify(node)); - node.type = 'CallExpression'; - node.callee = { - type: 'Identifier', - name: 'p5.dynamicNode', - }; - node.arguments = [original]; - }, - AssignmentExpression(node, _state, ancestors) { - if (ancestors.some(nodeIsUniform)) { return; } - if (node.operator !== '=') { - const methodName = replaceBinaryOperator(node.operator.replace('=','')); - const rightReplacementNode = { - type: 'CallExpression', - callee: { - type: 'MemberExpression', - object: node.left, - property: { - type: 'Identifier', - name: methodName, - }, - }, - arguments: [node.right] - } - node.operator = '='; - node.right = rightReplacementNode; - } - if (_state.varyings[node.left.name]) { - node.type = 'ExpressionStatement'; - node.expression = { - type: 'CallExpression', - callee: { - type: 'MemberExpression', - object: { - type: 'Identifier', - name: node.left.name - }, - property: { - type: 'Identifier', - name: 'bridge', - } - }, - arguments: [node.right], - } - } - }, - BinaryExpression(node, _state, ancestors) { - // Don't convert uniform default values to node methods, as - // they should be evaluated at runtime, not compiled. - if (ancestors.some(nodeIsUniform)) { return; } - // If the left hand side of an expression is one of these types, - // we should construct a node from it. - const unsafeTypes = ['Literal', 'ArrayExpression', 'Identifier']; - if (unsafeTypes.includes(node.left.type)) { - const leftReplacementNode = { - type: 'CallExpression', - callee: { - type: 'Identifier', - name: 'p5.dynamicNode', - }, - arguments: [node.left] - } - node.left = leftReplacementNode; - } - // Replace the binary operator with a call expression - // in other words a call to BaseNode.mult(), .div() etc. - node.type = 'CallExpression'; - node.callee = { - type: 'MemberExpression', - object: node.left, - property: { - type: 'Identifier', - name: replaceBinaryOperator(node.operator), - }, - }; - node.arguments = [node.right]; - }, - } - - // Javascript Node API. - class BaseNode { - constructor(isInternal, type) { - if (new.target === BaseNode) { - throw new TypeError('Cannot construct BaseNode instances directly. This is an abstract class.'); - } - this.type = type; - this.componentNames = []; - this.componentsChanged = false; - // For tracking recursion depth and creating temporary variables - this.isInternal = isInternal; - this.usedIn = []; - this.dependsOn = []; - this.srcLine = null; - this.usedInConditional = false; - // Stack Capture is used to get the original line of user code for Debug purposes - if (GLOBAL_SHADER.srcLocations === true && isInternal === false) { - try { - throw new Error('StackCapture'); - } catch (e) { - const lines = e.stack.split('\n'); - let userSketchLineIndex = 5; - if (isBinaryExpressionNode(this)) { userSketchLineIndex--; }; - this.srcLine = lines[userSketchLineIndex].trim(); - } - } - } - - addVectorComponents() { - if (this.type.startsWith('vec')) { - const vectorDimensions = parseInt(this.type.slice(3)); - this.componentNames = ['x', 'y', 'z', 'w'].slice(0, vectorDimensions); - const proxy = this; - for (let componentName of this.componentNames) { - let value = new ComponentNode(proxy, componentName, 'float', true); - Object.defineProperty(this, componentName, { - get() { - return value; - }, - set(newValue) { - this.componentsChanged = true; - if (isUnaryExpressionNode(this)) { - this.node.value = newValue; - } else { - value = newValue; - } - } - }) - } - } - } - - forceTemporaryVariable() { - if (!(isFloatNode(this) && isVectorNode(this.parent)) || !isVariableNode(this)) - this.useTemp = true; - } - - assertUsedInConditional(branch) { - this.usedInConditional = true; - this.usedIn.push(branch); - this.forceTemporaryVariable(); - } - - isUsedInConditional() { - return this.usedInConditional; - } - - checkConditionalDependencies(context) { - context.ifs.forEach((statement) => { - const isUsedSatisfied = () => statement.usedInSatisfied.length >= 1; - const isDepsSatisfied = () => statement.dependsOn.length === statement.dependsOnSatisfied.length; - if (statement.insertionPoint > -1 || !statement.usedIn.length) return; - if (statement.dependsOn.some(d => d.node === this) && !statement.dependsOnSatisfied.includes(this)) { - statement.dependsOnSatisfied.push(this); - } - if (statement.usedIn.includes(this) && !statement.usedInSatisfied.includes(this)) { - statement.usedInSatisfied.push(this); - } - if (isDepsSatisfied() && isUsedSatisfied()) { - statement.saveState(context, isDepsSatisfied(), isUsedSatisfied()); - } - }); - } - - // The base node implements a version of toGLSL which determines whether the generated code should be stored in a temporary variable. - toGLSLBase(context){ - let result; - if (this.shouldUseTemporaryVariable()) { - let oldLength = context.declarations.length; - result = this.getTemporaryVariable(context); - let diff = context.declarations.length - 1 - oldLength; - diff = diff > 0 ? diff : undefined; - this.dependsOn.forEach(dependency => { - if (dependency.isVector) { - const dependencies = dependency.originalComponents.map((component, i) => - component === dependency.currentComponents[i] - ); - context.updateComponents(dependency.node, diff, dependencies); - } else { - context.updateComponents(dependency.node, diff); - } - }); - } else { - result = this.toGLSL(context); - } - this.checkConditionalDependencies(context) - return result; - } - - shouldUseTemporaryVariable() { - if (this.componentsChanged || hasTemporaryVariable(this) || this.useTemp) { return true; } - if (this.isInternal || isVariableNode(this) || isConditionalNode(this) || this.type === 'sampler2D') { return false; } - - // return false; - // Swizzles must use temporary variables as otherwise they will not be registered - let score = 0; - score += isFunctionCallNode(this) * 2; - score += isBinaryExpressionNode(this) * 2; - score += isVectorType(this) * 3; - score += this.usedIn.length; - return score >= 4; - } - - getTemporaryVariable(context) { - if (!this.temporaryVariable) { - this.temporaryVariable = `temp_${context.getNextID()}`; - let line = ''; - if (this.srcLine) { - line += `\n// From ${this.srcLine}\n`; - } - line += ' ' + this.type + ' ' + this.temporaryVariable + ' = ' + this.toGLSL(context) + ';'; - context.declarations.push(line); - } - return this.temporaryVariable; - }; - - // Binary Operators - add(other) { return binaryExpressionNodeConstructor(this, this.enforceType(other), '+'); } - sub(other) { return binaryExpressionNodeConstructor(this, this.enforceType(other), '-'); } - mult(other) { return binaryExpressionNodeConstructor(this, this.enforceType(other), '*'); } - div(other) { return binaryExpressionNodeConstructor(this, this.enforceType(other), '/'); } - mod(other) { return binaryExpressionNodeConstructor(this, this.enforceType(other), '%'); } - - // Check that the types of the operands are compatible. - enforceType(other){ - if (isShaderNode(other)){ - if (!isGLSLNativeType(other.type)) { - throw new TypeError (`You've tried to perform an operation on a struct of type: ${other.type}. Try accessing a member on that struct with '.'`) - } - if (!isGLSLNativeType(other.type)) { - throw new TypeError (`You've tried to perform an operation on a struct of type: ${other.type}. Try accessing a member on that struct with '.'`) - } - if ((isFloatType(this) || isVectorType(this)) && isIntType(other)) { - return new FloatNode(other) - } - return other; - } - else if (typeof other === 'number') { - if (isIntType(this)) { - return new IntNode(other); - } - return new FloatNode(other); - } - else if (Array.isArray(other)) { - return nodeConstructors.dynamicVector(other); - // return nodeConstructors[`vec${other.length}`](other); - } - else { - return nodeConstructors[this.type](other); - } - } - - toFloat() { - if (isFloatType(this)) { - return this; - } else if (isIntType(this)) { - return new FloatNode(this); - } - } - - toGLSL(context){ - throw new TypeError('Not supposed to call this function on BaseNode, which is an abstract class.'); - } - } - - // Primitive Types - class IntNode extends BaseNode { - constructor(x = 0, isInternal = false) { - super(isInternal, 'int'); - this.x = x; - } - - toGLSL(context) { - if (isShaderNode(this.x)) { - let code = this.x.toGLSLBase(context); - return isIntType(this.x.type) ? code : `int(${code})`; - } - else if (typeof this.x === 'number') { - return `${Math.floor(this.x)}`; - } - else { - return `int(${this.x})`; - } - } - } - - class FloatNode extends BaseNode { - constructor(x = 0, isInternal = false, _parent = false){ - super(isInternal, 'float'); - if (Array.isArray(x)) { - x = x[0]; - } - if (_parent) { - const { parent, name } = _parent - this.name = name; - this.parent = parent; - } - this.x = x; - } - - toGLSL(context) { - if (isShaderNode(this.x)) { - let code = this.x.toGLSLBase(context); - return isFloatType(this.x) ? code : `float(${code})`; - } - else if (typeof this.x === 'number') { - return `${this.x.toFixed(4)}`; - } - else { - return `float(${this.x})`; - } - } - } - - class VectorNode extends BaseNode { - constructor(values, type, isInternal = false) { - super(isInternal, type); - this.originalValues = conformVectorParameters(values, parseInt(type.slice(3))); - this.componentNames = ['x', 'y', 'z', 'w'].slice(0, this.originalValues.length); - } - - addVectorComponents() { - const values = this.originalValues; - this.componentsChanged = false; - - this.componentNames.forEach((componentName, i) => { - const info = { name: componentName, parent: this }; - let value = isFloatNode(values[i]) ? values[i] : new FloatNode(values[i], true, info); - Object.defineProperty(this, componentName, { - get() { - return value; - }, - set(newValue) { - this.componentsChanged = true; - if (isUnaryExpressionNode(this)) { - this.node.value = newValue; - } else { - value = isFloatNode(newValue) ? newValue : new FloatNode(newValue, true, info); - } - } - }) - }); - this.originalValues = this.componentNames.map(name => this[name]); - } - - toGLSL(context) { - if ((!this.componentsChanged || !this.defined) && !this.oldName) { - let glslArgs = this.componentNames.map((_name, i) => this.originalValues[i].toGLSLBase(context)).join(', '); - this.defined = true; - return `${this.type}(${glslArgs})`; - } else { - return this.temporaryVariable; - } - } - } - - // Function Call Nodes - class FunctionCallNode extends BaseNode { - constructor(name, userArgs, properties, isInternal = false) { - let functionSignature; - const determineFunctionSignature = (props) => { - let genType; - let similarity = 0; - - const valid = userArgs.every((userArg, i) => { - const userType = getType(userArg); - let expectedArgType = props.args[i]; - - if (expectedArgType === 'genType') { - // We allow conversions from float -> vec if one argument is a vector. - if (genType === undefined || (genType === 'float' && userType.startsWith('vec'))) { - genType = userType - }; - expectedArgType = genType; - } - similarity += (userType === expectedArgType); - return userType === expectedArgType || (userType === 'float' && expectedArgType.startsWith('vec')); - }) - - return { ...props, valid, similarity, genType } - } - - if (Array.isArray(properties)) { - // Check if the right number of parameters were provided - let possibleOverloads = properties.filter(o => o.args.length === userArgs.length); - if (possibleOverloads.length === 0) { - const argsLengthSet = new Set(); - const argsLengthArr = []; - properties.forEach((p) => argsLengthSet.add(p.args.length)); - argsLengthSet.forEach((len) => argsLengthArr.push(`${len}`)); - const argsLengthStr = argsLengthArr.join(' or '); - throw new Error(`Function '${name}' has ${properties.length} variants which expect ${argsLengthStr} arguments, but ${userArgs.length} arguments were provided.`); - } - const findBestOverload = function (best, current) { - current = determineFunctionSignature(current); - if (!current.valid) { return best; } - if (!best || current.similarity > best.similarity) { - best = current; - } - return best; - } - functionSignature = possibleOverloads.reduce(findBestOverload, null); - } else { - functionSignature = determineFunctionSignature(properties); - } - - if (!functionSignature || !functionSignature.valid) { - const argsStrJoin = (args) => `(${args.map((arg) => arg).join(', ')})`; - const expectedArgsString = Array.isArray(properties) ? - properties.map(prop => argsStrJoin(prop.args)).join(' or ') - : argsStrJoin(properties.args); - const providedArgsString = argsStrJoin(userArgs.map((a)=>getType(a))); - throw new Error(`Function '${name}' was called with wrong arguments. Most likely, you provided mixed lengths vectors as arguments.\nExpected argument types: ${expectedArgsString}\nProvided argument types: ${providedArgsString}\nAll of the arguments with expected type 'genType' should have a matching type. If one of those is different, try to find where it was created. - `); - } - - if (userArgs.length !== functionSignature.args.length) { - throw new Error(`Function '${name}' expects ${functionSignature.args.length} arguments, but ${userArgs.length} were provided.`); - } - - userArgs = userArgs.map((arg, i) => { - if (!isShaderNode(arg)) { - const typeName = functionSignature.args[i] === 'genType' ? functionSignature.genType : functionSignature.args[i]; - arg = nodeConstructors[typeName](arg); - } else if (isFloatType(arg) && functionSignature.args[i] === 'genType' && functionSignature.genType !== 'float') { - arg = nodeConstructors[functionSignature.genType](arg); - } - return arg; - }) - - if (functionSignature.returnType === 'genType') { - functionSignature.returnType = functionSignature.genType; - } - - super(isInternal, functionSignature.returnType); - - this.name = name; - this.args = userArgs; - this.argumentTypes = functionSignature.args; - } - - deconstructArgs(context) { - let argsString = this.args.map((argNode, i) => { - if (isIntType(argNode) && this.argumentTypes[i] != 'float') { - argNode = argNode.toFloat(); - } - argNode.toGLSLBase(context); - return argNode.toGLSLBase(context); - }).join(', '); - return argsString; - } - - toGLSL(context) { - return `${this.name}(${this.deconstructArgs(context)})`; - } - } - - // Variables and member variable nodes - class VariableNode extends BaseNode { - constructor(name, type, isInternal = false) { - super(isInternal, type); - this.name = name; - } - - toGLSL(context) { - return `${this.name}`; - } - } - - class ComponentNode extends BaseNode { - constructor(parent, componentName, type, isInternal = false) { - super(isInternal, type); - this.parent = parent; - this.componentName = componentName; - this.type = type; - } - toGLSL(context) { - let parentName = this.parent.toGLSLBase(context); - if (!isVariableNode(this.parent) && !hasTemporaryVariable(this.parent)) { - parentName = `(${parentName})`; - } - return `${parentName}.${this.componentName}`; - } - } - - // - class VaryingNode extends VariableNode { - constructor(name, type, isInternal = false) { - super(name, type, isInternal); - this.timesChanged = 0; - this.tempVars = 0; - } - - getValue() { - const context = GLOBAL_SHADER.context; - if (!context.varyings[this.name] || !this.timesChanged) { - return this; - } - - let values = context.varyings[this.name].splice(0, this.timesChanged); - let snapshot; - values.forEach((val, i) => { - let { value } = val; - context.declarations.push(` ${this.name} = ${value.toGLSLBase(context)};`); - if (i === values.length - 1) { - const tempName = `${this.name}_${this.tempVars++}` - snapshot = dynamicAddSwizzleTrap(new VariableNode(tempName, this.type, true)); - context.declarations.push(` ${this.type} ${tempName} = ${this.name};`); - } - }); - - this.timesChanged = 0; - return snapshot; - } - - bridge(value) { - if (!isShaderNode(value) || this.type.startsWith('vec') && getType(value) === 'float') { - value = nodeConstructors[this.type](value) - } - GLOBAL_SHADER.registerVarying(this, value); - this.timesChanged += 1 - } - } - - // Binary Operator Nodes - class BinaryExpressionNode extends BaseNode { - constructor(left, right, operator, isInternal = false) { - super(isInternal, null); - this.operator = operator; - this.left = left; - this.right = right; - for (const operand of [left, right]) { - operand.usedIn.push(this); - } - this.type = this.determineType(); - } - - // We know that both this.left and this.right are nodes because of BaseNode.enforceType - determineType() { - if (['==', '>', '>=', '<', '<=', '||', '!', '&&'].includes(this.operator)) { - return 'bool'; - } - else if (this.left.type === this.right.type) { - return this.left.type; - } - else if (isVectorType(this.left) && isFloatType(this.right)) { - return this.left.type; - } - else if (isVectorType(this.right) && isFloatType(this.left)) { - return this.right.type; - } - else if (isFloatType(this.left) && isIntType(this.right) - || isIntType(this.left) && isFloatType(this.right) - ) { - return 'float'; - } - else { - throw new Error('Incompatible types for binary operator'); - } - } - - processOperand(operand, context) { - if (operand.temporaryVariable) { return operand.temporaryVariable; } - let code = operand.toGLSLBase(context); - if (isBinaryExpressionNode(operand) && !operand.temporaryVariable) { - code = `(${code})`; - } - if (this.type === 'float' && isIntType(operand)) { - code = `float(${code})`; - } - return code; - } - - toGLSL(context) { - const a = this.processOperand(this.left, context); - const b = this.processOperand(this.right, context); - return `${a} ${this.operator} ${b}`; - } - } - - class ModulusNode extends BinaryExpressionNode { - constructor(a, b, isInternal) { - super(a, b, isInternal); - } - toGLSL(context) { - // Switch on type between % or mod() - if (isVectorType(this) || isFloatType(this)) { - return `mod(${this.left.toGLSLBase(context)}, ${this.right.toGLSLBase(context)})`; - } - return `${this.processOperand(context, this.left)} % ${this.processOperand(context, this.right)}`; - } - } - - class UnaryExpressionNode extends BaseNode { - constructor(node, operator, isInternal = false) { - super(isInternal, node.type) - this.node = node; - this.operator = operator; - } - - toGLSL(context) { - let mainStr = this.node.toGLSLBase(context); - if (!isVariableNode(this.node) && !hasTemporaryVariable(this.node) && !isPrimitiveNode(this.node)) { - mainStr = `(${mainStr})` - } - return `${this.operator}${mainStr}` - } - } - - // Conditions and logical modifiers - BaseNode.prototype.equalTo = function(other) { - return binaryExpressionNodeConstructor(this, this.enforceType(other), '=='); - } - - BaseNode.prototype.greaterThan = function(other) { - return binaryExpressionNodeConstructor(this, this.enforceType(other), '>'); - } - - BaseNode.prototype.greaterThanEqualTo = function(other) { - return binaryExpressionNodeConstructor(this, this.enforceType(other), '>='); - } - - BaseNode.prototype.lessThan = function(other) { - return binaryExpressionNodeConstructor(this, this.enforceType(other), '<'); - } - - BaseNode.prototype.lessThanEqualTo = function(other) { - return binaryExpressionNodeConstructor(this, this.enforceType(other), '<='); } - - BaseNode.prototype.not = function() { - return new UnaryExpressionNode(this.condition, '!', true); - } - - BaseNode.prototype.or = function(other) { - return new binaryExpressionNodeConstructor(this, this.enforceType(other), '||', true); - } - - BaseNode.prototype.and = function(other) { - return new binaryExpressionNodeConstructor(this, this.enforceType(other), '&&', true); - } - - function branch(callback) { - const branch = new BranchNode(); - callback(); - BRANCH = null; - return branch; - } - - class ConditionalNode { - constructor(condition, branchCallback) { - this.dependsOn = []; - this.usedIn = []; - this.dependsOnSatisfied = []; - this.usedInSatisfied = []; - this.states = []; - this.if(condition, branchCallback); - this.insertionPoint = -1; - this.elseIfs = []; - this.elseBranch = null; - GLOBAL_SHADER.context.ifs.push(this); - } - - if(condition, branchCallback) { - this.condition = condition; - this.conditionString = condition.toGLSL(GLOBAL_SHADER.context); - this.ifBranch = branch(branchCallback); - this.ifBranch.parent = this; - } - - elseIf(condition, branchCallback) { - let elseBranch = branch(branchCallback); - branchCallback.parent = this; - this.elseIfs.push({ condition, elseBranch }); - return this; - } - - else(branchCallback) { - this.elseBranch = branch(branchCallback); - this.elseBranch.parent = this - return this; - } - - thenDiscard() { - return new ConditionalDiscard(this.condition); - }; - - saveState(context, usedInSatisfied, dependsOnSatisfied) { - this.states.push({ - line: context.declarations.length, - usedInSatisfied, - dependsOnSatisfied - }); - this.insertionPoint = context.declarations.length - 1; - } - - toGLSL(context) { - const oldLength = context.declarations.length; - this.dependsOn.forEach(dep => context.updateComponents(dep.node)); - const newLength = context.declarations.length; - const diff = newLength - oldLength; - this.insertionPoint += diff; - - let codelines = [ - `\n if (${this.conditionString}) {`, - `\n ${this.ifBranch.toGLSL(context)}`, - `\n }` - ]; - - if (this.elseIfs.length) { - this.elseIfs.forEach((elif) => { - let { condition, elseBranch } = elif; - codelines.push(` else if (${condition.toGLSL(context)}) {`); - codelines.push(`\n ${elseBranch.toGLSL(context)}`); - codelines.push(`\n }`); - }) - } - - if (this.elseBranch) { - codelines.push(` else {`); - codelines.push(`\n ${this.elseBranch.toGLSL(context)}`); - codelines.push(`\n }\n`); - } - codelines.push('\n'); - return codelines.flat().join(''); - } - }; - - fn.assign = function(node, value) { - if (!BRANCH) { - throw new error('assign() is supposed to be used inside of conditional branchs. Use the "=" operator as normal otherwise.'); - } - BRANCH.assign(node, value); - } - - class BranchNode { - constructor() { - BRANCH = this; - this.statements = []; - this.assignments = []; - this.dependsOn = []; - this.declarations = []; - let parent = null; - Object.defineProperty(this, 'parent', { - get() { - return parent; - }, - set(newParent) { - newParent.dependsOn.push(...this.dependsOn) - parent = newParent; - } - }) - } - - assign(node, value) { - if (!isShaderNode(value) || value.type !== node.type) { - value = nodeConstructors[node.type](value); - this.declarations.push(value); - this.assignments.push({ node }); - } else { - this.assignments.push({ node, value }); - } - node = node.parent ? node.parent : node; - value = value.parent ? value.parent : value; - if ([node, value].some(n => this.dependsOn.some(d=>d.node===n))) { - return; - } - node.assertUsedInConditional(this); - this.dependsOn.push(makeDependencyObject(node)) - if (value.shouldUseTemporaryVariable()) { - value.assertUsedInConditional(this); - this.dependsOn.push(makeDependencyObject(value)); - } - } - - toGLSL(context) { - let declarationsIndex = 0; - this.assignments.forEach(({ node, value }) => { - let statement; - let result; - - if (!value) { - let decl = this.declarations[declarationsIndex]; - declarationsIndex++; - decl.temporaryVariable = `temp_${context.getNextID()}`; - this.statements.push( - `${decl.type} ${decl.temporaryVariable} = ${decl.toGLSL(context)};` - ); - result = decl.toGLSLBase(context); - } else { - result = value.toGLSLBase(context); - } - - if (isVariableNode(node) || hasTemporaryVariable(node)) { - statement = `${node.toGLSLBase(context)} = ${result};`; - } - else if (isFloatNode(node) && node.name) { - statement = `${node.parent.toGLSLBase(context)}.${node.name} = ${result};`; - } - else { - node.temporaryVariable = `temp_${context.getNextID()}`; - statement = `${node.type} ${node.toGLSLBase(context)} = ${result};` - } - - this.statements.push(statement); - }) - - return this.statements.join(`\n `); - } - } - - class ConditionalDiscard { - constructor(condition){ - this.condition = condition; - } - toGLSL(context) { - context.discardConditions.push(`if (${this.condition}{discard;})`); - } - } - - // Node Helper functions - function getType(node) { - if (isShaderNode(node)) { return node.type; } - else if (Array.isArray(node) && node.length > 1) { return `vec${node.length}`; } - else if (typeof node === 'number' || (Array.isArray(node) && node.length === 1)) { - return 'float'; - } - return undefined; - } - - function computeVectorLength(values) { - let length = 0; - if (Array.isArray(values)) { - for(let val of values) { - if (isVectorType(val)) { - length += parseInt(val.type.slice(3)); - } - else length += 1; - } - } - else if (isVectorType(values)) { - length += parseInt(val.type.slice(3)); - } - if (![2, 3, 4].includes(length)) { - throw new Error(`You have attempted to construct a vector with ${length} values. Only vec2, vec3, and vec4 types are supported.`) - } - return length - } - - p5.dynamicNode = function (input) { - if (isShaderNode(input)) { - return input; - } - else if (typeof input === 'number') { - return new FloatNode(input); - } - else if (Array.isArray(input)) { - return nodeConstructors.dynamicVector(input); - } - } - - // For replacing unary expressions - p5.unaryNode = function(input, sign) { - input = p5.dynamicNode(input); - return dynamicAddSwizzleTrap(new UnaryExpressionNode(input, sign)); - } - - function isShaderNode(node) { - return (node instanceof BaseNode); - } - - function isIntType(node) { - return (isShaderNode(node) && (node.type === 'int')); - } - - function isFloatType(node) { - return (isShaderNode(node) && (node.type === 'float')); - } - - function isFloatNode(node) { - return (node instanceof FloatNode); - } - - function isVectorType(node) { - return (isShaderNode(node) && (node.type === 'vec2'|| node.type === 'vec3' || node.type === 'vec4')); - } - - function isBinaryExpressionNode(node) { - return (node instanceof BinaryExpressionNode); - } - - function isVariableNode(node) { - return (node instanceof VariableNode || node instanceof ComponentNode); - } - - function isConditionalNode(node) { - return (node instanceof ConditionalNode || node instanceof BranchNode) - } - - function hasTemporaryVariable(node) { - return (node.temporaryVariable); - } - - function isPrimitiveNode(node) { - return (node instanceof FloatNode || node instanceof IntNode || node instanceof VectorNode); - } - - function isFunctionCallNode(node) { - return (node instanceof FunctionCallNode); - } - - function isVectorNode(node) { - return (node instanceof VectorNode) - } - - function isUnaryExpressionNode(node) { - return (node instanceof UnaryExpressionNode) - } - - // Helper function to check if a type is a user defined struct or native type - function isGLSLNativeType(typeName) { - // Supported types for now - const glslNativeTypes = ['int', 'float', 'vec2', 'vec3', 'vec4', 'sampler2D']; - return glslNativeTypes.includes(typeName); - } - - // Shader Generator - // This class is responsible for converting the nodes into an object containing GLSL code, to be used by p5.Shader.modify - - class ShaderGenerator { - constructor(userCallback, originalShader, srcLocations) { - GLOBAL_SHADER = this; - this.userCallback = userCallback; - this.srcLocations = srcLocations; - this.generateHookOverrides(originalShader); - this.output = { - vertexDeclarations: new Set(), - fragmentDeclarations: new Set(), - uniforms: {}, - }; - this.uniformNodes = []; - this.resetGLSLContext(); - this.isGenerating = false; - } - - generate() { - const prevFESDisabled = p5.disableFriendlyErrors; - // We need a custom error handling system within shader generation - p5.disableFriendlyErrors = true; - - this.isGenerating = true; - this.userCallback(); - this.output.vertexDeclarations = [...this.output.vertexDeclarations].join('\n'); - this.output.fragmentDeclarations = [...this.output.fragmentDeclarations].join('\n'); - this.isGenerating = false; - - this.cleanup(); - p5.disableFriendlyErrors = prevFESDisabled; - return this.output; - } - - // This method generates the hook overrides which the user calls in their modify function. - generateHookOverrides(originalShader) { - const availableHooks = { - ...originalShader.hooks.vertex, - ...originalShader.hooks.fragment, - } - - const windowOverrides = {}; - const fnOverrides = {}; - - Object.keys(availableHooks).forEach((hookName) => { - const hookTypes = originalShader.hookTypes(hookName); - - // These functions are where the user code is executed - this[hookTypes.name] = function(userCallback) { - // Create the initial nodes which are passed to the user callback - // Also generate a string of the arguments for the code generation - const argNodes = [] - const argsArray = []; - - hookTypes.parameters.forEach((parameter) => { - // For hooks with structs as input we should pass an object populated with variable nodes - if (!isGLSLNativeType(parameter.type.typeName)) { - const structArg = {}; - parameter.type.properties.forEach((property) => { - structArg[property.name] = variableConstructor(`${parameter.name}.${property.name}`, property.type.typeName, true); - }); - argNodes.push(structArg); - } else { - argNodes.push( - variableConstructor(parameter.name, parameter.type.typeName, true) - ); - } - const qualifiers = parameter.type.qualifiers.length > 0 ? parameter.type.qualifiers.join(' ') : ''; - argsArray.push(`${qualifiers} ${parameter.type.typeName} ${parameter.name}`.trim()) - }) - - let returnedValue = userCallback(...argNodes); - const expectedReturnType = hookTypes.returnType; - const toGLSLResults = {}; - - // If the expected return type is a struct we need to evaluate each of its properties - if (!isGLSLNativeType(expectedReturnType.typeName)) { - Object.entries(returnedValue).forEach(([propertyName, propertyNode]) => { - propertyNode = p5.dynamicNode(propertyNode); - toGLSLResults[propertyName] = propertyNode.toGLSLBase(this.context); - this.context.updateComponents(propertyNode); - }); - } else { - if (!isShaderNode(returnedValue)) { - returnedValue = nodeConstructors[expectedReturnType.typeName](returnedValue) - } else if (isFloatType(returnedValue) && expectedReturnType.typeName.startsWith('vec')) { - returnedValue = nodeConstructors[expectedReturnType.typeName](returnedValue); - } - toGLSLResults['notAProperty'] = returnedValue.toGLSLBase(this.context); - this.context.updateComponents(returnedValue); - } - - this.context.ifs.forEach((statement) => { - if (statement.usedIn.length === 0) { return; } - const lines = statement.toGLSL(this.context); - this.context.declarations.splice(statement.insertionPoint, 0, lines); - }) - // Build the final GLSL string. - // The order of this code is a bit confusing, we need to call toGLSLBase - let codeLines = [ - `(${argsArray.join(', ')}) {`, - ...this.context.declarations, - `\n ${hookTypes.returnType.typeName} finalReturnValue;` - ]; - - Object.entries(toGLSLResults).forEach(([propertyName, result]) => { - const propString = expectedReturnType.properties ? `.${propertyName}` : ''; - codeLines.push(` finalReturnValue${propString} = ${result};`) - }); - - this.context.declarations = []; - for (let key in this.context.varyings) { - const declArray = this.context.varyings[key]; - const finalVaryingAssignments = []; - declArray.forEach(obj => { - const { node, value } = obj; - finalVaryingAssignments.push(` ${node.name} = ${value.toGLSLBase(this.context)};`) - finalVaryingAssignments.unshift(...this.context.declarations); - node.timesChanged = 0; - }); - codeLines.push(...finalVaryingAssignments); - } - - codeLines.push(' return finalReturnValue;', '}'); - this.output[hookName] = codeLines.join('\n'); - this.resetGLSLContext(); - } - windowOverrides[hookTypes.name] = window[hookTypes.name]; - fnOverrides[hookTypes.name] = fn[hookTypes.name]; - - // Expose the Functions to global scope for users to use - window[hookTypes.name] = function(userOverride) { - GLOBAL_SHADER[hookTypes.name](userOverride); - }; - fn[hookTypes.name] = function(userOverride) { - GLOBAL_SHADER[hookTypes.name](userOverride); - }; - }); - - - this.cleanup = () => { - for (const key in windowOverrides) { - window[key] = windowOverrides[key]; - } - for (const key in fnOverrides) { - fn[key] = fnOverrides[key]; - } - }; - } - - registerVarying(node, value) { - if (!Array.isArray(this.context.varyings[node.name])) { - this.context.varyings[node.name] = []; - } - this.context.varyings[node.name].push({ node, value }); - this.output.vertexDeclarations.add(`OUT ${node.type} ${node.name};`); - this.output.fragmentDeclarations.add(`IN ${node.type} ${node.name};`); - } - - resetGLSLContext() { - this.uniformNodes.forEach((node) => { - node.usedIn = []; - node.temporaryVariable = undefined; - }); - this.context = { - id: 0, - getNextID() { return this.id++ }, - declarations: [], - varyings: [], - ifs: [], - updateComponents: function(node, _emplaceAt, _changedComponents) { - if (node.componentsChanged) { - if (!_changedComponents) { - _changedComponents = node.componentNames.map(() => true); - } - const lines = []; - if (isVectorNode(node)) { - node.componentNames.forEach((name, i) => { - if (!_changedComponents[i]) return; - if (node[name] !== node.originalValues[i]) { - const replacement = nodeConstructors['float'](node[name]); - const line = ` ${node.temporaryVariable}.${name} = ${replacement.toGLSLBase(this)};`; - lines.push(line); - } - }); - } else { - const components = node.componentNames.map((name) => { - return node[name] - }); - const replacement = nodeConstructors[node.type](components); - const line = ` ${node.temporaryVariable} = ${replacement.toGLSLBase(this)};` - lines.push(line); - } - if (_emplaceAt) { - this.declarations.splice(_emplaceAt, 0, ...lines) - } else { - this.declarations.push(...lines); - } - node.componentsChanged = false; - } - } - } - this.uniformNodes = []; - } - } - - // User function helpers - function makeDependencyObject(dep) { - if (isVectorType(dep)) { - return { - node: dep, - isVector: true, - originalComponents: [...dep.componentNames.map(name => dep[name])], - get currentComponents() { - return dep.componentNames.map(name => dep[name]); - } - }; - } else { - return { - node: dep, - isVector: false - }; - } - } - - function makeDependencyArray(dependencies) { - return dependencies.map(dep => makeDependencyObject(dep)); - } - - function conformVectorParameters(value, vectorDimensions) { - // Allow arguments as arrays or otherwise. The following are all equivalent: - // ([0,0,0,0]) (0,0,0,0) (0) ([0]) - if (!Array.isArray(value)) { - value = [value]; - } - value = value.flat(); - value = value.map(val => { - if (isVectorType(val)) { - const componentArray = val.componentNames.map(comp => val[comp]); - return componentArray; - } else { - return val; - } - }).flat(); - // Populate arguments so uniformVector3(0) becomes [0,0,0] - if (value.length === 1 && !isVectorNode(value[0])) { - value = Array(vectorDimensions).fill(value[0]); - } - return value; - } - - function swizzleTrap(size) { - const swizzleSets = [ - ['x', 'y', 'z', 'w'], - ['r', 'g', 'b', 'a'], - ['s', 't', 'p', 'q'] - ].map(s => s.slice(0, size)); - return { - get(target, property, receiver) { - if (property in target) { - return Reflect.get(...arguments); - } else { - for (const set of swizzleSets) { - if ([...property].every(char => set.includes(char))) { - if (property.length === 1) { - return target[swizzleSets[0][set.indexOf(property[0])]] - } - const components = [...property].map(char => { - const index = set.indexOf(char); - const mappedChar = swizzleSets[0][index]; - return target[mappedChar]; - }); - - const type = `vec${property.length}`; - return nodeConstructors[type](components); - } - } - } - }, - set(target, property, value, receiver) { - for (const set of swizzleSets) { - const propertyCharArray = [...property]; - if (propertyCharArray.every(char => set.includes(char))) { - const newValues = Array.isArray(value) ? value : Array(property.length).fill(value); - propertyCharArray.forEach((char, i) => { - const index = set.indexOf(char); - const realProperty = swizzleSets[0][index]; - const descriptor = Object.getOwnPropertyDescriptor(target, realProperty); - Reflect.set(target, realProperty, newValues[i], receiver); - }); - return true; - } - } - return Reflect.set(...arguments); - } - } - } - - // User functions - fn.If = function (condition, branch) { - return new ConditionalNode(condition, branch); - } - - fn.instanceID = function() { - return variableConstructor('gl_InstanceID', 'int'); - } - - fn.getTexture = function(...userArgs) { - const props = { args: ['sampler2D', 'vec2'], returnType: 'vec4', isp5Function: true }; - return fnNodeConstructor('getTexture', userArgs, props); - } - - function dynamicAddSwizzleTrap(node, _size) { - if (node.type.startsWith('vec') || _size) { - const size = _size ? _size : parseInt(node.type.slice(3)); - node = new Proxy(node, swizzleTrap(size)); - node.addVectorComponents(); - } - return node; - } - - function binaryExpressionNodeConstructor(a, b, operator, isInternal) { - let node; - if (operator === '%') { - node = new ModulusNode(a, b); - } else { - node = new BinaryExpressionNode(a, b, operator, isInternal); - } - return dynamicAddSwizzleTrap(node); - } - - function variableConstructor(name, type, isInternal) { - const node = new VariableNode(name, type, isInternal); - return dynamicAddSwizzleTrap(node); - } - - function fnNodeConstructor(name, userArgs, properties, isInternal) { - let node = new FunctionCallNode(name, userArgs, properties, isInternal); - node = dynamicAddSwizzleTrap(node); - node.dependsOn = makeDependencyArray(node.args); - const dependsOnConditionals = node.args.map(arg => { - const conditionals = arg.usedIn.filter(n => isConditionalNode(n)).map(c => { - if (c instanceof BranchNode) { - return c.parent; - } else { - return c; - } - }); - return conditionals; - }).flat(); - dependsOnConditionals.forEach(conditional => conditional.usedIn.push(node)); - - return node; - } - - const nodeConstructors = { - int: (value) => new IntNode(value), - float: (value) => new FloatNode(value), - vec2: (value) => dynamicAddSwizzleTrap(new VectorNode(value, 'vec2')), - vec3: (value) => dynamicAddSwizzleTrap(new VectorNode(value, 'vec3')), - vec4: (value) => dynamicAddSwizzleTrap(new VectorNode(value, 'vec4')), - dynamicVector: function(value) { - const size = computeVectorLength(value); - return this[`vec${size}`](value); - }, - }; - - // Generating uniformFloat, uniformVec, createFloat, etc functions - // Maps a GLSL type to the name suffix for method names - const GLSLTypesToIdentifiers = { - int: 'Int', - float: 'Float', - vec2: 'Vector2', - vec3: 'Vector3', - vec4: 'Vector4', - sampler2D: 'Texture', - }; - - for (const glslType in GLSLTypesToIdentifiers) { - // Generate uniform*() Methods for creating uniforms - const typeIdentifier = GLSLTypesToIdentifiers[glslType]; - const uniformMethodName = `uniform${typeIdentifier}`; - - ShaderGenerator.prototype[uniformMethodName] = function(...args) { - let [name, ...defaultValue] = args; - if (glslType.startsWith('vec') && !(defaultValue[0] instanceof Function)) { - defaultValue = conformVectorParameters(defaultValue, parseInt(glslType.slice(3))); - this.output.uniforms[`${glslType} ${name}`] = defaultValue; - } - else { - this.output.uniforms[`${glslType} ${name}`] = defaultValue[0]; - } - const uniform = variableConstructor(name, glslType, false); - this.uniformNodes.push(uniform); - return uniform; - }; - - fn[uniformMethodName] = function (...args) { - return GLOBAL_SHADER[uniformMethodName](...args); - }; - - - // We don't need a texture creation method. - if (glslType === 'sampler2D') { continue; } - - const varyingMethodName = `varying${typeIdentifier}`; - ShaderGenerator.prototype[varyingMethodName] = function(name) { - return dynamicAddSwizzleTrap(new VaryingNode(name, glslType, false)); - } - - fn[varyingMethodName] = function (name) { - return GLOBAL_SHADER[varyingMethodName](name); - }; - - // Generate the creation methods for creating variables in shaders - const originalFn = fn[glslType]; - fn[glslType] = function (...value) { - if (GLOBAL_SHADER?.isGenerating) { - if (glslType.startsWith('vec')) { - value = conformVectorParameters(value, parseInt(glslType.slice(3))); - } else { - value = value[0]; - } - return nodeConstructors[glslType](value); - } else if (originalFn) { - return originalFn.apply(this, value); - } else { - p5._friendlyError( - `It looks like you've called ${glslType} outside of a shader's modify() function.` - ); - } - }; - } - - // GLSL Built in functions - // Add a whole lot of these functions. - // https://docs.gl/el3/abs - const builtInGLSLFunctions = { - //////////// Trigonometry ////////// - 'acos': { args: ['genType'], returnType: 'genType', isp5Function: true}, - 'acosh': { args: ['genType'], returnType: 'genType', isp5Function: false}, - 'asin': { args: ['genType'], returnType: 'genType', isp5Function: true}, - 'asinh': { args: ['genType'], returnType: 'genType', isp5Function: false}, - 'atan': [ - { args: ['genType'], returnType: 'genType', isp5Function: false}, - { args: ['genType', 'genType'], returnType: 'genType', isp5Function: false}, - ], - 'atanh': { args: ['genType'], returnType: 'genType', isp5Function: false}, - 'cos': { args: ['genType'], returnType: 'genType', isp5Function: true}, - 'cosh': { args: ['genType'], returnType: 'genType', isp5Function: false}, - 'degrees': { args: ['genType'], returnType: 'genType', isp5Function: true}, - 'radians': { args: ['genType'], returnType: 'genType', isp5Function: true}, - 'sin': { args: ['genType'], returnType: 'genType' , isp5Function: true}, - 'sinh': { args: ['genType'], returnType: 'genType', isp5Function: false}, - 'tan': { args: ['genType'], returnType: 'genType', isp5Function: true}, - 'tanh': { args: ['genType'], returnType: 'genType', isp5Function: false}, - - ////////// Mathematics ////////// - 'abs': { args: ['genType'], returnType: 'genType', isp5Function: true}, - 'ceil': { args: ['genType'], returnType: 'genType', isp5Function: true}, - 'clamp': { args: ['genType', 'genType', 'genType'], returnType: 'genType', isp5Function: false}, - 'dFdx': { args: ['genType'], returnType: 'genType', isp5Function: false}, - 'dFdy': { args: ['genType'], returnType: 'genType', isp5Function: false}, - 'exp': { args: ['genType'], returnType: 'genType', isp5Function: true}, - 'exp2': { args: ['genType'], returnType: 'genType', isp5Function: false}, - 'floor': { args: ['genType'], returnType: 'genType', isp5Function: true}, - 'fma': { args: ['genType', 'genType', 'genType'], returnType: 'genType', isp5Function: false}, - 'fract': { args: ['genType'], returnType: 'genType', isp5Function: true}, - 'fwidth': { args: ['genType'], returnType: 'genType', isp5Function: false}, - 'inversesqrt': { args: ['genType'], returnType: 'genType', isp5Function: true}, - // 'isinf': {}, - // 'isnan': {}, - 'log': { args: ['genType'], returnType: 'genType', isp5Function: true}, - 'log2': { args: ['genType'], returnType: 'genType', isp5Function: false}, - 'max': [ - { args: ['genType', 'genType'], returnType: 'genType', isp5Function: true}, - { args: ['genType', 'float'], returnType: 'genType', isp5Function: true}, - ], - 'min': [ - { args: ['genType', 'genType'], returnType: 'genType', isp5Function: true}, - { args: ['genType', 'float'], returnType: 'genType', isp5Function: true}, - ], - 'mix': [ - { args: ['genType', 'genType', 'genType'], returnType: 'genType', isp5Function: false}, - { args: ['genType', 'genType', 'float'], returnType: 'genType', isp5Function: false}, - ], - // 'mod': {}, - // 'modf': {}, - 'pow': { args: ['genType', 'genType'], returnType: 'genType', isp5Function: true}, - 'round': { args: ['genType'], returnType: 'genType', isp5Function: true}, - 'roundEven': { args: ['genType'], returnType: 'genType', isp5Function: false}, - // 'sign': {}, - 'smoothstep': [ - { args: ['genType', 'genType', 'genType'], returnType: 'genType', isp5Function: false}, - { args: ['float', 'float', 'genType'], returnType: 'genType', isp5Function: false}, - ], - 'sqrt': { args: ['genType'], returnType: 'genType', isp5Function: true}, - 'step': { args: ['genType', 'genType'], returnType: 'genType', isp5Function: false}, - 'noise': { args: ['vec2'], returnType: 'float', isp5Function: false }, - 'trunc': { args: ['genType'], returnType: 'genType', isp5Function: false}, - - ////////// Vector ////////// - 'cross': { args: ['vec3', 'vec3'], returnType: 'vec3', isp5Function: true}, - 'distance': { args: ['genType', 'genType'], returnType: 'float', isp5Function: true}, - 'dot': { args: ['genType', 'genType'], returnType: 'float', isp5Function: true}, - // 'equal': {}, - 'faceforward': { args: ['genType', 'genType', 'genType'], returnType: 'genType', isp5Function: false}, - 'length': { args: ['genType'], returnType: 'float', isp5Function: false}, - 'normalize': { args: ['genType'], returnType: 'genType', isp5Function: true}, - // 'notEqual': {}, - 'reflect': { args: ['genType', 'genType'], returnType: 'genType', isp5Function: false}, - 'refract': { args: ['genType', 'genType', 'float'], returnType: 'genType', isp5Function: false}, - - ////////// Texture sampling ////////// - 'texture': {args: ['sampler2D', 'vec2'], returnType: 'vec4', isp5Function: true}, - } - - Object.entries(builtInGLSLFunctions).forEach(([functionName, properties]) => { - const isp5Function = Array.isArray(properties) ? properties[0].isp5Function : properties.isp5Function; - if (isp5Function) { - const originalFn = fn[functionName]; - fn[functionName] = function (...args) { - if (GLOBAL_SHADER?.isGenerating) { - return fnNodeConstructor(functionName, args, properties) - } else { - return originalFn.apply(this, args); - } - } - } else { - fn[functionName] = function (...args) { - if (GLOBAL_SHADER?.isGenerating) { - return new fnNodeConstructor(functionName, args, properties); - } else { - p5._friendlyError( - `It looks like you've called ${functionName} outside of a shader's modify() function.` - ); - } - } - } - }) - // Alias GLSL's mix function as lerp in p5.strands - // Bridging p5.js lerp and GLSL mix for consistency in shader expressions - const originalLerp = fn.lerp; - fn.lerp = function (...args) { - if (GLOBAL_SHADER?.isGenerating) { - return this.mix(...args); // Use mix inside p5.strands - } else { - return originalLerp.apply(this, args); // Fallback to normal p5.js lerp - } - }; - const originalNoise = fn.noise; - fn.noise = function (...args) { - if (!GLOBAL_SHADER?.isGenerating) { - return originalNoise.apply(this, args); // fallback to regular p5.js noise - } - - GLOBAL_SHADER.output.vertexDeclarations.add(noiseGLSL); - GLOBAL_SHADER.output.fragmentDeclarations.add(noiseGLSL); - // Handle noise(x, y) as noise(vec2) - let nodeArgs; - if (args.length === 2) { - nodeArgs = [fn.vec2(args[0], args[1])]; - } else { - nodeArgs = args; - } - - return fnNodeConstructor('noise', nodeArgs, { - args: ['vec2'], - returnType: 'float' - }); - }; - -} - - -export default shadergenerator; - -// if (typeof p5 !== 'undefined') { -// p5.registerAddon(shadergenerator) -// } From 100304fcbbaec9ee73bb05b4dcdf0957503aaf02 Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Mon, 15 Sep 2025 17:51:38 +0100 Subject: [PATCH 60/69] remove dev console.log --- src/strands/p5.strands.js | 3 --- src/strands/strands_transpiler.js | 1 - 2 files changed, 4 deletions(-) diff --git a/src/strands/p5.strands.js b/src/strands/p5.strands.js index 0f1ea06f25..357a09745e 100644 --- a/src/strands/p5.strands.js +++ b/src/strands/p5.strands.js @@ -83,9 +83,6 @@ function strands(p5, fn) { // 3. Generate shader code hooks object from the IR // ....... const hooksObject = generateShaderCode(strandsContext); - console.log(hooksObject); - console.log(hooksObject['vec4 getFinalColor']); - console.log(hooksObject['Vertex getWorldInputs']); // Reset the strands runtime context deinitStrandsContext(strandsContext); diff --git a/src/strands/strands_transpiler.js b/src/strands/strands_transpiler.js index 594af2cbac..98db275fc0 100644 --- a/src/strands/strands_transpiler.js +++ b/src/strands/strands_transpiler.js @@ -225,7 +225,6 @@ const ASTCallbacks = { transpiledSource.lastIndexOf('}') ).replaceAll(';', '') ); - console.log(transpiledSource); return () => internalStrandsCallback(p5, ...scopeKeys.map(key => scope[key])); } \ No newline at end of file From 2b863e69eea033243939e7efb055f4c792f40e27 Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Tue, 16 Sep 2025 19:59:05 +0100 Subject: [PATCH 61/69] add instance mode changes, fix bug where struct properties returned in hooks didnt cast from float->vec types --- preview/global/sketch.js | 1 - src/strands/p5.strands.js | 2 +- src/strands/strands_api.js | 19 +++++++++++++++---- src/strands/strands_transpiler.js | 14 +++++++++----- 4 files changed, 25 insertions(+), 11 deletions(-) diff --git a/preview/global/sketch.js b/preview/global/sketch.js index 1d4b5b1270..990b750d27 100644 --- a/preview/global/sketch.js +++ b/preview/global/sketch.js @@ -78,7 +78,6 @@ function bloomShaderCallback() { const originalCol = getTexture(preBlur, input.texCoord); const intensity = max(originalCol, 0.1) * 12.2; - const bloom = originalCol + blurredCol * intensity; return [bloom.rgb, 1]; }); diff --git a/src/strands/p5.strands.js b/src/strands/p5.strands.js index 357a09745e..5992f2f182 100644 --- a/src/strands/p5.strands.js +++ b/src/strands/p5.strands.js @@ -69,7 +69,7 @@ function strands(p5, fn) { // #7955 Wrap function declaration code in brackets so anonymous functions are not top level statements, which causes an error in acorn when parsing // https://github.com/acornjs/acorn/issues/1385 const sourceString = `(${shaderModifier.toString()})`; - strandsCallback = transpileStrandsToJS(sourceString, options.srcLocations, scope); + strandsCallback = transpileStrandsToJS(p5, sourceString, options.srcLocations, scope); } else { strandsCallback = shaderModifier; } diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index 4cead87299..a663134531 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -45,7 +45,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { }; } if (arity === 'unary') { - fn[name] = function (nodeOrValue) { + p5[name] = function (nodeOrValue) { const { id, dimension } = build.unaryOpNode(strandsContext, nodeOrValue, opCode); return createStrandsNode(id, dimension, strandsContext); } @@ -72,7 +72,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { return null; } - fn.strandsNode = function(...args) { + p5.strandsNode = function(...args) { if (args.length === 1 && args[0] instanceof StrandsNode) { return args[0]; } @@ -262,7 +262,7 @@ export function createShaderHooksFunctions(strandsContext, fn, shader) { ...shader.hooks.fragment, } const hookTypes = Object.keys(availableHooks).map(name => shader.hookTypes(name)); - const cfg = strandsContext.cfg; + const { cfg, dag } = strandsContext; for (const hookType of hookTypes) { const hookImplementation = function(hookUserCallback) { @@ -280,9 +280,20 @@ export function createShaderHooksFunctions(strandsContext, fn, shader) { const expectedStructType = structType(expectedReturnType); if (userReturned instanceof StrandsNode) { const returnedNode = getNodeDataFromID(strandsContext.dag, userReturned.id); - if (!returnedNode.baseType === expectedStructType.typeName) { + + 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; } else { diff --git a/src/strands/strands_transpiler.js b/src/strands/strands_transpiler.js index 98db275fc0..dd39a21c87 100644 --- a/src/strands/strands_transpiler.js +++ b/src/strands/strands_transpiler.js @@ -45,7 +45,7 @@ const ASTCallbacks = { node.type = 'CallExpression' node.callee = { type: 'Identifier', - name: unaryFnName, + name: `__p5.${unaryFnName}`, } node.arguments = [node.argument] } @@ -68,7 +68,7 @@ const ASTCallbacks = { type: 'CallExpression', callee: { type: 'Identifier', - name: unaryFnName + name: `__p5.${unaryFnName}` }, arguments: [node.argument.object], }; @@ -133,7 +133,7 @@ const ASTCallbacks = { node.type = 'CallExpression'; node.callee = { type: 'Identifier', - name: 'strandsNode', + name: '__p5.strandsNode', }; node.arguments = [original]; }, @@ -187,7 +187,7 @@ const ASTCallbacks = { type: 'CallExpression', callee: { type: 'Identifier', - name: 'strandsNode', + name: '__p5.strandsNode', }, arguments: [node.left] } @@ -208,7 +208,7 @@ const ASTCallbacks = { }, } - export function transpileStrandsToJS(sourceString, srcLocations, scope) { + export function transpileStrandsToJS(p5, sourceString, srcLocations, scope) { const ast = parse(sourceString, { ecmaVersion: 2021, locations: srcLocations @@ -217,6 +217,10 @@ const ASTCallbacks = { const transpiledSource = escodegen.generate(ast); const scopeKeys = Object.keys(scope); const internalStrandsCallback = new Function( + // Create a parameter called __p5, not just p5, because users of instance mode + // may pass in a variable called p5 as a scope variable. If we rely on a variable called + // p5, then the scope variable called p5 might accidentally override internal function + // calls to p5 static methods. '__p5', ...scopeKeys, transpiledSource From 646234512e8ae6cea11beda057b79dce5dfdf3b6 Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Tue, 16 Sep 2025 20:01:21 +0100 Subject: [PATCH 62/69] mark atan as p5 function, prevent bug where using atan outside strands logged a warning --- src/strands/strands_builtins.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/strands/strands_builtins.js b/src/strands/strands_builtins.js index e6b985e3bb..3eb76c8ff6 100644 --- a/src/strands/strands_builtins.js +++ b/src/strands/strands_builtins.js @@ -9,8 +9,8 @@ const builtInGLSLFunctions = { asin: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}], asinh: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}], atan: [ - { params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}, - { params: [GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}, + { params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}, + { params: [GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}, ], atanh: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}], cos: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}], From 37abf7f713a33088708cfd6ff17d48286dae643f Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Tue, 16 Sep 2025 20:06:39 +0100 Subject: [PATCH 63/69] add back documentation --- src/strands/p5.strands.js | 504 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 504 insertions(+) diff --git a/src/strands/p5.strands.js b/src/strands/p5.strands.js index 5992f2f182..f57763c82f 100644 --- a/src/strands/p5.strands.js +++ b/src/strands/p5.strands.js @@ -101,3 +101,507 @@ export default strands; if (typeof p5 !== 'undefined') { p5.registerAddon(strands) } + +/* ------------------------------------------------------------- */ +/** + * @method getWorldInputs + * @description + * Registers a callback to modify the world-space properties of each vertex in a shader. This hook can be used inside baseColorShader().modify() and similar shader modify() calls to customize vertex positions, normals, texture coordinates, and colors before rendering. "World space" refers to the coordinate system of the 3D scene, before any camera or projection transformations are applied. + * + * The callback receives a vertex object with the following properties: + * - `position`: a three-component vector representing the original position of the vertex. + * - `normal`: a three-component vector representing the direction the surface is facing. + * - `texCoord`: a two-component vector representing the texture coordinates. + * - `color`: a four-component vector representing the color of the vertex (red, green, blue, alpha). + * + * This hook is available in: + * - baseMaterialShader() + * - baseNormalShader() + * - baseColorShader() + * - baseStrokeShader() + * + * @param {Function} callback + * A callback function which receives a vertex object containing position (vec3), normal (vec3), texCoord (vec2), and color (vec4) properties. The function should return the modified vertex object. + * + * @example + *
+ * + * let myShader; + * function setup() { + * createCanvas(200, 200, WEBGL); + * myShader = baseMaterialShader().modify(() => { + * let t = uniformFloat(() => millis()); + * getWorldInputs(inputs => { + * // Move the vertex up and down in a wave in world space + * // In world space, moving the object (e.g., with translate()) will affect these coordinates +* // The sphere is ~50 units tall here, so 20 gives a noticeable wave + * inputs.position.y += 20 * sin(t * 0.001 + inputs.position.x * 0.05); + * return inputs; + * }); + * }); + * } + * function draw() { + * background(255); + * shader(myShader); + * lights(); + * noStroke(); + * fill('red'); + * sphere(50); + * } + * + *
+ */ + +/** + * @method combineColors + * @description + * Registers a callback to customize how color components are combined in the fragment shader. This hook can be used inside baseMaterialShader().modify() and similar shader modify() calls to control the final color output of a material. The callback receives an object with the following properties: + * + * - `baseColor`: a three-component vector representing the base color (red, green, blue). + * - `diffuse`: a single number representing the diffuse reflection. + * - `ambientColor`: a three-component vector representing the ambient color. + * - `ambient`: a single number representing the ambient reflection. + * - `specularColor`: a three-component vector representing the specular color. + * - `specular`: a single number representing the specular reflection. + * - `emissive`: a three-component vector representing the emissive color. + * - `opacity`: a single number representing the opacity. + * + * The callback should return a vector with four components (red, green, blue, alpha) for the final color. + * + * This hook is available in: + * - baseMaterialShader() + * + * @param {Function} callback + * A callback function which receives the object described above and returns a vector with four components for the final color. + * + * @example + *
+ * + * let myShader; + * function setup() { + * createCanvas(200, 200, WEBGL); + * myShader = baseMaterialShader().modify(() => { + * combineColors(components => { + * // Custom color combination: add a green tint using vector properties + * return [ + * components.baseColor * components.diffuse + + * components.ambientColor * components.ambient + + * components.specularColor * components.specular + + * components.emissive + + * [0, 0.2, 0], // Green tint for visibility + * components.opacity + * ]; + * }); + * }); + * } + * function draw() { + * background(255); + * shader(myShader); + * lights(); + * noStroke(); + * fill('white'); + * sphere(50); + * } + * + *
+ */ + +/** + * @method beforeVertex + * @private + * @description + * Registers a callback to run custom code at the very start of the vertex shader. This hook can be used inside baseColorShader().modify() and similar shader modify() calls to set up variables or perform calculations that affect every vertex before processing begins. The callback receives no arguments. + * + * Note: This hook is currently limited to per-vertex operations; storing variables for later use is not supported. + * + * This hook is available in: + * - baseColorShader() + * - baseMaterialShader() + * - baseNormalShader() + * - baseStrokeShader() + * + * @param {Function} callback + * A callback function which is called before each vertex is processed. + */ + +/** + * @method afterVertex + * @private + * @description + * Registers a callback to run custom code at the very end of the vertex shader. This hook can be used inside baseColorShader().modify() and similar shader modify() calls to perform cleanup or final calculations after all vertex processing is done. The callback receives no arguments. + * + * Note: This hook is currently limited to per-vertex operations; storing variables for later use is not supported. + * + * This hook is available in: + * - baseColorShader() + * - baseMaterialShader() + * - baseNormalShader() + * - baseStrokeShader() + * + * @param {Function} callback + * A callback function which is called after each vertex is processed. + */ + +/** + * @method beforeFragment + * @private + * @description + * Registers a callback to run custom code at the very start of the fragment shader. This hook can be used inside baseColorShader().modify() and similar shader modify() calls to set up variables or perform calculations that affect every pixel before color calculations begin. The callback receives no arguments. + * + * This hook is available in: + * - baseColorShader() + * - baseMaterialShader() + * - baseNormalShader() + * - baseStrokeShader() + * + * @param {Function} callback + * A callback function which is called before each fragment is processed. + * + * @example + *
+ * + * let myShader; + * function setup() { + * createCanvas(200, 200, WEBGL); + * myShader = baseColorShader().modify(() => { + * beforeFragment(() => { + * // Set a value for use in getFinalColor + * this.brightness = 0.5 + 0.5 * sin(millis() * 0.001); + * }); + * getFinalColor(color => { + * // Use the value set in beforeFragment to tint the color + * color.r *= this.brightness; // Tint red channel + * return color; + * }); + * }); + * } + * function draw() { + * background(220); + * shader(myShader); + * noStroke(); + * fill('teal'); + * box(100); + * } + * + *
+ */ + +/** + * @method getPixelInputs + * @description + * Registers a callback to modify the properties of each fragment (pixel) before the final color is calculated in the fragment shader. This hook can be used inside baseMaterialShader().modify() and similar shader modify() calls to adjust per-pixel data before lighting/mixing. + * + * The callback receives an `Inputs` object. Available fields depend on the shader: + * + * - In baseMaterialShader(): + * - `normal`: a three-component vector representing the surface normal. + * - `texCoord`: a two-component vector representing the texture coordinates (u, v). + * - `ambientLight`: a three-component vector representing the ambient light color. + * - `ambientMaterial`: a three-component vector representing the material's ambient color. + * - `specularMaterial`: a three-component vector representing the material's specular color. + * - `emissiveMaterial`: a three-component vector representing the material's emissive color. + * - `color`: a four-component vector representing the base color (red, green, blue, alpha). + * - `shininess`: a number controlling specular highlights. + * - `metalness`: a number controlling the metalness factor. + * + * - In baseStrokeShader(): + * - `color`: a four-component vector representing the stroke color (red, green, blue, alpha). + * - `tangent`: a two-component vector representing the stroke tangent. + * - `center`: a two-component vector representing the cap/join center. + * - `position`: a two-component vector representing the current fragment position. + * - `strokeWeight`: a number representing the stroke weight in pixels. + * + * Return the modified object to update the fragment. + * + * This hook is available in: + * - baseMaterialShader() + * - baseStrokeShader() + * + * @param {Function} callback + * A callback function which receives the fragment inputs object and should return it after making any changes. + * + * @example + *
+ * + * let myShader; + * function setup() { + * createCanvas(200, 200, WEBGL); + * myShader = baseMaterialShader().modify(() => { + * let t = uniformFloat(() => millis()); + * getPixelInputs(inputs => { + * // Animate alpha (transparency) based on x position + * inputs.color.a = 0.5 + 0.5 * sin(inputs.texCoord.x * 10.0 + t * 0.002); + * return inputs; + * }); + * }); + * } + * function draw() { + * background(240); + * shader(myShader); + * lights(); + * noStroke(); + * fill('purple'); + * circle(0, 0, 100); + * } + * + *
+ */ + +/** + * @method shouldDiscard + * @private + * @description + * Registers a callback to decide whether to discard (skip drawing) a fragment (pixel) in the fragment shader. This hook can be used inside baseStrokeShader().modify() and similar shader modify() calls to create effects like round points or custom masking. The callback receives a boolean: + * - `willDiscard`: true if the fragment would be discarded by default + * + * Return true to discard the fragment, or false to keep it. + * + * This hook is available in: + * - baseStrokeShader() + * + * @param {Function} callback + * A callback function which receives a boolean and should return a boolean. + * + * @example + *
+ * + * let myShader; + * function setup() { + * createCanvas(200, 200, WEBGL); + * myShader = baseStrokeShader().modify({ + * 'bool shouldDiscard': '(bool outside) { return outside; }' + * }); + * } + * function draw() { + * background(255); + * strokeShader(myShader); + * strokeWeight(30); + * line(-width/3, 0, width/3, 0); + * } + * + *
+ */ + +/** + * @method getFinalColor + * @description + * Registers a callback to change the final color of each pixel after all lighting and mixing is done in the fragment shader. This hook can be used inside baseColorShader().modify() and similar shader modify() calls to adjust the color before it appears on the screen. The callback receives a four component vector representing red, green, blue, and alpha. + * + * Return a new color array to change the output color. + * + * This hook is available in: + * - baseColorShader() + * - baseMaterialShader() + * - baseNormalShader() + * - baseStrokeShader() + * + * @param {Function} callback + * A callback function which receives the color array and should return a color array. + * + * @example + *
+ * + * let myShader; + * function setup() { + * createCanvas(200, 200, WEBGL); + * myShader = baseColorShader().modify(() => { + * getFinalColor(color => { + * // Add a blue tint to the output color + * color.b += 0.4; + * return color; + * }); + * }); + * } + * function draw() { + * background(230); + * shader(myShader); + * noStroke(); + * fill('green'); + * circle(0, 0, 100); + * } + * + *
+ */ + +/** + * @method afterFragment + * @private + * @description + * Registers a callback to run custom code at the very end of the fragment shader. This hook can be used inside baseColorShader().modify() and similar shader modify() calls to perform cleanup or final per-pixel effects after all color calculations are done. The callback receives no arguments. + * + * This hook is available in: + * - baseColorShader() + * - baseMaterialShader() + * - baseNormalShader() + * - baseStrokeShader() + * + * @param {Function} callback + * A callback function which is called after each fragment is processed. + * + * @example + *
+ * + * let myShader; + * function setup() { + * createCanvas(200, 200, WEBGL); + * myShader = baseColorShader().modify(() => { + * getFinalColor(color => { + * // Add a purple tint to the color + * color.b += 0.2; + * return color; + * }); + * afterFragment(() => { + * // This hook runs after the final color is set for each fragment. + * // You could use this for debugging or advanced effects. + * }); + * }); + * } + * function draw() { + * background(240); + * shader(myShader); + * noStroke(); + * fill('purple'); + * sphere(60); + * } + * + *
+ */ + +/** + * @method getColor + * @description + * Registers a callback to set the final color for each pixel in a filter shader. This hook can be used inside baseFilterShader().modify() and similar shader modify() calls to control the output color for each pixel. The callback receives the following arguments: + * - `inputs`: an object with the following properties: + * - `texCoord`: a two-component vector representing the texture coordinates (u, v). + * - `canvasSize`: a two-component vector representing the canvas size in pixels (width, height). + * - `texelSize`: a two-component vector representing the size of a single texel in texture space. + * - `canvasContent`: a texture containing the sketch's contents before the filter is applied. + * + * Return a four-component vector `[r, g, b, a]` for the pixel. + * + * This hook is available in: + * - baseFilterShader() + * + * @param {Function} callback + * A callback function which receives the inputs object and canvasContent, and should return a color array. + * + * @example + *
+ * + * let myShader; + * function setup() { + * createCanvas(200, 200, WEBGL); + * myShader = baseFilterShader().modify(() => { + * getColor((inputs, canvasContent) => { + * // Warp the texture coordinates for a wavy effect + * let warped = [inputs.texCoord.x, inputs.texCoord.y + 0.1 * sin(inputs.texCoord.x * 10.0)]; + * return getTexture(canvasContent, warped); + * }); + * }); + * } + * function draw() { + * background(180); + * // Draw something to the canvas + * fill('yellow'); + * circle(0, 0, 150); + * filter(myShader); + * } + * + *
+ */ + +/** + * @method getObjectInputs + * @description + * Registers a callback to modify the properties of each vertex before any transformations are applied in the vertex shader. This hook can be used inside baseColorShader().modify() and similar shader modify() calls to move, color, or otherwise modify the raw model data. The callback receives an object with the following properties: + * + * - `position`: a three-component vector representing the original position of the vertex. + * - `normal`: a three-component vector representing the direction the surface is facing. + * - `texCoord`: a two-component vector representing the texture coordinates. + * - `color`: a four-component vector representing the color of the vertex (red, green, blue, alpha). + * + * Return the modified object to update the vertex. + * + * This hook is available in: + * - baseColorShader() + * - baseMaterialShader() + * - baseNormalShader() + * - baseStrokeShader() + * + * @param {Function} callback + * A callback function which receives the vertex object and should return it after making any changes. + * + * @example + *
+ * + * let myShader; + * function setup() { + * createCanvas(200, 200, WEBGL); + * myShader = baseColorShader().modify(() => { + * let t = uniformFloat(() => millis()); + * getObjectInputs(inputs => { + * // Create a sine wave along the x axis in object space + * inputs.position.y += sin(t * 0.001 + inputs.position.x); + * return inputs; + * }); + * }); + * } + * function draw() { + * background(220); + * shader(myShader); + * noStroke(); + * fill('orange'); + * sphere(50); + * } + * + *
+ */ + +/** + * @method getCameraInputs + * @description + * Registers a callback to adjust vertex properties after the model has been transformed by the camera, but before projection, in the vertex shader. This hook can be used inside baseColorShader().modify() and similar shader modify() calls to create effects that depend on the camera's view. The callback receives an object with the following properties: + * + * - `position`: a three-component vector representing the position after camera transformation. + * - `normal`: a three-component vector representing the normal after camera transformation. + * - `texCoord`: a two-component vector representing the texture coordinates. + * - `color`: a four-component vector representing the color of the vertex (red, green, blue, alpha). + * + * Return the modified object to update the vertex. + * + * This hook is available in: + * - baseColorShader() + * - baseMaterialShader() + * - baseNormalShader() + * - baseStrokeShader() + * + * @param {Function} callback + * A callback function which receives the vertex object and should return it after making any changes. + * + * @example + *
+ * + * let myShader; + * function setup() { + * createCanvas(200, 200, WEBGL); + * myShader = baseColorShader().modify(() => { + * getCameraInputs(inputs => { + * // Move vertices in camera space based on their x position + * let t = uniformFloat(() => millis()); + * inputs.position.y += 30 * sin(inputs.position.x * 0.05 + t * 0.001); + * // Tint all vertices blue + * inputs.color.b = 1; + * return inputs; + * }); + * }); + * } + * function draw() { + * background(200); + * shader(myShader); + * noStroke(); + * fill('red'); + * sphere(50); + * } + * + *
+ */ \ No newline at end of file From f3afffc738fc4a11f30bb04aa5372c2a6fbc0c4f Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Tue, 16 Sep 2025 20:07:20 +0100 Subject: [PATCH 64/69] add todo for internal parser options --- src/strands/p5.strands.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/strands/p5.strands.js b/src/strands/p5.strands.js index f57763c82f..2c62a38b78 100644 --- a/src/strands/p5.strands.js +++ b/src/strands/p5.strands.js @@ -61,6 +61,7 @@ function strands(p5, fn) { // const backend = glslBackend; initStrandsContext(strandsContext, glslBackend); createShaderHooksFunctions(strandsContext, fn, this); + // TODO: expose this, is internal for debugging for now. const options = { parser: true, srcLocations: false }; // 1. Transpile from strands DSL to JS From bf92d1c10e6d07c808014b457f8e9611bb39d3e3 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Tue, 16 Sep 2025 20:59:32 -0400 Subject: [PATCH 65/69] Fix issue with strands being immediately active --- src/strands/p5.strands.js | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/src/strands/p5.strands.js b/src/strands/p5.strands.js index 2c62a38b78..384b6068d7 100644 --- a/src/strands/p5.strands.js +++ b/src/strands/p5.strands.js @@ -18,23 +18,29 @@ function strands(p5, fn) { ////////////////////////////////////////////// // Global Runtime ////////////////////////////////////////////// - function initStrandsContext(ctx, backend) { + function initStrandsContext(ctx, backend, { active = false } = {}) { ctx.dag = createDirectedAcyclicGraph(); ctx.cfg = createControlFlowGraph(); ctx.uniforms = []; + ctx.vertexDeclarations = new Set(); + ctx.fragmentDeclarations = new Set(); ctx.hooks = []; ctx.backend = backend; - ctx.active = true; + ctx.active = active; ctx.previousFES = p5.disableFriendlyErrors; ctx.windowOverrides = {}; ctx.fnOverrides = {}; - p5.disableFriendlyErrors = true; + if (active) { + p5.disableFriendlyErrors = true; + } } function deinitStrandsContext(ctx) { ctx.dag = createDirectedAcyclicGraph(); ctx.cfg = createControlFlowGraph(); ctx.uniforms = []; + ctx.vertexDeclarations = new Set(); + ctx.fragmentDeclarations = new Set(); ctx.hooks = []; ctx.active = false; p5.disableFriendlyErrors = ctx.previousFES; @@ -54,12 +60,12 @@ function strands(p5, fn) { // Entry Point ////////////////////////////////////////////// const oldModify = p5.Shader.prototype.modify; - + p5.Shader.prototype.modify = function(shaderModifier, scope = {}) { if (shaderModifier instanceof Function) { // Reset the context object every time modify is called; // const backend = glslBackend; - initStrandsContext(strandsContext, glslBackend); + initStrandsContext(strandsContext, glslBackend, { active: true }); createShaderHooksFunctions(strandsContext, fn, this); // TODO: expose this, is internal for debugging for now. const options = { parser: true, srcLocations: false }; @@ -74,22 +80,22 @@ function strands(p5, fn) { } else { strandsCallback = shaderModifier; } - + // 2. Build the IR from JavaScript API const globalScope = createBasicBlock(strandsContext.cfg, BlockType.GLOBAL); pushBlock(strandsContext.cfg, globalScope); strandsCallback(); popBlock(strandsContext.cfg); - + // 3. Generate shader code hooks object from the IR // ....... const hooksObject = generateShaderCode(strandsContext); - + // Reset the strands runtime context deinitStrandsContext(strandsContext); // Call modify with the generated hooks object - return oldModify.call(this, hooksObject); + return oldModify.call(this, hooksObject); } else { return oldModify.call(this, shaderModifier) @@ -108,13 +114,13 @@ if (typeof p5 !== 'undefined') { * @method getWorldInputs * @description * Registers a callback to modify the world-space properties of each vertex in a shader. This hook can be used inside baseColorShader().modify() and similar shader modify() calls to customize vertex positions, normals, texture coordinates, and colors before rendering. "World space" refers to the coordinate system of the 3D scene, before any camera or projection transformations are applied. - * + * * The callback receives a vertex object with the following properties: * - `position`: a three-component vector representing the original position of the vertex. * - `normal`: a three-component vector representing the direction the surface is facing. * - `texCoord`: a two-component vector representing the texture coordinates. * - `color`: a four-component vector representing the color of the vertex (red, green, blue, alpha). - * + * * This hook is available in: * - baseMaterialShader() * - baseNormalShader() @@ -515,7 +521,7 @@ if (typeof p5 !== 'undefined') { * @method getObjectInputs * @description * Registers a callback to modify the properties of each vertex before any transformations are applied in the vertex shader. This hook can be used inside baseColorShader().modify() and similar shader modify() calls to move, color, or otherwise modify the raw model data. The callback receives an object with the following properties: - * + * * - `position`: a three-component vector representing the original position of the vertex. * - `normal`: a three-component vector representing the direction the surface is facing. * - `texCoord`: a two-component vector representing the texture coordinates. @@ -562,7 +568,7 @@ if (typeof p5 !== 'undefined') { * @method getCameraInputs * @description * Registers a callback to adjust vertex properties after the model has been transformed by the camera, but before projection, in the vertex shader. This hook can be used inside baseColorShader().modify() and similar shader modify() calls to create effects that depend on the camera's view. The callback receives an object with the following properties: - * + * * - `position`: a three-component vector representing the position after camera transformation. * - `normal`: a three-component vector representing the normal after camera transformation. * - `texCoord`: a two-component vector representing the texture coordinates. @@ -605,4 +611,4 @@ if (typeof p5 !== 'undefined') { * } * * - */ \ No newline at end of file + */ From a269bd79f35ea1343febdb66fb3a3819ea8293aa Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Tue, 16 Sep 2025 21:02:13 -0400 Subject: [PATCH 66/69] Add back alias for previous uniformVector2 syntax --- src/strands/strands_api.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index 18b54a8791..0bb0ce9866 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -163,6 +163,11 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { strandsContext.uniforms.push({ name, typeInfo, defaultValue }); return createStrandsNode(id, dimension, strandsContext); }; + if (pascalTypeName.startsWith('Vec')) { + // For compatibility, also alias uniformVec2 as uniformVector2, what we initially + // documented these as + fn[`uniform${pascalTypeName.replace('Vec', 'Vector')}`] = fn[`uniform${pascalTypeName}`]; + } const originalp5Fn = fn[typeInfo.fnName]; fn[typeInfo.fnName] = function(...args) { From 18eb43c2099d7d170081a05e842545e0251d9b29 Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Thu, 18 Sep 2025 11:58:35 +0100 Subject: [PATCH 67/69] add comments for clarity on swizzling and onrebind --- src/strands/ir_builders.js | 24 +++++++++++++++++++----- src/strands/strands_codegen.js | 1 + 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/strands/ir_builders.js b/src/strands/ir_builders.js index 420cd44380..5245896c8a 100644 --- a/src/strands/ir_builders.js +++ b/src/strands/ir_builders.js @@ -465,10 +465,10 @@ export function swizzleTrap(id, dimension, strandsContext, onRebind) { } }, set(target, property, value, receiver) { - for (const basis of swizzleSets) { + for (const swizzleSet of swizzleSets) { const chars = [...property]; const valid = - chars.every(c => basis.includes(c)) && + chars.every(c => swizzleSet.includes(c)) && new Set(chars).size === chars.length && target.dimension >= chars.length; @@ -476,23 +476,29 @@ export function swizzleTrap(id, dimension, strandsContext, onRebind) { const dim = target.dimension; + // 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); for (let i = 0; i < dim; i++) { const { id, dimension } = swizzleNode(strandsContext, target, 'xyzw'[i]); lanes[i] = createStrandsNode(id, dimension, strandsContext); } + // The scalars array contains the individual components of the users values. + // This may not be the most efficient way, as we swizzle each component individually, + // so that .xyz becomes .x, .y, .z let scalars = []; if (value instanceof StrandsNode) { if (value.dimension === 1) { scalars = Array(chars.length).fill(value); - } else if (value.dimension >= chars.length) { + } else if (value.dimension === chars.length) { for (let k = 0; k < chars.length; k++) { const { id, dimension } = swizzleNode(strandsContext, value, 'xyzw'[k]); scalars.push(createStrandsNode(id, dimension, strandsContext)); } } else { - FES.userError('type error', `Swizzle assignment: RHS vector too short (need ${chars.length}, got ${value.dimension}).`); + FES.userError('type error', `Swizzle assignment: RHS vector does not match LHS vector (need ${chars.length}, got ${value.dimension}).`); } } else if (Array.isArray(value)) { const flat = value.flat(Infinity); @@ -509,8 +515,11 @@ export function swizzleTrap(id, dimension, strandsContext, onRebind) { FES.userError('type error', `Unsupported RHS for swizzle assignment: ${value}`); } + // The canonical index refers to the actual value's position in the vector lanes + // 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 = basis.indexOf(chars[j]); + const canonicalIndex = swizzleSet.indexOf(chars[j]); lanes[canonicalIndex] = scalars[j]; } @@ -523,6 +532,11 @@ export function swizzleTrap(id, dimension, strandsContext, onRebind) { ); target.id = newID; + + // 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, + // and make a new ID for the struct with these new values if (typeof onRebind === 'function') { onRebind(newID); } diff --git a/src/strands/strands_codegen.js b/src/strands/strands_codegen.js index 3569079289..e210c56bee 100644 --- a/src/strands/strands_codegen.js +++ b/src/strands/strands_codegen.js @@ -32,6 +32,7 @@ export function generateShaderCode(strandsContext) { const firstLine = backend.hookEntry(hookType); backend.generateReturnStatement(strandsContext, generationContext, rootNodeID); hooksObj[`${hookType.returnType.typeName} ${hookType.name}`] = [firstLine, ...generationContext.codeLines, '}'].join('\n'); + console.log(hooksObj[`${hookType.returnType.typeName} ${hookType.name}`]); } return hooksObj; From e81920f835cf8943ddf1ce9760bd4893dab321f3 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Thu, 18 Sep 2025 10:57:51 -0400 Subject: [PATCH 68/69] Parse hookTypes into a strands codegen type --- src/strands/ir_types.js | 41 +++++------------------------- src/strands/strands_codegen.js | 6 ++++- src/strands/strands_glslBackend.js | 34 ++++++++++++------------- 3 files changed, 29 insertions(+), 52 deletions(-) diff --git a/src/strands/ir_types.js b/src/strands/ir_types.js index aa86fcd129..67829d6b03 100644 --- a/src/strands/ir_types.js +++ b/src/strands/ir_types.js @@ -67,13 +67,13 @@ export const DataType = { sampler2D: { fnName: "texture", baseType: BaseType.SAMPLER2D, dimension: 1, priority: -10 }, } export const structType = function (hookType) { - // const hookType = hookType?.name ? hookType - let T = hookType.type === undefined ? hookType : hookType.type; - const structType = { + let T = hookType.type === undefined ? hookType : hookType.type; + const structType = { name: hookType.name, properties: [], typeName: T.typeName, }; + // TODO: handle struct properties that are themselves structs for (const prop of T.properties) { const propType = TypeInfoFromGLSLName[prop.type.typeName]; structType.properties.push( @@ -83,35 +83,8 @@ export const structType = function (hookType) { return structType; }; -export const StructType = { - Vertex: { - name: 'Vertex', - properties: [ - { name: "position", dataType: DataType.float3 }, - { name: "normal", dataType: DataType.float3 }, - { name: "texCoord", dataType: DataType.float2 }, - { name: "color", dataType: DataType.float4 }, - ] - }, - StrokeVertex: { - name: 'StrokeVertex', - properties: [ - { name: "position", dataType: DataType.float3 }, - { name: "tangentIn", dataType: DataType.float3 }, - { name: "tangentOut", dataType: DataType.float3 }, - { name: "color", dataType: DataType.float4 }, - { name: "weight", dataType: DataType.float1 }, - ] - }, - FitlerInputs: { - - }, -} - export function isStructType(typeName) { - const cap = typeName.charAt(0).toUpperCase() - return cap === typeName.charAt(0); - // return Object.keys(StructType).includes(typeName); + return !isNativeType(typeName); } export function isNativeType(typeName) { @@ -153,8 +126,8 @@ export const OpCode = { }, Unary: { LOGICAL_NOT: 100, - NEGATE: 101, - PLUS: 102, + NEGATE: 101, + PLUS: 102, SWIZZLE: 103, }, Nary: { @@ -235,4 +208,4 @@ export const BlockType = { export const BlockTypeToName = Object.fromEntries( Object.entries(BlockType).map(([key, val]) => [val, key]) -); \ No newline at end of file +); diff --git a/src/strands/strands_codegen.js b/src/strands/strands_codegen.js index ab5253818d..911ab8376b 100644 --- a/src/strands/strands_codegen.js +++ b/src/strands/strands_codegen.js @@ -1,4 +1,5 @@ import { sortCFG } from "./ir_cfg"; +import { structType, TypeInfoFromGLSLName } from './ir_types'; export function generateShaderCode(strandsContext) { const { @@ -35,7 +36,10 @@ export function generateShaderCode(strandsContext) { } const firstLine = backend.hookEntry(hookType); - backend.generateReturnStatement(strandsContext, generationContext, rootNodeID); + let returnType = hookType.returnType.properties + ? structType(hookType.returnType) + : 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}`]); } diff --git a/src/strands/strands_glslBackend.js b/src/strands/strands_glslBackend.js index c009f807a6..70552bad1b 100644 --- a/src/strands/strands_glslBackend.js +++ b/src/strands/strands_glslBackend.js @@ -1,4 +1,4 @@ -import { NodeType, OpCodeToSymbol, BlockType, OpCode, NodeTypeToName, isStructType, StructType, BaseType } from "./ir_types"; +import { NodeType, OpCodeToSymbol, BlockType, OpCode, NodeTypeToName, isStructType, BaseType } from "./ir_types"; import { getNodeDataFromID, extractNodeTypeInfo } from "./ir_dag"; import * as FES from './strands_FES' @@ -44,7 +44,7 @@ const cfgHandlers = { } } }, - + [BlockType.IF_COND](blockID, strandsContext, generationContext) { const { dag, cfg } = strandsContext; const conditionID = cfg.blockConditions[blockID]; @@ -56,23 +56,23 @@ const cfgHandlers = { generationContext.write(`}`) return; }, - + [BlockType.IF_BODY](blockID, strandsContext, generationContext) { - + }, - + [BlockType.ELIF_BODY](blockID, strandsContext, generationContext) { - + }, - + [BlockType.ELSE_BODY](blockID, strandsContext, generationContext) { - + }, - + [BlockType.MERGE](blockID, strandsContext, generationContext) { this[BlockType.DEFAULT](blockID, strandsContext, generationContext); }, - + [BlockType.FUNCTION](blockID, strandsContext, generationContext) { this[BlockType.DEFAULT](blockID, strandsContext, generationContext); }, @@ -110,27 +110,27 @@ export const glslBackend = { 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) { + generateReturnStatement(strandsContext, generationContext, rootNodeID, returnType) { const dag = strandsContext.dag; const rootNode = getNodeDataFromID(dag, rootNodeID); if (isStructType(rootNode.baseType)) { - const structTypeInfo = StructType[rootNode.baseType]; + const structTypeInfo = returnType; for (let i = 0; i < structTypeInfo.properties.length; i++) { const prop = structTypeInfo.properties[i]; const val = this.generateExpression(generationContext, dag, rootNode.dependsOn[i]); if (prop.name !== val) { generationContext.write( `${rootNode.identifier}.${prop.name} = ${val};` - ) + ) } } - } + } generationContext.write(`return ${this.generateExpression(generationContext, dag, rootNodeID)};`); }, @@ -143,14 +143,14 @@ export const glslBackend = { case NodeType.LITERAL: if (node.baseType === BaseType.FLOAT) { return node.value.toFixed(4); - } + } 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) { From ecc80612181d8b494137f23668234b21ce26d929 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Thu, 18 Sep 2025 11:40:15 -0400 Subject: [PATCH 69/69] Merge atan test file into trigonometry tests and get it working --- test/unit/math/atan.js | 33 --------------------------------- test/unit/math/trigonometry.js | 22 +++++++++++----------- 2 files changed, 11 insertions(+), 44 deletions(-) delete mode 100644 test/unit/math/atan.js diff --git a/test/unit/math/atan.js b/test/unit/math/atan.js deleted file mode 100644 index 19a14e4664..0000000000 --- a/test/unit/math/atan.js +++ /dev/null @@ -1,33 +0,0 @@ -import trigonometry from '../../../src/math/trigonometry.js'; -import { assert } from 'chai'; - -suite('atan', function() { - const mockP5 = { - RADIANS: 'radians', - DEGREES: 'degrees', - _validateParameters: () => {} - }; - const mockP5Prototype = {}; - - beforeEach(function() { - mockP5Prototype._angleMode = mockP5.RADIANS; - mockP5Prototype.angleMode = function(mode) { - this._angleMode = mode; - }; - trigonometry(mockP5, mockP5Prototype); - }); - - test('should return the correct value for atan(0.5) in radians', function() { - mockP5Prototype.angleMode(mockP5.RADIANS); - const expected = 0.4636476090008061; // pre-calculated value - const actual = mockP5Prototype.atan(0.5); - assert.closeTo(actual, expected, 1e-10); - }); - - test('should return the correct value for atan(0.5) in degrees', function() { - mockP5Prototype.angleMode(mockP5.DEGREES); - const expected = 26.56505117707799; // pre-calculated value - const actual = mockP5Prototype.atan(0.5); - assert.closeTo(actual, expected, 1e-10); - }); -}); diff --git a/test/unit/math/trigonometry.js b/test/unit/math/trigonometry.js index b8c2c86e2a..62b8bbd532 100644 --- a/test/unit/math/trigonometry.js +++ b/test/unit/math/trigonometry.js @@ -7,11 +7,11 @@ suite('Trigonometry', function() { let y = 1; let ratio = 0.5; - const mockP5 = { - _validateParameters: vi.fn() - }; const mockP5Prototype = { }; + const mockP5 = Object.create(mockP5Prototype, { + _validateParameters: vi.fn() + }); beforeEach(async function() { trigonometry(mockP5, mockP5Prototype); @@ -47,7 +47,7 @@ suite('Trigonometry', function() { }); }; - suite.todo('p5.prototype.angleMode', function() { + suite('p5.prototype.angleMode', function() { test('should set constant to DEGREES', function() { mockP5Prototype.angleMode(mockP5.DEGREES); assert.equal(mockP5Prototype.angleMode(), mockP5.DEGREES); @@ -102,31 +102,31 @@ suite('Trigonometry', function() { }); }); - suite.todo('p5.prototype.asin', function() { + suite('p5.prototype.asin', function() { ahandleDegreesAndRadians('asin'); }); - suite.todo('p5.prototype.atan', function() { + suite('p5.prototype.atan', function() { ahandleDegreesAndRadians('atan'); }); - suite.todo('p5.prototype.acos', function() { + suite('p5.prototype.acos', function() { ahandleDegreesAndRadians('acos'); }); - suite.todo('p5.prototype.sin', function() { + suite('p5.prototype.sin', function() { handleDegreesAndRadians('sin'); }); - suite.todo('p5.prototype.cos', function() { + suite('p5.prototype.cos', function() { handleDegreesAndRadians('cos'); }); - suite.todo('p5.prototype.tan', function() { + suite('p5.prototype.tan', function() { handleDegreesAndRadians('tan'); }); - suite.todo('p5.prototype.atan2', function() { + suite('p5.prototype.atan2', function() { test('should handle degrees', function() { mockP5Prototype.angleMode(mockP5.DEGREES); assert.equal(