MFSM || user guide

Alexandre R.J. François

Code release version: 0.8
ARJF © 2001-2006

contents

introduction

MFSM (Modular Flow Scheduling Middleware) is an architectural middleware implementing the architectural abstractions defined by the SAI style for design, analysis and implementation of complex integrated systems involving distributed asynchronous processing of generic data streams.

MFSM is an open source project, released under the GNU Lesser General Public License. A core library (named FSF for Flow Scheduling Framework) contains an extensible set of classes that can be specialized to define new data structures and processes, or encapsulate existing ones (e.g. from specialized libraries). Software modules regroup specialized elements that implement specific algorithms or data structures. The mission of the MFSM project encompasses cataloging and publishing of such modules. The MFSM project also publishes extensive documentation, including this user guide, a reference guide and a number of tutorials.

Content

This user guide is an example-based, code-level tutorial on the use of the MFSM architectural middleware for implementing software systems designed in the SAI style.

The guide first presents a gentle introduction to SAI concepts, and specifies the notation conventions adopted throughout the document. A first example illustrates the necessary steps to setup and cleanup an application that uses MFSM, and describes how to instantiate and connect the graph elements. The next sections describe the specification and implementation of custom cells and custom nodes.

A last section (currently under construction...) will demonstrate the use of the barrier synchronization elements that are part of FSF.

Disclaimer

The code appearing in this guide was developed by the author, and is part of open source libraries, modules and examples released under the GNU Lesser General Public License. They are available for download on the MFSM web site. This code is provided here in the hope that it will be useful, but without any warranty; without even the implied warranty of merchantability or fitness for a particular purpose. Complete license information is provided in the downloadable packages.

SAI in a nutshell

The Software Architecture for Immersipresence (SAI) framework (Francois, 2003) offers a unifying approach to the distributed implementation of algorithms and their easy integration into complex systems that exhibit desirable qualities such as efficiency, scalability, extensibility, reusability and interoperability. SAI provides a general formalism for the design, analysis and implementation of complex software systems of asynchronous interacting processing components. The SAI model builds on three pilars: (1) explicit account of time both in data and processing models; (2) distinction between persistent and volatile data; (3) asynchronous parallelism.

SAI specifies a formal architectural style (Francois, 2004) comprised of an extensible data model and an hybrid (shared memory and message-passing) distributed asynchronous parallel processing model. Figure 1 presents an overview of SAI defining elements in their standard notation.

Summary of notation for SAI designs
Figure 1: Overview of SAI elements. Cells are represented as squares, sources as circles. Source-cell connections are drawn as fat lines, while cell-cell connections are drawn as thin arrows crossing over the cells. When color is available, cells are colored in green (reserved for processing); sources, source-cell connections, passive pulses are in colored in red (persistent information); streams and active pulses are colored in blue (volatile information).

In SAI, all data is encapsulated in pulses. A pulse is the carrier for all the synchronous data corresponding to a given time stamp. Information in a pulse is organized as a mono-rooted composition hierarchy of node instances. The nodes constitute an extensible set of atomic data units that implement or encapsulate specific data structures. Pulses holding volatile data flow down streams defined by connections between processing centers called cells, in a message passing fashion. They trigger computations, and are thus called active pulses. In contrast, pulses holding persistent information are held in repositories called sources, where the processing centers can access them in a concurrent shared memory access fashion. Processing in a cell may result in the augmentation of the active pulse (input data), augmentation and/or update of the passive pulse (process parameters). The processing of active pulses is carried in parallel, as they are received by the cell. Data binding is performed dynamically in an operation called {em filtering}. Active and passive filters qualitatively specify, for each cell, the target data in respective pulses. This hybrid model combining message passing and shared repository communication, combined with a unified data model, constitutes a universal processing framework.

A particular system architecture is specified at the conceptual level by a set of source and cell instances, and their inter-connections. The logical level specification of a design describes, for each cell, its active and passive filters and its output structure, and for each source, the structure of its passive pulse.

notations

