aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorGravatar Herb Derby <herb@google.com>2018-06-04 15:49:15 -0400
committerGravatar Mike Klein <mtklein@chromium.org>2018-06-06 19:07:52 +0000
commit4ffa027cf6232ec7c314f6d81ff494fc0b5ccd42 (patch)
treea089748a5787adefdc14ca792fd188218cd18c47
parent975f8eeb447a9981977f7db715f95935ad583b70 (diff)
Smarter use of glyph cache
Change-Id: Ic9bea7310b375575503042881d9d54ff13996729 Reviewed-on: https://skia-review.googlesource.com/131924 Reviewed-by: Mike Klein <mtklein@chromium.org>
-rw-r--r--include/core/SkCanvas.h3
-rw-r--r--src/core/SkCanvas.cpp10
-rw-r--r--src/core/SkDevice.cpp5
-rw-r--r--src/core/SkGlyphRun.cpp172
-rw-r--r--src/core/SkGlyphRun.h31
-rw-r--r--src/gpu/text/GrTextContext.cpp10
-rw-r--r--tests/GlyphRunTest.cpp4
7 files changed, 125 insertions, 110 deletions
diff --git a/include/core/SkCanvas.h b/include/core/SkCanvas.h
index 44044b5cd0..2c8a0ebe4f 100644
--- a/include/core/SkCanvas.h
+++ b/include/core/SkCanvas.h
@@ -26,6 +26,7 @@ class SkDraw;
class SkDrawable;
class SkDrawFilter;
struct SkDrawShadowRec;
+class SkGlyphSet;
class SkImage;
class SkImageFilter;
class SkMetaData;
@@ -2709,6 +2710,8 @@ private:
void validateClip() const {}
#endif
+ std::unique_ptr<SkGlyphSet> fScratchGlyphSet;
+
typedef SkRefCnt INHERITED;
};
diff --git a/src/core/SkCanvas.cpp b/src/core/SkCanvas.cpp
index dcff1b0cc3..4a6251f110 100644
--- a/src/core/SkCanvas.cpp
+++ b/src/core/SkCanvas.cpp
@@ -590,6 +590,8 @@ void SkCanvas::init(sk_sp<SkBaseDevice> device, InitFlags flags) {
device->androidFramework_setDeviceClipRestriction(&fClipRestrictionRect);
}
+
+ fScratchGlyphSet = skstd::make_unique<SkGlyphSet>();
}
SkCanvas::SkCanvas()
@@ -2448,7 +2450,7 @@ void SkCanvas::onDrawText(const void* text, size_t byteLength, SkScalar x, SkSca
while (iter.next()) {
auto glyphRun = SkGlyphRun::MakeFromDrawText(
- looper.paint(), text, byteLength, SkPoint::Make(x, y));
+ looper.paint(), text, byteLength, SkPoint::Make(x, y), fScratchGlyphSet.get());
iter.fDevice->drawGlyphRun(looper.paint(), &glyphRun);
}
@@ -2461,7 +2463,8 @@ void SkCanvas::onDrawPosText(const void* text, size_t byteLength, const SkPoint
LOOPER_BEGIN(paint, SkDrawFilter::kText_Type, nullptr)
while (iter.next()) {
- auto glyphRun = SkGlyphRun::MakeFromDrawPosText(looper.paint(), text, byteLength, pos);
+ auto glyphRun = SkGlyphRun::MakeFromDrawPosText(
+ looper.paint(), text, byteLength, pos, fScratchGlyphSet.get());
iter.fDevice->drawGlyphRun(looper.paint(), &glyphRun);
}
@@ -2475,7 +2478,8 @@ void SkCanvas::onDrawPosTextH(const void* text, size_t byteLength, const SkScala
while (iter.next()) {
auto glyphRun =
- SkGlyphRun::MakeFromDrawPosTextH(looper.paint(), text, byteLength, xpos, constY);
+ SkGlyphRun::MakeFromDrawPosTextH(
+ looper.paint(), text, byteLength, xpos, constY, fScratchGlyphSet.get());
iter.fDevice->drawGlyphRun(looper.paint(), &glyphRun);
}
diff --git a/src/core/SkDevice.cpp b/src/core/SkDevice.cpp
index 1bad6dba6e..8321718bab 100644
--- a/src/core/SkDevice.cpp
+++ b/src/core/SkDevice.cpp
@@ -160,10 +160,11 @@ void SkBaseDevice::drawTextBlob(const SkTextBlob* blob, SkScalar x, SkScalar y,
switch (it.positioning()) {
case SkTextBlob::kDefault_Positioning: {
+ SkGlyphSet glyphSet;
auto origin = SkPoint::Make(x + offset.x(), y + offset.y());
auto glyphRun =
- SkGlyphRun::MakeFromDrawText(runPaint,
- (const char*) it.glyphs(), textLen, origin);
+ SkGlyphRun::MakeFromDrawText(
+ runPaint, (const char*) it.glyphs(), textLen, origin, &glyphSet);
this->drawPosText(
it.glyphs(), textLen, glyphRun.getPositions(), 2,
SkPoint::Make(0, 0), runPaint);
diff --git a/src/core/SkGlyphRun.cpp b/src/core/SkGlyphRun.cpp
index a25b9afa8e..41f50134e9 100644
--- a/src/core/SkGlyphRun.cpp
+++ b/src/core/SkGlyphRun.cpp
@@ -21,61 +21,7 @@
namespace {
-// A faster set implementation that does not need any initialization, and reading the set items
-// is order the number of items, and not the size of the universe.
-// This implementation is based on the paper by Briggs and Torczon, "An Efficient Representation
-// for Sparse Sets"
-class GlyphSet {
-public:
- GlyphSet(uint32_t glyphUniverseSize)
- : fUniverseSize{glyphUniverseSize}
- , fIndexes{skstd::make_unique_default<uint16_t[]>(2 * glyphUniverseSize)}
- , fUniqueGlyphIDs{&fIndexes[glyphUniverseSize]} {
- SkASSERT(glyphUniverseSize <= (1 << 16));
- sk_msan_mark_initialized(fIndexes.get(), &fIndexes[glyphUniverseSize], "works with uninited");
- }
-
- uint16_t add(SkGlyphID glyphID) {
- if (glyphID >= fUniverseSize) {
- glyphID = kUndefGlyph;
- }
- auto index = fIndexes[glyphID];
- if (index < fUniqueCount && fUniqueGlyphIDs[index] == glyphID) {
- return index;
- }
-
- fUniqueGlyphIDs[fUniqueCount] = glyphID;
- fIndexes[glyphID] = fUniqueCount;
- fUniqueCount += 1;
- return fUniqueCount - 1;
- }
-
- std::tuple<uint16_t, std::unique_ptr<SkGlyphID[]>> uniqueGlyphIDs() const {
- auto uniqueGlyphs = skstd::make_unique_default<SkGlyphID[]>(fUniqueCount);
- memcpy(uniqueGlyphs.get(), fUniqueGlyphIDs, fUniqueCount * sizeof(SkGlyphID));
- return std::make_tuple(fUniqueCount, std::move(uniqueGlyphs));
- }
-
-private:
- static constexpr SkGlyphID kUndefGlyph{0};
- const uint32_t fUniverseSize;
- uint16_t fUniqueCount{0};
- std::unique_ptr<uint16_t[]> fIndexes;
- SkGlyphID* fUniqueGlyphIDs;
- };
-
-template<typename T>
-bool is_aligned(const void* ptr) {
- uintptr_t bits = reinterpret_cast<uintptr_t>(ptr);
- return (bits & (alignof(T) - 1)) == 0;
-}
-
-template<typename T>
-bool is_aligned_size(size_t size) {
- return size % sizeof(T) == 0;
-}
-
-SkTypeface::Encoding convert_encoding(SkPaint::TextEncoding encoding) {
+static SkTypeface::Encoding convert_encoding(SkPaint::TextEncoding encoding) {
switch (encoding) {
case SkPaint::kUTF8_TextEncoding: return SkTypeface::kUTF8_Encoding;
case SkPaint::kUTF16_TextEncoding: return SkTypeface::kUTF16_Encoding;
@@ -84,32 +30,28 @@ SkTypeface::Encoding convert_encoding(SkPaint::TextEncoding encoding) {
}
}
-using Core = std::tuple<size_t, std::unique_ptr<uint16_t[]>,
- uint16_t, std::unique_ptr<SkGlyphID[]>>;
+using Core = std::tuple<size_t, std::unique_ptr<uint16_t[]>, std::vector<SkGlyphID>>;
-Core make_from_glyphids(size_t glyphCount, const SkGlyphID* glyphs, SkGlyphID maxGlyphID) {
- if (glyphCount == 0) { return Core(0, nullptr, 0, nullptr); }
+Core make_from_glyphids(
+ size_t glyphCount, const SkGlyphID* glyphs, SkGlyphID maxGlyphID, SkGlyphSet* glyphSet) {
+ if (glyphCount == 0) { return Core(0, nullptr, std::vector<SkGlyphID>()); }
- GlyphSet glyphSet{maxGlyphID};
+ glyphSet->reuse(maxGlyphID);
auto denseIndex = skstd::make_unique_default<uint16_t[]>(glyphCount);
for (size_t i = 0; i < glyphCount; i++) {
- denseIndex[i] = glyphSet.add(glyphs[i]);
+ denseIndex[i] = glyphSet->add(glyphs[i]);
}
- std::unique_ptr<SkGlyphID[]> uniqueGlyphIDs;
- uint16_t uniqueCount;
- std::tie(uniqueCount, uniqueGlyphIDs) = glyphSet.uniqueGlyphIDs();
-
- return Core(glyphCount, std::move(denseIndex), uniqueCount, std::move(uniqueGlyphIDs));
+ return Core(glyphCount, std::move(denseIndex), glyphSet->uniqueGlyphIDs());
}
Core make_from_utfn(size_t byteLength, const void* utfN, const SkTypeface& typeface,
- SkTypeface::Encoding encoding) {
+ SkTypeface::Encoding encoding, SkGlyphSet* glyphSet) {
auto count = SkUTFN_CountUnichars(encoding, utfN, byteLength);
if (count <= 0) {
- return Core(0, nullptr, 0, nullptr);
+ return Core(0, nullptr, std::vector<SkGlyphID>());
}
auto glyphs = skstd::make_unique_default<SkGlyphID[]>(count);
@@ -117,17 +59,18 @@ Core make_from_utfn(size_t byteLength, const void* utfN, const SkTypeface& typef
// TODO: move to using cached version.
typeface.charsToGlyphs(utfN, encoding, glyphs.get(), count);
- return make_from_glyphids(count, glyphs.get(), typeface.countGlyphs());
+ return make_from_glyphids(count, glyphs.get(), typeface.countGlyphs(), glyphSet);
}
-Core make_core(const SkPaint& paint, const void* bytes, size_t byteLength) {
+Core make_core(const SkPaint& paint, const void* bytes, size_t byteLength, SkGlyphSet* glyphSet) {
auto encoding = paint.getTextEncoding();
auto typeface = SkPaintPriv::GetTypefaceOrDefault(paint);
if (encoding == SkPaint::kGlyphID_TextEncoding) {
return make_from_glyphids(
- byteLength / 2, reinterpret_cast<const SkGlyphID*>(bytes), typeface->countGlyphs());
+ byteLength / 2, reinterpret_cast<const SkGlyphID*>(bytes),
+ typeface->countGlyphs(), glyphSet);
} else {
- return make_from_utfn(byteLength, bytes, *typeface, convert_encoding(encoding));
+ return make_from_utfn(byteLength, bytes, *typeface, convert_encoding(encoding), glyphSet);
}
}
@@ -135,20 +78,20 @@ Core make_core(const SkPaint& paint, const void* bytes, size_t byteLength) {
SkGlyphRun SkGlyphRun::MakeFromDrawText(
const SkPaint& paint, const void* bytes, size_t byteLength,
- const SkPoint origin) {
+ const SkPoint origin, SkGlyphSet* glyphSet) {
size_t runSize;
std::unique_ptr<uint16_t[]> denseIndex;
- uint16_t uniqueSize;
- std::unique_ptr<SkGlyphID[]> uniqueGlyphIDs;
- std::tie(runSize, denseIndex, uniqueSize, uniqueGlyphIDs) = make_core(paint, bytes, byteLength);
+ std::vector<SkGlyphID> uniqueGlyphIDs;
+ std::tie(runSize, denseIndex, uniqueGlyphIDs) = make_core(paint, bytes, byteLength, glyphSet);
if (runSize == 0) { return SkGlyphRun{}; }
- auto advances = skstd::make_unique_default<SkPoint[]>(uniqueSize);
+ auto advances = skstd::make_unique_default<SkPoint[]>(uniqueGlyphIDs.size());
{
auto cache = SkStrikeCache::FindOrCreateStrikeExclusive(paint);
- cache->getAdvances(SkSpan<SkGlyphID>{uniqueGlyphIDs.get(), uniqueSize}, advances.get());
+ cache->getAdvances(SkSpan<SkGlyphID>{uniqueGlyphIDs.data(),
+ uniqueGlyphIDs.size()}, advances.get());
}
auto positions = skstd::make_unique_default<SkPoint[]>(runSize);
@@ -171,17 +114,16 @@ SkGlyphRun SkGlyphRun::MakeFromDrawText(
}
return SkGlyphRun{
- runSize, std::move(denseIndex), std::move(positions), uniqueSize, std::move(uniqueGlyphIDs)};
+ runSize, std::move(denseIndex), std::move(positions), std::move(uniqueGlyphIDs)};
}
SkGlyphRun SkGlyphRun::MakeFromDrawPosTextH(
const SkPaint& paint, const void* bytes, size_t byteLength,
- const SkScalar xpos[], SkScalar constY) {
+ const SkScalar xpos[], SkScalar constY, SkGlyphSet* glyphSet) {
size_t runSize;
std::unique_ptr<uint16_t[]> denseIndex;
- uint16_t uniqueSize;
- std::unique_ptr<SkGlyphID[]> uniqueGlyphIDs;
- std::tie(runSize, denseIndex, uniqueSize, uniqueGlyphIDs) = make_core(paint, bytes, byteLength);
+ std::vector<SkGlyphID> uniqueGlyphIDs;
+ std::tie(runSize, denseIndex, uniqueGlyphIDs) = make_core(paint, bytes, byteLength, glyphSet);
if (runSize == 0) { return SkGlyphRun{}; }
@@ -192,17 +134,16 @@ SkGlyphRun SkGlyphRun::MakeFromDrawPosTextH(
}
return SkGlyphRun{
- runSize, std::move(denseIndex), std::move(positions), uniqueSize, std::move(uniqueGlyphIDs)};
+ runSize, std::move(denseIndex), std::move(positions), std::move(uniqueGlyphIDs)};
}
SkGlyphRun SkGlyphRun::MakeFromDrawPosText(
const SkPaint& paint, const void* bytes, size_t byteLength,
- const SkPoint pos[]) {
+ const SkPoint pos[], SkGlyphSet* glyphSet) {
size_t runSize;
std::unique_ptr<uint16_t[]> denseIndex;
- uint16_t uniqueSize;
- std::unique_ptr<SkGlyphID[]> uniqueGlyphIDs;
- std::tie(runSize, denseIndex, uniqueSize, uniqueGlyphIDs) = make_core(paint, bytes, byteLength);
+ std::vector<SkGlyphID> uniqueGlyphIDs;
+ std::tie(runSize, denseIndex, uniqueGlyphIDs) = make_core(paint, bytes, byteLength, glyphSet);
if (runSize == 0) { return SkGlyphRun{}; }
@@ -211,7 +152,7 @@ SkGlyphRun SkGlyphRun::MakeFromDrawPosText(
memcpy(positions.get(), pos, sizeof(SkPoint) * runSize);
return SkGlyphRun{
- runSize, std::move(denseIndex), std::move(positions), uniqueSize, std::move(uniqueGlyphIDs)};
+ runSize, std::move(denseIndex), std::move(positions), std::move(uniqueGlyphIDs)};
}
std::unique_ptr<SkGlyphID[]> SkGlyphRun::copyGlyphIDs() const {
@@ -227,10 +168,55 @@ std::unique_ptr<SkGlyphID[]> SkGlyphRun::copyGlyphIDs() const {
SkGlyphRun::SkGlyphRun(size_t runSize,
std::unique_ptr<uint16_t[]>&& denseIndex,
std::unique_ptr<SkPoint[]>&& positions,
- uint16_t uniqueSize,
- std::unique_ptr<SkGlyphID[]>&& uniqueGlyphIDs)
+ std::vector<SkGlyphID>&& uniqueGlyphIDs)
: fDenseIndex{std::move(denseIndex)}
, fPositions{std::move(positions)}
, fUniqueGlyphs{std::move(uniqueGlyphIDs)}
- , fRunSize{runSize}
- , fUniqueSize{uniqueSize} { }
+ , fRunSize{runSize} { }
+
+uint16_t SkGlyphSet::add(SkGlyphID glyphID) {
+ static constexpr SkGlyphID kUndefGlyph{0};
+
+ if (glyphID >= fUniverseSize) {
+ glyphID = kUndefGlyph;
+ }
+
+ if (glyphID >= fIndices.size()) {
+ fIndices.resize(glyphID + 1);
+ }
+
+ auto index = fIndices[glyphID];
+ if (index < fUniqueGlyphIDs.size() && fUniqueGlyphIDs[index] == glyphID) {
+ return index;
+ }
+
+ uint16_t newIndex = SkTo<uint16_t>(fUniqueGlyphIDs.size());
+ fUniqueGlyphIDs.push_back(glyphID);
+ fIndices[glyphID] = newIndex;
+ return newIndex;
+}
+
+std::vector<SkGlyphID> SkGlyphSet::uniqueGlyphIDs() {
+ return fUniqueGlyphIDs;
+}
+
+void SkGlyphSet::reuse(uint32_t glyphUniverseSize) {
+ SkASSERT(glyphUniverseSize <= (1 << 16));
+ fUniverseSize = glyphUniverseSize;
+ // If we're hanging onto these arrays for a long time, we don't want their size to drift
+ // endlessly upwards. It's unusual to see more than 256 unique glyphs used in a run,
+ // or a typeface with more than 4096 possible glyphs.
+ if (fUniqueGlyphIDs.size() > 256) {
+ fUniqueGlyphIDs.resize(256);
+ fUniqueGlyphIDs.shrink_to_fit();
+ }
+ fUniqueGlyphIDs.clear();
+
+ if (glyphUniverseSize < 4096 && fIndices.size() > 4096) {
+ fIndices.resize(4096);
+ fIndices.shrink_to_fit();
+ }
+
+ // No need to clear fIndices here... SkGlyphSet's set insertion algorithm is designed to work
+ // correctly even when the fIndexes buffer is uninitialized!
+} \ No newline at end of file
diff --git a/src/core/SkGlyphRun.h b/src/core/SkGlyphRun.h
index 17b4297208..53d57af8c4 100644
--- a/src/core/SkGlyphRun.h
+++ b/src/core/SkGlyphRun.h
@@ -17,22 +17,39 @@
#include "SkPoint.h"
#include "SkTypes.h"
+// A faster set implementation that does not need any initialization, and reading the set items
+// is order the number of items, and not the size of the universe.
+// This implementation is based on the paper by Briggs and Torczon, "An Efficient Representation
+// for Sparse Sets"
+class SkGlyphSet {
+public:
+ SkGlyphSet() = default;
+ uint16_t add(SkGlyphID glyphID);
+ std::vector<SkGlyphID> uniqueGlyphIDs();
+ void reuse(uint32_t glyphUniverseSize);
+
+private:
+ uint32_t fUniverseSize{0};
+ std::vector<uint16_t> fIndices;
+ std::vector<SkGlyphID> fUniqueGlyphIDs;
+};
+
class SkGlyphRun {
public:
SkGlyphRun() = default;
SkGlyphRun(SkGlyphRun&&) = default;
static SkGlyphRun MakeFromDrawText(
const SkPaint& paint, const void* bytes, size_t byteLength,
- SkPoint origin);
+ SkPoint origin, SkGlyphSet* glyphSet);
static SkGlyphRun MakeFromDrawPosTextH(
const SkPaint& paint, const void* bytes, size_t byteLength,
- const SkScalar xpos[], SkScalar constY);
+ const SkScalar xpos[], SkScalar constY, SkGlyphSet* glyphSet);
static SkGlyphRun MakeFromDrawPosText(
const SkPaint& paint, const void* bytes, size_t byteLength,
- const SkPoint pos[]);
+ const SkPoint pos[], SkGlyphSet* glyphSet);
size_t runSize() const { return fRunSize; }
- uint16_t uniqueSize() const { return fUniqueSize; }
+ uint16_t uniqueSize() const { return fUniqueGlyphs.size(); }
// copyGlyphIDs is temporary glue to work with the existing system. Don't use with new code.
std::unique_ptr<SkGlyphID[]> copyGlyphIDs() const;
@@ -44,14 +61,12 @@ private:
SkGlyphRun(size_t runSize,
std::unique_ptr<uint16_t[]>&& denseIndex,
std::unique_ptr<SkPoint[]>&& positions,
- uint16_t uniqueSize,
- std::unique_ptr<SkGlyphID[]>&& uniqueGlyphIDs);
+ std::vector<SkGlyphID>&& uniqueGlyphIDs);
std::unique_ptr<uint16_t[]> fDenseIndex;
std::unique_ptr<SkPoint[]> fPositions;
- std::unique_ptr<SkGlyphID[]> fUniqueGlyphs;
+ std::vector<SkGlyphID> fUniqueGlyphs;
const size_t fRunSize{0};
- const uint16_t fUniqueSize{0};
};
template <typename T>
diff --git a/src/gpu/text/GrTextContext.cpp b/src/gpu/text/GrTextContext.cpp
index 72926fe280..033506f220 100644
--- a/src/gpu/text/GrTextContext.cpp
+++ b/src/gpu/text/GrTextContext.cpp
@@ -210,10 +210,11 @@ void GrTextContext::regenerateTextBlob(GrTextBlob* cacheBlob,
shaderCaps.supportsDistanceFieldText(), fOptions)) {
switch (it.positioning()) {
case SkTextBlob::kDefault_Positioning: {
+ SkGlyphSet glyphSet;
auto origin = SkPoint::Make(x + offset.x(), y + offset.y());
auto glyphRun =
SkGlyphRun::MakeFromDrawText(runPaint.skPaint(),
- (const char*)it.glyphs(), textLen, origin);
+ (const char*)it.glyphs(), textLen, origin, &glyphSet);
this->drawDFPosText(cacheBlob, run, glyphCache, props, runPaint,
scalerContextFlags, viewMatrix, (const char*)it.glyphs(),
@@ -239,10 +240,12 @@ void GrTextContext::regenerateTextBlob(GrTextBlob* cacheBlob,
} else {
switch (it.positioning()) {
case SkTextBlob::kDefault_Positioning: {
+ SkGlyphSet glyphSet;
auto origin = SkPoint::Make(x + offset.x(), y + offset.y());
auto glyphRun =
SkGlyphRun::MakeFromDrawText(
- runPaint.skPaint(), (const char*) it.glyphs(), textLen, origin);
+ runPaint.skPaint(), (const char*) it.glyphs(), textLen, origin,
+ &glyphSet);
this->DrawBmpPosText(cacheBlob, run, glyphCache, props, runPaint,
scalerContextFlags, viewMatrix, (const char*) it.glyphs(),
@@ -758,8 +761,9 @@ std::unique_ptr<GrDrawOp> GrTextContext::createOp_TestingOnly(GrContext* context
// right now we don't handle textblobs, nor do we handle drawPosText. Since we only intend to
// test the text op with this unit test, that is okay.
+ SkGlyphSet glyphSet;
auto origin = SkPoint::Make(x, y);
- auto glyphRun = SkGlyphRun::MakeFromDrawText(skPaint, text, textLen, origin);
+ auto glyphRun = SkGlyphRun::MakeFromDrawText(skPaint, text, textLen, origin, &glyphSet);
sk_sp<GrTextBlob> blob(textContext->makeDrawPosTextBlob(
context->contextPriv().getTextBlobCache(), glyphCache,
diff --git a/tests/GlyphRunTest.cpp b/tests/GlyphRunTest.cpp
index cad01acef7..7a80b7be93 100644
--- a/tests/GlyphRunTest.cpp
+++ b/tests/GlyphRunTest.cpp
@@ -16,5 +16,7 @@ DEF_TEST(GlyphRunInfo, reporter) {
SkPaint paint;
paint.setTextEncoding(SkPaint::kGlyphID_TextEncoding);
- SkGlyphRun::MakeFromDrawText(paint, glyphs, count, SkPoint::Make(0, 0));
+ SkGlyphSet glyphSet;
+
+ SkGlyphRun::MakeFromDrawText(paint, glyphs, count, SkPoint::Make(0, 0), &glyphSet);
} \ No newline at end of file