Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 118 additions & 5 deletions src/strands/p5.strands.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ import {
initGlobalStrandsAPI,
createShaderHooksFunctions,
} from "./strands_api";
import {
createStrandsShaderNameMap,
createStrandsShaderNameState
} from './strands_names';

function strands(p5, fn) {
// Whether or not strands callbacks should be forced to be executed in global mode.
Expand All @@ -29,14 +33,83 @@ function strands(p5, fn) {
//////////////////////////////////////////////
// Global Runtime
//////////////////////////////////////////////

/**
* @private
* @typedef {Object} StrandsContext
* @property {Object} p5 Reference to the p5 class.
* @property {Boolean} _builtinGlobalsAccessorsInstalled Whether builtin global accessors have been installed on `window`, `p5.prototype`, and `p5.Graphics.prototype` as needed.
* @property {Object} dag DAG for the current strands IR.
* @property {Object} cfg CFG for the current strands IR.
* @property {Array} uniforms Collected uniforms and their default value providers.
* @property {Object} shaderNameMap Bidirectional name map used to translate between user-facing and generated internal shader variable names.
* @property {Object} shaderNameState Numeric suffix counter used when generating internal shader names.
* @property {Set} vertexDeclarations Declarations outside the generated hook functions to prepend to vertex shader code.
* @property {Set} fragmentDeclarations Declarations outside the generated hook functions to prepend to fragment shader code.
* @property {Set} computeDeclarations Declarations outside the generated hook functions to prepend to compute shader code.
* @property {Array} hooks Collected hook IR entries to turn into shader hook source.
* @property {Object} backend Active shader backend used for code generation and backend-specific helpers.
* @property {Boolean} active Whether strands interception is currently active.
* @property {Object} renderer Renderer whose shader is being modified.
* @property {Object} baseShader Base shader being modified in the current pass.
* @property {Boolean} previousFES Previous value of `p5.disableFriendlyErrors`, restored after the pass.
* @property {Object} windowOverrides Original temporary hook targets saved from `window`.
* @property {Object} fnOverrides Original temporary hook targets saved from `p5.prototype`.
* @property {Object} graphicsOverrides Original temporary hook targets saved from `p5.Graphics.prototype`.
* @property {Number} _noiseOctaves Noise octave override captured by `noiseDetail()`.
* @property {Number} _noiseAmpFalloff Noise falloff override captured by `noiseDetail()`.
* @property {Number} _randomSeed Random seed override captured by `randomSeed()`.
* @property {Object} _builtinGlobals Cache of builtin-global uniform nodes for the current DAG.
* @property {Map} sharedVariables Shared variable metadata that tracks vertex/fragment usage to decide whether each variable becomes a local declaration or a varying.
* @property {Object} activeHook Hook currently being recorded, if any.
* @property {Boolean} _instanceIDUsedInFragment Whether fragment-stage code referenced `instanceID`, requiring it to be passed from the vertex shader to the fragment shader.
*/

/**
* Initializes the persistent strands context.
*
* Some strands context fields should persist across multiple shader `modify()` calls.
* e.g. there is no need to set p5 class reference multiple times,
* and the builtin globals accessors should only be installed once.
*
* @private
* @param {StrandsContext} ctx The strands context object.
*/
function initPersistentStrandsContext(ctx) {
ctx.p5 = p5;
ctx._builtinGlobalsAccessorsInstalled = false;
resetTransientStrandsContext(ctx);
}

function createBuiltinGlobalsCache(dag) {
return {
dag,
nodes: new Map(),
uniformsAdded: new Set()
};
}

/**
* Initializes the transient strands context for one active shader `modify()` pass.
*
* @private
* @param {StrandsContext} ctx The strands context object.
* @param {Object} backend The backend to use for shader execution.
* @param {Object} [options] Options for initializing the context.
* @param {Boolean} [options.active] Whether the context is active.
* @param {Object} [options.renderer] The renderer to use.
* @param {Object} [options.baseShader] The base shader to use.
*/
function initStrandsContext(
ctx,
backend,
{ active = false, renderer = null, baseShader = null } = {},
{ active = false, renderer = null, baseShader = null } = {}
) {
ctx.dag = createDirectedAcyclicGraph();
ctx.cfg = createControlFlowGraph();
ctx.uniforms = [];
ctx.shaderNameMap = createStrandsShaderNameMap();
ctx.shaderNameState = createStrandsShaderNameState();
ctx.vertexDeclarations = new Set();
ctx.fragmentDeclarations = new Set();
ctx.computeDeclarations = new Set();
Expand All @@ -49,23 +122,58 @@ function strands(p5, fn) {
ctx.windowOverrides = {};
ctx.fnOverrides = {};
ctx.graphicsOverrides = {};
ctx._noiseOctaves = null;
ctx._noiseAmpFalloff = null;
ctx._randomSeed = null;
ctx._builtinGlobals = createBuiltinGlobalsCache(ctx.dag);
ctx.sharedVariables = new Map();
ctx.activeHook = undefined;
ctx._instanceIDUsedInFragment = false;
if (active) {
p5.disableFriendlyErrors = true;
}
ctx.p5 = p5;
}

function deinitStrandsContext(ctx) {
/**
* Resets the transient fields of the strands context.
*
* @private
* @param {StrandsContext} ctx The strands context object.
*/
function resetTransientStrandsContext(ctx) {
ctx.dag = createDirectedAcyclicGraph();
ctx.cfg = createControlFlowGraph();
ctx.uniforms = [];
ctx.shaderNameMap = createStrandsShaderNameMap();
ctx.shaderNameState = createStrandsShaderNameState();
ctx.vertexDeclarations = new Set();
ctx.fragmentDeclarations = new Set();
ctx.computeDeclarations = new Set();
ctx.hooks = [];
ctx.backend = undefined;
ctx.active = false;
ctx.renderer = null;
ctx.baseShader = null;
ctx.previousFES = p5.disableFriendlyErrors;
ctx.windowOverrides = {};
ctx.fnOverrides = {};
ctx.graphicsOverrides = {};
ctx._noiseOctaves = null;
ctx._noiseAmpFalloff = null;
ctx._randomSeed = null;
ctx._builtinGlobals = createBuiltinGlobalsCache(ctx.dag);
ctx.sharedVariables = new Map();
ctx.activeHook = undefined;
ctx._instanceIDUsedInFragment = false;
}

/**
* Deinitializes the strands context after a shader `modify()` pass is complete.
*
* @private
* @param {StrandsContext} ctx The strands context object.
*/
function deinitStrandsContext(ctx) {
p5.disableFriendlyErrors = ctx.previousFES;
for (const key in ctx.windowOverrides) {
window[key] = ctx.windowOverrides[key];
Expand All @@ -84,10 +192,11 @@ function strands(p5, fn) {
}
}
}
resetTransientStrandsContext(ctx);
}

const strandsContext = {};
initStrandsContext(strandsContext);
initPersistentStrandsContext(strandsContext);
initGlobalStrandsAPI(p5, fn, strandsContext);

function withTempGlobalMode(pInst, callback) {
Expand Down Expand Up @@ -146,12 +255,16 @@ function strands(p5, fn) {
typeof shaderModifier === "string"
? `(${shaderModifier})`
: `(${shaderModifier.toString()})`;
strandsCallback = transpileStrandsToJS(
const transpiledStrands = transpileStrandsToJS(
p5,
sourceString,
options.srcLocations,
scope,
this.hooks.shaderNameState
);
strandsCallback = transpiledStrands.callback;
strandsContext.shaderNameMap = transpiledStrands.shaderNameMap;
strandsContext.shaderNameState = transpiledStrands.shaderNameState;
} else {
strandsCallback = shaderModifier;
}
Expand Down
120 changes: 97 additions & 23 deletions src/strands/strands_api.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ import * as DAG from './ir_dag';
import * as FES from './strands_FES'
import { getNodeDataFromID } from './ir_dag'
import { StrandsNode, createStrandsNode } from './strands_node'
import {
getOrCreateInternalShaderName,
isReservedStrandsName,
STRANDS_INTERNAL_NAME_PREFIX
} from './strands_names';

const BUILTIN_GLOBAL_SPECS = {
width: { typeInfo: DataType.float1, get: (p) => p.width },
Expand Down Expand Up @@ -212,6 +217,53 @@ function augmentFnTemporary(fn, strandsContext, name, value) {
}
}

/**
* Validates a shader variable name to ensure it does not use reserved prefixes.
*
* @private
* @param {Object} strandsContext The strands context object to use for name map lookup.
* @param {string} name The shader variable name to validate.
* @param {string} apiName The name of the API function calling this validation. Used for FES error message generation.
* @return {void} Throws an error if the name is invalid, otherwise returns silently
*/
function validateShaderName(strandsContext, name, apiName) {
if (typeof name !== 'string') return;
if (strandsContext.shaderNameMap?.internalToExternal?.[name]) return;
if (isReservedStrandsName(name)) {
FES.userError(
'parameter validation error',
`${apiName}("${name}") uses the reserved internal p5.strands prefix "${STRANDS_INTERNAL_NAME_PREFIX}". ` +
'or the reserved "gl_" prefix. Please choose a different explicit shader name.'
);
}
}

/**
* Resolves a shader variable name to its internal representation with validation.
*
* If the internal name does not exist yet, it will be created and stored
* in the shaderNameMap for future reference.
*
* @private
* @param {Object} strandsContext The strands context object to use for name map lookup.
* @param {string} name The shader variable name to resolve.
* @param {string} apiName The name of the API function calling this resolution. Used for FES error message generation.
* @return {string} The internal shader variable name.
*/
function resolveShaderName(strandsContext, name, apiName) {
if (typeof name !== 'string') return name;
if (strandsContext.shaderNameMap?.internalToExternal?.[name]) {
return name;
}

validateShaderName(strandsContext, name, apiName);
return getOrCreateInternalShaderName(
strandsContext.shaderNameMap,
strandsContext.shaderNameState,
name
);
}

//////////////////////////////////////////////
// User nodes
//////////////////////////////////////////////
Expand Down Expand Up @@ -401,9 +453,6 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) {
const originalRandomSeed = fn.randomSeed;
const originalMillis = fn.millis;

strandsContext._noiseOctaves = null;
strandsContext._noiseAmpFalloff = null;

augmentFn(fn, p5, 'noiseDetail', function (lod, falloff = 0.5) {
if (!strandsContext.active) {
return originalNoiseDetail.apply(this, arguments);
Expand Down Expand Up @@ -463,8 +512,6 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) {
return createStrandsNode(id, dimension, strandsContext);
});

strandsContext._randomSeed = null;

augmentFn(fn, p5, 'randomSeed', function (seed) {
if (!strandsContext.active) {
return originalRandomSeed.apply(this, arguments);
Expand Down Expand Up @@ -598,24 +645,41 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) {
}
}
augmentFn(fn, p5, `uniform${pascalTypeName}`, function(name, defaultValue) {
const { id, dimension } = build.variableNode(strandsContext, typeInfo, name);
strandsContext.uniforms.push({ name, typeInfo, defaultValue });
const shaderName = resolveShaderName(
strandsContext,
name,
`uniform${pascalTypeName}`
);
const { id, dimension } = build.variableNode(
strandsContext,
typeInfo,
shaderName
);
strandsContext.uniforms.push({
name: shaderName,
typeInfo,
defaultValue
});
return createStrandsNode(id, dimension, strandsContext);
});
// Shared variables with smart context detection
augmentFn(fn, p5, `shared${pascalTypeName}`, function(name) {
const { id, dimension } = build.variableNode(strandsContext, typeInfo, name);

// Initialize shared variables tracking if not present
if (!strandsContext.sharedVariables) {
strandsContext.sharedVariables = new Map();
}
const shaderName = resolveShaderName(
strandsContext,
name,
`shared${pascalTypeName}`
);
const { id, dimension } = build.variableNode(
strandsContext,
typeInfo,
shaderName
);

// Track this shared variable for smart declaration generation
strandsContext.sharedVariables.set(name, {
strandsContext.sharedVariables.set(shaderName, {
typeInfo,
usedInVertex: false,
usedInFragment: false,
usedInFragment: false
});

return createStrandsNode(id, dimension, strandsContext);
Expand Down Expand Up @@ -669,6 +733,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) {

// Storage buffer uniform function for compute shaders
fn.uniformStorage = function(name, bufferOrSchema) {
const shaderName = resolveShaderName(strandsContext, name, 'uniformStorage');
let schema = null;
let defaultValue = null;

Expand Down Expand Up @@ -696,18 +761,18 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) {
const { id, dimension } = build.variableNode(
strandsContext,
{ baseType: 'storage', dimension: 1 },
name
shaderName
);
strandsContext.uniforms.push({
name,
name: shaderName,
typeInfo: { baseType: 'storage', dimension: 1, schema },
defaultValue,
defaultValue
});

// Create StrandsNode with _originalIdentifier set (like varying variables)
// This enables proper assignment node creation and ordering preservation
const node = createStrandsNode(id, dimension, strandsContext);
node._originalIdentifier = name;
node._originalIdentifier = shaderName;
node._originalBaseType = 'storage';
node._originalDimension = 1;
node._schema = schema;
Expand Down Expand Up @@ -822,17 +887,26 @@ function enforceReturnTypeMatch(strandsContext, expectedType, returned, hookName
return returnedNodeID;
}
export function createShaderHooksFunctions(strandsContext, fn, shader) {
installBuiltinGlobalAccessors(strandsContext)
installBuiltinGlobalAccessors(strandsContext);
// shader.hooks.vertex/fragment/compute mix callable hook entries with stage
// metadata such as `declarations` carried over from earlier modify() calls.
const isHookEntry = ([name]) => name !== 'declarations';

// Add shader context to hooks before spreading
const vertexHooksWithContext = Object.fromEntries(
Object.entries(shader.hooks.vertex).map(([name, hook]) => [name, { ...hook, shaderContext: 'vertex' }])
Object.entries(shader.hooks.vertex)
.filter(isHookEntry)
.map(([name, hook]) => [name, { ...hook, shaderContext: 'vertex' }])
);
const fragmentHooksWithContext = Object.fromEntries(
Object.entries(shader.hooks.fragment).map(([name, hook]) => [name, { ...hook, shaderContext: 'fragment' }])
Object.entries(shader.hooks.fragment)
.filter(isHookEntry)
.map(([name, hook]) => [name, { ...hook, shaderContext: 'fragment' }])
);
const computeHooksWithContext = Object.fromEntries(
Object.entries(shader.hooks.compute).map(([name, hook]) => [name, { ...hook, shaderContext: 'compute' }])
Object.entries(shader.hooks.compute)
.filter(isHookEntry)
.map(([name, hook]) => [name, { ...hook, shaderContext: 'compute' }])
);

const availableHooks = {
Expand Down
2 changes: 2 additions & 0 deletions src/strands/strands_codegen.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ export function generateShaderCode(strandsContext) {
uniforms: {},
storageUniforms: {},
varyingVariables: [],
shaderNameMap: strandsContext.shaderNameMap,
shaderNameState: strandsContext.shaderNameState
};

for (const {name, typeInfo, defaultValue} of strandsContext.uniforms) {
Expand Down
Loading