The graphical notation introduced above is sufficient for specifying systems at the conceptual level--a set of source and cell instances, and their inter-connections. The specialized cells may be accompanied by a description of the task they implement. Source and cell instances may be given names for easy reference. In some cases, important data nodes and outputs may be specified schematically to emphasize some design aspects.

Logical level specifications require additional notation conventions to represent nodes (and pulses) and filters.

Specialized node types are identified by their type name (string). Node instances are identified by their (instance) name. The notation adopted to represent node instances and hierarchies of node instances makes use of nested parentheses, e.g.: (NODE_TYPE_ID ``Node name'' (...) ... ). This notation will be used to specify a cell's output, and for logical specification of active and passive pulses.

The notation adopted for specifying filters and hierarchies of filters is nested square brackets. Each filter specifies a node type, a node instance name or name pattern (with wildcard characters), an optional handle name, and an eventual list of subfilters, e.g.: [NODE_TYPE_ID ``Node name'' handle_id [...] ... ]. Optional filters are indicated by a star, e.g.: [NODE_TYPE_ID ``Node name'' handle_id]*.

The table below summarizes the notations for logical level cell definition using the nested square brackets and nested parentheses notations introduced above for filters and nodes respectively. By convention, in the cell output specification, (x) represents the pulse's root, (.) represents the node corresponding to the root of the active filter, and (..) represents its parent node.
ClassName (ParentClass)CELL_TYPE_ID
Active filter[NODE_TYPE_ID "Node name" [...] ... ]
Passive filter[NODE_TYPE_ID "Node name" [...] ... ]
Output(NODE_TYPE_ID "default output base name--more if needed" (...) ... )

a first example

In this section, a simple example console application, called example1, illustrates all the steps from design to implementation. The code for the example can be downloaded from the MFSM project page on SourceForge.net.

Suppose the goal of the program is to generate a stream at a given frame rate, and display the time stamp of each pulse in seconds and in h/m/s formats. Note that in MFSM version 0.8 and higher, the time stamp is the number of nanoseconds elapsed since the Epoch (00:00:00 UTC, January 1, 1970) on Mac OS X and Linux platforms, and the number of nanoseconds elapsed since the system was last booted on Win32 platforms.

FSF contains a fundamental cell used to generate empty pulses at a given frame rate: the Pulsar. Its specifications are as follows:
fsf::CPulsar (fsf::CCell)FSF_PULSAR
Active filter(no input)
Passive filter[FSF_INT64_NODE "Pulse delay"]
Output(FSF_ACTIVE_PULSE "Root")

A pulsar does not have any input. One parameter, of integral type, specifies the time delay between two consecutive output pulses.

In this section, a cell capable of looking up the time stamp of each incoming active pulse and printing it to the console in a given format, is supposed to be available. The actual implementation of this cell is addressed in the next section about custom cells. The cell specifications are as follows:
myspace::CMyCell (fsf::CCell)MY_CELL
Active filter[FSF_ACTIVE_PULSE "Root"]
Passive filter[FSF_PASSIVE_PULSE "Root"]
Output(no output)

It is straightforward to implement our simple application from from one instance of Pulsar and one instance of MyCell. Figure 4 shows the conceptual system graph.

Conceptual system graph for example1
Figure 4: Conceptual system graph for example1

Two functional units can be identified:

The following subsections explain in detail how to code the simple application in C++: setup the system, build and run the application graph, clean-up the objects allocated.

Setting-up the system

The unique instance of the system class is automatically created when first needed (Singleton pattern). First and foremost, the nodes and cells defined in each module used must be registered with the system. As a result, although FSF is highly multi-threaded, the unique instance will be allocated before multi-threading kicks in, and thus a double lock mechanism is probably not needed in the implementation of the CSystem class.

Factories can be registered in any order. They are not necessary for pre-coded application graphs, although they are used by the scripting module functions (see below). Factories are necessary for dynamic, run-time application graph building and/or modification, which is out of the scope of this introduction. Each module is required to declare and implement a function called RegisterFactories for registering factories in the system for all nodes and cells implemented in the module.

