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:
- There’s not enough documentation.
- 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:
template <typename InArrayT, typename OutArrayT, typename ScaleArrayT>
void operator()(InArrayT *inArray,
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,
// 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,
// Create the functor:
// 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);
For those in the “too much documentation!” camp, concise, example-based documentation is provided in the VTK sources:
- ExampleDataArrayRangeDispatch.cxx Examples of how ranges and dispatchers interact.
- ExampleDataArrayRangeAPI.cxx Examples of how the ranges and their associated iterators and references can be used.
For those who carry the “not enough documentation!” banner, these references may be of interest:
- vtkArrayDispatch.h Complete vtkArrayDispatch documentation.
- vtkDataArrayRange.h Overview of ranges (accompanies ExampleDataArrayRangeAPI.cxx).
- Ranges blog post Provides some historical perspective and justification for adding ranges.
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: