Caching

Caching of image results is extremely useful in improving overall Node performance. By default, NUKE caches the inputs of a node if it has its control panel open, if it is requested multiple times, or if a Tile or Interest object is produced on it. It does this by calculating a check-sum (or hash) on the incoming input images and the Node’s current parameters, and storing the calculating output in the cache using the hash as the key. When a downstream Node requests the output from this Node, it can calculate the hash (from its inputs and parameters) and if the hash exists in the cache, the Node can simply read the stored output from the cache instead of re-calculating it. Clearly the computed hash will change if the input images change (i.e. on a different frame / view, change to upstream Node etc.), or if the Nodes parameters are changed by the user.

Another way to exploit the cache is to store intermediate steps of your algorithm. For example, say you have an algorithm with two sequential steps; the first is extremely processor intensive, the second is not processor intensive and has an associated (knob) parameter with it. As it stands, every time the user adjusts the knob, the algorithm has to compute both steps, and could leave the user waiting a long time. However, the output of the first step does not depend on this parameter. In this case, it would be useful to create an internal cache which stores the output of the first step.This then improves the overall interactivity of your Node. Below is an engine function which can be used as an example of how to check, read and store data in NUKE’s global image cache.

virtual void engine( int y,int x,int r, ChannelMask channels, Row& row ) {
  // engine calls are multi-threaded so any processing must be locked
  if (_isFirstTime) {
    Guard guard(_lock);
    if (_isFirstTime) {

      /* try fetch input image */
      if( fetchRGBAImage(vec_src_image, height, width, 0) ){
        /* now going to see if the input is in the cache */
        Image_Cache *i_cache = &Image_Cache::mainCache();
        printf("Checking active cache: %d.\n", i_cache->is_active());

          /* need to copy the array to pass it into the hash sum */
        float *arr_src_image = new float[vec_src_image.size()];
        std::copy( vec_src_image.begin(), vec_src_image.end(), arr_src_image);
        size_t desired_read_bytes = (vec_src_image.size())*sizeof(float);

        DD::Image::Hash hash;
        hash.reset();
        hash.append(arr_src_image, desired_read_bytes);
        hash.append(_param_blur);
        printf("Printing hash value: %d.\n", (int)hash.value());
        printf("Has file: %d.\n", i_cache->has_file(hash));

          /* freeing temp array */
        delete [] (arr_src_image);

          /* is our blurred image already in the cache? */
        bool cache_read_success = true;
        if( i_cache->is_active() && i_cache->has_file(hash) ){
          DD::Image::ImageCacheRead* cache_read = i_cache->open( hash );
          float *arr_dst_image = new float[width*height*4];

          size_t read_bytes = i_cache->read(arr_dst_image, desired_read_bytes, cache_read);

          if( !i_cache->is_read() || read_bytes != desired_read_bytes ){
            cache_read_success = false;
          }else{
            /* blurred image is in cache & was read successfully */
            for(int i=0;i<vec_dst_image.size();i++)
              vec_dst_image[i] = arr_dst_image[i];
          }

          i_cache->close( cache_read );

            /* freeing memory */
          delete [] (arr_dst_image);
        }else {
          cache_read_success = false;
        }

          /* did we get the cached blur image or do we need to calculate it? */
        if(!cache_read_success){
          /* blur the image, very naiively, do not use this code for anything useful! */
          float blur_sum; int blur_counter;
          for(int i=0;i<height;i++)
            for(int j=0;j<width;j++)
              for(int c=0;c<4;c++){
                blur_sum = 0; blur_counter = 0;
                for (int u=std::max(0,i-_param_blur); u<=std::min(height-1,i+_param_blur); u++)
                  for (int v=std::max(0,j-_param_blur); v<=std::min(width-1,j+_param_blur); v++){
                    blur_sum += vec_src_image[(u*width + v)*4 + c];
                    blur_counter++;
                  }
                vec_dst_image[(i*width + j)*4 + c] = (blur_counter > 0) ? blur_sum / (float)blur_counter : 0;
              }
          /* write result to cache */
          DD::Image::ImageCacheWrite* cache_write = i_cache->create( hash );
          float *arr_dst_image = new float[vec_dst_image.size()];
          for(int i=0;i<vec_dst_image.size();i++)
            arr_dst_image[i] = vec_dst_image[i];

          size_t desired_write_bytes = (vec_dst_image.size())*sizeof(float);

          i_cache->write( arr_dst_image, desired_write_bytes, cache_write);

            /* freeing memory */
          delete [] (arr_dst_image);

          if(!i_cache->is_written())
            printf("Error saving blurred image to cache (is written: %d).\n", (int)i_cache->is_written());

          i_cache->close( cache_write );
        }

        /* now just going to add the constant to the image */
        for(int i = 0; i<vec_dst_image.size(); i++)
          vec_dst_image[i] = vec_dst_image[i] + _param_constant;

      }else {
        /* fill with zeros */
        for(int i = 0; i<vec_dst_image.size(); i++)
          vec_dst_image[i] = 0;
      }

      _isFirstTime = false;
    }
  }

  // Copy unprocessed channels through to the output
  // NOTE: this must happen first as .get(...) changes row

  ChannelSet enginePass = channels;
  enginePass -= _outChannels;
  if (enginePass) {
    input0().get( y, x, r, enginePass, row );
  }


  // an example to set the output data, per image buffer
  ChannelSet engineOut = channels;
  engineOut &= _outChannels;

  if (engineOut) {
    foreach (z, engineOut) {
      float * row1 = row.writable(z);

      int channel_skip = 0;
      if(z == Chan_Red)
        channel_skip = 0;
      else if(z == Chan_Green)
        channel_skip = 1;
      else if(z == Chan_Blue)
        channel_skip = 2;
      else if(z == Chan_Alpha)
        channel_skip = 3;

      for(int xx=x; xx<r; xx++)
        row1[xx] = vec_dst_image[y*n_channels*width + (xx-x)*n_channels + channel_skip];
    }
  }

}

The example algorithm is composed of two parts.The first involves blurring the image using a naive averaging filter of length _param_blur taps.The second simply adds a constant, _param_constant, to the input. The blurring operation is going to be more computationally expensive operation, and so we’d like to cache its output, to allow playing with the _param_constant without re-blurring the input every time.

In the code, we begin by reading the input image to be blurred, and creating a DD::Image::Hash on the image and all the elements that the blur function depends on. It is important to correctly determine the dependencies of the algorithm step you’re working on. If you forget to include a variable or parameter in the checksum, the stored output will effectively be independent of that parameter. In this case, the blurring operation depends only on the input image and the _param_blur parameter value. We then check if it exists in the cache,and try to pull it. If it doesn’t exist, we do the blurring and try to store the output. If you try this example for yourself, you’ll see that the first time the plug-in is run on a frame it takes a short time to run. If you then play with the _param_constant knob, it takes hardly any time to add the constant to the image blur, when traditionally the entire algorithm output would have to be re-calculated.This internal caching trick is very useful, and can be extended to more interesting scenarios, such as your storing your own data structs or classes (preferably with serialization).