// register system factories
fsf::RegisterFactories();

// register myspace factories
myspace::RegisterFactories();

Building the graph

A scripting module (namespace scripting) provides shortcut functions to instantiate sources and cells, instantiate nodes and place them in source pulses, connect cells to other cells and to sources. The name ``scripting'' comes from the fact that the functions provided by this module are coding equivalents of user actions in an interactive system. In particular, the scripting module uses aspects of the MFSM implementation that are related to dynamic system evolution, such as class factories. Note that the scripting module itself does not implement any node or cell class and thus does not register any factory (there is no scripting::RegisterFactories).

The code for building an application graph instantiates and connects all the elements of the conceptual graph. In this simple example, the graph can be divided into two functional subgraphs: the pulsing unit, built around the Pulsar instance, and the display unit built around the MyCell instance. Each functional subgraph in this case corresponds to one source and one cell (minimal computing units).

Each minimal unit, consisting of one cell and one source whose pulse contains the cell's parameters, can be coded following these steps:

  1. Instantiate the source.
  2. Instantiate the parameter node(s). Each node is placed in the source's passive pulse hierarchy. Optional steps for each node include setting its name and data member initial values.
  3. Instantiate the cell. Optional steps include setting the output base name, the active and passive filters. The cell is then connected to its source, and to the cell directly upstream on the active stream, if any.

These principles can be used as general guidelines and adapted to code any graph. The following code builds the graph for example1, first the pulsing unit, then the display unit. Successful instantiation of all graph elements is checked, as failure to register the appropriate factories will result in the failure to instantiate a given cell or node.

// build graph
bool bSuccess=true;

////////////
// Pulsar //
////////////

// create the source
fsf::CSource *pPSource=new fsf::CSource;
bSuccess &= (pPSource!=NULL);

// parameter nodes
fsf::Int64Node *pPulseRate=static_cast<fsf::Int64Node*>(
    scripting::CreateNode(std::string("FSF_INT64_NODE"),pPSource->GetPulse()));
bSuccess &= (pPulseRate!=NULL);
if(bSuccess){
    // set name
    pPulseRate->SetName(std::string("Pulse delay"));
    // set parameter values
    fsf::Time tPulseDelay=static_cast<fsf::Time>(1000000000.0f/fPulseRate);
    pPulseRate->SetData(nPulseDelay);
}

// cell
fsf::CCell *pPcell=static_cast<fsf::CCell*>(
    scripting::CreateCell(std::string("FSF_PULSAR")));
bSuccess &= (pPcell!=NULL);
if(bSuccess){
    // connect with source
    scripting::ConnectSource(pPcell,pPSource);
}
		
/////////////
// My cell //
/////////////

// create the source
fsf::CSource *pPSource=new fsf::CSource;
bSuccess &= (pMySource!=NULL);

// cell
fsf::CCell *pMyCell=static_cast<fsf::CCell*>(
    scripting::CreateCell(std::string("MY_CELL")));
bSuccess &= (pMyCell!=NULL);
if(bSuccess){
    // connect with source
    scripting::ConnectSource(pMyCell,pMySource);

    // connect with Pcell
    scripting::ConnectUpstreamCell(pMyCell,pPcell);
}

// Check everything went OK...

if(bSuccess==false){
    cout << "Some elements in the graph "
	 << "could not be instantiated..." << endl;
    return (-1);
}

Running the graph

Once the graph is completed, the cells must be activated. The Pulsar instance is the origin of the active stream and starts generating empty pulses as soon as it is activated. The MyCell instance, once activated, will process incoming pulses in parallel, asynchronously. The cells can be started in any order.

// Run
pMyCell->On();
pPcell->On();

Cleaning-up

Although this aspect is not evident in this simple example, cells can be turned on and off at any time, elements (sources and cells) can be connected and disconnected at any time, new ones can be created and existing ones destroyed at any time.

The following code stops the cells, disconnects and destroys the different elements instantiated when building the graph, and finally destroys the unique system instance.

