Equalizer logo
Collage logo
GPU-SD logo

Compression Plugin API

Author: Stefan Eilemann
State:

API Documentation

Overview

The compression plugin API is an interface to create binary compression modules which are loaded at runtime ('plugins'), allowing third-party developers to implement new compression schemes. Initially the plugins are used for eq::Image and eq::net::Object data transmission.

Requirements

Design

Image compressors are implemented as shared libraries (.so/.dll). Equalizer loads all plugins from a given directory matching a given pattern. The plugin API is a versioned C API in order to allow backward compatibility and avoid C++ name mangling issues.

One compressor instance is only used from one thread at a given time, that is, compress or decompress can be called from different threads, but never at the same time. A compress call invalidates the previous result and internal data structures can be reused. Equalizer will try to reuse compressor instances.

(cf. Issue 6) The input data is structured. One compressor can support multiple dimensionalities of data, which it declares as part of its capabilities. The number of dimensions in the input and output data is given as a flag. The input dimensions give an offset and a size for each dimension in the format dim0_offset, dim0_size, dim1_offset, ..., dimN_size. The offset does not apply to the input pointer, it is merely a hint on where the data is positioned, e.g., where a 2D image is positioned in a virtual framebuffer. The size of the input data is mul( inDims[1,3,...,n] ) * sizeof( info->dataType ).

API

Plugin API (eq/plugin/compressor.h)

See API Documentation

Equalizer API

  // initialized by EQ_PLUGIN_PATH:
  EQ_EXPORT const StringVector& Global::getPluginDirectories() const;
  EQ_EXPORT void Global::addPluginDirectory( const std::string& path );
  EQ_EXPORT void Global::removePluginDirectory( const std::string& path );

  PluginRegistry& Global::getPluginRegistry();
  class PluginRegistry
  {
  public:
      void init();
      void exit();

      const Compressors& getCompressors() const;
  };

  class Compressor
  {
  public:
      bool init( const std::string& filename );
      void exit();

      const EqCompressorInfoVector& getInfos() const;

      // resolved function pointers mirroring C API
      typedef ... FooFunc; // see C API
      CompressFunc compress;
      GetNumResultsFunc getNumResults;
      GetResultFunc getResult;
      DecompressFunc decompress;

  private:
      EqCompressorInfoVector _infos;
      base::DSO _dso;
  };

Example Plugin

  #include eq/plugin/compressor.h

  size_t EqCompressorGetNumCompressors()
  {
      return 1;
  }

  void EqCompressorGetInfo( const size_t n, EqCompressorInfo* const info )
  {
      assert( n == 0 );
      info->version = EQ_COMPRESSOR_VERSION;
      info->type = EQ_COMPRESSOR_RLE_4_BYTE;
      info->tokenType = EQ_COMPRESSOR_DATA_4_BYTE;
      info->capabilities = EQ_COMPRESSOR_DATA_1D | EQ_COMPRESSOR_DATA_2D | 
                           EQ_COMPRESSOR_IGNORE_MSE;
      info->quality = 1.f;
      info->ratio = .8f;
      info->speed = 0.95f;
  }

  void* EqCompressorNewCompressor( const unsigned name )
  {
      return new eq::plugin::RLE4BCompressor;
  }
  void EqCompressorDeleteCompressor( const unsigned name, 
                                     void* const compressor )
  {
      delete (eq::plugin::RLE4BCompressor*)( compressor );
  }

  void* EqCompressorNewDecompressor( const unsigned name ) { return 0; }
  void EqCompressorDeleteDecompressor( const unsigned name,
                                       void* const decompressor ) { /* nop */ }

  void EqCompressorCompress( const unsigned name,
                             void* const compressor,
                             void* const in, const uint64_t* inDims,
                             const uint64_t flags )
  {
      const bool ignoreAlpha = (flags & EQ_COMPRESSOR_IGNORE_MSE);
      const uint64_t inSize = (flags & EQ_COMPRESSOR_DATA_1D) ?
                                  inDims[1] * 4 : inDims[1] * inDims[3] * 4;
      (eq::plugin::RLE4BCompressor*)( compressor )->compress( in, inSize,
                                                              ignoreAlpha );
  }

  uint32_t EqCompressorGetNumResults( const unsigned name,
                                      void* const compressor )
  {
      return (eq::plugin::RLE4BCompressor*)( compressor )->getChunks().size();
  }
  void EqCompressorGetResult( const unsigned name, void* const compressor,
                              const uint32_t i,
                              void** const out, uint64_t* const outSize )
  {
      const eq::plugin::RLE4BCompressor::Chunk* chunk =
          (eq::plugin::RLE4BCompressor*)( compressor )->getChunks()[ i ];
      *out = chunk->data;
      *outSize = chunk->size;
  }

  void EqCompressorDecompress( const unsigned name, void* const decompressor,
                               const void** const in,
                               const uint64_t* const inSizes,
                               const uint32_t numInputs,
                               void* const out, const uint64_t* outDims,
                               const uint64_t flags )
  {
      const uint64_t outSize = (flags & EQ_COMPRESSOR_DATA_1D) ?
                                  outDims[1] * 4 : outDims[1] * outDims[3] * 4;
      eq::plugin::RLE4BCompressor::decompress( in, inSizes, out, outSize );
  }

