6.8. Notification and Evaluation

If you are creating new engine classes, you may need to understand the following details on how changed values are propagated through an Inventor scene graph and engine network. Because evaluation can be slow, Inventor uses “lazy” evaluation for data in its engine network. Engines are evaluated only when their values are needed, and they are not reevaluated unnecessarily. For some engines, you might want to know whether a certain input has changed, and then take some action based on that knowledge. In such cases, you write an inputChanged() method for the engine. This section provides important background information on how and when an engine calls its inputChanged() and evaluate() methods. It also includes several examples of engines that implement an inputChanged() method.

Whenever a change is made to a field, notification propagates through the scene graph that the values dependent on this new value must be reevaluated. However, the evaluation does not occur until the value is requested through a call to getValue() (or getNum(), [ ], or getValues()). In other words, notification of a changed value is pushed through the scene graph. Any values needing reevaluation are marked as needing evaluation. Since no evaluation is performed at this time, notification is an efficient process.

Evaluation, by contrast, can be a slow process, depending on the nature of the engines. For this reason, evaluation is pulled through the scene graph on demand. Whenever a getValue() is called, if the field is connected to other fields or engines, those connections are traced, and fields marked as needing evaluation are evaluated before the new value is sent to its destination.

For example, consider the scene graph and engine network shown in Figure 6.2, “ A Simple Engine Network ”.

A Simple Engine Network

Figure 6.2.  A Simple Engine Network



This scene graph contains four nodes: A, B, C, and D. Nodes B, C, and D contain fields that are connected to engines. For simplicity, assume that Nodes B, C, and D contain only one field, and Engines 1 and 2 have one input and one output each. Engine 1 adds 1 to its input and copies the result to its output. Engine 2 adds 2 to its input and copies the result to its output. The engines are connected to the nodes as shown in the diagram. If you change the value of the field in Node B from 1 to 3, the following things occur:

  1. Node B notifies A that its field has changed. If the scene graph is contained within a render area, A, in turn, notifies the redraw sensor.

  2. Node B notifies Engine 1 that its field needs to be evaluated. Then Engine 1's inputChanged() method is called.

  3. Engine 1 notifies the field in Node C, which is marked as needing evaluation. C notifies A, which notifies the redraw sensor.

  4. Engine 2's field is marked as needing evaluation, then its inputChanged() method is called.

  5. Engine 2 notifies Node D, which is marked as needing evaluation. D notifies A, which notifies the redraw sensor.

Steps 1 through 5 illustrate the push model of notification.

At this point, the value in Node B has changed to 3, but the value of the field in Node C is still 2, and the value of the field in Node D is still 4. When a getValue() is called on the field in Node D, the fields and engines that have been marked as needing evaluation are evaluated, and the new values propagate through the scene graph. At this point, the value of the field in Node C becomes 4, and the value of the field in Node D becomes 6.

If a getValue() is called on the field in Node C instead of Node D, however, Node D remains out of date, because its value was not needed.

An output, like a field connection, can be enabled and disabled. If an output is disabled, notification is not propagated through it, and none of the fields connected to it are marked as needing evaluation.

What Inventor Does behind the Scenes

Table 6.1, “Sequence for Notification and Evaluation summarizes the process of notification and evaluation. Column 3 of the table provides some additional information about what Inventor does behind the scenes. Whenever setValue() or anything that changes a field is called by the application, Inventor automatically calls the engine's inputChanged() method. Similarly, whenever getValue() or any accessor of a field is called by the application, Inventor calls the engine's evaluate() method.

Note that the only time an engine knows when its input has changed is when Inventor calls its inputChanged() method. This notification is guaranteed to happen every time a setValue() occurs on one of the engine's inputs. Also, note that inputChanged() is always called before evaluate(). In fact, inputChanged() could be called several times before evaluate() is called (if several setValue() calls occur before a getValue() call occurs).

Table 6.1. Sequence for Notification and Evaluation

Program Calls

Effect

Inventor Calls

setValue()

Notification of the change is propagated through the scene graph

inputChanged()

getValue()

Updated values are pulled through the scene graph as each evaluate() method produces new output values

evaluate()

