1Internal Architecture Overview 2============================== 3 4External API 5************ 6 7Configs 8+++++++ 9 10At the highest level, we have OCIO::Configs. This represents the entirety of the 11current color "universe". Configs are serialized as .ocio files, read at runtime, 12and are often used in a 'read-only' context. 13 14Config are loaded at runtime to allow for customized color handling in a show- 15dependent manner. 16 17Example Configs: 18 19* ACES (Academy's standard color workflow) 20* spi-vfx (Used on some Imageworks VFX shows such as spiderman, etc). 21* and others 22 23 24ColorSpaces 25+++++++++++ 26 27The meat of an OCIO::Config is a list of named ColorSpaces. ColorSpace often 28correspond to input image states, output image states, or image states used for 29internal processing. 30 31Example ColorSpaces (from ACES configuration): 32 33* aces (HDR, scene-linear) 34* adx10 (log-like density encoding space) 35* slogf35 (sony F35 slog camera encoding) 36* rrt_srgb (baked in display transform, suitable for srgb display) 37* rrt_p3dci (baked in display transform, suitable for dcip3 display) 38 39 40Transforms 41++++++++++ 42 43ColorSpaces contain an ordered list of transforms, which define the conversion 44to and from the Config's "reference" space. 45 46Transforms are the atomic units available to the designer in order to specify a 47color conversion. 48 49Examples of OCIO::Transforms are: 50 51* File-based transforms (1d lut, 3d lut, mtx... anything, really.) 52* Math functions (gamma, log, mtx) 53* The 'meta' GroupTransform, which contains itself an ordered lists of transforms 54* The 'meta' LookTransform, which contains an ordered lists of transforms 55 56 57For example, the adx10 ColorSpace (in one particular ACES configuration) 58-Transform FROM adx, to our reference ColorSpace: 59 60#. Apply FileTransform adx_adx10_to_cdd.spimtx 61#. Apply FileTransform adx_cdd_to_cid.spimtx 62#. Apply FileTransform adx_cid_to_rle.spi1d 63#. Apply LogTransform base 10 (inverse) 64#. Apply FileTransform adx_exp_to_aces.spimtx 65 66 67If we have an image in the reference ColorSpace (unnamed), we can convert TO 68adx by applying each in the inverse direction: 69 70#. Apply FileTransform adx_exp_to_aces.spimtx (inverse) 71#. Apply LogTransform base 10 (forward) 72#. Apply FileTransform adx_cid_to_rle.spi1d (inverse) 73#. Apply FileTransform adx_cdd_to_cid.spimtx (inverse) 74#. Apply FileTransform adx_adx10_to_cdd.spimtx (inverse) 75 76 77Note that this isn't possible in all cases (what if a lut or matrix is not 78invertible?), but conceptually it's a simple way to think about the design. 79 80 81Summary 82+++++++ 83 84Configs and ColorSpaces are just a bookkeeping device used to get and ordered 85lists of Transforms corresponding to image color transformation. 86 87Transforms are visible to the person AUTHORING the OCIO config, but are 88NOT visible to the client applications. Client apps need only concern themselves 89with Configs and Processors. 90 91 92OCIO::Processors 93++++++++++++++++ 94 95A processor corresponds to a 'baked' color transformation. You specify two arguments 96when querying a processor: the :ref:`colorspace_section` you are coming from, 97and the :ref:`colorspace_section` you are going to. 98 99Once you have the processor, you can apply the color transformation using the 100"apply" function. For the CPU veseion, first wrap your image in an 101ImageDesc class, and then call apply to process in place. 102 103Example: 104 105.. code-block:: cpp 106 107 #include <OpenColorIO/OpenColorIO.h> 108 namespace OCIO = OCIO_NAMESPACE; 109 110 try 111 { 112 // Get the global OpenColorIO config 113 // This will auto-initialize (using $OCIO) on first use 114 OCIO::ConstConfigRcPtr config = OCIO::GetCurrentConfig(); 115 116 // Get the processor corresponding to this transform. 117 // These strings, in this example, are specific to the above 118 // example. ColorSpace names should NEVER be hard-coded into client 119 // software, but should be dynamically queried at runtime from the library 120 OCIO::ConstProcessorRcPtr processor = config->getProcessor("adx10", "aces"); 121 122 // Wrap the image in a light-weight ImageDescription 123 OCIO::PackedImageDesc img(imageData, w, h, 4); 124 125 // Apply the color transformation (in place) 126 processor->apply(img); 127 } 128 catch(OCIO::Exception & exception) 129 { 130 std::cerr << "OpenColorIO Error: " << exception.what() << std::endl; 131 } 132 133 134The GPU code path is similar. You get the processor from the config, and then 135query the shaderText and the lut3d. The client loads these to the GPU themselves, 136and then makes the appropriate calls to the newly defined function. 137 138See `src/apps/ociodisplay` for an example. 139 140 141Internal API 142************ 143 144 145The Op Abstraction 146++++++++++++++++++ 147 148It is a useful abstraction, both for code-reuse and optimization, to not relying 149on the transforms to do pixel processing themselves. 150 151Consider that the FileTransform represents a wide-range of image processing 152operations (basically all of em), many of which are really complex. For example, 153the houdini lut format in a single file may contain a log convert, a 1d lut, and 154then a 3d lut; all of which need to be applied in a row! If we don't want the 155FileTransform to know how to process all possible pixel operations, it's much 156simpler to make light-weight processing operations, which the transforms can 157create to do the dirty work as needed. 158 159All image processing operations (ops) are a class that present the same 160interface, and it's rather simple: 161 162.. code-block:: cpp 163 164 virtual void apply(float* rgbaBuffer, long numPixels) 165 166Basically, given a packed float array with the specified number of pixels, process em. 167 168Examples of ops include Lut1DOp, Lut3DOp, MtxOffsetOp, LogOp, etc. 169 170Thus, the job of a transform becomes much simpler and they're only responsible 171for converting themselves to a list of ops. A simple FileTransform that only has 172a single 1D lut internally may just generate a single Lut1DOp, but a 173FileTransform that references a more complex format (such as the houdini lut case 174referenced above) may generate a few ops: 175 176.. code-block:: cpp 177 178 void FileFormatHDL::BuildFileOps(OpRcPtrVec & ops, 179 const Config& /*config*/, 180 const ConstContextRcPtr & /*context*/, 181 CachedFileRcPtr untypedCachedFile, 182 const FileTransform& fileTransform, 183 TransformDirection dir) const { 184 185 // Code omitted which loads the lut file into the file cache... 186 187 CreateLut1DOp(ops, cachedFile->lut1D, 188 fileTransform.getInterpolation(), dir); 189 CreateLut3DOp(ops, cachedFile->lut3D, 190 fileTransform.getInterpolation(), dir); 191 192See (``src/core/*Ops.h``) for the available ops. 193 194Note that while compositors often have complex, branching trees of image processing 195operations, we just have a linear list of ops, lending itself very well to 196optimization. 197 198Before the ops are run, they are optimized. (Collapsed with appropriate neighbors, etc). 199 200 201An Example 202++++++++++ 203 204Let us consider the internal steps when getProcessor() is called to convert from ColorSpace 205'adx10' to ColorSpace 'aces': 206 207* The first step is to turn this ColorSpace conversion into an ordered list of transforms. 208We do this by creating a single of the conversions from 'adx10' to reference, and then 209adding the transforms required to go from reference to 'aces'. 210* The Transform list is then converted into a list of ops. It is during this stage luts, 211are loaded, etc. 212 213 214CPU CODE PATH 215+++++++++++++ 216 217The master list of ops is then optimized, and stored internally in the processor. 218 219.. code-block:: cpp 220 221 FinalizeOpVec(m_cpuOps); 222 223 224During Processor::apply(...), a subunit of pixels in the image are formatted into a sequential rgba block. (Block size is optimized for computational (SSE) simplicity and performance, and is typically similar in size to an image scanline) 225 226.. code-block:: cpp 227 228 float * rgbaBuffer = 0; 229 long numPixels = 0; 230 while(true) { 231 scanlineHelper.prepRGBAScanline(&rgbaBuffer, &numPixels); 232 ... 233 234 235Then for each op, op->apply is called in-place. 236 237.. code-block:: cpp 238 239 for(OpRcPtrVec::size_type i=0, size = m_cpuOps.size(); i<size; ++i) 240 { 241 m_cpuOps[i]->apply(rgbaBuffer, numPixels); 242 } 243 244 245After all ops have been applied, the results are copied back to the source 246 247.. code-block:: cpp 248 249 scanlineHelper.finishRGBAScanline(); 250 251 252GPU CODE PATH 253+++++++++++++ 254 255#. The master list of ops is partitioned into 3 ordered lists: 256 257- As many ops as possible from the BEGINNING of the op-list that can be done 258 analytically in shader text. (called gpu-preops) 259- As many ops as possible from the END of the op-list that can be done 260 analytically in shader text. (called gpu-postops) 261- The left-over ops in the middle that cannot support shader text, and thus 262 will be baked into a 3dlut. (called gpu-lattice) 263 264 265#. Between the first an the second lists (gpu-preops, and gpu-latticeops), we 266analyze the op-stream metadata and determine the appropriate allocation to use. 267(to minimize clamping, quantization, etc). This is accounted for here by 268interserting a forward allocation to the end of the pre-ops, and the inverse 269allocation to the start of the lattice ops. 270 271See https://github.com/imageworks/OpenColorIO/blob/master/src/core/NoOps.cpp#L183 272 273#. The 3 lists of ops are then optimized individually, and stored on the processor. 274The Lut3d is computed by applying the gpu-lattice ops, on the CPU, to a lut3d 275image. 276 277The shader text is computed by calculating the shader for the gpu-preops, adding 278a sampling function of the 3d lut, and then calculating the shader for the gpu 279post ops. 280 281