Transaction Management

This chapter will detail the way HOOPS Luminate uses transactions. Transactions are used to record and apply changes to data managed by the engine. This mechanism is at the heart of the HOOPS Luminate design, and all the engine APIs are relying on it. From this, we derive two classic application architectures, detailed in these pages:

  • Single-Threaded Graphic Applications: This page covers all the details to build a single threaded UI application interacting with HOOPS Luminate. Most applications can be based on this simple model.

  • Multi-Threaded Performance Critical Applications: This page covers in detail the application architecture that can be put in place to sustain the performance needs of a time critical application such as a simulation engine or a video game.

We’ll also review some specific points in detail here:

  • Software Rendering and Parallel Data Edition: a HOOPS Luminate application can render an image in software and continue its work on other data in parallel. All the details are reviewed in this page.

  • Modifying Images During a Draw: Images are special HOOPS Luminate objects, that are only enforcing part of the transaction model. Consequently, there are some specifics to be known about images and how they can be edited.

What is a Transaction?

A transaction in HOOPS Luminate terminology is ‘all what happens’ between a RED::IResourceManager::BeginState and a RED::IResourceManager::EndState call. The contents of a frame to be rendered is defined by those changes that have happened during the transaction. The RED::State class is the transaction object that will be used by all API methods to write data to HOOPS Luminate:

Writing to HOOPS Luminate Objects using a Transaction

An application needs a HOOPS Luminate transaction handle to perform any change into HOOPS Luminate. A transaction is opened using RED::IResourceManager::BeginState, and is ended using RED::IResourceManager::EndState. A transaction should only be closed after all needed changes have been made for the current frame, before drawing.

RED::Object* resmgr = RED::Factory::CreateInstance( CID_REDResourceManager );
RED::IResourceManager* iresmgr = resmgr->As< RED::IResourceManager >();

// Open a transaction to start defining a frame:
const RED::State& state = iresmgr->BeginState();

// Do some changes in HOOPS Luminate (for instance, set an option value in a 'camera' object):
RED::IOptions* ioptions = camera->As< RED::IOptions >();
RC_TEST( ioptions->SetOptionValue( RED::OPTIONS_POLYGON_FILL_MODE, 1, state ) );

// Do all other changes that have to be done by the application.

// Close the transaction:
RC_TEST( iresmgr->EndState() );

// Call the rendering methods (for instance, we'll draw a window 'window'):
RED::IWindow* iwindow = window->As< RED::IWindow >();
RC_TEST( iwindow->FrameDrawing() );

// Repeat the process: reopen another transaction to start defining the next frame...

A transaction that is not closed will not be rendered. All the changes made during the course of the transaction will not take effect if the transaction is not closed.

Using Transactions to Write to HOOPS Luminate

There are two categories of objects in HOOPS Luminate: those that are using transaction and atomic objects that are not using transactions. Generally speaking, atomic objects can be created on the stack and are the parameters manipulated by transactional classes. Let’s illustrate this using a simple example with transformation shapes and matrices:

// Access our resource manager, create a transaction:
RED::Object* resmgr = RED::Factory::CreateInstance( CID_REDResourceManager );
RED::IResourceManager* iresmgr = resmgr->As< RED::IResourceManager >();
const RED::State& state = iresmgr->BeginState();

// Assume that 'node' is a transform shape, that has a single teapot as child geometry:
RED::ITransformShape* inode = node->As< RED::ITransformShape >();

// Create a matrix on the stack that define a transaction (stateless):
RED::Matrix matrix;
matrix.SetTranslation( RED::Vector3( 100.0, 100.0, 0.0 ) );

// Assign this matrix to the transform using the transaction:
RC_TEST( inode->SetMatrix( &matrix, state ) );

// Keep track of the transaction number (will be used later on, not now):
int num_state = state.GetNumber();

// Close the transaction to validate the changes before rendering:
RC_TEST( iresmgr->EndState() );

// Let be 'iwindow' our RED::IWindow interface. Let's draw:
RC_TEST( iwindow->FrameDrawing() );

The matrix is a stateless, atomic object. The RED::ITransformShape interface points to an object which is transaction managed.

../../../_images/using_transactions_good.png

Transaction used the right way