// Stop the cells
cout << "Stopping the cells..." << endl;
pPcell->Off();
pMyCell->Off();

// Clean up
scripting::DisconnectSource(pPcell);
scripting::DisconnectStream(pPcell);
delete pPcell;
delete pPSource;

scripting::DisconnectSource(pMyCell);
scripting::Disconnectstream(pMyCell);
delete pMyCell;
delete pMySource;

// Delete unique system instance
fsf::CSystem::DeleteInstance();

Running the program

The example implementation allows to specify the pulse rate on the command line (the default rate is 30 Hz). Below is a sample output from the program.

>example1 -f 30
Pulse: 1120808859 s = 311335 h 47 min 39 s
Pulse: 1120808859 s = 311335 h 47 min 39 s
Pulse: 1120808859 s = 311335 h 47 min 39 s
Pulse: 1120808859 s = 311335 h 47 min 39 s
Pulse: 1120808859 s = 311335 h 47 min 39 s

custom cells

If one of the goals of MFSM is to allow rapid development of applications from existing modules, one of its main strenghts is to allow easy development of custom elements that will interoperate seamlessly with existing or third party components. The example developed in the previous section, example1, uses the cell MyCell to look up the time stamp of each incoming active pulse and print it to the console in a given format. This section details how to describe and implement such custom cells.

Time stamp display

Any processing cell must be derived from the base Cell (CCell). It is characterized by an identification string (used for to link it to its factory). A complete description includes the active and passive filters, the process output, a list of data members and member functions with a short description.

A custom cell must implement the default constructor, and overload a number of virtual functions which characterize the cell:

The specifications of the cell used in example1 are as follows:
myspace::CMyCell (fsf::CCell)MY_CELL
Active filter[FSF_ACTIVE_PULSE "Root"]
Passive filter[FSF_PASSIVE_PULSE "Root"]
Output(no output)

Print time stamp of incoming pulse to the console in seconds and in h/m/s format.

Member functions

Constructors, destructor and other functions that are part of any derived cell class
  • CMyCell() : default constructor
  • virtual void GetTypeID(std::string &str) : factory mapping key
Active stream processing
  • virtual void Process(CPassiveHandle *pPassiveHandle, CActiveHandle *pActiveHandle, CActivePulse *pActivePulse) : specialized process function

The following code is the class declaration

class CMyCell : public fsf::CCell {
public:
    CMyCell();

    // Factory mapping key
    virtual void GetTypeID(std::string &str) { str.assign("MY_CELL"); }

    // Specialized processing function
    virtual void Process(fsf::CPassiveHandle *pPassiveHandle,
        fsf::CActiveHandle *pActiveHandle,
	fsf::CActivePulse *pActivePulse);
};

In the constructor, the default output name base is set, and both passive and active filters are instantiated from the corresponding template classes.

CMyCell::CMyCell()
    : CCell() {
    // default output name
    m_strOutputName.assign("Doesn't matter..."); // no output
    // set the filters
    m_pPassiveFilter=new fsf::CPassiveFilter<fsf::CPassivePulse>(std::string("Root"));
    m_pActiveFilter=new fsf::CActiveFilter<fsf::CActivePulse>(std::string("Root"));
}

When the process function is executed, filtering of passive and active streams has succeeded. The active and passive handles are thus bound to the nodes satisfying the filters. When the filters are complex (hierarchies of filters), the passive and active handles point to the roots). For this very simple cell, no output is generated.

void CMyCell::Process(fsf::CPassiveHandle *pPassiveHandle,
    fsf::CActiveHandle *pActiveHandle,
    fsf::CActivePulse *pActivePulse) {
    
    // get input active node from active handle
    fsf::CNode *pNode=static_cast<fsf::CNode*>(pActiveHandle->GetNode());

    // compute the node time stamp in h/m/s format
    long tsec=static_cast<long>(pNode->GetTime()/1000000000); // time unit is ns
    long h=tsec/3600;
    long m=(tsec-h*3600)/60;
    long s=tsec-h*3600-m*60;

    // print time stamp in s and in h/m/s on the console
    cout << "Pulse: " << tsec << " s = "
         << h << " h " << m << " min " << s << " s " << endl;
}

