Live Layers Overview

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:

  1. Support the full USD feature set. Anything that can be done in USD is supported with live layers.

  2. Multiple simultaneous editors. This includes being able to edit the same prims, and even the same attributes, at the exact same time.

  3. 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:

sequenceDiagram %%{init: { 'theme': 'forest' } }%% actor A as Alice participant AQ as Alice's Queue participant S as Server participant BQ as Bob's Queue actor B as Bob A->>+AQ: set /cube.color = Red B->>+BQ: set /cube.color = Blue AQ->>-S: Red S-->>A: Ok! /cube.color is Red S->>B: /cube.color is Red BQ->>-S: Blue S-->>B: Ok! /cube.color is Blue S->>A: /cube.color is Blue

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 updaten 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 OmniUsdObjectData.

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 me many different edits in the same message, and they are probably not all going to be ignored.

sequenceDiagram %%{init: { 'theme': 'forest' } }%% actor A as Alice participant AQ as Alice's Queue participant S as Server participant BQ as Bob's Queue actor B as Bob A->>+AQ: Delete /cube B->>+BQ: set /cube.color = Blue AQ->>-S: .. S-->>A: Ok! S->>B: /cube is deleted BQ->>-S: .. note over S: Server ignores this S-->>B: Ok! /cube.color = Blue note left of B: Bob ignores this S->>A: /cube.color = Blue note right of A: Alice ignores this

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.

Initial State

Node ID

Parent Node

Name

1

0

/

843

1

cube1

257

843

color

726

843

size

sequenceDiagram %%{init: { 'theme': 'forest' } }%% actor A as Alice participant AQ as Alice's Queue participant S as Server participant BQ as Bob's Queue actor B as Bob A->>+AQ: Rename Node 843 to cube2 B->>+BQ: Set Node 257 default = Blue AQ->>-S: .. S-->>A: Ok! S->>B: Node 843 renamed to cube2 BQ->>-S: .. S-->>B: Ok! S->>A: Set Node 257 default = Blue

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.

Initial State

Node ID

Parent Node

Name

1

0

/

843

1

cube

sequenceDiagram %%{init: { 'theme': 'forest' } }%% actor A as Alice participant AQ as Alice's Queue participant S as Server participant BQ as Bob's Queue actor B as Bob A->>+AQ: Delete Node 843 B->>+BQ: Create Node 257 (parent=843, name=color) B->>+BQ: Set Node 257 default = Blue AQ->>-S: .. S-->>A: Ok! S->>B: Node 843 deleted BQ->>-S: .. S-->>B: Ok! BQ->>-S: .. S-->>B: Ok!

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

sequenceDiagram %%{init: { 'theme': 'forest' } }%% actor A as Alice participant AQ as Alice's Queue participant S as Server participant BQ as Bob's Queue actor B as Bob A->>+AQ: Create Node 843 (name=cube) B->>+BQ: Create Node 348 (name=cube) AQ->>-S: .. S-->>A: Ok! S->>B: Create Node 843 (name=cube) BQ->>-S: ..

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.