1 /*
2 * Copyright 2018 Google Inc.
3 *
4 * Use of this source code is governed by a BSD-style license that can be
5 * found in the LICENSE file.
6 */
7
8 #include "src/core/SkStrikeCache.h"
9
10 #include <cctype>
11
12 #include "include/core/SkGraphics.h"
13 #include "include/core/SkTraceMemoryDump.h"
14 #include "include/core/SkTypeface.h"
15 #include "include/private/SkMutex.h"
16 #include "include/private/SkTemplates.h"
17 #include "src/core/SkGlyphRunPainter.h"
18 #include "src/core/SkStrike.h"
19
20 class SkStrikeCache::Node final : public SkStrikeInterface {
21 public:
Node(SkStrikeCache * strikeCache,const SkDescriptor & desc,std::unique_ptr<SkScalerContext> scaler,const SkFontMetrics & metrics,std::unique_ptr<SkStrikePinner> pinner)22 Node(SkStrikeCache* strikeCache,
23 const SkDescriptor& desc,
24 std::unique_ptr<SkScalerContext> scaler,
25 const SkFontMetrics& metrics,
26 std::unique_ptr<SkStrikePinner> pinner)
27 : fStrikeCache{strikeCache}
28 , fStrike{desc, std::move(scaler), metrics}
29 , fPinner{std::move(pinner)} {}
30
rounding() const31 SkVector rounding() const override {
32 return fStrike.rounding();
33 }
34
subpixelMask() const35 SkIPoint subpixelMask() const override {
36 return fStrike.subpixelMask();
37 }
38
39 SkSpan<const SkGlyphPos>
prepareForDrawingRemoveEmpty(const SkPackedGlyphID packedGlyphIDs[],const SkPoint positions[],size_t n,int maxDimension,PreparationDetail detail,SkGlyphPos results[])40 prepareForDrawingRemoveEmpty(const SkPackedGlyphID packedGlyphIDs[],
41 const SkPoint positions[],
42 size_t n,
43 int maxDimension,
44 PreparationDetail detail,
45 SkGlyphPos results[]) override {
46 return fStrike.prepareForDrawingRemoveEmpty(packedGlyphIDs,
47 positions,
48 n,
49 maxDimension,
50 detail,
51 results);
52 }
53
getDescriptor() const54 const SkDescriptor& getDescriptor() const override {
55 return fStrike.getDescriptor();
56 }
57
onAboutToExitScope()58 void onAboutToExitScope() override {
59 fStrikeCache->attachNode(this);
60 }
61
62 SkStrikeCache* const fStrikeCache;
63 Node* fNext{nullptr};
64 Node* fPrev{nullptr};
65 SkStrike fStrike;
66 std::unique_ptr<SkStrikePinner> fPinner;
67 };
68
GlobalStrikeCache()69 SkStrikeCache* SkStrikeCache::GlobalStrikeCache() {
70 static auto* cache = new SkStrikeCache;
71 return cache;
72 }
73
ExclusiveStrikePtr(SkStrikeCache::Node * node)74 SkStrikeCache::ExclusiveStrikePtr::ExclusiveStrikePtr(SkStrikeCache::Node* node)
75 : fNode{node} {}
76
ExclusiveStrikePtr()77 SkStrikeCache::ExclusiveStrikePtr::ExclusiveStrikePtr()
78 : fNode{nullptr} {}
79
ExclusiveStrikePtr(ExclusiveStrikePtr && o)80 SkStrikeCache::ExclusiveStrikePtr::ExclusiveStrikePtr(ExclusiveStrikePtr&& o)
81 : fNode{o.fNode} {
82 o.fNode = nullptr;
83 }
84
85 SkStrikeCache::ExclusiveStrikePtr&
operator =(ExclusiveStrikePtr && o)86 SkStrikeCache::ExclusiveStrikePtr::operator = (ExclusiveStrikePtr&& o) {
87 if (fNode != nullptr) {
88 fNode->fStrikeCache->attachNode(fNode);
89 }
90 fNode = o.fNode;
91 o.fNode = nullptr;
92 return *this;
93 }
94
~ExclusiveStrikePtr()95 SkStrikeCache::ExclusiveStrikePtr::~ExclusiveStrikePtr() {
96 if (fNode != nullptr) {
97 fNode->fStrikeCache->attachNode(fNode);
98 }
99 }
100
get() const101 SkStrike* SkStrikeCache::ExclusiveStrikePtr::get() const {
102 return &fNode->fStrike;
103 }
104
operator ->() const105 SkStrike* SkStrikeCache::ExclusiveStrikePtr::operator -> () const {
106 return this->get();
107 }
108
operator *() const109 SkStrike& SkStrikeCache::ExclusiveStrikePtr::operator * () const {
110 return *this->get();
111 }
112
operator bool() const113 SkStrikeCache::ExclusiveStrikePtr::operator bool () const {
114 return fNode != nullptr;
115 }
116
operator ==(const SkStrikeCache::ExclusiveStrikePtr & lhs,const SkStrikeCache::ExclusiveStrikePtr & rhs)117 bool operator == (const SkStrikeCache::ExclusiveStrikePtr& lhs,
118 const SkStrikeCache::ExclusiveStrikePtr& rhs) {
119 return lhs.fNode == rhs.fNode;
120 }
121
operator ==(const SkStrikeCache::ExclusiveStrikePtr & lhs,decltype(nullptr) )122 bool operator == (const SkStrikeCache::ExclusiveStrikePtr& lhs, decltype(nullptr)) {
123 return lhs.fNode == nullptr;
124 }
125
operator ==(decltype(nullptr) ,const SkStrikeCache::ExclusiveStrikePtr & rhs)126 bool operator == (decltype(nullptr), const SkStrikeCache::ExclusiveStrikePtr& rhs) {
127 return nullptr == rhs.fNode;
128 }
129
~SkStrikeCache()130 SkStrikeCache::~SkStrikeCache() {
131 Node* node = fHead;
132 while (node) {
133 Node* next = node->fNext;
134 delete node;
135 node = next;
136 }
137 }
138
CreateScalerContext(const SkDescriptor & desc,const SkScalerContextEffects & effects,const SkTypeface & typeface)139 std::unique_ptr<SkScalerContext> SkStrikeCache::CreateScalerContext(
140 const SkDescriptor& desc,
141 const SkScalerContextEffects& effects,
142 const SkTypeface& typeface) {
143 auto scaler = typeface.createScalerContext(effects, &desc, true /* can fail */);
144
145 // Check if we can create a scaler-context before creating the glyphcache.
146 // If not, we may have exhausted OS/font resources, so try purging the
147 // cache once and try again
148 // pass true the first time, to notice if the scalercontext failed,
149 if (scaler == nullptr) {
150 PurgeAll();
151 scaler = typeface.createScalerContext(effects, &desc, false /* must succeed */);
152 }
153 return scaler;
154 }
155
findOrCreateStrikeExclusive(const SkDescriptor & desc,const SkScalerContextEffects & effects,const SkTypeface & typeface)156 SkExclusiveStrikePtr SkStrikeCache::findOrCreateStrikeExclusive(
157 const SkDescriptor& desc, const SkScalerContextEffects& effects, const SkTypeface& typeface)
158 {
159 return SkExclusiveStrikePtr(this->findOrCreateStrike(desc, effects, typeface));
160 }
161
findOrCreateStrike(const SkDescriptor & desc,const SkScalerContextEffects & effects,const SkTypeface & typeface)162 auto SkStrikeCache::findOrCreateStrike(const SkDescriptor& desc,
163 const SkScalerContextEffects& effects,
164 const SkTypeface& typeface) -> Node* {
165 Node* node = this->findAndDetachStrike(desc);
166 if (node == nullptr) {
167 auto scaler = CreateScalerContext(desc, effects, typeface);
168 node = this->createStrike(desc, std::move(scaler));
169 }
170 return node;
171 }
172
findOrCreateScopedStrike(const SkDescriptor & desc,const SkScalerContextEffects & effects,const SkTypeface & typeface)173 SkScopedStrike SkStrikeCache::findOrCreateScopedStrike(const SkDescriptor& desc,
174 const SkScalerContextEffects& effects,
175 const SkTypeface& typeface) {
176 return SkScopedStrike{this->findOrCreateStrike(desc, effects, typeface)};
177 }
178
PurgeAll()179 void SkStrikeCache::PurgeAll() {
180 GlobalStrikeCache()->purgeAll();
181 }
182
Dump()183 void SkStrikeCache::Dump() {
184 SkDebugf("GlyphCache [ used budget ]\n");
185 SkDebugf(" bytes [ %8zu %8zu ]\n",
186 SkGraphics::GetFontCacheUsed(), SkGraphics::GetFontCacheLimit());
187 SkDebugf(" count [ %8zu %8zu ]\n",
188 SkGraphics::GetFontCacheCountUsed(), SkGraphics::GetFontCacheCountLimit());
189
190 int counter = 0;
191
192 auto visitor = [&counter](const SkStrike& cache) {
193 const SkScalerContextRec& rec = cache.getScalerContext()->getRec();
194
195 SkDebugf("index %d\n", counter);
196 SkDebugf("%s", rec.dump().c_str());
197 counter += 1;
198 };
199
200 GlobalStrikeCache()->forEachStrike(visitor);
201 }
202
203 namespace {
204 const char gGlyphCacheDumpName[] = "skia/sk_glyph_cache";
205 } // namespace
206
DumpMemoryStatistics(SkTraceMemoryDump * dump)207 void SkStrikeCache::DumpMemoryStatistics(SkTraceMemoryDump* dump) {
208 dump->dumpNumericValue(gGlyphCacheDumpName, "size", "bytes", SkGraphics::GetFontCacheUsed());
209 dump->dumpNumericValue(gGlyphCacheDumpName, "budget_size", "bytes",
210 SkGraphics::GetFontCacheLimit());
211 dump->dumpNumericValue(gGlyphCacheDumpName, "glyph_count", "objects",
212 SkGraphics::GetFontCacheCountUsed());
213 dump->dumpNumericValue(gGlyphCacheDumpName, "budget_glyph_count", "objects",
214 SkGraphics::GetFontCacheCountLimit());
215
216 if (dump->getRequestedDetails() == SkTraceMemoryDump::kLight_LevelOfDetail) {
217 dump->setMemoryBacking(gGlyphCacheDumpName, "malloc", nullptr);
218 return;
219 }
220
221 auto visitor = [&dump](const SkStrike& cache) {
222 const SkTypeface* face = cache.getScalerContext()->getTypeface();
223 const SkScalerContextRec& rec = cache.getScalerContext()->getRec();
224
225 SkString fontName;
226 face->getFamilyName(&fontName);
227 // Replace all special characters with '_'.
228 for (size_t index = 0; index < fontName.size(); ++index) {
229 if (!std::isalnum(fontName[index])) {
230 fontName[index] = '_';
231 }
232 }
233
234 SkString dumpName = SkStringPrintf(
235 "%s/%s_%d/%p", gGlyphCacheDumpName, fontName.c_str(), rec.fFontID, &cache);
236
237 dump->dumpNumericValue(dumpName.c_str(),
238 "size", "bytes", cache.getMemoryUsed());
239 dump->dumpNumericValue(dumpName.c_str(),
240 "glyph_count", "objects", cache.countCachedGlyphs());
241 dump->setMemoryBacking(dumpName.c_str(), "malloc", nullptr);
242 };
243
244 GlobalStrikeCache()->forEachStrike(visitor);
245 }
246
247
attachNode(Node * node)248 void SkStrikeCache::attachNode(Node* node) {
249 if (node == nullptr) {
250 return;
251 }
252 SkAutoSpinlock ac(fLock);
253
254 this->validate();
255 node->fStrike.validate();
256
257 this->internalAttachToHead(node);
258 this->internalPurge();
259 }
260
findStrikeExclusive(const SkDescriptor & desc)261 SkExclusiveStrikePtr SkStrikeCache::findStrikeExclusive(const SkDescriptor& desc) {
262 return SkExclusiveStrikePtr(this->findAndDetachStrike(desc));
263 }
264
findAndDetachStrike(const SkDescriptor & desc)265 auto SkStrikeCache::findAndDetachStrike(const SkDescriptor& desc) -> Node* {
266 SkAutoSpinlock ac(fLock);
267
268 for (Node* node = internalGetHead(); node != nullptr; node = node->fNext) {
269 if (node->fStrike.getDescriptor() == desc) {
270 this->internalDetachCache(node);
271 return node;
272 }
273 }
274
275 return nullptr;
276 }
277
278
loose_compare(const SkDescriptor & lhs,const SkDescriptor & rhs)279 static bool loose_compare(const SkDescriptor& lhs, const SkDescriptor& rhs) {
280 uint32_t size;
281 auto ptr = lhs.findEntry(kRec_SkDescriptorTag, &size);
282 SkScalerContextRec lhsRec;
283 std::memcpy(&lhsRec, ptr, size);
284
285 ptr = rhs.findEntry(kRec_SkDescriptorTag, &size);
286 SkScalerContextRec rhsRec;
287 std::memcpy(&rhsRec, ptr, size);
288
289 // If these don't match, there's no way we can use these strikes interchangeably.
290 // Note that a typeface from each renderer maps to a unique proxy typeface on the GPU,
291 // keyed in the glyph cache using fontID in the SkDescriptor. By limiting this search
292 // to descriptors with the same fontID, we ensure that a renderer never uses glyphs
293 // generated by a different renderer.
294 return
295 lhsRec.fFontID == rhsRec.fFontID &&
296 lhsRec.fTextSize == rhsRec.fTextSize &&
297 lhsRec.fPreScaleX == rhsRec.fPreScaleX &&
298 lhsRec.fPreSkewX == rhsRec.fPreSkewX &&
299 lhsRec.fPost2x2[0][0] == rhsRec.fPost2x2[0][0] &&
300 lhsRec.fPost2x2[0][1] == rhsRec.fPost2x2[0][1] &&
301 lhsRec.fPost2x2[1][0] == rhsRec.fPost2x2[1][0] &&
302 lhsRec.fPost2x2[1][1] == rhsRec.fPost2x2[1][1];
303 }
304
desperationSearchForImage(const SkDescriptor & desc,SkGlyph * glyph,SkStrike * targetCache)305 bool SkStrikeCache::desperationSearchForImage(const SkDescriptor& desc, SkGlyph* glyph,
306 SkStrike* targetCache) {
307 SkAutoSpinlock ac(fLock);
308
309 SkGlyphID glyphID = glyph->getGlyphID();
310 for (Node* node = internalGetHead(); node != nullptr; node = node->fNext) {
311 if (loose_compare(node->fStrike.getDescriptor(), desc)) {
312 if (SkGlyph *fallback = node->fStrike.glyphOrNull(glyph->getPackedID())) {
313 // This desperate-match node may disappear as soon as we drop fLock, so we
314 // need to copy the glyph from node into this strike, including a
315 // deep copy of the mask.
316 targetCache->mergeGlyphAndImage(glyph->getPackedID(), *fallback);
317 return true;
318 }
319
320 // Look for any sub-pixel pos for this glyph, in case there is a pos mismatch.
321 if (const auto* fallback = node->fStrike.getCachedGlyphAnySubPix(glyphID)) {
322 targetCache->mergeGlyphAndImage(glyph->getPackedID(), *fallback);
323 return true;
324 }
325 }
326 }
327
328 return false;
329 }
330
desperationSearchForPath(const SkDescriptor & desc,SkGlyphID glyphID,SkPath * path)331 bool SkStrikeCache::desperationSearchForPath(
332 const SkDescriptor& desc, SkGlyphID glyphID, SkPath* path) {
333 SkAutoSpinlock ac(fLock);
334
335 // The following is wrong there is subpixel positioning with paths...
336 // Paths are only ever at sub-pixel position (0,0), so we can just try that directly rather
337 // than try our packed position first then search all others on failure like for masks.
338 //
339 // This will have to search the sub-pixel positions too.
340 // There is also a problem with accounting for cache size with shared path data.
341 for (Node* node = internalGetHead(); node != nullptr; node = node->fNext) {
342 if (loose_compare(node->fStrike.getDescriptor(), desc)) {
343 if (SkGlyph *from = node->fStrike.glyphOrNull(SkPackedGlyphID{glyphID})) {
344 if (from->setPathHasBeenCalled() && from->path() != nullptr) {
345 // We can just copy the path out by value here, so no need to worry
346 // about the lifetime of this desperate-match node.
347 *path = *from->path();
348 return true;
349 }
350 }
351 }
352 }
353 return false;
354 }
355
createStrikeExclusive(const SkDescriptor & desc,std::unique_ptr<SkScalerContext> scaler,SkFontMetrics * maybeMetrics,std::unique_ptr<SkStrikePinner> pinner)356 SkExclusiveStrikePtr SkStrikeCache::createStrikeExclusive(
357 const SkDescriptor& desc,
358 std::unique_ptr<SkScalerContext> scaler,
359 SkFontMetrics* maybeMetrics,
360 std::unique_ptr<SkStrikePinner> pinner)
361 {
362 return SkExclusiveStrikePtr(
363 this->createStrike(desc, std::move(scaler), maybeMetrics, std::move(pinner)));
364 }
365
createStrike(const SkDescriptor & desc,std::unique_ptr<SkScalerContext> scaler,SkFontMetrics * maybeMetrics,std::unique_ptr<SkStrikePinner> pinner)366 auto SkStrikeCache::createStrike(
367 const SkDescriptor& desc,
368 std::unique_ptr<SkScalerContext> scaler,
369 SkFontMetrics* maybeMetrics,
370 std::unique_ptr<SkStrikePinner> pinner) -> Node* {
371 SkFontMetrics fontMetrics;
372 if (maybeMetrics != nullptr) {
373 fontMetrics = *maybeMetrics;
374 } else {
375 scaler->getFontMetrics(&fontMetrics);
376 }
377
378 return new Node{this, desc, std::move(scaler), fontMetrics, std::move(pinner)};
379 }
380
purgeAll()381 void SkStrikeCache::purgeAll() {
382 SkAutoSpinlock ac(fLock);
383 this->internalPurge(fTotalMemoryUsed);
384 }
385
getTotalMemoryUsed() const386 size_t SkStrikeCache::getTotalMemoryUsed() const {
387 SkAutoSpinlock ac(fLock);
388 return fTotalMemoryUsed;
389 }
390
getCacheCountUsed() const391 int SkStrikeCache::getCacheCountUsed() const {
392 SkAutoSpinlock ac(fLock);
393 return fCacheCount;
394 }
395
getCacheCountLimit() const396 int SkStrikeCache::getCacheCountLimit() const {
397 SkAutoSpinlock ac(fLock);
398 return fCacheCountLimit;
399 }
400
setCacheSizeLimit(size_t newLimit)401 size_t SkStrikeCache::setCacheSizeLimit(size_t newLimit) {
402 static const size_t minLimit = 256 * 1024;
403 if (newLimit < minLimit) {
404 newLimit = minLimit;
405 }
406
407 SkAutoSpinlock ac(fLock);
408
409 size_t prevLimit = fCacheSizeLimit;
410 fCacheSizeLimit = newLimit;
411 this->internalPurge();
412 return prevLimit;
413 }
414
getCacheSizeLimit() const415 size_t SkStrikeCache::getCacheSizeLimit() const {
416 SkAutoSpinlock ac(fLock);
417 return fCacheSizeLimit;
418 }
419
setCacheCountLimit(int newCount)420 int SkStrikeCache::setCacheCountLimit(int newCount) {
421 if (newCount < 0) {
422 newCount = 0;
423 }
424
425 SkAutoSpinlock ac(fLock);
426
427 int prevCount = fCacheCountLimit;
428 fCacheCountLimit = newCount;
429 this->internalPurge();
430 return prevCount;
431 }
432
getCachePointSizeLimit() const433 int SkStrikeCache::getCachePointSizeLimit() const {
434 SkAutoSpinlock ac(fLock);
435 return fPointSizeLimit;
436 }
437
setCachePointSizeLimit(int newLimit)438 int SkStrikeCache::setCachePointSizeLimit(int newLimit) {
439 if (newLimit < 0) {
440 newLimit = 0;
441 }
442
443 SkAutoSpinlock ac(fLock);
444
445 int prevLimit = fPointSizeLimit;
446 fPointSizeLimit = newLimit;
447 return prevLimit;
448 }
449
forEachStrike(std::function<void (const SkStrike &)> visitor) const450 void SkStrikeCache::forEachStrike(std::function<void(const SkStrike&)> visitor) const {
451 SkAutoSpinlock ac(fLock);
452
453 this->validate();
454
455 for (Node* node = this->internalGetHead(); node != nullptr; node = node->fNext) {
456 visitor(node->fStrike);
457 }
458 }
459
internalPurge(size_t minBytesNeeded)460 size_t SkStrikeCache::internalPurge(size_t minBytesNeeded) {
461 this->validate();
462
463 size_t bytesNeeded = 0;
464 if (fTotalMemoryUsed > fCacheSizeLimit) {
465 bytesNeeded = fTotalMemoryUsed - fCacheSizeLimit;
466 }
467 bytesNeeded = SkTMax(bytesNeeded, minBytesNeeded);
468 if (bytesNeeded) {
469 // no small purges!
470 bytesNeeded = SkTMax(bytesNeeded, fTotalMemoryUsed >> 2);
471 }
472
473 int countNeeded = 0;
474 if (fCacheCount > fCacheCountLimit) {
475 countNeeded = fCacheCount - fCacheCountLimit;
476 // no small purges!
477 countNeeded = SkMax32(countNeeded, fCacheCount >> 2);
478 }
479
480 // early exit
481 if (!countNeeded && !bytesNeeded) {
482 return 0;
483 }
484
485 size_t bytesFreed = 0;
486 int countFreed = 0;
487
488 // Start at the tail and proceed backwards deleting; the list is in LRU
489 // order, with unimportant entries at the tail.
490 Node* node = this->internalGetTail();
491 while (node != nullptr && (bytesFreed < bytesNeeded || countFreed < countNeeded)) {
492 Node* prev = node->fPrev;
493
494 // Only delete if the strike is not pinned.
495 if (node->fPinner == nullptr || node->fPinner->canDelete()) {
496 bytesFreed += node->fStrike.getMemoryUsed();
497 countFreed += 1;
498 this->internalDetachCache(node);
499 delete node;
500 }
501 node = prev;
502 }
503
504 this->validate();
505
506 #ifdef SPEW_PURGE_STATUS
507 if (countFreed) {
508 SkDebugf("purging %dK from font cache [%d entries]\n",
509 (int)(bytesFreed >> 10), countFreed);
510 }
511 #endif
512
513 return bytesFreed;
514 }
515
internalAttachToHead(Node * node)516 void SkStrikeCache::internalAttachToHead(Node* node) {
517 SkASSERT(nullptr == node->fPrev && nullptr == node->fNext);
518 if (fHead) {
519 fHead->fPrev = node;
520 node->fNext = fHead;
521 }
522 fHead = node;
523
524 if (fTail == nullptr) {
525 fTail = node;
526 }
527
528 fCacheCount += 1;
529 fTotalMemoryUsed += node->fStrike.getMemoryUsed();
530 }
531
internalDetachCache(Node * node)532 void SkStrikeCache::internalDetachCache(Node* node) {
533 SkASSERT(fCacheCount > 0);
534 fCacheCount -= 1;
535 fTotalMemoryUsed -= node->fStrike.getMemoryUsed();
536
537 if (node->fPrev) {
538 node->fPrev->fNext = node->fNext;
539 } else {
540 fHead = node->fNext;
541 }
542 if (node->fNext) {
543 node->fNext->fPrev = node->fPrev;
544 } else {
545 fTail = node->fPrev;
546 }
547 node->fPrev = node->fNext = nullptr;
548 }
549
ValidateGlyphCacheDataSize()550 void SkStrikeCache::ValidateGlyphCacheDataSize() {
551 #ifdef SK_DEBUG
552 GlobalStrikeCache()->validateGlyphCacheDataSize();
553 #endif
554 }
555
556 #ifdef SK_DEBUG
validateGlyphCacheDataSize() const557 void SkStrikeCache::validateGlyphCacheDataSize() const {
558 this->forEachStrike(
559 [](const SkStrike& cache) { cache.forceValidate();
560 });
561 }
562 #endif
563
564 #ifdef SK_DEBUG
validate() const565 void SkStrikeCache::validate() const {
566 size_t computedBytes = 0;
567 int computedCount = 0;
568
569 const Node* node = fHead;
570 while (node != nullptr) {
571 computedBytes += node->fStrike.getMemoryUsed();
572 computedCount += 1;
573 node = node->fNext;
574 }
575
576 SkASSERTF(fCacheCount == computedCount, "fCacheCount: %d, computedCount: %d", fCacheCount,
577 computedCount);
578 SkASSERTF(fTotalMemoryUsed == computedBytes, "fTotalMemoryUsed: %d, computedBytes: %d",
579 fTotalMemoryUsed, computedBytes);
580 }
581 #endif
582
583 ////////////////////////////////////////////////////////////////////////////////////////////////////
584