Serialization

NVIDIA PhysX SDK

Serialization

Serialization is the process by which a collection of PhysX objects is stored in a persistent form outside the PhysX runtime, such as on disk. Deserialization is the reverse process, i.e. the loading of those objects into another instance of the PhysX runtime. PhysX 3 features two serialization APIs:

  • API-level serialization to RepX, a versioned XML data format.
  • Binary serialization, which serializes objects into a block of memory from which the PhysX runtime can later load them without allocation or copying.

Both serialization systems use their own meta data representation of PhysX data structures. The meta data used by RepX captures the PhysX objects at the API-level. It allows forward conversions of serialized data from earlier PhysX 3 versions to later ones. The binary serialization meta data captures the internal data structures of PhysX objects and can be used to convert binary representations between different platforms.

Note

cooking also generates a binary output stream. However the primary purpose of cooking is to translate from a user format to a format suitable for the SDK runtime, and so it is not considered a serialization mechanism. Loading a cooked mesh from a stream involves allocation and endian conversion, so is much less efficient than PhysX' binary serialization mechanism. See Shapes and Geometries for more details about cooking.

Binary Serialization

Binary Serialization allows the creation of memory blocks from which PhysX can later construct objects. The PhysX runtime constructs the objects in place, making this an efficient mechanism for loading objects. You may instance collections of objects simply by making multiple copies of a memory block and deserializing them.

The data is specific to a platform and SDK version. When exported from the runtime it is always targeted at the platform on which it was created, although PhysX can retarget it at another platform in a post-processing step. This allows the conversion of binary assets from authoring platforms (Windows, MacOs and Linux) to other platforms.

Framework Classes

  • PxSerializable is the base class for the objects that can be serialized.
  • PxCollection is a collection of PxSerializable objects.
  • PxSerialObjectRef is a 64 bit type, which is used as a reference to a serialized object.
  • PxUserReferences is a map from object references to serializable objects.

Serializing Objects

The simplest scenario is serializing a complete object graph (for example, an actor, its shapes, and the materials and meshes they reference.)

To serialize objects, add them to a collection:

PxRigidDynamic* dynamic = PxCreateDynamic(...);                // create a rigid dynamic

...

PxCollection* collection = physics->createCollection();
dynamic->collectForExport(*collection);                        // add it to the collection

...

material->collectForExport(*collection);                       // for each material referenced by an actor in the collection

...

mesh->collectForExport(*collection);                           // for each mesh referenced by an actor in the collection

In general, you need to manually add to a collection all of the objects which you want serialized. However, for certain objects collectForExport() automatically adds other objects to the collection

Rigid Actors shapes owned by the actor
Articulations links and joints owned by the articulation
Cloth the cloth fabric

When all the objects have been added, create an implementation of the PxOutputStream interface, then serialize the collection:

PxOutputStream& s = ...;                                       // implemented by the application
collection->serialize(s);
collection->release();

To deserialize, first create a collection, then populate it by deserializing from a memory block:

void* memory128 = ...;                                         // a 128-byte aligned buffer previously loaded from disk by the user
PxCollection* collection = physics->createCollection();
collection->deserialize(memory128, NULL, NULL);

To add all the objects to the scene and release the collection:

physics->addCollection(*collection, scene);
collection->release();

Memory Management

Management of memory blocks containing deserialized objects is left to users. It is the user's responsibility to:

  • allocate the memory block. Note that it must be properly aligned, to a PX_SERIAL_FILE_ALIGN (128) bytes boundary.
  • fill the block with serialized data, typically by loading it from disk.
  • deallocate the memory block when the objects within have been released by PhysX.

Although the user owns the memory block, the PhysX runtime owns any deserialized objects it contains. Concretely, calling release() on an object that was created by deserialization will cause its destructor to run, but will not deallocate its memory. If you deallocate the block before the destructors have run for all the objects it contains, the PhysX runtime will likely crash.

Traversing Collections

You can iterate over a collection, for example to ensure the objects you intend to serialize have all been added by collectForExport(). When doing so you can use PhysX' dynamic typing mechanism to classify the objects:

PxCollection* collection;
PxU32 size = collection->getNbObjects();
for(PxU32 i=0; i<size; i++)
{
    PxSerializable* object = collection->getObject(i);
    if(!object->is<PxActor>())
       continue;

    switch((PxConcreteType)object->getConcreteType())
    {
    case PxConcreteType::eRIGID_DYNAMIC:
    ...
    }
}