Factory registration

Any module must define a RegisterFactories function that registers its node and cell factories with the system. Following is the code for the RegisterFactories function that registers the factory for CMyCell cell class.

// Factory registration
void RegisterFactories(){
    using namespace fsf;

    CSystem *pSystem=CSystem::GetInstance();

    std::string strAlex("Alexandre R.J. Francois");

    ////////////////////////////////////////////////
    // Node factories
    ////////////////////////////////////////////////////////

    ////////////////////////////////////////////////
    // Cell factories
    ////////////////////////////////////////////////////////

    pSystem->RegisterCellFactory(std::string("MY_CELL"),
        new CCellFactory<CMyCell>(std::string("My cell"),strAlex));
}

Pulse frequency display

Here is a slightly more complex example, called example2. The goal is now to display not the pulse time stamp, but the pulse frequency. This requires to compute the time delay between two consecutive pulses. Some data must therefore be shared between the threads processing each pulse: the time stamp of the last pulse processed is saved in a node on the passive pulse. Each time the process function is called, the last time stamp value is retrived from the node, and the node value is updated with the time stamp of the pulse being processed. The difference between the two time stamps is then computed and formatted to be output to the console. The corresponding cell will be called MyCell2.

Figure 5 shows the conceptual level system graph.

Conceptual system graph for example2
Figure 5: Conceptual system graph for example2

The specifications for MyCell2 are as follows:
myspace::CMyCell2 (fsf::CCell)MY_CELL2
Active filter[FSF_ACTIVE_PULSE "Root"]
Passive filter[FSF_INT64_NODE "Last time"]
Output(no output)

Compute and print pulse frequency to the console in Hz.

Member functions

Constructors, destructor and other functions that are part of any derived cell class
  • CMyCell2() : default constructor
  • virtual void GetTypeID(std::string &str) : factory mapping key
Active stream processing
  • virtual void Process(CPassiveHandle *pPassiveHandle, CActiveHandle *pActiveHandle, CActivePulse *pActivePulse) : specialized process function

The class declaration is identical to that of MyCell. The only differences are to the passive filter in the constructor, and to the process function.

The code for the new constructor is as follows:

CMyCell2::CMyCell2()
    : CCell() {
    // default output name
    m_strOutputName.assign("Doesn't matter..."); // no output
    // set the filters
    m_pPassiveFilter=new fsf::CPassiveFilter<fsf::Int64Node>(std::string("Last time"));
    m_pActiveFilter=new fsf::CActiveFilter<fsf::CActivePulse>(std::string("Root"));
}

Note the change in the passive filter: in order to compute a delay, the process function must access and update the value of the time stamp of the last pulse handled.

The code for the new process function is as follows:

