Bundles

Overview

Bundles are versatile data structure designed for exchanging multiple attributes between nodes.

With regular attributes, each attribute represents a distinct data type. These attributes are connected individually from one node’s output to another’s input like in the following image:

flowchart LR subgraph NodeA AttributeA1 AttributeA2 end subgraph NodeB AttributeB1 AttributeB2 end AttributeA1 --- AttributeB1 AttributeA2 --- AttributeB2

A Bundle, on the other hand, encapsulate multiple attributes into a single entity. This bundle can then be connected from node’s output to another’s input as one unit. Furthermore, the tree-like structure of bundles allows for the organization of attributes into sub-groups

Structure

Bundle is a dynamic, comparable to a dictionary data structure, designed to organize attributes into logical groups. The organization of attributes within bundles is enabled by their ability to recursively contain other bundles.

The similarity to a dictionary comes from organizing attributes and child bundles into key-value pairs, where each key is a name that maps directly to either an attribute or a child bundle.

Following image is a conceptual representation of a Bundle carrying three attributes and two child bundles.

flowchart TB subgraph Bundle direction TB subgraph Attributes["Attributes:"] direction LR AttrName0("Name") --> AttrValue0("DataHandle") AttrName1("Name") --> AttrValue1("DataHandle") AttrName2("Name") --> AttrValue2("DataHandle") end subgraph Children["ChildBundles:"] direction LR ChildName0("Name") --> ChildValue0("Handle") ChildName1("Name") --> ChildValue1("Handle") end end style Attributes stroke-dasharray: 5 5 style Children stroke-dasharray: 5 5 classDef hidden display: none

(The image above does not suggest how bundles are represented in the memory.)

Unlike dictionaries, bundles create a tree-like structure, where each bundle can have sub-bundles as its children. The relationship between parent and children is bidirectional; the parent is aware of its children, just as the children are aware of their parent.

flowchart TB BundleA("Root") BundleB("Child") BundleC("Child") BundleE("GrandChild") BundleA == child ==> BundleB BundleA == child ==> BundleC BundleB == child ==> BundleE BundleB -. parent .-> BundleA BundleC -. parent .-> BundleA BundleE -. parent .-> BundleB

Bundle interfaces

When passed through the graph, bundles at a node’s input are strictly read-only. Nodes are only allowed to write to their own output bundles. This leads to the existence of two interface sets: one for read-only access and another for write operations.

flowchart LR NodeA:::hidden -- "input bundle\n(read-only)" --> NodeB("Node") NodeB -- "output bundle\n(read-write)" --> NodeC:::hidden classDef hidden display: none;

There are multiple interfaces available for interacting with bundles. For node writers who are beginning to work with these, the lowest level bundle interfaces are:

Low level interfaces

IBundle2, IConstBundle2 and IBundleFactory

These successor interfaces to IBundle are designed to handle recursive bundles, and they retain all functionalities offered by the original version. hey are compatible with both C++ and Python.

IBundle2 and IConstBundle2 are specifically tailored for managing input and output in recursive bundles. IConstBundle2 is responsible for reading data from input bundles, where IBundle2 is for writing.

flowchart LR NodeA:::hidden -- "input bundle\n(IConstBundle2)" --> NodeB("Node") NodeB -- "output bundle\n(IBundle2)" --> NodeC:::hidden classDef hidden display: none;

The IBundleFactory is used for creating instances of bundle interfaces. An example of obtaining the instances of low level IBundle2 interface:

// get output writable bundle interface through factory.
auto const computeGraph = carb::getCachedInterface<ComputeGraph>();
ObjectPtr<IBundleFactory> const iBundleFactory = computeGraph->getBundleFactoryInterfacePtr();
ObjectPtr<IBundle2> bundle = iBundleFactory->getBundle(contextObj, bundleHandle);

Note

The node authors would not directly acquire those interfaces; instead, they would access them via the OGN wrappers.

OGN layer

BundleContents

The above are low level interfaces. To access bundle attributes, the OGN layer offers higher-level API wrappers, such as BundleContents. The BundleContents is designed for constructing and caching the bundle interface for repeated use directly from the database:

Example of accessing read-only BundleContents:

static bool compute(OgnSpecializedBundleConsumerDatabase& db)
{
    // get access to read-only BundleContents
    auto const& bundleContents = db.inputs.bundle();
    if (auto const spec = getMyBundleSpecialization(bundleContents))
    {
        db.outputs.diameter() = spec.getDiameter();
        db.outputs.type() = spec.getTypeToken();
    }
    return true;
}

Specializing bundles

The Bundle interface is a generic interface that gives developers the flexibility to determine where data is located and how it is grouped within child bundles. Typically, node writers responsible for designing data exchange between nodes might consider creating a custom helper class. This class would offer common functions for manipulating attributes and organizing them into child bundles, thereby reducing the reliance on low-level APIs.

