This document describes the established code patterns used throughout VL.Fuse. Follow these patterns when contributing to maintain consistency.
All shader nodes receive NodeContext as the first constructor parameter:
// Correct
public MyShaderNode(NodeContext nodeContext, ShaderNode<float> input)
: base(nodeContext, "MyShader")
{
SetInputs(new[] { input });
}
// Incorrect - missing NodeContext
public MyShaderNode(ShaderNode<float> input) // Don't do thisNodes generate code via overridable template methods:
public class MyOperation<T> : ResultNode<T>
{
// Return the template with ${placeholders}
protected override string ImplementationTemplate()
{
return "${a} * ${b}";
}
// Provide values for placeholders
protected override Dictionary<string, string> CreateTemplateMap()
{
var map = base.CreateTemplateMap();
map["a"] = Ins[0].ID;
map["b"] = Ins[1].ID;
return map;
}
}Use visitors to analyze or transform the node graph:
// Define a visitor
public class MyVisitor : IShaderNodeVisitor
{
public List<string> Results = new();
public void Visit(AbstractShaderNode node, int recursionLevel)
{
if (node is MyNodeType myNode)
{
Results.Add(myNode.SomeProperty);
}
}
}
// Use the visitor
var visitor = new MyVisitor();
rootNode.PreOrderVisit(visitor);
var results = visitor.Results;Register dependencies using the Property system:
public class MyBufferNode : ShaderNode<float>
{
public MyBufferNode(NodeContext nodeContext, Buffer buffer)
: base(nodeContext, "MyBuffer")
{
// Register as an input dependency
AddProperty(Inputs, new BufferInput(buffer));
// Register required mixin
AddProperty(Mixins, "FuseCommonBuffer");
}
}Property IDs (defined in AbstractShaderNode):
Mixins- Shader mixins to includeInputs- GPU parametersCompositions- Shader compositionsDeclarations- Field declarationsStructs- Struct definitionsStreams- Stream definitions
Mark methods accessed via VL reflection:
// ReSharper disable once UnusedMember.Global
// accessed from vl
public static ToShaderFX<T> RegisterShaderFX<T>(ShaderNode<T> theGpuValue)
{
return new ToShaderFX<T>(theGpuValue);
}
// USED BY VL - do not remove
public string TypeOverride { get; set; }Use generics for type-safe shader operations:
public class Multiply<T> : ResultNode<T> where T : struct
{
public Multiply(NodeContext nodeContext, ShaderNode<T> a, ShaderNode<T> b)
: base(nodeContext, "Multiply")
{
SetInputs(new AbstractShaderNode[] { a, b });
}
protected override string ImplementationTemplate()
{
return "${a} * ${b}";
}
}Configure how null inputs are treated:
public class MyNode : ShaderNode<float>
{
public MyNode(NodeContext nodeContext)
: base(nodeContext, "MyNode")
{
// Options:
NullInputMode = HandleNullInputMode.SetOff; // Skip node if null (default)
NullInputMode = HandleNullInputMode.ReplaceWithDefault; // Use Default value
NullInputMode = HandleNullInputMode.Remove; // Remove from Ins list
}
}| Element | Convention | Example |
|---|---|---|
| Classes | PascalCase | ShaderNode, BufferInput |
| Public methods | PascalCase | BuildSource(), TypeName() |
| Public properties | PascalCase | HashCode, SourceCode |
| Private fields | _camelCase | _hasNullInput, _contexts |
| Parameters | camelCase | nodeContext, theInput |
| Constants | PascalCase | Mixins, Declarations |
| Generic types | Single letter | T, TNode, TProperty |
Node IDs follow the pattern: Name_HashCode
public override string ID => Name + "_" + HashCode;
// Result: "Multiply_123456789"| Element | Convention | Example |
|---|---|---|
| Shader name | PascalCase | FuseCommonBuffer |
| Functions | camelCase | sbLoad(), sdRoundBox() |
| Parameters | camelCase | sBuffer, defaultValue |
| File suffix | Purpose indicator | _ShaderFX, _ComputeFX |
shader MyShader : FuseCommonTypes, FuseCommonBuffer
{
// Inherits from base shaders
}Use type suffixes when Stride can't differentiate:
// Can't overload on StructuredBuffer<T> type alone
uint sbSize(StructuredBuffer<float> sBuffer) { ... }
uint sbSize2(StructuredBuffer<float2> sBuffer) { ... }
uint sbSize3(StructuredBuffer<float3> sBuffer) { ... }Always check buffer size before access:
float sbLoad(uint index, StructuredBuffer<float> sBuffer, float defaultValue = 0.)
{
float value = defaultValue;
uint count = sbSize(sBuffer);
if (count > 0) value = sBuffer[index % count];
return value;
}Organize shader code with clear section headers:
////////////////////////////////////////////////////////////////
//
// Section Name
//
////////////////////////////////////////////////////////////////using System.Collections.Generic;
using VL.Core;
namespace Fuse;
/// <summary>
/// Brief description of what this node does.
/// </summary>
public class MyNewNode<T> : ResultNode<T> where T : struct
{
public MyNewNode(NodeContext nodeContext, ShaderNode<T> input)
: base(nodeContext, "MyNewNode")
{
SetInputs(new AbstractShaderNode[] { input });
}
protected override string ImplementationTemplate()
{
return "myFunction(${arguments})";
}
protected override Dictionary<string, string> CreateTemplateMap()
{
var map = base.CreateTemplateMap();
// Add custom mappings
return map;
}
}// File: vl/shaders/MyFeature.sdsl
shader MyFeature : FuseCommonTypes
{
////////////////////////////////////////////////////////////////
//
// Feature Description
//
////////////////////////////////////////////////////////////////
// @purpose: Brief description
// @param paramName: Description of parameter
// @returns: Description of return value
float myFunction(float param)
{
return param * 2.0;
}
};// Bad - breaks node identification
public MyNode() : base(null, "MyNode") { }// Bad - will cause conflicts
return "myVar";
// Good - use unique ID
return ID; // "MyNode_123456"// Bad
Ins.Add(someNode);
// Good
SetInputs(new[] { someNode });
// or
AddInput(someNode);// Bad - crashes on null
protected override string ImplementationTemplate()
{
return $"{Ins[0].ID} + {Ins[1].ID}"; // Crashes if Ins[1] is null
}
// Good - let null handling work
// Configure NullInputMode and let the base class handle it