blob: 5dbfd5e36c17c828c8b9094392d11c655ff8a3c2 [file] [log] [blame]
dvonbeck09e12a62016-08-12 12:50:36 -07001/*
2 * Copyright 2016 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
Brian Salomon6a639042016-12-14 11:08:17 -05008#include "GrAnalyticRectOp.h"
dvonbeck09e12a62016-08-12 12:50:36 -07009
Brian Salomon5ec9def2016-12-20 15:34:05 -050010#include "GrDrawOpTest.h"
dvonbeck09e12a62016-08-12 12:50:36 -070011#include "GrGeometryProcessor.h"
Brian Salomon742e31d2016-12-07 17:06:19 -050012#include "GrOpFlushState.h"
dvonbeck09e12a62016-08-12 12:50:36 -070013#include "GrProcessor.h"
14#include "GrResourceProvider.h"
15#include "SkRRect.h"
16#include "SkStrokeRec.h"
dvonbeck09e12a62016-08-12 12:50:36 -070017#include "glsl/GrGLSLFragmentShaderBuilder.h"
18#include "glsl/GrGLSLGeometryProcessor.h"
19#include "glsl/GrGLSLProgramDataManager.h"
dvonbeck09e12a62016-08-12 12:50:36 -070020#include "glsl/GrGLSLUniformHandler.h"
21#include "glsl/GrGLSLUtil.h"
Brian Salomondad29232016-12-01 16:40:24 -050022#include "glsl/GrGLSLVarying.h"
23#include "glsl/GrGLSLVertexShaderBuilder.h"
Brian Salomon89527432016-12-16 09:52:16 -050024#include "ops/GrMeshDrawOp.h"
dvonbeck09e12a62016-08-12 12:50:36 -070025
26namespace {
27
28struct RectVertex {
Brian Salomon6a639042016-12-14 11:08:17 -050029 SkPoint fPos;
30 GrColor fColor;
31 SkPoint fCenter;
dvonbeck09e12a62016-08-12 12:50:36 -070032 SkVector fDownDir;
33 SkScalar fHalfWidth;
34 SkScalar fHalfHeight;
35};
dvonbeck09e12a62016-08-12 12:50:36 -070036}
37
38///////////////////////////////////////////////////////////////////////////////
39
40/**
41 * The output of this effect is the input color and coverage for an arbitrarily oriented rect. The
42 * rect is specified as:
43 * Center of the rect
44 * Unit vector point down the height of the rect
45 * Half width + 0.5
46 * Half height + 0.5
47 * The center and vector are stored in a vec4 varying ("RectEdge") with the
48 * center in the xy components and the vector in the zw components.
49 * The munged width and height are stored in a vec2 varying ("WidthHeight")
50 * with the width in x and the height in y.
51 */
52class RectGeometryProcessor : public GrGeometryProcessor {
53public:
54 RectGeometryProcessor(const SkMatrix& localMatrix) : fLocalMatrix(localMatrix) {
55 this->initClassID<RectGeometryProcessor>();
Brian Salomon6a639042016-12-14 11:08:17 -050056 fInPosition = &this->addVertexAttrib("inPosition", kVec2f_GrVertexAttribType,
57 kHigh_GrSLPrecision);
58 fInColor = &this->addVertexAttrib("inColor", kVec4ub_GrVertexAttribType);
59 fInRectEdge = &this->addVertexAttrib("inRectEdge", kVec4f_GrVertexAttribType);
bsalomon6cb807b2016-08-17 11:33:39 -070060 fInWidthHeight = &this->addVertexAttrib("inWidthHeight", kVec2f_GrVertexAttribType);
dvonbeck09e12a62016-08-12 12:50:36 -070061 }
62
Mike Kleinfc6c37b2016-09-27 09:34:10 -040063 bool implementsDistanceVector() const override { return true; }
dvonbeck09e12a62016-08-12 12:50:36 -070064
Brian Salomon6a639042016-12-14 11:08:17 -050065 const Attribute* inPosition() const { return fInPosition; }
66 const Attribute* inColor() const { return fInColor; }
67 const Attribute* inRectEdge() const { return fInRectEdge; }
dvonbeck09e12a62016-08-12 12:50:36 -070068 const Attribute* inWidthHeight() const { return fInWidthHeight; }
69
70 const SkMatrix& localMatrix() const { return fLocalMatrix; }
71
72 virtual ~RectGeometryProcessor() {}
73
74 const char* name() const override { return "RectEdge"; }
75
76 class GLSLProcessor : public GrGLSLGeometryProcessor {
77 public:
78 GLSLProcessor() {}
79
Brian Salomon6a639042016-12-14 11:08:17 -050080 void onEmitCode(EmitArgs& args, GrGPArgs* gpArgs) override {
dvonbeck09e12a62016-08-12 12:50:36 -070081 const RectGeometryProcessor& rgp = args.fGP.cast<RectGeometryProcessor>();
82 GrGLSLVertexBuilder* vertBuilder = args.fVertBuilder;
83 GrGLSLVaryingHandler* varyingHandler = args.fVaryingHandler;
84 GrGLSLUniformHandler* uniformHandler = args.fUniformHandler;
85
86 // emit attributes
87 varyingHandler->emitAttributes(rgp);
88
89 // setup the varying for the position
90 GrGLSLVertToFrag positionVary(kVec2f_GrSLType);
91 varyingHandler->addVarying("Position", &positionVary);
92 vertBuilder->codeAppendf("%s = %s;", positionVary.vsOut(), rgp.inPosition()->fName);
93
94 // setup the varying for the center point and the unit vector that points down the
95 // height of the rect
96 GrGLSLVertToFrag rectEdgeVary(kVec4f_GrSLType);
97 varyingHandler->addVarying("RectEdge", &rectEdgeVary);
98 vertBuilder->codeAppendf("%s = %s;", rectEdgeVary.vsOut(), rgp.inRectEdge()->fName);
99
100 // setup the varying for the width/2+.5 and height/2+.5
101 GrGLSLVertToFrag widthHeightVary(kVec2f_GrSLType);
102 varyingHandler->addVarying("WidthHeight", &widthHeightVary);
Brian Salomon6a639042016-12-14 11:08:17 -0500103 vertBuilder->codeAppendf("%s = %s;", widthHeightVary.vsOut(),
104 rgp.inWidthHeight()->fName);
dvonbeck09e12a62016-08-12 12:50:36 -0700105
106 GrGLSLPPFragmentBuilder* fragBuilder = args.fFragBuilder;
107
108 // setup pass through color
109 varyingHandler->addPassThroughAttribute(rgp.inColor(), args.fOutputColor);
110
111 // Setup position
112 this->setupPosition(vertBuilder, gpArgs, rgp.inPosition()->fName);
113
114 // emit transforms
115 this->emitTransforms(vertBuilder,
116 varyingHandler,
117 uniformHandler,
118 gpArgs->fPositionVar,
119 rgp.inPosition()->fName,
120 rgp.localMatrix(),
bsalomona624bf32016-09-20 09:12:47 -0700121 args.fFPCoordTransformHandler);
dvonbeck09e12a62016-08-12 12:50:36 -0700122
123 // TODO: compute all these offsets, spans, and scales in the VS
124 fragBuilder->codeAppendf("float insetW = min(1.0, %s.x) - 0.5;",
125 widthHeightVary.fsIn());
126 fragBuilder->codeAppendf("float insetH = min(1.0, %s.y) - 0.5;",
127 widthHeightVary.fsIn());
128 fragBuilder->codeAppend("float outset = 0.5;");
129 // For rects > 1 pixel wide and tall the span's are noops (i.e., 1.0). For rects
130 // < 1 pixel wide or tall they serve to normalize the < 1 ramp to a 0 .. 1 range.
131 fragBuilder->codeAppend("float spanW = insetW + outset;");
132 fragBuilder->codeAppend("float spanH = insetH + outset;");
133 // For rects < 1 pixel wide or tall, these scale factors are used to cap the maximum
134 // value of coverage that is used. In other words it is the coverage that is
135 // used in the interior of the rect after the ramp.
136 fragBuilder->codeAppend("float scaleW = min(1.0, 2.0*insetW/spanW);");
137 fragBuilder->codeAppend("float scaleH = min(1.0, 2.0*insetH/spanH);");
138 // Compute the coverage for the rect's width
Brian Salomon6a639042016-12-14 11:08:17 -0500139 fragBuilder->codeAppendf("vec2 offset = %s.xy - %s.xy;", positionVary.fsIn(),
140 rectEdgeVary.fsIn());
dvonbeck09e12a62016-08-12 12:50:36 -0700141 fragBuilder->codeAppendf("float perpDot = abs(offset.x * %s.w - offset.y * %s.z);",
142 rectEdgeVary.fsIn(), rectEdgeVary.fsIn());
143
144 if (args.fDistanceVectorName) {
145 fragBuilder->codeAppendf("float widthDistance = %s.x - perpDot;",
146 widthHeightVary.fsIn());
147 }
148
149 fragBuilder->codeAppendf(
150 "float coverage = scaleW*clamp((%s.x-perpDot)/spanW, 0.0, 1.0);",
151 widthHeightVary.fsIn());
152 // Compute the coverage for the rect's height and merge with the width
Brian Salomon6a639042016-12-14 11:08:17 -0500153 fragBuilder->codeAppendf("perpDot = abs(dot(offset, %s.zw));", rectEdgeVary.fsIn());
dvonbeck09e12a62016-08-12 12:50:36 -0700154
155 if (args.fDistanceVectorName) {
156 fragBuilder->codeAppendf("float heightDistance = %s.y - perpDot;",
157 widthHeightVary.fsIn());
158 }
159
160 fragBuilder->codeAppendf(
161 "coverage = coverage*scaleH*clamp((%s.y-perpDot)/spanH, 0.0, 1.0);",
162 widthHeightVary.fsIn());
163
164 fragBuilder->codeAppendf("%s = vec4(coverage);", args.fOutputCoverage);
165
166 if (args.fDistanceVectorName) {
Brian Salomon6a639042016-12-14 11:08:17 -0500167 fragBuilder->codeAppend("// Calculating distance vector\n");
168 fragBuilder->codeAppend("vec2 dvAxis;");
169 fragBuilder->codeAppend("float dvLength;");
dvonbeck09e12a62016-08-12 12:50:36 -0700170
Brian Salomon6a639042016-12-14 11:08:17 -0500171 fragBuilder->codeAppend("if (heightDistance < widthDistance) {");
dvonbeck09e12a62016-08-12 12:50:36 -0700172 fragBuilder->codeAppendf(" dvAxis = %s.zw;", rectEdgeVary.fsIn());
Brian Salomon6a639042016-12-14 11:08:17 -0500173 fragBuilder->codeAppend(" dvLength = heightDistance;");
174 fragBuilder->codeAppend("} else {");
175 fragBuilder->codeAppendf(" dvAxis = vec2(-%s.w, %s.z);", rectEdgeVary.fsIn(),
176 rectEdgeVary.fsIn());
177 fragBuilder->codeAppend(" dvLength = widthDistance;");
178 fragBuilder->codeAppend("}");
dvonbeck09e12a62016-08-12 12:50:36 -0700179
Brian Salomon6a639042016-12-14 11:08:17 -0500180 fragBuilder->codeAppend("float dvSign = sign(dot(offset, dvAxis));");
jvanverth6c177a12016-08-17 07:59:41 -0700181 fragBuilder->codeAppendf("%s = vec4(dvSign * dvAxis, dvLength, 0.0);",
dvonbeck09e12a62016-08-12 12:50:36 -0700182 args.fDistanceVectorName);
dvonbeck09e12a62016-08-12 12:50:36 -0700183 }
184 }
185
186 static void GenKey(const GrGeometryProcessor& gp,
Brian Salomon94efbf52016-11-29 13:43:05 -0500187 const GrShaderCaps&,
dvonbeck09e12a62016-08-12 12:50:36 -0700188 GrProcessorKeyBuilder* b) {
189 b->add32(0x0);
190 }
191
bsalomona624bf32016-09-20 09:12:47 -0700192 void setData(const GrGLSLProgramDataManager& pdman, const GrPrimitiveProcessor& primProc,
193 FPCoordTransformIter&& transformIter) override {
194 const RectGeometryProcessor& rgp = primProc.cast<RectGeometryProcessor>();
Brian Salomon6a639042016-12-14 11:08:17 -0500195 this->setTransformDataHelper(rgp.fLocalMatrix, pdman, &transformIter);
dvonbeck09e12a62016-08-12 12:50:36 -0700196 }
197
198 private:
199 typedef GrGLSLGeometryProcessor INHERITED;
200 };
201
Brian Salomon94efbf52016-11-29 13:43:05 -0500202 void getGLSLProcessorKey(const GrShaderCaps& caps, GrProcessorKeyBuilder* b) const override {
dvonbeck09e12a62016-08-12 12:50:36 -0700203 GLSLProcessor::GenKey(*this, caps, b);
204 }
205
Brian Salomon94efbf52016-11-29 13:43:05 -0500206 GrGLSLPrimitiveProcessor* createGLSLInstance(const GrShaderCaps&) const override {
dvonbeck09e12a62016-08-12 12:50:36 -0700207 return new GLSLProcessor();
208 }
209
210private:
Brian Salomon6a639042016-12-14 11:08:17 -0500211 SkMatrix fLocalMatrix;
dvonbeck09e12a62016-08-12 12:50:36 -0700212
213 const Attribute* fInPosition;
214 const Attribute* fInColor;
215 const Attribute* fInRectEdge;
216 const Attribute* fInWidthHeight;
217
218 GR_DECLARE_GEOMETRY_PROCESSOR_TEST;
219
220 typedef GrGeometryProcessor INHERITED;
221};
222
223GR_DEFINE_GEOMETRY_PROCESSOR_TEST(RectGeometryProcessor);
224
Hal Canary6f6961e2017-01-31 13:50:44 -0500225#if GR_TEST_UTILS
dvonbeck09e12a62016-08-12 12:50:36 -0700226sk_sp<GrGeometryProcessor> RectGeometryProcessor::TestCreate(GrProcessorTestData* d) {
Brian Salomon6a639042016-12-14 11:08:17 -0500227 return sk_sp<GrGeometryProcessor>(new RectGeometryProcessor(GrTest::TestMatrix(d->fRandom)));
dvonbeck09e12a62016-08-12 12:50:36 -0700228}
Hal Canary6f6961e2017-01-31 13:50:44 -0500229#endif
dvonbeck09e12a62016-08-12 12:50:36 -0700230
231///////////////////////////////////////////////////////////////////////////////
232
Brian Salomon6a639042016-12-14 11:08:17 -0500233class AnalyticRectOp final : public GrMeshDrawOp {
dvonbeck09e12a62016-08-12 12:50:36 -0700234public:
Brian Salomon25a88092016-12-01 09:36:50 -0500235 DEFINE_OP_CLASS_ID
dvonbeck09e12a62016-08-12 12:50:36 -0700236
Brian Salomon6a639042016-12-14 11:08:17 -0500237 AnalyticRectOp(GrColor color, const SkMatrix& viewMatrix, const SkRect& rect,
238 const SkRect& croppedRect, const SkRect& bounds)
239 : INHERITED(ClassID()), fViewMatrixIfUsingLocalCoords(viewMatrix) {
dvonbeck09e12a62016-08-12 12:50:36 -0700240 SkPoint center = SkPoint::Make(rect.centerX(), rect.centerY());
241 viewMatrix.mapPoints(&center, 1);
242 SkScalar halfWidth = viewMatrix.mapRadius(SkScalarHalf(rect.width()));
243 SkScalar halfHeight = viewMatrix.mapRadius(SkScalarHalf(rect.height()));
244 SkVector downDir = viewMatrix.mapVector(0.0f, 1.0f);
245 downDir.normalize();
246
247 SkRect deviceSpaceCroppedRect = croppedRect;
248 viewMatrix.mapRect(&deviceSpaceCroppedRect);
249
Brian Salomon6a639042016-12-14 11:08:17 -0500250 fGeoData.emplace_back(
251 Geometry{color, center, downDir, halfWidth, halfHeight, deviceSpaceCroppedRect});
dvonbeck09e12a62016-08-12 12:50:36 -0700252
253 this->setBounds(bounds, HasAABloat::kYes, IsZeroArea::kNo);
254 }
255
Brian Salomon53e4c3c2016-12-21 11:38:53 -0500256 const char* name() const override { return "AnalyticRectOp"; }
dvonbeck09e12a62016-08-12 12:50:36 -0700257
258 SkString dumpInfo() const override {
259 SkString string;
260 for (int i = 0; i < fGeoData.count(); ++i) {
261 string.appendf("Color: 0x%08x Rect [C:(%.2f, %.2f) D:<%.2f,%.3f> W/2:%.2f H/2:%.2f]\n",
Brian Salomon6a639042016-12-14 11:08:17 -0500262 fGeoData[i].fColor, fGeoData[i].fCenter.x(), fGeoData[i].fCenter.y(),
dvonbeck09e12a62016-08-12 12:50:36 -0700263 fGeoData[i].fDownDir.x(), fGeoData[i].fDownDir.y(),
Brian Salomon6a639042016-12-14 11:08:17 -0500264 fGeoData[i].fHalfWidth, fGeoData[i].fHalfHeight);
dvonbeck09e12a62016-08-12 12:50:36 -0700265 }
Brian Salomon7c3e7182016-12-01 09:35:30 -0500266 string.append(DumpPipelineInfo(*this->pipeline()));
dvonbeck09e12a62016-08-12 12:50:36 -0700267 string.append(INHERITED::dumpInfo());
268 return string;
269 }
270
Brian Salomon92aee3d2016-12-21 09:20:25 -0500271private:
Brian Salomon5298dc82017-02-22 11:52:03 -0500272 void getFragmentProcessorAnalysisInputs(FragmentProcessorAnalysisInputs* input) const override {
273 input->colorInput()->setToConstant(fGeoData[0].fColor);
274 input->coverageInput()->setToUnknown();
dvonbeck09e12a62016-08-12 12:50:36 -0700275 }
276
Brian Salomon92aee3d2016-12-21 09:20:25 -0500277 void applyPipelineOptimizations(const GrPipelineOptimizations& optimizations) override {
278 optimizations.getOverrideColorIfSet(&fGeoData[0].fColor);
279 if (!optimizations.readsLocalCoords()) {
dvonbeck09e12a62016-08-12 12:50:36 -0700280 fViewMatrixIfUsingLocalCoords.reset();
281 }
282 }
283
284 void onPrepareDraws(Target* target) const override {
285 SkMatrix localMatrix;
286 if (!fViewMatrixIfUsingLocalCoords.invert(&localMatrix)) {
287 return;
288 }
289
290 // Setup geometry processor
Hal Canary144caf52016-11-07 17:57:18 -0500291 sk_sp<GrGeometryProcessor> gp(new RectGeometryProcessor(localMatrix));
dvonbeck09e12a62016-08-12 12:50:36 -0700292
293 int instanceCount = fGeoData.count();
294 size_t vertexStride = gp->getVertexStride();
295 SkASSERT(vertexStride == sizeof(RectVertex));
296 QuadHelper helper;
Brian Salomon6a639042016-12-14 11:08:17 -0500297 RectVertex* verts =
298 reinterpret_cast<RectVertex*>(helper.init(target, vertexStride, instanceCount));
dvonbeck09e12a62016-08-12 12:50:36 -0700299 if (!verts) {
300 return;
301 }
302
303 for (int i = 0; i < instanceCount; i++) {
304 const Geometry& geom = fGeoData[i];
305
Brian Salomon6a639042016-12-14 11:08:17 -0500306 GrColor color = geom.fColor;
307 SkPoint center = geom.fCenter;
308 SkVector downDir = geom.fDownDir;
309 SkScalar halfWidth = geom.fHalfWidth;
310 SkScalar halfHeight = geom.fHalfHeight;
311 SkRect croppedRect = geom.fCroppedRect;
dvonbeck09e12a62016-08-12 12:50:36 -0700312
313 SkVector rightDir;
314 downDir.rotateCCW(&rightDir);
315
316 verts[0].fPos = {croppedRect.fLeft, croppedRect.fTop};
317 verts[0].fColor = color;
318 verts[0].fCenter = center;
319 verts[0].fDownDir = downDir;
320 verts[0].fHalfWidth = halfWidth;
321 verts[0].fHalfHeight = halfHeight;
322
323 verts[1].fPos = {croppedRect.fRight, croppedRect.fTop};
324 verts[1].fColor = color;
325 verts[1].fCenter = center;
326 verts[1].fDownDir = downDir;
327 verts[1].fHalfWidth = halfWidth;
328 verts[1].fHalfHeight = halfHeight;
329
330 verts[2].fPos = {croppedRect.fRight, croppedRect.fBottom};
331 verts[2].fColor = color;
332 verts[2].fCenter = center;
333 verts[2].fDownDir = downDir;
334 verts[2].fHalfWidth = halfWidth;
335 verts[2].fHalfHeight = halfHeight;
336
337 verts[3].fPos = {croppedRect.fLeft, croppedRect.fBottom};
338 verts[3].fColor = color;
339 verts[3].fCenter = center;
340 verts[3].fDownDir = downDir;
341 verts[3].fHalfWidth = halfWidth;
342 verts[3].fHalfHeight = halfHeight;
343
344 verts += kVerticesPerQuad;
345 }
Hal Canary144caf52016-11-07 17:57:18 -0500346 helper.recordDraw(target, gp.get());
dvonbeck09e12a62016-08-12 12:50:36 -0700347 }
348
Brian Salomon25a88092016-12-01 09:36:50 -0500349 bool onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
Brian Salomon6a639042016-12-14 11:08:17 -0500350 AnalyticRectOp* that = t->cast<AnalyticRectOp>();
dvonbeck09e12a62016-08-12 12:50:36 -0700351 if (!GrPipeline::CanCombine(*this->pipeline(), this->bounds(), *that->pipeline(),
352 that->bounds(), caps)) {
353 return false;
354 }
355
356 if (!fViewMatrixIfUsingLocalCoords.cheapEqualTo(that->fViewMatrixIfUsingLocalCoords)) {
357 return false;
358 }
359
360 fGeoData.push_back_n(that->fGeoData.count(), that->fGeoData.begin());
361 this->joinBounds(*that);
362 return true;
363 }
364
365 struct Geometry {
Brian Salomon6a639042016-12-14 11:08:17 -0500366 GrColor fColor;
367 SkPoint fCenter;
dvonbeck09e12a62016-08-12 12:50:36 -0700368 SkVector fDownDir;
369 SkScalar fHalfWidth;
370 SkScalar fHalfHeight;
Brian Salomon6a639042016-12-14 11:08:17 -0500371 SkRect fCroppedRect;
dvonbeck09e12a62016-08-12 12:50:36 -0700372 };
373
Brian Salomon6a639042016-12-14 11:08:17 -0500374 SkMatrix fViewMatrixIfUsingLocalCoords;
dvonbeck09e12a62016-08-12 12:50:36 -0700375 SkSTArray<1, Geometry, true> fGeoData;
376
Brian Salomondad29232016-12-01 16:40:24 -0500377 typedef GrMeshDrawOp INHERITED;
dvonbeck09e12a62016-08-12 12:50:36 -0700378};
379
Brian Salomon649a3412017-03-09 13:50:43 -0500380std::unique_ptr<GrMeshDrawOp> GrAnalyticRectOp::Make(GrColor color,
381 const SkMatrix& viewMatrix,
382 const SkRect& rect,
383 const SkRect& croppedRect,
384 const SkRect& bounds) {
385 return std::unique_ptr<GrMeshDrawOp>(
Brian Salomonf8334782017-01-03 09:42:58 -0500386 new AnalyticRectOp(color, viewMatrix, rect, croppedRect, bounds));
dvonbeck09e12a62016-08-12 12:50:36 -0700387}
388
Hal Canary6f6961e2017-01-31 13:50:44 -0500389#if GR_TEST_UTILS
dvonbeck09e12a62016-08-12 12:50:36 -0700390
Brian Salomon5ec9def2016-12-20 15:34:05 -0500391DRAW_OP_TEST_DEFINE(AnalyticRectOp) {
dvonbeck09e12a62016-08-12 12:50:36 -0700392 SkMatrix viewMatrix = GrTest::TestMatrix(random);
393 GrColor color = GrRandomColor(random);
394 SkRect rect = GrTest::TestSquare(random);
395 SkRect croppedRect = GrTest::TestSquare(random);
396 SkRect bounds = GrTest::TestSquare(random);
Brian Salomon649a3412017-03-09 13:50:43 -0500397 return std::unique_ptr<GrMeshDrawOp>(
Brian Salomonf8334782017-01-03 09:42:58 -0500398 new AnalyticRectOp(color, viewMatrix, rect, croppedRect, bounds));
dvonbeck09e12a62016-08-12 12:50:36 -0700399}
400
401#endif