Implementing a rendering server and client

Introduction

This is a set of two tutorials that you'll find in the [installation folder] / BuildImage / [operating system] filetree:

These tutorials explore a possible usage of REDnet and REDsdk and provide a possible approach to rendering through another process. One fact is that while a rendering occurs in an application, generally speaking, it's rather difficult to continue working or to do something else: this may be due to the fact that all CPU cores are involved in the rendering, but this may also be due to the fact that the host application was not designed to do two tasks in parallel and to let the user continue its work while a rendering occurs in parallel.

Using a client / server architecture in this case can be a convenient way to reduce the complexity of an application while improving its flexibility: in providing the capability to launch a rendering on a server (local or distant), the application can benefit from render batching features as well as from usability features by letting the application fully interactive while the server is rendering one or several images.

Basic architecture

We have proposed something very simple and very limited through these two tutorials: the purpose of the RenderingServer and RenderingClient tutorials is really to draft a skeleton of a client / server architecture based on REDnet. Our scope is then fairly limited:

From this we can draft the tutorials communication mechanism, viewing threads in both the server and the client programs:

A draft diagram of the client / server messages in our implementation

First, we see that we have 4 threads involved: since we have a server and a client, each using 2 threads because of the RNET::IPeer running in each program. So this means that if both programs are locally executed on the host computer, we'll consume up to 4 threads on the host computer. This is too much.

Consequently, we'll use two mechanisms to reduce the server CPU overhead to zero when it idles:

  1. Set a RNET::IPeer::SetPollTimeout to a delay time that'll be the maximum time the network thread will wait listening to other peers. From a network standpoint, this is the time spent in the 'select' operating system call. Please note that we also setup a similar polling timeout in the client to reduce the cost of its network thread, since we don't need a real-time feedback here.
  2. Put the server main thread to sleep while there's no scene to process. Waking it up every second is largely enough to check the availability of incoming scenes to render.

Then, the client will communicate with the server using a very simple mechanism:

  1. Transmit the scene from the client to the server. As shown in the picture above, the transmission starts from the client through RNET::IPeer::SendMessage, goes to the client's network thread, that'll effectively send it to the server. There can be a delay between the transmission and the reception. This is shown by the "network delay" boxes.
  2. On reception, the server will render the scene and continuously send back feedback images of the rendered scene back to the client.

We made this as simple as possible, with no hand shaking messages or the like. Of course, more evolved servers will require a more complex communication system.

Setup the peer-to-peer connection

We don't use a dispatcher to manage the peer-to-peer communication here: we don't need it for such a simple process to process communication. Therefore we simply select our network adapter on the server and start the peer:

RED::String host_name;
RED::Vector< RNET::Adapter > host_adapters;

// REDnet initialization:
RC_ERROR_INFO( RNET::System::Initialize( host_name, host_adapters ), "REDnet failed to initialize!" );

if( host_adapters.empty() == true )
  RC_ERROR_INFO( RED_FAIL, "No host adapter!" );

// Display communication informations:
fprintf( stdout, "\n" );
fprintf( stdout, "Host               : %s\n", host_name.Buffer() );
fprintf( stdout, "Network adapter    : %s\n", host_adapters[ 0 ].GetDescription().Buffer() );
fprintf( stdout, "IP address         : %s\n", host_adapters[ 0 ].GetAddress().ToString().Buffer() );
fprintf( stdout, "\n" );

// Create a peer:
int id = RED::Object::GetIDFromString( "RenderingServer" );

RED::Object* peer = RED::Factory::CreatePeer();
if( peer == NULL )
  RC_ERROR_INFO( RED_ALLOC_FAILURE, "Failed to create a peer!" );

RNET::IPeer* ipeer = peer->As< RNET::IPeer >();

ipeer->SetOnConnectionReady( OnConnectionReady, NULL );
ipeer->SetOnDataReceived( OnDataReceived, NULL );
ipeer->SetPollTimeout( 500 );

// Start the peer:
RC_ERROR_INFO( ipeer->Start( id, 1, "", 0, 18000 ), "REDnet failed to start the peer!" );

We then do something similar on the client side:

RED::String host_name;
RED::Vector< RNET::Adapter > host_adapters;

// REDnet initialization:
RC_ERROR_INFO( RNET::System::Initialize( host_name, host_adapters ), "REDnet failed to initialize!" );

if( host_adapters.empty() == true )
  RC_ERROR_INFO( RED_FAIL, "No host adapter!" );

// Display communication informations:
RFK::TutorialApplication::SetMessage( RED::String( "Our host = %1 - IP address = %2 - Network adapter = %3" )
                                      .Arg( host_name )
                                      .Arg( host_adapters[ 0 ].GetAddress().ToString() )
                                      .Arg( host_adapters[ 0 ].GetDescription() ) );

// Create a peer:
g_peer = RED::Factory::CreatePeer();
if( g_peer == NULL )
  RC_ERROR_INFO( RED_ALLOC_FAILURE, "Failed to create a peer!" );

RNET::IPeer* ipeer = g_peer->As< RNET::IPeer >();

ipeer->SetOnDataReceived( OnDataReceived, NULL );
ipeer->SetPollTimeout( 100 );

// Start the peer:
int id = RED::Object::GetIDFromString( "RenderingClient" );
RC_ERROR_INFO( ipeer->Start( id, 1, "", 0, 18001 ), "REDnet failed to start the peer!" );

// Connect to the rendering server:
RNET::Address addr( RED::String( "%1:18000" ).Arg( host_adapters[ 0 ].GetAddress().ToString() ) );
RC_ERROR( ipeer->ConnectTo( addr ) );

// Wait up to 5 seconds for the connexion to be established:
RED::Timer tim;
RED::Vector< int > connid;

tim.Start();

while( connid.empty() == true && tim.MSElapsed() < 5000.0f )
{
  RC_ERROR( ipeer->GetConnectionsIDList( connid ) );
}

if( connid.empty() == true )
  RC_ERROR_INFO( RED_FAIL, "Could not connect to the rendering server!" );

g_connid = connid[ 0 ];

We start using another port number as the server's one, and we explicitely connect to the server. Finally, we let ourselves 5 seconds to establish the communication between the client and the server. Once we're connected, we have the connection ID that we'll use to send messages.

Sending messages

Message transmission simply occurs using the RNET::IPeer::SendMessage method of the API. Here, we have used custom messages typed as RNET::MST_DATA_CUSTOM, so we can implement any kind of messaging protocol between the two peers.

A word on image streaming using RNET

There are two important points to keep in mind here about images transmitted between the client and the server:

  1. We DON'T use RNET::IPeer::SendImage: this method has been designed for a server to web browser client transmission, and takes into consideration the maximal allowed bandwidth we can afford to keep a minimal transfer delay. Hence, this method can't be used to transmit an uncompressed, raw HDR image between the server and the client.
  2. We do transfer images produced by RED::IWindow::FrameTracingImages instead of final images (that we could get back through render images). The reason for this is to be able to do the tonemapping on the client side, interactively, while the server continues its job. On the client side, received images are then set as the RED::IViewpointRenderList::SetViewpointSoftImages, so that they can be re-used by the REDsdk GPU tonemapping pipeline.