ozz-animation

Documentation - Advanced

Thread safety

ozz run-time library are thread safe, and was design as such from the ground. Ozz does not impose a way to distribute the workload across different threads, but rather propose thread-safe data structures and processing unit, that can be distributed. This data-oriented approach makes everything clear about thread safety:

The multi-threading sample demonstrates how ozz-animation runtime can (naively yet safely) be used in a multi-threaded context, using openmp to distribute jobs execution across threads.

File IO management

IO operations in ozz are done through the ozz::io::Stream interface, which conforms to crt FILE API. ozz propose two implementations of the Stream interface:

One can implement ozz::io::Stream interface to remap IO operations to his own IO management strategy. This is useful for example to create a read or write stream mapped to a sub-part of a file (or a package) that contains many other data but ozz data. Overloading ozz::io::Stream interface requires to implement the following functions:

  // Tests whether a file is opened.
  virtual bool opened() const = 0;

  // Reads _size bytes of data to _buffer from the stream. _buffer must be big
  // enough to store _size bytes. The position indicator of the stream is
  // advanced by the total amount of bytes read.
  // Returns the number of bytes actually read, which may be less than _size.
  virtual std::size_t Read(void* _buffer, std::size_t _size) = 0;

  // Writes _size bytes of data from _buffer to the stream. The position
  // indicator of the stream is advanced by the total number of bytes written.
  // Returns the number of bytes actually written, which may be less than _size.
  virtual std::size_t Write(const void* _buffer, std::size_t _size) = 0;

  // Sets the position indicator associated with the stream to a new position
  // defined by adding _offset to a reference position specified by _origin.
  // Returns a zero value if successful, otherwise returns a non-zero value.
  virtual int Seek(int _offset, Origin _origin) = 0;

  // Returns the current value of the position indicator of the stream.
  // Returns -1 if an error occurs.
  virtual int Tell() const = 0;

Serialization mechanics

Serialization is based on the concept of archives. Archives are similar to c++ iostream. Data can be saved to a OArchive with the << operator, or loaded from a IArchive with the >> operator. Archives read or write data to ozz::io::Stream objects.

Primitive data types are simply saved/loaded to/from archives, while struct and class are saved/loaded through Save/Load intrusive or non-intrusive functions:

  void ObjectType::Save(ozz::io::OArchive*) const;
  void ObjectType::Load(ozz::io::IArchive*);
  namespace ozz {
  namespace io {
  void Save(OArchive& _archive, const _Ty* _ty, std::size_t _count);
  void Load(IArchive& _archive, _Ty* _ty, std::size_t _count);
  }  // io
  }  // ozz

Serializing arrays

Arrays of struct/class or primitive types can be saved/loaded with the helper function ozz::io::MakeArray() which is then streamed in or out using << and >> archive operators:

archive << ozz::io::MakeArray(my_array, count);

Versioning

Versioning can be done using OZZ_IO_TYPE_VERSION macros. Type version is saved in the OArchive, and is given back to Load functions to allow to manually handle version modifications.

namespace ozz {
namespace io {
OZZ_IO_TYPE_VERSION(1, _ObjectType_)
}  // io
}  // ozz

Versioning can be disabled using OZZ_IO_TYPE_NOT_VERSIONABLE like macros. It can be used to optimize native/simple object types. It can not be re-enabled afterward without braking version compatibility.

Tagging

Objects can be assigned a tag (a string) using OZZ_IO_TYPE_TAG macros. A tag allows to check the type of the object to read from an archive.

namespace ozz {
namespace io {
OZZ_IO_TYPE_TAG("_tag_", _ObjectType_)
}  // io
}  // ozz

When reading from an IArchive, an automatic assertion check is performed for each object that has a tag declared. The check can also be done manually, before actually reading and object, to ensure an archive contains the expected object type.

archive.TestTag<_ObjectType_>()

Endianness

Endianness (big or little endian) can be specified while constructing an output archive (ozz::io::OArchive). Input archives automatically handle endianness conversion if the native platform endian mode differs from the archive one.

Error management

IArchive and OArchive expect valid streams as argument, respectively opened for reading and writing. Archives do NOT perform error detection while reading or writing. All errors are considered programming errors. This leads to the following assertions on the user side:

Memory management

Dynamic memory allocations, from any ozz library, all go through a custom allocator. This allocator can be used to redirect all allocations to your own memory manager, defining your own memory management strategy. To redirect memory allocations, override ozz::memory::Allocator object and implement its interface:

  // Allocates _size bytes on the specified _alignment boundaries.
  // Malloc function conforms with standard malloc function specifications.
  virtual void* Malloc(std::size_t _size, std::size_t _alignment) = 0;

  // Frees a block that was allocated with Allocate or Reallocate.
  // Argument _block can be NULL.
  // Free function conforms with standard free function specifications.
  virtual void Free(void* _block) = 0;

  // Changes the size of a block that was allocated with Allocate.
  // Argument _block can be NULL.
  // Realloc function conforms with standard realloc function specifications.
  virtual void* Realloc(void* _block,
                        std::size_t _size,
                        std::size_t _alignment) = 0;	

