Live Layer Details
Omniverse Live Layers allow multiple people on multiple different machines to edit the same USD layer at the same time, and be able to see each others edits in real time.
The goals of live layers, in order of importance, are:
Support the full USD feature set. Anything that can be done in USD is supported with live layers.
Multiple simultaneous editors. This includes being able to edit the same prims, and even the same attributes, at the exact same time.
High performance. The power of live layers comes from their interactive speeds. However, performance must not come at the cost of correctness.
A USD layer is essentially a hierarchy of key/value pairs. If you were to represent it in JSON, a simple layer with a cube in it might look like this:
{
"SdfSpecType": "PseudoRoot",
"upAxis": "Z",
"defaultPrim": "cube",
"primChildren": [
{
"SdfSpecType": "Prim",
"name": "cube",
"typeName": "Cube",
"properties": [
{
"SdfSpecType": "Attribute",
"name": "extent",
"default": [(-50, -50, -50), (50, 50, 50)]
},
{
"SdfSpecType": "Attribute",
"name": "size",
"default": 100
},
{
"SdfSpecType": "Attribute",
"name": "primvars:displayColor",
"default": [(1, 0, 0)]
},
]
}
]
}
Given the above layer definition, one can easily imagine a system where you send commands between clients such as, “set /cube.size = 75” and that definitely works in simple cases. Indeed, that is exactly what the original prototype for live layers did.
Now let’s consider the various edge cases and how we solved them.
Simultaneous Updating of Fields
Consider the following, where Alice and Bob both update the color of the cube at the same time:
This will work, in the sense that all participants have the same state at the end, but it results in an undesirable flicker for Bob. First his cube turns blue, then red, then back to blue.
We handle this case by ignoring field updates caused by other users until the server has acknowledged our own field update. This is done per-field, so we do still apply updates to fields made by other users as long as we have not also updated that same field.
Receiving Updates Out of Order
It’s possible to receive updates from the server out of order. To prevent the chaos that would ensue, the server includes a sequence number with each update (both remote updates and acknowledgements of your own updates). If a client has update N, then receives update N+2, it will hold that update in a queue until it receives update N+1 (at which point it will process both updates). This is handled by OmniUsdLiveData.
Handling Deleted Nodes
Sometimes a user may be editing a node that another user has simultaneously deleted. In this case, all participants ignore the edit to the deleted node. Note the server still sends an acknowledgement and forwards the edit to the other clients because there can be many different edits in the same message, and they are probably not all going to be ignored.
This is also the case if the edit is more complicated, such as adding a child node to a node that another user has deleted.
Handling Renamed Nodes
If a user edits a node that another user has renamed, we could handle that the same way we handle deleting nodes (by ignoring it), however that’s not a great user experience. More importantly, it can cause divergent state: one user does an edit then a rename, the other user does a rename then ignores the edit. Handling that divergent state is not impossible, but it’s much easier to just handle renames by referring to nodes by a unique ID number rather than by name. We store node parents by ID number as well, so we can also handle moving nodes the same way.
Node ID |
Parent Node |
Name |
---|---|---|
1 |
0 |
/ |
843 |
1 |
cube1 |
257 |
843 |
color |
726 |
843 |
size |
Note
Remember from the example above that attributes are themselves nodes, and the attribute value is named “default” because the attribute may have timesamples. See the documentation on UsdAttribute::Get() for more information about this.
Creating Nodes
In this example, Bob creates a node as a child of cube, and immediately sets a value. At the same time Alice has deleted the cube.
Node ID |
Parent Node |
Name |
---|---|---|
1 |
0 |
/ |
843 |
1 |
cube |
In this case, we see that Bob needs to refer to the node by ID (to set the color) even before the server has acknowledged that he has created it. It’s obvious then that the server cannot assign node IDs.
We considered many different solutions to this problem, including referring to nodes with temporary IDs until the server assigns a permanent one, but ultimately we decided to go a much simpler route of selecting a random 64 bit number. The chance of collision may appear to be N/4 billion (where N is the number of nodes in the layer), however since the client knows which IDs are already in-use, if the selected random number is in-use, we select a new one. This means the real risk of collision is for two different clients to create nodes with the same ID at the exact same time.
Name Collisions
The problem with referring to nodes by number rather than by name is it opens up the possibility that two clients can create different nodes with the same name.
Reconciling this case is easy on the server (just ignore the second node creation), and is also easy to handle on the client that “won” (the client whose node was created first). The client who “lost” (the one whose node creation was ignored by the server) must recognize this case when it receives the remote update for a node creation with the same name, and delete his own node prior to creating the new node.
Children Order
In USD, the order of children nodes matters. USD stores them as a list of names in special fields. For example:
primChildren = ["cube", "sphere", "cone"]
properties = ["size", "color"]
If we just sent that list over like we do with all other attributes, we could easily get into a situation where two clients add child prims at the same time and set the primChildren field to include their own new child prim. Since the server does not merge attributes (it only keeps the most recently set one), the list would be missing a child. Additionally, renaming would also cause problems with this approach.
To solve this, we added an explicit “reorder” command to change the order of child nodes. When USD sets one of the special children fields, we convert that to a reorder command which references the child nodes by ID. The server (and other clients) reorder the child nodes which are listed, and leave any child nodes which are unlisted in the same place they originally were. Child nodes listed in the reorder command which don’t exist are assumed to have been deleted and ignored.