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 viaSV_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.