Working with vtkDataArrays: 2019 Edition

November 14, 2019

As discussed at last week’s hackathon, VTK is moving in the direction of deprecating the GetVoidPointer/vtkTemplateMacro method of working with data arrays. Efforts are underway to update many existing filters and algorithms to use newer techniques, namely vtkArrayDispatch and vtkDataArrayRange.

Two key pieces of feedback emerged from the hackathon regarding these utilities:

  1. There’s not enough documentation.
  2. There’s too much documentation.

As a result, Allie has been updating the documentation, hoping to produce a Goldilocks-like compromise that will ease the community’s transition towards using these tools. Meanwhile, Rob has been working hard to convert VTK filters to demonstrate how existing code can be ported to use the new approach. This post provides a high level overview of these techniques and links to additional resources where developers can learn more about them.

Dispatchers and Ranges: What do they do?

The vtkArrayDispatch.h header provides a number of “dispatchers” that can be used to safely cast a vtkDataArray object to a type that provides a more efficient API. This effectively replaces vtkTemplateMacro with a more idiomatic C++ approach that supports all vtkDataArray subclasses, including the in-situ arrays that do not use the same memory layout as the standard VTK arrays. The dispatchers inspect an array’s derived type and execute an algorithm with the downcasted array, allowing more performant APIs to be used. The dispatchers are more flexible than vtkTemplateMacro: They can dispatch multiple arrays at once, and can be restricted to only check for a few array types to reduce compile time / binary size.

The vtkDataArrayRange.h header provides two utilities that simplify writing algorithms with vtkDataArrays: TupleRange and ValueRange. These are STL-range-like objects that abstract the details of accessing/modifying data from any vtkDataArray. They provide a consistent API that allows a developer to write an algorithm once, and then reuse the implementation with any vtkDataArray-derived object with the best possible performance. For example, a range constructed from vtkFloatArray will use a non-virtual inline API that optimizes to raw memory accesses. However, using a vtkDataArray to create the range uses a the slower virtual double-precision API, suitable as a fallback path for less common array types. These ranges also allow optimized access to the data stored by the more esoteric in-situ arrays, such as vtkScaledSOADataArrays.

How are they used?

To use these tools, an algorithm is implemented in a functor (ie. a struct with a definition of operator()) that accepts the arrays as templated arguments. The arrays are used to construct either TupleRanges or ValueRanges using the methods vtk::DataArrayTupleRange or vtk::DataArrayValueRange, respectively. The ranges are used to perform all array accesses required by the algorithm.

To use the functor, a dispatcher is selected from the options provided in vtkArrayDispatch.h and configured with any desired constraints. The dispatcher’s Execute method attempts to match the input arrays against the constraints, and if successful, casts the arrays to their derived types and executes the functor with them.

If the input arrays don’t satisfy the constraints, Execute returns false, and the functor can then be called directly with the vtkDataArray pointers. This allows common usecases to have fast paths, while still supporting uncommon usecases with a slower fallback path.

For example, the following functor and dispatch implement an algorithm that scales an array by a per-tuple factor and writes the result into another array. A fast-path is used when the arrays all hold floats or doubles, but falls back to the slower vtkDataArray interface for other types.

 // The functor that implements the algorithm:
 struct ScaleVectorsWorker
 {
   template <typename InArrayT, typename OutArrayT, typename ScaleArrayT>
   void operator()(InArrayT *inArray,
                   OutArrayT *outArray,
                   ScaleArrayT *scaleFactors) const
   {
     // The type used by the scaleFactor array's API (double for
     // vtkDataArray, or the actual storage type for derived classes).
     using ScalarT = vtk::GetAPIType<ScaleArrayT>;
 
     // TupleRanges iterate tuple-by-tuple:
     const auto inRange = vtk::DataArrayTupleRange(inArray);
     auto outRange = vtk::DataArrayTupleRange(outArray);
     // ValueRanges iterate value-by-value (as if GetVoidPointer were used):
     const auto scaleRange = vtk::DataArrayValueRange<1>(scaleFactors);
 
     const vtk::TupleIdType numTuples = inRange.size();
     const vtk::ComponentIdType numComps = inRange.GetTupleSize(); 

     for (vtk::TupleIdType tupleId = 0; tupleId < numTuples; ++tupleId)
     {
       const ScalarT scaleFactor = scaleRange[tupleId];
       const auto inTuple = inRange[tupleId];
       auto outTuple = outRange[tupleId];
 
       for (vtk::ComponentIdType compId = 0; compId < numComps; ++compId)
       {
         outTuple[compId] = scaleFactor * inTuple[compId];
       }
     }
   }
 };
 
 // Code to call the dispatcher:
 void ScaleVectors(vtkDataArray *inArray,
                   vtkDataArray *outArray,
                   vtkDataArray *scaleFactors)
 {
   // Create an alias for a dispatcher that handles three arrays and only
   // generates code for cases where all three arrays use float or double:
   using FastPathTypes = vtkArrayDispatch::Reals;
   using Dispatcher = vtkArrayDispatch::Dispatch3ByValueType<FastPathTypes,
                                                             FastPathTypes,
                                                             FastPathTypes>;
 
   // Create the functor:
   ScaleVectorsWorker worker;
 
   // Check if the arrays are using float/double, and if so,
   // run an optimized specialization of the algorithm.
   if (!Dispatcher::Execute(inArray, outArray, scaleFactors, worker))
   {
     // If Execute(...) fails, the arrays don't match the constraints.
     // Run the algorithm using the slower vtkDataArray double API instead:
     worker(inArray, outArray, scaleFactors);
   }
 }