Partial Serialization

The above code in (serializingObjects) serializes complete object graphs. Another common use case is where a collection of actors and joints - say, a rag doll - will be deserialized multiple times, with each instance sharing the same materials and meshes. To achieve this, serialize two collections:

  • a collection of the materials and meshes that will be deserialized just once
  • a collection of actors and joints which will be copied and deserialized multiple times

The second of these will be a partial object graph: there will be some objects which are not serialized with the collection, but to which the collection will contain references.

The application has to take two steps in order to deal with partial object graphs:

  1. On serialization: Provide consistent reference identities (PxSerialObjectRef) to serializable objects for the referencing and referenced collections.
  2. On deserialization: Provide the referencing collections with appropriate information to reestablish the references to the deserialized objects in referenced collections.

PxCollection provides two functions for declaring object references before serializing:

  • PxCollection::setObjectRef specifies a reference to an object that is in the collection. The reference will be serialized along with the collection and recreated when it is deserialized to support lookup for objects within the collection.
  • PxCollection::addExternalRef specifies a reference for an object that will not be serialized with the collection, but is referenced by an object the collection contains. You must provide a lookup for this reference when deserializing the collection.

They are used as follows:

PxConvexMesh** convexes;        // An array of mNbConvexes convexes
PxRigidDynamic** actors;        // An array of mNbConvexes actors referencing the convexes

PxPhysics* physics;             // The physics SDK object
PxOutputStream& convexStream;   // Output stream for the convex collection
PxOutputStream& actorStream;    // Output stream for the actor collection

PxCollection* convexCollection = physics->createCollection();
PxCollection* actorCollection = physics->createCollection();

for(PxU32 i=0;i<mNbConvexes;i++)
{
    convexes[i]->collectForExport(*convexCollection);
    convexCollection->setObjectRef(convexes[i], (PxSerialObjectRef)i);        // a 'name' for the convex in the convex collection
    actorCollection->addExternalRef(convexes[i], (PxSerialObjectRef)i);       // a 'resolve-on-deserialization' reference to the convex in the actor collection
}

// serialize the convexes and the references in their collection
convexCollection->serialize(convexStream);
convexCollection->release();

// Add actors to collection
for(PxU32 i=0;i<mNbConvexes;i++)
    actors[i]->collectForExport(*actorCollection);
actorCollection->serialize(actorStream);
actorCollection->release();

PhysX 3 expects references (PxSerialObjectRef) to be unique per collection. The application has to make sure they are consistent across different collections for making partial object graphs deserialize correctly. On deserialization PxUserReferences container objects can be used to handle associations between references and deserialized objects. When you deserialize a collection you may supply

  • a PxUserReferences object to the deserializer to resolve the collection's external references.
  • a PxUserReferences object which the deserializer populates with the object references that were serialized with the collection.

The two PxUserReferences objects are optional arguments to the deserializer, and may be the same. To deserialize the collections:

PxPhysics* physics;             // The physics SDK object
PxScene* scene;                 // the scene into which the objects will be inserted
void* convexMemory128;          // aligned memory containing serialized convexes
void* actorMemory128;           // aligned memory containing serialized actors

PxCollection* convexCollection = physics->createCollection();
PxCollection* actorCollection = physics->createCollection();
PxUserReferences* convexRefs = physics->createUserReferences();

// deserialize the convexes, populating convexRefs with the serialized references
// that where specified with PxCollection::setObjectRef

convexCollection->deserialize(convexMemory128, convexRefs, NULL);
physics->addCollection(*convexCollection, scene);
convexCollection->release();

// deserialize the actors, using convexRefs to resolve references
// that where specified with PxCollection::addExternalRef

actorCollection->deserialize(actorMemory128, NULL, convexRefs);
physics->addCollection(*actorCollection, scene);
actorCollection->release();

convexRefs->release();

If there are references in the collection to objects not contained within it, and they cannot be resolved using the PxUserReferences passed at deserialization time, an error occurs and deserialization is aborted.

You can add references to a PxUserReferences object manually with PxUserReferences::setObjectRef(...), as well as via deserialization. This may be useful, for example, if you are using a mixture of serialized and procedurally created objects - for example, a predefined material library for your application that is not itself serialized but which serialized objects must reference.

You can also use PxUserReferences to find objects in a collection in order to fix up references with gameplay objects:

PxPhysics* physics;             // The physics SDK object
void* memory128;                // aligned memory containing serialized objects

PxCollection* collection = physics->createCollection();
PxUserReferences* userRefs = physics->createUserReferences();

// deserialize objects and fill userRefs with objects for which
// PxCollection::setObjectRef was called before serialization
collection->deserialize(memory128, userRefs, NULL);

// receive a list of all deserialized objects which have user references
#define MAX_USER_REFS 100
PxSerialObjectAndRef userRefBuffer[MAX_USER_REFS];
userRefs->getObjectRefs(userRefBuffer, MAX_USER_REFS);

// iterate over the list to path up gameplay objects
for (PxU32 i = 0; i < userRefs->getNbObjectRefs(); i++)
{
    PxActor* actor = userRefBuffer[i].serializable->is<PxActor>();
    if (actor)
    {
        // this assumes that findGamePlayObjectFromRef is able to locate
        // the corresponding game play object from a PxSerialObjectRef
        actor->userData = findGamePlayObjectFromRef(userRefBuffer[i].ref);
    }
}

In order to iterate over user references and external references of a collection the following two methods can be used:

  • PxCollection::getObjectRefs creates a PxUserReferences instance containing all the user references that where set with PxCollection::setObjectRef(...).
  • PxCollection::getExternalRefs creates a PxUserReferences instance containing all the external references that where added with PxCollection::addExternalRef(...).

Note that both methods don't provide the corresponding references for deserialized collections. Serialized user references can only be obtained by the first PxUserReferences argument to PxCollection::deserialize(...). The set of external references that are needed to deserialize a collection is expected to be managed by the application. There is currently no support to query the external references needed to deserialize a collection.

Serializing Everything

PhysX provides two utility functions for serializing the entirety of the PhysX runtime: PxCollectForExportSDK and PxCollectForExportScene:

PxPhysics* physics;    // The physics SDK object
PxScene* scene;        // The physics scene
PxOutputStream& s;     // The user-defined stream doing the actual write to disk

// 1) create a collection
PxCollection* collection = physics->createCollection();

// 2) collect objects to serialize
PxCollectForExportSDK(*physics, *collection);    // Collects all objects from the physics SDK.
PxCollectForExportScene(*scene, *collection);    // Collects all objects from the scene.

// 3) serialize collection and release it
collection->serialize(s);
collection->release();

Deserialization is as previously:

PxPhysics* physics;        // The physics SDK object
PxScene* scene;            // The physics scene

void* memory128 = ...;     // a 128-byte aligned buffer previously loaded from disk by the user
PxCollection* collection = physics->createCollection();
collection->deserialize(memory128, NULL, NULL);
physics->addCollection(*collection, scene);
collection->release();

Object Names

Some objects, such as shapes and actors, can be given names using the PxShape::setName() and PxActor::setName() functions. The SDK does not own those names, i.e. the strings remain in user memory. When serializing objects, you may choose whether to preserve those names in the serialized data, or to discard them to create smaller data. If you choose to preserve the names, they will be serialized along with the objects themselves when calling PxCollection::serialize(). On deserialization, the names will live within the user-provided memory block.

Use the 'exportNames' parameter of the PxCollection::serialize() function to control this behavior.

Retargeting

Binary serialized data is platform-specific, and when serialized it always targets the platform on which it was created. The binary converter in the cooking library retargets data from one platform to another. So to deploy data for PS3, XBox etc, typically you will serialize on PC, then use the converter to retarget for each platform.

The converter requires meta-data for the source and target platforms, which contains information about the binary layout of objects for that platform. To obtain metadata, use the function provided in the extensions library for each platform:

void PxDumpMetaData(PxOutputStream& stream, const PxPhysics& physics);

On each target platform, run it once and keep generated data around.

Assuming you have initialized the cooking library, convert data as follows:

PxErrorCallback* myErrorCallback;       // an error callback implemented by the application
PxInputStream& srcMetadata;             // metadata for the 'from' platform
PxInputStream& dstMetadata;             // metadata for the 'to' platform

PxInputStream& srcAsset;                // stream containing source asset
PxU32 srcAssetSize;                     // size of the source asset
PxOutputStream& dstAsset;               // output stream for retargeted asset

PxBinaryConverter* converter = cooking->createBinaryConverter(myErrorCallback);
converter->setMetaData(srcMetadata, dstMetadata);
converter->convert(srcAsset, srcAssetSize, dstAsset);

API-level Serialization with RepX

RepX stands for Representation X and is the XML serialization format for PhysX 3. This format is intended to be a user-level format meaning the data is in a format that matches the public API. It is also intended to be backwards compatible, thus assets saved in RepX for version 3.0 of PhysX will load in version 3.1, 3.2, etc. We do not intend for RepX to be used in performance critical or memory constrained situations.

RepX itself consist of a core module with a set of extensions. Extensions are responsible for transforming objects coming from the outside world (called 'live' objects) into a sort of key-value pair format. The library takes care of serializing/deserializing this format. A RepX collection is a set of objects that are transformed into the key-value pair format but still held in memory.

RepX collections may depend on objects in other collections in order to be fully realized into live objects. To facilitate this, RepX, similar to the binary serialization system, has an Id assignment system where 64 bit identifiers are assigned to objects. This happens when a live object is added to a collection and the id defaults to the memory address of the object. This design was intended so that one would serialize various buffers (convex mesh, triangle mesh, height field, etc.) into one collection file, and then using the same id map serialize a set of scene objects into another RepX file. Then the user could deserialize the buffer collection using the original ids once, but deserialize the scene RepX collection multiple times requesting RepX to generate new ids upon deserialization into live objects.

To use RepX, there are two headers you need. The first is RepX.h and this is absolutely required as it describes the base types. The second is RepXUtility.h and this makes using RepX much easier, but requires the PhysX extensions to be loaded to work. Even if you do not intend to use RepXUtility.h we recommend you use it for examples on how to do things like:

  1. Create a collection with the various extensions loaded.
  2. Copy objects into the collection.
  3. Instantiate a collection into a scene.
  4. Convert a RepX collection into a binary collection.
  5. Upgrade a RepX collection from a past version to the current version (requires RepXUpgrader project).

Here is an example:

RepXCollection* theCollection = createCollection(physics.getTolerancesScale(), PxGetFoundation().getAllocatorCallback());
RepXIdToRepXObjectMap* theIdMap =  RepXIdToRepXObjectMap::create(PxGetFoundation().getAllocatorCallback());
addToRepXCollectionNF( theCollection, theIdMap, thePhysicsObject ); //add physcis object
addObjectsToScene(theCollection, physics, cooking, scene, mStringTable );
theCollection->destroy();
theIdMap->destroy();

A RepX (Representation X) collection is PhysX's forward-compatible storage format. Using RepX you can store PhysX assets in a way that we guarantee support for them in future versions of PhysX. RepX also includes facilities for instantiating a set of assets multiple times into a scene and for of course upgrading a RepX collection from an older version to a newer version as well as converting a RepX collection to a PhysX binary collection. Currently RepX's storage format is ASCII-XML.

RepX.h contains the base collection definitions and RepXUtility.h contains functionality that cannot be included in RepX because it relies on PhysXExtensions. Users who have PhysXExtensions compiled into their SDK should use RepXUtility wherever possible and users who do not should still look to the RepXUtility header for examples on how to do the base operations.

RepX is a base key-value data store that is specialized towards the various PhysX datatypes via extensions. An extension needs to provide the capability to go from a 'live' object to a RepX data store value and back. Extensions are created and registered when the collection is created an destroyed when the collection is destroyed. So, to extend RepX to store a different datatype, be it a custom joint or specific game information users will need to implement a RepX extension.

RepX identifies live types via a 'fat pointer' combination of a void* and a const char* type name. This name is used to link the void* pointer to the appropriate extension necessary to serialize the pointer. RepX types also have a user-supplied id that is used to link dependent types to other objects in the collection. This id defaults to the memory address of the object if not supplied. All of the world-to-RepX functions along with the RepX-to-world functions take an id map. One constraint that RepX has is that a base object needs to be added to the collection before any dependent object. Thus PxConvexMesh needs to be added before the PxConvexMeshGeometry that refers to it.

To create a RepX collection, we have provided a few createCollection functions in RepXUtility.h. These functions create all known extensions for both the core RepX types and types that rely on PhysXExtensions.h and then create a new collection. The various overloads are for passing in custom allocators and for creating a collection and immediately deserializing a data source back to a RepX collection.

Saving to RepX involves making a decision about whether to save the PxPhysics objects (which I will later refer to as 'buffers') separate from the scene objects. Saving them separate adds complexity but gives you the option of instantiating the scene objects, perhaps with different global transforms several times into the PhysX scene.