Blocking Notification

An engine can implement the inputChanged() method so that it blocks notification under certain conditions. AnSoGate SoGate SoGate engine provides an example of blocking notification. When its enable field is set to FALSE (the default), notification of the setValue() calls does not propagate through it to the rest of the scene graph. However, if the trigger field is touched, the following occurs:

  • Inventor automatically calls inputChanged(). The inputChanged() method for the gate engines sets the enable field to TRUE.

When getValue() is called on a gate engine's output, the following occurs:

  • Inventor automatically calls evaluate() on the gate engine. The gate engines' evaluate() method outputs one value and sets the enable field to FALSE.

Here is the code for the gate engines' inputChanged() and evaluate() methods:

void
SoGate::inputChanged(SoField *whichInput)
{
   if (whichInput == &enable) 
      output.enable(enable.getValue());
   else if (whichInput == &trigger)
      output.enable(TRUE);
}

void 
SoGate::evaluate()
{
   trigger.getValue(); // Clears notification
   SO_ENGINE_OUTPUT(output, type, 
                    setValues(0, input.getNum(),
                    input.getValues(0)));
   // Get rid of any extra values in output
   SO_ENGINE_OUTPUT(output, type, setNum(input.getNum()));
   output.enable(enable.getValue());
}

Do not call setValue() within an inputChanged() method. You can call getValue() within inputChanged(), but be aware that doing this could slow down notification because getValue() causes Inventor to pause and pull new values through the scene graph.

Creating a Fan-in Engine

The following engine provides a simple example of how an inputChanged() method is used to take different actions based on which input was touched. This engine accepts up to four inputs and produces one output (simulating fan-in of connections, which is not allowed directly). It simply copies the most recently touched input to the output. Example 6-7 shows the header file for the fan-in engine. Example 6-8 shows the source file for this class.

Example 6.7.  FanIn.h

#include <Inventor/engines/SoSubEngine.h>
#include <Inventor/fields/SoSFFloat.h>

// This engine class takes up to four inputs (input0 through
// input3) and outputs the most recently changed one to the
// output.

class FanIn : public SoEngine {

 public:

   SO_ENGINE_HEADER(FanIn);

   // Inputs:
   SoSFFloat       input0;
   SoSFFloat       input1;
   SoSFFloat       input2;
   SoSFFloat       input3;

   // Output:
   SoEngineOutput  output;  // (SoSFFloat)

   // Initialization
   static void initClass();

   // Constructor
   FanIn();

 private:
   // This saves a pointer to the field that changed most
   // recently
   SoSFFloat       *lastChangedField;

   // Destructor
   virtual ~FanIn();

   // This is called when an input value changes - we will use
   // it to figure out which input was changed most recently
   virtual void inputChanged(SoField *whichField);

   // Evaluation method
   virtual void evaluate();
};


Example 6.8.  FanIn.c++

#include "FanIn.h"

SO_ENGINE_SOURCE(FanIn);

// Initializes the FanIn class.

void
FanIn::initClass()
{
   SO_ENGINE_INIT_CLASS(FanIn, SoEngine, "Engine");
}

// Constructor

FanIn::FanIn()
{
   // Do standard constructor stuff
   SO_ENGINE_CONSTRUCTOR(FanIn);

   // Define input fields and their default values
   SO_ENGINE_ADD_INPUT(input0,  (0.0));
   SO_ENGINE_ADD_INPUT(input1,  (0.0));
   SO_ENGINE_ADD_INPUT(input2,  (0.0));
   SO_ENGINE_ADD_INPUT(input3,  (0.0));

   // Define the output, specifying its type
   SO_ENGINE_ADD_OUTPUT(output, SoSFFloat);

   // Initialize the pointer that indicates which field changed
   // most recently
   lastChangedField = NULL;
}

// Destructor. Does nothing.

FanIn::~FanIn()
{
}

// This is called when one of our input fields changes. We will
// use it to determine which input value to output.

void
FanIn::inputChanged(SoField *whichField)
{
   // Save a pointer to the field that changed; cast it to an
   // SoSFFloat, since that's the only input field type we have
   lastChangedField = (SoSFFloat *) whichField;
}

