From 23ff7e6b7af635a50e67a200d81a4aaf68aaaedf Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Tue, 24 Jun 2025 16:47:20 +0100 Subject: [PATCH 01/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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); }