… and replace ozz default allocator with your own, using ozz::memory::SetDefaulAllocator() function.

Maths

ozz provides different math libraries, depending on how math data are intended to be used and processed:

Fpu-based library

Provides math objects (vectors, primitives…) and operations. These are intended to be used when access simplicity and storage size are more important than algorithm performance. The ozz::math::Float3 (x, y, z components vector) math objects for example provides a simple access to each of its components (x, y and z), simple initialization functions and constructors, simple and usual math/geometric operations (additions, cross-product…) which make it very simple to use. On the other end the implementation relies on fpu scalar operations, with no vectorial (SIMD, SSE…) optimizations. The following files are provided:

SIMD math library

Provides SIMD optimized math objects (matrices, vectors…) and operations, intended to be used when performance is important and most of all data access pattern cope with SIMD restrictions:

This library is implemented in a single file ozz/base/maths/simd_math.h which provides the following math structure:

Current version has SSE2 to SSE4 implementation, and a reference fpu one used as fallback when SSE isn’t available. Adding other SIMD implementation (VMX…) is doable thanks to the complete existing unit-tests suite.

SoA SIMD math library

This library provides math objects and operations with an SoA (Struct Of Array) data layout. That means that every component of a vector for example, is in fact an array of 4 values (standard SIMD size). A single ozz::math::SoaFloat3 is in fact storing 4 vectors in the form xxxx, yyyy and zzzz. Data access simplicity is of course limited, but performance benefit is important because this data layout removes some of the SIMD constraints:

Furthermore instruction throughput is maximized over AoS (Array of Struct) as dependencies between instructions during math standard operations is reduced. The drawback is that it’s impossible to execute conditional operations on one element of the array inside a SoA data (as they are SIMD registers). The solution in this case is to execute operations for the 2 code paths of the condition and use masks to compose the result (see ozz::math::Select functions). SoA comparison expressions returns ozz::math::SimdInt4 values (instead of bools for AoS) which can directly be used for masking or Select like functions. This library relies on these following files to provides SoA structures similar with the fpu-based library:

The library is implemented over the SIMD math library and takes all the benefits of SSE or other SIMD implementations. ozz-animation make an heavy use of SoA data structures (see ozz::animation::BlendingJob for example), which could be used as examples. Data oriented programming is very important to maximize benefits of SoA structures, allowing to arrange the data according to the processing that’s operating on them.

Containers

Standard containers

ozz base library remaps stl standard containers (vectors, maps..) to ozz memory allocators. The purpose is to track memory allocations and avoid leaks.

Note that these containers are not used by the runtime libraries.

Custom containers

Base library implement an linked node list (ozz::container::IntrusiveList), with the benefit of many O1 algorithms and a different memory scheme compared to std::list. It’s templatized for easier reuse and compatible with std::list API and stl algorithms.

Options

This library implements a command line option processing utility. It helps with command line parsing by converting arguments to c++ objects of type bool, int, float or c-string. Unlike getogt(), program options can be scattered in the source files (a la google-gflags). Options are collected by a parser which then automatically generate the help/usage screen based on registered options. This library is made of a single .h and .cc with no other dependency, to make it simple to be reused.

To set an option from the command line, use the form --option=value for non-boolean options, and --option/–nooption for booleans. For example, --var=46 will set var variable to 46. If var type is not compatible with the specified argument type (in this case an integer, a float or a string), then the parser displays the help message and requires application to exit.

Boolean options can be set using different syntax:

Specifying an option (in the command line) that has not been registered is an error, the parsing library will request the application to exit.

As in getopt() and gflags, – by itself terminates flags processing. So in: foo --f1=1 -- --f2=2, f1 is considered but f2 is not.

Parsing is invoked through ozz::options::ParseCommandLine() function, providing argc and argv arguments of the main function. This function also takes as argument two strings to specify the version and usage message.

To declare/register a new option, use OZZ_OPTIONS_DECLARE_xxx like macros. Supported options types are bool, int, float and string (c string). OZZ_OPTIONS_DECLARE_xxx macros arguments allow to give the option a:

So for example, in order to define a boolean “verbose” option, that is false by default and optional (ie: not required):

OZZ_OPTIONS_DECLARE_BOOL(verbose, "Display verbose output", false, false);

This option can then be referenced from the code using OPTIONS_verbose c++ global variable, that implement an automatic cast operator to the option’s type (bool in this case).

The parser also integrates built-in options: