Skill: tebjan/vvvv-custom-nodes (version 1.0.0)

--- name: vvvv-custom-nodes description: "Helps write C# node classes for vvvv gamma — the [ProcessNode] pattern, Update() method, out parameters, pin configuration, change detection, stateless operation nodes, and service consumption via NodeContext (IFrameClock, Game access, logging). Use when writing a node class, adding pins, implementing change detection, accessing services in node constructors, or creating stateless utility methods. Requires [assembly: ImportAsIs]." license: CC-BY-SA-4.0 compatibility: Designed for coding AI agents assisting with vvvv gamma development metadata: author: Tebjan Halm version: "1.0" --- # Writing Custom Nodes for vvvv gamma ## ProcessNode Pattern — The Core Pattern Every stateful C# node in vvvv gamma uses `[ProcessNode]`: ```csharp [ProcessNode] public class MyTransform : IDisposable { private float _lastInput; private float _cachedResult; /// <summary> /// Transforms input values with caching. /// </summary> public void Update( out float result, // OUT parameters FIRST out string error, // More out params float input = 0f, // Value inputs with defaults AFTER bool reset = false) { error = null; if (input != _lastInput || reset) { _cachedResult = ExpensiveComputation(input); _lastInput = input; } result = _cachedResult; // ALWAYS output cached data } public void Dispose() { /* cleanup */ } } ``` **Prerequisite**: `[ProcessNode]` only works if `[assembly: ImportAsIs]` is set on the assembly. Projects created by vvvv include this automatically. For library-level `ImportAsIs` with Namespace/Category parameters, see vvvv-node-libraries. ### Non-Negotiable Rules 1. **`[ProcessNode]` attribute** on every stateful node class 2. **No "Node" in the vvvv-visible name** — everything in vvvv is already a node, so "Node" suffix is redundant 3. **`out` parameters FIRST**, value inputs with defaults AFTER 4. **XML comments** on class and Update method (shown as tooltip in vvvv) 5. **ZERO allocations in Update** — no `new`, no LINQ, cache everything 6. **Change detection** — only recompute when inputs actually change 7. **Always output latest data** — even when no work is done, output cached result 8. **No unnecessary public members** — data flows through Update in/out params only 9. **`IDisposable`** for any node holding native/unmanaged resources ### Live Reload Behavior When the .vl document references a .csproj source project, vvvv compiles C# via Roslyn at runtime. On .cs file save, vvvv recompiles and restarts affected nodes: 1. `Dispose()` is called on the current instance (if `IDisposable`) 2. The new constructor runs with a fresh `NodeContext` 3. `Update()` resumes on the next frame Implications for node authors: - **Instance fields reset** — any state accumulated during runtime (caches, counters, connections) is lost on code change. This is expected. - **Static fields also reset** — the entire in-memory assembly is replaced. Do not rely on static state to survive edits. - **Dispose must be thorough** — native handles, network connections, and GPU resources must be released. Leaks accumulate across reloads during development. - **Constructor must be fast** — it runs each time you save. Defer heavy initialization to the first `Update()` call using a `_needsInit` flag. - The rules about caching and change detection above exist partly because of this: your code runs in a program that never stops. Allocations in `Update()` cause GC pressure in a 60 FPS loop that may run for hours. ### Class Naming vs Node Name The rule is: **users must never see "Node" in vvvv's node browser**. How you achieve this: ```csharp // Simple: class name IS the node name — no suffix needed [ProcessNode] public class Wander { } // vvvv shows: "Wander" // Class has "Node" suffix + Name property strips it — also fine [ProcessNode(Name = "Scan")] public class ScanNode { } // vvvv shows: "Scan" // Completely different internal name — fine when class manages another type [ProcessNode(Name = "MeshRenderer", Category = "Stride.Rendering.Custom")] public class CustomMeshRenderer { } // vvvv shows: "MeshRenderer" ``` ### HasStateOutput — Exposing Node Instance ```csharp [ProcessNode(HasStateOutput = true)] public class ParticleSystem { // The node itself becomes an output pin, // allowing downstream nodes to call methods on it public void Update(out int particleCount, ...) { ... } } ``` Alternative: return `this` from a method to expose the instance. ### Pin Visibility ```csharp public void Update( out Spread<float> result, [Pin(Visibility = PinVisibility.OnlyInspector)] out string error, float input = 0f, [Pin(Visibility = PinVisibility.Optional)] bool advanced = false) { // PinVisibility values: // Visible — always shown (default) // Optional — user can show/hide // Hidden — not visible, only via inspector // OnlyInspector — only in inspector panel (use for debug/error outputs) } ``` ### Pin Groups (Collection Inputs) For Spread inputs with add/remove buttons in vvvv: ```csharp public void Update( out float result, [Pin(Name = "Input", PinGroupKind = PinGroupKind.Collection, PinGroupDefaultCount = 2)] Spread<IRenderer?> input) { } ``` ### DefaultValue for Complex Types For defaults that cannot be C# literal expressions: ```csharp public void Update( [DefaultValue(typeof(Color4), "0.1, 0.1, 0.15, 1.0")] Color4 clearColor, [DefaultValue(typeof(Int2), "1920, 1080")] Int2 size, bool clear = true) { } ``` ## Constructor Patterns **Simple node** (no special context): ```csharp public MyNode() { } ``` **Node needing NodeContext** (for AppHost, services, Fuse shader graphs): ```csharp public MyNode(NodeContext nodeContext) { _nodeContext = nodeContext; // Access: nodeContext.AppHost.IsExported, nodeContext.AppHost.Services, etc. } ``` For IFrameClock injection, Stride Game access, logging, and service consumption patterns, see [services.md](services.md). ## Change Detection Patterns ### Simple — Direct Field Comparison ```csharp private float _lastParam; private Result _cached; public void Update(out Result result, float param = 0f) { if (param != _lastParam) { _cached = Compute(param); _lastParam = param; } result = _cached; } ``` ### Multi-Input — Hash Check ```csharp private int _lastHash; private Config _cached; public void Update(out Config config, float a = 0f, int b = 0, string c = "") { int hash = HashCode.Combine(a, b, c); if (hash != _lastHash) { _cached = new Config(a, b, c); _lastHash = hash; } config = _cached; } ``` ### Reference Types — Identity Check ```csharp if (!ReferenceEquals(newBuffer, _lastBuffer)) { ProcessBuffer(newBuffer); _lastBuffer = newBuffer; } ``` ### Rebuild Flag — For Pipeline/Effect State When multiple setters can invalidate state: ```csharp private bool _needsRebuild = true; public void SetShader(ShaderStage vs) { _shader = vs; _needsRebuild = true; } public void Update(out Effect effect) { if (_needsRebuild) { RebuildPipeline(); _needsRebuild = false; } effect = _cachedEffect; } ``` ### Quick Reference | Input Type | Comparison | Notes | |---|---|---| | Value types (int, float, bool) | `!=` | Direct comparison | | Reference types (objects) | `ReferenceEquals()` | Identity, not equality | | Multiple inputs | `HashCode.Combine()` | Single hash for dirty check | | Collections | Length + sample elements | Full comparison too expensive | | Multiple setters | `_needsRebuild` flag | Set flag in setters, check in Update | ## Return-Based Output (Single Output) When a node has a single primary output, you can return it directly instead of using `out`: ```csharp [ProcessNode] public class NoiseSteering { private SteeringConfig? _cached; public ISteering Update( float strength = 2.0f, float noiseFrequency = 0.05f, int priority = 0) { if (_cached is null || _cached.Strength != strength || _cached.NoiseFrequency != noiseFrequency || _cached.Priority != priority) { _cached = new SteeringConfig(strength, noiseFrequency, priority); } return _cached; } } ``` Mix return + `out` when you have one primary output plus additional outputs: ```csharp public ReadOnlySpan<ParticleState> Update( SimulationConfig config, float deltaTime, out TimingStats stats) // Secondary output via out { // Returns primary output, secondary via out } ``` ## Rising Edge Detection (Bang/Trigger) For boolean inputs that should trigger once (not every frame they're true): ```csharp private bool _lastTrigger; public void Update(out bool triggered, bool trigger = false) { triggered = trigger && !_lastTrigger; // Rising edge only _lastTrigger = trigger; } ``` ## Operation Nodes vvvv auto-generates nodes from **all public C# methods** — no attribute needed. Don't create `[ProcessNode]` wrappers for simple methods that just forward calls. Struct `Split()` methods also become nodes automatically. Static methods auto-generate nodes — no `[ProcessNode]` needed: ```csharp public static class MathOps { public static float Remap(float value, float inMin = 0f, float inMax = 1f, float outMin = 0f, float outMax = 1f) { float t = (value - inMin) / (inMax - inMin); return outMin + t * (outMax - outMin); } } ``` ### When to Use Which | Pattern | Use When | |---|---| | `[ProcessNode]` class | Manages state between frames, caching, dirty-checking | | Static method | Pure function, no state, transforms input to output | | Struct + `Split()` | Data containers that unpack into separate pins | ## Memory & Performance in Update Loop - **No `new` keyword** in hot paths - **No LINQ** (`.Where`, `.Select`, `.ToList`) — hidden allocations via enumerators - **Cache collections** — pre-allocate, reuse arrays/lists - **No string concatenation** — use `StringBuilder` if needed, but avoid in hot path - **Vector types**: `System.Numerics` internally for SIMD, `Stride.Core.Mathematics` at API boundaries - **Zero-cost conversion**: `Unsafe.As<StrideVector3, NumericsVector3>(ref val)` For custom regions (delegate-based, ICustomRegion, IRegion\<TInlay\>), see [regions.md](regions.md). For advanced patterns (FragmentSelection, Smell, Dynamic Enums, Settings/Split, Pin Name Derivation), see [advanced.md](advanced.md). For service consumption (IFrameClock, Game, Logging), see [services.md](services.md). For working with public channels from C# nodes, see vvvv-channels. For code examples, see [examples.md](examples.md). For starter templates, see [templates/](templates/).

