OLD | NEW |
---|---|
1 | 1 |
2 /* | 2 /* |
3 * Copyright 2011 Google Inc. | 3 * Copyright 2011 Google Inc. |
4 * | 4 * |
5 * Use of this source code is governed by a BSD-style license that can be | 5 * Use of this source code is governed by a BSD-style license that can be |
6 * found in the LICENSE file. | 6 * found in the LICENSE file. |
7 */ | 7 */ |
8 #ifndef SkPictureFlat_DEFINED | 8 #ifndef SkPictureFlat_DEFINED |
9 #define SkPictureFlat_DEFINED | 9 #define SkPictureFlat_DEFINED |
10 | 10 |
(...skipping 133 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
144 // | 144 // |
145 // The following templated classes provide an efficient way to store and compare | 145 // The following templated classes provide an efficient way to store and compare |
146 // objects that have been flattened (i.e. serialized in an ordered binary | 146 // objects that have been flattened (i.e. serialized in an ordered binary |
147 // format). | 147 // format). |
148 // | 148 // |
149 // SkFlatData: is a simple indexable container for the flattened data | 149 // SkFlatData: is a simple indexable container for the flattened data |
150 // which is agnostic to the type of data is is indexing. It is | 150 // which is agnostic to the type of data is is indexing. It is |
151 // also responsible for flattening/unflattening objects but | 151 // also responsible for flattening/unflattening objects but |
152 // details of that operation are hidden in the provided procs | 152 // details of that operation are hidden in the provided procs |
153 // SkFlatDictionary: is an abstract templated dictionary that maintains a | 153 // SkFlatDictionary: is an abstract templated dictionary that maintains a |
154 // searchable set of SkFlataData objects of type T. | 154 // searchable set of SkFlatData objects of type T. |
155 // SkFlatController: is an interface provided to SkFlatDictionary which handles | 155 // SkFlatController: is an interface provided to SkFlatDictionary which handles |
156 // allocation and unallocation in some cases. It also holds | 156 // allocation (and unallocation in some cases). It also holds |
157 // ref count recorders and the like. | 157 // ref count recorders and the like. |
158 // | 158 // |
159 // NOTE: any class that wishes to be used in conjunction with SkFlatDictionary | 159 // NOTE: any class that wishes to be used in conjunction with SkFlatDictionary |
160 // must subclass the dictionary and provide the necessary flattening procs. | 160 // must subclass the dictionary and provide the necessary flattening procs. |
161 // The end of this header contains dictionary subclasses for some common classes | 161 // The end of this header contains dictionary subclasses for some common classes |
162 // like SkBitmap, SkMatrix, SkPaint, and SkRegion. SkFlatController must also | 162 // like SkBitmap, SkMatrix, SkPaint, and SkRegion. SkFlatController must also |
163 // be implemented, or SkChunkFlatController can be used to use an | 163 // be implemented, or SkChunkFlatController can be used to use an |
164 // SkChunkAllocator and never do replacements. | 164 // SkChunkAllocator and never do replacements. |
165 // | 165 // |
166 // | 166 // |
167 /////////////////////////////////////////////////////////////////////////////// | 167 /////////////////////////////////////////////////////////////////////////////// |
168 | 168 |
169 class SkFlatData; | 169 class SkFlatData; |
170 | 170 |
171 class SkFlatController : public SkRefCnt { | 171 class SkFlatController : public SkRefCnt { |
172 public: | 172 public: |
173 SK_DECLARE_INST_COUNT(SkFlatController) | 173 SK_DECLARE_INST_COUNT(SkFlatController) |
174 | 174 |
175 SkFlatController(); | 175 SkFlatController(); |
176 virtual ~SkFlatController(); | 176 virtual ~SkFlatController(); |
177 /** | 177 /** |
178 * Provide a new block of memory for the SkFlatDictionary to use. | 178 * Return a new block of memory for the SkFlatDictionary to use. |
179 * This memory is owned by the controller and has the same lifetime unless y ou | |
180 * call unalloc(), in which case it may be freed early. | |
179 */ | 181 */ |
180 virtual void* allocThrow(size_t bytes) = 0; | 182 virtual void* allocThrow(size_t bytes) = 0; |
181 | 183 |
182 /** | 184 /** |
183 * Unallocate a previously allocated block, returned by allocThrow. | 185 * Hint that this block, which was allocated with allocThrow, is no longer n eeded. |
184 * Implementation should at least perform an unallocation if passed the last | 186 * The implementation may choose to free this memory any time beteween now a nd destruction. |
185 * pointer returned by allocThrow. If findAndReplace() is intended to be | |
186 * used, unalloc should also be able to unallocate the SkFlatData that is | |
187 * provided. | |
188 */ | 187 */ |
189 virtual void unalloc(void* ptr) = 0; | 188 virtual void unalloc(void* ptr) = 0; |
190 | 189 |
191 /** | 190 /** |
192 * Used during creation and unflattening of SkFlatData objects. If the | 191 * Used during creation and unflattening of SkFlatData objects. If the |
193 * objects being flattened contain bitmaps they are stored in this heap | 192 * objects being flattened contain bitmaps they are stored in this heap |
194 * and the flattenable stores the index to the bitmap on the heap. | 193 * and the flattenable stores the index to the bitmap on the heap. |
195 * This should be set by the protected setBitmapHeap. | 194 * This should be set by the protected setBitmapHeap. |
196 */ | 195 */ |
197 SkBitmapHeap* getBitmapHeap() { return fBitmapHeap; } | 196 SkBitmapHeap* getBitmapHeap() { return fBitmapHeap; } |
(...skipping 195 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
393 } | 392 } |
394 | 393 |
395 enum { | 394 enum { |
396 kInCache_Sentinel = 0, | 395 kInCache_Sentinel = 0, |
397 kCandidate_Sentinel = ~0U, | 396 kCandidate_Sentinel = ~0U, |
398 }; | 397 }; |
399 void setSentinel(uint32_t value) { | 398 void setSentinel(uint32_t value) { |
400 SkASSERT(SkIsAlign4(fFlatSize)); | 399 SkASSERT(SkIsAlign4(fFlatSize)); |
401 this->data32()[fFlatSize >> 2] = value; | 400 this->data32()[fFlatSize >> 2] = value; |
402 } | 401 } |
402 | |
403 void init(int index, int32_t size); | |
404 template <class T> friend class SkFlatDictionary; // For init(). | |
403 }; | 405 }; |
404 | 406 |
405 template <class T> | 407 template <class T> |
406 class SkFlatDictionary { | 408 class SkFlatDictionary { |
409 static const size_t kWriteBufferGrowthBytes = 1024; | |
410 | |
407 public: | 411 public: |
408 SkFlatDictionary(SkFlatController* controller) | 412 SkFlatDictionary(SkFlatController* controller) |
409 : fController(controller) { | 413 : fFlattenProc(NULL) |
410 fFlattenProc = NULL; | 414 , fUnflattenProc(NULL) |
411 fUnflattenProc = NULL; | 415 , fController(SkRef(controller)) |
412 SkASSERT(controller); | 416 , fNextIndex(1) // set to 1 since returning a zero from find() indicates fa ilure |
413 fController->ref(); | 417 , fDataSize(0) |
414 // set to 1 since returning a zero from find() indicates failure | 418 , fFlatData(this->allocFlatData(fDataSize)) |
415 fNextIndex = 1; | 419 , fWriteBuffer(kWriteBufferGrowthBytes) |
420 , fWriteBufferReady(false) { | |
416 sk_bzero(fHash, sizeof(fHash)); | 421 sk_bzero(fHash, sizeof(fHash)); |
417 // index 0 is always empty since it is used as a signal that find failed | 422 // index 0 is always empty since it is used as a signal that find failed |
418 fIndexedData.push(NULL); | 423 fIndexedData.push(NULL); |
419 } | 424 } |
420 | 425 |
421 virtual ~SkFlatDictionary() { | |
422 fController->unref(); | |
423 } | |
424 | |
425 int count() const { | 426 int count() const { |
426 SkASSERT(fIndexedData.count() == fSortedData.count()+1); | 427 SkASSERT(fIndexedData.count() == fSortedData.count()+1); |
427 return fSortedData.count(); | 428 return fSortedData.count(); |
428 } | 429 } |
429 | 430 |
430 const SkFlatData* operator[](int index) const { | 431 const SkFlatData* operator[](int index) const { |
431 SkASSERT(index >= 0 && index < fSortedData.count()); | 432 SkASSERT(index >= 0 && index < fSortedData.count()); |
432 return fSortedData[index]; | 433 return fSortedData[index]; |
433 } | 434 } |
434 | 435 |
(...skipping 90 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
525 SkASSERT(fIndexedData.count() == fSortedData.count()+1); | 526 SkASSERT(fIndexedData.count() == fSortedData.count()+1); |
526 const SkFlatData* element = fIndexedData[index]; | 527 const SkFlatData* element = fIndexedData[index]; |
527 SkASSERT(index == element->index()); | 528 SkASSERT(index == element->index()); |
528 | 529 |
529 T* dst = new T; | 530 T* dst = new T; |
530 this->unflatten(dst, element); | 531 this->unflatten(dst, element); |
531 return dst; | 532 return dst; |
532 } | 533 } |
533 | 534 |
534 const SkFlatData* findAndReturnFlat(const T& element) { | 535 const SkFlatData* findAndReturnFlat(const T& element) { |
535 SkFlatData* flat = SkFlatData::Create(fController, &element, fNextIndex, fFlattenProc); | 536 const SkFlatData& temp = this->getTemporary(element, fNextIndex); |
reed1
2013/07/22 18:07:49
scratch
mtklein
2013/07/22 18:54:48
Done.
| |
536 | 537 |
537 int hashIndex = ChecksumToHashIndex(flat->checksum()); | 538 // See if we have it in the hash? |
539 const int hashIndex = ChecksumToHashIndex(temp.checksum()); | |
538 const SkFlatData* candidate = fHash[hashIndex]; | 540 const SkFlatData* candidate = fHash[hashIndex]; |
539 if (candidate && !SkFlatData::Compare(*flat, *candidate)) { | 541 if (candidate != NULL && SkFlatData::Compare(temp, *candidate) == 0) ret urn candidate; |
540 fController->unalloc(flat); | |
541 return candidate; | |
542 } | |
543 | 542 |
544 int index = SkTSearch<const SkFlatData, | 543 // See if we have it at all? |
545 SkFlatData::Less>((const SkFlatData**) fSortedData .begin(), | 544 const int index = SkTSearch<const SkFlatData, SkFlatData::Less>(fSortedD ata.begin(), |
546 fSortedData.count(), flat, sizeo f(flat)); | 545 fSortedD ata.count(), |
546 &temp, | |
547 sizeof(& temp)); | |
547 if (index >= 0) { | 548 if (index >= 0) { |
548 fController->unalloc(flat); | 549 // Found. Update hash before we return. |
549 fHash[hashIndex] = fSortedData[index]; | 550 fHash[hashIndex] = fSortedData[index]; |
550 return fSortedData[index]; | 551 return fSortedData[index]; |
551 } | 552 } |
552 | 553 |
553 index = ~index; | 554 // We don't have it. Add it. |
554 *fSortedData.insert(index) = flat; | 555 SkFlatData* durable = this->getDurable(); |
reed1
2013/07/22 18:07:49
Need some clearer naming or something to describe
mtklein
2013/07/22 18:54:48
How's this?
| |
555 *fIndexedData.insert(flat->index()) = flat; | 556 *fSortedData.insert(~index) = durable; // SkTSearch returned bit-not of where to insert. |
557 *fIndexedData.insert(durable->index()) = durable; | |
558 fHash[hashIndex] = durable; | |
559 | |
560 SkASSERT(durable->index() == fNextIndex); | |
556 SkASSERT(fSortedData.count() == fNextIndex); | 561 SkASSERT(fSortedData.count() == fNextIndex); |
562 SkASSERT(fIndexedData.count() == fNextIndex+1); | |
557 fNextIndex++; | 563 fNextIndex++; |
558 flat->setSentinelInCache(); | 564 |
559 fHash[hashIndex] = flat; | 565 return durable; |
560 SkASSERT(fIndexedData.count() == fSortedData.count()+1); | |
561 return flat; | |
562 } | 566 } |
563 | 567 |
564 protected: | 568 protected: |
565 void (*fFlattenProc)(SkOrderedWriteBuffer&, const void*); | 569 void (*fFlattenProc)(SkOrderedWriteBuffer&, const void*); |
566 void (*fUnflattenProc)(SkOrderedReadBuffer&, void*); | 570 void (*fUnflattenProc)(SkOrderedReadBuffer&, void*); |
567 | 571 |
568 private: | 572 private: |
573 // Layout: [ SkFlatData header, 20 bytes ] [ data ..., 4-byte aligned ] [ se ntinel, 4 bytes] | |
574 uint8_t* allocFlatData(size_t dataSize) { | |
575 SkASSERT(SkIsAlign4(dataSize)); | |
576 return (uint8_t*) fController->allocThrow(sizeof(SkFlatData) + dataSize + sizeof(uint32_t)); | |
577 } | |
578 | |
579 // Returns a pointer to where we should start writing flattened data (just p ast the header). | |
580 static uint8_t* dataStart(uint8_t* flatData) { | |
581 return flatData + sizeof(SkFlatData); | |
582 } | |
583 | |
584 // We have to delay fWriteBuffer's initialization until its first use; fCont roller might not | |
585 // be fully set up by the time we get it in the constructor. | |
586 void lazyWriteBufferInit() { | |
587 if (fWriteBufferReady) return; | |
reed1
2013/07/22 18:07:49
LW nit {}
mtklein
2013/07/22 18:54:48
Done.
| |
588 // Without a bitmap heap, we'll flatten bitmaps into paints. That's nev er what you want. | |
589 SkASSERT(fController->getBitmapHeap() != NULL); | |
590 fWriteBuffer.setBitmapHeap(fController->getBitmapHeap()); | |
591 fWriteBuffer.setTypefaceRecorder(fController->getTypefaceSet()); | |
592 fWriteBuffer.setNamedFactoryRecorder(fController->getNamedFactorySet()); | |
593 fWriteBuffer.setFlags(fController->getWriteBufferFlags()); | |
594 fWriteBufferReady = true; | |
595 } | |
596 | |
597 // This reference is valid only until the next call to getTemporary() or get Durable(). | |
598 const SkFlatData& getTemporary(const T& element, int index) { | |
599 this->lazyWriteBufferInit(); | |
600 | |
601 // If all the flattened bytes fit into fFlatData, we can skip the call t o writeToMemory. | |
602 fWriteBuffer.reset(this->dataStart(fFlatData), fDataSize); | |
603 fFlattenProc(fWriteBuffer, &element); | |
604 const size_t size = fWriteBuffer.size(); | |
reed1
2013/07/22 18:07:49
bytesWritten() ?
mtklein
2013/07/22 18:54:48
Done.
| |
605 | |
606 if (!fWriteBuffer.wroteOnlyToStorage()) { | |
607 // It didn't all fit. Copy into a bigger replacement fFlatData. | |
608 SkASSERT(size > fDataSize); | |
609 uint8_t* bigger = this->allocFlatData(size); | |
610 fWriteBuffer.writeToMemory(this->dataStart(bigger)); | |
reed1
2013/07/22 18:07:49
can SkFlatData have a dataStart() method, so that
mtklein
2013/07/22 18:54:48
Done.
| |
611 fController->unalloc(fFlatData); | |
612 fDataSize = size; | |
613 fFlatData = bigger; | |
614 } | |
615 | |
616 // The data is in fFlatData now, but we need to stamp its header and tra iling sentinel. | |
617 SkFlatData* temp = (SkFlatData*)fFlatData; | |
618 temp->init(index, size); | |
619 return *temp; | |
620 } | |
621 | |
622 // This result is owned by fController and lives as long as it does (unless unalloc'd). | |
623 SkFlatData* getDurable() { | |
624 SkFlatData* durable = (SkFlatData*)fFlatData; | |
625 durable->setSentinelInCache(); | |
626 // The next flat data we see has a good chance of being at least this bi g. | |
627 fFlatData = this->allocFlatData(fDataSize); | |
628 return durable; | |
629 } | |
630 | |
569 void unflatten(T* dst, const SkFlatData* element) const { | 631 void unflatten(T* dst, const SkFlatData* element) const { |
570 element->unflatten(dst, fUnflattenProc, | 632 element->unflatten(dst, fUnflattenProc, |
571 fController->getBitmapHeap(), | 633 fController->getBitmapHeap(), |
572 fController->getTypefacePlayback()); | 634 fController->getTypefacePlayback()); |
573 } | 635 } |
574 | 636 |
575 void unflattenIntoArray(T* array) const { | 637 void unflattenIntoArray(T* array) const { |
576 const int count = fSortedData.count(); | 638 const int count = fSortedData.count(); |
577 SkASSERT(fIndexedData.count() == fSortedData.count()+1); | 639 SkASSERT(fIndexedData.count() == fSortedData.count()+1); |
578 const SkFlatData* const* iter = fSortedData.begin(); | 640 const SkFlatData* const* iter = fSortedData.begin(); |
579 for (int i = 0; i < count; ++i) { | 641 for (int i = 0; i < count; ++i) { |
580 const SkFlatData* element = iter[i]; | 642 const SkFlatData* element = iter[i]; |
581 int index = element->index() - 1; | 643 int index = element->index() - 1; |
582 SkASSERT((unsigned)index < (unsigned)count); | 644 SkASSERT((unsigned)index < (unsigned)count); |
583 unflatten(&array[index], element); | 645 unflatten(&array[index], element); |
584 } | 646 } |
585 } | 647 } |
586 | 648 |
587 SkFlatController * const fController; | 649 SkAutoTUnref<SkFlatController> fController; |
588 int fNextIndex; | 650 size_t fDataSize; |
651 uint8_t* fFlatData; // length == sizeof(SkFlatData) + fDataSize + sizeof(ui nt32_t) | |
652 SkOrderedWriteBuffer fWriteBuffer; | |
653 bool fWriteBufferReady; | |
589 | 654 |
590 // SkFlatDictionary has two copies of the data one indexed by the | 655 // SkFlatDictionary has two copies of the data one indexed by the |
591 // SkFlatData's index and the other sorted. The sorted data is used | 656 // SkFlatData's index and the other sorted. The sorted data is used |
592 // for finding and uniquification while the indexed copy is used | 657 // for finding and uniquification while the indexed copy is used |
593 // for standard array-style lookups based on the SkFlatData's index | 658 // for standard array-style lookups based on the SkFlatData's index |
594 // (as in 'unflatten'). | 659 // (as in 'unflatten'). |
660 int fNextIndex; | |
595 SkTDArray<const SkFlatData*> fIndexedData; | 661 SkTDArray<const SkFlatData*> fIndexedData; |
596 // fSortedData is sorted by checksum/size/data. | 662 // fSortedData is sorted by checksum/size/data. |
597 SkTDArray<const SkFlatData*> fSortedData; | 663 SkTDArray<const SkFlatData*> fSortedData; |
598 | 664 |
599 enum { | 665 enum { |
600 // Determined by trying diff values on picture-recording benchmarks | 666 // Determined by trying diff values on picture-recording benchmarks |
601 // (e.g. PictureRecordBench.cpp), choosing the smallest value that | 667 // (e.g. PictureRecordBench.cpp), choosing the smallest value that |
602 // showed a big improvement. Even better would be to benchmark diff | 668 // showed a big improvement. Even better would be to benchmark diff |
603 // values on recording representative web-pages or other "real" content. | 669 // values on recording representative web-pages or other "real" content. |
604 HASH_BITS = 7, | 670 HASH_BITS = 7, |
(...skipping 33 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
638 | 704 |
639 class SkChunkFlatController : public SkFlatController { | 705 class SkChunkFlatController : public SkFlatController { |
640 public: | 706 public: |
641 SkChunkFlatController(size_t minSize) | 707 SkChunkFlatController(size_t minSize) |
642 : fHeap(minSize) | 708 : fHeap(minSize) |
643 , fTypefaceSet(SkNEW(SkRefCntSet)) { | 709 , fTypefaceSet(SkNEW(SkRefCntSet)) { |
644 this->setTypefaceSet(fTypefaceSet); | 710 this->setTypefaceSet(fTypefaceSet); |
645 this->setTypefacePlayback(&fTypefacePlayback); | 711 this->setTypefacePlayback(&fTypefacePlayback); |
646 } | 712 } |
647 | 713 |
648 ~SkChunkFlatController() { | |
649 fTypefaceSet->unref(); | |
650 } | |
651 | |
652 virtual void* allocThrow(size_t bytes) SK_OVERRIDE { | 714 virtual void* allocThrow(size_t bytes) SK_OVERRIDE { |
653 return fHeap.allocThrow(bytes); | 715 fLastAllocated = fHeap.allocThrow(bytes); |
716 return fLastAllocated; | |
654 } | 717 } |
655 | 718 |
656 virtual void unalloc(void* ptr) SK_OVERRIDE { | 719 virtual void unalloc(void* ptr) SK_OVERRIDE { |
657 (void) fHeap.unalloc(ptr); | 720 // fHeap can only free a pointer if it was the last one allocated. Othe rwise, we'll just |
721 // have to wait until fHeap is destroyed. | |
722 if (ptr == fLastAllocated) (void)fHeap.unalloc(ptr); | |
658 } | 723 } |
659 | 724 |
660 void setupPlaybacks() const { | 725 void setupPlaybacks() const { |
661 fTypefacePlayback.reset(fTypefaceSet); | 726 fTypefacePlayback.reset(fTypefaceSet.get()); |
662 } | 727 } |
663 | 728 |
664 void setBitmapStorage(SkBitmapHeap* heap) { | 729 void setBitmapStorage(SkBitmapHeap* heap) { |
665 this->setBitmapHeap(heap); | 730 this->setBitmapHeap(heap); |
666 } | 731 } |
667 | 732 |
668 private: | 733 private: |
669 SkChunkAlloc fHeap; | 734 SkChunkAlloc fHeap; |
670 SkRefCntSet* fTypefaceSet; | 735 SkAutoTUnref<SkRefCntSet> fTypefaceSet; |
736 void* fLastAllocated; | |
671 mutable SkTypefacePlayback fTypefacePlayback; | 737 mutable SkTypefacePlayback fTypefacePlayback; |
672 }; | 738 }; |
673 | 739 |
674 class SkBitmapDictionary : public SkFlatDictionary<SkBitmap> { | |
675 public: | |
676 SkBitmapDictionary(SkFlatController* controller) | |
677 : SkFlatDictionary<SkBitmap>(controller) { | |
678 fFlattenProc = &SkFlattenObjectProc<SkBitmap>; | |
679 fUnflattenProc = &SkUnflattenObjectProc<SkBitmap>; | |
680 } | |
681 }; | |
682 | |
683 class SkMatrixDictionary : public SkFlatDictionary<SkMatrix> { | 740 class SkMatrixDictionary : public SkFlatDictionary<SkMatrix> { |
684 public: | 741 public: |
685 SkMatrixDictionary(SkFlatController* controller) | 742 SkMatrixDictionary(SkFlatController* controller) |
686 : SkFlatDictionary<SkMatrix>(controller) { | 743 : SkFlatDictionary<SkMatrix>(controller) { |
687 fFlattenProc = &flattenMatrix; | 744 fFlattenProc = &flattenMatrix; |
688 fUnflattenProc = &unflattenMatrix; | 745 fUnflattenProc = &unflattenMatrix; |
689 } | 746 } |
690 | 747 |
691 static void flattenMatrix(SkOrderedWriteBuffer& buffer, const void* obj) { | 748 static void flattenMatrix(SkOrderedWriteBuffer& buffer, const void* obj) { |
692 buffer.getWriter32()->writeMatrix(*(SkMatrix*)obj); | 749 buffer.getWriter32()->writeMatrix(*(SkMatrix*)obj); |
(...skipping 24 matching lines...) Expand all Loading... | |
717 static void flattenRegion(SkOrderedWriteBuffer& buffer, const void* obj) { | 774 static void flattenRegion(SkOrderedWriteBuffer& buffer, const void* obj) { |
718 buffer.getWriter32()->writeRegion(*(SkRegion*)obj); | 775 buffer.getWriter32()->writeRegion(*(SkRegion*)obj); |
719 } | 776 } |
720 | 777 |
721 static void unflattenRegion(SkOrderedReadBuffer& buffer, void* obj) { | 778 static void unflattenRegion(SkOrderedReadBuffer& buffer, void* obj) { |
722 buffer.getReader32()->readRegion((SkRegion*)obj); | 779 buffer.getReader32()->readRegion((SkRegion*)obj); |
723 } | 780 } |
724 }; | 781 }; |
725 | 782 |
726 #endif | 783 #endif |
OLD | NEW |