Implementation

  PluginRegistry::init (called by eq::init)
      for each plugin directory // need new eq::base helper to read directories
          for each file libeqCompressor*(.dll|.so)
              init Compressor dll:
                  open DSO
                  resolve function pointers based on version

  PluginRegistry::exit (called by eq::exit)
      for each compressor
          exit compressor (closes DSO)
          delete compressor         

  File structure:
      src/plugins/interface/compressor.h (installs to eq/plugin/compressor.h)
      src/plugins/examples/compressors.h (does not link against libeq!) 
      src/plugins/examples/compressors.cpp   resolves name -> algorithm
      src/plugins/examples/compressorBase.h/cpp  
      src/plugins/examples/compressorRLE1.h/cpp  
      src/plugins/examples/compressorRLE4.h/cpp  

Restrictions

Issues

1. How does Equalizer select the Compressor at runtime?

Certain parameters exclude some compressors up-front, such as lossy compression and supported formats. After this exclusion a number of potential candidates are available. The goal is to select automatically the compressor which provides the best overall system performance.

For image compression, the compressor can be dynamically adapted based on past performances. The compression should always save time, that is, the time to transmit 'uncompressed - compressed' bytes should be longer than the time to compress. If that is not the case, a faster compressor is selected. If the compressor is substantially faster, a slower compressor is chosen. We assume that decompression is faster than compression.

For object data compression automatic selection of the compressor is harder, since the structure of the data varies from object to object. OPEN ISSUE

2. How is the quality set for algorithms which support varying quality?

3. Is in-place compression supported?

Resolved: yes

In-Place compression refers to the usage of the input data memory for output.

Image data can not be compressed in-place, since the uncompressed data is needed along the compressed data, if the image is used locally as input. Object data can be compressed in-place, since it is only retained for transmission by the DataOStream.

The compression call has a flag set (INPLACE) if in-place compression is allowed. The compressor can then write into the input data, and will return one result pointer to the input data.

4. Should the compressor parameters be mutable?

Resolved: yes

Certain compressor parameters, e.g., the compression ratio, are application-specific. The value given by the compressor is an estimate. Equalizer might update these values at run-time, based on the current data. This will allow a better selection of the compressor.

5. How can GPU-assisted compression be supported?

Resolved: Through a new API version

On-GPU compression uses the current framebuffer or a texture as data source. A new compression entry point has to be defined which transmits the necessary parameters, e.g., PixelViewport or texture ID. New data types will be added to define the GPU source types.

6. How can inter-frame compression be implemented?

Resolved: Compressor saves previous frame(s)

The compressor maintains a copy of the previous data, based on the input dimensions. Based on that copy, inter-frame compression algorithms can be applied. A compressor declares the dimensionalities it supports. The compression routine is called with the current dimensionality of the data.

7. How to support Image::ignoreAlpha?

Resolved: through a flag

The application can tell the image to ignore the alpha channel, even though it is used during readback for performance reasons. The compressor input and output data is GL_RGBA, but the alpha channel is compressed away.

The caller will pass the flag EQ_COMPRESSOR_IGNORE_MSE to the compression routine. This tells the compressor that it can ignore the most significant element. The flag is invalid for non-vector formats. The decompressor will not get passed this flag, it is the compressors responsibility to create valid output for both cases (flag set, not set).

8. Can compressors be chained?

Resolved: Not in version 1

The use case for chaining compressors is to combine multiple compression algorithms, e.g., RLE + Huffman. The main issue is that the output of a compressor is unstructured data, that is, it can only be processed by a EQ_COMPRESSOR_DATATYPE_BYTE compressor. Later Equalizer versions could automatically try out compressor chains. Often it is desirable for performance reasons to combine the algorithms in one compressor, e.g., the RLE compression can produce the histogram for Huffman compression as a side product.