1 /*
2 Copyright (c) 2003-2010 Sony Pictures Imageworks Inc., et al.
3 All Rights Reserved.
4 
5 Redistribution and use in source and binary forms, with or without
6 modification, are permitted provided that the following conditions are
7 met:
8 * Redistributions of source code must retain the above copyright
9   notice, this list of conditions and the following disclaimer.
10 * Redistributions in binary form must reproduce the above copyright
11   notice, this list of conditions and the following disclaimer in the
12   documentation and/or other materials provided with the distribution.
13 * Neither the name of Sony Pictures Imageworks nor the names of its
14   contributors may be used to endorse or promote products derived from
15   this software without specific prior written permission.
16 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17 "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18 LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19 A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20 OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21 SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22 LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23 DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24 THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26 OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27 */
28 
29 #include <OpenColorIO/OpenColorIO.h>
30 
31 #include "GpuShaderUtils.h"
32 #include "HashUtils.h"
33 #include "MatrixOps.h"
34 #include "MathUtils.h"
35 
36 #include <cstring>
37 #include <sstream>
38 
39 OCIO_NAMESPACE_ENTER
40 {
41     namespace
42     {
43         void ApplyScale(float* rgbaBuffer, long numPixels,
44                         const float* scale4)
45         {
46             for(long pixelIndex=0; pixelIndex<numPixels; ++pixelIndex)
47             {
48                 rgbaBuffer[0] *= scale4[0];
49                 rgbaBuffer[1] *= scale4[1];
50                 rgbaBuffer[2] *= scale4[2];
51                 rgbaBuffer[3] *= scale4[3];
52 
53                 rgbaBuffer += 4;
54             }
55         }
56 
57         void ApplyOffset(float* rgbaBuffer, long numPixels,
58                          const float* offset4)
59         {
60             for(long pixelIndex=0; pixelIndex<numPixels; ++pixelIndex)
61             {
62                 rgbaBuffer[0] += offset4[0];
63                 rgbaBuffer[1] += offset4[1];
64                 rgbaBuffer[2] += offset4[2];
65                 rgbaBuffer[3] += offset4[3];
66 
67                 rgbaBuffer += 4;
68             }
69         }
70 
71         void ApplyMatrix(float* rgbaBuffer, long numPixels,
72                          const float* mat44)
73         {
74             float r,g,b,a;
75 
76             for(long pixelIndex=0; pixelIndex<numPixels; ++pixelIndex)
77             {
78                 r = rgbaBuffer[0];
79                 g = rgbaBuffer[1];
80                 b = rgbaBuffer[2];
81                 a = rgbaBuffer[3];
82 
83                 rgbaBuffer[0] = r*mat44[0] + g*mat44[1] + b*mat44[2] + a*mat44[3];
84                 rgbaBuffer[1] = r*mat44[4] + g*mat44[5] + b*mat44[6] + a*mat44[7];
85                 rgbaBuffer[2] = r*mat44[8] + g*mat44[9] + b*mat44[10] + a*mat44[11];
86                 rgbaBuffer[3] = r*mat44[12] + g*mat44[13] + b*mat44[14] + a*mat44[15];
87 
88                 rgbaBuffer += 4;
89             }
90         }
91     }
92 
93 
94 
95 
96 
97 
98 
99     ///////////////////////////////////////////////////////////////////////////
100 
101 
102 
103 
104 
105 
106 
107 
108     namespace
109     {
110         class MatrixOffsetOp : public Op
111         {
112         public:
113             MatrixOffsetOp(const float * m44,
114                            const float * offset4,
115                            TransformDirection direction);
116             virtual ~MatrixOffsetOp();
117 
118             virtual OpRcPtr clone() const;
119 
120             virtual std::string getInfo() const;
121             virtual std::string getCacheID() const;
122 
123             virtual bool isNoOp() const;
124             virtual bool isSameType(const OpRcPtr & op) const;
125             virtual bool isInverse(const OpRcPtr & op) const;
126             virtual bool canCombineWith(const OpRcPtr & op) const;
127             virtual void combineWith(OpRcPtrVec & ops, const OpRcPtr & secondOp) const;
128 
129             virtual bool hasChannelCrosstalk() const;
130             virtual void finalize();
131             virtual void apply(float* rgbaBuffer, long numPixels) const;
132 
133             virtual bool supportsGpuShader() const;
134             virtual void writeGpuShader(std::ostream & shader,
135                                         const std::string & pixelName,
136                                         const GpuShaderDesc & shaderDesc) const;
137 
138         private:
139             bool m_isNoOp;
140             float m_m44[16];
141             float m_offset4[4];
142             TransformDirection m_direction;
143 
144             // Set in finalize
145             bool m_m44IsIdentity;
146             bool m_m44IsDiagonal;
147             bool m_offset4IsIdentity;
148             float m_m44_inv[16];
149             std::string m_cacheID;
150         };
151 
152 
153         typedef OCIO_SHARED_PTR<MatrixOffsetOp> MatrixOffsetOpRcPtr;
154 
155 
156         MatrixOffsetOp::MatrixOffsetOp(const float * m44,
157                                        const float * offset4,
158                                        TransformDirection direction):
159                                        Op(),
160                                        m_isNoOp(false),
161                                        m_direction(direction),
162                                        m_m44IsIdentity(false),
163                                        m_offset4IsIdentity(false)
164         {
165             if(m_direction == TRANSFORM_DIR_UNKNOWN)
166             {
167                 throw Exception("Cannot apply MatrixOffsetOp op, unspecified transform direction.");
168             }
169 
170             memcpy(m_m44, m44, 16*sizeof(float));
171             memcpy(m_offset4, offset4, 4*sizeof(float));
172 
173             memset(m_m44_inv, 0, 16*sizeof(float));
174 
175             // This Op will be a NoOp if and old if both the offset and matrix
176             // are identity. This hold true no matter what the direction is,
177             // so we can compute this ahead of time.
178             m_isNoOp = (IsVecEqualToZero(m_offset4, 4) && IsM44Identity(m_m44));
179         }
180 
181         OpRcPtr MatrixOffsetOp::clone() const
182         {
183             OpRcPtr op = OpRcPtr(new MatrixOffsetOp(m_m44, m_offset4, m_direction));
184             return op;
185         }
186 
187         MatrixOffsetOp::~MatrixOffsetOp()
188         { }
189 
190         std::string MatrixOffsetOp::getInfo() const
191         {
192             return "<MatrixOffsetOp>";
193         }
194 
195         std::string MatrixOffsetOp::getCacheID() const
196         {
197             return m_cacheID;
198         }
199 
200         bool MatrixOffsetOp::isNoOp() const
201         {
202             return m_isNoOp;
203         }
204 
205         bool MatrixOffsetOp::isSameType(const OpRcPtr & op) const
206         {
207             MatrixOffsetOpRcPtr typedRcPtr = DynamicPtrCast<MatrixOffsetOp>(op);
208             if(!typedRcPtr) return false;
209             return true;
210         }
211 
212         bool MatrixOffsetOp::isInverse(const OpRcPtr & op) const
213         {
214             MatrixOffsetOpRcPtr typedRcPtr = DynamicPtrCast<MatrixOffsetOp>(op);
215             if(!typedRcPtr) return false;
216 
217             if(GetInverseTransformDirection(m_direction) != typedRcPtr->m_direction)
218                 return false;
219 
220             float error = std::numeric_limits<float>::min();
221             if(!VecsEqualWithRelError(m_m44, 16, typedRcPtr->m_m44, 16, error))
222                 return false;
223             if(!VecsEqualWithRelError(m_offset4, 4,typedRcPtr->m_offset4, 4, error))
224                 return false;
225 
226             return true;
227         }
228 
229         bool MatrixOffsetOp::canCombineWith(const OpRcPtr & op) const
230         {
231             return isSameType(op);
232         }
233 
234         void MatrixOffsetOp::combineWith(OpRcPtrVec & ops, const OpRcPtr & secondOp) const
235         {
236             MatrixOffsetOpRcPtr typedRcPtr = DynamicPtrCast<MatrixOffsetOp>(secondOp);
237             if(!typedRcPtr)
238             {
239                 std::ostringstream os;
240                 os << "MatrixOffsetOp can only be combined with other ";
241                 os << "MatrixOffsetOps.  secondOp:" << secondOp->getInfo();
242                 throw Exception(os.str().c_str());
243             }
244 
245             float mout[16];
246             float vout[4];
247 
248             if(m_direction == TRANSFORM_DIR_FORWARD &&
249                 typedRcPtr->m_direction == TRANSFORM_DIR_FORWARD)
250             {
251                 GetMxbCombine(mout, vout,
252                               m_m44, m_offset4,
253                               typedRcPtr->m_m44, typedRcPtr->m_offset4);
254             }
255             else if(m_direction == TRANSFORM_DIR_FORWARD &&
256                     typedRcPtr->m_direction == TRANSFORM_DIR_INVERSE)
257             {
258                 float minv2[16];
259                 float vinv2[4];
260 
261                 if(!GetMxbInverse(minv2, vinv2, typedRcPtr->m_m44, typedRcPtr->m_offset4))
262                 {
263                     std::ostringstream os;
264                     os << "Cannot invert second MatrixOffsetOp op. ";
265                     os << "Matrix inverse does not exist for (";
266                     for(int i=0; i<16; ++i)
267                     {
268                         os << typedRcPtr->m_m44[i] << " ";
269                     }
270                     os << ").";
271                     throw Exception(os.str().c_str());
272                 }
273 
274                 GetMxbCombine(mout, vout,
275                               m_m44, m_offset4,
276                               minv2, vinv2);
277             }
278             else if(m_direction == TRANSFORM_DIR_INVERSE &&
279                     typedRcPtr->m_direction == TRANSFORM_DIR_FORWARD)
280             {
281                 float minv1[16];
282                 float vinv1[4];
283 
284                 if(!GetMxbInverse(minv1, vinv1, m_m44, m_offset4))
285                 {
286                     std::ostringstream os;
287                     os << "Cannot invert primary MatrixOffsetOp op. ";
288                     os << "Matrix inverse does not exist for (";
289                     for(int i=0; i<16; ++i)
290                     {
291                         os << m_m44[i] << " ";
292                     }
293                     os << ").";
294                     throw Exception(os.str().c_str());
295                 }
296 
297                 GetMxbCombine(mout, vout,
298                               minv1, vinv1,
299                               typedRcPtr->m_m44, typedRcPtr->m_offset4);
300 
301             }
302             else if(m_direction == TRANSFORM_DIR_INVERSE &&
303                     typedRcPtr->m_direction == TRANSFORM_DIR_INVERSE)
304             {
305                 float minv1[16];
306                 float vinv1[4];
307                 float minv2[16];
308                 float vinv2[4];
309 
310                 if(!GetMxbInverse(minv1, vinv1, m_m44, m_offset4))
311                 {
312                     std::ostringstream os;
313                     os << "Cannot invert primary MatrixOffsetOp op. ";
314                     os << "Matrix inverse does not exist for (";
315                     for(int i=0; i<16; ++i)
316                     {
317                         os << m_m44[i] << " ";
318                     }
319                     os << ").";
320                     throw Exception(os.str().c_str());
321                 }
322 
323                 if(!GetMxbInverse(minv2, vinv2, typedRcPtr->m_m44, typedRcPtr->m_offset4))
324                 {
325                     std::ostringstream os;
326                     os << "Cannot invert second MatrixOffsetOp op. ";
327                     os << "Matrix inverse does not exist for (";
328                     for(int i=0; i<16; ++i)
329                     {
330                         os << typedRcPtr->m_m44[i] << " ";
331                     }
332                     os << ").";
333                     throw Exception(os.str().c_str());
334                 }
335 
336                 GetMxbCombine(mout, vout,
337                               minv1, vinv1,
338                               minv2, vinv2);
339             }
340             else
341             {
342                 std::ostringstream os;
343                 os << "MatrixOffsetOp cannot combine ops with unspecified ";
344                 os << "directions. First op: " << m_direction << " ";
345                 os << "secondOp:" << typedRcPtr->m_direction;
346                 throw Exception(os.str().c_str());
347             }
348 
349             CreateMatrixOffsetOp(ops,
350                                  mout, vout,
351                                  TRANSFORM_DIR_FORWARD);
352         }
353 
354         bool MatrixOffsetOp::hasChannelCrosstalk() const
355         {
356             return (!m_m44IsDiagonal);
357         }
358 
359         void MatrixOffsetOp::finalize()
360         {
361             m_offset4IsIdentity = IsVecEqualToZero(m_offset4, 4);
362             m_m44IsIdentity = IsM44Identity(m_m44);
363             m_m44IsDiagonal = IsM44Diagonal(m_m44);
364 
365             if(m_direction == TRANSFORM_DIR_INVERSE)
366             {
367                 if(!GetM44Inverse(m_m44_inv, m_m44))
368                 {
369                     std::ostringstream os;
370                     os << "Cannot apply MatrixOffsetOp op. ";
371                     os << "Matrix inverse does not exist for m44 (";
372                     for(int i=0; i<16; ++i) os << m_m44[i] << " ";
373                     os << ").";
374                     throw Exception(os.str().c_str());
375                 }
376             }
377 
378             // Create the cacheID
379             md5_state_t state;
380             md5_byte_t digest[16];
381             md5_init(&state);
382             md5_append(&state, (const md5_byte_t *)m_m44,     (int)(16*sizeof(float)));
383             md5_append(&state, (const md5_byte_t *)m_offset4, (int)(4*sizeof(float)));
384             md5_finish(&state, digest);
385 
386             std::ostringstream cacheIDStream;
387             cacheIDStream << "<MatrixOffsetOp ";
388             cacheIDStream << GetPrintableHash(digest) << " ";
389             cacheIDStream << TransformDirectionToString(m_direction) << " ";
390             cacheIDStream << ">";
391 
392             m_cacheID = cacheIDStream.str();
393         }
394 
395         void MatrixOffsetOp::apply(float* rgbaBuffer, long numPixels) const
396         {
397             if(m_direction == TRANSFORM_DIR_FORWARD)
398             {
399                 if(!m_m44IsIdentity)
400                 {
401                     if(m_m44IsDiagonal)
402                     {
403                         float scale[4];
404                         GetM44Diagonal(scale, m_m44);
405                         ApplyScale(rgbaBuffer, numPixels, scale);
406                     }
407                     else
408                     {
409                         ApplyMatrix(rgbaBuffer, numPixels, m_m44);
410                     }
411                 }
412 
413                 if(!m_offset4IsIdentity)
414                 {
415                     ApplyOffset(rgbaBuffer, numPixels, m_offset4);
416                 }
417             }
418             else if(m_direction == TRANSFORM_DIR_INVERSE)
419             {
420                 if(!m_offset4IsIdentity)
421                 {
422                     float offset_inv[] = { -m_offset4[0],
423                                            -m_offset4[1],
424                                            -m_offset4[2],
425                                            -m_offset4[3] };
426 
427                     ApplyOffset(rgbaBuffer, numPixels, offset_inv);
428                 }
429 
430                 if(!m_m44IsIdentity)
431                 {
432                     if(m_m44IsDiagonal)
433                     {
434                         float scale[4];
435                         GetM44Diagonal(scale, m_m44_inv);
436                         ApplyScale(rgbaBuffer, numPixels, scale);
437                     }
438                     else
439                     {
440                         ApplyMatrix(rgbaBuffer, numPixels, m_m44_inv);
441                     }
442                 }
443             }
444         } // Op::process
445 
446         bool MatrixOffsetOp::supportsGpuShader() const
447         {
448             return true;
449         }
450 
451         void MatrixOffsetOp::writeGpuShader(std::ostream & shader,
452                                             const std::string & pixelName,
453                                             const GpuShaderDesc & shaderDesc) const
454         {
455             GpuLanguage lang = shaderDesc.getLanguage();
456 
457             // TODO: This should not act upon alpha,
458             // since we dont apply it on the CPU?
459 
460             if(m_direction == TRANSFORM_DIR_FORWARD)
461             {
462                 if(!m_m44IsIdentity)
463                 {
464                     if(m_m44IsDiagonal)
465                     {
466                         shader << pixelName << " = ";
467                         float scale[4];
468                         GetM44Diagonal(scale, m_m44);
469                         Write_half4(shader, scale, lang);
470                         shader << " * " << pixelName << ";\n";
471                     }
472                     else
473                     {
474                         shader << pixelName << " = ";
475                         Write_mtx_x_vec(shader,
476                                         GpuTextHalf4x4(m_m44, lang), pixelName,
477                                         lang);
478                         shader << ";\n";
479                     }
480                 }
481 
482                 if(!m_offset4IsIdentity)
483                 {
484                     shader << pixelName << " = ";
485                     Write_half4(shader, m_offset4, lang);
486                     shader << " + " << pixelName << ";\n";
487                 }
488             }
489             else if(m_direction == TRANSFORM_DIR_INVERSE)
490             {
491                 if(!m_offset4IsIdentity)
492                 {
493                     float offset_inv[] = { -m_offset4[0],
494                                            -m_offset4[1],
495                                            -m_offset4[2],
496                                            -m_offset4[3] };
497 
498                     shader << pixelName << " = ";
499                     Write_half4(shader, offset_inv, lang);
500                     shader << " + " << pixelName << ";\n";
501                 }
502 
503                 if(!m_m44IsIdentity)
504                 {
505                     if(m_m44IsDiagonal)
506                     {
507                         shader << pixelName << " = ";
508                         float scale[4];
509                         GetM44Diagonal(scale, m_m44_inv);
510                         Write_half4(shader, scale, lang);
511                         shader << " * " << pixelName << ";\n";
512                     }
513                     else
514                     {
515                         shader << pixelName << " = ";
516                         Write_mtx_x_vec(shader,
517                                         GpuTextHalf4x4(m_m44_inv, lang), pixelName,
518                                         lang);
519                         shader << ";\n";
520                     }
521                 }
522             }
523         }
524 
525     }  // Anon namespace
526 
527 
528 
529 
530 
531 
532 
533 
534 
535 
536     ///////////////////////////////////////////////////////////////////////////
537 
538 
539 
540 
541 
542 
543 
544 
545 
546     void CreateScaleOp(OpRcPtrVec & ops,
547                        const float * scale4,
548                        TransformDirection direction)
549     {
550         float offset4[4] = { 0.0f, 0.0f, 0.0f, 0.0f };
551         CreateScaleOffsetOp(ops, scale4, offset4, direction);
552     }
553 
554     void CreateMatrixOp(OpRcPtrVec & ops,
555                         const float * m44,
556                         TransformDirection direction)
557     {
558         float offset4[4] = { 0.0f, 0.0f, 0.0f, 0.0f };
559         CreateMatrixOffsetOp(ops, m44, offset4, direction);
560     }
561 
562     void CreateOffsetOp(OpRcPtrVec & ops,
563                         const float * offset4,
564                         TransformDirection direction)
565     {
566         float scale4[4] = { 1.0f, 1.0f, 1.0f, 1.0f };
567         CreateScaleOffsetOp(ops, scale4, offset4, direction);
568     }
569 
570     void CreateScaleOffsetOp(OpRcPtrVec & ops,
571                              const float * scale4, const float * offset4,
572                              TransformDirection direction)
573     {
574         float m44[16];
575         memset(m44, 0, 16*sizeof(float));
576 
577         m44[0] = scale4[0];
578         m44[5] = scale4[1];
579         m44[10] = scale4[2];
580         m44[15] = scale4[3];
581 
582         CreateMatrixOffsetOp(ops,
583                              m44, offset4,
584                              direction);
585     }
586 
587     void CreateSaturationOp(OpRcPtrVec & ops,
588                             float sat,
589                             const float * lumaCoef3,
590                             TransformDirection direction)
591     {
592         float matrix[16];
593         float offset[4];
594         MatrixTransform::Sat(matrix, offset,
595                              sat, lumaCoef3);
596 
597         CreateMatrixOffsetOp(ops, matrix, offset, direction);
598     }
599 
600     void CreateMatrixOffsetOp(OpRcPtrVec & ops,
601                               const float * m44, const float * offset4,
602                               TransformDirection direction)
603     {
604         bool mtxIsIdentity = IsM44Identity(m44);
605         bool offsetIsIdentity = IsVecEqualToZero(offset4, 4);
606         if(mtxIsIdentity && offsetIsIdentity) return;
607 
608         ops.push_back( MatrixOffsetOpRcPtr(new MatrixOffsetOp(m44,
609             offset4, direction)) );
610     }
611 
612     void CreateFitOp(OpRcPtrVec & ops,
613                      const float * oldmin4, const float * oldmax4,
614                      const float * newmin4, const float * newmax4,
615                      TransformDirection direction)
616     {
617         float matrix[16];
618         float offset[4];
619         MatrixTransform::Fit(matrix, offset,
620                              oldmin4, oldmax4,
621                              newmin4, newmax4);
622 
623         CreateMatrixOffsetOp(ops, matrix, offset, direction);
624     }
625 
626 }
627 OCIO_NAMESPACE_EXIT
628 
629 
630 
631 ////////////////////////////////////////////////////////////////////////////////
632 
633 
634 #ifdef OCIO_UNIT_TEST
635 
636 namespace OCIO = OCIO_NAMESPACE;
637 #include "UnitTest.h"
638 
639 OCIO_NAMESPACE_USING
640 
OIIO_ADD_TEST(MatrixOps,Combining)641 OIIO_ADD_TEST(MatrixOps, Combining)
642 {
643     float m1[16] = { 1.1f, 0.2f, 0.3f, 0.4f,
644                      0.5f, 1.6f, 0.7f, 0.8f,
645                      0.2f, 0.1f, 1.1f, 0.2f,
646                      0.3f, 0.4f, 0.5f, 1.6f };
647 
648     float v1[4] = { -0.5f, -0.25f, 0.25f, 0.0f };
649 
650     float m2[16] = { 1.1f, -0.1f, -0.1f, 0.0f,
651                      0.1f, 0.9f, -0.2f, 0.0f,
652                      0.05f, 0.0f, 1.1f, 0.0f,
653                      0.0f, 0.0f, 0.0f, 1.0f };
654     float v2[4] = { -0.2f, -0.1f, -0.1f, -0.2f };
655 
656     const float source[] = { 0.1f, 0.2f, 0.3f, 0.4f,
657                              -0.1f, -0.2f, 50.0f, 123.4f,
658                              1.0f, 1.0f, 1.0f, 1.0f };
659     float error = 1e-4f;
660 
661     {
662         OpRcPtrVec ops;
663         CreateMatrixOffsetOp(ops, m1, v1, TRANSFORM_DIR_FORWARD);
664         CreateMatrixOffsetOp(ops, m2, v2, TRANSFORM_DIR_FORWARD);
665         OIIO_CHECK_EQUAL(ops.size(), 2);
666         ops[0]->finalize();
667         ops[1]->finalize();
668 
669         OpRcPtrVec combined;
670         ops[0]->combineWith(combined, ops[1]);
671         OIIO_CHECK_EQUAL(combined.size(), 1);
672         combined[0]->finalize();
673 
674         for(int test=0; test<3; ++test)
675         {
676             float tmp[4];
677             memcpy(tmp, &source[4*test], 4*sizeof(float));
678             ops[0]->apply(tmp, 1);
679             ops[1]->apply(tmp, 1);
680 
681             float tmp2[4];
682             memcpy(tmp2, &source[4*test], 4*sizeof(float));
683             combined[0]->apply(tmp2, 1);
684 
685             for(unsigned int i=0; i<4; ++i)
686             {
687                 OIIO_CHECK_CLOSE(tmp2[i], tmp[i], error);
688             }
689         }
690     }
691 
692 
693     {
694         OpRcPtrVec ops;
695         CreateMatrixOffsetOp(ops, m1, v1, TRANSFORM_DIR_FORWARD);
696         CreateMatrixOffsetOp(ops, m2, v2, TRANSFORM_DIR_INVERSE);
697         OIIO_CHECK_EQUAL(ops.size(), 2);
698         ops[0]->finalize();
699         ops[1]->finalize();
700 
701         OpRcPtrVec combined;
702         ops[0]->combineWith(combined, ops[1]);
703         OIIO_CHECK_EQUAL(combined.size(), 1);
704         combined[0]->finalize();
705 
706 
707         for(int test=0; test<3; ++test)
708         {
709             float tmp[4];
710             memcpy(tmp, &source[4*test], 4*sizeof(float));
711             ops[0]->apply(tmp, 1);
712             ops[1]->apply(tmp, 1);
713 
714             float tmp2[4];
715             memcpy(tmp2, &source[4*test], 4*sizeof(float));
716             combined[0]->apply(tmp2, 1);
717 
718             for(unsigned int i=0; i<4; ++i)
719             {
720                 OIIO_CHECK_CLOSE(tmp2[i], tmp[i], error);
721             }
722         }
723     }
724 
725     {
726         OpRcPtrVec ops;
727         CreateMatrixOffsetOp(ops, m1, v1, TRANSFORM_DIR_INVERSE);
728         CreateMatrixOffsetOp(ops, m2, v2, TRANSFORM_DIR_FORWARD);
729         OIIO_CHECK_EQUAL(ops.size(), 2);
730         ops[0]->finalize();
731         ops[1]->finalize();
732 
733         OpRcPtrVec combined;
734         ops[0]->combineWith(combined, ops[1]);
735         OIIO_CHECK_EQUAL(combined.size(), 1);
736         combined[0]->finalize();
737 
738         for(int test=0; test<3; ++test)
739         {
740             float tmp[4];
741             memcpy(tmp, &source[4*test], 4*sizeof(float));
742             ops[0]->apply(tmp, 1);
743             ops[1]->apply(tmp, 1);
744 
745             float tmp2[4];
746             memcpy(tmp2, &source[4*test], 4*sizeof(float));
747             combined[0]->apply(tmp2, 1);
748 
749             for(unsigned int i=0; i<4; ++i)
750             {
751                 OIIO_CHECK_CLOSE(tmp2[i], tmp[i], error);
752             }
753         }
754     }
755 
756     {
757         OpRcPtrVec ops;
758         CreateMatrixOffsetOp(ops, m1, v1, TRANSFORM_DIR_INVERSE);
759         CreateMatrixOffsetOp(ops, m2, v2, TRANSFORM_DIR_INVERSE);
760         OIIO_CHECK_EQUAL(ops.size(), 2);
761         ops[0]->finalize();
762         ops[1]->finalize();
763 
764         OpRcPtrVec combined;
765         ops[0]->combineWith(combined, ops[1]);
766         OIIO_CHECK_EQUAL(combined.size(), 1);
767         combined[0]->finalize();
768 
769         for(int test=0; test<3; ++test)
770         {
771             float tmp[4];
772             memcpy(tmp, &source[4*test], 4*sizeof(float));
773             ops[0]->apply(tmp, 1);
774             ops[1]->apply(tmp, 1);
775 
776             float tmp2[4];
777             memcpy(tmp2, &source[4*test], 4*sizeof(float));
778             combined[0]->apply(tmp2, 1);
779 
780             for(unsigned int i=0; i<4; ++i)
781             {
782                 OIIO_CHECK_CLOSE(tmp2[i], tmp[i], error);
783             }
784         }
785     }
786 }
787 
788 #endif
789