epoger@google.com | ec3ed6a | 2011-07-28 14:26:00 +0000 | [diff] [blame] | 1 | |
| 2 | /* |
| 3 | * Copyright 2011 Google Inc. |
| 4 | * |
| 5 | * Use of this source code is governed by a BSD-style license that can be |
| 6 | * found in the LICENSE file. |
| 7 | */ |
reed@android.com | 8a1c16f | 2008-12-17 15:59:43 +0000 | [diff] [blame] | 8 | #ifndef SkPictureFlat_DEFINED |
| 9 | #define SkPictureFlat_DEFINED |
| 10 | |
| 11 | #include "SkChunkAlloc.h" |
| 12 | #include "SkBitmap.h" |
djsollen@google.com | 2b2ede3 | 2012-04-12 13:24:04 +0000 | [diff] [blame] | 13 | #include "SkOrderedReadBuffer.h" |
djsollen@google.com | d2700ee | 2012-05-30 16:54:13 +0000 | [diff] [blame] | 14 | #include "SkOrderedWriteBuffer.h" |
reed@android.com | 8a1c16f | 2008-12-17 15:59:43 +0000 | [diff] [blame] | 15 | #include "SkPicture.h" |
| 16 | #include "SkMatrix.h" |
| 17 | #include "SkPaint.h" |
| 18 | #include "SkPath.h" |
| 19 | #include "SkRegion.h" |
djsollen@google.com | d2700ee | 2012-05-30 16:54:13 +0000 | [diff] [blame] | 20 | #include "SkTSearch.h" |
reed@android.com | 8a1c16f | 2008-12-17 15:59:43 +0000 | [diff] [blame] | 21 | |
| 22 | enum DrawType { |
| 23 | UNUSED, |
| 24 | CLIP_PATH, |
| 25 | CLIP_REGION, |
| 26 | CLIP_RECT, |
| 27 | CONCAT, |
| 28 | DRAW_BITMAP, |
| 29 | DRAW_BITMAP_MATRIX, |
reed@google.com | f0b5e11 | 2011-09-07 11:57:34 +0000 | [diff] [blame] | 30 | DRAW_BITMAP_NINE, |
reed@android.com | 8a1c16f | 2008-12-17 15:59:43 +0000 | [diff] [blame] | 31 | DRAW_BITMAP_RECT, |
reed@google.com | 2a98181 | 2011-04-14 18:59:28 +0000 | [diff] [blame] | 32 | DRAW_CLEAR, |
reed@android.com | cb60844 | 2009-12-04 21:32:27 +0000 | [diff] [blame] | 33 | DRAW_DATA, |
reed@android.com | 8a1c16f | 2008-12-17 15:59:43 +0000 | [diff] [blame] | 34 | DRAW_PAINT, |
| 35 | DRAW_PATH, |
| 36 | DRAW_PICTURE, |
| 37 | DRAW_POINTS, |
| 38 | DRAW_POS_TEXT, |
reed@google.com | 9efd9a0 | 2012-01-30 15:41:43 +0000 | [diff] [blame] | 39 | DRAW_POS_TEXT_TOP_BOTTOM, // fast variant of DRAW_POS_TEXT |
reed@android.com | 8a1c16f | 2008-12-17 15:59:43 +0000 | [diff] [blame] | 40 | DRAW_POS_TEXT_H, |
| 41 | DRAW_POS_TEXT_H_TOP_BOTTOM, // fast variant of DRAW_POS_TEXT_H |
| 42 | DRAW_RECT, |
| 43 | DRAW_SPRITE, |
| 44 | DRAW_TEXT, |
| 45 | DRAW_TEXT_ON_PATH, |
| 46 | DRAW_TEXT_TOP_BOTTOM, // fast variant of DRAW_TEXT |
| 47 | DRAW_VERTICES, |
| 48 | RESTORE, |
| 49 | ROTATE, |
| 50 | SAVE, |
| 51 | SAVE_LAYER, |
| 52 | SCALE, |
reed@android.com | 6e073b9 | 2009-01-06 15:03:30 +0000 | [diff] [blame] | 53 | SET_MATRIX, |
reed@android.com | 8a1c16f | 2008-12-17 15:59:43 +0000 | [diff] [blame] | 54 | SKEW, |
| 55 | TRANSLATE |
| 56 | }; |
| 57 | |
| 58 | enum DrawVertexFlags { |
| 59 | DRAW_VERTICES_HAS_TEXS = 0x01, |
| 60 | DRAW_VERTICES_HAS_COLORS = 0x02, |
| 61 | DRAW_VERTICES_HAS_INDICES = 0x04 |
| 62 | }; |
| 63 | |
reed@google.com | 83ab495 | 2011-11-11 21:34:54 +0000 | [diff] [blame] | 64 | /////////////////////////////////////////////////////////////////////////////// |
| 65 | // clipparams are packed in 5 bits |
| 66 | // doAA:1 | regionOp:4 |
| 67 | |
| 68 | static inline uint32_t ClipParams_pack(SkRegion::Op op, bool doAA) { |
| 69 | unsigned doAABit = doAA ? 1 : 0; |
| 70 | return (doAABit << 4) | op; |
| 71 | } |
| 72 | |
| 73 | static inline SkRegion::Op ClipParams_unpackRegionOp(uint32_t packed) { |
| 74 | return (SkRegion::Op)(packed & 0xF); |
| 75 | } |
| 76 | |
| 77 | static inline bool ClipParams_unpackDoAA(uint32_t packed) { |
| 78 | return SkToBool((packed >> 4) & 1); |
| 79 | } |
| 80 | |
| 81 | /////////////////////////////////////////////////////////////////////////////// |
| 82 | |
reed@android.com | 8a1c16f | 2008-12-17 15:59:43 +0000 | [diff] [blame] | 83 | class SkRefCntPlayback { |
| 84 | public: |
| 85 | SkRefCntPlayback(); |
reed@android.com | 04225dc | 2009-03-20 04:59:37 +0000 | [diff] [blame] | 86 | virtual ~SkRefCntPlayback(); |
reed@android.com | 8a1c16f | 2008-12-17 15:59:43 +0000 | [diff] [blame] | 87 | |
| 88 | int count() const { return fCount; } |
| 89 | |
mike@reedtribe.org | e9e08cc | 2011-04-29 01:44:52 +0000 | [diff] [blame] | 90 | void reset(const SkRefCntSet*); |
reed@android.com | 8a1c16f | 2008-12-17 15:59:43 +0000 | [diff] [blame] | 91 | |
| 92 | void setCount(int count); |
| 93 | SkRefCnt* set(int index, SkRefCnt*); |
| 94 | |
| 95 | virtual void setupBuffer(SkFlattenableReadBuffer& buffer) const { |
| 96 | buffer.setRefCntArray(fArray, fCount); |
| 97 | } |
| 98 | |
| 99 | protected: |
| 100 | int fCount; |
| 101 | SkRefCnt** fArray; |
| 102 | }; |
| 103 | |
| 104 | class SkTypefacePlayback : public SkRefCntPlayback { |
| 105 | public: |
reed@android.com | 8a1c16f | 2008-12-17 15:59:43 +0000 | [diff] [blame] | 106 | virtual void setupBuffer(SkFlattenableReadBuffer& buffer) const { |
| 107 | buffer.setTypefaceArray((SkTypeface**)fArray, fCount); |
| 108 | } |
| 109 | }; |
| 110 | |
| 111 | class SkFactoryPlayback { |
| 112 | public: |
| 113 | SkFactoryPlayback(int count) : fCount(count) { |
| 114 | fArray = SkNEW_ARRAY(SkFlattenable::Factory, count); |
| 115 | } |
| 116 | |
| 117 | ~SkFactoryPlayback() { |
| 118 | SkDELETE_ARRAY(fArray); |
| 119 | } |
| 120 | |
| 121 | SkFlattenable::Factory* base() const { return fArray; } |
| 122 | |
| 123 | void setupBuffer(SkFlattenableReadBuffer& buffer) const { |
| 124 | buffer.setFactoryPlayback(fArray, fCount); |
| 125 | } |
| 126 | |
| 127 | private: |
| 128 | int fCount; |
| 129 | SkFlattenable::Factory* fArray; |
| 130 | }; |
| 131 | |
djsollen@google.com | d2700ee | 2012-05-30 16:54:13 +0000 | [diff] [blame] | 132 | /////////////////////////////////////////////////////////////////////////////// |
| 133 | // |
| 134 | // |
| 135 | // The following templated classes provide an efficient way to store and compare |
| 136 | // objects that have been flattened (i.e. serialized in an ordered binary |
| 137 | // format). |
| 138 | // |
| 139 | // SkFlatData: is a simple indexable container for the flattened data |
| 140 | // which is agnostic to the type of data is is indexing. It is |
| 141 | // also responsible for flattening/unflattening objects but |
| 142 | // details of that operation are hidden in the provided procs |
| 143 | // SkFlatDictionary: is a abstract templated dictionary that maintains a |
| 144 | // searchable set of SkFlataData objects of type T. |
| 145 | // |
| 146 | // NOTE: any class that wishes to be used in conjunction with SkFlatDictionary |
| 147 | // must subclass the dictionary and provide the necessary flattening procs. |
| 148 | // The end of this header contains dictionary subclasses for some common classes |
| 149 | // like SkBitmap, SkMatrix, SkPaint, and SkRegion. |
| 150 | // |
| 151 | // |
| 152 | /////////////////////////////////////////////////////////////////////////////// |
| 153 | |
reed@android.com | 8a1c16f | 2008-12-17 15:59:43 +0000 | [diff] [blame] | 154 | class SkFlatData { |
| 155 | public: |
djsollen@google.com | d2700ee | 2012-05-30 16:54:13 +0000 | [diff] [blame] | 156 | |
reed@android.com | 8a1c16f | 2008-12-17 15:59:43 +0000 | [diff] [blame] | 157 | static int Compare(const SkFlatData* a, const SkFlatData* b) { |
reed@google.com | b4ed017 | 2012-07-10 13:29:52 +0000 | [diff] [blame^] | 158 | size_t bytes = a->bytesToCompare(); |
| 159 | SkASSERT(SkIsAlign4(bytes)); |
reed@google.com | 142e1fe | 2012-07-09 17:44:44 +0000 | [diff] [blame] | 160 | |
reed@google.com | b4ed017 | 2012-07-10 13:29:52 +0000 | [diff] [blame^] | 161 | const uint32_t* a_ptr = a->dataToCompare(); |
| 162 | const uint32_t* b_ptr = b->dataToCompare(); |
| 163 | const uint32_t* stop = a_ptr + bytes / sizeof(uint32_t); |
junov@chromium.org | ef76060 | 2012-06-27 20:03:16 +0000 | [diff] [blame] | 164 | while(a_ptr < stop) { |
| 165 | if (*a_ptr != *b_ptr) { |
| 166 | return (*a_ptr < *b_ptr) ? -1 : 1; |
| 167 | } |
| 168 | a_ptr++; |
| 169 | b_ptr++; |
| 170 | } |
| 171 | return 0; |
reed@android.com | 8a1c16f | 2008-12-17 15:59:43 +0000 | [diff] [blame] | 172 | } |
| 173 | |
| 174 | int index() const { return fIndex; } |
reed@google.com | b4ed017 | 2012-07-10 13:29:52 +0000 | [diff] [blame^] | 175 | const void* data() const { return (const char*)this + sizeof(*this); } |
| 176 | void* data() { return (char*)this + sizeof(*this); } |
| 177 | // Our data is always 32bit aligned, so we can offer this accessor |
| 178 | uint32_t* data32() { return (uint32_t*)this->data(); } |
reed@android.com | 8a1c16f | 2008-12-17 15:59:43 +0000 | [diff] [blame] | 179 | |
| 180 | #ifdef SK_DEBUG_SIZE |
djsollen@google.com | d2700ee | 2012-05-30 16:54:13 +0000 | [diff] [blame] | 181 | size_t size() const { return sizeof(SkFlatData) + fAllocSize; } |
reed@android.com | 8a1c16f | 2008-12-17 15:59:43 +0000 | [diff] [blame] | 182 | #endif |
| 183 | |
djsollen@google.com | d2700ee | 2012-05-30 16:54:13 +0000 | [diff] [blame] | 184 | static SkFlatData* Create(SkChunkAlloc* heap, const void* obj, int index, |
| 185 | void (*flattenProc)(SkOrderedWriteBuffer&, const void*), |
| 186 | SkRefCntSet* refCntRecorder = NULL, |
junov@chromium.org | 4866cc0 | 2012-06-01 21:23:07 +0000 | [diff] [blame] | 187 | SkRefCntSet* faceRecorder = NULL, |
| 188 | uint32_t writeBufferflags = 0); |
djsollen@google.com | d2700ee | 2012-05-30 16:54:13 +0000 | [diff] [blame] | 189 | void unflatten(void* result, |
| 190 | void (*unflattenProc)(SkOrderedReadBuffer&, void*), |
| 191 | SkRefCntPlayback* refCntPlayback = NULL, |
| 192 | SkTypefacePlayback* facePlayback = NULL) const; |
| 193 | |
| 194 | private: |
junov@chromium.org | ef76060 | 2012-06-27 20:03:16 +0000 | [diff] [blame] | 195 | // Data members add-up to 128 bits of storage, so data() is 128-bit |
| 196 | // aligned, which helps performance of memcpy in SkWriter32::flatten |
reed@android.com | 8a1c16f | 2008-12-17 15:59:43 +0000 | [diff] [blame] | 197 | int fIndex; |
reed@google.com | b4ed017 | 2012-07-10 13:29:52 +0000 | [diff] [blame^] | 198 | |
| 199 | // From here down is the data we look at in the search/sort. We always begin |
| 200 | // with the checksum and then length. |
junov@chromium.org | ef76060 | 2012-06-27 20:03:16 +0000 | [diff] [blame] | 201 | uint32_t fChecksum; |
reed@google.com | b4ed017 | 2012-07-10 13:29:52 +0000 | [diff] [blame^] | 202 | int32_t fAllocSize; |
| 203 | // uint32_t data[] |
| 204 | |
| 205 | const uint32_t* dataToCompare() const { return &fChecksum; } |
| 206 | size_t bytesToCompare() const { |
| 207 | return sizeof(fChecksum) + sizeof(fAllocSize) + fAllocSize; |
| 208 | } |
reed@android.com | 8a1c16f | 2008-12-17 15:59:43 +0000 | [diff] [blame] | 209 | }; |
| 210 | |
djsollen@google.com | d2700ee | 2012-05-30 16:54:13 +0000 | [diff] [blame] | 211 | template <class T> |
| 212 | class SkFlatDictionary { |
reed@android.com | 8a1c16f | 2008-12-17 15:59:43 +0000 | [diff] [blame] | 213 | public: |
djsollen@google.com | d2700ee | 2012-05-30 16:54:13 +0000 | [diff] [blame] | 214 | SkFlatDictionary(SkChunkAlloc* heap) { |
| 215 | fFlattenProc = NULL; |
| 216 | fUnflattenProc = NULL; |
| 217 | fHeap = heap; |
| 218 | // set to 1 since returning a zero from find() indicates failure |
| 219 | fNextIndex = 1; |
| 220 | } |
reed@android.com | 8a1c16f | 2008-12-17 15:59:43 +0000 | [diff] [blame] | 221 | |
djsollen@google.com | d2700ee | 2012-05-30 16:54:13 +0000 | [diff] [blame] | 222 | int count() const { return fData.count(); } |
| 223 | |
| 224 | const SkFlatData* operator[](int index) const { |
| 225 | SkASSERT(index >= 0 && index < fData.count()); |
| 226 | return fData[index]; |
| 227 | } |
| 228 | |
| 229 | /** |
| 230 | * Clears the dictionary of all entries. However, it does NOT free the |
| 231 | * memory that was allocated for each entry. |
| 232 | */ |
| 233 | void reset() { fData.reset(); fNextIndex = 1; } |
| 234 | |
| 235 | /** |
| 236 | * Given an element of type T it returns its index in the dictionary. If |
| 237 | * the element wasn't previously in the dictionary it is automatically added |
| 238 | */ |
| 239 | int find(const T* element, SkRefCntSet* refCntRecorder = NULL, |
junov@chromium.org | 4866cc0 | 2012-06-01 21:23:07 +0000 | [diff] [blame] | 240 | SkRefCntSet* faceRecorder = NULL, uint32_t writeBufferflags = 0) { |
djsollen@google.com | d2700ee | 2012-05-30 16:54:13 +0000 | [diff] [blame] | 241 | if (element == NULL) |
| 242 | return 0; |
| 243 | SkFlatData* flat = SkFlatData::Create(fHeap, element, fNextIndex, |
junov@chromium.org | 4866cc0 | 2012-06-01 21:23:07 +0000 | [diff] [blame] | 244 | fFlattenProc, refCntRecorder, faceRecorder, writeBufferflags); |
djsollen@google.com | d2700ee | 2012-05-30 16:54:13 +0000 | [diff] [blame] | 245 | int index = SkTSearch<SkFlatData>((const SkFlatData**) fData.begin(), |
| 246 | fData.count(), flat, sizeof(flat), &SkFlatData::Compare); |
| 247 | if (index >= 0) { |
| 248 | (void)fHeap->unalloc(flat); |
| 249 | return fData[index]->index(); |
reed@android.com | 8a1c16f | 2008-12-17 15:59:43 +0000 | [diff] [blame] | 250 | } |
djsollen@google.com | d2700ee | 2012-05-30 16:54:13 +0000 | [diff] [blame] | 251 | index = ~index; |
| 252 | *fData.insert(index) = flat; |
| 253 | SkASSERT(fData.count() == fNextIndex); |
| 254 | return fNextIndex++; |
reed@android.com | 8a1c16f | 2008-12-17 15:59:43 +0000 | [diff] [blame] | 255 | } |
| 256 | |
djsollen@google.com | d2700ee | 2012-05-30 16:54:13 +0000 | [diff] [blame] | 257 | /** |
| 258 | * Given a pointer to a array of type T we allocate the array and fill it |
| 259 | * with the unflattened dictionary contents. The return value is the size of |
| 260 | * the allocated array. |
| 261 | */ |
| 262 | int unflattenDictionary(T*& array, SkRefCntPlayback* refCntPlayback = NULL, |
| 263 | SkTypefacePlayback* facePlayback = NULL) const { |
| 264 | int elementCount = fData.count(); |
| 265 | if (elementCount > 0) { |
| 266 | array = SkNEW_ARRAY(T, elementCount); |
| 267 | for (const SkFlatData** elementPtr = fData.begin(); |
| 268 | elementPtr != fData.end(); elementPtr++) { |
| 269 | const SkFlatData* element = *elementPtr; |
| 270 | int index = element->index() - 1; |
| 271 | element->unflatten(&array[index], fUnflattenProc, |
| 272 | refCntPlayback, facePlayback); |
| 273 | } |
| 274 | } |
| 275 | return elementCount; |
reed@android.com | 8a1c16f | 2008-12-17 15:59:43 +0000 | [diff] [blame] | 276 | } |
djsollen@google.com | d2700ee | 2012-05-30 16:54:13 +0000 | [diff] [blame] | 277 | |
| 278 | protected: |
| 279 | void (*fFlattenProc)(SkOrderedWriteBuffer&, const void*); |
| 280 | void (*fUnflattenProc)(SkOrderedReadBuffer&, void*); |
reed@android.com | 8a1c16f | 2008-12-17 15:59:43 +0000 | [diff] [blame] | 281 | |
| 282 | private: |
djsollen@google.com | d2700ee | 2012-05-30 16:54:13 +0000 | [diff] [blame] | 283 | SkChunkAlloc* fHeap; |
| 284 | int fNextIndex; |
| 285 | SkTDArray<const SkFlatData*> fData; |
reed@android.com | 8a1c16f | 2008-12-17 15:59:43 +0000 | [diff] [blame] | 286 | }; |
| 287 | |
djsollen@google.com | d2700ee | 2012-05-30 16:54:13 +0000 | [diff] [blame] | 288 | /////////////////////////////////////////////////////////////////////////////// |
| 289 | // Some common dictionaries are defined here for both reference and convenience |
| 290 | /////////////////////////////////////////////////////////////////////////////// |
| 291 | |
| 292 | template <class T> |
| 293 | static void SkFlattenObjectProc(SkOrderedWriteBuffer& buffer, const void* obj) { |
| 294 | ((T*)obj)->flatten(buffer); |
| 295 | } |
| 296 | |
| 297 | template <class T> |
| 298 | static void SkUnflattenObjectProc(SkOrderedReadBuffer& buffer, void* obj) { |
| 299 | ((T*)obj)->unflatten(buffer); |
| 300 | } |
| 301 | |
| 302 | class SkBitmapDictionary : public SkFlatDictionary<SkBitmap> { |
reed@android.com | 8a1c16f | 2008-12-17 15:59:43 +0000 | [diff] [blame] | 303 | public: |
djsollen@google.com | d2700ee | 2012-05-30 16:54:13 +0000 | [diff] [blame] | 304 | SkBitmapDictionary(SkChunkAlloc* heap) : SkFlatDictionary<SkBitmap>(heap) { |
| 305 | fFlattenProc = &SkFlattenObjectProc<SkBitmap>; |
| 306 | fUnflattenProc = &SkUnflattenObjectProc<SkBitmap>; |
reed@android.com | 8a1c16f | 2008-12-17 15:59:43 +0000 | [diff] [blame] | 307 | } |
reed@android.com | 8a1c16f | 2008-12-17 15:59:43 +0000 | [diff] [blame] | 308 | }; |
| 309 | |
djsollen@google.com | d2700ee | 2012-05-30 16:54:13 +0000 | [diff] [blame] | 310 | class SkMatrixDictionary : public SkFlatDictionary<SkMatrix> { |
| 311 | public: |
| 312 | SkMatrixDictionary(SkChunkAlloc* heap) : SkFlatDictionary<SkMatrix>(heap) { |
| 313 | fFlattenProc = &flattenMatrix; |
| 314 | fUnflattenProc = &unflattenMatrix; |
reed@android.com | 8a1c16f | 2008-12-17 15:59:43 +0000 | [diff] [blame] | 315 | } |
reed@android.com | 8a1c16f | 2008-12-17 15:59:43 +0000 | [diff] [blame] | 316 | |
djsollen@google.com | d2700ee | 2012-05-30 16:54:13 +0000 | [diff] [blame] | 317 | static void flattenMatrix(SkOrderedWriteBuffer& buffer, const void* obj) { |
| 318 | buffer.getWriter32()->writeMatrix(*(SkMatrix*)obj); |
| 319 | } |
| 320 | |
| 321 | static void unflattenMatrix(SkOrderedReadBuffer& buffer, void* obj) { |
| 322 | buffer.getReader32()->readMatrix((SkMatrix*)obj); |
| 323 | } |
reed@android.com | 8a1c16f | 2008-12-17 15:59:43 +0000 | [diff] [blame] | 324 | }; |
| 325 | |
djsollen@google.com | d2700ee | 2012-05-30 16:54:13 +0000 | [diff] [blame] | 326 | class SkPaintDictionary : public SkFlatDictionary<SkPaint> { |
| 327 | public: |
| 328 | SkPaintDictionary(SkChunkAlloc* heap) : SkFlatDictionary<SkPaint>(heap) { |
| 329 | fFlattenProc = &SkFlattenObjectProc<SkPaint>; |
| 330 | fUnflattenProc = &SkUnflattenObjectProc<SkPaint>; |
| 331 | } |
| 332 | }; |
| 333 | |
| 334 | class SkRegionDictionary : public SkFlatDictionary<SkRegion> { |
| 335 | public: |
| 336 | SkRegionDictionary(SkChunkAlloc* heap) : SkFlatDictionary<SkRegion>(heap) { |
| 337 | fFlattenProc = &flattenRegion; |
| 338 | fUnflattenProc = &unflattenRegion; |
reed@android.com | 8a1c16f | 2008-12-17 15:59:43 +0000 | [diff] [blame] | 339 | } |
| 340 | |
djsollen@google.com | d2700ee | 2012-05-30 16:54:13 +0000 | [diff] [blame] | 341 | static void flattenRegion(SkOrderedWriteBuffer& buffer, const void* obj) { |
| 342 | buffer.getWriter32()->writeRegion(*(SkRegion*)obj); |
reed@android.com | 8a1c16f | 2008-12-17 15:59:43 +0000 | [diff] [blame] | 343 | } |
reed@android.com | 8a1c16f | 2008-12-17 15:59:43 +0000 | [diff] [blame] | 344 | |
djsollen@google.com | d2700ee | 2012-05-30 16:54:13 +0000 | [diff] [blame] | 345 | static void unflattenRegion(SkOrderedReadBuffer& buffer, void* obj) { |
| 346 | buffer.getReader32()->readRegion((SkRegion*)obj); |
| 347 | } |
reed@android.com | 8a1c16f | 2008-12-17 15:59:43 +0000 | [diff] [blame] | 348 | }; |
| 349 | |
| 350 | #endif |