Live Layer Wire Format

Omniverse live layers encode their data in a format we refer to as a “wire format” rather than “file format” because the layers are not actually stored as files on disk on either the server (they’re stored in a database) or the clients (they’re stored in memory).

You could of course write the data to disk (and we do for unit tests), but live layers are not intended to be stored as regular files.

Omniverse Object Delta Format

The Omniverse Object Delta Format is encoded as a flatbuffer with the schema defined here and a compiled version in packman at omniverse.nucleus-objects.schema.linux-x86_64 (note: depsite having “linux-x86_64” in the name, that package works on all platforms because it is code only).

The schema looks like this:

../_images/OmniObjectDelta-fbs.drawio.svg

OmniObjectDelta

This is the “root” of the flatbuffer, which contains:

  • isDiff: True indicates that this is a “diff” rather than a “delta”. A “diff” only contains the “Diff” structural commands, which allows for some optimizations. This is used when you initially request an object from the server.

  • baseVersion: Only valid if “isDiff” is true. This is the version (sequence number) that the diff was based on. This is 0 if the diff was generated “from scratch” and there is no base version. This can also be sent mid-session to indicate that a user cleared the entire layer (by calling layer->Clear()).

  • structCommands: An array of commands that modify the structure of the tree itself. These must be applied in order.

  • setFields: An array of fields to set after applying structural commands.

  • timeSamples: An array of timesamples to set after applying structural commands.

  • sourceFormat: No longer used. We previously used this if you created a live layer by calling “Export” from a non-live layer. This field stored the original format of the non-live layer, but we determined that it was not actually important. It’s left here for backwards compatibility.

Structural Commands

Nodes in the flatbuffer schema are are named sections for historical reasons that nobody can remember. This document (and the code itself) uses “node” and “section” interchangeably.

CreateSection

This creates a new “section” which is a node in the live layer tree. This is a no-op if the parent does not exist. If there already exists a node with the same parentId and the same sectionName, this is ignored (and the node will not be created).

  • parentId: The ID of the parent node. This is 0 when creating the root node (which is always sectionId 1).

  • sectionId: The ID of this node. This is randomly generated (as described in Creating Nodes). It must not be 0, or previously used. It must be 1 for the root node.

  • sectionName: The relative name of this section.

  • sectionType: This maps directly to SdfSpecType

DeleteSection

Deletes a node from the tree. This is a no-op if the node has already been deleted.

  • parentId: The ID of the parent node. This is no longer used, but left in for backwards compatibility.

  • sectionId: The ID of the node to delete.

MoveSection

Moves or renames a node in the tree. This is a no-op if the node has been deleted. This node is deleted if the new parent node has been deleted. This node is also deleted if there already exists a node with the same newParentId and newName.

  • oldParentId: The ID of the old parent node.

  • newParentId: The ID of the new parent node (could be the same as the oldParentId).

  • sectionId: The ID of the node which is getting moved/renamed.

  • newName: The new name of the node (could be the same as the existing node name).

The reason we send both the old parent id and the new parent id is to determine if this was intended to be a move or a rename. For example if you just want to rename the node, you set newParentId = oldParentId, then if another client simultaneously moves the node, so it has a different parent id, we still apply the rename, even though the parent id no longer matches. In retrospect, we should have created an explicit “RenameSection” command to handle this case, but it is not worth changing now because that would break backwards compatibilty.

ReorderChildren

As described in Children Order, the order of child nodes matters in USD. Child nodes are stored in the database (and in memory, for that matter) in unordered maps, and USD uses special “children” fields to define the order of the child nodes. This command is used to change the order of the child nodes, which implicitly sets those “children” fields.

  • sectionId: The parent node for which we are changing the child node order

  • childrenListId: Some spec types may have up to 3 different children lists. This indicates which one we are changing. The exact index depends on sectionType (specType). The mapping is in ChildrenHelper::GetChildrenIndex.

  • childrenList: An array of children node IDs in the desired order.

If a child node in childrenList no longer exists, it is ignored. If a child node exists which is not in childrenList, its position remains unchanged.

