Skip to main content

Overview

HNode is built as a Unity application with a modular plugin architecture that processes DMX lighting data in real-time. The system receives Art-Net DMX packets over the network, processes them through customizable generators, serializes the data into video textures, and outputs via Spout for use in other applications.

Core Architecture Components

Art-Net Receiver

Listens for incoming Art-Net DMX packets on UDP port 6454 (configurable)

DMX Manager

Manages DMX universe data with support for up to 16,384 channels across multiple universes

Plugin System

Extensible architecture with Serializers, Generators, and Exporters

Texture Pipeline

Converts DMX data to/from video textures using configurable pixel mapping formats

Application Structure

The Unity application follows a centralized architecture with the Loader class serving as the main orchestrator.

Loader (Main Orchestrator)

The Loader.cs component handles:
  • Plugin Discovery: Automatically discovers all implementations of IDMXSerializer, IDMXGenerator, and IExporter interfaces using reflection
  • Configuration Management: Loads and saves show configurations as YAML files (.shwcfg)
  • Component Initialization: Sets up Spout receivers/senders, Art-Net receiver, and texture readers/writers
  • UI Coordination: Manages dynamic UI generation for all active plugins
Loader.cs:46-49
//load in all the serializers
serializers = GetAllInterfaceImplementations<IDMXSerializer>();
generators = GetAllInterfaceImplementations<IDMXGenerator>();
exporters = GetAllInterfaceImplementations<IExporter>();

Show Configuration

The ShowConfiguration class is the heart of HNode’s configuration system, defining:
  • Serializer: Converts DMX bytes to video pixels
  • Deserializer: Converts video pixels back to DMX bytes
  • Transcode Mode: Enable to convert between different pixel mapping formats
  • Universe Counts: Configure how many universes to process
  • Masked Channels: List<DMXChannelRange> defining channels to hide/show
  • Invert Mask: Flip behavior to only show masked channels
  • Auto Mask on Zero: Automatically make zero-value channels transparent
  • Spout Names: Input and output Spout stream names
  • Art-Net Settings: IP address (0.0.0.0 for all interfaces) and port
  • Resolutions: Separate input/output texture resolutions
  • Framerate: Target application framerate (1-60 FPS)

Data Flow Pipeline

HNode processes DMX data through a well-defined pipeline every frame:
1

Art-Net Reception

The ArtNetReceiver listens on UDP for incoming Art-Net packets. When received, the DmxManager stores the 512-byte universe data in a dictionary keyed by universe number.
DmxManager.cs:64-75
public void ReceivedDmxPacket(ReceivedData<DmxPacket> receivedData)
{
    var packet = receivedData.Packet;
    var universe = packet.Universe;
    if (!DmxDictionary.ContainsKey(universe)) 
        DmxDictionary.Add(universe, packet.Dmx);
    Buffer.BlockCopy(packet.Dmx, 0, DmxDictionary[universe], 0, 512);
}
2

DMX Merging

The TextureWriter merges all universes into a single contiguous List<byte>. In transcode mode, it uses data from the TextureReader instead of Art-Net.
TextureWriter.cs:76-96
if (Loader.showconf.Transcode)
{
    mergedDmxValues = reader.dmxData.ToList();
}
else if (dmxManager.Universes().Length != 0)
{
    var universeCount = dmxManager.Universes().Max() + 1;
    for (ushort u = 0; u < universeCount; u++)
    {
        byte[] dmxValues = dmxManager.DmxValues(u);
        mergedDmxValues.AddRange(dmxValues);
    }
}
3

Generator Processing

Each active IDMXGenerator modifies the DMX data in sequence. Generators can read, write, or transform channel values.
TextureWriter.cs:99-105
//now run the generators in order
foreach (var generator in Loader.showconf.Generators)
{
    generator.GenerateDMX(ref mergedDmxValues);
}
4

Serialization

