aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorGravatar robertphillips <robertphillips@google.com>2016-08-10 12:00:09 -0700
committerGravatar Commit bot <commit-bot@chromium.org>2016-08-10 12:00:09 -0700
commitf5a83e818483ef910ffd107df8f98b5ee24671f5 (patch)
tree0cf2910d3ecd52613dcdb3b050e4528b456eba52
parent69aaa5a49a10454d573cbd8c5d980029d78ae459 (diff)
Create blurred RRect mask on GPU (rather than uploading it)
This CL doesn't try to resolve any of the larger issues. It just moves the computation of the blurred RRect to the gpu and sets up to start using vertex attributes for a nine patch draw (i.e., returning the texture coordinates) All blurred rrects using the "analytic" path will change slightly with this CL. GOLD_TRYBOT_URL= https://gold.skia.org/search?issue=2222083004 Committed: https://skia.googlesource.com/skia/+/75ccdc77a70ec2083141bf9ba98eb2f01ece2479 Committed: https://skia.googlesource.com/skia/+/94b5c5a41160e0f55e267fc3d830df65736fac50 Review-Url: https://codereview.chromium.org/2222083004
-rw-r--r--include/core/SkMaskFilter.h3
-rw-r--r--include/effects/SkBlurMaskFilter.h10
-rw-r--r--src/core/SkMaskFilter.cpp2
-rw-r--r--src/effects/SkBlurMaskFilter.cpp173
-rw-r--r--src/effects/SkGpuBlurUtils.cpp7
-rw-r--r--src/effects/SkGpuBlurUtils.h4
-rw-r--r--src/gpu/SkGpuDevice.cpp2
-rw-r--r--src/gpu/SkGpuDevice_drawTexture.cpp2
-rw-r--r--tests/BlurTest.cpp62
9 files changed, 203 insertions, 62 deletions
diff --git a/include/core/SkMaskFilter.h b/include/core/SkMaskFilter.h
index 908226c8ad..e30fc62548 100644
--- a/include/core/SkMaskFilter.h
+++ b/include/core/SkMaskFilter.h
@@ -17,6 +17,7 @@
#include "SkStrokeRec.h"
class GrClip;
+class GrContext;
class GrDrawContext;
class GrPaint;
class GrRenderTarget;
@@ -122,7 +123,7 @@ public:
* Try to directly render a rounded rect mask filter into the target. Returns
* true if drawing was successful.
*/
- virtual bool directFilterRRectMaskGPU(GrTextureProvider* texProvider,
+ virtual bool directFilterRRectMaskGPU(GrContext*,
GrDrawContext* drawContext,
GrPaint* grp,
const GrClip&,
diff --git a/include/effects/SkBlurMaskFilter.h b/include/effects/SkBlurMaskFilter.h
index 3ba91774f0..4b037e70cd 100644
--- a/include/effects/SkBlurMaskFilter.h
+++ b/include/effects/SkBlurMaskFilter.h
@@ -65,6 +65,16 @@ public:
SkScalar blurRadius);
#endif
+ static bool ComputeBlurredRRectParams(const SkRRect& rrect,
+ SkScalar sigma,
+ SkRRect* rrectToDraw,
+ SkISize* widthHeight,
+ SkScalar xs[4],
+ int* numXs,
+ SkScalar ys[4],
+ int* numYs);
+
+
SK_DECLARE_FLATTENABLE_REGISTRAR_GROUP()
private:
diff --git a/src/core/SkMaskFilter.cpp b/src/core/SkMaskFilter.cpp
index fccce45a77..352cf71db9 100644
--- a/src/core/SkMaskFilter.cpp
+++ b/src/core/SkMaskFilter.cpp
@@ -324,7 +324,7 @@ bool SkMaskFilter::canFilterMaskGPU(const SkRRect& devRRect,
}
-bool SkMaskFilter::directFilterRRectMaskGPU(GrTextureProvider* texProvider,
+bool SkMaskFilter::directFilterRRectMaskGPU(GrContext*,
GrDrawContext* drawContext,
GrPaint* grp,
const GrClip&,
diff --git a/src/effects/SkBlurMaskFilter.cpp b/src/effects/SkBlurMaskFilter.cpp
index 4215733492..f66cb9c8db 100644
--- a/src/effects/SkBlurMaskFilter.cpp
+++ b/src/effects/SkBlurMaskFilter.cpp
@@ -22,7 +22,7 @@
#include "GrTexture.h"
#include "GrFragmentProcessor.h"
#include "GrInvariantOutput.h"
-#include "SkDraw.h"
+#include "GrStyle.h"
#include "effects/GrSimpleTextureEffect.h"
#include "glsl/GrGLSLFragmentProcessor.h"
#include "glsl/GrGLSLFragmentShaderBuilder.h"
@@ -56,7 +56,7 @@ public:
const SkMatrix& viewMatrix,
const SkStrokeRec& strokeRec,
const SkPath& path) const override;
- bool directFilterRRectMaskGPU(GrTextureProvider* texProvider,
+ bool directFilterRRectMaskGPU(GrContext*,
GrDrawContext* drawContext,
GrPaint* grp,
const GrClip&,
@@ -136,6 +136,64 @@ sk_sp<SkMaskFilter> SkBlurMaskFilter::Make(SkBlurStyle style, SkScalar sigma, ui
return sk_sp<SkMaskFilter>(new SkBlurMaskFilterImpl(sigma, style, flags));
}
+bool SkBlurMaskFilter::ComputeBlurredRRectParams(const SkRRect& rrect,
+ SkScalar sigma,
+ SkRRect* rrectToDraw,
+ SkISize* widthHeight,
+ SkScalar xs[4],
+ int* numXs,
+ SkScalar ys[4],
+ int* numYs) {
+ unsigned int blurRadius = 3*SkScalarCeilToInt(sigma-1/6.0f);
+
+ const SkRect& orig = rrect.getBounds();
+ const SkVector& radiiUL = rrect.radii(SkRRect::kUpperLeft_Corner);
+ const SkVector& radiiUR = rrect.radii(SkRRect::kUpperRight_Corner);
+ const SkVector& radiiLR = rrect.radii(SkRRect::kLowerRight_Corner);
+ const SkVector& radiiLL = rrect.radii(SkRRect::kLowerLeft_Corner);
+
+ const int left = SkScalarCeilToInt(SkTMax<SkScalar>(radiiUL.fX, radiiLL.fX));
+ const int top = SkScalarCeilToInt(SkTMax<SkScalar>(radiiUL.fY, radiiUR.fY));
+ const int right = SkScalarCeilToInt(SkTMax<SkScalar>(radiiUR.fX, radiiLR.fX));
+ const int bot = SkScalarCeilToInt(SkTMax<SkScalar>(radiiLL.fY, radiiLR.fY));
+
+ // This is a conservative check for nine-patchability
+ if (orig.fLeft + left + blurRadius >= orig.fRight - right - blurRadius ||
+ orig.fTop + top + blurRadius >= orig.fBottom - bot - blurRadius) {
+ return false;
+ }
+
+ int newRRWidth, newRRHeight;
+
+ // 3x3 case
+ newRRWidth = 2*blurRadius + left + right + 1;
+ newRRHeight = 2*blurRadius + top + bot + 1;
+ widthHeight->fWidth = newRRWidth + 2 * blurRadius;
+ widthHeight->fHeight = newRRHeight + 2 * blurRadius;
+ // TODO: need to return non-normalized indices
+ xs[0] = 0.0f;
+ xs[1] = (blurRadius + left) / (float) widthHeight->fWidth;
+ xs[2] = (blurRadius + left + 1.0f) / widthHeight->fWidth;
+ xs[3] = 1.0f;
+ *numXs = 4;
+ ys[0] = 0.0f;
+ ys[1] = (blurRadius + top) / (float) widthHeight->fHeight;
+ ys[2] = (blurRadius + top + 1.0f) / widthHeight->fHeight;
+ ys[3] = 1.0f;
+ *numYs = 4;
+
+ const SkRect newRect = SkRect::MakeXYWH(SkIntToScalar(blurRadius), SkIntToScalar(blurRadius),
+ SkIntToScalar(newRRWidth), SkIntToScalar(newRRHeight));
+ SkVector newRadii[4];
+ newRadii[0] = { SkScalarCeilToScalar(radiiUL.fX), SkScalarCeilToScalar(radiiUL.fY) };
+ newRadii[1] = { SkScalarCeilToScalar(radiiUR.fX), SkScalarCeilToScalar(radiiUR.fY) };
+ newRadii[2] = { SkScalarCeilToScalar(radiiLR.fX), SkScalarCeilToScalar(radiiLR.fY) };
+ newRadii[3] = { SkScalarCeilToScalar(radiiLL.fX), SkScalarCeilToScalar(radiiLL.fY) };
+
+ rrectToDraw->setRectRadii(newRect, newRadii);
+ return true;
+}
+
///////////////////////////////////////////////////////////////////////////////
SkBlurMaskFilterImpl::SkBlurMaskFilterImpl(SkScalar sigma, SkBlurStyle style, uint32_t flags)
@@ -599,6 +657,7 @@ void SkBlurMaskFilterImpl::flatten(SkWriteBuffer& buffer) const {
buffer.writeUInt(fBlurFlags);
}
+
#if SK_SUPPORT_GPU
class GrGLRectBlurEffect;
@@ -910,7 +969,7 @@ bool SkBlurMaskFilterImpl::directFilterMaskGPU(GrTextureProvider* texProvider,
class GrRRectBlurEffect : public GrFragmentProcessor {
public:
- static sk_sp<GrFragmentProcessor> Make(GrTextureProvider*, float sigma, const SkRRect&);
+ static sk_sp<GrFragmentProcessor> Make(GrContext*, float sigma, const SkRRect&);
virtual ~GrRRectBlurEffect() {};
const char* name() const override { return "GrRRectBlur"; }
@@ -939,13 +998,55 @@ private:
typedef GrFragmentProcessor INHERITED;
};
+static sk_sp<GrTexture> make_rrect_blur_mask(GrContext* context,
+ const SkRRect& rrect,
+ float sigma,
+ bool doAA) {
+ SkRRect rrectToDraw;
+ SkISize size;
+ SkScalar xs[4], ys[4];
+ int numXs, numYs;
+
+ SkBlurMaskFilter::ComputeBlurredRRectParams(rrect, sigma, &rrectToDraw, &size,
+ xs, &numXs, ys, &numYs);
+
+ // TODO: this could be approx but the texture coords will need to be updated
+ sk_sp<GrDrawContext> dc(context->makeDrawContext(SkBackingFit::kExact,
+ size.fWidth, size.fHeight,
+ kAlpha_8_GrPixelConfig, nullptr));
+ if (!dc) {
+ return nullptr;
+ }
+
+ GrPaint grPaint;
+ grPaint.setAntiAlias(doAA);
+
+ dc->clear(nullptr, 0x0, true);
+ dc->drawRRect(GrNoClip(), grPaint, SkMatrix::I(), rrectToDraw, GrStyle::SimpleFill());
+
+ sk_sp<GrTexture> tex(dc->asTexture());
+ sk_sp<GrDrawContext> dc2(SkGpuBlurUtils::GaussianBlur(context,
+ tex.get(),
+ nullptr,
+ SkIRect::MakeWH(size.fWidth,
+ size.fHeight),
+ nullptr,
+ sigma, sigma, SkBackingFit::kExact));
+ if (!dc2) {
+ return nullptr;
+ }
-sk_sp<GrFragmentProcessor> GrRRectBlurEffect::Make(GrTextureProvider* texProvider, float sigma,
- const SkRRect& rrect) {
+ return dc2->asTexture();
+}
+
+sk_sp<GrFragmentProcessor> GrRRectBlurEffect::Make(GrContext* context, float sigma,
+ const SkRRect& rrect) {
if (rrect.isCircle()) {
- return GrCircleBlurFragmentProcessor::Make(texProvider, rrect.rect(), sigma);
+ return GrCircleBlurFragmentProcessor::Make(context->textureProvider(),
+ rrect.rect(), sigma);
}
+ // TODO: loosen this up
if (!rrect.isSimpleCircular()) {
return nullptr;
}
@@ -968,55 +1069,17 @@ sk_sp<GrFragmentProcessor> GrRRectBlurEffect::Make(GrTextureProvider* texProvide
builder[1] = cornerRadius;
builder.finish();
- SkAutoTUnref<GrTexture> blurNinePatchTexture(texProvider->findAndRefTextureByUniqueKey(key));
+ sk_sp<GrTexture> blurNinePatchTexture(
+ context->textureProvider()->findAndRefTextureByUniqueKey(key));
if (!blurNinePatchTexture) {
- SkMask mask;
-
- unsigned int smallRectSide = 2*(blurRadius + cornerRadius) + 1;
-
- mask.fBounds = SkIRect::MakeWH(smallRectSide, smallRectSide);
- mask.fFormat = SkMask::kA8_Format;
- mask.fRowBytes = mask.fBounds.width();
- mask.fImage = SkMask::AllocImage(mask.computeTotalImageSize());
- SkAutoMaskFreeImage amfi(mask.fImage);
-
- memset(mask.fImage, 0, mask.computeTotalImageSize());
-
- SkRect smallRect;
- smallRect.setWH(SkIntToScalar(smallRectSide), SkIntToScalar(smallRectSide));
-
- SkRRect smallRRect;
- smallRRect.setRectXY(smallRect, SkIntToScalar(cornerRadius), SkIntToScalar(cornerRadius));
-
- SkPath path;
- path.addRRect(smallRRect);
-
- SkDraw::DrawToMask(path, &mask.fBounds, nullptr, nullptr, &mask,
- SkMask::kJustRenderImage_CreateMode, SkStrokeRec::kFill_InitStyle);
-
- SkMask blurredMask;
- if (!SkBlurMask::BoxBlur(&blurredMask, mask, sigma, kNormal_SkBlurStyle,
- kHigh_SkBlurQuality, nullptr, true)) {
- return nullptr;
- }
-
- unsigned int texSide = smallRectSide + 2*blurRadius;
- GrSurfaceDesc texDesc;
- texDesc.fWidth = texSide;
- texDesc.fHeight = texSide;
- texDesc.fConfig = kAlpha_8_GrPixelConfig;
- texDesc.fIsMipMapped = false;
-
- blurNinePatchTexture.reset(
- texProvider->createTexture(texDesc, SkBudgeted::kYes , blurredMask.fImage, 0));
- SkMask::FreeImage(blurredMask.fImage);
+ blurNinePatchTexture = make_rrect_blur_mask(context, rrect, sigma, true);
if (!blurNinePatchTexture) {
return nullptr;
}
- texProvider->assignUniqueKeyToTexture(key, blurNinePatchTexture);
+ context->textureProvider()->assignUniqueKeyToTexture(key, blurNinePatchTexture.get());
}
- return sk_sp<GrFragmentProcessor>(new GrRRectBlurEffect(sigma, rrect, blurNinePatchTexture));
+ return sk_sp<GrFragmentProcessor>(new GrRRectBlurEffect(sigma, rrect, blurNinePatchTexture.get()));
}
void GrRRectBlurEffect::onComputeInvariantOutput(GrInvariantOutput* inout) const {
@@ -1050,7 +1113,7 @@ sk_sp<GrFragmentProcessor> GrRRectBlurEffect::TestCreate(GrProcessorTestData* d)
SkScalar sigma = d->fRandom->nextRangeF(1.f,10.f);
SkRRect rrect;
rrect.setRectXY(SkRect::MakeWH(w, h), r, r);
- return GrRRectBlurEffect::Make(d->fContext->textureProvider(), sigma, rrect);
+ return GrRRectBlurEffect::Make(d->fContext, sigma, rrect);
}
//////////////////////////////////////////////////////////////////////////////
@@ -1153,7 +1216,7 @@ GrGLSLFragmentProcessor* GrRRectBlurEffect::onCreateGLSLInstance() const {
return new GrGLRRectBlurEffect;
}
-bool SkBlurMaskFilterImpl::directFilterRRectMaskGPU(GrTextureProvider* texProvider,
+bool SkBlurMaskFilterImpl::directFilterRRectMaskGPU(GrContext* context,
GrDrawContext* drawContext,
GrPaint* grp,
const GrClip& clip,
@@ -1172,12 +1235,14 @@ bool SkBlurMaskFilterImpl::directFilterRRectMaskGPU(GrTextureProvider* texProvid
SkScalar xformedSigma = this->computeXformedSigma(viewMatrix);
- sk_sp<GrFragmentProcessor> fp(GrRRectBlurEffect::Make(texProvider, xformedSigma, rrect));
+ sk_sp<GrFragmentProcessor> fp(GrRRectBlurEffect::Make(context, xformedSigma, rrect));
if (!fp) {
return false;
}
- grp->addCoverageFragmentProcessor(std::move(fp));
+ GrPaint newPaint(*grp);
+ newPaint.addCoverageFragmentProcessor(std::move(fp));
+ newPaint.setAntiAlias(false);
SkMatrix inverse;
if (!viewMatrix.invert(&inverse)) {
@@ -1189,7 +1254,7 @@ bool SkBlurMaskFilterImpl::directFilterRRectMaskGPU(GrTextureProvider* texProvid
SkRect proxyRect = rrect.rect();
proxyRect.outset(extra, extra);
- drawContext->fillRectWithLocalMatrix(clip, *grp, SkMatrix::I(), proxyRect, inverse);
+ drawContext->fillRectWithLocalMatrix(clip, newPaint, SkMatrix::I(), proxyRect, inverse);
return true;
}
diff --git a/src/effects/SkGpuBlurUtils.cpp b/src/effects/SkGpuBlurUtils.cpp
index 869cd76c5b..ebb480d3df 100644
--- a/src/effects/SkGpuBlurUtils.cpp
+++ b/src/effects/SkGpuBlurUtils.cpp
@@ -186,7 +186,8 @@ sk_sp<GrDrawContext> GaussianBlur(GrContext* context,
const SkIRect& dstBounds,
const SkIRect* srcBounds,
float sigmaX,
- float sigmaY) {
+ float sigmaY,
+ SkBackingFit fit) {
SkASSERT(context);
SkIRect clearRect;
int scaleFactorX, radiusX;
@@ -226,7 +227,7 @@ sk_sp<GrDrawContext> GaussianBlur(GrContext* context,
const int height = dstBounds.height();
const GrPixelConfig config = srcTexture->config();
- sk_sp<GrDrawContext> dstDrawContext(context->makeDrawContext(SkBackingFit::kApprox,
+ sk_sp<GrDrawContext> dstDrawContext(context->makeDrawContext(fit,
width, height, config, colorSpace,
0, kDefault_GrSurfaceOrigin));
if (!dstDrawContext) {
@@ -246,7 +247,7 @@ sk_sp<GrDrawContext> GaussianBlur(GrContext* context,
return dstDrawContext;
}
- sk_sp<GrDrawContext> tmpDrawContext(context->makeDrawContext(SkBackingFit::kApprox,
+ sk_sp<GrDrawContext> tmpDrawContext(context->makeDrawContext(fit,
width, height, config, colorSpace,
0, kDefault_GrSurfaceOrigin));
if (!tmpDrawContext) {
diff --git a/src/effects/SkGpuBlurUtils.h b/src/effects/SkGpuBlurUtils.h
index a5de6a242a..a12a08873c 100644
--- a/src/effects/SkGpuBlurUtils.h
+++ b/src/effects/SkGpuBlurUtils.h
@@ -29,6 +29,7 @@ namespace SkGpuBlurUtils {
* no pixels will be sampled outside of this rectangle.
* @param sigmaX The blur's standard deviation in X.
* @param sigmaY The blur's standard deviation in Y.
+ * @param fit backing fit for the returned draw context
* @return The drawContext containing the blurred result.
*/
sk_sp<GrDrawContext> GaussianBlur(GrContext* context,
@@ -37,7 +38,8 @@ namespace SkGpuBlurUtils {
const SkIRect& dstBounds,
const SkIRect* srcBounds,
float sigmaX,
- float sigmaY);
+ float sigmaY,
+ SkBackingFit fit = SkBackingFit::kApprox);
};
#endif
diff --git a/src/gpu/SkGpuDevice.cpp b/src/gpu/SkGpuDevice.cpp
index cd34b1feba..fb513f4483 100644
--- a/src/gpu/SkGpuDevice.cpp
+++ b/src/gpu/SkGpuDevice.cpp
@@ -436,7 +436,7 @@ void SkGpuDevice::drawRRect(const SkDraw& draw, const SkRRect& rrect,
// clipped out
return;
}
- if (paint.getMaskFilter()->directFilterRRectMaskGPU(fContext->textureProvider(),
+ if (paint.getMaskFilter()->directFilterRRectMaskGPU(fContext,
fDrawContext.get(),
&grPaint,
fClip,
diff --git a/src/gpu/SkGpuDevice_drawTexture.cpp b/src/gpu/SkGpuDevice_drawTexture.cpp
index 2843c31996..e141cc2d62 100644
--- a/src/gpu/SkGpuDevice_drawTexture.cpp
+++ b/src/gpu/SkGpuDevice_drawTexture.cpp
@@ -226,7 +226,7 @@ void SkGpuDevice::drawTextureProducerImpl(GrTextureProducer* producer,
// First see if we can do the draw + mask filter direct to the dst.
SkStrokeRec rec(SkStrokeRec::kFill_InitStyle);
- if (mf->directFilterRRectMaskGPU(fContext->textureProvider(),
+ if (mf->directFilterRRectMaskGPU(fContext,
fDrawContext.get(),
&grPaint,
clip,
diff --git a/tests/BlurTest.cpp b/tests/BlurTest.cpp
index 6ccb0471aa..32e2930171 100644
--- a/tests/BlurTest.cpp
+++ b/tests/BlurTest.cpp
@@ -574,4 +574,66 @@ DEF_GPUTEST_FOR_RENDERING_CONTEXTS(SmallBoxBlurBug, reporter, ctxInfo) {
#endif
+
+DEF_TEST(BlurredRRectNinePatchComputation, reporter) {
+ const SkRect r = SkRect::MakeXYWH(10, 10, 100, 100);
+
+ bool ninePatchable;
+ SkRRect rrectToDraw;
+ SkISize size;
+ SkScalar xs[4], ys[4];
+ int numXs, numYs;
+
+ // not nine-patchable
+ {
+ SkVector radii[4] = { { 100, 100 }, { 0, 0 }, { 100, 100 }, { 0, 0 } };
+
+ SkRRect rr;
+ rr.setRectRadii(r, radii);
+
+ ninePatchable = SkBlurMaskFilter::ComputeBlurredRRectParams(rr, 3.0f, &rrectToDraw, &size,
+ xs, &numXs, ys, &numYs);
+ REPORTER_ASSERT(reporter, !ninePatchable);
+ }
+
+ // simple circular
+ {
+ SkRRect rr;
+ rr.setRectXY(r, 10, 10);
+
+ ninePatchable = SkBlurMaskFilter::ComputeBlurredRRectParams(rr, 3.0f, &rrectToDraw, &size,
+ xs, &numXs, ys, &numYs);
+ REPORTER_ASSERT(reporter, ninePatchable);
+ REPORTER_ASSERT(reporter, SkScalarNearlyEqual(SkIntToScalar(size.fWidth), 57.0f));
+ REPORTER_ASSERT(reporter, SkScalarNearlyEqual(SkIntToScalar(size.fHeight), 57.0));
+ REPORTER_ASSERT(reporter, 4 == numXs && 4 == numYs);
+ for (int i = 0; i < numXs; ++i) {
+ REPORTER_ASSERT(reporter, xs[i] >= 0.0f && xs[i] <= 1.0f);
+ }
+ for (int i = 0; i < numYs; ++i) {
+ REPORTER_ASSERT(reporter, ys[i] >= 0.0f && ys[i] <= 1.0f);
+ }
+ }
+
+ // simple elliptical
+ {
+ SkRRect rr;
+ rr.setRectXY(r, 2, 10);
+
+ ninePatchable = SkBlurMaskFilter::ComputeBlurredRRectParams(rr, 3.0f, &rrectToDraw, &size,
+ xs, &numXs, ys, &numYs);
+ REPORTER_ASSERT(reporter, ninePatchable);
+ REPORTER_ASSERT(reporter, SkScalarNearlyEqual(SkIntToScalar(size.fWidth), 41.0f));
+ REPORTER_ASSERT(reporter, SkScalarNearlyEqual(SkIntToScalar(size.fHeight), 57.0));
+ REPORTER_ASSERT(reporter, 4 == numXs && 4 == numYs);
+ for (int i = 0; i < numXs; ++i) {
+ REPORTER_ASSERT(reporter, xs[i] >= 0.0f && xs[i] <= 1.0f);
+ }
+ for (int i = 0; i < numYs; ++i) {
+ REPORTER_ASSERT(reporter, ys[i] >= 0.0f && ys[i] <= 1.0f);
+ }
+ }
+
+}
+
///////////////////////////////////////////////////////////////////////////////////////////