For example if a node has the children “ABCDEFG” and childrenList indicates “GCHDB”, the new list will be “AGCDEFB”. The “H” is ignored, because it no longer exists. The nodes ‘A’, ‘E’ and ‘F’ stay in their same place because they are not in “childrenList”. The other nodes are rearranged to match the order in “childrenList”.

DiffChangeSection

This command is only valid if isDiff is true. This allows for directly setting all the values of a node (and creates it if necessary).

  • parentId: ID of the parent node.

  • sectionId: ID of the node we are changing.

  • sectionName: New name of the node.

  • sectionType: New type of the node.

  • sectionOrder: Order of this node in the parent list. For example if a node has 2 children, one of the children will have sectionOrder=0, the next will have sectionOrder=1

Values are set even if unchanged. For example, sectionName must always be set, even if the node was not renamed.

DiffDeleteSection

This command is only valid if isDiff is true. Delete a node.

  • sectionId: ID of the node to delete.

SetFields & TimeSamples

The setFields and timesamples arrays are both used to set the values of fields of nodes in the tree.

They are very similar to each other, but SetField uses a string as the key name, and TimeSample uses a double.

  • sectionId: The parent node ID

  • keyName: (SetField) The field name.

  • time: (TimeSample) The timesample key.

  • valueOrExtHash: For small values (less than 64KB) this is the value itself. For large values this is the SHA1 hash of the value.

  • extValueSize: Size of the value, only for values stored outside the flatbuffer.

  • extValueIndex: Index of the value in the parts array (see Omniverse Multi-Part Format).

  • setOrder: This is used to instruct clients to set the fields in a particular order. This was added to ensure all clients receive notices in the same order. We store this explicitly rather than relying on the order of the setFields array itself because fields are stored internally in an unordered map.

Omniverse Multi-Part Format

The multi-part format is used whenever there are large values. Currently “large” is defined as 65,535 bytes.

The reason the multi-part format exists is because flatbuffers uses 32 bit signed offsets, so the total size cannot be larger than 2GB. Ideally nobody would create a live layer which is larger than 2GB, but our top goal is to support anything that USD can do, and USD can create layers which are larger than 2GB.

After the 4 byte identifier, there is a 32 bit unsigned integer which stores the number of parts, followed by a table of 64 bit unsigned integers which are offsets to each part.

The first part is a flatbuffer of the same format as “OODF”. The other parts are the actual values (one part per large value).

Value Format

Field values are VtValues encoded using a method similar to how USD crate values pack data. The code is in OmniUsdValueHelper.cpp.

The packing format is purely a client to client contract. The server doesn’t unpack values at all.

Each value starts with a header: * 7 bit type * 1 bit isArray flag * 8 bit version number. * 64 bit count (only if isArray is set) * 32 bit count (only for string types)

Following the header is the packed data blob. The packing algorithm depends on the type of value, and the version number.

Currently all types are at version 0. Adding a new version should be done with caution because it is not a backwards compatible change. The safest way to roll this out is to add support for reading the new version, but always write the old version. After enough time has elapsed, and you are sure that all clients have been updated, you can switch to writing the new version. An example of adding a new version is if you want to add compression to a type.

Similarly, adding a new type is not a backwards compatible change. There is not really a safe way to roll out a new type, but USD itself has the same issue with crate files. For this reason it’s safe to assume that new types are unlikely to be added (hopefully).

The list of types is defined in UsdTypes.h and is currently:

// Array types.
USD_TYPE(Bool, 1, bool, 0)
USD_TYPE(UChar, 2, uint8_t, 0)
USD_TYPE(Int, 3, int, 0)
USD_TYPE(UInt, 4, unsigned int, 0)
USD_TYPE(Int64, 5, int64_t, 0)
USD_TYPE(UInt64, 6, uint64_t, 0)

USD_TYPE(Half, 7, GfHalf, 0)
USD_TYPE(Float, 8, float, 0)
USD_TYPE(Double, 9, double, 0)

USD_TYPE(String, 10, std::string, 0)

USD_TYPE(Token, 11, TfToken, 0)