The active IDMXSerializer converts each DMX channel value into pixels in a Color32[] array, applying channel masking rules.
TextureWriter.cs:113-146
var ChannelsToSerialize = Math.Min(
    (long)Loader.showconf.SerializeUniverseCount * 512, 
    mergedDmxValues.Count
);
for (int i = 0; i < ChannelsToSerialize; i++)
{
    // Check masking rules
    if (isMasked) continue;
    
    Loader.showconf.Serializer.SerializeChannel(
        ref pixels, mergedDmxValues[i], i, 
        TextureWidth, TextureHeight
    );
}
5

Export & Output

Each active IExporter processes the final DMX data. The pixel array is uploaded to a Texture2D and sent via Spout.
TextureWriter.cs:150-162
Loader.showconf.Serializer.CompleteFrame(
    ref pixels, ref mergedDmxValues, 
    TextureWidth, TextureHeight
);
foreach (var exporter in Loader.showconf.Exporters)
{
    exporter.CompleteFrame(ref mergedDmxValues);
}

texture.SetPixels32(pixels);
texture.Apply();

Plugin System

HNode’s extensibility comes from its plugin interface system. All plugins implement:

IConstructable Interface

IConstructable.cs:4-8
public interface IConstructable
{
    void Construct();   // Called when plugin is activated
    void Deconstruct(); // Called when plugin is deactivated
}

IUserInterface Interface

Plugins can create custom UI elements that appear in the HNode interface:
  • ConstructUserInterface(RectTransform rect): Create UI elements
  • DeconstructUserInterface(): Clean up UI elements
  • UpdateUserInterface(): Update UI values each frame
All plugins are automatically discovered at startup using reflection. Simply implement the appropriate interface and HNode will find it!

Plugin Lifecycle

  1. Discovery: On startup, Loader scans all assemblies for interface implementations
  2. Construction: When added to a show configuration, Construct() is called
  3. Execution: Plugin methods are called each frame during the data pipeline
  4. Deconstruction: When removed or app closes, Deconstruct() is called

Transcode Mode

Transcode mode enables format conversion between different pixel mapping standards:
1

Input

A Spout source provides video in one format (e.g., VRSL)
2

Deserialization

The Deserializer converts pixels back to DMX channel values
3

Re-serialization

The Serializer converts DMX to a different pixel format (e.g., Binary)
4

Output

The converted video is sent via Spout in the new format
In transcode mode, Art-Net input is disabled and the TextureReader provides DMX data instead of the DmxManager.

Configuration Persistence

Show configurations are saved as YAML files with the .shwcfg extension:
# Example show configuration
serializer: !VRSL
  gammaCorrection: true
  rgbGridMode: false
  outputConfig: 0

transcode: false
transcodeUniverseCount: 3
serializeUniverseCount: 2147483647

generators:
- !Strobe
  channel: 0.1
  valueOn: 255
  valueOff: 0
  frequency: 2

exporters:
- !MIDIDMX
  midiDevice: "loopMIDI Port"
  channelsPerUpdate: 100
Always use the Save/Load buttons in HNode rather than manually editing YAML files. Manual edits may cause parsing errors.

Performance Considerations

HNode is optimized for real-time performance:
  • Unity Profiler Integration: Key sections are wrapped in Profiler.BeginSample()/EndSample()
  • Array Pre-allocation: DMX lists use EnsureCapacity() to avoid resizing
  • Minimal GC Pressure: Reuses arrays and avoids unnecessary allocations
  • Configurable Universe Counts: Limit processing to only needed universes
TextureWriter.cs:166-172
frameTime.text = $"Serialization Time: {timer.ElapsedMilliseconds} ms";
frameTime.text += $"\nDMX Channels: {mergedDmxValues.Count}";
var bytesPerSecond = mergedDmxValues.Count / UnityEngine.Time.smoothDeltaTime;
var prettyBytes = ByteSize.FromBytes(bytesPerSecond).ToString("0.##");
frameTime.text += $"\nData Throughput: {prettyBytes}/s";
Monitor the serialization time display in HNode. If it exceeds 16ms, reduce the number of universes or lower the target framerate.

Build docs developers (and LLMs) love