How DirectX 12 Work Graphs Pass Node Records

DirectX 12 Work Graphs introduce an autonomous, GPU-driven execution model where compute nodes dynamically schedule and dispatch downstream child compute nodes without CPU intervention. Communication between these nodes relies on node records—strongly typed payload buffers that a producer node allocates and populates for a consumer node. This article details the allocation mechanisms, HLSL interfaces, and memory management strategies used to transfer payload records from parent to child compute nodes in DirectX 12.

Record Struct Definition

Data exchange begins with a user-defined HLSL structure representing the payload. Both the producing (parent) and consuming (child) nodes must reference this payload definition. The receiving child node explicitly declares this structure as its input parameter, which informs the driver of the memory layout expected upon invocation.

struct ChildPayload
{
    uint2 TileID;
    float Threshold;
};

Declaring Output Interfaces in the Parent Node

Parent nodes define outputs using specialized HLSL objects that specify the target child node and the payload type:

  • NodeOutput<T>: Used to output records targeting a specific child node identified by a node identifier.
  • NodeOutputArray<T>: Used when directing records to an array of nodes selected dynamically at runtime.

The output declaration binds the payload type T to the target node using the [NodeId("TargetNodeName")] attribute:

[NodeLaunch("broadcasting")]
[NodeDispatchGrid(1, 1, 1)]
[NumThreads(64, 1, 1)]
void ParentNode(
    [MaxRecords(64)] NodeOutput<ChildPayload> childOutput)
{
    // Node logic...
}

The [MaxRecords(N)] attribute provides a static upper bound to the driver for the maximum number of records the thread or thread group can allocate, allowing the runtime to size scratch and payload memory appropriately.

Allocating Node Records

Before writing data, a parent node must allocate space for the payload. Allocations can occur at the individual thread level or cooperatively at the thread-group level:

  • Thread-Level Allocation: A single thread calls childOutput.GetThreadNodeOutputRecords(count) to reserve one or more records dedicated to that thread.
  • Group-Level Allocation: For collective decisions, the group calls childOutput.GetGroupNodeOutputRecords(count), where all threads in the thread group share the reservation pool, usually indexed via SV_GroupIndex.

DirectX 12 also supports ThreadNodeOutputRecords<T> and GroupNodeOutputRecords<T> wrappers to manage the lifetime and direct array indexing of the reserved records.

Writing to the Payload Buffer

Once reserved, the method returns a handle or reference to the allocated memory. The parent node writes data directly into the fields of the record structure:

ThreadNodeOutputRecords<ChildPayload> records = childOutput.GetThreadNodeOutputRecords(1);
records[0].TileID = uint2(10, 20);
records[0].Threshold = 0.75f;

These writes target memory managed internally by the work graph runtime, eliminating manual buffer offset calculations and standard UAV atomic pointer manipulation.

Record Finalization and Launch

Record commitment notifies the hardware scheduler that the payload is complete and ready for downstream processing:

  • Explicit Output Completion: Calling records.OutputComplete() explicitly finalizes the records, allowing the GPU work-creation pipeline to queue the target child node immediately.
  • Implicit Scope Exit: If OutputComplete() is not invoked explicitly, the runtime automatically finalizes records when the record handle exits scope.

After completion, the driver routes the payload records into the execution queue of the specified child compute node. The child receives the data via its [NodeLaunch] entry point parameters, executing across the GPU with hardware-level load balancing.

Runtime Memory Management

Underlying payload memory is provisioned through the work graph backing store. During graph creation, the application queries the graph's memory requirements using ID3D12StateObjectProperties1::GetWorkGraphMemoryRequirements and assigns a dedicated backing buffer via a GPU virtual address. The runtime slices this backing store into internal ring buffers and transient pools that handle record allocations, child node dispatch arguments, and synchronization automatically.