About vvvv-spreads
vvvv-spreads is a skill for developers working with Spread collections in vvvv and C#. It covers creation, access, and common transformations, with guidance on immutability and reducing allocations.
Key features
- 01Create collections with SpreadBuilder
- 02Understand element access and cyclic indexing
- 03Implement map, filter, zip, and accumulation
- 04Choose collection input types by use
- 05Reduce allocations with caching and Span
Use cases
- 01Transform or filter node input collections
- 02Process element-wise operations across collections
- 03Optimize output for per-frame simulation data
- 04Avoid extra allocations in hot paths
Install
Copy the command and run it in a terminal.
npx skills add https:/ / github.com/ tebjan/ vvvv-skills --skill vvvv-spreadsAfter installing, type/vvvv-spreadsin Claude Code to confirm it loaded.
Bundled filesexamples.md
Details
From the project READMEvvvv Spreads
What Are Spreads
Spread<T> is vvvv's immutable collection type, conceptually similar to ImmutableArray<T>. Nodes return collections as Spread<T>; they never take Spread<T> as an input type (see Collection Inputs).
Key properties:
- Immutable — never modify in place, always create new spreads
- Value semantics — two spreads with same elements are considered equal
- Cyclic indexing — in visual patches, indexing wraps around (not in C# API)
- Never null — use
Spread<T>.Emptyinstead ofnull
Creating Spreads
SpreadBuilder (Primary Method)
var builder = new SpreadBuilder<float>(expectedCount);
for (int i = 0; i < count; i++)
builder.Add(ComputeValue(i));
Spread<float> result = builder.ToSpread();
From Existing Data
// From array (extension method)
Spread<int> fromArray = new int[] { 1, 2, 3 }.ToSpread();
// From array (static factory)
Spread<Waypoint> fromResult = Spread.Create(resultArray);
// Empty spread (NEVER use null)
Spread<float> empty = Spread<float>.Empty;
// Single element
var single = new SpreadBuilder<float>(1);
single.Add(42f);
Spread<float> one = single.ToSpread();
Accessing Elements
// Always check Count before indexing
if (spread.Count > 0)
{
float first = spread[0];
float last = spread[spread.Count - 1];
}
// Iterate (preferred — no allocation)
foreach (var item in spread)
Process(item);
// Index access in loop
for (int i = 0; i < spread.Count; i++)
Process(spread[i]);
Common Patterns in C#
Map (Transform Each Element)
public static Spread<float> Scale(IReadOnlyList<float> input, float factor = 1f)
{
var builder = new SpreadBuilder<float>(input.Count);
foreach (var value in input)
builder.Add(value * factor);
return builder.ToSpread();
}
Filter
public static Spread<float> FilterAbove(IEnumerable<float> input, float threshold = 0.5f)
{
var builder = new SpreadBuilder<float>();
foreach (var value in input)
{
if (value > threshold)
builder.Add(value);
}
return builder.ToSpread();
}
Zip (Process Two Spreads Together)
public static Spread<float> Add(IReadOnlyList<float> a, IReadOnlyList<float> b)
{
int count = Math.Max(a.Count, b.Count);
var builder = new SpreadBuilder<float>(count);
for (int i = 0; i < count; i++)
{
float va = a.Count > 0 ? a[i % a.Count] : 0f;
float vb = b.Count > 0 ? b[i % b.Count] : 0f;
builder.Add(va + vb);
}
return builder.ToSpread();
}
Accumulate (Running Total)
public static Spread<float> RunningSum(IReadOnlyList<float> input)
{
var builder = new SpreadBuilder<float>(input.Count);
float sum = 0f;
foreach (var value in input)
{
sum += value;
builder.Add(sum);
}
return builder.ToSpread();
}
ReadOnlySpan as High-Performance Alternative
For hot-path output (e.g., per-frame simulation data), ReadOnlySpan<T> avoids allocation entirely:
[ProcessNode]
public class ParticleSimulator
{
private ParticleState[] _states;
public ReadOnlySpan<ParticleState> Update(SimulationConfig config, float deltaTime)
{
// Simulate into pre-allocated array — zero allocation
Simulate(_states, config, deltaTime);
return _states.AsSpan();
}
}
Collection Inputs: Never Spread<T>
| The node needs | Input type |
|---|---|
| the count, or index access | IReadOnlyList<T> |
| only to iterate | IEnumerable<T> |
A Spread<T> connects to both (it implements IReadOnlyList<T>), and so do arrays, lists and every
other collection a user wires. An input typed Spread<T> accepts nothing else and forces
conversions. Outputs stay Spread<T>.
The one place VL.StandardLibs declares a Spread<T> parameter is a collection pin group
(PinGroupKind.Collection, the add/remove-button pins). Check before giving a pin group another type.
Performance Rules
- Pre-allocate builder:
new SpreadBuilder<T>(expectedCount)when count is known - No LINQ in hot paths:
.Where(),.Select(),.ToList()create hidden allocations - Cache spreads: If output doesn't change, return the cached spread reference
- Avoid repeated
.ToSpread(): Build once, output the result - For large spreads: Consider
Span<T>internally, convert to Spread at the API boundary - Spread change detection: Since Spreads are immutable, reference equality (
!=orReferenceEquals) is sufficient — if the reference changed, the content changed
Spreads in ProcessNodes
[ProcessNode]
public class SpreadProcessor
{
private IReadOnlyList<float> _lastInput = Spread<float>.Empty;
private Spread<float> _cachedOutput = Spread<float>.Empty;
public void Update(
out Spread<float> output,
IReadOnlyList<float>? input = null)
{
input ??= Spread<float>.Empty;
if (!ReferenceEquals(input, _lastInput))
{
var builder = new SpreadBuilder<float>(input.Count);
foreach (var v in input)
builder.Add(v * 2f);
_cachedOutput = builder.ToSpread();
_lastInput = input;
}
output = _cachedOutput;
}
}
For more code examples, see examples.md.
Keep this knowledge growing
Leave the next patcher one fewer mystery. On first use of any skill in this collection with no successful check in seven days, compare the source with tebjan/vvvv-skills. If no record exists, check once. Share one local check timestamp/revision across these skills; do not poll per invocation. Preserve local extensions and pinned project versions; checking is not permission to install updates. If offline, record the deferred check and continue the task.
When actual work exposes a gap, capture the tested vvvv/package versions, a minimal reproduction or authoritative source, and the observed result. Treat other users' reports as leads, not facts until verified. Improve the smallest relevant instruction; do not turn one example into a universal rule. Redact private data and obtain publishing authorization before submitting upstream. See the contribution policy. Merged improvements reach users through their installer/update workflow, not through a local edit or an automatic overwrite of everyone's skills.