path: advanced.md
content: # Advanced Node Patterns

## Contents
- FragmentSelection (explicit control over node operations)
- Smell Attribute (internal/experimental nodes)
- Dynamic Enums (runtime-updating dropdowns)
- Settings / Split() Pattern (JSON + vvvv pin control)
- Pin Name Derivation (camelCase to pin names)
- Node Browser Categories via ImportType

## FragmentSelection (Advanced Nodes)

For complex nodes where you need explicit control over which methods become node operations:

```csharp
[ProcessNode(Name = "RenderWindow", Category = "Stride.Rendering",
             FragmentSelection = FragmentSelection.Explicit)]
public sealed class RenderWindow : IDisposable
{
    [Fragment]
    public RenderWindow(NodeContext nodeContext) { }

    [Fragment]
    public void SomeMethod(out RectangleF clientBounds, ...) { }
}
```

Only methods marked `[Fragment]` are exposed. Without `FragmentSelection.Explicit`, all public methods become node operations.

## Smell Attribute (Internal/Experimental Nodes)

```csharp
[Smell(SymbolSmell.Internal)]       // Hidden from public node browser
[Smell(SymbolSmell.Experimental)]   // Shown with experimental warning
```

## Dynamic Enums

Runtime-updating dropdowns for vvvv:

```csharp
public class MyEnumDefinition : DynamicEnumDefinitionBase<MyEnumDefinition>
{
    protected override IReadOnlyDictionary<string, object> GetEntries() { ... }
}

public class MyEnum : DynamicEnumBase<MyEnum, MyEnumDefinition>
{
    public MyEnum(string value) : base(value) { }
}
```

Trigger updates via `SetEntries()` which calls `trigger.OnNext("")`.

The definition is a singleton — only one instance exists globally, ensuring all nodes share identical entries.

## Settings / Split() Pattern

When a settings class needs JSON serialization but should NOT auto-expose properties as vvvv pins:

```csharp
public class EffectSettings
{
    // Internal + [JsonInclude] = JSON works, but vvvv does NOT create pins
    [JsonInclude] internal Vector3 offset = new(0, 0, 0);
    [JsonInclude] internal float intensity = 0f;

    // Split() IS the vvvv-visible API — becomes a Split node
    public void Split(out Vector3 offset, out float intensity)
    {
        offset = this.offset;
        intensity = this.intensity;
    }
}
```

Public properties on classes get auto-exposed as pins by vvvv. Use `internal` + `[JsonInclude]` to prevent this.

## Pin Name Derivation

Parameter names are auto-split from camelCase into vvvv pin names:
- `settings` → pin "Settings"
- `settingsJson` → pin "Settings Json"
- `customInstanceId` → pin "Custom Instance Id"

This matters for `SetPinValue()` calls — the name must match exactly.

## Node Browser Categories via ImportType

Control where your node appears in the node browser:

```csharp
[assembly: ImportType(typeof(MyProcessor), Name = "MyProcessor", Category = "My.Category")]
```

Category uses dot notation: `IO.Ports`, `Stride.Textures.Source`, etc.

For library-level `ImportAsIs` namespace/category configuration, see vvvv-node-libraries.


path: regions.md
content: # Custom Regions

Regions are "nodes with a hole" — blocks where users define inner logic via a callback mechanism. Three API flavors exist.

## Flavor 1: Delegate-Based Regions

Simplest approach. Use `Func<>`, `Action<>`, or custom delegates.

**Stateless** — single delegate:
```csharp
public static float MyRegion(float input, Func<float, float> body)
{
    return body(input);
}
```

**Stateful** — two delegates (create + update):
```csharp
public static void MyStatefulRegion<TState>(
    Func<TState> create,
    Action<TState> update)
{
    var state = create();
    update(state);
}
```