Here, we open the transaction, we do the changes, we close the transaction and we draw. The RED::IWindow::FrameDrawing call reflect the changes because the transaction was closed before rendering. Now, if we don’t close the transaction using RED::IResourceManager::EndState before drawing, the object will not move!

Using Transaction Numbers to Read from HOOPS Luminate

We have seen above how we could write data to HOOPS Luminate using a transaction. For instance, RED::ITransformShape::SetMatrix uses a RED::State transaction. Now if we want to query the transformation matrix which is set in the transform shape of our example, how should we proceed? The answer is quite simple:

// Access the matrix that was set in the 'node' transformation shape:
const RED::Matrix* matrix2;
RC_TEST( inode->GetMatrix( matrix2 ) );

We can use the RED::ITransformShape::GetMatrix that matches the corresponding Set. This method does not require any parameter other than the matrix that we do want to query. Here, we’ll retrieve a ‘matrix2’ value equal to ‘matrix’. However, it has an optional ‘iStateNumber’ parameter. This parameter is by default set to -1 in all HOOPS Luminate API methods. If set to -1, it indicates to access the most recent version of the matrix that was set in the object. If set to a given transaction number, it can be used to retrieve the node’s matrix value that was set in the past:

// Same as above, '-1' tells the engine to retrieve the last version of the matrix:
RC_TEST( inode->GetMatrix( matrix2, -1 ) );

// Query the older version of the matrix. This is the version of the matrix before 'num_state':
const RED::Matrix* matrix3;
RC_TEST( inode->GetMatrix( matrix3, num_state - 1 ) );

We have defined a new matrix with a translation during the transaction ‘num_state’. So if we do a query for a transaction number before ‘num_state’, we’ll retrieve the value of the matrix before the operation.

In HOOPS Luminate, all transaction managed objects keep two versions of their data: the current version which is defined during the current transaction, and the previous one. This choice allows a very efficient design for time critical applications, as we’ll review here: Multi-Threaded Performance Critical Applications.

So, querying at other transaction numbers before ‘num_state -1’ is pointless. The engine only recall the current and previous version of transaction managed data.

Transaction Workflow Errors

The most common mistake in transaction management is generally due to an attempt to write to a HOOPS Luminate object without having an open transaction available. 99.99% of the times, this results in a RED_WORKFLOW_ERROR being returned by the HOOPS Luminate API. A call to RED::IResourceManager::BeginState must be done before any change can occur in HOOPS Luminate data.

Multiple Transaction Errors

From a strict API standpoint, it’s not forbidden to open and close multiple transactions before calling a rendering method, like in the example below:

iresmgr->BeginState();
// Do changes...
RC_TEST( iresmgr->EndState() );

iresmgr->BeginState();
// Do changes...
RC_TEST( iresmgr->EndState() );

iresmgr->BeginState();
// Do changes...
RC_TEST( iresmgr->EndState() );

// Draw. This is too late! Two transactions have been wasted!
RC_TEST( iwindow->FrameDrawing() );

However, this will result in bad performances for the application. Each time RED::IResourceManager::EndState is being called, the whole engine data cluster is being checked for updates. This can be a time consuming process. So as a general rule, please make sure that every transaction is actually rendered by at least one window, to avoid wasting time. A transaction whose contents are not rendered is useless.

Redundancy in Opening or Closing Transactions

It may be difficult for an application to know whether a transaction is opened or not. For instance, user interface callbacks methods are called after user interactions, so a transaction may be open or not, sometimes, it’s not easy to know. In this case, it may be worth mentioning the fact that a transaction can be opened several times or closed several times with no effect:

const RED::State& state = iresmgr->BeginState();
iresmgr->BeginState(); // No effect.
iresmgr->BeginState(); // No effect.

// Do changes...

RC_TEST( iresmgr->EndState() );
RC_TEST( iresmgr->EndState() ); // No effect
RC_TEST( iresmgr->EndState() ); // No effect

So instead of calling RED::IResourceManager::GetState to retrieve the current transaction, if an application method needs to be 100% sure that a transaction is opened, it can call RED::IResourceManager::BeginState: the call will open the transaction if it’s not opened yet, and simply return the current transaction if it’s opened already.