// This is the evaluation routine.

void
FanIn::evaluate()
{
   // If evaluate() is called, we must have been notified at
   // some point of a change (including a new connection), so
   // our lastChangedField should never be NULL here. Check it
   // anyway, just for completeness
   if (lastChangedField == NULL) {
     fprintf(stderr, "Uh-oh, there's a NULL lastChangedField"
             "in FanIn::evaluate()!\n");
     return;
   }

   // Output the value from the last changed field
   float   value = lastChangedField->getValue();
   SO_ENGINE_OUTPUT(output, SoSFFloat, setValue(value));
}


Creating a Second-Ticker Engine

As we saw with the gate engines, it's often convenient to enable and disable outputs in an inputChanged() method in order to control propagation of notification. The following engine illustrates how disabling outputs can be useful. This engine has one input, timeIn, and one output, timeOut. It outputs the time once a second and blocks notification in between “ticks.” This engine could be used if you want to render a scene only once a second. Because the realTime global field changes 60 times per second, you could use this engine to block notification in between. Example 6-9 shows the header file for the Ticker engine. Example 6-10 shows the source file for this class.

Be careful when disabling outputs. If all outputs are disabled, evaluate() is never called.

Example 6.9.  Ticker.h

#include <Inventor/engines/SoSubEngine.h>
#include <Inventor/fields/SoSFTime.h>

class Ticker : public SoEngine {

 public:

   SO_ENGINE_HEADER(Ticker);

   // Input:
   SoSFTime        timeIn;

   // Output:
   SoEngineOutput  timeOut;  // (SoSFTime)

   // Initialization
   static void initClass();

   // Constructor
   Ticker();

 private:
   // This saves the number of seconds of the last time this
   // engine was notified of a change
   double          lastSeconds;

   // Destructor
   virtual ~Ticker();

   // This is called when our input value changes - we will use
   // it to see if we need to output a new time value.
   virtual void inputChanged(SoField *whichField);

   // Evaluation method
   virtual void evaluate();
};


Example 6.10.  Ticker.c++

#include <Inventor/SoDB.h>
#include "Ticker.h"

SO_ENGINE_SOURCE(Ticker);

// Initializes the Ticker class.

void
Ticker::initClass()
{
   SO_ENGINE_INIT_CLASS(Ticker, SoEngine, "Engine");
}

// Constructor

Ticker::Ticker()
{
   // Do standard constructor stuff
   SO_ENGINE_CONSTRUCTOR(Ticker);

   // Define input field and its default value
   SO_ENGINE_ADD_INPUT(timeIn,  (SbTime::zero()));

   // Define the output, specifying its type
   SO_ENGINE_ADD_OUTPUT(timeOut, SoSFTime);

   // Initialize the variable that stores the number of seconds
   // of the last time the input changed
   lastSeconds = -1.0;

   // Connect to the global "realTime" field by default. This
   // way, users do not have to make this connection explicitly,
   // and can change it if they want.
   timeIn.connectFrom(SoDB::getGlobalField("realTime"));
}

// Destructor. Does nothing.

Ticker::~Ticker()
{
}

// This is called when one of our input fields changes. We will
// use it to determine whether to produce any output. Since we
// have only 1 input field, we don't have to see which field
// changed.

void
Ticker::inputChanged(SoField *)
{

   // Get the current input time and get rid of any fractional
   // part, using the math library's floor() function
   SbTime   currentTime = timeIn.getValue();
   double   currentSeconds = floor(currentTime.getValue());

   // If the new number of seconds is different from the last
   // one we stored, enable the output. The next time this
   // engine is evaluated, the time will be output.
   if (currentSeconds != lastSeconds)
     timeOut.enable(TRUE);

   // Otherwise, make sure the output is disabled, since we
   // don't want to output any values until we cross the next
   // second barrier.
   else
     timeOut.enable(FALSE);
}

// This is the evaluation routine.

void
Ticker::evaluate()
{
   // Output the current number of seconds
   SbTime   currentTime(lastSeconds);
   SO_ENGINE_OUTPUT(timeOut, SoSFTime, setValue(currentTime));
}