USD_TYPE(AssetPath, 12, SdfAssetPath, 0)

USD_TYPE(Matrix2d, 13, GfMatrix2d, 0)
USD_TYPE(Matrix3d, 14, GfMatrix3d, 0)
USD_TYPE(Matrix4d, 15, GfMatrix4d, 0)

USD_TYPE(Quatd, 16, GfQuatd, 0)
USD_TYPE(Quatf, 17, GfQuatf, 0)
USD_TYPE(Quath, 18, GfQuath, 0)

USD_TYPE(Vec2d, 19, GfVec2d, 0)
USD_TYPE(Vec2f, 20, GfVec2f, 0)
USD_TYPE(Vec2h, 21, GfVec2h, 0)
USD_TYPE(Vec2i, 22, GfVec2i, 0)

USD_TYPE(Vec3d, 23, GfVec3d, 0)
USD_TYPE(Vec3f, 24, GfVec3f, 0)
USD_TYPE(Vec3h, 25, GfVec3h, 0)
USD_TYPE(Vec3i, 26, GfVec3i, 0)

USD_TYPE(Vec4d, 27, GfVec4d, 0)
USD_TYPE(Vec4f, 28, GfVec4f, 0)
USD_TYPE(Vec4h, 29, GfVec4h, 0)
USD_TYPE(Vec4i, 30, GfVec4i, 0)

// Non-array types.
USD_TYPE(Dictionary, 31, VtDictionary, 0)

USD_TYPE(TokenListOp, 32, SdfTokenListOp, 0)
USD_TYPE(StringListOp, 33, SdfStringListOp, 0)
USD_TYPE(PathListOp, 34, SdfPathListOp, 0)
USD_TYPE(ReferenceListOp, 35, SdfReferenceListOp, 0)
USD_TYPE(IntListOp, 36, SdfIntListOp, 0)
USD_TYPE(Int64ListOp, 37, SdfInt64ListOp, 0)
USD_TYPE(UIntListOp, 38, SdfUIntListOp, 0)
USD_TYPE(UInt64ListOp, 39, SdfUInt64ListOp, 0)

USD_TYPE(PathVector, 40, SdfPathVector, 0)
USD_TYPE(TokenVector, 41, std::vector<TfToken>, 0)

USD_TYPE(Specifier, 42, SdfSpecifier, 0)
USD_TYPE(Permission, 43, SdfPermission, 0)
USD_TYPE(Variability, 44, SdfVariability, 0)

USD_TYPE(VariantSelectionMap, 45, SdfVariantSelectionMap, 0)
USD_TYPE(TimeSamples, 46, SdfTimeSampleMap, 0)
USD_TYPE(Payload, 47, SdfPayload, 0)

USD_TYPE(DoubleVector, 48, std::vector<double>, 0)
USD_TYPE(LayerOffsetVector, 49, std::vector<SdfLayerOffset>, 0)
USD_TYPE(StringVector, 50, std::vector<std::string>, 0)

USD_TYPE(ValueBlock, 51, SdfValueBlock, 0)
USD_TYPE(Value, 52, VtValue, 0)

USD_TYPE(UnregisteredValue, 53, SdfUnregisteredValue, 0)
USD_TYPE(UnregisteredValueListOp, 54, SdfUnregisteredValueListOp, 0)

USD_TYPE(Path, 55, SdfPath, 0)
USD_TYPE(SpecType, 56, SdfSpecType, 0)

// Crate version 0.8.0
USD_TYPE(PayloadListOp, 57, SdfPayloadListOp, 0)

// Crate version 0.9.0
USD_TYPE(TimeCode, 58, SdfTimeCode, 0)

// USD Resolver 2.0
USD_TYPE(Range1d, 59, GfRange1d, 0)
USD_TYPE(Range2d, 60, GfRange2d, 0)
USD_TYPE(Range3d, 61, GfRange3d, 0)

USD_TYPE(Range1f, 62, GfRange1f, 0)
USD_TYPE(Range2f, 63, GfRange2f, 0)
USD_TYPE(Range3f, 64, GfRange3f, 0)