An example of custom specialization would look as follows:

template <ogn::eAttributeType AttributeType, ogn::eMemoryType MemoryType, PtrToPtrKind GpuPtrType>
class MyBundleSpecialization
{
public:
    DEFINE_BUNDLE_TYPE_TRAITS(AttributeType, MemoryType, GpuPtrType);

    explicit MyBundleSpecialization(IBundle2Ptr_t bundle) noexcept;

    // Access to diameter attribute
    void setDiameter(float value) noexcept;
    float getDiameter() const noexcept;

    // Access to defined type
    fabric::TokenC getTypeToken() const noexcept;

    // Check validity of the underlying bundle
    bool isValid() const noexcept;
    operator bool() const noexcept;

private:
    IBundle2Ptr_t m_bundle;
};

And the example definition of getDiameter function:

template <ogn::eAttributeType AttributeType, ogn::eMemoryType MemoryType, PtrToPtrKind GpuPtrType>
float MyBundleSpecialization<AttributeType, MemoryType, GpuPtrType>::getDiameter() const noexcept
{
    auto& tokensAndTypes = MySpecializationTokensAndTypes;
    using DiameterType = decltype(tokensAndTypes->diameterDefault);

    if (auto handle = m_bundle->getConstAttributeByName(tokensAndTypes->diameterToken); handle.isValid())
    {
        ogn::RuntimeAttribute<ogn::kOgnInput, MemoryType, GpuPtrType> const attr{ m_bundle->getContext(), handle };
        return *attr.template get<DiameterType>();
    }
    return 0.0f;
}

During graph evaluation, bundles are passed between nodes. OmniGraph provides a set of generic nodes for manipulating bundles, such as Remove Attributes and Insert Attributes. These nodes consume a bundle and operate without awareness of its specialization, working directly with the lowest-level interface, IBundle2.

On certain occasions, developers might decide that specific attributes should be exclusively managed by their custom APIs. This is to ensure these attributes are “protected” from accidental modifications by anyone accessing them through the IBundle2 interface. An example of such a attribute could be an enumeration with a predefined number of entries:

///! Enumeration of all supported types in MyBundleSpecialization
enum class MyBundleSpecializationType
{
    Circle = 0,
    Sphere = 1,
};

It could lead to undesired sideffects if another node that is not intended to work with specialized bundle, accidentally modifies stored “type” and set it to a value that is out of range of the enumeration.

The IBundle2 interface provides methods to query information about all available attributes in a bundle. These functions include getAttributes, getAttributeNames, and getAttributeTypes. It is possible to exclude certain attributes or child bundles from being listed by these functions. This feature can be used to “hide” specific attributes within a bundle. By adding double underscore prefix to attribute name, the attribute becomes “hidden”:

struct MySpecializationTokensAndTypesType
{
    fabric::Token typeToken{ "__myType" };
    fabric::Type typeType{ fabric::BaseDataType::eUInt };
    unsigned int typeDefault = static_cast<unsigned int>(MyBundleSpecializationType::Circle);

    fabric::Token diameterToken{ "diameter" };
    fabric::Type diameterType{ fabric::BaseDataType::eFloat };
    float diameterDefault = 10.f;

    fabric::Token circleTypeToken{ "Circle" };
    fabric::Token sphereTypeToken{ "Sphere" };
};

Only one way to obtain a handle to the “hidden” attribute is to request it by its name(getAttributeByName):

template <ogn::eAttributeType AttributeType, ogn::eMemoryType MemoryType, PtrToPtrKind GpuPtrType>
fabric::TokenC MyBundleSpecialization<AttributeType, MemoryType, GpuPtrType>::getTypeToken() const noexcept
{
    auto& tokensAndTypes = MySpecializationTokensAndTypes;
    using TypeType = decltype(tokensAndTypes->typeDefault);

    if (auto handle = m_bundle->getConstAttributeByName(tokensAndTypes->typeToken); handle.isValid())
    {
        ogn::RuntimeAttribute<ogn::kOgnInput, MemoryType, GpuPtrType> const attr{ m_bundle->getContext(), handle };
        return tokensAndTypes.asToken(static_cast<MyBundleSpecializationType>(*attr.template get<TypeType>()));
    }
    return fabric::kUninitializedToken;
}

Similarly, the validation of a specialized bundle can be performed by verifying the presence of a specific (“hidden”) attribute:

template <ogn::eAttributeType AttributeType, ogn::eMemoryType MemoryType, PtrToPtrKind GpuPtrType>
bool MyBundleSpecialization<AttributeType, MemoryType, GpuPtrType>::isValid() const noexcept
{
    return m_bundle && m_bundle->isValid() &&
           m_bundle->getConstAttributeByName(MySpecializationTokensAndTypes->typeToken) !=
               ConstAttributeDataHandle::invalidHandle();
}