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:
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.
(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.
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.
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.
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();
}