$npx -y skills add tebjan/vvvv-skills --skill vvvv-spreadsHelps write code using vvvv gamma's Spread<T> immutable collection type and SpreadBuilder<T>. Use when working with Spreads, SpreadBuilder, collections, arrays, iteration, mapping, filtering, zipping, accumulating, or converting between Span and Spread. Trigger whenever the user
| 1 | # vvvv Spreads |
| 2 | |
| 3 | ## What Are Spreads |
| 4 | |
| 5 | `Spread<T>` is vvvv's immutable collection type, conceptually similar to `ImmutableArray<T>`. It is the primary way to pass collections between nodes. |
| 6 | |
| 7 | Key properties: |
| 8 | - **Immutable** — never modify in place, always create new spreads |
| 9 | - **Value semantics** — two spreads with same elements are considered equal |
| 10 | - **Cyclic indexing** — in visual patches, indexing wraps around (not in C# API) |
| 11 | - **Never null** — use `Spread<T>.Empty` instead of `null` |
| 12 | |
| 13 | ## Creating Spreads |
| 14 | |
| 15 | ### SpreadBuilder (Primary Method) |
| 16 | |
| 17 | ```csharp |
| 18 | var builder = new SpreadBuilder<float>(expectedCount); |
| 19 | for (int i = 0; i < count; i++) |
| 20 | builder.Add(ComputeValue(i)); |
| 21 | Spread<float> result = builder.ToSpread(); |
| 22 | ``` |
| 23 | |
| 24 | ### From Existing Data |
| 25 | |
| 26 | ```csharp |
| 27 | // From array (extension method) |
| 28 | Spread<int> fromArray = new int[] { 1, 2, 3 }.ToSpread(); |
| 29 | |
| 30 | // From array (static factory) |
| 31 | Spread<Waypoint> fromResult = Spread.Create(resultArray); |
| 32 | |
| 33 | // Empty spread (NEVER use null) |
| 34 | Spread<float> empty = Spread<float>.Empty; |
| 35 | |
| 36 | // Single element |
| 37 | var single = new SpreadBuilder<float>(1); |
| 38 | single.Add(42f); |
| 39 | Spread<float> one = single.ToSpread(); |
| 40 | ``` |
| 41 | |
| 42 | ## Accessing Elements |
| 43 | |
| 44 | ```csharp |
| 45 | // Always check Count before indexing |
| 46 | if (spread.Count > 0) |
| 47 | { |
| 48 | float first = spread[0]; |
| 49 | float last = spread[spread.Count - 1]; |
| 50 | } |
| 51 | |
| 52 | // Iterate (preferred — no allocation) |
| 53 | foreach (var item in spread) |
| 54 | Process(item); |
| 55 | |
| 56 | // Index access in loop |
| 57 | for (int i = 0; i < spread.Count; i++) |
| 58 | Process(spread[i]); |
| 59 | ``` |
| 60 | |
| 61 | ## Common Patterns in C# |
| 62 | |
| 63 | ### Map (Transform Each Element) |
| 64 | |
| 65 | ```csharp |
| 66 | public static Spread<float> Scale(Spread<float> input, float factor = 1f) |
| 67 | { |
| 68 | var builder = new SpreadBuilder<float>(input.Count); |
| 69 | foreach (var value in input) |
| 70 | builder.Add(value * factor); |
| 71 | return builder.ToSpread(); |
| 72 | } |
| 73 | ``` |
| 74 | |
| 75 | ### Filter |
| 76 | |
| 77 | ```csharp |
| 78 | public static Spread<float> FilterAbove(Spread<float> input, float threshold = 0.5f) |
| 79 | { |
| 80 | var builder = new SpreadBuilder<float>(); |
| 81 | foreach (var value in input) |
| 82 | { |
| 83 | if (value > threshold) |
| 84 | builder.Add(value); |
| 85 | } |
| 86 | return builder.ToSpread(); |
| 87 | } |
| 88 | ``` |
| 89 | |
| 90 | ### Zip (Process Two Spreads Together) |
| 91 | |
| 92 | ```csharp |
| 93 | public static Spread<float> Add(Spread<float> a, Spread<float> b) |
| 94 | { |
| 95 | int count = Math.Max(a.Count, b.Count); |
| 96 | var builder = new SpreadBuilder<float>(count); |
| 97 | for (int i = 0; i < count; i++) |
| 98 | { |
| 99 | float va = a.Count > 0 ? a[i % a.Count] : 0f; |
| 100 | float vb = b.Count > 0 ? b[i % b.Count] : 0f; |
| 101 | builder.Add(va + vb); |
| 102 | } |
| 103 | return builder.ToSpread(); |
| 104 | } |
| 105 | ``` |
| 106 | |
| 107 | ### Accumulate (Running Total) |
| 108 | |
| 109 | ```csharp |
| 110 | public static Spread<float> RunningSum(Spread<float> input) |
| 111 | { |
| 112 | var builder = new SpreadBuilder<float>(input.Count); |
| 113 | float sum = 0f; |
| 114 | foreach (var value in input) |
| 115 | { |
| 116 | sum += value; |
| 117 | builder.Add(sum); |
| 118 | } |
| 119 | return builder.ToSpread(); |
| 120 | } |
| 121 | ``` |
| 122 | |
| 123 | ## ReadOnlySpan as High-Performance Alternative |
| 124 | |
| 125 | For hot-path output (e.g., per-frame simulation data), `ReadOnlySpan<T>` avoids allocation entirely: |
| 126 | |
| 127 | ```csharp |
| 128 | [ProcessNode] |
| 129 | public class ParticleSimulator |
| 130 | { |
| 131 | private ParticleState[] _states; |
| 132 | |
| 133 | public ReadOnlySpan<ParticleState> Update(SimulationConfig config, float deltaTime) |
| 134 | { |
| 135 | // Simulate into pre-allocated array — zero allocation |
| 136 | Simulate(_states, config, deltaTime); |
| 137 | return _states.AsSpan(); |
| 138 | } |
| 139 | } |
| 140 | ``` |
| 141 | |
| 142 | Use `Spread<T>` for infrequent config inputs; use `ReadOnlySpan<T>` for high-frequency frame data. |
| 143 | |
| 144 | ## Performance Rules |
| 145 | |
| 146 | - **Pre-allocate builder**: `new SpreadBuilder<T>(expectedCount)` when count is known |
| 147 | - **No LINQ in hot paths**: `.Where()`, `.Select()`, `.ToList()` create hidden allocations |
| 148 | - **Cache spreads**: If output doesn't change, return the cached spread reference |
| 149 | - **Avoid repeated `.ToSpread()`**: Build once, output the result |
| 150 | - **For large spreads**: Consider `Span<T>` internally, convert to Spread at the API boundary |
| 151 | - **Spread change detection**: Since Spreads are immutable, reference equality (`!=` or `ReferenceEquals`) is sufficient — if the reference changed, the content changed |
| 152 | |
| 153 | ## Spreads in ProcessNodes |
| 154 | |
| 155 | ```csharp |
| 156 | [ProcessNode] |
| 157 | public class SpreadProcessor |
| 158 | { |
| 159 | private Spread<float> _lastInput = Spread<float>.Empty; |
| 160 | private Spread<float> _cachedOutput = Spread<float>.Empty; |
| 161 | |
| 162 | public void Update( |
| 163 | out Spread<float> output, |
| 164 | Spread<float> input = default) |
| 165 | { |
| 166 | input |