Further Reading

For those in the “too much documentation!” camp, concise, example-based documentation is provided in the VTK sources:

For those who carry the “not enough documentation!” banner, these references may be of interest:

To see how these tools are used in practice and find examples of how to update existing algorithms to use them, check out some of Rob Maynard’s recent PRs on Gitlab:

Tags:

5 comments to Working with vtkDataArrays: 2019 Edition

  1. What is the current advice for readers such as HDF5 or TIFF where they read into a buffer in C order for example and we need to translate to Fortran order for vtkImageData? What about cases where we need to perform ordering dependent translations, i.e. making sure slices are in XY for a projection series?

    Does this access pattern offer hope for vtkImageData where we could use the more conventional C ordering in the future? When we hit some C APIs such as OpenGL we need to pass in a void* pointer with a known ordering, or is there a new pattern being considered there?

    I think this looks great, just trying to see how this fits into work we do in Tomviz and where vtkDataArray is used beneath data structures such as vtkImageData to store voxel values.

    1. As it stands, much of VTK still uses GetVoidPointer and is incompatible with the vtkSOADataArrayTemplate layout that would be needed to store that data directly. We’re currently updating a lot of the code to use the new dispatch/ranges, but imaging is an area that will take quite a bit of effort to port due to infrastructure that heavily relies on assuming AOS memory ordering. Additionally, there’s a discussion of moving all image processing to ITK, so we’re holding off on investing the effort needed to update these classes for now.

      But yes — if/when those classes support SOA arrays via vtkArrayDispatch and ranges, we could just pull the component arrays directly out of an external library, put them in a vtkSOADataArrayTemplate, and use them directly in VTK.

      If interfacing to an external API like OpenGL that expects a buffer with a particular layout, it won’t be possible to use these ranges to circumvent those restrictions — we’ll still need to construct those buffers to get data into the library. Libraries that use iterators are compatible, but not those that pass pointers.

      1. Thank you for the response. It would be a shame to move imaging to a hard ITK requirement. Better optional integration would be great, along with making it easier to bring in the ITK filters. Rendering slices, volumes, contours etc will hopefully remain independent/relatively small – a lot of that is pretty fast due to some tight integration. As we move to increased generality it is important to keep in mind compile times, binary size, etc too.

        1. re: compile times, binary size — these are definitely the bane of generic code! The vtkArrayDispatch utility helps quite a bit in that regard. Even if only using AOS arrays, there are a number of dispatchers that pare down the list of generated functor implementations. For example, DispatchByValueType<Reals> only generates paths for float/double arrays, and Dispatch2SameValueType takes two arrays but only generates paths where the arrays have the same value type.

          By using the vtkDataArrayRange utilities, the same implementation can be used for both the optimized, known-type paths and a slower fallback path using vtkDataArray. This makes it much easier to just specialize an algorithm for common arrays without having to maintain two implementations of the same algorithm. This helps developers limit the number of generated optimized paths without sacrificing functionality.

          Together, these provide quite a bit more flexibility and control over codegen than the older GetVoidPointer/vtkTemplateType macro system.

Leave a Reply to Marcus D. HanwellCancel reply