Author: Stefan Eilemann
State:
- CPU compressors implemented in 0.9
- GPU transfer compressors implemented in 0.9.2
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
- Backward and forward binary compatibility
- Runtime load and init of plugins
- Runtime query of the following parameters:
- Supported data type:
- RGB8, RGBA8, DEPTH, DEPTH_STENCIL image data
- RGB16f, RGBA16f, RGB32f, RGBA32f image data
- Unstructured binary data
- Quality (Lossy, lossless)
- Speed
- Compression Ratio
- Re-usability of compressors for different tasks
- No performance penalty over current 'inline' implementation
- Ability to write multiple outputs, as used by the component-wise RLE compressor.
- Ability to re-use memory
- Ability to compress in-place
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)
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.