In any case, we provide three functions in RepXUtility.h which allow various combinations of use cases. These functions build the RepX collection using the object's base address as its id.

  1. addSDKItemsToRepX - add all the buffers present in the PxPhysics object (PxConvexMesh, PxHeightField, PxTriangleMesh and PxMaterial) to the RepX collection.
  2. addSceneItemsToRepX - add all the objects in a given PxScene (PxRigidDynamic, PxRigidStatic, PxArticulation, any PxJoints, etc.), to a RepX collection.
  3. addItemsToRepX - calls addSDKItemsToRepX followed by addSceneItemsToRepX.

When RepX instantiates an object it asks the appropriate extension to create a live object. This is achieved through use of the given key-value data object. RepX then calls a callback passed into the instantiation function with the newly created object. It is then the responsibility of the callback to add the object to a PxScene if desired. We have wrapped up the common case of this operation with a function addObjectsToScene.

For the use case where you want to instantiate buffer and scene RepXCollections separately, you would need to call a more low level function (also available in RepXUtility.h), instantiateCollection for the scene objects with a flag, inAddOriginalIdsToObjectMap set to false. This tells RepX to just use the newly generated object's address as its id. You could call this several times with the same scene RepX collection safely but you need to share the id map between all instantiations so that RepX can find the buffers instantiated with inAddOriginalIdsToObjectMap set to true.

Upgrading a RepX collection from an older version to a newer one is easy. You need to link with the RepXUpgrader static library and call the appropriate physx::repx::RepXUpgrader::upgradeCollection function.

Implementing your own custom extension is a bit more involved. You need to assign an ascii name to your extension and implement the RepXExtension interface. You will then need to add that extension to the rest of the extensions when a RepX collection is created. When your target objects are added to the RepXCollection, they will need to be tagged with your extension's ascii name.

Converting between RepX and Binary Serializable Collections

PhysX provides a couple of convenience functions in RepXUtility.h to easen the conversion between binary and RepX data.

  • addObjectsToPxCollection(...) takes a RepXCollection and instantiates the objects contained in the physics SDK. In the process it creates two PxCollection instances, one for PxPhysics serializables and one for PxScene serializables. Optionally a PxUserReferences can be provided that is populated with RepX ids of PxPhysics serializables. Note that unlike with PxCollection::deserialize(...) the resulting PxCollection instances will also have the deserialized ids available with PxCollection::getObjectRefs(...). Additionally, the PxCollection containing the PxScene serializables will have the external references added, that can be queried with PxCollection::getExternalRefs.
  • deserializeFromRepX(...) does the same as addObjectsToPxCollection(...) but takes a repX stream as input.
  • pxCollectionToRepXCollection(...) takes a PxCollection and returns a RepXCollection on success. The in/out parameter inAnonymousNameStart serves as the first value used for 64 bit RepX Ids assigned to the resulting RepX objects and is incremented as needed.
  • serializeToRepX(...) does the same as pxCollectionToRepXCollection(...) but additionally serializes the collection to an output stream in RepX format.

Example for serializing a PxCollection to a RepX stream:

PxCollection* collection = physics.createCollection();
PxCollectForExportSDK(physics, *collection); //collect buffer objects in physics
PxCollectForExportScene(scene, *collection); //collect scene level objects

PxDefaultFileOutputStream outStream(pathToRepXFile);
PxU64 start = 0x80000000; //start of reference ids for exported collection objects
serializeToRepX(outStream, collection, start);

collection->release();

Example for deserializing a PxCollection from a RepX stream:

PxDefaultFileInputData data(pathToRepXFile);

PxCollection* bufferCollection = physics.createCollection();
PxCollection* sceneCollection = physics.createCollection();
PxStringTable* stringTable = NULL; //we are not interested in object names here
PxUserReferences* externalRefs = NULL; //we assume there are no external references
PxUserReferences* userRefs = NULL; //would be used to receive references and then pass to dependent deserialization calls

deserializeFromRepX(data, physics, cooking, stringTable, externalRefs, *bufferCollection, *sceneCollection, userRefs);
physics.addCollection(*sceneCollection, scene); //add the scene level objects to the PxScene scene.

bufferCollection->release();
sceneCollection->release();

Example for upgrading a RepX stream:

PxDefaultFileInputData data(pathTo30RepXFile); //load an older 3.x repx file
RepXCollection* collection = createCollection(data, foundation.getAllocatorCallback());
RepXCollection* upgraded = &RepXUpgrader::upgradeCollection(*collection); //upgrade repx file to current sdk version format
PxDefaultFileOutputStream outStream(pathToNewRepXFile);
upgraded->save(outStream); //save the result to file