void CMyCell2::Process(fsf::CPassiveHandle *pPassiveHandle,
    fsf::CActiveHandle *pActiveHandle,
    fsf::CActivePulse *pActivePulse) {

    // get pointer to last time node from passive handle
    fsf::Int64Node *pLastTime=static_cast<fsf::Int64Node*>(pPassiveHandle->GetNode());
    // get input active node from active handle
    fsf::CNode *pNode=static_cast<fsf::CNode*>(pActiveHandle->GetNode());

    if(m_bReset){
        // set inital time stamp
	pLastTime->SetData(pNode->GetTime());
	m_bReset=false;
    }
    else{
        // get last time value
	fsf::Time tLastTime=static_cast<fsf::Time>(pLastTime->GetData());
	// update node value with current pulse time stamp value
	pLastTime->SetData(pNode->GetTime());
	// compute and print time difference and corresponding frequency
	fsf::Time dtms=((pNode->GetTime()-tLastTime)/1000000LL;
	cout << "Pulse interval: " << dtms << " ms <=> "
	     << 1000.0f/dtms << " Hz" << endl;
    }
}

The code for building the application graph for example2 is very similar to that of example1. The only difference is in the output subgraph: the node storing the "last time stamp" value must be instantiated and placed on the passive pulse held by pMySource. Of course, the cell to be instantiated now is of type MY_CELL2. The corresponding code is as follows:

///////////////
// My cell 2 //
///////////////

// create the source
fsf::CSource *pPSource=new fsf::CSource;
bSuccess &= (pMySource!=NULL);

// last time stamp
fsf::Int64Node *pLastTime=static_cast<fsf::Int64Node*>(
    scripting::CreateNode(std::string("FSF_INT64_NODE"),pMySource->GetPulse()));
bSuccess &= (pLastTime!=NULL);
if(bSuccess){
    // set name
    pLastTime->SetName(std::string("Last time"));
}

// cell
fsf::CCell *pMyCell=static_cast<fsf::CCell*>(
    scripting::CreateCell(std::string("MY_CELL2")));
bSuccess &= (pMyCell!=NULL);
if(bSuccess){
    // connect with source
    scripting::ConnectSource(pMyCell,pMySource);

    // connect with Pcell
    scripting::ConnectUpstreamCell(pMyCell,pPcell);
}

The example implementation allows to specify the pulse rate on the command line (the default rate is 30 Hz). Below is a sample output from the program.

>example2 -f 30
Pulse interval: 34 ms <=> 29.4118 Hz
Pulse interval: 34 ms <=> 29.4118 Hz
Pulse interval: 34 ms <=> 29.4118 Hz
Pulse interval: 34 ms <=> 29.4118 Hz
Pulse interval: 34 ms <=> 29.4118 Hz
Pulse interval: 34 ms <=> 29.4118 Hz
Pulse interval: 34 ms <=> 29.4118 Hz
Pulse interval: 34 ms <=> 29.4118 Hz
Pulse interval: 34 ms <=> 29.4118 Hz

custom nodes

The definition and implementation of custom nodes is illustrated with a generic image node. The specifications for the image node image::CImage, defined in the Image module, are as follows.

CImage (fsf::CCharBuffer) "IMAGE_IMAGE"

Image node. [ImageModule.h]

Data members

  • int m_nNbChannels : number of channels
  • int m_nDepth : pixel depth, one of IMAGE_DEPTH_??? . Also indicates if image is floating point.
  • int m_nWidth : image width
  • int m_nHeight : image height
  • int m_nWidthStep : aligned width (in bytes)

Member functions

Constructors, destructor and other functions that are part of any derived node class
  • CImage() : default constructor
  • CImage(CNode *pParent, fsf::Time tTime=0) : initialization constructor
  • CImage(const std::string &strName, fsf::Time tTime=0) : initialization constructor
  • CImage(int nWidth, int nHeight, int nNbChannels=3, int nDepth=IMAGE_DEPTH_8U, fsf::CNode *pParent=NULL, fsf::Time tTime=0) : initialization constructor
  • CImage(const std::string &strName, int nWidth, int nHeight, int nNbChannels=3, int nDepth=IMAGE_DEPTH_8U, fsf::CNode *pParent=NULL, fsf::Time tTime=0) : initialization constructor
  • CImage(const CImage&) : copy constructor
  • CImage& operator=(const CImage&) : assignment operator
  • virtual CNode* Clone() : virtual cloning member function (necessary for run-time polymorphism)
  • virtual void GetTypeID(std::string &str) : factory mapping key
Memory allocation
  • int ComputeWidthStep(bool bNoAlign=false) : utility private member used to compute aligned width length (m_nWidthStep) from non aligned width value
  • virtual void Allocate(bool bNoAlign=false) : allocate data buffer, with or without word alignment; buffer values are not set
Data copy
  • void CopyParameters(const CImage&) : copy parameter values from argument
  • void CopyImageData(const CImage&) : copy image buffer values from argument
Image parameters access
  • int Width() : return image width
  • int Height() : return image height
  • int NbChannels() : return image number of channels
  • int PixelDepth() : return image pixel depth
  • int WidthStep() : return image width step
Image parameters setting
  • void SetWidth(int nWidth) : set image width (does not affect buffer)
  • void SetHeight(int nHeight) : set image height (does not affect buffer)
  • void SetNbChannels(int nNbChannels) : set image number of channels (does not affect buffer)
  • void SetPixelDepth(int nDepth) : set image pixel depth (does not affect buffer)
  • void SetWidthStep(int nWidthStep) : set image aligned width (does not affect buffer)
Drawing utilities
  • void DrawPoint(int x, int y, const unsigned char *col) : set pixel value
  • void DrawLine(int xo, int yo, int xd, int yd, const unsigned char *col) : draw straight line
  • void DrawRectangle(int x, int y, int w, int h, const unsigned char *col) : draw rectangle (contours)
  • void FillRectangle(int x, int y, int w, int h, const unsigned char *col) : draw a filled rectangle

One solution when designing the image node was to encapsulate an existing image structure. Unfortunately, each image processing library comes with its own image structure. Committing to a given library might prevent access to other libraries, and prove restrictive in the long term. The image node defined in the Image Module provides a minimum representation to ensure its compatibility with existing image structures. However the image node does not contain any field specific of particular image formats, to ensure the widest compatibility. When needed, more specific image nodes may be derived from this base image node for leveraging specific library features. Because of inheritance properties, these specialized image nodes will be usable with all processes defined for the base image node.

Any node type specialization must be derived from the base node fsf::CNode or a derived node. A node type is characterized by an identification string (used to link it to its factory). A complete node type description includes a list of data members and member functions, and a short description of its semantics.

The image node is derived from the character buffer node fsf::CCharBuffer defined in the FSF library. An image buffer is indeed a character buffer. The smallest set of parameters needed to make the character buffer usable as an image buffer are the image width and height, the number of channels and the pixel depth. Since some libraries require data line alignment for optimal performance, the actual aligned width (width step) must also be stored. A utility protected member function is used to compute the aligned width.

class CImage : public fsf::CCharBuffer {
protected:
    int m_nNbChannels; // Number of channels
    int m_nDepth; // Pixel depth IMAGE_DEPTH_*

    int m_nWidth; // Image width
    int m_nHeight; // Image height
    int m_nWidthStep; // Aligned width (in bytes)

    // utility protected member function
    int ComputeWidthStep(bool bNoAlign=false);

Any custom node class must implement a number of constructors: the default constructor, all the constructors defined in the the base node class (these must define default values for the local data members), additional constructors for specifying local data members initial values, and the copy constructor. When necessary, the virtual destructor must also be overloaded.

public:
    // Default constructor
    CImage() : CCharBuffer(),
        m_nNbChannels(3), m_nDepth(IMAGE_DEPTH_8U),
        m_nWidth(0), m_nHeight(0), m_nWidthStep(0) {}

    // Constructors with default values for local data members
    CImage(fsf::CNode *pParent, fsf::Time tTime=0)
        : CCharBuffer(pParent,tTime),
        m_nNbChannels(3), m_nDepth(IMAGE_DEPTH_8U),
        m_nWidth(0), m_nHeight(0), m_nWidthStep(0) {}

    CImage(const string &strName,
        fsf::CNode *pParent=NULL, fsf::Time tTime=0)
        : CCharBuffer(strName,pParent,tTime),
        m_nNbChannels(3), m_nDepth(IMAGE_DEPTH_8U),
        m_nWidth(0), m_nHeight(0), m_nWidthStep(0) {}

    // Constructors with local data members initial values input
    CImage(int nWidth, int nHeight,
        int nNbChannels=3, int nDepth=IMAGE_DEPTH_8U,
        fsf::CNode *pParent=NULL, fsf::Time tTime=0)
        : CCharBuffer(pParent,tTime),
        m_nNbChannels(nNbChannels), m_nDepth(nDepth),
        m_nWidth(nWidth), m_nHeight(nHeight), m_nWidthStep(0) {}

    CImage(const string &strName, int nWidth, int nHeight,
        int nNbChannels=3, int nDepth=IMAGE_DEPTH_8U,
        fsf::CNode *pParent=NULL, fsf::Time tTime=0)
        : CCharBuffer(strName,pParent,tTime),
        m_nNbChannels(nNbChannels), m_nDepth(nDepth),
        m_nWidth(nWidth), m_nHeight(nHeight), m_nWidthStep(0) {}
  
    // Copy constructor
    CImage(const CImage&);

No destructor overload is needed here, since the destructor for the character buffer parent class takes care of deallocating the buffer if needed.

The custom node class must also overload a number of virtual functions which characterize the node:

    // Assignment operator
    CImage& operator=(const CImage&);

    // Cloning: necessary for run-time polymorphism
    virtual fsf::CNode *Clone() { return new CImage(*this); }

    // Factory mapping key
    virtual void GetTypeID(string &str) { str.assign("IMAGE_IMAGE"); }

A set of member functions provides basic access to local data members (set and get operations). A memory allocation function and high level parameter and image data (buffer content) copy functions complete the set of tools offered by the image node.

    void CopyParameters(const CImage&);
    void CopyImageData(const CImage&);

    // Image parameters setting
    void SetWidth(int nWidth) { m_nWidth=nWidth; }
    void SetHeight(int nHeight) { m_nHeight=nHeight; }
    void SetNbChannels(int nNbChannels) { m_nNbChannels=nNbChannels; }
    void SetPixelDepth(int nDepth) { m_nDepth=nDepth; }
    void SetWidthStep(int nWidthStep) { m_nWidthStep=nWidthStep; }

    // Image parameters access
    int Width() const { return m_nWidth; }
    int Height() const { return m_nHeight; }
    int NbChannels() const { return m_nNbChannels; }
    int PixelDepth() const { return m_nDepth; }
    int WidthStep() const { return m_nWidthStep; }

    // Memory allocation
    void Allocate(bool bNoAlign=false);

    // Drawing primitives
    void DrawPoint(int x, int y, const unsigned char *col);
    void DrawLine(int xo, int yo, int xd, int yd, const unsigned char *col);
    void DrawRectangle(int x, int y, int w, int h, const unsigned char *col);
    void FillRectangle(int x, int y, int w, int h, const unsigned char *col);
};

When an image node instance is created, its parameters must be set. Constructors provide default values, set functions allow to change the values explicitly. The corresponding buffer must then be allocated by a call to the Allocate function. The image node instance can then be used for processing.

Any module must define a RegisterFactories function that registers its node and cell factories with the system. Following is the code for the image::RegisterFactories function. Apart from the image node image::CImage, the module also implements a number of cells that provide access to its various data members. Their description can be found in the Image module documentation. Since an example of cell factory registration is provided in the previous section, the code for cell factory registration has been ommitted below.

void image::RegisterFactories(){
    using namespace fsf;
    using namespace image;

    CSystem *pSystem=CSystem::GetInstance();

    // Node factories
    pSystem->RegisterNodeFactory(std::string("IMAGE_IMAGE"),
        new CNodeFactory(std::string("Image node"),
        std::string("Alexandre R.J. Francois")));

    // Cell factories
    ...
}

barrier synchronization

Coming soon...

references

Alexandre R.J. François, Software Architecture for Immersipresence, IMSC Technical Report IMSC-03-001, University of Southern California, Los Angeles, December 2003. [pdf] [BibRef]

Alexandre R.J. François, "A Hybrid Architectural Style for Distributed Parallel Processing of Generic Data Streams," Proceedings of the International Conference on Software Engineering, pp. 367-376, Edinburgh, Scotland, UK, May 2004. [pdf] [BibRef]

Alexandre R.J. François, SAI: Architecting Distributed Asynchronous Software Systems, IMSC Technical Report IMSC-05-003, University of Southern California, Los Angeles, September 2005. [pdf] [BibRef]

ARJF © 2001-2006