Custom delegate types enable named pins and multiple outputs:
```csharp
public delegate void MyRegionBody(float x, float y, out float result);
```

## Flavor 2: ICustomRegion API (since vvvv 4.0)

Define a Process with an `ICustomRegion` input pin on `Update`.

Key interface members:
- `Input` / `Outputs` — static reflection of border control points (BCPs)
- `InputValues` / `SetOutputValues` — read/write BCP data
- `CreateRegionPatch` — instantiate a user patch, returns `ICustomRegionPatch`

Use the `CustomRegionPatch` helper node to instantiate one patch per frame, handle update, and auto-dispose on shutdown.

### Configuration Options

| Option | Values | Purpose |
|---|---|---|
| `Node Or Region` | Node, Region, Both | How it appears in node browser |
| `Supported Control Points` | None, Border, Accumulator, Splicer | What border shapes users can add |
| `Control Point Type Constraint` | (type) | Restrict allowed types on control points |

### Control Point Shapes

| Shape | Name | Behavior |
|---|---|---|
| Rectangle | **Border** | Data passes through |
| Diamond | **Accumulator** | Output = input if region doesn't execute |
| Triangle | **Splicer** | Split/join semantics |

Limitation: cannot define multiple control point kinds simultaneously on one region.

## Flavor 3: IRegion\<TInlay\> API (since vvvv 7.0)

Most generalized. Define the inner patch shape via an interface.

Required operations on the region:
- `Update`
- `SetPatchInlayFactory`
- `AcknowledgeInput` / `RetrieveInput`
- `AcknowledgeOutput` / `RetrieveOutput`

Example: `IfElse` region with `IIfElsePatch` interface defining `Then` and `Else` operations.

Reference implementation: `VL.StandardLibs/VL.TestNodes/src/IfElseRegion.cs`

Limitations:
- Inlay interface must contain an `Update` operation
- Inlay interface must not inherit from other interfaces
- Generics on inlay interfaces are untested

## When to Use Which

| Flavor | Use When |
|---|---|
| Delegate-based | Simple callbacks, no border control points needed |
| `ICustomRegion` | Need border controls (Border, Accumulator, Splicer) |
| `IRegion<TInlay>` | Need custom patch shapes with multiple operations |


path: templates/stateful-node.cs
content: using VL.Core;
using VL.Core.Import;

/// <summary>
/// TODO: Describe what this node does (shown as tooltip in vvvv).
/// </summary>
[ProcessNode]
public class MyStatefulNode : IDisposable
{
    // Track previous inputs for change detection
    private float _lastParam1;
    private int _lastParam2;

    // Cached output — always output this, even when no work is done
    private float _cachedResult;

    /// <param name="nodeContext">Provided by the VL runtime</param>
    /// <param name="initialValue">The starting value for the computation</param>
    public MyStatefulNode(NodeContext nodeContext, float initialValue = 0f)
    {
        _cachedResult = initialValue;
    }

    /// <summary>
    /// Called every frame (~60 FPS). Keep fast — zero allocations.
    /// </summary>
    /// <param name="result">The computed result</param>
    /// <param name="error">Error message if something went wrong, null otherwise</param>
    /// <param name="param1">First input value</param>
    /// <param name="param2">Second input value</param>
    public void Update(
        out float result,
        out string error,
        float param1 = 0f,
        int param2 = 0)
    {
        error = null;

        // Change detection — only recompute when inputs change
        if (param1 != _lastParam1 || param2 != _lastParam2)
        {
            _cachedResult = DoWork(param1, param2);
            _lastParam1 = param1;
            _lastParam2 = param2;
        }

        // ALWAYS output cached data
        result = _cachedResult;
    }

    private float DoWork(float a, int b)
    {
        // Expensive computation goes here
        return a * b;
    }

    public void Dispose()
    {
        // Clean up native/unmanaged resources
    }
}


path: templates/stateless-node.cs
content: using VL.Lib.Collections;

/// <summary>
/// TODO: Describe what these operations do.
/// Static methods auto-generate nodes — no [ProcessNode] needed.
/// </summary>
public static class MyOperations
{
    /// <summary>
    /// Remaps a value from one range to another.
    /// </summary>
    public static float Remap(float value, float inMin = 0f, float inMax = 1f,
                              float outMin = 0f, float outMax = 1f)
    {
        float t = (value - inMin) / (inMax - inMin);
        return outMin + t * (outMax - outMin);
    }

    /// <summary>
    /// Clamps a value between min and max.
    /// </summary>
    public static float Clamp(float value, float min = 0f, float max = 1f)
    {
        return value < min ? min : value > max ? max : value;
    }
}


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector:  ()