Sessions

Sessions are a central element of Met.3D. A session is the container for all state that makes up one user’s visualization setup. It owns the session-specific objects, coordinates their lifecycle, provides access between them, and handles persistence. This includes the objects used to build the visualization as well as the window layout and other session-wide settings. Global resources such as datasets are not owned by the session, but the session tracks which of them are required.

This enables us to inform the user when re-opening the session, which datasets are required for the session to successfully load. Normally, datasets are global objects in the system and are not part of a session. Instead, the session provides access to the global datasets, and stores references to datasets accessed through it.

In this documentation, session refers to an instance of MSession, while session object refers to an object that is owned by a session.

Implementation

Sessions are implemented as the MSession class in gxfw/msession.h.

They contain save and load logic, store all session objects and provide access to them. Additionally, they provide access to global datasets and store references to accessed datasets.

Using Qt’s signal-slot mechanism, it also provides several signals for changes that occurred in the session. For example creating an actor will emit a signal, so that other objects, that are part of the session, can react to that.

Objects stored in a session do not share a common parent class. This was for legacy reasons.

Data

The data stored inside the session is not stored in the MSession instance directly. Instead, it has a nested struct that contains the session data, called MSession::Data. This data struct is a member of the session object and is the ownership container for all dynamically allocated session objects. It implements a destructor that cleans up all objects stored in the data struct.

This is an important design decision. When creating a session from file, the file might be corrupt. Since that is happening in the constructor of the session, we want to cancel the construction by throwing an exception. This causes stack unwinding, which automatically calls the destructors of all fully constructed members of the session object. If some objects were already loaded from file and created on the heap, these would not clean up via the stack unwinding. Because the Data destructor is called, it will delete all objects already stored in the session. This will prevent an otherwise possible memory leak.

For objects whose lifetime is managed by the session, the corresponding storage should be added to MSession::Data. Additionally, they should be cleaned up in its destructor.

Storing objects in a session

Objects are stored in the MSession::Data struct of the session. There are currently a number of different objects already part of a session:

  • Actors (Includes a default scene light)

  • Scenes

  • Views

  • Time and Ensemble synchronization controls

  • Bonding boxes

  • Waypoints

  • Camera animations

  • Animation settings

Most are stored as lists or maps, as multiple of them can be part of a session.

Typically, the session object provides methods to create, get, and remove objects stored in a session. New session-owned objects should be created and destroyed by the session.

Some existing objects predate this architecture and are instead created elsewhere and registered with the session. Still, the session takes ownership once the object is registered with it.

When working with an existing object, check its MSession interface to determine which convention it follows. Actors follow the legacy path because they are created by actor factories. Making the session responsible for creating actors would require duplicating the actor factory interface in MSession to support the different ways actors can be created. Still, after registering them with the session, they are owned by it and stored in MSession::Data.

Additionally, the session provides signals for creation / registration and destruction of objects stored within. There are also some extra signals for renames of relevant objects.

Generally, the lifecycle of a session object that is stored in a session is as follows:

  1. Created by session or registered to it.

  2. Session has ownership over object.

  3. Session object is accessible by other objects of the session.

  4. Session emits creation signal.

The creation signal is emitted only after the object has been registered and is accessible through the session. It is useful if another session object provides a selection of existing session objects. For example, an actor might provide a selection of transfer function actors, so it needs to know when a new transfer function was created.

The object can be accessed through the session’s getters by other session objects. These do not transfer ownership to the caller. They remain valid until the removal process begins. The object is still alive while the removal signal is emitted, but it must not be retained after the signal returns. Removing the object from the session before emitting the signal ensures that signal handlers no longer see the object as a session object, while the object itself remains alive so that handlers can still access it during cleanup.

Then, when the session object should be destroyed:

  1. Remove session object from session.

  2. Emit removed signal.

  3. Delete the session object itself.

The object is removed from the session before the removal signal is emitted, but remains alive while the signal is being processed. It is deleted after the signal has been emitted.

A session also creates a default scene light actor. It is used as the default light source for scenes created in the session. This ensures that newly created scenes have a usable lighting setup without each scene having to create and manage its own default light. This is not deletable by the user.

The session also stores the window layout, so reopening a session restores the arrangement of the application’s views and windows.

Saving and Loading

The session contains multiple member functions to save and load the session to and from different targets.

These are:

  • MSession::saveSessionZipTo(QString) – ZIP files for snapshot sessions.

  • MSession::saveSessionConfigTo(QString) – Plain text .session.config files for simple sessions.

The different save/load functions handle the file format. MSession::saveToConfiguration() and MSession::loadFromConfiguration() contain the actual serialization logic. These use QSettings to actually store the session save data.

Each type of session object should be stored into their own subgroup within the QSettings object. For example:

Listing 21 Example: Saving camera sequences to the session file.
 1MSession::saveToConfiguration(QSettings *config)
 2{
 3    // ...
 4    // Camera Sequences.
 5
 6    // ==========================================
 7    config->beginGroup("CameraSequences");
 8    config->beginWriteArray("sequences");
 9
10    for (int i = 0; i < data.cameraSequences.count(); i++)
11    {
12        config->setArrayIndex(i);
13        data.cameraSequences.at(i)->saveToSettings(config);
14    }
15
16    config->endArray();
17    config->endGroup();
18    // ==========================================
19
20    // ...
21}

The session loader creates one progress handle per top-level configuration group. When adding a new session object group, remove its corresponding progress handle after loading it.

Listing 22 Example: Loading camera sequences from the session file.
 1MSession::loadFromConfiguration(QSettings *config)
 2{
 3    // ...
 4
 5    QString taskName = "Loading session...";
 6    auto progressHandles = MProgressBar::getInstance()->addTasks(config->childGroups().size(), taskName);
 7
 8    // ...
 9
10    // Camera Sequences.
11    // ==========================================
12    config->beginGroup("CameraSequences");
13    LOG4CPLUS_INFO(mlog, "\t - Loading camera sequences.");
14
15    int numSequences = config->beginReadArray("sequences");
16    for (int i = 0; i < numSequences; i++)
17    {
18        config->setArrayIndex(i);
19        auto *sequence = createCameraSequence();
20        sequence->loadFromSettings(config);
21    }
22
23    config->endArray();
24    config->endGroup();
25    progressHandles.removeFirst();
26    // ==========================================
27
28    // ...
29}

The order in which session objects are loaded is important, as objects loaded later can reference objects loaded earlier. For example, loading a scene requires all actors to be loaded first, as they are referenced by the scene. When adding a new session object that references another session object, its load section must come after the referenced object’s load section.

The save order should mirror it for readability.

Versioning

Session files store the version of Met.3D that was used to write it. It is read via MVersion::readConfigVersion(config) and returns a MVersion for ease-of-use. The version can be used when loading a session requires special handling for a format change that cannot otherwise be made backwards compatible.

Renaming keys or groups in the config would break backwards compatibility with existing configs. This should be avoided if possible. If it is not possible, create a code path that can load the previous version’s config files. If that is also not possible, inform the user and give the option to cancel loading via a message box.

Adding a new object type is backwards compatible. Opening a newer session in an older Met.3D version can cause that object to be lost when the session is subsequently saved. For older configs the new object group just does not exist and is treated as if no objects were loaded. For configs that contain that object but are opened in an old version, the group is simply ignored, and the objects will be missing. If the session is subsequently saved by the older version, the unknown data is lost because the older version cannot deserialize or serialize it.

Global dataset references

Datasets are global and are not owned by a session. A session nevertheless needs to know which datasets its objects depend on so that those datasets can be restored when loading the session. MSession::DataSourceHandle<T> represents a session-scoped reference to one global dataset. The session tracks a dataset as long as at least one session object references it.

Session objects should not take ownership of global datasets directly. They should always obtain a DataSourceHandle<T> from their session.

The handle contains a reference to the session and the dataset. Multiple session objects can reference the same global dataset, requiring us to track how many currently reference it. The session stores all referenced datasets and their reference count in MSession::Data::referencedDataSources. The handle has access to that member to manipulate the reference counts.

The session provides access to global datasets via MSession::getDataSource<T>(QString).

When an object, that is part of a session, wants to reference a dataset, it should acquire such a handle from its session. When a DataSourceHandle<T> is destroyed, it decreases the reference count in MSession::Data::referencedDataSources. If the count reaches 0, the dataset is removed from the session’s list of referenced datasets.

When the session is saved to file, all referenced dataset names will also be written to file. After loading the list of referenced dataset names, the user is presented with a dialog if any datasets are missing. The user can then load the missing datasets, discard the dataset references, or cancel the loading process.

Obtaining a DataSourceHandle<T> is therefore not only a way to access the dataset; it also records the dependency in the session. Code that directly accesses a global dataset without obtaining a handle does not create a session dependency.

Obtaining the session

All session objects should be passed their session in their constructor. They should also provide a getter for the session. Since session objects do not share a common base class, this convention is up to the developer to implement.

Session objects should use their stored session reference when they need to access other session objects or session resources. They should not use the current session for this, as the current session may change independently of the lifetime of the session object.

If you need to access the current session outside a session object or its scope, you can use MMainWindow::getCurrentSession(). This returns the currently open session.

Adding a new session object

Adding a new session object to a session requires the following additions:

  1. A new session object class

  2. A constructor that accepts the owning session as parameter.

  3. Expose session via getters on your session object.

  4. Storage for the session objects to MSession::Data.

  5. Creation/access/removal methods to MSession.

  6. Creation and removal signals.

  7. Serialization in MSession::saveToConfiguration().

  8. Deserialization in MSession::loadFromConfiguration(), respecting dependency order.

  9. Cleanup in MSession::Data destructor.